@mutmutco/cli 3.106.0 → 3.107.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +777 -35
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5020,6 +5020,20 @@ var import_node_path2 = require("node:path");
5020
5020
 
5021
5021
  // ../infra/compat.mjs
5022
5022
  var CLIENT_VERSION_HEADER = "x-client-version";
5023
+ function parseSemver(s) {
5024
+ if (typeof s !== "string") return null;
5025
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+]|$)/.exec(s.trim());
5026
+ if (!m) return null;
5027
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
5028
+ }
5029
+ function versionAtLeast(v, min) {
5030
+ const a = parseSemver(v);
5031
+ const b = parseSemver(min);
5032
+ if (!a || !b) return false;
5033
+ if (a.major !== b.major) return a.major > b.major;
5034
+ if (a.minor !== b.minor) return a.minor > b.minor;
5035
+ return a.patch >= b.patch;
5036
+ }
5023
5037
 
5024
5038
  // src/client-version.ts
5025
5039
  function resolveClientVersionManifestCandidates(distDir = __dirname) {
@@ -9814,7 +9828,7 @@ var LINK_RE = /\]\(([^)#\s]+?\.md)(?:#[^)]*)?\)/g;
9814
9828
  var CMD_RE = /`mmi-cli ((?:[a-z][a-z-]*)(?: [a-z][a-z-]*){0,3})/g;
9815
9829
  var RETIRED_RE = /\b(retired|historical|removed|deleted|gone|no longer|superseded|legacy)\b/i;
9816
9830
  var ROOT_DOCS = ["README.md", "architecture.md"];
9817
- var SKIP_WALK = ["docs/Archive/", "docs/incidents/", "docs/research/"];
9831
+ var SKIP_WALK = ["docs/Archive/", "docs/incidents/", "docs/research/", "docs/decisions/"];
9818
9832
  function refFirstSegment(ref) {
9819
9833
  return ref.replace(/^(\.\/)+/, "").split("/")[0];
9820
9834
  }
@@ -10117,7 +10131,7 @@ function isRoutableDocsPath(relPath) {
10117
10131
  const normalized = relPath.replace(/\\/g, "/");
10118
10132
  return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
10119
10133
  }
10120
- var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
10134
+ var GENERATED_HEADER = "<!-- Generated by `mmi-cli oracle docs index --write`. Do not edit by hand. -->";
10121
10135
  function plainInline(text) {
10122
10136
  return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
10123
10137
  }
@@ -10162,7 +10176,7 @@ function renderDocsIndex(entries) {
10162
10176
  "",
10163
10177
  "# Documentation index",
10164
10178
  "",
10165
- "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
10179
+ "Generated from the `docs/` tree by `mmi-cli oracle docs index --write`. `--check` fails on drift, so this index",
10166
10180
  "can never diverge from the records it lists.",
10167
10181
  ""
10168
10182
  ];
@@ -19706,9 +19720,15 @@ async function claimOneBoardItem(ctx, selector, options) {
19706
19720
  const contest = await checkLaneContest(client, item);
19707
19721
  if (contest.contested) throw new Error(laneContestMessage(item.ref, contest, "claim"));
19708
19722
  }
19723
+ if (options.check) {
19724
+ return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true, checked: true };
19725
+ }
19709
19726
  await postClaimMarkerComment(client, item);
19710
19727
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true };
19711
19728
  }
19729
+ if (options.check) {
19730
+ return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, checked: true };
19731
+ }
19712
19732
  try {
19713
19733
  await client.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
19714
19734
  } catch (e) {
@@ -19764,7 +19784,7 @@ async function claimBoardIssues(options, deps = {}) {
19764
19784
  const ref = `${selector.repo}#${selector.number}`;
19765
19785
  try {
19766
19786
  const result = await claimOneBoardItem(ctx, selector, options);
19767
- results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, alreadyClaimed: result.alreadyClaimed };
19787
+ results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
19768
19788
  } catch (e) {
19769
19789
  results[index] = { ref, claimed: false, reason: e.message };
19770
19790
  }
@@ -20748,7 +20768,7 @@ function isHouseRoot(token) {
20748
20768
  return HOUSE_ROOTS.includes(token);
20749
20769
  }
20750
20770
  var COMPAT_SHIM_DEATH_WAVE = "Wave 3";
20751
- var COMPAT_SHIM_NOTE = `compatibility shim \u2014 the flat Wave 0 path; use the canonical house-prefixed form; dies in ${COMPAT_SHIM_DEATH_WAVE} (#4316)`;
20771
+ var FLAT_ALIAS_CUT_NOTE = `flat Wave 0 alias \u2014 removed in ${COMPAT_SHIM_DEATH_WAVE} (#4316, #4438); only the canonical house-prefixed path parses`;
20752
20772
  var HOUSE_QUESTIONS = {
20753
20773
  oracle: "Is it live?",
20754
20774
  harbour: "Is it bounded?",
@@ -20808,6 +20828,8 @@ var HOUSE_MAP = {
20808
20828
  report: "learning",
20809
20829
  // friction reports
20810
20830
  "skill-lesson": "learning",
20831
+ "closure-rate": "learning",
20832
+ // closure rate per loop kind, computed at read (#4440)
20811
20833
  // --- core — the front door itself ---------------------------------------------------------------
20812
20834
  commands: "core",
20813
20835
  whoami: "core",
@@ -20842,6 +20864,11 @@ function canonicalPathFor(path2) {
20842
20864
  if (!house) return void 0;
20843
20865
  return house === "core" ? path2 : `${house} ${path2}`;
20844
20866
  }
20867
+ function canonicalArgvFor(args) {
20868
+ const lookup = args.filter((tok) => !tok.startsWith("-")).slice(0, 2).join(" ");
20869
+ const house = houseForPath(lookup);
20870
+ return house && house !== "core" ? [house, ...args] : args;
20871
+ }
20845
20872
 
20846
20873
  // src/command-taxonomy.ts
20847
20874
  var COMMAND_METADATA = /* @__PURE__ */ Symbol.for("mmi.commandTaxonomy.metadata");
@@ -20853,10 +20880,10 @@ var PRIMARY_GROUPS = [
20853
20880
  // step invokes (`docs refs`, `tests policy`), not org-plane operations (#3605). `spawn policy`
20854
20881
  // joins them on the same footing (#3979).
20855
20882
  ["Setup and support", ["bootstrap", "secrets", "docs", "repo-index", "tests", "spawn"]],
20856
- ["Coordinate and improve", ["wave", "report", "skill-lesson"]]
20883
+ ["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
20857
20884
  ];
20858
20885
  var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin"]);
20859
- var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "repo-index", "find", "tests", "spawn", "wave", "report", "skill-lesson"]);
20886
+ var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "repo-index", "find", "tests", "spawn", "wave", "report", "skill-lesson", "closure-rate"]);
20860
20887
  var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
20861
20888
  var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
20862
20889
  var topLevelPosition = 0;
