@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.
- package/dist/main.cjs +799 -35
- 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
|
|
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.
|
|
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
|
|
21230
|
+
houses,
|
|
21180
21231
|
doors: buildDoorsCatalog(tree),
|
|
21181
|
-
|
|
21232
|
+
flat_alias_cut: {
|
|
21182
21233
|
canonical: "mmi-cli <house> <command> \u2026",
|
|
21183
|
-
|
|
21184
|
-
|
|
21185
|
-
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
|
|
21284
|
-
`
|
|
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
|
|
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
|
-
|
|
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
|
|
36842
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
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\`
|
|
39971
|
-
`
|
|
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