@mutmutco/cli 2.68.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -17
  2. package/dist/main.cjs +455 -374
  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) {
4271
+ if (!pathContained(real, repoAnchor) || !rootCandidateStillAllowed(c, repoAnchor)) {
4262
4272
  result.skipped += 1;
4263
4273
  continue;
4264
4274
  }
4265
- const containment = rootScoped ? repoAnchor : anchor;
4266
- if (!containment || !pathContained(real, containment) || rootScoped && !rootCandidateStillAllowed(c, repoAnchor)) {
4267
- result.skipped += 1;
4268
- continue;
4269
- }
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,12 +9668,17 @@ 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) {
@@ -9981,8 +9982,8 @@ async function correlateRun(deps, args) {
9981
9982
  function correlateTenantRun(deps, since, titleIncludes) {
9982
9983
  return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch", titleIncludes });
9983
9984
  }
9984
- function correlatePublishRun(deps, since) {
9985
- return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
9985
+ function correlatePublishRun(deps, since, titleIncludes) {
9986
+ return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch", titleIncludes });
9986
9987
  }
9987
9988
  function correlateControlRun(deps, since, titleIncludes) {
9988
9989
  return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
@@ -10268,7 +10269,7 @@ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFail
10268
10269
  deployStatus: "failure"
10269
10270
  };
10270
10271
  }
10271
- const { runId, runUrl } = await correlatePublishRun(deps, since);
10272
+ const { runId, runUrl } = await correlatePublishRun(deps, since, [ctx.slug, stage2]);
10272
10273
  const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
10273
10274
  return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
10274
10275
  }
@@ -10408,7 +10409,7 @@ async function pushRcAlignment(deps) {
10408
10409
  await deps.run("git", ["push", "origin", "main:rc"]);
10409
10410
  return "origin/rc aligned to the released main";
10410
10411
  } catch (e) {
10411
- return `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
10412
+ 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
10413
  }
10413
10414
  }
10414
10415
  async function runTrainApplyPipeline(mode, input) {
@@ -11612,9 +11613,7 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
11612
11613
  var OWNER = "mutmutco";
11613
11614
  var LOCKED_APP = "mmi-github-app";
11614
11615
  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
- };
11616
+ var REQUIRED_DATA_ACCESS = {};
11618
11617
  function lockedBranches(repoClass, releaseTrack) {
11619
11618
  if (releaseTrack) return branchesForTrack(releaseTrack);
11620
11619
  return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
@@ -14326,7 +14325,7 @@ async function secretsUse(deps, key, opts) {
14326
14325
  ` \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
14326
  ` \u2022 Runtime / agents: read it keylessly at runtime via the box's OIDC role. Never bake it into an image or commit it.`,
14328
14327
  ` \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."
14328
+ 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
14329
  ].join("\n")
14331
14330
  );
14332
14331
  return;
@@ -14620,7 +14619,7 @@ function registerEdgeCommands(program3) {
14620
14619
  // src/doctor-run.ts
14621
14620
  var import_node_fs17 = require("node:fs");
14622
14621
  var import_node_child_process10 = require("node:child_process");
14623
- var import_promises3 = require("node:fs/promises");
14622
+ var import_promises2 = require("node:fs/promises");
14624
14623
  var import_node_path17 = require("node:path");
14625
14624
  var import_node_os6 = require("node:os");
14626
14625
 
@@ -14945,6 +14944,15 @@ function buildGitignoreManagedBlockCheck(input) {
14945
14944
  const { added, removed, seeded } = diffManagedGitignoreBlock(input.content);
14946
14945
  return { ...base, ok: false, contentToWrite: content, added, removed, seeded };
14947
14946
  }
14947
+ var REPO_LOCAL_WORKTREE_LABEL = "worktree location (canonical sibling path)";
14948
+ 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/`.";
14949
+ function buildRepoLocalWorktreeCheck(input) {
14950
+ return {
14951
+ ok: !(input.isOrgRepo && input.hasRepoLocalWorktrees),
14952
+ label: REPO_LOCAL_WORKTREE_LABEL,
14953
+ fix: REPO_LOCAL_WORKTREE_FIX
14954
+ };
14955
+ }
14948
14956
  var SCRATCH_GC_LABEL = "scratch housekeeping (tmp/, plans/, browser artifacts)";
14949
14957
  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
14958
  function buildScratchGcCheck(plan) {
@@ -15118,10 +15126,6 @@ var OPENCODE_WORKFLOW_COMMANDS = [
15118
15126
  "release",
15119
15127
  "hotfix",
15120
15128
  "bootstrap",
15121
- "grind",
15122
- "build",
15123
- "handoff",
15124
- "coop",
15125
15129
  "browser-automation"
15126
15130
  ];
15127
15131
  function opencodeCommandDescription(command) {
@@ -15377,6 +15381,21 @@ function buildOpencodeVersionCheck(input) {
15377
15381
  }
15378
15382
  return { ...base, ok: false, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
15379
15383
  }
15384
+ function pickOpencodeActiveVersion(input) {
15385
+ for (const value of [input.envStamp, input.cacheVersion, input.diskVersion]) {
15386
+ const trimmed = value?.trim();
15387
+ if (trimmed) return trimmed;
15388
+ }
15389
+ return void 0;
15390
+ }
15391
+ function planOpencodePluginCacheQuarantine(input) {
15392
+ if (!input.cacheExists) return { quarantine: false };
15393
+ if (!isSemverVersion2(input.cacheVersion) || !isSemverVersion2(input.releasedVersion)) return { quarantine: false };
15394
+ if (compareVersions(input.cacheVersion, input.releasedVersion) < 0) {
15395
+ return { quarantine: true, cacheVersion: input.cacheVersion, releasedVersion: input.releasedVersion };
15396
+ }
15397
+ return { quarantine: false };
15398
+ }
15380
15399
  var OPENCODE_HOOK_ACTIVE_LABEL = "OpenCode MMI adapter hooks active (shell.env stamp)";
15381
15400
  function buildOpencodeHookActiveCheck(input) {
15382
15401
  const base = { ok: true, label: OPENCODE_HOOK_ACTIVE_LABEL, fix: OPENCODE_RECOVERY };
@@ -16381,8 +16400,59 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
16381
16400
  return false;
16382
16401
  }
16383
16402
  }
16403
+ function opencodePackagesRoot() {
16404
+ return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
16405
+ }
16406
+ function opencodePluginCacheDirs() {
16407
+ const root = opencodePackagesRoot();
16408
+ try {
16409
+ 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));
16410
+ } catch {
16411
+ return [];
16412
+ }
16413
+ }
16414
+ function readOpencodeCacheDirVersion(cacheDir) {
16415
+ try {
16416
+ const parsed = JSON.parse(
16417
+ (0, import_node_fs17.readFileSync)((0, import_node_path17.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
16418
+ );
16419
+ return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
16420
+ } catch {
16421
+ return void 0;
16422
+ }
16423
+ }
16424
+ function readOpencodeLoadedCacheVersion() {
16425
+ const versions = opencodePluginCacheDirs().map(readOpencodeCacheDirVersion).filter((v) => Boolean(v));
16426
+ if (!versions.length) return void 0;
16427
+ return versions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
16428
+ }
16429
+ function quarantineStaleOpencodePluginCaches(releasedVersion, log) {
16430
+ let moved = false;
16431
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
16432
+ for (const cacheDir of opencodePluginCacheDirs()) {
16433
+ const plan = planOpencodePluginCacheQuarantine({
16434
+ cacheExists: true,
16435
+ cacheVersion: readOpencodeCacheDirVersion(cacheDir),
16436
+ releasedVersion
16437
+ });
16438
+ if (!plan.quarantine) continue;
16439
+ try {
16440
+ const quarantineRoot = (0, import_node_path17.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
16441
+ (0, import_node_fs17.mkdirSync)(quarantineRoot, { recursive: true });
16442
+ (0, import_node_fs17.renameSync)(cacheDir, (0, import_node_path17.join)(quarantineRoot, (0, import_node_path17.basename)(cacheDir)));
16443
+ log(` \u21BB quarantined stale OpenCode plugin cache ${plan.cacheVersion} (< ${plan.releasedVersion}) \u2014 OpenCode reinstalls the current adapter on next start`);
16444
+ moved = true;
16445
+ } catch {
16446
+ }
16447
+ }
16448
+ return moved;
16449
+ }
16384
16450
  function opencodeInstalledVersionForDoctor() {
16385
- return process.env.MMI_OPENCODE_PLUGIN_VERSION || readOpencodeAdapterDiskVersion();
16451
+ return pickOpencodeActiveVersion({
16452
+ envStamp: process.env.MMI_OPENCODE_PLUGIN_VERSION,
16453
+ cacheVersion: readOpencodeLoadedCacheVersion(),
16454
+ diskVersion: readOpencodeAdapterDiskVersion()
16455
+ });
16386
16456
  }
16387
16457
  function opencodePluginVersionsForReport() {
16388
16458
  return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
@@ -16820,6 +16890,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
16820
16890
  }
16821
16891
  }
16822
16892
  checks.push(gitignoreCheck);
16893
+ checks.push(buildRepoLocalWorktreeCheck({
16894
+ isOrgRepo,
16895
+ hasRepoLocalWorktrees: (0, import_node_fs17.existsSync)((0, import_node_path17.join)(process.cwd(), ".worktrees"))
16896
+ }));
16823
16897
  let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo, installed, surface });
16824
16898
  if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
16825
16899
  if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
@@ -16906,8 +16980,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
16906
16980
  releasedVersion
16907
16981
  });
