@mutmutco/cli 3.75.0 → 3.77.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 +1290 -283
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3416,7 +3416,7 @@ var program = new Command();
3416
3416
 
3417
3417
  // src/index.ts
3418
3418
  var import_promises10 = require("node:fs/promises");
3419
- var import_node_fs33 = require("node:fs");
3419
+ var import_node_fs34 = require("node:fs");
3420
3420
  var import_node_child_process15 = require("node:child_process");
3421
3421
 
3422
3422
  // src/cli-shared.ts
@@ -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_path33 = 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
+ };
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"}` };
6794
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 };
@@ -14448,7 +14866,8 @@ function trainPlan(command, options = {}) {
14448
14866
  { label: "fold the version bump into the release commit (Hub: full distribution set; app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
14449
14867
  { label: "tag release and publish GitHub Release", gated: true },
14450
14868
  { label: "trigger the repo deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release; registry-publish: own publish.yml auto-fires, watched on that repo, never a central dispatch (#2428); other models deploy via their own workflow", gated: true },
14451
- { label: "roll development forward", gated: true }
14869
+ { label: "roll development forward", gated: true },
14870
+ { label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli org project sync-info --apply", gated: true }
14452
14871
  ];
14453
14872
  }
14454
14873
  if (options.dev) {
@@ -14463,7 +14882,8 @@ function trainPlan(command, options = {}) {
14463
14882
  { label: "tag release and publish GitHub Release", gated: true },
14464
14883
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch", gated: true },
14465
14884
  { label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli runtime tenant control <owner/repo> rc retire", gated: true },
14466
- { label: "roll development forward and align rc to the released main", gated: true }
14885
+ { label: "roll development forward and align rc to the released main", gated: true },
14886
+ { label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli org project sync-info --apply", gated: true }
14467
14887
  ];
14468
14888
  }
14469
14889
  return [
@@ -14476,7 +14896,8 @@ function trainPlan(command, options = {}) {
14476
14896
  { label: "fold the version bump into the release commit (app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
14477
14897
  { label: "tag release and publish GitHub Release", gated: true },
14478
14898
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release, correlate/watch those runs; registry-publish: no manual dispatch, own publish.yml auto-fires on the release, correlate/watch that run on the product repo (#2428)", gated: true },
14479
- { label: "roll development forward", gated: true }
14899
+ { label: "roll development forward", gated: true },
14900
+ { label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli org project sync-info --apply", gated: true }
14480
14901
  ];
14481
14902
  }
14482
14903
  return [
@@ -15164,17 +15585,23 @@ function partialTrainRecoveryError(cause, input) {
15164
15585
  const causeMessage = cause instanceof Error ? cause.message : String(cause);
15165
15586
  const branch = input.stage;
15166
15587
  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";
15588
+ const tenant2 = input.deployModel === "tenant-container";
15589
+ 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
15590
  return new Error(
15171
15591
  `${causeMessage}
15172
15592
 
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.`
15593
+ partial train state for ${input.tag}:
15594
+ - tag ${input.tag}: PUSHED to origin
15595
+ - origin/${branch}: NOT pushed
15596
+ - ${releaseState}
15597
+ ` + deployLine + `
15598
+
15599
+ The tag is public and correct \u2014 do not delete or force-move it.
15600
+
15601
+ Resume it: mmi-cli release --resume${input.stage === "main" ? "" : " (main-stage partials only)"}
15602
+
15603
+ 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).
15604
+ 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
15605
  );
15179
15606
  }
15180
15607
  async function probeRemoteTag(deps, tag) {
@@ -15349,6 +15776,10 @@ async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumera
15349
15776
  workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
15350
15777
  return workflowRuns;
15351
15778
  }
15779
+ async function enumerateOwnWorkflowRuns(deps, repo, headSha) {
15780
+ const runs = await discoverShaWorkflowRuns(deps, repo, headSha, /* @__PURE__ */ new Set());
15781
+ return runs.length ? runs : [{ workflow: `sha-enumeration(${headSha.slice(0, 7)}) no runs yet`, conclusion: "pending" }];
15782
+ }
15352
15783
  var NON_DEPLOY_EVENTS = /* @__PURE__ */ new Set([
15353
15784
  "pull_request",
15354
15785
  "pull_request_target",
@@ -15425,7 +15856,11 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
15425
15856
  }
15426
15857
  if (model === "registry-publish") {
15427
15858
  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" };
15859
+ if (ref === "rc" || !autoRunHeadSha) return { note, deployStatus: "pending" };
15860
+ if (!watch) {
15861
+ const listed = await enumerateOwnWorkflowRuns(deps, ctx.repo, autoRunHeadSha);
15862
+ return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
15863
+ }
15429
15864
  const since = autoRunSince ?? (deps.now ?? Date.now)();
15430
15865
  const workflowRuns = await watchOwnWorkflowRuns(
15431
15866
  deps,
@@ -15440,8 +15875,12 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
15440
15875
  }
15441
15876
  if (model === "hub-serverless") {
15442
15877
  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
15878
  if (!autoRunHeadSha) return { note, deployStatus: "pending" };
15879
+ if (!watch) {
15880
+ if (ref === "rc") return { note, deployStatus: "pending" };
15881
+ const listed = await enumerateOwnWorkflowRuns(deps, HUB_REPO3, autoRunHeadSha);
15882
+ return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
15883
+ }
15445
15884
  const since = autoRunSince ?? (deps.now ?? Date.now)();
15446
15885
  const targets = ref === "rc" ? [{ workflow: "deploy.yml", event: "push", branch: "rc" }] : [
15447
15886
  { workflow: "deploy.yml", event: "release" },
@@ -15747,7 +16186,7 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
15747
16186
  try {
15748
16187
  checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
15749
16188
  } catch (e) {
15750
- throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
16189
+ throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main", deployModel });
15751
16190
  }
15752
16191
  if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
15753
16192
  await runGitPush(deps, ["push", "origin", "main"]);
@@ -15767,6 +16206,61 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
15767
16206
  }
15768
16207
  return { checks, releaseUrl, announceNote, dispatch };
15769
16208
  }
16209
+ async function runReleaseResume(deps, options = {}) {
16210
+ const watch = options.watch ?? false;
16211
+ const ctx = await buildTrainApplyContext(deps);
16212
+ await requireCleanTree(deps);
16213
+ await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
16214
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
16215
+ const deployModel = await preflight(deps, ctx, "main", meta);
16216
+ const tags = clean2(await deps.run("git", ["tag", "--list", "v*", "--sort=-v:refname"])).split("\n").map((t) => t.trim()).filter(Boolean);
16217
+ const tag = tags[0];
16218
+ if (!tag) throw new Error("release --resume: no v* tag found \u2014 there is no partial release to resume");
16219
+ const tagSha = await probeRemoteTag(deps, tag);
16220
+ if (!tagSha) {
16221
+ throw new Error(
16222
+ `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.`
16223
+ );
16224
+ }
16225
+ const base = { command: "release-resume", repo: ctx.repo, tag, tagSha, deployModel };
16226
+ if (!await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo)) {
16227
+ throw new Error(
16228
+ `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.`
16229
+ );
16230
+ }
16231
+ try {
16232
+ await deps.run("git", ["merge-base", "--is-ancestor", "origin/main", tagSha]);
16233
+ } catch {
16234
+ throw new Error(
16235
+ `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.`
16236
+ );
16237
+ }
16238
+ const steps = [];
16239
+ await runGitPush(deps, ["push", "origin", `${tagSha}:refs/heads/main`]);
16240
+ steps.push(`pushed ${tagSha.slice(0, 12)} to origin/main`);
16241
+ const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
16242
+ steps.push(`created the GitHub Release for ${tag}`);
16243
+ await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
16244
+ steps.push("verified the Release published against the tagged commit");
16245
+ const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
16246
+ const autoRunSince = (deps.now ?? Date.now)();
16247
+ const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
16248
+ const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
16249
+ const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
16250
+ steps.push(`dispatched the ${deployModel} deploy path`);
16251
+ const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
16252
+ steps.push(`development roll-forward: ${devRollForward.status}`);
16253
+ return {
16254
+ ...base,
16255
+ resumed: true,
16256
+ steps,
16257
+ releaseUrl,
16258
+ announceNote,
16259
+ dispatch,
16260
+ devRollForward,
16261
+ note: `resumed and completed ${tag} \u2014 the original version was preserved, not re-cut`
16262
+ };
16263
+ }
15770
16264
  async function pushRcAlignment(deps) {
15771
16265
  try {
15772
16266
  await deps.run("git", ["push", "origin", "main:rc"]);
@@ -15815,7 +16309,7 @@ async function runTrainApplyPipeline(mode, input) {
15815
16309
  try {
15816
16310
  checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
15817
16311
  } catch (e) {
15818
- throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc" });
16312
+ throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc", deployModel: deployModel2 });
15819
16313
  }
15820
16314
  const autoRunSince = (deps.now ?? Date.now)();
15821
16315
  await runGitPush(deps, ["push", "origin", "rc"]);
@@ -16132,14 +16626,14 @@ async function runTenantReconcile(deps, options) {
16132
16626
  };
16133
16627
  }
16134
16628
  function tenantControlWatches(action) {
16135
- return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker";
16629
+ return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker" || action === "logs";
16136
16630
  }
16137
16631
  async function runTenantControl(deps, options) {
16138
16632
  const { repo, stage, action } = options;
16139
16633
  const watch = options.watch ?? tenantControlWatches(action);
16140
16634
  const base = { command: "tenant-control", repo, stage, action };
16141
16635
  const since = (deps.now ?? Date.now)();
16142
- const d = await deps.dispatchTenantControl({ repo, stage, action });
16636
+ const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines });
16143
16637
  if (!d.ok) {
16144
16638
  const transport = d.category === "transport-failed";
16145
16639
  return {
@@ -16156,10 +16650,12 @@ async function runTenantControl(deps, options) {
16156
16650
  if (action === "retire") {
16157
16651
  result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
16158
16652
  }
16159
- if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker")) {
16653
+ if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker" || action === "logs")) {
16160
16654
  const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
16161
16655
  if (action === "status") {
16162
16656
  result.serviceState = parseStatusSnippet(output).serviceState;
16657
+ } else if (action === "logs") {
16658
+ result.logs = output;
16163
16659
  } else if (action === "verify-secrets") {
16164
16660
  result.secrets = parseVerifySecrets(output);
16165
16661
  result.secretsRaw = output;
@@ -18130,6 +18626,101 @@ function docsAuditStatus(fetch2, opts) {
18130
18626
  return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
18131
18627
  }
18132
18628
 
18629
+ // src/project-info-sync.ts
18630
+ var import_node_fs22 = require("node:fs");
18631
+ var import_node_path20 = require("node:path");
18632
+ var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
18633
+ updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
18634
+ projectV2 { id }
18635
+ }
18636
+ }`;
18637
+ function shortDescriptionFromReadme(markdown) {
18638
+ const lines = markdown.replace(/\r/g, "").split("\n");
18639
+ const h1 = lines.findIndex((line) => /^#\s+\S/.test(line.trim()));
18640
+ const paragraph = [];
18641
+ for (const raw of lines.slice(h1 >= 0 ? h1 + 1 : 0)) {
18642
+ const line = raw.trim();
18643
+ if (!line) {
18644
+ if (paragraph.length) break;
18645
+ continue;
18646
+ }
18647
+ if (/^(?:#|<!--|!\[|\[!\[|<img\b)/i.test(line)) {
18648
+ if (paragraph.length) break;
18649
+ continue;
18650
+ }
18651
+ paragraph.push(line);
18652
+ }
18653
+ const text = paragraph.join(" ").replace(/!\[[^\]]*]\([^)]*\)/g, "").replace(/\[([^\]]+)]\([^)]*\)/g, "$1").replace(/[*_`~]|<[^>]+>/g, "").replace(/\s+/g, " ").trim();
18654
+ if (!text) throw new Error("org project sync-info: README.md has no reader-facing description below its H1");
18655
+ const sentence = text.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? text;
18656
+ return sentence.length <= 240 ? sentence : `${sentence.slice(0, 237).trimEnd()}...`;
18657
+ }
18658
+ function entriesFor(project2, projects) {
18659
+ return project2.projectId ? projects.filter((entry) => entry.projectId === project2.projectId) : [project2];
18660
+ }
18661
+ function branchFor(repo, projects) {
18662
+ const entry = projects.find((p) => (p.repos ?? []).some((r) => r.toLowerCase() === repo.toLowerCase()));
18663
+ return typeof entry?.branch === "string" && entry.branch.trim() ? entry.branch.trim() : entry?.releaseTrack === "trunk" || entry?.class === "content" ? "main" : "development";
18664
+ }
18665
+ function sharedName(entries, fallback) {
18666
+ const names = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
18667
+ if (names.length <= 1) return names[0] ?? fallback;
18668
+ let prefix = names[0];
18669
+ for (const name of names.slice(1)) {
18670
+ while (prefix && !name.toLowerCase().startsWith(prefix.toLowerCase())) prefix = prefix.slice(0, -1);
18671
+ }
18672
+ return prefix.replace(/[-_\s]+$/, "") || fallback;
18673
+ }
18674
+ function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
18675
+ if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
18676
+ const readmePath = (0, import_node_path20.join)(repoRoot2, "README.md");
18677
+ if (!(0, import_node_fs22.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
18678
+ const entries = entriesFor(project2, projects);
18679
+ const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
18680
+ const projectName = sharedName(entries, project2.name?.trim() || targetRepo2.split("/").pop() || targetRepo2);
18681
+ if (!memberRepos.length) throw new Error(`org project sync-info: project ${projectName} has no registered member repos`);
18682
+ const entryNames = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
18683
+ const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs22.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
18684
+ const lines = [
18685
+ `# ${projectName}`,
18686
+ "",
18687
+ shortDescription,
18688
+ "",
18689
+ "## Member repos",
18690
+ "",
18691
+ ...memberRepos.map((repo) => {
18692
+ const entry = projects.find((p) => (p.repos ?? []).some((r) => r.toLowerCase() === repo.toLowerCase()));
18693
+ const name = entry?.name?.trim() || repo.split("/")[1];
18694
+ const base = `https://github.com/${repo}`;
18695
+ const branch = branchFor(repo, projects);
18696
+ return `- [${name}](${base}) \u2014 [README](${base}/blob/${branch}/README.md) \xB7 [architecture](${base}/blob/${branch}/architecture.md)`;
18697
+ })
18698
+ ];
18699
+ const targetBase = `https://github.com/${targetRepo2}`;
18700
+ const targetBranch = branchFor(targetRepo2, projects);
18701
+ const orgDocs = [
18702
+ (0, import_node_fs22.existsSync)((0, import_node_path20.join)(repoRoot2, "docs", "org-readme.md")) ? `- [Org identity](${targetBase}/blob/${targetBranch}/docs/org-readme.md)` : "",
18703
+ (0, import_node_fs22.existsSync)((0, import_node_path20.join)(repoRoot2, "docs", "org-architecture.md")) ? `- [Org architecture](${targetBase}/blob/${targetBranch}/docs/org-architecture.md)` : ""
18704
+ ].filter(Boolean);
18705
+ if (orgDocs.length) lines.push("", "## Organisation docs", "", ...orgDocs);
18706
+ return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines.join("\n")}
18707
+ ` };
18708
+ }
18709
+ async function syncProjectInfo(plan, client, apply) {
18710
+ if (apply) {
18711
+ await client.graphql(UPDATE_PROJECT_INFO, {
18712
+ projectId: plan.projectId,
18713
+ shortDescription: plan.shortDescription,
18714
+ readme: plan.readme
18715
+ });
18716
+ }
18717
+ return {
18718
+ ...plan,
18719
+ applied: apply,
18720
+ note: apply ? `Project ${plan.projectName} information synchronized` : `Project ${plan.projectName} information would be synchronized (dry-run; pass --apply)`
18721
+ };
18722
+ }
18723
+
18133
18724
  // src/oauth.ts
18134
18725
  var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
18135
18726
  var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
@@ -18968,8 +19559,8 @@ function writeError(res) {
18968
19559
  }
18969
19560
 
18970
19561
  // src/secrets-commands.ts
18971
- var import_node_fs22 = require("node:fs");
18972
- var import_node_path20 = require("node:path");
19562
+ var import_node_fs23 = require("node:fs");
19563
+ var import_node_path21 = require("node:path");
18973
19564
  var import_node_os8 = require("node:os");
18974
19565
 
18975
19566
  // src/project-runtime.ts
@@ -19093,18 +19684,18 @@ function collectMap(value, previous = []) {
19093
19684
  return [...previous, value];
19094
19685
  }
19095
19686
  async function decryptRailsCredentials(input) {
19096
- const appDir = (0, import_node_path20.resolve)(input.appDir ?? process.cwd());
19687
+ const appDir = (0, import_node_path21.resolve)(input.appDir ?? process.cwd());
19097
19688
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
19098
19689
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
19099
- const credentialsPath = (0, import_node_path20.resolve)(appDir, credentialsFile);
19100
- const masterKeyPath = (0, import_node_path20.resolve)(appDir, masterKeyFile);
19690
+ const credentialsPath = (0, import_node_path21.resolve)(appDir, credentialsFile);
19691
+ const masterKeyPath = (0, import_node_path21.resolve)(appDir, masterKeyFile);
19101
19692
  const env = {
19102
19693
  ...process.env,
19103
19694
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
19104
19695
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
19105
19696
  };
19106
- if ((0, import_node_fs22.existsSync)(masterKeyPath)) {
19107
- env.RAILS_MASTER_KEY = (0, import_node_fs22.readFileSync)(masterKeyPath, "utf8").trim();
19697
+ if ((0, import_node_fs23.existsSync)(masterKeyPath)) {
19698
+ env.RAILS_MASTER_KEY = (0, import_node_fs23.readFileSync)(masterKeyPath, "utf8").trim();
19108
19699
  }
19109
19700
  const script = [
19110
19701
  'require "json"',
@@ -19114,9 +19705,9 @@ async function decryptRailsCredentials(input) {
19114
19705
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
19115
19706
  "puts JSON.generate(config.config)"
19116
19707
  ].join("\n");
19117
- const scriptDir = (0, import_node_fs22.mkdtempSync)((0, import_node_path20.join)((0, import_node_os8.tmpdir)(), "mmi-rails-decrypt-"));
19118
- const scriptPath = (0, import_node_path20.join)(scriptDir, "decrypt.rb");
19119
- (0, import_node_fs22.writeFileSync)(scriptPath, script, "utf8");
19708
+ const scriptDir = (0, import_node_fs23.mkdtempSync)((0, import_node_path21.join)((0, import_node_os8.tmpdir)(), "mmi-rails-decrypt-"));
19709
+ const scriptPath = (0, import_node_path21.join)(scriptDir, "decrypt.rb");
19710
+ (0, import_node_fs23.writeFileSync)(scriptPath, script, "utf8");
19120
19711
  try {
19121
19712
  const args = ["exec", "ruby", scriptPath];
19122
19713
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -19128,7 +19719,7 @@ async function decryptRailsCredentials(input) {
19128
19719
  });
19129
19720
  return JSON.parse(stdout);
19130
19721
  } finally {
19131
- (0, import_node_fs22.rmSync)(scriptDir, { recursive: true, force: true });
19722
+ (0, import_node_fs23.rmSync)(scriptDir, { recursive: true, force: true });
19132
19723
  }
19133
19724
  }
19134
19725
  async function readSecretStdin() {
@@ -19218,7 +19809,7 @@ function registerSecretsCommands(program3) {
19218
19809
  let body;
19219
19810
  if (o.file) {
19220
19811
  try {
19221
- body = (0, import_node_fs22.readFileSync)((0, import_node_path20.resolve)(o.file), "utf8");
19812
+ body = (0, import_node_fs23.readFileSync)((0, import_node_path21.resolve)(o.file), "utf8");
19222
19813
  } catch (e) {
19223
19814
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
19224
19815
  }
@@ -19323,7 +19914,7 @@ function registerSecretsCommands(program3) {
19323
19914
  {
19324
19915
  ...d,
19325
19916
  decryptRailsCredentials,
19326
- removeFile: (path2) => (0, import_node_fs22.unlinkSync)((0, import_node_path20.resolve)(o.appDir ?? process.cwd(), path2))
19917
+ removeFile: (path2) => (0, import_node_fs23.unlinkSync)((0, import_node_path21.resolve)(o.appDir ?? process.cwd(), path2))
19327
19918
  },
19328
19919
  {
19329
19920
  repo: o.repo,
@@ -19517,7 +20108,7 @@ function checkGithubPools(probe) {
19517
20108
  }
19518
20109
 
19519
20110
  // src/box-commands.ts
19520
- var import_node_fs23 = require("node:fs");
20111
+ var import_node_fs24 = require("node:fs");
19521
20112
 
19522
20113
  // src/box.ts
19523
20114
  var BOX_KEYS = {
@@ -19720,7 +20311,7 @@ function registerBoxCommands(program3) {
19720
20311
  }
19721
20312
  if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
19722
20313
  else if (o.ssh && o.script) {
19723
- (0, import_node_fs23.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
20314
+ (0, import_node_fs24.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
19724
20315
  console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
19725
20316
  } else if (o.ssh) console.log(`${formatSshRecipe(found)}
19726
20317
  ${SSH_RECIPE_AGENT_NOTE}`);
@@ -20412,7 +21003,7 @@ function registerSchedulesCommands(program3) {
20412
21003
 
20413
21004
  // src/file-lock.ts
20414
21005
  var import_promises4 = require("node:fs/promises");
20415
- var import_node_path21 = require("node:path");
21006
+ var import_node_path22 = require("node:path");
20416
21007
  var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
20417
21008
  var IMMEDIATE_RETRY_BUDGET = 3;
20418
21009
  var FileLockBusyError = class extends Error {
@@ -20497,7 +21088,7 @@ async function releaseFileLock(lockPath, guard) {
20497
21088
  }
20498
21089
  async function withFileLock(lockPath, opts, fn) {
20499
21090
  const resolved = resolveFileLockOpts(opts);
20500
- await (0, import_promises4.mkdir)((0, import_node_path21.dirname)(lockPath), { recursive: true }).catch(() => void 0);
21091
+ await (0, import_promises4.mkdir)((0, import_node_path22.dirname)(lockPath), { recursive: true }).catch(() => void 0);
20501
21092
  const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
20502
21093
  try {
20503
21094
  return await fn();
@@ -20508,7 +21099,7 @@ async function withFileLock(lockPath, opts, fn) {
20508
21099
 
20509
21100
  // src/schedules-lift-command.ts
20510
21101
  var import_promises5 = require("node:fs/promises");
20511
- var import_node_path22 = require("node:path");
21102
+ var import_node_path23 = require("node:path");
20512
21103
 
20513
21104
  // src/schedules-lift.ts
20514
21105
  var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
@@ -20614,7 +21205,7 @@ async function readWorkflowFiles(dir) {
20614
21205
  const files = [];
20615
21206
  for (const name of names.sort()) {
20616
21207
  if (!/\.ya?ml$/.test(name)) continue;
20617
- files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises5.readFile)((0, import_node_path22.join)(dir, name), "utf8") });
21208
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises5.readFile)((0, import_node_path23.join)(dir, name), "utf8") });
20618
21209
  }
20619
21210
  return files;
20620
21211
  }
@@ -21156,7 +21747,63 @@ function registerQueryCommands(program3) {
21156
21747
  }
21157
21748
 
21158
21749
  // src/bootstrap-commands.ts
21159
- var import_node_fs24 = require("node:fs");
21750
+ var import_node_fs25 = require("node:fs");
21751
+ var import_node_os9 = require("node:os");
21752
+ var import_node_path24 = require("node:path");
21753
+
21754
+ // src/bootstrap-drift.ts
21755
+ function byteComparableSeeds(manifest, cls) {
21756
+ return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
21757
+ }
21758
+ function compareSeedBytes(hubContent, repoContent) {
21759
+ if (repoContent === null) return "absent";
21760
+ const normalize = (s) => s.replace(/\r\n/g, "\n");
21761
+ return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
21762
+ }
21763
+ function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
21764
+ const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
21765
+ const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
21766
+ const findings = [];
21767
+ for (const seed of seeds) {
21768
+ const hub = hubContents.get(seed.target);
21769
+ if (hub == null) {
21770
+ findings.push({
21771
+ repo,
21772
+ target: seed.target,
21773
+ state: "drift",
21774
+ detail: `the Hub's own copy could not be read \u2014 the manifest declares a file MMI-Hub does not have`
21775
+ });
21776
+ continue;
21777
+ }
21778
+ const state = compareSeedBytes(hub, byTarget.get(seed.target) ?? null);
21779
+ if (state === "match") continue;
21780
+ const why = seed.waivers?.[slug];
21781
+ if (why) {
21782
+ findings.push({ repo, target: seed.target, state: "waived", detail: `${state} \u2014 waived: ${why}` });
21783
+ continue;
21784
+ }
21785
+ findings.push({
21786
+ repo,
21787
+ target: seed.target,
21788
+ state,
21789
+ 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)"
21790
+ });
21791
+ }
21792
+ return findings;
21793
+ }
21794
+ function renderSeedDriftReport(findings, reposAudited, seedsPerRepo) {
21795
+ const lines = [`org-seed drift: ${reposAudited} repo(s) audited, ${seedsPerRepo} byte-comparable seed(s) each`];
21796
+ const waived = findings.filter((f) => f.state === "waived");
21797
+ const real = findings.filter((f) => f.state !== "waived");
21798
+ for (const f of waived) lines.push(` WAIVED ${f.repo} ${f.target} \u2014 ${f.detail}`);
21799
+ if (!real.length) {
21800
+ 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)` : ""));
21801
+ return lines.join("\n");
21802
+ }
21803
+ for (const f of real) lines.push(` ${f.state.toUpperCase().padEnd(6)} ${f.repo} ${f.target} \u2014 ${f.detail}`);
21804
+ lines.push(` \u2014 ${real.length} finding(s)${waived.length ? `, ${waived.length} waived` : ""}`);
21805
+ return lines.join("\n");
21806
+ }
21160
21807
 
21161
21808
  // src/bootstrap-verify.ts
21162
21809
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
@@ -21716,13 +22363,13 @@ function registerBootstrapCommands(program3) {
21716
22363
  client: defaultGitHubClient(),
21717
22364
  projectMeta: meta,
21718
22365
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
21719
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs24.existsSync)(path2) ? (0, import_node_fs24.readFileSync)(path2, "utf8") : null,
22366
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs25.existsSync)(path2) ? (0, import_node_fs25.readFileSync)(path2, "utf8") : null,
21720
22367
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
21721
22368
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
21722
22369
  // #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
21723
22370
  // permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
21724
22371
  // sanction, which is the pre-#3664 behaviour.
21725
- sanctionedAdmins: (0, import_node_fs24.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs24.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
22372
+ sanctionedAdmins: (0, import_node_fs25.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs25.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
21726
22373
  requiredGcpApis: (() => {
21727
22374
  const v = meta?.requiredGcpApis;
21728
22375
  if (Array.isArray(v)) return v;
@@ -21757,12 +22404,76 @@ function registerBootstrapCommands(program3) {
21757
22404
  else console.log(renderOrgRulesetDriftReport(plan));
21758
22405
  if (plan.action !== "noop") process.exitCode = 1;
21759
22406
  });
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) => {
22407
+ 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 () => {
22408
+ const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
22409
+ const manifestPath = "skills/bootstrap/seeds/manifest.json";
22410
+ if (!(0, import_node_fs25.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`);
22411
+ const manifest = loadBootstrapSeeds((0, import_node_fs25.readFileSync)(manifestPath, "utf8"));
22412
+ const hubContents = /* @__PURE__ */ new Map();
22413
+ for (const s of manifest.seeds) {
22414
+ if (s.ownership !== "org" || s.source !== "self") continue;
22415
+ hubContents.set(s.target, (0, import_node_fs25.existsSync)(s.target) ? (0, import_node_fs25.readFileSync)(s.target, "utf8") : null);
22416
+ }
22417
+ let targets;
22418
+ let classOf = (_repo) => "deployable";
22419
+ if (o.repo) {
22420
+ targets = [o.repo];
22421
+ } else {
22422
+ const projects = await fetchProjectsList(registryClientDeps(await loadConfig()));
22423
+ if (!projects || projects.length === 0) {
22424
+ 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)");
22425
+ }
22426
+ targets = collectRegistryRepos(projects);
22427
+ const byRepo = /* @__PURE__ */ new Map();
22428
+ for (const p of projects) for (const r of p.repos ?? []) byRepo.set((r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p.class ?? "deployable");
22429
+ classOf = (repo) => byRepo.get(repo.toLowerCase()) ?? "deployable";
22430
+ }
22431
+ const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
22432
+ const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
22433
+ const findings = [];
22434
+ let seedsPerRepo = 0;
22435
+ for (const repo of targets) {
22436
+ const cls = classOf(repo);
22437
+ const seeds = byteComparableSeeds(manifest, cls);
22438
+ seedsPerRepo = Math.max(seedsPerRepo, seeds.length);
22439
+ const baseBranch = cls === "content" ? "main" : "development";
22440
+ const reads = [];
22441
+ for (const seed of seeds) {
22442
+ let content = null;
22443
+ try {
22444
+ const r = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
22445
+ const parsed = JSON.parse(r.stdout);
22446
+ content = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
22447
+ } catch {
22448
+ content = null;
22449
+ }
22450
+ reads.push({ target: seed.target, content });
22451
+ }
22452
+ findings.push(...auditRepoSeedDrift(repo, seeds, hubContents, reads));
22453
+ }
22454
+ if (o.json) {
22455
+ console.log(JSON.stringify({
22456
+ // #3842: `ok` reflects real findings; waivers ride the payload so a consumer can see every
22457
+ // standing exception without them counting as drift.
22458
+ ok: findings.every((f) => f.state === "waived"),
22459
+ scope: o.repo ? "single-repo" : "fleet",
22460
+ reposAudited: targets.length,
22461
+ seedsPerRepo,
22462
+ waived: findings.filter((f) => f.state === "waived").length,
22463
+ findings
22464
+ }, null, 2));
22465
+ } else {
22466
+ console.log(renderSeedDriftReport(findings, targets.length, seedsPerRepo));
22467
+ }
22468
+ if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
22469
+ });
22470
+ 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
22471
  const o = {
21762
22472
  class: rawValue("--class", "deployable"),
21763
22473
  projectType: rawValue("--project-type", ""),
21764
22474
  deployModel: rawValue("--deploy-model", ""),
21765
22475
  releaseTrack: rawValue("--release-track", ""),
22476
+ only: rawValue("--only", ""),
21766
22477
  execute: rawFlag("--execute"),
21767
22478
  json: rawFlag("--json")
21768
22479
  };