@@ -20906,6 +20933,7 @@ var COMMAND_OWNERSHIP = {
20906
20933
  wave: { module_owner: "cli/src/wave-land.ts", consumer: "campaign-orchestrator" },
20907
20934
  report: { module_owner: "cli/src/report.ts", consumer: "campaign-orchestrator" },
20908
20935
  "skill-lesson": { module_owner: "cli/src/skill-lesson.ts", consumer: "campaign-orchestrator" },
20936
+ "closure-rate": { module_owner: "cli/src/learning-closure-rate.ts", consumer: "campaign-orchestrator" },
20909
20937
  org: { module_owner: "cli/src/command-consolidation.ts", consumer: "org-operator" },
20910
20938
  runtime: { module_owner: "cli/src/command-consolidation.ts", consumer: "runtime-operator" },
20911
20939
  plugin: { module_owner: "cli/src/plugin-guard-io.ts", consumer: "host-runtime" }
@@ -21121,7 +21149,7 @@ function buildCommand(cmd, path2) {
21121
21149
  ...metadata
21122
21150
  };
21123
21151
  if (cmd._allowUnknownOption) out.parses_own_argv = true;
21124
- if (path2 && house !== "core") out.shim = true;
21152
+ if (path2 && house !== "core") out.flat_removed = true;
21125
21153
  const description = cmd.description();
21126
21154
  if (description) out.description = description;
21127
21155
  const examples = readExamples(cmd);
@@ -21170,19 +21198,23 @@ function buildCommandManifest(program3) {
21170
21198
  const primaryTree = primaryCopy(tree, true);
21171
21199
  const primaryIndex = [];
21172
21200
  collectLeaves(primaryTree, primaryIndex);
21201
+ const houses = buildHouses(tree);
21173
21202
  const manifest = {
21174
21203
  schema_version: 2,
21175
21204
  scope: "all",
21176
21205
  name: tree.name,
21177
21206
  tree,
21178
21207
  index,
21179
- houses: buildHouses(tree),
21208
+ houses,
21180
21209
  doors: buildDoorsCatalog(tree),
21181
- shim_contract: {
21210
+ flat_alias_cut: {
21182
21211
  canonical: "mmi-cli <house> <command> \u2026",
21183
- shim: "mmi-cli <command> \u2026",
21184
- dies: COMPAT_SHIM_DEATH_WAVE,
21185
- note: COMPAT_SHIM_NOTE
21212
+ removed: "mmi-cli <command> \u2026",
21213
+ wave: COMPAT_SHIM_DEATH_WAVE,
21214
+ note: FLAT_ALIAS_CUT_NOTE,
21215
+ // The cut record names every removed alias by path — the same entry points the houses block
21216
+ // presents, so the record can never drift from what actually refuses.
21217
+ removed_aliases: [...new Set(houses.flatMap((house) => house.commands.map((entry) => entry.path)))].sort()
21186
21218
  },
21187
21219
  primary_tree: primaryTree,
21188
21220
  primary_index: primaryIndex,
@@ -21280,8 +21312,8 @@ function formatManifestHuman(manifest, options = {}) {
21280
21312
  }
21281
21313
  lines.push(
21282
21314
  "",
21283
- `Canonical form: \`mmi-cli <house> <command> \u2026\`. The flat paths above are compatibility shims,`,
21284
- `kept for one window so fleet scripts keep working \u2014 they die in ${COMPAT_SHIM_DEATH_WAVE} (#4316).`,
21315
+ `Canonical form: \`mmi-cli <house> <command> \u2026\`. The paths above are house-relative \u2014 their flat`,
21316
+ `Wave 0 aliases were removed in ${COMPAT_SHIM_DEATH_WAVE} (#4316, #4438); core stays unprefixed.`,
21285
21317
  "",
21286
21318
  options.all ? "Use `mmi-cli explain <group|command>` for a focused map." : "Use `mmi-cli explain <group|command>` for detail or `mmi-cli commands --all` for operational depth."
21287
21319
  );
@@ -28658,7 +28690,7 @@ function registerBootstrapCommands(program3) {
28658
28690
  target: DOCS_INDEX_PATH,
28659
28691
  action: indexAction,
28660
28692
  ownership: "org",
28661
- reason: indexCurrent === null ? "generated routing index (#3545)" : "routing index present \u2014 regenerate with `mmi-cli docs index --write`, which sees the whole tree"
28693
+ reason: indexCurrent === null ? "generated routing index (#3545)" : "routing index present \u2014 regenerate with `mmi-cli oracle docs index --write`, which sees the whole tree"
28662
28694
  });
28663
28695
  if (o.execute && indexAction === "create") {
28664
28696
  await putSeed(DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0);
@@ -29668,10 +29700,13 @@ function registerBoardCommands(program3) {
29668
29700
  return failGraceful(`board read failed: ${withDiscoverMissDetail(e.message)}`);
29669
29701
  }
29670
29702
  }
29703
+ function checkVerdict(ref, alreadyClaimed) {
29704
+ return alreadyClaimed ? `Check ${ref}: claimed and In Progress, no live contest - claim would renew the lease (nothing written)` : `Check ${ref}: free - claim would proceed (nothing written)`;
29705
+ }
29671
29706
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
29672
29707
  board.command("read", { isDefault: true }).description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--live", "bypass the projection and read the authoritative GitHub Project v2 board").addHelpText("after", "\nBy default, read uses the explicitly-stale Hub projection and prints its watermark. --live is authoritative.\n--allow-partial applies to the live paginated path and detail reads; the projection row itself is atomic.\n").action((o) => runBoardRead(o));
29673
29708
  withExamples(mutating(
29674
- board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
29709
+ board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
29675
29710
  (_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
29676
29711
  ).action(async (issueRefs, o) => {
29677
29712
  if (issueRefs.length === 1) {
@@ -29683,11 +29718,12 @@ function registerBoardCommands(program3) {
29683
29718
  repo: o.repo,
29684
29719
  assignee: o.for,
29685
29720
  force: o.force,
29721
+ check: o.check,
29686
29722
  allowPartial: o.allowPartial
29687
29723
  });
29688
- invalidateStatuslineBoardCache();
29724
+ if (!result.checked) invalidateStatuslineBoardCache();
29689
29725
  if (o.json) return console.log(JSON.stringify(result));
29690
- console.log(result.partial ? `Partially claimed ${result.item.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.item.ref} - In Progress (no change)` : `Claimed ${result.item.ref} - In Progress`);
29726
+ console.log(result.checked ? checkVerdict(result.item.ref, result.alreadyClaimed) : result.partial ? `Partially claimed ${result.item.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.item.ref} - In Progress (no change)` : `Claimed ${result.item.ref} - In Progress`);
29691
29727
  } catch (e) {
29692
29728
  return failGraceful(`board claim failed: ${e.message}`);
29693
29729
  }
@@ -29700,14 +29736,15 @@ function registerBoardCommands(program3) {
29700
29736
  repo: o.repo,
29701
29737
  assignee: o.for,
29702
29738
  force: o.force,
29739
+ check: o.check,
29703
29740
  allowPartial: o.allowPartial
29704
29741
  });
29705
- if (bulk.results.some((r) => r.claimed)) invalidateStatuslineBoardCache();
29742
+ if (bulk.results.some((r) => r.claimed && !r.checked)) invalidateStatuslineBoardCache();
29706
29743
  if (o.json) {
29707
29744
  console.log(JSON.stringify(bulk.results));
29708
29745
  } else {
29709
29746
  for (const result of bulk.results) {
29710
- console.log(result.claimed ? result.partial ? `Partially claimed ${result.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.ref} - In Progress (no change)` : `Claimed ${result.ref} - In Progress` : `Skipped ${result.ref}: ${result.reason}`);
29747
+ console.log(result.claimed ? result.checked ? checkVerdict(result.ref, result.alreadyClaimed) : result.partial ? `Partially claimed ${result.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.ref} - In Progress (no change)` : `Claimed ${result.ref} - In Progress` : `Skipped ${result.ref}: ${result.reason}`);
29711
29748
  }
29712
29749
  }
29713
29750
  if (bulk.failed > 0) process.exitCode = 1;
@@ -29716,11 +29753,13 @@ function registerBoardCommands(program3) {
29716
29753
  }
29717
29754
  }), [
29718
29755
  "mmi-cli board claim 2680",
29719
- "mmi-cli board claim 2680 2681 --for teammate-login"
29756
+ "mmi-cli board claim 2680 2681 --for teammate-login",
29757
+ "mmi-cli board claim 2680 --check"
29720
29758
  ], [
29721
29759
  "Pass raw issue numbers/refs, not URLs.",
29722
29760
  "Claim already assigns and moves Status to In Progress, so do not also board move it.",
29723
- "Multiple refs are handled as a batch and return per-item results."
29761
+ "Multiple refs are handled as a batch and return per-item results.",
29762
+ "--check is the live gate read (it calls GitHub); --dry-run only echoes the parsed argv plan."
29724
29763
  ]);
29725
29764
  board.command("show <issue>").description("print one board item (status, assignees, type, url) with its body and comments").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return the item even if its body/comments fetch fails").action(async (issueRef, o) => {
29726
29765
  try {
@@ -32635,6 +32674,392 @@ function registerTrainCommands(program3) {
32635
32674
  });
32636
32675
  }
32637
32676
 
32677
+ // src/fleet-track-inventory.ts
32678
+ var NO_DEPLOY_SURFACE_MODELS = /* @__PURE__ */ new Set(["none", "content"]);
32679
+ function ghErrorText(e) {
32680
+ const err = e;
32681
+ return String(err?.stderr || err?.message || e).trim().replace(/\s+/g, " ");
32682
+ }
32683
+ function isGhNotFound2(e) {
32684
+ const err = e;
32685
+ return /\bNOT[_ ]FOUND\b|HTTP 404/i.test(`${err?.stderr ?? ""} ${err?.message ?? ""} ${err?.stdout ?? ""}`);
32686
+ }
32687
+ async function readFleetPluginProbe(deps, repo, branch) {
32688
+ const [owner, name] = repo.split("/");
32689
+ const ref = branch ? `?ref=${encodeURIComponent(branch)}` : "";
32690
+ try {
32691
+ const raw = await deps.ghJson([
32692
+ "api",
32693
+ `repos/${encodeURIComponent(owner ?? "")}/${encodeURIComponent(name ?? "")}/contents/.claude-plugin/plugin.json${ref}`
32694
+ ]);
32695
+ if (raw.encoding !== "base64" || typeof raw.content !== "string") {
32696
+ return { repo, state: "unknown", unknown: "plugin.json contents read returned no base64 content" };
32697
+ }
32698
+ const decoded = Buffer.from(raw.content.replace(/\s/g, ""), "base64").toString("utf8");
32699
+ const manifest = JSON.parse(decoded);
32700
+ return {
32701
+ repo,
32702
+ state: "present",
32703
+ version: typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0
32704
+ };
32705
+ } catch (e) {
32706
+ return isGhNotFound2(e) ? { repo, state: "absent" } : { repo, state: "unknown", unknown: `plugin surface read failed: ${ghErrorText(e)}` };
32707
+ }
32708
+ }
32709
+ async function mapBounded2(values, limit, read) {
32710
+ const results = new Array(values.length);
32711
+ let next = 0;
32712
+ const worker = async () => {
32713
+ while (next < values.length) {
32714
+ const index = next++;
32715
+ results[index] = await read(values[index]);
32716
+ }
32717
+ };
32718
+ await Promise.all(Array.from({ length: Math.min(limit, values.length) }, () => worker()));
32719
+ return results;
32720
+ }
32721
+ function validRepo2(repo) {
32722
+ return typeof repo === "string" && /^[^/\s]+\/[^/\s]+$/.test(repo.trim());
32723
+ }
32724
+ function classifySurfaces(project2, plugin) {
32725
+ const board = project2?.projectId && project2.projectNumber !== void 0 && project2.statusFieldId ? "present" : "absent";
32726
+ const vault = typeof project2?.vaultPath === "string" && project2.vaultPath.trim() ? "present" : "absent";
32727
+ const deployModel = typeof project2?.deployModel === "string" ? project2.deployModel : void 0;
32728
+ const deploy = deployModel && !NO_DEPLOY_SURFACE_MODELS.has(deployModel) ? "present" : "absent";
32729
+ return { board, vault, deploy, plugin: plugin?.state ?? "unknown" };
32730
+ }
32731
+ function classificationsOf(surfaces) {
32732
+ const classes = [];
32733
+ if (surfaces.board === "absent") classes.push("no-board");
32734
+ if (surfaces.vault === "absent") classes.push("no-vault");
32735
+ if (surfaces.plugin === "absent") classes.push("no-plugin");
32736
+ if (surfaces.deploy === "absent") classes.push("no-deploy-surface");
32737
+ return classes;
32738
+ }
32739
+ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generatedAt) {
32740
+ const releaseByRepo = new Map(releaseProbes.map((p) => [p.repo.toLowerCase(), p]));
32741
+ const pluginByRepo = new Map(pluginProbes.map((p) => [p.repo.toLowerCase(), p]));
32742
+ const projectByRepo = /* @__PURE__ */ new Map();
32743
+ const anomalies = [];
32744
+ for (const project2 of projects) {
32745
+ const repos2 = (project2.repos ?? []).filter(validRepo2);
32746
+ if (!repos2.length) {
32747
+ anomalies.push({
32748
+ kind: "project-without-repo",
32749
+ slug: String(project2.slug ?? "?"),
32750
+ name: String(project2.name ?? project2.slug ?? "?")
32751
+ });
32752
+ }
32753
+ for (const repo of repos2) {
32754
+ const key = repo.trim().toLowerCase();
32755
+ projectByRepo.set(key, projectByRepo.get(key) ?? project2);
32756
+ }
32757
+ }
32758
+ const repos = orgVersionRepos(projects).map((repo) => {
32759
+ const project2 = projectByRepo.get(repo.toLowerCase());
32760
+ const release = releaseByRepo.get(repo.toLowerCase());
32761
+ const plugin = pluginByRepo.get(repo.toLowerCase());
32762
+ const surfaces = classifySurfaces(project2, plugin);
32763
+ const unknowns = [...release?.unknowns ?? []];
32764
+ if (!release) unknowns.push("GitHub release evidence was not collected");
32765
+ if (plugin?.unknown) unknowns.push(plugin.unknown);
32766
+ if (!plugin) unknowns.push("plugin surface was not probed");
32767
+ return {
32768
+ repo,
32769
+ slug: String(project2?.slug ?? repo.split("/")[1] ?? repo).toLowerCase(),
32770
+ track: resolveReleaseTrack(project2, void 0, repo),
32771
+ declaredTrack: typeof project2?.releaseTrack === "string" ? project2.releaseTrack : null,
32772
+ branch: typeof project2?.branch === "string" ? project2.branch : null,
32773
+ repoClass: typeof project2?.class === "string" ? project2.class : null,
32774
+ deployModel: typeof project2?.deployModel === "string" ? project2.deployModel : null,
32775
+ surfaces,
32776
+ classifications: classificationsOf(surfaces),
32777
+ pluginVersion: plugin?.version ?? null,
32778
+ release: {
32779
+ tag: release?.releaseTag ?? null,
32780
+ version: release?.releasedVersion ?? null,
32781
+ at: release?.releasedAt ?? null,
32782
+ coordinatedVersion: release?.coordinatedVersion ?? null,
32783
+ coordinatedSource: release?.coordinatedSource ?? null
32784
+ },
32785
+ ci: {
32786
+ declared: typeof project2?.ci === "string" ? project2.ci : null,
32787
+ requiredChecks: Array.isArray(project2?.requiredChecks) ? project2.requiredChecks : null,
32788
+ exemptReason: typeof project2?.ciExemptReason === "string" ? project2.ciExemptReason : null
32789
+ },
32790
+ source: { registry: "hub-registry-live", release: "github-releases-live", plugin: "github-contents-live" },
32791
+ freshness: { readAt: generatedAt, releasedAt: release?.releasedAt ?? null },
32792
+ unknowns
32793
+ };
32794
+ });
32795
+ const tracks = { full: 0, direct: 0, trunk: 0 };
32796
+ for (const row of repos) tracks[row.track] += 1;
32797
+ return {
32798
+ generatedAt,
32799
+ source: {
32800
+ roster: "hub-registry-live",
32801
+ releases: "github-releases-live",
32802
+ plugin: "github-contents-live",
32803
+ committedInventory: "never"
32804
+ },
32805
+ counts: {
32806
+ projects: projects.length,
32807
+ registeredRepos: repos.length,
32808
+ tracks,
32809
+ noBoard: repos.filter((r) => r.classifications.includes("no-board")).length,
32810
+ noVault: repos.filter((r) => r.classifications.includes("no-vault")).length,
32811
+ noPlugin: repos.filter((r) => r.classifications.includes("no-plugin")).length,
32812
+ noDeploySurface: repos.filter((r) => r.classifications.includes("no-deploy-surface")).length,
32813
+ unknownRows: repos.filter((r) => r.unknowns.length > 0).length,
32814
+ anomalies: anomalies.length
32815
+ },
32816
+ repos,
32817
+ anomalies
32818
+ };
32819
+ }
32820
+ function defaultFleetTrackInventoryDeps() {
32821
+ return {
32822
+ projects: async () => {
32823
+ const cfg = await loadConfig();
32824
+ const projects = await fetchProjectsList(registryClientDeps(cfg));
32825
+ if (!projects) throw new Error("live Hub registry roster is unavailable \u2014 a committed inventory is never current truth, so there is no fallback; retry when the Hub API is reachable");
32826
+ return projects;
32827
+ },
32828
+ query: queryDeps(),
32829
+ now: () => /* @__PURE__ */ new Date()
32830
+ };
32831
+ }
32832
+ async function readFleetTrackInventory(deps = defaultFleetTrackInventoryDeps()) {
32833
+ const projects = await deps.projects();
32834
+ const repos = orgVersionRepos(projects);
32835
+ const branchByRepo = /* @__PURE__ */ new Map();
32836
+ for (const project2 of projects) {
32837
+ for (const repo of (project2.repos ?? []).filter(validRepo2)) {
32838
+ const key = repo.trim().toLowerCase();
32839
+ if (!branchByRepo.has(key)) {
32840
+ branchByRepo.set(key, typeof project2.branch === "string" ? project2.branch : void 0);
32841
+ }
32842
+ }
32843
+ }
32844
+ const [releaseProbes, pluginProbes] = await Promise.all([
32845
+ mapBounded2(repos, 6, (repo) => readOrgVersionProbe(deps.query, repo)),
32846
+ mapBounded2(repos, 6, (repo) => readFleetPluginProbe(deps.query, repo, branchByRepo.get(repo.toLowerCase())))
32847
+ ]);
32848
+ return buildFleetTrackInventory(projects, releaseProbes, pluginProbes, deps.now().toISOString());
32849
+ }
32850
+ function formatFleetTrackInventory(report) {
32851
+ const { counts } = report;
32852
+ const lines = [
32853
+ `fleet version-track inventory \u2014 ${counts.registeredRepos} registered repositories (${counts.projects} registry projects)`,
32854
+ `source: live Hub registry + live GitHub releases/contents \xB7 read ${report.generatedAt} \xB7 committed inventory: never`,
32855
+ `tracks: ${counts.tracks.full} full \xB7 ${counts.tracks.direct} direct \xB7 ${counts.tracks.trunk} trunk`,
32856
+ `classified: ${counts.noBoard} no-board \xB7 ${counts.noVault} no-vault \xB7 ${counts.noPlugin} no-plugin \xB7 ${counts.noDeploySurface} no-deploy-surface \xB7 ${counts.unknownRows} rows with unknowns`,
32857
+ "",
32858
+ "repository | track | deploy model | released | plugin | classifications"
32859
+ ];
32860
+ for (const row of report.repos) {
32861
+ lines.push(
32862
+ `${row.repo} | ${row.track}${row.declaredTrack ? "" : " (derived)"} | ${row.deployModel ?? "UNDECLARED"} | ${row.release.version ?? "UNKNOWN"} | ${row.pluginVersion ?? (row.surfaces.plugin === "present" ? "present" : row.surfaces.plugin)} | ${row.classifications.join(", ") || "none"}`
32863
+ );
32864
+ for (const unknown of row.unknowns) lines.push(` ? ${unknown}`);
32865
+ }
32866
+ if (report.anomalies.length) {
32867
+ lines.push("", "anomalies:");
32868
+ for (const anomaly of report.anomalies) {
32869
+ lines.push(` ? ${anomaly.slug} (${anomaly.name}) \u2014 registry project has no registered repository`);
32870
+ }
32871
+ }
32872
+ return lines.join("\n");
32873
+ }
32874
+ function registerFleetTrackInventory(program3) {
32875
+ const train = program3.commands.find((c) => c.name() === "train");
32876
+ if (!train) throw new Error("train inventory registration requires the train command group");
32877
+ train.command("inventory").description("live fleet version-track inventory (#4451) \u2014 every registered repo's track, deploy model, CI contract, release evidence, and explicit no-board/no-vault/no-plugin/no-deploy-surface classification").option("--json", "machine-readable output (the typed D2b input shape)").action(async (o) => {
32878
+ try {
32879
+ const report = await readFleetTrackInventory();
32880
+ console.log(o.json ? JSON.stringify(report, null, 2) : formatFleetTrackInventory(report));
32881
+ } catch (e) {
32882
+ return failGraceful(`train inventory: ${e.message}`);
32883
+ }
32884
+ });
32885
+ }
32886
+
32887
+ // ../infra/src/version-gate.ts
32888
+ var HUB_REPO5 = "mutmutco/mmi-hub";
32889
+ var NO_RELEASE_UNKNOWN = "no published GitHub Release";
32890
+ function semverEqual(a, b) {
32891
+ return versionAtLeast(a, b) && versionAtLeast(b, a);
32892
+ }
32893
+ function semverRelation(a, b) {
32894
+ if (!parseSemver(a) || !parseSemver(b)) return null;
32895
+ if (semverEqual(a, b)) return "equal";
32896
+ return versionAtLeast(a, b) ? "ahead" : "behind";
32897
+ }
32898
+ function classifyRow(row) {
32899
+ const findings = [];
32900
+ const reasons = /* @__PURE__ */ new Set();
32901
+ const releaseRequired = row.track !== "trunk";
32902
+ for (const unknown of row.unknowns) {
32903
+ if (!releaseRequired && unknown === NO_RELEASE_UNKNOWN) continue;
32904
+ reasons.add(unknown);
32905
+ }
32906
+ const { version, coordinatedVersion, coordinatedSource, tag } = row.release;
32907
+ if (releaseRequired && !version && ![...reasons].some((r) => /release/i.test(r))) {
32908
+ reasons.add(NO_RELEASE_UNKNOWN);
32909
+ }
32910
+ if (releaseRequired && version && !coordinatedVersion && ![...reasons].some((r) => /version lock|distribution-bom|package\.json/i.test(r))) {
32911
+ reasons.add(`coordinated version pointer unknown at ${tag ?? "the release tag"}`);
32912
+ }
32913
+ if (version && coordinatedVersion) {
32914
+ const relation = semverRelation(version, coordinatedVersion);
32915
+ if (relation === null) {
32916
+ reasons.add(`released ${version} and ${coordinatedSource ?? "version lock"} ${coordinatedVersion} are not comparable semver`);
32917
+ } else if (relation !== "equal") {
32918
+ findings.push(`released ${version} is ${relation === "ahead" ? "ahead of" : "behind"} the tag-scoped version lock ${coordinatedVersion} (${coordinatedSource ?? "unknown source"})`);
32919
+ }
32920
+ }
32921
+ if (row.pluginVersion && version) {
32922
+ const relation = semverRelation(row.pluginVersion, version);
32923
+ if (relation === null) {
32924
+ reasons.add(`plugin surface version ${row.pluginVersion} and released ${version} are not comparable semver`);
32925
+ } else if (relation === "behind") {
32926
+ findings.push(`plugin surface ships ${row.pluginVersion}, behind the released ${version}`);
32927
+ }
32928
+ }
32929
+ const unknownReasons = [...reasons].sort((a, b) => a.localeCompare(b));
32930
+ return {
32931
+ repo: row.repo,
32932
+ track: row.track,
32933
+ deployModel: row.deployModel,
32934
+ verdict: findings.length > 0 ? "drift" : unknownReasons.length > 0 ? "unknown" : "conforming",
32935
+ findings,
32936
+ unknownReasons,
32937
+ checkedAt: row.freshness.readAt
32938
+ };
32939
+ }
32940
+ function canaryOrder(a, b) {
32941
+ const rank = { conforming: 0, unknown: 1, drift: 2 };
32942
+ if (rank[a.result.verdict] !== rank[b.result.verdict]) return rank[a.result.verdict] - rank[b.result.verdict];
32943
+ const aAt = a.releasedAt ? Date.parse(a.releasedAt) : Number.NEGATIVE_INFINITY;
32944
+ const bAt = b.releasedAt ? Date.parse(b.releasedAt) : Number.NEGATIVE_INFINITY;
32945
+ if (aAt !== bAt) return bAt - aAt;
32946
+ return a.result.repo.localeCompare(b.result.repo);
32947
+ }
32948
+ function buildCanaryClasses(rows, resultByRepo) {
32949
+ const byModel = /* @__PURE__ */ new Map();
32950
+ for (const row of rows) {
32951
+ const model = row.deployModel ?? "undeclared";
32952
+ const result = resultByRepo.get(row.repo);
32953
+ if (!result) continue;
32954
+ const bucket = byModel.get(model) ?? [];
32955
+ bucket.push({ result, releasedAt: row.freshness.releasedAt });
32956
+ byModel.set(model, bucket);
32957
+ }
32958
+ return [...byModel.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([deployModel, members]) => {
32959
+ const ordered = [...members].sort(canaryOrder);
32960
+ return {
32961
+ deployModel,
32962
+ canary: ordered[0]?.result.repo ?? null,
32963
+ cohort: ordered.slice(1).map((m) => m.result.repo),
32964
+ conforming: members.filter((m) => m.result.verdict === "conforming").length,
32965
+ drift: members.filter((m) => m.result.verdict === "drift").length,
32966
+ unknown: members.filter((m) => m.result.verdict === "unknown").length
32967
+ };
32968
+ });
32969
+ }
32970
+ function evaluateFleetLockstepGate(rows, evaluatedAt) {
32971
+ const results = rows.map(classifyRow);
32972
+ const resultByRepo = new Map(results.map((r) => [r.repo, r]));
32973
+ const drift = results.filter((r) => r.verdict === "drift").map((r) => ({ repo: r.repo, findings: [...r.findings] }));
32974
+ const unknowns = results.filter((r) => r.verdict === "unknown").map((r) => ({ repo: r.repo, reasons: [...r.unknownReasons] }));
32975
+ const conformingRepos = results.filter((r) => r.verdict === "conforming").map((r) => r.repo);
32976
+ const hubOnly = rows.length > 0 && rows.every((row) => row.repo.trim().toLowerCase() === HUB_REPO5);
32977
+ const failReasons = [];
32978
+ if (rows.length === 0) failReasons.push("empty roster: no registered repositories were evaluated");
32979
+ if (hubOnly) failReasons.push("hub-only coverage: the fleet gate refuses a result covering only mutmutco/MMI-Hub");
32980
+ if (drift.length > 0) failReasons.push(`${drift.length} repositor${drift.length === 1 ? "y" : "ies"} with drift`);
32981
+ if (unknowns.length > 0) failReasons.push(`${unknowns.length} repositor${unknowns.length === 1 ? "y" : "ies"} with unknown evidence`);
32982
+ return {
32983
+ gate: "fleet-lockstep-conformance",
32984
+ mode: "dry-run",
32985
+ enforcement: "none",
32986
+ verdict: failReasons.length === 0 && conformingRepos.length === rows.length ? "pass" : "fail",
32987
+ failReasons,
32988
+ denominator: rows.length,
32989
+ conforming: conformingRepos.length,
32990
+ conformingRepos,
32991
+ drift,
32992
+ unknowns,
32993
+ canaryClasses: buildCanaryClasses(rows, resultByRepo),
32994
+ hubOnly,
32995
+ repos: results,
32996
+ evaluatedAt
32997
+ };
32998
+ }
32999
+
33000
+ // src/fleet-lockstep-gate.ts
33001
+ async function runFleetLockstepGate(deps = defaultFleetTrackInventoryDeps()) {
33002
+ const inventory = await readFleetTrackInventory(deps);
33003
+ return evaluateFleetLockstepGate(inventory.repos, inventory.generatedAt);
33004
+ }
33005
+ function fleetGateConformanceEvidence(result) {
33006
+ return result.repos.map((row) => ({
33007
+ repo: row.repo,
33008
+ verdict: row.verdict,
33009
+ checkedAt: row.checkedAt,
33010
+ findings: row.findings.length ? [...row.findings] : void 0,
33011
+ unknownEvidence: row.unknownReasons.length ? [...row.unknownReasons] : void 0
33012
+ }));
33013
+ }
33014
+ function formatFleetLockstepGate(result) {
33015
+ const lines = [
33016
+ `fleet lockstep gate (dry-run) \u2014 ${result.conforming}/${result.denominator} conforming \xB7 ${result.verdict.toUpperCase()}`,
33017
+ `evaluated ${result.evaluatedAt} \xB7 live D2a inventory \xB7 enforcement: none (D2c #4453)`
33018
+ ];
33019
+ if (result.failReasons.length) {
33020
+ lines.push("", "fail reasons:");
33021
+ for (const reason of result.failReasons) lines.push(` ! ${reason}`);
33022
+ }
33023
+ if (result.conformingRepos.length) {
33024
+ lines.push("", `conforming set (${result.conformingRepos.length}):`);
33025
+ for (const repo of result.conformingRepos) lines.push(` = ${repo}`);
33026
+ }
33027
+ if (result.drift.length) {
33028
+ lines.push("", `drift (${result.drift.length}):`);
33029
+ for (const row of result.drift) {
33030
+ lines.push(` x ${row.repo}`);
33031
+ for (const finding of row.findings) lines.push(` ${finding}`);
33032
+ }
33033
+ }
33034
+ if (result.unknowns.length) {
33035
+ lines.push("", `unknown evidence (${result.unknowns.length}):`);
33036
+ for (const row of result.unknowns) {
33037
+ lines.push(` ? ${row.repo}`);
33038
+ for (const reason of row.reasons) lines.push(` ${reason}`);
33039
+ }
33040
+ }
33041
+ lines.push("", "canary classes by deploy model (canary -> cohort):");
33042
+ for (const cls of result.canaryClasses) {
33043
+ lines.push(
33044
+ ` ${cls.deployModel}: canary ${cls.canary ?? "NONE"}` + (cls.cohort.length ? ` -> ${cls.cohort.join(", ")}` : "") + ` (${cls.conforming} conforming \xB7 ${cls.drift} drift \xB7 ${cls.unknown} unknown)`
33045
+ );
33046
+ }
33047
+ return lines.join("\n");
33048
+ }
33049
+ function registerFleetLockstepGate(program3) {
33050
+ const train = program3.commands.find((c) => c.name() === "train");
33051
+ if (!train) throw new Error("train gate registration requires the train command group");
33052
+ train.command("gate").description("fleet lockstep N-of-N conformance DRY-RUN gate (#4452) \u2014 live D2a inventory in, fail-closed verdict out: denominator, conforming set, exact drift, canary classes by deploy model, named unknowns; no ruleset enforcement (D2c #4453)").option("--json", "machine-readable output (the typed gate result)").action(async (o) => {
33053
+ try {
33054
+ const result = await runFleetLockstepGate();
33055
+ console.log(o.json ? JSON.stringify(result, null, 2) : formatFleetLockstepGate(result));
33056
+ process.exitCode = result.verdict === "pass" ? 0 : 1;
33057
+ } catch (e) {
33058
+ return failGraceful(`train gate: ${e.message}`);
33059
+ }
33060
+ });
33061
+ }
33062
+
32638
33063
  // src/deploy-status.ts
