@mutmutco/cli 3.32.0 → 3.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/main.cjs +306 -838
  3. package/package.json +2 -2
package/dist/main.cjs CHANGED
@@ -3520,7 +3520,10 @@ async function cleanExit(code) {
3520
3520
  await new Promise((resolve5) => setImmediate(resolve5));
3521
3521
  process.exit(code);
3522
3522
  }
3523
- var CLI_EXIT_WATCHDOG_MS = 1e4;
3523
+ var CLI_EXIT_WATCHDOG_MS = (() => {
3524
+ const testOverride = Number(process.env.MMI_CLI_TEST_EXIT_WATCHDOG_MS);
3525
+ return process.env.NODE_ENV === "test" && Number.isFinite(testOverride) && testOverride >= 25 ? testOverride : 1e4;
3526
+ })();
3524
3527
  async function finishCliRun(watchdogMs = CLI_EXIT_WATCHDOG_MS) {
3525
3528
  const timer = setTimeout(() => {
3526
3529
  console.error(
@@ -4928,7 +4931,8 @@ function retriedFetch(deps, url, init) {
4928
4931
  const headers = { ...clientVersionHeaders(), ...init.headers };
4929
4932
  return fetchWithRetry(deps.fetch ?? fetch, url, { ...init, headers }, {
4930
4933
  attempts: RETRY_ATTEMPTS,
4931
- timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS
4934
+ timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS,
4935
+ sleep: deps.retrySleep
4932
4936
  });
4933
4937
  }
4934
4938
  async function fetchTrainAuthority(repo, deps) {
@@ -4992,23 +4996,6 @@ async function fetchProjectBySlug(slug, deps) {
4992
4996
  const res = await fetchProjectBySlugChecked(slug, deps);
4993
4997
  return res.ok ? res.project : null;
4994
4998
  }
4995
- async function fetchDeployStatusBySlug(slug, deps) {
4996
- if (!deps.baseUrl || !slug) return null;
4997
- const token = await deps.token();
4998
- if (!token) return null;
4999
- try {
5000
- const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
5001
- method: "GET",
5002
- headers: { Authorization: `Bearer ${token}` }
5003
- });
5004
- if (!res.ok) return null;
5005
- const body = await res.json();
5006
- if (!body?.stages) return null;
5007
- return { stages: body.stages, deployState: body.deployState ?? {} };
5008
- } catch {
5009
- return null;
5010
- }
5011
- }
5012
4999
  async function fetchDeployFactsBySlug(slug, deps) {
5013
5000
  if (!deps.baseUrl || !slug) return null;
5014
5001
  const token = await deps.token();
@@ -5079,8 +5066,8 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
5079
5066
  const token = await deps.token();
5080
5067
  if (!token) return { ok: false, status: 0, body: null, error: "no Hub session token (run `gh auth login`)" };
5081
5068
  const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS;
5082
- const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs });
5083
- const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs });
5069
+ const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs, sleep: deps.retrySleep });
5070
+ const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs, sleep: deps.retrySleep });
5084
5071
  try {
5085
5072
  const res = await send(`${deps.baseUrl.replace(/\/$/, "")}${pathSuffix}`, {
5086
5073
  method,
@@ -5106,9 +5093,6 @@ async function upsertProject(slug, patch, deps) {
5106
5093
  async function retireProject(slug, deps) {
5107
5094
  return postJson(`/projects/${encodeURIComponent(slug)}`, {}, deps, "DELETE", { noRetry: true });
5108
5095
  }
5109
- async function attestAppGaps(slug, repo, deps) {
5110
- return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
5111
- }
5112
5096
  async function setDeployCoords(slug, payload, deps) {
5113
5097
  return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
5114
5098
  }
@@ -5118,6 +5102,9 @@ async function recordDocsAudit(verdict, deps) {
5118
5102
  async function tenantControl(payload, deps) {
5119
5103
  return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
5120
5104
  }
5105
+ async function tenantReconcile(payload, deps) {
5106
+ return postJson("/tenant-reconcile", payload, deps, "POST", { noRetry: true });
5107
+ }
5121
5108
  async function tenantDeploy(payload, deps) {
5122
5109
  return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
5123
5110
  }
@@ -6202,9 +6189,6 @@ function resolveDeployModel(meta, repo) {
6202
6189
  if (projectType === "cli-tool") return "registry-publish";
6203
6190
  return "tenant-container";
6204
6191
  }
6205
- function projectTypeClearsWebProfile(projectType, deployModel) {
6206
- return projectType === "content" || projectType === "desktop-app" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
6207
- }
6208
6192
  function inferReleaseTrackFromBranches(hints) {
6209
6193
  if (hints.hasRcBranch) return void 0;
6210
6194
  if (hints.hasDevelopmentBranch && hints.hasMainBranch) return "direct";
@@ -7172,9 +7156,10 @@ async function secretsUse(deps, key, opts) {
7172
7156
  "",
7173
7157
  "Consume it WITHOUT committing it:",
7174
7158
  ` \u2022 Keyless run: \`mmi-cli secrets use ${key}${opts.slug ? ` --slug ${opts.slug}` : ""} -- <command>\` injects it into the command's env (never printed). A granted non-master can USE an org secret this way.`,
7175
- ` \u2022 Runtime / agents: read it keylessly at runtime via the box's OIDC role. Never bake it into an image or commit it.`,
7159
+ " \u2022 Runtime: the central deploy injects the declared stage env; broker consumers use the workload's scoped runtime token. Never bake a value into an image or commit it.",
7176
7160
  ` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
7177
- tier === "project" ? " \u2022 One stageless value per repo (/mmi-future/<slug>/<KEY>) \u2014 no dev/rc/main copies. Project-admins self-serve their repo keys." : " \u2022 Org-infra/cross-slug keys remain master-gated unless granted; project-admins self-serve their own repo keys."
7161
+ " \u2022 Own project: the stageless canonical and any declared dev/rc/main overrides are project-admin self-service.",
7162
+ " \u2022 Org-infra/cross-slug keys remain master-gated unless exactly granted."
7178
7163
  ].join("\n")
7179
7164
  );
7180
7165
  return;
@@ -10386,7 +10371,14 @@ var import_node_util6 = require("node:util");
10386
10371
  var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
10387
10372
  var DOCKER_TIMEOUT_MS = 15e3;
10388
10373
  var EARLY_EXIT_GRACE_MS = 2e3;
10389
- function waitForProcessStability(child2, graceMs = EARLY_EXIT_GRACE_MS) {
10374
+ function earlyExitGraceMs() {
10375
+ if (process.env.NODE_ENV === "test") {
10376
+ const testOverride = Number(process.env.MMI_CLI_TEST_EARLY_EXIT_GRACE_MS);
10377
+ if (Number.isFinite(testOverride) && testOverride >= 25) return testOverride;
10378
+ }
10379
+ return EARLY_EXIT_GRACE_MS;
10380
+ }
10381
+ function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
10390
10382
  return new Promise((resolve5, reject) => {
10391
10383
  let settled = false;
10392
10384
  const finish = (fn) => {
@@ -11504,20 +11496,18 @@ for (const [helpGroup, names] of PRIMARY_GROUPS) {
11504
11496
  for (const name of names) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
11505
11497
  }
11506
11498
  HELP_GROUP_ORDER.set("Operations", HELP_GROUP_ORDER.size);
11507
- HELP_GROUP_ORDER.set("Compatibility", HELP_GROUP_ORDER.size);
11508
- HELP_GROUP_ORDER.set("Delegated", HELP_GROUP_ORDER.size);
11509
11499
  for (const name of OPERATIONAL_TOP_LEVEL) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
11510
- TOP_LEVEL_ORDER.set("lane", topLevelPosition++);
11511
- TOP_LEVEL_ORDER.set("fusion", topLevelPosition++);
11512
11500
  var PATH_OVERRIDES = {
11513
11501
  "board slice-refresh": { category: "internal", discovery: "hidden", help_group: "Operations" },
11514
- lane: { category: "delegated", discovery: "hidden", help_group: "Delegated", replacement: "jerv-cli lane" },
11515
- fusion: { category: "delegated", discovery: "hidden", help_group: "Delegated", replacement: "jerv-cli fusion" }
11502
+ "secrets find": { category: "admin", discovery: "all-only", help_group: "Operations" },
11503
+ "secrets catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
11504
+ "secrets doctor": { category: "admin", discovery: "all-only", help_group: "Operations" },
11505
+ "secrets org-catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
11506
+ "secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
11507
+ "secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
11516
11508
  };
11517
11509
  var HIDDEN_OPTIONS = {
11518
11510
  "worktree create": ["--base"],
11519
- "org project doctor": ["--v2"],
11520
- "org project heal": ["--v2"],
11521
11511
  "org project set": ["--set"]
11522
11512
  };
11523
11513
  function primaryMetadata(name) {
@@ -11535,7 +11525,6 @@ function primaryMetadata(name) {
11535
11525
  function topLevelMetadata(name) {
11536
11526
  const primary = primaryMetadata(name);
11537
11527
  if (primary) return primary;
11538
- if (name === "lane" || name === "fusion") return PATH_OVERRIDES[name];
11539
11528
  if (!OPERATIONAL_TOP_LEVEL.has(name)) {
11540
11529
  throw new Error(`command taxonomy: unclassified top-level command "${name}"`);
11541
11530
  }
@@ -11605,113 +11594,7 @@ function commandHelpGroupRank(group) {
11605
11594
  return HELP_GROUP_ORDER.get(group) ?? Number.MAX_SAFE_INTEGER;
11606
11595
  }
11607
11596
  function isCanonicalSuggestion(metadata) {
11608
- return metadata.category !== "internal" && metadata.category !== "delegated" && metadata.category !== "shim" && metadata.category !== "obsolete";
11609
- }
11610
-
11611
- // src/command-shims.ts
11612
- var COMMAND_SHIMS = [
11613
- { legacy: "project deploy list", canonical: "org project deploy get", category: "shim" },
11614
- { legacy: "project resolve", canonical: "org project deploy get", category: "obsolete" },
11615
- { legacy: "tenant readiness", canonical: "runtime tenant status", category: "shim" },
11616
- { legacy: "secrets edit", canonical: "secrets set", category: "shim" },
11617
- { legacy: "registry org", canonical: "org config get", category: "shim" },
11618
- { legacy: "rules gitignore", canonical: "org rules gitignore", category: "shim" },
11619
- { legacy: "deploy status", canonical: "runtime deploy status", category: "shim" },
11620
- { legacy: "full-track readiness", canonical: "train readiness", category: "shim" },
11621
- { legacy: "docs-audit", canonical: "docs audit", category: "shim", subtree: true },
11622
- { legacy: "plugin-heal", canonical: "plugin heal", category: "shim" },
11623
- { legacy: "plugin-prune", canonical: "plugin prune", category: "shim" },
11624
- { legacy: "session-start", canonical: "plugin session-start", category: "shim" },
11625
- { legacy: "port-range", canonical: "stage port-range", category: "shim" },
11626
- { legacy: "schedules", canonical: "org schedules", category: "shim" },
11627
- { legacy: "guard", canonical: "plugin guard", category: "shim" },
11628
- { legacy: "project", canonical: "org project", category: "shim", subtree: true },
11629
- { legacy: "oauth", canonical: "org oauth", category: "shim", subtree: true },
11630
- { legacy: "access", canonical: "org access", category: "shim", subtree: true },
11631
- { legacy: "tenant", canonical: "runtime tenant", category: "shim", subtree: true },
11632
- { legacy: "box", canonical: "runtime box", category: "shim", subtree: true },
11633
- { legacy: "edge", canonical: "runtime edge", category: "shim", subtree: true },
11634
- { legacy: "gc", canonical: "worktree gc", category: "shim", subtree: true }
11635
- ];
11636
- function rewriteLegacyArgv(argv) {
11637
- const commandArgs = argv.slice(2);
11638
- for (const shim of COMMAND_SHIMS) {
11639
- const legacy = shim.legacy.split(" ");
11640
- if (!legacy.every((token, index) => commandArgs[index] === token)) continue;
11641
- const canonical = shim.canonical.split(" ");
11642
- return {
11643
- argv: [...argv.slice(0, 2), ...canonical, ...commandArgs.slice(legacy.length)],
11644
- shim,
11645
- warning: `mmi-cli: \`${shim.legacy}\` moved to \`${shim.canonical}\`; use \`mmi-cli ${shim.canonical}\`.`
11646
- };
11647
- }
11648
- return { argv: [...argv] };
11649
- }
11650
-
11651
- // src/command-consolidation.ts
11652
- var COMMAND_NAMESPACES_CONSOLIDATED = /* @__PURE__ */ Symbol.for("mmi.commandNamespaces.consolidated");
11653
- function child(parent, name) {
11654
- const found = parent.commands.find((command) => command.name() === name);
11655
- if (!found) throw new Error(`command consolidation: missing "${parent.name()} ${name}"`);
11656
- return found;
11657
- }
11658
- function detach(parent, command) {
11659
- const commands = parent.commands;
11660
- const index = commands.indexOf(command);
11661
- if (index < 0) throw new Error(`command consolidation: "${command.name()}" is not a child of "${parent.name()}"`);
11662
- commands.splice(index, 1);
11663
- return command;
11664
- }
11665
- function move(parent, destination, name, nextName = name) {
11666
- const command = detach(parent, child(parent, name));
11667
- command.name(nextName);
11668
- destination.addCommand(command);
11669
- return command;
11670
- }
11671
- function remove(parent, name) {
11672
- detach(parent, child(parent, name));
11673
- }
11674
- function consolidateCommandNamespaces(program3) {
11675
- const org = program3.command("org").description("organization projects, access, rules, credentials, and schedules");
11676
- const runtime = program3.command("runtime").description("tenant, deploy, box, and edge operations");
11677
- const plugin = program3.command("plugin").description("plugin lifecycle and guard operations");
11678
- move(program3, org, "project");
11679
- move(program3, org, "oauth");
11680
- move(program3, org, "access");
11681
- move(program3, org, "schedules");
11682
- const config = org.command("config").description("organization configuration");
11683
- const registry2 = child(program3, "registry");
11684
- move(registry2, config, "org", "get");
11685
- remove(program3, "registry");
11686
- const orgRules = org.command("rules").description("organization-managed repository rules");
11687
- const rules2 = child(program3, "rules");
11688
- move(rules2, orgRules, "gitignore");
11689
- remove(program3, "rules");
11690
- move(program3, runtime, "tenant");
11691
- const runtimeDeploy = runtime.command("deploy").description("runtime deployment status");
11692
- const deploy = child(program3, "deploy");
11693
- move(deploy, runtimeDeploy, "status");
11694
- remove(program3, "deploy");
11695
- move(program3, runtime, "box");
11696
- move(program3, runtime, "edge");
11697
- move(program3, plugin, "guard");
11698
- move(program3, plugin, "plugin-heal", "heal");
11699
- move(program3, plugin, "plugin-prune", "prune");
11700
- move(program3, plugin, "session-start");
11701
- const docs2 = child(program3, "docs");
11702
- move(program3, docs2, "docs-audit", "audit");
11703
- const train = child(program3, "train");
11704
- const fullTrack2 = child(program3, "full-track");
11705
- move(fullTrack2, train, "readiness");
11706
- remove(program3, "full-track");
11707
- const worktree2 = child(program3, "worktree");
11708
- move(program3, worktree2, "gc");
11709
- const stage = child(program3, "stage");
11710
- move(program3, stage, "port-range");
11711
- program3[COMMAND_NAMESPACES_CONSOLIDATED] = true;
11712
- }
11713
- function hasConsolidatedCommandNamespaces(program3) {
11714
- return program3[COMMAND_NAMESPACES_CONSOLIDATED] === true;
11597
+ return metadata.category !== "internal";
11715
11598
  }
11716
11599
 
11717
11600
  // src/command-manifest.ts
@@ -11780,63 +11663,6 @@ function buildCommand(cmd, path2) {
11780
11663
  if (commonMistakes) out.common_mistakes = commonMistakes;
11781
11664
  return out;
11782
11665
  }
11783
- function findTreeCommand(root, path2) {
11784
- if (root.path === path2) return root;
11785
- for (const child2 of root.subcommands) {
11786
- const found = findTreeCommand(child2, path2);
11787
- if (found) return found;
11788
- }
11789
- return void 0;
11790
- }
11791
- function virtualCopy(source, path2, shim) {
11792
- return {
11793
- ...source,
11794
- name: path2.split(" ").at(-1),
11795
- path: path2,
11796
- category: shim.category,
11797
- discovery: "all-only",
11798
- help_group: "Compatibility",
11799
- replacement: source.path,
11800
- subcommands: shim.subtree ? source.subcommands.map((child2) => virtualCopy(child2, `${path2} ${child2.name}`, shim)) : []
11801
- };
11802
- }
11803
- function virtualParent(path2, shim) {
11804
- return {
11805
- name: path2.split(" ").at(-1),
11806
- path: path2,
11807
- description: `compatibility namespace; use mmi-cli ${shim.canonical}`,
11808
- arguments: [],
11809
- options: [],
11810
- subcommands: [],
11811
- category: "shim",
11812
- discovery: "all-only",
11813
- help_group: "Compatibility",
11814
- replacement: shim.canonical
11815
- };
11816
- }
11817
- function insertVirtualShim(root, shim) {
11818
- const canonical = findTreeCommand(root, shim.canonical);
11819
- if (!canonical) throw new Error(`command manifest: shim target missing "${shim.canonical}"`);
11820
- const parts = shim.legacy.split(" ");
11821
- let parent = root;
11822
- for (let index = 0; index < parts.length - 1; index += 1) {
11823
- const path2 = parts.slice(0, index + 1).join(" ");
11824
- let next = parent.subcommands.find((command) => command.name === parts[index]);
11825
- if (!next) {
11826
- next = virtualParent(path2, shim);
11827
- parent.subcommands.push(next);
11828
- }
11829
- parent = next;
11830
- }
11831
- const copy = virtualCopy(canonical, shim.legacy, shim);
11832
- const existingIndex = parent.subcommands.findIndex((command) => command.name === copy.name);
11833
- if (existingIndex >= 0) parent.subcommands[existingIndex] = copy;
11834
- else parent.subcommands.push(copy);
11835
- }
11836
- function addVirtualShims(root) {
11837
- for (const shim of COMMAND_SHIMS.filter((entry) => entry.subtree)) insertVirtualShim(root, shim);
11838
- for (const shim of COMMAND_SHIMS.filter((entry) => !entry.subtree)) insertVirtualShim(root, shim);
11839
- }
11840
11666
  function primaryCopy(node, root = false) {
11841
11667
  if (!root && node.discovery !== "primary") return void 0;
11842
11668
  const subcommands = node.subcommands.map((child2) => primaryCopy(child2)).filter((child2) => Boolean(child2));
@@ -11856,7 +11682,6 @@ function collectLeaves(node, acc) {
11856
11682
  }
11857
11683
  function buildCommandManifest(program3) {
11858
11684
  const tree = buildCommand(program3, "");
11859
- if (hasConsolidatedCommandNamespaces(program3)) addVirtualShims(tree);
11860
11685
  const index = [];
11861
11686
  collectLeaves(tree, index);
11862
11687
  const primaryTree = primaryCopy(tree, true);
@@ -11913,10 +11738,71 @@ function formatManifestHuman(manifest, options = {}) {
11913
11738
  }
11914
11739
  render(command, 1);
11915
11740
  }
11916
- lines.push("", 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 operations and compatibility.");
11741
+ lines.push("", 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.");
11917
11742
  return lines.join("\n");
11918
11743
  }
11919
11744
 
11745
+ // src/command-consolidation.ts
11746
+ function child(parent, name) {
11747
+ const found = parent.commands.find((command) => command.name() === name);
11748
+ if (!found) throw new Error(`command consolidation: missing "${parent.name()} ${name}"`);
11749
+ return found;
11750
+ }
11751
+ function detach(parent, command) {
11752
+ const commands = parent.commands;
11753
+ const index = commands.indexOf(command);
11754
+ if (index < 0) throw new Error(`command consolidation: "${command.name()}" is not a child of "${parent.name()}"`);
11755
+ commands.splice(index, 1);
11756
+ return command;
11757
+ }
11758
+ function move(parent, destination, name, nextName = name) {
11759
+ const command = detach(parent, child(parent, name));
11760
+ command.name(nextName);
11761
+ destination.addCommand(command);
11762
+ return command;
11763
+ }
11764
+ function remove(parent, name) {
11765
+ detach(parent, child(parent, name));
11766
+ }
11767
+ function consolidateCommandNamespaces(program3) {
11768
+ const org = program3.command("org").description("organization projects, access, rules, credentials, and schedules");
11769
+ const runtime = program3.command("runtime").description("tenant, deploy, box, and edge operations");
11770
+ const plugin = program3.command("plugin").description("plugin lifecycle and guard operations");
11771
+ move(program3, org, "project");
11772
+ move(program3, org, "oauth");
11773
+ move(program3, org, "access");
11774
+ move(program3, org, "schedules");
11775
+ const config = org.command("config").description("organization configuration");
11776
+ const registry2 = child(program3, "registry");
11777
+ move(registry2, config, "org", "get");
11778
+ remove(program3, "registry");
11779
+ const orgRules = org.command("rules").description("organization-managed repository rules");
11780
+ const rules2 = child(program3, "rules");
11781
+ move(rules2, orgRules, "gitignore");
11782
+ remove(program3, "rules");
11783
+ move(program3, runtime, "tenant");
11784
+ const runtimeDeploy = runtime.command("deploy").description("runtime deployment status");
11785
+ const deploy = child(program3, "deploy");
11786
+ move(deploy, runtimeDeploy, "status");
11787
+ remove(program3, "deploy");
11788
+ move(program3, runtime, "box");
11789
+ move(program3, runtime, "edge");
11790
+ move(program3, plugin, "guard");
11791
+ move(program3, plugin, "plugin-heal", "heal");
11792
+ move(program3, plugin, "plugin-prune", "prune");
11793
+ move(program3, plugin, "session-start");
11794
+ const docs2 = child(program3, "docs");
11795
+ move(program3, docs2, "docs-audit", "audit");
11796
+ const train = child(program3, "train");
11797
+ const fullTrack2 = child(program3, "full-track");
11798
+ move(fullTrack2, train, "readiness");
11799
+ remove(program3, "full-track");
11800
+ const worktree2 = child(program3, "worktree");
11801
+ move(program3, worktree2, "gc");
11802
+ const stage = child(program3, "stage");
11803
+ move(program3, stage, "port-range");
11804
+ }
11805
+
11920
11806
  // src/version-lag.ts
11921
11807
  var VERSION_LABEL = "installed plugin/adapter cache freshness";
11922
11808
  var VERSION_FIX = "update the MMI plugin via /plugin; standalone npm CLI: npm install -g @mutmutco/cli@latest";
@@ -12449,6 +12335,14 @@ function parseVerifySecrets(stdout) {
12449
12335
  }
12450
12336
  return out;
12451
12337
  }
12338
+ function parseVerifyBroker(stdout) {
12339
+ const out = [];
12340
+ for (const line of stdout.split("\n")) {
12341
+ const match = /^(\S+):\s*(reachable|missing|denied|error)\b/.exec(line.trim());
12342
+ if (match) out.push({ key: match[1], status: match[2] });
12343
+ }
12344
+ return out;
12345
+ }
12452
12346
 
12453
12347
  // src/train-apply.ts
12454
12348
  function resolveDeployModel2(meta, repo) {
@@ -12732,7 +12626,7 @@ function classifyProjectGetFailure(text) {
12732
12626
  }
12733
12627
  async function loadProjectMeta(deps, ctx) {
12734
12628
  try {
12735
- const out = await deps.runSelf(["project", "get", ctx.repo]);
12629
+ const out = await deps.runSelf(["org", "project", "get", ctx.repo]);
12736
12630
  const parsed = JSON.parse(out);
12737
12631
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
12738
12632
  return { status: "read-error", error: "invalid registry META JSON" };
@@ -12851,6 +12745,9 @@ function correlatePublishRun(deps, since, titleIncludes) {
12851
12745
  function correlateControlRun(deps, since, titleIncludes) {
12852
12746
  return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
12853
12747
  }
12748
+ function correlateReconcileRun(deps, since, titleIncludes) {
12749
+ return correlateRun(deps, { workflow: "tenant-reconcile.yml", since, mode: "dispatch", titleIncludes });
12750
+ }
12854
12751
  async function correlateWorkflowRun(deps, args) {
12855
12752
  return correlateRun(deps, { ...args, mode: "workflow" });
12856
12753
  }
@@ -13906,8 +13803,36 @@ async function runTenantRedeploy(deps, options) {
13906
13803
  const d = await dispatchDeploy(deps, ctx, stage, ref, deployModel, watch);
13907
13804
  return { ...ctx, command: "tenant-redeploy", stage, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
13908
13805
  }
13806
+ async function runTenantReconcile(deps, options) {
13807
+ const parts = options.repo.split("/");
13808
+ if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error(`repo must be owner/name, got ${options.repo}`);
13809
+ if (!["dev", "rc", "main"].includes(options.stage)) throw new Error(`stage must be dev, rc, or main, got ${options.stage}`);
13810
+ if (!deps.dispatchTenantReconcile) throw new Error("tenant reconcile dispatch is unavailable");
13811
+ const stage = options.stage;
13812
+ const base = { command: "tenant-reconcile", repo: options.repo, stage };
13813
+ const since = (deps.now ?? Date.now)();
13814
+ const dispatched = await deps.dispatchTenantReconcile({ repo: options.repo, stage });
13815
+ if (!dispatched.ok) {
13816
+ return {
13817
+ ...base,
13818
+ dispatched: false,
13819
+ conclusion: "failure",
13820
+ note: `tenant reconcile rejected: ${dispatched.error ?? "request rejected by the Hub"}`
13821
+ };
13822
+ }
13823
+ const slug = parts[1].toLowerCase();
13824
+ const run = await correlateReconcileRun(deps, since, [slug, stage]);
13825
+ const conclusion = options.watch ?? true ? await watchTenantRun(deps, run.runId) : "pending";
13826
+ return {
13827
+ ...base,
13828
+ dispatched: true,
13829
+ ...run,
13830
+ conclusion,
13831
+ note: conclusion === "success" ? "tenant-reconcile run succeeded" : conclusion === "failure" ? "tenant-reconcile run failed \u2014 inspect the run before redeploying" : "tenant-reconcile dispatched but not confirmed \u2014 do not redeploy until the run succeeds"
13832
+ };
13833
+ }
13909
13834
  function tenantControlWatches(action) {
13910
- return action === "status" || action === "retire" || action === "verify-secrets";
13835
+ return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker";
13911
13836
  }
13912
13837
  async function runTenantControl(deps, options) {
13913
13838
  const { repo, stage, action } = options;
@@ -13931,13 +13856,16 @@ async function runTenantControl(deps, options) {
13931
13856
  if (action === "retire") {
13932
13857
  result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
13933
13858
  }
13934
- if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets")) {
13859
+ if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker")) {
13935
13860
  const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
13936
13861
  if (action === "status") {
13937
13862
  result.serviceState = parseStatusSnippet(output).serviceState;
13938
- } else {
13863
+ } else if (action === "verify-secrets") {
13939
13864
  result.secrets = parseVerifySecrets(output);
13940
13865
  result.secretsRaw = output;
13866
+ } else {
13867
+ result.broker = parseVerifyBroker(output);
13868
+ result.brokerRaw = output;
13941
13869
  }
13942
13870
  }
13943
13871
  result.note = conclusion === "success" ? `tenant-control ${action} run succeeded` : conclusion === "failure" ? `tenant-control ${action} run failed \u2014 inspect the run` : runId == null ? `dispatched tenant-control.yml (${action}) \u2014 run not correlated; check the Actions tab` : `dispatched tenant-control.yml (${action}) \u2014 not watched`;
@@ -13987,6 +13915,26 @@ function renderVerifySecrets(body) {
13987
13915
  return { lines, failure: null };
13988
13916
  }
13989
13917
 
13918
+ // src/tenant-verify-broker.ts
13919
+ function renderVerifyBroker(input) {
13920
+ if (input.broker.length === 0) {
13921
+ const raw = input.raw?.trim();
13922
+ if (raw?.startsWith("no broker-readable runtime secrets declared")) return { lines: [raw], failure: null };
13923
+ return {
13924
+ lines: raw ? raw.split("\n") : ["verify-broker: no per-key verdicts returned"],
13925
+ failure: "verify-broker returned no per-key verdicts, so broker access was not verified"
13926
+ };
13927
+ }
13928
+ const lines = input.broker.map((item) => `${item.key}: ${item.status}`);
13929
+ const reachable = input.broker.filter((item) => item.status === "reachable").length;
13930
+ const bad = input.broker.length - reachable;
13931
+ lines.push(`verify-broker: ${reachable} reachable, ${bad} failed`);
13932
+ return {
13933
+ lines,
13934
+ failure: bad > 0 ? `${bad} of ${input.broker.length} broker read(s) failed; redeploy refreshes an expired runtime token` : null
13935
+ };
13936
+ }
13937
+
13990
13938
  // src/hotfix-coverage.ts
13991
13939
  var import_node_child_process8 = require("node:child_process");
13992
13940
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
@@ -15009,14 +14957,10 @@ function loadAccessTargets(projectsJson) {
15009
14957
  const seen = /* @__PURE__ */ new Set();
15010
14958
  const targets = [];
15011
14959
  for (const project2 of projects) {
15012
- const embeddedContent = new Set(
15013
- (project2.fanoutTargets ?? []).filter((r) => r.class === "content" && r.repo).map((r) => r.repo.toLowerCase())
15014
- );
15015
14960
  for (const repo of project2.repos ?? []) {
15016
14961
  if (seen.has(repo)) continue;
15017
14962
  seen.add(repo);
15018
- const repoName = repo.split("/").pop()?.toLowerCase() ?? repo.toLowerCase();
15019
- const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) ? "content" : "deployable";
14963
+ const cls = project2.class === "content" || project2.deployModel === "content" ? "content" : "deployable";
15020
14964
  const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
15021
14965
  targets.push({ repo, class: cls, releaseTrack });
15022
14966
  }
@@ -15096,9 +15040,13 @@ function renderAccessReport(report) {
15096
15040
  var import_node_fs16 = require("node:fs");
15097
15041
  var import_node_path14 = require("node:path");
15098
15042
  var DOCS_INDEX_PATH = "docs/index.md";
15043
+ function isRoutableDocsPath(relPath) {
15044
+ const normalized = relPath.replace(/\\/g, "/");
15045
+ return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
15046
+ }
15099
15047
  var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
15100
- function escapeCell(text) {
15101
- return text.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
15048
+ function plainInline(text) {
15049
+ 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();
15102
15050
  }
15103
15051
  function extractDocMeta(relPath, raw) {
15104
15052
  const lines = raw.split(/\r?\n/);
@@ -15126,7 +15074,7 @@ function extractDocMeta(relPath, raw) {
15126
15074
  break;
15127
15075
  }
15128
15076
  if (trimmed.startsWith("#")) continue;
15129
- scope = escapeCell(trimmed);
15077
+ scope = plainInline(trimmed);
15130
15078
  break;
15131
15079
  }
15132
15080
  if (!title) {
@@ -15138,16 +15086,16 @@ function extractDocMeta(relPath, raw) {
15138
15086
  function renderDocsIndex(entries) {
15139
15087
  const lines = [
15140
15088
  GENERATED_HEADER,
15089
+ "",
15141
15090
  "# Documentation index",
15142
15091
  "",
15143
15092
  "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
15144
15093
  "can never diverge from the records it lists.",
15145
- "",
15146
- "| Document | Scope |",
15147
- "| --- | --- |"
15094
+ ""
15148
15095
  ];
15149
15096
  for (const entry of entries) {
15150
- lines.push(`| [${escapeCell(entry.title)}](${entry.path}) | ${entry.scope} |`);
15097
+ const link = `[${plainInline(entry.title)}](${entry.path})`;
15098
+ lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
15151
15099
  }
15152
15100
  return lines.join("\n") + "\n";
15153
15101
  }
@@ -15183,7 +15131,7 @@ function createDocsIndexDeps(repoRoot2) {
15183
15131
  const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
15184
15132
  const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
15185
15133
  return {
15186
- listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter((p) => p !== "index.md").sort() : [],
15134
+ listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
15187
15135
  readDoc: (relPath) => (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
15188
15136
  readIndex: () => (0, import_node_fs16.existsSync)(indexPath) ? (0, import_node_fs16.readFileSync)(indexPath, "utf8") : null,
15189
15137
  writeIndex: (content) => (0, import_node_fs16.writeFileSync)(indexPath, content, "utf8")
@@ -15290,427 +15238,6 @@ function docsAuditStatus(fetch2, opts) {
15290
15238
  return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
15291
15239
  }
15292
15240
 
15293
- // src/project-readiness.ts
15294
- function stagesForTrack(meta) {
15295
- return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
15296
- }
15297
- function stageInTrack(meta, stage) {
15298
- return stagesForTrack(meta).includes(stage);
15299
- }
15300
- function dnsErrorToResolution(code) {
15301
- return code === "ENOTFOUND" || code === "EAI_NONAME" ? false : void 0;
15302
- }
15303
- var STAGES = ["dev", "rc", "main"];
15304
- var DEFAULT_RUNTIME_SECRET_NAMES2 = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
15305
- function boardRegistryGaps(meta) {
15306
- if (!meta?.projectId) return [];
15307
- if (meta.projectNumber != null) return [];
15308
- return ["projectNumber"];
15309
- }
15310
- function previewRegistryMetaMerge(existing, patch) {
15311
- const out = { ...existing ?? {} };
15312
- for (const [key, value] of Object.entries(patch)) {
15313
- if (value === null) delete out[key];
15314
- else out[key] = value;
15315
- }
15316
- return out;
15317
- }
15318
- function boardLinkWriteError(patch, existing) {
15319
- const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
15320
- const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
15321
- if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
15322
- return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
15323
- }
15324
- if (patch.projectNumber === null && boardRegistryGaps(previewRegistryMetaMerge(existing, patch)).length) {
15325
- return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
15326
- }
15327
- return null;
15328
- }
15329
- function boardRegistryGapMessage(repo) {
15330
- return `Board META incomplete for ${repo}: registry has projectId but no projectNumber \u2014 board claim and auto-add will fail until projectNumber is backfilled (re-run \`node infra/migrate/seed-registry.mjs\` or \`mmi-cli bootstrap apply --execute\` with board vars)`;
15331
- }
15332
- var slugOfRepo = slugOf;
15333
- function repoFrom(repoOrSlug, slug) {
15334
- return repoOrSlug.includes("/") ? repoOrSlug : `mutmutco/${slug}`;
15335
- }
15336
- function defaultSubdomain(slug) {
15337
- return slug.replace(/^[a-z]+-/, "");
15338
- }
15339
- function projectRequiresGoogleOAuth(meta, model) {
15340
- if (!meta || model === "content" || model === "none" || meta.deployModel === "content" || meta.deployModel === "none" || meta.class === "content") return false;
15341
- const projectType = resolveProjectType(meta, meta.repos?.[0] ?? "");
15342
- if (projectType !== "web-app") return false;
15343
- return Boolean(meta.oauth && typeof meta.oauth === "object");
15344
- }
15345
- function declaresNoPublicEdge(meta) {
15346
- const ed = meta?.edgeDomains;
15347
- return Boolean(ed && typeof ed === "object" && !Array.isArray(ed) && Object.keys(ed).length === 0);
15348
- }
15349
- function declaresWorker(meta) {
15350
- return meta?.projectType === "worker";
15351
- }
15352
- function isCentralContainerModel(model) {
15353
- return model === "tenant-container" || model === "solo-container";
15354
- }
15355
- function isNoEdgeTenantWorker(meta, model) {
15356
- return isCentralContainerModel(model) && (declaresNoPublicEdge(meta) || declaresWorker(meta));
15357
- }
15358
- function projectRequiresDeployCoords(model, stage, meta) {
15359
- if (!isCentralContainerModel(model)) return false;
15360
- if (stage && isNoEdgeTenantWorker(meta, model)) return stage === "main";
15361
- return true;
15362
- }
15363
- function projectRequiresDeployState(model, stage) {
15364
- return model === "hub-serverless" && stage !== "dev";
15365
- }
15366
- function stageRequiredSecrets(stage, meta) {
15367
- const contract = meta.requiredRuntimeSecrets;
15368
- const extra = !Array.isArray(contract) && Array.isArray(contract?.[stage]) ? contract[stage] ?? [] : [];
15369
- const model = resolveDeployModel(meta, meta.repos?.[0] ?? "");
15370
- if (isNoEdgeTenantWorker(meta, model) && stage !== "main") return [];
15371
- const defaults = projectRequiresGoogleOAuth(meta, model) ? DEFAULT_RUNTIME_SECRET_NAMES2 : [];
15372
- return [.../* @__PURE__ */ new Set([...defaults, ...extra])];
15373
- }
15374
- function stageKey2(stage, key) {
15375
- return key.includes("/") ? key : `${stage}/${key}`;
15376
- }
15377
- function materializedRuntimeSecretName(entry) {
15378
- return entry.includes(":") ? entry.split(":").pop() : entry;
15379
- }
15380
- function runtimeSecretStreamGap(stage, meta, presentSecrets) {
15381
- const streamed = new Set(
15382
- (contractByStage(meta.requiredRuntimeSecrets)[stage] ?? []).map(materializedRuntimeSecretName)
15383
- );
15384
- const seen = /* @__PURE__ */ new Set();
15385
- const gap = [];
15386
- for (const name of stageRequiredSecrets(stage, meta).map(materializedRuntimeSecretName)) {
15387
- if (seen.has(name)) continue;
15388
- seen.add(name);
15389
- if (!streamed.has(name) && (presentSecrets.has(stageKey2(stage, name)) || presentSecrets.has(name))) gap.push(name);
15390
- }
15391
- return gap;
15392
- }
15393
- function hasRuntimeSecretContract(contract) {
15394
- if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
15395
- return ["dev", "rc", "main"].some((stage) => Array.isArray(contract[stage]));
15396
- }
15397
- function appAttestationOf(meta) {
15398
- const a = meta?.appAttested;
15399
- if (!a || typeof a !== "object" || Array.isArray(a)) return null;
15400
- const { at, by } = a;
15401
- return typeof at === "string" && at.length > 0 && typeof by === "string" && by.length > 0 ? { at, by } : null;
15402
- }
15403
- function attestedLine(att) {
15404
- return `App-owned readiness attested by @${att.by} on ${att.at.slice(0, 10)} \u2014 the static checklist is cleared (the doctor reads no product repo files); re-run \`mmi-cli org project attest\` after app-owned structural changes.`;
15405
- }
15406
- var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
15407
- function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
15408
- const attested = appAttestationOf(meta);
15409
- const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-app" && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
15410
- const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
15411
- if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
15412
- if (projectType === "content" || model === "content") return ["Content repo: keep app-owned work to docs/content changes; release train does not apply."];
15413
- if (projectType === "desktop-app") {
15414
- return [
15415
- "Desktop app: no Hub deploy coords, Google OAuth, DWD, or runtime tenant control by default; keep app-owned installer/packaging outside the tenant train.",
15416
- "Keep README.md and architecture.md explicit about the per-OS build and distribution path (installer, and any registry publish alongside it) so readiness does not guess web infra."
15417
- ];
15418
- }
15419
- if (projectType === "desktop-game") {
15420
- return [
15421
- "Desktop game: no Hub deploy coords, Google OAuth, DWD, or runtime tenant control by default; keep app-owned release packaging outside the tenant train.",
15422
- "Keep README.md and architecture.md explicit about the game build/release path so readiness does not guess web infra."
15423
- ];
15424
- }
15425
- if (projectType === "non-deployable" || model === "none") {
15426
- return [
15427
- "Non-deployable repo: no Hub deploy coords, Google OAuth, DWD, or runtime tenant control by default.",
15428
- "Keep README.md and architecture.md explicit about the repo purpose and any project-admin workflow it still needs."
15429
- ];
15430
- }
15431
- if (model === "hub-serverless") {
15432
- return [
15433
- "Hub serverless deploy stays on the Hub-owned workflows; deploy.yml stamps per-stage deploy state onto state-only DEPLOY# rows (no tenant coords).",
15434
- "Make runtime config fail clearly for missing required env in prod/rc instead of relying on hidden defaults."
15435
- ];
15436
- }
15437
- if (model === "serverless") {
15438
- return [
15439
- "Serverless deploys use the central serverless path; do not add tenant compose or box plumbing.",
15440
- "Make runtime config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
15441
- "Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
15442
- ];
15443
- }
15444
- if (projectType === "cli-tool" || model === "registry-publish") {
15445
- return [
15446
- "Distributable CLI/plugin: ship a publishable package.json (name, version, bin/exports) \u2014 release publishes to npm (plugin-marketplace publish is future work when a plugin tenant lands), not a box deploy; no Hub deploy coords, edge, or OAuth.",
15447
- "Keep the published version in lockstep with the release tag; make the publish idempotent (skip when the version is already on the registry).",
15448
- "Keep app-owned README.md and architecture.md aligned with the registry-publish release path."
15449
- ];
15450
- }
15451
- const gaps = [
15452
- 'Ensure the compose is fileless (#2662) \u2014 drop env_file: and mark each runtime-secret service with the mmi.runtime-env: "true" label; the box injects coords + declared runtime secrets into the container process env at deploy (no .env on disk); the app reads plain environment variables and never self-loads SSM.',
15453
- "Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
15454
- "Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
15455
- ];
15456
- if (isNoEdgeTenantWorker(meta, model)) {
15457
- gaps.unshift("No public edge (worker/outbound-only tenant \u2014 `projectType: worker` or `edgeDomains: {}`): skip Cloudflare edge auto-heal, OAuth defaults, and dev/rc remote deploy coords unless META explicitly adds them.");
15458
- }
15459
- if (contractUndeclared) {
15460
- gaps.unshift(CONTRACT_UNDECLARED_LINE);
15461
- }
15462
- if (slug === "mmi-katip") {
15463
- gaps.push("Katip-specific app plan: declare Google Workspace service-account requirements, use the service account numeric OAuth2 client ID for DWD, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
15464
- }
15465
- if (model === "solo-container" || model === "tenant-container") {
15466
- if (typeof mainDeployFact?.port === "number") {
15467
- gaps.push(`Seed DEPLOY#main healthUrl to http://127.0.0.1:${mainDeployFact.port}/health during bootstrap (tenant reconcile/control health gate \u2014 #1202; port read from DEPLOY#main).`);
15468
- } else if (!appAttestationOf(meta)) {
15469
- gaps.push("Seed DEPLOY#main healthUrl from the DEPLOY#main edgeVhost.port during bootstrap (tenant reconcile/control health gate \u2014 #1202); doctor has no committed DEPLOY port to cite yet.");
15470
- }
15471
- }
15472
- if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
15473
- return gaps;
15474
- }
15475
- function edgeDomainsByStage(meta) {
15476
- const ed = meta?.edgeDomains;
15477
- if (!ed || typeof ed !== "object" || Array.isArray(ed)) return {};
15478
- const out = {};
15479
- for (const stage of STAGES) {
15480
- const v = ed[stage];
15481
- if (typeof v === "string" && v.trim()) out[stage] = v.trim();
15482
- }
15483
- return out;
15484
- }
15485
- async function probeEdgeDomains(meta, resolveDns) {
15486
- const byStage = edgeDomainsByStage(meta);
15487
- const results = await Promise.all(
15488
- Object.entries(byStage).map(async ([stage, host]) => {
15489
- let resolved;
15490
- try {
15491
- resolved = await resolveDns(host);
15492
- } catch {
15493
- resolved = void 0;
15494
- }
15495
- return resolved === false ? { stage, host } : void 0;
15496
- })
15497
- );
15498
- return results.filter((r) => r !== void 0);
15499
- }
15500
- function contractByStage(contract) {
15501
- return contract && !Array.isArray(contract) ? contract : {};
15502
- }
15503
- function ensureStageNames(names, required) {
15504
- return [.../* @__PURE__ */ new Set([...names ?? [], ...required])];
15505
- }
15506
- function sameNames(a, b) {
15507
- const left = new Set(a ?? []);
15508
- const right = new Set(b ?? []);
15509
- if (left.size !== right.size) return false;
15510
- return [...left].every((name) => right.has(name));
15511
- }
15512
- function sameStageContract(a, b) {
15513
- return STAGES.every((stage) => sameNames(a[stage], b[stage]));
15514
- }
15515
- function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
15516
- const slug = slugOfRepo(repoOrSlug);
15517
- const repo = repoFrom(repoOrSlug, slug);
15518
- const sub = defaultSubdomain(slug);
15519
- const patch = {};
15520
- const confidentType = resolveProjectTypeConfident(meta, repo);
15521
- const projectType = confidentType ?? resolveProjectType(meta, repo);
15522
- const model = resolveDeployModel(meta, repo);
15523
- if (confidentType && !meta?.class) patch.class = confidentType === "content" ? "content" : "deployable";
15524
- if (confidentType) {
15525
- if (!meta?.projectType) patch.projectType = confidentType;
15526
- if (!meta?.deployModel) patch.deployModel = model;
15527
- }
15528
- if (!meta?.vaultPath) patch.vaultPath = `/mmi-future/${slug}`;
15529
- if (confidentType && !meta?.edgeDomains && model === "tenant-container" && projectType !== "worker") {
15530
- patch.edgeDomains = {
15531
- dev: `dev.${sub}.mutatismutandis.co`,
15532
- rc: `rc.${sub}.mutatismutandis.co`,
15533
- main: `${sub}.mutatismutandis.co`
15534
- };
15535
- }
15536
- if (meta && projectTypeClearsWebProfile(projectType, model)) {
15537
- if (meta.oauth !== void 0) patch.oauth = null;
15538
- if (meta.edgeDomains !== void 0) patch.edgeDomains = null;
15539
- }
15540
- if (slug === "mmi-katip") {
15541
- const required = ["GOOGLE_IMPERSONATE_USER", "GOOGLE_ADMIN_USER"];
15542
- const current = contractByStage(meta?.requiredRuntimeSecrets);
15543
- const next = {
15544
- dev: ensureStageNames(current.dev, required),
15545
- rc: ensureStageNames(current.rc, required),
15546
- main: ensureStageNames(current.main, required)
15547
- };
15548
- if (!sameStageContract(current, next)) {
15549
- patch.requiredRuntimeSecrets = next;
15550
- }
15551
- }
15552
- const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli org project set ${repo} --project-type <web-app|hub-service|content|desktop-app|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
15553
- if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
15554
- return { slug, patch, appOwnedGaps };
15555
- }
15556
- async function runV2Heal(repoOrSlug, opts, deps) {
15557
- const slug = slugOfRepo(repoOrSlug);
15558
- const read = await deps.fetchProject(slug);
15559
- if (!read.ok) {
15560
- return { status: "aborted", slug, error: `registry read failed for ${slug} (${read.error}) \u2014 aborting; a failed read must not be treated as a missing project` };
15561
- }
15562
- const plan = buildV2HealPatch(repoOrSlug, read.project);
15563
- if (!opts.apply) return { status: "planned", slug, patch: plan.patch, appOwnedGaps: plan.appOwnedGaps };
15564
- if (!Object.keys(plan.patch).length) return { status: "noop", slug, appOwnedGaps: plan.appOwnedGaps };
15565
- const write = await deps.upsertProject(slug, plan.patch);
15566
- if (!write.ok) return { status: "write-failed", slug, write };
15567
- return { status: "applied", slug, applied: Object.keys(plan.patch), appOwnedGaps: plan.appOwnedGaps, result: write.body };
15568
- }
15569
- async function buildV2Doctor(repoOrSlug, deps) {
15570
- const slug = slugOfRepo(repoOrSlug);
15571
- const repo = repoFrom(repoOrSlug, slug);
15572
- const read = await deps.getProject(slug);
15573
- if (!read.ok) {
15574
- const degradedSecrets = {
15575
- dev: { required: [], present: [], missing: [] },
15576
- rc: { required: [], present: [], missing: [] },
15577
- main: { required: [], present: [], missing: [] }
15578
- };
15579
- const degradedStage = {
15580
- dev: { ok: false, required: false },
15581
- rc: { ok: false, required: false },
15582
- main: { ok: false, required: false }
15583
- };
15584
- return {
15585
- ok: false,
15586
- repo,
15587
- slug,
15588
- registryError: read.error,
15589
- hubOwned: { meta: { ok: false, missing: [] }, deployCoords: degradedStage, deployState: degradedStage, secrets: degradedSecrets },
15590
- autoHealAvailable: [],
15591
- appOwnedGaps: [`Hub registry read failed (${read.error}) \u2014 diagnosis degraded; nothing is known about META, coords, or gaps. Likely transient (cold start, network, or auth blip): retry \`mmi-cli org project doctor\` shortly.`]
15592
- };
15593
- }
15594
- const meta = read.project;
15595
- const projectType = resolveProjectType(meta, repo);
15596
- const model = resolveDeployModel(meta, repo);
15597
- const mainDeployFact = meta && deps.getDeployFact ? await deps.getDeployFact(slug, "main") : null;
15598
- const autoHeal = buildV2HealPatch(repo, meta, mainDeployFact);
15599
- if (!meta) {
15600
- const emptySecrets = {
15601
- dev: { required: [], present: [], missing: [] },
15602
- rc: { required: [], present: [], missing: [] },
15603
- main: { required: [], present: [], missing: [] }
15604
- };
15605
- const emptyCoords = {
15606
- dev: { ok: false, required: true },
15607
- rc: { ok: false, required: true },
15608
- main: { ok: false, required: true }
15609
- };
15610
- const emptyState = Object.fromEntries(STAGES.map((stage) => {
15611
- const required = projectRequiresDeployState(model, stage);
15612
- return [stage, { required, ok: !required }];
15613
- }));
15614
- return {
15615
- ok: false,
15616
- repo,
15617
- slug,
15618
- hubOwned: { meta: { ok: false, missing: [`PROJECT#${slug}/META`] }, deployCoords: emptyCoords, deployState: emptyState, secrets: emptySecrets },
15619
- autoHealAvailable: Object.keys(autoHeal.patch),
15620
- appOwnedGaps: autoHeal.appOwnedGaps
15621
- };
15622
- }
15623
- let presentSecrets = /* @__PURE__ */ new Set();
15624
- let secretsError;
15625
- try {
15626
- presentSecrets = new Set(await deps.listSecrets(repo));
15627
- } catch (e) {
15628
- secretsError = e?.message || "secrets list failed";
15629
- }
15630
- const deployCoords = Object.fromEntries(await Promise.all(STAGES.map(async (stage) => {
15631
- const required = stageInTrack(meta, stage) && projectRequiresDeployCoords(model, stage, meta);
15632
- return [stage, { required, ok: required ? await deps.hasDeployCoords(slug, stage) : true }];
15633
- })));
15634
- const deployState = Object.fromEntries(await Promise.all(STAGES.map(async (stage) => {
15635
- const required = stageInTrack(meta, stage) && projectRequiresDeployState(model, stage);
15636
- return [stage, { required, ok: required ? await deps.hasDeployState(slug, stage) : true }];
15637
- })));
15638
- const secrets = Object.fromEntries(STAGES.map((stage) => {
15639
- const names = stageInTrack(meta, stage) ? stageRequiredSecrets(stage, meta) : [];
15640
- const satisfied = (key) => presentSecrets.has(stageKey2(stage, key)) || !key.includes("/") && presentSecrets.has(key);
15641
- const required = names.map((key) => stageKey2(stage, key));
15642
- const present = names.filter(satisfied).map((key) => stageKey2(stage, key));
15643
- const missing = names.filter((key) => !satisfied(key)).map((key) => stageKey2(stage, key));
15644
- return [stage, { required, present, missing }];
15645
- }));
15646
- const runtimeSecretStreamWarnings = Object.fromEntries(STAGES.map((stage) => [
15647
- stage,
15648
- stageInTrack(meta, stage) ? runtimeSecretStreamGap(stage, meta, presentSecrets) : []
15649
- ]));
15650
- const runtimeSecretStreamWarningRows = STAGES.map((stage) => ({ stage, names: runtimeSecretStreamWarnings[stage] })).filter((row) => row.names.length > 0);
15651
- const metaMissing = ["class", "projectType", "deployModel", "vaultPath"].filter((key) => meta[key] === void 0).concat(boardRegistryGaps(meta));
15652
- const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets).every((v) => v.missing.length === 0);
15653
- const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
15654
- return {
15655
- ok,
15656
- repo,
15657
- slug,
15658
- class: meta.class,
15659
- projectType,
15660
- deployModel: model,
15661
- hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets },
15662
- secretsError,
15663
- autoHealAvailable: Object.keys(autoHeal.patch),
15664
- appOwnedGaps: autoHeal.appOwnedGaps,
15665
- ...edgeDomainWarnings.length ? { edgeDomainWarnings } : {},
15666
- ...runtimeSecretStreamWarningRows.length ? { runtimeSecretStreamWarnings: runtimeSecretStreamWarningRows } : {},
15667
- appAttested: appAttestationOf(meta) ?? void 0
15668
- };
15669
- }
15670
- function renderReadinessIssueBody(existingBody, report, opts = {}) {
15671
- const start = "<!-- mmi-v2-readiness:start -->";
15672
- const end = "<!-- mmi-v2-readiness:end -->";
15673
- const stageLines = STAGES.map((stage) => {
15674
- const coordState = report.hubOwned.deployCoords[stage];
15675
- const coords = !coordState.required ? "coords n/a" : coordState.ok ? "coords ok" : "coords missing/unverified";
15676
- const state = report.hubOwned.deployState[stage];
15677
- const statePart = !state.required ? [] : [state.ok ? "deploy state stamped" : "deploy state not stamped"];
15678
- const missing = report.hubOwned.secrets[stage].missing;
15679
- return `- ${stage}: ${[coords, ...statePart].join("; ")}; ${missing.length ? `missing ${missing.join(", ")}` : "required secret names present"}`;
15680
- });
15681
- const section = [
15682
- start,
15683
- "## Hub v2 readiness",
15684
- "",
15685
- `Repo: ${report.repo}`,
15686
- `Project type: ${report.projectType ?? "(unresolved)"}`,
15687
- `Deploy model: ${report.deployModel ?? "(unresolved)"}`,
15688
- `Overall: ${report.ok ? "ready" : "not ready"}`,
15689
- "",
15690
- "### Hub-owned diagnosis",
15691
- `- META: ${report.hubOwned.meta.ok ? "ok" : `missing ${report.hubOwned.meta.missing.join(", ")}`}`,
15692
- ...stageLines,
15693
- ...report.secretsError ? [`- secrets UNVERIFIED (treated as not ready): ${report.secretsError}`] : [],
15694
- ...(report.edgeDomainWarnings ?? []).map(
15695
- (w) => `- \u26A0 edge domain does not resolve in DNS (advisory): ${w.stage} \u2192 ${w.host}; verify the registry edgeDomains value against the live public host`
15696
- ),
15697
- ...(report.runtimeSecretStreamWarnings ?? []).map(
15698
- (w) => `- \u26A0 required secrets provisioned but not in requiredRuntimeSecrets (advisory): ${w.stage} \u2192 ${w.names.join(", ")}; add them to the registry stream list or they will not be materialized into tenant.env`
15699
- ),
15700
- "",
15701
- "### Auto-heal applied / available",
15702
- ...opts.healed?.length ? opts.healed.map((x) => `- ${x}`) : report.autoHealAvailable.map((x) => `- ${x}`),
15703
- "",
15704
- "### App-owned implementation plan",
15705
- ...report.appOwnedGaps.map((x) => `- ${x}`),
15706
- end
15707
- ].join("\n");
15708
- const re = new RegExp(`${start}[\\s\\S]*?${end}`);
15709
- return re.test(existingBody) ? existingBody.replace(re, () => section) : `${existingBody.trim()}
15710
-
15711
- ${section}`.trim();
15712
- }
15713
-
15714
15241
  // src/oauth.ts
15715
15242
  var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
15716
15243
  var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
@@ -15718,7 +15245,7 @@ var ENV_PREFIXES = ["", "dev", "rc"];
15718
15245
  var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
15719
15246
  var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
15720
15247
  var uniq = (xs) => [...new Set(xs)];
15721
- function defaultSubdomain2(slug) {
15248
+ function defaultSubdomain(slug) {
15722
15249
  const i = slug.indexOf("-");
15723
15250
  return i === -1 ? slug : slug.slice(i + 1);
15724
15251
  }
@@ -15771,7 +15298,7 @@ function parseOauthConfig(mmiConfig, slug) {
15771
15298
  throw new Error("oauth must be an object when configured");
15772
15299
  }
15773
15300
  const raw = rawUnknown;
15774
- const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
15301
+ const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain(slug)];
15775
15302
  const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
15776
15303
  const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
15777
15304
  if (!callbackPath.startsWith("/")) {
@@ -15782,7 +15309,7 @@ function parseOauthConfig(mmiConfig, slug) {
15782
15309
  }
15783
15310
  const meta = mmiConfig ?? {};
15784
15311
  const rawFofuSub = raw.fofuSubdomain;
15785
- const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
15312
+ const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain(slug) : void 0;
15786
15313
  return { subdomains, domains, callbackPath, fofuSubdomain };
15787
15314
  }
15788
15315
  function probeRedirectUri(callbackPath, port = 9123) {
@@ -15810,6 +15337,26 @@ var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
15810
15337
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
15811
15338
  var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
15812
15339
  var BUILD_SECRET_REF_RE = /^(?:[A-Z_][A-Z0-9_]*=)?(?:[A-Z_][A-Z0-9_]*|(?:mm-fofu|_org\/[a-z0-9][a-z0-9-]*):[A-Z_][A-Z0-9_]*|@github-packages-token)$/;
15340
+ function previewRegistryMetaMerge(existing, patch) {
15341
+ const out = { ...existing ?? {} };
15342
+ for (const [key, value] of Object.entries(patch)) {
15343
+ if (value === null) delete out[key];
15344
+ else out[key] = value;
15345
+ }
15346
+ return out;
15347
+ }
15348
+ function boardLinkWriteError(patch, existing) {
15349
+ const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
15350
+ const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
15351
+ if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
15352
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
15353
+ }
15354
+ const preview = previewRegistryMetaMerge(existing, patch);
15355
+ if (patch.projectNumber === null && preview.projectId && preview.projectNumber == null) {
15356
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
15357
+ }
15358
+ return null;
15359
+ }
15813
15360
  function parseSecretsCatalogVar(raw) {
15814
15361
  let parsed;
15815
15362
  try {
@@ -16321,17 +15868,44 @@ function buildSetDeployPatch(_slug, input) {
16321
15868
  };
16322
15869
  }
16323
15870
  var DEPLOY_STAGE_BRANCH = { dev: "development", rc: "rc", main: "main" };
15871
+ function filelessTransitionGuide(repo, stage) {
15872
+ const authority = stage === "main" ? "Authority: main is master-admin only." : "Authority: a project-admin may complete this for their own repo; master may also do it.";
15873
+ return [
15874
+ `Fileless transition for ${repo} (${stage}):`,
15875
+ ` ${authority}`,
15876
+ ` mmi-cli org access role ${repo}`,
15877
+ ` mmi-cli secrets list --repo ${repo} # names/capabilities only`,
15878
+ ` mmi-cli secrets preflight --repo ${repo} --stage ${stage}`,
15879
+ ` mmi-cli org project set-deploy ${repo} --stage ${stage} --no-env-file true`,
15880
+ ` mmi-cli runtime tenant reconcile ${repo} ${stage} --watch`,
15881
+ ` mmi-cli runtime tenant redeploy ${repo} ${stage} --watch`,
15882
+ ` mmi-cli runtime tenant control ${repo} ${stage} verify-secrets --watch`,
15883
+ ` mmi-cli runtime tenant control ${repo} ${stage} verify-broker --watch # broker consumers`,
15884
+ " Missing/undeclared key (project-admin own project):",
15885
+ ` mmi-cli secrets declare <KEY> --repo ${repo} --purpose "<purpose>" --group "<group>" --consumers runtime --stages ${stage}`,
15886
+ ` mmi-cli secrets set ${stage}/<KEY> --repo ${repo} # value from secure prompt/stdin; never argv/stdout`,
15887
+ ` mmi-cli secrets request <KEY> --repo ${repo} --reason "wire requiredRuntimeSecrets for ${stage}" # only if env injection needs master wiring`,
15888
+ " Runbook: https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/tenant-fileless-migration.md"
15889
+ ].join("\n");
15890
+ }
16324
15891
  function composeDeclaresEnvFile(composeText) {
16325
15892
  return composeText.split(/\r?\n/).some((line) => /^\s*env_file\s*:/.test(line));
16326
15893
  }
16327
15894
  function composeHasRuntimeEnvLabel(composeText) {
16328
- return /mmi\.runtime-env/.test(composeText);
15895
+ return composeText.split(/\r?\n/).some((raw) => {
15896
+ const line = raw.replace(/(^|\s)#.*$/, "$1");
15897
+ return /(^|[\s"'])mmi\.runtime-env\s*:\s*["']?true["']?\s*$/.test(line) || /mmi\.runtime-env\s*=\s*["']?true["']?\s*$/.test(line);
15898
+ });
16329
15899
  }
16330
15900
  function evaluateFilelessComposeGuard(input) {
16331
15901
  if (input.force) return { ok: true, warn: `--force: skipping the fileless-compose guard for ${input.branch}` };
16332
15902
  if (input.composeText === null) {
16333
- return { ok: true, warn: `could not read docker-compose.yml on ${input.branch} \u2014 skipping the fileless-compose guard (run from the target repo checkout with the branch fetched to enable it)` };
15903
+ return { ok: false, reason: `could not read docker-compose.yml on ${input.branch} \u2014 refusing to change noEnvFile without verifying the target branch compose. Run from the target repo checkout with origin/${input.branch} fetched, or use --force only after an independent verification.` };
16334
15904
  }
15905
+ if (input.noEnvFile === false && composeHasRuntimeEnvLabel(input.composeText)) {
15906
+ return { ok: false, reason: `the ${input.branch} branch marks a service mmi.runtime-env=true \u2014 setting noEnvFile=false would make the box take the legacy path and refuse this fileless compose (exit 78). Keep noEnvFile=true or land a verified legacy compose first.` };
15907
+ }
15908
+ if (input.noEnvFile === false) return { ok: true };
16335
15909
  if (composeDeclaresEnvFile(input.composeText)) {
16336
15910
  return {
16337
15911
  ok: false,
@@ -16393,15 +15967,16 @@ function evaluatePromotionFilelessGuard(input) {
16393
15967
  return {
16394
15968
  ok: false,
16395
15969
  reason: `the ${branch} docker-compose.yml marks a service fileless (mmi.runtime-env: "true") but DEPLOY#${stage}.noEnvFile is not true \u2014 the ${stage} deploy would fail exit-78 ("compose is fileless but DEPLOY#${stage} noEnvFile is not true"). Fix BEFORE promoting, then re-run:
16396
- mmi-cli org project set-deploy --stage ${stage} --no-env-file true # register the fileless flag
16397
- gh workflow run tenant-reconcile.yml --repo mutmutco/MMI-Hub # re-render the box control script (#1056)
16398
- (or pass --skip-compose-guard to override this preflight)`
15970
+ ${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
15971
+ Override only after independent verification: --skip-compose-guard`
16399
15972
  };
16400
15973
  }
16401
15974
  if (noEnvFile && composeDeclaresEnvFile(input.composeText)) {
16402
15975
  return {
16403
15976
  ok: false,
16404
- reason: `DEPLOY#${stage}.noEnvFile is true but the ${branch} docker-compose.yml still declares env_file: \u2014 the box writes no .env, so docker compose dies on the missing file. Land the fileless compose (drop env_file:, add the mmi.runtime-env label) on ${branch}, or set-deploy --no-env-file false (or --skip-compose-guard to override).`
15977
+ reason: `DEPLOY#${stage}.noEnvFile is true but the ${branch} docker-compose.yml still declares env_file: \u2014 the box writes no .env, so docker compose dies on the missing file. Land the fileless compose (drop env_file:, add the mmi.runtime-env label) on ${branch}.
15978
+ ${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
15979
+ Rollback only after independent verification: set-deploy --no-env-file false; preflight override: --skip-compose-guard.`
16405
15980
  };
16406
15981
  }
16407
15982
  return { ok: true };
@@ -16490,6 +16065,27 @@ var import_node_fs17 = require("node:fs");
16490
16065
  var import_node_path15 = require("node:path");
16491
16066
  var import_node_os5 = require("node:os");
16492
16067
 
16068
+ // src/project-runtime.ts
16069
+ function hasRuntimeSecretContract(contract) {
16070
+ if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
16071
+ return ["dev", "rc", "main"].some((stage) => Array.isArray(contract[stage]));
16072
+ }
16073
+ function projectRequiresGoogleOAuth(meta, model) {
16074
+ if (!meta || model === "content" || model === "none" || meta.deployModel === "content" || meta.deployModel === "none" || meta.class === "content") return false;
16075
+ const projectType = resolveProjectType(meta, meta.repos?.[0] ?? "");
16076
+ return projectType === "web-app" && Boolean(meta.oauth && typeof meta.oauth === "object");
16077
+ }
16078
+ function edgeDomainsByStage(meta) {
16079
+ const edgeDomains = meta?.edgeDomains;
16080
+ if (!edgeDomains || typeof edgeDomains !== "object" || Array.isArray(edgeDomains)) return {};
16081
+ const out = {};
16082
+ for (const stage of ["dev", "rc", "main"]) {
16083
+ const value = edgeDomains[stage];
16084
+ if (typeof value === "string" && value.trim()) out[stage] = value.trim();
16085
+ }
16086
+ return out;
16087
+ }
16088
+
16493
16089
  // src/secrets-diff.ts
16494
16090
  var TIMEOUT_MS2 = 8e3;
16495
16091
  var OWNER3 = "mutmutco";
@@ -16661,13 +16257,13 @@ async function withSecrets(run) {
16661
16257
  await run(makeSecretsDeps(cfg));
16662
16258
  }
16663
16259
  function registerSecretsCommands(program3) {
16664
- const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
16260
+ const secrets = program3.command("secrets").description("project vault \u2014 project-admins self-serve their own repo's full tree (stageless + dev/rc/main); org-infra namespaces are master-gated");
16665
16261
  secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
16666
16262
  secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
16667
- secrets.command("find <intent>").description("resolve a plain-language intent to the canonical secret(s) + the exact get command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
16263
+ secrets.command("find <intent>").description("resolve a plain-language intent to canonical secret names + the exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
16668
16264
  if (!await secretsFind(d, intent, o)) process.exitCode = 1;
16669
16265
  }));
16670
- secrets.command("catalog").description("the secret catalog \u2014 grouped, with each secret's exact get command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--drift", "include the bidirectional reconciler drift report").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCatalog(d, o)));
16266
+ secrets.command("catalog").description("the secret catalog \u2014 grouped, with each secret's exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--drift", "include the bidirectional reconciler drift report").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCatalog(d, o)));
16671
16267
  secrets.command("doctor").description("vault drift report \u2014 declared-missing / orphan / off-scheme / duplicate (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets(async (d) => {
16672
16268
  if (!await secretsDoctor(d, o)) process.exitCode = 1;
16673
16269
  }));
@@ -16736,7 +16332,7 @@ function registerSecretsCommands(program3) {
16736
16332
  const facts = await fetchDeployFactsBySlug(slug, regDeps);
16737
16333
  const fact = facts?.stages?.[stage] ?? null;
16738
16334
  const composeText = sameRepo ? await readStageBranchCompose(branch) : null;
16739
- const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false });
16335
+ const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false, repo });
16740
16336
  if (!verdict.ok) {
16741
16337
  d.err(`secrets preflight: ${verdict.reason}`);
16742
16338
  filelessOk = false;
@@ -16778,7 +16374,7 @@ function registerSecretsCommands(program3) {
16778
16374
  if (!ok) process.exitCode = 1;
16779
16375
  });
16780
16376
  secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
16781
- secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
16377
+ secrets.command("copy").description("copy provider keys between stages in your own project vault (audit-logged; encryption keys blocked)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
16782
16378
  const stages = ["dev", "rc", "main"];
16783
16379
  if (!stages.includes(o.from) || !stages.includes(o.to)) {
16784
16380
  return fail("secrets copy: --from and --to must be dev, rc, or main");
@@ -16818,13 +16414,13 @@ function registerSecretsCommands(program3) {
16818
16414
  );
16819
16415
  if (!ok) process.exitCode = 1;
16820
16416
  }));
16821
- secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
16822
- secrets.command("use <key> [command...]").description("consume a secret KEYLESS: `use <key> -- <cmd>` injects it into the command env (never printed); a granted non-master can use an org secret. No command prints guidance only.").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
16417
+ secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
16418
+ secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
16823
16419
  const ok = await secretsUse(d, key, { ...o, command });
16824
16420
  if (ok === false) process.exitCode = 1;
16825
16421
  }));
16826
- secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
16827
- secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-tier secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
16422
+ secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to one specific org-infra secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
16423
+ secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-infra secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
16828
16424
  }
16829
16425
 
16830
16426
  // src/app-actor.ts
@@ -18603,7 +18199,7 @@ function registerBootstrapCommands(program3) {
18603
18199
  else console.log(renderOrgRulesetDriftReport(plan));
18604
18200
  if (plan.action !== "noop") process.exitCode = 1;
18605
18201
  });
18606
- bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
18202
+ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
18607
18203
  const o = {
18608
18204
  class: rawValue("--class", "deployable"),
18609
18205
  projectType: rawValue("--project-type", ""),
@@ -18919,12 +18515,12 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
18919
18515
  function shellFor(platform2 = process.platform) {
18920
18516
  return platform2 === "win32" ? "powershell" : "bash";
18921
18517
  }
18922
- function isCentralContainerModel2(model) {
18518
+ function isCentralContainerModel(model) {
18923
18519
  return model === "tenant-container" || model === "solo-container";
18924
18520
  }
18925
18521
  function deriveStageGap(inputs) {
18926
18522
  const missing = [];
18927
- if (!isCentralContainerModel2(inputs.deployModel)) {
18523
+ if (!isCentralContainerModel(inputs.deployModel)) {
18928
18524
  return `local stage default applies to central-container repos only (tenant-container/solo-container; registry deployModel = ${inputs.deployModel ?? "unset"})`;
18929
18525
  }
18930
18526
  if (!inputs.hasCompose) missing.push("docker-compose.yml");
@@ -19396,7 +18992,7 @@ function registerBoardCommands(program3) {
19396
18992
  expected: [...BOARD_STATUSES]
19397
18993
  });
19398
18994
  }
19399
- async function runBulkMove(issueRefs, o, status, failLabel) {
18995
+ async function runBulkMove(issueRefs, o, status) {
19400
18996
  try {
19401
18997
  const bulk = await moveBoardIssues({
19402
18998
  config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
@@ -19414,7 +19010,7 @@ function registerBoardCommands(program3) {
19414
19010
  }
19415
19011
  if (bulk.failed > 0) process.exitCode = 1;
19416
19012
  } catch (e) {
19417
- return failGraceful(`${failLabel} failed: ${e.message}`);
19013
+ return failGraceful(`board move failed: ${e.message}`);
19418
19014
  }
19419
19015
  }
19420
19016
  withExamples(mutating(
@@ -19436,7 +19032,7 @@ function registerBoardCommands(program3) {
19436
19032
  }
19437
19033
  return;
19438
19034
  }
19439
- await runBulkMove(issueRefs, o, canonicalStatus, "board move");
19035
+ await runBulkMove(issueRefs, o, canonicalStatus);
19440
19036
  }), [
19441
19037
  'mmi-cli board move "In Progress" 2680',
19442
19038
  "mmi-cli board move Done 2680 2681 2682"
@@ -19469,19 +19065,6 @@ function registerBoardCommands(program3) {
19469
19065
  return failGraceful(`board set-priority failed: ${e.message}`);
19470
19066
  }
19471
19067
  });
19472
- mutating(board.command("done <issues...>").description("set one or more board items' Status to Done (does not close the GitHub issue; use `gh issue close`)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails")).action(async (issueRefs, o) => {
19473
- if (issueRefs.length === 1) {
19474
- try {
19475
- const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: "Done", repo: o.repo, allowPartial: o.allowPartial });
19476
- if (o.json) return console.log(JSON.stringify(result));
19477
- console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> Done`);
19478
- } catch (e) {
19479
- return failGraceful(`board done failed: ${e.message}`);
19480
- }
19481
- return;
19482
- }
19483
- await runBulkMove(issueRefs, o, "Done", "board done");
19484
- });
19485
19068
  board.command("unclaim <issue>").description("clear the assignee and reset the board Status (defaults to Todo) \u2014 the inverse of claim").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--to-status <status>", `set Status after unclaim (default Todo; one of ${BOARD_STATUSES.join(", ")})`).option("--allow-partial", "return success JSON if the assignee clear or status move fails").action(async (issueRef, o) => {
19486
19069
  const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
19487
19070
  try {
@@ -19986,7 +19569,7 @@ async function remoteBranchExists2(branch, options = {}) {
19986
19569
  }
19987
19570
  var COMPOSE_TIMEOUT_MS = 12e4;
19988
19571
  function spawnDeferredGcSweep() {
19989
- spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19572
+ spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19990
19573
  }
19991
19574
  async function createDeferredWorktreeStore() {
19992
19575
  try {
@@ -21074,7 +20657,7 @@ function buildPluginGuardDecision(i) {
21074
20657
  if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
21075
20658
  return { state: "healthy" };
21076
20659
  }
21077
- function buildGuardSessionStartLine(state, opts = {}) {
20660
+ function buildPluginGuardLine(state, opts = {}) {
21078
20661
  if (state === "healthy" || state === "not-org") return { exitCode: 0 };
21079
20662
  const recovery = opts.recovery ?? "mmi-cli plugin heal";
21080
20663
  const restartHint = opts.restartHint ?? "restart your agent host / reload plugins";
@@ -21281,8 +20864,7 @@ async function applyPluginHeal(surface, log, opts) {
21281
20864
  }
21282
20865
  return true;
21283
20866
  }
21284
- async function runGuard(opts = {}, readOrigin) {
21285
- void opts;
20867
+ async function runGuard(readOrigin) {
21286
20868
  try {
21287
20869
  const surface = detectSurface(process.env);
21288
20870
  if (surfaceToken(surface) !== "claude") {
@@ -21292,7 +20874,7 @@ async function runGuard(opts = {}, readOrigin) {
21292
20874
  const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
21293
20875
  const input = snapshotPluginGuardInput(surface, isOrgRepo);
21294
20876
  const { state } = buildPluginGuardDecision(input);
21295
- const { line, exitCode } = buildGuardSessionStartLine(state);
20877
+ const { line, exitCode } = buildPluginGuardLine(state);
21296
20878
  if (line) console.error(line);
21297
20879
  process.exitCode = exitCode;
21298
20880
  } catch {
@@ -21448,7 +21030,7 @@ function formatDeployStatus(r) {
21448
21030
  }
21449
21031
 
21450
21032
  // src/deploy-commands.ts
21451
- var STAGES2 = ["dev", "rc", "main"];
21033
+ var STAGES = ["dev", "rc", "main"];
21452
21034
  async function probeHttpBounded(url, timeoutMs = 5e3) {
21453
21035
  const controller = new AbortController();
21454
21036
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -21465,7 +21047,7 @@ async function probeHttpBounded(url, timeoutMs = 5e3) {
21465
21047
  function registerDeployCommands(program3) {
21466
21048
  const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
21467
21049
  deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
21468
- if (!STAGES2.includes(stage)) {
21050
+ if (!STAGES.includes(stage)) {
21469
21051
  return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
21470
21052
  }
21471
21053
  try {
@@ -23026,7 +22608,7 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
23026
22608
  console.log("mmi-cli org rules gitignore: up to date");
23027
22609
  }
23028
22610
  });
23029
- program2.command("commands").description("print the grouped agentic-coding map; use --all for operational depth").option("--all", "include operational, internal, compatibility, and delegated depth").option("--json", "machine-readable command manifest").option("--primary", "with --json, emit only the compact primary tree and index").action((o) => {
22611
+ program2.command("commands").description("print the grouped agentic-coding map; use --all for operational depth").option("--all", "include operational and internal depth").option("--json", "machine-readable command manifest").option("--primary", "with --json, emit only the compact primary tree and index").action((o) => {
23030
22612
  const manifest = buildCommandManifest(program2);
23031
22613
  if (o.json) {
23032
22614
  consoleIo.log(JSON.stringify(o.primary ? primaryCommandManifest(manifest) : manifest, null, 2));
@@ -23468,7 +23050,7 @@ async function reportWrite(label, res) {
23468
23050
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
23469
23051
  }
23470
23052
  var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
23471
- tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, rc-only retire, read-only verify-secrets) for a tenant via the central tenant-control.yml workflow; project-admin dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets watch by default)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
23053
+ tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; project-admin own dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker watch by default)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
23472
23054
  try {
23473
23055
  const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch });
23474
23056
  if (!o.json && action === "verify-secrets" && result.secrets) {
@@ -23476,6 +23058,10 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
23476
23058
  const { lines, failure } = renderVerifySecrets(body);
23477
23059
  for (const line of lines) printLine(line);
23478
23060
  if (failure) return failGraceful(`runtime tenant control ${stage} verify-secrets: ${failure}`);
23061
+ } else if (!o.json && action === "verify-broker" && result.broker) {
23062
+ const { lines, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
23063
+ for (const line of lines) printLine(line);
23064
+ if (failure) return failGraceful(`runtime tenant control ${stage} verify-broker: ${failure}`);
23479
23065
  } else {
23480
23066
  printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
23481
23067
  }
@@ -23486,6 +23072,15 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
23486
23072
  return failGraceful(`runtime tenant control: ${e.message}`);
23487
23073
  }
23488
23074
  });
23075
+ tenant.command("reconcile <owner/repo> <stage>").description("re-render this tenant stage's generated box assets from registry truth; project-admin own dev/rc, master main; watches by default").option("--watch", "wait for the tenant-reconcile.yml run (default)").option("--no-watch", "return after dispatch; do not redeploy until the reconcile run succeeds").option("--json", "machine-readable output").action(async (repo, stage, o) => {
23076
+ try {
23077
+ const result = await runTenantReconcile(trainApplyDeps(), { repo, stage, watch: o.watch });
23078
+ printLine(o.json ? JSON.stringify(result, null, 2) : `${result.note}${result.runUrl ? ` \u2014 ${result.runUrl}` : ""}`);
23079
+ if (result.conclusion === "failure" || !result.dispatched) process.exitCode = 1;
23080
+ } catch (e) {
23081
+ return failGraceful(`runtime tenant reconcile: ${e.message}`);
23082
+ }
23083
+ });
23489
23084
  tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints").action(async (repo, stage) => {
23490
23085
  if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
23491
23086
  const cfg = await loadConfig();
@@ -23527,14 +23122,6 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
23527
23122
  return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
23528
23123
  }
23529
23124
  });
23530
- async function resolveDnsBounded(host, timeoutMs = 3e3) {
23531
- const { lookup } = await import("node:dns/promises");
23532
- const probe = lookup(host).then(() => true).catch((e) => dnsErrorToResolution(e?.code));
23533
- const timeout = new Promise((resolve5) => {
23534
- setTimeout(() => resolve5(void 0), timeoutMs).unref?.();
23535
- });
23536
- return Promise.race([probe, timeout]);
23537
- }
23538
23125
  async function probeHttpBounded2(url, timeoutMs = 5e3) {
23539
23126
  const controller = new AbortController();
23540
23127
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -23565,68 +23152,7 @@ async function buildTenantRuntimeStatusFor(target, stage, cfg) {
23565
23152
  lastTenantDeployRun: (await fetchLastTenantDeployRun(slug, stage)).run
23566
23153
  });
23567
23154
  }
23568
- async function v2ReadinessDeps(cfg) {
23569
- const reg = registryClientDeps(cfg);
23570
- return {
23571
- resolveDns: (host) => resolveDnsBounded(host),
23572
- // Checked read (#727/#733): the doctor distinguishes a FAILED read (degraded report) from a 404.
23573
- getProject: (slug) => fetchProjectBySlugChecked(slug, reg),
23574
- hasDeployCoords: async (slug, stage) => {
23575
- const status = await fetchDeployStatusBySlug(slug, reg);
23576
- return Boolean(status?.stages[stage]);
23577
- },
23578
- hasDeployState: async (slug, stage) => {
23579
- const status = await fetchDeployStatusBySlug(slug, reg);
23580
- return Boolean(status?.deployState[stage]);
23581
- },
23582
- getDeployFact: async (slug, stage) => {
23583
- const facts = await fetchDeployFactsBySlug(slug, reg);
23584
- const fact = facts?.stages[stage];
23585
- return fact ? { port: fact.port, domain: fact.domain } : null;
23586
- },
23587
- listSecrets: async (targetRepo2) => {
23588
- const apiUrl = cfg.sagaApiUrl;
23589
- if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
23590
- const qs = new URLSearchParams({ repo: targetRepo2 }).toString();
23591
- const res = await fetchWithRetry(fetch, `${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
23592
- method: "GET",
23593
- headers: await hubHeaders()
23594
- }, { attempts: 2, timeoutMs: 5e3 });
23595
- if (!res.ok) throw new Error(`secrets list failed for ${targetRepo2}: HTTP ${res.status}`);
23596
- const body = await res.json();
23597
- return (body.secrets ?? []).map((s) => s.key).filter(Boolean);
23598
- }
23599
- };
23600
- }
23601
- async function updateV2ReadinessIssue(repo, report, healed) {
23602
- const title = "v2 readiness: central deploy + secrets alignment";
23603
- const freshBody = renderReadinessIssueBody("", report, { healed });
23604
- const list = await execFileP2("gh", ["issue", "list", "--repo", repo, "--state", "open", "--search", "v2 readiness in:title", "--json", "number,title", "--limit", "20"], { timeout: 2e4 });
23605
- const issues = JSON.parse(list.stdout || "[]");
23606
- const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
23607
- if (!existing) {
23608
- const create = await bodyArgsViaFile(["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"]);
23609
- try {
23610
- const created = await execFileP2("gh", create.args, { timeout: GH_MUTATION_TIMEOUT_MS });
23611
- const url = created.stdout.trim();
23612
- const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
23613
- return { number, url, action: "created" };
23614
- } finally {
23615
- await create.cleanup();
23616
- }
23617
- }
23618
- const view = await execFileP2("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
23619
- const current = JSON.parse(view.stdout || "{}");
23620
- const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
23621
- const edit = await bodyArgsViaFile(["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody]);
23622
- try {
23623
- await execFileP2("gh", edit.args, { timeout: GH_MUTATION_TIMEOUT_MS });
23624
- } finally {
23625
- await edit.cleanup();
23626
- }
23627
- return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
23628
- }
23629
- var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); attest is project-admin; set is master-only");
23155
+ var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
23630
23156
  async function projectTarget(commandName, explicitTarget) {
23631
23157
  return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
23632
23158
  }
@@ -23686,69 +23212,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
23686
23212
  const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
23687
23213
  console.log(JSON.stringify(payload));
23688
23214
  });
23689
- project.command("doctor [owner/repo]").description("diagnose Hub v2 readiness for a repo without reading product repo files \u2014 appOwnedGaps are advisory templates; clear them with `org project attest`; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--json", "machine-readable output").action(async (repo, _o) => {
23690
- const cfg = await loadConfig();
23691
- let target;
23692
- try {
23693
- target = await projectTarget("org project doctor", repo);
23694
- } catch (e) {
23695
- return fail(e.message);
23696
- }
23697
- const report = await buildV2Doctor(target, await v2ReadinessDeps(cfg));
23698
- console.log(JSON.stringify(report));
23699
- if (!report.ok) process.exitCode = 1;
23700
- });
23701
- project.command("heal [owner/repo]").description("repair Hub-owned v2 readiness state only; never product repo files; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--apply", "apply the Hub-owned registry patch").option("--json", "machine-readable output").action(async (repo, o) => {
23702
- const cfg = await loadConfig();
23703
- let target;
23704
- try {
23705
- target = await projectTarget("org project heal", repo);
23706
- } catch (e) {
23707
- return fail(e.message);
23708
- }
23709
- const reg = registryClientDeps(cfg);
23710
- const out = await runV2Heal(target, { apply: o.apply }, {
23711
- fetchProject: (s) => fetchProjectBySlugChecked(s, reg),
23712
- upsertProject: (s, patch) => upsertProject(s, patch, reg)
23713
- });
23714
- if (out.status === "aborted") return failGraceful(`org project heal: ${out.error}`);
23715
- if (out.status === "planned") {
23716
- console.log(JSON.stringify({ ok: true, slug: out.slug, dryRun: true, patch: out.patch, appOwnedGaps: out.appOwnedGaps }));
23717
- return;
23718
- }
23719
- if (out.status === "noop") {
23720
- console.log(JSON.stringify({ ok: true, slug: out.slug, applied: [], appOwnedGaps: out.appOwnedGaps }));
23721
- return;
23722
- }
23723
- if (out.status === "write-failed") return reportWrite("org project heal", out.write);
23724
- console.log(JSON.stringify({ ok: true, slug: out.slug, applied: out.applied, appOwnedGaps: out.appOwnedGaps, result: out.result }));
23725
- });
23726
- project.command("readiness [owner/repo]").description("update the repo v2 readiness issue with Hub diagnosis and app-owned tasks; defaults to the current repo").option("--update-issue", "create/update the bounded v2 readiness issue section").option("--json", "machine-readable output").action(async (repo, o) => {
23727
- if (!o.updateIssue) return fail("org project readiness: pass --update-issue");
23728
- const cfg = await loadConfig();
23729
- let target;
23730
- try {
23731
- target = await projectTarget("org project readiness", repo);
23732
- } catch (e) {
23733
- return fail(e.message);
23734
- }
23735
- const report = await buildV2Doctor(target, await v2ReadinessDeps(cfg));
23736
- const issue2 = await updateV2ReadinessIssue(target, report, []);
23737
- console.log(JSON.stringify({ ok: true, repo: target, issue: issue2, ready: report.ok }));
23738
- });
23739
- project.command("attest [owner/repo]").description("attest this repo's app-owned v2 readiness work is done (project-admin of the repo or master) \u2014 clears doctor's static appOwnedGaps; re-running overwrites; defaults to the current repo").option("--json", "machine-readable output").action(async (repoOrSlug) => {
23740
- const cfg = await loadConfig();
23741
- let target;
23742
- try {
23743
- target = await projectTarget("org project attest", repoOrSlug);
23744
- } catch (e) {
23745
- return fail(e.message);
23746
- }
23747
- const repo = target.includes("/") ? target : `mutmutco/${slugOf(target)}`;
23748
- const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
23749
- return reportWrite("org project attest", res);
23750
- });
23751
- project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
23215
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
23752
23216
  const cfg = await loadConfig();
23753
23217
  let target;
23754
23218
  try {
@@ -23862,7 +23326,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
23862
23326
  console.log(JSON.stringify(report, null, 2));
23863
23327
  if (!report.rcand.canApply) process.exitCode = 1;
23864
23328
  });
23865
- project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage> Hetzner deploy coords for a tenant (master-only) \u2014 omitted flags keep their stored value; seeds a freshly-bootstrapped tenant; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into; omit to keep the stored value (required only for a NEW hetzner-ssh row \u2014 `mmi-cli runtime box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").option("--substrate <substrate>", "hetzner-ssh; omit to leave the row unchanged").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) \u2014 omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 true = #2662 env passthrough, no .env symlink; omit to leave the row unchanged").option("--force", "skip the #2748 fileless-compose guard (flip noEnvFile=true even if the stage branch compose still declares env_file:)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
23329
+ project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row \u2014 project-admin may set only --no-env-file on their own existing dev/rc row; master may seed/change all coords and main; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into; omit to keep the stored value (required only for a NEW hetzner-ssh row \u2014 `mmi-cli runtime box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").option("--substrate <substrate>", "hetzner-ssh; omit to leave the row unchanged").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) \u2014 omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 own-repo project-admin dev/rc; master main; true = env passthrough with no .env symlink").option("--force", "explicit recovery override: skip fileless-compose verification only after independent proof").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
23866
23330
  const cfg = await loadConfig();
23867
23331
  let target;
23868
23332
  try {
@@ -23876,17 +23340,23 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
23876
23340
  if (v !== "true" && v !== "false") return fail("org project set-deploy: --no-env-file must be true or false");
23877
23341
  noEnvFile = v === "true";
23878
23342
  }
23879
- if (noEnvFile === true) {
23343
+ if (noEnvFile !== void 0) {
23880
23344
  const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
23881
23345
  if (branch) {
23882
23346
  const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
23883
23347
  const sameRepo = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
23884
23348
  if (sameRepo) {
23885
- const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force) });
23886
- if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}`);
23349
+ const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force), noEnvFile });
23350
+ if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
23351
+ ${filelessTransitionGuide(target, o.stage)}`);
23887
23352
  if (verdict.warn) console.error(`org project set-deploy: ${verdict.warn}`);
23353
+ } else if (!o.force) {
23354
+ return fail(
23355
+ `org project set-deploy: cannot verify ${target} ${branch} from this checkout \u2014 refusing to change noEnvFile. Run from the target repo with origin/${branch} fetched.
23356
+ ${filelessTransitionGuide(target, o.stage)}`
23357
+ );
23888
23358
  } else {
23889
- console.error(`org project set-deploy: --no-env-file guard skipped \u2014 run from the ${target} checkout to verify the ${branch} branch compose (#2748).`);
23359
+ console.error(`org project set-deploy: --force: skipping cross-repo fileless-compose verification for ${target} ${branch}.`);
23890
23360
  }
23891
23361
  }
23892
23362
  }
@@ -23906,6 +23376,7 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
23906
23376
  clearAliases: o.clearAliases,
23907
23377
  noEnvFile
23908
23378
  });
23379
+ if (target.includes("/")) body.repo = target;
23909
23380
  } catch (e) {
23910
23381
  return fail(e.message);
23911
23382
  }
@@ -24753,6 +24224,12 @@ function trainApplyDeps() {
24753
24224
  const body = res.body;
24754
24225
  return { ok: false, category: body?.category, error: body?.error ?? res.error };
24755
24226
  },
24227
+ dispatchTenantReconcile: async ({ repo, stage }) => {
24228
+ const res = await tenantReconcile({ repo, stage }, registryClientDeps(await loadConfig()));
24229
+ if (res.ok) return { ok: true };
24230
+ const body = res.body;
24231
+ return { ok: false, category: body?.category, error: body?.error ?? res.error };
24232
+ },
24756
24233
  // Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
24757
24234
  // release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
24758
24235
  // fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
@@ -25026,7 +24503,7 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
25026
24503
  mmiDoctorDeps()
25027
24504
  );
25028
24505
  });
25029
- program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
24506
+ program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk").action(() => runGuard());
25030
24507
  program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
25031
24508
  function directoryBytes(path2) {
25032
24509
  let total = 0;
@@ -25170,18 +24647,9 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
25170
24647
  appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
25171
24648
  });
25172
24649
  installProcessBackstop();
25173
- program2.command("lane", { hidden: true }).action(() => {
25174
- fail("lane orchestration lives in jerv-cli \u2014 try: jerv-cli lane --help");
25175
- });
25176
- program2.command("fusion", { hidden: true }).action(() => {
25177
- fail("fusion orchestration lives in jerv-cli \u2014 try: jerv-cli fusion --help");
25178
- });
25179
24650
  consolidateCommandNamespaces(program2);
25180
24651
  applyCommandTaxonomy(program2);
25181
- var rewrittenArgv = rewriteLegacyArgv(process.argv);
25182
- if (rewrittenArgv.warning) process.stderr.write(`${rewrittenArgv.warning}
25183
- `);
25184
- program2.parseAsync(rewrittenArgv.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
24652
+ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
25185
24653
  // Annotate the CommonJS export names for ESM import in node:
25186
24654
  0 && (module.exports = {
25187
24655
  DEFAULT_PRIORITY,