16908
16982
  if (!opencodeVersionCheck.ok && repairFull) {
16909
- if (await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m))) {
16910
- opencodeInstalledVersion = readOpencodeAdapterDiskVersion() ?? opencodeInstalledVersion;
16983
+ const refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
16984
+ const quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
16985
+ if (refreshed || quarantined) {
16986
+ opencodeInstalledVersion = opencodeInstalledVersionForDoctor() ?? opencodeInstalledVersion;
16911
16987
  opencodeVersionCheck = buildOpencodeVersionCheck({
16912
16988
  isOrgRepo,
16913
16989
  installedVersion: opencodeInstalledVersion,
@@ -17080,7 +17156,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
17080
17156
  releasedVersion,
17081
17157
  hubCheckout: hubCheckoutForCursorSeed(),
17082
17158
  execFileP: execFileP2,
17083
- mkdtemp: (prefix) => (0, import_promises3.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
17159
+ mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
17084
17160
  log: (m) => io.err(m)
17085
17161
  });
17086
17162
  if (seeded) {
@@ -17323,7 +17399,6 @@ function repoFromSelector(selector) {
17323
17399
  async function loadConfigForBoardSelector(selector, repoOption) {
17324
17400
  return loadConfigForRepo(repoFromSelector(selector) ?? repoOption);
17325
17401
  }
17326
- var DEFAULT_RULES_SOURCE = "https://raw.githubusercontent.com/mutmutco/MMI-Hub/development";
17327
17402
  function renderGcApplyResult(result) {
17328
17403
  const lines = ["gc apply result:"];
17329
17404
  lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
@@ -17439,65 +17514,11 @@ async function requireFreshTrainCli(commandName) {
17439
17514
  releasedVersion: await fetchNpmReleasedVersion()
17440
17515
  });
17441
17516
  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`);
17517
+ throw new Error(staleTrainCliMessage(report, commandName));
17443
17518
  }
17444
17519
  var program2 = new Command();
17445
17520
  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
- });
17521
+ var rules = program2.command("rules").description("org-managed .gitignore delivery");
17501
17522
  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
17523
  const path2 = (0, import_node_path18.join)(process.cwd(), ".gitignore");
17503
17524
  const current = (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null;
@@ -17519,30 +17540,6 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
17519
17540
  console.log("mmi-cli rules gitignore: up to date");
17520
17541
  }
17521
17542
  });
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
17543
  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
17544
  const manifest = buildCommandManifest(program2);
17548
17545
  consoleIo.log(o.json ? JSON.stringify(manifest, null, 2) : formatManifestHuman(manifest));
@@ -18453,8 +18450,8 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
18453
18450
  let northStarSlug;
18454
18451
  let extraLabels = [];
18455
18452
  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 });
18453
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
18454
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18458
18455
  if (o.northStar !== void 0) {
18459
18456
  northStarSlug = normalizeNorthStarSlug(o.northStar);
18460
18457
  body = appendNorthStarLine(body, northStarSlug);
@@ -18511,6 +18508,20 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
18511
18508
  ...parentLinkFields(parent, parentLinkError)
18512
18509
  }));
18513
18510
  });
18511
+ 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) => {
18512
+ const n = Number(number);
18513
+ if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
18514
+ const repo = await resolveRepo(o.repo);
18515
+ if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
18516
+ const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,labels,author,assignees,milestone,body";
18517
+ try {
18518
+ const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", fields]);
18519
+ console.log(JSON.stringify(data));
18520
+ } catch (e) {
18521
+ const err = e;
18522
+ return fail(`issue view: ${(err.stderr || err.message || String(e)).trim()}`);
18523
+ }
18524
+ });
18514
18525
  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
18526
  const number = Number(o.number);
18516
18527
  if (!Number.isInteger(number) || number <= 0) return fail("issue discover-related: --number must be a positive integer");
@@ -18566,7 +18577,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
18566
18577
  }
18567
18578
  let body;
18568
18579
  try {
18569
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises4.readFile, readStdin });
18580
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18570
18581
  } catch (e) {
18571
18582
  return fail(`issue comment: ${e.message}`);
18572
18583
  }
@@ -18628,8 +18639,8 @@ program2.command("report").description("file a friction report on the Hub board
18628
18639
  const targetRepo2 = o.repo ?? HUB_REPO2;
18629
18640
  const sourceRepo = await resolveRepo(void 0);
18630
18641
  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 });
18642
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
18643
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18633
18644
  priority = normalizePriority(o.priority);
18634
18645
  args = buildIssueArgs({
18635
18646
  type: o.type,
@@ -18702,8 +18713,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
18702
18713
  let args;
18703
18714
  try {
18704
18715
  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 });
18716
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18717
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
18707
18718
  title = buildSkillLessonTitle(skill, rawTitle);
18708
18719
  priority = normalizePriority(o.priority);
18709
18720
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -18754,14 +18765,28 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
18754
18765
  let body;
18755
18766
  let title;
18756
18767
  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 });
18768
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
18769
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
18759
18770
  } catch (e) {
18760
18771
  return fail(`pr create: ${e.message}`);
18761
18772
  }
18762
18773
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
18763
18774
  console.log(JSON.stringify(created));
18764
18775
  });
18776
+ 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) => {
18777
+ const n = Number(number);
18778
+ if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
18779
+ const repo = await resolveRepo(o.repo);
18780
+ if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
18781
+ const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
18782
+ try {
18783
+ const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", fields]);
18784
+ console.log(JSON.stringify(data));
18785
+ } catch (e) {
18786
+ const err = e;
18787
+ return fail(`pr view: ${(err.stderr || err.message || String(e)).trim()}`);
18788
+ }
18789
+ });
18765
18790
  async function listCiWorkflowPaths(cwd = process.cwd()) {
18766
18791
  const wfDir = (0, import_node_path18.join)(cwd, ".github", "workflows");
18767
18792
  if (!(0, import_node_fs18.existsSync)(wfDir)) return [];
@@ -18930,15 +18955,15 @@ async function createDeferredWorktreeStore() {
18930
18955
  return {
18931
18956
  read: async () => {
18932
18957
  try {
18933
- return parseDeferredWorktreesFile(await (0, import_promises4.readFile)(registryPath, "utf8"));
18958
+ return parseDeferredWorktreesFile(await (0, import_promises3.readFile)(registryPath, "utf8"));
18934
18959
  } catch {
18935
18960
  return [];
18936
18961
  }
18937
18962
  },
18938
18963
  write: async (entries) => {
18939
18964
  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");
18965
+ await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
18966
+ await (0, import_promises3.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
18942
18967
  } catch {
18943
18968
  }
18944
18969
  }
@@ -18979,7 +19004,7 @@ var realWorktreeDirRemover = {
18979
19004
  (0, import_node_fs18.unlinkSync)(p);
18980
19005
  }
18981
19006
  },
18982
- removeTree: (p) => (0, import_promises4.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
19007
+ removeTree: (p) => (0, import_promises3.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
18983
19008
  };
18984
19009
  async function resolvePrimaryCheckout(execGit) {
18985
19010
  try {
@@ -19037,9 +19062,9 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
19037
19062
  const beforeWorktrees = parseWorktreePorcelain(
19038
19063
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
19039
19064
  );
19040
- const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists2(headRef);
19065
+ const remoteBefore = await remoteBranchExists2(headRef);
19041
19066
  let remoteDeleteAttempted = false;
19042
- let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
19067
+ let remoteNotAttemptedReason;
19043
19068
  await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
19044
19069
  const message = String(e.message || "");
19045
19070
  if (/already been merged/i.test(message)) {
@@ -19061,10 +19086,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
19061
19086
  }
19062
19087
  }
19063
19088
  if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
19064
- const remoteBranch = repoArgs.length ? buildRemoteBranchCleanupReport(headRef, {
19065
- attempted: false,
19066
- reason: remoteNotAttemptedReason
19067
- }) : await buildPrMergeRemoteBranchCleanupReport(headRef, {
19089
+ const remoteBranch = await buildPrMergeRemoteBranchCleanupReport(headRef, {
19068
19090
  exists: remoteBranchExists2
19069
19091
  }, {
19070
19092
  attempted: remoteDeleteAttempted,
@@ -19074,11 +19096,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
19074
19096
  const deferredStore = await createDeferredWorktreeStore();
19075
19097
  let localCleanup;
19076
19098
  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, {
19099
+ localCleanup = await cleanupPrMergeLocalBranch(headRef, {
19082
19100
  beforeWorktrees,
19083
19101
  startingPath,
19084
19102
  pathExists: (p) => (0, import_node_fs18.existsSync)(p),
@@ -19205,6 +19223,30 @@ board.command("backfill-priority").description("set board Priority from priority
19205
19223
  return failGraceful(`board backfill-priority failed: ${e.message}`);
19206
19224
  }
19207
19225
  });
19226
+ 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) => {
19227
+ let priority;
19228
+ try {
19229
+ priority = normalizePriority(priorityArg);
19230
+ } catch (e) {
19231
+ return fail(`board set-priority failed: ${e.message}`);
19232
+ }
19233
+ try {
19234
+ const defaultRepo = await resolveRepo(o.repo) ?? "";
19235
+ const selector = parseIssueSelector(issueRef, defaultRepo);
19236
+ if (!selector.repo) {
19237
+ return fail("board set-priority failed: could not resolve the repo \u2014 pass owner/repo#123 or use --repo");
19238
+ }
19239
+ const result = await setBoardPriorityForSelector({
19240
+ config: await loadConfigForBoardSelector(issueRef, o.repo),
19241
+ selector,
19242
+ priority
19243
+ });
19244
+ if (o.json) return console.log(JSON.stringify(result));
19245
+ console.log(`Set ${result.ref} Priority -> ${result.priority}`);
19246
+ } catch (e) {
19247
+ return failGraceful(`board set-priority failed: ${e.message}`);
19248
+ }
19249
+ });
19208
19250
  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
19251
  try {
19210
19252
  const result = await prunePriorityLabels({
@@ -19286,14 +19328,11 @@ function stageKeepAlive() {
19286
19328
  }
19287
19329
  async function resolveStage() {
19288
19330
  const cfg = await loadConfig();
19289
- const local = cfg.stage;
19290
19331
  const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch((e) => ({ ok: false, error: e.message }));
19291
19332
  const project2 = read.ok ? read.project : null;
19292
19333
  const portRangeMeta = project2?.portRange ?? void 0;
19293
19334
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
19294
19335
  return decideStage({
19295
- local,
19296
- shell: shellFor(),
19297
19336
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
19298
19337
  hasCompose: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), "docker-compose.yml")),
19299
19338
  hasEnvExample: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), ".env.example"))
@@ -19317,19 +19356,8 @@ async function fetchStageVaultEnvMerge() {
19317
19356
  }
19318
19357
  function stageStepsFor(res, stops = true) {
19319
19358
  if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
19320
- if (res.source === "local") return stagePlan(res.config ?? {}, stops);
19321
19359
  return [{ label: `no local stage to run \u2014 ${res.gap ?? "stage config gap"}` }];
19322
19360
  }
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
19361
  function reportedStageUrl(res, result) {
19334
19362
  if (!res.derived) return void 0;
19335
19363
  return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
@@ -19420,7 +19448,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
19420
19448
  const res = await resolveStage();
19421
19449
  if (o.apply) {
19422
19450
  if (res.source === "none") return failGraceful(`stage: ${res.gap}`);
19423
- const cfg = res.config ?? res.derived.config;
19451
+ const cfg = res.derived.config;
19424
19452
  const hold = stageKeepAlive();
19425
19453
  try {
19426
19454
  const result = await runStage(cfg, stageScopedRunOpts({ timeoutMs: o.timeoutMs }));
@@ -19434,9 +19462,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
19434
19462
  }
19435
19463
  }
19436
19464
  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);
19465
+ if (o.json) return console.log(JSON.stringify({ command: "stage", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
19440
19466
  console.log(renderSteps("mmi-cli stage: dry-run plan", steps));
19441
19467
  });
19442
19468
  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 +19483,12 @@ stage.command("start").description("start the configured local stage process and
19457
19483
  const res = await resolveStage();
19458
19484
  if (!o.apply) {
19459
19485
  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);
19486
+ if (o.json) return printLine(JSON.stringify({ command: "stage start", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
19463
19487
  return printLine(renderSteps("mmi-cli stage start: dry-run plan", steps));
19464
19488
  }
19465
19489
  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;
19490
+ const cfg = res.derived.config;
19491
+ const vaultEnvMerge = await fetchStageVaultEnvMerge();
19468
19492
  try {
19469
19493
  const hold = stageKeepAlive();
19470
19494
  let printed = false;
@@ -19492,14 +19516,12 @@ stage.command("run").description("force-stop previous stage, build, start, and h
19492
19516
  const res = await resolveStage();
19493
19517
  if (!o.apply) {
19494
19518
  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);
19519
+ if (o.json) return printLine(JSON.stringify({ command: "stage run", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
19498
19520
  return printLine(renderSteps("mmi-cli stage run: dry-run plan", steps));
19499
19521
  }
19500
19522
  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;
19523
+ const cfg = res.derived.config;
19524
+ const vaultEnvMerge = await fetchStageVaultEnvMerge();
19503
19525
  try {
19504
19526
  const hold = stageKeepAlive();
19505
19527
  let printed = false;
@@ -19569,8 +19591,8 @@ function trainApplyDeps() {
19569
19591
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
19570
19592
  announce: (args) => announceRelease({
19571
19593
  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)
19594
+ readFile: (path2) => (0, import_promises3.readFile)(path2, "utf8"),
19595
+ removeFile: (path2) => (0, import_promises3.unlink)(path2)
19574
19596
  }, args),
19575
19597
  fetchEdgeDomains: async (slug) => {
19576
19598
  const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
@@ -19635,7 +19657,9 @@ for (const commandName of ["rcand", "release"]) {
19635
19657
  return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
19636
19658
  }
19637
19659
  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`);
19660
+ const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
19661
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
19662
+ if (!guard.ok) return fail(`${commandName}: ${guard.message}`);
19639
19663
  }
19640
19664
  if (o.apply) {
19641
19665
  try {
@@ -19788,6 +19812,14 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
19788
19812
  console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
19789
19813
  if (!report.ok) process.exitCode = 1;
19790
19814
  });
19815
+ 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 () => {
19816
+ const json = rawFlag("--json");
19817
+ const live = await fetchOrgNoAgentFilesRuleset(defaultGitHubClient());
19818
+ const plan = planOrgNoAgentFilesRuleset(live);
19819
+ if (json) console.log(JSON.stringify({ action: plan.action, id: plan.id, drift: plan.drift, desired: plan.desired }, null, 2));
19820
+ else console.log(renderOrgRulesetDriftReport(plan));
19821
+ if (plan.action !== "noop") process.exitCode = 1;
19822
+ });
19791
19823
  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
19824
  const o = {
19793
19825
  class: rawValue("--class", "deployable"),
@@ -19811,7 +19843,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19811
19843
  const baseBranch = o.class === "content" ? "main" : "development";
19812
19844
  const slug = parsedRepo.slug;
19813
19845
  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;
19846
+ const readFile2 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
19815
19847
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
19816
19848
  const rawVars = {};
19817
19849
  for (const value of rawValues("--var")) {
@@ -19844,6 +19876,25 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19844
19876
  applyDeployModel = payload.deployModel;
19845
19877
  } catch {
19846
19878
  }
19879
+ let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
19880
+ let seededToBranch = 0;
19881
+ if (o.execute) {
19882
+ let branchRules = [];
19883
+ try {
19884
+ branchRules = JSON.parse((await gh(["api", `repos/${repo}/rules/branches/${baseBranch}`])).stdout || "[]");
19885
+ } catch {
19886
+ branchRules = [];
19887
+ }
19888
+ seedPlan = planSeedDelivery(branchRules, slug, baseBranch);
19889
+ if (seedPlan.branch) {
19890
+ const headSha = (await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"])).stdout.trim();
19891
+ try {
19892
+ await gh(["api", "-X", "POST", `repos/${repo}/git/refs`, "-f", `ref=refs/heads/${seedPlan.branch}`, "-f", `sha=${headSha}`]);
19893
+ } catch (e) {
19894
+ if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
19895
+ }
19896
+ }
19897
+ }
19847
19898
  for (const seed of manifest.seeds) {
19848
19899
  if (!seed.classes.includes(o.class)) continue;
19849
19900
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
@@ -19853,7 +19904,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19853
19904
  let remoteContent = null;
19854
19905
  if (resolved.source !== "fanout") {
19855
19906
  try {
19856
- const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${baseBranch}`]);
19907
+ const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedPlan.ref}`]);
19857
19908
  exists = true;
19858
19909
  try {
19859
19910
  const parsed = JSON.parse(r.stdout);
@@ -19869,13 +19920,44 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19869
19920
  }
19870
19921
  const planned = planSeedAction(resolved, exists);
19871
19922
  const isBlock = resolved.source === "managed-block";
19872
- const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile3) : null;
19923
+ const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile2) : null;
19873
19924
  const action = reconcileSeedAction(planned, content, isBlock);
19874
19925
  actions.push(action);
19875
19926
  if (o.execute && (action.action === "create" || action.action === "update")) {
19876
- await gh(contentPutArgs(repo, resolved.target, content, baseBranch, action.action === "update" ? sha : void 0));
19927
+ await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
19877
19928
  applied.push(`${action.action} ${resolved.target}`);
19929
+ if (seedPlan.mode === "pr") seededToBranch++;
19930
+ }
19931
+ }
19932
+ let seedPrUrl;
19933
+ if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
19934
+ await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
19935
+ });
19936
+ const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
19937
+ const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
19938
+ if (prDecision.action === "reuse") {
19939
+ seedPrUrl = prDecision.url;
19940
+ } else {
19941
+ const created = await ghCreate([
19942
+ "pr",
19943
+ "create",
19944
+ "--repo",
19945
+ repo,
19946
+ "--base",
19947
+ baseBranch,
19948
+ "--head",
19949
+ seedPlan.branch,
19950
+ "--title",
19951
+ `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
19952
+ "--body",
19953
+ `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").`
19954
+ ]);
19955
+ seedPrUrl = created.url;
19878
19956
  }
