@mutmutco/cli 2.68.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -17
  2. package/dist/main.cjs +465 -378
  3. package/package.json +3 -3
package/dist/main.cjs CHANGED
@@ -3407,42 +3407,104 @@ function useColor() {
3407
3407
  var program = new Command();
3408
3408
 
3409
3409
  // src/index.ts
3410
- var import_promises4 = require("node:fs/promises");
3410
+ var import_promises3 = require("node:fs/promises");
3411
3411
  var import_node_fs18 = require("node:fs");
3412
3412
 
3413
- // src/rules-sync.ts
3414
- function normalizeEol(s) {
3415
- return s.replace(/\r\n/g, "\n");
3416
- }
3417
- function needsUpdate(source, current) {
3418
- if (current === null) return true;
3419
- return normalizeEol(source) !== normalizeEol(current);
3413
+ // src/bootstrap-org-ruleset.ts
3414
+ var ORG_NO_AGENT_FILES_RULESET_NAME = "mmi-no-agent-files-org";
3415
+ var ORG_LOGIN = "mutmutco";
3416
+ var ORG_NO_AGENT_FILES_EXCLUDED_REPOS = ["Jerv-PowerTools"];
3417
+ var NO_AGENT_FILES_GLOBS = [
3418
+ "AGENTS.md",
3419
+ "**/AGENTS.md",
3420
+ "CLAUDE.md",
3421
+ "**/CLAUDE.md",
3422
+ "GEMINI.md",
3423
+ "**/GEMINI.md",
3424
+ ".claude/**",
3425
+ "**/.claude/**",
3426
+ ".cursor/rules/**",
3427
+ "**/.cursor/rules/**",
3428
+ ".codex/**",
3429
+ "**/.codex/**",
3430
+ ".agents/**",
3431
+ "**/.agents/**"
3432
+ ];
3433
+ function orgNoAgentFilesRulesetDesired() {
3434
+ return {
3435
+ name: ORG_NO_AGENT_FILES_RULESET_NAME,
3436
+ target: "push",
3437
+ enforcement: "active",
3438
+ bypass_actors: [],
3439
+ conditions: { repository_name: { include: ["~ALL"], exclude: [...ORG_NO_AGENT_FILES_EXCLUDED_REPOS] } },
3440
+ rules: [{ type: "file_path_restriction", parameters: { restricted_file_paths: [...NO_AGENT_FILES_GLOBS] } }]
3441
+ };
3420
3442
  }
3421
- function isRulesSource(orgRulesSource) {
3422
- return orgRulesSource === "self";
3443
+ function sameSet(a, b) {
3444
+ const A = new Set(a ?? []);
3445
+ const B = new Set(b ?? []);
3446
+ if (A.size !== B.size) return false;
3447
+ for (const v of A) if (!B.has(v)) return false;
3448
+ return true;
3423
3449
  }
3424
- function rulesSourceAuthHeaders(sourceUrl, token) {
3425
- if (!token) return void 0;
3426
- try {
3427
- const host = new URL(sourceUrl).hostname.toLowerCase();
3428
- if (host === "raw.githubusercontent.com" || host === "api.github.com") {
3429
- return { Authorization: `Bearer ${token}` };
3430
- }
3431
- } catch {
3432
- return void 0;
3433
- }
3434
- return void 0;
3450
+ function diffOrgRuleset(live, desired = orgNoAgentFilesRulesetDesired()) {
3451
+ const drift = [];
3452
+ if (!live) return [`ruleset '${desired.name}' is absent`];
3453
+ if (live.name !== desired.name) drift.push(`name: '${live.name}' != '${desired.name}'`);
3454
+ if (live.target !== desired.target) drift.push(`target: '${live.target}' != '${desired.target}'`);
3455
+ if (live.enforcement !== desired.enforcement) drift.push(`enforcement: '${live.enforcement}' != '${desired.enforcement}'`);
3456
+ const bypass = Array.isArray(live.bypass_actors) ? live.bypass_actors : [];
3457
+ if (bypass.length !== 0) drift.push(`bypass_actors: ${bypass.length} actor(s) present \u2014 must be empty (no bypass)`);
3458
+ const include = live.conditions?.repository_name?.include;
3459
+ const exclude = live.conditions?.repository_name?.exclude;
3460
+ if (!sameSet(include, desired.conditions.repository_name.include)) {
3461
+ drift.push(`conditions.include: [${(include ?? []).join(", ")}] != [${desired.conditions.repository_name.include.join(", ")}]`);
3462
+ }
3463
+ if (!sameSet(exclude, desired.conditions.repository_name.exclude)) {
3464
+ drift.push(`conditions.exclude: [${(exclude ?? []).join(", ")}] != [${desired.conditions.repository_name.exclude.join(", ")}]`);
3465
+ }
3466
+ const rules2 = Array.isArray(live.rules) ? live.rules : [];
3467
+ const fpr = rules2.filter((r) => r?.type === "file_path_restriction");
3468
+ if (fpr.length !== 1) {
3469
+ drift.push(`rules: expected exactly 1 file_path_restriction, found ${fpr.length}`);
3470
+ } else if (!sameSet(fpr[0]?.parameters?.restricted_file_paths, desired.rules[0].parameters.restricted_file_paths)) {
3471
+ const got = fpr[0]?.parameters?.restricted_file_paths ?? [];
3472
+ drift.push(`restricted_file_paths: ${got.length} glob(s) differ from the codified ${desired.rules[0].parameters.restricted_file_paths.length}`);
3473
+ }
3474
+ return drift;
3475
+ }
3476
+ function planOrgNoAgentFilesRuleset(live) {
3477
+ const desired = orgNoAgentFilesRulesetDesired();
3478
+ if (!live) return { action: "create", drift: [`ruleset '${desired.name}' is absent`], desired };
3479
+ const drift = diffOrgRuleset(live, desired);
3480
+ return { action: drift.length === 0 ? "noop" : "update", drift, desired, id: live.id };
3481
+ }
3482
+ function renderOrgRulesetDriftReport(plan) {
3483
+ const head = `mmi-cli bootstrap org-ruleset: ${ORG_NO_AGENT_FILES_RULESET_NAME} (org ${ORG_LOGIN})`;
3484
+ if (plan.action === "noop") {
3485
+ return `${head}
3486
+ OK \u2014 live ruleset matches the codified IaC (${NO_AGENT_FILES_GLOBS.length} globs, include ~ALL, empty bypass, exclude ${ORG_NO_AGENT_FILES_EXCLUDED_REPOS.join(", ")}).`;
3487
+ }
3488
+ const verb = plan.action === "create" ? "MISSING \u2014 codified IaC would CREATE it" : "DRIFT \u2014 live ruleset diverges from the codified IaC";
3489
+ return [
3490
+ `${head}`,
3491
+ verb,
3492
+ ...plan.drift.map((d) => ` - ${d}`),
3493
+ "This command is read-only; it never mutates the live ruleset. Recreate/repair via the GitHub UI or a",
3494
+ "deliberate POST/PUT of orgNoAgentFilesRulesetDesired() \u2014 see cli/src/bootstrap-org-ruleset.ts."
3495
+ ].join("\n");
3435
3496
  }
3436
- function resolveRulesBase(orgRulesSource, defaultBase) {
3437
- const isUrl = typeof orgRulesSource === "string" && /^https?:\/\//i.test(orgRulesSource);
3438
- return (isUrl ? orgRulesSource : defaultBase).replace(/\/$/, "");
3497
+ async function fetchOrgNoAgentFilesRuleset(client, org = ORG_LOGIN) {
3498
+ const list = await client.rest("GET", `orgs/${org}/rulesets`, { timeoutMs: 2e4 });
3499
+ const summary = (list ?? []).find((r) => r?.name === ORG_NO_AGENT_FILES_RULESET_NAME);
3500
+ if (summary?.id == null) return null;
3501
+ return await client.rest("GET", `orgs/${org}/rulesets/${summary.id}`, { timeoutMs: 2e4 }) ?? null;
3439
3502
  }
3440
3503
 
3441
3504
  // src/index.ts
3442
3505
  var import_node_child_process11 = require("node:child_process");
3443
3506
 
3444
3507
  // src/cli-shared.ts
3445
- var import_promises = require("node:fs/promises");
3446
3508
  var import_node_child_process3 = require("node:child_process");
3447
3509
  var import_node_util4 = require("node:util");
3448
3510
 
@@ -3887,16 +3949,8 @@ async function hubHeaders(extra = {}) {
3887
3949
  const base = { ...clientVersionHeaders(), ...extra };
3888
3950
  return t ? { ...base, Authorization: `Bearer ${t}` } : base;
3889
3951
  }
3890
- var CONFIG_FILE = ".mmi/config.json";
3891
3952
  async function loadConfig() {
3892
- let file = {};
3893
- try {
3894
- file = JSON.parse(await (0, import_promises.readFile)(CONFIG_FILE, "utf8"));
3895
- } catch {
3896
- file = {};
3897
- }
3898
- if (!file.sagaApiUrl) file.sagaApiUrl = defaultHubUrl();
3899
- return file;
3953
+ return { sagaApiUrl: defaultHubUrl() };
3900
3954
  }
3901
3955
  async function originRemoteUrl() {
3902
3956
  return gitOut(["config", "--get", "remote.origin.url"]);
@@ -3922,29 +3976,8 @@ function fail(msg) {
3922
3976
  hardExit(1);
3923
3977
  }
3924
3978
  function installProcessBackstop() {
3925
- process.on("unhandledRejection", (reason) => fail(reason instanceof Error ? reason.message : String(reason)));
3926
- process.on("uncaughtException", (err) => fail(err instanceof Error ? err.message : String(err)));
3927
- }
3928
-
3929
- // src/docs-sync.ts
3930
- var SYNCED_DOCS = ["README.md", "architecture.md"];
3931
- async function syncDocs(deps, docs2 = SYNCED_DOCS) {
3932
- const updated = [];
3933
- const skippedDirty = [];
3934
- for (const file of docs2) {
3935
- if (await deps.isDirty(file)) {
3936
- skippedDirty.push(file);
3937
- continue;
3938
- }
3939
- const origin = await deps.originContent(file);
3940
- if (origin === null) continue;
3941
- const local = await deps.localContent(file);
3942
- if (needsUpdate(origin, local)) {
3943
- await deps.writeDoc(file, normalizeEol(origin));
3944
- updated.push(file);
3945
- }
3946
- }
3947
- return { updated, skippedDirty };
3979
+ process.on("unhandledRejection", (reason) => void failGraceful(reason instanceof Error ? reason.message : String(reason)));
3980
+ process.on("uncaughtException", (err) => void failGraceful(err instanceof Error ? err.message : String(err)));
3948
3981
  }
3949
3982
 
3950
3983
  // src/session-start.ts
@@ -3979,8 +4012,12 @@ function hashContent(s) {
3979
4012
  return (h >>> 0).toString(16);
3980
4013
  }
3981
4014
 
4015
+ // src/rules-sync.ts
4016
+ function normalizeEol(s) {
4017
+ return s.replace(/\r\n/g, "\n");
4018
+ }
4019
+
3982
4020
  // src/scratch-gc.ts
3983
- var HEAD_TS_STALE_MS = 48 * 36e5;
3984
4021
  var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
3985
4022
  var ROOT_SCRATCH_STALE_MS = 24 * 36e5;
3986
4023
  var SCRATCH_GC_THROTTLE_MS = 24 * 36e5;
@@ -4002,16 +4039,16 @@ function markScratchGcRun(stampPath, now = Date.now()) {
4002
4039
  }
4003
4040
  }
4004
4041
  function executeScratchGc(repoRoot, opts, now = Date.now()) {
4005
- const snap = collectScratchSnapshot(repoRoot);
4042
+ const snap = collectScratchSnapshot(repoRoot, { project: opts.project });
4006
4043
  const plan = planScratchGc(snap, now);
4007
4044
  if (!opts.apply) return { plan };
4008
- return { plan, applied: applyScratchGc(plan, snap.mmiRoot, now) };
4045
+ return { plan, applied: applyScratchGc(plan, repoRoot, now, opts.project) };
4009
4046
  }
4010
- function buildPrMergeScratchHousekeeping(repoRoot, deps = {}) {
4047
+ function buildPrMergeScratchHousekeeping(repoRoot, deps = {}, project2) {
4011
4048
  const runScratchGc = deps.runScratchGc ?? executeScratchGc;
4012
4049
  try {
4013
- const scratchApply = runScratchGc(repoRoot, { apply: true });
4014
- const scratchAfter = runScratchGc(repoRoot, { apply: false });
4050
+ const scratchApply = runScratchGc(repoRoot, { apply: true, project: project2 });
4051
+ const scratchAfter = runScratchGc(repoRoot, { apply: false, project: project2 });
4015
4052
  const applied = scratchApply.applied;
4016
4053
  const pruned = applied?.pruned.length ?? 0;
4017
4054
  const skipped = applied?.skipped ?? 0;
@@ -4043,13 +4080,10 @@ function buildPrMergeScratchHousekeeping(repoRoot, deps = {}) {
4043
4080
  }
4044
4081
  var ROOT_SCRATCH_DIRS = /* @__PURE__ */ new Set(["tmp", ".playwright-mcp", "test-results", "playwright-report"]);
4045
4082
  var ROOT_SCRATCH_FILE_PREFIXES = ["tmp_"];
4046
- var NEVER_BASENAMES = /* @__PURE__ */ new Set([".session", "config.json"]);
4047
4083
  function planScratchGc(snap, now = Date.now()) {
4048
4084
  const candidates = [];
4049
4085
  const normalizePath = (p) => p.replace(/\\/g, "/");
4050
- const repoRoot = normalizePath((snap.repoRoot ?? (0, import_node_path5.dirname)(snap.mmiRoot)).replace(/[\\/]+$/, ""));
4051
- const headTsPrefix = `${snap.mmiRoot.replace(/[\\/]+$/, "")}/head-ts/`.replace(/\\/g, "/");
4052
- const days = (ms) => `${Math.floor(ms / 864e5)}d`;
4086
+ const repoRoot = normalizePath(snap.repoRoot.replace(/[\\/]+$/, ""));
4053
4087
  for (const f of snap.rootScratchFiles ?? []) {
4054
4088
  const dir = normalizePath(f.dir).replace(/\/+$/, "");
4055
4089
  const age = now - f.mtimeMs;
@@ -4078,15 +4112,6 @@ function planScratchGc(snap, now = Date.now()) {
4078
4112
  });
4079
4113
  }
4080
4114
  }
4081
- for (const f of snap.mmiFiles) {
4082
- const age = now - f.mtimeMs;
4083
- const add = (family, reason) => candidates.push({ path: f.path, family, kind: "file", tier: "safe-auto", reason, bytes: f.bytes });
4084
- if (NEVER_BASENAMES.has(f.name)) continue;
4085
- if (f.path.replace(/\\/g, "/").startsWith(headTsPrefix) && !f.name.startsWith(".")) {
4086
- if (age > HEAD_TS_STALE_MS) add("head-ts", `stale throttle stamp (${days(age)} old)`);
4087
- continue;
4088
- }
4089
- }
4090
4115
  if (snap.syncQueueSlugs === null) {
4091
4116
  for (const f of snap.planMdFiles) {
4092
4117
  if (now - f.mtimeMs <= PLAN_ADVISORY_AGE_MS) continue;
@@ -4151,14 +4176,6 @@ function syncedPlanMetaEntry(meta, project2, slug, hash) {
4151
4176
  const entry = meta[metaKey(project2, slug)];
4152
4177
  return Boolean(entry?.hash === hash && entry.syncedAt);
4153
4178
  }
4154
- function readProject(repoRoot) {
4155
- try {
4156
- const cfg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(repoRoot, ".mmi", "config.json"), "utf8"));
4157
- if (typeof cfg.project === "string" && cfg.project.trim()) return cfg.project.trim();
4158
- } catch {
4159
- }
4160
- return void 0;
4161
- }
4162
4179
  function readPlanMeta(plansRoot) {
4163
4180
  try {
4164
4181
  return parseMeta((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(plansRoot, ".plan-meta.json"), "utf8"));
@@ -4196,7 +4213,7 @@ function physicalPlanCandidateStillAllowed(candidatePath, repoAnchor, lstat = im
4196
4213
  }
4197
4214
  }
4198
4215
  function formatScratchGcPlan(plan, applied, applyResult) {
4199
- if (plan.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local saga/plan scratch is clean.";
4216
+ if (plan.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local scratch is clean.";
4200
4217
  const lines = [];
4201
4218
  const bytes = applyResult?.bytes ?? plan.safeAuto.reduce((n, c) => n + c.bytes, 0);
4202
4219
  const safeCount = applyResult ? applyResult.pruned.length : plan.safeAuto.length;
@@ -4240,34 +4257,22 @@ function treeOlderThan(root, now, floor) {
4240
4257
  }
4241
4258
  return true;
4242
4259
  }
4243
- function applyScratchGc(plan, mmiRoot, now = Date.now()) {
4260
+ function applyScratchGc(plan, repoRoot, now = Date.now(), project2) {
4244
4261
  const result = { pruned: [], skipped: 0, bytes: 0 };
4245
4262
  let repoAnchor;
4246
- let anchor;
4247
4263
  try {
4248
- repoAnchor = (0, import_node_fs5.realpathSync)((0, import_node_path5.dirname)(mmiRoot)).replace(/\\/g, "/").replace(/\/+$/, "");
4264
+ repoAnchor = (0, import_node_fs5.realpathSync)(repoRoot).replace(/\\/g, "/").replace(/\/+$/, "");
4249
4265
  } catch {
4250
4266
  return result;
4251
4267
  }
4252
- try {
4253
- anchor = (0, import_node_fs5.realpathSync)(mmiRoot).replace(/\\/g, "/").replace(/\/+$/, "");
4254
- } catch {
4255
- anchor = void 0;
4256
- }
4257
4268
  for (const c of plan.safeAuto) {
4258
4269
  try {
4259
4270
  const real = (0, import_node_fs5.realpathSync)(c.path).replace(/\\/g, "/");
4260
- const rootScoped = c.family === "scratch-dir" || c.family === "scratch-file" || c.family === "plan";
4261
- if (!rootScoped && !anchor) {
4262
- result.skipped += 1;
4263
- continue;
4264
- }
4265
- const containment = rootScoped ? repoAnchor : anchor;
4266
- if (!containment || !pathContained(real, containment) || rootScoped && !rootCandidateStillAllowed(c, repoAnchor)) {
4271
+ if (!pathContained(real, repoAnchor) || !rootCandidateStillAllowed(c, repoAnchor)) {
4267
4272
  result.skipped += 1;
4268
4273
  continue;
4269
4274
  }
4270
- if (rootScoped && trackedPathStatus(c, repoAnchor) !== false) {
4275
+ if (trackedPathStatus(c, repoAnchor) !== false) {
4271
4276
  result.skipped += 1;
4272
4277
  continue;
4273
4278
  }
@@ -4298,14 +4303,13 @@ function applyScratchGc(plan, mmiRoot, now = Date.now()) {
4298
4303
  result.skipped += 1;
4299
4304
  continue;
4300
4305
  }
4301
- const project2 = readProject(repoAnchor);
4302
4306
  const meta = readPlanMeta(plansRoot);
4303
4307
  if (!meta || !syncedPlanMetaEntry(meta, project2, slug, currentHash)) {
4304
4308
  result.skipped += 1;
4305
4309
  continue;
4306
4310
  }
4307
4311
  }
4308
- const floor = c.family === "head-ts" ? HEAD_TS_STALE_MS : c.family === "plan" ? PLAN_ADVISORY_AGE_MS : c.family === "scratch-dir" || c.family === "scratch-file" ? ROOT_SCRATCH_STALE_MS : 0;
4312
+ const floor = c.family === "plan" ? PLAN_ADVISORY_AGE_MS : c.family === "scratch-dir" || c.family === "scratch-file" ? ROOT_SCRATCH_STALE_MS : 0;
4309
4313
  if (floor > 0 && now - st.mtimeMs <= floor) {
4310
4314
  result.skipped += 1;
4311
4315
  continue;
@@ -4389,8 +4393,7 @@ function rootScratchDirSnapshot(root, readdir, stat) {
4389
4393
  function collectScratchSnapshot(repoRoot, deps = {}) {
4390
4394
  const readdir = deps.readdir ?? import_node_fs5.readdirSync;
4391
4395
  const stat = deps.stat ?? import_node_fs5.statSync;
4392
- const readFile3 = deps.readFile ?? import_node_fs5.readFileSync;
4393
- const mmiRoot = (0, import_node_path5.join)(repoRoot, ".mmi");
4396
+ const readFile2 = deps.readFile ?? import_node_fs5.readFileSync;
4394
4397
  const plansRoot = (0, import_node_path5.join)(repoRoot, "plans");
4395
4398
  const rootScratchFiles = [];
4396
4399
  try {
@@ -4414,20 +4417,6 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
4414
4417
  }
4415
4418
  } catch {
4416
4419
  }
4417
- const mmiFiles = [];
4418
- try {
4419
- for (const ent of readdir(mmiRoot, { recursive: true, withFileTypes: true })) {
4420
- if (!ent.isFile()) continue;
4421
- const dir = ent.parentPath ?? ent.path ?? mmiRoot;
4422
- const full = (0, import_node_path5.join)(dir, ent.name);
4423
- try {
4424
- const st = stat(full);
4425
- mmiFiles.push({ path: full, dir, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
4426
- } catch {
4427
- }
4428
- }
4429
- } catch {
4430
- }
4431
4420
  const planMdFiles = [];
4432
4421
  try {
4433
4422
  for (const ent of readdir(plansRoot, { withFileTypes: true })) {
@@ -4435,7 +4424,7 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
4435
4424
  const full = (0, import_node_path5.join)(plansRoot, ent.name);
4436
4425
  try {
4437
4426
  const st = stat(full);
4438
- const raw = readFile3(full, "utf8");
4427
+ const raw = readFile2(full, "utf8");
4439
4428
  planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size, kind: "file", hash: hashContent(normalizeEol(raw)) });
4440
4429
  } catch {
4441
4430
  }
@@ -4444,20 +4433,15 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
4444
4433
  }
4445
4434
  let planMeta = {};
4446
4435
  try {
4447
- planMeta = parseMeta(readFile3((0, import_node_path5.join)(plansRoot, ".plan-meta.json"), "utf8"));
4436
+ planMeta = parseMeta(readFile2((0, import_node_path5.join)(plansRoot, ".plan-meta.json"), "utf8"));
4448
4437
  } catch {
4449
4438
  planMeta = {};
4450
4439
  }
4451
- let project2;
4452
- try {
4453
- const cfg = JSON.parse(readFile3((0, import_node_path5.join)(repoRoot, ".mmi", "config.json"), "utf8"));
4454
- if (typeof cfg.project === "string" && cfg.project.trim()) project2 = cfg.project.trim();
4455
- } catch {
4456
- }
4440
+ const project2 = deps.project?.trim() || void 0;
4457
4441
  const syncQueueSlugs = (() => {
4458
4442
  let queueRaw;
4459
4443
  try {
4460
- queueRaw = readFile3((0, import_node_path5.join)(plansRoot, ".sync-queue.json"), "utf8");
4444
+ queueRaw = readFile2((0, import_node_path5.join)(plansRoot, ".sync-queue.json"), "utf8");
4461
4445
  } catch (e) {
4462
4446
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
4463
4447
  return code === "ENOENT" || code === "ENOTDIR" ? /* @__PURE__ */ new Set() : null;
@@ -4470,7 +4454,7 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
4470
4454
  return null;
4471
4455
  }
4472
4456
  })();
4473
- return { repoRoot, mmiRoot, mmiFiles, rootScratchFiles, planMdFiles, planMeta, project: project2, syncQueueSlugs };
4457
+ return { repoRoot, rootScratchFiles, planMdFiles, planMeta, project: project2, syncQueueSlugs };
4474
4458
  }
4475
4459
 
4476
4460
  // src/repo-runtime-state.ts
@@ -5062,7 +5046,6 @@ async function moveBoardItem(options, deps = {}) {
5062
5046
  const lookup = await fetchIssueProjectItem(client, cfg, selector);
5063
5047
  const item = lookup.item;
5064
5048
  if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
5065
- if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
5066
5049
  const optionId = cfg.statusOptions[options.status];
5067
5050
  try {
5068
5051
  await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
@@ -5252,6 +5235,36 @@ async function setBoardItemPriority(client, cfg, itemId, priority) {
5252
5235
  await updateItemSingleSelect(client, cfg.projectId, itemId, cfg.priorityFieldId, optionId);
5253
5236
  return cliPriorityToFieldName(priority);
5254
5237
  }
5238
+ var defaultRetrySleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
5239
+ async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
5240
+ const attempts = Math.max(1, opts.attempts ?? 5);
5241
+ const delayMs = opts.delayMs ?? 300;
5242
+ const sleep = opts.sleep ?? defaultRetrySleep;
5243
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
5244
+ const itemId = (await fetchIssueProjectItem(client, cfg, selector)).item?.itemId;
5245
+ if (itemId) return itemId;
5246
+ if (attempt < attempts - 1) await sleep(delayMs * (attempt + 1));
5247
+ }
5248
+ return void 0;
5249
+ }
5250
+ async function setBoardPriorityForSelector(options, deps = {}) {
5251
+ const cfg = resolveBoardConfig(options.config);
5252
+ if (!isPriorityFieldConfigured(cfg)) {
5253
+ throw new Error("priority field is not configured in Hub registry META (priorityFieldId + priorityOptions)");
5254
+ }
5255
+ const client = deps.client ?? defaultGitHubClient();
5256
+ const itemId = await resolveProjectItemIdWithRetry(client, cfg, options.selector, options.retry);
5257
+ if (!itemId) {
5258
+ throw new Error(
5259
+ `issue #${options.selector.number} is not on the ${options.selector.repo} board yet (no Project v2 item) \u2014 add it to the board first, then retry`
5260
+ );
5261
+ }
5262
+ const priority = await setBoardItemPriority(client, cfg, itemId, options.priority);
5263
+ if (!priority) {
5264
+ throw new Error("priority field is not configured in Hub registry META (priorityFieldId + priorityOptions)");
5265
+ }
5266
+ return { ref: `${options.selector.repo}#${options.selector.number}`, itemId, priority };
5267
+ }
5255
5268
  async function backfillBoardPriorities(options, deps = {}) {
5256
5269
  const cfg = resolveBoardConfig(options.config);
5257
5270
  if (!isPriorityFieldConfigured(cfg)) {
@@ -5872,6 +5885,8 @@ function parseGhPrChecksResult(stdout, stderr) {
5872
5885
  }
5873
5886
  var PR_CHECKS_POLL_MS = 3e4;
5874
5887
  var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
5888
+ var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
5889
+ var PR_CHECKS_CONFIRM_MS = 5e3;
5875
5890
  async function waitForPrChecks(deps) {
5876
5891
  const { policy, reason } = await deps.resolvePolicy();
5877
5892
  if (policy === "no-ci") {
@@ -5880,10 +5895,20 @@ async function waitForPrChecks(deps) {
5880
5895
  const now = deps.now ?? (() => Date.now());
5881
5896
  const deadline = now() + PR_CHECKS_TIMEOUT_MS;
5882
5897
  let lastDetail = "pending";
5898
+ let successStreak = 0;
5883
5899
  while (now() < deadline) {
5884
5900
  const state = await deps.pollChecks();
5885
- if (state === "success") return { policy, status: "success", detail: "checks-success" };
5886
5901
  if (state === "failure") return { policy, status: "failure", detail: lastDetail };
5902
+ if (state === "success") {
5903
+ successStreak += 1;
5904
+ if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
5905
+ return { policy, status: "success", detail: "checks-success" };
5906
+ }
5907
+ lastDetail = "confirming-success";
5908
+ await deps.sleep(PR_CHECKS_CONFIRM_MS);
5909
+ continue;
5910
+ }
5911
+ successStreak = 0;
5887
5912
  if (state === "no-checks-reported") {
5888
5913
  lastDetail = "no-checks-reported (waiting for workflow to queue)";
5889
5914
  await deps.sleep(PR_CHECKS_POLL_MS);
@@ -6065,6 +6090,10 @@ var MANAGED_GITIGNORE_LINES = [
6065
6090
  "docs/superpowers/",
6066
6091
  ".playwright-mcp/",
6067
6092
  ".claude/worktrees/",
6093
+ // #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<branch>` (outside the tree,
6094
+ // via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
6095
+ // doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
6096
+ // into every repo's .gitignore.
6068
6097
  ".aws-sam/",
6069
6098
  "/*.png",
6070
6099
  // Runtime secrets/config are vault-only (AGENTS.md "Privileged ops") — never commit a local .env. The
@@ -6329,10 +6358,10 @@ function labelsToPrune(orgLabelNames) {
6329
6358
  const org = new Set(orgLabelNames);
6330
6359
  return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
6331
6360
  }
6332
- function resolveSeedContent(seed, vars, readFile3) {
6333
- if (seed.source === "self") return readFile3(seed.target);
6361
+ function resolveSeedContent(seed, vars, readFile2) {
6362
+ if (seed.source === "self") return readFile2(seed.target);
6334
6363
  if (seed.source.startsWith("seed:")) {
6335
- const tmpl = readFile3(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
6364
+ const tmpl = readFile2(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
6336
6365
  return tmpl == null ? null : renderSeed(tmpl, vars);
6337
6366
  }
6338
6367
  return null;
@@ -6496,6 +6525,23 @@ function decideFanoutPrAction(openPrs) {
6496
6525
  }
6497
6526
  return { action: "create" };
6498
6527
  }
6528
+ function decideSeedDelivery(branchRules) {
6529
+ const rules2 = Array.isArray(branchRules) ? branchRules : [];
6530
+ const types = new Set(
6531
+ rules2.map((r) => r?.type).filter((t) => typeof t === "string")
6532
+ );
6533
+ const gating = ["pull_request", "required_status_checks"].filter((t) => types.has(t));
6534
+ if (gating.length) return { mode: "pr", reason: `base branch is protected (${gating.join(", ")}) \u2014 seed via a branch + PR` };
6535
+ return { mode: "direct", reason: "base branch is unprotected \u2014 direct PUT is safe" };
6536
+ }
6537
+ function planSeedDelivery(branchRules, slug, baseBranch) {
6538
+ const decision = decideSeedDelivery(branchRules);
6539
+ if (decision.mode === "pr") {
6540
+ const branch = `bootstrap-seed-${slug}`;
6541
+ return { mode: "pr", ref: branch, branch, reason: decision.reason };
6542
+ }
6543
+ return { mode: "direct", ref: baseBranch, reason: decision.reason };
6544
+ }
6499
6545
  function contentPutArgs(repo, path2, content, branch, sha) {
6500
6546
  const args = [
6501
6547
  "api",
@@ -8599,7 +8645,7 @@ async function collectWaveStatus(deps) {
8599
8645
  } catch {
8600
8646
  dirty = true;
8601
8647
  }
8602
- const headText = await deps.fetchSagaHeadText?.(wt.branch).catch(() => void 0);
8648
+ const headText = await deps.fetchHeadText?.(wt.branch).catch(() => void 0);
8603
8649
  rows.push({
8604
8650
  path: wt.path,
8605
8651
  branch: wt.branch,
@@ -8641,12 +8687,11 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
8641
8687
  }
8642
8688
 
8643
8689
  // src/attach-to-project.ts
8644
- async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m)) {
8690
+ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
8645
8691
  let projectItemId;
8646
8692
  try {
8647
8693
  const boardCfg = resolveBoardConfig(cfg);
8648
- const lookup = await fetchIssueProjectItem(client, boardCfg, selector);
8649
- projectItemId = lookup.item?.itemId;
8694
+ projectItemId = await resolveProjectItemIdWithRetry(client, boardCfg, selector, retry);
8650
8695
  } catch (e) {
8651
8696
  warn(`warning: issue #${selector.number} board item lookup failed after auto-add: ${e.message}
8652
8697
  `);
@@ -8671,7 +8716,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
8671
8716
  }
8672
8717
 
8673
8718
  // src/gh-create.ts
8674
- var import_promises2 = require("node:fs/promises");
8719
+ var import_promises = require("node:fs/promises");
8675
8720
  var import_node_os3 = require("node:os");
8676
8721
  var import_node_path12 = require("node:path");
8677
8722
  var import_node_crypto3 = require("node:crypto");
@@ -8712,9 +8757,13 @@ async function bodyArgsViaFile(args, deps = {}) {
8712
8757
  const i = args.indexOf("--body");
8713
8758
  if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
8714
8759
  } };
8715
- const write = deps.write ?? import_promises2.writeFile;
8716
- const remove = deps.remove ?? import_promises2.unlink;
8717
- const file = (0, import_node_path12.join)(deps.dir ?? (0, import_node_os3.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
8760
+ const write = deps.write ?? import_promises.writeFile;
8761
+ const remove = deps.remove ?? import_promises.unlink;
8762
+ const ensureDir = deps.ensureDir ?? import_promises.mkdir;
8763
+ const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
8764
+ const file = (0, import_node_path12.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
8765
+ await ensureDir((0, import_node_path12.dirname)(file), { recursive: true }).catch(() => {
8766
+ });
8718
8767
  await write(file, args[i + 1], "utf8");
8719
8768
  return {
8720
8769
  args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
@@ -9133,6 +9182,9 @@ function parseNpmVersion(stdout) {
9133
9182
  const v = stdout.trim();
9134
9183
  return /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v) ? v : void 0;
9135
9184
  }
9185
+ function staleTrainCliMessage(report, commandName) {
9186
+ return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`mmi-cli doctor --apply --no-repo-writes\` to update to ${report.releasedVersion}, then rerun ${commandName} so it uses the current train path`;
9187
+ }
9136
9188
  function versionAutoUpdateAction(report, hasPluginRoot) {
9137
9189
  if (report.ok || report.staleAgainst !== "released") return "none";
9138
9190
  return hasPluginRoot ? "plugin-pull" : "npm";
@@ -9296,14 +9348,6 @@ function trainPlanTargetsFromTags(tags, explicitRaw) {
9296
9348
  }
9297
9349
  return targets;
9298
9350
  }
9299
- function stagePlan(stage2 = {}, stops = true) {
9300
- return [
9301
- ...stops ? [{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" }] : [],
9302
- { label: "run local build", command: stage2.build || "(no stage.build configured)" },
9303
- { label: "start local stage", command: stage2.up || "(no stage.up configured)" },
9304
- { label: "check health", command: stage2.healthUrl ? `curl --fail ${stage2.healthUrl}` : "(no stage.healthUrl configured)" }
9305
- ];
9306
- }
9307
9351
  function derivedStagePlan(derived, shell2, stops = true) {
9308
9352
  const { port } = derived;
9309
9353
  const envOrder = ["MMI_STAGE", "MMI_PORT", "COMPOSE_PROFILES"];
@@ -9463,55 +9507,8 @@ function deriveStage(inputs) {
9463
9507
  function stageUrlForPort(port) {
9464
9508
  return `http://127.0.0.1:${port}/`;
9465
9509
  }
9466
- var POSIX_ONLY = [
9467
- /(^|[^&|])&&([^&]|$)/,
9468
- // && chaining (cmd.exe uses it too, but PowerShell <7 / the typical agent shell does not)
9469
- /\|\|/,
9470
- // || chaining
9471
- // `VAR=value cmd` LEADING env prefix only — anchored to a command boundary (start-of-string or after a
9472
- // ; & | separator) so it never fires on a flag value mid-command (`-e DEBUG=true app`, `--env X=y svc`),
9473
- // which are valid cross-shell. The value excludes separators so it can't span past the command boundary.
9474
- /(?:^|[;&|])\s*[A-Za-z_][A-Za-z0-9_]*=[^\s;&|]+\s+\S/,
9475
- /(^|[\s;&|])(cp|rm|mv|ln|cat|touch|export)\s/
9476
- // bare POSIX file/env builtins
9477
- ];
9478
- function looksPosixOnly(command) {
9479
- if (!command?.trim()) return false;
9480
- return POSIX_ONLY.some((re) => re.test(command));
9481
- }
9482
- function stalePosixFields(config, shell2) {
9483
- if (shell2 !== "powershell") return [];
9484
- const fields = [];
9485
- if (looksPosixOnly(config?.build)) fields.push("build");
9486
- if (looksPosixOnly(config?.up)) fields.push("up");
9487
- return fields;
9488
- }
9489
- function sanitizeLocalStage(local, stale) {
9490
- if (!stale.length) return local;
9491
- const clean3 = { ...local };
9492
- for (const field of stale) delete clean3[field];
9493
- return clean3;
9494
- }
9495
- function staleNote(staleFields, outcome) {
9496
- const list = staleFields.join(", ");
9497
- const plural = staleFields.length > 1 ? "fields" : "field";
9498
- return `local .mmi stage ${plural} ${list} ${staleFields.length > 1 ? "are" : "is"} POSIX-only and unusable on PowerShell \u2014 ${outcome}`;
9499
- }
9500
9510
  function decideStage(inputs) {
9501
- const { local, shell: shell2, registry: registry2, hasCompose, hasEnvExample } = inputs;
9502
- const staleFields = stalePosixFields(local, shell2);
9503
- const stale = staleFields.length > 0;
9504
- const upStale = staleFields.includes("up");
9505
- if (local?.up?.trim() && !upStale) {
9506
- if (!stale) return { source: "local", config: local };
9507
- return {
9508
- source: "local",
9509
- config: sanitizeLocalStage(local, staleFields),
9510
- staleIgnored: true,
9511
- staleFields,
9512
- gap: staleNote(staleFields, `kept the cross-shell parts of the local recipe and ignored ${staleFields.join(", ")}`)
9513
- };
9514
- }
9511
+ const { registry: registry2, hasCompose, hasEnvExample } = inputs;
9515
9512
  const deriveInputs = {
9516
9513
  portRange: registry2.portRange,
9517
9514
  deployModel: registry2.deployModel,
@@ -9519,19 +9516,10 @@ function decideStage(inputs) {
9519
9516
  hasEnvExample
9520
9517
  };
9521
9518
  const derived = deriveStage(deriveInputs);
9522
- if (derived) {
9523
- return {
9524
- source: "derived",
9525
- derived,
9526
- registryError: registry2.error,
9527
- staleIgnored: stale || void 0,
9528
- staleFields: stale ? staleFields : void 0
9529
- };
9530
- }
9519
+ if (derived) return { source: "derived", derived, registryError: registry2.error };
9531
9520
  const registryGap = registry2.error ? `Hub registry read failed (${registry2.error}) \u2014 cannot derive a default local stage` : null;
9532
- const baseGap = registryGap ?? deriveStageGap(deriveInputs);
9533
- const gap = stale ? `local .mmi stage recipe is POSIX-only (${staleFields.join(", ")}) and unusable on PowerShell; ${baseGap ?? "no registry-derived default available"}` : baseGap ?? "no stage.up configured and no registry-derived default available";
9534
- return { source: "none", gap, staleIgnored: stale || void 0, staleFields: stale ? staleFields : void 0, registryError: registry2.error };
9521
+ const gap = registryGap ?? deriveStageGap(deriveInputs) ?? "no registry-derived default available";
9522
+ return { source: "none", gap, registryError: registry2.error };
9535
9523
  }
9536
9524
 
9537
9525
  // src/stage-live.ts
@@ -9661,6 +9649,14 @@ function requireValue(value, label) {
9661
9649
  if (!value) throw new Error(`${label} could not be resolved`);
9662
9650
  return value;
9663
9651
  }
9652
+ function planTrainApplyRepoGuard(applyRepo, cwdRepo, rerun) {
9653
+ if (cwdRepo && cwdRepo.toLowerCase() === applyRepo.toLowerCase()) return { ok: true };
9654
+ const where = cwdRepo ? `this checkout is ${cwdRepo}` : "this checkout could not be identified";
9655
+ return {
9656
+ ok: false,
9657
+ message: `--apply runs from the current checkout, not --repo (${where}). cd into the ${applyRepo} checkout and rerun without --repo: ${rerun}`
9658
+ };
9659
+ }
9664
9660
  async function resolveFoldPaths(deps, model) {
9665
9661
  if (model === "hub-serverless") {
9666
9662
  return (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
@@ -9672,17 +9668,28 @@ async function resolveFoldPaths(deps, model) {
9672
9668
  }
9673
9669
  return ["package.json", "package-lock.json"];
9674
9670
  }
9671
+ async function installAppFoldDeps(deps) {
9672
+ const hasLock = await deps.run("git", ["cat-file", "-e", "HEAD:package-lock.json"]).then(() => true).catch(() => false);
9673
+ await deps.run("npm", hasLock ? ["ci"] : ["install"]);
9674
+ }
9675
9675
  async function foldReleaseVersion(deps, model, tag, foldPaths) {
9676
9676
  if (foldPaths.length === 0) return "no version manifest to fold \u2014 the tag is the version";
9677
9677
  const version = tag.replace(/^v/, "");
9678
9678
  if (model === "hub-serverless") {
9679
9679
  await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version]);
9680
9680
  } else {
9681
+ await installAppFoldDeps(deps);
9681
9682
  await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
9682
9683
  }
9683
9684
  for (const path2 of foldPaths) {
9684
9685
  await deps.run("git", ["add", "--", path2]).catch(() => void 0);
9685
9686
  }
9687
+ const strayVersionEdits = (await deps.run("git", ["diff", "--name-only"])).split("\n").map((s) => s.trim()).filter(Boolean);
9688
+ if (strayVersionEdits.length > 0) {
9689
+ throw new Error(
9690
+ `version fold left tracked changes outside the fold set uncommitted: ${strayVersionEdits.join(", ")}. The repo's \`version\` script modified paths the train does not stage (fold set: ${foldPaths.join(", ") || "(none)"}). Add them to that script's git-add list (or the fold set) so the release commit is complete \u2014 shipping now would tag a main whose version-lockstep check is red.`
9691
+ );
9692
+ }
9686
9693
  const staged = (await deps.run("git", ["diff", "--cached", "--name-only"])).trim();
9687
9694
  if (!staged) return `version fold: manifests already at ${version} \u2014 nothing committed`;
9688
9695
  await deps.run("git", ["commit", "-m", `chore(release): bump distribution to ${tag}`]);
@@ -9981,8 +9988,8 @@ async function correlateRun(deps, args) {
9981
9988
  function correlateTenantRun(deps, since, titleIncludes) {
9982
9989
  return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch", titleIncludes });
9983
9990
  }
9984
- function correlatePublishRun(deps, since) {
9985
- return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
9991
+ function correlatePublishRun(deps, since, titleIncludes) {
9992
+ return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch", titleIncludes });
9986
9993
  }
9987
9994
  function correlateControlRun(deps, since, titleIncludes) {
9988
9995
  return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
@@ -10067,7 +10074,7 @@ async function rollDevelopmentForward(deps, ctx, tag) {
10067
10074
  status: "pr-pending",
10068
10075
  prNumber: existing.number,
10069
10076
  prUrl: existing.url,
10070
- note: `alignment PR already open: ${existing.url} \u2014 land it with \`gh pr merge ${existing.number} --merge\``
10077
+ note: `alignment PR already open: ${existing.url} \u2014 land it with \`mmi-cli pr merge ${existing.number} --auto --merge\``
10071
10078
  };
10072
10079
  }
10073
10080
  const ahead = clean(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
@@ -10076,14 +10083,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
10076
10083
  }
10077
10084
  const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
10078
10085
 
10079
- \`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** (\`gh pr merge --merge\`, not squash) so the merge parentage survives and the misalignment guard stays satisfied.`;
10086
+ \`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** \u2014 \`mmi-cli pr merge <n> --auto --merge\` (not squash) so the merge parentage survives and the misalignment guard stays satisfied. \`--auto\` waits out the checks this PR triggers, which otherwise block an immediate merge right after the release.`;
10080
10087
  const url = clean(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
10081
10088
  const number = parsePrNumber(url);
10082
10089
  return {
10083
10090
  status: "pr-pending",
10084
10091
  prNumber: number,
10085
10092
  prUrl: url || void 0,
10086
- note: `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"} \u2014 land it with \`gh pr merge ${number ?? "<number>"} --merge\``
10093
+ note: `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"} \u2014 land it with \`mmi-cli pr merge ${number ?? "<number>"} --auto --merge\``
10087
10094
  };
10088
10095
  }
10089
10096
  function resolveContextState(context, checkRuns, statuses) {
@@ -10268,7 +10275,7 @@ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFail
10268
10275
  deployStatus: "failure"
10269
10276
  };
10270
10277
  }
10271
- const { runId, runUrl } = await correlatePublishRun(deps, since);
10278
+ const { runId, runUrl } = await correlatePublishRun(deps, since, [ctx.slug, stage2]);
10272
10279
  const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
10273
10280
  return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
10274
10281
  }
@@ -10408,7 +10415,7 @@ async function pushRcAlignment(deps) {
10408
10415
  await deps.run("git", ["push", "origin", "main:rc"]);
10409
10416
  return "origin/rc aligned to the released main";
10410
10417
  } catch (e) {
10411
- return `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
10418
+ return `rc alignment push failed \u2014 do NOT bare-push rc (Step 5 forbids it; branch protection rejects it). rc realigns on the next \`mmi-cli rcand\`, or rerun via the train: ${e instanceof Error ? e.message : String(e)}`;
10412
10419
  }
10413
10420
  }
10414
10421
  async function runTrainApplyPipeline(mode, input) {
@@ -11612,9 +11619,7 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
11612
11619
  var OWNER = "mutmutco";
11613
11620
  var LOCKED_APP = "mmi-github-app";
11614
11621
  var OVERGRANT_ROLES = /* @__PURE__ */ new Set(["admin", "maintain"]);
11615
- var REQUIRED_DATA_ACCESS = {
11616
- "mutmutco/MM-Chat": [{ name: "kb-projection-reader", dbRole: "kb_reader", vaultParamNeedle: "KB_READ_DB_URL" }]
11617
- };
11622
+ var REQUIRED_DATA_ACCESS = {};
11618
11623
  function lockedBranches(repoClass, releaseTrack) {
11619
11624
  if (releaseTrack) return branchesForTrack(releaseTrack);
11620
11625
  return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
@@ -14326,7 +14331,7 @@ async function secretsUse(deps, key, opts) {
14326
14331
  ` \u2022 Keyless run: \`mmi-cli secrets use ${key}${opts.slug ? ` --slug ${opts.slug}` : ""} -- <command>\` injects it into the command's env (never printed). A granted non-master can USE an org secret this way.`,
14327
14332
  ` \u2022 Runtime / agents: read it keylessly at runtime via the box's OIDC role. Never bake it into an image or commit it.`,
14328
14333
  ` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
14329
- tier === "project" ? " \u2022 Bare keys default to dev/. Use an explicit rc/<KEY> or main/<KEY> when the stage needs its own value." : " \u2022 For your own product repo, project-admins self-serve this stage key. Org-infra/cross-slug keys remain master-gated unless granted."
14334
+ tier === "project" ? " \u2022 One stageless value per repo (/mmi-future/<slug>/<KEY>) \u2014 no dev/rc/main copies. Project-admins self-serve their repo keys." : " \u2022 Org-infra/cross-slug keys remain master-gated unless granted; project-admins self-serve their own repo keys."
14330
14335
  ].join("\n")
14331
14336
  );
14332
14337
  return;
@@ -14620,7 +14625,7 @@ function registerEdgeCommands(program3) {
14620
14625
  // src/doctor-run.ts
14621
14626
  var import_node_fs17 = require("node:fs");
14622
14627
  var import_node_child_process10 = require("node:child_process");
14623
- var import_promises3 = require("node:fs/promises");
14628
+ var import_promises2 = require("node:fs/promises");
14624
14629
  var import_node_path17 = require("node:path");
14625
14630
  var import_node_os6 = require("node:os");
14626
14631
 
@@ -14945,6 +14950,15 @@ function buildGitignoreManagedBlockCheck(input) {
14945
14950
  const { added, removed, seeded } = diffManagedGitignoreBlock(input.content);
14946
14951
  return { ...base, ok: false, contentToWrite: content, added, removed, seeded };
14947
14952
  }
14953
+ var REPO_LOCAL_WORKTREE_LABEL = "worktree location (canonical sibling path)";
14954
+ var REPO_LOCAL_WORKTREE_FIX = "repo-local `.worktrees/` is not the canonical worktree path (#2321) \u2014 use `mmi-cli worktree create <branch>`, which provisions the sibling `../mmi-worktrees/<branch>` outside the tree. Move your work there, then remove the in-repo `.worktrees/`.";
14955
+ function buildRepoLocalWorktreeCheck(input) {
14956
+ return {
14957
+ ok: !(input.isOrgRepo && input.hasRepoLocalWorktrees),
14958
+ label: REPO_LOCAL_WORKTREE_LABEL,
14959
+ fix: REPO_LOCAL_WORKTREE_FIX
14960
+ };
14961
+ }
14948
14962
  var SCRATCH_GC_LABEL = "scratch housekeeping (tmp/, plans/, browser artifacts)";
14949
14963
  var SCRATCH_GC_FIX = "run `mmi-cli gc --scratch --dry-run` to inspect; run `mmi-cli gc --scratch --apply` to prune safe scratch; for kept plans run `mmi-cli northstar status` or `mmi-cli northstar sync --wait` first";
14950
14964
  function buildScratchGcCheck(plan) {
@@ -15118,10 +15132,6 @@ var OPENCODE_WORKFLOW_COMMANDS = [
15118
15132
  "release",
15119
15133
  "hotfix",
15120
15134
  "bootstrap",
15121
- "grind",
15122
- "build",
15123
- "handoff",
15124
- "coop",
15125
15135
  "browser-automation"
15126
15136
  ];
15127
15137
  function opencodeCommandDescription(command) {
@@ -15377,6 +15387,21 @@ function buildOpencodeVersionCheck(input) {
15377
15387
  }
15378
15388
  return { ...base, ok: false, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
15379
15389
  }
15390
+ function pickOpencodeActiveVersion(input) {
15391
+ for (const value of [input.envStamp, input.cacheVersion, input.diskVersion]) {
15392
+ const trimmed = value?.trim();
15393
+ if (trimmed) return trimmed;
15394
+ }
15395
+ return void 0;
15396
+ }
15397
+ function planOpencodePluginCacheQuarantine(input) {
15398
+ if (!input.cacheExists) return { quarantine: false };
15399
+ if (!isSemverVersion2(input.cacheVersion) || !isSemverVersion2(input.releasedVersion)) return { quarantine: false };
15400
+ if (compareVersions(input.cacheVersion, input.releasedVersion) < 0) {
15401
+ return { quarantine: true, cacheVersion: input.cacheVersion, releasedVersion: input.releasedVersion };
15402
+ }
15403
+ return { quarantine: false };
15404
+ }
15380
15405
  var OPENCODE_HOOK_ACTIVE_LABEL = "OpenCode MMI adapter hooks active (shell.env stamp)";
15381
15406
  function buildOpencodeHookActiveCheck(input) {
15382
15407
  const base = { ok: true, label: OPENCODE_HOOK_ACTIVE_LABEL, fix: OPENCODE_RECOVERY };
@@ -16381,8 +16406,59 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
16381
16406
  return false;
16382
16407
  }
16383
16408
  }
16409
+ function opencodePackagesRoot() {
16410
+ return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
16411
+ }
16412
+ function opencodePluginCacheDirs() {
16413
+ const root = opencodePackagesRoot();
16414
+ try {
16415
+ return (0, import_node_fs17.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("opencode-mmi@") && !e.name.startsWith(".")).map((e) => (0, import_node_path17.join)(root, e.name));
16416
+ } catch {
16417
+ return [];
16418
+ }
16419
+ }
16420
+ function readOpencodeCacheDirVersion(cacheDir) {
16421
+ try {
16422
+ const parsed = JSON.parse(
16423
+ (0, import_node_fs17.readFileSync)((0, import_node_path17.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
16424
+ );
16425
+ return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
16426
+ } catch {
16427
+ return void 0;
16428
+ }
16429
+ }
16430
+ function readOpencodeLoadedCacheVersion() {
16431
+ const versions = opencodePluginCacheDirs().map(readOpencodeCacheDirVersion).filter((v) => Boolean(v));
16432
+ if (!versions.length) return void 0;
16433
+ return versions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
16434
+ }
16435
+ function quarantineStaleOpencodePluginCaches(releasedVersion, log) {
16436
+ let moved = false;
16437
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
16438
+ for (const cacheDir of opencodePluginCacheDirs()) {
16439
+ const plan = planOpencodePluginCacheQuarantine({
16440
+ cacheExists: true,
16441
+ cacheVersion: readOpencodeCacheDirVersion(cacheDir),
16442
+ releasedVersion
16443
+ });
16444
+ if (!plan.quarantine) continue;
16445
+ try {
16446
+ const quarantineRoot = (0, import_node_path17.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
16447
+ (0, import_node_fs17.mkdirSync)(quarantineRoot, { recursive: true });
16448
+ (0, import_node_fs17.renameSync)(cacheDir, (0, import_node_path17.join)(quarantineRoot, (0, import_node_path17.basename)(cacheDir)));
16449
+ log(` \u21BB quarantined stale OpenCode plugin cache ${plan.cacheVersion} (< ${plan.releasedVersion}) \u2014 OpenCode reinstalls the current adapter on next start`);
16450
+ moved = true;
16451
+ } catch {
16452
+ }
16453
+ }
16454
+ return moved;
16455
+ }
16384
16456
  function opencodeInstalledVersionForDoctor() {
16385
- return process.env.MMI_OPENCODE_PLUGIN_VERSION || readOpencodeAdapterDiskVersion();
16457
+ return pickOpencodeActiveVersion({
16458
+ envStamp: process.env.MMI_OPENCODE_PLUGIN_VERSION,
16459
+ cacheVersion: readOpencodeLoadedCacheVersion(),
16460
+ diskVersion: readOpencodeAdapterDiskVersion()
16461
+ });
16386
16462
  }
16387
16463
  function opencodePluginVersionsForReport() {
16388
16464
  return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
@@ -16820,6 +16896,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
16820
16896
  }
16821
16897
  }
16822
16898
  checks.push(gitignoreCheck);
16899
+ checks.push(buildRepoLocalWorktreeCheck({
16900
+ isOrgRepo,
16901
+ hasRepoLocalWorktrees: (0, import_node_fs17.existsSync)((0, import_node_path17.join)(process.cwd(), ".worktrees"))
16902
+ }));
16823
16903
  let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo, installed, surface });
16824
16904
  if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
16825
16905
  if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
@@ -16906,8 +16986,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
16906
16986
  releasedVersion
16907
16987
  });
16908
16988
  if (!opencodeVersionCheck.ok && repairFull) {
16909
- if (await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m))) {
16910
- opencodeInstalledVersion = readOpencodeAdapterDiskVersion() ?? opencodeInstalledVersion;
16989
+ const refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
16990
+ const quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
16991
+ if (refreshed || quarantined) {
16992
+ opencodeInstalledVersion = opencodeInstalledVersionForDoctor() ?? opencodeInstalledVersion;
16911
16993
  opencodeVersionCheck = buildOpencodeVersionCheck({
16912
16994
  isOrgRepo,
16913
16995
  installedVersion: opencodeInstalledVersion,
@@ -17080,7 +17162,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17080
17162
  releasedVersion,
17081
17163
  hubCheckout: hubCheckoutForCursorSeed(),
17082
17164
  execFileP: execFileP2,
17083
- mkdtemp: (prefix) => (0, import_promises3.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
17165
+ mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
17084
17166
  log: (m) => io.err(m)
17085
17167
  });
17086
17168
  if (seeded) {
@@ -17323,7 +17405,6 @@ function repoFromSelector(selector) {
17323
17405
  async function loadConfigForBoardSelector(selector, repoOption) {
17324
17406
  return loadConfigForRepo(repoFromSelector(selector) ?? repoOption);
17325
17407
  }
17326
- var DEFAULT_RULES_SOURCE = "https://raw.githubusercontent.com/mutmutco/MMI-Hub/development";
17327
17408
  function renderGcApplyResult(result) {
17328
17409
  const lines = ["gc apply result:"];
17329
17410
  lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
@@ -17439,65 +17520,11 @@ async function requireFreshTrainCli(commandName) {
17439
17520
  releasedVersion: await fetchNpmReleasedVersion()
17440
17521
  });
17441
17522
  if (report.ok) return;
17442
- throw new Error(`running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; run doctor/update first so ${commandName} uses the current train path`);
17523
+ throw new Error(staleTrainCliMessage(report, commandName));
17443
17524
  }
17444
17525
  var program2 = new Command();
17445
17526
  program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
17446
- async function runRulesSync(opts, io = consoleIo) {
17447
- const cfg = await loadConfig();
17448
- if (isRulesSource(cfg.orgRulesSource)) {
17449
- if (!opts.quiet) io.log('mmi-cli rules: source repo (orgRulesSource: "self") \u2014 skipping self-sync');
17450
- return true;
17451
- }
17452
- if (!await isOrgRegisteredRepo(cfg)) {
17453
- if (!opts.quiet) io.log("mmi-cli rules: not an org repo \u2014 skipping spine delivery");
17454
- return true;
17455
- }
17456
- const base = resolveRulesBase(cfg.orgRulesSource, DEFAULT_RULES_SOURCE);
17457
- const token = await githubToken();
17458
- let changed = 0;
17459
- const files = [
17460
- "AGENTS.md",
17461
- "CLAUDE.md",
17462
- ".claude/settings.json",
17463
- ".claude/output-styles/mmi-plain.md",
17464
- ".cursor/rules/mmi-plain-language.mdc",
17465
- ".cursor/rules/mmi-tool-economy.mdc",
17466
- ".cursor/rules/mmi-code-economy.mdc"
17467
- ];
17468
- const fetched = await Promise.all(files.map(async (file) => {
17469
- try {
17470
- const url = `${base}/${file}`;
17471
- const res = await fetch(url, { headers: rulesSourceAuthHeaders(url, token), signal: AbortSignal.timeout(1e4) });
17472
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
17473
- return { file, source: await res.text() };
17474
- } catch (e) {
17475
- return { file, error: e.message };
17476
- }
17477
- }));
17478
- const failures = fetched.filter((entry) => "error" in entry);
17479
- for (const failure of failures) {
17480
- io.err(`mmi-cli rules: could not fetch ${failure.file} (${failure.error}); left it untouched`);
17481
- }
17482
- for (const entry of fetched) {
17483
- if ("error" in entry) continue;
17484
- const { file, source } = entry;
17485
- const current = (0, import_node_fs18.existsSync)(file) ? await (0, import_promises4.readFile)(file, "utf8") : null;
17486
- if (needsUpdate(source, current)) {
17487
- const slash = file.lastIndexOf("/");
17488
- if (slash > 0) (0, import_node_fs18.mkdirSync)(file.slice(0, slash), { recursive: true });
17489
- await (0, import_promises4.writeFile)(file, normalizeEol(source), "utf8");
17490
- changed++;
17491
- if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
17492
- }
17493
- }
17494
- if (!opts.quiet && changed === 0) io.log("mmi-cli rules: up to date");
17495
- return failures.length === 0;
17496
- }
17497
- var rules = program2.command("rules").description("org rules delivery");
17498
- rules.command("sync").option("--quiet", "stay silent unless something changed or errored").description("fetch the org-delivered files (AGENTS.md / CLAUDE.md / .claude/settings.json / output style / Cursor rule) from MMI-Hub and write them verbatim (org-owned, whole-file)").action(async (opts) => {
17499
- if (!await runRulesSync(opts)) process.exitCode = 1;
17500
- });
17527
+ var rules = program2.command("rules").description("org-managed .gitignore delivery");
17501
17528
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
17502
17529
  const path2 = (0, import_node_path18.join)(process.cwd(), ".gitignore");
17503
17530
  const current = (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null;
@@ -17519,30 +17546,6 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
17519
17546
  console.log("mmi-cli rules gitignore: up to date");
17520
17547
  }
17521
17548
  });
17522
- async function runDocsSync(opts, io = consoleIo) {
17523
- const ref = await gitOut(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
17524
- const def = (ref.startsWith("origin/") ? ref.slice("origin/".length) : ref) || "development";
17525
- await gitOut(["fetch", "origin", def, "--quiet"]);
17526
- const result = await syncDocs({
17527
- isDirty: async (f) => await gitOut(["status", "--porcelain", "--", f]) !== "",
17528
- originContent: async (f) => {
17529
- try {
17530
- return (await execFileP2("git", ["show", `origin/${def}:${f}`], { maxBuffer: 10 * 1024 * 1024 })).stdout;
17531
- } catch {
17532
- return null;
17533
- }
17534
- },
17535
- localContent: async (f) => (0, import_node_fs18.existsSync)(f) ? await (0, import_promises4.readFile)(f, "utf8") : null,
17536
- writeDoc: async (f, c) => {
17537
- await (0, import_promises4.writeFile)(f, c, "utf8");
17538
- }
17539
- });
17540
- for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
17541
- if (!opts.quiet && result.skippedDirty.length) io.log(`mmi-cli docs: kept local edits in ${result.skippedDirty.join(", ")}`);
17542
- if (!opts.quiet && result.updated.length === 0 && result.skippedDirty.length === 0) io.log("mmi-cli docs: up to date");
17543
- }
17544
- var docs = program2.command("docs").description("repo-owned authoritative docs");
17545
- docs.command("sync").option("--quiet", "stay silent unless something changed or errored").description("refresh README.md / architecture.md from the repo default branch; never clobbers uncommitted edits").action((opts) => runDocsSync(opts));
17546
17549
  program2.command("commands").description("print the command manifest \u2014 every subcommand + its flags (ground against this instead of guessing)").option("--json", "machine-readable JSON: { name, version, tree, index } \u2014 index is a flat list of every leaf command path").action((o) => {
17547
17550
  const manifest = buildCommandManifest(program2);
17548
17551
  consoleIo.log(o.json ? JSON.stringify(manifest, null, 2) : formatManifestHuman(manifest));
@@ -18453,8 +18456,8 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
18453
18456
  let northStarSlug;
18454
18457
  let extraLabels = [];
18455
18458
  try {
18456
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises4.readFile, readStdin });
18457
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises4.readFile, readStdin });
18459
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
18460
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18458
18461
  if (o.northStar !== void 0) {
18459
18462
  northStarSlug = normalizeNorthStarSlug(o.northStar);
18460
18463
  body = appendNorthStarLine(body, northStarSlug);
@@ -18511,6 +18514,20 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
18511
18514
  ...parentLinkFields(parent, parentLinkError)
18512
18515
  }));
18513
18516
  });
18517
+ issue.command("view <number>").description("read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json <fields>", "comma-separated gh --json field list (overrides the default field set)").action(async (number, o) => {
18518
+ const n = Number(number);
18519
+ if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
18520
+ const repo = await resolveRepo(o.repo);
18521
+ if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
18522
+ const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,labels,author,assignees,milestone,body";
18523
+ try {
18524
+ const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", fields]);
18525
+ console.log(JSON.stringify(data));
18526
+ } catch (e) {
18527
+ const err = e;
18528
+ return fail(`issue view: ${(err.stderr || err.message || String(e)).trim()}`);
18529
+ }
18530
+ });
18514
18531
  issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").action(async (o) => {
18515
18532
  const number = Number(o.number);
18516
18533
  if (!Number.isInteger(number) || number <= 0) return fail("issue discover-related: --number must be a positive integer");
@@ -18566,7 +18583,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
18566
18583
  }
18567
18584
  let body;
18568
18585
  try {
18569
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises4.readFile, readStdin });
18586
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18570
18587
  } catch (e) {
18571
18588
  return fail(`issue comment: ${e.message}`);
18572
18589
  }
@@ -18628,8 +18645,8 @@ program2.command("report").description("file a friction report on the Hub board
18628
18645
  const targetRepo2 = o.repo ?? HUB_REPO2;
18629
18646
  const sourceRepo = await resolveRepo(void 0);
18630
18647
  try {
18631
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises4.readFile, readStdin });
18632
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises4.readFile, readStdin });
18648
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
18649
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18633
18650
  priority = normalizePriority(o.priority);
18634
18651
  args = buildIssueArgs({
18635
18652
  type: o.type,
@@ -18702,8 +18719,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
18702
18719
  let args;
18703
18720
  try {
18704
18721
  skill = assertSkillName(o.skill);
18705
- rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises4.readFile, readStdin });
18706
- const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises4.readFile, readStdin });
18722
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18723
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
18707
18724
  title = buildSkillLessonTitle(skill, rawTitle);
18708
18725
  priority = normalizePriority(o.priority);
18709
18726
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -18754,14 +18771,28 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
18754
18771
  let body;
18755
18772
  let title;
18756
18773
  try {
18757
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises4.readFile, readStdin });
18758
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises4.readFile, readStdin });
18774
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
18775
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18759
18776
  } catch (e) {
18760
18777
  return fail(`pr create: ${e.message}`);
18761
18778
  }
18762
18779
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
18763
18780
  console.log(JSON.stringify(created));
18764
18781
  });
18782
+ pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json <fields>", "comma-separated gh --json field list (overrides the default field set)").action(async (number, o) => {
18783
+ const n = Number(number);
18784
+ if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
18785
+ const repo = await resolveRepo(o.repo);
18786
+ if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
18787
+ const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
18788
+ try {
18789
+ const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", fields]);
18790
+ console.log(JSON.stringify(data));
18791
+ } catch (e) {
18792
+ const err = e;
18793
+ return fail(`pr view: ${(err.stderr || err.message || String(e)).trim()}`);
18794
+ }
18795
+ });
18765
18796
  async function listCiWorkflowPaths(cwd = process.cwd()) {
18766
18797
  const wfDir = (0, import_node_path18.join)(cwd, ".github", "workflows");
18767
18798
  if (!(0, import_node_fs18.existsSync)(wfDir)) return [];
@@ -18930,15 +18961,15 @@ async function createDeferredWorktreeStore() {
18930
18961
  return {
18931
18962
  read: async () => {
18932
18963
  try {
18933
- return parseDeferredWorktreesFile(await (0, import_promises4.readFile)(registryPath, "utf8"));
18964
+ return parseDeferredWorktreesFile(await (0, import_promises3.readFile)(registryPath, "utf8"));
18934
18965
  } catch {
18935
18966
  return [];
18936
18967
  }
18937
18968
  },
18938
18969
  write: async (entries) => {
18939
18970
  try {
18940
- await (0, import_promises4.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
18941
- await (0, import_promises4.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
18971
+ await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
18972
+ await (0, import_promises3.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
18942
18973
  } catch {
18943
18974
  }
18944
18975
  }
@@ -18979,7 +19010,7 @@ var realWorktreeDirRemover = {
18979
19010
  (0, import_node_fs18.unlinkSync)(p);
18980
19011
  }
18981
19012
  },
18982
- removeTree: (p) => (0, import_promises4.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
19013
+ removeTree: (p) => (0, import_promises3.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
18983
19014
  };
18984
19015
  async function resolvePrimaryCheckout(execGit) {
18985
19016
  try {
@@ -19037,9 +19068,9 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
19037
19068
  const beforeWorktrees = parseWorktreePorcelain(
19038
19069
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
19039
19070
  );
19040
- const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists2(headRef);
19071
+ const remoteBefore = await remoteBranchExists2(headRef);
19041
19072
  let remoteDeleteAttempted = false;
19042
- let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
19073
+ let remoteNotAttemptedReason;
19043
19074
  await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
19044
19075
  const message = String(e.message || "");
19045
19076
  if (/already been merged/i.test(message)) {
@@ -19061,10 +19092,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
19061
19092
  }
19062
19093
  }
19063
19094
  if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
19064
- const remoteBranch = repoArgs.length ? buildRemoteBranchCleanupReport(headRef, {
19065
- attempted: false,
19066
- reason: remoteNotAttemptedReason
19067
- }) : await buildPrMergeRemoteBranchCleanupReport(headRef, {
19095
+ const remoteBranch = await buildPrMergeRemoteBranchCleanupReport(headRef, {
19068
19096
  exists: remoteBranchExists2
19069
19097
  }, {
19070
19098
  attempted: remoteDeleteAttempted,
@@ -19074,11 +19102,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
19074
19102
  const deferredStore = await createDeferredWorktreeStore();
19075
19103
  let localCleanup;
19076
19104
  try {
19077
- localCleanup = repoArgs.length ? {
19078
- branch: headRef,
19079
- localBranch: { name: headRef, status: "not-attempted", reason: "repo-option" },
19080
- worktree: void 0
19081
- } : await cleanupPrMergeLocalBranch(headRef, {
19105
+ localCleanup = await cleanupPrMergeLocalBranch(headRef, {
19082
19106
  beforeWorktrees,
19083
19107
  startingPath,
19084
19108
  pathExists: (p) => (0, import_node_fs18.existsSync)(p),
@@ -19205,6 +19229,30 @@ board.command("backfill-priority").description("set board Priority from priority
19205
19229
  return failGraceful(`board backfill-priority failed: ${e.message}`);
19206
19230
  }
19207
19231
  });
19232
+ board.command("set-priority <issue> <priority>").description(`set a board item's Priority field (${CLI_PRIORITIES.join("|")}) \u2014 recovers a Priority lost on the create-time auto-add race (#2349)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").action(async (issueRef, priorityArg, o) => {
19233
+ let priority;
19234
+ try {
19235
+ priority = normalizePriority(priorityArg);
19236
+ } catch (e) {
19237
+ return fail(`board set-priority failed: ${e.message}`);
19238
+ }
19239
+ try {
19240
+ const defaultRepo = await resolveRepo(o.repo) ?? "";
19241
+ const selector = parseIssueSelector(issueRef, defaultRepo);
19242
+ if (!selector.repo) {
19243
+ return fail("board set-priority failed: could not resolve the repo \u2014 pass owner/repo#123 or use --repo");
19244
+ }
19245
+ const result = await setBoardPriorityForSelector({
19246
+ config: await loadConfigForBoardSelector(issueRef, o.repo),
19247
+ selector,
19248
+ priority
19249
+ });
19250
+ if (o.json) return console.log(JSON.stringify(result));
19251
+ console.log(`Set ${result.ref} Priority -> ${result.priority}`);
19252
+ } catch (e) {
19253
+ return failGraceful(`board set-priority failed: ${e.message}`);
19254
+ }
19255
+ });
19208
19256
  board.command("prune-priority-labels").description("remove retired priority:* labels (#416) from issues whose board Priority field is already set").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--dry-run", "report what would be removed without writing").option("--concurrency <n>", "parallel issue edits (default 8)", "8").action(async (o) => {
19209
19257
  try {
19210
19258
  const result = await prunePriorityLabels({
@@ -19286,14 +19334,11 @@ function stageKeepAlive() {
19286
19334
  }
19287
19335
  async function resolveStage() {
19288
19336
  const cfg = await loadConfig();
19289
- const local = cfg.stage;
19290
19337
  const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch((e) => ({ ok: false, error: e.message }));
19291
19338
  const project2 = read.ok ? read.project : null;
19292
19339
  const portRangeMeta = project2?.portRange ?? void 0;
19293
19340
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
19294
19341
  return decideStage({
19295
- local,
19296
- shell: shellFor(),
19297
19342
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
19298
19343
  hasCompose: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), "docker-compose.yml")),
19299
19344
  hasEnvExample: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), ".env.example"))
@@ -19317,19 +19362,8 @@ async function fetchStageVaultEnvMerge() {
19317
19362
  }
19318
19363
  function stageStepsFor(res, stops = true) {
19319
19364
  if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
19320
- if (res.source === "local") return stagePlan(res.config ?? {}, stops);
19321
19365
  return [{ label: `no local stage to run \u2014 ${res.gap ?? "stage config gap"}` }];
19322
19366
  }
19323
- function staleStageNote(res) {
19324
- if (!res.staleIgnored) return null;
19325
- const fields = res.staleFields ?? [];
19326
- const list = fields.join(", ");
19327
- const label = fields.length > 1 ? `fields ${list}` : `field ${list || "build/up"}`;
19328
- if (res.source === "local") {
19329
- return `note: POSIX-only .mmi stage ${label} ignored on PowerShell \u2014 kept the rest of the local recipe`;
19330
- }
19331
- return `note: stale POSIX-only .mmi stage ${label} ignored on PowerShell \u2014 using the registry-derived default`;
19332
- }
19333
19367
  function reportedStageUrl(res, result) {
19334
19368
  if (!res.derived) return void 0;
19335
19369
  return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
@@ -19420,7 +19454,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
19420
19454
  const res = await resolveStage();
19421
19455
  if (o.apply) {
19422
19456
  if (res.source === "none") return failGraceful(`stage: ${res.gap}`);
19423
- const cfg = res.config ?? res.derived.config;
19457
+ const cfg = res.derived.config;
19424
19458
  const hold = stageKeepAlive();
19425
19459
  try {
19426
19460
  const result = await runStage(cfg, stageScopedRunOpts({ timeoutMs: o.timeoutMs }));
@@ -19434,9 +19468,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
19434
19468
  }
19435
19469
  }
19436
19470
  const steps = stageStepsFor(res);
19437
- if (o.json) return console.log(JSON.stringify({ command: "stage", source: res.source, url: res.derived?.url, staleIgnored: res.staleIgnored, staleFields: res.staleFields, registryError: res.registryError, steps }, null, 2));
19438
- const note = staleStageNote(res);
19439
- if (note) printLine(note);
19471
+ if (o.json) return console.log(JSON.stringify({ command: "stage", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
19440
19472
  console.log(renderSteps("mmi-cli stage: dry-run plan", steps));
19441
19473
  });
19442
19474
  stage.command("stop").description("stop the previous local stage process recorded in tmp/stage/state.json").option("--json", "machine-readable output").option("--apply", "kill the recorded process tree and remove the state file").action(async () => {
@@ -19457,14 +19489,12 @@ stage.command("start").description("start the configured local stage process and
19457
19489
  const res = await resolveStage();
19458
19490
  if (!o.apply) {
19459
19491
  const steps = stageStepsFor(res, false);
19460
- if (o.json) return printLine(JSON.stringify({ command: "stage start", source: res.source, url: res.derived?.url, staleIgnored: res.staleIgnored, staleFields: res.staleFields, registryError: res.registryError, steps }, null, 2));
19461
- const note = staleStageNote(res);
19462
- if (note) printLine(note);
19492
+ if (o.json) return printLine(JSON.stringify({ command: "stage start", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
19463
19493
  return printLine(renderSteps("mmi-cli stage start: dry-run plan", steps));
19464
19494
  }
19465
19495
  if (res.source === "none") return failGraceful(`stage start: ${res.gap}`);
19466
- const cfg = res.config ?? res.derived.config;
19467
- const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
19496
+ const cfg = res.derived.config;
19497
+ const vaultEnvMerge = await fetchStageVaultEnvMerge();
19468
19498
  try {
19469
19499
  const hold = stageKeepAlive();
19470
19500
  let printed = false;
@@ -19492,14 +19522,12 @@ stage.command("run").description("force-stop previous stage, build, start, and h
19492
19522
  const res = await resolveStage();
19493
19523
  if (!o.apply) {
19494
19524
  const steps = stageStepsFor(res);
19495
- if (o.json) return printLine(JSON.stringify({ command: "stage run", source: res.source, url: res.derived?.url, staleIgnored: res.staleIgnored, staleFields: res.staleFields, registryError: res.registryError, steps }, null, 2));
19496
- const note = staleStageNote(res);
19497
- if (note) printLine(note);
19525
+ if (o.json) return printLine(JSON.stringify({ command: "stage run", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
19498
19526
  return printLine(renderSteps("mmi-cli stage run: dry-run plan", steps));
19499
19527
  }
19500
19528
  if (res.source === "none") return failGraceful(`stage run: ${res.gap}`);
19501
- const cfg = res.config ?? res.derived.config;
19502
- const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
19529
+ const cfg = res.derived.config;
19530
+ const vaultEnvMerge = await fetchStageVaultEnvMerge();
19503
19531
  try {
19504
19532
  const hold = stageKeepAlive();
19505
19533
  let printed = false;
@@ -19569,8 +19597,8 @@ function trainApplyDeps() {
19569
19597
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
19570
19598
  announce: (args) => announceRelease({
19571
19599
  run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
19572
- readFile: (path2) => (0, import_promises4.readFile)(path2, "utf8"),
19573
- removeFile: (path2) => (0, import_promises4.unlink)(path2)
19600
+ readFile: (path2) => (0, import_promises3.readFile)(path2, "utf8"),
19601
+ removeFile: (path2) => (0, import_promises3.unlink)(path2)
19574
19602
  }, args),
19575
19603
  fetchEdgeDomains: async (slug) => {
19576
19604
  const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
@@ -19603,7 +19631,7 @@ function renderTrainApply(commandName, r) {
19603
19631
  if (r.rcRetirement) base = `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})`;
19604
19632
  if (r.devRollForward) {
19605
19633
  const f = r.devRollForward;
19606
- base = f.status === "pr-pending" ? `${base}; dev roll-forward: ALIGNMENT PR PENDING \u2014 land it with \`gh pr merge ${f.prNumber ?? "<number>"} --merge\`${f.prUrl ? ` (${f.prUrl})` : ""}` : `${base}; dev roll-forward: ${f.note}`;
19634
+ base = f.status === "pr-pending" ? `${base}; dev roll-forward: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli pr merge ${f.prNumber ?? "<number>"} --auto --merge\`${f.prUrl ? ` (${f.prUrl})` : ""}` : `${base}; dev roll-forward: ${f.note}`;
19607
19635
  }
19608
19636
  if (r.checkout) {
19609
19637
  base = `${base}; checkout: ${r.checkout.note}`;
@@ -19635,7 +19663,9 @@ for (const commandName of ["rcand", "release"]) {
19635
19663
  return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
19636
19664
  }
19637
19665
  if (o.apply && o.repo) {
19638
- return fail(`${commandName}: --repo is read-only for dry-run planning; --apply must run from the target repo checkout`);
19666
+ const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
19667
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
19668
+ if (!guard.ok) return fail(`${commandName}: ${guard.message}`);
19639
19669
  }
19640
19670
  if (o.apply) {
19641
19671
  try {
@@ -19788,6 +19818,14 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
19788
19818
  console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
19789
19819
  if (!report.ok) process.exitCode = 1;
19790
19820
  });
19821
+ bootstrap.command("org-ruleset").description("drift-check the codified org no-agent-files push ruleset (mmi-no-agent-files-org) against live; read-only, never mutates (#2196)").option("--json", "machine-readable output").action(async () => {
19822
+ const json = rawFlag("--json");
19823
+ const live = await fetchOrgNoAgentFilesRuleset(defaultGitHubClient());
19824
+ const plan = planOrgNoAgentFilesRuleset(live);
19825
+ if (json) console.log(JSON.stringify({ action: plan.action, id: plan.id, drift: plan.drift, desired: plan.desired }, null, 2));
19826
+ else console.log(renderOrgRulesetDriftReport(plan));
19827
+ if (plan.action !== "noop") process.exitCode = 1;
19828
+ });
19791
19829
  bootstrap.command("apply <repo>").description("idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
19792
19830
  const o = {
19793
19831
  class: rawValue("--class", "deployable"),
@@ -19811,7 +19849,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19811
19849
  const baseBranch = o.class === "content" ? "main" : "development";
19812
19850
  const slug = parsedRepo.slug;
19813
19851
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
19814
- const readFile3 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
19852
+ const readFile2 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
19815
19853
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
19816
19854
  const rawVars = {};
19817
19855
  for (const value of rawValues("--var")) {
@@ -19844,6 +19882,25 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19844
19882
  applyDeployModel = payload.deployModel;
19845
19883
  } catch {
19846
19884
  }
19885
+ let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
19886
+ let seededToBranch = 0;
19887
+ if (o.execute) {
19888
+ let branchRules = [];
19889
+ try {
19890
+ branchRules = JSON.parse((await gh(["api", `repos/${repo}/rules/branches/${baseBranch}`])).stdout || "[]");
19891
+ } catch {
19892
+ branchRules = [];
19893
+ }
19894
+ seedPlan = planSeedDelivery(branchRules, slug, baseBranch);
19895
+ if (seedPlan.branch) {
19896
+ const headSha = (await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"])).stdout.trim();
19897
+ try {
19898
+ await gh(["api", "-X", "POST", `repos/${repo}/git/refs`, "-f", `ref=refs/heads/${seedPlan.branch}`, "-f", `sha=${headSha}`]);
19899
+ } catch (e) {
19900
+ if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
19901
+ }
19902
+ }
19903
+ }
19847
19904
  for (const seed of manifest.seeds) {
19848
19905
  if (!seed.classes.includes(o.class)) continue;
19849
19906
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
@@ -19853,7 +19910,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19853
19910
  let remoteContent = null;
19854
19911
  if (resolved.source !== "fanout") {
19855
19912
  try {
19856
- const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${baseBranch}`]);
19913
+ const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedPlan.ref}`]);
19857
19914
  exists = true;
19858
19915
  try {
19859
19916
  const parsed = JSON.parse(r.stdout);
@@ -19869,13 +19926,44 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19869
19926
  }
19870
19927
  const planned = planSeedAction(resolved, exists);
19871
19928
  const isBlock = resolved.source === "managed-block";
19872
- const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile3) : null;
19929
+ const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile2) : null;
19873
19930
  const action = reconcileSeedAction(planned, content, isBlock);
19874
19931
  actions.push(action);
19875
19932
  if (o.execute && (action.action === "create" || action.action === "update")) {
19876
- await gh(contentPutArgs(repo, resolved.target, content, baseBranch, action.action === "update" ? sha : void 0));
19933
+ await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
19877
19934
  applied.push(`${action.action} ${resolved.target}`);
19935
+ if (seedPlan.mode === "pr") seededToBranch++;
19936
+ }
19937
+ }
19938
+ let seedPrUrl;
19939
+ if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
19940
+ await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
19941
+ });
19942
+ const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
19943
+ const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
19944
+ if (prDecision.action === "reuse") {
19945
+ seedPrUrl = prDecision.url;
19946
+ } else {
19947
+ const created = await ghCreate([
19948
+ "pr",
19949
+ "create",
19950
+ "--repo",
19951
+ repo,
19952
+ "--base",
19953
+ baseBranch,
19954
+ "--head",
19955
+ seedPlan.branch,
19956
+ "--title",
19957
+ `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
19958
+ "--body",
19959
+ `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").`
19960
+ ]);
19961
+ seedPrUrl = created.url;
19878
19962
  }
19963
+ await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
19964
+ if (!/already/i.test(String(e.message ?? ""))) throw e;
19965
+ });
19966
+ applied.push(`seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)`);
19879
19967
  }
19880
19968
  if (o.execute && o.class === "deployable") {
19881
19969
  try {
@@ -19886,7 +19974,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19886
19974
  }
19887
19975
  const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
19888
19976
  if (rulesetSeed) {
19889
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile3);
19977
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile2);
19890
19978
  if (rulesetContent) {
19891
19979
  try {
19892
19980
  const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
@@ -19999,9 +20087,9 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19999
20087
  "--head",
20000
20088
  branchName,
20001
20089
  "--title",
20002
- `bootstrap: register ${parsedRepo.name} for org-spine fanout`,
20090
+ `bootstrap: register ${parsedRepo.name} for the org-managed .gitignore fanout`,
20003
20091
  "--body",
20004
- `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#933): adds ${parsedRepo.name} to projects.json + .github/fanout-targets.json so the spine fans out to it.`
20092
+ `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#933): adds ${parsedRepo.name} to projects.json + .github/fanout-targets.json so the org-managed .gitignore block reaches it (hub-v3 retired the whole-spine fanout).`
20005
20093
  ]);
20006
20094
  fanoutPrUrl = created.url;
20007
20095
  }
@@ -20014,7 +20102,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
20014
20102
  return failGraceful(`bootstrap apply: fanout registration failed: ${e.message}`);
20015
20103
  }
20016
20104
  }
20017
- if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
20105
+ if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
20018
20106
  else {
20019
20107
  console.log(renderSeedPlan(actions));
20020
20108
  if (o.execute) console.log(`
@@ -20081,13 +20169,12 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
20081
20169
  ));
20082
20170
  program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
20083
20171
  program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
20084
- program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process; docs sync runs detached").action(async () => {
20172
+ program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
20085
20173
  if (isInsideRepoSubdir(process.cwd())) {
20086
20174
  console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
20087
20175
  return;
20088
20176
  }
20089
20177
  if (!await isOrgRepoRoot()) return;
20090
- spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
20091
20178
  spawnDeferredGcSweep();
20092
20179
  const { parallel, sequential } = buildSessionStartPlan({
20093
20180
  // whoami (#879): surface the resolved human so agents act --for them without asking. Silent
@@ -20103,7 +20190,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
20103
20190
  boardSlice: (io) => runBoardSlice(io, {
20104
20191
  loadConfig: () => loadConfigForRepo(),
20105
20192
  readBoard,
20106
- // #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
20193
+ // #1813: warm the slice cache out-of-band (detached) so the ~20s live read
20107
20194
  // never costs banner time and next session's glance renders instantly within budget.
20108
20195
  scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
20109
20196
  }),