@mutmutco/cli 3.106.0 → 3.107.1

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 +799 -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
  ];
@@ -12039,6 +12053,25 @@ async function alignExistingHotfixReleaseMarker(deps, version) {
12039
12053
  if (out === "stamped") return `jervaiseRelease hotfix marker stamped for ${version}`;
12040
12054
  return null;
12041
12055
  }
12056
+ async function clearStaleHotfixReleaseMarker(deps, version) {
12057
+ const script = [
12058
+ "const fs = require('fs');",
12059
+ "const p = 'package.json';",
12060
+ "if (!fs.existsSync(p)) { process.stdout.write('absent'); process.exit(0); }",
12061
+ "const j = JSON.parse(fs.readFileSync(p, 'utf8'));",
12062
+ "const marker = j.jervaiseRelease;",
12063
+ "if (!(marker && typeof marker === 'object' && marker.kind === 'hotfix')) {",
12064
+ " process.stdout.write('skip');",
12065
+ " process.exit(0);",
12066
+ "}",
12067
+ "delete j.jervaiseRelease;",
12068
+ "fs.writeFileSync(p, JSON.stringify(j, null, 2) + '\\n');",
12069
+ "process.stdout.write('cleared');"
12070
+ ].join("");
12071
+ const out = (await deps.run("node", ["-e", script])).trim();
12072
+ if (out === "cleared") return `stale jervaiseRelease hotfix marker cleared for train line ${version}`;
12073
+ return null;
12074
+ }
12042
12075
  async function installAppFoldDeps(deps) {
12043
12076
  const hasLock = await deps.run("git", ["cat-file", "-e", "HEAD:package-lock.json"]).then(() => true).catch(() => false);
12044
12077
  await deps.run("npm", hasLock ? ["ci"] : ["install"]);
@@ -12115,6 +12148,9 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
12115
12148
  if (model === "registry-publish" && Number.isFinite(patch) && patch > 0) {
12116
12149
  await alignExistingHotfixReleaseMarker(deps, version);
12117
12150
  }
12151
+ if (model === "registry-publish" && Number.isFinite(patch) && patch === 0) {
12152
+ await clearStaleHotfixReleaseMarker(deps, version);
12153
+ }
12118
12154
  }
12119
12155
  for (const path2 of foldPaths) {
12120
12156
  await deps.run("git", ["add", "--", path2]).catch(() => void 0);
@@ -19706,9 +19742,15 @@ async function claimOneBoardItem(ctx, selector, options) {
19706
19742
  const contest = await checkLaneContest(client, item);
19707
19743
  if (contest.contested) throw new Error(laneContestMessage(item.ref, contest, "claim"));
19708
19744
  }
19745
+ if (options.check) {
19746
+ return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true, checked: true };
19747
+ }
19709
19748
  await postClaimMarkerComment(client, item);
19710
19749
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true };
19711
19750
  }
19751
+ if (options.check) {
19752
+ return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, checked: true };
19753
+ }
19712
19754
  try {
19713
19755
  await client.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
19714
19756
  } catch (e) {
@@ -19764,7 +19806,7 @@ async function claimBoardIssues(options, deps = {}) {
19764
19806
  const ref = `${selector.repo}#${selector.number}`;
19765
19807
  try {
19766
19808
  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 };
19809
+ 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
19810
  } catch (e) {
19769
19811
  results[index] = { ref, claimed: false, reason: e.message };
19770
19812
  }
@@ -20748,7 +20790,7 @@ function isHouseRoot(token) {
20748
20790
  return HOUSE_ROOTS.includes(token);
20749
20791
  }
20750
20792
  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)`;
20793
+ 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
20794
  var HOUSE_QUESTIONS = {
20753
20795
  oracle: "Is it live?",
20754
20796
  harbour: "Is it bounded?",
@@ -20808,6 +20850,8 @@ var HOUSE_MAP = {
20808
20850
  report: "learning",
20809
20851
  // friction reports
20810
20852
  "skill-lesson": "learning",
20853
+ "closure-rate": "learning",
20854
+ // closure rate per loop kind, computed at read (#4440)
20811
20855
  // --- core — the front door itself ---------------------------------------------------------------
20812
20856
  commands: "core",
20813
20857
  whoami: "core",
@@ -20842,6 +20886,11 @@ function canonicalPathFor(path2) {
20842
20886
  if (!house) return void 0;
20843
20887
  return house === "core" ? path2 : `${house} ${path2}`;
20844
20888
  }
20889
+ function canonicalArgvFor(args) {
20890
+ const lookup = args.filter((tok) => !tok.startsWith("-")).slice(0, 2).join(" ");
20891
+ const house = houseForPath(lookup);
20892
+ return house && house !== "core" ? [house, ...args] : args;
20893
+ }
20845
20894
 
20846
20895
  // src/command-taxonomy.ts
20847
20896
  var COMMAND_METADATA = /* @__PURE__ */ Symbol.for("mmi.commandTaxonomy.metadata");
@@ -20853,10 +20902,10 @@ var PRIMARY_GROUPS = [
20853
20902
  // step invokes (`docs refs`, `tests policy`), not org-plane operations (#3605). `spawn policy`
20854
20903
  // joins them on the same footing (#3979).
20855
20904
  ["Setup and support", ["bootstrap", "secrets", "docs", "repo-index", "tests", "spawn"]],
20856
- ["Coordinate and improve", ["wave", "report", "skill-lesson"]]
20905
+ ["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
20857
20906
  ];
20858
20907
  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"]);
20908
+ var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "repo-index", "find", "tests", "spawn", "wave", "report", "skill-lesson", "closure-rate"]);
20860
20909
  var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
20861
20910
  var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
20862
20911
  var topLevelPosition = 0;
@@ -20906,6 +20955,7 @@ var COMMAND_OWNERSHIP = {
20906
20955
  wave: { module_owner: "cli/src/wave-land.ts", consumer: "campaign-orchestrator" },
20907
20956
  report: { module_owner: "cli/src/report.ts", consumer: "campaign-orchestrator" },
20908
20957
  "skill-lesson": { module_owner: "cli/src/skill-lesson.ts", consumer: "campaign-orchestrator" },
20958
+ "closure-rate": { module_owner: "cli/src/learning-closure-rate.ts", consumer: "campaign-orchestrator" },
20909
20959
  org: { module_owner: "cli/src/command-consolidation.ts", consumer: "org-operator" },
20910
20960
  runtime: { module_owner: "cli/src/command-consolidation.ts", consumer: "runtime-operator" },
20911
20961
  plugin: { module_owner: "cli/src/plugin-guard-io.ts", consumer: "host-runtime" }
@@ -21121,7 +21171,7 @@ function buildCommand(cmd, path2) {
21121
21171
  ...metadata
21122
21172
  };
21123
21173
  if (cmd._allowUnknownOption) out.parses_own_argv = true;
21124
- if (path2 && house !== "core") out.shim = true;
21174
+ if (path2 && house !== "core") out.flat_removed = true;
21125
21175
  const description = cmd.description();
21126
21176
  if (description) out.description = description;
21127
21177
  const examples = readExamples(cmd);
@@ -21170,19 +21220,23 @@ function buildCommandManifest(program3) {
21170
21220
  const primaryTree = primaryCopy(tree, true);
21171
21221
  const primaryIndex = [];
21172
21222
  collectLeaves(primaryTree, primaryIndex);
21223
+ const houses = buildHouses(tree);
21173
21224
  const manifest = {
21174
21225
  schema_version: 2,
21175
21226
  scope: "all",
21176
21227
  name: tree.name,
21177
21228
  tree,
21178
21229
  index,
21179
- houses: buildHouses(tree),
21230
+ houses,
21180
21231
  doors: buildDoorsCatalog(tree),
21181
- shim_contract: {
21232
+ flat_alias_cut: {
21182
21233
  canonical: "mmi-cli <house> <command> \u2026",
21183
- shim: "mmi-cli <command> \u2026",
21184
- dies: COMPAT_SHIM_DEATH_WAVE,
21185
- note: COMPAT_SHIM_NOTE
21234
+ removed: "mmi-cli <command> \u2026",
21235
+ wave: COMPAT_SHIM_DEATH_WAVE,
21236
+ note: FLAT_ALIAS_CUT_NOTE,
21237
+ // The cut record names every removed alias by path — the same entry points the houses block
21238
+ // presents, so the record can never drift from what actually refuses.
21239
+ removed_aliases: [...new Set(houses.flatMap((house) => house.commands.map((entry) => entry.path)))].sort()
21186
21240
  },
21187
21241
  primary_tree: primaryTree,
21188
21242
  primary_index: primaryIndex,
@@ -21280,8 +21334,8 @@ function formatManifestHuman(manifest, options = {}) {
21280
21334
  }
21281
21335
  lines.push(
21282
21336
  "",
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).`,
21337
+ `Canonical form: \`mmi-cli <house> <command> \u2026\`. The paths above are house-relative \u2014 their flat`,
21338
+ `Wave 0 aliases were removed in ${COMPAT_SHIM_DEATH_WAVE} (#4316, #4438); core stays unprefixed.`,
21285
21339
  "",