19957
+ await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
19958
+ if (!/already/i.test(String(e.message ?? ""))) throw e;
19959
+ });
19960
+ applied.push(`seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)`);
19879
19961
  }
19880
19962
  if (o.execute && o.class === "deployable") {
19881
19963
  try {
@@ -19886,7 +19968,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19886
19968
  }
19887
19969
  const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
19888
19970
  if (rulesetSeed) {
19889
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile3);
19971
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile2);
19890
19972
  if (rulesetContent) {
19891
19973
  try {
19892
19974
  const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
@@ -19999,9 +20081,9 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19999
20081
  "--head",
20000
20082
  branchName,
20001
20083
  "--title",
20002
- `bootstrap: register ${parsedRepo.name} for org-spine fanout`,
20084
+ `bootstrap: register ${parsedRepo.name} for the org-managed .gitignore fanout`,
20003
20085
  "--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.`
20086
+ `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
20087
  ]);
20006
20088
  fanoutPrUrl = created.url;
20007
20089
  }
@@ -20014,7 +20096,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
20014
20096
  return failGraceful(`bootstrap apply: fanout registration failed: ${e.message}`);
20015
20097
  }
20016
20098
  }
20017
- if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
20099
+ 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
20100
  else {
20019
20101
  console.log(renderSeedPlan(actions));
20020
20102
  if (o.execute) console.log(`
@@ -20081,13 +20163,12 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
20081
20163
  ));
20082
20164
  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
20165
  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 () => {
20166
+ program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
20085
20167
  if (isInsideRepoSubdir(process.cwd())) {
20086
20168
  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
20169
  return;
20088
20170
  }
20089
20171
  if (!await isOrgRepoRoot()) return;
20090
- spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
20091
20172
  spawnDeferredGcSweep();
20092
20173
  const { parallel, sequential } = buildSessionStartPlan({
20093
20174
  // whoami (#879): surface the resolved human so agents act --for them without asking. Silent
@@ -20103,7 +20184,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
20103
20184
  boardSlice: (io) => runBoardSlice(io, {
20104
20185
  loadConfig: () => loadConfigForRepo(),
20105
20186
  readBoard,
20106
- // #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
20187
+ // #1813: warm the slice cache out-of-band (detached) so the ~20s live read
20107
20188
  // never costs banner time and next session's glance renders instantly within budget.
20108
20189
  scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
20109
20190
  }),