@mutmutco/cli 3.75.0 → 3.76.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 (2) hide show
  1. package/dist/main.cjs +1040 -186
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3579,7 +3579,12 @@ var ERROR_CODES = {
3579
3579
  /** Missing / rejected credentials on a path that needs auth. */
3580
3580
  ERR_NO_AUTH: "ERR_NO_AUTH",
3581
3581
  /** The operation partially succeeded (some units done, some failed). */
3582
- ERR_PARTIAL: "ERR_PARTIAL"
3582
+ ERR_PARTIAL: "ERR_PARTIAL",
3583
+ /** The request was well-formed and every referent resolved, but the target's CURRENT STATE forbids the
3584
+ * mutation (HTTP 409 semantics) — e.g. reparenting an issue that already has a parent. Deliberately not
3585
+ * one code per API constraint: retry is futile until the named state is changed, and that is the fact a
3586
+ * caller has to act on, whichever rule produced it. */
3587
+ ERR_STATE_CONFLICT: "ERR_STATE_CONFLICT"
3583
3588
  };
3584
3589
  var ERROR_CODE_REFERENCE = [
3585
3590
  {
@@ -3621,6 +3626,11 @@ var ERROR_CODE_REFERENCE = [
3621
3626
  code: ERROR_CODES.ERR_PARTIAL,
3622
3627
  meaning: "A batch operation completed some units and failed others.",
3623
3628
  typical_fix: "Read the per-unit results, fix the failed inputs, and rerun only the failed units."
3629
+ },
3630
+ {
3631
+ code: ERROR_CODES.ERR_STATE_CONFLICT,
3632
+ meaning: "The request was valid and every referent resolved, but the target resource's current state forbids it.",
3633
+ typical_fix: "A plain retry fails identically. Read the state field the envelope names (e.g. `current_parent`), change that state deliberately with the command that owns it, then retry."
3624
3634
  }
3625
3635
  ];
3626
3636
  function buildErrorEnvelope(message, payload) {
@@ -3629,6 +3639,7 @@ function buildErrorEnvelope(message, payload) {
3629
3639
  if (payload.expected !== void 0) env.expected = payload.expected;
3630
3640
  if (payload.did_you_mean !== void 0) env.did_you_mean = payload.did_you_mean;
3631
3641
  if (payload.corrected_command !== void 0) env.corrected_command = payload.corrected_command;
3642
+ if (payload.current_parent !== void 0) env.current_parent = payload.current_parent;
3632
3643
  return env;
3633
3644
  }
3634
3645
  function formatErrorEnvelope(message, payload) {
@@ -4067,11 +4078,18 @@ function fail(msg, payload) {
4067
4078
  }
4068
4079
  hardExit(1);
4069
4080
  }
4070
- function planMutation(opts, args, commandName, planFn) {
4081
+ async function failGracefulEnvelope(msg, payload) {
4082
+ if (payload && argvWantsMachineFailure()) {
4083
+ console.error(formatErrorEnvelope(msg, payload));
4084
+ return cleanExit(1);
4085
+ }
4086
+ return failGraceful(msg);
4087
+ }
4088
+ async function planMutation(opts, args, commandName, planFn) {
4071
4089
  const validateOnly = opts.validateOnly === true;
4072
4090
  const dryRun = opts.dryRun === true;
4073
4091
  if (!validateOnly && !dryRun) return null;
4074
- const planned = planFn ? planFn(opts, args) : { command: commandName, args };
4092
+ const planned = planFn ? await planFn(opts, args) : { command: commandName, args };
4075
4093
  return validateOnly ? { ok: true, planned } : { dry_run: true, planned };
4076
4094
  }
4077
4095
  function commandPath(cmd) {
@@ -4097,11 +4115,11 @@ function mutating(cmd, planFn) {
4097
4115
  cmd.option("--dry-run", "resolve + validate, print the planned action as JSON, and exit without writing");
4098
4116
  cmd.option("--validate-only", "validate flags/enums/refs, print {ok,planned} or the C2 error envelope, and exit without writing");
4099
4117
  const registerAction = cmd.action.bind(cmd);
4100
- cmd.action = ((handler) => registerAction((...actionArgs) => {
4118
+ cmd.action = ((handler) => registerAction(async (...actionArgs) => {
4101
4119
  const command = actionArgs[actionArgs.length - 1];
4102
4120
  const opts = command.opts();
4103
4121
  const positionals = actionArgs.slice(0, -2);
4104
- const out = planMutation(opts, positionals, commandPath(command), planFn);
4122
+ const out = await planMutation(opts, positionals, commandPath(command), planFn);
4105
4123
  if (out) {
4106
4124
  console.log(JSON.stringify(out));
4107
4125
  return;
@@ -4128,6 +4146,97 @@ function printLine(value) {
4128
4146
  `);
4129
4147
  }
4130
4148
 
4149
+ // src/issue-surface.ts
4150
+ var SURFACE_PREFIX = "surface:";
4151
+ var SURFACE_READ_TIMEOUT_MS = 1e4;
4152
+ function labelsCarrySurface(labels) {
4153
+ return (labels ?? []).some((l) => l.trim().toLowerCase().startsWith(SURFACE_PREFIX));
4154
+ }
4155
+ function surfaceLabel(value) {
4156
+ const v = value.trim();
4157
+ return v.toLowerCase().startsWith(SURFACE_PREFIX) ? v : `${SURFACE_PREFIX}${v}`;
4158
+ }
4159
+ async function readRepoSurfaceLabels(repo, deps = {}) {
4160
+ const run = deps.run ?? execFileP2;
4161
+ try {
4162
+ const { stdout } = await run(
4163
+ "gh",
4164
+ ["label", "list", "--repo", repo, "--limit", "1000", "--json", "name"],
4165
+ { timeout: SURFACE_READ_TIMEOUT_MS }
4166
+ );
4167
+ const parsed = JSON.parse(stdout);
4168
+ if (!Array.isArray(parsed)) return void 0;
4169
+ return parsed.map((l) => l.name).filter((n) => typeof n === "string" && n.toLowerCase().startsWith(SURFACE_PREFIX));
4170
+ } catch {
4171
+ return void 0;
4172
+ }
4173
+ }
4174
+ async function checkSurfaceRequirement(input, deps = {}) {
4175
+ if (input.waiver) {
4176
+ const reason = input.waiver.reason.trim();
4177
+ if (!reason) {
4178
+ return {
4179
+ enforcing: false,
4180
+ refusal: {
4181
+ message: `${input.command ?? "issue create"}: a surface waiver without a reason is not a ruling \u2014 declare WHY this filing is genuinely surface-less, or drop the waiver and let the preflight run`,
4182
+ payload: {
4183
+ code: ERROR_CODES.ERR_MISSING_FLAG,
4184
+ offending_flag: "waiver.reason",
4185
+ expected: ["a non-empty waiver reason"]
4186
+ }
4187
+ }
4188
+ };
4189
+ }
4190
+ return {
4191
+ enforcing: false,
4192
+ waiver: { applied: true, reason }
4193
+ };
4194
+ }
4195
+ const read = deps.read ?? readRepoSurfaceLabels;
4196
+ const known = await read(input.repo, { run: deps.run });
4197
+ if (known === void 0) {
4198
+ return {
4199
+ enforcing: false,
4200
+ warn: `warning: could not read ${input.repo}'s labels, so the surface-label requirement was not checked; if that board enforces one surface:* label per open issue, add one with \`mmi-cli issue edit <n> --add-label surface:<value>\``
4201
+ };
4202
+ }
4203
+ if (known.length === 0) return { enforcing: false };
4204
+ if (labelsCarrySurface(input.labels)) return { enforcing: true };
4205
+ const command = input.command ?? "issue create";
4206
+ const where = input.rowLabel ? `${input.rowLabel}: ` : "";
4207
+ return {
4208
+ enforcing: true,
4209
+ refusal: {
4210
+ message: `${command}: ${where}${input.repo} requires every open issue to carry exactly one surface:* label, and this one sets none \u2014 filing it unlabeled reds the board check on every open PR in that repo. Pass --surface <value>, or --no-surface if this filing is genuinely exempt`,
4211
+ payload: {
4212
+ code: ERROR_CODES.ERR_MISSING_FLAG,
4213
+ offending_flag: "--surface",
4214
+ // Advisory, not a closed enum: the board rule is "exactly one surface:* label", whatever its value.
4215
+ expected: [...known].sort()
4216
+ }
4217
+ }
4218
+ };
4219
+ }
4220
+ function conflictingSurfaceInputs(surfaceFlag, labels) {
4221
+ if (!surfaceFlag) return void 0;
4222
+ const wanted = surfaceLabel(surfaceFlag).toLowerCase();
4223
+ const fromLabels = (labels ?? []).map((l) => l.trim()).filter((l) => l.toLowerCase().startsWith(SURFACE_PREFIX));
4224
+ const disagreeing = fromLabels.filter((l) => l.toLowerCase() !== wanted);
4225
+ if (!disagreeing.length) return void 0;
4226
+ return {
4227
+ message: `issue create: --surface ${surfaceLabel(surfaceFlag)} contradicts --label ${disagreeing.join(", ")} \u2014 an issue carries exactly one surface, so name it once`,
4228
+ payload: {
4229
+ code: ERROR_CODES.ERR_CONFLICTING_FLAGS,
4230
+ offending_flag: "--surface",
4231
+ expected: [surfaceLabel(surfaceFlag), ...disagreeing]
4232
+ }
4233
+ };
4234
+ }
4235
+ async function surfaceLabelApplies(repo, deps = {}) {
4236
+ const known = await (deps.read ?? readRepoSurfaceLabels)(repo, { run: deps.run });
4237
+ return (known?.length ?? 0) > 0;
4238
+ }
4239
+
4131
4240
  // src/session-start.ts
4132
4241
  var import_node_fs8 = require("node:fs");
4133
4242
  var import_node_path6 = require("node:path");
@@ -6378,10 +6487,12 @@ async function primaryCheckoutRootOf(git2) {
6378
6487
  return void 0;
6379
6488
  }
6380
6489
  }
6490
+ var SHA_LIKE_RE = /^[0-9a-f]{7,40}$/i;
6381
6491
  function resolveWorktreeBase(from, remote) {
6382
6492
  const remotePrefix = `${remote}/`;
6383
- const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
6384
- return { base: from, fetchBranch };
6493
+ if (from.startsWith(remotePrefix)) return { base: from, fetchBranch: from.slice(remotePrefix.length) };
6494
+ if (SHA_LIKE_RE.test(from)) return { base: from };
6495
+ return { base: from, fetchBranch: from, preferRemote: `${remotePrefix}${from}` };
6385
6496
  }
6386
6497
  var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration/i;
6387
6498
  function isGitConfigLockError(error) {
@@ -6491,7 +6602,7 @@ function commandLadderHint() {
6491
6602
  }
6492
6603
 
6493
6604
  // src/index.ts
6494
- var import_node_path31 = require("node:path");
6605
+ var import_node_path32 = require("node:path");
6495
6606
 
6496
6607
  // src/merge-ci-policy.ts
6497
6608
  function resolveMergeCiPolicy(input) {
@@ -6776,22 +6887,80 @@ function patchRulesetRequiredContexts(body, contexts) {
6776
6887
  function findProductRuleset(rulesets) {
6777
6888
  return rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
6778
6889
  }
6779
- async function latestCompletedGateRun(repo, client, query) {
6780
- const res = await client.rest(
6781
- "GET",
6782
- `repos/${repo}/actions/workflows/gate.yml/runs?status=completed&per_page=1${query}`,
6783
- { timeoutMs: 2e4 }
6784
- );
6785
- return res?.workflow_runs?.[0];
6890
+ async function completedGateRuns(repo, client, query, perPage = 1, gateFiles = DEFAULT_GATE_FILES) {
6891
+ const merged = [];
6892
+ let lastError;
6893
+ let anyRead = false;
6894
+ for (const file of gateFiles) {
6895
+ try {
6896
+ const res = await client.rest(
6897
+ "GET",
6898
+ `repos/${repo}/actions/workflows/${encodeURIComponent(file)}/runs?status=completed&per_page=${perPage}${query}`,
6899
+ { timeoutMs: 2e4 }
6900
+ );
6901
+ anyRead = true;
6902
+ merged.push(...res?.workflow_runs ?? []);
6903
+ } catch (e) {
6904
+ lastError = e;
6905
+ }
6906
+ }
6907
+ if (!anyRead && lastError) throw lastError;
6908
+ return merged.sort((a, b) => Date.parse(b.updated_at ?? "") - Date.parse(a.updated_at ?? ""));
6786
6909
  }
6787
- async function gateIsProvenGreen(repo, client, baseBranch) {
6910
+ var GATE_STREAK_PAGE = 30;
6911
+ var DEFAULT_GATE_FILES = ["gate.yml"];
6912
+ async function readDefaultBranchGate(repo, client, baseBranch, now = Date.now(), gateFiles = DEFAULT_GATE_FILES) {
6913
+ const named = gateFiles.length === 1 ? gateFiles[0] : `[${gateFiles.join(", ")}]`;
6914
+ let runs;
6915
+ let onBaseBranch = true;
6788
6916
  try {
6789
- const onBase = await latestCompletedGateRun(repo, client, `&branch=${encodeURIComponent(baseBranch)}`);
6790
- const latest = onBase ?? await latestCompletedGateRun(repo, client, "");
6791
- return latest?.conclusion === "success";
6792
- } catch {
6793
- return false;
6917
+ runs = await completedGateRuns(repo, client, `&branch=${encodeURIComponent(baseBranch)}`, GATE_STREAK_PAGE, gateFiles);
6918
+ if (!runs.length) {
6919
+ runs = await completedGateRuns(repo, client, "", GATE_STREAK_PAGE, gateFiles);
6920
+ onBaseBranch = false;
6921
+ }
6922
+ } catch (e) {
6923
+ return { state: "unknown", detail: `${named} run history unreadable \u2014 ${e.message}` };
6924
+ }
6925
+ const latest = runs[0];
6926
+ if (!latest) {
6927
+ return {
6928
+ state: "unknown",
6929
+ detail: `no completed ${named} run on ${baseBranch} or any branch \u2014 a freshly seeded gate and a gate that cannot pass look the same from here`
6930
+ };
6794
6931
  }
6932
+ const where = onBaseBranch ? baseBranch : `${latest.head_branch ?? "any branch"} (no completed run on ${baseBranch})`;
6933
+ const common = {
6934
+ conclusion: latest.conclusion ?? void 0,
6935
+ branch: latest.head_branch ?? void 0,
6936
+ runUrl: latest.html_url ?? void 0
6937
+ };
6938
+ if (latest.conclusion === "success") {
6939
+ return { ...common, state: "green", detail: `latest completed ${named} run on ${where} succeeded at ${latest.updated_at ?? "unknown time"}` };
6940
+ }
6941
+ let since = latest;
6942
+ let bounded = runs.length >= GATE_STREAK_PAGE;
6943
+ for (const run of runs) {
6944
+ if (run.conclusion === "success") {
6945
+ bounded = false;
6946
+ break;
6947
+ }
6948
+ since = run;
6949
+ }
6950
+ const sinceAt = since.updated_at ?? latest.updated_at ?? void 0;
6951
+ const redDays = sinceAt ? Math.max(0, Math.floor((now - Date.parse(sinceAt)) / 864e5)) : void 0;
6952
+ const age = redDays == null ? "unknown duration" : `${redDays} day${redDays === 1 ? "" : "s"}${bounded ? " or longer" : ""}`;
6953
+ return {
6954
+ ...common,
6955
+ state: "red",
6956
+ redSince: sinceAt,
6957
+ redDays,
6958
+ redSinceBounded: bounded,
6959
+ detail: `gate.yml is failing on ${where} \u2014 ${latest.conclusion ?? "not successful"} since ${sinceAt ?? "an unknown time"} (${age})`
6960
+ };
6961
+ }
6962
+ async function gateIsProvenGreen(repo, client, baseBranch, gateFiles = DEFAULT_GATE_FILES) {
6963
+ return (await readDefaultBranchGate(repo, client, baseBranch, Date.now(), gateFiles)).state === "green";
6795
6964
  }
6796
6965
  async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active") {
6797
6966
  const body = { ...rulesetBody, enforcement };
@@ -6919,6 +7088,16 @@ function collectPullRequestWorkflowContexts(workflows) {
6919
7088
  }
6920
7089
  return [...contexts].sort((a, b) => a.localeCompare(b));
6921
7090
  }
7091
+ function pathFilteredPullRequestWorkflows(workflows) {
7092
+ const filtered = [];
7093
+ for (const wf of workflows) {
7094
+ if (!workflowTriggersPullRequest(wf.body)) continue;
7095
+ const onBlock = extractOnBlock(wf.body);
7096
+ if (!onBlock) continue;
7097
+ if (/^\s*paths(-ignore)?\s*:/m.test(onBlock)) filtered.push(wf.path);
7098
+ }
7099
+ return filtered.sort((a, b) => a.localeCompare(b));
7100
+ }
6922
7101
  function contextsMatchRuleset(required, emitted) {
6923
7102
  if (required.size !== emitted.size) return false;
6924
7103
  for (const c of required) if (!emitted.has(c)) return false;
@@ -7732,29 +7911,32 @@ function decideSeedDelivery(branchRules) {
7732
7911
  if (gating.length) return { mode: "pr", reason: `base branch is protected (${gating.join(", ")}) \u2014 seed via a branch + PR` };
7733
7912
  return { mode: "direct", reason: "base branch is unprotected \u2014 direct PUT is safe" };
7734
7913
  }
7735
- function planSeedDelivery(branchRules, slug, baseBranch) {
7914
+ function planSeedDelivery(branchRules, slug, baseBranch, branchPrefix = "bootstrap-seed") {
7736
7915
  const decision = decideSeedDelivery(branchRules);
7737
7916
  if (decision.mode === "pr") {
7738
- const branch = `bootstrap-seed-${slug}`;
7917
+ const branch = `${branchPrefix}-${slug}`;
7739
7918
  return { mode: "pr", ref: branch, branch, reason: decision.reason };
7740
7919
  }
7741
7920
  return { mode: "direct", ref: baseBranch, reason: decision.reason };
7742
7921
  }
7743
- function contentPutArgs(repo, path2, content, branch, sha) {
7744
- const args = [
7922
+ function contentPutBody(path2, content, branch, sha) {
7923
+ const body = {
7924
+ message: `bootstrap: seed ${path2}`,
7925
+ content: Buffer.from(content, "utf8").toString("base64"),
7926
+ branch
7927
+ };
7928
+ if (sha) body.sha = sha;
7929
+ return body;
7930
+ }
7931
+ function contentPutInputArgs(repo, path2, inputFile) {
7932
+ return [
7745
7933
  "api",
7746
7934
  "-X",
7747
7935
  "PUT",
7748
7936
  `repos/${repo}/contents/${path2.split("/").map(encodeURIComponent).join("/")}`,
7749
- "-f",
7750
- `message=bootstrap: seed ${path2}`,
7751
- "-f",
7752
- `content=${Buffer.from(content, "utf8").toString("base64")}`,
7753
- "-f",
7754
- `branch=${branch}`
7937
+ "--input",
7938
+ inputFile
7755
7939
  ];
7756
- if (sha) args.push("-f", `sha=${sha}`);
7757
- return args;
7758
7940
  }
7759
7941
 
7760
7942
  // src/ci-audit.ts
@@ -7764,6 +7946,9 @@ var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
7764
7946
  var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
7765
7947
  var GATE_TEMPLATE_SEED = "seed:gate.template.yml";
7766
7948
  var RULESET_TEMPLATE_SEED = "seed:mmi-product-required-checks.template.json";
7949
+ var RULESET_REFERENCE_MATCH_LABEL = "committed ruleset reference matches the live ruleset (#3816)";
7950
+ var GATE_GREEN_ON_BASE_LABEL = "gate is green on the default branch (#3819)";
7951
+ var RECONCILE_SEED_BRANCH_PREFIX = "ci-reconcile-ruleset-ref";
7767
7952
  function slugFromRepo(repo) {
7768
7953
  return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
7769
7954
  }
@@ -7777,6 +7962,9 @@ function classifyRepo(repo, meta) {
7777
7962
  function rulesetStatusChecks(rulesets) {
7778
7963
  return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
7779
7964
  }
7965
+ function sortedUnique(values) {
7966
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
7967
+ }
7780
7968
  async function restJson(deps, path2, fallback) {
7781
7969
  try {
7782
7970
  return await deps.client.rest("GET", path2) ?? fallback;
@@ -7820,6 +8008,11 @@ async function listWorkflowPaths(deps, repo, branch) {
7820
8008
  return e?.status === 404 ? [] : void 0;
7821
8009
  }
7822
8010
  }
8011
+ var AGENT_PR_BOOKKEEPING_CONTEXTS = /* @__PURE__ */ new Set(["guard", "verdict"]);
8012
+ function gateWorkflowFiles(prWorkflowPaths) {
8013
+ const files = prWorkflowPaths.map((p) => p.slice(p.lastIndexOf("/") + 1)).filter((f) => f !== "agent-pr.yml");
8014
+ return files.length ? files : DEFAULT_GATE_FILES;
8015
+ }
7823
8016
  async function resolveEmittedPrContexts(deps, repo, branch) {
7824
8017
  const paths = await listWorkflowPaths(deps, repo, branch) ?? [];
7825
8018
  const workflows = [];
@@ -7946,7 +8139,7 @@ async function auditRepoCi(repo, deps) {
7946
8139
  return emittedPrContexts;
7947
8140
  };
7948
8141
  if (explicitNoCi) {
7949
- const emitted = await getEmittedPrContexts();
8142
+ const emitted = (await getEmittedPrContexts()).filter((c) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(c));
7950
8143
  if (emitted.length > 0) {
7951
8144
  checks.push({
7952
8145
  ok: false,
@@ -8001,6 +8194,34 @@ async function auditRepoCi(repo, deps) {
8001
8194
  });
8002
8195
  }
8003
8196
  }
8197
+ const productRuleset = rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
8198
+ const committedReferenceRaw = await fetchFileContent(deps, repo, baseBranch, PRODUCT_RULESET_REF);
8199
+ if (productRuleset && committedReferenceRaw != null) {
8200
+ let committedContexts = null;
8201
+ try {
8202
+ committedContexts = rulesetRequiredContexts(stripRulesetComment(committedReferenceRaw));
8203
+ } catch {
8204
+ committedContexts = null;
8205
+ }
8206
+ const live = sortedUnique(rulesetRequiredContexts(productRuleset));
8207
+ const declared = committedContexts == null ? null : sortedUnique(committedContexts);
8208
+ const aligned = declared != null && declared.length === live.length && declared.every((c, i) => c === live[i]);
8209
+ checks.push({
8210
+ ok: aligned,
8211
+ label: RULESET_REFERENCE_MATCH_LABEL,
8212
+ detail: aligned ? `[${live.join(", ")}] on both sides` : declared == null ? `${PRODUCT_RULESET_REF} on ${baseBranch} is not parseable JSON \u2014 it cannot be compared to the live ruleset` : `${PRODUCT_RULESET_REF} declares [${declared.join(", ")}] but the live ${PRODUCT_RULESET_NAME} ruleset requires [${live.join(", ")}]`,
8213
+ remediation: aligned ? void 0 : `mmi-cli ci reconcile --repo ${repo} --apply`
8214
+ });
8215
+ }
8216
+ }
8217
+ if (deployableGated || repoClass === "hub") {
8218
+ const gate = await readDefaultBranchGate(repo, deps.client, baseBranch, Date.now(), gateWorkflowFiles(prWorkflowPaths));
8219
+ checks.push({
8220
+ ok: gate.state !== "red",
8221
+ label: GATE_GREEN_ON_BASE_LABEL,
8222
+ detail: gate.state === "green" ? void 0 : gate.detail,
8223
+ remediation: gate.state === "red" ? `Fix the gate in ${repo} \u2014 it blocks every PR there, and ${PRODUCT_RULESET_NAME} activation is held back by the #3694 guard until it is green${gate.runUrl ? ` (${gate.runUrl})` : ""}` : void 0
8224
+ });
8004
8225
  }
8005
8226
  const workflowPaths = repoClass === "deployable" ? prWorkflowPaths : [];
8006
8227
  const { policy, reason } = resolveMergeCiPolicy({
@@ -8022,16 +8243,21 @@ async function auditRepoCi(repo, deps) {
8022
8243
  detail: `${policy} (${reason})`
8023
8244
  });
8024
8245
  }
8246
+ const NO_CANCEL_WAIVER = /ci-audit:\s*allow-no-cancel-in-progress\b[^\n]*#\d+/i;
8025
8247
  if (deployableGated || repoClass === "hub") {
8026
8248
  const allPaths = await listWorkflowPaths(deps, repo, baseBranch) ?? [];
8027
8249
  const prPaths = await filterPullRequestTriggered(deps, repo, baseBranch, allPaths);
8028
8250
  const missing = [];
8251
+ const waived = [];
8029
8252
  const gateBodies = [];
8030
8253
  for (const path2 of prPaths) {
8031
8254
  const body = await fetchFileContent(deps, repo, baseBranch, path2);
8032
8255
  if (body == null) continue;
8033
8256
  if (isGateWorkflowPath(path2)) gateBodies.push({ path: path2, body });
8034
- if (!/^\s*concurrency:/m.test(body) || !/cancel-in-progress:\s*true/.test(body)) missing.push(path2);
8257
+ if (!/^\s*concurrency:/m.test(body) || !/cancel-in-progress:\s*true/.test(body)) {
8258
+ if (NO_CANCEL_WAIVER.test(body)) waived.push(path2);
8259
+ else missing.push(path2);
8260
+ }
8035
8261
  }
8036
8262
  if (gateBodies.length > 0) {
8037
8263
  const budget = checkGateBudget(gateBodies, { hubLocalAllowed: repoClass === "hub" });
@@ -8043,10 +8269,14 @@ async function auditRepoCi(repo, deps) {
8043
8269
  });
8044
8270
  }
8045
8271
  if (prPaths.length > 0) {
8272
+ const waivedNote = waived.length ? `${waived.length} waived by declared marker: ${waived.join(", ")}` : void 0;
8046
8273
  checks.push({
8047
8274
  ok: missing.length === 0,
8048
8275
  label: "PR workflows cancel superseded runs (#3001)",
8049
- detail: missing.length ? `missing per-PR concurrency cancel-in-progress: ${missing.join(", ")}` : void 0,
8276
+ detail: [
8277
+ missing.length ? `missing per-PR concurrency cancel-in-progress: ${missing.join(", ")}` : void 0,
8278
+ waivedNote
8279
+ ].filter(Boolean).join("; ") || void 0,
8050
8280
  remediation: missing.length ? `Add the per-PR concurrency block from skills/bootstrap/seeds/gate.template.yml (group: <workflow>-PR-or-ref, cancel-in-progress: true) to: ${missing.join(", ")}` : void 0
8051
8281
  });
8052
8282
  }
@@ -8070,24 +8300,38 @@ async function auditRepoCi(repo, deps) {
8070
8300
  const ok = checks.every((c) => c.ok);
8071
8301
  return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
8072
8302
  }
8303
+ var REGISTRY_UNREADABLE_DETAIL = `the registry roster read returned nothing \u2014 the fleet was NOT audited. 0 repos read from the registry; the number left unaudited is unknown because the roster is what names them. This is "cannot audit the fleet", never "the fleet is green". Under GitHub Actions the CLI needs a CI Hub session (MMI_HUB_TOKEN, minted from the job's OIDC claim via POST /auth/session-ci) \u2014 an App installation token has no /user identity and can never pass /auth/session (#3215).`;
8073
8304
  async function auditOrgCi(deps, repoFilter) {
8305
+ if (repoFilter) {
8306
+ const report = await auditRepoCi(repoFilter, deps);
8307
+ return { ok: report.ok, scope: "single-repo", repos: [report] };
8308
+ }
8074
8309
  const projects = await deps.listProjects();
8075
- if (!projects) {
8076
- const single = repoFilter ?? HUB_REPO;
8077
- const report = await auditRepoCi(single, deps);
8078
- return { ok: report.ok, repos: [report] };
8310
+ if (!projects || projects.length === 0) {
8311
+ return { ok: false, scope: "registry-unreadable", repos: [], scopeDetail: REGISTRY_UNREADABLE_DETAIL };
8079
8312
  }
8080
- const targets = repoFilter ? [repoFilter] : collectRegistryRepos(projects);
8313
+ const targets = collectRegistryRepos(projects);
8081
8314
  const repos = [];
8082
8315
  for (const repo of targets) {
8083
8316
  repos.push(await auditRepoCi(repo, deps));
8084
8317
  }
8085
- return { ok: repos.every((r) => r.ok), repos };
8318
+ return { ok: repos.every((r) => r.ok), scope: "fleet", repos };
8319
+ }
8320
+ function renderCiAuditScopeLine(report) {
8321
+ if (report.scope === "registry-unreadable") {
8322
+ return `scope: REGISTRY UNREADABLE \u2014 repos audited: 0 \u2014 ${report.scopeDetail ?? REGISTRY_UNREADABLE_DETAIL}`;
8323
+ }
8324
+ if (report.scope === "single-repo") {
8325
+ return `scope: single-repo (explicit --repo) \u2014 repos audited: ${report.repos.length} \u2014 this is NOT a fleet verdict`;
8326
+ }
8327
+ return `scope: fleet (registry roster) \u2014 repos audited: ${report.repos.length}`;
8086
8328
  }
8087
8329
  function renderCiAuditMarkdown(report) {
8088
8330
  const lines = [
8089
8331
  `# CI merge-readiness audit`,
8090
8332
  "",
8333
+ renderCiAuditScopeLine(report),
8334
+ "",
8091
8335
  `Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok).length}/${report.repos.length} repos ready)`,
8092
8336
  "",
8093
8337
  "| Repo | Class | Policy | OK | Top gap |",
@@ -8100,7 +8344,10 @@ function renderCiAuditMarkdown(report) {
8100
8344
  return lines.join("\n");
8101
8345
  }
8102
8346
  function renderCiAuditText(report) {
8103
- const lines = [`mmi-cli ci audit: ${report.ok ? "OK" : "GAPS"} (${report.repos.length} repos)`];
8347
+ const lines = [
8348
+ `mmi-cli ci audit: ${report.ok ? "OK" : "GAPS"} (${report.repos.length} repos)`,
8349
+ renderCiAuditScopeLine(report)
8350
+ ];
8104
8351
  for (const r of report.repos) {
8105
8352
  lines.push(`
8106
8353
  ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${r.ok ? "OK" : "GAP"}`);
@@ -8148,6 +8395,17 @@ async function fetchRulesetSeedBody(deps, repo) {
8148
8395
  return null;
8149
8396
  }
8150
8397
  }
8398
+ async function readLiveProductContexts(deps, repo) {
8399
+ try {
8400
+ const list = await deps.client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
8401
+ const existing = findProductRuleset(list ?? []);
8402
+ if (existing?.id == null) return null;
8403
+ const detail = await deps.client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
8404
+ return sortedUnique(rulesetRequiredContexts(detail));
8405
+ } catch {
8406
+ return null;
8407
+ }
8408
+ }
8151
8409
  async function contentSha(deps, repo, branch, path2) {
8152
8410
  try {
8153
8411
  const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
@@ -8173,48 +8431,101 @@ async function putSeedFile(deps, repo, path2, content, branch) {
8173
8431
  if (sha) body.sha = sha;
8174
8432
  await deps.client.rest("PUT", `repos/${repo}/contents/${encodedPath}`, { body });
8175
8433
  }
8434
+ async function deliverSeedFile(deps, repo, path2, content, baseBranch) {
8435
+ let branchRules = [];
8436
+ try {
8437
+ branchRules = await deps.client.rest("GET", `repos/${repo}/rules/branches/${encodeURIComponent(baseBranch)}`);
8438
+ } catch {
8439
+ branchRules = [];
8440
+ }
8441
+ const plan = planSeedDelivery(branchRules, slugFromRepo(repo), baseBranch, RECONCILE_SEED_BRANCH_PREFIX);
8442
+ if (plan.mode === "direct" || !plan.branch) {
8443
+ await putSeedFile(deps, repo, path2, content, baseBranch);
8444
+ return { plan };
8445
+ }
8446
+ const head = await deps.client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(baseBranch)}`);
8447
+ const sha = head?.object?.sha;
8448
+ if (!sha) throw new Error(`cannot resolve ${baseBranch} head on ${repo} \u2014 seed branch not cut`);
8449
+ try {
8450
+ await deps.client.rest("POST", `repos/${repo}/git/refs`, { body: { ref: `refs/heads/${plan.branch}`, sha } });
8451
+ } catch (e) {
8452
+ if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
8453
+ }
8454
+ await putSeedFile(deps, repo, path2, content, plan.branch);
8455
+ const owner = repo.includes("/") ? repo.split("/")[0] : "mutmutco";
8456
+ const open2 = await deps.client.rest(
8457
+ "GET",
8458
+ `repos/${repo}/pulls?state=open&base=${encodeURIComponent(baseBranch)}&head=${encodeURIComponent(`${owner}:${plan.branch}`)}`
8459
+ ).catch(() => []);
8460
+ const decision = decideSeedPrAction((open2 ?? []).map((pr2) => ({ url: pr2?.html_url })));
8461
+ if (decision.action === "reuse") return { plan, prUrl: decision.url };
8462
+ const created = await deps.client.rest("POST", `repos/${repo}/pulls`, {
8463
+ body: {
8464
+ title: `chore: reconcile ${path2} with the live ${PRODUCT_RULESET_NAME} ruleset (#3816)`,
8465
+ head: plan.branch,
8466
+ base: baseBranch,
8467
+ body: `\`mmi-cli ci reconcile --apply\` brings this repo's committed ruleset reference into agreement with its LIVE \`${PRODUCT_RULESET_NAME}\` ruleset.
8468
+
8469
+ The base branch is protected, so the reference cannot be written directly \u2014 the same branch+PR seed delivery \`bootstrap apply\` uses (#2286.1). Refs mutmutco/MMI-Hub#3816.
8470
+ `
8471
+ }
8472
+ });
8473
+ return { plan, prUrl: created?.html_url };
8474
+ }
8176
8475
  async function seedGateYml(repo, deps, meta, result) {
8177
8476
  const baseBranch = "development";
8178
8477
  const releaseTrack = isReleaseTrack(meta?.releaseTrack) ? meta?.releaseTrack : void 0;
8179
8478
  const parsed = parseOwnerRepo(repo);
8479
+ const refOnlyVars = () => withDerivedRepoVars({ REPO_SLUG: parsed.slug }, parsed, "deployable", releaseTrack);
8180
8480
  if (await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH)) {
8181
- await seedRulesetRefIfMissing(repo, deps, withDerivedRepoVars({ REPO_SLUG: parsed.slug }, parsed, "deployable", releaseTrack), baseBranch, result);
8182
- return;
8481
+ return await seedRulesetRefIfMissing(repo, deps, refOnlyVars(), baseBranch, result);
8183
8482
  }
8184
8483
  if (!deps.readSeedFile && !deps.renderSeed) {
8185
8484
  result.skipped.push(`gate.yml missing but no seed-template source wired \u2014 run bootstrap apply`);
8186
- return;
8485
+ return "none";
8486
+ }
8487
+ const prTriggered = await prTriggeredWorkflowsOnRef(deps, repo, baseBranch, "deployable");
8488
+ if (prTriggered === void 0) {
8489
+ result.skipped.push("gate.yml missing and the workflows listing is unreadable \u2014 never seeding a gate on UNKNOWN");
8490
+ return "none";
8491
+ }
8492
+ if (prTriggered.length > 0) {
8493
+ return await seedRulesetRefIfMissing(repo, deps, refOnlyVars(), baseBranch, result);
8187
8494
  }
8188
8495
  const gate = meta?.gate;
8189
8496
  if (!gate || typeof gate === "object" && Object.keys(gate).length === 0) {
8190
8497
  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`);
8191
- return;
8498
+ return "none";
8192
8499
  }
8193
8500
  const derivedVars = withDerivedRepoVars({ ...gateConfigToVars(gate), REPO_SLUG: parsed.slug }, parsed, "deployable", releaseTrack);
8194
8501
  const rendered = renderSeedBody(deps, GATE_TEMPLATE_SEED, PRODUCT_GATE_PATH, derivedVars);
8195
8502
  if (rendered == null) {
8196
8503
  result.errors.push(`gate.yml re-seed: could not render ${GATE_TEMPLATE_SEED} (template unreadable or unfilled)`);
8197
- return;
8504
+ return "none";
8198
8505
  }
8199
8506
  try {
8200
8507
  await putSeedFile(deps, repo, PRODUCT_GATE_PATH, rendered, baseBranch);
8201
8508
  result.applied.push(`seeded ${PRODUCT_GATE_PATH}`);
8202
8509
  } catch (e) {
8203
8510
  result.errors.push(`gate.yml re-seed failed: ${e.message}`);
8204
- return;
8511
+ return "none";
8205
8512
  }
8206
- await seedRulesetRefIfMissing(repo, deps, derivedVars, baseBranch, result);
8513
+ return await seedRulesetRefIfMissing(repo, deps, derivedVars, baseBranch, result);
8207
8514
  }
8208
8515
  async function seedRulesetRefIfMissing(repo, deps, derivedVars, baseBranch, result) {
8209
- if (await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF)) return;
8210
- if (!deps.readSeedFile && !deps.renderSeed) return;
8516
+ if (await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF)) return "committed";
8517
+ if (!deps.readSeedFile && !deps.renderSeed) return "none";
8211
8518
  const rulesetBody = renderSeedBody(deps, RULESET_TEMPLATE_SEED, PRODUCT_RULESET_REF, derivedVars);
8212
- if (rulesetBody == null) return;
8519
+ if (rulesetBody == null) return "none";
8213
8520
  try {
8214
- await putSeedFile(deps, repo, PRODUCT_RULESET_REF, rulesetBody, baseBranch);
8215
- result.applied.push(`seeded ${PRODUCT_RULESET_REF}`);
8521
+ const { plan, prUrl } = await deliverSeedFile(deps, repo, PRODUCT_RULESET_REF, rulesetBody, baseBranch);
8522
+ result.applied.push(
8523
+ `seeded ${PRODUCT_RULESET_REF}` + (plan.mode === "direct" ? ` (committed to ${baseBranch})` : ` (${plan.reason}${prUrl ? ` \u2014 ${prUrl}` : ""})`)
8524
+ );
8525
+ return plan.mode === "direct" ? "committed" : "pending";
8216
8526
  } catch (e) {
8217
8527
  result.errors.push(`ruleset reference seed failed: ${e.message}`);
8528
+ return "none";
8218
8529
  }
8219
8530
  }
8220
8531
  async function parkProductRuleset(repo, deps) {
@@ -8248,19 +8559,47 @@ async function applyCiReconcileRepo(repo, deps) {
8248
8559
  const meta = await deps.getProjectMeta(slugFromRepo(repo));
8249
8560
  const report = await auditRepoCi(repo, deps);
8250
8561
  if (report.class !== "deployable" || report.explicitNoCi) return merge;
8251
- await seedGateYml(repo, deps, meta, merge);
8562
+ const refDelivery = await seedGateYml(repo, deps, meta, merge);
8563
+ const baseBranch = "development";
8252
8564
  const driftCheck = report.checks.find((c) => c.label === "required check contexts match PR workflows");
8253
8565
  const requiredCheck = report.checks.find((c) => c.label === "product required status checks active");
8254
- if (requiredCheck?.ok && (driftCheck?.ok ?? true)) {
8255
- merge.skipped.push("product ruleset already active and aligned");
8566
+ const referenceCheck = report.checks.find((c) => c.label === RULESET_REFERENCE_MATCH_LABEL);
8567
+ if (requiredCheck?.ok && (driftCheck?.ok ?? true) && (referenceCheck?.ok ?? true)) {
8568
+ merge.skipped.push("product ruleset already active, aligned, and matching its committed reference");
8256
8569
  return merge;
8257
8570
  }
8258
8571
  const raw = await fetchRulesetSeedBody(deps, repo);
8259
8572
  if (!raw) {
8260
- merge.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
8573
+ if (refDelivery === "pending") {
8574
+ merge.skipped.push(
8575
+ `${PRODUCT_RULESET_REF} was delivered on a seed branch this run \u2014 the base branch is protected, so it lands when that PR merges; re-run the reconcile then to activate the ruleset`
8576
+ );
8577
+ } else {
8578
+ merge.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
8579
+ }
8261
8580
  return merge;
8262
8581
  }
8263
- if (!await gateIsProvenGreen(repo, deps.client, "development")) {
8582
+ const activationNeeded = !requiredCheck?.ok || !(driftCheck?.ok ?? true);
8583
+ const prWorkflows = await prTriggeredWorkflowsOnRef(deps, repo, baseBranch, "deployable") ?? [];
8584
+ const gateFiles = gateWorkflowFiles(prWorkflows);
8585
+ const allPrWorkflows = await listWorkflowPaths(deps, repo, baseBranch) ?? [];
8586
+ if (activationNeeded) {
8587
+ const bodies = [];
8588
+ for (const path2 of allPrWorkflows) {
8589
+ const body = await fetchFileContent(deps, repo, baseBranch, path2);
8590
+ if (body) bodies.push({ path: path2, body });
8591
+ }
8592
+ const filteredPaths = new Set(pathFilteredPullRequestWorkflows(bodies).filter((p) => !p.endsWith("/agent-pr.yml")));
8593
+ const safeContexts = new Set(collectPullRequestWorkflowContexts(bodies.filter((b) => !filteredPaths.has(b.path))));
8594
+ const unsafe = collectPullRequestWorkflowContexts(bodies.filter((b) => filteredPaths.has(b.path))).filter((c) => !safeContexts.has(c));
8595
+ if (unsafe.length) {
8596
+ merge.skipped.push(
8597
+ `product ruleset left non-enforcing \u2014 [${unsafe.join(", ")}] ${unsafe.length === 1 ? "is" : "are"} emitted ONLY by path-filtered workflow(s) (${[...filteredPaths].join(", ")}), so the context is not reported on a PR outside those paths and requiring it would block that PR forever (#3836). Give the gate a companion job that runs unconditionally, then re-run this command`
8598
+ );
8599
+ return merge;
8600
+ }
8601
+ }
8602
+ if (activationNeeded && !await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles)) {
8264
8603
  merge.skipped.push(
8265
8604
  "product ruleset left non-enforcing \u2014 the gate is not green on development; activating it would block every PR (#3694)"
8266
8605
  );
@@ -8268,29 +8607,55 @@ async function applyCiReconcileRepo(repo, deps) {
8268
8607
  }
8269
8608
  try {
8270
8609
  let body = stripRulesetComment(raw);
8271
- if (!driftCheck?.ok) {
8272
- const baseBranch = "development";
8610
+ let targetContexts;
8611
+ if (!(driftCheck?.ok ?? true)) {
8273
8612
  const emitted = registryRequiredContexts(meta) ?? await resolveEmittedPrContexts(deps, repo, baseBranch);
8274
8613
  if (!emitted.length) {
8275
8614
  merge.errors.push("cannot reconcile ruleset contexts \u2014 no PR workflow job ids found");
8276
8615
  return merge;
8277
8616
  }
8617
+ targetContexts = emitted;
8278
8618
  body = patchRulesetRequiredContexts(body, emitted);
8279
- const activation2 = await activateProductRuleset(repo, body, deps.client);
8280
- if (activation2.action === "skipped") merge.skipped.push(activation2.detail ?? "product ruleset");
8281
- else merge.applied.push(`product ruleset ${activation2.action}${activation2.detail ? `: ${activation2.detail}` : ""}`);
8619
+ } else if (!(referenceCheck?.ok ?? true)) {
8620
+ const live = await readLiveProductContexts(deps, repo);
8621
+ if (live == null) {
8622
+ merge.errors.push(`cannot read the live ${PRODUCT_RULESET_NAME} ruleset \u2014 ${PRODUCT_RULESET_REF} left untouched`);
8623
+ return merge;
8624
+ }
8625
+ targetContexts = live;
8626
+ body = patchRulesetRequiredContexts(body, live);
8627
+ } else {
8628
+ targetContexts = rulesetRequiredContexts(body);
8629
+ }
8630
+ if (!(referenceCheck?.ok ?? true) || !(driftCheck?.ok ?? true)) {
8631
+ let comment;
8282
8632
  try {
8283
- await putSeedFile(deps, repo, PRODUCT_RULESET_REF, `${JSON.stringify(body, null, 2)}
8284
- `, baseBranch);
8285
- merge.applied.push(`reconciled ${PRODUCT_RULESET_REF} contexts \u2192 [${emitted.join(", ")}]`);
8633
+ comment = JSON.parse(raw)._comment;
8634
+ } catch {
8635
+ comment = void 0;
8636
+ }
8637
+ const fileBody = comment === void 0 ? body : { _comment: comment, ...body };
8638
+ try {
8639
+ const { plan, prUrl } = await deliverSeedFile(
8640
+ deps,
8641
+ repo,
8642
+ PRODUCT_RULESET_REF,
8643
+ `${JSON.stringify(fileBody, null, 2)}
8644
+ `,
8645
+ baseBranch
8646
+ );
8647
+ merge.applied.push(
8648
+ plan.mode === "pr" ? `${PRODUCT_RULESET_REF} contexts \u2192 [${targetContexts.join(", ")}] delivered on ${plan.branch} (${plan.reason})${prUrl ? `: ${prUrl}` : ""}` : `reconciled ${PRODUCT_RULESET_REF} contexts \u2192 [${targetContexts.join(", ")}]`
8649
+ );
8286
8650
  } catch (e) {
8287
- merge.errors.push(`ruleset reference commit failed (live ruleset updated): ${e.message}`);
8651
+ merge.errors.push(`ruleset reference delivery failed: ${e.message}`);
8288
8652
  }
8289
- return merge;
8290
8653
  }
8291
- const activation = await activateProductRuleset(repo, body, deps.client);
8292
- if (activation.action === "skipped") merge.skipped.push(activation.detail ?? "product ruleset");
8293
- else merge.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
8654
+ if (activationNeeded) {
8655
+ const activation = await activateProductRuleset(repo, body, deps.client);
8656
+ if (activation.action === "skipped") merge.skipped.push(activation.detail ?? "product ruleset");
8657
+ else merge.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
8658
+ }
8294
8659
  } catch (e) {
8295
8660
  merge.errors.push(e.message);
8296
8661
  }
@@ -9147,7 +9512,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
9147
9512
  }
9148
9513
 
9149
9514
  // src/index.ts
9150
- var import_node_os12 = require("node:os");
9515
+ var import_node_os13 = require("node:os");
9151
9516
 
9152
9517
  // src/board.ts
9153
9518
  var import_node_child_process8 = require("node:child_process");
@@ -11079,6 +11444,16 @@ async function fetchSecretForUse(deps, { repo, key, slug }) {
11079
11444
 
11080
11445
  // src/report.ts
11081
11446
  var HUB_REPO2 = "mutmutco/MMI-Hub";
11447
+ var REPORT_LABEL = "report";
11448
+ var REPORT_SURFACE_WAIVER_REASON = "Hub-only friction filing; attribution repo is footer-only; no honest single surface";
11449
+ function preflightReportSurface() {
11450
+ return checkSurfaceRequirement({
11451
+ repo: HUB_REPO2,
11452
+ labels: [REPORT_LABEL],
11453
+ command: "report",
11454
+ waiver: { reason: REPORT_SURFACE_WAIVER_REASON }
11455
+ });
11456
+ }
11082
11457
  function findDuplicateReport(source, openReports, threshold = 0.6) {
11083
11458
  const normalizedTitle = normalizeTitle(source.title);
11084
11459
  let best;
@@ -11561,6 +11936,49 @@ function resolveParentField(payload) {
11561
11936
  const parsed = typeof raw === "string" ? parseParentIssueUrl(raw) : null;
11562
11937
  return parsed ? { parent: parsed } : { parentReadError: `unrecognised parent_issue_url: ${JSON.stringify(raw)}` };
11563
11938
  }
11939
+ async function readIssueParent(runGh, repo, number) {
11940
+ try {
11941
+ const stdout = await runGh(["api", `repos/${repo}/issues/${number}`], RESOLVE_ID_TIMEOUT_MS);
11942
+ const resolved = resolveParentField(JSON.parse(stdout));
11943
+ return "parent" in resolved ? resolved.parent : void 0;
11944
+ } catch {
11945
+ return void 0;
11946
+ }
11947
+ }
11948
+ function isParentConflictMessage(text) {
11949
+ return typeof text === "string" && /sub[- ]?issue may only have one parent/i.test(text);
11950
+ }
11951
+ function buildReparentConflictPayload(input) {
11952
+ const { childRepo, childNumber, requestedParent, currentParent, offendingFlag } = input;
11953
+ const child2 = `${childRepo}#${childNumber}`;
11954
+ const payload = { code: ERROR_CODES.ERR_STATE_CONFLICT };
11955
+ if (offendingFlag) payload.offending_flag = offendingFlag;
11956
+ if (!currentParent) {
11957
+ return {
11958
+ message: currentParent === null ? `${child2} cannot be reparented under ${requestedParent} \u2014 GitHub refused it under the one-parent rule, but reading the issue back showed no parent at all. Something changed it concurrently; re-read with \`mmi-cli issue view ${childNumber} --repo ${childRepo}\` and retry` : `${child2} cannot be reparented under ${requestedParent} \u2014 GitHub allows a sub-issue exactly one parent and it already has one. Its current parent could not be read; check with \`mmi-cli issue view ${childNumber} --repo ${childRepo}\``,
11959
+ payload
11960
+ };
11961
+ }
11962
+ const current = `${currentParent.repo}#${currentParent.number}`;
11963
+ const sameRepo2 = currentParent.repo.toLowerCase() === childRepo.toLowerCase();
11964
+ const repoFlag = ` --repo ${childRepo}`;
11965
+ payload.current_parent = current;
11966
+ const requested = parseIssueRef(requestedParent);
11967
+ const alreadyThere = currentParent.number === requested.number && (requested.repo ?? childRepo).toLowerCase() === currentParent.repo.toLowerCase();
11968
+ if (!alreadyThere) {
11969
+ payload.corrected_command = `mmi-cli issue unlink-child ${sameRepo2 ? String(currentParent.number) : current} ${childNumber}${repoFlag} && mmi-cli issue edit ${childNumber}${repoFlag} --parent ${requestedParent}`;
11970
+ }
11971
+ return {
11972
+ message: alreadyThere ? `${child2} is already a sub-issue of ${current} \u2014 GitHub allows a sub-issue exactly one parent, so this reparent is a no-op it refuses rather than performs; nothing to do` : `${child2} already has a parent (${current}) \u2014 GitHub allows a sub-issue exactly one parent. Reparenting it under ${requestedParent} would REMOVE it from ${current}; unlink it there first if that is what you want`,
11973
+ payload
11974
+ };
11975
+ }
11976
+ async function classifyReparentFailure(e, runGh, childRepo, childNumber, requestedParent, offendingFlag) {
11977
+ const err = e;
11978
+ if (!isParentConflictMessage((err.stderr || err.message || String(e)).trim())) return void 0;
11979
+ const currentParent = await readIssueParent(runGh, childRepo, childNumber);
11980
+ return buildReparentConflictPayload({ childRepo, childNumber, requestedParent, currentParent, offendingFlag });
11981
+ }
11564
11982
  function parentLinkFields(result, error) {
11565
11983
  if (result) return { parent: result };
11566
11984
  if (error) return { parentLinkError: error };
@@ -15164,17 +15582,23 @@ function partialTrainRecoveryError(cause, input) {
15164
15582
  const causeMessage = cause instanceof Error ? cause.message : String(cause);
15165
15583
  const branch = input.stage;
15166
15584
  const releaseState = input.stage === "rc" ? "GitHub Release n/a" : "GitHub Release not created";
15167
- const releaseCommand = input.stage === "main" ? `
15168
- 2. gh release create ${input.tag} --target main --generate-notes --latest --repo ${input.repo}` : "";
15169
- const deployStep = input.stage === "main" ? "3" : "2";
15585
+ const tenant2 = input.deployModel === "tenant-container";
15586
+ const deployLine = tenant2 ? ` - deploy: dispatched by the train after the branch push; recover with \`mmi-cli runtime tenant redeploy ${input.repo} ${input.stage} --watch\` once the branch and release exist.` : ` - deploy: this repo is \`${input.deployModel ?? "not tenant-container"}\` \u2014 it has NO tenant runtime. Its own release/main-push workflows publish and deploy once the branch and Release exist. Do NOT run the tenant redeployer.`;
15170
15587
  return new Error(
15171
15588
  `${causeMessage}
15172
15589
 
15173
- partial train state: tag ${input.tag} is already pushed; origin/${branch} has not been pushed; ${releaseState}; deploy not dispatched.
15174
- Recovery sequence:
15175
- 1. git push origin ${branch}` + releaseCommand + `
15176
- ${deployStep}. mmi-cli runtime tenant redeploy ${input.repo} ${input.stage} --watch
15177
- Do not delete or force-move the pushed tag; rerun the train only after confirming the branch, release, and deploy states above.`
15590
+ partial train state for ${input.tag}:
15591
+ - tag ${input.tag}: PUSHED to origin
15592
+ - origin/${branch}: NOT pushed
15593
+ - ${releaseState}
15594
+ ` + deployLine + `
15595
+
15596
+ The tag is public and correct \u2014 do not delete or force-move it.
15597
+
15598
+ Resume it: mmi-cli release --resume${input.stage === "main" ? "" : " (main-stage partials only)"}
15599
+
15600
+ That completes THIS release at ${input.tag} \u2014 branch push, GitHub Release, the deploy path named above, then the development roll-forward. It preserves the version and refuses unless the partial state is proven (tag on origin, not on origin/main, no Release).
15601
+ Do NOT rerun the train instead: with ${input.tag} on origin the cycle resolver returns the NEXT version, so a rerun cuts a second release rather than finishing this one \u2014 and MMI_RELEASE_VERSION cannot pin it back, because it refuses a version that is not ahead of the latest tag, which ${input.tag} now is.`
15178
15602
  );
15179
15603
  }
15180
15604
  async function probeRemoteTag(deps, tag) {
@@ -15349,6 +15773,10 @@ async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumera
15349
15773
  workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
15350
15774
  return workflowRuns;
15351
15775
  }
15776
+ async function enumerateOwnWorkflowRuns(deps, repo, headSha) {
15777
+ const runs = await discoverShaWorkflowRuns(deps, repo, headSha, /* @__PURE__ */ new Set());
15778
+ return runs.length ? runs : [{ workflow: `sha-enumeration(${headSha.slice(0, 7)}) no runs yet`, conclusion: "pending" }];
15779
+ }
15352
15780
  var NON_DEPLOY_EVENTS = /* @__PURE__ */ new Set([
15353
15781
  "pull_request",
15354
15782
  "pull_request_target",
@@ -15425,7 +15853,11 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
15425
15853
  }
15426
15854
  if (model === "registry-publish") {
15427
15855
  const note = ref === "rc" ? "no dispatch on rc: registry-publish repos have no rc-stage Release to publish from" : "no central dispatch: this repo's own publish.yml auto-fires on the published Release (#2428)";
15428
- if (ref === "rc" || !watch || !autoRunHeadSha) return { note, deployStatus: "pending" };
15856
+ if (ref === "rc" || !autoRunHeadSha) return { note, deployStatus: "pending" };
15857
+ if (!watch) {
15858
+ const listed = await enumerateOwnWorkflowRuns(deps, ctx.repo, autoRunHeadSha);
15859
+ return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
15860
+ }
15429
15861
  const since = autoRunSince ?? (deps.now ?? Date.now)();
15430
15862
  const workflowRuns = await watchOwnWorkflowRuns(
15431
15863
  deps,
@@ -15440,8 +15872,12 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
15440
15872
  }
15441
15873
  if (model === "hub-serverless") {
15442
15874
  const note = ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
15443
- if (!watch) return { note, deployStatus: "pending" };
15444
15875
  if (!autoRunHeadSha) return { note, deployStatus: "pending" };
15876
+ if (!watch) {
15877
+ if (ref === "rc") return { note, deployStatus: "pending" };
15878
+ const listed = await enumerateOwnWorkflowRuns(deps, HUB_REPO3, autoRunHeadSha);
15879
+ return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
15880
+ }
15445
15881
  const since = autoRunSince ?? (deps.now ?? Date.now)();
15446
15882
  const targets = ref === "rc" ? [{ workflow: "deploy.yml", event: "push", branch: "rc" }] : [
15447
15883
  { workflow: "deploy.yml", event: "release" },
@@ -15747,7 +16183,7 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
15747
16183
  try {
15748
16184
  checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
15749
16185
  } catch (e) {
15750
- throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
16186
+ throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main", deployModel });
15751
16187
  }
15752
16188
  if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
15753
16189
  await runGitPush(deps, ["push", "origin", "main"]);
@@ -15767,6 +16203,61 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
15767
16203
  }
15768
16204
  return { checks, releaseUrl, announceNote, dispatch };
15769
16205
  }
16206
+ async function runReleaseResume(deps, options = {}) {
16207
+ const watch = options.watch ?? false;
16208
+ const ctx = await buildTrainApplyContext(deps);
16209
+ await requireCleanTree(deps);
16210
+ await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
16211
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
16212
+ const deployModel = await preflight(deps, ctx, "main", meta);
16213
+ const tags = clean2(await deps.run("git", ["tag", "--list", "v*", "--sort=-v:refname"])).split("\n").map((t) => t.trim()).filter(Boolean);
16214
+ const tag = tags[0];
16215
+ if (!tag) throw new Error("release --resume: no v* tag found \u2014 there is no partial release to resume");
16216
+ const tagSha = await probeRemoteTag(deps, tag);
16217
+ if (!tagSha) {
16218
+ throw new Error(
16219
+ `release --resume: ${tag} is not on origin \u2014 nothing was partially released. A local-only tag is not a partial release; run the train normally.`
16220
+ );
16221
+ }
16222
+ const base = { command: "release-resume", repo: ctx.repo, tag, tagSha, deployModel };
16223
+ if (!await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo)) {
16224
+ throw new Error(
16225
+ `release --resume: ${tag} is NOT a resumable partial \u2014 it is either already reachable from origin/main or it already has a GitHub Release, which means that release completed. (This probe also answers "not resumable" when it cannot prove otherwise \u2014 an unreadable Release or a failed ancestry check is never treated as safe to resume.) Nothing was written.`
16226
+ );
16227
+ }
16228
+ try {
16229
+ await deps.run("git", ["merge-base", "--is-ancestor", "origin/main", tagSha]);
16230
+ } catch {
16231
+ throw new Error(
16232
+ `release --resume: origin/main is not an ancestor of ${tag} (${tagSha.slice(0, 12)}) \u2014 main has moved on since the partial release, so finishing it here would not be a fast-forward. Resolve by hand; nothing was written.`
16233
+ );
16234
+ }
16235
+ const steps = [];
16236
+ await runGitPush(deps, ["push", "origin", `${tagSha}:refs/heads/main`]);
16237
+ steps.push(`pushed ${tagSha.slice(0, 12)} to origin/main`);
16238
+ const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
16239
+ steps.push(`created the GitHub Release for ${tag}`);
16240
+ await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
16241
+ steps.push("verified the Release published against the tagged commit");
16242
+ const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
16243
+ const autoRunSince = (deps.now ?? Date.now)();
16244
+ const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
16245
+ const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
16246
+ const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
16247
+ steps.push(`dispatched the ${deployModel} deploy path`);
16248
+ const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
16249
+ steps.push(`development roll-forward: ${devRollForward.status}`);
16250
+ return {
16251
+ ...base,
16252
+ resumed: true,
16253
+ steps,
16254
+ releaseUrl,
16255
+ announceNote,
16256
+ dispatch,
16257
+ devRollForward,
16258
+ note: `resumed and completed ${tag} \u2014 the original version was preserved, not re-cut`
16259
+ };
16260
+ }
15770
16261
  async function pushRcAlignment(deps) {
15771
16262
  try {
15772
16263
  await deps.run("git", ["push", "origin", "main:rc"]);
@@ -15815,7 +16306,7 @@ async function runTrainApplyPipeline(mode, input) {
15815
16306
  try {
15816
16307
  checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
15817
16308
  } catch (e) {
15818
- throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc" });
16309
+ throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc", deployModel: deployModel2 });
15819
16310
  }
15820
16311
  const autoRunSince = (deps.now ?? Date.now)();
15821
16312
  await runGitPush(deps, ["push", "origin", "rc"]);
@@ -16132,14 +16623,14 @@ async function runTenantReconcile(deps, options) {
16132
16623
  };
16133
16624
  }
16134
16625
  function tenantControlWatches(action) {
16135
- return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker";
16626
+ return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker" || action === "logs";
16136
16627
  }
16137
16628
  async function runTenantControl(deps, options) {
16138
16629
  const { repo, stage, action } = options;
16139
16630
  const watch = options.watch ?? tenantControlWatches(action);
16140
16631
  const base = { command: "tenant-control", repo, stage, action };
16141
16632
  const since = (deps.now ?? Date.now)();
16142
- const d = await deps.dispatchTenantControl({ repo, stage, action });
16633
+ const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines });
16143
16634
  if (!d.ok) {
16144
16635
  const transport = d.category === "transport-failed";
16145
16636
  return {
@@ -16156,10 +16647,12 @@ async function runTenantControl(deps, options) {
16156
16647
  if (action === "retire") {
16157
16648
  result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
16158
16649
  }
16159
- if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker")) {
16650
+ if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker" || action === "logs")) {
16160
16651
  const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
16161
16652
  if (action === "status") {
16162
16653
  result.serviceState = parseStatusSnippet(output).serviceState;
16654
+ } else if (action === "logs") {
16655
+ result.logs = output;
16163
16656
  } else if (action === "verify-secrets") {
16164
16657
  result.secrets = parseVerifySecrets(output);
16165
16658
  result.secretsRaw = output;
@@ -21157,6 +21650,62 @@ function registerQueryCommands(program3) {
21157
21650
 
21158
21651
  // src/bootstrap-commands.ts
21159
21652
  var import_node_fs24 = require("node:fs");
21653
+ var import_node_os9 = require("node:os");
21654
+ var import_node_path23 = require("node:path");
21655
+
21656
+ // src/bootstrap-drift.ts
21657
+ function byteComparableSeeds(manifest, cls) {
21658
+ return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
21659
+ }
21660
+ function compareSeedBytes(hubContent, repoContent) {
21661
+ if (repoContent === null) return "absent";
21662
+ const normalize = (s) => s.replace(/\r\n/g, "\n");
21663
+ return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
21664
+ }
21665
+ function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
21666
+ const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
21667
+ const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
21668
+ const findings = [];
21669
+ for (const seed of seeds) {
21670
+ const hub = hubContents.get(seed.target);
21671
+ if (hub == null) {
21672
+ findings.push({
21673
+ repo,
21674
+ target: seed.target,
21675
+ state: "drift",
21676
+ detail: `the Hub's own copy could not be read \u2014 the manifest declares a file MMI-Hub does not have`
21677
+ });
21678
+ continue;
21679
+ }
21680
+ const state = compareSeedBytes(hub, byTarget.get(seed.target) ?? null);
21681
+ if (state === "match") continue;
21682
+ const why = seed.waivers?.[slug];
21683
+ if (why) {
21684
+ findings.push({ repo, target: seed.target, state: "waived", detail: `${state} \u2014 waived: ${why}` });
21685
+ continue;
21686
+ }
21687
+ findings.push({
21688
+ repo,
21689
+ target: seed.target,
21690
+ state,
21691
+ detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 propagate with `mmi-cli bootstrap apply <repo> --only <target> --execute`, or, if this repo is RIGHT to differ, declare a waiver for it on the seed in the manifest (#3842)"
21692
+ });
21693
+ }
21694
+ return findings;
21695
+ }
21696
+ function renderSeedDriftReport(findings, reposAudited, seedsPerRepo) {
21697
+ const lines = [`org-seed drift: ${reposAudited} repo(s) audited, ${seedsPerRepo} byte-comparable seed(s) each`];
21698
+ const waived = findings.filter((f) => f.state === "waived");
21699
+ const real = findings.filter((f) => f.state !== "waived");
21700
+ for (const f of waived) lines.push(` WAIVED ${f.repo} ${f.target} \u2014 ${f.detail}`);
21701
+ if (!real.length) {
21702
+ lines.push(" clean \u2014 every repo carries the Hub's bytes for every org-owned whole-file seed" + (waived.length ? ` (${waived.length} declared waiver(s) above)` : ""));
21703
+ return lines.join("\n");
21704
+ }
21705
+ for (const f of real) lines.push(` ${f.state.toUpperCase().padEnd(6)} ${f.repo} ${f.target} \u2014 ${f.detail}`);
21706
+ lines.push(` \u2014 ${real.length} finding(s)${waived.length ? `, ${waived.length} waived` : ""}`);
21707
+ return lines.join("\n");
21708
+ }
21160
21709
 
21161
21710
  // src/bootstrap-verify.ts
21162
21711
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
@@ -21757,12 +22306,76 @@ function registerBootstrapCommands(program3) {
21757
22306
  else console.log(renderOrgRulesetDriftReport(plan));
21758
22307
  if (plan.action !== "noop") process.exitCode = 1;
21759
22308
  });
21760
- 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)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).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) => {
22309
+ bootstrap.command("drift").description("#3818: compare every org-owned whole-file seed against MMI-Hub's copy across the registry roster; read-only").option("--repo <owner/repo>", "audit one repo instead of the roster (never a fleet verdict)").option("--json", "machine-readable output").action(async () => {
22310
+ const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
22311
+ const manifestPath = "skills/bootstrap/seeds/manifest.json";
22312
+ if (!(0, import_node_fs24.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
22313
+ const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
22314
+ const hubContents = /* @__PURE__ */ new Map();
22315
+ for (const s of manifest.seeds) {
22316
+ if (s.ownership !== "org" || s.source !== "self") continue;
22317
+ hubContents.set(s.target, (0, import_node_fs24.existsSync)(s.target) ? (0, import_node_fs24.readFileSync)(s.target, "utf8") : null);
22318
+ }
22319
+ let targets;
22320
+ let classOf = (_repo) => "deployable";
22321
+ if (o.repo) {
22322
+ targets = [o.repo];
22323
+ } else {
22324
+ const projects = await fetchProjectsList(registryClientDeps(await loadConfig()));
22325
+ if (!projects || projects.length === 0) {
22326
+ return failGraceful("bootstrap drift: the registry roster is unreadable or empty \u2014 refusing to report a fleet verdict from a scope this command could not establish (#3808)");
22327
+ }
22328
+ targets = collectRegistryRepos(projects);
22329
+ const byRepo = /* @__PURE__ */ new Map();
22330
+ for (const p of projects) for (const r of p.repos ?? []) byRepo.set((r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p.class ?? "deployable");
22331
+ classOf = (repo) => byRepo.get(repo.toLowerCase()) ?? "deployable";
22332
+ }
22333
+ const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
22334
+ const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
22335
+ const findings = [];
22336
+ let seedsPerRepo = 0;
22337
+ for (const repo of targets) {
22338
+ const cls = classOf(repo);
22339
+ const seeds = byteComparableSeeds(manifest, cls);
22340
+ seedsPerRepo = Math.max(seedsPerRepo, seeds.length);
22341
+ const baseBranch = cls === "content" ? "main" : "development";
22342
+ const reads = [];
22343
+ for (const seed of seeds) {
22344
+ let content = null;
22345
+ try {
22346
+ const r = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
22347
+ const parsed = JSON.parse(r.stdout);
22348
+ content = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
22349
+ } catch {
22350
+ content = null;
22351
+ }
22352
+ reads.push({ target: seed.target, content });
22353
+ }
22354
+ findings.push(...auditRepoSeedDrift(repo, seeds, hubContents, reads));
22355
+ }
22356
+ if (o.json) {
22357
+ console.log(JSON.stringify({
22358
+ // #3842: `ok` reflects real findings; waivers ride the payload so a consumer can see every
22359
+ // standing exception without them counting as drift.
22360
+ ok: findings.every((f) => f.state === "waived"),
22361
+ scope: o.repo ? "single-repo" : "fleet",
22362
+ reposAudited: targets.length,
22363
+ seedsPerRepo,
22364
+ waived: findings.filter((f) => f.state === "waived").length,
22365
+ findings
22366
+ }, null, 2));
22367
+ } else {
22368
+ console.log(renderSeedDriftReport(findings, targets.length, seedsPerRepo));
22369
+ }
22370
+ if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
22371
+ });
22372
+ 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)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
21761
22373
  const o = {
21762
22374
  class: rawValue("--class", "deployable"),
21763
22375
  projectType: rawValue("--project-type", ""),
21764
22376
  deployModel: rawValue("--deploy-model", ""),
21765
22377
  releaseTrack: rawValue("--release-track", ""),
22378
+ only: rawValue("--only", ""),
21766
22379
  execute: rawFlag("--execute"),
21767
22380
  json: rawFlag("--json")
21768
22381
  };
@@ -21782,9 +22395,28 @@ function registerBootstrapCommands(program3) {
21782
22395
  const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
21783
22396
  const baseBranch = o.class === "content" ? "main" : "development";
21784
22397
  const slug = parsedRepo.slug;
22398
+ const onlyTarget = o.only.trim();
22399
+ const seedsToApply = onlyTarget ? manifest.seeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : manifest.seeds;
22400
+ if (onlyTarget && !seedsToApply.length) {
22401
+ const known = manifest.seeds.map((s) => s.target).join("\n ");
22402
+ return fail(`bootstrap apply: --only '${onlyTarget}' names no seed in ${manifestPath}. Declared targets:
22403
+ ${known}`);
22404
+ }
21785
22405
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
21786
22406
  const readFile9 = (p) => (0, import_node_fs24.existsSync)(p) ? (0, import_node_fs24.readFileSync)(p, "utf8") : null;
21787
22407
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
22408
+ const putSeed = async (target, content, ref, sha) => {
22409
+ const tmp = (0, import_node_path23.join)((0, import_node_os9.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
22410
+ (0, import_node_fs24.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
22411
+ try {
22412
+ await gh(contentPutInputArgs(repo, target, tmp));
22413
+ } finally {
22414
+ try {
22415
+ (0, import_node_fs24.unlinkSync)(tmp);
22416
+ } catch {
22417
+ }
22418
+ }
22419
+ };
21788
22420
  const rawVars = {};
21789
22421
  for (const value of cmdOpts.var ?? []) {
21790
22422
  const eq = value.indexOf("=");
@@ -21849,7 +22481,7 @@ function registerBootstrapCommands(program3) {
21849
22481
  }
21850
22482
  }
21851
22483
  const docsForIndex = [];
21852
- for (const seed of manifest.seeds) {
22484
+ for (const seed of seedsToApply) {
21853
22485
  if (!seed.classes.includes(o.class)) continue;
21854
22486
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
21855
22487
  if (!seedMatchesProjectType(seed, applyProjectType)) continue;
@@ -21881,12 +22513,12 @@ function registerBootstrapCommands(program3) {
21881
22513
  docsForIndex.push({ path: resolved.target, content: docBody });
21882
22514
  }
21883
22515
  if (o.execute && (action.action === "create" || action.action === "update")) {
21884
- await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
22516
+ await putSeed(resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0);
21885
22517
  applied.push(`${action.action} ${resolved.target}`);
21886
22518
  if (seedPlan.mode === "pr") seededToBranch++;
21887
22519
  }
21888
22520
  }
21889
- const indexContent = seededDocsIndex(docsForIndex);
22521
+ const indexContent = onlyTarget ? null : seededDocsIndex(docsForIndex);
21890
22522
  if (indexContent) {
21891
22523
  let indexCurrent = null;
21892
22524
  try {
@@ -21903,7 +22535,7 @@ function registerBootstrapCommands(program3) {
21903
22535
  reason: indexCurrent === null ? "generated routing index (#3545)" : "routing index present \u2014 regenerate with `mmi-cli docs index --write`, which sees the whole tree"
21904
22536
  });
21905
22537
  if (o.execute && indexAction === "create") {
21906
- await gh(contentPutArgs(repo, DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0));
22538
+ await putSeed(DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0);
21907
22539
  applied.push(`${indexAction} ${DOCS_INDEX_PATH}`);
21908
22540
  if (seedPlan.mode === "pr") seededToBranch++;
21909
22541
  }
@@ -21927,9 +22559,13 @@ function registerBootstrapCommands(program3) {
21927
22559
  "--head",
21928
22560
  seedPlan.branch,
21929
22561
  "--title",
21930
- `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
22562
+ onlyTarget ? `chore: propagate org-owned ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
21931
22563
  "--body",
21932
- `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected").`
22564
+ onlyTarget ? `Auto-opened by \`mmi-cli bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
22565
+
22566
+ \`${onlyTarget}\` is an org-owned seed declared in MMI-Hub's \`skills/bootstrap/seeds/manifest.json\`; this PR brings this repo's copy to the Hub's. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
22567
+
22568
+ \`${baseBranch}\` is protected (${seedPlan.reason}), so delivery goes via this branch + PR \u2014 a direct contents PUT 409s on a protected base.` : `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected").`
21933
22569
  ]);
21934
22570
  seedPrUrl = created.url;
21935
22571
  }
@@ -21945,7 +22581,7 @@ function registerBootstrapCommands(program3) {
21945
22581
  });
21946
22582
  applied.push(autoMergeEnabled ? `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)` : `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge refused \u2014 PR is already clean. Land it: mmi-cli pr land <n> --repo ${repo})`);
21947
22583
  }
21948
- if (o.execute && o.class === "deployable") {
22584
+ if (o.execute && !onlyTarget && o.class === "deployable") {
21949
22585
  try {
21950
22586
  await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
21951
22587
  applied.push("merge settings: allow_auto_merge, squash, delete-branch-on-merge");
@@ -21974,7 +22610,7 @@ function registerBootstrapCommands(program3) {
21974
22610
  }
21975
22611
  }
21976
22612
  }
21977
- if (o.execute) {
22613
+ if (o.execute && !onlyTarget) {
21978
22614
  for (const l of manifest.labels) {
21979
22615
  try {
21980
22616
  await gh(["label", "create", l.name, "--color", l.color, "--description", l.description, "--force", "-R", repo]);
@@ -21994,17 +22630,19 @@ function registerBootstrapCommands(program3) {
21994
22630
  }
21995
22631
  }
21996
22632
  const ddbWrites = [];
21997
- let registerPayload;
21998
- try {
21999
- registerPayload = buildRegisterPayload(repo, o.class, vars, {
22000
- projectType: o.projectType || void 0,
22001
- deployModel: o.deployModel || void 0,
22002
- releaseTrack: bootstrapReleaseTrack
22003
- });
22004
- } catch (e) {
22005
- return fail(`bootstrap apply: ${e.message}`);
22633
+ let registerPayload = {};
22634
+ if (!onlyTarget) {
22635
+ try {
22636
+ registerPayload = buildRegisterPayload(repo, o.class, vars, {
22637
+ projectType: o.projectType || void 0,
22638
+ deployModel: o.deployModel || void 0,
22639
+ releaseTrack: bootstrapReleaseTrack
22640
+ });
22641
+ } catch (e) {
22642
+ return fail(`bootstrap apply: ${e.message}`);
22643
+ }
22006
22644
  }
22007
- if (o.execute) {
22645
+ if (o.execute && !onlyTarget) {
22008
22646
  const cfg = await loadConfig();
22009
22647
  const res = await registerProject(registerPayload, registryClientDeps(cfg));
22010
22648
  if (res.ok) {
@@ -22015,7 +22653,7 @@ function registerBootstrapCommands(program3) {
22015
22653
  applied.push(`ddb register ${registerPayload.slug} (failed: ${why})`);
22016
22654
  }
22017
22655
  }
22018
- if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
22656
+ if (o.json) console.log(JSON.stringify({ repo, class: o.class, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
22019
22657
  else {
22020
22658
  console.log(renderSeedPlan(actions));
22021
22659
  if (o.execute) console.log(`
@@ -22027,11 +22665,11 @@ LIVE apply to ${repo}:
22027
22665
 
22028
22666
  // src/stage-commands.ts
22029
22667
  var import_node_fs26 = require("node:fs");
22030
- var import_node_path24 = require("node:path");
22668
+ var import_node_path25 = require("node:path");
22031
22669
 
22032
22670
  // src/port-registry.ts
22033
22671
  var import_node_fs25 = require("node:fs");
22034
- var import_node_path23 = require("node:path");
22672
+ var import_node_path24 = require("node:path");
22035
22673
 
22036
22674
  // ../infra/port-geometry.mjs
22037
22675
  var PORT_BLOCK = 100;
@@ -22084,22 +22722,22 @@ function existingPortRange(repo, registry2) {
22084
22722
  return registry2[repo] ?? null;
22085
22723
  }
22086
22724
  function portRangeInfraAt(root, source) {
22087
- const registryPath = (0, import_node_path23.join)(root, "infra", "port-ranges.json");
22088
- const ddbScriptPath = (0, import_node_path23.join)(root, "infra", "port-ddb.mjs");
22725
+ const registryPath = (0, import_node_path24.join)(root, "infra", "port-ranges.json");
22726
+ const ddbScriptPath = (0, import_node_path24.join)(root, "infra", "port-ddb.mjs");
22089
22727
  if (!(0, import_node_fs25.existsSync)(registryPath) || !(0, import_node_fs25.existsSync)(ddbScriptPath)) return null;
22090
22728
  return { root, source, registryPath, ddbScriptPath };
22091
22729
  }
22092
22730
  function resolvePortRangeInfra(cwd, packageDir) {
22093
22731
  const direct = portRangeInfraAt(cwd, "cwd");
22094
22732
  if (direct) return direct;
22095
- for (let dir = cwd; ; dir = (0, import_node_path23.dirname)(dir)) {
22096
- const sibling = portRangeInfraAt((0, import_node_path23.join)(dir, "MMI-Hub"), "sibling-hub");
22733
+ for (let dir = cwd; ; dir = (0, import_node_path24.dirname)(dir)) {
22734
+ const sibling = portRangeInfraAt((0, import_node_path24.join)(dir, "MMI-Hub"), "sibling-hub");
22097
22735
  if (sibling) return sibling;
22098
- const parent = (0, import_node_path23.dirname)(dir);
22736
+ const parent = (0, import_node_path24.dirname)(dir);
22099
22737
  if (parent === dir) break;
22100
22738
  }
22101
22739
  if (packageDir) {
22102
- const pkgRoot = (0, import_node_path23.join)(packageDir, "..", "..");
22740
+ const pkgRoot = (0, import_node_path24.join)(packageDir, "..", "..");
22103
22741
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
22104
22742
  if (pkgFrom) return pkgFrom;
22105
22743
  }
@@ -22275,8 +22913,8 @@ function registerStageCommands(program3) {
22275
22913
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
22276
22914
  return decideStage({
22277
22915
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
22278
- hasCompose: (0, import_node_fs26.existsSync)((0, import_node_path24.join)(process.cwd(), "docker-compose.yml")),
22279
- hasEnvExample: (0, import_node_fs26.existsSync)((0, import_node_path24.join)(process.cwd(), ".env.example"))
22916
+ hasCompose: (0, import_node_fs26.existsSync)((0, import_node_path25.join)(process.cwd(), "docker-compose.yml")),
22917
+ hasEnvExample: (0, import_node_fs26.existsSync)((0, import_node_path25.join)(process.cwd(), ".env.example"))
22280
22918
  });
22281
22919
  }
22282
22920
  async function fetchStageVaultEnvMerge() {
@@ -22716,8 +23354,8 @@ function registerBoardCommands(program3) {
22716
23354
  // src/merge-cleanup.ts
22717
23355
  var import_node_fs27 = require("node:fs");
22718
23356
  var import_promises7 = require("node:fs/promises");
22719
- var import_node_path26 = require("node:path");
22720
- var import_node_os9 = require("node:os");
23357
+ var import_node_path27 = require("node:path");
23358
+ var import_node_os10 = require("node:os");
22721
23359
  var import_node_child_process13 = require("node:child_process");
22722
23360
 
22723
23361
  // src/board-advance.ts
@@ -22804,7 +23442,7 @@ function boardAdvanceFailureMessage(result) {
22804
23442
 
22805
23443
  // src/deferred-registry-store.ts
22806
23444
  var import_promises6 = require("node:fs/promises");
22807
- var import_node_path25 = require("node:path");
23445
+ var import_node_path26 = require("node:path");
22808
23446
  var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
22809
23447
  async function atomicWrite(target, contents) {
22810
23448
  const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
@@ -22855,12 +23493,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
22855
23493
  },
22856
23494
  // Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
22857
23495
  write: async (entries) => {
22858
- await (0, import_promises6.mkdir)((0, import_node_path25.dirname)(registryPath), { recursive: true });
23496
+ await (0, import_promises6.mkdir)((0, import_node_path26.dirname)(registryPath), { recursive: true });
22859
23497
  await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
22860
23498
  },
22861
23499
  // Serialized read-modify-write under the repo-wide lock (#2846).
22862
23500
  update: async (mutate) => {
22863
- await (0, import_promises6.mkdir)((0, import_node_path25.dirname)(registryPath), { recursive: true });
23501
+ await (0, import_promises6.mkdir)((0, import_node_path26.dirname)(registryPath), { recursive: true });
22864
23502
  const deadline = Date.now() + opts.maxWaitMs;
22865
23503
  for (; ; ) {
22866
23504
  const guard = await acquireLock(lockPath, opts, deadline);
@@ -23021,7 +23659,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
23021
23659
  );
23022
23660
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
23023
23661
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
23024
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path26.dirname)((0, import_node_path26.dirname)(worktreeGitRoot)) : repoRoot2;
23662
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path27.dirname)((0, import_node_path27.dirname)(worktreeGitRoot)) : repoRoot2;
23025
23663
  const gcActor = describeActor({ env: process.env, surface: detectSurface(process.env), cwd: process.cwd() });
23026
23664
  const owners = readWorktreeOwners(primaryRepoRoot);
23027
23665
  const removalNow = Date.now();
@@ -23145,8 +23783,8 @@ async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
23145
23783
  const commits = JSON.parse(raw).commits ?? [];
23146
23784
  const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
23147
23785
  if (!body) return void 0;
23148
- const dir = (0, import_node_fs27.mkdtempSync)((0, import_node_path26.join)((0, import_node_os9.tmpdir)(), "mmi-squash-body-"));
23149
- const path2 = (0, import_node_path26.join)(dir, "body.txt");
23786
+ const dir = (0, import_node_fs27.mkdtempSync)((0, import_node_path27.join)((0, import_node_os10.tmpdir)(), "mmi-squash-body-"));
23787
+ const path2 = (0, import_node_path27.join)(dir, "body.txt");
23150
23788
  (0, import_node_fs27.writeFileSync)(path2, `${body}
23151
23789
  `, "utf8");
23152
23790
  return { path: path2, cleanup: () => {
@@ -23542,7 +24180,7 @@ async function fetchRestCorePool(gh = defaultGhApi) {
23542
24180
  // src/worktree-lifecycle-commands.ts
23543
24181
  var import_node_fs28 = require("node:fs");
23544
24182
  var import_promises8 = require("node:fs/promises");
23545
- var import_node_path27 = require("node:path");
24183
+ var import_node_path28 = require("node:path");
23546
24184
  var GH_TIMEOUT_MS = 2e4;
23547
24185
  var DEFAULT_BASE = "origin/development";
23548
24186
  var DEFAULT_REMOTE = "origin";
@@ -23678,7 +24316,7 @@ function classifyStaleLeaks(input) {
23678
24316
  var defaultOrphanDirScanDeps = {
23679
24317
  listDirs: (root) => {
23680
24318
  try {
23681
- return (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path27.join)(root, e.name));
24319
+ return (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path28.join)(root, e.name));
23682
24320
  } catch {
23683
24321
  return [];
23684
24322
  }
@@ -23825,13 +24463,13 @@ function registerWorktreeCommands(program3) {
23825
24463
  const headBorn = await execFileP2("git", ["-C", wtPath || ".", "rev-parse", "--verify", "--quiet", "HEAD"], { timeout: GIT_TIMEOUT_MS }).then(() => true).catch(() => false);
23826
24464
  const branch = headBorn ? (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS })).stdout.trim() : (await execFileP2("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
23827
24465
  if (!wtPath || !branch) return fail("worktree land: not inside a git worktree");
23828
- const gitFile = (0, import_node_path27.join)(wtPath, ".git");
24466
+ const gitFile = (0, import_node_path28.join)(wtPath, ".git");
23829
24467
  const isLinked = (0, import_node_fs28.existsSync)(gitFile) && (0, import_node_fs28.statSync)(gitFile).isFile();
23830
24468
  if (apply && !isLinked) {
23831
24469
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
23832
24470
  }
23833
24471
  const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
23834
- const primaryCheckout = commonDir ? (0, import_node_path27.dirname)(commonDir) : wtPath;
24472
+ const primaryCheckout = commonDir ? (0, import_node_path28.dirname)(commonDir) : wtPath;
23835
24473
  const localBranchNames = await execFileP2("git", ["-C", primaryCheckout, "for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS }).then(({ stdout }) => new Set((stdout || "").split("\n").map((l) => l.trim()).filter(Boolean))).catch(() => void 0);
23836
24474
  const orphan = classifyOrphanedWorktree({
23837
24475
  branch,
@@ -24018,7 +24656,7 @@ async function gatherWorktreeContext() {
24018
24656
  if (s) stages.push({ path: wt.path, port: s.port });
24019
24657
  }
24020
24658
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
24021
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path27.dirname)((0, import_node_path27.dirname)(worktreeGitRoot)) : repoRoot2;
24659
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path28.dirname)((0, import_node_path28.dirname)(worktreeGitRoot)) : repoRoot2;
24022
24660
  const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
24023
24661
  let orphanDirs = [];
24024
24662
  if ((0, import_node_fs28.existsSync)(wtRoot)) {
@@ -24047,6 +24685,14 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
24047
24685
  var import_node_fs29 = require("node:fs");
24048
24686
  var import_node_crypto5 = require("node:crypto");
24049
24687
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
24688
+ var ReparentConflictError = class extends Error {
24689
+ constructor(message, payload) {
24690
+ super(message);
24691
+ this.payload = payload;
24692
+ this.name = "ReparentConflictError";
24693
+ }
24694
+ payload;
24695
+ };
24050
24696
  async function editIssue(client, options, deps = {}) {
24051
24697
  const parsed = parseIssueRef(options.ref);
24052
24698
  const repo = parsed.repo ?? options.defaultRepo;
@@ -24095,7 +24741,12 @@ async function editIssue(client, options, deps = {}) {
24095
24741
  let parentResult;
24096
24742
  if (options.parent) {
24097
24743
  const run = deps.runGh ?? ghRunner;
24098
- parentResult = await linkSubIssue(run, options.parent, options.ref, repo);
24744
+ try {
24745
+ parentResult = await linkSubIssue(run, options.parent, options.ref, repo);
24746
+ } catch (e) {
24747
+ const conflict = await classifyReparentFailure(e, run, repo, parsed.number, options.parent, "--parent");
24748
+ throw conflict ? new ReparentConflictError(conflict.message, conflict.payload) : e;
24749
+ }
24099
24750
  }
24100
24751
  return {
24101
24752
  number: parsed.number,
@@ -24285,7 +24936,7 @@ ${spec.body ?? ""}`;
24285
24936
  const hash = (0, import_node_crypto5.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
24286
24937
  return `${batchKey}:${hash}`;
24287
24938
  }
24288
- var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo"]);
24939
+ var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
24289
24940
  function validateBatchSpecs(specs) {
24290
24941
  const errors = [];
24291
24942
  const validated = [];
@@ -24326,10 +24977,47 @@ function validateBatchSpecs(specs) {
24326
24977
  errors.push({ row, error: e.message });
24327
24978
  continue;
24328
24979
  }
24980
+ if (spec.surface !== void 0) {
24981
+ if (typeof spec.surface !== "string" || !spec.surface.trim()) {
24982
+ errors.push({ row, error: "surface must be a non-empty string" });
24983
+ continue;
24984
+ }
24985
+ if (!labelsCarrySurface(spec.labels)) spec.labels = [...spec.labels ?? [], surfaceLabel(spec.surface)];
24986
+ delete spec.surface;
24987
+ }
24329
24988
  validated.push({ row, spec, priority, type: spec.type });
24330
24989
  }
24331
24990
  return { ok: errors.length === 0, errors, validated };
24332
24991
  }
24992
+ async function preflightBatchSurfaces(validated, rowRepo, options) {
24993
+ const applies = /* @__PURE__ */ new Map();
24994
+ const appliesTo = async (repo) => {
24995
+ if (!applies.has(repo)) applies.set(repo, await surfaceLabelApplies(repo));
24996
+ return applies.get(repo);
24997
+ };
24998
+ if (options.surface) {
24999
+ for (const { spec } of validated) {
25000
+ if (labelsCarrySurface(spec.labels)) continue;
25001
+ if (await appliesTo(rowRepo(spec))) spec.labels = [...spec.labels ?? [], surfaceLabel(options.surface)];
25002
+ }
25003
+ }
25004
+ if (options.noSurface) return [];
25005
+ const errors = [];
25006
+ const cache = /* @__PURE__ */ new Map();
25007
+ for (const { row, spec } of validated) {
25008
+ const repo = rowRepo(spec);
25009
+ const key = [repo, labelsCarrySurface(spec.labels) ? "has" : "none"].join("::");
25010
+ let verdict = cache.get(key);
25011
+ if (!verdict) {
25012
+ verdict = await checkSurfaceRequirement({ repo, labels: spec.labels, command: "issue create --batch" });
25013
+ cache.set(key, verdict);
25014
+ if (verdict.warn) process.stderr.write(`${verdict.warn}
25015
+ `);
25016
+ }
25017
+ if (verdict.refusal) errors.push({ row, error: verdict.refusal.message });
25018
+ }
25019
+ return errors;
25020
+ }
24333
25021
  async function createIssuesBatch(specs, options, deps = {}) {
24334
25022
  const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
24335
25023
  const client = deps.client ?? defaultGitHubClient();
@@ -24345,6 +25033,12 @@ ${lines}`);
24345
25033
  throw new Error("could not resolve repo \u2014 pass --repo owner/repo or set repo per row");
24346
25034
  }
24347
25035
  const rowRepo = (spec) => spec.repo ?? defaultRepo;
25036
+ const surfaceErrors = await preflightBatchSurfaces(validation.validated, rowRepo, options);
25037
+ if (surfaceErrors.length) {
25038
+ const lines = surfaceErrors.map((e) => ` row ${e.row}: ${e.error}`).join("\n");
25039
+ throw new Error(`batch validation failed (${surfaceErrors.length} error(s)):
25040
+ ${lines}`);
25041
+ }
24348
25042
  const labelsByRepo = /* @__PURE__ */ new Map();
24349
25043
  for (const { spec } of validation.validated) {
24350
25044
  if (!spec.labels?.length) continue;
@@ -24451,7 +25145,10 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
24451
25145
  });
24452
25146
  console.log(JSON.stringify(result));
24453
25147
  } catch (e) {
24454
- return failGraceful(`issue edit failed: ${e.message}`);
25148
+ return failGracefulEnvelope(
25149
+ `issue edit failed: ${e.message}`,
25150
+ e instanceof ReparentConflictError ? e.payload : void 0
25151
+ );
24455
25152
  }
24456
25153
  });
24457
25154
  mutating(
@@ -24580,14 +25277,28 @@ ${lines}`);
24580
25277
  }
24581
25278
  const batchParent = opts.parent;
24582
25279
  const batchPriority = opts.priority ? normalizePriority(opts.priority) : void 0;
25280
+ const batchSurface = typeof opts.surface === "string" && opts.surface.trim() ? opts.surface : void 0;
25281
+ const batchNoSurface = rawFlag("--no-surface");
24583
25282
  if (opts.dryRun || opts.validateOnly) {
25283
+ const defaultRepo = await resolveRepo(opts.repo);
25284
+ const planRowRepo = (spec) => spec.repo ?? defaultRepo;
25285
+ const surfaceErrors = defaultRepo || validation.validated.every((v) => v.spec.repo) ? await preflightBatchSurfaces(validation.validated, planRowRepo, { surface: batchSurface, noSurface: batchNoSurface }) : [];
25286
+ if (surfaceErrors.length) {
25287
+ const lines = surfaceErrors.map((e) => ` row ${e.row}: ${e.error}`).join("\n");
25288
+ return fail(`issue create --batch: validation failed (${surfaceErrors.length} error(s)):
25289
+ ${lines}`, {
25290
+ code: ERROR_CODES.ERR_MISSING_FLAG,
25291
+ offending_flag: "--surface"
25292
+ });
25293
+ }
24584
25294
  const planned = validation.validated.map((v) => ({
24585
25295
  row: v.row,
24586
25296
  type: v.type,
24587
25297
  title: v.spec.title,
24588
25298
  priority: v.spec.priority ? v.priority : batchPriority ?? v.priority,
24589
25299
  repo: v.spec.repo,
24590
- ...v.spec.parent ?? batchParent ? { parent: v.spec.parent ?? batchParent } : {}
25300
+ ...v.spec.parent ?? batchParent ? { parent: v.spec.parent ?? batchParent } : {},
25301
+ ...v.spec.labels?.length ? { labels: v.spec.labels } : {}
24591
25302
  }));
24592
25303
  console.log(JSON.stringify(opts.validateOnly ? { ok: true, planned } : { dry_run: true, planned }));
24593
25304
  return;
@@ -24597,7 +25308,9 @@ ${lines}`);
24597
25308
  repo: opts.repo,
24598
25309
  idempotencyKey: opts.idempotencyKey,
24599
25310
  parent: batchParent,
24600
- priority: batchPriority
25311
+ priority: batchPriority,
25312
+ surface: batchSurface,
25313
+ noSurface: batchNoSurface
24601
25314
  }, { attach: batchAttach });
24602
25315
  console.log(JSON.stringify(result));
24603
25316
  if (result.failures.length) process.exitCode = 1;
@@ -24625,7 +25338,7 @@ ${lines}`);
24625
25338
 
24626
25339
  // src/train-commands.ts
24627
25340
  var import_node_fs30 = require("node:fs");
24628
- var import_node_path28 = require("node:path");
25341
+ var import_node_path29 = require("node:path");
24629
25342
 
24630
25343
  // src/train-status.ts
24631
25344
  function buildTrainStatusReport(input) {
@@ -24665,7 +25378,7 @@ function formatTrainStatus(r) {
24665
25378
  // src/train-commands.ts
24666
25379
  function readRepoVersion() {
24667
25380
  try {
24668
- return JSON.parse((0, import_node_fs30.readFileSync)((0, import_node_path28.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
25381
+ return JSON.parse((0, import_node_fs30.readFileSync)((0, import_node_path29.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
24669
25382
  } catch {
24670
25383
  return void 0;
24671
25384
  }
@@ -24812,8 +25525,8 @@ function registerDeployCommands(program3) {
24812
25525
 
24813
25526
  // src/discovery-commands.ts
24814
25527
  var import_node_fs31 = require("node:fs");
24815
- var import_node_os10 = require("node:os");
24816
- var import_node_path29 = require("node:path");
25528
+ var import_node_os11 = require("node:os");
25529
+ var import_node_path30 = require("node:path");
24817
25530
  var GC_GH_TIMEOUT_MS3 = 2e4;
24818
25531
  async function collectStatus() {
24819
25532
  let branch = "";
@@ -24988,10 +25701,10 @@ async function collectOnboardStatus() {
24988
25701
  else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
24989
25702
  else nextCommand = "mmi-cli board read \u2014 no claimable items found";
24990
25703
  }
24991
- const home = (0, import_node_os10.homedir)();
25704
+ const home = (0, import_node_os11.homedir)();
24992
25705
  const plugin = onboardPluginGate({
24993
- readKnown: () => readFileSyncSafe((0, import_node_path29.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs31.readFileSync),
24994
- readSettings: () => readFileSyncSafe((0, import_node_path29.join)(home, ".claude", "settings.json"), import_node_fs31.readFileSync)
25706
+ readKnown: () => readFileSyncSafe((0, import_node_path30.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs31.readFileSync),
25707
+ readSettings: () => readFileSyncSafe((0, import_node_path30.join)(home, ".claude", "settings.json"), import_node_fs31.readFileSync)
24995
25708
  });
24996
25709
  return { track, board, registry: registry2, secrets, plugin, nextCommand };
24997
25710
  }
@@ -26564,8 +27277,8 @@ function ghAccountCaveat(announcedLogin, accounts) {
26564
27277
 
26565
27278
  // src/doctor-io.ts
26566
27279
  var import_node_fs32 = require("node:fs");
26567
- var import_node_os11 = require("node:os");
26568
- var import_node_path30 = require("node:path");
27280
+ var import_node_os12 = require("node:os");
27281
+ var import_node_path31 = require("node:path");
26569
27282
  var import_node_child_process14 = require("node:child_process");
26570
27283
  var import_node_util8 = require("node:util");
26571
27284
  var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process14.execFile);
@@ -26573,7 +27286,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
26573
27286
  function installedClaudePluginVersion() {
26574
27287
  try {
26575
27288
  const file = JSON.parse(
26576
- (0, import_node_fs32.readFileSync)((0, import_node_path30.join)((0, import_node_os11.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
27289
+ (0, import_node_fs32.readFileSync)((0, import_node_path31.join)((0, import_node_os12.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
26577
27290
  );
26578
27291
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
26579
27292
  if (versions.length === 0) return void 0;
@@ -26613,7 +27326,7 @@ function worktreeRootSync() {
26613
27326
  }
26614
27327
  var gitignorePath = () => {
26615
27328
  const root = worktreeRootSync();
26616
- return root === null ? null : (0, import_node_path30.join)(root, ".gitignore");
27329
+ return root === null ? null : (0, import_node_path31.join)(root, ".gitignore");
26617
27330
  };
26618
27331
  function readGitignore() {
26619
27332
  const path2 = gitignorePath();
@@ -26652,7 +27365,7 @@ async function repoRoot() {
26652
27365
  }
26653
27366
  function hasRepoLocalWorktrees() {
26654
27367
  const root = worktreeRootSync();
26655
- return root !== null && (0, import_node_fs32.existsSync)((0, import_node_path30.join)(root, ".worktrees"));
27368
+ return root !== null && (0, import_node_fs32.existsSync)((0, import_node_path31.join)(root, ".worktrees"));
26656
27369
  }
26657
27370
 
26658
27371
  // src/index.ts
@@ -26667,7 +27380,8 @@ async function readDocsAuditFetch(repo) {
26667
27380
  const list = await fetchDocsAuditList(registryClientDeps(await loadConfig()));
26668
27381
  if ("notArmed" in list) return { notArmed: true };
26669
27382
  if (!list.ok) return { ok: false, error: list.error };
26670
- const row = list.rows.find((r) => r.repo === repo);
27383
+ const wanted = repo.toLowerCase();
27384
+ const row = list.rows.find((r) => String(r.repo ?? "").toLowerCase() === wanted);
26671
27385
  return {
26672
27386
  ok: true,
26673
27387
  verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
@@ -26696,12 +27410,12 @@ function ghMultiAccountCaveat(announcedLogin) {
26696
27410
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
26697
27411
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
26698
27412
  function envHealLockPath(home) {
26699
- return (0, import_node_path31.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
27413
+ return (0, import_node_path32.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
26700
27414
  }
26701
27415
  async function withEnvHealLock(what, run) {
26702
27416
  try {
26703
27417
  return await withFileLock(
26704
- envHealLockPath((0, import_node_os12.homedir)()),
27418
+ envHealLockPath((0, import_node_os13.homedir)()),
26705
27419
  { staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
26706
27420
  run
26707
27421
  );
@@ -26804,7 +27518,7 @@ function mmiDoctorDeps(opts = {}) {
26804
27518
  const configRoot = surfaceConfigRoot(surface);
26805
27519
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
26806
27520
  const plan = buildPluginCachePlan(
26807
- (0, import_node_os12.homedir)(),
27521
+ (0, import_node_os13.homedir)(),
26808
27522
  running,
26809
27523
  pluginCacheFsDeps(configRoot, () => 0),
26810
27524
  { configRoot, includeStaging: surface !== "codex" }
@@ -26857,11 +27571,11 @@ function mmiDoctorDeps(opts = {}) {
26857
27571
  marketplaceRows: () => {
26858
27572
  try {
26859
27573
  if (detectSurface(process.env) === "codex") return [];
26860
- const home = (0, import_node_os12.homedir)();
27574
+ const home = (0, import_node_os13.homedir)();
26861
27575
  return marketplaceRows(
26862
27576
  MMI_MARKETPLACE_NAME,
26863
- readFileSyncSafe((0, import_node_path31.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs33.readFileSync),
26864
- readFileSyncSafe((0, import_node_path31.join)(home, ".claude", "settings.json"), import_node_fs33.readFileSync)
27577
+ readFileSyncSafe((0, import_node_path32.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs33.readFileSync),
27578
+ readFileSyncSafe((0, import_node_path32.join)(home, ".claude", "settings.json"), import_node_fs33.readFileSync)
26865
27579
  );
26866
27580
  } catch {
26867
27581
  return [];
@@ -27067,7 +27781,7 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
27067
27781
  });
27068
27782
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
27069
27783
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
27070
- const path2 = (0, import_node_path31.join)(process.cwd(), ".gitignore");
27784
+ const path2 = (0, import_node_path32.join)(process.cwd(), ".gitignore");
27071
27785
  const current = (0, import_node_fs33.existsSync)(path2) ? (0, import_node_fs33.readFileSync)(path2, "utf8") : null;
27072
27786
  const plan = planManagedGitignore(current);
27073
27787
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
@@ -27234,7 +27948,7 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
27234
27948
  if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
27235
27949
  let root;
27236
27950
  if (o.root !== void 0) {
27237
- root = (0, import_node_path31.resolve)(o.root);
27951
+ root = (0, import_node_path32.resolve)(o.root);
27238
27952
  if (!(0, import_node_fs33.existsSync)(root) || !(0, import_node_fs33.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
27239
27953
  const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
27240
27954
  if (isPathUnderDirectory(gcRepoRoot, root)) {
@@ -27335,7 +28049,7 @@ function acquireWorktreeSetupLock(worktreeRoot) {
27335
28049
  };
27336
28050
  };
27337
28051
  try {
27338
- (0, import_node_fs33.mkdirSync)((0, import_node_path31.dirname)(lockPath), { recursive: true });
28052
+ (0, import_node_fs33.mkdirSync)((0, import_node_path32.dirname)(lockPath), { recursive: true });
27339
28053
  return take();
27340
28054
  } catch {
27341
28055
  try {
@@ -27376,11 +28090,25 @@ withExamples(mutating(
27376
28090
  }
27377
28091
  const repoRoot2 = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
27378
28092
  const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
27379
- const { base, fetchBranch } = resolveWorktreeBase(fromRef, o.remote);
28093
+ const { base: fallbackBase, fetchBranch, preferRemote } = resolveWorktreeBase(fromRef, o.remote);
28094
+ let base = fallbackBase;
27380
28095
  step = `fetch the base ref ${fromRef}`;
27381
28096
  if (fetchBranch) {
27382
28097
  const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
27383
- if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
28098
+ if (fetchErr && !preferRemote) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
28099
+ }
28100
+ const revParseRef = async (ref) => {
28101
+ try {
28102
+ return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
28103
+ } catch {
28104
+ return void 0;
28105
+ }
28106
+ };
28107
+ if (preferRemote && await revParseRef(preferRemote)) base = preferRemote;
28108
+ if (!o.json) {
28109
+ const baseSha = (await revParseRef(base))?.slice(0, 12) ?? "unresolved";
28110
+ const localOnly = preferRemote && base !== preferRemote ? ` (no ${preferRemote} \u2014 local ref)` : "";
28111
+ console.error(` base ${base} ${baseSha}${localOnly}`);
27384
28112
  }
27385
28113
  step = `git worktree add ${wtPath}`;
27386
28114
  await addWorktreeRobust(wtPath, branch, base, {
@@ -27642,12 +28370,27 @@ docsAudit.command("record").description("write a dated janitor verdict for one r
27642
28370
  await failGraceful(e.message);
27643
28371
  }
27644
28372
  });
27645
- docsAudit.command("status").description("read the janitor dead-man verdict back for one repo \u2014 missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").action(async (o) => {
28373
+ docsAudit.command("status").description("read the janitor dead-man verdict back for one repo \u2014 missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").option("--json", "machine-readable output \u2014 the discrete state rather than the sentence").action(async (o) => {
27646
28374
  try {
27647
28375
  const repo = o.repo ?? await currentRepoFullName();
27648
28376
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
27649
- const result = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
27650
- console.log(result.line);
28377
+ const fetched = await readDocsAuditFetch(repo);
28378
+ const result = docsAuditStatus(fetched, { repo, today });
28379
+ if (o.json) {
28380
+ const verdict = "ok" in fetched && fetched.ok ? fetched.verdict : null;
28381
+ console.log(JSON.stringify({
28382
+ repo,
28383
+ armed: !("notArmed" in fetched),
28384
+ ok: result.ok,
28385
+ state: result.state,
28386
+ date: verdict?.date ?? null,
28387
+ outcome: verdict?.outcome ?? null,
28388
+ checkerVendor: verdict?.checkerVendor ?? null,
28389
+ line: result.line
28390
+ }, null, 2));
28391
+ } else {
28392
+ console.log(result.line);
28393
+ }
27651
28394
  if (!result.ok) process.exitCode = 1;
27652
28395
  } catch (e) {
27653
28396
  await failGraceful(e.message);
@@ -27663,17 +28406,25 @@ async function reportWrite(label, res) {
27663
28406
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
27664
28407
  }
27665
28408
  var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
27666
- 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) => {
28409
+ 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/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
27667
28410
  try {
27668
- const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch });
28411
+ let lines;
28412
+ if (o.lines !== void 0) {
28413
+ if (action !== "logs") return fail("runtime tenant control: --lines is valid only for the logs action");
28414
+ lines = Number(o.lines);
28415
+ if (!Number.isInteger(lines) || lines < 1 || lines > 2e3) return fail("runtime tenant control: --lines must be an integer between 1 and 2000");
28416
+ }
28417
+ const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines });
27669
28418
  if (!o.json && action === "verify-secrets" && result.secrets) {
27670
28419
  const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
27671
- const { lines, failure } = renderVerifySecrets(body);
27672
- for (const line of lines) printLine(line);
28420
+ const { lines: lines2, failure } = renderVerifySecrets(body);
28421
+ for (const line of lines2) printLine(line);
27673
28422
  if (failure) return failGraceful(`runtime tenant control ${stage} verify-secrets: ${failure}`);
28423
+ } else if (!o.json && action === "logs" && result.logs) {
28424
+ printLine(result.logs);
27674
28425
  } else if (!o.json && action === "verify-broker" && result.broker) {
27675
- const { lines, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
27676
- for (const line of lines) printLine(line);
28426
+ const { lines: lines2, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
28427
+ for (const line of lines2) printLine(line);
27677
28428
  if (failure) return failGraceful(`runtime tenant control ${stage} verify-broker: ${failure}`);
27678
28429
  } else {
27679
28430
  printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
@@ -28138,16 +28889,43 @@ function resolveCreateType(raw, command, labels) {
28138
28889
  }
28139
28890
  return raw;
28140
28891
  }
28892
+ function resolveCreateSurface(opts) {
28893
+ return typeof opts.surface === "string" && opts.surface.trim() ? surfaceLabel(opts.surface) : void 0;
28894
+ }
28895
+ function surfaceWaived() {
28896
+ return rawFlag("--no-surface");
28897
+ }
28141
28898
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
28142
28899
  withExamples(mutating(
28143
- issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks or newlines needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
28900
+ issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks or newlines needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Required when the target repo runs the one-surface-label board rule; any value satisfies it, so this is not a closed enum").option("--no-surface", "file without a surface label on a repo that requires one \u2014 for a genuinely exempt filing (e.g. a coop proof issue that spans every surface)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
28144
28901
  // --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
28145
28902
  // ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
28146
28903
  // (`--parent`) and title-source are validated by the action on a real run.
28147
- (opts) => {
28904
+ async (opts) => {
28148
28905
  const type = resolveCreateType(opts.type, "issue create", opts.label);
28149
28906
  const priority = resolveCreatePriority(opts.priority, "issue create");
28150
- return { command: "issue create", type, title: opts.title ?? opts.titleFile, priority, repo: opts.repo };
28907
+ const planLabels = opts.label ?? [];
28908
+ const clash = conflictingSurfaceInputs(opts.surface, planLabels);
28909
+ if (clash) fail(clash.message, clash.payload);
28910
+ const planRepo = opts.batch || surfaceWaived() ? void 0 : await resolveRepo(opts.repo);
28911
+ const surface = resolveCreateSurface(opts);
28912
+ if (planRepo) {
28913
+ const { refusal, warn } = await checkSurfaceRequirement({
28914
+ repo: planRepo,
28915
+ labels: [...planLabels, ...surface ? [surface] : []]
28916
+ });
28917
+ if (warn) process.stderr.write(`${warn}
28918
+ `);
28919
+ if (refusal) fail(refusal.message, refusal.payload);
28920
+ }
28921
+ return {
28922
+ command: "issue create",
28923
+ type,
28924
+ title: opts.title ?? opts.titleFile,
28925
+ priority,
28926
+ repo: opts.repo,
28927
+ ...surface ? { surface } : {}
28928
+ };
28151
28929
  }
28152
28930
  ).action(async (o) => {
28153
28931
  let args;
@@ -28157,6 +28935,7 @@ withExamples(mutating(
28157
28935
  let issueType;
28158
28936
  let extraLabels = [];
28159
28937
  let targetRepo2;
28938
+ let surfaceFlagLabel;
28160
28939
  try {
28161
28940
  issueType = resolveCreateType(o.type, "issue create", o.label);
28162
28941
  title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises10.readFile, readStdin });
@@ -28164,6 +28943,13 @@ withExamples(mutating(
28164
28943
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
28165
28944
  priority = resolveCreatePriority(o.priority, "issue create");
28166
28945
  extraLabels = [...o.label ?? []];
28946
+ const clash = conflictingSurfaceInputs(typeof o.surface === "string" ? o.surface : void 0, extraLabels);
28947
+ if (clash) return fail(clash.message, clash.payload);
28948
+ const surfaceFromFlag = resolveCreateSurface(o);
28949
+ if (surfaceFromFlag && !labelsCarrySurface(extraLabels)) {
28950
+ extraLabels.push(surfaceFromFlag);
28951
+ surfaceFlagLabel = surfaceFromFlag;
28952
+ }
28167
28953
  targetRepo2 = await resolveRepo(o.repo);
28168
28954
  if (!targetRepo2) {
28169
28955
  return fail("issue create: could not resolve the target repo \u2014 run inside a git checkout or pass --repo <owner/repo>");
@@ -28180,6 +28966,27 @@ withExamples(mutating(
28180
28966
  } catch (e) {
28181
28967
  return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
28182
28968
  }
28969
+ {
28970
+ const { refusal, warn, enforcing } = await checkSurfaceRequirement({ repo: targetRepo2, labels: extraLabels });
28971
+ if (warn) process.stderr.write(`${warn}
28972
+ `);
28973
+ if (!enforcing && surfaceFlagLabel) {
28974
+ extraLabels = extraLabels.filter((l) => l !== surfaceFlagLabel);
28975
+ args = buildIssueArgs({
28976
+ type: issueType,
28977
+ title,
28978
+ body,
28979
+ priority,
28980
+ repo: targetRepo2,
28981
+ labels: extraLabels.length ? extraLabels : void 0
28982
+ });
28983
+ process.stderr.write(
28984
+ `warning: --surface ${surfaceFlagLabel} was dropped \u2014 ${targetRepo2} defines no surface:* labels, and creating one here would switch the one-surface-label rule on for every later filing in it. Use --label ${surfaceFlagLabel} if you really mean to start that taxonomy.
28985
+ `
28986
+ );
28987
+ }
28988
+ if (refusal && !surfaceWaived()) return fail(refusal.message, refusal.payload);
28989
+ }
28183
28990
  await ensureLabelsExist(extraLabels, targetRepo2);
28184
28991
  const created = await ghCreate(args);
28185
28992
  const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo2, priority);
@@ -28289,6 +29096,15 @@ jsonParity(issue.command("link-child <parent> <child>").description("link an exi
28289
29096
  const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
28290
29097
  console.log(JSON.stringify(result));
28291
29098
  } catch (e) {
29099
+ let conflict;
29100
+ try {
29101
+ const child2 = parseIssueRef(childRef);
29102
+ const childRepo = child2.repo ?? defaultRepo;
29103
+ if (childRepo) conflict = await classifyReparentFailure(e, ghRunner2, childRepo, child2.number, parentRef);
29104
+ } catch {
29105
+ conflict = void 0;
29106
+ }
29107
+ if (conflict) return fail(`issue link-child: ${conflict.message}`, conflict.payload);
28292
29108
  const err = e;
28293
29109
  const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
28294
29110
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
@@ -28380,6 +29196,9 @@ program2.command("report").description("file a friction report on the Hub board
28380
29196
  }
28381
29197
  const cfg = await loadConfig();
28382
29198
  if (!cfg.sagaApiUrl) return fail("report: Hub API URL not configured");
29199
+ const { warn: surfaceWarn } = await preflightReportSurface();
29200
+ if (surfaceWarn) process.stderr.write(`${surfaceWarn}
29201
+ `);
28383
29202
  const result = await fileReport(
28384
29203
  {
28385
29204
  apiUrl: cfg.sagaApiUrl,
@@ -28452,6 +29271,14 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
28452
29271
  return console.log(JSON.stringify({ deduped: true, number: dup.number, url: dup.url, score: dup.score }));
28453
29272
  }
28454
29273
  }
29274
+ const { warn: surfaceWarn } = await checkSurfaceRequirement({
29275
+ repo: targetRepo2,
29276
+ labels: [SKILL_LESSON_LABEL],
29277
+ command: "skill-lesson",
29278
+ waiver: { reason: "tooling lesson spans product surfaces \u2014 coop-proof class (#3789)" }
29279
+ });
29280
+ if (surfaceWarn) process.stderr.write(`${surfaceWarn}
29281
+ `);
28455
29282
  try {
28456
29283
  await execFileP2("gh", ["label", "create", SKILL_LESSON_LABEL, "--color", "c2e0c6", "--repo", targetRepo2], { timeout: GH_MUTATION_TIMEOUT_MS });
28457
29284
  } catch {
@@ -28501,11 +29328,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
28501
29328
  }
28502
29329
  });
28503
29330
  async function listCiWorkflowPaths(cwd = process.cwd()) {
28504
- const wfDir = (0, import_node_path31.join)(cwd, ".github", "workflows");
29331
+ const wfDir = (0, import_node_path32.join)(cwd, ".github", "workflows");
28505
29332
  if (!(0, import_node_fs33.existsSync)(wfDir)) return [];
28506
29333
  return (0, import_node_fs33.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
28507
29334
  try {
28508
- return workflowReportsPrChecks((0, import_node_fs33.readFileSync)((0, import_node_path31.join)(wfDir, name), "utf8"));
29335
+ return workflowReportsPrChecks((0, import_node_fs33.readFileSync)((0, import_node_path32.join)(wfDir, name), "utf8"));
28509
29336
  } catch {
28510
29337
  return true;
28511
29338
  }
@@ -28537,16 +29364,16 @@ function ciAuditDeps() {
28537
29364
  // gate re-seed step is skipped gracefully rather than failing mid-run.
28538
29365
  readSeedFile: (path2) => {
28539
29366
  if (!root) return null;
28540
- const fullPath = (0, import_node_path31.join)(root, path2);
29367
+ const fullPath = (0, import_node_path32.join)(root, path2);
28541
29368
  return (0, import_node_fs33.existsSync)(fullPath) ? (0, import_node_fs33.readFileSync)(fullPath, "utf8") : null;
28542
29369
  }
28543
29370
  };
28544
29371
  }
28545
29372
  function hubRoot() {
28546
- const fromPkg = (0, import_node_path31.join)(__dirname, "..", "..");
29373
+ const fromPkg = (0, import_node_path32.join)(__dirname, "..", "..");
28547
29374
  const marker = "skills/bootstrap/seeds/manifest.json";
28548
- if ((0, import_node_fs33.existsSync)((0, import_node_path31.join)(fromPkg, marker))) return fromPkg;
28549
- if ((0, import_node_fs33.existsSync)((0, import_node_path31.join)(process.cwd(), marker))) return process.cwd();
29375
+ if ((0, import_node_fs33.existsSync)((0, import_node_path32.join)(fromPkg, marker))) return fromPkg;
29376
+ if ((0, import_node_fs33.existsSync)((0, import_node_path32.join)(process.cwd(), marker))) return process.cwd();
28550
29377
  return null;
28551
29378
  }
28552
29379
  pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
@@ -28934,8 +29761,8 @@ function trainApplyDeps() {
28934
29761
  // Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
28935
29762
  // workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
28936
29763
  // outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
28937
- dispatchTenantControl: async ({ repo, stage, action }) => {
28938
- const res = await tenantControl({ repo, stage, action }, registryClientDeps(await loadConfig()));
29764
+ dispatchTenantControl: async ({ repo, stage, action, lines }) => {
29765
+ const res = await tenantControl({ repo, stage, action, ...lines != null ? { lines } : {} }, registryClientDeps(await loadConfig()));
28939
29766
  if (res.ok) return { ok: true };
28940
29767
  const body = res.body;
28941
29768
  return { ok: false, category: body?.category, error: body?.error ?? res.error };
@@ -28983,9 +29810,19 @@ function renderDeployLine(d) {
28983
29810
  if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
28984
29811
  else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
28985
29812
  else if (d.runId != null) parts.push(`deploy: UNVERIFIED \u2014 dispatched, not resolved (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
29813
+ else if (d.workflowRuns?.length) parts.push("deploy: UNVERIFIED \u2014 the runs above are enumerated, not resolved; watch each to conclusion before calling this release healthy (#3322)");
28986
29814
  else parts.push("deploy: UNVERIFIED \u2014 no run correlated or watched; resolve every workflow run on the release SHA before calling this release healthy (#3322)");
28987
29815
  return parts.join("; ");
28988
29816
  }
29817
+ function renderReleaseResume(r) {
29818
+ const lines = [`mmi-cli release --resume: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}] \u2014 ${r.note}`];
29819
+ for (const step of r.steps) lines.push(` - ${step}`);
29820
+ if (r.releaseUrl) lines.push(` release: ${r.releaseUrl}`);
29821
+ if (r.announceNote) lines.push(` announce: ${r.announceNote}`);
29822
+ if (r.dispatch) lines.push(` deploy: ${r.dispatch.note}`);
29823
+ if (r.devRollForward) lines.push(` development: ${r.devRollForward.note}`);
29824
+ return lines.join("\n");
29825
+ }
28989
29826
  function renderTrainApply(commandName, r) {
28990
29827
  let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
28991
29828
  if (r.versionFold) base = `${base}; ${r.versionFold}`;
@@ -29023,7 +29860,12 @@ for (const commandName of ["rcand", "release"]) {
29023
29860
  const RELEASE_ONLY_FLAGS = [
29024
29861
  { flags: "--announce-summary-file <path>", description: "agent-curated summary lines for the Hub Slack announcement (#883)" },
29025
29862
  { flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
29026
- { flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" }
29863
+ { flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
29864
+ // #3851: finish a release that pushed its tag and then stopped. NOT a rerun — a rerun would cut the
29865
+ // NEXT version, because the cycle resolver advances past the pushed tag and MMI_RELEASE_VERSION
29866
+ // refuses a version that is not ahead of the latest. This is the only path allowed to target an
29867
+ // existing tag, and it proves the release is genuinely partial before writing anything.
29868
+ { flags: "--resume", description: "finish a release whose tag was pushed but whose main push / Release / deploy never happened \u2014 preserves the original version, refuses unless the partial state is proven (#3851)" }
29027
29869
  ];
29028
29870
  for (const f of RELEASE_ONLY_FLAGS) {
29029
29871
  if (commandName === "release") {
@@ -29047,6 +29889,18 @@ for (const commandName of ["rcand", "release"]) {
29047
29889
  if (o.announceSummaryFile && commandName !== "release") {
29048
29890
  return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no Hub Slack announcement. Run: mmi-cli release --announce-summary-file <path>`);
29049
29891
  }
29892
+ if (o.resume && commandName !== "release") {
29893
+ return fail(`${commandName}: --resume applies only to release \u2014 it finishes a partially-released main. Run: mmi-cli release --resume`);
29894
+ }
29895
+ if (o.resume) {
29896
+ if (o.apply) return fail("release: --resume and --apply are mutually exclusive \u2014 --apply cuts the NEXT version, --resume finishes the one whose tag is already pushed");
29897
+ try {
29898
+ const result = await runReleaseResume(trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile });
29899
+ return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleaseResume(result));
29900
+ } catch (e) {
29901
+ return failGraceful(`release --resume: ${e.message}`);
29902
+ }
29903
+ }
29050
29904
  if (o.apply && o.repo) {
29051
29905
  const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
29052
29906
  const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
@@ -29252,7 +30106,7 @@ function directoryBytes(path2) {
29252
30106
  return 0;
29253
30107
  }
29254
30108
  for (const entry of entries) {
29255
- const child2 = (0, import_node_path31.join)(path2, entry.name);
30109
+ const child2 = (0, import_node_path32.join)(path2, entry.name);
29256
30110
  if (entry.isDirectory()) total += directoryBytes(child2);
29257
30111
  else {
29258
30112
  try {
@@ -29282,7 +30136,7 @@ function pluginCacheFsDeps(configRoot, dirBytes) {
29282
30136
  dirBytes,
29283
30137
  listStagingDirs: (root) => (0, import_node_fs33.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
29284
30138
  try {
29285
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path31.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs33.statSync)(p).mtimeMs) };
30139
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path32.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs33.statSync)(p).mtimeMs) };
29286
30140
  } catch {
29287
30141
  return { name: d.name, mtimeMs: Date.now() };
29288
30142
  }
@@ -29296,7 +30150,7 @@ function stagingApplyFsGuard(configRoot) {
29296
30150
  return {
29297
30151
  referencedPaths: () => readInstalledPluginRefs(configRoot),
29298
30152
  mtimeMs: (name) => {
29299
- const p = (0, import_node_path31.join)(stagingRoot, name);
30153
+ const p = (0, import_node_path32.join)(stagingRoot, name);
29300
30154
  if (!(0, import_node_fs33.existsSync)(p)) return null;
29301
30155
  try {
29302
30156
  return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs33.statSync)(q).mtimeMs);
@@ -29319,7 +30173,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
29319
30173
  return;
29320
30174
  }
29321
30175
  const plan = buildPluginCachePlan(
29322
- (0, import_node_os12.homedir)(),
30176
+ (0, import_node_os13.homedir)(),
29323
30177
  running,
29324
30178
  pluginCacheFsDeps(configRoot, directoryBytes),
29325
30179
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }