@mutmutco/cli 3.31.0 → 3.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +10 -10
  2. package/dist/main.cjs +809 -1001
  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(
@@ -4337,7 +4340,7 @@ function formatScratchGcPlan(plan, applied, applyResult) {
4337
4340
  function scratchGcBannerLine(safeAutoPruned, advisory) {
4338
4341
  const parts = [];
4339
4342
  if (safeAutoPruned > 0) parts.push(`pruned ${safeAutoPruned} scratch item(s)`);
4340
- if (advisory > 0) parts.push(`${advisory} kept scratch item(s) need review \u2014 \`mmi-cli gc --scratch --dry-run\``);
4343
+ if (advisory > 0) parts.push(`${advisory} kept scratch item(s) need review \u2014 \`mmi-cli worktree gc --scratch --dry-run\``);
4341
4344
  return parts.length ? `[cleanup] ${parts.join("; ")}` : void 0;
4342
4345
  }
4343
4346
  function isLinkLike(path2, symbolic = false) {
@@ -4359,9 +4362,9 @@ function treeOlderThan(root, now, floor) {
4359
4362
  if (now - st.mtimeMs <= floor) return false;
4360
4363
  if (!st.isDirectory()) continue;
4361
4364
  for (const ent of (0, import_node_fs6.readdirSync)(current, { withFileTypes: true })) {
4362
- const child = (0, import_node_path4.join)(current, ent.name);
4363
- if (isLinkLike(child, ent.isSymbolicLink())) return false;
4364
- stack.push(child);
4365
+ const child2 = (0, import_node_path4.join)(current, ent.name);
4366
+ if (isLinkLike(child2, ent.isSymbolicLink())) return false;
4367
+ stack.push(child2);
4365
4368
  }
4366
4369
  }
4367
4370
  return true;
@@ -4481,16 +4484,16 @@ function rootScratchDirSnapshot(root, readdir, stat2) {
4481
4484
  while (stack.length) {
4482
4485
  const current = stack.pop();
4483
4486
  for (const ent of readdir(current, { withFileTypes: true })) {
4484
- const child = (0, import_node_path4.join)(current, ent.name);
4487
+ const child2 = (0, import_node_path4.join)(current, ent.name);
4485
4488
  if (ent.isSymbolicLink?.()) return null;
4486
4489
  try {
4487
- (0, import_node_fs6.readlinkSync)(child);
4490
+ (0, import_node_fs6.readlinkSync)(child2);
4488
4491
  return null;
4489
4492
  } catch {
4490
4493
  }
4491
- const st = stat2(child);
4494
+ const st = stat2(child2);
4492
4495
  mtimeMs = Math.max(mtimeMs, st.mtimeMs);
4493
- if (ent.isDirectory()) stack.push(child);
4496
+ if (ent.isDirectory()) stack.push(child2);
4494
4497
  else if (ent.isFile()) bytes += st.size;
4495
4498
  }
4496
4499
  }
@@ -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
  }
@@ -5156,7 +5143,7 @@ var import_node_fs10 = require("node:fs");
5156
5143
 
5157
5144
  // src/gc.ts
5158
5145
  var import_node_path7 = require("node:path");
5159
- var DEFERRED_SWEEP_COMMAND = "mmi-cli gc sweep-deferred";
5146
+ var DEFERRED_SWEEP_COMMAND = "mmi-cli worktree gc sweep-deferred";
5160
5147
  var DEFERRED_NOTE = "Worktree cleanup queued (IDE lock). Detached sweep will retry automatically \u2014 no human action required.";
5161
5148
  var PRESERVED_WORKTREE_CONFIG = "mmi.preservedWorktreeBranch";
5162
5149
  var WORKTREE_LOCK_RE = /EPERM|EBUSY|EACCES|ENOTEMPTY|permission denied|access is denied|used by another process|resource busy|directory not empty/i;
@@ -5858,11 +5845,11 @@ function selectSafeWorktreeCwd(worktrees, targetPath, options) {
5858
5845
  return worktrees.find((w) => !samePath(w.path, targetPath) && exists(w.path))?.path;
5859
5846
  }
5860
5847
  function isPathUnderDirectory(childPath, parentPath) {
5861
- const child = normPath(childPath);
5848
+ const child2 = normPath(childPath);
5862
5849
  const parent = normPath(parentPath);
5863
- if (!child || !parent) return false;
5864
- if (child === parent) return true;
5865
- return child.startsWith(`${parent}/`);
5850
+ if (!child2 || !parent) return false;
5851
+ if (child2 === parent) return true;
5852
+ return child2.startsWith(`${parent}/`);
5866
5853
  }
5867
5854
  function planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, currentCwd) {
5868
5855
  if (!safeCwd || !isPathUnderDirectory(currentCwd, targetPath)) return void 0;
@@ -6113,7 +6100,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
6113
6100
  return report;
6114
6101
  }
6115
6102
  function formatGcPlan(plan, apply) {
6116
- const lines = [`mmi-cli gc: ${apply ? "apply" : "dry-run"}`];
6103
+ const lines = [`mmi-cli worktree gc: ${apply ? "apply" : "dry-run"}`];
6117
6104
  if (!plan.branches.length && !plan.trackingRefs.length && !plan.worktreeDirs.length) lines.push("nothing to clean");
6118
6105
  if (plan.branches.length) {
6119
6106
  lines.push("local branches:");
@@ -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";
@@ -6579,10 +6563,10 @@ async function secretsCapabilities(deps, opts) {
6579
6563
  } catch (e) {
6580
6564
  const message = e.message;
6581
6565
  if (isTimeoutError(e)) {
6582
- deps.err(`access capabilities: timed out after ${CAPABILITIES_TIMEOUT_MS}ms while aggregating vault scopes for ${repo}. No access conclusion was made; retry with a warm Hub or run scoped reads such as \`mmi-cli secrets list --repo ${repo}\` while investigating the slow source.`);
6566
+ deps.err(`org access capabilities: timed out after ${CAPABILITIES_TIMEOUT_MS}ms while aggregating vault scopes for ${repo}. No access conclusion was made; retry with a warm Hub or run scoped reads such as \`mmi-cli secrets list --repo ${repo}\` while investigating the slow source.`);
6583
6567
  return;
6584
6568
  }
6585
- deps.err(`access capabilities: ${message}`);
6569
+ deps.err(`org access capabilities: ${message}`);
6586
6570
  return;
6587
6571
  }
6588
6572
  if (!res.ok) {
@@ -6591,10 +6575,10 @@ async function secretsCapabilities(deps, opts) {
6591
6575
  deps.err(upgrade);
6592
6576
  } else if (res.status === 404) {
6593
6577
  deps.err(
6594
- "access capabilities: the Hub API did not recognize /secrets/capabilities (HTTP 404). This endpoint ships in the Hub, so a 404 means the deployed Hub predates this command and should answer once a Hub release carrying this command is deployed \u2014 it is not an authorization or missing-credential error."
6578
+ "org access capabilities: the Hub API did not recognize /secrets/capabilities (HTTP 404). This endpoint ships in the Hub, so a 404 means the deployed Hub predates this command and should answer once a Hub release carrying this command is deployed \u2014 it is not an authorization or missing-credential error."
6595
6579
  );
6596
6580
  } else {
6597
- deps.err(`access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
6581
+ deps.err(`org access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
6598
6582
  }
6599
6583
  return;
6600
6584
  }
@@ -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;
@@ -7644,9 +7629,9 @@ function resolveBoardConfig(cfg) {
7644
7629
  const gap = diagnoseBoardConfigGap(cfg);
7645
7630
  if (gap) {
7646
7631
  throw gap.kind === "no-project-detected" ? new Error(
7647
- "no MMI project detected for this workspace (no Hub registry match for this repo, or no git origin in this folder); run `mmi-cli board read --repo <owner/repo>` to target a specific registered repo, or `mmi-cli project get <owner/repo>` to check registration"
7632
+ "no MMI project detected for this workspace (no Hub registry match for this repo, or no git origin in this folder); run `mmi-cli board read --repo <owner/repo>` to target a specific registered repo, or `mmi-cli org project get <owner/repo>` to check registration"
7648
7633
  ) : new Error(
7649
- `Hub registry board META missing ${gap.missing.join(", ")}; run \`gh auth login\`, then \`mmi-cli project get <owner/repo>\`, or ask a master-admin to register/backfill board coords`
7634
+ `Hub registry board META missing ${gap.missing.join(", ")}; run \`gh auth login\`, then \`mmi-cli org project get <owner/repo>\`, or ask a master-admin to register/backfill board coords`
7650
7635
  );
7651
7636
  }
7652
7637
  return {
@@ -9120,7 +9105,7 @@ function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none")
9120
9105
  message: [
9121
9106
  `${staleNote} and are not green (${checks}).`,
9122
9107
  "Fix the checks or use `mmi-cli pr land` so the CLI waits.",
9123
- "Then correct registry META: `mmi-cli project set <owner/repo> --var requiredChecks=<check-run-display-names>`."
9108
+ "Then correct registry META: `mmi-cli org project set <owner/repo> --var requiredChecks=<check-run-display-names>`."
9124
9109
  ].join(" ")
9125
9110
  };
9126
9111
  }
@@ -9961,7 +9946,7 @@ async function auditRepoCi(repo, deps) {
9961
9946
  ok: false,
9962
9947
  label: "registry no-ci contradicts PR workflows",
9963
9948
  detail: `registry META declares no-ci but pull_request workflows emit [${emitted.join(", ")}]`,
9964
- remediation: `Fix the workflows or correct registry META: mmi-cli project set ${repo} --var requiredChecks=${JSON.stringify(emitted)}`
9949
+ remediation: `Fix the workflows or correct registry META: mmi-cli org project set ${repo} --var requiredChecks=${JSON.stringify(emitted)}`
9965
9950
  });
9966
9951
  }
9967
9952
  }
@@ -10022,7 +10007,7 @@ async function auditRepoCi(repo, deps) {
10022
10007
  ok: explicitNoCi,
10023
10008
  label: "registry META declares intentional no-ci",
10024
10009
  detail: explicitNoCi ? void 0 : "set ci:none and requiredChecks:[] in registry META",
10025
- remediation: `mmi-cli project set ${repo} --var ci=none --var requiredChecks=[]`
10010
+ remediation: `mmi-cli org project set ${repo} --var ci=none --var requiredChecks=[]`
10026
10011
  });
10027
10012
  } else if (deployableGated) {
10028
10013
  checks.push({
@@ -10060,7 +10045,7 @@ async function auditRepoCi(repo, deps) {
10060
10045
  ok: trackExpectsRc === rcPresent,
10061
10046
  label: "release track matches branch topology",
10062
10047
  detail: trackExpectsRc === rcPresent ? void 0 : `registry resolves '${effectiveTrack}' but the rc branch ${rcPresent ? "exists" : "is absent"} \u2014 set release track '${wantTrack}'`,
10063
- remediation: trackExpectsRc === rcPresent ? void 0 : `mmi-cli project set ${repo} --release-track ${wantTrack}`
10048
+ remediation: trackExpectsRc === rcPresent ? void 0 : `mmi-cli org project set ${repo} --release-track ${wantTrack}`
10064
10049
  });
10065
10050
  }
10066
10051
  }
@@ -10185,7 +10170,7 @@ async function seedGateYml(repo, deps, meta, result) {
10185
10170
  }
10186
10171
  const gate = meta?.gate;
10187
10172
  if (!gate || typeof gate === "object" && Object.keys(gate).length === 0) {
10188
- result.skipped.push(`gate.yml missing \u2014 no registry gate config; set \`mmi-cli project set ${repo} --var gate={...}\` first, then re-run reconcile`);
10173
+ result.skipped.push(`gate.yml missing \u2014 no registry gate config; set \`mmi-cli org project set ${repo} --var gate={...}\` first, then re-run reconcile`);
10189
10174
  return;
10190
10175
  }
10191
10176
  const derivedVars = withDerivedRepoVars({ ...gateConfigToVars(gate), REPO_SLUG: parsed.slug }, parsed, "deployable", releaseTrack);
@@ -10386,20 +10371,27 @@ var import_node_util6 = require("node:util");
10386
10371
  var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
10387
10372
  var DOCKER_TIMEOUT_MS = 15e3;
10388
10373
  var EARLY_EXIT_GRACE_MS = 2e3;