21286
21340
  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
21341
  );
@@ -28658,7 +28712,7 @@ function registerBootstrapCommands(program3) {
28658
28712
  target: DOCS_INDEX_PATH,
28659
28713
  action: indexAction,
28660
28714
  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"
28715
+ 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
28716
  });
28663
28717
  if (o.execute && indexAction === "create") {
28664
28718
  await putSeed(DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0);
@@ -29668,10 +29722,13 @@ function registerBoardCommands(program3) {
29668
29722
  return failGraceful(`board read failed: ${withDiscoverMissDetail(e.message)}`);
29669
29723
  }
29670
29724
  }
29725
+ function checkVerdict(ref, alreadyClaimed) {
29726
+ 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)`;
29727
+ }
29671
29728
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
29672
29729
  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
29730
  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"),
29731
+ 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
29732
  (_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
29676
29733
  ).action(async (issueRefs, o) => {
29677
29734
  if (issueRefs.length === 1) {
@@ -29683,11 +29740,12 @@ function registerBoardCommands(program3) {
29683
29740
  repo: o.repo,
29684
29741
  assignee: o.for,
29685
29742
  force: o.force,
29743
+ check: o.check,
29686
29744
  allowPartial: o.allowPartial
29687
29745
  });
29688
- invalidateStatuslineBoardCache();
29746
+ if (!result.checked) invalidateStatuslineBoardCache();
29689
29747
  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`);
29748
+ 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
29749
  } catch (e) {
29692
29750
  return failGraceful(`board claim failed: ${e.message}`);
29693
29751
  }
@@ -29700,14 +29758,15 @@ function registerBoardCommands(program3) {
29700
29758
  repo: o.repo,
29701
29759
  assignee: o.for,
29702
29760
  force: o.force,
29761
+ check: o.check,
29703
29762
  allowPartial: o.allowPartial
29704
29763
  });