32639
33064
  function buildDeployStatusReport(input) {
32640
33065
  const deploy = input.deployFacts?.stages[input.stage] ?? null;
@@ -33619,6 +34044,12 @@ function parseWorktreePorcelain2(stdout) {
33619
34044
  var CLOSING_MENTION_RE = /\b(close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi;
33620
34045
  var NEGATION_RE = /\b(?:not|never|cannot|can't|don't|doesn't|didn't|won't|wouldn't|shouldn't|mustn't|without)\b/i;
33621
34046
  var CLAUSE_WINDOW = 120;
34047
+ var NEGATED_CLOSING_PHRASE_RE = /\b(?:(?:does|do|did|will|would|should|must|can)\s+not|(?:is|was|are|were)\s+not|doesn't|don't|didn't|won't|wouldn't|shouldn't|mustn't|can't|cannot|never|without|not)\s+(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi;
34048
+ function rewriteNegatedClosingPhrases(text) {
34049
+ if (!text.includes("#")) return text;
34050
+ const segments = text.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g);
34051
+ return segments.map((seg, i) => i % 2 === 1 ? seg : seg.replace(NEGATED_CLOSING_PHRASE_RE, (_m, n) => `leaves #${n} open`)).join("");
34052
+ }
33622
34053
  function findClosingMentions(text) {
33623
34054
  const mentions = [];
33624
34055
  for (const match of text.matchAll(CLOSING_MENTION_RE)) {
@@ -33692,11 +34123,12 @@ ${typeof body === "string" ? body : ""}`;
33692
34123
  function negatedClosingRefusalMessage(negated, context = "pr merge") {
33693
34124
  const named = negated.map((n) => `#${n}`).join(", ");
33694
34125
  const first = `#${negated[0] ?? "N"}`;
33695
- return `${context}: REFUSED \u2014 GitHub will close ${named} on merge, but the PR body says it does not (GitHub's closing-keyword parser is negation-blind: "does not close ${first}" still closes it). Reword the body so the keyword is gone (e.g. "leaves ${first} open"), or re-run with --force to merge anyway and let ${named} close.`;
34126
+ return `${context}: REFUSED \u2014 GitHub will close ${named} on merge, but the PR body says it does not (GitHub's closing-keyword parser is negation-blind: "does not close ${first}" still closes it). Reword so the keyword is gone \u2014 use "Part of ${first}", "Refs ${first}", or "leaves ${first} open"; never "does not close ${first}". Or re-run with --force to merge anyway and let ${named} close.`;
33696
34127
  }
33697
34128
  function commitClosingRefusalMessage(closing, context = "pr merge") {
33698
34129
  const named = closing.map((n) => `#${n}`).join(", ");
33699
- return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove the closing keyword from the commit message, or re-run with --force to merge anyway and let ${named} close.`;
34130
+ const first = `#${closing[0] ?? "N"}`;
34131
+ return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), or re-run with --force to merge anyway and let ${named} close.`;
33700
34132
  }
33701
34133
  function alreadyClosedCommitClosingMessage(closed, context = "pr merge") {
33702
34134
  const named = closed.map((n) => `#${n}`).join(", ");
@@ -33887,6 +34319,297 @@ function registerSessionReport(program3) {
33887
34319
  });
33888
34320
  }
33889
34321
 
34322
+ // src/learning-closure-rate.ts
34323
+ var CLOSURE_RATE_TIMEOUT_MS = 2e4;
34324
+ var LOOP_ITEM_CAP = 100;
34325
+ var EVIDENCE_CONCURRENCY = 4;
34326
+ var RED_TEAM_FINDING_LABEL2 = "red-team";
34327
+ var RULING_LOOP_STATE_LABELS = ["loop-state:closed-deferred", "loop-state:closed-invalid"];
34328
+ var CLOSED_MERGED_LOOP_STATE_LABEL = "loop-state:closed-merged";
34329
+ var LOOP_KINDS2 = [
34330
+ { kind: "friction", label: REPORT_LABEL },
34331
+ { kind: "lesson", label: SKILL_LESSON_LABEL },
34332
+ { kind: "red-team", label: RED_TEAM_FINDING_LABEL2 }
34333
+ ];
34334
+ function prIsMerged(pr2) {
34335
+ return String(pr2.state ?? "").toUpperCase() === "MERGED";
34336
+ }
34337
+ function mergedPrsOf(linkedPrs) {
34338
+ return linkedPrs.filter(prIsMerged);
34339
+ }
34340
+ function hasRecordedRuling(labels) {
34341
+ return labels.some((label) => RULING_LOOP_STATE_LABELS.includes(label));
34342
+ }
34343
+ function classifyLoopClosure(item, evidence) {
34344
+ if (String(item.state ?? "").toUpperCase() !== "CLOSED") return "open";
34345
+ if (mergedPrsOf(evidence.linkedPrs).length > 0) return "closed";
34346
+ if (item.labels.includes(CLOSED_MERGED_LOOP_STATE_LABEL)) return "closed";
34347
+ if (hasRecordedRuling(item.labels)) return "closed";
34348
+ return "closed-no-evidence";
34349
+ }
34350
+ var round3 = (x) => Math.round(x * 1e3) / 1e3;
34351
+ function computeLoopKindRate(spec, verdicts) {
34352
+ const numerator = verdicts.filter((v) => v === "closed").length;
34353
+ const unknownEvidence = verdicts.filter((v) => v === "closed-no-evidence").length;
34354
+ const denominator = verdicts.length;
34355
+ return {
34356
+ kind: spec.kind,
34357
+ label: spec.label,
34358
+ numerator,
34359
+ denominator,
34360
+ unknownEvidence,
34361
+ rate: denominator > 0 ? round3(numerator / denominator) : null
34362
+ };
34363
+ }
34364
+ function fmtRate(rate) {
34365
+ return rate === null ? "\u2014" : rate.toFixed(3);
34366
+ }
34367
+ function formatClosureSummary(report) {
34368
+ const kindWidth = Math.max(...report.kinds.map((k) => k.kind.length));
34369
+ const labelWidth = Math.max(...report.kinds.map((k) => k.label.length));
34370
+ const lines = [`learning closure rate \u2014 ${report.repo} (computed at read; #4440)`];
34371
+ for (const k of report.kinds) {
34372
+ const head = `${k.kind.padEnd(kindWidth)} (label ${k.label.padEnd(labelWidth)})`;
34373
+ const body = k.denominator === 0 ? "no loop items" : `${k.numerator}/${k.denominator} closed \xB7 ${k.unknownEvidence} unknown evidence \xB7 rate ${fmtRate(k.rate)}`;
34374
+ lines.push(` ${head}: ${body}`);
34375
+ }
34376
+ const t = report.totals;
34377
+ lines.push(
34378
+ ` ${"total".padEnd(kindWidth)}: ${t.numerator}/${t.denominator} closed \xB7 ${t.unknownEvidence} unknown evidence \xB7 rate ${fmtRate(t.rate)}`,
34379
+ "A closed loop without a merged PR or a recorded ruling reads as open."
34380
+ );
34381
+ if (report.evidencePartial) {
34382
+ lines.push("note: at least one closed item had an unreadable linked-PR read \u2014 counted open (unknown evidence).");
34383
+ }
34384
+ return lines.join("\n");
34385
+ }
34386
+ function buildClosureReport(repo, perKind, evidencePartial) {
34387
+ const kinds = perKind.map(({ spec, verdicts }) => computeLoopKindRate(spec, verdicts));
34388
+ const numerator = kinds.reduce((acc, k) => acc + k.numerator, 0);
34389
+ const denominator = kinds.reduce((acc, k) => acc + k.denominator, 0);
34390
+ const unknownEvidence = kinds.reduce((acc, k) => acc + k.unknownEvidence, 0);
34391
+ const report = {
34392
+ repo,
34393
+ kinds,
34394
+ totals: {
34395
+ numerator,
34396
+ denominator,
34397
+ unknownEvidence,
34398
+ rate: denominator > 0 ? round3(numerator / denominator) : null
34399
+ },
34400
+ evidencePartial,
34401
+ summary: ""
34402
+ };
34403
+ report.summary = formatClosureSummary(report);
34404
+ return report;
34405
+ }
34406
+ function splitRepo2(repo) {
34407
+ const parts = repo.split("/");
34408
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
34409
+ throw new QueryReadError("BAD_INPUT", `invalid repo "${repo}" (expected owner/repo)`);
34410
+ }
34411
+ return { owner: parts[0], name: parts[1] };
34412
+ }
34413
+ async function mapPooled(items, limit, worker) {
34414
+ const out = new Array(items.length);
34415
+ let next = 0;
34416
+ const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
34417
+ while (next < items.length) {
34418
+ const index = next;
34419
+ next += 1;
34420
+ out[index] = await worker(items[index]);
34421
+ }
34422
+ });
34423
+ await Promise.all(runners);
34424
+ return out;
34425
+ }
34426
+ async function readEvidence(deps, owner, name, item) {
34427
+ try {
34428
+ const resp = await deps.ghJson(prForIssueGraphqlArgs(owner, name, item.number), CLOSURE_RATE_TIMEOUT_MS);
34429
+ return { linkedPrs: extractPrForIssueResponse(resp), readFailed: false };
34430
+ } catch (e) {
34431
+ if (e instanceof QueryReadError && e.code === "NOT_FOUND") {
34432
+ return { linkedPrs: [], readFailed: false };
34433
+ }
34434
+ return { linkedPrs: [], readFailed: true };
34435
+ }
34436
+ }
34437
+ async function runLearningClosureRate(deps, opts = {}) {
34438
+ const repo = opts.repo ?? HUB_REPO3;
34439
+ const { owner, name } = splitRepo2(repo);
34440
+ let evidencePartial = false;
34441
+ const perKind = [];
34442
+ for (const spec of LOOP_KINDS2) {
34443
+ const rows = await deps.ghJson(
34444
+ buildIssueListArgs({ label: spec.label, state: "all", limit: LOOP_ITEM_CAP }, repo),
34445
+ CLOSURE_RATE_TIMEOUT_MS
34446
+ );
34447
+ const items = shapeIssueList(rows);
34448
+ const closed = items.filter((item) => String(item.state ?? "").toUpperCase() === "CLOSED");
34449
+ const evidence = await mapPooled(closed, EVIDENCE_CONCURRENCY, (item) => readEvidence(deps, owner, name, item));
34450
+ if (evidence.some((ev) => ev.readFailed)) evidencePartial = true;
34451
+ const evidenceByNumber = new Map(closed.map((item, i) => [item.number, evidence[i]]));
34452
+ const verdicts = items.map(
34453
+ (item) => classifyLoopClosure(item, evidenceByNumber.get(item.number) ?? { linkedPrs: [], readFailed: false })
34454
+ );
34455
+ perKind.push({ spec, verdicts });
34456
+ }
34457
+ return buildClosureReport(repo, perKind, evidencePartial);
34458
+ }
34459
+ function closureRateFail(e) {
34460
+ if (e instanceof QueryReadError) {
34461
+ const code = e.code === "NOT_FOUND" ? ERROR_CODES.ERR_NOT_FOUND : e.code === "NO_AUTH" ? ERROR_CODES.ERR_NO_AUTH : ERROR_CODES.ERR_BAD_ENUM;
34462
+ return fail(`closure-rate: ${e.message}`, code === ERROR_CODES.ERR_BAD_ENUM ? void 0 : { code });
34463
+ }
34464
+ const err = e;
34465
+ return fail(`closure-rate: ${(err.stderr || err.message || String(e)).trim()}`);
34466
+ }
34467
+ function registerLearningClosureRateCommand(program3) {
34468
+ const deps = queryDeps();
34469
+ withExamples(
34470
+ program3.command("closure-rate").description(
34471
+ "learning closure rate per loop kind (friction, lesson, red-team) computed at read from board + linked-PR/ruling evidence \u2014 a closed loop without a merged PR or a recorded ruling reads as open (#4440)"
34472
+ ).option("--repo <owner/repo>", `repo holding the loop items (defaults to ${HUB_REPO3}, the central Hub board)`).option("--json", "print the structured LearningClosureReport JSON (default is the human summary)").action(async (o) => {
34473
+ try {
34474
+ const report = await runLearningClosureRate(deps, { repo: o.repo });
34475
+ if (o.json) console.log(JSON.stringify(report, null, 2));
34476
+ else console.log(report.summary);
34477
+ } catch (e) {
34478
+ closureRateFail(e);
34479
+ }
34480
+ }),
34481
+ [
34482
+ "mmi-cli learning closure-rate",
34483
+ "mmi-cli learning closure-rate --repo mutmutco/MMI-Hub --json"
34484
+ ],
34485
+ "loop items are read newest-first with a bounded cap of 100 per kind \u2014 a rate over a longer history needs the cap raised, never an unbounded scan"
34486
+ );
34487
+ }
34488
+
34489
+ // ../infra/src/fleet-health.ts
34490
+ function uniqueRepos(repos) {
34491
+ const unique = /* @__PURE__ */ new Map();
34492
+ for (const raw of repos) {
34493
+ const repo = raw.trim();
34494
+ if (repo) unique.set(repo.toLowerCase(), unique.get(repo.toLowerCase()) ?? repo);
34495
+ }
34496
+ return [...unique.values()].sort((a, b) => a.localeCompare(b));
34497
+ }
34498
+ function buildFleetHealthReport(registeredRepos, evidence, asOf, maxAgeMs = 24 * 60 * 60 * 1e3) {
34499
+ const repos = uniqueRepos(registeredRepos);
34500
+ const evidenceByRepo = new Map(evidence.map((row) => [row.repo.trim().toLowerCase(), row]));
34501
+ const nowMs = Date.parse(asOf);
34502
+ const drift = [];
34503
+ const unknownEvidence = [];
34504
+ let conforming = 0;
34505
+ let fresh = 0;
34506
+ let stale = 0;
34507
+ let freshnessUnknown = 0;
34508
+ for (const repo of repos) {
34509
+ const row = evidenceByRepo.get(repo.toLowerCase());
34510
+ const reasons = [...row?.unknownEvidence ?? []];
34511
+ let isFresh = false;
34512
+ if (!row) {
34513
+ reasons.push("no #4318 dry-run conformance evidence");
34514
+ freshnessUnknown += 1;
34515
+ } else if (!row.checkedAt) {
34516
+ reasons.push("conformance freshness is unknown");
34517
+ freshnessUnknown += 1;
34518
+ } else {
34519
+ const checkedMs = Date.parse(row.checkedAt);
34520
+ if (!Number.isFinite(checkedMs) || !Number.isFinite(nowMs) || checkedMs > nowMs) {
34521
+ reasons.push(`invalid conformance timestamp: ${row.checkedAt}`);
34522
+ freshnessUnknown += 1;
34523
+ } else if (nowMs - checkedMs > maxAgeMs) {
34524
+ reasons.push(`conformance evidence is stale (${row.checkedAt})`);
34525
+ stale += 1;
34526
+ } else {
34527
+ isFresh = true;
34528
+ fresh += 1;
34529
+ }
34530
+ }
34531
+ if (row?.verdict === "drift") {
34532
+ drift.push({
34533
+ repo,
34534
+ findings: row.findings?.length ? [...row.findings] : ["#4318 verdict reported drift without a finding"]
34535
+ });
34536
+ }
34537
+ if (row?.verdict === "unknown" && reasons.length === 0) reasons.push("#4318 verdict is unknown");
34538
+ if (row?.verdict === "conforming" && isFresh && reasons.length === 0) conforming += 1;
34539
+ if (reasons.length > 0) unknownEvidence.push({ repo, reasons });
34540
+ }
34541
+ const verdict = drift.length > 0 ? "drift" : repos.length > 0 && conforming === repos.length && unknownEvidence.length === 0 ? "conforming" : "unknown";
34542
+ return {
34543
+ verdict,
34544
+ source: "#4318-dry-run-conformance",
34545
+ denominator: repos.length,
34546
+ conforming,
34547
+ drift,
34548
+ freshness: {
34549
+ asOf,
34550
+ maxAgeSeconds: Math.floor(maxAgeMs / 1e3),
34551
+ fresh,
34552
+ stale,
34553
+ unknown: freshnessUnknown
34554
+ },
34555
+ unknownEvidence,
34556
+ readOnly: true
34557
+ };
34558
+ }
34559
+ function fleetHealthExitCode(report) {
34560
+ return report.verdict === "conforming" ? 0 : 1;
34561
+ }
34562
+
34563
+ // src/org-health-query.ts
34564
+ var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
34565
+ function defaultOrgHealthQueryDeps() {
34566
+ return {
34567
+ registeredRepos: async () => {
34568
+ const cfg = await loadConfig();
34569
+ const projects = await fetchProjectsList(registryClientDeps(cfg));
34570
+ if (!projects) throw new Error("live Hub registry roster is unavailable");
34571
+ return orgVersionRepos(projects);
34572
+ },
34573
+ conformanceEvidence: async () => fleetGateConformanceEvidence(await runFleetLockstepGate()),
34574
+ now: () => /* @__PURE__ */ new Date()
34575
+ };
34576
+ }
34577
+ async function runOrgHealthQuery(deps, maxAgeMs = DEFAULT_MAX_AGE_MS) {
34578
+ const repos = await deps.registeredRepos();
34579
+ const evidence = await deps.conformanceEvidence(repos);
34580
+ return buildFleetHealthReport(repos, evidence, deps.now().toISOString(), maxAgeMs);
34581
+ }
34582
+ function formatOrgHealth(report) {
34583
+ const lines = [
34584
+ `org health \u2014 ${report.conforming}/${report.denominator} conforming \xB7 ${report.verdict.toUpperCase()}`,
34585
+ `source: ${report.source} \xB7 read ${report.freshness.asOf}`,
34586
+ `freshness: ${report.freshness.fresh} fresh \xB7 ${report.freshness.stale} stale \xB7 ${report.freshness.unknown} unknown`
34587
+ ];
34588
+ if (report.drift.length) {
34589
+ lines.push("", "drift:");
34590
+ for (const row of report.drift) lines.push(` ${row.repo}: ${row.findings.join("; ")}`);
34591
+ }
34592
+ if (report.unknownEvidence.length) {
34593
+ lines.push("", "unknown evidence:");
34594
+ for (const row of report.unknownEvidence) lines.push(` ${row.repo}: ${row.reasons.join("; ")}`);
34595
+ }
34596
+ return lines.join("\n");
34597
+ }
34598
+ function registerOrgHealthQuery(program3, deps = defaultOrgHealthQueryDeps()) {
34599
+ const org = program3.commands.find((command) => command.name() === "org");
34600
+ if (!org) throw new Error("org health registration requires consolidated command namespaces");
34601
+ org.command("health").description("live read of #4318 fleet lockstep evidence \u2014 N-of-N, named drift, freshness, and unknowns; never writes state").option("--json", "machine-readable output").action(async (options) => {
34602
+ try {
34603
+ const report = await runOrgHealthQuery(deps);
34604
+ console.log(options.json ? JSON.stringify(report, null, 2) : formatOrgHealth(report));
34605
+ process.exitCode = fleetHealthExitCode(report);
34606
+ } catch (error) {
34607
+ console.error(`org health: ${error.message}`);
34608
+ process.exitCode = 1;
34609
+ }
34610
+ });
34611
+ }
34612
+
33890
34613
  // src/plugin-release-catchup.ts