@@ -21778,13 +22489,32 @@ function registerBootstrapCommands(program3) {
21778
22489
  return fail(`bootstrap apply: ${e.message}`);
21779
22490
  }
21780
22491
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
21781
- if (!(0, import_node_fs24.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
21782
- const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
22492
+ if (!(0, import_node_fs25.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
22493
+ const manifest = loadBootstrapSeeds((0, import_node_fs25.readFileSync)(manifestPath, "utf8"));
21783
22494
  const baseBranch = o.class === "content" ? "main" : "development";
21784
22495
  const slug = parsedRepo.slug;
22496
+ const onlyTarget = o.only.trim();
22497
+ const seedsToApply = onlyTarget ? manifest.seeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : manifest.seeds;
22498
+ if (onlyTarget && !seedsToApply.length) {
22499
+ const known = manifest.seeds.map((s) => s.target).join("\n ");
22500
+ return fail(`bootstrap apply: --only '${onlyTarget}' names no seed in ${manifestPath}. Declared targets:
22501
+ ${known}`);
22502
+ }
21785
22503
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
21786
- const readFile9 = (p) => (0, import_node_fs24.existsSync)(p) ? (0, import_node_fs24.readFileSync)(p, "utf8") : null;
22504
+ const readFile9 = (p) => (0, import_node_fs25.existsSync)(p) ? (0, import_node_fs25.readFileSync)(p, "utf8") : null;
21787
22505
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
22506
+ const putSeed = async (target, content, ref, sha) => {
22507
+ const tmp = (0, import_node_path24.join)((0, import_node_os9.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
22508
+ (0, import_node_fs25.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
22509
+ try {
22510
+ await gh(contentPutInputArgs(repo, target, tmp));
22511
+ } finally {
22512
+ try {
22513
+ (0, import_node_fs25.unlinkSync)(tmp);
22514
+ } catch {
22515
+ }
22516
+ }
22517
+ };
21788
22518
  const rawVars = {};
21789
22519
  for (const value of cmdOpts.var ?? []) {
21790
22520
  const eq = value.indexOf("=");
@@ -21849,7 +22579,7 @@ function registerBootstrapCommands(program3) {
21849
22579
  }
21850
22580
  }
21851
22581
  const docsForIndex = [];
21852
- for (const seed of manifest.seeds) {
22582
+ for (const seed of seedsToApply) {
21853
22583
  if (!seed.classes.includes(o.class)) continue;
21854
22584
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
21855
22585
  if (!seedMatchesProjectType(seed, applyProjectType)) continue;
@@ -21881,12 +22611,12 @@ function registerBootstrapCommands(program3) {
21881
22611
  docsForIndex.push({ path: resolved.target, content: docBody });
21882
22612
  }
21883
22613
  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));
22614
+ await putSeed(resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0);
21885
22615
  applied.push(`${action.action} ${resolved.target}`);
21886
22616
  if (seedPlan.mode === "pr") seededToBranch++;
21887
22617
  }
21888
22618
  }
21889
- const indexContent = seededDocsIndex(docsForIndex);
22619
+ const indexContent = onlyTarget ? null : seededDocsIndex(docsForIndex);
21890
22620
  if (indexContent) {
21891
22621
  let indexCurrent = null;
21892
22622
  try {
@@ -21903,7 +22633,7 @@ function registerBootstrapCommands(program3) {
21903
22633
  reason: indexCurrent === null ? "generated routing index (#3545)" : "routing index present \u2014 regenerate with `mmi-cli docs index --write`, which sees the whole tree"
21904
22634
  });
21905
22635
  if (o.execute && indexAction === "create") {
21906
- await gh(contentPutArgs(repo, DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0));
22636
+ await putSeed(DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0);
21907
22637
  applied.push(`${indexAction} ${DOCS_INDEX_PATH}`);
21908
22638
  if (seedPlan.mode === "pr") seededToBranch++;
21909
22639
  }
@@ -21927,9 +22657,13 @@ function registerBootstrapCommands(program3) {
21927
22657
  "--head",
21928
22658
  seedPlan.branch,
21929
22659
  "--title",
21930
- `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
22660
+ onlyTarget ? `chore: propagate org-owned ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
21931
22661
  "--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").`
22662
+ onlyTarget ? `Auto-opened by \`mmi-cli bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
22663
+
22664
+ \`${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.
22665
+
22666
+ \`${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
22667
  ]);
21934
22668
  seedPrUrl = created.url;
21935
22669
  }
@@ -21945,7 +22679,7 @@ function registerBootstrapCommands(program3) {
21945
22679
  });
21946
22680
  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
22681
  }
21948
- if (o.execute && o.class === "deployable") {
22682
+ if (o.execute && !onlyTarget && o.class === "deployable") {
21949
22683
  try {
21950
22684
  await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
21951
22685
  applied.push("merge settings: allow_auto_merge, squash, delete-branch-on-merge");
@@ -21974,7 +22708,7 @@ function registerBootstrapCommands(program3) {
21974
22708
  }
21975
22709
  }
21976
22710
  }
21977
- if (o.execute) {
22711
+ if (o.execute && !onlyTarget) {
21978
22712
  for (const l of manifest.labels) {
21979
22713
  try {
21980
22714
  await gh(["label", "create", l.name, "--color", l.color, "--description", l.description, "--force", "-R", repo]);
@@ -21994,17 +22728,19 @@ function registerBootstrapCommands(program3) {
21994
22728
  }
21995
22729
  }
21996
22730
  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}`);
22731
+ let registerPayload = {};
22732
+ if (!onlyTarget) {
22733
+ try {
22734
+ registerPayload = buildRegisterPayload(repo, o.class, vars, {
22735
+ projectType: o.projectType || void 0,
22736
+ deployModel: o.deployModel || void 0,
22737
+ releaseTrack: bootstrapReleaseTrack
22738
+ });
22739
+ } catch (e) {
22740
+ return fail(`bootstrap apply: ${e.message}`);
22741
+ }
22006
22742
  }
22007
- if (o.execute) {
22743
+ if (o.execute && !onlyTarget) {
22008
22744
  const cfg = await loadConfig();
22009
22745
  const res = await registerProject(registerPayload, registryClientDeps(cfg));
22010
22746
  if (res.ok) {
@@ -22015,7 +22751,7 @@ function registerBootstrapCommands(program3) {
22015
22751
  applied.push(`ddb register ${registerPayload.slug} (failed: ${why})`);
22016
22752
  }
22017
22753
  }
22018
- if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
22754
+ 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
22755
  else {
22020
22756
  console.log(renderSeedPlan(actions));
22021
22757
  if (o.execute) console.log(`
@@ -22026,12 +22762,12 @@ LIVE apply to ${repo}:
22026
22762
  }
22027
22763
 
22028
22764
  // src/stage-commands.ts
22029
- var import_node_fs26 = require("node:fs");
22030
- var import_node_path24 = require("node:path");
22765
+ var import_node_fs27 = require("node:fs");
22766
+ var import_node_path26 = require("node:path");
22031
22767
 
22032
22768
  // src/port-registry.ts
22033
- var import_node_fs25 = require("node:fs");
22034
- var import_node_path23 = require("node:path");
22769
+ var import_node_fs26 = require("node:fs");
22770
+ var import_node_path25 = require("node:path");
22035
22771
 
22036
22772
  // ../infra/port-geometry.mjs
22037
22773
  var PORT_BLOCK = 100;
@@ -22045,8 +22781,8 @@ function nextPortBlock(registry2) {
22045
22781
  return [base, base + PORT_SPAN];
22046
22782
  }
22047
22783
  function loadPortRegistry(path2) {
22048
- if (!(0, import_node_fs25.existsSync)(path2)) return {};
22049
- const raw = JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8"));
22784
+ if (!(0, import_node_fs26.existsSync)(path2)) return {};
22785
+ const raw = JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8"));
22050
22786
  const out = {};
22051
22787
  for (const [key, value] of Object.entries(raw)) {
22052
22788
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -22060,9 +22796,9 @@ function ensurePortRange(repo, path2) {
22060
22796
  const existing = registry2[repo];
22061
22797
  if (existing) return existing;
22062
22798
  const range = nextPortBlock(registry2);
22063
- const raw = (0, import_node_fs25.existsSync)(path2) ? JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8")) : {};
22799
+ const raw = (0, import_node_fs26.existsSync)(path2) ? JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8")) : {};
22064
22800
  raw[repo] = range;
22065
- (0, import_node_fs25.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
22801
+ (0, import_node_fs26.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
22066
22802
  return range;
22067
22803
  }
22068
22804
  function portCursorSeed(registry2) {
@@ -22084,22 +22820,22 @@ function existingPortRange(repo, registry2) {
22084
22820
  return registry2[repo] ?? null;
22085
22821
  }
22086
22822
  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");
22089
- if (!(0, import_node_fs25.existsSync)(registryPath) || !(0, import_node_fs25.existsSync)(ddbScriptPath)) return null;
22823
+ const registryPath = (0, import_node_path25.join)(root, "infra", "port-ranges.json");
22824
+ const ddbScriptPath = (0, import_node_path25.join)(root, "infra", "port-ddb.mjs");
22825
+ if (!(0, import_node_fs26.existsSync)(registryPath) || !(0, import_node_fs26.existsSync)(ddbScriptPath)) return null;
22090
22826
  return { root, source, registryPath, ddbScriptPath };
22091
22827
  }
22092
22828
  function resolvePortRangeInfra(cwd, packageDir) {
22093
22829
  const direct = portRangeInfraAt(cwd, "cwd");
22094
22830
  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");
22831
+ for (let dir = cwd; ; dir = (0, import_node_path25.dirname)(dir)) {
22832
+ const sibling = portRangeInfraAt((0, import_node_path25.join)(dir, "MMI-Hub"), "sibling-hub");
22097
22833
  if (sibling) return sibling;
22098
- const parent = (0, import_node_path23.dirname)(dir);
22834
+ const parent = (0, import_node_path25.dirname)(dir);
22099
22835
  if (parent === dir) break;
22100
22836
  }
22101
22837
  if (packageDir) {
22102
- const pkgRoot = (0, import_node_path23.join)(packageDir, "..", "..");
22838
+ const pkgRoot = (0, import_node_path25.join)(packageDir, "..", "..");
22103
22839
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
22104
22840
  if (pkgFrom) return pkgFrom;
22105
22841
  }
@@ -22275,8 +23011,8 @@ function registerStageCommands(program3) {
22275
23011
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
22276
23012
  return decideStage({
22277
23013
  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"))
23014
+ hasCompose: (0, import_node_fs27.existsSync)((0, import_node_path26.join)(process.cwd(), "docker-compose.yml")),
23015
+ hasEnvExample: (0, import_node_fs27.existsSync)((0, import_node_path26.join)(process.cwd(), ".env.example"))
22280
23016
  });
22281
23017
  }
22282
23018
  async function fetchStageVaultEnvMerge() {
@@ -22714,10 +23450,10 @@ function registerBoardCommands(program3) {
22714
23450
  }
22715
23451
 
22716
23452
  // src/merge-cleanup.ts
22717
- var import_node_fs27 = require("node:fs");
23453
+ var import_node_fs28 = require("node:fs");
22718
23454
  var import_promises7 = require("node:fs/promises");
22719
- var import_node_path26 = require("node:path");
22720
- var import_node_os9 = require("node:os");
23455
+ var import_node_path28 = require("node:path");
23456
+ var import_node_os10 = require("node:os");
22721
23457
  var import_node_child_process13 = require("node:child_process");
22722
23458
 
22723
23459
  // src/board-advance.ts
@@ -22804,7 +23540,7 @@ function boardAdvanceFailureMessage(result) {
22804
23540
 
22805
23541
  // src/deferred-registry-store.ts
22806
23542
  var import_promises6 = require("node:fs/promises");
22807
- var import_node_path25 = require("node:path");
23543
+ var import_node_path27 = require("node:path");
22808
23544
  var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
22809
23545
  async function atomicWrite(target, contents) {
22810
23546
  const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
@@ -22855,12 +23591,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
22855
23591
  },
22856
23592
  // Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
22857
23593
  write: async (entries) => {
22858
- await (0, import_promises6.mkdir)((0, import_node_path25.dirname)(registryPath), { recursive: true });
23594
+ await (0, import_promises6.mkdir)((0, import_node_path27.dirname)(registryPath), { recursive: true });
22859
23595
  await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
22860
23596
  },
22861
23597
  // Serialized read-modify-write under the repo-wide lock (#2846).
22862
23598
  update: async (mutate) => {
22863
- await (0, import_promises6.mkdir)((0, import_node_path25.dirname)(registryPath), { recursive: true });
23599
+ await (0, import_promises6.mkdir)((0, import_node_path27.dirname)(registryPath), { recursive: true });
22864
23600
  const deadline = Date.now() + opts.maxWaitMs;
22865
23601
  for (; ; ) {
22866
23602
  const guard = await acquireLock(lockPath, opts, deadline);
@@ -23021,7 +23757,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
23021
23757
  );
23022
23758
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
23023
23759
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
23024
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path26.dirname)((0, import_node_path26.dirname)(worktreeGitRoot)) : repoRoot2;
23760
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path28.dirname)((0, import_node_path28.dirname)(worktreeGitRoot)) : repoRoot2;
23025
23761
  const gcActor = describeActor({ env: process.env, surface: detectSurface(process.env), cwd: process.cwd() });
23026
23762
  const owners = readWorktreeOwners(primaryRepoRoot);
23027
23763
  const removalNow = Date.now();
@@ -23052,7 +23788,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
23052
23788
  const cleanup = await cleanupPrMergeLocalBranch(branch.branch, {
23053
23789
  beforeWorktrees,
23054
23790
  startingPath: branch.worktreePath,
23055
- pathExists: (p) => (0, import_node_fs27.existsSync)(p),
23791
+ pathExists: (p) => (0, import_node_fs28.existsSync)(p),
23056
23792
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
23057
23793
  teardownWorktreeStage,
23058
23794
  deferredStore,
@@ -23080,7 +23816,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
23080
23816
  for (const wt of worktreeDirsToRemove) {
23081
23817
  try {
23082
23818
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
23083
- realpath: (path2) => (0, import_node_fs27.realpathSync)(path2)
23819
+ realpath: (path2) => (0, import_node_fs28.realpathSync)(path2)
23084
23820
  });
23085
23821
  if (!cleanupTarget.ok) {
23086
23822
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -23145,13 +23881,13 @@ async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
23145
23881
  const commits = JSON.parse(raw).commits ?? [];
23146
23882
  const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
23147
23883
  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");
23150
- (0, import_node_fs27.writeFileSync)(path2, `${body}
23884
+ const dir = (0, import_node_fs28.mkdtempSync)((0, import_node_path28.join)((0, import_node_os10.tmpdir)(), "mmi-squash-body-"));
23885
+ const path2 = (0, import_node_path28.join)(dir, "body.txt");
23886
+ (0, import_node_fs28.writeFileSync)(path2, `${body}
23151
23887
  `, "utf8");
23152
23888
  return { path: path2, cleanup: () => {
23153
23889
  try {
23154
- (0, import_node_fs27.rmSync)(dir, { recursive: true, force: true });
23890
+ (0, import_node_fs28.rmSync)(dir, { recursive: true, force: true });
23155
23891
  } catch {
23156
23892
  }
23157
23893
  } };
@@ -23273,13 +24009,13 @@ var realWorktreeDirRemover = {
23273
24009
  probe: (p) => {
23274
24010
  let st;
23275
24011
  try {
23276
- st = (0, import_node_fs27.lstatSync)(p);
24012
+ st = (0, import_node_fs28.lstatSync)(p);
23277
24013
  } catch {
23278
24014
  return null;
23279
24015
  }
23280
24016
  if (st.isSymbolicLink()) return "link";
23281
24017
  try {
23282
- (0, import_node_fs27.readlinkSync)(p);
24018
+ (0, import_node_fs28.readlinkSync)(p);
23283
24019
  return "link";
23284
24020
  } catch {
23285
24021
  }
@@ -23287,7 +24023,7 @@ var realWorktreeDirRemover = {
23287
24023
  },
23288
24024
  readdir: (p) => {
23289
24025
  try {
23290
- return (0, import_node_fs27.readdirSync)(p);
24026
+ return (0, import_node_fs28.readdirSync)(p);
23291
24027
  } catch {
23292
24028
  return [];
23293
24029
  }
@@ -23296,9 +24032,9 @@ var realWorktreeDirRemover = {
23296
24032
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
23297
24033
  detachLink: (p) => {
23298
24034
  try {
23299
- (0, import_node_fs27.rmdirSync)(p);
24035
+ (0, import_node_fs28.rmdirSync)(p);
23300
24036
  } catch {
23301
- (0, import_node_fs27.unlinkSync)(p);
24037
+ (0, import_node_fs28.unlinkSync)(p);
23302
24038
  }
23303
24039
  },
23304
24040
  removeTree: (p) => (0, import_promises7.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -23331,9 +24067,9 @@ async function worktreeHasStageState(worktreePath) {
23331
24067
  }
23332
24068
  }
23333
24069
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
23334
- if (!(0, import_node_fs27.existsSync)(statePath)) return false;
24070
+ if (!(0, import_node_fs28.existsSync)(statePath)) return false;
23335
24071
  try {
23336
- const state = JSON.parse((0, import_node_fs27.readFileSync)(statePath, "utf8"));
24072
+ const state = JSON.parse((0, import_node_fs28.readFileSync)(statePath, "utf8"));
23337
24073
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
23338
24074
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
23339
24075
  } catch {
@@ -23540,9 +24276,9 @@ async function fetchRestCorePool(gh = defaultGhApi) {
23540
24276
  }
23541
24277
 
23542
24278
  // src/worktree-lifecycle-commands.ts
23543
- var import_node_fs28 = require("node:fs");
24279
+ var import_node_fs29 = require("node:fs");
23544
24280
  var import_promises8 = require("node:fs/promises");
23545
- var import_node_path27 = require("node:path");
24281
+ var import_node_path29 = require("node:path");
23546
24282
  var GH_TIMEOUT_MS = 2e4;
23547
24283
  var DEFAULT_BASE = "origin/development";
23548
24284
  var DEFAULT_REMOTE = "origin";
@@ -23678,7 +24414,7 @@ function classifyStaleLeaks(input) {
23678
24414
  var defaultOrphanDirScanDeps = {
23679
24415
  listDirs: (root) => {
23680
24416
  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));
24417
+ return (0, import_node_fs29.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path29.join)(root, e.name));
23682
24418
  } catch {
23683
24419
  return [];
23684
24420
  }
@@ -23825,13 +24561,13 @@ function registerWorktreeCommands(program3) {
23825
24561
  const headBorn = await execFileP2("git", ["-C", wtPath || ".", "rev-parse", "--verify", "--quiet", "HEAD"], { timeout: GIT_TIMEOUT_MS }).then(() => true).catch(() => false);
23826
24562
  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
24563
  if (!wtPath || !branch) return fail("worktree land: not inside a git worktree");
23828
- const gitFile = (0, import_node_path27.join)(wtPath, ".git");
23829
- const isLinked = (0, import_node_fs28.existsSync)(gitFile) && (0, import_node_fs28.statSync)(gitFile).isFile();
24564
+ const gitFile = (0, import_node_path29.join)(wtPath, ".git");
24565
+ const isLinked = (0, import_node_fs29.existsSync)(gitFile) && (0, import_node_fs29.statSync)(gitFile).isFile();
23830
24566
  if (apply && !isLinked) {
23831
24567
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
23832
24568
  }
23833
24569
  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;
24570
+ const primaryCheckout = commonDir ? (0, import_node_path29.dirname)(commonDir) : wtPath;
23835
24571
  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
24572
  const orphan = classifyOrphanedWorktree({
23837
24573
  branch,
@@ -24018,10 +24754,10 @@ async function gatherWorktreeContext() {
24018
24754
  if (s) stages.push({ path: wt.path, port: s.port });
24019
24755
  }
24020
24756
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
24021
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path27.dirname)((0, import_node_path27.dirname)(worktreeGitRoot)) : repoRoot2;
24757
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path29.dirname)((0, import_node_path29.dirname)(worktreeGitRoot)) : repoRoot2;
24022
24758
  const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
24023
24759
  let orphanDirs = [];
24024
- if ((0, import_node_fs28.existsSync)(wtRoot)) {
24760
+ if ((0, import_node_fs29.existsSync)(wtRoot)) {
24025
24761
  orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
24026
24762
  ...defaultOrphanDirScanDeps,
24027
24763
  listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
@@ -24044,9 +24780,17 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
24044
24780
  }
24045
24781
 
24046
24782
  // src/issue-commands.ts
24047
- var import_node_fs29 = require("node:fs");
24783
+ var import_node_fs30 = require("node:fs");
24048
24784
  var import_node_crypto5 = require("node:crypto");
24049
24785
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
24786
+ var ReparentConflictError = class extends Error {
24787
+ constructor(message, payload) {
24788
+ super(message);
24789
+ this.payload = payload;
24790
+ this.name = "ReparentConflictError";
24791
+ }
24792
+ payload;
24793
+ };
24050
24794
  async function editIssue(client, options, deps = {}) {
24051
24795
  const parsed = parseIssueRef(options.ref);
24052
24796
  const repo = parsed.repo ?? options.defaultRepo;
@@ -24054,7 +24798,7 @@ async function editIssue(client, options, deps = {}) {
24054
24798
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
24055
24799
  const patch = {};
24056
24800
  let bodyChanged = false;
24057
- const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs29.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
24801
+ const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs30.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
24058
24802
  if (options.titleFile !== void 0) {
24059
24803
  patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
24060
24804
  } else if (options.title !== void 0) {
@@ -24095,7 +24839,12 @@ async function editIssue(client, options, deps = {}) {
24095
24839
  let parentResult;
24096
24840
  if (options.parent) {
24097
24841
  const run = deps.runGh ?? ghRunner;
24098
- parentResult = await linkSubIssue(run, options.parent, options.ref, repo);
24842
+ try {
24843
+ parentResult = await linkSubIssue(run, options.parent, options.ref, repo);
24844
+ } catch (e) {
24845
+ const conflict = await classifyReparentFailure(e, run, repo, parsed.number, options.parent, "--parent");
24846
+ throw conflict ? new ReparentConflictError(conflict.message, conflict.payload) : e;
24847
+ }
24099
24848
  }
24100
24849
  return {
24101
24850
  number: parsed.number,
@@ -24285,7 +25034,7 @@ ${spec.body ?? ""}`;
24285
25034
  const hash = (0, import_node_crypto5.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
24286
25035
  return `${batchKey}:${hash}`;
24287
25036
  }
24288
- var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo"]);
25037
+ var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
24289
25038
  function validateBatchSpecs(specs) {
24290
25039
  const errors = [];
24291
25040
  const validated = [];
@@ -24326,10 +25075,47 @@ function validateBatchSpecs(specs) {
24326
25075
  errors.push({ row, error: e.message });
24327
25076
  continue;
24328
25077
  }
25078
+ if (spec.surface !== void 0) {
25079
+ if (typeof spec.surface !== "string" || !spec.surface.trim()) {
25080
+ errors.push({ row, error: "surface must be a non-empty string" });
25081
+ continue;
25082
+ }
25083
+ if (!labelsCarrySurface(spec.labels)) spec.labels = [...spec.labels ?? [], surfaceLabel(spec.surface)];
25084
+ delete spec.surface;
25085
+ }
24329
25086
  validated.push({ row, spec, priority, type: spec.type });
24330
25087
  }
24331
25088
  return { ok: errors.length === 0, errors, validated };
24332
25089
  }
25090
+ async function preflightBatchSurfaces(validated, rowRepo, options) {
25091
+ const applies = /* @__PURE__ */ new Map();
25092
+ const appliesTo = async (repo) => {
25093
+ if (!applies.has(repo)) applies.set(repo, await surfaceLabelApplies(repo));
25094
+ return applies.get(repo);
25095
+ };
25096
+ if (options.surface) {
25097
+ for (const { spec } of validated) {
25098
+ if (labelsCarrySurface(spec.labels)) continue;
25099
+ if (await appliesTo(rowRepo(spec))) spec.labels = [...spec.labels ?? [], surfaceLabel(options.surface)];
25100
+ }
25101
+ }
25102
+ if (options.noSurface) return [];
25103
+ const errors = [];
25104
+ const cache = /* @__PURE__ */ new Map();
25105
+ for (const { row, spec } of validated) {
25106
+ const repo = rowRepo(spec);
25107
+ const key = [repo, labelsCarrySurface(spec.labels) ? "has" : "none"].join("::");
25108
+ let verdict = cache.get(key);
25109
+ if (!verdict) {
25110
+ verdict = await checkSurfaceRequirement({ repo, labels: spec.labels, command: "issue create --batch" });
25111
+ cache.set(key, verdict);
25112
+ if (verdict.warn) process.stderr.write(`${verdict.warn}
25113
+ `);
25114
+ }
25115
+ if (verdict.refusal) errors.push({ row, error: verdict.refusal.message });
25116
+ }
25117
+ return errors;
25118
+ }
24333
25119
  async function createIssuesBatch(specs, options, deps = {}) {
24334
25120
  const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
24335
25121
  const client = deps.client ?? defaultGitHubClient();
@@ -24345,6 +25131,12 @@ ${lines}`);
24345
25131
  throw new Error("could not resolve repo \u2014 pass --repo owner/repo or set repo per row");
24346
25132
  }
24347
25133
  const rowRepo = (spec) => spec.repo ?? defaultRepo;
25134
+ const surfaceErrors = await preflightBatchSurfaces(validation.validated, rowRepo, options);
25135
+ if (surfaceErrors.length) {
25136
+ const lines = surfaceErrors.map((e) => ` row ${e.row}: ${e.error}`).join("\n");
25137
+ throw new Error(`batch validation failed (${surfaceErrors.length} error(s)):
25138
+ ${lines}`);
25139
+ }
24348
25140
  const labelsByRepo = /* @__PURE__ */ new Map();
24349
25141
  for (const { spec } of validation.validated) {
24350
25142
  if (!spec.labels?.length) continue;
@@ -24451,7 +25243,10 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
24451
25243
  });
24452
25244
  console.log(JSON.stringify(result));
24453
25245
  } catch (e) {
24454
- return failGraceful(`issue edit failed: ${e.message}`);
25246
+ return failGracefulEnvelope(
25247
+ `issue edit failed: ${e.message}`,
25248
+ e instanceof ReparentConflictError ? e.payload : void 0
25249
+ );
24455
25250
  }
24456
25251
  });
24457
25252
  mutating(
@@ -24566,7 +25361,7 @@ function extendCreateCommand(issue2, batchAttach) {
24566
25361
  if (opts.batch) {
24567
25362
  let specs;
24568
25363
  try {
24569
- const raw = (0, import_node_fs29.readFileSync)(opts.batch, "utf8");
25364
+ const raw = (0, import_node_fs30.readFileSync)(opts.batch, "utf8");
24570
25365
  specs = JSON.parse(raw);
24571
25366
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
24572
25367
  } catch (e) {
@@ -24580,14 +25375,28 @@ ${lines}`);
24580
25375
  }
24581
25376
  const batchParent = opts.parent;
24582
25377
  const batchPriority = opts.priority ? normalizePriority(opts.priority) : void 0;
25378
+ const batchSurface = typeof opts.surface === "string" && opts.surface.trim() ? opts.surface : void 0;
25379
+ const batchNoSurface = rawFlag("--no-surface");
24583
25380
  if (opts.dryRun || opts.validateOnly) {
25381
+ const defaultRepo = await resolveRepo(opts.repo);
25382
+ const planRowRepo = (spec) => spec.repo ?? defaultRepo;
25383
+ const surfaceErrors = defaultRepo || validation.validated.every((v) => v.spec.repo) ? await preflightBatchSurfaces(validation.validated, planRowRepo, { surface: batchSurface, noSurface: batchNoSurface }) : [];
25384
+ if (surfaceErrors.length) {
25385
+ const lines = surfaceErrors.map((e) => ` row ${e.row}: ${e.error}`).join("\n");
25386
+ return fail(`issue create --batch: validation failed (${surfaceErrors.length} error(s)):
25387
+ ${lines}`, {
25388
+ code: ERROR_CODES.ERR_MISSING_FLAG,
25389
+ offending_flag: "--surface"
25390
+ });
25391
+ }
24584
25392
  const planned = validation.validated.map((v) => ({
24585
25393
  row: v.row,
24586
25394
  type: v.type,
24587
25395
  title: v.spec.title,
24588
25396
  priority: v.spec.priority ? v.priority : batchPriority ?? v.priority,
24589
25397
  repo: v.spec.repo,
24590
- ...v.spec.parent ?? batchParent ? { parent: v.spec.parent ?? batchParent } : {}
25398
+ ...v.spec.parent ?? batchParent ? { parent: v.spec.parent ?? batchParent } : {},
25399
+ ...v.spec.labels?.length ? { labels: v.spec.labels } : {}
24591
25400
  }));
24592
25401
  console.log(JSON.stringify(opts.validateOnly ? { ok: true, planned } : { dry_run: true, planned }));
24593
25402
  return;
@@ -24597,7 +25406,9 @@ ${lines}`);
24597
25406
  repo: opts.repo,
24598
25407
  idempotencyKey: opts.idempotencyKey,
24599
25408
  parent: batchParent,
24600
- priority: batchPriority
25409
+ priority: batchPriority,
25410
+ surface: batchSurface,
25411
+ noSurface: batchNoSurface
24601
25412
  }, { attach: batchAttach });
24602
25413
  console.log(JSON.stringify(result));
24603
25414
  if (result.failures.length) process.exitCode = 1;
@@ -24624,8 +25435,8 @@ ${lines}`);
24624
25435
  }
24625
25436
 
24626
25437
  // src/train-commands.ts
24627
- var import_node_fs30 = require("node:fs");
24628
- var import_node_path28 = require("node:path");
25438
+ var import_node_fs31 = require("node:fs");
25439
+ var import_node_path30 = require("node:path");
24629
25440
 
24630
25441
  // src/train-status.ts
24631
25442
  function buildTrainStatusReport(input) {
@@ -24665,7 +25476,7 @@ function formatTrainStatus(r) {
24665
25476
  // src/train-commands.ts
24666
25477
  function readRepoVersion() {
24667
25478
  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;
25479
+ return JSON.parse((0, import_node_fs31.readFileSync)((0, import_node_path30.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
24669
25480
  } catch {
24670
25481
  return void 0;
24671
25482
  }
@@ -24811,9 +25622,9 @@ function registerDeployCommands(program3) {
24811
25622
  }
24812
25623
 
24813
25624
  // src/discovery-commands.ts
24814
- var import_node_fs31 = require("node:fs");
24815
- var import_node_os10 = require("node:os");
24816
- var import_node_path29 = require("node:path");
25625
+ var import_node_fs32 = require("node:fs");
25626
+ var import_node_os11 = require("node:os");
25627
+ var import_node_path31 = require("node:path");
24817
25628
  var GC_GH_TIMEOUT_MS3 = 2e4;
24818
25629
  async function collectStatus() {
24819
25630
  let branch = "";
@@ -24988,10 +25799,10 @@ async function collectOnboardStatus() {
24988
25799
  else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
24989
25800
  else nextCommand = "mmi-cli board read \u2014 no claimable items found";
24990
25801
  }
24991
- const home = (0, import_node_os10.homedir)();
25802
+ const home = (0, import_node_os11.homedir)();
24992
25803
  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)
25804
+ readKnown: () => readFileSyncSafe((0, import_node_path31.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs32.readFileSync),
25805
+ readSettings: () => readFileSyncSafe((0, import_node_path31.join)(home, ".claude", "settings.json"), import_node_fs32.readFileSync)
24995
25806
  });
24996
25807
  return { track, board, registry: registry2, secrets, plugin, nextCommand };
24997
25808
  }
@@ -26503,17 +27314,17 @@ function parseOriginRepo(remoteUrl) {
26503
27314
  }
26504
27315
  function ghHostsConfigPath(env, platform2) {
26505
27316
  const sep2 = platform2 === "win32" ? "\\" : "/";
26506
- const join27 = (...parts) => parts.join(sep2);
27317
+ const join28 = (...parts) => parts.join(sep2);
26507
27318
  const explicit = env.GH_CONFIG_DIR?.trim();
26508
- if (explicit) return join27(explicit, "hosts.yml");
27319
+ if (explicit) return join28(explicit, "hosts.yml");
26509
27320
  if (platform2 === "win32") {
26510
27321
  const appData = (env.AppData ?? env.APPDATA)?.trim();
26511
- return appData ? join27(appData, "GitHub CLI", "hosts.yml") : void 0;
27322
+ return appData ? join28(appData, "GitHub CLI", "hosts.yml") : void 0;
26512
27323
  }
26513
27324
  const xdg = env.XDG_CONFIG_HOME?.trim();
26514
- if (xdg) return join27(xdg, "gh", "hosts.yml");
27325
+ if (xdg) return join28(xdg, "gh", "hosts.yml");
26515
27326
  const home = env.HOME?.trim();
26516
- return home ? join27(home, ".config", "gh", "hosts.yml") : void 0;
27327
+ return home ? join28(home, ".config", "gh", "hosts.yml") : void 0;
26517
27328
  }
26518
27329
  function parseGhHostsAccounts(yaml, host = "github.com") {
26519
27330
  let hostIndent = null;
@@ -26563,9 +27374,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
26563
27374
  }
26564
27375
 
26565
27376
  // src/doctor-io.ts
26566
- var import_node_fs32 = require("node:fs");
26567
- var import_node_os11 = require("node:os");
26568
- var import_node_path30 = require("node:path");
27377
+ var import_node_fs33 = require("node:fs");
27378
+ var import_node_os12 = require("node:os");
27379
+ var import_node_path32 = require("node:path");
26569
27380
  var import_node_child_process14 = require("node:child_process");
26570
27381
  var import_node_util8 = require("node:util");
26571
27382
  var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process14.execFile);
@@ -26573,7 +27384,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
26573
27384
  function installedClaudePluginVersion() {
26574
27385
  try {
26575
27386
  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")
27387
+ (0, import_node_fs33.readFileSync)((0, import_node_path32.join)((0, import_node_os12.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
26577
27388
  );
26578
27389
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
26579
27390
  if (versions.length === 0) return void 0;
@@ -26613,13 +27424,13 @@ function worktreeRootSync() {
26613
27424
  }
26614
27425
  var gitignorePath = () => {
26615
27426
  const root = worktreeRootSync();
26616
- return root === null ? null : (0, import_node_path30.join)(root, ".gitignore");
27427
+ return root === null ? null : (0, import_node_path32.join)(root, ".gitignore");
26617
27428
  };
26618
27429
  function readGitignore() {
26619
27430
  const path2 = gitignorePath();
26620
27431
  if (path2 === null) return null;
26621
27432
  try {
26622
- return (0, import_node_fs32.readFileSync)(path2, "utf8");
27433
+ return (0, import_node_fs33.readFileSync)(path2, "utf8");
26623
27434
  } catch {
26624
27435
  return null;
26625
27436
  }
@@ -26628,7 +27439,7 @@ function writeGitignore(content) {
26628
27439
  const path2 = gitignorePath();
26629
27440
  if (path2 === null) return false;
26630
27441
  try {
26631
- (0, import_node_fs32.writeFileSync)(path2, content, "utf8");
27442
+ (0, import_node_fs33.writeFileSync)(path2, content, "utf8");
26632
27443
  return true;
26633
27444
  } catch {
26634
27445
  return false;
@@ -26652,7 +27463,7 @@ async function repoRoot() {
26652
27463
  }
26653
27464
  function hasRepoLocalWorktrees() {
26654
27465
  const root = worktreeRootSync();
26655
- return root !== null && (0, import_node_fs32.existsSync)((0, import_node_path30.join)(root, ".worktrees"));
27466
+ return root !== null && (0, import_node_fs33.existsSync)((0, import_node_path32.join)(root, ".worktrees"));
26656
27467
  }
26657
27468
 
26658
27469
  // src/index.ts
@@ -26667,7 +27478,8 @@ async function readDocsAuditFetch(repo) {
26667
27478
  const list = await fetchDocsAuditList(registryClientDeps(await loadConfig()));
26668
27479
  if ("notArmed" in list) return { notArmed: true };
26669
27480
  if (!list.ok) return { ok: false, error: list.error };
26670
- const row = list.rows.find((r) => r.repo === repo);
27481
+ const wanted = repo.toLowerCase();
27482
+ const row = list.rows.find((r) => String(r.repo ?? "").toLowerCase() === wanted);
26671
27483
  return {
26672
27484
  ok: true,
26673
27485
  verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
@@ -26687,8 +27499,8 @@ ${r.stderr ?? ""}`).catch(() => "");
26687
27499
  function ghMultiAccountCaveat(announcedLogin) {
26688
27500
  try {
26689
27501
  const hostsPath = ghHostsConfigPath(process.env, process.platform);
26690
- if (!hostsPath || !(0, import_node_fs33.existsSync)(hostsPath)) return void 0;
26691
- return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs33.readFileSync)(hostsPath, "utf8")));
27502
+ if (!hostsPath || !(0, import_node_fs34.existsSync)(hostsPath)) return void 0;
27503
+ return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs34.readFileSync)(hostsPath, "utf8")));
26692
27504
  } catch {
26693
27505
  return void 0;
26694
27506
  }
@@ -26696,12 +27508,12 @@ function ghMultiAccountCaveat(announcedLogin) {
26696
27508
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
26697
27509
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
26698
27510
  function envHealLockPath(home) {
26699
- return (0, import_node_path31.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
27511
+ return (0, import_node_path33.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
26700
27512
  }
26701
27513
  async function withEnvHealLock(what, run) {
26702
27514
  try {
26703
27515
  return await withFileLock(
26704
- envHealLockPath((0, import_node_os12.homedir)()),
27516
+ envHealLockPath((0, import_node_os13.homedir)()),
26705
27517
  { staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
26706
27518
  run
26707
27519
  );
@@ -26804,7 +27616,7 @@ function mmiDoctorDeps(opts = {}) {
26804
27616
  const configRoot = surfaceConfigRoot(surface);
26805
27617
  const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
26806
27618
  const plan = buildPluginCachePlan(
26807
- (0, import_node_os12.homedir)(),
27619
+ (0, import_node_os13.homedir)(),
26808
27620
  running,
26809
27621
  pluginCacheFsDeps(configRoot, () => 0),
26810
27622
  { configRoot, includeStaging: surface !== "codex" }
@@ -26857,11 +27669,11 @@ function mmiDoctorDeps(opts = {}) {
26857
27669
  marketplaceRows: () => {
26858
27670
  try {
26859
27671
  if (detectSurface(process.env) === "codex") return [];
26860
- const home = (0, import_node_os12.homedir)();
27672
+ const home = (0, import_node_os13.homedir)();
26861
27673
  return marketplaceRows(
26862
27674
  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)
27675
+ readFileSyncSafe((0, import_node_path33.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs34.readFileSync),
27676
+ readFileSyncSafe((0, import_node_path33.join)(home, ".claude", "settings.json"), import_node_fs34.readFileSync)
26865
27677
  );
26866
27678
  } catch {
26867
27679
  return [];
@@ -27067,19 +27879,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
27067
27879
  });
27068
27880
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
27069
27881
  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");
27071
- const current = (0, import_node_fs33.existsSync)(path2) ? (0, import_node_fs33.readFileSync)(path2, "utf8") : null;
27882
+ const path2 = (0, import_node_path33.join)(process.cwd(), ".gitignore");
27883
+ const current = (0, import_node_fs34.existsSync)(path2) ? (0, import_node_fs34.readFileSync)(path2, "utf8") : null;
27072
27884
  const plan = planManagedGitignore(current);
27073
27885
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
27074
27886
  if (opts.json) {
27075
- if (opts.write && plan.changed) (0, import_node_fs33.writeFileSync)(path2, plan.content, "utf8");
27887
+ if (opts.write && plan.changed) (0, import_node_fs34.writeFileSync)(path2, plan.content, "utf8");
27076
27888
  console.log(JSON.stringify(plan, null, 2));
27077
27889
  if (!opts.write && plan.changed) process.exitCode = 1;
27078
27890
  return;
27079
27891
  }
27080
27892
  if (opts.write) {
27081
27893
  if (plan.changed) {
27082
- (0, import_node_fs33.writeFileSync)(path2, plan.content, "utf8");
27894
+ (0, import_node_fs34.writeFileSync)(path2, plan.content, "utf8");
27083
27895
  console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
27084
27896
  } else {
27085
27897
  console.log("mmi-cli org rules gitignore: up to date");
@@ -27234,8 +28046,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
27234
28046
  if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
27235
28047
  let root;
27236
28048
  if (o.root !== void 0) {
27237
- root = (0, import_node_path31.resolve)(o.root);
27238
- 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`);
28049
+ root = (0, import_node_path33.resolve)(o.root);
28050
+ if (!(0, import_node_fs34.existsSync)(root) || !(0, import_node_fs34.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
27239
28051
  const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
27240
28052
  if (isPathUnderDirectory(gcRepoRoot, root)) {
27241
28053
  return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
@@ -27300,7 +28112,7 @@ async function primaryCheckoutRoot(from) {
27300
28112
  return primaryCheckoutRootOf(async (args) => (await execFileP2("git", ["-C", from, ...args], { timeout: GIT_TIMEOUT_MS })).stdout);
27301
28113
  }
27302
28114
  async function unprovenWorktreeReason(wtPath, repoRoot2) {
27303
- if (!(0, import_node_fs33.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
28115
+ if (!(0, import_node_fs34.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
27304
28116
  const porcelain = (await execFileP2("git", ["-C", repoRoot2, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
27305
28117
  const registered = parseWorktreePorcelainEntries(porcelain);
27306
28118
  if (!registered.length) {
@@ -27321,26 +28133,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
27321
28133
  function acquireWorktreeSetupLock(worktreeRoot) {
27322
28134
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
27323
28135
  const take = () => {
27324
- const fd = (0, import_node_fs33.openSync)(lockPath, "wx");
28136
+ const fd = (0, import_node_fs34.openSync)(lockPath, "wx");
27325
28137
  try {
27326
- (0, import_node_fs33.writeSync)(fd, String(Date.now()));
28138
+ (0, import_node_fs34.writeSync)(fd, String(Date.now()));
27327
28139
  } finally {
27328
- (0, import_node_fs33.closeSync)(fd);
28140
+ (0, import_node_fs34.closeSync)(fd);
27329
28141
  }
27330
28142
  return () => {
27331
28143
  try {
27332
- (0, import_node_fs33.rmSync)(lockPath, { force: true });
28144
+ (0, import_node_fs34.rmSync)(lockPath, { force: true });
27333
28145
  } catch {
27334
28146
  }
27335
28147
  };
27336
28148
  };
27337
28149
  try {
27338
- (0, import_node_fs33.mkdirSync)((0, import_node_path31.dirname)(lockPath), { recursive: true });
28150
+ (0, import_node_fs34.mkdirSync)((0, import_node_path33.dirname)(lockPath), { recursive: true });
27339
28151
  return take();
27340
28152
  } catch {
27341
28153
  try {
27342
- if (Date.now() - (0, import_node_fs33.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
27343
- (0, import_node_fs33.rmSync)(lockPath, { force: true });
28154
+ if (Date.now() - (0, import_node_fs34.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
28155
+ (0, import_node_fs34.rmSync)(lockPath, { force: true });
27344
28156
  return take();
27345
28157
  }
27346
28158
  } catch {
@@ -27376,11 +28188,25 @@ withExamples(mutating(
27376
28188
  }
27377
28189
  const repoRoot2 = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
27378
28190
  const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
27379
- const { base, fetchBranch } = resolveWorktreeBase(fromRef, o.remote);
28191
+ const { base: fallbackBase, fetchBranch, preferRemote } = resolveWorktreeBase(fromRef, o.remote);
28192
+ let base = fallbackBase;
27380
28193
  step = `fetch the base ref ${fromRef}`;
27381
28194
  if (fetchBranch) {
27382
28195
  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`);
28196
+ if (fetchErr && !preferRemote) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
28197
+ }
28198
+ const revParseRef = async (ref) => {
28199
+ try {
28200
+ return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
28201
+ } catch {
28202
+ return void 0;
28203
+ }
28204
+ };
28205
+ if (preferRemote && await revParseRef(preferRemote)) base = preferRemote;
28206
+ if (!o.json) {
28207
+ const baseSha = (await revParseRef(base))?.slice(0, 12) ?? "unresolved";
28208
+ const localOnly = preferRemote && base !== preferRemote ? ` (no ${preferRemote} \u2014 local ref)` : "";
28209
+ console.error(` base ${base} ${baseSha}${localOnly}`);
27384
28210
  }
27385
28211
  step = `git worktree add ${wtPath}`;
27386
28212
  await addWorktreeRobust(wtPath, branch, base, {
@@ -27642,12 +28468,27 @@ docsAudit.command("record").description("write a dated janitor verdict for one r
27642
28468
  await failGraceful(e.message);
27643
28469
  }
27644
28470
  });
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) => {
28471
+ 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
28472
  try {
27647
28473
  const repo = o.repo ?? await currentRepoFullName();
27648
28474
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
27649
- const result = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
27650
- console.log(result.line);
28475
+ const fetched = await readDocsAuditFetch(repo);
28476
+ const result = docsAuditStatus(fetched, { repo, today });
28477
+ if (o.json) {
28478
+ const verdict = "ok" in fetched && fetched.ok ? fetched.verdict : null;
28479
+ console.log(JSON.stringify({
28480
+ repo,
28481
+ armed: !("notArmed" in fetched),
28482
+ ok: result.ok,
28483
+ state: result.state,
28484
+ date: verdict?.date ?? null,
28485
+ outcome: verdict?.outcome ?? null,
28486
+ checkerVendor: verdict?.checkerVendor ?? null,
28487
+ line: result.line
28488
+ }, null, 2));
28489
+ } else {
28490
+ console.log(result.line);
28491
+ }
27651
28492
  if (!result.ok) process.exitCode = 1;
27652
28493
  } catch (e) {
27653
28494
  await failGraceful(e.message);
@@ -27663,17 +28504,25 @@ async function reportWrite(label, res) {
27663
28504
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
27664
28505
  }
27665
28506
  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) => {
28507
+ 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
28508
  try {
27668
- const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch });
28509
+ let lines;
28510
+ if (o.lines !== void 0) {
28511
+ if (action !== "logs") return fail("runtime tenant control: --lines is valid only for the logs action");
28512
+ lines = Number(o.lines);
28513
+ if (!Number.isInteger(lines) || lines < 1 || lines > 2e3) return fail("runtime tenant control: --lines must be an integer between 1 and 2000");
28514
+ }
28515
+ const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines });
27669
28516
  if (!o.json && action === "verify-secrets" && result.secrets) {
27670
28517
  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);
28518
+ const { lines: lines2, failure } = renderVerifySecrets(body);
28519
+ for (const line of lines2) printLine(line);
27673
28520
  if (failure) return failGraceful(`runtime tenant control ${stage} verify-secrets: ${failure}`);
28521
+ } else if (!o.json && action === "logs" && result.logs) {
28522
+ printLine(result.logs);
27674
28523
  } 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);
28524
+ const { lines: lines2, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
28525
+ for (const line of lines2) printLine(line);
27677
28526
  if (failure) return failGraceful(`runtime tenant control ${stage} verify-broker: ${failure}`);
27678
28527
  } else {
27679
28528
  printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
@@ -27769,6 +28618,34 @@ var project = program2.command("project").description("the DDB org registry \u20
27769
28618
  async function projectTarget(commandName, explicitTarget) {
27770
28619
  return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
27771
28620
  }
28621
+ async function runProjectInfoSync(target, apply) {
28622
+ const currentRepo = await resolveRepo();
28623
+ if (!currentRepo) throw new Error("org project sync-info: run from the repository whose Project information is being synchronized");
28624
+ if (target.toLowerCase() !== currentRepo.toLowerCase() && slugOf(target) !== slugOf(currentRepo)) {
28625
+ throw new Error(`org project sync-info: ${target} is not the current checkout (${currentRepo}); run from the target repository`);
28626
+ }
28627
+ const targetRepo2 = currentRepo;
28628
+ const cfg = await loadConfig();
28629
+ const registry2 = registryClientDeps(cfg);
28630
+ const [read, projects] = await Promise.all([
28631
+ fetchProjectBySlugChecked(slugOf(targetRepo2), registry2),
28632
+ fetchProjectsList(registry2)
28633
+ ]);
28634
+ if (!read.ok) throw new Error(`org project sync-info: Hub registry read failed (${read.error})`);
28635
+ if (!read.project) throw new Error(`org project sync-info: no registry META for ${targetRepo2}`);
28636
+ if (!projects) throw new Error("org project sync-info: Hub project list unavailable");
28637
+ if (apply) {
28638
+ const authority = await fetchTrainAuthority(targetRepo2, registry2);
28639
+ if (!authority.ok) throw new Error(`org project sync-info: train authority unverified (${authority.error})`);
28640
+ if (!authority.authority.train) {
28641
+ throw new Error(`org project sync-info: ${authority.authority.login} has no train authority for ${targetRepo2}`);
28642
+ }
28643
+ }
28644
+ const repoRoot2 = await gitOut(["rev-parse", "--show-toplevel"]);
28645
+ if (!repoRoot2) throw new Error("org project sync-info: cannot resolve the current repository root");
28646
+ const plan = buildProjectInfoSyncPlan(targetRepo2, read.project, projects, repoRoot2);
28647
+ return syncProjectInfo(plan, defaultGitHubClient(), apply);
28648
+ }
27772
28649
  project.command("list").description("list all projects (identity + board, never deploy coords)").option("--json", "machine-readable output").action(async (o) => {
27773
28650
  const cfg = await loadConfig();
27774
28651
  const projects = await fetchProjectsList(registryClientDeps(cfg));
@@ -27809,6 +28686,18 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
27809
28686
  );
27810
28687
  }
27811
28688
  });
28689
+ project.command("sync-info [owner/repo]").description("synchronize the owning GitHub Project's short description + thin README from this repo's README and the registry's current member repos; dry-run by default").option("--apply", "write the synchronized Project information (train-authority gated)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
28690
+ let target;
28691
+ try {
28692
+ target = await projectTarget("org project sync-info", repoOrSlug);
28693
+ const result = await runProjectInfoSync(target, Boolean(o.apply));
28694
+ if (o.json) return printLine(JSON.stringify(result, null, 2));
28695
+ printLine(`org project sync-info: ${result.note}`);
28696
+ printLine(` members: ${result.memberRepos.join(", ")}`);
28697
+ } catch (e) {
28698
+ return failGraceful(e.message);
28699
+ }
28700
+ });
27812
28701
  var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
27813
28702
  projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
27814
28703
  const cfg = await loadConfig();
@@ -27841,7 +28730,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
27841
28730
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
27842
28731
  if (o.secretsFile) {
27843
28732
  try {
27844
- vars.push(`secrets=${(0, import_node_fs33.readFileSync)(o.secretsFile, "utf8")}`);
28733
+ vars.push(`secrets=${(0, import_node_fs34.readFileSync)(o.secretsFile, "utf8")}`);
27845
28734
  } catch (e) {
27846
28735
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
27847
28736
  }
@@ -28138,16 +29027,43 @@ function resolveCreateType(raw, command, labels) {
28138
29027
  }
28139
29028
  return raw;
28140
29029
  }
29030
+ function resolveCreateSurface(opts) {
29031
+ return typeof opts.surface === "string" && opts.surface.trim() ? surfaceLabel(opts.surface) : void 0;
29032
+ }
29033
+ function surfaceWaived() {
29034
+ return rawFlag("--no-surface");
29035
+ }
28141
29036
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
28142
29037
  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"),
29038
+ 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
29039
  // --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
28145
29040
  // ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
28146
29041
  // (`--parent`) and title-source are validated by the action on a real run.
28147
- (opts) => {
29042
+ async (opts) => {
28148
29043
  const type = resolveCreateType(opts.type, "issue create", opts.label);
28149
29044
  const priority = resolveCreatePriority(opts.priority, "issue create");
28150
- return { command: "issue create", type, title: opts.title ?? opts.titleFile, priority, repo: opts.repo };
29045
+ const planLabels = opts.label ?? [];
29046
+ const clash = conflictingSurfaceInputs(opts.surface, planLabels);
29047
+ if (clash) fail(clash.message, clash.payload);
29048
+ const planRepo = opts.batch || surfaceWaived() ? void 0 : await resolveRepo(opts.repo);
29049
+ const surface = resolveCreateSurface(opts);
29050
+ if (planRepo) {
29051
+ const { refusal, warn } = await checkSurfaceRequirement({
29052
+ repo: planRepo,
29053
+ labels: [...planLabels, ...surface ? [surface] : []]
29054
+ });
29055
+ if (warn) process.stderr.write(`${warn}
29056
+ `);
29057
+ if (refusal) fail(refusal.message, refusal.payload);
29058
+ }
29059
+ return {
29060
+ command: "issue create",
29061
+ type,
29062
+ title: opts.title ?? opts.titleFile,
29063
+ priority,
29064
+ repo: opts.repo,
29065
+ ...surface ? { surface } : {}
29066
+ };
28151
29067
  }
28152
29068
  ).action(async (o) => {
28153
29069
  let args;
@@ -28157,6 +29073,7 @@ withExamples(mutating(
28157
29073
  let issueType;
28158
29074
  let extraLabels = [];
28159
29075
  let targetRepo2;
29076
+ let surfaceFlagLabel;
28160
29077
  try {
28161
29078
  issueType = resolveCreateType(o.type, "issue create", o.label);
28162
29079
  title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises10.readFile, readStdin });
@@ -28164,6 +29081,13 @@ withExamples(mutating(
28164
29081
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
28165
29082
  priority = resolveCreatePriority(o.priority, "issue create");
28166
29083
  extraLabels = [...o.label ?? []];
29084
+ const clash = conflictingSurfaceInputs(typeof o.surface === "string" ? o.surface : void 0, extraLabels);
29085
+ if (clash) return fail(clash.message, clash.payload);
29086
+ const surfaceFromFlag = resolveCreateSurface(o);
29087
+ if (surfaceFromFlag && !labelsCarrySurface(extraLabels)) {
29088
+ extraLabels.push(surfaceFromFlag);
29089
+ surfaceFlagLabel = surfaceFromFlag;
29090
+ }
28167
29091
  targetRepo2 = await resolveRepo(o.repo);
28168
29092
  if (!targetRepo2) {
28169
29093
  return fail("issue create: could not resolve the target repo \u2014 run inside a git checkout or pass --repo <owner/repo>");
@@ -28180,6 +29104,27 @@ withExamples(mutating(
28180
29104
  } catch (e) {
28181
29105
  return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
28182
29106
  }
29107
+ {
29108
+ const { refusal, warn, enforcing } = await checkSurfaceRequirement({ repo: targetRepo2, labels: extraLabels });
29109
+ if (warn) process.stderr.write(`${warn}
29110
+ `);
29111
+ if (!enforcing && surfaceFlagLabel) {
29112
+ extraLabels = extraLabels.filter((l) => l !== surfaceFlagLabel);
29113
+ args = buildIssueArgs({
29114
+ type: issueType,
29115
+ title,
29116
+ body,
29117
+ priority,
29118
+ repo: targetRepo2,
29119
+ labels: extraLabels.length ? extraLabels : void 0
29120
+ });
29121
+ process.stderr.write(
29122
+ `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.
29123
+ `
29124
+ );
29125
+ }
29126
+ if (refusal && !surfaceWaived()) return fail(refusal.message, refusal.payload);
29127
+ }
28183
29128
  await ensureLabelsExist(extraLabels, targetRepo2);
28184
29129
  const created = await ghCreate(args);
28185
29130
  const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo2, priority);
@@ -28289,6 +29234,15 @@ jsonParity(issue.command("link-child <parent> <child>").description("link an exi
28289
29234
  const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
28290
29235
  console.log(JSON.stringify(result));
28291
29236
  } catch (e) {
29237
+ let conflict;
29238
+ try {
29239
+ const child2 = parseIssueRef(childRef);
29240
+ const childRepo = child2.repo ?? defaultRepo;
29241
+ if (childRepo) conflict = await classifyReparentFailure(e, ghRunner2, childRepo, child2.number, parentRef);
29242
+ } catch {
29243
+ conflict = void 0;
29244
+ }
29245
+ if (conflict) return fail(`issue link-child: ${conflict.message}`, conflict.payload);
28292
29246
  const err = e;
28293
29247
  const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
28294
29248
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
@@ -28380,6 +29334,9 @@ program2.command("report").description("file a friction report on the Hub board
28380
29334
  }
28381
29335
  const cfg = await loadConfig();
28382
29336
  if (!cfg.sagaApiUrl) return fail("report: Hub API URL not configured");
29337
+ const { warn: surfaceWarn } = await preflightReportSurface();
29338
+ if (surfaceWarn) process.stderr.write(`${surfaceWarn}
29339
+ `);
28383
29340
  const result = await fileReport(
28384
29341
  {
28385
29342
  apiUrl: cfg.sagaApiUrl,
@@ -28452,6 +29409,14 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
28452
29409
  return console.log(JSON.stringify({ deduped: true, number: dup.number, url: dup.url, score: dup.score }));
28453
29410
  }
28454
29411
  }
29412
+ const { warn: surfaceWarn } = await checkSurfaceRequirement({
29413
+ repo: targetRepo2,
29414
+ labels: [SKILL_LESSON_LABEL],
29415
+ command: "skill-lesson",
29416
+ waiver: { reason: "tooling lesson spans product surfaces \u2014 coop-proof class (#3789)" }
29417
+ });
29418
+ if (surfaceWarn) process.stderr.write(`${surfaceWarn}
29419
+ `);
28455
29420
  try {
28456
29421
  await execFileP2("gh", ["label", "create", SKILL_LESSON_LABEL, "--color", "c2e0c6", "--repo", targetRepo2], { timeout: GH_MUTATION_TIMEOUT_MS });
28457
29422
  } catch {
@@ -28501,11 +29466,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
28501
29466
  }
28502
29467
  });
28503
29468
  async function listCiWorkflowPaths(cwd = process.cwd()) {
28504
- const wfDir = (0, import_node_path31.join)(cwd, ".github", "workflows");
28505
- if (!(0, import_node_fs33.existsSync)(wfDir)) return [];
28506
- return (0, import_node_fs33.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
29469
+ const wfDir = (0, import_node_path33.join)(cwd, ".github", "workflows");
29470
+ if (!(0, import_node_fs34.existsSync)(wfDir)) return [];
29471
+ return (0, import_node_fs34.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
28507
29472
  try {
28508
- return workflowReportsPrChecks((0, import_node_fs33.readFileSync)((0, import_node_path31.join)(wfDir, name), "utf8"));
29473
+ return workflowReportsPrChecks((0, import_node_fs34.readFileSync)((0, import_node_path33.join)(wfDir, name), "utf8"));
28509
29474
  } catch {
28510
29475
  return true;
28511
29476
  }
@@ -28537,16 +29502,16 @@ function ciAuditDeps() {
28537
29502
  // gate re-seed step is skipped gracefully rather than failing mid-run.
28538
29503
  readSeedFile: (path2) => {
28539
29504
  if (!root) return null;
28540
- const fullPath = (0, import_node_path31.join)(root, path2);
28541
- return (0, import_node_fs33.existsSync)(fullPath) ? (0, import_node_fs33.readFileSync)(fullPath, "utf8") : null;
29505
+ const fullPath = (0, import_node_path33.join)(root, path2);
29506
+ return (0, import_node_fs34.existsSync)(fullPath) ? (0, import_node_fs34.readFileSync)(fullPath, "utf8") : null;
28542
29507
  }
28543
29508
  };
28544
29509
  }
28545
29510
  function hubRoot() {
28546
- const fromPkg = (0, import_node_path31.join)(__dirname, "..", "..");
29511
+ const fromPkg = (0, import_node_path33.join)(__dirname, "..", "..");
28547
29512
  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();
29513
+ if ((0, import_node_fs34.existsSync)((0, import_node_path33.join)(fromPkg, marker))) return fromPkg;
29514
+ if ((0, import_node_fs34.existsSync)((0, import_node_path33.join)(process.cwd(), marker))) return process.cwd();
28550
29515
  return null;
28551
29516
  }
28552
29517
  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) => {
@@ -28849,7 +29814,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
28849
29814
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
28850
29815
  beforeWorktrees,
28851
29816
  startingPath,
28852
- pathExists: (p) => (0, import_node_fs33.existsSync)(p),
29817
+ pathExists: (p) => (0, import_node_fs34.existsSync)(p),
28853
29818
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
28854
29819
  teardownWorktreeStage,
28855
29820
  deferredStore,
@@ -28934,8 +29899,8 @@ function trainApplyDeps() {
28934
29899
  // Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
28935
29900
  // workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
28936
29901
  // 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()));
29902
+ dispatchTenantControl: async ({ repo, stage, action, lines }) => {
29903
+ const res = await tenantControl({ repo, stage, action, ...lines != null ? { lines } : {} }, registryClientDeps(await loadConfig()));
28939
29904
  if (res.ok) return { ok: true };
28940
29905
  const body = res.body;
28941
29906
  return { ok: false, category: body?.category, error: body?.error ?? res.error };
@@ -28983,9 +29948,19 @@ function renderDeployLine(d) {
28983
29948
  if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
28984
29949
  else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
28985
29950
  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)`);
29951
+ 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
29952
  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
29953
  return parts.join("; ");
28988
29954
  }
29955
+ function renderReleaseResume(r) {
29956
+ const lines = [`mmi-cli release --resume: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}] \u2014 ${r.note}`];
29957
+ for (const step of r.steps) lines.push(` - ${step}`);
29958
+ if (r.releaseUrl) lines.push(` release: ${r.releaseUrl}`);
29959
+ if (r.announceNote) lines.push(` announce: ${r.announceNote}`);
29960
+ if (r.dispatch) lines.push(` deploy: ${r.dispatch.note}`);
29961
+ if (r.devRollForward) lines.push(` development: ${r.devRollForward.note}`);
29962
+ return lines.join("\n");
29963
+ }
28989
29964
  function renderTrainApply(commandName, r) {
28990
29965
  let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
28991
29966
  if (r.versionFold) base = `${base}; ${r.versionFold}`;
@@ -29005,6 +29980,9 @@ function renderTrainApply(commandName, r) {
29005
29980
  if (r.checkout) {
29006
29981
  base = `${base}; checkout: ${r.checkout.note}`;
29007
29982
  }
29983
+ if (r.projectInfoSync) {
29984
+ base = `${base}; project info: ${r.projectInfoSync.note}`;
29985
+ }
29008
29986
  return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
29009
29987
  }
29010
29988
  function renderTenantRedeploy(r) {
@@ -29023,7 +30001,12 @@ for (const commandName of ["rcand", "release"]) {
29023
30001
  const RELEASE_ONLY_FLAGS = [
29024
30002
  { flags: "--announce-summary-file <path>", description: "agent-curated summary lines for the Hub Slack announcement (#883)" },
29025
30003
  { 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)" }
30004
+ { 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)" },
30005
+ // #3851: finish a release that pushed its tag and then stopped. NOT a rerun — a rerun would cut the
30006
+ // NEXT version, because the cycle resolver advances past the pushed tag and MMI_RELEASE_VERSION
30007
+ // refuses a version that is not ahead of the latest. This is the only path allowed to target an
30008
+ // existing tag, and it proves the release is genuinely partial before writing anything.
30009
+ { 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
30010
  ];
29028
30011
  for (const f of RELEASE_ONLY_FLAGS) {
29029
30012
  if (commandName === "release") {
@@ -29047,6 +30030,18 @@ for (const commandName of ["rcand", "release"]) {
29047
30030
  if (o.announceSummaryFile && commandName !== "release") {
29048
30031
  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
30032
  }
30033
+ if (o.resume && commandName !== "release") {
30034
+ return fail(`${commandName}: --resume applies only to release \u2014 it finishes a partially-released main. Run: mmi-cli release --resume`);
30035
+ }
30036
+ if (o.resume) {
30037
+ 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");
30038
+ try {
30039
+ const result = await runReleaseResume(trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile });
30040
+ return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleaseResume(result));
30041
+ } catch (e) {
30042
+ return failGraceful(`release --resume: ${e.message}`);
30043
+ }
30044
+ }
29050
30045
  if (o.apply && o.repo) {
29051
30046
  const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
29052
30047
  const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
@@ -29056,7 +30051,19 @@ for (const commandName of ["rcand", "release"]) {
29056
30051
  try {
29057
30052
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
29058
30053
  const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev });
29059
- return printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainApply(commandName, result));
30054
+ let projectInfoSync;
30055
+ if (commandName === "release") {
30056
+ try {
30057
+ projectInfoSync = await runProjectInfoSync(result.repo, true);
30058
+ } catch (e) {
30059
+ const error = e.message;
30060
+ projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
30061
+ }
30062
+ }
30063
+ const reported = { ...result, ...projectInfoSync ? { projectInfoSync } : {} };
30064
+ printLine(o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported));
30065
+ if (projectInfoSync && "error" in projectInfoSync) process.exitCode = 1;
30066
+ return;
29060
30067
  } catch (e) {
29061
30068
  return failGraceful(`${commandName}: ${e.message}`);
29062
30069
  }
@@ -29209,12 +30216,12 @@ access.command("audit").description("audit collaborator roles + train-branch pus
29209
30216
  targets = resolution.targets;
29210
30217
  }
29211
30218
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
29212
- const fileMatrix = (0, import_node_fs33.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs33.readFileSync)("access-matrix.json", "utf8")) : {};
30219
+ const fileMatrix = (0, import_node_fs34.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs34.readFileSync)("access-matrix.json", "utf8")) : {};
29213
30220
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
29214
30221
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
29215
- const fileContracts = (0, import_node_fs33.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs33.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
30222
+ const fileContracts = (0, import_node_fs34.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs34.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
29216
30223
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
29217
- const sanctioned = (0, import_node_fs33.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs33.readFileSync)("access-matrix.json", "utf8")) : {};
30224
+ const sanctioned = (0, import_node_fs34.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs34.readFileSync)("access-matrix.json", "utf8")) : {};
29218
30225
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
29219
30226
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
29220
30227
  if (!report.ok) process.exitCode = 1;
@@ -29247,16 +30254,16 @@ function directoryBytes(path2) {
29247
30254
  let total = 0;
29248
30255
  let entries;
29249
30256
  try {
29250
- entries = (0, import_node_fs33.readdirSync)(path2, { withFileTypes: true });
30257
+ entries = (0, import_node_fs34.readdirSync)(path2, { withFileTypes: true });
29251
30258
  } catch {
29252
30259
  return 0;
29253
30260
  }
29254
30261
  for (const entry of entries) {
29255
- const child2 = (0, import_node_path31.join)(path2, entry.name);
30262
+ const child2 = (0, import_node_path33.join)(path2, entry.name);
29256
30263
  if (entry.isDirectory()) total += directoryBytes(child2);
29257
30264
  else {
29258
30265
  try {
29259
- total += (0, import_node_fs33.statSync)(child2).size;
30266
+ total += (0, import_node_fs34.statSync)(child2).size;
29260
30267
  } catch {
29261
30268
  }
29262
30269
  }
@@ -29264,25 +30271,25 @@ function directoryBytes(path2) {
29264
30271
  return total;
29265
30272
  }
29266
30273
  function listDirEntries(dir) {
29267
- return (0, import_node_fs33.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
30274
+ return (0, import_node_fs34.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
29268
30275
  }
29269
30276
  function readInstalledPluginRefs(configRoot) {
29270
30277
  const p = installedPluginsPathForConfig(configRoot);
29271
- if (!(0, import_node_fs33.existsSync)(p)) return [];
30278
+ if (!(0, import_node_fs34.existsSync)(p)) return [];
29272
30279
  try {
29273
- return installedPluginPaths((0, import_node_fs33.readFileSync)(p, "utf8"));
30280
+ return installedPluginPaths((0, import_node_fs34.readFileSync)(p, "utf8"));
29274
30281
  } catch {
29275
30282
  return null;
29276
30283
  }
29277
30284
  }
29278
30285
  function pluginCacheFsDeps(configRoot, dirBytes) {
29279
30286
  return {
29280
- exists: (p) => (0, import_node_fs33.existsSync)(p),
29281
- listVersionDirs: (root) => (0, import_node_fs33.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
30287
+ exists: (p) => (0, import_node_fs34.existsSync)(p),
30288
+ listVersionDirs: (root) => (0, import_node_fs34.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
29282
30289
  dirBytes,
29283
- listStagingDirs: (root) => (0, import_node_fs33.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
30290
+ listStagingDirs: (root) => (0, import_node_fs34.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
29284
30291
  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) };
30292
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path33.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs34.statSync)(p).mtimeMs) };
29286
30293
  } catch {
29287
30294
  return { name: d.name, mtimeMs: Date.now() };
29288
30295
  }
@@ -29296,10 +30303,10 @@ function stagingApplyFsGuard(configRoot) {
29296
30303
  return {
29297
30304
  referencedPaths: () => readInstalledPluginRefs(configRoot),
29298
30305
  mtimeMs: (name) => {
29299
- const p = (0, import_node_path31.join)(stagingRoot, name);
29300
- if (!(0, import_node_fs33.existsSync)(p)) return null;
30306
+ const p = (0, import_node_path33.join)(stagingRoot, name);
30307
+ if (!(0, import_node_fs34.existsSync)(p)) return null;
29301
30308
  try {
29302
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs33.statSync)(q).mtimeMs);
30309
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs34.statSync)(q).mtimeMs);
29303
30310
  } catch {
29304
30311
  return null;
29305
30312
  }
@@ -29319,13 +30326,13 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
29319
30326
  return;
29320
30327
  }
29321
30328
  const plan = buildPluginCachePlan(
29322
- (0, import_node_os12.homedir)(),
30329
+ (0, import_node_os13.homedir)(),
29323
30330
  running,
29324
30331
  pluginCacheFsDeps(configRoot, directoryBytes),
29325
30332
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }
29326
30333
  );
29327
30334
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
29328
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs33.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
30335
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs34.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
29329
30336
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
29330
30337
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
29331
30338
  else console.log(renderPluginCachePlan(plan, result));