29705
- if (bulk.results.some((r) => r.claimed)) invalidateStatuslineBoardCache();
29764
+ if (bulk.results.some((r) => r.claimed && !r.checked)) invalidateStatuslineBoardCache();
29706
29765
  if (o.json) {
29707
29766
  console.log(JSON.stringify(bulk.results));
29708
29767
  } else {
29709
29768
  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}`);
29769
+ 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
29770
  }
29712
29771
  }
29713
29772
  if (bulk.failed > 0) process.exitCode = 1;
@@ -29716,11 +29775,13 @@ function registerBoardCommands(program3) {
29716
29775
  }
29717
29776
  }), [
29718
29777
  "mmi-cli board claim 2680",
29719
- "mmi-cli board claim 2680 2681 --for teammate-login"
29778
+ "mmi-cli board claim 2680 2681 --for teammate-login",
29779
+ "mmi-cli board claim 2680 --check"
29720
29780
  ], [
29721
29781
  "Pass raw issue numbers/refs, not URLs.",
29722
29782
  "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."
29783
+ "Multiple refs are handled as a batch and return per-item results.",
29784
+ "--check is the live gate read (it calls GitHub); --dry-run only echoes the parsed argv plan."
29724
29785
  ]);
29725
29786
  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
29787
  try {
@@ -32635,6 +32696,392 @@ function registerTrainCommands(program3) {
32635
32696
  });
32636
32697
  }
32637
32698
 
32699
+ // src/fleet-track-inventory.ts
32700
+ var NO_DEPLOY_SURFACE_MODELS = /* @__PURE__ */ new Set(["none", "content"]);
32701
+ function ghErrorText(e) {
32702
+ const err = e;
32703
+ return String(err?.stderr || err?.message || e).trim().replace(/\s+/g, " ");
32704
+ }
32705
+ function isGhNotFound2(e) {
32706
+ const err = e;
32707
+ return /\bNOT[_ ]FOUND\b|HTTP 404/i.test(`${err?.stderr ?? ""} ${err?.message ?? ""} ${err?.stdout ?? ""}`);
32708
+ }
32709
+ async function readFleetPluginProbe(deps, repo, branch) {
32710
+ const [owner, name] = repo.split("/");
32711
+ const ref = branch ? `?ref=${encodeURIComponent(branch)}` : "";
32712
+ try {
32713
+ const raw = await deps.ghJson([
32714
+ "api",
32715
+ `repos/${encodeURIComponent(owner ?? "")}/${encodeURIComponent(name ?? "")}/contents/.claude-plugin/plugin.json${ref}`
32716
+ ]);
32717
+ if (raw.encoding !== "base64" || typeof raw.content !== "string") {
32718
+ return { repo, state: "unknown", unknown: "plugin.json contents read returned no base64 content" };
32719
+ }
32720
+ const decoded = Buffer.from(raw.content.replace(/\s/g, ""), "base64").toString("utf8");
32721
+ const manifest = JSON.parse(decoded);
32722
+ return {
32723
+ repo,
32724
+ state: "present",
32725
+ version: typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0
32726
+ };
32727
+ } catch (e) {
32728
+ return isGhNotFound2(e) ? { repo, state: "absent" } : { repo, state: "unknown", unknown: `plugin surface read failed: ${ghErrorText(e)}` };
32729
+ }
32730
+ }
32731
+ async function mapBounded2(values, limit, read) {
32732
+ const results = new Array(values.length);
32733
+ let next = 0;
32734
+ const worker = async () => {
32735
+ while (next < values.length) {
32736
+ const index = next++;
32737
+ results[index] = await read(values[index]);
32738
+ }
32739
+ };
32740
+ await Promise.all(Array.from({ length: Math.min(limit, values.length) }, () => worker()));
32741
+ return results;
32742
+ }
32743
+ function validRepo2(repo) {
32744
+ return typeof repo === "string" && /^[^/\s]+\/[^/\s]+$/.test(repo.trim());
32745
+ }
32746
+ function classifySurfaces(project2, plugin) {
32747
+ const board = project2?.projectId && project2.projectNumber !== void 0 && project2.statusFieldId ? "present" : "absent";
32748
+ const vault = typeof project2?.vaultPath === "string" && project2.vaultPath.trim() ? "present" : "absent";
32749
+ const deployModel = typeof project2?.deployModel === "string" ? project2.deployModel : void 0;
32750
+ const deploy = deployModel && !NO_DEPLOY_SURFACE_MODELS.has(deployModel) ? "present" : "absent";
32751
+ return { board, vault, deploy, plugin: plugin?.state ?? "unknown" };
32752
+ }
32753
+ function classificationsOf(surfaces) {
32754
+ const classes = [];
32755
+ if (surfaces.board === "absent") classes.push("no-board");
32756
+ if (surfaces.vault === "absent") classes.push("no-vault");
32757
+ if (surfaces.plugin === "absent") classes.push("no-plugin");
32758
+ if (surfaces.deploy === "absent") classes.push("no-deploy-surface");
32759
+ return classes;
32760
+ }
32761
+ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generatedAt) {
32762
+ const releaseByRepo = new Map(releaseProbes.map((p) => [p.repo.toLowerCase(), p]));
32763
+ const pluginByRepo = new Map(pluginProbes.map((p) => [p.repo.toLowerCase(), p]));
32764
+ const projectByRepo = /* @__PURE__ */ new Map();
32765
+ const anomalies = [];
32766
+ for (const project2 of projects) {
32767
+ const repos2 = (project2.repos ?? []).filter(validRepo2);
32768
+ if (!repos2.length) {
32769
+ anomalies.push({
32770
+ kind: "project-without-repo",
32771
+ slug: String(project2.slug ?? "?"),
32772
+ name: String(project2.name ?? project2.slug ?? "?")
32773
+ });
32774
+ }
32775
+ for (const repo of repos2) {
32776
+ const key = repo.trim().toLowerCase();
32777
+ projectByRepo.set(key, projectByRepo.get(key) ?? project2);
32778
+ }
32779
+ }
32780
+ const repos = orgVersionRepos(projects).map((repo) => {
32781
+ const project2 = projectByRepo.get(repo.toLowerCase());
32782
+ const release = releaseByRepo.get(repo.toLowerCase());
32783
+ const plugin = pluginByRepo.get(repo.toLowerCase());
32784
+ const surfaces = classifySurfaces(project2, plugin);
32785
+ const unknowns = [...release?.unknowns ?? []];
32786
+ if (!release) unknowns.push("GitHub release evidence was not collected");
32787
+ if (plugin?.unknown) unknowns.push(plugin.unknown);
32788
+ if (!plugin) unknowns.push("plugin surface was not probed");
32789
+ return {
32790
+ repo,
32791
+ slug: String(project2?.slug ?? repo.split("/")[1] ?? repo).toLowerCase(),
32792
+ track: resolveReleaseTrack(project2, void 0, repo),
32793
+ declaredTrack: typeof project2?.releaseTrack === "string" ? project2.releaseTrack : null,
32794
+ branch: typeof project2?.branch === "string" ? project2.branch : null,
32795
+ repoClass: typeof project2?.class === "string" ? project2.class : null,
32796
+ deployModel: typeof project2?.deployModel === "string" ? project2.deployModel : null,
32797
+ surfaces,
32798
+ classifications: classificationsOf(surfaces),
32799
+ pluginVersion: plugin?.version ?? null,
32800
+ release: {
32801
+ tag: release?.releaseTag ?? null,
32802
+ version: release?.releasedVersion ?? null,
32803
+ at: release?.releasedAt ?? null,
32804
+ coordinatedVersion: release?.coordinatedVersion ?? null,
32805
+ coordinatedSource: release?.coordinatedSource ?? null
32806
+ },
32807
+ ci: {
32808
+ declared: typeof project2?.ci === "string" ? project2.ci : null,
32809
+ requiredChecks: Array.isArray(project2?.requiredChecks) ? project2.requiredChecks : null,
32810
+ exemptReason: typeof project2?.ciExemptReason === "string" ? project2.ciExemptReason : null
32811
+ },
32812
+ source: { registry: "hub-registry-live", release: "github-releases-live", plugin: "github-contents-live" },
32813
+ freshness: { readAt: generatedAt, releasedAt: release?.releasedAt ?? null },
32814
+ unknowns
32815
+ };
32816
+ });
32817
+ const tracks = { full: 0, direct: 0, trunk: 0 };
32818
+ for (const row of repos) tracks[row.track] += 1;
32819
+ return {
32820
+ generatedAt,
32821
+ source: {
32822
+ roster: "hub-registry-live",
32823
+ releases: "github-releases-live",
32824
+ plugin: "github-contents-live",
32825
+ committedInventory: "never"
32826
+ },
32827
+ counts: {
32828
+ projects: projects.length,
32829
+ registeredRepos: repos.length,
32830
+ tracks,
32831
+ noBoard: repos.filter((r) => r.classifications.includes("no-board")).length,
32832
+ noVault: repos.filter((r) => r.classifications.includes("no-vault")).length,
32833
+ noPlugin: repos.filter((r) => r.classifications.includes("no-plugin")).length,
32834
+ noDeploySurface: repos.filter((r) => r.classifications.includes("no-deploy-surface")).length,
32835
+ unknownRows: repos.filter((r) => r.unknowns.length > 0).length,
32836
+ anomalies: anomalies.length
32837
+ },
32838
+ repos,
32839
+ anomalies
32840
+ };
32841
+ }
32842
+ function defaultFleetTrackInventoryDeps() {
32843
+ return {
32844
+ projects: async () => {
32845
+ const cfg = await loadConfig();
32846
+ const projects = await fetchProjectsList(registryClientDeps(cfg));
32847
+ 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");
32848
+ return projects;
32849
+ },
32850
+ query: queryDeps(),
32851
+ now: () => /* @__PURE__ */ new Date()
32852
+ };
32853
+ }
32854
+ async function readFleetTrackInventory(deps = defaultFleetTrackInventoryDeps()) {
32855
+ const projects = await deps.projects();
32856
+ const repos = orgVersionRepos(projects);
32857
+ const branchByRepo = /* @__PURE__ */ new Map();
32858
+ for (const project2 of projects) {
32859
+ for (const repo of (project2.repos ?? []).filter(validRepo2)) {
32860
+ const key = repo.trim().toLowerCase();
32861
+ if (!branchByRepo.has(key)) {
32862
+ branchByRepo.set(key, typeof project2.branch === "string" ? project2.branch : void 0);
32863
+ }
32864
+ }
32865
+ }
32866
+ const [releaseProbes, pluginProbes] = await Promise.all([
32867
+ mapBounded2(repos, 6, (repo) => readOrgVersionProbe(deps.query, repo)),
32868
+ mapBounded2(repos, 6, (repo) => readFleetPluginProbe(deps.query, repo, branchByRepo.get(repo.toLowerCase())))
32869
+ ]);
32870
+ return buildFleetTrackInventory(projects, releaseProbes, pluginProbes, deps.now().toISOString());
32871
+ }
32872
+ function formatFleetTrackInventory(report) {
32873
+ const { counts } = report;
32874
+ const lines = [
32875
+ `fleet version-track inventory \u2014 ${counts.registeredRepos} registered repositories (${counts.projects} registry projects)`,
32876
+ `source: live Hub registry + live GitHub releases/contents \xB7 read ${report.generatedAt} \xB7 committed inventory: never`,
32877
+ `tracks: ${counts.tracks.full} full \xB7 ${counts.tracks.direct} direct \xB7 ${counts.tracks.trunk} trunk`,
32878
+ `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`,
32879
+ "",
32880
+ "repository | track | deploy model | released | plugin | classifications"
32881
+ ];
32882
+ for (const row of report.repos) {
32883
+ lines.push(
32884
+ `${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"}`
32885
+ );
32886
+ for (const unknown of row.unknowns) lines.push(` ? ${unknown}`);
32887
+ }
32888
+ if (report.anomalies.length) {
32889
+ lines.push("", "anomalies:");
32890
+ for (const anomaly of report.anomalies) {
32891
+ lines.push(` ? ${anomaly.slug} (${anomaly.name}) \u2014 registry project has no registered repository`);
32892
+ }
32893
+ }
32894
+ return lines.join("\n");
32895
+ }
32896
+ function registerFleetTrackInventory(program3) {
32897
+ const train = program3.commands.find((c) => c.name() === "train");
32898
+ if (!train) throw new Error("train inventory registration requires the train command group");
32899
+ 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) => {
32900
+ try {
32901
+ const report = await readFleetTrackInventory();
32902
+ console.log(o.json ? JSON.stringify(report, null, 2) : formatFleetTrackInventory(report));
32903
+ } catch (e) {
32904
+ return failGraceful(`train inventory: ${e.message}`);
32905
+ }
32906
+ });
32907
+ }
32908
+
32909
+ // ../infra/src/version-gate.ts
32910
+ var HUB_REPO5 = "mutmutco/mmi-hub";
32911
+ var NO_RELEASE_UNKNOWN = "no published GitHub Release";
32912
+ function semverEqual(a, b) {
32913
+ return versionAtLeast(a, b) && versionAtLeast(b, a);
32914
+ }
32915
+ function semverRelation(a, b) {
32916
+ if (!parseSemver(a) || !parseSemver(b)) return null;
32917
+ if (semverEqual(a, b)) return "equal";
32918
+ return versionAtLeast(a, b) ? "ahead" : "behind";
32919
+ }
32920
+ function classifyRow(row) {
32921
+ const findings = [];
32922
+ const reasons = /* @__PURE__ */ new Set();
32923
+ const releaseRequired = row.track !== "trunk";
32924
+ for (const unknown of row.unknowns) {
32925
+ if (!releaseRequired && unknown === NO_RELEASE_UNKNOWN) continue;
32926
+ reasons.add(unknown);
32927
+ }
32928
+ const { version, coordinatedVersion, coordinatedSource, tag } = row.release;
32929
+ if (releaseRequired && !version && ![...reasons].some((r) => /release/i.test(r))) {
32930
+ reasons.add(NO_RELEASE_UNKNOWN);
32931
+ }
32932
+ if (releaseRequired && version && !coordinatedVersion && ![...reasons].some((r) => /version lock|distribution-bom|package\.json/i.test(r))) {
32933
+ reasons.add(`coordinated version pointer unknown at ${tag ?? "the release tag"}`);
32934
+ }
32935
+ if (version && coordinatedVersion) {
32936
+ const relation = semverRelation(version, coordinatedVersion);
32937
+ if (relation === null) {
32938
+ reasons.add(`released ${version} and ${coordinatedSource ?? "version lock"} ${coordinatedVersion} are not comparable semver`);
32939
+ } else if (relation !== "equal") {
32940
+ findings.push(`released ${version} is ${relation === "ahead" ? "ahead of" : "behind"} the tag-scoped version lock ${coordinatedVersion} (${coordinatedSource ?? "unknown source"})`);
32941
+ }
32942
+ }
32943
+ if (row.pluginVersion && version) {
32944
+ const relation = semverRelation(row.pluginVersion, version);
32945
+ if (relation === null) {
32946
+ reasons.add(`plugin surface version ${row.pluginVersion} and released ${version} are not comparable semver`);
32947
+ } else if (relation === "behind") {
32948
+ findings.push(`plugin surface ships ${row.pluginVersion}, behind the released ${version}`);
32949
+ }
32950
+ }
32951
+ const unknownReasons = [...reasons].sort((a, b) => a.localeCompare(b));
32952
+ return {
32953
+ repo: row.repo,
32954
+ track: row.track,
32955
+ deployModel: row.deployModel,
32956
+ verdict: findings.length > 0 ? "drift" : unknownReasons.length > 0 ? "unknown" : "conforming",
32957
+ findings,
32958
+ unknownReasons,
32959
+ checkedAt: row.freshness.readAt
32960
+ };
32961
+ }
32962
+ function canaryOrder(a, b) {
32963
+ const rank = { conforming: 0, unknown: 1, drift: 2 };
32964
+ if (rank[a.result.verdict] !== rank[b.result.verdict]) return rank[a.result.verdict] - rank[b.result.verdict];
32965
+ const aAt = a.releasedAt ? Date.parse(a.releasedAt) : Number.NEGATIVE_INFINITY;
32966
+ const bAt = b.releasedAt ? Date.parse(b.releasedAt) : Number.NEGATIVE_INFINITY;
32967
+ if (aAt !== bAt) return bAt - aAt;
32968
+ return a.result.repo.localeCompare(b.result.repo);
32969
+ }
32970
+ function buildCanaryClasses(rows, resultByRepo) {
32971
+ const byModel = /* @__PURE__ */ new Map();
32972
+ for (const row of rows) {
32973
+ const model = row.deployModel ?? "undeclared";
32974
+ const result = resultByRepo.get(row.repo);
32975
+ if (!result) continue;
32976
+ const bucket = byModel.get(model) ?? [];
32977
+ bucket.push({ result, releasedAt: row.freshness.releasedAt });
32978
+ byModel.set(model, bucket);
32979
+ }
32980
+ return [...byModel.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([deployModel, members]) => {
32981
+ const ordered = [...members].sort(canaryOrder);
32982
+ return {
32983
+ deployModel,
32984
+ canary: ordered[0]?.result.repo ?? null,
32985
+ cohort: ordered.slice(1).map((m) => m.result.repo),
32986
+ conforming: members.filter((m) => m.result.verdict === "conforming").length,
32987
+ drift: members.filter((m) => m.result.verdict === "drift").length,
32988
+ unknown: members.filter((m) => m.result.verdict === "unknown").length
32989
+ };
32990
+ });
32991
+ }
32992
+ function evaluateFleetLockstepGate(rows, evaluatedAt) {
32993
+ const results = rows.map(classifyRow);
32994
+ const resultByRepo = new Map(results.map((r) => [r.repo, r]));
32995
+ const drift = results.filter((r) => r.verdict === "drift").map((r) => ({ repo: r.repo, findings: [...r.findings] }));
32996
+ const unknowns = results.filter((r) => r.verdict === "unknown").map((r) => ({ repo: r.repo, reasons: [...r.unknownReasons] }));
32997
+ const conformingRepos = results.filter((r) => r.verdict === "conforming").map((r) => r.repo);
32998
+ const hubOnly = rows.length > 0 && rows.every((row) => row.repo.trim().toLowerCase() === HUB_REPO5);
32999
+ const failReasons = [];
33000
+ if (rows.length === 0) failReasons.push("empty roster: no registered repositories were evaluated");
33001
+ if (hubOnly) failReasons.push("hub-only coverage: the fleet gate refuses a result covering only mutmutco/MMI-Hub");
33002
+ if (drift.length > 0) failReasons.push(`${drift.length} repositor${drift.length === 1 ? "y" : "ies"} with drift`);
33003
+ if (unknowns.length > 0) failReasons.push(`${unknowns.length} repositor${unknowns.length === 1 ? "y" : "ies"} with unknown evidence`);
33004
+ return {
33005
+ gate: "fleet-lockstep-conformance",
33006
+ mode: "dry-run",
33007
+ enforcement: "none",
33008
+ verdict: failReasons.length === 0 && conformingRepos.length === rows.length ? "pass" : "fail",
33009
+ failReasons,
33010
+ denominator: rows.length,
33011
+ conforming: conformingRepos.length,
33012
+ conformingRepos,
33013
+ drift,
33014
+ unknowns,
33015
+ canaryClasses: buildCanaryClasses(rows, resultByRepo),
33016
+ hubOnly,
33017
+ repos: results,
33018
+ evaluatedAt
33019
+ };
33020
+ }
33021
+
33022
+ // src/fleet-lockstep-gate.ts
33023
+ async function runFleetLockstepGate(deps = defaultFleetTrackInventoryDeps()) {
33024
+ const inventory = await readFleetTrackInventory(deps);
33025
+ return evaluateFleetLockstepGate(inventory.repos, inventory.generatedAt);
33026
+ }
33027
+ function fleetGateConformanceEvidence(result) {
33028
+ return result.repos.map((row) => ({
33029
+ repo: row.repo,
33030
+ verdict: row.verdict,
33031
+ checkedAt: row.checkedAt,
33032
+ findings: row.findings.length ? [...row.findings] : void 0,
33033
+ unknownEvidence: row.unknownReasons.length ? [...row.unknownReasons] : void 0
33034
+ }));
33035
+ }
33036
+ function formatFleetLockstepGate(result) {
33037
+ const lines = [
33038
+ `fleet lockstep gate (dry-run) \u2014 ${result.conforming}/${result.denominator} conforming \xB7 ${result.verdict.toUpperCase()}`,
33039
+ `evaluated ${result.evaluatedAt} \xB7 live D2a inventory \xB7 enforcement: none (D2c #4453)`
33040
+ ];
33041
+ if (result.failReasons.length) {
33042
+ lines.push("", "fail reasons:");
33043
+ for (const reason of result.failReasons) lines.push(` ! ${reason}`);
33044
+ }
33045
+ if (result.conformingRepos.length) {
33046
+ lines.push("", `conforming set (${result.conformingRepos.length}):`);
33047
+ for (const repo of result.conformingRepos) lines.push(` = ${repo}`);
33048
+ }
33049
+ if (result.drift.length) {
33050
+ lines.push("", `drift (${result.drift.length}):`);
33051
+ for (const row of result.drift) {
33052
+ lines.push(` x ${row.repo}`);
33053
+ for (const finding of row.findings) lines.push(` ${finding}`);
33054
+ }
33055
+ }
33056
+ if (result.unknowns.length) {
33057
+ lines.push("", `unknown evidence (${result.unknowns.length}):`);
33058
+ for (const row of result.unknowns) {
33059
+ lines.push(` ? ${row.repo}`);
33060
+ for (const reason of row.reasons) lines.push(` ${reason}`);
33061
+ }
33062
+ }
33063
+ lines.push("", "canary classes by deploy model (canary -> cohort):");
33064
+ for (const cls of result.canaryClasses) {
33065
+ lines.push(
33066
+ ` ${cls.deployModel}: canary ${cls.canary ?? "NONE"}` + (cls.cohort.length ? ` -> ${cls.cohort.join(", ")}` : "") + ` (${cls.conforming} conforming \xB7 ${cls.drift} drift \xB7 ${cls.unknown} unknown)`
33067
+ );
33068
+ }
33069
+ return lines.join("\n");
33070
+ }
33071
+ function registerFleetLockstepGate(program3) {
33072
+ const train = program3.commands.find((c) => c.name() === "train");
33073
+ if (!train) throw new Error("train gate registration requires the train command group");
33074
+ 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) => {
33075
+ try {
33076
+ const result = await runFleetLockstepGate();
33077
+ console.log(o.json ? JSON.stringify(result, null, 2) : formatFleetLockstepGate(result));
33078
+ process.exitCode = result.verdict === "pass" ? 0 : 1;
33079
+ } catch (e) {
33080
+ return failGraceful(`train gate: ${e.message}`);
33081
+ }
33082
+ });
33083
+ }
33084
+
32638
33085
  // src/deploy-status.ts
32639
33086
  function buildDeployStatusReport(input) {
32640
33087
  const deploy = input.deployFacts?.stages[input.stage] ?? null;
@@ -33619,6 +34066,12 @@ function parseWorktreePorcelain2(stdout) {
33619
34066
  var CLOSING_MENTION_RE = /\b(close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi;
33620
34067
  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
34068
  var CLAUSE_WINDOW = 120;
34069
+ 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;
34070
+ function rewriteNegatedClosingPhrases(text) {
34071
+ if (!text.includes("#")) return text;
34072
+ const segments = text.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g);
34073
+ return segments.map((seg, i) => i % 2 === 1 ? seg : seg.replace(NEGATED_CLOSING_PHRASE_RE, (_m, n) => `leaves #${n} open`)).join("");
34074
+ }
33622
34075
  function findClosingMentions(text) {
33623
34076
  const mentions = [];
33624
34077
  for (const match of text.matchAll(CLOSING_MENTION_RE)) {
@@ -33692,11 +34145,12 @@ ${typeof body === "string" ? body : ""}`;
33692
34145
  function negatedClosingRefusalMessage(negated, context = "pr merge") {
33693
34146
  const named = negated.map((n) => `#${n}`).join(", ");
33694
34147
  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.`;
34148
+ 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
34149
  }
33697
34150
  function commitClosingRefusalMessage(closing, context = "pr merge") {
33698
34151
  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.`;
34152
+ const first = `#${closing[0] ?? "N"}`;
34153
+ 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
34154
  }
33701
34155
  function alreadyClosedCommitClosingMessage(closed, context = "pr merge") {
33702
34156
  const named = closed.map((n) => `#${n}`).join(", ");
@@ -33887,6 +34341,297 @@ function registerSessionReport(program3) {
33887
34341
  });
33888
34342
  }
33889
34343
 
34344
+ // src/learning-closure-rate.ts
34345
+ var CLOSURE_RATE_TIMEOUT_MS = 2e4;
34346
+ var LOOP_ITEM_CAP = 100;
34347
+ var EVIDENCE_CONCURRENCY = 4;
34348
+ var RED_TEAM_FINDING_LABEL2 = "red-team";
34349
+ var RULING_LOOP_STATE_LABELS = ["loop-state:closed-deferred", "loop-state:closed-invalid"];
34350
+ var CLOSED_MERGED_LOOP_STATE_LABEL = "loop-state:closed-merged";
34351
+ var LOOP_KINDS2 = [
34352
+ { kind: "friction", label: REPORT_LABEL },
34353
+ { kind: "lesson", label: SKILL_LESSON_LABEL },
34354
+ { kind: "red-team", label: RED_TEAM_FINDING_LABEL2 }
34355
+ ];
34356
+ function prIsMerged(pr2) {
34357
+ return String(pr2.state ?? "").toUpperCase() === "MERGED";
34358
+ }
34359
+ function mergedPrsOf(linkedPrs) {
34360
+ return linkedPrs.filter(prIsMerged);
34361
+ }
34362
+ function hasRecordedRuling(labels) {
34363
+ return labels.some((label) => RULING_LOOP_STATE_LABELS.includes(label));
34364
+ }
34365
+ function classifyLoopClosure(item, evidence) {
34366
+ if (String(item.state ?? "").toUpperCase() !== "CLOSED") return "open";
34367
+ if (mergedPrsOf(evidence.linkedPrs).length > 0) return "closed";
34368
+ if (item.labels.includes(CLOSED_MERGED_LOOP_STATE_LABEL)) return "closed";
34369
+ if (hasRecordedRuling(item.labels)) return "closed";
34370
+ return "closed-no-evidence";
34371
+ }
34372
+ var round3 = (x) => Math.round(x * 1e3) / 1e3;
34373
+ function computeLoopKindRate(spec, verdicts) {
34374
+ const numerator = verdicts.filter((v) => v === "closed").length;
34375
+ const unknownEvidence = verdicts.filter((v) => v === "closed-no-evidence").length;
34376
+ const denominator = verdicts.length;
34377
+ return {
34378
+ kind: spec.kind,
34379
+ label: spec.label,
34380
+ numerator,
34381
+ denominator,
34382
+ unknownEvidence,
34383
+ rate: denominator > 0 ? round3(numerator / denominator) : null
34384
+ };
34385
+ }
34386
+ function fmtRate(rate) {
34387
+ return rate === null ? "\u2014" : rate.toFixed(3);
34388
+ }
34389
+ function formatClosureSummary(report) {
34390
+ const kindWidth = Math.max(...report.kinds.map((k) => k.kind.length));
34391
+ const labelWidth = Math.max(...report.kinds.map((k) => k.label.length));
34392
+ const lines = [`learning closure rate \u2014 ${report.repo} (computed at read; #4440)`];
34393
+ for (const k of report.kinds) {
34394
+ const head = `${k.kind.padEnd(kindWidth)} (label ${k.label.padEnd(labelWidth)})`;
34395
+ const body = k.denominator === 0 ? "no loop items" : `${k.numerator}/${k.denominator} closed \xB7 ${k.unknownEvidence} unknown evidence \xB7 rate ${fmtRate(k.rate)}`;
34396
+ lines.push(` ${head}: ${body}`);
34397
+ }
34398
+ const t = report.totals;
34399
+ lines.push(
34400
+ ` ${"total".padEnd(kindWidth)}: ${t.numerator}/${t.denominator} closed \xB7 ${t.unknownEvidence} unknown evidence \xB7 rate ${fmtRate(t.rate)}`,
34401
+ "A closed loop without a merged PR or a recorded ruling reads as open."
34402
+ );
34403
+ if (report.evidencePartial) {
34404
+ lines.push("note: at least one closed item had an unreadable linked-PR read \u2014 counted open (unknown evidence).");
34405
+ }
34406
+ return lines.join("\n");
34407
+ }
34408
+ function buildClosureReport(repo, perKind, evidencePartial) {
34409
+ const kinds = perKind.map(({ spec, verdicts }) => computeLoopKindRate(spec, verdicts));
34410
+ const numerator = kinds.reduce((acc, k) => acc + k.numerator, 0);
34411
+ const denominator = kinds.reduce((acc, k) => acc + k.denominator, 0);
34412
+ const unknownEvidence = kinds.reduce((acc, k) => acc + k.unknownEvidence, 0);
34413
+ const report = {
34414
+ repo,
34415
+ kinds,
34416
+ totals: {
34417
+ numerator,
34418
+ denominator,
34419
+ unknownEvidence,
34420
+ rate: denominator > 0 ? round3(numerator / denominator) : null
34421
+ },
34422
+ evidencePartial,
34423
+ summary: ""
34424
+ };
34425
+ report.summary = formatClosureSummary(report);
34426
+ return report;
34427
+ }
34428
+ function splitRepo2(repo) {
34429
+ const parts = repo.split("/");
34430
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
34431
+ throw new QueryReadError("BAD_INPUT", `invalid repo "${repo}" (expected owner/repo)`);
34432
+ }
34433
+ return { owner: parts[0], name: parts[1] };
34434
+ }
34435
+ async function mapPooled(items, limit, worker) {
34436
+ const out = new Array(items.length);
34437
+ let next = 0;
34438
+ const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
34439
+ while (next < items.length) {
34440
+ const index = next;
34441
+ next += 1;
34442
+ out[index] = await worker(items[index]);
34443
+ }
34444
+ });
34445
+ await Promise.all(runners);
34446
+ return out;
34447
+ }
34448
+ async function readEvidence(deps, owner, name, item) {
34449
+ try {
34450
+ const resp = await deps.ghJson(prForIssueGraphqlArgs(owner, name, item.number), CLOSURE_RATE_TIMEOUT_MS);
34451
+ return { linkedPrs: extractPrForIssueResponse(resp), readFailed: false };
34452
+ } catch (e) {
34453
+ if (e instanceof QueryReadError && e.code === "NOT_FOUND") {
34454
+ return { linkedPrs: [], readFailed: false };
34455
+ }
34456
+ return { linkedPrs: [], readFailed: true };
34457
+ }
34458
+ }
34459
+ async function runLearningClosureRate(deps, opts = {}) {
34460
+ const repo = opts.repo ?? HUB_REPO3;
34461
+ const { owner, name } = splitRepo2(repo);
34462
+ let evidencePartial = false;
34463
+ const perKind = [];
34464
+ for (const spec of LOOP_KINDS2) {
34465
+ const rows = await deps.ghJson(
34466
+ buildIssueListArgs({ label: spec.label, state: "all", limit: LOOP_ITEM_CAP }, repo),
34467
+ CLOSURE_RATE_TIMEOUT_MS
34468
+ );
34469
+ const items = shapeIssueList(rows);
34470
+ const closed = items.filter((item) => String(item.state ?? "").toUpperCase() === "CLOSED");
34471
+ const evidence = await mapPooled(closed, EVIDENCE_CONCURRENCY, (item) => readEvidence(deps, owner, name, item));
34472
+ if (evidence.some((ev) => ev.readFailed)) evidencePartial = true;
34473
+ const evidenceByNumber = new Map(closed.map((item, i) => [item.number, evidence[i]]));
34474
+ const verdicts = items.map(
34475
+ (item) => classifyLoopClosure(item, evidenceByNumber.get(item.number) ?? { linkedPrs: [], readFailed: false })
34476
+ );
34477
+ perKind.push({ spec, verdicts });
34478
+ }
34479
+ return buildClosureReport(repo, perKind, evidencePartial);
34480
+ }
34481
+ function closureRateFail(e) {
34482
+ if (e instanceof QueryReadError) {
34483
+ 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;
34484
+ return fail(`closure-rate: ${e.message}`, code === ERROR_CODES.ERR_BAD_ENUM ? void 0 : { code });
34485
+ }
34486
+ const err = e;
34487
+ return fail(`closure-rate: ${(err.stderr || err.message || String(e)).trim()}`);
34488
+ }
34489
+ function registerLearningClosureRateCommand(program3) {
34490
+ const deps = queryDeps();
34491
+ withExamples(
34492
+ program3.command("closure-rate").description(
34493
+ "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)"
34494
+ ).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) => {
34495
+ try {
34496
+ const report = await runLearningClosureRate(deps, { repo: o.repo });
34497
+ if (o.json) console.log(JSON.stringify(report, null, 2));
34498
+ else console.log(report.summary);
34499
+ } catch (e) {
34500
+ closureRateFail(e);
34501
+ }
34502
+ }),
34503
+ [
34504
+ "mmi-cli learning closure-rate",
34505
+ "mmi-cli learning closure-rate --repo mutmutco/MMI-Hub --json"
34506
+ ],
34507
+ "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"
34508
+ );
34509
+ }
34510
+
34511
+ // ../infra/src/fleet-health.ts
34512
+ function uniqueRepos(repos) {
34513
+ const unique = /* @__PURE__ */ new Map();
34514
+ for (const raw of repos) {
34515
+ const repo = raw.trim();
34516
+ if (repo) unique.set(repo.toLowerCase(), unique.get(repo.toLowerCase()) ?? repo);
34517
+ }
34518
+ return [...unique.values()].sort((a, b) => a.localeCompare(b));
34519
+ }
34520
+ function buildFleetHealthReport(registeredRepos, evidence, asOf, maxAgeMs = 24 * 60 * 60 * 1e3) {
34521
+ const repos = uniqueRepos(registeredRepos);
34522
+ const evidenceByRepo = new Map(evidence.map((row) => [row.repo.trim().toLowerCase(), row]));
34523
+ const nowMs = Date.parse(asOf);
34524
+ const drift = [];
34525
+ const unknownEvidence = [];
34526
+ let conforming = 0;
34527
+ let fresh = 0;
34528
+ let stale = 0;
34529
+ let freshnessUnknown = 0;
34530
+ for (const repo of repos) {
34531
+ const row = evidenceByRepo.get(repo.toLowerCase());
34532
+ const reasons = [...row?.unknownEvidence ?? []];
34533
+ let isFresh = false;
34534
+ if (!row) {
34535
+ reasons.push("no #4318 dry-run conformance evidence");
34536
+ freshnessUnknown += 1;
34537
+ } else if (!row.checkedAt) {
34538
+ reasons.push("conformance freshness is unknown");
34539
+ freshnessUnknown += 1;
34540
+ } else {
34541
+ const checkedMs = Date.parse(row.checkedAt);
34542
+ if (!Number.isFinite(checkedMs) || !Number.isFinite(nowMs) || checkedMs > nowMs) {
34543
+ reasons.push(`invalid conformance timestamp: ${row.checkedAt}`);
34544
+ freshnessUnknown += 1;
34545
+ } else if (nowMs - checkedMs > maxAgeMs) {
34546
+ reasons.push(`conformance evidence is stale (${row.checkedAt})`);
34547
+ stale += 1;
34548
+ } else {
34549
+ isFresh = true;
34550
+ fresh += 1;
34551
+ }
34552
+ }
34553
+ if (row?.verdict === "drift") {
34554
+ drift.push({
34555
+ repo,
34556
+ findings: row.findings?.length ? [...row.findings] : ["#4318 verdict reported drift without a finding"]
34557
+ });
34558
+ }
34559
+ if (row?.verdict === "unknown" && reasons.length === 0) reasons.push("#4318 verdict is unknown");
34560
+ if (row?.verdict === "conforming" && isFresh && reasons.length === 0) conforming += 1;
34561
+ if (reasons.length > 0) unknownEvidence.push({ repo, reasons });
34562
+ }
34563
+ const verdict = drift.length > 0 ? "drift" : repos.length > 0 && conforming === repos.length && unknownEvidence.length === 0 ? "conforming" : "unknown";
34564
+ return {
34565
+ verdict,
34566
+ source: "#4318-dry-run-conformance",
34567
+ denominator: repos.length,
34568
+ conforming,
34569
+ drift,
34570
+ freshness: {
34571
+ asOf,
34572
+ maxAgeSeconds: Math.floor(maxAgeMs / 1e3),
34573
+ fresh,
34574
+ stale,
34575
+ unknown: freshnessUnknown
34576
+ },
34577
+ unknownEvidence,
34578
+ readOnly: true
34579
+ };
34580
+ }
34581
+ function fleetHealthExitCode(report) {
34582
+ return report.verdict === "conforming" ? 0 : 1;
34583
+ }
34584
+
34585
+ // src/org-health-query.ts
34586
+ var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
34587
+ function defaultOrgHealthQueryDeps() {
34588
+ return {
34589
+ registeredRepos: async () => {
34590
+ const cfg = await loadConfig();
34591
+ const projects = await fetchProjectsList(registryClientDeps(cfg));
34592
+ if (!projects) throw new Error("live Hub registry roster is unavailable");
34593
+ return orgVersionRepos(projects);
34594
+ },
34595
+ conformanceEvidence: async () => fleetGateConformanceEvidence(await runFleetLockstepGate()),
34596
+ now: () => /* @__PURE__ */ new Date()
34597
+ };
34598
+ }
34599
+ async function runOrgHealthQuery(deps, maxAgeMs = DEFAULT_MAX_AGE_MS) {
34600
+ const repos = await deps.registeredRepos();
34601
+ const evidence = await deps.conformanceEvidence(repos);
34602
+ return buildFleetHealthReport(repos, evidence, deps.now().toISOString(), maxAgeMs);
34603
+ }
34604
+ function formatOrgHealth(report) {
34605
+ const lines = [
34606
+ `org health \u2014 ${report.conforming}/${report.denominator} conforming \xB7 ${report.verdict.toUpperCase()}`,
34607
+ `source: ${report.source} \xB7 read ${report.freshness.asOf}`,
34608
+ `freshness: ${report.freshness.fresh} fresh \xB7 ${report.freshness.stale} stale \xB7 ${report.freshness.unknown} unknown`
34609
+ ];
34610
+ if (report.drift.length) {
34611
+ lines.push("", "drift:");
34612
+ for (const row of report.drift) lines.push(` ${row.repo}: ${row.findings.join("; ")}`);
34613
+ }
34614
+ if (report.unknownEvidence.length) {
34615
+ lines.push("", "unknown evidence:");
34616
+ for (const row of report.unknownEvidence) lines.push(` ${row.repo}: ${row.reasons.join("; ")}`);
34617
+ }
34618
+ return lines.join("\n");
34619
+ }
34620
+ function registerOrgHealthQuery(program3, deps = defaultOrgHealthQueryDeps()) {
34621
+ const org = program3.commands.find((command) => command.name() === "org");
34622
+ if (!org) throw new Error("org health registration requires consolidated command namespaces");
34623
+ 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) => {
34624
+ try {
34625
+ const report = await runOrgHealthQuery(deps);
34626
+ console.log(options.json ? JSON.stringify(report, null, 2) : formatOrgHealth(report));
34627
+ process.exitCode = fleetHealthExitCode(report);
34628
+ } catch (error) {
34629
+ console.error(`org health: ${error.message}`);
34630
+ process.exitCode = 1;
34631
+ }
34632
+ });
34633
+ }
34634
+
33890
34635
  // src/plugin-release-catchup.ts