33891
34614
  var import_node_fs43 = require("node:fs");
33892
34615
  var import_node_path40 = require("node:path");
@@ -36838,9 +37561,9 @@ program2.name("mmi-cli").description("MMI Future Hub CLI ? the org control plane
36838
37561
  program2.addHelpText(
36839
37562
  "before",
36840
37563
  `Houses (canonical roots): oracle \xB7 harbour \xB7 devops \xB7 vault \xB7 learning
36841
- \`mmi-cli <house> <command> \u2026\` is the canonical form; the flat \`mmi-cli <command> \u2026\` paths below
36842
- are compatibility shims that die in ${COMPAT_SHIM_DEATH_WAVE} (#4316). \`mmi-cli commands\` shows the
36843
- house-shaped tree; \`mmi-cli <house> --help\` lists one house.
37564
+ \`mmi-cli <house> <command> \u2026\` is the only invocation form for the paths below \u2014 their flat
37565
+ \`mmi-cli <command> \u2026\` aliases were removed in ${COMPAT_SHIM_DEATH_WAVE} (#4316, #4438); core commands stay
37566
+ unprefixed. \`mmi-cli commands\` shows the house-shaped tree; \`mmi-cli <house> --help\` lists one house.
36844
37567
  `
36845
37568
  );
36846
37569
  function appActorDeps() {
@@ -37722,7 +38445,11 @@ docs.command("index").description("regenerate docs/index.md from the docs/ tree
37722
38445
  docs.command("refs").description("deterministic doc reference gate: every backticked repo path, relative .md link, `mmi-cli` command ref, and `<!-- pinned by ? -->` comment across docs/** + README.md + architecture.md must resolve, or exit 1 (fleet-portable port of the Hub gate scripts/check-doc-refs.mjs, #3339)").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").action(async (o) => {
37723
38446
  try {
37724
38447
  const root = await repoRoot();
37725
- const commandPaths = new Set(buildCommandManifest(program2).index.map((entry) => entry.path));
38448
+ const commandPaths = new Set(
38449
+ buildCommandManifest(program2).index.map(
38450
+ (entry) => entry.house && entry.house !== "core" ? `${entry.house} ${entry.path}` : entry.path
38451
+ )
38452
+ );
37726
38453
  const result = runDocRefs(root, { commandPaths });
37727
38454
  if (o.json) {
37728
38455
  consoleIo.log(JSON.stringify({ ok: result.ok, docCount: result.docCount, findings: result.findings, warnings: result.warnings }, null, 2));
@@ -38749,6 +39476,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
38749
39476
  return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
38750
39477
  }
38751
39478
  body = normalizeClosingDirectives(body);
39479
+ body = rewriteNegatedClosingPhrases(body);
38752
39480
  const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
38753
39481
  if (docsCheck && !docsCheck.ok) {
38754
39482
  return fail(`pr create: ${docsCheck.detail} ? ${docsCheck.fix}`);
@@ -39216,11 +39944,14 @@ registerQueryCommands(program2);
39216
39944
  registerWorktreeCommands(program2);
39217
39945
  registerIssueLifecycleCommands(program2, { attach: attachToProject });
39218
39946
  registerTrainCommands(program2);
39947
+ registerFleetTrackInventory(program2);
39948
+ registerFleetLockstepGate(program2);
39219
39949
  registerDeployCommands(program2);
39220
39950
  registerDiscoveryCommands(program2);
39221
39951
  registerExplainCommand(program2);
39222
39952
  registerPrLifecycleCommands(program2);
39223
39953
  registerSessionReport(program2);
39954
+ registerLearningClosureRateCommand(program2);
39224
39955
  registerBoardCommands(program2);
39225
39956
  registerStageCommands(program2);
39226
39957
  var GH_TRAIN_TIMEOUT_MS = 3e4;
@@ -39237,7 +39968,10 @@ function trainApplyDeps() {
39237
39968
  throw surfaceTrainSubprocessFailure(e, file, args);
39238
39969
  }
39239
39970
  },
39240
- runSelf: async (args) => (await execFileP2(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
39971
+ // canonicalArgvFor (#4438): callers hand this seam house-relative paths (`org project get`,
39972
+ // `secrets preflight`); since the Wave 3 flat-alias cut only the canonical house-prefixed form
39973
+ // parses, so the ONE self-spawn seam derives the prefix through the same rule the refusal names.
39974
+ runSelf: async (args) => (await execFileP2(process.execPath, [process.argv[1], ...canonicalArgvFor(args)], { timeout: 3e4 })).stdout,
39241
39975
  trainAuthority: async (repo) => {
39242
39976
  const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
39243
39977
  return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
@@ -39917,12 +40651,21 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
39917
40651
  });
39918
40652
  installProcessBackstop();
39919
40653
  consolidateCommandNamespaces(program2);
40654
+ registerOrgHealthQuery(program2);
39920
40655
  applyCommandTaxonomy(program2);
39921
40656
  function resolveHouseShim(argv) {
39922
40657
  const houseToken = argv[2];
39923
40658
  if (!houseToken) return;
39924
40659
  if (!isHouseRoot(houseToken)) {
39925
- return;
40660
+ const flatTokens = argv.slice(2, 4).filter((tok) => !tok.startsWith("-"));
40661
+ const flatPath = flatTokens.join(" ");
40662
+ const flatHouse = houseForPath(flatPath);
40663
+ if (!flatHouse || flatHouse === "core") return;
40664
+ process.stderr.write(
40665
+ `mmi-cli: the flat '${flatPath}' alias was removed in ${COMPAT_SHIM_DEATH_WAVE} (#4316) \u2014 run: mmi-cli ${canonicalPathFor(flatPath)} \u2026
40666
+ `
40667
+ );
40668
+ hardExit(2);
39926
40669
  }
39927
40670
  const remainder = argv.slice(3);
39928
40671
  const first = remainder[0];
@@ -39941,10 +40684,9 @@ function resolveHouseShim(argv) {
39941
40684
  argv.splice(2, 1);
39942
40685
  return;
39943
40686
  }
39944
- const canonical = actual ? `mmi-cli ${actual === "core" ? "" : `${actual} `}${lookupPath}`.replace(/\s+/g, " ").trim() : void 0;
39945
40687
  if (actual) {
39946
40688
  process.stderr.write(
39947
- `mmi-cli ${houseToken}: '${lookupPath}' lives in house '${actual}' \u2014 run: ${canonical} \u2026
40689
+ `mmi-cli ${houseToken}: '${lookupPath}' lives in house '${actual}' \u2014 run: mmi-cli ${canonicalPathFor(lookupPath)} \u2026
39948
40690
  `
39949
40691
  );
39950
40692
  } else {
@@ -39967,8 +40709,8 @@ function printHouseRootHelp(house) {
39967
40709
  }
39968
40710
  lines.push(
39969
40711
  "",
39970
- `Canonical: \`mmi-cli ${house} <command> \u2026\`. The flat \`mmi-cli <command> \u2026\` form is a compatibility`,
39971
- `shim keeping fleet scripts working for one window \u2014 shims die in ${COMPAT_SHIM_DEATH_WAVE} (#4316).`
40712
+ `Canonical: \`mmi-cli ${house} <command> \u2026\`. The flat \`mmi-cli <command> \u2026\` compatibility shims`,
40713
+ `were removed in ${COMPAT_SHIM_DEATH_WAVE} (#4316, #4438) \u2014 only the canonical house-prefixed form parses.`
39972
40714
  );
39973
40715
  consoleIo.log(lines.join("\n"));
39974
40716
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.106.0",
3
+ "version": "3.107.0",
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",