@mutmutco/cli 3.32.0 → 3.34.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 +334 -943
  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;
@@ -8934,6 +8919,14 @@ function defaultWorktreePath(repoRoot2, branch) {
8934
8919
  const safe = branch.replace(/[/\\]+/g, "-");
8935
8920
  return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot2), "mmi-worktrees", safe);
8936
8921
  }
8922
+ async function primaryCheckoutRootOf(git) {
8923
+ try {
8924
+ const out = (await git(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
8925
+ return out ? (0, import_node_path11.dirname)(out) : void 0;
8926
+ } catch {
8927
+ return void 0;
8928
+ }
8929
+ }
8937
8930
  function resolveWorktreeBase(from, remote) {
8938
8931
  const remotePrefix = `${remote}/`;
8939
8932
  const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
@@ -10386,7 +10379,22 @@ var import_node_util6 = require("node:util");
10386
10379
  var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
10387
10380
  var DOCKER_TIMEOUT_MS = 15e3;
10388
10381
  var EARLY_EXIT_GRACE_MS = 2e3;
10389
- function waitForProcessStability(child2, graceMs = EARLY_EXIT_GRACE_MS) {
10382
+ function earlyExitGraceMs() {
10383
+ if (process.env.NODE_ENV === "test") {
10384
+ const testOverride = Number(process.env.MMI_CLI_TEST_EARLY_EXIT_GRACE_MS);
10385
+ if (Number.isFinite(testOverride) && testOverride >= 25) return testOverride;
10386
+ }
10387
+ return EARLY_EXIT_GRACE_MS;
10388
+ }
10389
+ var HEALTH_POLL_INTERVAL_MS = 1e3;
10390
+ function healthPollIntervalMs() {
10391
+ if (process.env.NODE_ENV === "test") {
10392
+ const testOverride = Number(process.env.MMI_CLI_TEST_HEALTH_POLL_MS);
10393
+ if (Number.isFinite(testOverride) && testOverride >= 10) return testOverride;
10394
+ }
10395
+ return HEALTH_POLL_INTERVAL_MS;
10396
+ }
10397
+ function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
10390
10398
  return new Promise((resolve5, reject) => {
10391
10399
  let settled = false;
10392
10400
  const finish = (fn) => {
@@ -10801,7 +10809,7 @@ function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
10801
10809
  async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
10802
10810
  await killTree(state.pid);
10803
10811
  if (state.teardown?.command.trim()) {
10804
- await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, timeoutMs);
10812
+ await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
10805
10813
  }
10806
10814
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
10807
10815
  (0, import_node_fs14.rmSync)(path2, { force: true });
@@ -10842,7 +10850,7 @@ async function waitForHealth(url, timeoutMs, anyStatus = false) {
10842
10850
  } catch (e) {
10843
10851
  last = e.message;
10844
10852
  }
10845
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
10853
+ await new Promise((resolve5) => setTimeout(resolve5, healthPollIntervalMs()));
10846
10854
  }
10847
10855
  throw new Error(`stage health check timed out for ${url}${last ? ` (${last})` : ""}`);
10848
10856
  }
@@ -11283,29 +11291,6 @@ function resolveIssueTitle(input, deps) {
11283
11291
  noun: "title"
11284
11292
  });
11285
11293
  }
11286
- var NORTH_STAR_BODY_PREFIX = "North Star:";
11287
- var NORTH_STAR_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
11288
- function normalizeNorthStarSlug(raw) {
11289
- const slug = raw.trim();
11290
- if (!slug || !NORTH_STAR_SLUG_RE.test(slug)) {
11291
- throw new Error(`invalid North Star slug "${raw}" \u2014 expected kebab-case like agent-session-workflow`);
11292
- }
11293
- return slug;
11294
- }
11295
- function northStarLabel(slug) {
11296
- return `northstar:${slug}`;
11297
- }
11298
- function appendNorthStarLine(body, slug) {
11299
- const line = `${NORTH_STAR_BODY_PREFIX} ${slug}`;
11300
- const existing = new RegExp(`^${NORTH_STAR_BODY_PREFIX}\\s+\\S+\\s*$`, "m");
11301
- if (existing.test(body)) return body;
11302
- const trimmed = body.replace(/\s+$/, "");
11303
- return trimmed ? `${trimmed}
11304
-
11305
- ${line}
11306
- ` : `${line}
11307
- `;
11308
- }
11309
11294
 
11310
11295
  // src/issue-view-json.ts
11311
11296
  var DEFAULT_ISSUE_VIEW_FIELDS = "number,title,state,url,labels,author,assignees,milestone,body";
@@ -11504,20 +11489,18 @@ for (const [helpGroup, names] of PRIMARY_GROUPS) {
11504
11489
  for (const name of names) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
11505
11490
  }
11506
11491
  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
11492
  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
11493
  var PATH_OVERRIDES = {
11513
11494
  "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" }
11495
+ "secrets find": { category: "admin", discovery: "all-only", help_group: "Operations" },
11496
+ "secrets catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
11497
+ "secrets doctor": { category: "admin", discovery: "all-only", help_group: "Operations" },
11498
+ "secrets org-catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
11499
+ "secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
11500
+ "secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
11516
11501
  };
11517
11502
  var HIDDEN_OPTIONS = {
11518
11503
  "worktree create": ["--base"],
11519
- "org project doctor": ["--v2"],
11520
- "org project heal": ["--v2"],
11521
11504
  "org project set": ["--set"]
11522
11505
  };
11523
11506
  function primaryMetadata(name) {
@@ -11535,7 +11518,6 @@ function primaryMetadata(name) {
11535
11518
  function topLevelMetadata(name) {
11536
11519
  const primary = primaryMetadata(name);
11537
11520
  if (primary) return primary;
11538
- if (name === "lane" || name === "fusion") return PATH_OVERRIDES[name];
11539
11521
  if (!OPERATIONAL_TOP_LEVEL.has(name)) {
11540
11522
  throw new Error(`command taxonomy: unclassified top-level command "${name}"`);
11541
11523
  }
@@ -11605,113 +11587,7 @@ function commandHelpGroupRank(group) {
11605
11587
  return HELP_GROUP_ORDER.get(group) ?? Number.MAX_SAFE_INTEGER;
11606
11588
  }
11607
11589
  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;
11590
+ return metadata.category !== "internal";
11715
11591
  }
11716
11592
 
11717
11593
  // src/command-manifest.ts
@@ -11780,63 +11656,6 @@ function buildCommand(cmd, path2) {
11780
11656
  if (commonMistakes) out.common_mistakes = commonMistakes;
11781
11657
  return out;
11782
11658
  }
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
11659
  function primaryCopy(node, root = false) {
11841
11660
  if (!root && node.discovery !== "primary") return void 0;
11842
11661
  const subcommands = node.subcommands.map((child2) => primaryCopy(child2)).filter((child2) => Boolean(child2));
@@ -11856,7 +11675,6 @@ function collectLeaves(node, acc) {
11856
11675
  }
11857
11676
  function buildCommandManifest(program3) {
11858
11677
  const tree = buildCommand(program3, "");
11859
- if (hasConsolidatedCommandNamespaces(program3)) addVirtualShims(tree);
11860
11678
  const index = [];
11861
11679
  collectLeaves(tree, index);
11862
11680
  const primaryTree = primaryCopy(tree, true);
@@ -11913,10 +11731,71 @@ function formatManifestHuman(manifest, options = {}) {
11913
11731
  }
11914
11732
  render(command, 1);
11915
11733
  }
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.");
11734
+ 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
11735
  return lines.join("\n");
11918
11736
  }
11919
11737
 
11738
+ // src/command-consolidation.ts
11739
+ function child(parent, name) {
11740
+ const found = parent.commands.find((command) => command.name() === name);
11741
+ if (!found) throw new Error(`command consolidation: missing "${parent.name()} ${name}"`);
11742
+ return found;
11743
+ }
11744
+ function detach(parent, command) {
11745
+ const commands = parent.commands;
11746
+ const index = commands.indexOf(command);
11747
+ if (index < 0) throw new Error(`command consolidation: "${command.name()}" is not a child of "${parent.name()}"`);
11748
+ commands.splice(index, 1);
11749
+ return command;
11750
+ }
11751
+ function move(parent, destination, name, nextName = name) {
11752
+ const command = detach(parent, child(parent, name));
11753
+ command.name(nextName);
11754
+ destination.addCommand(command);
11755
+ return command;
11756
+ }
11757
+ function remove(parent, name) {
11758
+ detach(parent, child(parent, name));
11759
+ }
11760
+ function consolidateCommandNamespaces(program3) {
11761
+ const org = program3.command("org").description("organization projects, access, rules, credentials, and schedules");
11762
+ const runtime = program3.command("runtime").description("tenant, deploy, box, and edge operations");
11763
+ const plugin = program3.command("plugin").description("plugin lifecycle and guard operations");
11764
+ move(program3, org, "project");
11765
+ move(program3, org, "oauth");
11766
+ move(program3, org, "access");
11767
+ move(program3, org, "schedules");
11768
+ const config = org.command("config").description("organization configuration");
11769
+ const registry2 = child(program3, "registry");
11770
+ move(registry2, config, "org", "get");
11771
+ remove(program3, "registry");
11772
+ const orgRules = org.command("rules").description("organization-managed repository rules");
11773
+ const rules2 = child(program3, "rules");
11774
+ move(rules2, orgRules, "gitignore");
11775
+ remove(program3, "rules");
11776
+ move(program3, runtime, "tenant");
11777
+ const runtimeDeploy = runtime.command("deploy").description("runtime deployment status");
11778
+ const deploy = child(program3, "deploy");
11779
+ move(deploy, runtimeDeploy, "status");
11780
+ remove(program3, "deploy");
11781
+ move(program3, runtime, "box");
11782
+ move(program3, runtime, "edge");
11783
+ move(program3, plugin, "guard");
11784
+ move(program3, plugin, "plugin-heal", "heal");
11785
+ move(program3, plugin, "plugin-prune", "prune");
11786
+ move(program3, plugin, "session-start");
11787
+ const docs2 = child(program3, "docs");
11788
+ move(program3, docs2, "docs-audit", "audit");
11789
+ const train = child(program3, "train");
11790
+ const fullTrack2 = child(program3, "full-track");
11791
+ move(fullTrack2, train, "readiness");
11792
+ remove(program3, "full-track");
11793
+ const worktree2 = child(program3, "worktree");
11794
+ move(program3, worktree2, "gc");
11795
+ const stage = child(program3, "stage");
11796
+ move(program3, stage, "port-range");
11797
+ }
11798
+
11920
11799
  // src/version-lag.ts
11921
11800
  var VERSION_LABEL = "installed plugin/adapter cache freshness";
11922
11801
  var VERSION_FIX = "update the MMI plugin via /plugin; standalone npm CLI: npm install -g @mutmutco/cli@latest";
@@ -12449,6 +12328,14 @@ function parseVerifySecrets(stdout) {
12449
12328
  }
12450
12329
  return out;
12451
12330
  }
12331
+ function parseVerifyBroker(stdout) {
12332
+ const out = [];
12333
+ for (const line of stdout.split("\n")) {
12334
+ const match = /^(\S+):\s*(reachable|missing|denied|error)\b/.exec(line.trim());
12335
+ if (match) out.push({ key: match[1], status: match[2] });
12336
+ }
12337
+ return out;
12338
+ }
12452
12339
 
12453
12340
  // src/train-apply.ts
12454
12341
  function resolveDeployModel2(meta, repo) {
@@ -12732,7 +12619,7 @@ function classifyProjectGetFailure(text) {
12732
12619
  }
12733
12620
  async function loadProjectMeta(deps, ctx) {
12734
12621
  try {
12735
- const out = await deps.runSelf(["project", "get", ctx.repo]);
12622
+ const out = await deps.runSelf(["org", "project", "get", ctx.repo]);
12736
12623
  const parsed = JSON.parse(out);
12737
12624
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
12738
12625
  return { status: "read-error", error: "invalid registry META JSON" };
@@ -12851,6 +12738,9 @@ function correlatePublishRun(deps, since, titleIncludes) {
12851
12738
  function correlateControlRun(deps, since, titleIncludes) {
12852
12739
  return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
12853
12740
  }
12741
+ function correlateReconcileRun(deps, since, titleIncludes) {
12742
+ return correlateRun(deps, { workflow: "tenant-reconcile.yml", since, mode: "dispatch", titleIncludes });
12743
+ }
12854
12744
  async function correlateWorkflowRun(deps, args) {
12855
12745
  return correlateRun(deps, { ...args, mode: "workflow" });
12856
12746
  }
@@ -13906,8 +13796,36 @@ async function runTenantRedeploy(deps, options) {
13906
13796
  const d = await dispatchDeploy(deps, ctx, stage, ref, deployModel, watch);
13907
13797
  return { ...ctx, command: "tenant-redeploy", stage, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
13908
13798
  }
13799
+ async function runTenantReconcile(deps, options) {
13800
+ const parts = options.repo.split("/");
13801
+ if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error(`repo must be owner/name, got ${options.repo}`);
13802
+ if (!["dev", "rc", "main"].includes(options.stage)) throw new Error(`stage must be dev, rc, or main, got ${options.stage}`);
13803
+ if (!deps.dispatchTenantReconcile) throw new Error("tenant reconcile dispatch is unavailable");
13804
+ const stage = options.stage;
13805
+ const base = { command: "tenant-reconcile", repo: options.repo, stage };
13806
+ const since = (deps.now ?? Date.now)();
13807
+ const dispatched = await deps.dispatchTenantReconcile({ repo: options.repo, stage });
13808
+ if (!dispatched.ok) {
13809
+ return {
13810
+ ...base,
13811
+ dispatched: false,
13812
+ conclusion: "failure",
13813
+ note: `tenant reconcile rejected: ${dispatched.error ?? "request rejected by the Hub"}`
13814
+ };
13815
+ }
13816
+ const slug = parts[1].toLowerCase();
13817
+ const run = await correlateReconcileRun(deps, since, [slug, stage]);
13818
+ const conclusion = options.watch ?? true ? await watchTenantRun(deps, run.runId) : "pending";
13819
+ return {
13820
+ ...base,
13821
+ dispatched: true,
13822
+ ...run,
13823
+ conclusion,
13824
+ 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"
13825
+ };
13826
+ }
13909
13827
  function tenantControlWatches(action) {
13910
- return action === "status" || action === "retire" || action === "verify-secrets";
13828
+ return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker";
13911
13829
  }
13912
13830
  async function runTenantControl(deps, options) {
13913
13831
  const { repo, stage, action } = options;
@@ -13931,13 +13849,16 @@ async function runTenantControl(deps, options) {
13931
13849
  if (action === "retire") {
13932
13850
  result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
13933
13851
  }
13934
- if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets")) {
13852
+ if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker")) {
13935
13853
  const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
13936
13854
  if (action === "status") {
13937
13855
  result.serviceState = parseStatusSnippet(output).serviceState;
13938
- } else {
13856
+ } else if (action === "verify-secrets") {
13939
13857
  result.secrets = parseVerifySecrets(output);
13940
13858
  result.secretsRaw = output;
13859
+ } else {
13860
+ result.broker = parseVerifyBroker(output);
13861
+ result.brokerRaw = output;
13941
13862
  }
13942
13863
  }
13943
13864
  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 +13908,26 @@ function renderVerifySecrets(body) {
13987
13908
  return { lines, failure: null };
13988
13909
  }
13989
13910
 
13911
+ // src/tenant-verify-broker.ts
13912
+ function renderVerifyBroker(input) {
13913
+ if (input.broker.length === 0) {
13914
+ const raw = input.raw?.trim();
13915
+ if (raw?.startsWith("no broker-readable runtime secrets declared")) return { lines: [raw], failure: null };
13916
+ return {
13917
+ lines: raw ? raw.split("\n") : ["verify-broker: no per-key verdicts returned"],
13918
+ failure: "verify-broker returned no per-key verdicts, so broker access was not verified"
13919
+ };
13920
+ }
13921
+ const lines = input.broker.map((item) => `${item.key}: ${item.status}`);
13922
+ const reachable = input.broker.filter((item) => item.status === "reachable").length;
13923
+ const bad = input.broker.length - reachable;
13924
+ lines.push(`verify-broker: ${reachable} reachable, ${bad} failed`);
13925
+ return {
13926
+ lines,
13927
+ failure: bad > 0 ? `${bad} of ${input.broker.length} broker read(s) failed; redeploy refreshes an expired runtime token` : null
13928
+ };
13929
+ }
13930
+
13990
13931
  // src/hotfix-coverage.ts
13991
13932
  var import_node_child_process8 = require("node:child_process");
13992
13933
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
@@ -15009,14 +14950,10 @@ function loadAccessTargets(projectsJson) {
15009
14950
  const seen = /* @__PURE__ */ new Set();
15010
14951
  const targets = [];
15011
14952
  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
14953
  for (const repo of project2.repos ?? []) {
15016
14954
  if (seen.has(repo)) continue;
15017
14955
  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";
14956
+ const cls = project2.class === "content" || project2.deployModel === "content" ? "content" : "deployable";
15020
14957
  const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
15021
14958
  targets.push({ repo, class: cls, releaseTrack });
15022
14959
  }
@@ -15096,9 +15033,13 @@ function renderAccessReport(report) {
15096
15033
  var import_node_fs16 = require("node:fs");
15097
15034
  var import_node_path14 = require("node:path");
15098
15035
  var DOCS_INDEX_PATH = "docs/index.md";
15036
+ function isRoutableDocsPath(relPath) {
15037
+ const normalized = relPath.replace(/\\/g, "/");
15038
+ return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
15039
+ }
15099
15040
  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();
15041
+ function plainInline(text) {
15042
+ 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
15043
  }
15103
15044
  function extractDocMeta(relPath, raw) {
15104
15045
  const lines = raw.split(/\r?\n/);
@@ -15126,7 +15067,7 @@ function extractDocMeta(relPath, raw) {
15126
15067
  break;
15127
15068
  }
15128
15069
  if (trimmed.startsWith("#")) continue;
15129
- scope = escapeCell(trimmed);
15070
+ scope = plainInline(trimmed);
15130
15071
  break;
15131
15072
  }
15132
15073
  if (!title) {
@@ -15138,16 +15079,16 @@ function extractDocMeta(relPath, raw) {
15138
15079
  function renderDocsIndex(entries) {
15139
15080
  const lines = [
15140
15081
  GENERATED_HEADER,
15082
+ "",
15141
15083
  "# Documentation index",
15142
15084
  "",
15143
15085
  "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
15144
15086
  "can never diverge from the records it lists.",
15145
- "",
15146
- "| Document | Scope |",
15147
- "| --- | --- |"
15087
+ ""
15148
15088
  ];
15149
15089
  for (const entry of entries) {
15150
- lines.push(`| [${escapeCell(entry.title)}](${entry.path}) | ${entry.scope} |`);
15090
+ const link = `[${plainInline(entry.title)}](${entry.path})`;
15091
+ lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
15151
15092
  }
15152
15093
  return lines.join("\n") + "\n";
15153
15094
  }
@@ -15183,7 +15124,7 @@ function createDocsIndexDeps(repoRoot2) {
15183
15124
  const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
15184
15125
  const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
15185
15126
  return {
15186
- listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter((p) => p !== "index.md").sort() : [],
15127
+ listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
15187
15128
  readDoc: (relPath) => (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
15188
15129
  readIndex: () => (0, import_node_fs16.existsSync)(indexPath) ? (0, import_node_fs16.readFileSync)(indexPath, "utf8") : null,
15189
15130
  writeIndex: (content) => (0, import_node_fs16.writeFileSync)(indexPath, content, "utf8")
@@ -15290,427 +15231,6 @@ function docsAuditStatus(fetch2, opts) {
15290
15231
  return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
15291
15232
  }
15292
15233
 
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
15234
  // src/oauth.ts
15715
15235
  var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
15716
15236
  var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
@@ -15718,7 +15238,7 @@ var ENV_PREFIXES = ["", "dev", "rc"];
15718
15238
  var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
15719
15239
  var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
15720
15240
  var uniq = (xs) => [...new Set(xs)];
15721
- function defaultSubdomain2(slug) {
15241
+ function defaultSubdomain(slug) {
15722
15242
  const i = slug.indexOf("-");
15723
15243
  return i === -1 ? slug : slug.slice(i + 1);
15724
15244
  }
@@ -15771,7 +15291,7 @@ function parseOauthConfig(mmiConfig, slug) {
15771
15291
  throw new Error("oauth must be an object when configured");
15772
15292
  }
15773
15293
  const raw = rawUnknown;
15774
- const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
15294
+ const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain(slug)];
15775
15295
  const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
15776
15296
  const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
15777
15297
  if (!callbackPath.startsWith("/")) {
@@ -15782,7 +15302,7 @@ function parseOauthConfig(mmiConfig, slug) {
15782
15302
  }
15783
15303
  const meta = mmiConfig ?? {};
15784
15304
  const rawFofuSub = raw.fofuSubdomain;
15785
- const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
15305
+ const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain(slug) : void 0;
15786
15306
  return { subdomains, domains, callbackPath, fofuSubdomain };
15787
15307
  }
15788
15308
  function probeRedirectUri(callbackPath, port = 9123) {
@@ -15810,6 +15330,26 @@ var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
15810
15330
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
15811
15331
  var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
15812
15332
  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)$/;
15333
+ function previewRegistryMetaMerge(existing, patch) {
15334
+ const out = { ...existing ?? {} };
15335
+ for (const [key, value] of Object.entries(patch)) {
15336
+ if (value === null) delete out[key];
15337
+ else out[key] = value;
15338
+ }
15339
+ return out;
15340
+ }
15341
+ function boardLinkWriteError(patch, existing) {
15342
+ const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
15343
+ const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
15344
+ if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
15345
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
15346
+ }
15347
+ const preview = previewRegistryMetaMerge(existing, patch);
15348
+ if (patch.projectNumber === null && preview.projectId && preview.projectNumber == null) {
15349
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
15350
+ }
15351
+ return null;
15352
+ }
15813
15353
  function parseSecretsCatalogVar(raw) {
15814
15354
  let parsed;
15815
15355
  try {
@@ -16321,17 +15861,44 @@ function buildSetDeployPatch(_slug, input) {
16321
15861
  };
16322
15862
  }
16323
15863
  var DEPLOY_STAGE_BRANCH = { dev: "development", rc: "rc", main: "main" };
15864
+ function filelessTransitionGuide(repo, stage) {
15865
+ 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.";
15866
+ return [
15867
+ `Fileless transition for ${repo} (${stage}):`,
15868
+ ` ${authority}`,
15869
+ ` mmi-cli org access role ${repo}`,
15870
+ ` mmi-cli secrets list --repo ${repo} # names/capabilities only`,
15871
+ ` mmi-cli secrets preflight --repo ${repo} --stage ${stage}`,
15872
+ ` mmi-cli org project set-deploy ${repo} --stage ${stage} --no-env-file true`,
15873
+ ` mmi-cli runtime tenant reconcile ${repo} ${stage} --watch`,
15874
+ ` mmi-cli runtime tenant redeploy ${repo} ${stage} --watch`,
15875
+ ` mmi-cli runtime tenant control ${repo} ${stage} verify-secrets --watch`,
15876
+ ` mmi-cli runtime tenant control ${repo} ${stage} verify-broker --watch # broker consumers`,
15877
+ " Missing/undeclared key (project-admin own project):",
15878
+ ` mmi-cli secrets declare <KEY> --repo ${repo} --purpose "<purpose>" --group "<group>" --consumers runtime --stages ${stage}`,
15879
+ ` mmi-cli secrets set ${stage}/<KEY> --repo ${repo} # value from secure prompt/stdin; never argv/stdout`,
15880
+ ` mmi-cli secrets request <KEY> --repo ${repo} --reason "wire requiredRuntimeSecrets for ${stage}" # only if env injection needs master wiring`,
15881
+ " Runbook: https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/tenant-fileless-migration.md"
15882
+ ].join("\n");
15883
+ }
16324
15884
  function composeDeclaresEnvFile(composeText) {
16325
15885
  return composeText.split(/\r?\n/).some((line) => /^\s*env_file\s*:/.test(line));
16326
15886
  }
16327
15887
  function composeHasRuntimeEnvLabel(composeText) {
16328
- return /mmi\.runtime-env/.test(composeText);
15888
+ return composeText.split(/\r?\n/).some((raw) => {
15889
+ const line = raw.replace(/(^|\s)#.*$/, "$1");
15890
+ return /(^|[\s"'])mmi\.runtime-env\s*:\s*["']?true["']?\s*$/.test(line) || /mmi\.runtime-env\s*=\s*["']?true["']?\s*$/.test(line);
15891
+ });
16329
15892
  }
16330
15893
  function evaluateFilelessComposeGuard(input) {
16331
15894
  if (input.force) return { ok: true, warn: `--force: skipping the fileless-compose guard for ${input.branch}` };
16332
15895
  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)` };
15896
+ 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.` };
15897
+ }
15898
+ if (input.noEnvFile === false && composeHasRuntimeEnvLabel(input.composeText)) {
15899
+ 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.` };
16334
15900
  }
15901
+ if (input.noEnvFile === false) return { ok: true };
16335
15902
  if (composeDeclaresEnvFile(input.composeText)) {
16336
15903
  return {
16337
15904
  ok: false,
@@ -16393,15 +15960,16 @@ function evaluatePromotionFilelessGuard(input) {
16393
15960
  return {
16394
15961
  ok: false,
16395
15962
  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)`
15963
+ ${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
15964
+ Override only after independent verification: --skip-compose-guard`
16399
15965
  };
16400
15966
  }
16401
15967
  if (noEnvFile && composeDeclaresEnvFile(input.composeText)) {
16402
15968
  return {
16403
15969
  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).`
15970
+ 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}.
15971
+ ${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
15972
+ Rollback only after independent verification: set-deploy --no-env-file false; preflight override: --skip-compose-guard.`
16405
15973
  };
16406
15974
  }
16407
15975
  return { ok: true };
@@ -16490,6 +16058,27 @@ var import_node_fs17 = require("node:fs");
16490
16058
  var import_node_path15 = require("node:path");
16491
16059
  var import_node_os5 = require("node:os");
16492
16060
 
16061
+ // src/project-runtime.ts
16062
+ function hasRuntimeSecretContract(contract) {
16063
+ if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
16064
+ return ["dev", "rc", "main"].some((stage) => Array.isArray(contract[stage]));
16065
+ }
16066
+ function projectRequiresGoogleOAuth(meta, model) {
16067
+ if (!meta || model === "content" || model === "none" || meta.deployModel === "content" || meta.deployModel === "none" || meta.class === "content") return false;
16068
+ const projectType = resolveProjectType(meta, meta.repos?.[0] ?? "");
16069
+ return projectType === "web-app" && Boolean(meta.oauth && typeof meta.oauth === "object");
16070
+ }
16071
+ function edgeDomainsByStage(meta) {
16072
+ const edgeDomains = meta?.edgeDomains;
16073
+ if (!edgeDomains || typeof edgeDomains !== "object" || Array.isArray(edgeDomains)) return {};
16074
+ const out = {};
16075
+ for (const stage of ["dev", "rc", "main"]) {
16076
+ const value = edgeDomains[stage];
16077
+ if (typeof value === "string" && value.trim()) out[stage] = value.trim();
16078
+ }
16079
+ return out;
16080
+ }
16081
+
16493
16082
  // src/secrets-diff.ts
16494
16083
  var TIMEOUT_MS2 = 8e3;
16495
16084
  var OWNER3 = "mutmutco";
@@ -16661,13 +16250,13 @@ async function withSecrets(run) {
16661
16250
  await run(makeSecretsDeps(cfg));
16662
16251
  }
16663
16252
  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");
16253
+ 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
16254
  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
16255
  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) => {
16256
+ 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
16257
  if (!await secretsFind(d, intent, o)) process.exitCode = 1;
16669
16258
  }));
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)));
16259
+ 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
16260
  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
16261
  if (!await secretsDoctor(d, o)) process.exitCode = 1;
16673
16262
  }));
@@ -16736,7 +16325,7 @@ function registerSecretsCommands(program3) {
16736
16325
  const facts = await fetchDeployFactsBySlug(slug, regDeps);
16737
16326
  const fact = facts?.stages?.[stage] ?? null;
16738
16327
  const composeText = sameRepo ? await readStageBranchCompose(branch) : null;
16739
- const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false });
16328
+ const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false, repo });
16740
16329
  if (!verdict.ok) {
16741
16330
  d.err(`secrets preflight: ${verdict.reason}`);
16742
16331
  filelessOk = false;
@@ -16778,7 +16367,7 @@ function registerSecretsCommands(program3) {
16778
16367
  if (!ok) process.exitCode = 1;
16779
16368
  });
16780
16369
  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) => {
16370
+ 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
16371
  const stages = ["dev", "rc", "main"];
16783
16372
  if (!stages.includes(o.from) || !stages.includes(o.to)) {
16784
16373
  return fail("secrets copy: --from and --to must be dev, rc, or main");
@@ -16818,13 +16407,13 @@ function registerSecretsCommands(program3) {
16818
16407
  );
16819
16408
  if (!ok) process.exitCode = 1;
16820
16409
  }));
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) => {
16410
+ 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)));
16411
+ 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
16412
  const ok = await secretsUse(d, key, { ...o, command });
16824
16413
  if (ok === false) process.exitCode = 1;
16825
16414
  }));
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, {})));
16415
+ 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, {})));
16416
+ 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
16417
  }
16829
16418
 
16830
16419
  // src/app-actor.ts
@@ -17216,9 +16805,9 @@ function extractWorkflowCrons(yamlText) {
17216
16805
  function workflowEntry(repo, workflowPath, yamlText) {
17217
16806
  const crons = extractWorkflowCrons(yamlText);
17218
16807
  if (!crons.length) return null;
17219
- const basename4 = workflowPath.split("/").pop() ?? workflowPath;
16808
+ const basename3 = workflowPath.split("/").pop() ?? workflowPath;
17220
16809
  return {
17221
- name: `${repo}/${basename4.replace(/\.ya?ml$/, "")}`,
16810
+ name: `${repo}/${basename3.replace(/\.ya?ml$/, "")}`,
17222
16811
  cadence: crons.join(" + "),
17223
16812
  executor: "github-actions",
17224
16813
  llm: llmFromHeader(yamlText),
@@ -18603,7 +18192,7 @@ function registerBootstrapCommands(program3) {
18603
18192
  else console.log(renderOrgRulesetDriftReport(plan));
18604
18193
  if (plan.action !== "noop") process.exitCode = 1;
18605
18194
  });
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) => {
18195
+ 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
18196
  const o = {
18608
18197
  class: rawValue("--class", "deployable"),
18609
18198
  projectType: rawValue("--project-type", ""),
@@ -18919,12 +18508,12 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
18919
18508
  function shellFor(platform2 = process.platform) {
18920
18509
  return platform2 === "win32" ? "powershell" : "bash";
18921
18510
  }
18922
- function isCentralContainerModel2(model) {
18511
+ function isCentralContainerModel(model) {
18923
18512
  return model === "tenant-container" || model === "solo-container";
18924
18513
  }
18925
18514
  function deriveStageGap(inputs) {
18926
18515
  const missing = [];
18927
- if (!isCentralContainerModel2(inputs.deployModel)) {
18516
+ if (!isCentralContainerModel(inputs.deployModel)) {
18928
18517
  return `local stage default applies to central-container repos only (tenant-container/solo-container; registry deployModel = ${inputs.deployModel ?? "unset"})`;
18929
18518
  }
18930
18519
  if (!inputs.hasCompose) missing.push("docker-compose.yml");
@@ -19396,7 +18985,7 @@ function registerBoardCommands(program3) {
19396
18985
  expected: [...BOARD_STATUSES]
19397
18986
  });
19398
18987
  }
19399
- async function runBulkMove(issueRefs, o, status, failLabel) {
18988
+ async function runBulkMove(issueRefs, o, status) {
19400
18989
  try {
19401
18990
  const bulk = await moveBoardIssues({
19402
18991
  config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
@@ -19414,7 +19003,7 @@ function registerBoardCommands(program3) {
19414
19003
  }
19415
19004
  if (bulk.failed > 0) process.exitCode = 1;
19416
19005
  } catch (e) {
19417
- return failGraceful(`${failLabel} failed: ${e.message}`);
19006
+ return failGraceful(`board move failed: ${e.message}`);
19418
19007
  }
19419
19008
  }
19420
19009
  withExamples(mutating(
@@ -19436,7 +19025,7 @@ function registerBoardCommands(program3) {
19436
19025
  }
19437
19026
  return;
19438
19027
  }
19439
- await runBulkMove(issueRefs, o, canonicalStatus, "board move");
19028
+ await runBulkMove(issueRefs, o, canonicalStatus);
19440
19029
  }), [
19441
19030
  'mmi-cli board move "In Progress" 2680',
19442
19031
  "mmi-cli board move Done 2680 2681 2682"
@@ -19469,19 +19058,6 @@ function registerBoardCommands(program3) {
19469
19058
  return failGraceful(`board set-priority failed: ${e.message}`);
19470
19059
  }
19471
19060
  });
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
19061
  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
19062
  const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
19487
19063
  try {
@@ -19986,7 +19562,7 @@ async function remoteBranchExists2(branch, options = {}) {
19986
19562
  }
19987
19563
  var COMPOSE_TIMEOUT_MS = 12e4;
19988
19564
  function spawnDeferredGcSweep() {
19989
- spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19565
+ spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19990
19566
  }
19991
19567
  async function createDeferredWorktreeStore() {
19992
19568
  try {
@@ -20232,7 +19808,6 @@ function classifyStaleLeaks(input) {
20232
19808
  const protectedBranches = input.protectedBranches ?? PROTECTED_BRANCHES2;
20233
19809
  const leaks = [];
20234
19810
  const worktreeBranches2 = new Set(input.worktrees.filter((w) => !w.primary).map((w) => w.branch));
20235
- const worktreeByBranch = new Map(input.worktrees.map((w) => [w.branch, w.path]));
20236
19811
  const stageByPath = new Map(input.stages.map((s) => [s.path, s.port]));
20237
19812
  for (const wt of input.worktrees) {
20238
19813
  if (wt.primary) continue;
@@ -20483,7 +20058,6 @@ async function editIssue(client, options, deps = {}) {
20483
20058
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
20484
20059
  const patch = {};
20485
20060
  let bodyChanged = false;
20486
- let northStarSlug;
20487
20061
  if (options.title !== void 0) patch.title = options.title;
20488
20062
  if (options.body !== void 0 || options.bodyFile !== void 0) {
20489
20063
  let body;
@@ -20493,29 +20067,14 @@ async function editIssue(client, options, deps = {}) {
20493
20067
  } else {
20494
20068
  body = options.body ?? "";
20495
20069
  }
20496
- if (options.northStar) {
20497
- northStarSlug = normalizeNorthStarSlug(options.northStar);
20498
- body = appendNorthStarLine(body, northStarSlug);
20499
- }
20500
20070
  patch.body = body;
20501
20071
  bodyChanged = true;
20502
- } else if (options.northStar) {
20503
- northStarSlug = normalizeNorthStarSlug(options.northStar);
20504
- const current = await client.rest("GET", `repos/${repo}/issues/${parsed.number}`);
20505
- patch.body = appendNorthStarLine(current?.body ?? "", northStarSlug);
20506
- bodyChanged = true;
20507
20072
  }
20508
20073
  if (patch.title !== void 0 || patch.body !== void 0) {
20509
20074
  await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, { body: patch });
20510
20075
  }
20511
20076
  const labelsAdded = [];
20512
20077
  const labelsRemoved = [];
20513
- if (options.northStar && northStarSlug) {
20514
- const nsLabel = northStarLabel(northStarSlug);
20515
- if (!options.addLabel?.includes(nsLabel)) {
20516
- (options.addLabel ??= []).push(nsLabel);
20517
- }
20518
- }
20519
20078
  if (options.addLabel?.length) {
20520
20079
  await client.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, { body: { labels: options.addLabel } });
20521
20080
  labelsAdded.push(...options.addLabel);
@@ -20545,8 +20104,7 @@ async function editIssue(client, options, deps = {}) {
20545
20104
  bodyChanged,
20546
20105
  labelsAdded,
20547
20106
  labelsRemoved,
20548
- ...parentResult ? { parent: parentResult } : {},
20549
- ...northStarSlug ? { northStarSlug } : {}
20107
+ ...parentResult ? { parent: parentResult } : {}
20550
20108
  };
20551
20109
  }
20552
20110
  async function closeIssue(client, options, deps = {}) {
@@ -20726,7 +20284,7 @@ ${spec.body ?? ""}`;
20726
20284
  const hash = (0, import_node_crypto5.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
20727
20285
  return `${batchKey}:${hash}`;
20728
20286
  }
20729
- var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "northStar", "parent", "repo"]);
20287
+ var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "parent", "repo"]);
20730
20288
  function validateBatchSpecs(specs) {
20731
20289
  const errors = [];
20732
20290
  const validated = [];
@@ -20766,28 +20324,6 @@ function validateBatchSpecs(specs) {
20766
20324
  }
20767
20325
  return { ok: errors.length === 0, errors, validated };
20768
20326
  }
20769
- var NORTH_STAR_LABEL_COLOR = "5319e7";
20770
- function northStarLabelsForBatch(specs) {
20771
- const labels = /* @__PURE__ */ new Set();
20772
- for (const spec of specs) {
20773
- if (spec.northStar) labels.add(northStarLabel(normalizeNorthStarSlug(spec.northStar)));
20774
- for (const l of spec.labels ?? []) if (l.startsWith("northstar:")) labels.add(l);
20775
- }
20776
- return [...labels].sort((a, b) => a.localeCompare(b));
20777
- }
20778
- async function ensureNorthStarLabels(client, repo, specs) {
20779
- const minted = [];
20780
- for (const name of northStarLabelsForBatch(specs)) {
20781
- try {
20782
- await client.rest("POST", `repos/${repo}/labels`, {
20783
- body: { name, color: NORTH_STAR_LABEL_COLOR, description: `North Star: ${name.slice("northstar:".length)}` }
20784
- });
20785
- minted.push(name);
20786
- } catch {
20787
- }
20788
- }
20789
- return minted;
20790
- }
20791
20327
  async function createIssuesBatch(specs, options, deps = {}) {
20792
20328
  const client = deps.client ?? defaultGitHubClient();
20793
20329
  const validation = validateBatchSpecs(specs);
@@ -20801,14 +20337,6 @@ ${lines}`);
20801
20337
  throw new Error("could not resolve repo \u2014 pass --repo owner/repo or set repo per row");
20802
20338
  }
20803
20339
  const rowRepo = (spec) => spec.repo ?? defaultRepo;
20804
- const byRepo = /* @__PURE__ */ new Map();
20805
- for (const v of validation.validated) {
20806
- const r = rowRepo(v.spec);
20807
- byRepo.set(r, [...byRepo.get(r) ?? [], v.spec]);
20808
- }
20809
- for (const [r, specsForRepo] of byRepo) {
20810
- await ensureNorthStarLabels(client, r, specsForRepo);
20811
- }
20812
20340
  const created = [];
20813
20341
  const failures = [];
20814
20342
  for (const entry of validation.validated) {
@@ -20823,12 +20351,6 @@ ${lines}`);
20823
20351
  }
20824
20352
  }
20825
20353
  let body = spec.body ?? "";
20826
- if (spec.northStar) {
20827
- const slug = normalizeNorthStarSlug(spec.northStar);
20828
- body = appendNorthStarLine(body, slug);
20829
- const nsLabel = northStarLabel(slug);
20830
- if (!spec.labels?.includes(nsLabel)) (spec.labels ??= []).push(nsLabel);
20831
- }
20832
20354
  if (rowKey) {
20833
20355
  body = appendIdempotencyMarker(body, rowKey);
20834
20356
  }
@@ -20882,7 +20404,7 @@ function registerIssueLifecycleCommands(program3) {
20882
20404
  const issue2 = program3.commands.find((c) => c.name() === "issue");
20883
20405
  if (!issue2) return;
20884
20406
  mutating(
20885
- issue2.command("edit <ref>").description("edit an issue title/body/labels, reparent it (--parent), or link a North Star \u2014 reparenting is impossible via raw gh").option("--title <title>", "new title").option("--body <body>", "new body (markdown)").option("--body-file <path|->", "read new body from a UTF-8 file, or stdin with -").option("--add-label <label...>", "label(s) to add (repeatable)").option("--remove-label <label...>", "label(s) to remove (repeatable)").option("--parent <ref>", "reparent: link under this parent (#123, owner/repo#123, or URL)").option("--north-star <slug>", "append a North Star body line + northstar:<slug> label").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
20407
+ issue2.command("edit <ref>").description("edit an issue title/body/labels or reparent it (--parent) \u2014 reparenting is impossible via raw gh").option("--title <title>", "new title").option("--body <body>", "new body (markdown)").option("--body-file <path|->", "read new body from a UTF-8 file, or stdin with -").option("--add-label <label...>", "label(s) to add (repeatable)").option("--remove-label <label...>", "label(s) to remove (repeatable)").option("--parent <ref>", "reparent: link under this parent (#123, owner/repo#123, or URL)").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
20886
20408
  (opts, args) => ({ command: "issue edit", ref: args[0], fields: Object.keys(opts).filter((k) => k !== "repo" && opts[k] !== void 0) })
20887
20409
  ).action(async (ref, o) => {
20888
20410
  try {
@@ -20895,8 +20417,7 @@ function registerIssueLifecycleCommands(program3) {
20895
20417
  bodyFile: o.bodyFile,
20896
20418
  addLabel: o.addLabel,
20897
20419
  removeLabel: o.removeLabel,
20898
- parent: o.parent,
20899
- northStar: o.northStar
20420
+ parent: o.parent
20900
20421
  });
20901
20422
  console.log(JSON.stringify(result));
20902
20423
  } catch (e) {
@@ -21074,7 +20595,7 @@ function buildPluginGuardDecision(i) {
21074
20595
  if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
21075
20596
  return { state: "healthy" };
21076
20597
  }
21077
- function buildGuardSessionStartLine(state, opts = {}) {
20598
+ function buildPluginGuardLine(state, opts = {}) {
21078
20599
  if (state === "healthy" || state === "not-org") return { exitCode: 0 };
21079
20600
  const recovery = opts.recovery ?? "mmi-cli plugin heal";
21080
20601
  const restartHint = opts.restartHint ?? "restart your agent host / reload plugins";
@@ -21281,8 +20802,7 @@ async function applyPluginHeal(surface, log, opts) {
21281
20802
  }
21282
20803
  return true;
21283
20804
  }
21284
- async function runGuard(opts = {}, readOrigin) {
21285
- void opts;
20805
+ async function runGuard(readOrigin) {
21286
20806
  try {
21287
20807
  const surface = detectSurface(process.env);
21288
20808
  if (surfaceToken(surface) !== "claude") {
@@ -21292,7 +20812,7 @@ async function runGuard(opts = {}, readOrigin) {
21292
20812
  const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
21293
20813
  const input = snapshotPluginGuardInput(surface, isOrgRepo);
21294
20814
  const { state } = buildPluginGuardDecision(input);
21295
- const { line, exitCode } = buildGuardSessionStartLine(state);
20815
+ const { line, exitCode } = buildPluginGuardLine(state);
21296
20816
  if (line) console.error(line);
21297
20817
  process.exitCode = exitCode;
21298
20818
  } catch {
@@ -21448,7 +20968,7 @@ function formatDeployStatus(r) {
21448
20968
  }
21449
20969
 
21450
20970
  // src/deploy-commands.ts
21451
- var STAGES2 = ["dev", "rc", "main"];
20971
+ var STAGES = ["dev", "rc", "main"];
21452
20972
  async function probeHttpBounded(url, timeoutMs = 5e3) {
21453
20973
  const controller = new AbortController();
21454
20974
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -21465,7 +20985,7 @@ async function probeHttpBounded(url, timeoutMs = 5e3) {
21465
20985
  function registerDeployCommands(program3) {
21466
20986
  const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
21467
20987
  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)) {
20988
+ if (!STAGES.includes(stage)) {
21469
20989
  return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
21470
20990
  }
21471
20991
  try {
@@ -23026,7 +22546,7 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
23026
22546
  console.log("mmi-cli org rules gitignore: up to date");
23027
22547
  }
23028
22548
  });
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) => {
22549
+ 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
22550
  const manifest = buildCommandManifest(program2);
23031
22551
  if (o.json) {
23032
22552
  consoleIo.log(JSON.stringify(o.primary ? primaryCommandManifest(manifest) : manifest, null, 2));
@@ -23189,13 +22709,8 @@ function runWorktreeInstall(command, cwd, quiet) {
23189
22709
  });
23190
22710
  });
23191
22711
  }
23192
- async function primaryCheckoutRoot(worktreeRoot) {
23193
- try {
23194
- const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
23195
- return out ? (0, import_node_path23.dirname)(out) : void 0;
23196
- } catch {
23197
- return void 0;
23198
- }
22712
+ async function primaryCheckoutRoot(from) {
22713
+ return primaryCheckoutRootOf(async (args) => (await execFileP2("git", ["-C", from, ...args], { timeout: GIT_TIMEOUT_MS })).stdout);
23199
22714
  }
23200
22715
  function makeProvisionDeps(worktreeRoot, quiet, log) {
23201
22716
  return {
@@ -23259,7 +22774,7 @@ withExamples(mutating(
23259
22774
  } else if (o.claim || o.for) {
23260
22775
  return fail("worktree create: --claim/--for need an issue-ref argument (e.g. worktree create 2687 --claim)");
23261
22776
  }
23262
- const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
22777
+ const repoRoot2 = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
23263
22778
  const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
23264
22779
  const { base, fetchBranch } = resolveWorktreeBase(fromRef, o.remote);
23265
22780
  if (fetchBranch) {
@@ -23468,7 +22983,7 @@ async function reportWrite(label, res) {
23468
22983
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
23469
22984
  }
23470
22985
  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) => {
22986
+ 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
22987
  try {
23473
22988
  const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch });
23474
22989
  if (!o.json && action === "verify-secrets" && result.secrets) {
@@ -23476,6 +22991,10 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
23476
22991
  const { lines, failure } = renderVerifySecrets(body);
23477
22992
  for (const line of lines) printLine(line);
23478
22993
  if (failure) return failGraceful(`runtime tenant control ${stage} verify-secrets: ${failure}`);
22994
+ } else if (!o.json && action === "verify-broker" && result.broker) {
22995
+ const { lines, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
22996
+ for (const line of lines) printLine(line);
22997
+ if (failure) return failGraceful(`runtime tenant control ${stage} verify-broker: ${failure}`);
23479
22998
  } else {
23480
22999
  printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
23481
23000
  }
@@ -23486,6 +23005,15 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
23486
23005
  return failGraceful(`runtime tenant control: ${e.message}`);
23487
23006
  }
23488
23007
  });
23008
+ 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) => {
23009
+ try {
23010
+ const result = await runTenantReconcile(trainApplyDeps(), { repo, stage, watch: o.watch });
23011
+ printLine(o.json ? JSON.stringify(result, null, 2) : `${result.note}${result.runUrl ? ` \u2014 ${result.runUrl}` : ""}`);
23012
+ if (result.conclusion === "failure" || !result.dispatched) process.exitCode = 1;
23013
+ } catch (e) {
23014
+ return failGraceful(`runtime tenant reconcile: ${e.message}`);
23015
+ }
23016
+ });
23489
23017
  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
23018
  if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
23491
23019
  const cfg = await loadConfig();
@@ -23527,14 +23055,6 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
23527
23055
  return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
23528
23056
  }
23529
23057
  });
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
23058
  async function probeHttpBounded2(url, timeoutMs = 5e3) {
23539
23059
  const controller = new AbortController();
23540
23060
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -23565,68 +23085,7 @@ async function buildTenantRuntimeStatusFor(target, stage, cfg) {
23565
23085
  lastTenantDeployRun: (await fetchLastTenantDeployRun(slug, stage)).run
23566
23086
  });
23567
23087
  }
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");
23088
+ var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
23630
23089
  async function projectTarget(commandName, explicitTarget) {
23631
23090
  return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
23632
23091
  }
@@ -23686,69 +23145,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
23686
23145
  const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
23687
23146
  console.log(JSON.stringify(payload));
23688
23147
  });
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) => {
23148
+ 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
23149
  const cfg = await loadConfig();
23753
23150
  let target;
23754
23151
  try {
@@ -23862,7 +23259,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
23862
23259
  console.log(JSON.stringify(report, null, 2));
23863
23260
  if (!report.rcand.canApply) process.exitCode = 1;
23864
23261
  });
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) => {
23262
+ 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
23263
  const cfg = await loadConfig();
23867
23264
  let target;
23868
23265
  try {
@@ -23876,17 +23273,23 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
23876
23273
  if (v !== "true" && v !== "false") return fail("org project set-deploy: --no-env-file must be true or false");
23877
23274
  noEnvFile = v === "true";
23878
23275
  }
23879
- if (noEnvFile === true) {
23276
+ if (noEnvFile !== void 0) {
23880
23277
  const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
23881
23278
  if (branch) {
23882
23279
  const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
23883
23280
  const sameRepo = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
23884
23281
  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}`);
23282
+ const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force), noEnvFile });
23283
+ if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
23284
+ ${filelessTransitionGuide(target, o.stage)}`);
23887
23285
  if (verdict.warn) console.error(`org project set-deploy: ${verdict.warn}`);
23286
+ } else if (!o.force) {
23287
+ return fail(
23288
+ `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.
23289
+ ${filelessTransitionGuide(target, o.stage)}`
23290
+ );
23888
23291
  } 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).`);
23292
+ console.error(`org project set-deploy: --force: skipping cross-repo fileless-compose verification for ${target} ${branch}.`);
23890
23293
  }
23891
23294
  }
23892
23295
  }
@@ -23906,6 +23309,7 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
23906
23309
  clearAliases: o.clearAliases,
23907
23310
  noEnvFile
23908
23311
  });
23312
+ if (target.includes("/")) body.repo = target;
23909
23313
  } catch (e) {
23910
23314
  return fail(e.message);
23911
23315
  }
@@ -24041,7 +23445,7 @@ function resolveCreateType(raw, command) {
24041
23445
  }
24042
23446
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
24043
23447
  withExamples(mutating(
24044
- issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--north-star <slug>", "link to a North Star plan \u2014 appends a body line + northstar:<slug> label (#1922)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
23448
+ issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
24045
23449
  // --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
24046
23450
  // ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
24047
23451
  // (`--parent`) and title-source are validated by the action on a real run.
@@ -24056,23 +23460,14 @@ withExamples(mutating(
24056
23460
  let body;
24057
23461
  let title;
24058
23462
  let issueType;
24059
- let northStarSlug;
24060
23463
  let extraLabels = [];
24061
23464
  try {
24062
23465
  issueType = resolveCreateType(o.type, "issue create");
24063
23466
  title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
24064
23467
  body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
24065
- if (o.northStar !== void 0) {
24066
- northStarSlug = normalizeNorthStarSlug(o.northStar);
24067
- body = appendNorthStarLine(body, northStarSlug);
24068
- }
24069
23468
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
24070
23469
  priority = resolveCreatePriority(o.priority, "issue create");
24071
23470
  extraLabels = [...o.label ?? []];
24072
- if (northStarSlug) {
24073
- const nsLabel = northStarLabel(northStarSlug);
24074
- if (!extraLabels.includes(nsLabel)) extraLabels.push(nsLabel);
24075
- }
24076
23471
  args = buildIssueArgs({
24077
23472
  type: issueType,
24078
23473
  title,
@@ -24114,7 +23509,6 @@ withExamples(mutating(
24114
23509
  priority,
24115
23510
  projectItemId,
24116
23511
  onBoard,
24117
- ...northStarSlug ? { northStarSlug } : {},
24118
23512
  ...parentLinkFields(parent, parentLinkError)
24119
23513
  }));
24120
23514
  }), [
@@ -24753,6 +24147,12 @@ function trainApplyDeps() {
24753
24147
  const body = res.body;
24754
24148
  return { ok: false, category: body?.category, error: body?.error ?? res.error };
24755
24149
  },
24150
+ dispatchTenantReconcile: async ({ repo, stage }) => {
24151
+ const res = await tenantReconcile({ repo, stage }, registryClientDeps(await loadConfig()));
24152
+ if (res.ok) return { ok: true };
24153
+ const body = res.body;
24154
+ return { ok: false, category: body?.category, error: body?.error ?? res.error };
24155
+ },
24756
24156
  // Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
24757
24157
  // release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
24758
24158
  // fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
@@ -25026,7 +24426,7 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
25026
24426
  mmiDoctorDeps()
25027
24427
  );
25028
24428
  });
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 }));
24429
+ program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk").action(() => runGuard());
25030
24430
  program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
25031
24431
  function directoryBytes(path2) {
25032
24432
  let total = 0;
@@ -25170,18 +24570,9 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
25170
24570
  appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
25171
24571
  });
25172
24572
  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
24573
  consolidateCommandNamespaces(program2);
25180
24574
  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));
24575
+ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
25185
24576
  // Annotate the CommonJS export names for ESM import in node:
25186
24577
  0 && (module.exports = {
25187
24578
  DEFAULT_PRIORITY,