33891
34636
  var import_node_fs43 = require("node:fs");
33892
34637
  var import_node_path40 = require("node:path");
@@ -36838,9 +37583,9 @@ program2.name("mmi-cli").description("MMI Future Hub CLI ? the org control plane
36838
37583
  program2.addHelpText(
36839
37584
  "before",
36840
37585
  `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.
37586
+ \`mmi-cli <house> <command> \u2026\` is the only invocation form for the paths below \u2014 their flat
37587
+ \`mmi-cli <command> \u2026\` aliases were removed in ${COMPAT_SHIM_DEATH_WAVE} (#4316, #4438); core commands stay
37588
+ unprefixed. \`mmi-cli commands\` shows the house-shaped tree; \`mmi-cli <house> --help\` lists one house.
36844
37589
  `
36845
37590
  );
36846
37591
  function appActorDeps() {
@@ -37722,7 +38467,11 @@ docs.command("index").description("regenerate docs/index.md from the docs/ tree
37722
38467
  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
38468
  try {
37724
38469
  const root = await repoRoot();
37725
- const commandPaths = new Set(buildCommandManifest(program2).index.map((entry) => entry.path));
38470
+ const commandPaths = new Set(
38471
+ buildCommandManifest(program2).index.map(
38472
+ (entry) => entry.house && entry.house !== "core" ? `${entry.house} ${entry.path}` : entry.path
38473
+ )
38474
+ );
37726
38475
  const result = runDocRefs(root, { commandPaths });
37727
38476
  if (o.json) {
37728
38477
  consoleIo.log(JSON.stringify({ ok: result.ok, docCount: result.docCount, findings: result.findings, warnings: result.warnings }, null, 2));
@@ -38749,6 +39498,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
38749
39498
  return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
38750
39499
  }
38751
39500
  body = normalizeClosingDirectives(body);
39501
+ body = rewriteNegatedClosingPhrases(body);
38752
39502
  const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
38753
39503
  if (docsCheck && !docsCheck.ok) {
38754
39504
  return fail(`pr create: ${docsCheck.detail} ? ${docsCheck.fix}`);
@@ -39216,11 +39966,14 @@ registerQueryCommands(program2);
39216
39966
  registerWorktreeCommands(program2);
39217
39967
  registerIssueLifecycleCommands(program2, { attach: attachToProject });
39218
39968
  registerTrainCommands(program2);
39969
+ registerFleetTrackInventory(program2);
39970
+ registerFleetLockstepGate(program2);
39219
39971
  registerDeployCommands(program2);
39220
39972
  registerDiscoveryCommands(program2);
39221
39973
  registerExplainCommand(program2);
39222
39974
  registerPrLifecycleCommands(program2);
39223
39975
  registerSessionReport(program2);
39976
+ registerLearningClosureRateCommand(program2);
39224
39977
  registerBoardCommands(program2);
39225
39978
  registerStageCommands(program2);
39226
39979
  var GH_TRAIN_TIMEOUT_MS = 3e4;
@@ -39237,7 +39990,10 @@ function trainApplyDeps() {
39237
39990
  throw surfaceTrainSubprocessFailure(e, file, args);
39238
39991
  }
39239
39992
  },
39240
- runSelf: async (args) => (await execFileP2(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
39993
+ // canonicalArgvFor (#4438): callers hand this seam house-relative paths (`org project get`,
39994
+ // `secrets preflight`); since the Wave 3 flat-alias cut only the canonical house-prefixed form
39995
+ // parses, so the ONE self-spawn seam derives the prefix through the same rule the refusal names.
39996
+ runSelf: async (args) => (await execFileP2(process.execPath, [process.argv[1], ...canonicalArgvFor(args)], { timeout: 3e4 })).stdout,
39241
39997
  trainAuthority: async (repo) => {
39242
39998
  const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
39243
39999
  return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
@@ -39917,12 +40673,21 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
39917
40673
  });
39918
40674
  installProcessBackstop();
39919
40675
  consolidateCommandNamespaces(program2);
40676
+ registerOrgHealthQuery(program2);
39920
40677
  applyCommandTaxonomy(program2);
39921
40678
  function resolveHouseShim(argv) {
39922
40679
  const houseToken = argv[2];
39923
40680
  if (!houseToken) return;
39924
40681
  if (!isHouseRoot(houseToken)) {
39925
- return;
40682
+ const flatTokens = argv.slice(2, 4).filter((tok) => !tok.startsWith("-"));
40683
+ const flatPath = flatTokens.join(" ");
40684
+ const flatHouse = houseForPath(flatPath);
40685
+ if (!flatHouse || flatHouse === "core") return;
40686
+ process.stderr.write(
40687
+ `mmi-cli: the flat '${flatPath}' alias was removed in ${COMPAT_SHIM_DEATH_WAVE} (#4316) \u2014 run: mmi-cli ${canonicalPathFor(flatPath)} \u2026
40688
+ `
40689
+ );
40690
+ hardExit(2);
39926
40691
  }
39927
40692
  const remainder = argv.slice(3);
39928
40693
  const first = remainder[0];
@@ -39941,10 +40706,9 @@ function resolveHouseShim(argv) {
39941
40706
  argv.splice(2, 1);
39942
40707
  return;
39943
40708
  }
39944
- const canonical = actual ? `mmi-cli ${actual === "core" ? "" : `${actual} `}${lookupPath}`.replace(/\s+/g, " ").trim() : void 0;
39945
40709
  if (actual) {
39946
40710
  process.stderr.write(
39947
- `mmi-cli ${houseToken}: '${lookupPath}' lives in house '${actual}' \u2014 run: ${canonical} \u2026
40711
+ `mmi-cli ${houseToken}: '${lookupPath}' lives in house '${actual}' \u2014 run: mmi-cli ${canonicalPathFor(lookupPath)} \u2026
39948
40712
  `
39949
40713
  );
39950
40714
  } else {
@@ -39967,8 +40731,8 @@ function printHouseRootHelp(house) {
39967
40731
  }
39968
40732
  lines.push(
39969
40733
  "",
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).`
40734
+ `Canonical: \`mmi-cli ${house} <command> \u2026\`. The flat \`mmi-cli <command> \u2026\` compatibility shims`,
40735
+ `were removed in ${COMPAT_SHIM_DEATH_WAVE} (#4316, #4438) \u2014 only the canonical house-prefixed form parses.`
39972
40736
  );
39973
40737
  consoleIo.log(lines.join("\n"));
39974
40738
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.106.0",
3
+ "version": "3.107.1",
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",