10389
- function waitForProcessStability(child, graceMs = EARLY_EXIT_GRACE_MS) {
10374
+ function earlyExitGraceMs() {
10375
+ if (process.env.NODE_ENV === "test") {
10376
+ const testOverride = Number(process.env.MMI_CLI_TEST_EARLY_EXIT_GRACE_MS);
10377
+ if (Number.isFinite(testOverride) && testOverride >= 25) return testOverride;
10378
+ }
10379
+ return EARLY_EXIT_GRACE_MS;
10380
+ }
10381
+ function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
10390
10382
  return new Promise((resolve5, reject) => {
10391
10383
  let settled = false;
10392
10384
  const finish = (fn) => {
10393
10385
  if (settled) return;
10394
10386
  settled = true;
10395
10387
  clearTimeout(timer);
10396
- child.removeAllListeners("error");
10397
- child.removeAllListeners("exit");
10388
+ child2.removeAllListeners("error");
10389
+ child2.removeAllListeners("exit");
10398
10390
  fn();
10399
10391
  };
10400
10392
  const timer = setTimeout(() => finish(resolve5), graceMs);
10401
- child.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
10402
- child.on("exit", (code, signal) => {
10393
+ child2.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
10394
+ child2.on("exit", (code, signal) => {
10403
10395
  const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
10404
10396
  finish(() => reject(new Error(`stage process exited before health check (${detail})`)));
10405
10397
  });
@@ -10534,9 +10526,9 @@ function parseStagePortFlag(raw) {
10534
10526
  return port;
10535
10527
  }
10536
10528
  function pathUnder(childPath, parentPath) {
10537
- const child = normPath2(childPath);
10529
+ const child2 = normPath2(childPath);
10538
10530
  const parent = normPath2(parentPath);
10539
- return Boolean(child && parent && (child === parent || child.startsWith(`${parent}/`)));
10531
+ return Boolean(child2 && parent && (child2 === parent || child2.startsWith(`${parent}/`)));
10540
10532
  }
10541
10533
  function stageStateMatchesRequiredCwd(state, requiredCwd) {
10542
10534
  if (!state) return false;
@@ -10890,7 +10882,7 @@ async function startStage(config = {}, opts = {}) {
10890
10882
  let up = sub(config.up.trim());
10891
10883
  if (opts.forceRecreate) up = appendForceRecreate(up);
10892
10884
  const identity = await resolveStageIdentity(cwd);
10893
- const child = (0, import_node_child_process7.spawn)(up, {
10885
+ const child2 = (0, import_node_child_process7.spawn)(up, {
10894
10886
  cwd,
10895
10887
  shell: true,
10896
10888
  // POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
@@ -10903,7 +10895,7 @@ async function startStage(config = {}, opts = {}) {
10903
10895
  env: { ...process.env, ...vaultProcessEnv, ...stageProcessEnv(stagePort, extraEnv) }
10904
10896
  });
10905
10897
  const state = {
10906
- pid: child.pid ?? 0,
10898
+ pid: child2.pid ?? 0,
10907
10899
  command: up,
10908
10900
  cwd,
10909
10901
  statePath,
@@ -10917,7 +10909,7 @@ async function startStage(config = {}, opts = {}) {
10917
10909
  if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, state);
10918
10910
  try {
10919
10911
  if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
10920
- else await waitForProcessStability(child);
10912
+ else await waitForProcessStability(child2);
10921
10913
  } catch (e) {
10922
10914
  await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd);
10923
10915
  throw e;
@@ -10932,7 +10924,7 @@ async function startStage(config = {}, opts = {}) {
10932
10924
  message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}`
10933
10925
  };
10934
10926
  opts.onReady?.(result);
10935
- child.unref();
10927
+ child2.unref();
10936
10928
  return result;
10937
10929
  }
10938
10930
  async function runStage(config = {}, opts = {}) {
@@ -11149,7 +11141,7 @@ async function bodyArgsViaFile(args, deps = {}) {
11149
11141
  if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
11150
11142
  } };
11151
11143
  const write = deps.write ?? import_promises.writeFile;
11152
- const remove = deps.remove ?? import_promises.unlink;
11144
+ const remove2 = deps.remove ?? import_promises.unlink;
11153
11145
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
11154
11146
  const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
11155
11147
  const file = (0, import_node_path13.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
@@ -11160,7 +11152,7 @@ async function bodyArgsViaFile(args, deps = {}) {
11160
11152
  args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
11161
11153
  cleanup: async () => {
11162
11154
  try {
11163
- await remove(file);
11155
+ await remove2(file);
11164
11156
  } catch {
11165
11157
  }
11166
11158
  }
@@ -11375,9 +11367,9 @@ async function resolveIssueNodeId(runGh, ref, fallbackRepo) {
11375
11367
  }
11376
11368
  async function linkSubIssue(runGh, parentRef, childRef, defaultRepo) {
11377
11369
  const parent = parseIssueRef(parentRef);
11378
- const child = parseIssueRef(childRef);
11370
+ const child2 = parseIssueRef(childRef);
11379
11371
  const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
11380
- const subIssueId = await resolveIssueNodeId(runGh, child, defaultRepo);
11372
+ const subIssueId = await resolveIssueNodeId(runGh, child2, defaultRepo);
11381
11373
  const stdout = await runGh(buildAddSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
11382
11374
  const result = parseAddSubIssueResult(stdout);
11383
11375
  if (!result) throw new Error(`addSubIssue returned an unexpected response:
@@ -11412,9 +11404,9 @@ function parseRemoveSubIssueResult(stdout) {
11412
11404
  }
11413
11405
  async function unlinkSubIssue(runGh, parentRef, childRef, defaultRepo) {
11414
11406
  const parent = parseIssueRef(parentRef);
11415
- const child = parseIssueRef(childRef);
11407
+ const child2 = parseIssueRef(childRef);
11416
11408
  const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
11417
- const subIssueId = await resolveIssueNodeId(runGh, child, defaultRepo);
11409
+ const subIssueId = await resolveIssueNodeId(runGh, child2, defaultRepo);
11418
11410
  const stdout = await runGh(buildRemoveSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
11419
11411
  const result = parseRemoveSubIssueResult(stdout);
11420
11412
  if (!result) throw new Error(`removeSubIssue returned an unexpected response:
@@ -11485,6 +11477,126 @@ function applyChecklistCheck(body, query, checked) {
11485
11477
  return { ok: true, edit: setChecklistMarker(body, sel.item, checked), item: sel.item };
11486
11478
  }
11487
11479
 
11480
+ // src/command-taxonomy.ts
11481
+ var COMMAND_METADATA = /* @__PURE__ */ Symbol.for("mmi.commandTaxonomy.metadata");
11482
+ var PRIMARY_GROUPS = [
11483
+ ["Orient", ["onboard", "status", "next", "doctor", "whoami", "commands", "explain"]],
11484
+ ["Plan and work", ["board", "issue", "worktree", "stage"]],
11485
+ ["Review and ship", ["pr", "ci", "rcand", "release", "hotfix", "train"]],
11486
+ ["Setup and support", ["bootstrap", "secrets", "docs"]],
11487
+ ["Coordinate and improve", ["wave", "report", "skill-lesson"]]
11488
+ ];
11489
+ var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin"]);
11490
+ var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "wave", "report", "skill-lesson"]);
11491
+ var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
11492
+ var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
11493
+ var topLevelPosition = 0;
11494
+ for (const [helpGroup, names] of PRIMARY_GROUPS) {
11495
+ HELP_GROUP_ORDER.set(helpGroup, HELP_GROUP_ORDER.size);
11496
+ for (const name of names) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
11497
+ }
11498
+ HELP_GROUP_ORDER.set("Operations", HELP_GROUP_ORDER.size);
11499
+ for (const name of OPERATIONAL_TOP_LEVEL) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
11500
+ var PATH_OVERRIDES = {
11501
+ "board slice-refresh": { category: "internal", discovery: "hidden", help_group: "Operations" },
11502
+ "secrets find": { category: "admin", discovery: "all-only", help_group: "Operations" },
11503
+ "secrets catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
11504
+ "secrets doctor": { category: "admin", discovery: "all-only", help_group: "Operations" },
11505
+ "secrets org-catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
11506
+ "secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
11507
+ "secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
11508
+ };
11509
+ var HIDDEN_OPTIONS = {
11510
+ "worktree create": ["--base"],
11511
+ "org project set": ["--set"]
11512
+ };
11513
+ function primaryMetadata(name) {
11514
+ for (const [helpGroup, names] of PRIMARY_GROUPS) {
11515
+ if (names.includes(name)) {
11516
+ return {
11517
+ category: SUPPORT_PRIMARY.has(name) ? "support" : "core",
11518
+ discovery: "primary",
11519
+ help_group: helpGroup
11520
+ };
11521
+ }
11522
+ }
11523
+ return void 0;
11524
+ }
11525
+ function topLevelMetadata(name) {
11526
+ const primary = primaryMetadata(name);
11527
+ if (primary) return primary;
11528
+ if (!OPERATIONAL_TOP_LEVEL.has(name)) {
11529
+ throw new Error(`command taxonomy: unclassified top-level command "${name}"`);
11530
+ }
11531
+ return {
11532
+ category: name === "plugin" ? "internal" : "admin",
11533
+ discovery: "all-only",
11534
+ help_group: "Operations"
11535
+ };
11536
+ }
11537
+ function setCommandMetadata(command, metadata) {
11538
+ command[COMMAND_METADATA] = metadata;
11539
+ }
11540
+ function commandMetadata(command) {
11541
+ return command[COMMAND_METADATA];
11542
+ }
11543
+ function classifyTree(command, path2, inherited) {
11544
+ const metadata = PATH_OVERRIDES[path2] ?? inherited;
11545
+ setCommandMetadata(command, metadata);
11546
+ if (metadata.discovery !== "primary") {
11547
+ command._hidden = true;
11548
+ }
11549
+ for (const option of command.options) {
11550
+ if ((HIDDEN_OPTIONS[path2] ?? []).includes(option.long ?? option.short ?? "")) option.hideHelp();
11551
+ }
11552
+ for (const child2 of command.commands) {
11553
+ const childPath = path2 ? `${path2} ${child2.name()}` : child2.name();
11554
+ classifyTree(child2, childPath, PATH_OVERRIDES[childPath] ?? metadata);
11555
+ }
11556
+ }
11557
+ function applyCommandTaxonomy(program3) {
11558
+ for (const command of program3.commands) {
11559
+ const metadata = topLevelMetadata(command.name());
11560
+ command.helpGroup(metadata.help_group);
11561
+ classifyTree(command, command.name(), metadata);
11562
+ }
11563
+ program3.configureHelp({
11564
+ visibleCommands(command) {
11565
+ const visible = command.commands.filter((child2) => !child2._hidden);
11566
+ const helpCommand = command._getHelpCommand();
11567
+ if (helpCommand && !helpCommand._hidden) visible.push(helpCommand);
11568
+ if (command === program3) visible.sort((a, b) => commandTaxonomyRank(a.name()) - commandTaxonomyRank(b.name()));
11569
+ return visible;
11570
+ },
11571
+ groupItems(unsortedItems, visibleItems, getGroup) {
11572
+ const groups = /* @__PURE__ */ new Map();
11573
+ for (const item of unsortedItems) {
11574
+ const group = getGroup(item);
11575
+ if (!groups.has(group)) groups.set(group, []);
11576
+ }
11577
+ for (const item of visibleItems) {
11578
+ const group = getGroup(item);
11579
+ if (!groups.has(group)) groups.set(group, []);
11580
+ groups.get(group).push(item);
11581
+ }
11582
+ return new Map([...groups].sort(([left], [right]) => {
11583
+ const leftRank = HELP_GROUP_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER;
11584
+ const rightRank = HELP_GROUP_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER;
11585
+ return leftRank - rightRank;
11586
+ }));
11587
+ }
11588
+ });
11589
+ }
11590
+ function commandTaxonomyRank(name) {
11591
+ return TOP_LEVEL_ORDER.get(name) ?? Number.MAX_SAFE_INTEGER;
11592
+ }
11593
+ function commandHelpGroupRank(group) {
11594
+ return HELP_GROUP_ORDER.get(group) ?? Number.MAX_SAFE_INTEGER;
11595
+ }
11596
+ function isCanonicalSuggestion(metadata) {
11597
+ return metadata.category !== "internal";
11598
+ }
11599
+
11488
11600
  // src/command-manifest.ts
11489
11601
  var EXAMPLES = /* @__PURE__ */ Symbol.for("mmi.commandManifest.examples");
11490
11602
  var COMMON_MISTAKES = /* @__PURE__ */ Symbol.for("mmi.commandManifest.commonMistakes");
@@ -11524,17 +11636,24 @@ function buildOption(opt) {
11524
11636
  if (opt.description) out.description = opt.description;
11525
11637
  if (opt.defaultValue !== void 0) out.default = opt.defaultValue;
11526
11638
  if (opt.argChoices && opt.argChoices.length) out.enum = [...opt.argChoices];
11639
+ if (opt.hidden) out.discovery = "all-only";
11527
11640
  return out;
11528
11641
  }
11529
11642
  function buildCommand(cmd, path2) {
11643
+ const metadata = commandMetadata(cmd) ?? {
11644
+ category: "core",
11645
+ discovery: "primary",
11646
+ help_group: "Plan and work"
11647
+ };
11530
11648
  const out = {
11531
11649
  name: cmd.name(),
11532
11650
  path: path2,
11533
11651
  arguments: cmd.registeredArguments.map(buildArgument),
11534
11652
  options: cmd.options.map(buildOption),
11535
11653
  subcommands: cmd.commands.map(
11536
- (child) => buildCommand(child, path2 ? `${path2} ${child.name()}` : child.name())
11537
- )
11654
+ (child2) => buildCommand(child2, path2 ? `${path2} ${child2.name()}` : child2.name())
11655
+ ),
11656
+ ...metadata
11538
11657
  };
11539
11658
  const description = cmd.description();
11540
11659
  if (description) out.description = description;
@@ -11544,42 +11663,146 @@ function buildCommand(cmd, path2) {
11544
11663
  if (commonMistakes) out.common_mistakes = commonMistakes;
11545
11664
  return out;
11546
11665
  }
11666
+ function primaryCopy(node, root = false) {
11667
+ if (!root && node.discovery !== "primary") return void 0;
11668
+ const subcommands = node.subcommands.map((child2) => primaryCopy(child2)).filter((child2) => Boolean(child2));
11669
+ if (root) subcommands.sort((a, b) => commandTaxonomyRank(a.name) - commandTaxonomyRank(b.name));
11670
+ return {
11671
+ ...node,
11672
+ options: node.options.filter((option) => option.discovery !== "all-only" && option.discovery !== "hidden"),
11673
+ subcommands
11674
+ };
11675
+ }
11547
11676
  function collectLeaves(node, acc) {
11548
11677
  if (node.subcommands.length === 0) {
11549
11678
  if (node.path) acc.push(node);
11550
11679
  return;
11551
11680
  }
11552
- for (const child of node.subcommands) collectLeaves(child, acc);
11681
+ for (const child2 of node.subcommands) collectLeaves(child2, acc);
11553
11682
  }
11554
11683
  function buildCommandManifest(program3) {
11555
11684
  const tree = buildCommand(program3, "");
11556
11685
  const index = [];
11557
11686
  collectLeaves(tree, index);
11558
- const manifest = { name: tree.name, tree, index, error_codes: ERROR_CODE_REFERENCE };
11687
+ const primaryTree = primaryCopy(tree, true);
11688
+ const primaryIndex = [];
11689
+ collectLeaves(primaryTree, primaryIndex);
11690
+ const manifest = {
11691
+ schema_version: 2,
11692
+ scope: "all",
11693
+ name: tree.name,
11694
+ tree,
11695
+ index,
11696
+ primary_tree: primaryTree,
11697
+ primary_index: primaryIndex,
11698
+ error_codes: ERROR_CODE_REFERENCE
11699
+ };
11559
11700
  const version = program3.version();
11560
11701
  if (version) manifest.version = version;
11561
11702
  return manifest;
11562
11703
  }
11704
+ function primaryCommandManifest(manifest) {
11705
+ return {
11706
+ schema_version: manifest.schema_version,
11707
+ scope: "primary",
11708
+ name: manifest.name,
11709
+ ...manifest.version ? { version: manifest.version } : {},
11710
+ tree: manifest.primary_tree,
11711
+ index: manifest.primary_index,
11712
+ error_codes: manifest.error_codes
11713
+ };
11714
+ }
11563
11715
  function argSignature(arg) {
11564
11716
  const inner = arg.variadic ? `${arg.name}...` : arg.name;
11565
11717
  return arg.required ? `<${inner}>` : `[${inner}]`;
11566
11718
  }
11567
- function formatManifestHuman(manifest) {
11568
- const lines = [];
11719
+ function formatManifestHuman(manifest, options = {}) {
11720
+ const root = options.all ? manifest.tree : manifest.primary_tree;
11721
+ const lines = [`${manifest.name}${manifest.version ? ` v${manifest.version}` : ""} \u2014 ${root.description ?? ""}`];
11569
11722
  const render = (node, depth) => {
11570
11723
  const indent = " ".repeat(depth);
11571
11724
  const args = node.arguments.map(argSignature).join(" ");
11572
- const head = depth === 0 ? `${node.name}${manifest.version ? ` v${manifest.version}` : ""}` : node.name + (args ? ` ${args}` : "");
11725
+ const head = node.name + (args ? ` ${args}` : "");
11573
11726
  lines.push(node.description ? `${indent}${head} \u2014 ${node.description}` : `${indent}${head}`);
11574
- for (const opt of node.options) {
11727
+ for (const opt of options.all ? node.options : []) {
11575
11728
  lines.push(`${indent} ${opt.flags}${opt.description ? ` ${opt.description}` : ""}`);
11576
11729
  }
11577
- for (const child of node.subcommands) render(child, depth + 1);
11730
+ if (options.all) for (const child2 of node.subcommands) render(child2, depth + 1);
11578
11731
  };
11579
- render(manifest.tree, 0);
11732
+ let currentGroup;
11733
+ const rootCommands = [...root.subcommands].sort((a, b) => commandHelpGroupRank(a.help_group) - commandHelpGroupRank(b.help_group) || commandTaxonomyRank(a.name) - commandTaxonomyRank(b.name));
11734
+ for (const command of rootCommands) {
11735
+ if (command.help_group !== currentGroup) {
11736
+ currentGroup = command.help_group;
11737
+ lines.push("", `${currentGroup}:`);
11738
+ }
11739
+ render(command, 1);
11740
+ }
11741
+ lines.push("", options.all ? "Use `mmi-cli explain <group|command>` for a focused map." : "Use `mmi-cli explain <group|command>` for detail or `mmi-cli commands --all` for operational depth.");
11580
11742
  return lines.join("\n");
11581
11743
  }
11582
11744
 
11745
+ // src/command-consolidation.ts
11746
+ function child(parent, name) {
11747
+ const found = parent.commands.find((command) => command.name() === name);
11748
+ if (!found) throw new Error(`command consolidation: missing "${parent.name()} ${name}"`);
11749
+ return found;
11750
+ }
11751
+ function detach(parent, command) {
11752
+ const commands = parent.commands;
11753
+ const index = commands.indexOf(command);
11754
+ if (index < 0) throw new Error(`command consolidation: "${command.name()}" is not a child of "${parent.name()}"`);
11755
+ commands.splice(index, 1);
11756
+ return command;
11757
+ }
11758
+ function move(parent, destination, name, nextName = name) {
11759
+ const command = detach(parent, child(parent, name));
11760
+ command.name(nextName);
11761
+ destination.addCommand(command);
11762
+ return command;
11763
+ }
11764
+ function remove(parent, name) {
11765
+ detach(parent, child(parent, name));
11766
+ }
11767
+ function consolidateCommandNamespaces(program3) {
11768
+ const org = program3.command("org").description("organization projects, access, rules, credentials, and schedules");
11769
+ const runtime = program3.command("runtime").description("tenant, deploy, box, and edge operations");
11770
+ const plugin = program3.command("plugin").description("plugin lifecycle and guard operations");
11771
+ move(program3, org, "project");
11772
+ move(program3, org, "oauth");
11773
+ move(program3, org, "access");
11774
+ move(program3, org, "schedules");
11775
+ const config = org.command("config").description("organization configuration");
11776
+ const registry2 = child(program3, "registry");
11777
+ move(registry2, config, "org", "get");
11778
+ remove(program3, "registry");
11779
+ const orgRules = org.command("rules").description("organization-managed repository rules");
11780
+ const rules2 = child(program3, "rules");
11781
+ move(rules2, orgRules, "gitignore");
11782
+ remove(program3, "rules");
11783
+ move(program3, runtime, "tenant");
11784
+ const runtimeDeploy = runtime.command("deploy").description("runtime deployment status");
11785
+ const deploy = child(program3, "deploy");
11786
+ move(deploy, runtimeDeploy, "status");
11787
+ remove(program3, "deploy");
11788
+ move(program3, runtime, "box");
11789
+ move(program3, runtime, "edge");
11790
+ move(program3, plugin, "guard");
11791
+ move(program3, plugin, "plugin-heal", "heal");
11792
+ move(program3, plugin, "plugin-prune", "prune");
11793
+ move(program3, plugin, "session-start");
11794
+ const docs2 = child(program3, "docs");
11795
+ move(program3, docs2, "docs-audit", "audit");
11796
+ const train = child(program3, "train");
11797
+ const fullTrack2 = child(program3, "full-track");
11798
+ move(fullTrack2, train, "readiness");
11799
+ remove(program3, "full-track");
11800
+ const worktree2 = child(program3, "worktree");
11801
+ move(program3, worktree2, "gc");
11802
+ const stage = child(program3, "stage");
11803
+ move(program3, stage, "port-range");
11804
+ }
11805
+
11583
11806
  // src/version-lag.ts
11584
11807
  var VERSION_LABEL = "installed plugin/adapter cache freshness";
11585
11808
  var VERSION_FIX = "update the MMI plugin via /plugin; standalone npm CLI: npm install -g @mutmutco/cli@latest";
@@ -11722,12 +11945,12 @@ function newestMtimeMs(path2, listDir, mtimeOf) {
11722
11945
  return newest;
11723
11946
  }
11724
11947
  for (const e of entries) {
11725
- const child = `${path2}/${e.name}`;
11948
+ const child2 = `${path2}/${e.name}`;
11726
11949
  let m = 0;
11727
- if (e.isDirectory) m = newestMtimeMs(child, listDir, mtimeOf);
11950
+ if (e.isDirectory) m = newestMtimeMs(child2, listDir, mtimeOf);
11728
11951
  else {
11729
11952
  try {
11730
- m = mtimeOf(child);
11953
+ m = mtimeOf(child2);
11731
11954
  } catch {
11732
11955
  m = 0;
11733
11956
  }
@@ -11803,12 +12026,12 @@ function buildPluginCachePlan(home, running, deps, opts = {}) {
11803
12026
  const bytes = opts.withBytes ? prune.reduce((sum, v) => sum + deps.dirBytes(`${cacheRoot}/${v}`), 0) : 0;
11804
12027
  return { cacheRoot, present: true, keep, prune, bytes, running, ...stagingFields };
11805
12028
  }
11806
- function applyPluginCachePlan(plan, remove, stagingGuard) {
12029
+ function applyPluginCachePlan(plan, remove2, stagingGuard) {
11807
12030
  const removed = [];
11808
12031
  const failed = [];
11809
12032
  for (const version of plan.prune) {
11810
12033
  try {
11811
- remove(`${plan.cacheRoot}/${version}`);
12034
+ remove2(`${plan.cacheRoot}/${version}`);
11812
12035
  removed.push(version);
11813
12036
  } catch (e) {
11814
12037
  failed.push({ version, error: e.message });
@@ -11836,7 +12059,7 @@ function applyPluginCachePlan(plan, remove, stagingGuard) {
11836
12059
  }
11837
12060
  }
11838
12061
  try {
11839
- remove(`${plan.stagingRoot}/${s.name}`);
12062
+ remove2(`${plan.stagingRoot}/${s.name}`);
11840
12063
  removedStaging.push(s.name);
11841
12064
  } catch (e) {
11842
12065
  failedStaging.push({ name: s.name, error: e.message });
@@ -11856,7 +12079,7 @@ var CONCURRENT_SESSION_WARNING = "only the version THIS session runs from is pro
11856
12079
  function renderPluginCachePlan(plan, applied) {
11857
12080
  const mb = (b) => `${(b / 1e6).toFixed(1)} MB`;
11858
12081
  if (!plan.present && plan.staging.length === 0) {
11859
- return `plugin-prune: no plugin cache at ${plan.cacheRoot} \u2014 nothing to prune`;
12082
+ return `plugin prune: no plugin cache at ${plan.cacheRoot} \u2014 nothing to prune`;
11860
12083
  }
11861
12084
  const lines = [];
11862
12085
  if (plan.present) {
@@ -12005,11 +12228,11 @@ function trainPlan(command, options = {}) {
12005
12228
  { label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
12006
12229
  { label: "verify current branch is development", gated: true },
12007
12230
  rcandVersionStep(options),
12008
- { label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
12231
+ { label: "verify registry META for this project", command: "mmi-cli org project get <owner/repo>", gated: true },
12009
12232
  { label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
12010
12233
  { label: "merge development to rc", gated: true },
12011
12234
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml auto-fires on rc push, correlate/watch that run", gated: true },
12012
- { label: "after a failed deploy, retry the existing rc ref (no re-tag/merge)", command: "mmi-cli tenant redeploy <owner/repo> rc --watch", gated: true }
12235
+ { label: "after a failed deploy, retry the existing rc ref (no re-tag/merge)", command: "mmi-cli runtime tenant redeploy <owner/repo> rc --watch", gated: true }
12013
12236
  ];
12014
12237
  }
12015
12238
  if (command === "release") {
@@ -12017,7 +12240,7 @@ function trainPlan(command, options = {}) {
12017
12240
  return [
12018
12241
  { label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
12019
12242
  { label: "verify current branch is development", gated: true },
12020
- { label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
12243
+ { label: "verify registry META for this project", command: "mmi-cli org project get <owner/repo>", gated: true },
12021
12244
  { label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
12022
12245
  { label: "merge development to main", gated: true },
12023
12246
  { label: "fold the version bump into the release commit (Hub: full distribution set; app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
@@ -12031,20 +12254,20 @@ function trainPlan(command, options = {}) {
12031
12254
  { label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
12032
12255
  { label: "verify current branch is development", gated: true },
12033
12256
  { label: "guard: refuse if origin/rc carries content not in development (a dev -> main release would drop it)", command: "git rev-list --count --right-only --cherry-pick --no-merges origin/development...origin/rc", gated: true },
12034
- { label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
12257
+ { label: "verify registry META for this project", command: "mmi-cli org project get <owner/repo>", gated: true },
12035
12258
  { label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
12036
12259
  { label: "merge development to main (rc skipped)", gated: true },
12037
12260
  { label: "fold the version bump into the release commit \u2014 runs inside the apply step, no separate bump PR", gated: true },
12038
12261
  { label: "tag release and publish GitHub Release", gated: true },
12039
12262
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch", gated: true },
12040
- { label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli tenant control <owner/repo> rc retire", gated: true },
12263
+ { label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli runtime tenant control <owner/repo> rc retire", gated: true },
12041
12264
  { label: "roll development forward and align rc to the released main", gated: true }
12042
12265
  ];
12043
12266
  }
12044
12267
  return [
12045
12268
  { label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
12046
12269
  { label: "verify current branch is rc", gated: true },
12047
- { label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
12270
+ { label: "verify registry META for this project", command: "mmi-cli org project get <owner/repo>", gated: true },
12048
12271
  { label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
12049
12272
  { label: "verify every main-only hotfix commit is covered by the rc candidate (the guard runs automatically inside the apply step below; --ack <sha> overrides a verified, trailer-less port)", command: "mmi-cli release --apply [--ack <sha>]", gated: true },
12050
12273
  { label: "merge rc to main", gated: true },
@@ -12112,6 +12335,14 @@ function parseVerifySecrets(stdout) {
12112
12335
  }
12113
12336
  return out;
12114
12337
  }
12338
+ function parseVerifyBroker(stdout) {
12339
+ const out = [];
12340
+ for (const line of stdout.split("\n")) {
12341
+ const match = /^(\S+):\s*(reachable|missing|denied|error)\b/.exec(line.trim());
12342
+ if (match) out.push({ key: match[1], status: match[2] });
12343
+ }
12344
+ return out;
12345
+ }
12115
12346
 
12116
12347
  // src/train-apply.ts
12117
12348
  function resolveDeployModel2(meta, repo) {
@@ -12395,7 +12626,7 @@ function classifyProjectGetFailure(text) {
12395
12626
  }
12396
12627
  async function loadProjectMeta(deps, ctx) {
12397
12628
  try {
12398
- const out = await deps.runSelf(["project", "get", ctx.repo]);
12629
+ const out = await deps.runSelf(["org", "project", "get", ctx.repo]);
12399
12630
  const parsed = JSON.parse(out);
12400
12631
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
12401
12632
  return { status: "read-error", error: "invalid registry META JSON" };
@@ -12415,7 +12646,7 @@ function requireProjectMetaForTrain(load, repo) {
12415
12646
  }
12416
12647
  if (load.status === "missing") {
12417
12648
  throw new Error(
12418
- `${label}: no registry META for ${repo} \u2014 train apply refused; bootstrap the project or verify META with \`mmi-cli project get\` before promoting.`
12649
+ `${label}: no registry META for ${repo} \u2014 train apply refused; bootstrap the project or verify META with \`mmi-cli org project get\` before promoting.`
12419
12650
  );
12420
12651
  }
12421
12652
  return load.meta;
@@ -12514,6 +12745,9 @@ function correlatePublishRun(deps, since, titleIncludes) {
12514
12745
  function correlateControlRun(deps, since, titleIncludes) {
12515
12746
  return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
12516
12747
  }
12748
+ function correlateReconcileRun(deps, since, titleIncludes) {
12749
+ return correlateRun(deps, { workflow: "tenant-reconcile.yml", since, mode: "dispatch", titleIncludes });
12750
+ }
12517
12751
  async function correlateWorkflowRun(deps, args) {
12518
12752
  return correlateRun(deps, { ...args, mode: "workflow" });
12519
12753
  }
@@ -12729,7 +12963,7 @@ function partialTrainRecoveryError(cause, input) {
12729
12963
  partial train state: tag ${input.tag} is already pushed; origin/${branch} has not been pushed; ${releaseState}; deploy not dispatched.
12730
12964
  Recovery sequence:
12731
12965
  1. git push origin ${branch}` + releaseCommand + `
12732
- ${deployStep}. mmi-cli tenant redeploy ${input.repo} ${input.stage} --watch
12966
+ ${deployStep}. mmi-cli runtime tenant redeploy ${input.repo} ${input.stage} --watch
12733
12967
  Do not delete or force-move the pushed tag; rerun the train only after confirming the branch, release, and deploy states above.`
12734
12968
  );
12735
12969
  }
@@ -12911,7 +13145,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
12911
13145
  if (dispatchFailure === "throw") throw e;
12912
13146
  const msg = e instanceof Error ? e.message : String(e);
12913
13147
  return {
12914
- note: `tenant-deploy dispatch FAILED: ${msg}. The promotion itself landed (merge/tag/Release pushed before the dispatch) \u2014 recover the deploy with \`mmi-cli tenant redeploy ${ctx.repo} ${stage}\`; never re-tag.`,
13148
+ note: `tenant-deploy dispatch FAILED: ${msg}. The promotion itself landed (merge/tag/Release pushed before the dispatch) \u2014 recover the deploy with \`mmi-cli runtime tenant redeploy ${ctx.repo} ${stage}\`; never re-tag.`,
12915
13149
  deployStatus: "failure"
12916
13150
  };
12917
13151
  }
@@ -13498,14 +13732,14 @@ async function attemptRetire(deps, ctx) {
13498
13732
  return {
13499
13733
  status: "retired",
13500
13734
  category: "retired",
13501
- note: `rc runtime retired (tenant-control.yml${r.runUrl ? `, ${r.runUrl}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
13735
+ note: `rc runtime retired (tenant-control.yml${r.runUrl ? `, ${r.runUrl}` : ""}) \u2014 registry coords kept; /rcand or runtime tenant redeploy recreates rc next cycle`
13502
13736
  };
13503
13737
  }
13504
13738
  if (r.category === "wait-timeout") {
13505
13739
  return {
13506
13740
  status: "failed",
13507
13741
  category: "wait-timeout",
13508
- note: `rc retire dispatched but the run could not be observed \u2014 verify with: mmi-cli tenant control ${ctx.repo} rc status${r.runUrl ? ` (run ${r.runUrl})` : ""}`
13742
+ note: `rc retire dispatched but the run could not be observed \u2014 verify with: mmi-cli runtime tenant control ${ctx.repo} rc status${r.runUrl ? ` (run ${r.runUrl})` : ""}`
13509
13743
  };
13510
13744
  }
13511
13745
  return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
@@ -13520,7 +13754,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
13520
13754
  if (deployStatus !== "success") {
13521
13755
  return {
13522
13756
  status: "skipped",
13523
- note: `prod deploy outcome unconfirmed (run without --watch) \u2014 rc runtime left untouched; after verifying prod, retire with: mmi-cli tenant control ${ctx.repo} rc retire`
13757
+ note: `prod deploy outcome unconfirmed (run without --watch) \u2014 rc runtime left untouched; after verifying prod, retire with: mmi-cli runtime tenant control ${ctx.repo} rc retire`
13524
13758
  };
13525
13759
  }
13526
13760
  try {
@@ -13542,7 +13776,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
13542
13776
  }
13543
13777
  const f = last;
13544
13778
  if (f.category === "wait-timeout") return f;
13545
- const note = `rc retirement failed (the release itself succeeded): ${f.note}. The rc runtime may be orphaned on the box \u2014 retire it with: mmi-cli tenant control ${ctx.repo} rc retire (or sweep all: mmi-cli tenant sweep-rc --retire --yes)`;
13779
+ const note = `rc retirement failed (the release itself succeeded): ${f.note}. The rc runtime may be orphaned on the box \u2014 retire it with: mmi-cli runtime tenant control ${ctx.repo} rc retire (or sweep all: mmi-cli runtime tenant sweep-rc --retire --yes)`;
13546
13780
  return { status: "failed", category: f.category, note };
13547
13781
  } catch (e) {
13548
13782
  const err = e;
@@ -13569,8 +13803,36 @@ async function runTenantRedeploy(deps, options) {
13569
13803
  const d = await dispatchDeploy(deps, ctx, stage, ref, deployModel, watch);
13570
13804
  return { ...ctx, command: "tenant-redeploy", stage, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
13571
13805
  }
13806
+ async function runTenantReconcile(deps, options) {
13807
+ const parts = options.repo.split("/");
13808
+ if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error(`repo must be owner/name, got ${options.repo}`);
13809
+ if (!["dev", "rc", "main"].includes(options.stage)) throw new Error(`stage must be dev, rc, or main, got ${options.stage}`);
13810
+ if (!deps.dispatchTenantReconcile) throw new Error("tenant reconcile dispatch is unavailable");
13811
+ const stage = options.stage;
13812
+ const base = { command: "tenant-reconcile", repo: options.repo, stage };
13813
+ const since = (deps.now ?? Date.now)();
13814
+ const dispatched = await deps.dispatchTenantReconcile({ repo: options.repo, stage });
13815
+ if (!dispatched.ok) {
13816
+ return {
13817
+ ...base,
13818
+ dispatched: false,
13819
+ conclusion: "failure",
13820
+ note: `tenant reconcile rejected: ${dispatched.error ?? "request rejected by the Hub"}`
13821
+ };
13822
+ }
13823
+ const slug = parts[1].toLowerCase();
13824
+ const run = await correlateReconcileRun(deps, since, [slug, stage]);
13825
+ const conclusion = options.watch ?? true ? await watchTenantRun(deps, run.runId) : "pending";
13826
+ return {
13827
+ ...base,
13828
+ dispatched: true,
13829
+ ...run,
13830
+ conclusion,
13831
+ note: conclusion === "success" ? "tenant-reconcile run succeeded" : conclusion === "failure" ? "tenant-reconcile run failed \u2014 inspect the run before redeploying" : "tenant-reconcile dispatched but not confirmed \u2014 do not redeploy until the run succeeds"
13832
+ };
13833
+ }
13572
13834
  function tenantControlWatches(action) {
13573
- return action === "status" || action === "retire" || action === "verify-secrets";
13835
+ return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker";
13574
13836
  }
13575
13837
  async function runTenantControl(deps, options) {
13576
13838
  const { repo, stage, action } = options;
@@ -13585,7 +13847,7 @@ async function runTenantControl(deps, options) {
13585
13847
  dispatched: false,
13586
13848
  conclusion: "failure",
13587
13849
  category: action === "retire" ? transport ? "transport-failed" : "dispatch-rejected" : void 0,
13588
- note: transport ? `tenant control ${action} dispatch failed (transport) \u2014 safe to retry` : `tenant control ${action} rejected: ${d.error ?? "request rejected by the Hub"}`
13850
+ note: transport ? `runtime tenant control ${action} dispatch failed (transport) \u2014 safe to retry` : `runtime tenant control ${action} rejected: ${d.error ?? "request rejected by the Hub"}`
13589
13851
  };
13590
13852
  }
13591
13853
  const { runId, runUrl } = await correlateControlRun(deps, since, [stage, action]);
@@ -13594,20 +13856,23 @@ async function runTenantControl(deps, options) {
13594
13856
  if (action === "retire") {
13595
13857
  result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
13596
13858
  }
13597
- if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets")) {
13859
+ if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker")) {
13598
13860
  const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
13599
13861
  if (action === "status") {
13600
13862
  result.serviceState = parseStatusSnippet(output).serviceState;
13601
- } else {
13863
+ } else if (action === "verify-secrets") {
13602
13864
  result.secrets = parseVerifySecrets(output);
13603
13865
  result.secretsRaw = output;
13866
+ } else {
13867
+ result.broker = parseVerifyBroker(output);
13868
+ result.brokerRaw = output;
13604
13869
  }
13605
13870
  }
13606
13871
  result.note = conclusion === "success" ? `tenant-control ${action} run succeeded` : conclusion === "failure" ? `tenant-control ${action} run failed \u2014 inspect the run` : runId == null ? `dispatched tenant-control.yml (${action}) \u2014 run not correlated; check the Actions tab` : `dispatched tenant-control.yml (${action}) \u2014 not watched`;
13607
13872
  return result;
13608
13873
  }
13609
13874
  function renderTenantControl(r) {
13610
- const head = `tenant control ${r.repo} ${r.stage} ${r.action}: ${r.conclusion}${r.category ? ` (${r.category})` : ""}`;
13875
+ const head = `runtime tenant control ${r.repo} ${r.stage} ${r.action}: ${r.conclusion}${r.category ? ` (${r.category})` : ""}`;
13611
13876
  const lines = [head];
13612
13877
  if (r.runUrl) lines.push(` run: ${r.runUrl}`);
13613
13878
  if (r.serviceState) lines.push(` serviceState: ${r.serviceState}`);
@@ -13650,6 +13915,26 @@ function renderVerifySecrets(body) {
13650
13915
  return { lines, failure: null };
13651
13916
  }
13652
13917
 
13918
+ // src/tenant-verify-broker.ts
13919
+ function renderVerifyBroker(input) {
13920
+ if (input.broker.length === 0) {
13921
+ const raw = input.raw?.trim();
13922
+ if (raw?.startsWith("no broker-readable runtime secrets declared")) return { lines: [raw], failure: null };
13923
+ return {
13924
+ lines: raw ? raw.split("\n") : ["verify-broker: no per-key verdicts returned"],
13925
+ failure: "verify-broker returned no per-key verdicts, so broker access was not verified"
13926
+ };
13927
+ }
13928
+ const lines = input.broker.map((item) => `${item.key}: ${item.status}`);
13929
+ const reachable = input.broker.filter((item) => item.status === "reachable").length;
13930
+ const bad = input.broker.length - reachable;
13931
+ lines.push(`verify-broker: ${reachable} reachable, ${bad} failed`);
13932
+ return {
13933
+ lines,
13934
+ failure: bad > 0 ? `${bad} of ${input.broker.length} broker read(s) failed; redeploy refreshes an expired runtime token` : null
13935
+ };
13936
+ }
13937
+
13653
13938
  // src/hotfix-coverage.ts
13654
13939
  var import_node_child_process8 = require("node:child_process");
13655
13940
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
@@ -13768,7 +14053,7 @@ function pickRepo(p) {
13768
14053
  }
13769
14054
  async function sweepRcOrphans(deps, opts) {
13770
14055
  const projects = await deps.listProjects();
13771
- if (!projects) throw new Error("project list unavailable \u2014 Hub unreachable or this repo is not bootstrapped");
14056
+ if (!projects) throw new Error("org project list unavailable \u2014 Hub unreachable or this repo is not bootstrapped");
13772
14057
  const targets = projects.filter(isRcBearingTenant);
13773
14058
  const stages = [];
13774
14059
  for (const p of targets) {
@@ -13805,7 +14090,7 @@ async function sweepRcOrphans(deps, opts) {
13805
14090
  };
13806
14091
  }
13807
14092
  function renderSweep(r) {
13808
- const lines = [`tenant sweep-rc: scanned ${r.scanned} tenant-container(s); ${r.running} rc runtime(s) running`];
14093
+ const lines = [`runtime tenant sweep-rc: scanned ${r.scanned} tenant-container(s); ${r.running} rc runtime(s) running`];
13809
14094
  for (const s of r.stages) {
13810
14095
  let line = ` ${s.repo} rc: ${s.orphanCandidate ? "RUNNING" : s.serviceState}`;
13811
14096
  if (s.retired) line += ` -> ${s.retired}${s.category ? ` (${s.category}${s.reason ? `/${s.reason}` : ""})` : ""}`;
@@ -13813,7 +14098,7 @@ function renderSweep(r) {
13813
14098
  lines.push(line);
13814
14099
  }
13815
14100
  if (r.running > 0 && !r.retireAttempted) {
13816
- lines.push("Retire an orphan with: mmi-cli tenant control <owner/repo> rc retire (or sweep all running: mmi-cli tenant sweep-rc --retire --yes)");
14101
+ lines.push("Retire an orphan with: mmi-cli runtime tenant control <owner/repo> rc retire (or sweep all running: mmi-cli runtime tenant sweep-rc --retire --yes)");
13817
14102
  }
13818
14103
  const undetermined = r.stages.filter((s) => s.serviceState === "unknown").length;
13819
14104
  if (undetermined > 0) {
@@ -14634,8 +14919,8 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
14634
14919
  repo: OWNER2,
14635
14920
  kind: "empty-target-inventory",
14636
14921
  severity: "high",
14637
- detail: "access audit received an empty repo inventory \u2014 zero repos to audit; a green audit of nothing is a false pass",
14638
- remediation: `confirm the Hub registry is reachable and returns projects, then re-run \`mmi-cli access audit\`; or audit one repo explicitly with \`mmi-cli access audit --repo <owner/repo>\``
14922
+ detail: "org access audit received an empty repo inventory \u2014 zero repos to audit; a green audit of nothing is a false pass",
14923
+ remediation: `confirm the Hub registry is reachable and returns projects, then re-run \`mmi-cli org access audit\`; or audit one repo explicitly with \`mmi-cli org access audit --repo <owner/repo>\``
14639
14924
  }],
14640
14925
  repos: []
14641
14926
  };
@@ -14650,7 +14935,7 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
14650
14935
  kind: "owner-baseline-unavailable",
14651
14936
  severity: "medium",
14652
14937
  detail: `could not resolve org owners (role=admin) from the GitHub API (${e.message}); the overgrant audit needs this baseline and was skipped to avoid false HIGH findings \u2014 re-run when the members API is reachable`,
14653
- remediation: `confirm the token can read GET /orgs/${OWNER2}/members?role=admin, then re-run \`mmi-cli access audit\``
14938
+ remediation: `confirm the token can read GET /orgs/${OWNER2}/members?role=admin, then re-run \`mmi-cli org access audit\``
14654
14939
  });
14655
14940
  return { ok: degraded.every((f) => f.severity !== "high"), owners: [], orgFindings: degraded, repos: [] };
14656
14941
  }
@@ -14672,14 +14957,10 @@ function loadAccessTargets(projectsJson) {
14672
14957
  const seen = /* @__PURE__ */ new Set();
14673
14958
  const targets = [];
14674
14959
  for (const project2 of projects) {
14675
- const embeddedContent = new Set(
14676
- (project2.fanoutTargets ?? []).filter((r) => r.class === "content" && r.repo).map((r) => r.repo.toLowerCase())
14677
- );
14678
14960
  for (const repo of project2.repos ?? []) {
14679
14961
  if (seen.has(repo)) continue;
14680
14962
  seen.add(repo);
14681
- const repoName = repo.split("/").pop()?.toLowerCase() ?? repo.toLowerCase();
14682
- const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) ? "content" : "deployable";
14963
+ const cls = project2.class === "content" || project2.deployModel === "content" ? "content" : "deployable";
14683
14964
  const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
14684
14965
  targets.push({ repo, class: cls, releaseTrack });
14685
14966
  }
@@ -14695,7 +14976,7 @@ function resolveOrgAuditTargets(registryProjects) {
14695
14976
  kind: "registry-unreadable",
14696
14977
  severity: "high",
14697
14978
  detail: "the project registry SSOT (Hub API) was unreachable or unconfigured \u2014 the audit target set is unknown; auditing the stale committed projects.json instead could pass green while missing live repos",
14698
- remediation: `confirm the Hub API base URL + token are configured and reachable, then re-run \`mmi-cli access audit\`; or audit one repo explicitly with \`mmi-cli access audit --repo <owner/repo>\``
14979
+ remediation: `confirm the Hub API base URL + token are configured and reachable, then re-run \`mmi-cli org access audit\`; or audit one repo explicitly with \`mmi-cli org access audit --repo <owner/repo>\``
14699
14980
  }
14700
14981
  };
14701
14982
  }
@@ -14740,7 +15021,7 @@ function dataAccessContractsFromProjects(projects) {
14740
15021
  return { consumers };
14741
15022
  }
14742
15023
  function renderAccessReport(report) {
14743
- const lines = [`mmi-cli access audit: ${report.ok ? "OK" : "CHECK"} (owners: ${report.owners.map((o) => "@" + o).join(", ") || "none"})`];
15024
+ const lines = [`mmi-cli org access audit: ${report.ok ? "OK" : "CHECK"} (owners: ${report.owners.map((o) => "@" + o).join(", ") || "none"})`];
14744
15025
  for (const finding of report.orgFindings ?? []) {
14745
15026
  lines.push(` [${finding.severity}] ${finding.kind}: ${finding.detail}`);
14746
15027
  if (finding.remediation) lines.push(` ${finding.remediation}`);
@@ -14759,9 +15040,13 @@ function renderAccessReport(report) {
14759
15040
  var import_node_fs16 = require("node:fs");
14760
15041
  var import_node_path14 = require("node:path");
14761
15042
  var DOCS_INDEX_PATH = "docs/index.md";
15043
+ function isRoutableDocsPath(relPath) {
15044
+ const normalized = relPath.replace(/\\/g, "/");
15045
+ return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
15046
+ }
14762
15047
  var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
14763
- function escapeCell(text) {
14764
- return text.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
15048
+ function plainInline(text) {
15049
+ return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
14765
15050
  }
14766
15051
  function extractDocMeta(relPath, raw) {
14767
15052
  const lines = raw.split(/\r?\n/);
@@ -14789,7 +15074,7 @@ function extractDocMeta(relPath, raw) {
14789
15074
  break;
14790
15075
  }
14791
15076
  if (trimmed.startsWith("#")) continue;
14792
- scope = escapeCell(trimmed);
15077
+ scope = plainInline(trimmed);
14793
15078
  break;
14794
15079
  }
14795
15080
  if (!title) {
@@ -14801,16 +15086,16 @@ function extractDocMeta(relPath, raw) {
14801
15086
  function renderDocsIndex(entries) {
14802
15087
  const lines = [
14803
15088
  GENERATED_HEADER,
15089
+ "",
14804
15090
  "# Documentation index",
14805
15091
  "",
14806
15092
  "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
14807
15093
  "can never diverge from the records it lists.",
14808
- "",
14809
- "| Document | Scope |",
14810
- "| --- | --- |"
15094
+ ""
14811
15095
  ];
14812
15096
  for (const entry of entries) {
14813
- lines.push(`| [${escapeCell(entry.title)}](${entry.path}) | ${entry.scope} |`);
15097
+ const link = `[${plainInline(entry.title)}](${entry.path})`;
15098
+ lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
14814
15099
  }
14815
15100
  return lines.join("\n") + "\n";
14816
15101
  }
@@ -14846,7 +15131,7 @@ function createDocsIndexDeps(repoRoot2) {
14846
15131
  const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
14847
15132
  const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
14848
15133
  return {
14849
- listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter((p) => p !== "index.md").sort() : [],
15134
+ listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
14850
15135
  readDoc: (relPath) => (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
14851
15136
  readIndex: () => (0, import_node_fs16.existsSync)(indexPath) ? (0, import_node_fs16.readFileSync)(indexPath, "utf8") : null,
14852
15137
  writeIndex: (content) => (0, import_node_fs16.writeFileSync)(indexPath, content, "utf8")
@@ -14880,20 +15165,20 @@ function docsAuditRecord(input) {
14880
15165
  const date = input.date.trim();
14881
15166
  const shaRange = input.shaRange.trim();
14882
15167
  const checkerVendor = input.checkerVendor.trim();
14883
- if (!repo) throw new Error("docs-audit record: repo is required");
14884
- if (!date) throw new Error("docs-audit record: date is required");
14885
- if (!isValidIsoDate(date)) throw new Error(`docs-audit record: date must be a real ISO date (YYYY-MM-DD), got "${date}"`);
14886
- if (!shaRange) throw new Error("docs-audit record: shaRange is required");
14887
- if (!checkerVendor) throw new Error("docs-audit record: checkerVendor is required");
15168
+ if (!repo) throw new Error("docs audit record: repo is required");
15169
+ if (!date) throw new Error("docs audit record: date is required");
15170
+ if (!isValidIsoDate(date)) throw new Error(`docs audit record: date must be a real ISO date (YYYY-MM-DD), got "${date}"`);
15171
+ if (!shaRange) throw new Error("docs audit record: shaRange is required");
15172
+ if (!checkerVendor) throw new Error("docs audit record: checkerVendor is required");
14888
15173
  if (input.outcome.kind === "refreshed" && !(Number.isInteger(input.outcome.count) && input.outcome.count > 0)) {
14889
- throw new Error("docs-audit record: a `refreshed` outcome must carry a positive integer count");
15174
+ throw new Error("docs audit record: a `refreshed` outcome must carry a positive integer count");
14890
15175
  }
14891
15176
  if (input.outcome.kind === "failed" && !input.outcome.reason.trim()) {
14892
- throw new Error("docs-audit record: a `failed` outcome must carry a reason");
15177
+ throw new Error("docs audit record: a `failed` outcome must carry a reason");
14893
15178
  }
14894
15179
  const outcome = serializeOutcome(input.outcome);
14895
15180
  if (!isValidOutcomeWire(outcome)) {
14896
- throw new Error(`docs-audit record: outcome serialized to an invalid wire form "${outcome}"`);
15181
+ throw new Error(`docs audit record: outcome serialized to an invalid wire form "${outcome}"`);
14897
15182
  }
14898
15183
  return { repo, date, shaRange, outcome, checkerVendor };
14899
15184
  }
@@ -14904,13 +15189,13 @@ function ageInDays(verdictDate, today) {
14904
15189
  }
14905
15190
  function docsAuditStatus(fetch2, opts) {
14906
15191
  if ("notArmed" in fetch2) {
14907
- return { ok: true, state: "not-armed", line: `docs-audit: ${opts.repo} janitor not armed (registry route not live yet)` };
15192
+ return { ok: true, state: "not-armed", line: `docs audit: ${opts.repo} janitor not armed (registry route not live yet)` };
14908
15193
  }
14909
15194
  if (!fetch2.ok) {
14910
- return { ok: false, state: "error", line: `docs-audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
15195
+ return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
14911
15196
  }
14912
15197
  if (fetch2.verdict === null) {
14913
- return { ok: false, state: "missing", line: `docs-audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
15198
+ return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
14914
15199
  }
14915
15200
  const verdict = fetch2.verdict;
14916
15201
  for (const field of ["repo", "shaRange", "checkerVendor"]) {
@@ -14920,7 +15205,7 @@ function docsAuditStatus(fetch2, opts) {
14920
15205
  return {
14921
15206
  ok: false,
14922
15207
  state: "malformed",
14923
- line: `docs-audit: ${opts.repo} malformed verdict ${field} (${shown}, expected a non-empty string) \u2014 registry record corrupt, treating as RED`
15208
+ line: `docs audit: ${opts.repo} malformed verdict ${field} (${shown}, expected a non-empty string) \u2014 registry record corrupt, treating as RED`
14924
15209
  };
14925
15210
  }
14926
15211
  }
@@ -14928,450 +15213,29 @@ function docsAuditStatus(fetch2, opts) {
14928
15213
  return {
14929
15214
  ok: false,
14930
15215
  state: "malformed",
14931
- line: `docs-audit: ${opts.repo} malformed verdict date "${verdict.date}" \u2014 registry record corrupt, treating as RED`
15216
+ line: `docs audit: ${opts.repo} malformed verdict date "${verdict.date}" \u2014 registry record corrupt, treating as RED`
14932
15217
  };
14933
15218
  }
14934
15219
  if (!isValidOutcomeWire(verdict.outcome)) {
14935
15220
  return {
14936
15221
  ok: false,
14937
15222
  state: "malformed",
14938
- line: `docs-audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
15223
+ line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
14939
15224
  };
14940
15225
  }
14941
15226
  if (!isValidIsoDate(opts.today)) {
14942
- throw new Error(`docs-audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
15227
+ throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
14943
15228
  }
14944
15229
  const cadence = opts.cadenceDays ?? 7;
14945
15230
  const grace = opts.graceDays ?? 3;
14946
15231
  const age = ageInDays(verdict.date, opts.today);
14947
15232
  if (age > cadence + grace) {
14948
- return { ok: false, state: "stale", line: `docs-audit: ${opts.repo} last verdict ${age}d old \u2014 janitor blind or dead` };
15233
+ return { ok: false, state: "stale", line: `docs audit: ${opts.repo} last verdict ${age}d old \u2014 janitor blind or dead` };
14949
15234
  }
14950
15235
  if (verdict.outcome === "failed") {
14951
- return { ok: false, state: "failed", line: `docs-audit: ${opts.repo} last run FAILED (${verdict.date}) \u2014 janitor needs attention` };
14952
- }
14953
- return { ok: true, state: "clean", line: `docs-audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
14954
- }
14955
-
14956
- // src/project-readiness.ts
14957
- function stagesForTrack(meta) {
14958
- return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
14959
- }
14960
- function stageInTrack(meta, stage) {
14961
- return stagesForTrack(meta).includes(stage);
14962
- }
14963
- function dnsErrorToResolution(code) {
14964
- return code === "ENOTFOUND" || code === "EAI_NONAME" ? false : void 0;
14965
- }
14966
- var STAGES = ["dev", "rc", "main"];
14967
- var DEFAULT_RUNTIME_SECRET_NAMES2 = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
14968
- function boardRegistryGaps(meta) {
14969
- if (!meta?.projectId) return [];
14970
- if (meta.projectNumber != null) return [];
14971
- return ["projectNumber"];
14972
- }
14973
- function previewRegistryMetaMerge(existing, patch) {
14974
- const out = { ...existing ?? {} };
14975
- for (const [key, value] of Object.entries(patch)) {
14976
- if (value === null) delete out[key];
14977
- else out[key] = value;
14978
- }
14979
- return out;
14980
- }
14981
- function boardLinkWriteError(patch, existing) {
14982
- const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
14983
- const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
14984
- if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
14985
- return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
14986
- }
14987
- if (patch.projectNumber === null && boardRegistryGaps(previewRegistryMetaMerge(existing, patch)).length) {
14988
- return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
14989
- }
14990
- return null;
14991
- }
14992
- function boardRegistryGapMessage(repo) {
14993
- 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)`;
14994
- }
14995
- var slugOfRepo = slugOf;
14996
- function repoFrom(repoOrSlug, slug) {
14997
- return repoOrSlug.includes("/") ? repoOrSlug : `mutmutco/${slug}`;
14998
- }
14999
- function defaultSubdomain(slug) {
15000
- return slug.replace(/^[a-z]+-/, "");
15001
- }
15002
- function projectRequiresGoogleOAuth(meta, model) {
15003
- if (!meta || model === "content" || model === "none" || meta.deployModel === "content" || meta.deployModel === "none" || meta.class === "content") return false;
15004
- const projectType = resolveProjectType(meta, meta.repos?.[0] ?? "");
15005
- if (projectType !== "web-app") return false;
15006
- return Boolean(meta.oauth && typeof meta.oauth === "object");
15007
- }
15008
- function declaresNoPublicEdge(meta) {
15009
- const ed = meta?.edgeDomains;
15010
- return Boolean(ed && typeof ed === "object" && !Array.isArray(ed) && Object.keys(ed).length === 0);
15011
- }
15012
- function declaresWorker(meta) {
15013
- return meta?.projectType === "worker";
15014
- }
15015
- function isCentralContainerModel(model) {
15016
- return model === "tenant-container" || model === "solo-container";
15017
- }
15018
- function isNoEdgeTenantWorker(meta, model) {
15019
- return isCentralContainerModel(model) && (declaresNoPublicEdge(meta) || declaresWorker(meta));
15020
- }
15021
- function projectRequiresDeployCoords(model, stage, meta) {
15022
- if (!isCentralContainerModel(model)) return false;
15023
- if (stage && isNoEdgeTenantWorker(meta, model)) return stage === "main";
15024
- return true;
15025
- }
15026
- function projectRequiresDeployState(model, stage) {
15027
- return model === "hub-serverless" && stage !== "dev";
15028
- }
15029
- function stageRequiredSecrets(stage, meta) {
15030
- const contract = meta.requiredRuntimeSecrets;
15031
- const extra = !Array.isArray(contract) && Array.isArray(contract?.[stage]) ? contract[stage] ?? [] : [];
15032
- const model = resolveDeployModel(meta, meta.repos?.[0] ?? "");
15033
- if (isNoEdgeTenantWorker(meta, model) && stage !== "main") return [];
15034
- const defaults = projectRequiresGoogleOAuth(meta, model) ? DEFAULT_RUNTIME_SECRET_NAMES2 : [];
15035
- return [.../* @__PURE__ */ new Set([...defaults, ...extra])];
15036
- }
15037
- function stageKey2(stage, key) {
15038
- return key.includes("/") ? key : `${stage}/${key}`;
15039
- }
15040
- function materializedRuntimeSecretName(entry) {
15041
- return entry.includes(":") ? entry.split(":").pop() : entry;
15042
- }
15043
- function runtimeSecretStreamGap(stage, meta, presentSecrets) {
15044
- const streamed = new Set(
15045
- (contractByStage(meta.requiredRuntimeSecrets)[stage] ?? []).map(materializedRuntimeSecretName)
15046
- );
15047
- const seen = /* @__PURE__ */ new Set();
15048
- const gap = [];
15049
- for (const name of stageRequiredSecrets(stage, meta).map(materializedRuntimeSecretName)) {
15050
- if (seen.has(name)) continue;
15051
- seen.add(name);
15052
- if (!streamed.has(name) && (presentSecrets.has(stageKey2(stage, name)) || presentSecrets.has(name))) gap.push(name);
15053
- }
15054
- return gap;
15055
- }
15056
- function hasRuntimeSecretContract(contract) {
15057
- if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
15058
- return ["dev", "rc", "main"].some((stage) => Array.isArray(contract[stage]));
15059
- }
15060
- function appAttestationOf(meta) {
15061
- const a = meta?.appAttested;
15062
- if (!a || typeof a !== "object" || Array.isArray(a)) return null;
15063
- const { at, by } = a;
15064
- return typeof at === "string" && at.length > 0 && typeof by === "string" && by.length > 0 ? { at, by } : null;
15065
- }
15066
- function attestedLine(att) {
15067
- 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 project attest\` after app-owned structural changes.`;
15068
- }
15069
- 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: [] }).";
15070
- function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
15071
- const attested = appAttestationOf(meta);
15072
- 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";
15073
- const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
15074
- if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
15075
- if (projectType === "content" || model === "content") return ["Content repo: keep app-owned work to docs/content changes; release train does not apply."];
15076
- if (projectType === "desktop-app") {
15077
- return [
15078
- "Desktop app: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned installer/packaging outside the tenant train.",
15079
- "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."
15080
- ];
15081
- }
15082
- if (projectType === "desktop-game") {
15083
- return [
15084
- "Desktop game: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned release packaging outside the tenant train.",
15085
- "Keep README.md and architecture.md explicit about the game build/release path so readiness does not guess web infra."
15086
- ];
15087
- }
15088
- if (projectType === "non-deployable" || model === "none") {
15089
- return [
15090
- "Non-deployable repo: no Hub deploy coords, Google OAuth, DWD, or tenant control by default.",
15091
- "Keep README.md and architecture.md explicit about the repo purpose and any project-admin workflow it still needs."
15092
- ];
15093
- }
15094
- if (model === "hub-serverless") {
15095
- return [
15096
- "Hub serverless deploy stays on the Hub-owned workflows; deploy.yml stamps per-stage deploy state onto state-only DEPLOY# rows (no tenant coords).",
15097
- "Make runtime config fail clearly for missing required env in prod/rc instead of relying on hidden defaults."
15098
- ];
15099
- }
15100
- if (model === "serverless") {
15101
- return [
15102
- "Serverless deploys use the central serverless path; do not add tenant compose or box plumbing.",
15103
- "Make runtime config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
15104
- "Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
15105
- ];
15106
- }
15107
- if (projectType === "cli-tool" || model === "registry-publish") {
15108
- return [
15109
- "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.",
15110
- "Keep the published version in lockstep with the release tag; make the publish idempotent (skip when the version is already on the registry).",
15111
- "Keep app-owned README.md and architecture.md aligned with the registry-publish release path."
15112
- ];
15113
- }
15114
- const gaps = [
15115
- '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.',
15116
- "Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
15117
- "Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
15118
- ];
15119
- if (isNoEdgeTenantWorker(meta, model)) {
15120
- 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.");
15121
- }
15122
- if (contractUndeclared) {
15123
- gaps.unshift(CONTRACT_UNDECLARED_LINE);
15124
- }
15125
- if (slug === "mmi-katip") {
15126
- 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.");
15127
- }
15128
- if (model === "solo-container" || model === "tenant-container") {
15129
- if (typeof mainDeployFact?.port === "number") {
15130
- 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).`);
15131
- } else if (!appAttestationOf(meta)) {
15132
- 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.");
15133
- }
15134
- }
15135
- if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
15136
- return gaps;
15137
- }
15138
- function edgeDomainsByStage(meta) {
15139
- const ed = meta?.edgeDomains;
15140
- if (!ed || typeof ed !== "object" || Array.isArray(ed)) return {};
15141
- const out = {};
15142
- for (const stage of STAGES) {
15143
- const v = ed[stage];
15144
- if (typeof v === "string" && v.trim()) out[stage] = v.trim();
15145
- }
15146
- return out;
15147
- }
15148
- async function probeEdgeDomains(meta, resolveDns) {
15149
- const byStage = edgeDomainsByStage(meta);
15150
- const results = await Promise.all(
15151
- Object.entries(byStage).map(async ([stage, host]) => {
15152
- let resolved;
15153
- try {
15154
- resolved = await resolveDns(host);
15155
- } catch {
15156
- resolved = void 0;
15157
- }
15158
- return resolved === false ? { stage, host } : void 0;
15159
- })
15160
- );
15161
- return results.filter((r) => r !== void 0);
15162
- }
15163
- function contractByStage(contract) {
15164
- return contract && !Array.isArray(contract) ? contract : {};
15165
- }
15166
- function ensureStageNames(names, required) {
15167
- return [.../* @__PURE__ */ new Set([...names ?? [], ...required])];
15168
- }
15169
- function sameNames(a, b) {
15170
- const left = new Set(a ?? []);
15171
- const right = new Set(b ?? []);
15172
- if (left.size !== right.size) return false;
15173
- return [...left].every((name) => right.has(name));
15174
- }
15175
- function sameStageContract(a, b) {
15176
- return STAGES.every((stage) => sameNames(a[stage], b[stage]));
15177
- }
15178
- function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
15179
- const slug = slugOfRepo(repoOrSlug);
15180
- const repo = repoFrom(repoOrSlug, slug);
15181
- const sub = defaultSubdomain(slug);
15182
- const patch = {};
15183
- const confidentType = resolveProjectTypeConfident(meta, repo);
15184
- const projectType = confidentType ?? resolveProjectType(meta, repo);
15185
- const model = resolveDeployModel(meta, repo);
15186
- if (confidentType && !meta?.class) patch.class = confidentType === "content" ? "content" : "deployable";
15187
- if (confidentType) {
15188
- if (!meta?.projectType) patch.projectType = confidentType;
15189
- if (!meta?.deployModel) patch.deployModel = model;
15190
- }
15191
- if (!meta?.vaultPath) patch.vaultPath = `/mmi-future/${slug}`;
15192
- if (confidentType && !meta?.edgeDomains && model === "tenant-container" && projectType !== "worker") {
15193
- patch.edgeDomains = {
15194
- dev: `dev.${sub}.mutatismutandis.co`,
15195
- rc: `rc.${sub}.mutatismutandis.co`,
15196
- main: `${sub}.mutatismutandis.co`
15197
- };
15236
+ return { ok: false, state: "failed", line: `docs audit: ${opts.repo} last run FAILED (${verdict.date}) \u2014 janitor needs attention` };
15198
15237
  }
15199
- if (meta && projectTypeClearsWebProfile(projectType, model)) {
15200
- if (meta.oauth !== void 0) patch.oauth = null;
15201
- if (meta.edgeDomains !== void 0) patch.edgeDomains = null;
15202
- }
15203
- if (slug === "mmi-katip") {
15204
- const required = ["GOOGLE_IMPERSONATE_USER", "GOOGLE_ADMIN_USER"];
15205
- const current = contractByStage(meta?.requiredRuntimeSecrets);
15206
- const next = {
15207
- dev: ensureStageNames(current.dev, required),
15208
- rc: ensureStageNames(current.rc, required),
15209
- main: ensureStageNames(current.main, required)
15210
- };
15211
- if (!sameStageContract(current, next)) {
15212
- patch.requiredRuntimeSecrets = next;
15213
- }
15214
- }
15215
- const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli 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).`];
15216
- if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
15217
- return { slug, patch, appOwnedGaps };
15218
- }
15219
- async function runV2Heal(repoOrSlug, opts, deps) {
15220
- const slug = slugOfRepo(repoOrSlug);
15221
- const read = await deps.fetchProject(slug);
15222
- if (!read.ok) {
15223
- 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` };
15224
- }
15225
- const plan = buildV2HealPatch(repoOrSlug, read.project);
15226
- if (!opts.apply) return { status: "planned", slug, patch: plan.patch, appOwnedGaps: plan.appOwnedGaps };
15227
- if (!Object.keys(plan.patch).length) return { status: "noop", slug, appOwnedGaps: plan.appOwnedGaps };
15228
- const write = await deps.upsertProject(slug, plan.patch);
15229
- if (!write.ok) return { status: "write-failed", slug, write };
15230
- return { status: "applied", slug, applied: Object.keys(plan.patch), appOwnedGaps: plan.appOwnedGaps, result: write.body };
15231
- }
15232
- async function buildV2Doctor(repoOrSlug, deps) {
15233
- const slug = slugOfRepo(repoOrSlug);
15234
- const repo = repoFrom(repoOrSlug, slug);
15235
- const read = await deps.getProject(slug);
15236
- if (!read.ok) {
15237
- const degradedSecrets = {
15238
- dev: { required: [], present: [], missing: [] },
15239
- rc: { required: [], present: [], missing: [] },
15240
- main: { required: [], present: [], missing: [] }
15241
- };
15242
- const degradedStage = {
15243
- dev: { ok: false, required: false },
15244
- rc: { ok: false, required: false },
15245
- main: { ok: false, required: false }
15246
- };
15247
- return {
15248
- ok: false,
15249
- repo,
15250
- slug,
15251
- registryError: read.error,
15252
- hubOwned: { meta: { ok: false, missing: [] }, deployCoords: degradedStage, deployState: degradedStage, secrets: degradedSecrets },
15253
- autoHealAvailable: [],
15254
- 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 project doctor\` shortly.`]
15255
- };
15256
- }
15257
- const meta = read.project;
15258
- const projectType = resolveProjectType(meta, repo);
15259
- const model = resolveDeployModel(meta, repo);
15260
- const mainDeployFact = meta && deps.getDeployFact ? await deps.getDeployFact(slug, "main") : null;
15261
- const autoHeal = buildV2HealPatch(repo, meta, mainDeployFact);
15262
- if (!meta) {
15263
- const emptySecrets = {
15264
- dev: { required: [], present: [], missing: [] },
15265
- rc: { required: [], present: [], missing: [] },
15266
- main: { required: [], present: [], missing: [] }
15267
- };
15268
- const emptyCoords = {
15269
- dev: { ok: false, required: true },
15270
- rc: { ok: false, required: true },
15271
- main: { ok: false, required: true }
15272
- };
15273
- const emptyState = Object.fromEntries(STAGES.map((stage) => {
15274
- const required = projectRequiresDeployState(model, stage);
15275
- return [stage, { required, ok: !required }];
15276
- }));
15277
- return {
15278
- ok: false,
15279
- repo,
15280
- slug,
15281
- hubOwned: { meta: { ok: false, missing: [`PROJECT#${slug}/META`] }, deployCoords: emptyCoords, deployState: emptyState, secrets: emptySecrets },
15282
- autoHealAvailable: Object.keys(autoHeal.patch),
15283
- appOwnedGaps: autoHeal.appOwnedGaps
15284
- };
15285
- }
15286
- let presentSecrets = /* @__PURE__ */ new Set();
15287
- let secretsError;
15288
- try {
15289
- presentSecrets = new Set(await deps.listSecrets(repo));
15290
- } catch (e) {
15291
- secretsError = e?.message || "secrets list failed";
15292
- }
15293
- const deployCoords = Object.fromEntries(await Promise.all(STAGES.map(async (stage) => {
15294
- const required = stageInTrack(meta, stage) && projectRequiresDeployCoords(model, stage, meta);
15295
- return [stage, { required, ok: required ? await deps.hasDeployCoords(slug, stage) : true }];
15296
- })));
15297
- const deployState = Object.fromEntries(await Promise.all(STAGES.map(async (stage) => {
15298
- const required = stageInTrack(meta, stage) && projectRequiresDeployState(model, stage);
15299
- return [stage, { required, ok: required ? await deps.hasDeployState(slug, stage) : true }];
15300
- })));
15301
- const secrets = Object.fromEntries(STAGES.map((stage) => {
15302
- const names = stageInTrack(meta, stage) ? stageRequiredSecrets(stage, meta) : [];
15303
- const satisfied = (key) => presentSecrets.has(stageKey2(stage, key)) || !key.includes("/") && presentSecrets.has(key);
15304
- const required = names.map((key) => stageKey2(stage, key));
15305
- const present = names.filter(satisfied).map((key) => stageKey2(stage, key));
15306
- const missing = names.filter((key) => !satisfied(key)).map((key) => stageKey2(stage, key));
15307
- return [stage, { required, present, missing }];
15308
- }));
15309
- const runtimeSecretStreamWarnings = Object.fromEntries(STAGES.map((stage) => [
15310
- stage,
15311
- stageInTrack(meta, stage) ? runtimeSecretStreamGap(stage, meta, presentSecrets) : []
15312
- ]));
15313
- const runtimeSecretStreamWarningRows = STAGES.map((stage) => ({ stage, names: runtimeSecretStreamWarnings[stage] })).filter((row) => row.names.length > 0);
15314
- const metaMissing = ["class", "projectType", "deployModel", "vaultPath"].filter((key) => meta[key] === void 0).concat(boardRegistryGaps(meta));
15315
- const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets).every((v) => v.missing.length === 0);
15316
- const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
15317
- return {
15318
- ok,
15319
- repo,
15320
- slug,
15321
- class: meta.class,
15322
- projectType,
15323
- deployModel: model,
15324
- hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets },
15325
- secretsError,
15326
- autoHealAvailable: Object.keys(autoHeal.patch),
15327
- appOwnedGaps: autoHeal.appOwnedGaps,
15328
- ...edgeDomainWarnings.length ? { edgeDomainWarnings } : {},
15329
- ...runtimeSecretStreamWarningRows.length ? { runtimeSecretStreamWarnings: runtimeSecretStreamWarningRows } : {},
15330
- appAttested: appAttestationOf(meta) ?? void 0
15331
- };
15332
- }
15333
- function renderReadinessIssueBody(existingBody, report, opts = {}) {
15334
- const start = "<!-- mmi-v2-readiness:start -->";
15335
- const end = "<!-- mmi-v2-readiness:end -->";
15336
- const stageLines = STAGES.map((stage) => {
15337
- const coordState = report.hubOwned.deployCoords[stage];
15338
- const coords = !coordState.required ? "coords n/a" : coordState.ok ? "coords ok" : "coords missing/unverified";
15339
- const state = report.hubOwned.deployState[stage];
15340
- const statePart = !state.required ? [] : [state.ok ? "deploy state stamped" : "deploy state not stamped"];
15341
- const missing = report.hubOwned.secrets[stage].missing;
15342
- return `- ${stage}: ${[coords, ...statePart].join("; ")}; ${missing.length ? `missing ${missing.join(", ")}` : "required secret names present"}`;
15343
- });
15344
- const section = [
15345
- start,
15346
- "## Hub v2 readiness",
15347
- "",
15348
- `Repo: ${report.repo}`,
15349
- `Project type: ${report.projectType ?? "(unresolved)"}`,
15350
- `Deploy model: ${report.deployModel ?? "(unresolved)"}`,
15351
- `Overall: ${report.ok ? "ready" : "not ready"}`,
15352
- "",
15353
- "### Hub-owned diagnosis",
15354
- `- META: ${report.hubOwned.meta.ok ? "ok" : `missing ${report.hubOwned.meta.missing.join(", ")}`}`,
15355
- ...stageLines,
15356
- ...report.secretsError ? [`- secrets UNVERIFIED (treated as not ready): ${report.secretsError}`] : [],
15357
- ...(report.edgeDomainWarnings ?? []).map(
15358
- (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`
15359
- ),
15360
- ...(report.runtimeSecretStreamWarnings ?? []).map(
15361
- (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`
15362
- ),
15363
- "",
15364
- "### Auto-heal applied / available",
15365
- ...opts.healed?.length ? opts.healed.map((x) => `- ${x}`) : report.autoHealAvailable.map((x) => `- ${x}`),
15366
- "",
15367
- "### App-owned implementation plan",
15368
- ...report.appOwnedGaps.map((x) => `- ${x}`),
15369
- end
15370
- ].join("\n");
15371
- const re = new RegExp(`${start}[\\s\\S]*?${end}`);
15372
- return re.test(existingBody) ? existingBody.replace(re, () => section) : `${existingBody.trim()}
15373
-
15374
- ${section}`.trim();
15238
+ return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
15375
15239
  }
15376
15240
 
15377
15241
  // src/oauth.ts
@@ -15381,7 +15245,7 @@ var ENV_PREFIXES = ["", "dev", "rc"];
15381
15245
  var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
15382
15246
  var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
15383
15247
  var uniq = (xs) => [...new Set(xs)];
15384
- function defaultSubdomain2(slug) {
15248
+ function defaultSubdomain(slug) {
15385
15249
  const i = slug.indexOf("-");
15386
15250
  return i === -1 ? slug : slug.slice(i + 1);
15387
15251
  }
@@ -15434,7 +15298,7 @@ function parseOauthConfig(mmiConfig, slug) {
15434
15298
  throw new Error("oauth must be an object when configured");
15435
15299
  }
15436
15300
  const raw = rawUnknown;
15437
- const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
15301
+ const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain(slug)];
15438
15302
  const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
15439
15303
  const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
15440
15304
  if (!callbackPath.startsWith("/")) {
@@ -15445,7 +15309,7 @@ function parseOauthConfig(mmiConfig, slug) {
15445
15309
  }
15446
15310
  const meta = mmiConfig ?? {};
15447
15311
  const rawFofuSub = raw.fofuSubdomain;
15448
- const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
15312
+ const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain(slug) : void 0;
15449
15313
  return { subdomains, domains, callbackPath, fofuSubdomain };
15450
15314
  }
15451
15315
  function probeRedirectUri(callbackPath, port = 9123) {
@@ -15473,42 +15337,62 @@ var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
15473
15337
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
15474
15338
  var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
15475
15339
  var BUILD_SECRET_REF_RE = /^(?:[A-Z_][A-Z0-9_]*=)?(?:[A-Z_][A-Z0-9_]*|(?:mm-fofu|_org\/[a-z0-9][a-z0-9-]*):[A-Z_][A-Z0-9_]*|@github-packages-token)$/;
15340
+ function previewRegistryMetaMerge(existing, patch) {
15341
+ const out = { ...existing ?? {} };
15342
+ for (const [key, value] of Object.entries(patch)) {
15343
+ if (value === null) delete out[key];
15344
+ else out[key] = value;
15345
+ }
15346
+ return out;
15347
+ }
15348
+ function boardLinkWriteError(patch, existing) {
15349
+ const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
15350
+ const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
15351
+ if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
15352
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
15353
+ }
15354
+ const preview = previewRegistryMetaMerge(existing, patch);
15355
+ if (patch.projectNumber === null && preview.projectId && preview.projectNumber == null) {
15356
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
15357
+ }
15358
+ return null;
15359
+ }
15476
15360
  function parseSecretsCatalogVar(raw) {
15477
15361
  let parsed;
15478
15362
  try {
15479
15363
  parsed = JSON.parse(raw);
15480
15364
  } catch {
15481
- throw new Error('project set: secrets must be JSON keyed by KEY, e.g. {"SCRAPER_API_KEY":{"key":"SCRAPER_API_KEY","purpose":"scrape provider key","group":"scraper","owner":"oguz","stages":[],"consumers":["box"]}}');
15365
+ throw new Error('org project set: secrets must be JSON keyed by KEY, e.g. {"SCRAPER_API_KEY":{"key":"SCRAPER_API_KEY","purpose":"scrape provider key","group":"scraper","owner":"oguz","stages":[],"consumers":["box"]}}');
15482
15366
  }
15483
15367
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15484
- throw new Error("project set: secrets must be a map keyed by canonical KEY");
15368
+ throw new Error("org project set: secrets must be a map keyed by canonical KEY");
15485
15369
  }
15486
15370
  const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
15487
15371
  const overrideMapKey = /^([A-Z_][A-Z0-9_]*)@(dev|rc|main)$/;
15488
15372
  for (const [mapKey, value] of Object.entries(parsed)) {
15489
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`project set: secrets["${mapKey}"] must be an object`);
15373
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`org project set: secrets["${mapKey}"] must be an object`);
15490
15374
  const e = value;
15491
15375
  if (typeof e.key !== "string" || !SECRET_ENV_NAME_RE.test(e.key)) {
15492
- throw new Error(`project set: secrets["${mapKey}"].key must be an env-name (UPPER_SNAKE)`);
15376
+ throw new Error(`org project set: secrets["${mapKey}"].key must be an env-name (UPPER_SNAKE)`);
15493
15377
  }
15494
15378
  const override = overrideMapKey.exec(mapKey);
15495
15379
  if (override) {
15496
15380
  if (e.key !== override[1] || !Array.isArray(e.stages) || e.stages.length !== 1 || e.stages[0] !== override[2]) {
15497
- throw new Error(`project set: secrets["${mapKey}"] is an override entry (#2528) \u2014 key must be ${override[1]} and stages exactly ["${override[2]}"]`);
15381
+ throw new Error(`org project set: secrets["${mapKey}"] is an override entry (#2528) \u2014 key must be ${override[1]} and stages exactly ["${override[2]}"]`);
15498
15382
  }
15499
15383
  } else if (e.key !== mapKey) {
15500
- throw new Error(`project set: secrets["${mapKey}"].key must equal the map key (or use the KEY@<stage> override form, #2528)`);
15384
+ throw new Error(`org project set: secrets["${mapKey}"].key must equal the map key (or use the KEY@<stage> override form, #2528)`);
15501
15385
  }
15502
- if (!nonEmpty(e.purpose) || !nonEmpty(e.group) || !nonEmpty(e.owner)) throw new Error(`project set: secrets["${mapKey}"] needs non-empty purpose, group, owner`);
15503
- if (e.provider !== void 0 && !nonEmpty(e.provider)) throw new Error(`project set: secrets["${mapKey}"].provider must be a non-empty string`);
15386
+ if (!nonEmpty(e.purpose) || !nonEmpty(e.group) || !nonEmpty(e.owner)) throw new Error(`org project set: secrets["${mapKey}"] needs non-empty purpose, group, owner`);
15387
+ if (e.provider !== void 0 && !nonEmpty(e.provider)) throw new Error(`org project set: secrets["${mapKey}"].provider must be a non-empty string`);
15504
15388
  if (!Array.isArray(e.stages) || e.stages.some((s) => !RUNTIME_SECRET_STAGES.includes(s))) {
15505
- throw new Error(`project set: secrets["${mapKey}"].stages must be a subset of dev/rc/main ([] = shared)`);
15389
+ throw new Error(`org project set: secrets["${mapKey}"].stages must be a subset of dev/rc/main ([] = shared)`);
15506
15390
  }
15507
15391
  if (!Array.isArray(e.consumers) || e.consumers.some((c) => !SECRET_CONSUMERS.includes(c))) {
15508
- throw new Error(`project set: secrets["${mapKey}"].consumers must be a subset of ${SECRET_CONSUMERS.join("/")}`);
15392
+ throw new Error(`org project set: secrets["${mapKey}"].consumers must be a subset of ${SECRET_CONSUMERS.join("/")}`);
15509
15393
  }
15510
15394
  if (e.aliases !== void 0 && (!Array.isArray(e.aliases) || e.aliases.some((a) => typeof a !== "string"))) {
15511
- throw new Error(`project set: secrets["${mapKey}"].aliases must be a string array`);
15395
+ throw new Error(`org project set: secrets["${mapKey}"].aliases must be a string array`);
15512
15396
  }
15513
15397
  }
15514
15398
  return parsed;
@@ -15518,19 +15402,19 @@ function parseRuntimeSecretsVar(raw) {
15518
15402
  try {
15519
15403
  parsed = JSON.parse(raw);
15520
15404
  } catch {
15521
- throw new Error('project set: requiredRuntimeSecrets must be JSON, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}');
15405
+ throw new Error('org project set: requiredRuntimeSecrets must be JSON, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}');
15522
15406
  }
15523
15407
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15524
- throw new Error("project set: requiredRuntimeSecrets must be a stage map (a flat array is not box-loadable)");
15408
+ throw new Error("org project set: requiredRuntimeSecrets must be a stage map (a flat array is not box-loadable)");
15525
15409
  }
15526
15410
  const map = parsed;
15527
15411
  const out = {};
15528
15412
  for (const [stage, names] of Object.entries(map)) {
15529
15413
  if (!RUNTIME_SECRET_STAGES.includes(stage)) {
15530
- throw new Error(`project set: requiredRuntimeSecrets stage "${stage}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
15414
+ throw new Error(`org project set: requiredRuntimeSecrets stage "${stage}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
15531
15415
  }
15532
15416
  if (!Array.isArray(names) || names.some((n) => typeof n !== "string" || !n.trim())) {
15533
- throw new Error(`project set: requiredRuntimeSecrets.${stage} must be an array of non-empty secret names`);
15417
+ throw new Error(`org project set: requiredRuntimeSecrets.${stage} must be an array of non-empty secret names`);
15534
15418
  }
15535
15419
  out[stage] = names;
15536
15420
  }
@@ -15541,13 +15425,13 @@ function parseBuildSecretsVar(raw) {
15541
15425
  try {
15542
15426
  parsed = JSON.parse(raw);
15543
15427
  } catch {
15544
- throw new Error('project set: requiredBuildSecrets must be JSON, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]');
15428
+ throw new Error('org project set: requiredBuildSecrets must be JSON, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]');
15545
15429
  }
15546
15430
  if (!Array.isArray(parsed)) {
15547
- throw new Error("project set: requiredBuildSecrets must be a flat array (build secrets are not stage-mapped)");
15431
+ throw new Error("org project set: requiredBuildSecrets must be a flat array (build secrets are not stage-mapped)");
15548
15432
  }
15549
15433
  if (parsed.some((n) => typeof n !== "string" || !BUILD_SECRET_REF_RE.test(n) || n === "@github-packages-token")) {
15550
- throw new Error("project set: requiredBuildSecrets must be a flat array of ENVID=<ref> or bare <ref> strings");
15434
+ throw new Error("org project set: requiredBuildSecrets must be a flat array of ENVID=<ref> or bare <ref> strings");
15551
15435
  }
15552
15436
  return parsed;
15553
15437
  }
@@ -15556,19 +15440,19 @@ function parseEdgeDomainsVar(raw) {
15556
15440
  try {
15557
15441
  parsed = JSON.parse(raw);
15558
15442
  } catch {
15559
- throw new Error('project set: edgeDomains must be JSON, e.g. {"dev":"dev.example.co","rc":"rc.example.co","main":"example.co"}');
15443
+ throw new Error('org project set: edgeDomains must be JSON, e.g. {"dev":"dev.example.co","rc":"rc.example.co","main":"example.co"}');
15560
15444
  }
15561
15445
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15562
- throw new Error("project set: edgeDomains must be a {dev,rc,main} map of domain strings");
15446
+ throw new Error("org project set: edgeDomains must be a {dev,rc,main} map of domain strings");
15563
15447
  }
15564
15448
  const map = parsed;
15565
15449
  const out = {};
15566
15450
  for (const [stage, domain] of Object.entries(map)) {
15567
15451
  if (!RUNTIME_SECRET_STAGES.includes(stage)) {
15568
- throw new Error(`project set: edgeDomains stage "${stage}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
15452
+ throw new Error(`org project set: edgeDomains stage "${stage}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
15569
15453
  }
15570
15454
  if (typeof domain !== "string" || !domain.trim()) {
15571
- throw new Error(`project set: edgeDomains.${stage} must be a non-empty domain string`);
15455
+ throw new Error(`org project set: edgeDomains.${stage} must be a non-empty domain string`);
15572
15456
  }
15573
15457
  out[stage] = domain.trim();
15574
15458
  }
@@ -15580,19 +15464,19 @@ function parsePriorityOptionsVar(raw) {
15580
15464
  try {
15581
15465
  parsed = JSON.parse(raw);
15582
15466
  } catch {
15583
- throw new Error('project set: priorityOptions must be JSON, e.g. {"Urgent":"<id>","High":"<id>","Medium":"<id>","Low":"<id>"}');
15467
+ throw new Error('org project set: priorityOptions must be JSON, e.g. {"Urgent":"<id>","High":"<id>","Medium":"<id>","Low":"<id>"}');
15584
15468
  }
15585
15469
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15586
- throw new Error("project set: priorityOptions must be a {Urgent,High,Medium,Low} map of option-id strings");
15470
+ throw new Error("org project set: priorityOptions must be a {Urgent,High,Medium,Low} map of option-id strings");
15587
15471
  }
15588
15472
  const map = parsed;
15589
15473
  const out = {};
15590
15474
  for (const [name, id] of Object.entries(map)) {
15591
15475
  if (!PRIORITY_OPTION_NAMES.includes(name)) {
15592
- throw new Error(`project set: priorityOptions "${name}" \u2014 expected only ${PRIORITY_OPTION_NAMES.join("/")}`);
15476
+ throw new Error(`org project set: priorityOptions "${name}" \u2014 expected only ${PRIORITY_OPTION_NAMES.join("/")}`);
15593
15477
  }
15594
15478
  if (typeof id !== "string" || !id.trim()) {
15595
- throw new Error(`project set: priorityOptions.${name} must be a non-empty option-id string`);
15479
+ throw new Error(`org project set: priorityOptions.${name} must be a non-empty option-id string`);
15596
15480
  }
15597
15481
  out[name] = id.trim();
15598
15482
  }
@@ -15603,7 +15487,7 @@ function parsePortRangeVar(raw) {
15603
15487
  try {
15604
15488
  parsed = JSON.parse(raw);
15605
15489
  } catch {
15606
- throw new Error('project set: portRange must be JSON, e.g. {"start":3700,"end":3710} or [3700,3710]');
15490
+ throw new Error('org project set: portRange must be JSON, e.g. {"start":3700,"end":3710} or [3700,3710]');
15607
15491
  }
15608
15492
  let start;
15609
15493
  let end;
@@ -15612,12 +15496,12 @@ function parsePortRangeVar(raw) {
15612
15496
  } else if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
15613
15497
  ({ start, end } = parsed);
15614
15498
  } else {
15615
- throw new Error("project set: portRange must be {start,end} or a [start,end] tuple");
15499
+ throw new Error("org project set: portRange must be {start,end} or a [start,end] tuple");
15616
15500
  }
15617
15501
  if (typeof start !== "number" || typeof end !== "number" || !Number.isFinite(start) || !Number.isFinite(end)) {
15618
- throw new Error("project set: portRange start/end must be finite numbers");
15502
+ throw new Error("org project set: portRange start/end must be finite numbers");
15619
15503
  }
15620
- if (start > end) throw new Error("project set: portRange start must be <= end");
15504
+ if (start > end) throw new Error("org project set: portRange start must be <= end");
15621
15505
  return { start, end };
15622
15506
  }
15623
15507
  function parseStatusOptionsVar(raw) {
@@ -15625,16 +15509,16 @@ function parseStatusOptionsVar(raw) {
15625
15509
  try {
15626
15510
  parsed = JSON.parse(raw);
15627
15511
  } catch {
15628
- throw new Error('project set: statusOptions must be JSON, e.g. {"Todo":"<id>","In Progress":"<id>","In Review":"<id>","Done":"<id>"}');
15512
+ throw new Error('org project set: statusOptions must be JSON, e.g. {"Todo":"<id>","In Progress":"<id>","In Review":"<id>","Done":"<id>"}');
15629
15513
  }
15630
15514
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15631
- throw new Error("project set: statusOptions must be a map of status name to option-id string");
15515
+ throw new Error("org project set: statusOptions must be a map of status name to option-id string");
15632
15516
  }
15633
15517
  const map = parsed;
15634
15518
  const out = {};
15635
15519
  for (const [name, id] of Object.entries(map)) {
15636
15520
  if (typeof id !== "string" || !id.trim()) {
15637
- throw new Error(`project set: statusOptions.${name} must be a non-empty option-id string`);
15521
+ throw new Error(`org project set: statusOptions.${name} must be a non-empty option-id string`);
15638
15522
  }
15639
15523
  out[name] = id.trim();
15640
15524
  }
@@ -15645,31 +15529,31 @@ function parseOauthVar(raw) {
15645
15529
  try {
15646
15530
  parsed = JSON.parse(raw);
15647
15531
  } catch {
15648
- throw new Error(`project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}`);
15532
+ throw new Error(`org project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}`);
15649
15533
  }
15650
15534
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15651
- throw new Error("project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
15535
+ throw new Error("org project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
15652
15536
  }
15653
15537
  const map = parsed;
15654
15538
  const out = {};
15655
15539
  for (const [key, value] of Object.entries(map)) {
15656
15540
  if (key === "subdomains" || key === "domains") {
15657
15541
  if (!Array.isArray(value) || value.some((v) => typeof v !== "string" || !v.trim())) {
15658
- throw new Error(`project set: oauth.${key} must be an array of non-empty strings`);
15542
+ throw new Error(`org project set: oauth.${key} must be an array of non-empty strings`);
15659
15543
  }
15660
15544
  out[key] = value.map((v) => v.trim());
15661
15545
  } else if (key === "callbackPath") {
15662
- if (typeof value !== "string" || !value.trim()) throw new Error("project set: oauth.callbackPath must be a non-empty string");
15546
+ if (typeof value !== "string" || !value.trim()) throw new Error("org project set: oauth.callbackPath must be a non-empty string");
15663
15547
  const callbackPath = value.trim();
15664
15548
  if (callbackPath !== DEFAULT_CALLBACK_PATH) {
15665
- throw new Error(`project set: oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
15549
+ throw new Error(`org project set: oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
15666
15550
  }
15667
15551
  out.callbackPath = callbackPath;
15668
15552
  } else if (key === "fofuSubdomain") {
15669
- if (typeof value !== "string") throw new Error('project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
15553
+ if (typeof value !== "string") throw new Error('org project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
15670
15554
  out.fofuSubdomain = value.trim();
15671
15555
  } else {
15672
- throw new Error(`project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath/fofuSubdomain`);
15556
+ throw new Error(`org project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath/fofuSubdomain`);
15673
15557
  }
15674
15558
  }
15675
15559
  return out;
@@ -15679,49 +15563,49 @@ function parseReposVar(raw) {
15679
15563
  try {
15680
15564
  parsed = JSON.parse(raw);
15681
15565
  } catch {
15682
- throw new Error('project set: repos must be a JSON array, e.g. ["mutmutco/mm-foo"]');
15566
+ throw new Error('org project set: repos must be a JSON array, e.g. ["mutmutco/mm-foo"]');
15683
15567
  }
15684
15568
  if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((r) => typeof r !== "string" || !r.trim())) {
15685
- throw new Error("project set: repos must be a non-empty array of owner/name strings");
15569
+ throw new Error("org project set: repos must be a non-empty array of owner/name strings");
15686
15570
  }
15687
15571
  return parsed.map((r) => r.trim());
15688
15572
  }
15689
15573
  function parsePublishRequiredVar(raw) {
15690
15574
  if (raw === "true") return true;
15691
15575
  if (raw === "false") return false;
15692
- throw new Error("project set: publishRequired must be true or false");
15576
+ throw new Error("org project set: publishRequired must be true or false");
15693
15577
  }
15694
15578
  function parseFofuEnabledVar(raw) {
15695
15579
  if (raw === "true") return true;
15696
15580
  if (raw === "false") return false;
15697
- throw new Error("project set: fofuEnabled must be true or false");
15581
+ throw new Error("org project set: fofuEnabled must be true or false");
15698
15582
  }
15699
15583
  function parseRuntimeVaultOnlyVar(raw) {
15700
15584
  if (raw === "true") return true;
15701
15585
  if (raw === "false") return false;
15702
- throw new Error("project set: runtimeVaultOnly must be true or false");
15586
+ throw new Error("org project set: runtimeVaultOnly must be true or false");
15703
15587
  }
15704
15588
  function parseConsumesDesignSystemVar(raw) {
15705
15589
  if (raw === "fofu") return raw;
15706
- throw new Error('project set: consumesDesignSystem must be "fofu"');
15590
+ throw new Error('org project set: consumesDesignSystem must be "fofu"');
15707
15591
  }
15708
15592
  function parsePublishDirVar(raw) {
15709
15593
  const v = raw.trim();
15710
15594
  if (v === "" || v === ".") {
15711
- throw new Error("project set: publishDir must be a non-empty relative subpath, e.g. packages/ui (omit it or --unset publishDir for the repo root)");
15595
+ throw new Error("org project set: publishDir must be a non-empty relative subpath, e.g. packages/ui (omit it or --unset publishDir for the repo root)");
15712
15596
  }
15713
15597
  if (!/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
15714
- throw new Error('project set: publishDir must be a safe relative subpath \u2014 no leading slash, no ".." segment');
15598
+ throw new Error('org project set: publishDir must be a safe relative subpath \u2014 no leading slash, no ".." segment');
15715
15599
  }
15716
15600
  return v;
15717
15601
  }
15718
15602
  function parseDsManifestPathVar(raw) {
15719
15603
  const v = raw.trim();
15720
15604
  if (v === "" || !/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
15721
- throw new Error('project set: dsManifestPath must be a safe relative path \u2014 no leading slash, no ".." segment');
15605
+ throw new Error('org project set: dsManifestPath must be a safe relative path \u2014 no leading slash, no ".." segment');
15722
15606
  }
15723
15607
  if (v !== "package.json" && !v.endsWith("/package.json")) {
15724
- throw new Error("project set: dsManifestPath must point to a package.json, e.g. web/package.json");
15608
+ throw new Error("org project set: dsManifestPath must point to a package.json, e.g. web/package.json");
15725
15609
  }
15726
15610
  return v;
15727
15611
  }
@@ -15730,10 +15614,10 @@ function parseRequiredChecksVar(raw) {
15730
15614
  try {
15731
15615
  parsed = JSON.parse(raw);
15732
15616
  } catch {
15733
- throw new Error('project set: requiredChecks must be a JSON array, e.g. ["gate"] or [] for an intentional no-ci repo');
15617
+ throw new Error('org project set: requiredChecks must be a JSON array, e.g. ["gate"] or [] for an intentional no-ci repo');
15734
15618
  }
15735
15619
  if (!Array.isArray(parsed) || parsed.some((c) => typeof c !== "string" || !c.trim())) {
15736
- throw new Error("project set: requiredChecks must be a JSON array of non-empty check-context strings (use [] for none)");
15620
+ throw new Error("org project set: requiredChecks must be a JSON array of non-empty check-context strings (use [] for none)");
15737
15621
  }
15738
15622
  return parsed.map((c) => c.trim());
15739
15623
  }
@@ -15742,22 +15626,22 @@ function parseGateVar(raw) {
15742
15626
  try {
15743
15627
  parsed = JSON.parse(raw);
15744
15628
  } catch {
15745
- throw new Error('project set: gate must be JSON, e.g. {"runtime":"python","cmd":"pytest","workdir":"app"}');
15629
+ throw new Error('org project set: gate must be JSON, e.g. {"runtime":"python","cmd":"pytest","workdir":"app"}');
15746
15630
  }
15747
15631
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15748
- throw new Error("project set: gate must be a {runtime,cmd,workdir,cacheDepPath,pyVersion} object");
15632
+ throw new Error("org project set: gate must be a {runtime,cmd,workdir,cacheDepPath,pyVersion} object");
15749
15633
  }
15750
15634
  const map = parsed;
15751
15635
  const out = {};
15752
15636
  for (const [key, value] of Object.entries(map)) {
15753
15637
  if (key === "runtime") {
15754
- if (value !== "node" && value !== "python") throw new Error('project set: gate.runtime must be "node" or "python"');
15638
+ if (value !== "node" && value !== "python") throw new Error('org project set: gate.runtime must be "node" or "python"');
15755
15639
  out.runtime = value;
15756
15640
  } else if (key === "cmd" || key === "workdir" || key === "cacheDepPath" || key === "pyVersion") {
15757
- if (typeof value !== "string" || !value.trim()) throw new Error(`project set: gate.${key} must be a non-empty string`);
15641
+ if (typeof value !== "string" || !value.trim()) throw new Error(`org project set: gate.${key} must be a non-empty string`);
15758
15642
  out[key] = value.trim();
15759
15643
  } else {
15760
- throw new Error(`project set: gate key "${key}" \u2014 expected only runtime/cmd/workdir/cacheDepPath/pyVersion`);
15644
+ throw new Error(`org project set: gate key "${key}" \u2014 expected only runtime/cmd/workdir/cacheDepPath/pyVersion`);
15761
15645
  }
15762
15646
  }
15763
15647
  return out;
@@ -15835,25 +15719,25 @@ function buildProjectSetPatch(input) {
15835
15719
  const patch = {};
15836
15720
  if (input.class) {
15837
15721
  if (input.class !== "deployable" && input.class !== "content") {
15838
- throw new Error("project set: --class must be deployable or content");
15722
+ throw new Error("org project set: --class must be deployable or content");
15839
15723
  }
15840
15724
  patch.class = input.class;
15841
15725
  }
15842
15726
  if (input.projectType) {
15843
15727
  if (!isProjectType(input.projectType)) {
15844
- throw new Error(`project set: --project-type must be one of: ${PROJECT_TYPES.join(", ")}`);
15728
+ throw new Error(`org project set: --project-type must be one of: ${PROJECT_TYPES.join(", ")}`);
15845
15729
  }
15846
15730
  patch.projectType = input.projectType;
15847
15731
  }
15848
15732
  if (input.deployModel) {
15849
15733
  if (!isDeployModel(input.deployModel)) {
15850
- throw new Error(`project set: --deploy-model must be one of: ${DEPLOY_MODELS.join(", ")}`);
15734
+ throw new Error(`org project set: --deploy-model must be one of: ${DEPLOY_MODELS.join(", ")}`);
15851
15735
  }
15852
15736
  patch.deployModel = input.deployModel;
15853
15737
  }
15854
15738
  if (input.releaseTrack) {
15855
15739
  if (!isReleaseTrack(input.releaseTrack)) {
15856
- throw new Error(`project set: --release-track must be one of: ${RELEASE_TRACKS.join(", ")}`);
15740
+ throw new Error(`org project set: --release-track must be one of: ${RELEASE_TRACKS.join(", ")}`);
15857
15741
  }
15858
15742
  patch.releaseTrack = input.releaseTrack;
15859
15743
  }
@@ -15862,16 +15746,16 @@ function buildProjectSetPatch(input) {
15862
15746
  if (eq <= 0) {
15863
15747
  const looksPositional = value.includes("/") || !value.includes("=");
15864
15748
  const hint = looksPositional ? " \u2014 if that is the [owner/repo] argument, put it BEFORE --var; a variadic flag swallows the tokens after it" : "";
15865
- throw new Error(`project set: --var must be KEY=VALUE (got "${value}")${hint}`);
15749
+ throw new Error(`org project set: --var must be KEY=VALUE (got "${value}")${hint}`);
15866
15750
  }
15867
15751
  const key = value.slice(0, eq);
15868
15752
  const raw = value.slice(eq + 1);
15869
15753
  if (!SETTABLE_VAR_KEY_SET.has(key)) {
15870
- throw new Error(`project set: --var KEY "${key}" is not settable \u2014 allowed keys: ${SETTABLE_VAR_KEYS.join(", ")}`);
15754
+ throw new Error(`org project set: --var KEY "${key}" is not settable \u2014 allowed keys: ${SETTABLE_VAR_KEYS.join(", ")}`);
15871
15755
  }
15872
15756
  if (key === "projectNumber") {
15873
15757
  const n = Number(raw);
15874
- if (!Number.isFinite(n)) throw new Error("project set: projectNumber must be numeric");
15758
+ if (!Number.isFinite(n)) throw new Error("org project set: projectNumber must be numeric");
15875
15759
  patch[key] = n;
15876
15760
  } else if (key === "requiredRuntimeSecrets") {
15877
15761
  patch[key] = parseRuntimeSecretsVar(raw);
@@ -15904,7 +15788,7 @@ function buildProjectSetPatch(input) {
15904
15788
  } else if (key === "dsManifestPath") {
15905
15789
  patch[key] = parseDsManifestPathVar(raw);
15906
15790
  } else if (key === "ci") {
15907
- if (raw !== "none") throw new Error('project set: ci must be "none" (or use --unset ci to require checks)');
15791
+ if (raw !== "none") throw new Error('org project set: ci must be "none" (or use --unset ci to require checks)');
15908
15792
  patch[key] = raw;
15909
15793
  } else if (key === "requiredChecks") {
15910
15794
  patch[key] = parseRequiredChecksVar(raw);
@@ -15916,7 +15800,7 @@ function buildProjectSetPatch(input) {
15916
15800
  }
15917
15801
  for (const key of input.unsets) {
15918
15802
  if (!UNSET_KEY_SET.has(key)) {
15919
- throw new Error(`project set: --unset must be one of: ${UNSET_KEYS.join(", ")}`);
15803
+ throw new Error(`org project set: --unset must be one of: ${UNSET_KEYS.join(", ")}`);
15920
15804
  }
15921
15805
  patch[key] = null;
15922
15806
  }
@@ -15925,7 +15809,7 @@ function buildProjectSetPatch(input) {
15925
15809
  patch.edgeDomains = null;
15926
15810
  }
15927
15811
  if (Object.keys(patch).length === 0) {
15928
- throw new Error("project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, and/or --clear-web-profile");
15812
+ throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, and/or --clear-web-profile");
15929
15813
  }
15930
15814
  return patch;
15931
15815
  }
@@ -15937,36 +15821,36 @@ function buildSetDeployPatch(_slug, input) {
15937
15821
  const clean4 = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
15938
15822
  const stage = clean4(input.stage);
15939
15823
  if (!stage || !DEPLOY_STAGES.includes(stage)) {
15940
- throw new Error(`project set-deploy: --stage must be one of: ${DEPLOY_STAGES.join(", ")}`);
15824
+ throw new Error(`org project set-deploy: --stage must be one of: ${DEPLOY_STAGES.join(", ")}`);
15941
15825
  }
15942
15826
  const substrate = clean4(input.substrate);
15943
15827
  if (substrate !== void 0 && !DEPLOY_SUBSTRATES.includes(substrate)) {
15944
- throw new Error(`project set-deploy: --substrate must be one of: ${DEPLOY_SUBSTRATES.join(", ")}`);
15828
+ throw new Error(`org project set-deploy: --substrate must be one of: ${DEPLOY_SUBSTRATES.join(", ")}`);
15945
15829
  }
15946
15830
  const sshHost = clean4(input.sshHost);
15947
15831
  const sshUser = clean4(input.sshUser);
15948
15832
  const deployPath = clean4(input.deployPath);
15949
15833
  const serviceName = clean4(input.service);
15950
15834
  for (const [label, v] of [["--ssh-host", sshHost], ["--ssh-user", sshUser], ["--deploy-path", deployPath], ["--service", serviceName]]) {
15951
- if (v !== void 0 && !DEPLOY_SHELL_SAFE_RE.test(v)) throw new Error(`project set-deploy: ${label} contains unsafe characters`);
15835
+ if (v !== void 0 && !DEPLOY_SHELL_SAFE_RE.test(v)) throw new Error(`org project set-deploy: ${label} contains unsafe characters`);
15952
15836
  }
15953
15837
  let port;
15954
15838
  if (input.port !== void 0 && input.port !== null && `${input.port}`.trim() !== "") {
15955
15839
  port = Number(input.port);
15956
- if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("project set-deploy: --port must be an integer 1..65535");
15840
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("org project set-deploy: --port must be an integer 1..65535");
15957
15841
  }
15958
15842
  const domain = clean4(input.domain);
15959
- if (domain !== void 0 && !DEPLOY_DOMAIN_RE.test(domain)) throw new Error(`project set-deploy: --domain must be a DNS hostname, got ${JSON.stringify(input.domain)}`);
15843
+ if (domain !== void 0 && !DEPLOY_DOMAIN_RE.test(domain)) throw new Error(`org project set-deploy: --domain must be a DNS hostname, got ${JSON.stringify(input.domain)}`);
15960
15844
  const aliases = (Array.isArray(input.aliases) ? input.aliases : []).map((a) => clean4(a)).filter((a) => a !== void 0);
15961
15845
  for (const a of aliases) {
15962
- if (!DEPLOY_DOMAIN_RE.test(a)) throw new Error(`project set-deploy: --alias must be a DNS hostname, got ${JSON.stringify(a)}`);
15846
+ if (!DEPLOY_DOMAIN_RE.test(a)) throw new Error(`org project set-deploy: --alias must be a DNS hostname, got ${JSON.stringify(a)}`);
15963
15847
  }
15964
15848
  const uniqueAliases = [...new Set(aliases)];
15965
15849
  if (input.clearAliases && uniqueAliases.length) {
15966
- throw new Error("project set-deploy: --clear-aliases cannot be combined with --alias (they contradict)");
15850
+ throw new Error("org project set-deploy: --clear-aliases cannot be combined with --alias (they contradict)");
15967
15851
  }
15968
15852
  if (input.noEnvFile !== void 0 && typeof input.noEnvFile !== "boolean") {
15969
- throw new Error("project set-deploy: --no-env-file must be true or false");
15853
+ throw new Error("org project set-deploy: --no-env-file must be true or false");
15970
15854
  }
15971
15855
  return {
15972
15856
  stage,
@@ -15984,17 +15868,44 @@ function buildSetDeployPatch(_slug, input) {
15984
15868
  };
15985
15869
  }
15986
15870
  var DEPLOY_STAGE_BRANCH = { dev: "development", rc: "rc", main: "main" };
15871
+ function filelessTransitionGuide(repo, stage) {
15872
+ const authority = stage === "main" ? "Authority: main is master-admin only." : "Authority: a project-admin may complete this for their own repo; master may also do it.";
15873
+ return [
15874
+ `Fileless transition for ${repo} (${stage}):`,
15875
+ ` ${authority}`,
15876
+ ` mmi-cli org access role ${repo}`,
15877
+ ` mmi-cli secrets list --repo ${repo} # names/capabilities only`,
15878
+ ` mmi-cli secrets preflight --repo ${repo} --stage ${stage}`,
15879
+ ` mmi-cli org project set-deploy ${repo} --stage ${stage} --no-env-file true`,
15880
+ ` mmi-cli runtime tenant reconcile ${repo} ${stage} --watch`,
15881
+ ` mmi-cli runtime tenant redeploy ${repo} ${stage} --watch`,
15882
+ ` mmi-cli runtime tenant control ${repo} ${stage} verify-secrets --watch`,
15883
+ ` mmi-cli runtime tenant control ${repo} ${stage} verify-broker --watch # broker consumers`,
15884
+ " Missing/undeclared key (project-admin own project):",
15885
+ ` mmi-cli secrets declare <KEY> --repo ${repo} --purpose "<purpose>" --group "<group>" --consumers runtime --stages ${stage}`,
15886
+ ` mmi-cli secrets set ${stage}/<KEY> --repo ${repo} # value from secure prompt/stdin; never argv/stdout`,
15887
+ ` mmi-cli secrets request <KEY> --repo ${repo} --reason "wire requiredRuntimeSecrets for ${stage}" # only if env injection needs master wiring`,
15888
+ " Runbook: https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/tenant-fileless-migration.md"
15889
+ ].join("\n");
15890
+ }
15987
15891
  function composeDeclaresEnvFile(composeText) {
15988
15892
  return composeText.split(/\r?\n/).some((line) => /^\s*env_file\s*:/.test(line));
15989
15893
  }
15990
15894
  function composeHasRuntimeEnvLabel(composeText) {
15991
- return /mmi\.runtime-env/.test(composeText);
15895
+ return composeText.split(/\r?\n/).some((raw) => {
15896
+ const line = raw.replace(/(^|\s)#.*$/, "$1");
15897
+ return /(^|[\s"'])mmi\.runtime-env\s*:\s*["']?true["']?\s*$/.test(line) || /mmi\.runtime-env\s*=\s*["']?true["']?\s*$/.test(line);
15898
+ });
15992
15899
  }
15993
15900
  function evaluateFilelessComposeGuard(input) {
15994
15901
  if (input.force) return { ok: true, warn: `--force: skipping the fileless-compose guard for ${input.branch}` };
15995
15902
  if (input.composeText === null) {
15996
- return { ok: true, warn: `could not read docker-compose.yml on ${input.branch} \u2014 skipping the fileless-compose guard (run from the target repo checkout with the branch fetched to enable it)` };
15903
+ return { ok: false, reason: `could not read docker-compose.yml on ${input.branch} \u2014 refusing to change noEnvFile without verifying the target branch compose. Run from the target repo checkout with origin/${input.branch} fetched, or use --force only after an independent verification.` };
15904
+ }
15905
+ if (input.noEnvFile === false && composeHasRuntimeEnvLabel(input.composeText)) {
15906
+ return { ok: false, reason: `the ${input.branch} branch marks a service mmi.runtime-env=true \u2014 setting noEnvFile=false would make the box take the legacy path and refuse this fileless compose (exit 78). Keep noEnvFile=true or land a verified legacy compose first.` };
15997
15907
  }
15908
+ if (input.noEnvFile === false) return { ok: true };
15998
15909
  if (composeDeclaresEnvFile(input.composeText)) {
15999
15910
  return {
16000
15911
  ok: false,
@@ -16056,15 +15967,16 @@ function evaluatePromotionFilelessGuard(input) {
16056
15967
  return {
16057
15968
  ok: false,
16058
15969
  reason: `the ${branch} docker-compose.yml marks a service fileless (mmi.runtime-env: "true") but DEPLOY#${stage}.noEnvFile is not true \u2014 the ${stage} deploy would fail exit-78 ("compose is fileless but DEPLOY#${stage} noEnvFile is not true"). Fix BEFORE promoting, then re-run:
16059
- mmi-cli project set-deploy --stage ${stage} --no-env-file true # register the fileless flag
16060
- gh workflow run tenant-reconcile.yml --repo mutmutco/MMI-Hub # re-render the box control script (#1056)
16061
- (or pass --skip-compose-guard to override this preflight)`
15970
+ ${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
15971
+ Override only after independent verification: --skip-compose-guard`
16062
15972
  };
16063
15973
  }
16064
15974
  if (noEnvFile && composeDeclaresEnvFile(input.composeText)) {
16065
15975
  return {
16066
15976
  ok: false,
16067
- reason: `DEPLOY#${stage}.noEnvFile is true but the ${branch} docker-compose.yml still declares env_file: \u2014 the box writes no .env, so docker compose dies on the missing file. Land the fileless compose (drop env_file:, add the mmi.runtime-env label) on ${branch}, or set-deploy --no-env-file false (or --skip-compose-guard to override).`
15977
+ reason: `DEPLOY#${stage}.noEnvFile is true but the ${branch} docker-compose.yml still declares env_file: \u2014 the box writes no .env, so docker compose dies on the missing file. Land the fileless compose (drop env_file:, add the mmi.runtime-env label) on ${branch}.
15978
+ ${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
15979
+ Rollback only after independent verification: set-deploy --no-env-file false; preflight override: --skip-compose-guard.`
16068
15980
  };
16069
15981
  }
16070
15982
  return { ok: true };
@@ -16080,11 +15992,11 @@ function vaultCleanupNote(slug) {
16080
15992
  function buildRetirePlan(input) {
16081
15993
  const target = input.target?.trim();
16082
15994
  if (!target) {
16083
- throw new Error("project retire: pass <owner/repo> or a slug to retire");
15995
+ throw new Error("org project retire: pass <owner/repo> or a slug to retire");
16084
15996
  }
16085
15997
  const slug = retireSlugOf(target);
16086
15998
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
16087
- throw new Error(`project retire: "${target}" does not resolve to a valid slug`);
15999
+ throw new Error(`org project retire: "${target}" does not resolve to a valid slug`);
16088
16000
  }
16089
16001
  const repo = target.includes("/") ? target : `mutmutco/${slug}`;
16090
16002
  return { slug, repo, apply: Boolean(input.apply), skipBoard: Boolean(input.skipBoard) };
@@ -16153,6 +16065,27 @@ var import_node_fs17 = require("node:fs");
16153
16065
  var import_node_path15 = require("node:path");
16154
16066
  var import_node_os5 = require("node:os");
16155
16067
 
16068
+ // src/project-runtime.ts
16069
+ function hasRuntimeSecretContract(contract) {
16070
+ if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
16071
+ return ["dev", "rc", "main"].some((stage) => Array.isArray(contract[stage]));
16072
+ }
16073
+ function projectRequiresGoogleOAuth(meta, model) {
16074
+ if (!meta || model === "content" || model === "none" || meta.deployModel === "content" || meta.deployModel === "none" || meta.class === "content") return false;
16075
+ const projectType = resolveProjectType(meta, meta.repos?.[0] ?? "");
16076
+ return projectType === "web-app" && Boolean(meta.oauth && typeof meta.oauth === "object");
16077
+ }
16078
+ function edgeDomainsByStage(meta) {
16079
+ const edgeDomains = meta?.edgeDomains;
16080
+ if (!edgeDomains || typeof edgeDomains !== "object" || Array.isArray(edgeDomains)) return {};
16081
+ const out = {};
16082
+ for (const stage of ["dev", "rc", "main"]) {
16083
+ const value = edgeDomains[stage];
16084
+ if (typeof value === "string" && value.trim()) out[stage] = value.trim();
16085
+ }
16086
+ return out;
16087
+ }
16088
+
16156
16089
  // src/secrets-diff.ts
16157
16090
  var TIMEOUT_MS2 = 8e3;
16158
16091
  var OWNER3 = "mutmutco";
@@ -16324,13 +16257,13 @@ async function withSecrets(run) {
16324
16257
  await run(makeSecretsDeps(cfg));
16325
16258
  }
16326
16259
  function registerSecretsCommands(program3) {
16327
- const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
16260
+ const secrets = program3.command("secrets").description("project vault \u2014 project-admins self-serve their own repo's full tree (stageless + dev/rc/main); org-infra namespaces are master-gated");
16328
16261
  secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
16329
16262
  secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
16330
- secrets.command("find <intent>").description("resolve a plain-language intent to the canonical secret(s) + the exact get command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
16263
+ secrets.command("find <intent>").description("resolve a plain-language intent to canonical secret names + the exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
16331
16264
  if (!await secretsFind(d, intent, o)) process.exitCode = 1;
16332
16265
  }));
16333
- secrets.command("catalog").description("the secret catalog \u2014 grouped, with each secret's exact get command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--drift", "include the bidirectional reconciler drift report").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCatalog(d, o)));
16266
+ secrets.command("catalog").description("the secret catalog \u2014 grouped, with each secret's exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--drift", "include the bidirectional reconciler drift report").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCatalog(d, o)));
16334
16267
  secrets.command("doctor").description("vault drift report \u2014 declared-missing / orphan / off-scheme / duplicate (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets(async (d) => {
16335
16268
  if (!await secretsDoctor(d, o)) process.exitCode = 1;
16336
16269
  }));
@@ -16399,7 +16332,7 @@ function registerSecretsCommands(program3) {
16399
16332
  const facts = await fetchDeployFactsBySlug(slug, regDeps);
16400
16333
  const fact = facts?.stages?.[stage] ?? null;
16401
16334
  const composeText = sameRepo ? await readStageBranchCompose(branch) : null;
16402
- const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false });
16335
+ const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false, repo });
16403
16336
  if (!verdict.ok) {
16404
16337
  d.err(`secrets preflight: ${verdict.reason}`);
16405
16338
  filelessOk = false;
@@ -16414,7 +16347,7 @@ function registerSecretsCommands(program3) {
16414
16347
  const ok = await secretsRequest(d, key, o);
16415
16348
  if (!ok) process.exitCode = 1;
16416
16349
  }));
16417
- secrets.command("declare <key>").description("project-admin self-declare: register a NEW secrets-catalog entry for your own repo (name/purpose/group/owner/consumers/stages metadata only \u2014 never a value); closes the declare-first master-only gap (MM-PM#261/#262). Catalog-declare only \u2014 wiring the key into requiredRuntimeSecrets stays master-only (`secrets request` to ask, or `project set --var requiredRuntimeSecrets=...`); dropped from self-serve after adversarial review found real escalation paths in that additive-merge logic (MMI-Hub#2780). Separate from `project set`, which stays master-only for this field.").requiredOption("--purpose <text>", "what this secret is for").requiredOption("--group <text>", "catalog grouping, e.g. database, auth").option("--owner <login>", "catalog owner login (default: your own resolved GitHub login)").option("--consumers <list>", "comma-separated consumers (default: box)").option("--stages <list>", "comma-separated dev/rc/main this entry applies to (default: none \u2014 one stageless shared value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
16350
+ secrets.command("declare <key>").description("project-admin self-declare: register a NEW secrets-catalog entry for your own repo (name/purpose/group/owner/consumers/stages metadata only \u2014 never a value); closes the declare-first master-only gap (MM-PM#261/#262). Catalog-declare only \u2014 wiring the key into requiredRuntimeSecrets stays master-only (`secrets request` to ask, or `org project set --var requiredRuntimeSecrets=...`); dropped from self-serve after adversarial review found real escalation paths in that additive-merge logic (MMI-Hub#2780). Separate from `org project set`, which stays master-only for this field.").requiredOption("--purpose <text>", "what this secret is for").requiredOption("--group <text>", "catalog grouping, e.g. database, auth").option("--owner <login>", "catalog owner login (default: your own resolved GitHub login)").option("--consumers <list>", "comma-separated consumers (default: box)").option("--stages <list>", "comma-separated dev/rc/main this entry applies to (default: none \u2014 one stageless shared value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
16418
16351
  const owner = o.owner ?? await githubLogin();
16419
16352
  if (!owner) {
16420
16353
  fail("secrets declare: could not resolve your GitHub login for --owner; pass --owner explicitly");
@@ -16441,8 +16374,7 @@ function registerSecretsCommands(program3) {
16441
16374
  if (!ok) process.exitCode = 1;
16442
16375
  });
16443
16376
  secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
16444
- secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
16445
- secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
16377
+ secrets.command("copy").description("copy provider keys between stages in your own project vault (audit-logged; encryption keys blocked)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
16446
16378
  const stages = ["dev", "rc", "main"];
16447
16379
  if (!stages.includes(o.from) || !stages.includes(o.to)) {
16448
16380
  return fail("secrets copy: --from and --to must be dev, rc, or main");
@@ -16482,13 +16414,13 @@ function registerSecretsCommands(program3) {
16482
16414
  );
16483
16415
  if (!ok) process.exitCode = 1;
16484
16416
  }));
16485
- 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)));
16486
- secrets.command("use <key> [command...]").description("consume a secret KEYLESS: `use <key> -- <cmd>` injects it into the command env (never printed); a granted non-master can use an org secret. No command prints guidance only.").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
16417
+ secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
16418
+ secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
16487
16419
  const ok = await secretsUse(d, key, { ...o, command });
16488
16420
  if (ok === false) process.exitCode = 1;
16489
16421
  }));
16490
- 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, {})));
16491
- secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-tier secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
16422
+ secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to one specific org-infra secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
16423
+ secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-infra secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
16492
16424
  }
16493
16425
 
16494
16426
  // src/app-actor.ts
@@ -16788,14 +16720,14 @@ async function fetchInventory() {
16788
16720
  }
16789
16721
  });
16790
16722
  if (incomplete.length === PROJECTS.length) {
16791
- throw new Error(`box: could not read any Hetzner project \u2014
16723
+ throw new Error(`runtime box: could not read any Hetzner project \u2014
16792
16724
  ${incomplete.join("\n ")}`);
16793
16725
  }
16794
16726
  return { boxes: boxes.sort((a, b) => a.name.localeCompare(b.name)), incomplete };
16795
16727
  }
16796
16728
  function warnIncomplete(incomplete) {
16797
- for (const f of incomplete) console.error(`box: WARNING \u2014 ${f}`);
16798
- if (incomplete.length) console.error("box: this inventory is INCOMPLETE \u2014 boxes may be missing from it.");
16729
+ for (const f of incomplete) console.error(`runtime box: WARNING \u2014 ${f}`);
16730
+ if (incomplete.length) console.error("runtime box: this inventory is INCOMPLETE \u2014 boxes may be missing from it.");
16799
16731
  }
16800
16732
  function registerBoxCommands(program3) {
16801
16733
  const box = program3.command("box").description("the box inventory \u2014 which machines exist, their addresses, and the vault key that opens each (never key values)");
@@ -16820,12 +16752,12 @@ function registerBoxCommands(program3) {
16820
16752
  if (result.kind === "unknown") {
16821
16753
  warnIncomplete(result.reasons);
16822
16754
  await failGraceful(
16823
- `box: cannot confirm whether '${name}' exists \u2014 part of the inventory could not be read (see above). Refusing to answer "no such box" from a list I know is incomplete.`
16755
+ `runtime box: cannot confirm whether '${name}' exists \u2014 part of the inventory could not be read (see above). Refusing to answer "no such box" from a list I know is incomplete.`
16824
16756
  );
16825
16757
  return;
16826
16758
  }
16827
16759
  if (result.kind === "absent") {
16828
- await failGraceful(`box: no box named '${name}'. Known boxes: ${result.known.join(", ") || "(none)"}`);
16760
+ await failGraceful(`runtime box: no box named '${name}'. Known boxes: ${result.known.join(", ") || "(none)"}`);
16829
16761
  return;
16830
16762
  }
16831
16763
  const found = result.box;
@@ -16940,7 +16872,7 @@ function awsScheduleNames(payload) {
16940
16872
  return schedules.map((s) => s && typeof s === "object" ? s.Name : void 0).filter((n) => typeof n === "string" && !isJervResource(n));
16941
16873
  }
16942
16874
  var DECLARED_ENTRIES = [
16943
- { name: "mmi-backup (mm-central, mmi-fofu, zuber, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh (verify: mmi-cli box get <box> --ssh)" },
16875
+ { name: "mmi-backup (mm-central, mmi-fofu, zuber, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh (verify: mmi-cli runtime box get <box> --ssh)" },
16944
16876
  { name: "zuber-bake", cadence: "*:05/30 (every 30 min)", executor: "zuber systemd timer", llm: "no", resolved: "declared", source: "zuber-bake.timer \u2192 rolling overlay bake, metro tier, next ~6h window (verify over ssh)" },
16945
16877
  { name: "zuber-bake-full", cadence: "00:30 UTC (03:30 TRT, daily)", executor: "zuber systemd timer", llm: "no", resolved: "declared", source: "zuber-bake-full.timer \u2192 full 24h overlay rebuild, all 81 il (verify over ssh)" }
16946
16878
  ];
@@ -16972,7 +16904,7 @@ function renderDocSection(entries, generatedAtIso) {
16972
16904
  }
16973
16905
  return [
16974
16906
  DOC_START_MARKER,
16975
- `_Generated by \`mmi-cli schedules --doc\` at ${generatedAtIso}. Never hand-edit this section._`,
16907
+ `_Generated by \`mmi-cli org schedules --doc\` at ${generatedAtIso}. Never hand-edit this section._`,
16976
16908
  "",
16977
16909
  lines.join("\n"),
16978
16910
  DOC_END_MARKER
@@ -16982,7 +16914,7 @@ function spliceDoc(docText, generatedSection) {
16982
16914
  const start = docText.indexOf(DOC_START_MARKER);
16983
16915
  const end = docText.indexOf(DOC_END_MARKER);
16984
16916
  if (start === -1 || end === -1 || end < start) {
16985
- throw new Error(`schedules --doc: the target doc is missing the ${DOC_START_MARKER} / ${DOC_END_MARKER} markers \u2014 refusing to guess where the generated inventory goes.`);
16917
+ throw new Error(`org schedules --doc: the target doc is missing the ${DOC_START_MARKER} / ${DOC_END_MARKER} markers \u2014 refusing to guess where the generated inventory goes.`);
16986
16918
  }
16987
16919
  return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
16988
16920
  }
@@ -17190,11 +17122,11 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
17190
17122
  };
17191
17123
  }
17192
17124
  function warnIncomplete2(incomplete) {
17193
- for (const f of incomplete) console.error(`schedules: WARNING \u2014 ${f}`);
17194
- if (incomplete.length) console.error("schedules: this notebook is INCOMPLETE \u2014 schedules may be missing from it.");
17125
+ for (const f of incomplete) console.error(`org schedules: WARNING \u2014 ${f}`);
17126
+ if (incomplete.length) console.error("org schedules: this notebook is INCOMPLETE \u2014 schedules may be missing from it.");
17195
17127
  }
17196
17128
  function reportDrift(drift) {
17197
- for (const d of drift) console.error(`schedules: DRIFT \u2014 ${d}`);
17129
+ for (const d of drift) console.error(`org schedules: DRIFT \u2014 ${d}`);
17198
17130
  }
17199
17131
  function registerSchedulesCommands(program3) {
17200
17132
  program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "splice the generated inventory into the given doc between the schedules:inventory markers (docs/schedules.md)").action(async (o) => {
@@ -17203,14 +17135,14 @@ function registerSchedulesCommands(program3) {
17203
17135
  if (o.doc) {
17204
17136
  if (incomplete.length) {
17205
17137
  warnIncomplete2(incomplete);
17206
- await failGraceful("schedules --doc: refusing to regenerate the doc from an incomplete read (see warnings above).");
17138
+ await failGraceful("org schedules --doc: refusing to regenerate the doc from an incomplete read (see warnings above).");
17207
17139
  return;
17208
17140
  }
17209
17141
  const docText = await (0, import_promises2.readFile)(o.doc, "utf8");
17210
17142
  const spliced = spliceDoc(docText, renderDocSection(entries, (/* @__PURE__ */ new Date()).toISOString()));
17211
17143
  await (0, import_promises2.writeFile)(o.doc, spliced, "utf8");
17212
17144
  reportDrift(drift);
17213
- console.log(`schedules: wrote ${entries.length} entries into ${o.doc}`);
17145
+ console.log(`org schedules: wrote ${entries.length} entries into ${o.doc}`);
17214
17146
  return;
17215
17147
  }
17216
17148
  if (o.json) {
@@ -17457,15 +17389,15 @@ async function runIssueChildren(deps, epic, opts) {
17457
17389
  const seen = /* @__PURE__ */ new Set([childKey(repo, ref.number)]);
17458
17390
  for (const raw of await queryChildren(deps, owner, name, ref.number)) {
17459
17391
  if (out.length >= CHILDREN_MAX_TOTAL) break;
17460
- const child = shapeChildNode(raw, 1, boardProjectId);
17461
- out.push(child);
17462
- seen.add(childKey(child.repo, child.number));
17392
+ const child2 = shapeChildNode(raw, 1, boardProjectId);
17393
+ out.push(child2);
17394
+ seen.add(childKey(child2.repo, child2.number));
17463
17395
  }
17464
17396
  if (!opts.recursive || out.length >= CHILDREN_MAX_TOTAL) return out;
17465
17397
  const queue = [];
17466
- for (const child of out) {
17467
- const sp = trySplitRepo(child.repo);
17468
- if (sp) queue.push({ owner: sp.owner, name: sp.name, number: child.number, depth: 1 });
17398
+ for (const child2 of out) {
17399
+ const sp = trySplitRepo(child2.repo);
17400
+ if (sp) queue.push({ owner: sp.owner, name: sp.name, number: child2.number, depth: 1 });
17469
17401
  }
17470
17402
  while (queue.length && out.length < CHILDREN_MAX_TOTAL) {
17471
17403
  const frame = queue.shift();
@@ -17478,13 +17410,13 @@ async function runIssueChildren(deps, epic, opts) {
17478
17410
  }
17479
17411
  for (const raw of nodes) {
17480
17412
  if (out.length >= CHILDREN_MAX_TOTAL) break;
17481
- const child = shapeChildNode(raw, frame.depth + 1, boardProjectId);
17482
- const key = childKey(child.repo, child.number);
17413
+ const child2 = shapeChildNode(raw, frame.depth + 1, boardProjectId);
17414
+ const key = childKey(child2.repo, child2.number);
17483
17415
  if (seen.has(key)) continue;
17484
17416
  seen.add(key);
17485
- out.push(child);
17486
- const sp = trySplitRepo(child.repo);
17487
- if (sp) queue.push({ owner: sp.owner, name: sp.name, number: child.number, depth: frame.depth + 1 });
17417
+ out.push(child2);
17418
+ const sp = trySplitRepo(child2.repo);
17419
+ if (sp) queue.push({ owner: sp.owner, name: sp.name, number: child2.number, depth: frame.depth + 1 });
17488
17420
  }
17489
17421
  }
17490
17422
  return out;
@@ -18235,7 +18167,7 @@ function registerBootstrapCommands(program3) {
18235
18167
  projectMeta: meta,
18236
18168
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
18237
18169
  readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null,
18238
- // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
18170
+ // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
18239
18171
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
18240
18172
  requiredGcpApis: (() => {
18241
18173
  const v = meta?.requiredGcpApis;
@@ -18267,7 +18199,7 @@ function registerBootstrapCommands(program3) {
18267
18199
  else console.log(renderOrgRulesetDriftReport(plan));
18268
18200
  if (plan.action !== "noop") process.exitCode = 1;
18269
18201
  });
18270
- bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
18202
+ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
18271
18203
  const o = {
18272
18204
  class: rawValue("--class", "deployable"),
18273
18205
  projectType: rawValue("--project-type", ""),
@@ -18583,12 +18515,12 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
18583
18515
  function shellFor(platform2 = process.platform) {
18584
18516
  return platform2 === "win32" ? "powershell" : "bash";
18585
18517
  }
18586
- function isCentralContainerModel2(model) {
18518
+ function isCentralContainerModel(model) {
18587
18519
  return model === "tenant-container" || model === "solo-container";
18588
18520
  }
18589
18521
  function deriveStageGap(inputs) {
18590
18522
  const missing = [];
18591
- if (!isCentralContainerModel2(inputs.deployModel)) {
18523
+ if (!isCentralContainerModel(inputs.deployModel)) {
18592
18524
  return `local stage default applies to central-container repos only (tenant-container/solo-container; registry deployModel = ${inputs.deployModel ?? "unset"})`;
18593
18525
  }
18594
18526
  if (!inputs.hasCompose) missing.push("docker-compose.yml");
@@ -18770,7 +18702,7 @@ function registerStageCommands(program3) {
18770
18702
  const read = await fetchProjectBySlugChecked(slug, reg);
18771
18703
  const decision = decidePortRange({ metaReadOk: read.ok, metaPortRange: read.ok ? metaPortRange(read.project) : null });
18772
18704
  if (decision.action === "fail") {
18773
- return failGraceful(`port-range: ${decision.reason}${read.ok ? "" : ` (${read.error})`}`);
18705
+ return failGraceful(`stage port-range: ${decision.reason}${read.ok ? "" : ` (${read.error})`}`);
18774
18706
  }
18775
18707
  if (decision.action === "return") {
18776
18708
  const [start2, end2] = decision.range;
@@ -18780,7 +18712,7 @@ function registerStageCommands(program3) {
18780
18712
  const infra = resolvePortRangeInfra(process.cwd(), __dirname);
18781
18713
  if (!infra) {
18782
18714
  return failGraceful(
18783
- `port-range: no MMI-Hub allocator files found (checked cwd ${process.cwd()}, sibling MMI-Hub dirs, and the installed package location); ensure the Hub's infra/port-ranges.json and infra/port-ddb.mjs are reachable`
18715
+ `stage port-range: no MMI-Hub allocator files found (checked cwd ${process.cwd()}, sibling MMI-Hub dirs, and the installed package location); ensure the Hub's infra/port-ranges.json and infra/port-ddb.mjs are reachable`
18784
18716
  );
18785
18717
  }
18786
18718
  const path2 = infra.registryPath;
@@ -18793,7 +18725,7 @@ function registerStageCommands(program3) {
18793
18725
  const { range: [start, end], source } = await ensurePortRangeAtomic(repo, path2, allocate);
18794
18726
  const write = await upsertProject(slug, { portRange: { start, end } }, reg);
18795
18727
  if (!write.ok && source === "ddb") {
18796
- return failGraceful(`port-range: block [${start}, ${end}] was allocated (cursor advanced) but NOT recorded in the registry META (${write.error ?? `HTTP ${write.status}`}) \u2014 fix auth/connectivity and retry so the block is persisted; do not re-run blind`);
18728
+ return failGraceful(`stage port-range: block [${start}, ${end}] was allocated (cursor advanced) but NOT recorded in the registry META (${write.error ?? `HTTP ${write.status}`}) \u2014 fix auth/connectivity and retry so the block is persisted; do not re-run blind`);
18797
18729
  }
18798
18730
  if (o.json) {
18799
18731
  printLine(JSON.stringify({ repo, portRange: [start, end], source: "allocated", persisted: write.ok, ...write.ok ? {} : { persistError: write.error ?? `HTTP ${write.status}` } }));
@@ -18833,7 +18765,7 @@ function registerStageCommands(program3) {
18833
18765
  },
18834
18766
  control: async ({ repo, action, host, ip }) => {
18835
18767
  const res = await tenantControl({ repo, stage: "dev", action, host, ip }, rcDeps);
18836
- if (!res.ok) throw new Error(`tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
18768
+ if (!res.ok) throw new Error(`runtime tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
18837
18769
  }
18838
18770
  };
18839
18771
  try {
@@ -19060,7 +18992,7 @@ function registerBoardCommands(program3) {
19060
18992
  expected: [...BOARD_STATUSES]
19061
18993
  });
19062
18994
  }
19063
- async function runBulkMove(issueRefs, o, status, failLabel) {
18995
+ async function runBulkMove(issueRefs, o, status) {
19064
18996
  try {
19065
18997
  const bulk = await moveBoardIssues({
19066
18998
  config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
@@ -19078,7 +19010,7 @@ function registerBoardCommands(program3) {
19078
19010
  }
19079
19011
  if (bulk.failed > 0) process.exitCode = 1;
19080
19012
  } catch (e) {
19081
- return failGraceful(`${failLabel} failed: ${e.message}`);
19013
+ return failGraceful(`board move failed: ${e.message}`);
19082
19014
  }
19083
19015
  }
19084
19016
  withExamples(mutating(
@@ -19100,7 +19032,7 @@ function registerBoardCommands(program3) {
19100
19032
  }
19101
19033
  return;
19102
19034
  }
19103
- await runBulkMove(issueRefs, o, canonicalStatus, "board move");
19035
+ await runBulkMove(issueRefs, o, canonicalStatus);
19104
19036
  }), [
19105
19037
  'mmi-cli board move "In Progress" 2680',
19106
19038
  "mmi-cli board move Done 2680 2681 2682"
@@ -19133,19 +19065,6 @@ function registerBoardCommands(program3) {
19133
19065
  return failGraceful(`board set-priority failed: ${e.message}`);
19134
19066
  }
19135
19067
  });
19136
- 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) => {
19137
- if (issueRefs.length === 1) {
19138
- try {
19139
- const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: "Done", repo: o.repo, allowPartial: o.allowPartial });
19140
- if (o.json) return console.log(JSON.stringify(result));
19141
- console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> Done`);
19142
- } catch (e) {
19143
- return failGraceful(`board done failed: ${e.message}`);
19144
- }
19145
- return;
19146
- }
19147
- await runBulkMove(issueRefs, o, "Done", "board done");
19148
- });
19149
19068
  board.command("unclaim <issue>").description("clear the assignee and reset the board Status (defaults to Todo) \u2014 the inverse of claim").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--to-status <status>", `set Status after unclaim (default Todo; one of ${BOARD_STATUSES.join(", ")})`).option("--allow-partial", "return success JSON if the assignee clear or status move fails").action(async (issueRef, o) => {
19150
19069
  const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
19151
19070
  try {
@@ -19443,7 +19362,7 @@ async function resolveBoardAdvanceForPr(prNumber, repoOption) {
19443
19362
  });
19444
19363
  }
19445
19364
  function renderGcApplyResult(result) {
19446
- const lines = ["gc apply result:"];
19365
+ const lines = ["worktree gc apply result:"];
19447
19366
  lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
19448
19367
  lines.push(` remote branches removed: ${result.removedRemoteBranches.length ? result.removedRemoteBranches.join(", ") : "none"}`);
19449
19368
  lines.push(` tracking refs removed: ${result.removedTrackingRefs.length ? result.removedTrackingRefs.join(", ") : "none"}`);
@@ -19488,7 +19407,7 @@ function evaluatePrMergeHousekeeping(report, context, force = false) {
19488
19407
  const detail = failed ? `scratch housekeeping failed${scratch?.error ? `: ${scratch.error}` : ""}` : `${prunable} prunable + ${unprunable} un-prunable scratch item(s) remain after cleanup`;
19489
19408
  return {
19490
19409
  blocked: true,
19491
- reason: `${context} housekeeping blocked merge: ${detail}. Run \`mmi-cli gc --scratch --apply\` to clear it, then retry \u2014 or re-run \`mmi-cli ${context} --force\` to acknowledge and land anyway. (Kept advisory plans/ scratch never blocks: it is gitignored and can never reach origin.)`
19410
+ reason: `${context} housekeeping blocked merge: ${detail}. Run \`mmi-cli worktree gc --scratch --apply\` to clear it, then retry \u2014 or re-run \`mmi-cli ${context} --force\` to acknowledge and land anyway. (Kept advisory plans/ scratch never blocks: it is gitignored and can never reach origin.)`
19492
19411
  };
19493
19412
  }
19494
19413
  function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
@@ -19650,7 +19569,7 @@ async function remoteBranchExists2(branch, options = {}) {
19650
19569
  }
19651
19570
  var COMPOSE_TIMEOUT_MS = 12e4;
19652
19571
  function spawnDeferredGcSweep() {
19653
- spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19572
+ spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19654
19573
  }
19655
19574
  async function createDeferredWorktreeStore() {
19656
19575
  try {
@@ -19907,7 +19826,7 @@ function classifyStaleLeaks(input) {
19907
19826
  kind: "stale-worktree",
19908
19827
  ref: wt.branch,
19909
19828
  detail: `merged/closed branch ${wt.branch} still checked out at ${wt.path}`,
19910
- remediation: `cd "${wt.path}" && mmi-cli worktree land --apply (or: mmi-cli gc --apply)`
19829
+ remediation: `cd "${wt.path}" && mmi-cli worktree land --apply (or: mmi-cli worktree gc --apply)`
19911
19830
  });
19912
19831
  }
19913
19832
  for (const branch of input.localBranches) {
@@ -19920,7 +19839,7 @@ function classifyStaleLeaks(input) {
19920
19839
  kind: "stale-branch",
19921
19840
  ref: branch,
19922
19841
  detail: `merged/closed branch ${branch} has no worktree`,
19923
- remediation: "mmi-cli gc --apply"
19842
+ remediation: "mmi-cli worktree gc --apply"
19924
19843
  });
19925
19844
  }
19926
19845
  for (const wt of input.worktrees) {
@@ -19944,7 +19863,7 @@ function classifyStaleLeaks(input) {
19944
19863
  kind: "orphan-dir",
19945
19864
  ref: dir,
19946
19865
  detail: `directory under the worktrees root is not a registered worktree`,
19947
- remediation: `mmi-cli gc --apply (or: Remove-Item/rm -rf "${dir}")`
19866
+ remediation: `mmi-cli worktree gc --apply (or: Remove-Item/rm -rf "${dir}")`
19948
19867
  });
19949
19868
  }
19950
19869
  }
@@ -20738,9 +20657,9 @@ function buildPluginGuardDecision(i) {
20738
20657
  if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
20739
20658
  return { state: "healthy" };
20740
20659
  }
20741
- function buildGuardSessionStartLine(state, opts = {}) {
20660
+ function buildPluginGuardLine(state, opts = {}) {
20742
20661
  if (state === "healthy" || state === "not-org") return { exitCode: 0 };
20743
- const recovery = opts.recovery ?? "mmi-cli plugin-heal";
20662
+ const recovery = opts.recovery ?? "mmi-cli plugin heal";
20744
20663
  const restartHint = opts.restartHint ?? "restart your agent host / reload plugins";
20745
20664
  const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its marketplace/cache is unresolved";
20746
20665
  return {
@@ -20945,8 +20864,7 @@ async function applyPluginHeal(surface, log, opts) {
20945
20864
  }
20946
20865
  return true;
20947
20866
  }
20948
- async function runGuard(opts = {}, readOrigin) {
20949
- void opts;
20867
+ async function runGuard(readOrigin) {
20950
20868
  try {
20951
20869
  const surface = detectSurface(process.env);
20952
20870
  if (surfaceToken(surface) !== "claude") {
@@ -20956,7 +20874,7 @@ async function runGuard(opts = {}, readOrigin) {
20956
20874
  const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
20957
20875
  const input = snapshotPluginGuardInput(surface, isOrgRepo);
20958
20876
  const { state } = buildPluginGuardDecision(input);
20959
- const { line, exitCode } = buildGuardSessionStartLine(state);
20877
+ const { line, exitCode } = buildPluginGuardLine(state);
20960
20878
  if (line) console.error(line);
20961
20879
  process.exitCode = exitCode;
20962
20880
  } catch {
@@ -21097,7 +21015,7 @@ function formatLastRun(r) {
21097
21015
  }
21098
21016
  function formatDeployStatus(r) {
21099
21017
  const lines = [
21100
- `deploy status \u2014 ${r.repo} (${r.slug}) stage ${r.stage}`,
21018
+ `runtime deploy status \u2014 ${r.repo} (${r.slug}) stage ${r.stage}`,
21101
21019
  "",
21102
21020
  `running version: ${r.runningVersion ?? "none stamped"}`,
21103
21021
  `last deploy run: ${formatLastRun(r)}`,
@@ -21112,7 +21030,7 @@ function formatDeployStatus(r) {
21112
21030
  }
21113
21031
 
21114
21032
  // src/deploy-commands.ts
21115
- var STAGES2 = ["dev", "rc", "main"];
21033
+ var STAGES = ["dev", "rc", "main"];
21116
21034
  async function probeHttpBounded(url, timeoutMs = 5e3) {
21117
21035
  const controller = new AbortController();
21118
21036
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -21129,8 +21047,8 @@ async function probeHttpBounded(url, timeoutMs = 5e3) {
21129
21047
  function registerDeployCommands(program3) {
21130
21048
  const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
21131
21049
  deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
21132
- if (!STAGES2.includes(stage)) {
21133
- return fail(`deploy status: <stage> must be dev, rc, or main`);
21050
+ if (!STAGES.includes(stage)) {
21051
+ return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
21134
21052
  }
21135
21053
  try {
21136
21054
  const cfg = await loadConfig();
@@ -21159,7 +21077,7 @@ function registerDeployCommands(program3) {
21159
21077
  }
21160
21078
  if (report.health?.ok === false) process.exitCode = 1;
21161
21079
  } catch (e) {
21162
- return failGraceful(`deploy status: ${e.message}`);
21080
+ return failGraceful(`runtime deploy status: ${e.message}`);
21163
21081
  }
21164
21082
  });
21165
21083
  }
@@ -21278,7 +21196,7 @@ async function collectOnboardStatus() {
21278
21196
  if (read.ok && read.project) {
21279
21197
  registry2 = { ok: true, detail: `project "${read.project.name ?? slug}" registered` };
21280
21198
  } else if (read.ok) {
21281
- registry2 = { ok: false, detail: `repo "${slug}" not registered \u2014 run mmi-cli bootstrap or mmi-cli project set <owner/repo>` };
21199
+ registry2 = { ok: false, detail: `repo "${slug}" not registered \u2014 run mmi-cli bootstrap or mmi-cli org project set <owner/repo>` };
21282
21200
  } else {
21283
21201
  registry2 = { ok: false, detail: `read failed: ${read.error}` };
21284
21202
  }
@@ -21305,7 +21223,7 @@ async function collectOnboardStatus() {
21305
21223
  if (!cfg.sagaApiUrl) {
21306
21224
  nextCommand = "mmi-cli doctor \u2014 fix Hub API URL configuration first";
21307
21225
  } else if (!registry2.ok) {
21308
- nextCommand = "mmi-cli project set <owner/repo>";
21226
+ nextCommand = "mmi-cli org project set <owner/repo>";
21309
21227
  } else if (!board.ok) {
21310
21228
  nextCommand = "mmi-cli board read";
21311
21229
  } else {
@@ -21374,10 +21292,11 @@ var LOOP_PLAYBOOKS = {
21374
21292
  agent: {
21375
21293
  title: "Agent",
21376
21294
  steps: [
21377
- { label: "Resume continuity from Jerv's Memory", command: "jerv-cli resume" },
21295
+ { label: "Orient in the current repository", command: "mmi-cli onboard" },
21378
21296
  { label: "Read the next board item", command: "mmi-cli board read" },
21379
- { label: "Create an isolated worktree from development", command: "mmi-cli worktree create feature/<issue-number>-<short-description> --from origin/development" },
21297
+ { label: "Claim it and create an isolated worktree", command: "mmi-cli worktree create <issue-number> --claim --from origin/development" },
21380
21298
  { label: "Build and test in the touched package", command: "npm test && npm run build" },
21299
+ { label: "Publish the branch", command: "git push -u origin HEAD" },
21381
21300
  { label: "Open the development-base PR", command: 'mmi-cli pr create --title "<title>" --body-file PR_BODY.md --base development' },
21382
21301
  { label: "Wait for checks and land to development", command: "mmi-cli pr checks-wait <PR-number> && mmi-cli pr land <PR-number>" },
21383
21302
  { label: "Release only after the gated train is authorized", command: "mmi-cli release --apply" }
@@ -21386,16 +21305,17 @@ var LOOP_PLAYBOOKS = {
21386
21305
  "start-work": {
21387
21306
  title: "Start Work",
21388
21307
  steps: [
21389
- { label: "Pull latest development", command: "git checkout development && git pull origin development" },
21390
- { label: "Create a feature branch", command: "git checkout -b feature/<issue-number>-<short-description>" },
21391
- { label: "Claim the board item", command: "mmi-cli board claim <issue-number>" },
21308
+ { label: "Orient in the current repository", command: "mmi-cli onboard" },
21309
+ { label: "Read the board item", command: "mmi-cli board show <issue-number>" },
21310
+ { label: "Claim it and create the worktree from development", command: "mmi-cli worktree create <issue-number> --claim --from origin/development" },
21392
21311
  { label: "Start a local stage (deployable repos)", command: "mmi-cli stage run --apply" }
21393
21312
  ]
21394
21313
  },
21395
21314
  "ship-pr": {
21396
21315
  title: "Ship PR",
21397
21316
  steps: [
21398
- { label: "Push branch and open PR", command: "git push -u origin HEAD && gh pr create --fill --base development" },
21317
+ { label: "Publish the branch", command: "git push -u origin HEAD" },
21318
+ { label: "Open the development-base PR", command: 'mmi-cli pr create --title "<title>" --body-file PR_BODY.md --base development' },
21399
21319
  { label: "Wait for CI checks", command: "mmi-cli pr checks-wait <PR-number>" },
21400
21320
  { label: "Land the PR (merge to development)", command: "mmi-cli pr land <PR-number>" }
21401
21321
  ]
@@ -21403,9 +21323,9 @@ var LOOP_PLAYBOOKS = {
21403
21323
  "hotfix": {
21404
21324
  title: "Hotfix",
21405
21325
  steps: [
21406
- { label: "Start hotfix", command: "mmi-cli hotfix start" },
21407
- { label: "Apply fix commits", command: "git cherry-pick -x <commit-sha>" },
21408
- { label: "Release hotfix to production", command: "mmi-cli hotfix release" }
21326
+ { label: "Create the main-base hotfix PR from an already-merged fix", command: "mmi-cli hotfix start --from <pr#|sha>" },
21327
+ { label: "Wait for the hotfix PR checks", command: "mmi-cli pr checks-wait <PR-number>" },
21328
+ { label: "After the PR is merged, run the gated release", command: "mmi-cli hotfix release <vX.Y.Z> --carries <pr#|sha>" }
21409
21329
  ]
21410
21330
  }
21411
21331
  };
@@ -21455,6 +21375,22 @@ function formatExplainCommand(cmd, rootName) {
21455
21375
  }
21456
21376
  return lines.join("\n").trimEnd();
21457
21377
  }
21378
+ function formatExplainGroup(cmd, rootName) {
21379
+ const lines = [
21380
+ `${rootName} ${cmd.path}${cmd.description ? ` \u2014 ${cmd.description}` : ""}`,
21381
+ `Category: ${cmd.category} \xB7 Discovery: ${cmd.discovery}`,
21382
+ "",
21383
+ "Commands:"
21384
+ ];
21385
+ for (const child2 of cmd.subcommands) {
21386
+ const args = child2.arguments.map((arg) => {
21387
+ const name = arg.variadic ? `${arg.name}...` : arg.name;
21388
+ return arg.required ? `<${name}>` : `[${name}]`;
21389
+ }).join(" ");
21390
+ lines.push(` ${child2.path}${args ? ` ${args}` : ""}${child2.description ? ` \u2014 ${child2.description}` : ""}`);
21391
+ }
21392
+ return lines.join("\n");
21393
+ }
21458
21394
  function formatExplainLoop(playbook) {
21459
21395
  const lines = [`${playbook.title} loop:`];
21460
21396
  for (const step of playbook.steps) {
@@ -21464,7 +21400,15 @@ function formatExplainLoop(playbook) {
21464
21400
  return lines.join("\n");
21465
21401
  }
21466
21402
  function findCommandInManifest(manifest, commandPath2) {
21467
- return manifest.index.find((c) => c.path === commandPath2 || c.path.endsWith(` ${commandPath2}`));
21403
+ const visit = (command) => {
21404
+ if (command.path === commandPath2) return command;
21405
+ for (const child2 of command.subcommands) {
21406
+ const found = visit(child2);
21407
+ if (found) return found;
21408
+ }
21409
+ return void 0;
21410
+ };
21411
+ return visit(manifest.tree);
21468
21412
  }
21469
21413
  function registerExplainCommand(program3) {
21470
21414
  program3.command("explain").description("print a command's purpose, flags, and examples from the live schema \u2014 one grounding call replaces trial-and-error").argument("[command...]", 'the command path to explain (e.g. "issue create")').option("--loop <name>", "print the canonical command sequence: agent | start-work | ship-pr | hotfix").action((commandArgs, opts) => {
@@ -21479,18 +21423,16 @@ function registerExplainCommand(program3) {
21479
21423
  }
21480
21424
  const manifest = buildCommandManifest(program3);
21481
21425
  if (!commandArgs.length) {
21482
- console.log(manifest.name + (manifest.version ? ` v${manifest.version}` : ""));
21483
- console.log(manifest.tree.description ?? "");
21484
- console.log("\nRun `mmi-cli commands` to list every subcommand + its flags.");
21426
+ console.log(formatManifestHuman(manifest));
21485
21427
  return;
21486
21428
  }
21487
21429
  const commandPath2 = commandArgs.join(" ");
21488
- const leaf = findCommandInManifest(manifest, commandPath2);
21489
- if (!leaf) {
21430
+ const command = findCommandInManifest(manifest, commandPath2);
21431
+ if (!command) {
21490
21432
  fail(`explain: unknown command "${commandPath2}"`, { code: ERROR_CODES.ERR_NOT_FOUND });
21491
21433
  return;
21492
21434
  }
21493
- console.log(formatExplainCommand(leaf, manifest.name));
21435
+ console.log(command.subcommands.length ? formatExplainGroup(command, manifest.name) : formatExplainCommand(command, manifest.name));
21494
21436
  });
21495
21437
  }
21496
21438
 
@@ -22127,7 +22069,7 @@ function checkClaudePlugin(probe) {
22127
22069
  return {
22128
22070
  ok: false,
22129
22071
  label: guardState === "no-install" ? "Claude plugin \u2014 not installed" : "Claude plugin \u2014 unresolved (marketplace/cache missing)",
22130
- fix: "run `mmi-cli plugin-heal` to reinstall the MMI marketplace + plugin, then restart Claude",
22072
+ fix: "run `mmi-cli plugin heal` to reinstall the MMI marketplace + plugin, then restart Claude",
22131
22073
  verbose: evidence
22132
22074
  };
22133
22075
  }
@@ -22186,7 +22128,7 @@ function checkPluginCache(input) {
22186
22128
  // Lead with the versioned-cache framing when there are stale versions; otherwise report the staging litter
22187
22129
  // on its own so a cache with zero versioned dirs still gets an honest ✗.
22188
22130
  detail: input.stale.length ? `${input.cached} versions cached (${parts.join("; ")})` : parts.join("; "),
22189
- fix: "run `mmi-cli plugin-prune` to review, then `mmi-cli plugin-prune --apply` to delete",
22131
+ fix: "run `mmi-cli plugin prune` to review, then `mmi-cli plugin prune --apply` to delete",
22190
22132
  verbose: evidence
22191
22133
  };
22192
22134
  }
@@ -22216,7 +22158,7 @@ function checkSchedules(probe) {
22216
22158
  ok: false,
22217
22159
  label: "schedules",
22218
22160
  detail: `${probe.incomplete.length} source(s) unreadable`,
22219
- fix: "run `mmi-cli schedules` for the full report \u2014 the notebook may be missing armed entries",
22161
+ fix: "run `mmi-cli org schedules` for the full report \u2014 the notebook may be missing armed entries",
22220
22162
  verbose: evidence
22221
22163
  };
22222
22164
  }
@@ -22225,7 +22167,7 @@ function checkSchedules(probe) {
22225
22167
  ok: false,
22226
22168
  label: "schedules",
22227
22169
  detail: `${probe.drift.length} drift finding(s)`,
22228
- fix: "run `mmi-cli schedules` \u2014 file/registry/live disagree; each drift line names its per-class remedy, applied in the owning repo",
22170
+ fix: "run `mmi-cli org schedules` \u2014 file/registry/live disagree; each drift line names its per-class remedy, applied in the owning repo",
22229
22171
  verbose: evidence
22230
22172
  };
22231
22173
  }
@@ -22581,11 +22523,13 @@ function allLongFlags() {
22581
22523
  if (cachedLongFlags) return cachedLongFlags;
22582
22524
  const acc = /* @__PURE__ */ new Set();
22583
22525
  const walk = (node) => {
22526
+ if (!isCanonicalSuggestion(node)) return;
22584
22527
  for (const opt of node.options) {
22528
+ if (opt.discovery) continue;
22585
22529
  const m = /--[\w-]+/.exec(opt.flags);
22586
22530
  if (m) acc.add(m[0]);
22587
22531
  }
22588
- for (const child of node.subcommands) walk(child);
22532
+ for (const child2 of node.subcommands) walk(child2);
22589
22533
  };
22590
22534
  walk(buildCommandManifest(program2).tree);
22591
22535
  cachedLongFlags = [...acc];
@@ -22621,7 +22565,7 @@ function envelopeAwareWriteErr(str) {
22621
22565
  process.stderr.write(str);
22622
22566
  }
22623
22567
  var program2 = new Command();
22624
- program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).configureOutput({ writeErr: envelopeAwareWriteErr }).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)");
22568
+ program2.name("mmi-cli").description("MMI Future Hub CLI \u2014 the org control plane for agentic coding.").version(resolveClientVersion()).configureOutput({ writeErr: envelopeAwareWriteErr }).showHelpAfterError("(run `mmi-cli commands` for the agentic-coding map, `mmi-cli explain <command>` for detail, or `mmi-cli commands --json --primary` for machine discovery). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)");
22625
22569
  function appActorDeps() {
22626
22570
  return {
22627
22571
  fetchSecret: async (key) => fetchSecretValue(makeSecretsDeps(await loadConfig()), key, { repo: APP_VAULT_REPO }),
@@ -22651,22 +22595,26 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
22651
22595
  if (opts.write) {
22652
22596
  if (plan.changed) {
22653
22597
  (0, import_node_fs27.writeFileSync)(path2, plan.content, "utf8");
22654
- console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
22598
+ console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
22655
22599
  } else {
22656
- console.log("mmi-cli rules gitignore: up to date");
22600
+ console.log("mmi-cli org rules gitignore: up to date");
22657
22601
  }
22658
22602
  return;
22659
22603
  }
22660
22604
  if (plan.changed) {
22661
- console.error(`mmi-cli rules gitignore: managed block drift (${drift}) \u2014 run \`mmi-cli rules gitignore --write\` and commit`);
22605
+ console.error(`mmi-cli org rules gitignore: managed block drift (${drift}) \u2014 run \`mmi-cli org rules gitignore --write\` and commit`);
22662
22606
  process.exitCode = 1;
22663
22607
  } else {
22664
- console.log("mmi-cli rules gitignore: up to date");
22608
+ console.log("mmi-cli org rules gitignore: up to date");
22665
22609
  }
22666
22610
  });
22667
- program2.command("commands").description("print the command manifest \u2014 every subcommand + its flags (ground against this instead of guessing)").option("--json", "machine-readable JSON: { name, version, tree, index } \u2014 index is a flat list of every leaf command path").action((o) => {
22611
+ program2.command("commands").description("print the grouped agentic-coding map; use --all for operational depth").option("--all", "include operational and internal depth").option("--json", "machine-readable command manifest").option("--primary", "with --json, emit only the compact primary tree and index").action((o) => {
22668
22612
  const manifest = buildCommandManifest(program2);
22669
- consoleIo.log(o.json ? JSON.stringify(manifest, null, 2) : formatManifestHuman(manifest));
22613
+ if (o.json) {
22614
+ consoleIo.log(JSON.stringify(o.primary ? primaryCommandManifest(manifest) : manifest, null, 2));
22615
+ return;
22616
+ }
22617
+ consoleIo.log(formatManifestHuman(manifest, { all: o.all }));
22670
22618
  });
22671
22619
  async function runWhoami(io = consoleIo) {
22672
22620
  const cfg = await loadConfig();
@@ -22743,21 +22691,21 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
22743
22691
  );
22744
22692
  if (o.json) return console.log(JSON.stringify(result));
22745
22693
  if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
22746
- if (result.removed.length) console.log(`gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
22747
- if (result.stillDeferred.length) console.log(`gc sweep-deferred: ${result.stillDeferred.length} still queued (will retry on next session)`);
22748
- if (result.skipped.length) console.log(`gc sweep-deferred: ${result.skipped.length} dropped (repurposed for different work since queued, left untouched)`);
22749
- if (!result.removed.length && !result.stillDeferred.length && !result.skipped.length) console.log("gc sweep-deferred: nothing queued");
22694
+ if (result.removed.length) console.log(`worktree gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
22695
+ if (result.stillDeferred.length) console.log(`worktree gc sweep-deferred: ${result.stillDeferred.length} still queued (will retry on next session)`);
22696
+ if (result.skipped.length) console.log(`worktree gc sweep-deferred: ${result.skipped.length} dropped (repurposed for different work since queued, left untouched)`);
22697
+ if (!result.removed.length && !result.stillDeferred.length && !result.skipped.length) console.log("worktree gc sweep-deferred: nothing queued");
22750
22698
  }
22751
22699
  } catch (e) {
22752
- fail(`gc sweep-deferred: ${e.message}`);
22700
+ fail(`worktree gc sweep-deferred: ${e.message}`);
22753
22701
  }
22754
22702
  }, DEFERRED_SWEEP_HARD_TIMEOUT_MS, () => {
22755
- console.error("gc sweep-deferred: exceeded hard timeout \u2014 exiting so the detached worker cannot leak (#2841)");
22703
+ console.error("worktree gc sweep-deferred: exceeded hard timeout \u2014 exiting so the detached worker cannot leak (#2841)");
22756
22704
  process.exit(process.exitCode ?? 0);
22757
22705
  });
22758
22706
  });
22759
22707
  gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch and plans whose current hash is confirmed synced to North Star (#1864) instead of git branches/refs").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
22760
- if (o.apply && o.dryRun) return fail("gc: choose either --dry-run or --apply");
22708
+ if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
22761
22709
  if (o.scratch) {
22762
22710
  try {
22763
22711
  const root = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
@@ -22765,11 +22713,11 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
22765
22713
  if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, ...run }, null, 2));
22766
22714
  return console.log(formatScratchGcPlan(run.plan, Boolean(o.apply), run.applied));
22767
22715
  } catch (e) {
22768
- return fail(`gc --scratch: ${e.message}`);
22716
+ return fail(`worktree gc --scratch: ${e.message}`);
22769
22717
  }
22770
22718
  }
22771
22719
  const limit = Number.parseInt(o.limit, 10);
22772
- if (!Number.isFinite(limit) || limit < 1) return fail("gc: --limit must be a positive integer");
22720
+ if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
22773
22721
  try {
22774
22722
  const plan = await gcPlan(o.remote, limit);
22775
22723
  if (o.apply && !o.json) console.log(formatGcPlan(plan, false));
@@ -22794,7 +22742,7 @@ ${renderGcApplyResult(applyResult)}`);
22794
22742
  }
22795
22743
  if (applyResult?.failed.length) process.exitCode = 1;
22796
22744
  } catch (e) {
22797
- fail(`gc: ${e.message}`);
22745
+ fail(`worktree gc: ${e.message}`);
22798
22746
  }
22799
22747
  });
22800
22748
  var NPM_PROVISION_TIMEOUT_MS = 3e5;
@@ -22804,19 +22752,19 @@ function runWorktreeInstall(command, cwd, quiet) {
22804
22752
  const file = isWin2 ? "cmd.exe" : bin;
22805
22753
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
22806
22754
  return new Promise((resolve5, reject) => {
22807
- const child = (0, import_node_child_process12.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
22755
+ const child2 = (0, import_node_child_process12.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
22808
22756
  const timer = setTimeout(() => {
22809
22757
  try {
22810
- child.kill();
22758
+ child2.kill();
22811
22759
  } catch {
22812
22760
  }
22813
22761
  reject(new Error(`${command} timed out after ${NPM_PROVISION_TIMEOUT_MS}ms in ${cwd}`));
22814
22762
  }, NPM_PROVISION_TIMEOUT_MS);
22815
- child.on("error", (e) => {
22763
+ child2.on("error", (e) => {
22816
22764
  clearTimeout(timer);
22817
22765
  reject(e);
22818
22766
  });
22819
- child.on("exit", (code) => {
22767
+ child2.on("exit", (code) => {
22820
22768
  clearTimeout(timer);
22821
22769
  if (code === 0) resolve5();
22822
22770
  else reject(new Error(`${command} exited ${code} in ${cwd}`));
@@ -22994,7 +22942,7 @@ async function attachToProject(issueNumber, repo, priority) {
22994
22942
  return { onBoard: false };
22995
22943
  }
22996
22944
  if (!cfg.projectId) {
22997
- console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
22945
+ console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli org project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
22998
22946
  return { onBoard: false };
22999
22947
  }
23000
22948
  try {
@@ -23074,7 +23022,7 @@ docsAudit.command("record").description("write a dated janitor verdict for one r
23074
23022
  if (o.outcome === "clean") outcome = { kind: "clean" };
23075
23023
  else if (o.outcome === "refreshed") outcome = { kind: "refreshed", count: Number(o.count) };
23076
23024
  else if (o.outcome === "failed") outcome = { kind: "failed", reason: o.reason ?? "" };
23077
- else return failGraceful(`docs-audit record: --outcome must be clean|refreshed|failed, got "${o.outcome}"`);
23025
+ else return failGraceful(`docs audit record: --outcome must be clean|refreshed|failed, got "${o.outcome}"`);
23078
23026
  const verdict = docsAuditRecord({ repo, date, shaRange: o.shaRange, outcome, checkerVendor: o.checkerVendor });
23079
23027
  await reportWrite("docs-audit record", await recordDocsAudit(verdict, registryClientDeps(await loadConfig())));
23080
23028
  } catch (e) {
@@ -23102,50 +23050,56 @@ async function reportWrite(label, res) {
23102
23050
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
23103
23051
  }
23104
23052
  var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
23105
- tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, rc-only retire, read-only verify-secrets) for a tenant via the central tenant-control.yml workflow; project-admin dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets watch by default)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
23053
+ tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; project-admin own dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker watch by default)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
23106
23054
  try {
23107
23055
  const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch });
23108
23056
  if (!o.json && action === "verify-secrets" && result.secrets) {
23109
23057
  const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
23110
23058
  const { lines, failure } = renderVerifySecrets(body);
23111
23059
  for (const line of lines) printLine(line);
23112
- if (failure) return failGraceful(`tenant control ${stage} verify-secrets: ${failure}`);
23060
+ if (failure) return failGraceful(`runtime tenant control ${stage} verify-secrets: ${failure}`);
23061
+ } else if (!o.json && action === "verify-broker" && result.broker) {
23062
+ const { lines, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
23063
+ for (const line of lines) printLine(line);
23064
+ if (failure) return failGraceful(`runtime tenant control ${stage} verify-broker: ${failure}`);
23113
23065
  } else {
23114
23066
  printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
23115
23067
  }
23116
23068
  if (result.conclusion === "failure") {
23117
- return failGraceful(`tenant control ${stage} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`);
23069
+ return failGraceful(`runtime tenant control ${stage} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`);
23118
23070
  }
23119
23071
  } catch (e) {
23120
- return failGraceful(`tenant control: ${e.message}`);
23072
+ return failGraceful(`runtime tenant control: ${e.message}`);
23121
23073
  }
23122
23074
  });
23123
- 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) => {
23124
- if (!["dev", "rc", "main"].includes(stage)) return fail("tenant status: <stage> must be dev, rc, or main");
23125
- const cfg = await loadConfig();
23126
- const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
23127
- console.log(JSON.stringify(result, null, 2));
23128
- if (result.publicProbe?.ok === false) process.exitCode = 1;
23075
+ tenant.command("reconcile <owner/repo> <stage>").description("re-render this tenant stage's generated box assets from registry truth; project-admin own dev/rc, master main; watches by default").option("--watch", "wait for the tenant-reconcile.yml run (default)").option("--no-watch", "return after dispatch; do not redeploy until the reconcile run succeeds").option("--json", "machine-readable output").action(async (repo, stage, o) => {
23076
+ try {
23077
+ const result = await runTenantReconcile(trainApplyDeps(), { repo, stage, watch: o.watch });
23078
+ printLine(o.json ? JSON.stringify(result, null, 2) : `${result.note}${result.runUrl ? ` \u2014 ${result.runUrl}` : ""}`);
23079
+ if (result.conclusion === "failure" || !result.dispatched) process.exitCode = 1;
23080
+ } catch (e) {
23081
+ return failGraceful(`runtime tenant reconcile: ${e.message}`);
23082
+ }
23129
23083
  });
23130
- tenant.command("readiness <owner/repo> <stage>").description("alias for tenant status: read-only tenant runtime readiness").action(async (repo, stage) => {
23131
- if (!["dev", "rc", "main"].includes(stage)) return fail("tenant readiness: <stage> must be dev, rc, or main");
23084
+ tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints").action(async (repo, stage) => {
23085
+ if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
23132
23086
  const cfg = await loadConfig();
23133
23087
  const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
23134
23088
  console.log(JSON.stringify(result, null, 2));
23135
23089
  if (result.publicProbe?.ok === false) process.exitCode = 1;
23136
23090
  });
23137
23091
  tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main, or `development` for dev \u2014 the promoted/staging ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage, o) => {
23138
- if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("tenant redeploy: <stage> must be dev, rc, or main");
23092
+ if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("runtime tenant redeploy: <stage> must be dev, rc, or main");
23139
23093
  try {
23140
23094
  const result = await runTenantRedeploy(trainApplyDeps(), { repo, stage, ref: o.ref, watch: o.watch });
23141
23095
  return printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantRedeploy(result));
23142
23096
  } catch (e) {
23143
- return failGraceful(`tenant redeploy: ${e.message}`);
23097
+ return failGraceful(`runtime tenant redeploy: ${e.message}`);
23144
23098
  }
23145
23099
  });
23146
23100
  tenant.command("sweep-rc").description("discover (and optionally retire) running rc tenant runtimes across tenant-containers \u2014 orphan cleanup after a failed post-release retire (#942)").option("--retire", "retire every running rc runtime found (requires --yes) \u2014 WARNING: tears down a legitimately-staged rc too").option("--yes", "confirm the destructive --retire").option("--json", "machine-readable output").action(async (o) => {
23147
23101
  if (o.retire && !o.yes) {
23148
- return fail("tenant sweep-rc --retire is destructive (it tears down EVERY running rc, including one legitimately staged between /rcand and /release) \u2014 re-run with --yes to confirm");
23102
+ return fail("runtime tenant sweep-rc --retire is destructive (it tears down EVERY running rc, including one legitimately staged between /rcand and /release) \u2014 re-run with --yes to confirm");
23149
23103
  }
23150
23104
  const cfg = await loadConfig();
23151
23105
  const cdeps = registryClientDeps(cfg);
@@ -23165,17 +23119,9 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
23165
23119
  }, { retire: !!o.retire });
23166
23120
  return printLine(o.json ? JSON.stringify(result) : renderSweep(result));
23167
23121
  } catch (e) {
23168
- return failGraceful(`tenant sweep-rc: ${e.message}`);
23122
+ return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
23169
23123
  }
23170
23124
  });
23171
- async function resolveDnsBounded(host, timeoutMs = 3e3) {
23172
- const { lookup } = await import("node:dns/promises");
23173
- const probe = lookup(host).then(() => true).catch((e) => dnsErrorToResolution(e?.code));
23174
- const timeout = new Promise((resolve5) => {
23175
- setTimeout(() => resolve5(void 0), timeoutMs).unref?.();
23176
- });
23177
- return Promise.race([probe, timeout]);
23178
- }
23179
23125
  async function probeHttpBounded2(url, timeoutMs = 5e3) {
23180
23126
  const controller = new AbortController();
23181
23127
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -23206,75 +23152,14 @@ async function buildTenantRuntimeStatusFor(target, stage, cfg) {
23206
23152
  lastTenantDeployRun: (await fetchLastTenantDeployRun(slug, stage)).run
23207
23153
  });
23208
23154
  }
23209
- async function v2ReadinessDeps(cfg) {
23210
- const reg = registryClientDeps(cfg);
23211
- return {
23212
- resolveDns: (host) => resolveDnsBounded(host),
23213
- // Checked read (#727/#733): the doctor distinguishes a FAILED read (degraded report) from a 404.
23214
- getProject: (slug) => fetchProjectBySlugChecked(slug, reg),
23215
- hasDeployCoords: async (slug, stage) => {
23216
- const status = await fetchDeployStatusBySlug(slug, reg);
23217
- return Boolean(status?.stages[stage]);
23218
- },
23219
- hasDeployState: async (slug, stage) => {
23220
- const status = await fetchDeployStatusBySlug(slug, reg);
23221
- return Boolean(status?.deployState[stage]);
23222
- },
23223
- getDeployFact: async (slug, stage) => {
23224
- const facts = await fetchDeployFactsBySlug(slug, reg);
23225
- const fact = facts?.stages[stage];
23226
- return fact ? { port: fact.port, domain: fact.domain } : null;
23227
- },
23228
- listSecrets: async (targetRepo2) => {
23229
- const apiUrl = cfg.sagaApiUrl;
23230
- if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
23231
- const qs = new URLSearchParams({ repo: targetRepo2 }).toString();
23232
- const res = await fetchWithRetry(fetch, `${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
23233
- method: "GET",
23234
- headers: await hubHeaders()
23235
- }, { attempts: 2, timeoutMs: 5e3 });
23236
- if (!res.ok) throw new Error(`secrets list failed for ${targetRepo2}: HTTP ${res.status}`);
23237
- const body = await res.json();
23238
- return (body.secrets ?? []).map((s) => s.key).filter(Boolean);
23239
- }
23240
- };
23241
- }
23242
- async function updateV2ReadinessIssue(repo, report, healed) {
23243
- const title = "v2 readiness: central deploy + secrets alignment";
23244
- const freshBody = renderReadinessIssueBody("", report, { healed });
23245
- const list = await execFileP2("gh", ["issue", "list", "--repo", repo, "--state", "open", "--search", "v2 readiness in:title", "--json", "number,title", "--limit", "20"], { timeout: 2e4 });
23246
- const issues = JSON.parse(list.stdout || "[]");
23247
- const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
23248
- if (!existing) {
23249
- const create = await bodyArgsViaFile(["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"]);
23250
- try {
23251
- const created = await execFileP2("gh", create.args, { timeout: GH_MUTATION_TIMEOUT_MS });
23252
- const url = created.stdout.trim();
23253
- const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
23254
- return { number, url, action: "created" };
23255
- } finally {
23256
- await create.cleanup();
23257
- }
23258
- }
23259
- const view = await execFileP2("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
23260
- const current = JSON.parse(view.stdout || "{}");
23261
- const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
23262
- const edit = await bodyArgsViaFile(["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody]);
23263
- try {
23264
- await execFileP2("gh", edit.args, { timeout: GH_MUTATION_TIMEOUT_MS });
23265
- } finally {
23266
- await edit.cleanup();
23267
- }
23268
- return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
23269
- }
23270
- var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); attest is project-admin; set is master-only");
23155
+ var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
23271
23156
  async function projectTarget(commandName, explicitTarget) {
23272
23157
  return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
23273
23158
  }
23274
23159
  project.command("list").description("list all projects (identity + board, never deploy coords)").option("--json", "machine-readable output").action(async (o) => {
23275
23160
  const cfg = await loadConfig();
23276
23161
  const projects = await fetchProjectsList(registryClientDeps(cfg));
23277
- if (!projects) return failGraceful("project list: Hub API unreachable or this repo is not bootstrapped");
23162
+ if (!projects) return failGraceful("org project list: Hub API unreachable or this repo is not bootstrapped");
23278
23163
  if (o.json) {
23279
23164
  console.log(JSON.stringify(projects));
23280
23165
  return;
@@ -23287,16 +23172,16 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
23287
23172
  const cfg = await loadConfig();
23288
23173
  let target;
23289
23174
  try {
23290
- target = await projectTarget("project get", repoOrSlug);
23175
+ target = await projectTarget("org project get", repoOrSlug);
23291
23176
  } catch (e) {
23292
23177
  return fail(e.message);
23293
23178
  }
23294
23179
  const read = await fetchProjectBySlugChecked(slugOf(target), registryClientDeps(cfg));
23295
23180
  if (!read.ok) {
23296
- return failGraceful(`project get: Hub registry read failed (${read.error}) \u2014 likely transient (cold start, network, or auth blip); retry shortly`);
23181
+ return failGraceful(`org project get: Hub registry read failed (${read.error}) \u2014 likely transient (cold start, network, or auth blip); retry shortly`);
23297
23182
  }
23298
23183
  if (!read.project) {
23299
- return failGraceful(`project get: no registry META for ${target} (unknown or unbootstrapped)`);
23184
+ return failGraceful(`org project get: no registry META for ${target} (unknown or unbootstrapped)`);
23300
23185
  }
23301
23186
  console.log(JSON.stringify(read.project));
23302
23187
  if (!o.json) {
@@ -23307,117 +23192,31 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
23307
23192
  console.error(
23308
23193
  `${m.name ?? target} \xB7 class ${m.class ?? "?"} \xB7 deploy ${m.deployModel ?? "?"}
23309
23194
  release track: ${track} \u2014 stages: ${stages}${note}
23310
- deploys run centrally (tenant-deploy.yml); product repos carry no deploy files. Deploy coords are OIDC-gated \u2014 see \`project resolve\`.`
23195
+ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files. Inspect nonsecret DEPLOY facts with \`mmi-cli org project deploy get\`; full coords remain OIDC-gated.`
23311
23196
  );
23312
23197
  }
23313
23198
  });
23314
- project.command("resolve <owner/repo>").description("deploy coords for a stage \u2014 for diagnosis. NOTE: /deploy-coords is OIDC-gated (a deploy job\u2019s id-token), so a gh-token CLI cannot read it from a dev machine").option("--stage <main|rc>", "deploy stage", "main").option("--json", "machine-readable output").action((_repoOrRepo, o) => {
23315
- const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect nonsecret DEPLOY facts with `mmi-cli project deploy get`. Full coords stay OIDC-gated.";
23316
- if (o.json) {
23317
- console.log(JSON.stringify({ ok: false, stage: o.stage, error: msg }));
23318
- process.exitCode = 1;
23319
- return;
23320
- }
23321
- fail(msg);
23322
- });
23323
23199
  var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
23324
23200
  projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
23325
23201
  const cfg = await loadConfig();
23326
23202
  let target;
23327
23203
  try {
23328
- target = await projectTarget("project deploy get", repoOrSlug);
23329
- } catch (e) {
23330
- return fail(e.message);
23331
- }
23332
- const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
23333
- if (!out) return failGraceful(`project deploy get: Hub deploy facts read failed for ${target}`);
23334
- const stage = o.stage?.trim();
23335
- if (stage && !["dev", "rc", "main"].includes(stage)) return fail("project deploy get: --stage must be dev, rc, or main");
23336
- const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
23337
- console.log(JSON.stringify(payload));
23338
- });
23339
- projectDeploy.command("list [owner/repo]").description("alias for project deploy get; lists all stages by default").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
23340
- const cfg = await loadConfig();
23341
- let target;
23342
- try {
23343
- target = await projectTarget("project deploy list", repoOrSlug);
23204
+ target = await projectTarget("org project deploy get", repoOrSlug);
23344
23205
  } catch (e) {
23345
23206
  return fail(e.message);
23346
23207
  }
23347
23208
  const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
23348
- if (!out) return failGraceful(`project deploy list: Hub deploy facts read failed for ${target}`);
23209
+ if (!out) return failGraceful(`org project deploy get: Hub deploy facts read failed for ${target}`);
23349
23210
  const stage = o.stage?.trim();
23350
- if (stage && !["dev", "rc", "main"].includes(stage)) return fail("project deploy list: --stage must be dev, rc, or main");
23211
+ if (stage && !["dev", "rc", "main"].includes(stage)) return fail("org project deploy get: --stage must be dev, rc, or main");
23351
23212
  const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
23352
23213
  console.log(JSON.stringify(payload));
23353
23214
  });
23354
- 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 `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) => {
23215
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
23355
23216
  const cfg = await loadConfig();
23356
23217
  let target;
23357
23218
  try {
23358
- target = await projectTarget("project doctor", repo);
23359
- } catch (e) {
23360
- return fail(e.message);
23361
- }
23362
- const report = await buildV2Doctor(target, await v2ReadinessDeps(cfg));
23363
- console.log(JSON.stringify(report));
23364
- if (!report.ok) process.exitCode = 1;
23365
- });
23366
- 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) => {
23367
- const cfg = await loadConfig();
23368
- let target;
23369
- try {
23370
- target = await projectTarget("project heal", repo);
23371
- } catch (e) {
23372
- return fail(e.message);
23373
- }
23374
- const reg = registryClientDeps(cfg);
23375
- const out = await runV2Heal(target, { apply: o.apply }, {
23376
- fetchProject: (s) => fetchProjectBySlugChecked(s, reg),
23377
- upsertProject: (s, patch) => upsertProject(s, patch, reg)
23378
- });
23379
- if (out.status === "aborted") return failGraceful(`project heal: ${out.error}`);
23380
- if (out.status === "planned") {
23381
- console.log(JSON.stringify({ ok: true, slug: out.slug, dryRun: true, patch: out.patch, appOwnedGaps: out.appOwnedGaps }));
23382
- return;
23383
- }
23384
- if (out.status === "noop") {
23385
- console.log(JSON.stringify({ ok: true, slug: out.slug, applied: [], appOwnedGaps: out.appOwnedGaps }));
23386
- return;
23387
- }
23388
- if (out.status === "write-failed") return reportWrite("project heal", out.write);
23389
- console.log(JSON.stringify({ ok: true, slug: out.slug, applied: out.applied, appOwnedGaps: out.appOwnedGaps, result: out.result }));
23390
- });
23391
- 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) => {
23392
- if (!o.updateIssue) return fail("project readiness: pass --update-issue");
23393
- const cfg = await loadConfig();
23394
- let target;
23395
- try {
23396
- target = await projectTarget("project readiness", repo);
23397
- } catch (e) {
23398
- return fail(e.message);
23399
- }
23400
- const report = await buildV2Doctor(target, await v2ReadinessDeps(cfg));
23401
- const issue2 = await updateV2ReadinessIssue(target, report, []);
23402
- console.log(JSON.stringify({ ok: true, repo: target, issue: issue2, ready: report.ok }));
23403
- });
23404
- 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) => {
23405
- const cfg = await loadConfig();
23406
- let target;
23407
- try {
23408
- target = await projectTarget("project attest", repoOrSlug);
23409
- } catch (e) {
23410
- return fail(e.message);
23411
- }
23412
- const repo = target.includes("/") ? target : `mutmutco/${slugOf(target)}`;
23413
- const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
23414
- return reportWrite("project attest", res);
23415
- });
23416
- 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) => {
23417
- const cfg = await loadConfig();
23418
- let target;
23419
- try {
23420
- target = await projectTarget("project set", repoOrSlug);
23219
+ target = await projectTarget("org project set", repoOrSlug);
23421
23220
  } catch (e) {
23422
23221
  return fail(e.message);
23423
23222
  }
@@ -23426,12 +23225,12 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
23426
23225
  const setAlias = o.set ?? [];
23427
23226
  const vars = [...o.var ?? [], ...setAlias];
23428
23227
  const dupe = duplicateVarKeyAcrossFlags(o.var ?? [], setAlias);
23429
- if (dupe) return fail(`project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
23228
+ if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
23430
23229
  if (o.secretsFile) {
23431
23230
  try {
23432
23231
  vars.push(`secrets=${(0, import_node_fs27.readFileSync)(o.secretsFile, "utf8")}`);
23433
23232
  } catch (e) {
23434
- return fail(`project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
23233
+ return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
23435
23234
  }
23436
23235
  }
23437
23236
  let patch;
@@ -23446,19 +23245,19 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
23446
23245
  clearWebProfile: Boolean(o.clearWebProfile)
23447
23246
  });
23448
23247
  } catch (e) {
23449
- return fail(e.message.replace(/^project set: /, "project set: "));
23248
+ return fail(e.message.replace(/^org project set: /, "org project set: "));
23450
23249
  }
23451
23250
  const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
23452
23251
  const boardError = boardLinkWriteError(patch, existing);
23453
- if (boardError) return fail(`project set: ${boardError}`);
23252
+ if (boardError) return fail(`org project set: ${boardError}`);
23454
23253
  const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
23455
- return reportWrite("project set", res);
23254
+ return reportWrite("org project set", res);
23456
23255
  });
23457
23256
  project.command("retire [owner/repo]").description("retire an orphaned registry slug (master-only): soft-delete the PROJECT# META row + drop the repo's board items; secrets/vault left alone. DRY-RUN by default \u2014 pass --apply to actually delete; defaults to the current repo").option("--apply", "actually delete (default is a dry-run preview of what would be removed)").option("--skip-board", "retire the registry META only \u2014 leave board items in place").option("--json", "machine-readable {slug, removedMeta, removedBoardItem} output").action(async (repoOrSlug, o) => {
23458
23257
  const cfg = await loadConfig();
23459
23258
  let target;
23460
23259
  try {
23461
- target = await projectTarget("project retire", repoOrSlug);
23260
+ target = await projectTarget("org project retire", repoOrSlug);
23462
23261
  } catch (e) {
23463
23262
  return fail(e.message);
23464
23263
  }
@@ -23484,7 +23283,7 @@ project.command("retire [owner/repo]").description("retire an orphaned registry
23484
23283
  printLine(JSON.stringify(structured));
23485
23284
  } else {
23486
23285
  const verb = result.applied ? "retired" : "WOULD retire (dry-run; pass --apply to delete)";
23487
- printLine(`project retire: ${verb} ${result.slug} (${result.repo})`);
23286
+ printLine(`org project retire: ${verb} ${result.slug} (${result.repo})`);
23488
23287
  if (result.applied) {
23489
23288
  printLine(` META: ${result.removedMeta.ok ? `removed=${result.removedMeta.removed ?? "unknown"}` : `FAILED \u2014 ${result.removedMeta.error}`}`);
23490
23289
  } else {
@@ -23499,10 +23298,10 @@ project.command("retire [owner/repo]").description("retire an orphaned registry
23499
23298
  printLine(` ${result.vaultNote}`);
23500
23299
  }
23501
23300
  if (result.applied && !result.removedMeta.ok) {
23502
- return failGraceful(`project retire: META delete failed \u2014 ${result.removedMeta.error}`);
23301
+ return failGraceful(`org project retire: META delete failed \u2014 ${result.removedMeta.error}`);
23503
23302
  }
23504
23303
  });
23505
- var fullTrack = program2.command("full-track").description("direct-to-full-track readiness audits");
23304
+ var fullTrack = program2.command("full-track").description("direct-to-train readiness audits");
23506
23305
  fullTrack.command("readiness <owner/repo>").description("aggregate branch topology, train authority, deploy facts, endpoint health, and rcand readiness for direct -> full switches").option("--json", "machine-readable output").action(async (repo, _o) => {
23507
23306
  const cfg = await loadConfig();
23508
23307
  const reg = registryClientDeps(cfg);
@@ -23515,7 +23314,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
23515
23314
  buildTenantRuntimeStatusFor(repo, "rc", cfg),
23516
23315
  buildTenantRuntimeStatusFor(repo, "main", cfg)
23517
23316
  ]);
23518
- if (!metaRead.ok) return failGraceful(`full-track readiness: Hub registry read failed (${metaRead.error})`);
23317
+ if (!metaRead.ok) return failGraceful(`train readiness: Hub registry read failed (${metaRead.error})`);
23519
23318
  const report = buildFullTrackReadinessReport({
23520
23319
  repo,
23521
23320
  slug,
@@ -23527,31 +23326,37 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
23527
23326
  console.log(JSON.stringify(report, null, 2));
23528
23327
  if (!report.rcand.canApply) process.exitCode = 1;
23529
23328
  });
23530
- 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 box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").option("--substrate <substrate>", "hetzner-ssh; omit to leave the row unchanged").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) \u2014 omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 true = #2662 env passthrough, no .env symlink; omit to leave the row unchanged").option("--force", "skip the #2748 fileless-compose guard (flip noEnvFile=true even if the stage branch compose still declares env_file:)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
23329
+ project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row \u2014 project-admin may set only --no-env-file on their own existing dev/rc row; master may seed/change all coords and main; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into; omit to keep the stored value (required only for a NEW hetzner-ssh row \u2014 `mmi-cli runtime box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").option("--substrate <substrate>", "hetzner-ssh; omit to leave the row unchanged").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) \u2014 omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 own-repo project-admin dev/rc; master main; true = env passthrough with no .env symlink").option("--force", "explicit recovery override: skip fileless-compose verification only after independent proof").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
23531
23330
  const cfg = await loadConfig();
23532
23331
  let target;
23533
23332
  try {
23534
- target = await projectTarget("project set-deploy", repoOrSlug);
23333
+ target = await projectTarget("org project set-deploy", repoOrSlug);
23535
23334
  } catch (e) {
23536
23335
  return fail(e.message);
23537
23336
  }
23538
23337
  let noEnvFile;
23539
23338
  if (typeof o.envFile === "string") {
23540
23339
  const v = o.envFile.trim().toLowerCase();
23541
- if (v !== "true" && v !== "false") return fail("project set-deploy: --no-env-file must be true or false");
23340
+ if (v !== "true" && v !== "false") return fail("org project set-deploy: --no-env-file must be true or false");
23542
23341
  noEnvFile = v === "true";
23543
23342
  }
23544
- if (noEnvFile === true) {
23343
+ if (noEnvFile !== void 0) {
23545
23344
  const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
23546
23345
  if (branch) {
23547
23346
  const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
23548
23347
  const sameRepo = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
23549
23348
  if (sameRepo) {
23550
- const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force) });
23551
- if (!verdict.ok) return fail(`project set-deploy: ${verdict.reason}`);
23552
- if (verdict.warn) console.error(`project set-deploy: ${verdict.warn}`);
23349
+ const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force), noEnvFile });
23350
+ if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
23351
+ ${filelessTransitionGuide(target, o.stage)}`);
23352
+ if (verdict.warn) console.error(`org project set-deploy: ${verdict.warn}`);
23353
+ } else if (!o.force) {
23354
+ return fail(
23355
+ `org project set-deploy: cannot verify ${target} ${branch} from this checkout \u2014 refusing to change noEnvFile. Run from the target repo with origin/${branch} fetched.
23356
+ ${filelessTransitionGuide(target, o.stage)}`
23357
+ );
23553
23358
  } else {
23554
- console.error(`project set-deploy: --no-env-file guard skipped \u2014 run from the ${target} checkout to verify the ${branch} branch compose (#2748).`);
23359
+ console.error(`org project set-deploy: --force: skipping cross-repo fileless-compose verification for ${target} ${branch}.`);
23555
23360
  }
23556
23361
  }
23557
23362
  }
@@ -23571,17 +23376,18 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
23571
23376
  clearAliases: o.clearAliases,
23572
23377
  noEnvFile
23573
23378
  });
23379
+ if (target.includes("/")) body.repo = target;
23574
23380
  } catch (e) {
23575
23381
  return fail(e.message);
23576
23382
  }
23577
23383
  const res = await setDeployCoords(slug, body, registryClientDeps(cfg));
23578
- return reportWrite("project set-deploy", res);
23384
+ return reportWrite("org project set-deploy", res);
23579
23385
  });
23580
23386
  var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
23581
23387
  registry.command("org").description("the org config (account id, region, orgProjectId, sagaApiUrl)").option("--json", "machine-readable output").action(async (_o) => {
23582
23388
  const cfg = await loadConfig();
23583
23389
  const org = await fetchOrgConfig(registryClientDeps(cfg));
23584
- if (!org) return failGraceful("registry org: Hub API unreachable, unseeded, or this repo is not bootstrapped");
23390
+ if (!org) return failGraceful("org config get: Hub API unreachable, unseeded, or this repo is not bootstrapped");
23585
23391
  console.log(JSON.stringify(org));
23586
23392
  });
23587
23393
  var oauth = program2.command("oauth").description("per-repo Google OAuth \u2014 plan the canonical URI set, verify the client is port-agnostic");
@@ -23593,7 +23399,7 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
23593
23399
  try {
23594
23400
  oc = parseOauthConfig(meta ?? {}, slug);
23595
23401
  } catch (e) {
23596
- return failGraceful(`oauth plan: ${e.message}`);
23402
+ return failGraceful(`org oauth plan: ${e.message}`);
23597
23403
  }
23598
23404
  const origins = expectedJsOrigins(oc);
23599
23405
  const redirects = expectedRedirectUris(oc);
@@ -23611,18 +23417,18 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
23611
23417
  console.log(`
23612
23418
  SSM cred params (under /mmi-future/${slug}/):`);
23613
23419
  ssm.forEach((k) => console.log(` ${k}`));
23614
- console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; store creds with `mmi-cli oauth set-creds`.");
23420
+ console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; store creds with `mmi-cli org oauth set-creds`.");
23615
23421
  });
23616
23422
  oauth.command("set-creds").description('store the OAuth client into the canonical stageless GOOGLE_CLIENT_ID/SECRET pair (pipe the Console "Download JSON" on stdin)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
23617
23423
  const raw = await readStdin();
23618
23424
  if (!raw.trim()) {
23619
- return fail("oauth set-creds: pipe the Google client JSON on stdin \u2014 e.g.\n mmi-cli oauth set-creds --repo <owner/repo> < client.json");
23425
+ return fail("org oauth set-creds: pipe the Google client JSON on stdin \u2014 e.g.\n mmi-cli org oauth set-creds --repo <owner/repo> < client.json");
23620
23426
  }
23621
23427
  let creds;
23622
23428
  try {
23623
23429
  creds = parseOauthClientJson(raw);
23624
23430
  } catch (e) {
23625
- return fail(`oauth set-creds: ${e.message}`);
23431
+ return fail(`org oauth set-creds: ${e.message}`);
23626
23432
  }
23627
23433
  await withSecrets(async (d) => {
23628
23434
  for (const key of oauthSsmKeys()) {
@@ -23632,7 +23438,7 @@ oauth.command("set-creds").description('store the OAuth client into the canonica
23632
23438
  return;
23633
23439
  }
23634
23440
  }
23635
- console.log(`OAuth client stored in the ${oauthSsmKeys().length} canonical stageless keys. Run \`mmi-cli oauth verify\` to confirm the client is port-agnostic.`);
23441
+ console.log(`OAuth client stored in the ${oauthSsmKeys().length} canonical stageless keys. Run \`mmi-cli org oauth verify\` to confirm the client is port-agnostic.`);
23636
23442
  });
23637
23443
  });
23638
23444
  oauth.command("verify").description("probe Google authorize with an arbitrary port (:9123) to confirm the client is port-agnostic").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--client-id <id>", "OAuth client_id (else read the stageless GOOGLE_CLIENT_ID from SSM)").option("--json", "machine-readable output").action(async (o) => {
@@ -23643,7 +23449,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
23643
23449
  try {
23644
23450
  oc = parseOauthConfig(meta ?? {}, slug);
23645
23451
  } catch (e) {
23646
- return failGraceful(`oauth verify: ${e.message}`);
23452
+ return failGraceful(`org oauth verify: ${e.message}`);
23647
23453
  }
23648
23454
  let clientId = o.clientId;
23649
23455
  if (!clientId) {
@@ -23652,7 +23458,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
23652
23458
  });
23653
23459
  }
23654
23460
  if (!clientId) {
23655
- return failGraceful("oauth verify: no client_id (pass --client-id, or provision the repo so GOOGLE_CLIENT_ID exists)");
23461
+ return failGraceful("org oauth verify: no client_id (pass --client-id, or provision the repo so GOOGLE_CLIENT_ID exists)");
23656
23462
  }
23657
23463
  const redirectUri = probeRedirectUri(oc.callbackPath);
23658
23464
  let body = "";
@@ -23660,7 +23466,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
23660
23466
  const res = await fetch(buildAuthorizeProbeUrl(clientId, redirectUri), { redirect: "follow", signal: AbortSignal.timeout(1e4) });
23661
23467
  body = await res.text();
23662
23468
  } catch (e) {
23663
- return failGraceful(`oauth verify: probe request failed: ${e.message}`);
23469
+ return failGraceful(`org oauth verify: probe request failed: ${e.message}`);
23664
23470
  }
23665
23471
  const mismatch = authorizeBodyHasMismatch(body);
23666
23472
  if (o.json) {
@@ -24418,6 +24224,12 @@ function trainApplyDeps() {
24418
24224
  const body = res.body;
24419
24225
  return { ok: false, category: body?.category, error: body?.error ?? res.error };
24420
24226
  },
24227
+ dispatchTenantReconcile: async ({ repo, stage }) => {
24228
+ const res = await tenantReconcile({ repo, stage }, registryClientDeps(await loadConfig()));
24229
+ if (res.ok) return { ok: true };
24230
+ const body = res.body;
24231
+ return { ok: false, category: body?.category, error: body?.error ?? res.error };
24232
+ },
24421
24233
  // Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
24422
24234
  // release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
24423
24235
  // fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
@@ -24479,7 +24291,7 @@ function renderTrainApply(commandName, r) {
24479
24291
  return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
24480
24292
  }
24481
24293
  function renderTenantRedeploy(r) {
24482
- return `mmi-cli tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
24294
+ return `mmi-cli runtime tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
24483
24295
  }
24484
24296
  async function resolveRcandPlanTargets() {
24485
24297
  try {
@@ -24619,7 +24431,7 @@ var access = program2.command("access").description("org access audit (read-only
24619
24431
  access.command("role [repo]").description("D14 train authority for a repo (server-side Hub check): master | project-admin | developer").option("--json", "machine-readable output").action(async (repoArg) => {
24620
24432
  const o = { json: rawFlag("--json") };
24621
24433
  const repo = repoArg ?? await resolveRepo();
24622
- if (!repo) return fail("access role: pass <owner/repo> or run inside a repo checkout");
24434
+ if (!repo) return fail("org access role: pass <owner/repo> or run inside a repo checkout");
24623
24435
  const cfg = await loadConfig();
24624
24436
  const verdict = await fetchTrainAuthority(repo, registryClientDeps(cfg));
24625
24437
  if (!verdict.ok) {
@@ -24628,7 +24440,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
24628
24440
  process.exitCode = 1;
24629
24441
  return;
24630
24442
  }
24631
- return failGraceful(`access role: ${verdict.error}`);
24443
+ return failGraceful(`org access role: ${verdict.error}`);
24632
24444
  }
24633
24445
  const a = verdict.authority;
24634
24446
  if (o.json) {
@@ -24646,7 +24458,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
24646
24458
  const cfg = await loadConfig();
24647
24459
  const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
24648
24460
  if (o.repo) {
24649
- if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
24461
+ if (o.class !== "deployable" && o.class !== "content") return failGraceful("org access audit: --class must be deployable or content");
24650
24462
  const meta = registryProjects?.find((project2) => (project2.repos ?? []).some((repo) => repo.toLowerCase() === o.repo.toLowerCase())) ?? null;
24651
24463
  const repoClass = o.class;
24652
24464
  targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
@@ -24672,7 +24484,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
24672
24484
  });
24673
24485
  access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
24674
24486
  var isWin2 = process.platform === "win32";
24675
- program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--ci", "stable CI output contract; no color, exit 0 for clear/advisory-only, 1 for hard gaps, 2 for doctor/tool failure").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").option("--self", "verify CLI/plugin version parity, PATH, gh auth scopes, and hook wiring (offline-safe; suggests plugin-heal on a hard gap) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n 2 doctor/tool execution failed before a normal verdict\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
24487
+ program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin-heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--ci", "stable CI output contract; no color, exit 0 for clear/advisory-only, 1 for hard gaps, 2 for doctor/tool failure").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").option("--self", "verify CLI/plugin version parity, PATH, gh auth scopes, and hook wiring (offline-safe; suggests plugin-heal on a hard gap) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n 2 doctor/tool execution failed before a normal verdict\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
24676
24488
  if (opts.guide) {
24677
24489
  consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
24678
24490
  return;
@@ -24691,7 +24503,7 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
24691
24503
  mmiDoctorDeps()
24692
24504
  );
24693
24505
  });
24694
- program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
24506
+ program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk").action(() => runGuard());
24695
24507
  program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
24696
24508
  function directoryBytes(path2) {
24697
24509
  let total = 0;
@@ -24702,11 +24514,11 @@ function directoryBytes(path2) {
24702
24514
  return 0;
24703
24515
  }
24704
24516
  for (const entry of entries) {
24705
- const child = (0, import_node_path23.join)(path2, entry.name);
24706
- if (entry.isDirectory()) total += directoryBytes(child);
24517
+ const child2 = (0, import_node_path23.join)(path2, entry.name);
24518
+ if (entry.isDirectory()) total += directoryBytes(child2);
24707
24519
  else {
24708
24520
  try {
24709
- total += (0, import_node_fs27.statSync)(child).size;
24521
+ total += (0, import_node_fs27.statSync)(child2).size;
24710
24522
  } catch {
24711
24523
  }
24712
24524
  }
@@ -24773,7 +24585,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
24773
24585
  });
24774
24586
  program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
24775
24587
  if (isInsideRepoSubdir(process.cwd())) {
24776
- console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
24588
+ console.error("[mmi-hook] plugin session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
24777
24589
  appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (repo subdirectory)" });
24778
24590
  return;
24779
24591
  }
@@ -24835,13 +24647,9 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
24835
24647
  appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
24836
24648
  });
24837
24649
  installProcessBackstop();
24838
- program2.command("lane", { hidden: true }).action(() => {
24839
- fail("lane orchestration lives in jerv-cli \u2014 try: jerv-cli lane --help");
24840
- });
24841
- program2.command("fusion", { hidden: true }).action(() => {
24842
- fail("fusion orchestration lives in jerv-cli \u2014 try: jerv-cli fusion --help");
24843
- });
24844
- program2.parseAsync().then(() => finishCliRun()).catch((e) => failGraceful(e.message));
24650
+ consolidateCommandNamespaces(program2);
24651
+ applyCommandTaxonomy(program2);
24652
+ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
24845
24653
  // Annotate the CommonJS export names for ESM import in node:
24846
24654
  0 && (module.exports = {
24847
24655
  DEFAULT_PRIORITY,