@mutmutco/cli 2.39.0 → 2.40.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/dist/main.cjs +2114 -1063
  2. package/dist/saga.cjs +4 -5
  3. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3391,8 +3391,8 @@ function useColor() {
3391
3391
  var program = new Command();
3392
3392
 
3393
3393
  // src/index.ts
3394
- var import_promises6 = require("node:fs/promises");
3395
- var import_node_fs19 = require("node:fs");
3394
+ var import_promises7 = require("node:fs/promises");
3395
+ var import_node_fs22 = require("node:fs");
3396
3396
 
3397
3397
  // src/rules-sync.ts
3398
3398
  function normalizeEol(s) {
@@ -3423,7 +3423,7 @@ function resolveRulesBase(orgRulesSource, defaultBase) {
3423
3423
  }
3424
3424
 
3425
3425
  // src/index.ts
3426
- var import_node_child_process10 = require("node:child_process");
3426
+ var import_node_child_process12 = require("node:child_process");
3427
3427
 
3428
3428
  // src/cli-shared.ts
3429
3429
  var import_promises = require("node:fs/promises");
@@ -3509,7 +3509,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3509
3509
  const attempts = opts.attempts ?? 3;
3510
3510
  const baseDelayMs = opts.baseDelayMs ?? 250;
3511
3511
  const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
3512
- const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3512
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3513
3513
  let lastErr;
3514
3514
  for (let i = 0; i < attempts; i++) {
3515
3515
  const isLast = i === attempts - 1;
@@ -3517,26 +3517,25 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3517
3517
  try {
3518
3518
  const res = await fetchImpl(url, attemptInit);
3519
3519
  if (!isLast && retryOn(res)) {
3520
- await sleep(baseDelayMs * 2 ** i);
3520
+ await sleep2(baseDelayMs * 2 ** i);
3521
3521
  continue;
3522
3522
  }
3523
3523
  return res;
3524
3524
  } catch (e) {
3525
3525
  lastErr = e;
3526
3526
  if (isLast) throw e;
3527
- await sleep(baseDelayMs * 2 ** i);
3527
+ await sleep2(baseDelayMs * 2 ** i);
3528
3528
  }
3529
3529
  }
3530
3530
  throw lastErr;
3531
3531
  }
3532
3532
 
3533
3533
  // src/clean-exit.ts
3534
+ var UNDICI_GLOBAL_DISPATCHER_SYMBOL = Object.getOwnPropertySymbols(globalThis).find(
3535
+ (s) => s.description === "undici.globalDispatcher.1" || s.description?.startsWith("undici.globalDispatcher.")
3536
+ ) ?? /* @__PURE__ */ Symbol.for("undici.globalDispatcher.1");
3534
3537
  function globalDispatcher() {
3535
- const g = globalThis;
3536
- const sym = Object.getOwnPropertySymbols(g).find(
3537
- (s) => s.description === "undici.globalDispatcher.1" || s.description?.startsWith("undici.globalDispatcher.")
3538
- );
3539
- return sym ? g[sym] : void 0;
3538
+ return globalThis[UNDICI_GLOBAL_DISPATCHER_SYMBOL];
3540
3539
  }
3541
3540
  function destroyHttpPool() {
3542
3541
  try {
@@ -4361,15 +4360,18 @@ function cursorProjectSlug(workspaceRoot) {
4361
4360
  function cursorProjectsRoot(env) {
4362
4361
  return (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0, import_node_os3.homedir)(), ".cursor", "projects");
4363
4362
  }
4364
- function resolveClaudeTranscriptPath(hook, env = process.env) {
4363
+ function claudeTranscriptCandidate(hook, env = process.env) {
4365
4364
  const sessionId = hook.session_id?.trim();
4366
4365
  if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return void 0;
4367
4366
  const cwd = hook.cwd?.trim() || env.CLAUDE_PROJECT_DIR?.trim();
4368
4367
  if (!cwd) return void 0;
4369
4368
  const encoded = cwd.replace(/[^A-Za-z0-9]/g, "-");
4370
4369
  const root = (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0, import_node_os3.homedir)(), ".claude", "projects");
4371
- const candidate = (0, import_node_path6.join)(root, encoded, `${sessionId}.jsonl`);
4372
- return (0, import_node_fs8.existsSync)(candidate) ? candidate : void 0;
4370
+ return (0, import_node_path6.join)(root, encoded, `${sessionId}.jsonl`);
4371
+ }
4372
+ function resolveClaudeTranscriptPath(hook, env = process.env) {
4373
+ const candidate = claudeTranscriptCandidate(hook, env);
4374
+ return candidate && (0, import_node_fs8.existsSync)(candidate) ? candidate : void 0;
4373
4375
  }
4374
4376
  function resolveCursorTranscriptPath(hook, env = process.env) {
4375
4377
  const conversationId = (hook.conversation_id ?? hook.conversationId ?? hook.session_id)?.trim();
@@ -5451,9 +5453,16 @@ function clearIngestSkip() {
5451
5453
  } catch {
5452
5454
  }
5453
5455
  }
5456
+ var INGEST_SKIP_REASON_MESSAGES = {
5457
+ "no-transcript": "no transcript path \u2014 hook payload lacks transcript_path and adapter found no file",
5458
+ "transcript-empty": "transcript had no user/assistant text",
5459
+ "transcript-unreadable": "transcript unreadable",
5460
+ "note-empty": "note source was empty"
5461
+ };
5454
5462
  function formatIngestSkip(record) {
5455
5463
  if (!record) return void 0;
5456
- const base2 = record.reason === "no-transcript" ? `last ingest skipped (${record.surface}): no transcript path \u2014 hook payload lacks transcript_path and adapter found no file` : record.reason === "transcript-empty" ? `last ingest skipped (${record.surface}): transcript had no user/assistant text` : record.reason === "transcript-unreadable" ? `last ingest skipped (${record.surface}): transcript unreadable` : record.reason === "note-empty" ? `last ingest skipped (${record.surface}): note source was empty` : `last ingest skipped (${record.surface}): ${record.reason}`;
5464
+ const suffix = INGEST_SKIP_REASON_MESSAGES[record.reason] ?? record.reason;
5465
+ const base2 = `last ingest skipped (${record.surface}): ${suffix}`;
5457
5466
  return record.detail ? `${base2} \u2014 ${record.detail}` : base2;
5458
5467
  }
5459
5468
 
@@ -5630,8 +5639,54 @@ async function secretsList(deps, opts) {
5630
5639
  deps.err(await upgradeMessage(res) ?? `secrets list failed: HTTP ${res.status}${await readErr(res)}`);
5631
5640
  return;
5632
5641
  }
5633
- const { secrets: secrets2 } = await res.json();
5634
- deps.log(formatSecretList(secrets2 ?? []));
5642
+ const { secrets } = await res.json();
5643
+ deps.log(formatSecretList(secrets ?? []));
5644
+ }
5645
+ function formatCapabilities(r) {
5646
+ const head = `@${r.login} on ${r.repo} \u2014 ${r.role}`;
5647
+ const items = [...r.capabilities ?? []].sort((a, b) => a.scope.localeCompare(b.scope));
5648
+ if (!items.length) return `${head}
5649
+
5650
+ no vault credentials visible`;
5651
+ const width = Math.max(...items.map((i) => i.scope.length));
5652
+ const lines = items.map(
5653
+ (i) => `${i.accessible ? "+" : " "} ${i.scope.padEnd(width)} ${i.tier.padEnd(7)} ${i.reason}`
5654
+ );
5655
+ const reachable = items.filter((i) => i.accessible).length;
5656
+ return [
5657
+ head,
5658
+ "",
5659
+ `${reachable}/${items.length} vault credential(s) reachable (names + tier + scope \u2014 values are never shown):`,
5660
+ "",
5661
+ ...lines,
5662
+ "",
5663
+ "+ = readable/usable now. Read one value: `mmi-cli secrets get <stage>/<KEY>` (only `get` prints a value)."
5664
+ ].join("\n");
5665
+ }
5666
+ async function secretsCapabilities(deps, opts) {
5667
+ const repo = await targetRepo(deps, opts);
5668
+ const qs = new URLSearchParams({ repo }).toString();
5669
+ let res;
5670
+ try {
5671
+ res = await deps.fetch(`${deps.apiUrl}/secrets/capabilities?${qs}`, {
5672
+ method: "GET",
5673
+ headers: await deps.headers(),
5674
+ signal: AbortSignal.timeout(TIMEOUT_MS)
5675
+ });
5676
+ } catch (e) {
5677
+ deps.err(`access capabilities: ${e.message}`);
5678
+ return;
5679
+ }
5680
+ if (!res.ok) {
5681
+ deps.err(await upgradeMessage(res) ?? `access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
5682
+ return;
5683
+ }
5684
+ const report = await res.json();
5685
+ if (opts.json) {
5686
+ deps.log(JSON.stringify(report, null, 2));
5687
+ return;
5688
+ }
5689
+ deps.log(formatCapabilities(report));
5635
5690
  }
5636
5691
  var DEFAULT_RUNTIME_SECRET_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
5637
5692
  function stringList(v) {
@@ -5663,8 +5718,8 @@ async function secretsPreflight(deps, opts) {
5663
5718
  deps.err(await upgradeMessage(res) ?? `secrets preflight failed: HTTP ${res.status}${await readErr(res)}`);
5664
5719
  return false;
5665
5720
  }
5666
- const { secrets: secrets2 } = await res.json();
5667
- const present = new Set((secrets2 ?? []).map((s) => s.key));
5721
+ const { secrets } = await res.json();
5722
+ const present = new Set((secrets ?? []).map((s) => s.key));
5668
5723
  const required = opts.required.map((key) => stageKey(opts.stage, key));
5669
5724
  const missing = required.filter((key) => !present.has(key));
5670
5725
  if (missing.length) {
@@ -6104,9 +6159,16 @@ function parseHonchoQueueStatus(json) {
6104
6159
  const inProgressRaw = o.in_progress_work_units;
6105
6160
  const pending = typeof pendingRaw === "number" ? pendingRaw : null;
6106
6161
  const inProgress = typeof inProgressRaw === "number" ? inProgressRaw : null;
6107
- const stalled = (pending ?? 0) > 0 && (inProgress ?? 0) === 0;
6162
+ const stalled = isDeriverStalledSnapshot(pending, inProgress);
6108
6163
  return { pending, inProgress, stalled };
6109
6164
  }
6165
+ function isDeriverStalledSnapshot(pending, inProgress) {
6166
+ return (pending ?? 0) > 0 && (inProgress ?? 0) === 0;
6167
+ }
6168
+ var DERIVER_STALL_CONFIRM_MS = 750;
6169
+ function sleep(ms) {
6170
+ return new Promise((resolve) => setTimeout(resolve, ms));
6171
+ }
6110
6172
  var enc = encodeURIComponent;
6111
6173
  var base = (apiUrl) => apiUrl.replace(/\/+$/, "");
6112
6174
  var honchoRoutes = {
@@ -6131,8 +6193,9 @@ async function request(cfg, fetchImpl, method, path2, body, timeoutMs) {
6131
6193
  signal: AbortSignal.timeout(timeoutMs)
6132
6194
  });
6133
6195
  }
6196
+ var HONCHO_CORRELATION_MARKER = /\bHONCHO_\d/;
6134
6197
  function formatPeerCardLines(lines, maxChars = DEFAULT_HONCHO_CARD_MAX_CHARS) {
6135
- const card = Array.isArray(lines) ? lines.filter((s) => typeof s === "string" && s.trim().length > 0).join("\n").trim() : "";
6198
+ const card = Array.isArray(lines) ? lines.filter((s) => typeof s === "string" && s.trim().length > 0 && !HONCHO_CORRELATION_MARKER.test(s)).join("\n").trim() : "";
6136
6199
  if (!card) return null;
6137
6200
  return capContent(card, maxChars);
6138
6201
  }
@@ -6239,6 +6302,23 @@ async function probeHoncho(cfg, fetchImpl = fetch, timeoutMs = 3e3, opts = {}) {
6239
6302
  }
6240
6303
  }
6241
6304
  }
6305
+ if (queue.stalled) {
6306
+ await sleep(DERIVER_STALL_CONFIRM_MS);
6307
+ const confirmRes = await request(
6308
+ cfg,
6309
+ fetchImpl,
6310
+ "GET",
6311
+ honchoRoutes.queueStatus(cfg.workspace),
6312
+ void 0,
6313
+ timeoutMs
6314
+ );
6315
+ if (confirmRes.ok) {
6316
+ try {
6317
+ queue = parseHonchoQueueStatus(await confirmRes.json());
6318
+ } catch {
6319
+ }
6320
+ }
6321
+ }
6242
6322
  return { reachable: true, status: healthRes.status, authOk, authStatus: authRes.status, queue };
6243
6323
  } catch {
6244
6324
  return { reachable: false, authOk: false, queue: emptyQueue };
@@ -6351,11 +6431,14 @@ async function runHonchoIngest(opts) {
6351
6431
  }
6352
6432
  }
6353
6433
  if (!messages.length) {
6434
+ const hint = ingestTranscriptFallbackHint(surface);
6435
+ const tried = !transcriptPath && surface !== "cursor" ? claudeTranscriptCandidate(hook) : void 0;
6436
+ const detail = [hint, tried ? `tried: ${tried}` : void 0].filter(Boolean).join(" \u2014 ") || void 0;
6354
6437
  recordIngestSkip({
6355
6438
  reason: transcriptPath ? "transcript-empty" : "no-transcript",
6356
6439
  surface,
6357
6440
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6358
- detail: ingestTranscriptFallbackHint(surface)
6441
+ detail
6359
6442
  });
6360
6443
  return;
6361
6444
  }
@@ -6571,9 +6654,24 @@ function registerHonchoCommands(program3) {
6571
6654
  }
6572
6655
 
6573
6656
  // src/throttle-commands.ts
6657
+ var import_node_child_process6 = require("node:child_process");
6574
6658
  var import_node_fs11 = require("node:fs");
6575
6659
  var import_node_path9 = require("node:path");
6576
- var THROTTLE_TRACE_PATH = (0, import_node_path9.join)(".mmi", "throttle", "trace.jsonl");
6660
+ var THROTTLE_TRACE_REL = (0, import_node_path9.join)(".mmi", "throttle", "trace.jsonl");
6661
+ function resolveRepoGitRoot(cwd = process.cwd()) {
6662
+ try {
6663
+ const root = (0, import_node_child_process6.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
6664
+ encoding: "utf8",
6665
+ timeout: 5e3
6666
+ }).trim();
6667
+ return root || cwd;
6668
+ } catch {
6669
+ return cwd;
6670
+ }
6671
+ }
6672
+ function resolveThrottleTracePath(cwd = process.cwd()) {
6673
+ return (0, import_node_path9.join)(resolveRepoGitRoot(cwd), THROTTLE_TRACE_REL);
6674
+ }
6577
6675
  function resolveModeFromEnv() {
6578
6676
  const v = String(process.env.MMI_THROTTLE_MODE ?? "block").trim().toLowerCase();
6579
6677
  if (v === "observe") return "observe";
@@ -6615,7 +6713,7 @@ function summarizeTrace(entries) {
6615
6713
  }
6616
6714
  return { denials, readBytesWouldBlock, byTool, byReason, bySurface, byMode };
6617
6715
  }
6618
- function runThrottleReport(io, tracePath = THROTTLE_TRACE_PATH) {
6716
+ function runThrottleReport(io, tracePath = resolveThrottleTracePath()) {
6619
6717
  const mode = resolveModeFromEnv();
6620
6718
  if (!(0, import_node_fs11.existsSync)(tracePath)) {
6621
6719
  io.log(`Throttle: no trace at ${tracePath} (gates have not denied anything yet).`);
@@ -6840,7 +6938,7 @@ function applyScratchGc(plan2, mmiRoot, now = Date.now()) {
6840
6938
  function collectScratchSnapshot(repoRoot, deps = {}) {
6841
6939
  const readdir = deps.readdir ?? import_node_fs12.readdirSync;
6842
6940
  const stat = deps.stat ?? import_node_fs12.statSync;
6843
- const readFile6 = deps.readFile ?? import_node_fs12.readFileSync;
6941
+ const readFile7 = deps.readFile ?? import_node_fs12.readFileSync;
6844
6942
  const mmiRoot = (0, import_node_path10.join)(repoRoot, ".mmi");
6845
6943
  const plansRoot = (0, import_node_path10.join)(repoRoot, "plans");
6846
6944
  const mmiFiles = [];
@@ -6873,7 +6971,7 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
6873
6971
  let syncQueueSlugs = null;
6874
6972
  let queueRaw;
6875
6973
  try {
6876
- queueRaw = readFile6((0, import_node_path10.join)(plansRoot, ".sync-queue.json"), "utf8");
6974
+ queueRaw = readFile7((0, import_node_path10.join)(plansRoot, ".sync-queue.json"), "utf8");
6877
6975
  } catch {
6878
6976
  syncQueueSlugs = /* @__PURE__ */ new Set();
6879
6977
  }
@@ -6995,7 +7093,7 @@ function northstarPointer(injected = false) {
6995
7093
  }
6996
7094
 
6997
7095
  // src/board.ts
6998
- var import_node_child_process6 = require("node:child_process");
7096
+ var import_node_child_process7 = require("node:child_process");
6999
7097
  var import_node_util5 = require("node:util");
7000
7098
 
7001
7099
  // src/board-priority.ts
@@ -7103,7 +7201,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
7103
7201
  var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
7104
7202
 
7105
7203
  // src/board.ts
7106
- var rawExecFileP3 = (0, import_node_util5.promisify)(import_node_child_process6.execFile);
7204
+ var rawExecFileP3 = (0, import_node_util5.promisify)(import_node_child_process7.execFile);
7107
7205
  var BOARD_GIT_TIMEOUT_MS = 1e4;
7108
7206
  var WRITE_PROBE_CONCURRENCY = 8;
7109
7207
  var CLAIM_CONCURRENCY = 5;
@@ -8292,7 +8390,7 @@ async function runNorthstarContext(io, deps) {
8292
8390
  }
8293
8391
 
8294
8392
  // src/index.ts
8295
- var import_node_path17 = require("node:path");
8393
+ var import_node_path19 = require("node:path");
8296
8394
 
8297
8395
  // src/merge-ci-policy.ts
8298
8396
  function resolveMergeCiPolicy(input) {
@@ -8363,31 +8461,49 @@ async function waitForPrChecks(deps) {
8363
8461
 
8364
8462
  // src/bootstrap-ruleset.ts
8365
8463
  var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
8366
- var PRODUCT_GATE_CONTEXT = "gate";
8367
8464
  function stripRulesetComment(raw) {
8368
8465
  const parsed = JSON.parse(raw);
8369
8466
  delete parsed._comment;
8370
8467
  return parsed;
8371
8468
  }
8372
- function rulesetHasGateContext(ruleset, context = PRODUCT_GATE_CONTEXT) {
8469
+ function rulesetRequiredContexts(ruleset) {
8470
+ const contexts = [];
8373
8471
  for (const rule of ruleset.rules ?? []) {
8374
8472
  if (rule.type !== "required_status_checks") continue;
8375
8473
  for (const check of rule.parameters?.required_status_checks ?? []) {
8376
- if (check.context === context) return true;
8474
+ if (check.context) contexts.push(check.context);
8377
8475
  }
8378
8476
  }
8379
- return false;
8477
+ return contexts;
8478
+ }
8479
+ function patchRulesetRequiredContexts(body, contexts) {
8480
+ const sorted = [...new Set(contexts)].sort((a, b) => a.localeCompare(b));
8481
+ const rules2 = (body.rules ?? []).map((rule) => {
8482
+ const r = rule;
8483
+ if (r.type !== "required_status_checks") return rule;
8484
+ return {
8485
+ ...r,
8486
+ parameters: {
8487
+ ...r.parameters,
8488
+ strict_required_status_checks_policy: r.parameters?.strict_required_status_checks_policy ?? false,
8489
+ required_status_checks: sorted.map((context) => ({ context }))
8490
+ }
8491
+ };
8492
+ });
8493
+ return { ...body, rules: rules2 };
8380
8494
  }
8381
8495
  function findProductRuleset(rulesets) {
8382
8496
  return rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
8383
8497
  }
8384
8498
  async function activateProductRuleset(repo, rulesetBody, client) {
8499
+ const want = new Set(rulesetRequiredContexts({ rules: rulesetBody.rules }));
8385
8500
  const list = await client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
8386
8501
  const existing = findProductRuleset(list ?? []);
8387
8502
  if (existing?.id != null) {
8388
8503
  const detail = await client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
8389
- if (detail.enforcement === "active" && rulesetHasGateContext(detail)) {
8390
- return { action: "skipped", detail: "active ruleset already requires gate" };
8504
+ const have = new Set(rulesetRequiredContexts(detail));
8505
+ if (detail.enforcement === "active" && have.size === want.size && [...want].every((c) => have.has(c))) {
8506
+ return { action: "skipped", detail: "active ruleset already matches required contexts" };
8391
8507
  }
8392
8508
  await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: rulesetBody, timeoutMs: 2e4 });
8393
8509
  return { action: "updated", detail: `ruleset ${existing.id}` };
@@ -8396,6 +8512,75 @@ async function activateProductRuleset(repo, rulesetBody, client) {
8396
8512
  return { action: "created" };
8397
8513
  }
8398
8514
 
8515
+ // src/workflow-context.ts
8516
+ function parseWorkflowJobIds(yaml) {
8517
+ const lines = yaml.split(/\r?\n/);
8518
+ let inJobs = false;
8519
+ let jobIndent = -1;
8520
+ const ids = [];
8521
+ for (const line of lines) {
8522
+ if (!inJobs) {
8523
+ if (/^jobs:\s*$/.test(line)) inJobs = true;
8524
+ continue;
8525
+ }
8526
+ if (/^\S/.test(line) && !line.startsWith("#")) break;
8527
+ const trimmed = line.trim();
8528
+ if (!trimmed || trimmed.startsWith("#")) continue;
8529
+ const match = line.match(/^(\s+)([A-Za-z0-9_-]+):\s*$/);
8530
+ if (!match) continue;
8531
+ const indent = match[1].length;
8532
+ if (jobIndent < 0) {
8533
+ jobIndent = indent;
8534
+ ids.push(match[2]);
8535
+ continue;
8536
+ }
8537
+ if (indent === jobIndent) ids.push(match[2]);
8538
+ else if (indent < jobIndent) break;
8539
+ }
8540
+ return ids;
8541
+ }
8542
+ function workflowTriggersPullRequest(yaml) {
8543
+ const onBlock = extractOnBlock(yaml);
8544
+ if (!onBlock) return false;
8545
+ return /\bpull_request\b/.test(onBlock);
8546
+ }
8547
+ function extractOnBlock(yaml) {
8548
+ const lines = yaml.split(/\r?\n/);
8549
+ let inOn = false;
8550
+ let onIndent = -1;
8551
+ const block = [];
8552
+ for (const line of lines) {
8553
+ if (!inOn) {
8554
+ if (/^on:\s*$/.test(line)) {
8555
+ inOn = true;
8556
+ onIndent = 0;
8557
+ } else if (/^on:\s+\S/.test(line)) {
8558
+ return line;
8559
+ }
8560
+ continue;
8561
+ }
8562
+ if (/^\S/.test(line) && !line.startsWith("#")) break;
8563
+ const match = line.match(/^(\s+)/);
8564
+ const indent = match ? match[1].length : 0;
8565
+ if (onIndent >= 0 && indent === 0 && line.trim()) break;
8566
+ block.push(line);
8567
+ }
8568
+ return block.length ? block.join("\n") : null;
8569
+ }
8570
+ function collectPullRequestWorkflowContexts(workflows) {
8571
+ const contexts = /* @__PURE__ */ new Set();
8572
+ for (const wf of workflows) {
8573
+ if (!workflowTriggersPullRequest(wf.body)) continue;
8574
+ for (const id of parseWorkflowJobIds(wf.body)) contexts.add(id);
8575
+ }
8576
+ return [...contexts].sort((a, b) => a.localeCompare(b));
8577
+ }
8578
+ function contextsMatchRuleset(required, emitted) {
8579
+ if (required.size !== emitted.size) return false;
8580
+ for (const c of required) if (!emitted.has(c)) return false;
8581
+ return true;
8582
+ }
8583
+
8399
8584
  // src/bootstrap-seeds.ts
8400
8585
  var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
8401
8586
  function loadBootstrapSeeds(manifestJson) {
@@ -8435,6 +8620,8 @@ var MANAGED_GITIGNORE_LINES = [
8435
8620
  '# Org-wide cleanliness (AGENTS.md "Repo cleanliness") \u2014 enforced by `mmi-cli doctor`.',
8436
8621
  "# Do not edit inside these markers; this block is regenerated on the next doctor run.",
8437
8622
  "/tmp/",
8623
+ // Ad-hoc agent scratch at repo root (e.g. tmp_diff.txt) — same class as /tmp/ (#1676).
8624
+ "/tmp_*",
8438
8625
  // Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are S3-synced
8439
8626
  // via `mmi-cli plan push`, never git-tracked (AGENTS.md "Repo cleanliness", #1550).
8440
8627
  "**/plans/",
@@ -8660,6 +8847,10 @@ function seedMatchesDeployModel(seed, deployModel) {
8660
8847
  if (!seed.deployModels?.length) return true;
8661
8848
  return deployModel != null && seed.deployModels.includes(deployModel);
8662
8849
  }
8850
+ function seedMatchesDashboard(seed, isDashboard) {
8851
+ if (!seed.dashboard) return true;
8852
+ return isDashboard;
8853
+ }
8663
8854
  function planSeedAction(seed, exists) {
8664
8855
  if (seed.source === "fanout") {
8665
8856
  return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
@@ -8707,10 +8898,10 @@ function labelsToPrune(orgLabelNames) {
8707
8898
  const org = new Set(orgLabelNames);
8708
8899
  return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
8709
8900
  }
8710
- function resolveSeedContent(seed, vars, readFile6) {
8711
- if (seed.source === "self") return readFile6(seed.target);
8901
+ function resolveSeedContent(seed, vars, readFile7) {
8902
+ if (seed.source === "self") return readFile7(seed.target);
8712
8903
  if (seed.source.startsWith("seed:")) {
8713
- const tmpl = readFile6(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
8904
+ const tmpl = readFile7(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
8714
8905
  return tmpl == null ? null : renderSeed(tmpl, vars);
8715
8906
  }
8716
8907
  return null;
@@ -8767,6 +8958,9 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
8767
8958
  class: cls,
8768
8959
  projectType,
8769
8960
  deployModel,
8961
+ // #1452: opt-in dashboard flag — only emitted when set (undefined keys are dropped below), so a
8962
+ // non-dashboard repo's META is byte-identical to before. Gates the @mutmutco components.json seed.
8963
+ dashboard: options.dashboard ? true : void 0,
8770
8964
  // #1359: always persist an explicit track so release tooling never guesses from absence alone.
8771
8965
  releaseTrack: resolveBootstrapReleaseTrack(cls, options.releaseTrack),
8772
8966
  // Board coords (from GraphQL at bootstrap, passed as --var by the skill).
@@ -8894,7 +9088,6 @@ function contentPutArgs(repo, path2, content, branch, sha) {
8894
9088
 
8895
9089
  // src/ci-audit.ts
8896
9090
  var HUB_REPO = "mutmutco/MMI-Hub";
8897
- var PRODUCT_GATE_CONTEXT2 = "gate";
8898
9091
  var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
8899
9092
  var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
8900
9093
  var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
@@ -8931,6 +9124,45 @@ async function rulesetDetails(deps, repo, list) {
8931
9124
  }
8932
9125
  return details;
8933
9126
  }
9127
+ async function fetchFileContent(deps, repo, branch, path2) {
9128
+ try {
9129
+ const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
9130
+ const file = await deps.client.rest(
9131
+ "GET",
9132
+ `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`
9133
+ );
9134
+ if (file.encoding !== "base64" || typeof file.content !== "string") return null;
9135
+ return Buffer.from(file.content, "base64").toString("utf8");
9136
+ } catch {
9137
+ return null;
9138
+ }
9139
+ }
9140
+ async function listWorkflowPaths(deps, repo, branch) {
9141
+ try {
9142
+ const encodedPath = ".github/workflows".split("/").map(encodeURIComponent).join("/");
9143
+ const entries = await deps.client.rest(
9144
+ "GET",
9145
+ `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`
9146
+ );
9147
+ return (entries ?? []).filter((e) => e.type === "file" && /\.ya?ml$/i.test(e.name ?? "")).map((e) => `.github/workflows/${e.name}`);
9148
+ } catch {
9149
+ return [];
9150
+ }
9151
+ }
9152
+ async function resolveEmittedPrContexts(deps, repo, branch) {
9153
+ const paths = await listWorkflowPaths(deps, repo, branch);
9154
+ const workflows = [];
9155
+ for (const path2 of paths) {
9156
+ const body = await fetchFileContent(deps, repo, branch, path2);
9157
+ if (body) workflows.push({ path: path2, body });
9158
+ }
9159
+ return collectPullRequestWorkflowContexts(workflows);
9160
+ }
9161
+ function registryRequiredContexts(meta) {
9162
+ const raw = meta?.requiredChecks;
9163
+ if (!Array.isArray(raw) || raw.length === 0) return null;
9164
+ return raw.filter((c) => typeof c === "string" && c.length > 0);
9165
+ }
8934
9166
  async function contentExists(deps, repo, branch, path2) {
8935
9167
  try {
8936
9168
  const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
@@ -9030,13 +9262,25 @@ async function auditRepoCi(repo, deps) {
9030
9262
  detail: missing.length ? `missing: ${missing.join(", ")}` : void 0
9031
9263
  });
9032
9264
  } else if (deployableGated) {
9033
- const missing = [PRODUCT_GATE_CONTEXT2].filter((c) => !statusChecks.has(c));
9265
+ const hasRequiredChecks = statusChecks.size > 0;
9034
9266
  checks.push({
9035
- ok: missing.length === 0,
9036
- label: "product gate status check active",
9037
- detail: missing.length ? `missing context: ${PRODUCT_GATE_CONTEXT2} \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset` : void 0,
9038
- remediation: missing.length ? `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)` : void 0
9267
+ ok: hasRequiredChecks,
9268
+ label: "product required status checks active",
9269
+ detail: hasRequiredChecks ? void 0 : `no required status checks active \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset`,
9270
+ remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)`
9039
9271
  });
9272
+ if (hasGateWorkflow) {
9273
+ const emitted = registryRequiredContexts(meta) ?? await resolveEmittedPrContexts(deps, repo, baseBranch);
9274
+ if (emitted.length > 0) {
9275
+ const aligned = contextsMatchRuleset(statusChecks, new Set(emitted));
9276
+ checks.push({
9277
+ ok: aligned,
9278
+ label: "required check contexts match PR workflows",
9279
+ detail: aligned ? void 0 : `ruleset requires [${[...statusChecks].sort().join(", ")}] but workflows emit [${emitted.join(", ")}]`,
9280
+ remediation: aligned ? void 0 : `mmi-cli ci reconcile --repo ${repo} --apply`
9281
+ });
9282
+ }
9283
+ }
9040
9284
  }
9041
9285
  const workflowPaths = hasGateWorkflow && repoClass === "deployable" ? [PRODUCT_GATE_PATH] : [];
9042
9286
  const { policy, reason } = resolveMergeCiPolicy({
@@ -9230,9 +9474,10 @@ async function applyCiReconcileRepo(repo, deps) {
9230
9474
  const report = await auditRepoCi(repo, deps);
9231
9475
  if (report.class !== "deployable") return merge;
9232
9476
  await seedGateYml(repo, deps, meta, merge);
9233
- const gateCheck = report.checks.find((c) => c.label === "product gate status check active");
9234
- if (gateCheck?.ok) {
9235
- merge.skipped.push("product ruleset already active");
9477
+ const driftCheck = report.checks.find((c) => c.label === "required check contexts match PR workflows");
9478
+ const requiredCheck = report.checks.find((c) => c.label === "product required status checks active");
9479
+ if (requiredCheck?.ok && (driftCheck?.ok ?? true)) {
9480
+ merge.skipped.push("product ruleset already active and aligned");
9236
9481
  return merge;
9237
9482
  }
9238
9483
  const raw = await fetchRulesetSeedBody(deps, repo);
@@ -9241,7 +9486,27 @@ async function applyCiReconcileRepo(repo, deps) {
9241
9486
  return merge;
9242
9487
  }
9243
9488
  try {
9244
- const body = stripRulesetComment(raw);
9489
+ let body = stripRulesetComment(raw);
9490
+ if (!driftCheck?.ok) {
9491
+ const baseBranch = "development";
9492
+ const emitted = registryRequiredContexts(meta) ?? await resolveEmittedPrContexts(deps, repo, baseBranch);
9493
+ if (!emitted.length) {
9494
+ merge.errors.push("cannot reconcile ruleset contexts \u2014 no PR workflow job ids found");
9495
+ return merge;
9496
+ }
9497
+ body = patchRulesetRequiredContexts(body, emitted);
9498
+ const activation2 = await activateProductRuleset(repo, body, deps.client);
9499
+ if (activation2.action === "skipped") merge.skipped.push(activation2.detail ?? "product ruleset");
9500
+ else merge.applied.push(`product ruleset ${activation2.action}${activation2.detail ? `: ${activation2.detail}` : ""}`);
9501
+ try {
9502
+ await putSeedFile(deps, repo, PRODUCT_RULESET_REF, `${JSON.stringify(body, null, 2)}
9503
+ `, baseBranch);
9504
+ merge.applied.push(`reconciled ${PRODUCT_RULESET_REF} contexts \u2192 [${emitted.join(", ")}]`);
9505
+ } catch (e) {
9506
+ merge.errors.push(`ruleset reference commit failed (live ruleset updated): ${e.message}`);
9507
+ }
9508
+ return merge;
9509
+ }
9245
9510
  const activation = await activateProductRuleset(repo, body, deps.client);
9246
9511
  if (activation.action === "skipped") merge.skipped.push(activation.detail ?? "product ruleset");
9247
9512
  else merge.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
@@ -9254,6 +9519,25 @@ async function applyCiReconcileRepo(repo, deps) {
9254
9519
  // src/pr-land.ts
9255
9520
  var PR_LAND_POLL_MS = 3e4;
9256
9521
  var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
9522
+ var PR_LAND_STATE_READ_RETRIES = 3;
9523
+ var PR_LAND_STATE_READ_DELAY_MS = 2e3;
9524
+ async function readGhPrStateWithRetry(fetchState2, options) {
9525
+ const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
9526
+ const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
9527
+ const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
9528
+ let lastError = "empty state";
9529
+ for (let attempt = 0; attempt < retries; attempt++) {
9530
+ try {
9531
+ const state = (await fetchState2()).trim();
9532
+ if (state) return { ok: true, state };
9533
+ lastError = "empty state";
9534
+ } catch (e) {
9535
+ lastError = String(e.message || "gh pr view failed");
9536
+ }
9537
+ if (attempt < retries - 1) await sleep2(delayMs);
9538
+ }
9539
+ return { ok: false, error: lastError };
9540
+ }
9257
9541
  async function runPrLand(prNumber, options, deps) {
9258
9542
  const repo = await deps.resolveRepo(prNumber, options.repo);
9259
9543
  const base2 = { status: "failed", repo, pr: prNumber };
@@ -9461,22 +9745,15 @@ function formatManifestHuman(manifest) {
9461
9745
  // src/config-discovery.ts
9462
9746
  function stripMutableBoardConfig(cfg) {
9463
9747
  const {
9464
- projectOwner,
9465
- projectNumber,
9466
- projectId,
9467
- statusFieldId,
9468
- statusOptions,
9469
- priorityFieldId,
9470
- priorityOptions,
9748
+ projectOwner: _projectOwner,
9749
+ projectNumber: _projectNumber,
9750
+ projectId: _projectId,
9751
+ statusFieldId: _statusFieldId,
9752
+ statusOptions: _statusOptions,
9753
+ priorityFieldId: _priorityFieldId,
9754
+ priorityOptions: _priorityOptions,
9471
9755
  ...rest
9472
9756
  } = cfg;
9473
- void projectOwner;
9474
- void projectNumber;
9475
- void projectId;
9476
- void statusFieldId;
9477
- void statusOptions;
9478
- void priorityFieldId;
9479
- void priorityOptions;
9480
9757
  return rest;
9481
9758
  }
9482
9759
  function boardConfigFromProject(meta, floor = {}) {
@@ -10160,7 +10437,7 @@ function buildRemoteBranchCleanupReport(branch, input) {
10160
10437
  return { name: branch, status: "not-attempted", reason: input.reason ?? "remote-check-unavailable" };
10161
10438
  }
10162
10439
  async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input, options = {}) {
10163
- const sleep = options.sleep ?? defaultSleep;
10440
+ const sleep2 = options.sleep ?? defaultSleep;
10164
10441
  const maxAttempts = options.maxAttempts ?? 5;
10165
10442
  const backoff = [500, 1e3, 1500, 2e3];
10166
10443
  if (!input.attempted) {
@@ -10171,7 +10448,7 @@ async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input, option
10171
10448
  }
10172
10449
  let existsAfter = await deps.exists(branch, { prune: true });
10173
10450
  for (let i = 1; i < maxAttempts && existsAfter === true; i++) {
10174
- await sleep(backoff[Math.min(i - 1, backoff.length - 1)]);
10451
+ await sleep2(backoff[Math.min(i - 1, backoff.length - 1)]);
10175
10452
  existsAfter = await deps.exists(branch, { prune: true });
10176
10453
  }
10177
10454
  return buildRemoteBranchCleanupReport(branch, {
@@ -10456,6 +10733,39 @@ function safeWorktreeRemoveCommand(safeCwd, targetPath) {
10456
10733
  function errorMessage(error) {
10457
10734
  return error instanceof Error ? error.message : String(error);
10458
10735
  }
10736
+ async function fastForwardCurrentBranch(git) {
10737
+ try {
10738
+ const before = (await git(["rev-parse", "HEAD"])).trim();
10739
+ await git(["pull", "--ff-only"]);
10740
+ const after = (await git(["rev-parse", "HEAD"])).trim();
10741
+ return { status: before === after ? "up-to-date" : "fast-forwarded" };
10742
+ } catch (e) {
10743
+ return { status: "failed", error: errorMessage(e) };
10744
+ }
10745
+ }
10746
+ async function returnCheckoutToBase(git, base2, mergedBranch, currentBranch) {
10747
+ if (currentBranch === mergedBranch) {
10748
+ const dirty = (await git(["status", "--porcelain"]).catch(() => "dirty") || "").trim().length > 0;
10749
+ if (dirty) {
10750
+ return { report: { branch: base2, switched: false, sync: "skipped", reason: "dirty-worktree" }, canDeleteBranch: false };
10751
+ }
10752
+ try {
10753
+ await git(["checkout", base2]);
10754
+ } catch (e) {
10755
+ return {
10756
+ report: { branch: base2, switched: false, sync: "skipped", reason: "checkout-failed", error: errorMessage(e) },
10757
+ canDeleteBranch: false
10758
+ };
10759
+ }
10760
+ const ff2 = await fastForwardCurrentBranch(git);
10761
+ return { report: { branch: base2, switched: true, sync: ff2.status, ...ff2.error ? { error: ff2.error } : {} }, canDeleteBranch: true };
10762
+ }
10763
+ if (currentBranch !== base2) {
10764
+ return { report: { branch: base2, switched: false, sync: "skipped", reason: "not-on-base" }, canDeleteBranch: true };
10765
+ }
10766
+ const ff = await fastForwardCurrentBranch(git);
10767
+ return { report: { branch: base2, switched: false, sync: ff.status, ...ff.error ? { error: ff.error } : {} }, canDeleteBranch: true };
10768
+ }
10459
10769
  async function cleanupPrMergeLocalBranch(branch, options) {
10460
10770
  const report = {
10461
10771
  branch,
@@ -10549,7 +10859,19 @@ async function cleanupPrMergeLocalBranch(branch, options) {
10549
10859
  }
10550
10860
  }
10551
10861
  const current = (await git(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || "").trim();
10552
- if (branch === current) {
10862
+ if (options.returnToBranch && options.returnToBranch !== branch) {
10863
+ const { report: checkout, canDeleteBranch } = await returnCheckoutToBase(
10864
+ git,
10865
+ options.returnToBranch,
10866
+ branch,
10867
+ current
10868
+ );
10869
+ report.checkout = checkout;
10870
+ if (!canDeleteBranch) {
10871
+ report.localBranch = { name: branch, status: "not-attempted", reason: checkout.reason ?? "current-branch" };
10872
+ return report;
10873
+ }
10874
+ } else if (branch === current) {
10553
10875
  report.localBranch = { name: branch, status: "not-attempted", reason: "current-branch" };
10554
10876
  return report;
10555
10877
  }
@@ -10736,10 +11058,10 @@ function trainPlan(command, options = {}) {
10736
11058
  { label: "no residue: development already has the fix; the next /release fold + back-merge re-aligns version manifests" }
10737
11059
  ];
10738
11060
  }
10739
- function bootstrapPlan(repo, repoClass) {
11061
+ function bootstrapPlan(repo, repoClass, opts = {}) {
10740
11062
  const branchModel = repoClass === "content" ? "content repo: main only" : "deployable repo: development, rc, main";
10741
11063
  const protectedBranches = repoClass === "content" ? "main" : "development, rc, main";
10742
- return [
11064
+ const steps = [
10743
11065
  { label: `create or inspect ${repo}` },
10744
11066
  { label: `provision branch model: ${branchModel}`, gated: true },
10745
11067
  { label: `apply branch protection / allowlist: ${protectedBranches}`, gated: true },
@@ -10748,6 +11070,13 @@ function bootstrapPlan(repo, repoClass) {
10748
11070
  { label: "commit .claude/settings.json and .cursor/rules/<repo>.mdc", gated: true },
10749
11071
  { label: `register fanout target on ${repoClass === "content" ? "main" : "development"}`, gated: true }
10750
11072
  ];
11073
+ if (opts.dashboard) {
11074
+ steps.push({
11075
+ label: "seed components.json wired to the @mutmutco registry; scaffold the app from the MMD-UI apps/starter",
11076
+ gated: true
11077
+ });
11078
+ }
11079
+ return steps;
10751
11080
  }
10752
11081
 
10753
11082
  // src/stage-default.ts
@@ -10863,7 +11192,7 @@ function decideStage(inputs) {
10863
11192
  }
10864
11193
 
10865
11194
  // src/cursor-plugin-seed.ts
10866
- var import_node_child_process7 = require("node:child_process");
11195
+ var import_node_child_process8 = require("node:child_process");
10867
11196
  var import_node_fs15 = require("node:fs");
10868
11197
  var import_node_os5 = require("node:os");
10869
11198
  var import_node_path14 = require("node:path");
@@ -10874,7 +11203,7 @@ function isSemverVersion(v) {
10874
11203
  var MMI_HUB_REPO = "mutmutco/MMI-Hub";
10875
11204
  var CURSOR_THIRD_PARTY_STATE_KEY = "cursor/thirdPartyExtensibilityEnabled";
10876
11205
  var PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
10877
- var execFileBuffer = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
11206
+ var execFileBuffer = (0, import_node_util6.promisify)(import_node_child_process8.execFile);
10878
11207
  function gitFetchReleaseTagArgs(hubCheckout, tag) {
10879
11208
  return ["-C", hubCheckout, "fetch", "origin", "tag", tag, "--quiet"];
10880
11209
  }
@@ -11008,8 +11337,10 @@ function buildAwsCrossAccountCheck(input) {
11008
11337
  fix: AWS_CROSS_ACCOUNT_FIX
11009
11338
  };
11010
11339
  }
11011
- var MMI_PLUGIN_ID = "mmi@mmi";
11012
- var PLUGIN_LABEL = "plugin install record (mmi@mmi for this project)";
11340
+ var MMI_PLUGIN_ID = "mmi@mutmutco";
11341
+ var LEGACY_MMI_PLUGIN_ID = "mmi@mmi";
11342
+ var LEGACY_MMI_MARKETPLACE = "mmi";
11343
+ var PLUGIN_LABEL = "plugin install record (mmi@mutmutco for this project)";
11013
11344
  function pluginInstallManualFix(projectPath, surface = "claude-cli") {
11014
11345
  const register = surface === "codex" ? `\`codex plugin add ${MMI_PLUGIN_ID}\`` : surface === "cursor" ? `import the MMI Team Marketplace in Cursor Dashboard \u2192 Settings \u2192 Plugins (or enable the MMI plugin from the marketplace panel)` : surface === "shell" ? `enable the MMI plugin in your client` : surface === "claude-vscode" ? `\`claude plugin enable ${MMI_PLUGIN_ID}\`` : `\`/plugin install ${MMI_PLUGIN_ID}\``;
11015
11346
  return `run ${register} then ${reloadAction(surface)} to register the install record for ${projectPath}`;
@@ -11027,6 +11358,10 @@ function hasUserInstallRecord(file, pluginId) {
11027
11358
  if (!Array.isArray(records)) return false;
11028
11359
  return records.some((r) => r.scope === "user");
11029
11360
  }
11361
+ function hasAnyPluginRecords(file, pluginId) {
11362
+ const records = file?.plugins?.[pluginId];
11363
+ return Array.isArray(records) && records.length > 0;
11364
+ }
11030
11365
  function buildPluginInstallRecordCheck(input) {
11031
11366
  const pluginId = input.pluginId ?? MMI_PLUGIN_ID;
11032
11367
  const base2 = {
@@ -11036,6 +11371,13 @@ function buildPluginInstallRecordCheck(input) {
11036
11371
  pluginId
11037
11372
  };
11038
11373
  if (!input.isOrgRepo || !isMmiPluginEnabled(input.settings)) return base2;
11374
+ if (hasAnyPluginRecords(input.installed, LEGACY_MMI_PLUGIN_ID) && !hasAnyPluginRecords(input.installed, pluginId)) {
11375
+ return {
11376
+ ...base2,
11377
+ ok: false,
11378
+ fix: `${CLAUDE_PLUGIN_RECOVERY} # then ${reloadAction("claude-cli")}`
11379
+ };
11380
+ }
11039
11381
  if (hasProjectInstallRecord(input.installed, pluginId, input.projectPath) || hasUserInstallRecord(input.installed, pluginId)) return base2;
11040
11382
  const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
11041
11383
  const recordToInsert = input.mirrorFrom ? {
@@ -11053,7 +11395,35 @@ function buildPluginInstallRecordCheck(input) {
11053
11395
  recordToInsert
11054
11396
  };
11055
11397
  }
11056
- var PLUGIN_DRIFT_LABEL = "plugin config drift (mmi@mmi duplicate rows / stale gitCommitSha)";
11398
+ var LEGACY_PLUGIN_ID_LABEL = "legacy MMI plugin ID (mmi@mmi \u2192 mmi@mutmutco)";
11399
+ function buildLegacyPluginInstallCheck(input) {
11400
+ const base2 = {
11401
+ ok: true,
11402
+ label: LEGACY_PLUGIN_ID_LABEL,
11403
+ fix: pluginRecoveryFix(input.surface ?? "claude-cli"),
11404
+ legacyPluginId: LEGACY_MMI_PLUGIN_ID
11405
+ };
11406
+ if (!input.isOrgRepo) return base2;
11407
+ const staleSurfaces = [];
11408
+ for (const source of input.sources) {
11409
+ if (hasAnyPluginRecords(source.installed, LEGACY_MMI_PLUGIN_ID)) staleSurfaces.push(source.surface);
11410
+ }
11411
+ if (staleSurfaces.length === 0) return base2;
11412
+ const fixParts = [];
11413
+ if (staleSurfaces.includes("claude")) {
11414
+ fixParts.push(`${CLAUDE_PLUGIN_RECOVERY} # then ${reloadAction("claude-cli")}`);
11415
+ }
11416
+ if (staleSurfaces.includes("codex")) {
11417
+ fixParts.push(`${CODEX_PLUGIN_RECOVERY} # then ${reloadAction("codex")}`);
11418
+ }
11419
+ return {
11420
+ ...base2,
11421
+ ok: false,
11422
+ staleSurfaces,
11423
+ fix: fixParts.join(" ; ") || base2.fix
11424
+ };
11425
+ }
11426
+ var PLUGIN_DRIFT_LABEL = "plugin config drift (mmi@mutmutco duplicate rows / stale gitCommitSha)";
11057
11427
  function recordFreshness(r) {
11058
11428
  return r.lastUpdated ?? r.installedAt ?? "";
11059
11429
  }
@@ -11226,18 +11596,20 @@ function reloadAction(surface) {
11226
11596
  return "restart Claude Code (or run /reload-plugins)";
11227
11597
  }
11228
11598
  }
11229
- var CLAUDE_PLUGIN_RECOVERY = "claude plugin marketplace remove mmi && claude plugin marketplace add mutmutco/MMI-Hub && claude plugin install mmi@mmi";
11599
+ var CLAUDE_PLUGIN_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub && claude plugin install mmi@mutmutco`;
11230
11600
  var CLAUDE_PLUGIN_HEAL_STEPS = [
11231
- { args: ["plugin", "marketplace", "remove", "mmi"], gated: false },
11601
+ { args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
11602
+ { args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
11232
11603
  { args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub"], gated: true },
11233
- { args: ["plugin", "install", "mmi@mmi"], gated: true },
11234
- { args: ["plugin", "enable", "mmi@mmi"], gated: false }
11604
+ { args: ["plugin", "install", "mmi@mutmutco"], gated: true },
11605
+ { args: ["plugin", "enable", "mmi@mutmutco"], gated: false }
11235
11606
  ];
11236
- var CODEX_PLUGIN_RECOVERY = "codex plugin marketplace remove mmi && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mmi";
11607
+ var CODEX_PLUGIN_RECOVERY = `codex plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && codex plugin marketplace remove mutmutco && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mutmutco`;
11237
11608
  var CODEX_PLUGIN_HEAL_STEPS = [
11238
- { args: ["plugin", "marketplace", "remove", "mmi"], gated: false },
11609
+ { args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
11610
+ { args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
11239
11611
  { args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
11240
- { args: ["plugin", "add", "mmi@mmi"], gated: true }
11612
+ { args: ["plugin", "add", "mmi@mutmutco"], gated: true }
11241
11613
  ];
11242
11614
  function healStepAborts(step, ok) {
11243
11615
  return !ok && step.gated;
@@ -11259,7 +11631,7 @@ function pluginRecoveryFix(surface) {
11259
11631
  }
11260
11632
  var PLUGIN_UPDATE_RECIPES = {
11261
11633
  claude: [CLAUDE_PLUGIN_RECOVERY],
11262
- codex: [CODEX_PLUGIN_RECOVERY, "codex plugin list # verify mmi@mmi shows the released version"],
11634
+ codex: [CODEX_PLUGIN_RECOVERY, "codex plugin list # verify mmi@mutmutco shows the released version"],
11263
11635
  cli: ["npm install -g @mutmutco/cli@latest"]
11264
11636
  };
11265
11637
  function highestSemver(versions) {
@@ -11589,82 +11961,401 @@ async function runStageLiveDown(deps, t) {
11589
11961
  };
11590
11962
  }
11591
11963
 
11592
- // src/stage-runner.ts
11593
- var import_node_child_process8 = require("node:child_process");
11964
+ // src/design-system.ts
11594
11965
  var import_node_fs16 = require("node:fs");
11595
11966
  var import_node_path15 = require("node:path");
11596
- var import_node_net2 = require("node:net");
11597
- var import_node_util7 = require("node:util");
11598
- var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process8.execFile);
11599
- var EARLY_EXIT_GRACE_MS = 2e3;
11600
- function waitForProcessStability(child, graceMs = EARLY_EXIT_GRACE_MS) {
11601
- return new Promise((resolve, reject) => {
11602
- let settled = false;
11603
- const finish = (fn) => {
11604
- if (settled) return;
11605
- settled = true;
11606
- clearTimeout(timer);
11607
- child.removeAllListeners("error");
11608
- child.removeAllListeners("exit");
11609
- fn();
11967
+ var UI_PACKAGE_CANDIDATES = ["@mutmutco/ui-dashboard", "@mutmutco/ui", "@mutmutco/theme"];
11968
+ var DESIGN_SYSTEM_VERSION_LABEL = "@mutmutco design-system npm package (vs @latest)";
11969
+ function dashboardConsumerRegistryFix(error) {
11970
+ return `Hub registry read failed (${error}) \u2014 could not verify dashboard UI state; likely transient (cold start, network, or auth blip) \u2014 retry shortly`;
11971
+ }
11972
+ var DESIGN_SYSTEM_FIX = (pkg) => `run \`npm update ${pkg}\` (or \`mmi-cli doctor --apply\` to update automatically)`;
11973
+ function buildDesignSystemVersionCheck(input) {
11974
+ const base2 = {
11975
+ ok: true,
11976
+ label: DESIGN_SYSTEM_VERSION_LABEL,
11977
+ fix: `install a current @mutmutco UI package \u2014 see MM-KB kb/guides/dashboard-ui.md`
11978
+ };
11979
+ if (!input.isConsumerRepo || !input.packageName) return base2;
11980
+ if (!input.installedVersion) {
11981
+ return {
11982
+ ok: false,
11983
+ label: DESIGN_SYSTEM_VERSION_LABEL,
11984
+ fix: `add ${input.packageName} to package.json, then npm install \u2014 see kb/guides/dashboard-ui.md`,
11985
+ packageName: input.packageName,
11986
+ latestVersion: input.latestVersion
11610
11987
  };
11611
- const timer = setTimeout(() => finish(resolve), graceMs);
11612
- child.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
11613
- child.on("exit", (code, signal) => {
11614
- const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
11615
- finish(() => reject(new Error(`stage process exited before health check (${detail})`)));
11616
- });
11617
- });
11988
+ }
11989
+ if (!input.latestVersion) {
11990
+ return {
11991
+ ...base2,
11992
+ packageName: input.packageName,
11993
+ installedVersion: input.installedVersion
11994
+ };
11995
+ }
11996
+ if (compareVersions(input.installedVersion, input.latestVersion) < 0) {
11997
+ return {
11998
+ ok: false,
11999
+ label: DESIGN_SYSTEM_VERSION_LABEL,
12000
+ fix: DESIGN_SYSTEM_FIX(input.packageName),
12001
+ packageName: input.packageName,
12002
+ installedVersion: input.installedVersion,
12003
+ latestVersion: input.latestVersion
12004
+ };
12005
+ }
12006
+ return {
12007
+ ...base2,
12008
+ packageName: input.packageName,
12009
+ installedVersion: input.installedVersion,
12010
+ latestVersion: input.latestVersion
12011
+ };
11618
12012
  }
11619
- function envFileKeys(content) {
11620
- const keys = /* @__PURE__ */ new Set();
11621
- for (const line of content.split(/\r?\n/)) {
11622
- const trimmed = line.trim();
11623
- if (!trimmed || trimmed.startsWith("#")) continue;
11624
- const eq = trimmed.indexOf("=");
11625
- if (eq > 0) keys.add(trimmed.slice(0, eq).trim());
12013
+ function readJsonFile(path2) {
12014
+ try {
12015
+ return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
12016
+ } catch {
12017
+ return void 0;
11626
12018
  }
11627
- return keys;
11628
12019
  }
11629
- function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
11630
- const example = normalizeEol(exampleContent);
11631
- const target = normalizeEol(targetContent);
11632
- const exampleKeys = envFileKeys(example);
11633
- const targetKeys = envFileKeys(target);
11634
- for (const key of exampleKeys) {
11635
- if (!targetKeys.has(key)) return `missing key ${key} from .env.example`;
12020
+ function isUiFactoryCheckout(root) {
12021
+ const pkg = readJsonFile((0, import_node_path15.join)(root, "package.json"));
12022
+ return pkg?.name === "mmd-ui" && pkg?.private === true;
12023
+ }
12024
+ function resolveDeclaredUiPackage(root) {
12025
+ const pkg = readJsonFile((0, import_node_path15.join)(root, "package.json"));
12026
+ if (!pkg) return void 0;
12027
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
12028
+ for (const name of UI_PACKAGE_CANDIDATES) {
12029
+ const spec = deps[name];
12030
+ if (spec && spec !== "*" && !spec.startsWith("workspace:")) return name;
11636
12031
  }
11637
12032
  return void 0;
11638
12033
  }
11639
- function stageStatePath(cwd = process.cwd()) {
11640
- return (0, import_node_path15.join)(cwd, "tmp", "stage", "state.json");
12034
+ function readLockfileInstalledVersion(root, packageName) {
12035
+ const lockPath = (0, import_node_path15.join)(root, "package-lock.json");
12036
+ if (!(0, import_node_fs16.existsSync)(lockPath)) return void 0;
12037
+ const lock = readJsonFile(lockPath);
12038
+ const node = lock?.packages?.[`node_modules/${packageName}`];
12039
+ const version = node?.version?.trim();
12040
+ return version && /^\d+\.\d+\.\d+/.test(version) ? version : void 0;
11641
12041
  }
11642
- function mergeEnvSecretsIntoFile(content, secrets2) {
11643
- const lines = content.split(/\r?\n/);
11644
- const indexByKey = /* @__PURE__ */ new Map();
11645
- for (let i = 0; i < lines.length; i++) {
11646
- const trimmed = lines[i].trim();
11647
- if (!trimmed || trimmed.startsWith("#")) continue;
11648
- const eq = trimmed.indexOf("=");
11649
- if (eq === -1) continue;
11650
- indexByKey.set(trimmed.slice(0, eq).trim(), i);
11651
- }
11652
- for (const [key, value] of Object.entries(secrets2)) {
11653
- const escaped = /[\s#"'\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
11654
- const line = `${key}=${escaped}`;
11655
- const idx = indexByKey.get(key);
11656
- if (idx != null) lines[idx] = line;
11657
- else lines.push(line);
12042
+ function npmUiPackageLatestArgs(packageName) {
12043
+ return ["view", packageName, "version"];
12044
+ }
12045
+ function parseNpmViewVersion(stdout) {
12046
+ const v = stdout.trim();
12047
+ return /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v) ? v : void 0;
12048
+ }
12049
+ function isDashboardMetaConsumer(meta) {
12050
+ return meta?.dashboard === true;
12051
+ }
12052
+ function designSystemSnapshot(root) {
12053
+ if (isUiFactoryCheckout(root)) {
12054
+ return {};
11658
12055
  }
11659
- const body = lines.join("\n");
11660
- return body.endsWith("\n") ? body : `${body}
11661
- `;
12056
+ const packageName = resolveDeclaredUiPackage(root);
12057
+ return {
12058
+ packageName,
12059
+ installedVersion: packageName ? readLockfileInstalledVersion(root, packageName) : void 0
12060
+ };
11662
12061
  }
11663
- var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
11664
- function posixOnlyShellProblems(command, field, platform2 = process.platform) {
11665
- if (platform2 !== "win32" || !command?.trim()) return [];
11666
- const problems = [];
11667
- if (/(^|&&|\||;)\s*[A-Za-z_][A-Za-z0-9_]*=\S/.test(command)) {
12062
+
12063
+ // src/design-system-registry.ts
12064
+ var import_node_crypto7 = require("node:crypto");
12065
+ var import_node_fs18 = require("node:fs");
12066
+ var import_node_path16 = require("node:path");
12067
+
12068
+ // src/atomic-write.ts
12069
+ var import_node_fs17 = require("node:fs");
12070
+ function atomicWriteFileSync(path2, content) {
12071
+ const tmp = `${path2}.${process.pid}.tmp`;
12072
+ (0, import_node_fs17.writeFileSync)(tmp, content, "utf8");
12073
+ (0, import_node_fs17.renameSync)(tmp, path2);
12074
+ }
12075
+
12076
+ // src/design-system-registry.ts
12077
+ var DESIGN_SYSTEM_CACHE_DIR = ".mmi/design-system/components";
12078
+ var DESIGN_SYSTEM_MANIFEST_PATH = ".mmi/design-system/manifest.json";
12079
+ var REGISTRY_COMPONENTS_LABEL = "@mutmutco registry components (.mmi cache vs live registry)";
12080
+ var REGISTRY_FIX = "run `mmi-cli doctor --apply` to pull registry components into `.mmi/design-system/components` \u2014 wire imports via `design-system.paths.json` (see kb/guides/dashboard-ui.md)";
12081
+ var REGISTRY_UNREACHABLE_FIX = "live @mutmutco registry unreachable \u2014 verify `components.json` `@mutmutco` registry URL and network, then retry `mmi-cli doctor`";
12082
+ function readJsonFile2(path2) {
12083
+ try {
12084
+ return JSON.parse((0, import_node_fs18.readFileSync)(path2, "utf8"));
12085
+ } catch {
12086
+ return void 0;
12087
+ }
12088
+ }
12089
+ function readComponentsJson(root) {
12090
+ return readJsonFile2((0, import_node_path16.join)(root, "components.json"));
12091
+ }
12092
+ function hasMutmutcoRegistry(root) {
12093
+ const url = readComponentsJson(root)?.registries?.["@mutmutco"];
12094
+ return typeof url === "string" && url.includes("{name}");
12095
+ }
12096
+ function resolveCacheDir(root) {
12097
+ const custom = readComponentsJson(root)?.mmi?.cacheDir;
12098
+ return (0, import_node_path16.join)(root, custom ?? DESIGN_SYSTEM_CACHE_DIR);
12099
+ }
12100
+ function resolveRegistryUrlTemplate(root) {
12101
+ return readComponentsJson(root)?.registries?.["@mutmutco"];
12102
+ }
12103
+ function registryItemUrl(template, name) {
12104
+ return template.replace("{name}", name);
12105
+ }
12106
+ function readDesignSystemManifest(root) {
12107
+ const raw = readJsonFile2((0, import_node_path16.join)(root, DESIGN_SYSTEM_MANIFEST_PATH));
12108
+ if (!raw || !Array.isArray(raw.components)) return void 0;
12109
+ return raw;
12110
+ }
12111
+ function listInstalledRegistryComponents(root) {
12112
+ const fromCfg = readComponentsJson(root)?.mmi?.installed?.filter((n) => typeof n === "string" && n.trim());
12113
+ if (fromCfg?.length) return [...new Set(fromCfg.map((n) => n.trim()))];
12114
+ const manifest = readDesignSystemManifest(root);
12115
+ if (manifest?.components?.length) return [...manifest.components];
12116
+ return scanCachedComponentNames(resolveCacheDir(root));
12117
+ }
12118
+ function scanCachedComponentNames(cacheDir) {
12119
+ if (!(0, import_node_fs18.existsSync)(cacheDir)) return [];
12120
+ const names = /* @__PURE__ */ new Set();
12121
+ const walk = (dir) => {
12122
+ for (const ent of (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true })) {
12123
+ const p = (0, import_node_path16.join)(dir, ent.name);
12124
+ if (ent.isDirectory()) walk(p);
12125
+ else if (ent.isFile() && /\.(tsx?|jsx?)$/.test(ent.name)) {
12126
+ names.add(ent.name.replace(/\.(tsx|ts|jsx|js)$/, ""));
12127
+ }
12128
+ }
12129
+ };
12130
+ try {
12131
+ walk(cacheDir);
12132
+ } catch {
12133
+ }
12134
+ return [...names].sort();
12135
+ }
12136
+ function contentHash(content) {
12137
+ return (0, import_node_crypto7.createHash)("sha256").update(content, "utf8").digest("hex");
12138
+ }
12139
+ function buildRegistryComponentsCheck(input) {
12140
+ const base2 = {
12141
+ ok: true,
12142
+ label: REGISTRY_COMPONENTS_LABEL,
12143
+ fix: "registry components are current in `.mmi/design-system/components`"
12144
+ };
12145
+ if (!input.isConsumerRepo || input.components.length === 0) return base2;
12146
+ if (input.registryUnreachable) {
12147
+ return {
12148
+ ok: false,
12149
+ label: REGISTRY_COMPONENTS_LABEL,
12150
+ fix: REGISTRY_UNREACHABLE_FIX,
12151
+ components: input.components,
12152
+ cacheVersion: input.cacheVersion,
12153
+ targetVersion: input.targetVersion
12154
+ };
12155
+ }
12156
+ const missing = input.missingComponents ?? [];
12157
+ const stale = input.staleComponents ?? [];
12158
+ const versionBehind = Boolean(input.targetVersion) && (!input.cacheVersion || compareVersions(input.cacheVersion, input.targetVersion) < 0);
12159
+ if (missing.length || stale.length || versionBehind) {
12160
+ return {
12161
+ ok: false,
12162
+ label: REGISTRY_COMPONENTS_LABEL,
12163
+ fix: REGISTRY_FIX,
12164
+ components: input.components,
12165
+ cacheVersion: input.cacheVersion,
12166
+ targetVersion: input.targetVersion,
12167
+ staleComponents: [.../* @__PURE__ */ new Set([...missing, ...stale])]
12168
+ };
12169
+ }
12170
+ return {
12171
+ ...base2,
12172
+ components: input.components,
12173
+ cacheVersion: input.cacheVersion,
12174
+ targetVersion: input.targetVersion
12175
+ };
12176
+ }
12177
+ function cacheRelativePath(target) {
12178
+ const prefix = "components/";
12179
+ return target.startsWith(prefix) ? target.slice(prefix.length) : target;
12180
+ }
12181
+ async function fetchRegistryItem(url, deps) {
12182
+ try {
12183
+ const res = await deps.fetch(url, { signal: AbortSignal.timeout(8e3) });
12184
+ if (!res.ok) return void 0;
12185
+ const json = await res.json();
12186
+ if (!json?.name || !Array.isArray(json.files)) return void 0;
12187
+ return json;
12188
+ } catch {
12189
+ return void 0;
12190
+ }
12191
+ }
12192
+ async function gatherRegistryComponentsState(root, targetVersion, deps) {
12193
+ const template = resolveRegistryUrlTemplate(root);
12194
+ const components = listInstalledRegistryComponents(root);
12195
+ if (!hasMutmutcoRegistry(root)) return { isConsumerRepo: false, components: [] };
12196
+ if (components.length === 0) return { isConsumerRepo: true, components: [] };
12197
+ if (!template) return { isConsumerRepo: true, components, registryUnreachable: true };
12198
+ const cacheDir = resolveCacheDir(root);
12199
+ const manifest = readDesignSystemManifest(root);
12200
+ const missing = [];
12201
+ const stale = [];
12202
+ let fetchFailed = false;
12203
+ for (const name of components) {
12204
+ const item = await fetchRegistryItem(registryItemUrl(template, name), deps);
12205
+ if (!item) {
12206
+ fetchFailed = true;
12207
+ continue;
12208
+ }
12209
+ let componentStale = false;
12210
+ for (const file of item.files) {
12211
+ if (!file.target || file.content == null) continue;
12212
+ const cachePath = (0, import_node_path16.join)(cacheDir, cacheRelativePath(file.target));
12213
+ if (!(0, import_node_fs18.existsSync)(cachePath)) {
12214
+ componentStale = true;
12215
+ break;
12216
+ }
12217
+ try {
12218
+ if (contentHash((0, import_node_fs18.readFileSync)(cachePath, "utf8")) !== contentHash(file.content)) {
12219
+ componentStale = true;
12220
+ break;
12221
+ }
12222
+ } catch {
12223
+ componentStale = true;
12224
+ break;
12225
+ }
12226
+ }
12227
+ if (componentStale) {
12228
+ if ((0, import_node_fs18.existsSync)((0, import_node_path16.join)(cacheDir, "ui", `${name}.tsx`)) || (0, import_node_fs18.existsSync)((0, import_node_path16.join)(cacheDir, `${name}.tsx`))) {
12229
+ stale.push(name);
12230
+ } else {
12231
+ missing.push(name);
12232
+ }
12233
+ }
12234
+ }
12235
+ return {
12236
+ isConsumerRepo: true,
12237
+ components,
12238
+ cacheVersion: manifest?.version,
12239
+ targetVersion,
12240
+ missingComponents: missing,
12241
+ staleComponents: stale,
12242
+ registryUnreachable: fetchFailed && missing.length === 0 && stale.length === 0
12243
+ };
12244
+ }
12245
+ async function applyRegistryComponentsSync(root, components, targetVersion, log, deps) {
12246
+ const template = resolveRegistryUrlTemplate(root);
12247
+ if (!template || components.length === 0) return { ok: true };
12248
+ const cacheDir = resolveCacheDir(root);
12249
+ deps.mkdir(cacheDir);
12250
+ for (const name of components) {
12251
+ log(` \u21BB syncing registry component @mutmutco/${name}\u2026`);
12252
+ const item = await fetchRegistryItem(registryItemUrl(template, name), deps);
12253
+ if (!item) return { ok: false };
12254
+ for (const file of item.files) {
12255
+ if (!file.target || file.content == null) continue;
12256
+ const outPath = (0, import_node_path16.join)(cacheDir, cacheRelativePath(file.target));
12257
+ deps.mkdir((0, import_node_path16.dirname)(outPath));
12258
+ const body = file.content.endsWith("\n") ? file.content : `${file.content}
12259
+ `;
12260
+ deps.writeFile(outPath, body);
12261
+ }
12262
+ }
12263
+ const manifestPath = (0, import_node_path16.join)(root, DESIGN_SYSTEM_MANIFEST_PATH);
12264
+ deps.mkdir((0, import_node_path16.dirname)(manifestPath));
12265
+ const manifest = {
12266
+ version: targetVersion,
12267
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
12268
+ components: [...components],
12269
+ registryUrl: template
12270
+ };
12271
+ deps.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
12272
+ `);
12273
+ return { ok: true, cacheVersion: targetVersion };
12274
+ }
12275
+ function defaultRegistrySyncDeps() {
12276
+ return {
12277
+ fetch,
12278
+ writeFile: (path2, content) => atomicWriteFileSync(path2, content),
12279
+ mkdir: (path2) => (0, import_node_fs18.mkdirSync)(path2, { recursive: true })
12280
+ };
12281
+ }
12282
+
12283
+ // src/stage-runner.ts
12284
+ var import_node_child_process9 = require("node:child_process");
12285
+ var import_node_fs19 = require("node:fs");
12286
+ var import_node_path17 = require("node:path");
12287
+ var import_node_net2 = require("node:net");
12288
+ var import_node_util7 = require("node:util");
12289
+ var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process9.execFile);
12290
+ var EARLY_EXIT_GRACE_MS = 2e3;
12291
+ function waitForProcessStability(child, graceMs = EARLY_EXIT_GRACE_MS) {
12292
+ return new Promise((resolve, reject) => {
12293
+ let settled = false;
12294
+ const finish = (fn) => {
12295
+ if (settled) return;
12296
+ settled = true;
12297
+ clearTimeout(timer);
12298
+ child.removeAllListeners("error");
12299
+ child.removeAllListeners("exit");
12300
+ fn();
12301
+ };
12302
+ const timer = setTimeout(() => finish(resolve), graceMs);
12303
+ child.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
12304
+ child.on("exit", (code, signal) => {
12305
+ const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
12306
+ finish(() => reject(new Error(`stage process exited before health check (${detail})`)));
12307
+ });
12308
+ });
12309
+ }
12310
+ function envFileKeys(content) {
12311
+ const keys = /* @__PURE__ */ new Set();
12312
+ for (const line of content.split(/\r?\n/)) {
12313
+ const trimmed = line.trim();
12314
+ if (!trimmed || trimmed.startsWith("#")) continue;
12315
+ const eq = trimmed.indexOf("=");
12316
+ if (eq > 0) keys.add(trimmed.slice(0, eq).trim());
12317
+ }
12318
+ return keys;
12319
+ }
12320
+ function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
12321
+ const example = normalizeEol(exampleContent);
12322
+ const target = normalizeEol(targetContent);
12323
+ const exampleKeys = envFileKeys(example);
12324
+ const targetKeys = envFileKeys(target);
12325
+ for (const key of exampleKeys) {
12326
+ if (!targetKeys.has(key)) return `missing key ${key} from .env.example`;
12327
+ }
12328
+ return void 0;
12329
+ }
12330
+ function stageStatePath(cwd = process.cwd()) {
12331
+ return (0, import_node_path17.join)(cwd, "tmp", "stage", "state.json");
12332
+ }
12333
+ function mergeEnvSecretsIntoFile(content, secrets) {
12334
+ const lines = content.split(/\r?\n/);
12335
+ const indexByKey = /* @__PURE__ */ new Map();
12336
+ for (let i = 0; i < lines.length; i++) {
12337
+ const trimmed = lines[i].trim();
12338
+ if (!trimmed || trimmed.startsWith("#")) continue;
12339
+ const eq = trimmed.indexOf("=");
12340
+ if (eq === -1) continue;
12341
+ indexByKey.set(trimmed.slice(0, eq).trim(), i);
12342
+ }
12343
+ for (const [key, value] of Object.entries(secrets)) {
12344
+ const escaped = /[\s#"'\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
12345
+ const line = `${key}=${escaped}`;
12346
+ const idx = indexByKey.get(key);
12347
+ if (idx != null) lines[idx] = line;
12348
+ else lines.push(line);
12349
+ }
12350
+ const body = lines.join("\n");
12351
+ return body.endsWith("\n") ? body : `${body}
12352
+ `;
12353
+ }
12354
+ var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
12355
+ function posixOnlyShellProblems(command, field, platform2 = process.platform) {
12356
+ if (platform2 !== "win32" || !command?.trim()) return [];
12357
+ const problems = [];
12358
+ if (/(^|&&|\||;)\s*[A-Za-z_][A-Za-z0-9_]*=\S/.test(command)) {
11668
12359
  problems.push(
11669
12360
  `stage.${field} uses POSIX inline env assignment (VAR=value command) which fails in cmd.exe on Windows; use 'set VAR=value && command' or a cross-platform launcher`
11670
12361
  );
@@ -11720,9 +12411,9 @@ async function shell(command, cwd, timeoutMs) {
11720
12411
  });
11721
12412
  }
11722
12413
  function readState(path2) {
11723
- if (!(0, import_node_fs16.existsSync)(path2)) return null;
12414
+ if (!(0, import_node_fs19.existsSync)(path2)) return null;
11724
12415
  try {
11725
- return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
12416
+ return JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
11726
12417
  } catch {
11727
12418
  return null;
11728
12419
  }
@@ -11774,7 +12465,7 @@ async function stopStage(opts = {}) {
11774
12465
  return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
11775
12466
  }
11776
12467
  await killTree(state.pid);
11777
- (0, import_node_fs16.rmSync)(statePath, { force: true });
12468
+ (0, import_node_fs19.rmSync)(statePath, { force: true });
11778
12469
  return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
11779
12470
  }
11780
12471
  async function startStage(config = {}, opts = {}) {
@@ -11783,7 +12474,7 @@ async function startStage(config = {}, opts = {}) {
11783
12474
  const cwd = opts.cwd ?? process.cwd();
11784
12475
  const statePath = opts.statePath ?? stageStatePath(cwd);
11785
12476
  const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
11786
- (0, import_node_fs16.mkdirSync)(dir, { recursive: true });
12477
+ (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
11787
12478
  let stagePort;
11788
12479
  if (config.portRange) {
11789
12480
  const [s, e] = config.portRange;
@@ -11793,14 +12484,14 @@ async function startStage(config = {}, opts = {}) {
11793
12484
  }
11794
12485
  const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
11795
12486
  if (config.ensureEnv) {
11796
- const target = (0, import_node_path15.join)(cwd, config.ensureEnv.target);
11797
- const example = (0, import_node_path15.join)(cwd, config.ensureEnv.example);
11798
- if (!(0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11799
- (0, import_node_fs16.copyFileSync)(example, target);
11800
- } else if ((0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11801
- const stale = detectStaleEnvFile((0, import_node_fs16.readFileSync)(example, "utf8"), (0, import_node_fs16.readFileSync)(target, "utf8"), {
11802
- exampleMtimeMs: (0, import_node_fs16.statSync)(example).mtimeMs,
11803
- targetMtimeMs: (0, import_node_fs16.statSync)(target).mtimeMs
12487
+ const target = (0, import_node_path17.join)(cwd, config.ensureEnv.target);
12488
+ const example = (0, import_node_path17.join)(cwd, config.ensureEnv.example);
12489
+ if (!(0, import_node_fs19.existsSync)(target) && (0, import_node_fs19.existsSync)(example)) {
12490
+ (0, import_node_fs19.copyFileSync)(example, target);
12491
+ } else if ((0, import_node_fs19.existsSync)(target) && (0, import_node_fs19.existsSync)(example)) {
12492
+ const stale = detectStaleEnvFile((0, import_node_fs19.readFileSync)(example, "utf8"), (0, import_node_fs19.readFileSync)(target, "utf8"), {
12493
+ exampleMtimeMs: (0, import_node_fs19.statSync)(example).mtimeMs,
12494
+ targetMtimeMs: (0, import_node_fs19.statSync)(target).mtimeMs
11804
12495
  });
11805
12496
  if (stale) {
11806
12497
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -11808,14 +12499,14 @@ async function startStage(config = {}, opts = {}) {
11808
12499
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
11809
12500
  }
11810
12501
  }
11811
- if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs16.existsSync)(target)) {
11812
- (0, import_node_fs16.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs16.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
12502
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs19.existsSync)(target)) {
12503
+ (0, import_node_fs19.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs19.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
11813
12504
  }
11814
12505
  }
11815
12506
  const extraEnv = {};
11816
12507
  for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
11817
12508
  const up = sub(config.up.trim());
11818
- const child = (0, import_node_child_process8.spawn)(up, {
12509
+ const child = (0, import_node_child_process9.spawn)(up, {
11819
12510
  cwd,
11820
12511
  shell: true,
11821
12512
  // POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
@@ -11834,13 +12525,13 @@ async function startStage(config = {}, opts = {}) {
11834
12525
  healthUrl: sub(config.healthUrl?.trim()) || void 0,
11835
12526
  port: stagePort
11836
12527
  };
11837
- (0, import_node_fs16.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
12528
+ (0, import_node_fs19.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
11838
12529
  try {
11839
12530
  if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
11840
12531
  else await waitForProcessStability(child);
11841
12532
  } catch (e) {
11842
12533
  await killTree(state.pid);
11843
- (0, import_node_fs16.rmSync)(statePath, { force: true });
12534
+ (0, import_node_fs19.rmSync)(statePath, { force: true });
11844
12535
  throw e;
11845
12536
  }
11846
12537
  const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
@@ -11884,7 +12575,10 @@ async function reconcileDirtyOrgSpineBeforePull(deps, branch, options = {}) {
11884
12575
  // src/git-clean-tree.ts
11885
12576
  function isAgentScratchPath(path2) {
11886
12577
  const normalized = path2.replace(/\\/g, "/").trim();
11887
- return normalized === ".mmi" || normalized.startsWith(".mmi/");
12578
+ if (normalized === ".mmi" || normalized.startsWith(".mmi/")) {
12579
+ return true;
12580
+ }
12581
+ return /^tmp_[^/]+$/.test(normalized);
11888
12582
  }
11889
12583
  function porcelainHasBlockingChanges(porcelain) {
11890
12584
  return porcelain.split("\n").some((line) => {
@@ -11895,6 +12589,35 @@ function porcelainHasBlockingChanges(porcelain) {
11895
12589
  });
11896
12590
  }
11897
12591
 
12592
+ // src/tenant-control-parse.ts
12593
+ var OUTPUT_BEGIN = "mmi-control-output-begin";
12594
+ var OUTPUT_END = "mmi-control-output-end";
12595
+ function extractControlOutputFromLog(log) {
12596
+ const lines = log.split(/\r?\n/);
12597
+ const start = lines.findIndex((l) => l.trim() === OUTPUT_BEGIN);
12598
+ if (start < 0) return "";
12599
+ const end = lines.findIndex((l, i) => i > start && l.trim() === OUTPUT_END);
12600
+ const slice = end < 0 ? lines.slice(start + 1) : lines.slice(start + 1, end);
12601
+ return slice.join("\n").trim();
12602
+ }
12603
+ function parseStatusSnippet(stdout) {
12604
+ const t = stdout.toLowerCase();
12605
+ const m = t.match(/service[:=]\s*(running|stopped|missing|up|down|absent)/);
12606
+ if (!m) return { serviceState: "unknown" };
12607
+ const token = m[1];
12608
+ if (token === "running" || token === "up") return { serviceState: "running" };
12609
+ if (token === "stopped" || token === "down") return { serviceState: "stopped" };
12610
+ return { serviceState: "missing" };
12611
+ }
12612
+ function parseVerifySecrets(stdout) {
12613
+ const out = [];
12614
+ for (const line of stdout.split("\n")) {
12615
+ const m = /^(\S+):\s*(match|mismatch|missing)\b/.exec(line.trim());
12616
+ if (m) out.push({ key: m[1], status: m[2] });
12617
+ }
12618
+ return out;
12619
+ }
12620
+
11898
12621
  // src/train-apply.ts
11899
12622
  function resolveDeployModel2(meta, repo) {
11900
12623
  const m = meta?.deployModel;
@@ -11969,7 +12692,8 @@ var ORG_SPINE_FILES = [
11969
12692
  ".claude/settings.json",
11970
12693
  ".claude/output-styles/mmi-plain.md",
11971
12694
  ".cursor/rules/mmi-plain-language.mdc",
11972
- ".cursor/rules/mmi-tool-economy.mdc"
12695
+ ".cursor/rules/mmi-tool-economy.mdc",
12696
+ ".cursor/rules/mmi-code-economy.mdc"
11973
12697
  ];
11974
12698
  function isSpinePath(path2) {
11975
12699
  return ORG_SPINE_FILES.includes(path2);
@@ -12101,53 +12825,23 @@ function requireProjectMetaForTrain(load, repo) {
12101
12825
  var CORRELATE_ATTEMPTS = 5;
12102
12826
  var CORRELATE_DELAY_MS = 1500;
12103
12827
  var CORRELATE_SKEW_SLACK_MS = 1e4;
12828
+ var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12829
+ function resolveSleep(deps) {
12830
+ return deps.sleep ?? defaultSleep2;
12831
+ }
12104
12832
  var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion}]";
12105
12833
  var TRAIN_COMMIT_STATUS_JQ = "[.statuses[]|{context:.context,state:.state}]";
12106
12834
  var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
12107
12835
  var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
12108
12836
  var TRAIN_CHECK_ATTEMPTS = 40;
12109
12837
  var TRAIN_CHECK_DELAY_MS = 15e3;
12110
- async function correlateDispatchedRun(deps, workflow, since) {
12111
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
12112
- const threshold = since - CORRELATE_SKEW_SLACK_MS;
12113
- for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
12114
- if (attempt > 0) await sleep(CORRELATE_DELAY_MS);
12115
- let rows;
12116
- try {
12117
- const out = await deps.run("gh", [
12118
- "run",
12119
- "list",
12120
- "--repo",
12121
- HUB_REPO3,
12122
- "--workflow",
12123
- workflow,
12124
- "--limit",
12125
- "10",
12126
- "--json",
12127
- "databaseId,url,event,createdAt"
12128
- ]);
12129
- rows = JSON.parse(out);
12130
- } catch {
12131
- continue;
12132
- }
12133
- const match = rows.filter((r) => r.event === "workflow_dispatch" && typeof r.databaseId === "number").map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
12134
- if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
12135
- }
12136
- return {};
12137
- }
12138
- function correlateTenantRun(deps, since) {
12139
- return correlateDispatchedRun(deps, "tenant-deploy.yml", since);
12140
- }
12141
- function correlatePublishRun(deps, since) {
12142
- return correlateDispatchedRun(deps, "tenant-publish.yml", since);
12143
- }
12144
- async function correlateWorkflowRun(deps, args) {
12145
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
12838
+ async function correlateRun(deps, args) {
12839
+ const sleep2 = resolveSleep(deps);
12146
12840
  const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
12147
12841
  let lastError;
12148
12842
  let parsedAnyResponse = false;
12149
12843
  for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
12150
- if (attempt > 0) await sleep(CORRELATE_DELAY_MS);
12844
+ if (attempt > 0) await sleep2(CORRELATE_DELAY_MS);
12151
12845
  const listArgs = [
12152
12846
  "run",
12153
12847
  "list",
@@ -12155,28 +12849,43 @@ async function correlateWorkflowRun(deps, args) {
12155
12849
  HUB_REPO3,
12156
12850
  "--workflow",
12157
12851
  args.workflow,
12158
- "--event",
12159
- args.event,
12160
- ...args.branch ? ["--branch", args.branch] : [],
12852
+ ...args.mode === "workflow" ? ["--event", args.event] : [],
12853
+ ...args.mode === "workflow" && args.branch ? ["--branch", args.branch] : [],
12161
12854
  "--limit",
12162
12855
  "10",
12163
12856
  "--json",
12164
- "databaseId,url,event,createdAt,status,conclusion,headSha"
12857
+ args.mode === "dispatch" ? "databaseId,url,event,createdAt" : "databaseId,url,event,createdAt,status,conclusion,headSha"
12165
12858
  ];
12166
12859
  let rows;
12167
12860
  try {
12168
12861
  rows = JSON.parse(await deps.run("gh", listArgs));
12169
12862
  parsedAnyResponse = true;
12170
12863
  } catch {
12171
- lastError = new Error(`could not list ${args.workflow} runs`);
12864
+ if (args.mode === "workflow") lastError = new Error(`could not list ${args.workflow} runs`);
12172
12865
  continue;
12173
12866
  }
12174
- const match = rows.filter((r) => r.event === args.event && r.headSha === args.headSha && typeof r.databaseId === "number").map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
12867
+ const match = rows.filter((r) => {
12868
+ if (typeof r.databaseId !== "number") return false;
12869
+ if (args.mode === "dispatch") return r.event === "workflow_dispatch";
12870
+ return r.event === args.event && r.headSha === args.headSha;
12871
+ }).map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
12175
12872
  if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
12176
12873
  }
12177
- if (!parsedAnyResponse && lastError) throw lastError;
12874
+ if (args.mode === "workflow" && !parsedAnyResponse && lastError) throw lastError;
12178
12875
  return {};
12179
12876
  }
12877
+ function correlateTenantRun(deps, since) {
12878
+ return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch" });
12879
+ }
12880
+ function correlatePublishRun(deps, since) {
12881
+ return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
12882
+ }
12883
+ function correlateControlRun(deps, since) {
12884
+ return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch" });
12885
+ }
12886
+ async function correlateWorkflowRun(deps, args) {
12887
+ return correlateRun(deps, { ...args, mode: "workflow" });
12888
+ }
12180
12889
  async function watchTenantRun(deps, runId) {
12181
12890
  if (runId == null) return "pending";
12182
12891
  try {
@@ -12186,6 +12895,13 @@ async function watchTenantRun(deps, runId) {
12186
12895
  return "failure";
12187
12896
  }
12188
12897
  }
12898
+ async function fetchControlRunLog(deps, runId) {
12899
+ try {
12900
+ return await deps.run("gh", ["run", "view", String(runId), "--repo", HUB_REPO3, "--log"]);
12901
+ } catch {
12902
+ return "";
12903
+ }
12904
+ }
12189
12905
  async function watchWorkflowRun(deps, workflow, run) {
12190
12906
  if (run.runId == null) return { workflow, conclusion: "pending" };
12191
12907
  const conclusion = await watchTenantRun(deps, run.runId);
@@ -12286,11 +13002,11 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
12286
13002
  if (required.length === 0) {
12287
13003
  return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
12288
13004
  }
12289
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
13005
+ const sleep2 = resolveSleep(deps);
12290
13006
  let lastStatus = "not checked";
12291
13007
  let lastError;
12292
13008
  for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
12293
- if (attempt > 0) await sleep(TRAIN_CHECK_DELAY_MS);
13009
+ if (attempt > 0) await sleep2(TRAIN_CHECK_DELAY_MS);
12294
13010
  let checkRuns;
12295
13011
  let statuses;
12296
13012
  try {
@@ -12366,18 +13082,77 @@ function isTransientDispatchFailure(e) {
12366
13082
  return /timed? ?out|timeout|aborted|network|fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(msg);
12367
13083
  }
12368
13084
  async function dispatchTenantDeployWithRetry(deps, input) {
12369
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
13085
+ const sleep2 = resolveSleep(deps);
12370
13086
  for (let attempt = 1; ; attempt++) {
12371
13087
  try {
12372
13088
  await deps.dispatchTenantDeploy(input);
12373
13089
  return;
12374
13090
  } catch (e) {
12375
13091
  if (attempt >= DISPATCH_ATTEMPTS || !isTransientDispatchFailure(e)) throw e;
12376
- await sleep(DISPATCH_RETRY_DELAY_MS * attempt);
13092
+ await sleep2(DISPATCH_RETRY_DELAY_MS * attempt);
12377
13093
  }
12378
13094
  }
12379
13095
  }
12380
- async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw") {
13096
+ function tenantPublishRecoveryCommand(slug, repo, ref, stage2, publishDir) {
13097
+ const parts = [
13098
+ `gh workflow run tenant-publish.yml --repo ${HUB_REPO3}`,
13099
+ `-f slug=${slug}`,
13100
+ `-f repo=${repo}`,
13101
+ `-f ref=${ref}`,
13102
+ `-f stage=${stage2}`
13103
+ ];
13104
+ if (publishDir && publishDir !== ".") parts.push(`-f publishDir=${publishDir}`);
13105
+ return parts.join(" ");
13106
+ }
13107
+ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure = "throw", publishDir) {
13108
+ const since = (deps.now ?? Date.now)();
13109
+ const dispatchArgs = [
13110
+ "workflow",
13111
+ "run",
13112
+ "tenant-publish.yml",
13113
+ "--repo",
13114
+ HUB_REPO3,
13115
+ "-f",
13116
+ `slug=${ctx.slug}`,
13117
+ "-f",
13118
+ `repo=${ctx.repo}`,
13119
+ "-f",
13120
+ `ref=${ref}`,
13121
+ "-f",
13122
+ `stage=${stage2}`
13123
+ ];
13124
+ if (publishDir && publishDir !== ".") dispatchArgs.push("-f", `publishDir=${publishDir}`);
13125
+ try {
13126
+ await deps.run("gh", dispatchArgs);
13127
+ } catch (e) {
13128
+ if (dispatchFailure === "throw") throw e;
13129
+ const msg = e instanceof Error ? e.message : String(e);
13130
+ const recovery = tenantPublishRecoveryCommand(ctx.slug, ctx.repo, ref, stage2, publishDir);
13131
+ return {
13132
+ note: `tenant-publish dispatch FAILED: ${msg}. The promotion itself landed \u2014 recover with \`${recovery}\``,
13133
+ deployStatus: "failure"
13134
+ };
13135
+ }
13136
+ const { runId, runUrl } = await correlatePublishRun(deps, since);
13137
+ const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
13138
+ return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
13139
+ }
13140
+ async function dispatchPublishIfRequired(deps, ctx, meta, model, stage2, publishRef, watch, dispatchFailure) {
13141
+ if (!meta.publishRequired || stage2 !== "main") return null;
13142
+ if (model !== "tenant-container" && model !== "solo-container") return null;
13143
+ return dispatchTenantPublish(deps, ctx, stage2, publishRef, watch, dispatchFailure, meta.publishDir);
13144
+ }
13145
+ function appendPublishDispatch(deploy, publish) {
13146
+ if (!publish) return deploy;
13147
+ return {
13148
+ note: `${deploy.note}; ${publish.note}`,
13149
+ runId: deploy.runId,
13150
+ runUrl: deploy.runUrl,
13151
+ workflowRuns: [...deploy.workflowRuns ?? [], ...publish.workflowRuns ?? [{ workflow: "tenant-publish.yml", runId: publish.runId, runUrl: publish.runUrl, conclusion: publish.deployStatus }]],
13152
+ deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
13153
+ };
13154
+ }
13155
+ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
12381
13156
  if (model === "tenant-container" || model === "solo-container") {
12382
13157
  const since = (deps.now ?? Date.now)();
12383
13158
  try {
@@ -12395,25 +13170,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
12395
13170
  return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
12396
13171
  }
12397
13172
  if (model === "registry-publish") {
12398
- const since = (deps.now ?? Date.now)();
12399
- await deps.run("gh", [
12400
- "workflow",
12401
- "run",
12402
- "tenant-publish.yml",
12403
- "--repo",
12404
- HUB_REPO3,
12405
- "-f",
12406
- `slug=${ctx.slug}`,
12407
- "-f",
12408
- `repo=${ctx.repo}`,
12409
- "-f",
12410
- `ref=${ref}`,
12411
- "-f",
12412
- `stage=${stage2}`
12413
- ]);
12414
- const { runId, runUrl } = await correlatePublishRun(deps, since);
12415
- const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
12416
- return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
13173
+ return dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure, publishDir);
12417
13174
  }
12418
13175
  if (model === "hub-serverless") {
12419
13176
  const note = ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
@@ -12455,107 +13212,99 @@ async function preflight(deps, ctx, stage2, meta) {
12455
13212
  await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
12456
13213
  return model;
12457
13214
  }
12458
- async function runTrainApply(command, deps, options = {}) {
12459
- const watch = options.watch ?? false;
12460
- const ctx = await buildTrainApplyContext(deps);
12461
- await requireCleanTree(deps);
12462
- await deps.run("git", ["fetch", "origin"]);
12463
- const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
12464
- const branchHints = await loadReleaseTrackBranchHints(deps);
12465
- const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
12466
- if (command === "rcand") {
12467
- await requireBranch(deps, "development");
12468
- if (directTrack) {
12469
- throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
12470
- }
12471
- await ffOnlyPull(deps, "development");
12472
- ensurePositiveCount(
12473
- await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
12474
- "nothing to promote: origin/development is not ahead of origin/rc"
12475
- );
12476
- const deployModel2 = await preflight(deps, ctx, "rc", meta);
12477
- const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
12478
- await deps.run("git", ["checkout", "rc"]);
12479
- await ffOnlyPull(deps, "rc");
12480
- await deps.run("git", ["merge", "development", "--no-edit"]);
12481
- const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
12482
- const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
12483
- const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
12484
- const resumeNote = resume.tag ? resume.note : void 0;
12485
- await ensureTagPushed(deps, tag2, rcSha);
12486
- const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
12487
- const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks2);
12488
- const autoRunSince2 = (deps.now ?? Date.now)();
12489
- await deps.run("git", ["push", "origin", "rc"]);
12490
- const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince2, rcSha);
12491
- return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
13215
+ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix, realignMessage) {
13216
+ const foldPaths = await resolveFoldPaths(deps, deployModel);
13217
+ const tolerated = [...foldPaths, ...RELEASE_TOLERATED_PATHS];
13218
+ const predicted = await predictMergeConflicts(deps, "origin/main", remoteRef);
13219
+ const predictedBlocking = predicted.filter((f) => !isSpinePath(f) && !tolerated.includes(f));
13220
+ if (predictedBlocking.length > 0) {
13221
+ throw new Error(`${blockingPrefix}: ${predictedBlocking.join(", ")} \u2014 no merge was started. ${realignMessage}`);
12492
13222
  }
12493
- if (directTrack) {
12494
- await requireBranch(deps, "development");
12495
- await ffOnlyPull(deps, "development");
12496
- ensurePositiveCount(
12497
- await deps.run("git", ["rev-list", "--count", "origin/main..origin/development"]),
12498
- "nothing to release: origin/development is not ahead of origin/main"
12499
- );
12500
- const deployModel2 = await preflight(deps, ctx, "main", meta);
12501
- const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
12502
- const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
12503
- const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
12504
- const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
12505
- const predictedBlocking2 = predicted2.filter((f) => !isSpinePath(f) && !tolerated2.includes(f));
12506
- if (predictedBlocking2.length > 0) {
12507
- throw new Error(
12508
- `development -> main merge would conflict on non-spine path(s): ${predictedBlocking2.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release.`
12509
- );
12510
- }
12511
- await deps.run("git", ["checkout", "main"]);
12512
- await ffOnlyPull(deps, "main");
12513
- if (predicted2.length === 0) {
12514
- await deps.run("git", ["merge", "development", "--no-edit"]);
12515
- } else {
12516
- await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
12517
- }
12518
- const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
12519
- const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
12520
- await ensureTagPushed(deps, tag2, releaseSha2);
12521
- const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
12522
- const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
12523
- await deps.run("git", ["push", "origin", "main"]);
12524
- const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
12525
- await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
12526
- const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
12527
- const autoRunSince2 = (deps.now ?? Date.now)();
12528
- const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2, "report");
12529
- const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
12530
- return {
12531
- ...ctx,
12532
- command,
12533
- stage: "main",
12534
- ref: "main",
12535
- tag: tag2,
12536
- deployModel: deployModel2,
12537
- promoted: true,
12538
- checks: checks2,
12539
- versionFold: versionFold2,
12540
- dispatch: d2.note,
12541
- runId: d2.runId,
12542
- runUrl: d2.runUrl,
12543
- workflowRuns: d2.workflowRuns,
12544
- deployStatus: d2.deployStatus,
12545
- rcRetirement: "not-applicable",
12546
- rcRetirementNote: "direct-track release skips rc; no rc runtime to retire",
12547
- devRollForward: devRollForward2,
12548
- announceNote: announceNote2,
12549
- // #1062: --dev on a direct-track repo is a friendly no-op — it already releases from development.
12550
- devNote: options.dev ? "--dev is a no-op on a direct-track repo \u2014 it already releases development -> main" : void 0,
12551
- release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
13223
+ return { foldPaths, tolerated, predicted };
13224
+ }
13225
+ async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted) {
13226
+ await deps.run("git", ["checkout", "main"]);
13227
+ await ffOnlyPull(deps, "main");
13228
+ if (predicted.length === 0) {
13229
+ await deps.run("git", ["merge", sourceRef, "--no-edit"]);
13230
+ } else {
13231
+ await mergeWithSpineResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
13232
+ }
13233
+ }
13234
+ async function mergeSourceToMain(deps, deployModel, args) {
13235
+ const { foldPaths, tolerated, predicted } = await preflightMergeToMain(
13236
+ deps,
13237
+ deployModel,
13238
+ args.remoteRef,
13239
+ args.blockingPrefix,
13240
+ args.realignMessage
13241
+ );
13242
+ await executeMergeToMain(deps, args.sourceRef, args.mergeLabel, tolerated, predicted);
13243
+ return { foldPaths };
13244
+ }
13245
+ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
13246
+ await ensureTagPushed(deps, tag, releaseSha);
13247
+ const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
13248
+ const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
13249
+ await deps.run("git", ["push", "origin", "main"]);
13250
+ const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
13251
+ await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
13252
+ const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
13253
+ const autoRunSince = (deps.now ?? Date.now)();
13254
+ const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
13255
+ const publishDispatch = deployDispatch.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report");
13256
+ let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
13257
+ if (!publishDispatch && deployDispatch.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
13258
+ dispatch = {
13259
+ ...dispatch,
13260
+ note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
12552
13261
  };
12553
13262
  }
12554
- if (command === "release" && options.dev) {
13263
+ return { checks, releaseUrl, announceNote, dispatch };
13264
+ }
13265
+ async function pushRcAlignment(deps) {
13266
+ try {
13267
+ await deps.run("git", ["push", "origin", "main:rc"]);
13268
+ return "origin/rc aligned to the released main";
13269
+ } catch (e) {
13270
+ return `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
13271
+ }
13272
+ }
13273
+ async function runTrainApplyPipeline(mode, input) {
13274
+ const { deps, ctx, command, meta, branchHints, watch, options } = input;
13275
+ const directTrack = input.directTrack ?? false;
13276
+ if (mode === "rcand") {
13277
+ await requireBranch(deps, "development");
13278
+ if (directTrack) {
13279
+ throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
13280
+ }
13281
+ await ffOnlyPull(deps, "development");
13282
+ ensurePositiveCount(
13283
+ await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
13284
+ "nothing to promote: origin/development is not ahead of origin/rc"
13285
+ );
13286
+ const deployModel2 = await preflight(deps, ctx, "rc", meta);
13287
+ const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
13288
+ await deps.run("git", ["checkout", "rc"]);
13289
+ await ffOnlyPull(deps, "rc");
13290
+ await deps.run("git", ["merge", "development", "--no-edit"]);
13291
+ const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
13292
+ const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
13293
+ const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
13294
+ const resumeNote = resume.tag ? resume.note : void 0;
13295
+ await ensureTagPushed(deps, tag2, rcSha);
13296
+ const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
13297
+ const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
13298
+ const autoRunSince = (deps.now ?? Date.now)();
13299
+ await deps.run("git", ["push", "origin", "rc"]);
13300
+ const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
13301
+ return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
13302
+ }
13303
+ if (mode === "release-dev") {
12555
13304
  await requireBranch(deps, "development");
12556
13305
  await ffOnlyPull(deps, "development");
12557
13306
  const hasRcBranch = branchHints.hasRcBranch ?? false;
12558
- if (hasRcBranch) {
13307
+ if (!directTrack && hasRcBranch) {
12559
13308
  const rcOnlyOut = (await deps.run("git", ["rev-list", "--count", "--right-only", "--cherry-pick", "--no-merges", "origin/development...origin/rc"])).trim();
12560
13309
  const rcOnly = Number.parseInt(rcOnlyOut, 10);
12561
13310
  if (!Number.isFinite(rcOnly)) throw new Error(`release --dev: could not count rc-only commits for the guard: ${rcOnlyOut || "(empty)"}`);
@@ -12571,47 +13320,44 @@ async function runTrainApply(command, deps, options = {}) {
12571
13320
  );
12572
13321
  const deployModel2 = await preflight(deps, ctx, "main", meta);
12573
13322
  const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
12574
- const rcShaAtRelease = hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
12575
- const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
12576
- const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
12577
- const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
12578
- const predictedBlocking2 = predicted2.filter((f) => !isSpinePath(f) && !tolerated2.includes(f));
12579
- if (predictedBlocking2.length > 0) {
12580
- throw new Error(
12581
- `development -> main merge would conflict on non-spine path(s): ${predictedBlocking2.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release.`
12582
- );
12583
- }
12584
- await deps.run("git", ["checkout", "main"]);
12585
- await ffOnlyPull(deps, "main");
12586
- if (predicted2.length === 0) {
12587
- await deps.run("git", ["merge", "development", "--no-edit"]);
12588
- } else {
12589
- await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
12590
- }
13323
+ const rcShaAtRelease = !directTrack && hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
13324
+ const { foldPaths: foldPaths2 } = await mergeSourceToMain(deps, deployModel2, {
13325
+ sourceRef: "development",
13326
+ remoteRef: "origin/development",
13327
+ mergeLabel: "development -> main",
13328
+ blockingPrefix: "development -> main merge would conflict on non-spine path(s)",
13329
+ realignMessage: "The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
13330
+ });
12591
13331
  const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
12592
13332
  const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
12593
- await ensureTagPushed(deps, tag2, releaseSha2);
12594
- const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
12595
- const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
12596
- await deps.run("git", ["push", "origin", "main"]);
12597
- const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
12598
- await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
12599
- const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
12600
- const autoRunSince2 = (deps.now ?? Date.now)();
12601
- const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2, "report");
12602
- const retirement2 = hasRcBranch ? await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease) : { status: "not-applicable", note: "no origin/rc branch \u2014 rc runtime retirement skipped" };
13333
+ const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
12603
13334
  const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
12604
- let rcAlignment2;
12605
- if (hasRcBranch) {
12606
- try {
12607
- await deps.run("git", ["push", "origin", "main:rc"]);
12608
- rcAlignment2 = "origin/rc aligned to the released main";
12609
- } catch (e) {
12610
- rcAlignment2 = `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
12611
- }
12612
- } else {
12613
- rcAlignment2 = "no origin/rc branch \u2014 rc alignment skipped";
13335
+ if (directTrack) {
13336
+ return {
13337
+ ...ctx,
13338
+ command,
13339
+ stage: "main",
13340
+ ref: "main",
13341
+ tag: tag2,
13342
+ deployModel: deployModel2,
13343
+ promoted: true,
13344
+ checks: checks2,
13345
+ versionFold: versionFold2,
13346
+ dispatch: d2.note,
13347
+ runId: d2.runId,
13348
+ runUrl: d2.runUrl,
13349
+ workflowRuns: d2.workflowRuns,
13350
+ deployStatus: d2.deployStatus,
13351
+ rcRetirement: "not-applicable",
13352
+ rcRetirementNote: "direct-track release skips rc; no rc runtime to retire",
13353
+ devRollForward: devRollForward2,
13354
+ announceNote: announceNote2,
13355
+ devNote: options.dev ? "--dev is a no-op on a direct-track repo \u2014 it already releases development -> main" : void 0,
13356
+ release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
13357
+ };
12614
13358
  }
13359
+ const retirement2 = hasRcBranch ? await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease) : { status: "not-applicable", note: "no origin/rc branch \u2014 rc runtime retirement skipped" };
13360
+ const rcAlignment2 = hasRcBranch ? await pushRcAlignment(deps) : "no origin/rc branch \u2014 rc alignment skipped";
12615
13361
  const environments2 = await buildEnvironments(deps, ctx, deployModel2, d2.deployStatus, retirement2);
12616
13362
  return {
12617
13363
  ...ctx,
@@ -12645,15 +13391,13 @@ async function runTrainApply(command, deps, options = {}) {
12645
13391
  "nothing to release: origin/rc is not ahead of origin/main"
12646
13392
  );
12647
13393
  const deployModel = await preflight(deps, ctx, "main", meta);
12648
- const foldPaths = await resolveFoldPaths(deps, deployModel);
12649
- const tolerated = [...foldPaths, ...RELEASE_TOLERATED_PATHS];
12650
- const predicted = await predictMergeConflicts(deps, "origin/main", "origin/rc");
12651
- const predictedBlocking = predicted.filter((f) => !isSpinePath(f) && !tolerated.includes(f));
12652
- if (predictedBlocking.length > 0) {
12653
- throw new Error(
12654
- `rc -> main merge would conflict on non-spine path(s): ${predictedBlocking.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release.`
12655
- );
12656
- }
13394
+ const { foldPaths, tolerated, predicted } = await preflightMergeToMain(
13395
+ deps,
13396
+ deployModel,
13397
+ "origin/rc",
13398
+ "rc -> main merge would conflict on non-spine path(s)",
13399
+ "The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release."
13400
+ );
12657
13401
  const coverage = deps.hotfixCoverage({ mainRef: "origin/main", rcRef: "origin/rc", ack: options.ack ?? [] });
12658
13402
  if (!coverage.ok) {
12659
13403
  const list = coverage.uncovered.map((c) => `${c.sha.slice(0, 8)} ${c.subject}`).join("; ");
@@ -12662,34 +13406,14 @@ async function runTrainApply(command, deps, options = {}) {
12662
13406
  );
12663
13407
  }
12664
13408
  const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
12665
- await deps.run("git", ["checkout", "main"]);
12666
- await ffOnlyPull(deps, "main");
12667
- if (predicted.length === 0) {
12668
- await deps.run("git", ["merge", "rc", "--no-edit"]);
12669
- } else {
12670
- await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs", tolerated);
12671
- }
13409
+ await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted);
12672
13410
  const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
12673
13411
  const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
12674
13412
  const releaseSha = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
12675
- await ensureTagPushed(deps, tag, releaseSha);
12676
- const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
12677
- const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
12678
- await deps.run("git", ["push", "origin", "main"]);
12679
- const autoRunSince = (deps.now ?? Date.now)();
12680
- const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
12681
- await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
12682
- const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
12683
- const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report");
13413
+ const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
12684
13414
  const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
12685
13415
  const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
12686
- let rcAlignment;
12687
- try {
12688
- await deps.run("git", ["push", "origin", "main:rc"]);
12689
- rcAlignment = "origin/rc aligned to the released main";
12690
- } catch (e) {
12691
- rcAlignment = `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
12692
- }
13416
+ const rcAlignment = await pushRcAlignment(deps);
12693
13417
  const environments = await buildEnvironments(deps, ctx, deployModel, d.deployStatus, retirement);
12694
13418
  return {
12695
13419
  ...ctx,
@@ -12716,6 +13440,29 @@ async function runTrainApply(command, deps, options = {}) {
12716
13440
  environments
12717
13441
  };
12718
13442
  }
13443
+ async function runTrainApply(command, deps, options = {}) {
13444
+ const watch = options.watch ?? false;
13445
+ const ctx = await buildTrainApplyContext(deps);
13446
+ await requireCleanTree(deps);
13447
+ await deps.run("git", ["fetch", "origin"]);
13448
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
13449
+ const branchHints = await loadReleaseTrackBranchHints(deps);
13450
+ const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
13451
+ const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options };
13452
+ if (command === "rcand") {
13453
+ if (directTrack) {
13454
+ throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
13455
+ }
13456
+ return runTrainApplyPipeline("rcand", pipelineInput);
13457
+ }
13458
+ if (directTrack) {
13459
+ return runTrainApplyPipeline("release-dev", { ...pipelineInput, directTrack: true });
13460
+ }
13461
+ if (command === "release" && options.dev) {
13462
+ return runTrainApplyPipeline("release-dev", pipelineInput);
13463
+ }
13464
+ return runTrainApplyPipeline("release-full", pipelineInput);
13465
+ }
12719
13466
  async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
12720
13467
  if (model !== "tenant-container") return void 0;
12721
13468
  const domains = deps.fetchEdgeDomains ? await deps.fetchEdgeDomains(ctx.slug).catch(() => null) : null;
@@ -12732,63 +13479,25 @@ async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
12732
13479
  if (rcDomains?.length) rc.domains = rcDomains;
12733
13480
  return { main, rc };
12734
13481
  }
12735
- var RETIRE_CATEGORIES = /* @__PURE__ */ new Set([
12736
- "retired",
12737
- "retired-edge-pending",
12738
- "ssm-command-failed",
12739
- "wait-timeout",
12740
- "transport-failed"
12741
- ]);
12742
- function retireCategoryFrom(text) {
12743
- try {
12744
- const c = JSON.parse(text).category;
12745
- return typeof c === "string" && RETIRE_CATEGORIES.has(c) ? c : void 0;
12746
- } catch {
12747
- return void 0;
12748
- }
12749
- }
12750
- var TRANSPORT_REASONS = /* @__PURE__ */ new Set(["invalid-instance", "invalid-document", "throttled", "timeout", "other"]);
12751
- function retireReasonFrom(text) {
12752
- try {
12753
- const r = JSON.parse(text).reason;
12754
- return typeof r === "string" && TRANSPORT_REASONS.has(r) ? r : void 0;
12755
- } catch {
12756
- return void 0;
12757
- }
12758
- }
12759
- function isRetryableTransport(reason) {
12760
- return reason === void 0 || reason === "throttled" || reason === "timeout" || reason === "other";
12761
- }
12762
13482
  var RETIRE_MAX_ATTEMPTS = 3;
12763
13483
  var RETIRE_BACKOFF_MS = 1500;
12764
13484
  async function attemptRetire(deps, ctx) {
12765
- try {
12766
- const out = await deps.runSelf(["tenant", "control", ctx.repo, "rc", "retire"]);
12767
- let commandId = "";
12768
- let category = retireCategoryFrom(out);
12769
- try {
12770
- commandId = String(JSON.parse(out).commandId ?? "");
12771
- } catch {
12772
- }
12773
- if (category === "retired-edge-pending") {
12774
- return { result: {
12775
- status: "retired",
12776
- category,
12777
- note: `rc runtime retired; edge vhost reconcile pending (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept`
12778
- } };
12779
- }
12780
- category = category ?? "retired";
12781
- return { result: {
13485
+ const r = await runTenantControl(deps, { repo: ctx.repo, stage: "rc", action: "retire", watch: true });
13486
+ if (r.category === "retired") {
13487
+ return {
12782
13488
  status: "retired",
12783
- category,
12784
- note: `rc runtime retired (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
12785
- } };
12786
- } catch (e) {
12787
- const err = e;
12788
- const category = retireCategoryFrom(err.stdout ?? "") ?? "transport-failed";
12789
- const reason = retireReasonFrom(err.stdout ?? "");
12790
- return { result: { status: "failed", category, note: `rc retirement failed (the release itself succeeded): ${err.message}` }, reason, message: err.message };
13489
+ category: "retired",
13490
+ note: `rc runtime retired (tenant-control.yml${r.runUrl ? `, ${r.runUrl}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
13491
+ };
13492
+ }
13493
+ if (r.category === "wait-timeout") {
13494
+ return {
13495
+ status: "failed",
13496
+ category: "wait-timeout",
13497
+ note: `rc retire dispatched but the run could not be observed \u2014 verify with: mmi-cli tenant control ${ctx.repo} rc status${r.runUrl ? ` (run ${r.runUrl})` : ""}`
13498
+ };
12791
13499
  }
13500
+ return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
12792
13501
  }
12793
13502
  async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
12794
13503
  if (model !== "tenant-container") {
@@ -12812,19 +13521,18 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
12812
13521
  note: `origin/rc moved past the released candidate (${releasedRcSha.slice(0, 7)} -> ${rcNow.slice(0, 7)}) \u2014 a new candidate is in flight; rc runtime left untouched`
12813
13522
  };
12814
13523
  }
12815
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
13524
+ const sleep2 = resolveSleep(deps);
12816
13525
  let last;
12817
13526
  for (let attempt = 1; attempt <= RETIRE_MAX_ATTEMPTS; attempt++) {
12818
13527
  last = await attemptRetire(deps, ctx);
12819
- if (last.result.status === "retired") return last.result;
12820
- const retryable = last.result.category === "transport-failed" && isRetryableTransport(last.reason);
12821
- if (!retryable || attempt === RETIRE_MAX_ATTEMPTS) break;
12822
- await sleep(RETIRE_BACKOFF_MS * attempt);
13528
+ if (last.status === "retired") return last;
13529
+ if (last.category !== "transport-failed" || attempt === RETIRE_MAX_ATTEMPTS) break;
13530
+ await sleep2(RETIRE_BACKOFF_MS * attempt);
12823
13531
  }
12824
13532
  const f = last;
12825
- const reasonSuffix = f.reason ? ` [reason: ${f.reason}]` : "";
12826
- const note = `rc retirement failed (the release itself succeeded)${reasonSuffix}: ${f.message ?? f.result.note}. The rc runtime may be orphaned on the box \u2014 retire it with: mmi-cli tenant control ${ctx.repo} rc retire (or sweep all: mmi-cli tenant sweep-rc --retire --yes)`;
12827
- return { status: "failed", category: f.result.category, note };
13533
+ if (f.category === "wait-timeout") return f;
13534
+ const note = `rc retirement failed (the release itself succeeded): ${f.note}. The rc runtime may be orphaned on the box \u2014 retire it with: mmi-cli tenant control ${ctx.repo} rc retire (or sweep all: mmi-cli tenant sweep-rc --retire --yes)`;
13535
+ return { status: "failed", category: f.category, note };
12828
13536
  } catch (e) {
12829
13537
  const err = e;
12830
13538
  return { status: "failed", category: "transport-failed", note: `rc retirement failed (the release itself succeeded): ${err.message}` };
@@ -12850,14 +13558,82 @@ async function runTenantRedeploy(deps, options) {
12850
13558
  const d = await dispatchDeploy(deps, ctx, stage2, ref, deployModel, watch);
12851
13559
  return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
12852
13560
  }
13561
+ function tenantControlWatches(action) {
13562
+ return action === "status" || action === "retire" || action === "verify-secrets";
13563
+ }
13564
+ async function runTenantControl(deps, options) {
13565
+ const { repo, stage: stage2, action } = options;
13566
+ const watch = options.watch ?? tenantControlWatches(action);
13567
+ const base2 = { command: "tenant-control", repo, stage: stage2, action };
13568
+ const since = (deps.now ?? Date.now)();
13569
+ const d = await deps.dispatchTenantControl({ repo, stage: stage2, action });
13570
+ if (!d.ok) {
13571
+ const transport = d.category === "transport-failed";
13572
+ return {
13573
+ ...base2,
13574
+ dispatched: false,
13575
+ conclusion: "failure",
13576
+ category: action === "retire" ? transport ? "transport-failed" : "dispatch-rejected" : void 0,
13577
+ note: transport ? `tenant control ${action} dispatch failed (transport) \u2014 safe to retry` : `tenant control ${action} rejected: ${d.error ?? "request rejected by the Hub"}`
13578
+ };
13579
+ }
13580
+ const { runId, runUrl } = await correlateControlRun(deps, since);
13581
+ const conclusion = watch ? await watchTenantRun(deps, runId) : "pending";
13582
+ const result = { ...base2, dispatched: true, runId, runUrl, conclusion, note: "" };
13583
+ if (action === "retire") {
13584
+ result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
13585
+ }
13586
+ if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets")) {
13587
+ const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
13588
+ if (action === "status") {
13589
+ result.serviceState = parseStatusSnippet(output).serviceState;
13590
+ } else {
13591
+ result.secrets = parseVerifySecrets(output);
13592
+ }
13593
+ }
13594
+ result.note = conclusion === "success" ? `tenant-control ${action} run succeeded` : conclusion === "failure" ? `tenant-control ${action} run failed \u2014 inspect the run` : runId == null ? `dispatched tenant-control.yml (${action}) \u2014 run not correlated; check the Actions tab` : `dispatched tenant-control.yml (${action}) \u2014 not watched`;
13595
+ return result;
13596
+ }
13597
+ function renderTenantControl(r) {
13598
+ const head = `tenant control ${r.repo} ${r.stage} ${r.action}: ${r.conclusion}${r.category ? ` (${r.category})` : ""}`;
13599
+ const lines = [head];
13600
+ if (r.runUrl) lines.push(` run: ${r.runUrl}`);
13601
+ if (r.serviceState) lines.push(` serviceState: ${r.serviceState}`);
13602
+ if (r.secrets?.length) {
13603
+ for (const s of r.secrets) lines.push(` ${s.key}: ${s.status}`);
13604
+ }
13605
+ lines.push(` ${r.note}`);
13606
+ return lines.join("\n");
13607
+ }
13608
+
13609
+ // src/tenant-verify-secrets.ts
13610
+ function renderVerifySecrets(body) {
13611
+ const secrets = body?.secrets ?? [];
13612
+ const counts = {
13613
+ match: secrets.filter((s) => s.status === "match").length,
13614
+ mismatch: secrets.filter((s) => s.status === "mismatch").length,
13615
+ missing: secrets.filter((s) => s.status === "missing").length
13616
+ };
13617
+ const lines = secrets.map((s) => `${s.key}: ${s.status}`);
13618
+ lines.push(`verify-secrets: ${counts.match} match, ${counts.mismatch} mismatch, ${counts.missing} missing`);
13619
+ const ssmStatus = body?.ssmStatus ?? "pending";
13620
+ if (ssmStatus !== "Success") {
13621
+ return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
13622
+ }
13623
+ const bad = counts.mismatch + counts.missing;
13624
+ if (bad > 0) {
13625
+ return { lines, failure: `${bad} of ${secrets.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
13626
+ }
13627
+ return { lines, failure: null };
13628
+ }
12853
13629
 
12854
13630
  // src/hotfix-coverage.ts
12855
- var import_node_child_process9 = require("node:child_process");
13631
+ var import_node_child_process10 = require("node:child_process");
12856
13632
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
12857
13633
  function checkHotfixCoverage(options = {}) {
12858
13634
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
12859
13635
  const ack = (options.ack ?? []).filter(Boolean);
12860
- const git = options.git ?? ((args, opts) => (0, import_node_child_process9.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
13636
+ const git = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
12861
13637
  const revList = (range) => {
12862
13638
  const out = git(["rev-list", "--no-merges", range]).trim();
12863
13639
  return out ? out.split("\n") : [];
@@ -12984,6 +13760,10 @@ function renderSweep(r) {
12984
13760
  if (r.running > 0 && !r.retireAttempted) {
12985
13761
  lines.push("Retire an orphan with: mmi-cli tenant control <owner/repo> rc retire (or sweep all running: mmi-cli tenant sweep-rc --retire --yes)");
12986
13762
  }
13763
+ const undetermined = r.stages.filter((s) => s.serviceState === "unknown").length;
13764
+ if (undetermined > 0) {
13765
+ lines.push(`WARNING: rc running-state could not be determined for ${undetermined} of ${r.scanned} tenant(s) \u2014 the "${r.running} running" count is a floor, not a clear bill. Reconcile the box assets (tenant-reconcile.yml) or inspect the tenant-control run log.`);
13766
+ }
12987
13767
  return lines.join("\n");
12988
13768
  }
12989
13769
 
@@ -13017,7 +13797,7 @@ function hotfixBranch(tag) {
13017
13797
  }
13018
13798
  async function resolveHotfixDeployModel(deps, ctx) {
13019
13799
  const load = await loadProjectMeta(deps, ctx);
13020
- const meta = load.status === "ok" ? load.meta : null;
13800
+ const meta = requireProjectMetaForTrain(load, ctx.repo);
13021
13801
  return resolveDeployModel2(meta, ctx.repo);
13022
13802
  }
13023
13803
  async function findHotfixPr(deps, ctx, tag) {
@@ -13132,9 +13912,9 @@ Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
13132
13912
  return { ...ctx, command: "hotfix-start", tag, version, branch, source: label, prUrl, reused: Boolean(remoteBranch), notes };
13133
13913
  }
13134
13914
  async function watchReleaseRun(deps, ctx, workflow, sha) {
13135
- const sleep = sleeper(deps);
13915
+ const sleep2 = sleeper(deps);
13136
13916
  for (let attempt = 0; attempt < HOTFIX_RUN_FIND_ATTEMPTS; attempt++) {
13137
- if (attempt > 0) await sleep(HOTFIX_RUN_FIND_DELAY_MS);
13917
+ if (attempt > 0) await sleep2(HOTFIX_RUN_FIND_DELAY_MS);
13138
13918
  let rows;
13139
13919
  try {
13140
13920
  const out = await deps.run("gh", [
@@ -13209,8 +13989,9 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13209
13989
  runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
13210
13990
  }
13211
13991
  deployNote = "watched release-triggered deploy.yml + publish.yml";
13212
- } else if (deployModel === "tenant-container" || deployModel === "solo-container") {
13213
- const dispatch = await dispatchDeploy(
13992
+ } else if (deployModel === "tenant-container" || deployModel === "solo-container" || deployModel === "registry-publish") {
13993
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
13994
+ const deploy = await dispatchDeploy(
13214
13995
  deps,
13215
13996
  ctx,
13216
13997
  "main",
@@ -13219,14 +14000,38 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13219
14000
  true,
13220
14001
  (deps.now ?? Date.now)(),
13221
14002
  mergedSha,
13222
- "report"
14003
+ "report",
14004
+ meta.publishDir
13223
14005
  );
14006
+ const publish = deploy.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report");
14007
+ let dispatch = appendPublishDispatch(deploy, publish);
14008
+ if (!publish && deploy.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
14009
+ dispatch = {
14010
+ ...dispatch,
14011
+ note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
14012
+ };
14013
+ }
13224
14014
  deployNote = dispatch.note;
13225
- runs.push({
13226
- workflow: "tenant-deploy.yml",
13227
- url: dispatch.runUrl,
13228
- conclusion: dispatch.deployStatus === "success" ? "success" : dispatch.deployStatus === "failure" ? "failure" : dispatch.deployStatus ?? "pending"
13229
- });
14015
+ if (deployModel !== "registry-publish") {
14016
+ runs.push({
14017
+ workflow: "tenant-deploy.yml",
14018
+ url: deploy.runUrl,
14019
+ conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
14020
+ });
14021
+ }
14022
+ if (publish?.runUrl) {
14023
+ runs.push({
14024
+ workflow: "tenant-publish.yml",
14025
+ url: publish.runUrl,
14026
+ conclusion: publish.deployStatus === "success" ? "success" : publish.deployStatus === "failure" ? "failure" : publish.deployStatus ?? "pending"
14027
+ });
14028
+ } else if (deployModel === "registry-publish") {
14029
+ runs.push({
14030
+ workflow: "tenant-publish.yml",
14031
+ url: deploy.runUrl,
14032
+ conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
14033
+ });
14034
+ }
13230
14035
  } else {
13231
14036
  deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
13232
14037
  }
@@ -13237,7 +14042,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13237
14042
  try {
13238
14043
  await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
13239
14044
  const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
13240
- const sleep = sleeper(deps);
14045
+ const sleep2 = sleeper(deps);
13241
14046
  let attempt = 0;
13242
14047
  for (; ; ) {
13243
14048
  attempt++;
@@ -13246,7 +14051,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13246
14051
  break;
13247
14052
  } catch (err) {
13248
14053
  if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
13249
- await sleep(HOTFIX_VERIFY_RETRY_MS);
14054
+ await sleep2(HOTFIX_VERIFY_RETRY_MS);
13250
14055
  }
13251
14056
  }
13252
14057
  const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
@@ -13295,6 +14100,7 @@ async function runHotfixStatus(deps, versionInput) {
13295
14100
  await deps.run("git", ["fetch", "origin", "--tags"]);
13296
14101
  let tag;
13297
14102
  let version;
14103
+ let warnings = [];
13298
14104
  if (versionInput) {
13299
14105
  ({ tag, version } = normalizeHotfixVersion(versionInput));
13300
14106
  } else {
@@ -13303,18 +14109,19 @@ async function runHotfixStatus(deps, versionInput) {
13303
14109
  const latestFacts = await gatherHotfixFacts(deps, ctx, latest, latest.slice(1));
13304
14110
  const latestDerived = deriveHotfixState(latestFacts);
13305
14111
  if (latestDerived.state !== "complete") {
13306
- return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived };
14112
+ return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived, warnings };
13307
14113
  }
13308
14114
  }
13309
- const inFlight = await findInFlightHotfixVersion(deps, ctx);
13310
- if (inFlight) {
13311
- const facts2 = await gatherHotfixFacts(deps, ctx, inFlight.tag, inFlight.version);
13312
- return { ...ctx, command: "hotfix-status", ...facts2, ...deriveHotfixState(facts2) };
14115
+ const found = await findInFlightHotfixVersion(deps, ctx, latest);
14116
+ warnings = supersededHotfixWarnings(found.superseded, latest);
14117
+ if (found.inFlight) {
14118
+ ({ tag, version } = found.inFlight);
14119
+ } else {
14120
+ ({ tag, version } = await deriveHotfixVersion(deps));
13313
14121
  }
13314
- ({ tag, version } = await deriveHotfixVersion(deps));
13315
14122
  }
13316
14123
  const facts = await gatherHotfixFacts(deps, ctx, tag, version);
13317
- return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
14124
+ return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts), warnings };
13318
14125
  }
13319
14126
  async function gatherHotfixFacts(deps, ctx, tag, version) {
13320
14127
  const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
@@ -13356,7 +14163,19 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
13356
14163
  const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
13357
14164
  return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
13358
14165
  }
13359
- async function findInFlightHotfixVersion(deps, ctx) {
14166
+ function compareHotfixVersions(a, b) {
14167
+ const pa = a.replace(/^v/, "").split(".").map(Number);
14168
+ const pb = b.replace(/^v/, "").split(".").map(Number);
14169
+ for (let i = 0; i < 3; i++) {
14170
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
14171
+ }
14172
+ return 0;
14173
+ }
14174
+ function supersededHotfixWarnings(superseded, latestMainTag) {
14175
+ if (superseded.length === 0) return [];
14176
+ return [`skipped superseded hotfix marker(s) ${superseded.join(", ")} \u2014 merged to main but never released and at/below the latest released tag ${latestMainTag ?? "(none)"}; later releases already absorbed them (#976). Not actionable \u2014 run mmi-cli hotfix start for a new fix.`];
14177
+ }
14178
+ async function findInFlightHotfixVersion(deps, ctx, latestMainTag) {
13360
14179
  const tags = /* @__PURE__ */ new Set();
13361
14180
  const out = await deps.run("gh", [
13362
14181
  "pr",
@@ -13382,20 +14201,20 @@ async function findInFlightHotfixVersion(deps, ctx) {
13382
14201
  const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
13383
14202
  if (m) tags.add(m[1]);
13384
14203
  }
13385
- const sorted = [...tags].sort((a, b) => {
13386
- const pa = a.slice(1).split(".").map(Number);
13387
- const pb = b.slice(1).split(".").map(Number);
13388
- for (let i = 0; i < 3; i++) {
13389
- if (pa[i] !== pb[i]) return pb[i] - pa[i];
13390
- }
13391
- return 0;
13392
- });
13393
- for (const tag of sorted) {
14204
+ const sorted = [...tags].sort((a, b) => compareHotfixVersions(b, a));
14205
+ const fresh = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) > 0) : sorted;
14206
+ for (const tag of fresh) {
13394
14207
  const version = tag.slice(1);
13395
14208
  const facts = await gatherHotfixFacts(deps, ctx, tag, version);
13396
- if (deriveHotfixState(facts).state !== "complete") return { tag, version };
14209
+ if (deriveHotfixState(facts).state !== "complete") return { inFlight: { tag, version }, superseded: [] };
13397
14210
  }
13398
- return null;
14211
+ const stale = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) <= 0) : [];
14212
+ const superseded = [];
14213
+ for (const tag of stale) {
14214
+ const facts = await gatherHotfixFacts(deps, ctx, tag, tag.slice(1));
14215
+ if (deriveHotfixState(facts).state !== "complete") superseded.push(tag);
14216
+ }
14217
+ return { inFlight: null, superseded };
13399
14218
  }
13400
14219
 
13401
14220
  // src/release-announce.ts
@@ -13525,7 +14344,7 @@ async function announceRelease(deps, args) {
13525
14344
  }
13526
14345
 
13527
14346
  // src/port-registry.ts
13528
- var import_node_fs17 = require("node:fs");
14347
+ var import_node_fs20 = require("node:fs");
13529
14348
 
13530
14349
  // ../infra/port-geometry.mjs
13531
14350
  var PORT_BLOCK = 100;
@@ -13539,8 +14358,8 @@ function nextPortBlock(registry2) {
13539
14358
  return [base2, base2 + PORT_SPAN];
13540
14359
  }
13541
14360
  function loadPortRegistry(path2) {
13542
- if (!(0, import_node_fs17.existsSync)(path2)) return {};
13543
- const raw = JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8"));
14361
+ if (!(0, import_node_fs20.existsSync)(path2)) return {};
14362
+ const raw = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
13544
14363
  const out = {};
13545
14364
  for (const [key, value] of Object.entries(raw)) {
13546
14365
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -13554,9 +14373,9 @@ function ensurePortRange(repo, path2) {
13554
14373
  const existing = registry2[repo];
13555
14374
  if (existing) return existing;
13556
14375
  const range = nextPortBlock(registry2);
13557
- const raw = (0, import_node_fs17.existsSync)(path2) ? JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8")) : {};
14376
+ const raw = (0, import_node_fs20.existsSync)(path2) ? JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8")) : {};
13558
14377
  raw[repo] = range;
13559
- (0, import_node_fs17.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
14378
+ (0, import_node_fs20.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
13560
14379
  return range;
13561
14380
  }
13562
14381
  function portCursorSeed(registry2) {
@@ -14279,17 +15098,16 @@ function renderBootstrapVerifyReport(report) {
14279
15098
  var PROJECTS_LIST_PATH = "/projects/list";
14280
15099
  var ORG_CONFIG_PATH = "/org/config";
14281
15100
  var PROJECTS_ENVELOPE_KEY = "projects";
15101
+ var REGISTRY_FETCH_TIMEOUT_MS = 8e3;
14282
15102
 
14283
15103
  // src/registry-client.ts
14284
- var DEFAULT_TIMEOUT_MS2 = 8e3;
14285
- var WAITED_TENANT_CONTROL_TIMEOUT_MS = 13e3;
14286
15104
  var TENANT_DEPLOY_TIMEOUT_MS = 12e4;
14287
15105
  var RETRY_ATTEMPTS = 3;
14288
15106
  function retriedFetch(deps, url, init) {
14289
15107
  const headers = { ...clientVersionHeaders(), ...init.headers };
14290
15108
  return fetchWithRetry(deps.fetch ?? fetch, url, { ...init, headers }, {
14291
15109
  attempts: RETRY_ATTEMPTS,
14292
- timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2
15110
+ timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS
14293
15111
  });
14294
15112
  }
14295
15113
  async function fetchTrainAuthority(repo, deps) {
@@ -14389,7 +15207,7 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
14389
15207
  if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
14390
15208
  const token = await deps.token();
14391
15209
  if (!token) return { ok: false, status: 0, body: null, error: "no Hub session token (run `gh auth login`)" };
14392
- const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
15210
+ const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS;
14393
15211
  const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs });
14394
15212
  const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs });
14395
15213
  try {
@@ -14418,38 +15236,12 @@ async function attestAppGaps(slug, repo, deps) {
14418
15236
  return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
14419
15237
  }
14420
15238
  async function tenantControl(payload, deps) {
14421
- const noRetry = payload.action === "retire";
14422
- const timeoutMs = payload.wait ? WAITED_TENANT_CONTROL_TIMEOUT_MS : void 0;
14423
- return postJson("/tenant-control", payload, deps, "POST", { noRetry, timeoutMs });
15239
+ return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
14424
15240
  }
14425
15241
  async function tenantDeploy(payload, deps) {
14426
15242
  return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
14427
15243
  }
14428
15244
 
14429
- // src/tenant-verify-secrets.ts
14430
- function tenantControlWait(action) {
14431
- return action === "status" || action === "retire" || action === "verify-secrets";
14432
- }
14433
- function renderVerifySecrets(body) {
14434
- const secrets2 = body?.secrets ?? [];
14435
- const counts = {
14436
- match: secrets2.filter((s) => s.status === "match").length,
14437
- mismatch: secrets2.filter((s) => s.status === "mismatch").length,
14438
- missing: secrets2.filter((s) => s.status === "missing").length
14439
- };
14440
- const lines = secrets2.map((s) => `${s.key}: ${s.status}`);
14441
- lines.push(`verify-secrets: ${counts.match} match, ${counts.mismatch} mismatch, ${counts.missing} missing`);
14442
- const ssmStatus = body?.ssmStatus ?? "pending";
14443
- if (ssmStatus !== "Success") {
14444
- return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
14445
- }
14446
- const bad = counts.mismatch + counts.missing;
14447
- if (bad > 0) {
14448
- return { lines, failure: `${bad} of ${secrets2.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
14449
- }
14450
- return { lines, failure: null };
14451
- }
14452
-
14453
15245
  // src/project-readiness.ts
14454
15246
  function stagesForTrack(meta) {
14455
15247
  return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
@@ -14771,14 +15563,14 @@ async function buildV2Doctor(repoOrSlug, deps) {
14771
15563
  const required = stageInTrack(meta, stage2) && projectRequiresDeployState(model, stage2);
14772
15564
  return [stage2, { required, ok: required ? await deps.hasDeployState(slug, stage2) : true }];
14773
15565
  })));
14774
- const secrets2 = Object.fromEntries(STAGES.map((stage2) => {
15566
+ const secrets = Object.fromEntries(STAGES.map((stage2) => {
14775
15567
  const required = stageInTrack(meta, stage2) ? stageRequiredSecrets(stage2, meta).map((key) => stageKey2(stage2, key)) : [];
14776
15568
  const present = required.filter((key) => presentSecrets.has(key));
14777
15569
  const missing = required.filter((key) => !presentSecrets.has(key));
14778
15570
  return [stage2, { required, present, missing }];
14779
15571
  }));
14780
15572
  const metaMissing = ["class", "projectType", "deployModel", "vaultPath", "kbPointer"].filter((key) => meta[key] === void 0).concat(boardRegistryGaps(meta));
14781
- const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets2).every((v) => v.missing.length === 0);
15573
+ const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets).every((v) => v.missing.length === 0);
14782
15574
  const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
14783
15575
  return {
14784
15576
  ok,
@@ -14787,7 +15579,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
14787
15579
  class: meta.class,
14788
15580
  projectType,
14789
15581
  deployModel: model,
14790
- hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets: secrets2 },
15582
+ hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets },
14791
15583
  secretsError,
14792
15584
  autoHealAvailable: Object.keys(autoHeal.patch),
14793
15585
  appOwnedGaps: autoHeal.appOwnedGaps,
@@ -14837,7 +15629,7 @@ ${section}`.trim();
14837
15629
  }
14838
15630
 
14839
15631
  // src/project-set.ts
14840
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "ci", "requiredChecks", "gate"];
15632
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dashboard", "ci", "requiredChecks", "gate"];
14841
15633
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
14842
15634
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
14843
15635
  function parseRuntimeSecretsVar(raw) {
@@ -14996,6 +15788,21 @@ function parsePublishRequiredVar(raw) {
14996
15788
  if (raw === "false") return false;
14997
15789
  throw new Error("project set: publishRequired must be true or false");
14998
15790
  }
15791
+ function parseDashboardVar(raw) {
15792
+ if (raw === "true") return true;
15793
+ if (raw === "false") return false;
15794
+ throw new Error("project set: dashboard must be true or false");
15795
+ }
15796
+ function parsePublishDirVar(raw) {
15797
+ const v = raw.trim();
15798
+ if (v === "" || v === ".") {
15799
+ throw new Error("project set: publishDir must be a non-empty relative subpath, e.g. packages/ui (omit it or --unset publishDir for the repo root)");
15800
+ }
15801
+ if (!/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
15802
+ throw new Error('project set: publishDir must be a safe relative subpath \u2014 no leading slash, no ".." segment');
15803
+ }
15804
+ return v;
15805
+ }
14999
15806
  function parseRequiredChecksVar(raw) {
15000
15807
  let parsed;
15001
15808
  try {
@@ -15046,6 +15853,8 @@ var SETTABLE_VAR_KEYS = [
15046
15853
  "repos",
15047
15854
  "oauth",
15048
15855
  "publishRequired",
15856
+ "publishDir",
15857
+ "dashboard",
15049
15858
  "requiredGcpApis",
15050
15859
  "requiredRuntimeSecrets",
15051
15860
  "edgeDomains",
@@ -15062,6 +15871,8 @@ var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
15062
15871
  var SETTABLE_VAR_HINTS = {
15063
15872
  projectNumber: "numeric",
15064
15873
  publishRequired: "true|false",
15874
+ publishDir: "relative subpath, e.g. packages/ui",
15875
+ dashboard: "true|false",
15065
15876
  repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
15066
15877
  oauth: "JSON {subdomains,domains,callbackPath}",
15067
15878
  requiredGcpApis: "comma-string",
@@ -15135,6 +15946,10 @@ function buildProjectSetPatch(input) {
15135
15946
  patch[key] = parseReposVar(raw);
15136
15947
  } else if (key === "publishRequired") {
15137
15948
  patch[key] = parsePublishRequiredVar(raw);
15949
+ } else if (key === "dashboard") {
15950
+ patch[key] = parseDashboardVar(raw);
15951
+ } else if (key === "publishDir") {
15952
+ patch[key] = parsePublishDirVar(raw);
15138
15953
  } else if (key === "ci") {
15139
15954
  if (raw !== "none") throw new Error('project set: ci must be "none" (or use --unset ci to require checks)');
15140
15955
  patch[key] = raw;
@@ -15199,11 +16014,16 @@ function parseKbTree(stdout, prefix) {
15199
16014
  return tree.filter((t) => t.type === "blob" && typeof t.path === "string" && t.path.startsWith("kb/")).map((t) => t.path).filter((p) => pre ? p.startsWith(pre) : true).sort();
15200
16015
  }
15201
16016
 
16017
+ // src/northstar-commands.ts
16018
+ var import_node_fs21 = require("node:fs");
16019
+ var import_node_child_process11 = require("node:child_process");
16020
+ var import_promises6 = require("node:fs/promises");
16021
+
15202
16022
  // src/plan.ts
15203
- var import_node_path16 = require("node:path");
16023
+ var import_node_path18 = require("node:path");
15204
16024
  var PLANS_DIR = "plans";
15205
- var META_FILE = (0, import_node_path16.join)(PLANS_DIR, ".plan-meta.json");
15206
- var planPath = (slug) => (0, import_node_path16.join)(PLANS_DIR, `${slug}.md`);
16025
+ var META_FILE = (0, import_node_path18.join)(PLANS_DIR, ".plan-meta.json");
16026
+ var planPath = (slug) => (0, import_node_path18.join)(PLANS_DIR, `${slug}.md`);
15207
16027
  var metaKey = (project2, slug) => `${project2}/${slug}`;
15208
16028
  function parseMeta(raw) {
15209
16029
  if (!raw) return {};
@@ -15228,7 +16048,7 @@ function hashContent(s) {
15228
16048
  function staleHint(slug) {
15229
16049
  return `remote "${slug}" is newer \u2014 run \`mmi-cli northstar pull ${slug}\` first (your local is based on an older version), or re-push with \`--force\` to overwrite`;
15230
16050
  }
15231
- var INDEX_FILE = (0, import_node_path16.join)(PLANS_DIR, ".index.json");
16051
+ var INDEX_FILE = (0, import_node_path18.join)(PLANS_DIR, ".index.json");
15232
16052
  var INDEX_TTL_MS = 6e4;
15233
16053
  function parseIndex(raw) {
15234
16054
  if (!raw) return null;
@@ -15257,7 +16077,7 @@ function mergeIndex(idx, scope, plans, now) {
15257
16077
  const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
15258
16078
  return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
15259
16079
  }
15260
- var QUEUE_FILE = (0, import_node_path16.join)(PLANS_DIR, ".sync-queue.json");
16080
+ var QUEUE_FILE = (0, import_node_path18.join)(PLANS_DIR, ".sync-queue.json");
15261
16081
  var QUEUE_MAX_ATTEMPTS = 10;
15262
16082
  function isValidQueueEntry(e) {
15263
16083
  if (!e || typeof e !== "object") return false;
@@ -15716,23 +16536,298 @@ async function planGraduate(deps, slug, opts = {}) {
15716
16536
  if (pushed) deps.log(`graduated ${slug}`);
15717
16537
  }
15718
16538
 
15719
- // src/atomic-write.ts
15720
- var import_node_fs18 = require("node:fs");
15721
- function atomicWriteFileSync(path2, content) {
15722
- const tmp = `${path2}.${process.pid}.tmp`;
15723
- (0, import_node_fs18.writeFileSync)(tmp, content, "utf8");
15724
- (0, import_node_fs18.renameSync)(tmp, path2);
15725
- }
15726
-
15727
- // src/oauth.ts
15728
- var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
15729
- var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
15730
- var ENV_PREFIXES = ["", "dev", "rc"];
15731
- var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
15732
- var SSM_ENVS = ["dev", "rc", "main"];
15733
- var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
15734
- var uniq = (xs) => [...new Set(xs)];
15735
- function defaultSubdomain2(slug) {
16539
+ // src/northstar-commands.ts
16540
+ var planSyncDetached = false;
16541
+ function detachPlanSync() {
16542
+ if (planSyncDetached) return;
16543
+ planSyncDetached = true;
16544
+ try {
16545
+ (0, import_node_child_process11.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
16546
+ detached: true,
16547
+ stdio: "ignore",
16548
+ windowsHide: true,
16549
+ cwd: process.cwd()
16550
+ }).unref();
16551
+ } catch {
16552
+ }
16553
+ }
16554
+ function makePlanDeps(cfg, io = consoleIo) {
16555
+ const ensureDir = () => (0, import_node_fs21.mkdirSync)(PLANS_DIR, { recursive: true });
16556
+ return {
16557
+ apiUrl: cfg.sagaApiUrl,
16558
+ fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
16559
+ headers: (extra) => hubHeaders(extra),
16560
+ project: async () => (await sagaKey(cfg)).project,
16561
+ readLocal: (slug) => {
16562
+ try {
16563
+ return (0, import_node_fs21.readFileSync)(planPath(slug), "utf8");
16564
+ } catch {
16565
+ return null;
16566
+ }
16567
+ },
16568
+ writeLocal: (slug, content) => {
16569
+ ensureDir();
16570
+ (0, import_node_fs21.writeFileSync)(planPath(slug), content, "utf8");
16571
+ },
16572
+ removeLocal: (slug) => {
16573
+ try {
16574
+ (0, import_node_fs21.rmSync)(planPath(slug));
16575
+ } catch {
16576
+ }
16577
+ },
16578
+ readMetaRaw: () => {
16579
+ try {
16580
+ return (0, import_node_fs21.readFileSync)(META_FILE, "utf8");
16581
+ } catch {
16582
+ return null;
16583
+ }
16584
+ },
16585
+ writeMetaRaw: (raw) => {
16586
+ ensureDir();
16587
+ atomicWriteFileSync(META_FILE, raw);
16588
+ },
16589
+ readIndexRaw: () => {
16590
+ try {
16591
+ return (0, import_node_fs21.readFileSync)(INDEX_FILE, "utf8");
16592
+ } catch {
16593
+ return null;
16594
+ }
16595
+ },
16596
+ writeIndexRaw: (raw) => {
16597
+ ensureDir();
16598
+ atomicWriteFileSync(INDEX_FILE, raw);
16599
+ },
16600
+ readQueueRaw: () => {
16601
+ try {
16602
+ return (0, import_node_fs21.readFileSync)(QUEUE_FILE, "utf8");
16603
+ } catch {
16604
+ return null;
16605
+ }
16606
+ },
16607
+ writeQueueRaw: (raw) => {
16608
+ ensureDir();
16609
+ atomicWriteFileSync(QUEUE_FILE, raw);
16610
+ },
16611
+ detachSync: detachPlanSync,
16612
+ log: (m) => io.log(m),
16613
+ err: (m) => io.err(m),
16614
+ now: () => (/* @__PURE__ */ new Date()).toISOString()
16615
+ };
16616
+ }
16617
+ function openInEditor(path2) {
16618
+ const editor = process.env.EDITOR || process.env.VISUAL;
16619
+ if (!editor) {
16620
+ console.log(`plan at ${path2} (set $EDITOR to open it automatically)`);
16621
+ return;
16622
+ }
16623
+ try {
16624
+ (0, import_node_child_process11.spawn)(editor, [path2], { stdio: "inherit" });
16625
+ } catch {
16626
+ console.log(`open ${path2} manually`);
16627
+ }
16628
+ }
16629
+ async function withPlan(quiet, run, io = consoleIo) {
16630
+ const cfg = await loadConfig();
16631
+ if (!cfg.sagaApiUrl) {
16632
+ if (!quiet) fail("plan: Hub API URL not configured");
16633
+ return;
16634
+ }
16635
+ await run(makePlanDeps(cfg, io));
16636
+ }
16637
+ async function gatherRelevanceSignals() {
16638
+ const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
16639
+ const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
16640
+ const signals = { branch, changedFiles: changed.length ? changed : void 0 };
16641
+ const issueNum = branch?.match(/\b(\d{2,})\b/)?.[1];
16642
+ if (issueNum) {
16643
+ try {
16644
+ const { stdout } = await execFileP2(
16645
+ "gh",
16646
+ ["issue", "view", issueNum, "--json", "title,labels", "--jq", "{title:.title,labels:[.labels[].name]}"],
16647
+ { timeout: 1e4 }
16648
+ );
16649
+ const j = JSON.parse(stdout);
16650
+ if (j.title) signals.issueTitle = j.title;
16651
+ if (j.labels?.length) signals.issueLabels = j.labels;
16652
+ } catch {
16653
+ }
16654
+ }
16655
+ return signals;
16656
+ }
16657
+ function registerNorthStarCommands(cmd) {
16658
+ cmd.command("push <slug>").description("push a North Star plan to the server (from plans/<slug>.md or --body-file)").option("--project <name>", "override the project key").option("--body-file <path|->", "read plan markdown from a UTF-8 file, or from stdin with -").option("--force", "overwrite the remote even if it changed since your last sync").option("--wait", "push synchronously (block until the server write lands)").action(async (slug, o) => {
16659
+ let content;
16660
+ if (o.bodyFile) {
16661
+ try {
16662
+ content = await resolveTextArg({ file: o.bodyFile }, { readFile: import_promises6.readFile, readStdin }, {
16663
+ value: "inline content",
16664
+ file: "--body-file",
16665
+ noun: "plan"
16666
+ });
16667
+ } catch (e) {
16668
+ console.error(e.message);
16669
+ process.exitCode = 1;
16670
+ return;
16671
+ }
16672
+ }
16673
+ return withPlan(false, async (d) => {
16674
+ const ok = await planPush(d, slug, { project: o.project, force: o.force, wait: o.wait, content });
16675
+ if (!ok) process.exitCode = 1;
16676
+ });
16677
+ });
16678
+ cmd.command("pull <slug>").description("pull a North Star plan from the server into plans/<slug>.md").option("--project <name>", "override the project key").option("--force", "overwrite local even if it has unpushed edits").action((slug, o) => withPlan(false, async (d) => {
16679
+ const ok = await planPull(d, slug, o);
16680
+ if (!ok) process.exitCode = 1;
16681
+ }));
16682
+ cmd.command("show <slug>").alias("get").description("print a North Star plan to stdout, read-only (no local copy written \u2014 mirrors `kb get`)").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, async (d) => {
16683
+ const ok = await planShow(d, slug, o);
16684
+ if (!ok) process.exitCode = 1;
16685
+ }));
16686
+ cmd.command("list").description("list your North Star plans (cross-device)").option("--project <name>", "filter by project").option("--json", "machine-readable output").option("--quiet", "silent when unconfigured/empty/unreachable (SessionStart hook)").option("--fresh", "bypass the local cache and read from the server").action((o) => withPlan(o.quiet ?? false, (d) => planList(d, o)));
16687
+ cmd.command("relevant").description("list North Stars relevant to your current task (branch + claimed issue + changed files)").option("--project <name>", "override the project key").option("--all", "include superseded/graduated and recent non-matching plans").option("--limit <n>", "max results (default 5)").option("--fresh", "bypass the local cache and read from the server").option("--json", "machine-readable output").action((o) => withPlan(false, async (d) => {
16688
+ const signals = await gatherRelevanceSignals();
16689
+ await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh, json: o.json });
16690
+ }));
16691
+ cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").option("--wait", "durable confirmation: drain, then report unresolved pushes and exit non-zero if any remain").action((o) => withPlan(o.quiet ?? false, async (d) => {
16692
+ const unresolved = await planSync(d, o);
16693
+ if (!o.wait) return;
16694
+ if (unresolved.length) {
16695
+ for (const e of unresolved) d.err(`${e.slug}: ${e.conflict ?? e.deadLettered ?? "still pending"}`);
16696
+ process.exitCode = 1;
16697
+ } else if (!o.quiet) {
16698
+ d.log("north star: all queued pushes landed");
16699
+ }
16700
+ }));
16701
+ cmd.command("status").description("show pending/conflicted background pushes and the plan-cache age").option("--json", "machine-readable output").action((o) => withPlan(false, (d) => planStatus(d, o)));
16702
+ cmd.command("reconcile").description("refresh stale local etags from the server without --force (recovers from an object-store re-stamp)").action(() => withPlan(false, (d) => planReconcile(d)));
16703
+ cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
16704
+ (slug, o) => withPlan(false, async (d) => {
16705
+ const ok = await planPull(d, slug, { project: o.project });
16706
+ if (!ok) {
16707
+ process.exitCode = 1;
16708
+ return;
16709
+ }
16710
+ openInEditor(planPath(slug));
16711
+ })
16712
+ );
16713
+ cmd.command("delete <slug>").description("delete a North Star plan from the server and the local copy").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, (d) => planDelete(d, slug, o)));
16714
+ cmd.command("graduate <slug>").description("mark a built-and-merged North Star plan as org-visible and push it").requiredOption("--merged-pr <url|number>", "merged PR URL or number proving the plan shipped").option("--org-visible", "confirm this plan is safe to queue for org KB curation").option("--project <name>", "override the project key").option("--force", "overwrite the remote even if it changed since your last sync").action(
16715
+ (slug, o) => withPlan(false, (d) => planGraduate(d, slug, o))
16716
+ );
16717
+ }
16718
+
16719
+ // src/secrets-commands.ts
16720
+ async function readSecretStdin() {
16721
+ if (process.stdin.isTTY) {
16722
+ process.stderr.write(
16723
+ 'secrets set: pipe the value on stdin (it is never an argument) \u2014 e.g.\n printf %s "$VALUE" | mmi-cli secrets set <KEY>\n'
16724
+ );
16725
+ return "";
16726
+ }
16727
+ const chunks = [];
16728
+ for await (const chunk of process.stdin) chunks.push(chunk);
16729
+ return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
16730
+ }
16731
+ function makeSecretsDeps(cfg) {
16732
+ return {
16733
+ apiUrl: cfg.sagaApiUrl,
16734
+ fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
16735
+ headers: (extra) => hubHeaders(extra),
16736
+ // Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
16737
+ // casing, which leaked mixed-case into `secrets where` output (#681).
16738
+ slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
16739
+ readSecretValue: () => readSecretStdin(),
16740
+ log: (m) => console.log(m),
16741
+ err: (m) => console.error(m)
16742
+ };
16743
+ }
16744
+ async function withSecrets(run) {
16745
+ const cfg = await loadConfig();
16746
+ if (!cfg.sagaApiUrl) {
16747
+ fail("secrets: Hub API URL not configured");
16748
+ return;
16749
+ }
16750
+ await run(makeSecretsDeps(cfg));
16751
+ }
16752
+ function registerSecretsCommands(program3) {
16753
+ const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
16754
+ secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsWhere(d, o)));
16755
+ secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsList(d, o)));
16756
+ secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").action(async (o) => {
16757
+ if (!["dev", "rc", "main"].includes(o.stage)) {
16758
+ return fail("secrets preflight: --stage must be dev, rc, or main");
16759
+ }
16760
+ const cfg = await loadConfig();
16761
+ if (!cfg.sagaApiUrl) {
16762
+ fail("secrets: Hub API URL not configured");
16763
+ return;
16764
+ }
16765
+ const d = makeSecretsDeps(cfg);
16766
+ const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
16767
+ const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
16768
+ const repo = o.repo ?? `mutmutco/${slug}`;
16769
+ const meta = await fetchProjectBySlug(slug, regDeps);
16770
+ const required = o.required?.length ? o.required : requiredRuntimeSecretNames(o.stage, meta?.requiredRuntimeSecrets, {
16771
+ includeGoogleOAuth: projectRequiresGoogleOAuth(meta, meta?.deployModel)
16772
+ });
16773
+ const centralContainer = meta?.deployModel === "tenant-container" || meta?.deployModel === "solo-container";
16774
+ if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
16775
+ d.err("secrets preflight: requiredRuntimeSecrets is unset for this deployable tenant \u2014 declare the per-stage contract in registry META (or an explicit empty map) before promoting");
16776
+ process.exitCode = 1;
16777
+ return;
16778
+ }
16779
+ const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
16780
+ if (!ok) process.exitCode = 1;
16781
+ });
16782
+ secrets.command("get <key>").description("print one secret value over TLS (prints once, raw \u2014 do not log/paste it)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
16783
+ const ok = await secretsGet(d, key, o);
16784
+ if (!ok) process.exitCode = 1;
16785
+ }));
16786
+ secrets.command("request <key>").description("approved escalation: create a Hub issue + admin DM for a missing secret (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--priority <priority>", "urgent | high | medium | low (default: medium)").option("--reason <text>", "why the secret is needed; safe metadata only").option("--context <text>", "lookup command/context; safe metadata only").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
16787
+ const ok = await secretsRequest(d, key, o);
16788
+ if (!ok) process.exitCode = 1;
16789
+ }));
16790
+ secrets.command("verify <key>").description("validate a known provider secret without printing its value").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
16791
+ const ok = await secretsVerify(d, key, o);
16792
+ if (!ok) process.exitCode = 1;
16793
+ }));
16794
+ secrets.command("set <key>").description("write/rotate a secret; value is read from stdin (never an argument)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
16795
+ const ok = await secretsSet(d, key, o);
16796
+ if (!ok) process.exitCode = 1;
16797
+ }));
16798
+ secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
16799
+ const ok = await secretsEdit(d, key, o);
16800
+ if (!ok) process.exitCode = 1;
16801
+ }));
16802
+ secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
16803
+ const stages = ["dev", "rc", "main"];
16804
+ if (!stages.includes(o.from) || !stages.includes(o.to)) {
16805
+ return fail("secrets copy: --from and --to must be dev, rc, or main");
16806
+ }
16807
+ const ok = await secretsCopy(d, {
16808
+ repo: o.repo,
16809
+ from: o.from,
16810
+ to: o.to,
16811
+ keys: o.keys.split(","),
16812
+ dryRun: o.dryRun
16813
+ });
16814
+ if (!ok) process.exitCode = 1;
16815
+ }));
16816
+ secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
16817
+ secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
16818
+ secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
16819
+ secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-tier secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
16820
+ }
16821
+
16822
+ // src/oauth.ts
16823
+ var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
16824
+ var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
16825
+ var ENV_PREFIXES = ["", "dev", "rc"];
16826
+ var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
16827
+ var SSM_ENVS = ["dev", "rc", "main"];
16828
+ var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
16829
+ var uniq = (xs) => [...new Set(xs)];
16830
+ function defaultSubdomain2(slug) {
15736
16831
  const i = slug.indexOf("-");
15737
16832
  return i === -1 ? slug : slug.slice(i + 1);
15738
16833
  }
@@ -15952,7 +17047,7 @@ async function fetchHubVersionInfo(baseUrl) {
15952
17047
  }
15953
17048
  function readRepoVersion() {
15954
17049
  try {
15955
- return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
17050
+ return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
15956
17051
  } catch {
15957
17052
  return void 0;
15958
17053
  }
@@ -15998,6 +17093,53 @@ async function fetchNpmReleasedVersion() {
15998
17093
  return void 0;
15999
17094
  }
16000
17095
  }
17096
+ async function fetchUiPackageLatestVersion(packageName) {
17097
+ try {
17098
+ const { stdout } = await runHostBin("npm", npmUiPackageLatestArgs(packageName), { timeout: NPM_VIEW_TIMEOUT_MS });
17099
+ return parseNpmViewVersion(stdout);
17100
+ } catch {
17101
+ return void 0;
17102
+ }
17103
+ }
17104
+ async function applyDesignSystemUpdate(check, log) {
17105
+ if (check.ok || !check.packageName) return check;
17106
+ try {
17107
+ log(` \u21BB updating ${check.packageName} ${check.installedVersion ?? "(missing)"} \u2192 ${check.latestVersion ?? "latest"}\u2026`);
17108
+ await runHostBin("npm", ["update", check.packageName], { timeout: NPM_UPDATE_TIMEOUT_MS });
17109
+ const installedVersion = designSystemSnapshot(process.cwd()).installedVersion ?? check.latestVersion;
17110
+ if (check.latestVersion && installedVersion && compareVersions(installedVersion, check.latestVersion) >= 0) {
17111
+ return { ...check, ok: true, installedVersion };
17112
+ }
17113
+ return { ...check, installedVersion };
17114
+ } catch {
17115
+ return check;
17116
+ }
17117
+ }
17118
+ async function applyRegistryComponentsSyncCheck(check, targetVersion, log) {
17119
+ if (check.ok || !check.components?.length) return check;
17120
+ const result = await applyRegistryComponentsSync(
17121
+ process.cwd(),
17122
+ check.components,
17123
+ targetVersion ?? check.targetVersion,
17124
+ log,
17125
+ defaultRegistrySyncDeps()
17126
+ );
17127
+ if (!result.ok) return check;
17128
+ const state = await gatherRegistryComponentsState(process.cwd(), targetVersion ?? check.targetVersion, { fetch });
17129
+ return buildRegistryComponentsCheck({ ...state, isConsumerRepo: true });
17130
+ }
17131
+ async function resolveDashboardConsumer(cfg) {
17132
+ if (!cfg.sagaApiUrl || isUiFactoryCheckout(process.cwd())) return { isConsumer: false };
17133
+ const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg));
17134
+ if (!read.ok) return { isConsumer: false, registryReadFailed: read.error };
17135
+ return { isConsumer: isDashboardMetaConsumer(read.project) };
17136
+ }
17137
+ function buildDesignSystemRegistryReadCheck(error) {
17138
+ return { ok: false, label: DESIGN_SYSTEM_VERSION_LABEL, fix: dashboardConsumerRegistryFix(error) };
17139
+ }
17140
+ function buildRegistryComponentsRegistryReadCheck(error) {
17141
+ return { ok: false, label: REGISTRY_COMPONENTS_LABEL, fix: dashboardConsumerRegistryFix(error) };
17142
+ }
16001
17143
  async function requireFreshTrainCli(commandName) {
16002
17144
  if (process.env.MMI_TRAIN_FRESH_OVERRIDE === "1") return;
16003
17145
  const report = buildVersionLagReport({
@@ -16019,8 +17161,8 @@ async function runClaudePlugin(args) {
16019
17161
  return false;
16020
17162
  }
16021
17163
  }
16022
- async function applyClaudePluginHeal(surface, log) {
16023
- if (surface !== "claude-cli" && surface !== "claude-vscode") return false;
17164
+ async function applyClaudePluginHeal(surface, log, opts) {
17165
+ if (!opts?.force && surface !== "claude-cli" && surface !== "claude-vscode") return false;
16024
17166
  log(" \u21BB reinstalling the MMI plugin via `claude plugin` (marketplace remove \u2192 add \u2192 install)\u2026");
16025
17167
  for (const step of CLAUDE_PLUGIN_HEAL_STEPS) {
16026
17168
  if (healStepAborts(step, await runClaudePlugin([...step.args]))) return false;
@@ -16035,8 +17177,8 @@ async function runCodexPlugin(args) {
16035
17177
  return false;
16036
17178
  }
16037
17179
  }
16038
- async function applyCodexPluginHeal(surface, log) {
16039
- if (surface !== "codex") return false;
17180
+ async function applyCodexPluginHeal(surface, log, opts) {
17181
+ if (!opts?.force && surface !== "codex") return false;
16040
17182
  log(" \u21BB reinstalling the MMI plugin via `codex plugin` (marketplace remove \u2192 add --ref main \u2192 add)\u2026");
16041
17183
  for (const step of CODEX_PLUGIN_HEAL_STEPS) {
16042
17184
  if (healStepAborts(step, await runCodexPlugin([...step.args]))) return false;
@@ -16060,7 +17202,8 @@ async function runRulesSync(opts, io = consoleIo) {
16060
17202
  ".claude/settings.json",
16061
17203
  ".claude/output-styles/mmi-plain.md",
16062
17204
  ".cursor/rules/mmi-plain-language.mdc",
16063
- ".cursor/rules/mmi-tool-economy.mdc"
17205
+ ".cursor/rules/mmi-tool-economy.mdc",
17206
+ ".cursor/rules/mmi-code-economy.mdc"
16064
17207
  ];
16065
17208
  const fetched = await Promise.all(files.map(async (file) => {
16066
17209
  try {
@@ -16079,11 +17222,11 @@ async function runRulesSync(opts, io = consoleIo) {
16079
17222
  for (const entry of fetched) {
16080
17223
  if ("error" in entry) continue;
16081
17224
  const { file, source } = entry;
16082
- const current = (0, import_node_fs19.existsSync)(file) ? await (0, import_promises6.readFile)(file, "utf8") : null;
17225
+ const current = (0, import_node_fs22.existsSync)(file) ? await (0, import_promises7.readFile)(file, "utf8") : null;
16083
17226
  if (needsUpdate(source, current)) {
16084
17227
  const slash = file.lastIndexOf("/");
16085
- if (slash > 0) (0, import_node_fs19.mkdirSync)(file.slice(0, slash), { recursive: true });
16086
- await (0, import_promises6.writeFile)(file, normalizeEol(source), "utf8");
17228
+ if (slash > 0) (0, import_node_fs22.mkdirSync)(file.slice(0, slash), { recursive: true });
17229
+ await (0, import_promises7.writeFile)(file, normalizeEol(source), "utf8");
16087
17230
  changed++;
16088
17231
  if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
16089
17232
  }
@@ -16108,9 +17251,9 @@ async function runDocsSync(opts, io = consoleIo) {
16108
17251
  return null;
16109
17252
  }
16110
17253
  },
16111
- localContent: async (f) => (0, import_node_fs19.existsSync)(f) ? await (0, import_promises6.readFile)(f, "utf8") : null,
17254
+ localContent: async (f) => (0, import_node_fs22.existsSync)(f) ? await (0, import_promises7.readFile)(f, "utf8") : null,
16112
17255
  writeDoc: async (f, c) => {
16113
- await (0, import_promises6.writeFile)(f, c, "utf8");
17256
+ await (0, import_promises7.writeFile)(f, c, "utf8");
16114
17257
  }
16115
17258
  });
16116
17259
  for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
@@ -16185,7 +17328,7 @@ function runWorktreeInstall(command, cwd, quiet) {
16185
17328
  const file = isWin ? "cmd.exe" : bin;
16186
17329
  const spawnArgs = isWin ? ["/c", bin, ...args] : args;
16187
17330
  return new Promise((resolve, reject) => {
16188
- const child = (0, import_node_child_process10.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
17331
+ const child = (0, import_node_child_process12.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
16189
17332
  const timer = setTimeout(() => {
16190
17333
  try {
16191
17334
  child.kill();
@@ -16207,7 +17350,7 @@ function runWorktreeInstall(command, cwd, quiet) {
16207
17350
  async function primaryCheckoutRoot(worktreeRoot) {
16208
17351
  try {
16209
17352
  const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
16210
- return out ? (0, import_node_path17.dirname)(out) : void 0;
17353
+ return out ? (0, import_node_path19.dirname)(out) : void 0;
16211
17354
  } catch {
16212
17355
  return void 0;
16213
17356
  }
@@ -16220,28 +17363,28 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
16220
17363
  };
16221
17364
  }
16222
17365
  function acquireWorktreeSetupLock(worktreeRoot) {
16223
- const lockPath = (0, import_node_path17.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
17366
+ const lockPath = (0, import_node_path19.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
16224
17367
  const take = () => {
16225
- const fd = (0, import_node_fs19.openSync)(lockPath, "wx");
17368
+ const fd = (0, import_node_fs22.openSync)(lockPath, "wx");
16226
17369
  try {
16227
- (0, import_node_fs19.writeSync)(fd, String(Date.now()));
17370
+ (0, import_node_fs22.writeSync)(fd, String(Date.now()));
16228
17371
  } finally {
16229
- (0, import_node_fs19.closeSync)(fd);
17372
+ (0, import_node_fs22.closeSync)(fd);
16230
17373
  }
16231
17374
  return () => {
16232
17375
  try {
16233
- (0, import_node_fs19.rmSync)(lockPath, { force: true });
17376
+ (0, import_node_fs22.rmSync)(lockPath, { force: true });
16234
17377
  } catch {
16235
17378
  }
16236
17379
  };
16237
17380
  };
16238
17381
  try {
16239
- (0, import_node_fs19.mkdirSync)((0, import_node_path17.dirname)(lockPath), { recursive: true });
17382
+ (0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(lockPath), { recursive: true });
16240
17383
  return take();
16241
17384
  } catch {
16242
17385
  try {
16243
- if (Date.now() - (0, import_node_fs19.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
16244
- (0, import_node_fs19.rmSync)(lockPath, { force: true });
17386
+ if (Date.now() - (0, import_node_fs22.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
17387
+ (0, import_node_fs22.rmSync)(lockPath, { force: true });
16245
17388
  return take();
16246
17389
  }
16247
17390
  } catch {
@@ -16320,361 +17463,90 @@ async function ghCreate(args) {
16320
17463
  try {
16321
17464
  const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
16322
17465
  return parseCreatedUrl(stdout);
16323
- } catch (e) {
16324
- await swapped.cleanup();
16325
- const err = e;
16326
- const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
16327
- fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
16328
- } finally {
16329
- await swapped.cleanup();
16330
- }
16331
- }
16332
- async function ghJson(args, timeout = 1e4) {
16333
- const { stdout } = await execFileP2("gh", args, { timeout });
16334
- return JSON.parse(stdout);
16335
- }
16336
- async function resolveRepo(repo) {
16337
- if (repo) return repo;
16338
- const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
16339
- if (fromOrigin) return fromOrigin;
16340
- try {
16341
- const { stdout } = await execFileP2("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
16342
- return stdout.trim() || void 0;
16343
- } catch {
16344
- return void 0;
16345
- }
16346
- }
16347
- async function attachToProject(issueNumber, repo, priority) {
16348
- const targetRepo2 = await resolveRepo(repo);
16349
- let cfg;
16350
- try {
16351
- cfg = await loadConfigForRepo(targetRepo2);
16352
- } catch (e) {
16353
- console.error(`issue create: board attach skipped \u2014 ${e.message}`);
16354
- return void 0;
16355
- }
16356
- if (!cfg.projectId) {
16357
- console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
16358
- return void 0;
16359
- }
16360
- try {
16361
- const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
16362
- if (targetRepo2) viewArgs.push("--repo", targetRepo2);
16363
- const { stdout: idOut } = await execFileP2("gh", viewArgs, { timeout: 1e4 });
16364
- const contentId = idOut.trim();
16365
- if (!contentId) throw new Error("could not resolve issue node id");
16366
- const { stdout } = await execFileP2("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: GH_MUTATION_TIMEOUT_MS });
16367
- const projectItemId = parseAddedItemId(stdout);
16368
- if (projectItemId && priority) {
16369
- try {
16370
- await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
16371
- } catch (e) {
16372
- const err = e;
16373
- process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
16374
- `);
16375
- }
16376
- }
16377
- return projectItemId;
16378
- } catch (e) {
16379
- const err = e;
16380
- process.stderr.write(`warning: issue #${issueNumber} created but NOT added to the project board: ${(err.stderr || err.message || String(e)).trim()}
16381
- `);
16382
- return void 0;
16383
- }
16384
- }
16385
- var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
16386
- function scheduleRelatedDiscovery(o) {
16387
- try {
16388
- const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
16389
- if (o.repo) args.push("--repo", o.repo);
16390
- (0, import_node_child_process10.spawn)(process.execPath, [process.argv[1], ...args], {
16391
- detached: true,
16392
- stdio: "ignore",
16393
- windowsHide: true,
16394
- cwd: process.cwd()
16395
- }).unref();
16396
- } catch {
16397
- }
16398
- }
16399
- var planSyncDetached = false;
16400
- function detachPlanSync() {
16401
- if (planSyncDetached) return;
16402
- planSyncDetached = true;
16403
- try {
16404
- (0, import_node_child_process10.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
16405
- detached: true,
16406
- stdio: "ignore",
16407
- windowsHide: true,
16408
- cwd: process.cwd()
16409
- }).unref();
16410
- } catch {
16411
- }
16412
- }
16413
- function makePlanDeps(cfg, io = consoleIo) {
16414
- const ensureDir = () => (0, import_node_fs19.mkdirSync)(PLANS_DIR, { recursive: true });
16415
- return {
16416
- apiUrl: cfg.sagaApiUrl,
16417
- fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
16418
- headers: (extra) => hubHeaders(extra),
16419
- project: async () => (await sagaKey(cfg)).project,
16420
- readLocal: (slug) => {
16421
- try {
16422
- return (0, import_node_fs19.readFileSync)(planPath(slug), "utf8");
16423
- } catch {
16424
- return null;
16425
- }
16426
- },
16427
- writeLocal: (slug, content) => {
16428
- ensureDir();
16429
- (0, import_node_fs19.writeFileSync)(planPath(slug), content, "utf8");
16430
- },
16431
- removeLocal: (slug) => {
16432
- try {
16433
- (0, import_node_fs19.rmSync)(planPath(slug));
16434
- } catch {
16435
- }
16436
- },
16437
- readMetaRaw: () => {
16438
- try {
16439
- return (0, import_node_fs19.readFileSync)(META_FILE, "utf8");
16440
- } catch {
16441
- return null;
16442
- }
16443
- },
16444
- writeMetaRaw: (raw) => {
16445
- ensureDir();
16446
- atomicWriteFileSync(META_FILE, raw);
16447
- },
16448
- readIndexRaw: () => {
16449
- try {
16450
- return (0, import_node_fs19.readFileSync)(INDEX_FILE, "utf8");
16451
- } catch {
16452
- return null;
16453
- }
16454
- },
16455
- writeIndexRaw: (raw) => {
16456
- ensureDir();
16457
- atomicWriteFileSync(INDEX_FILE, raw);
16458
- },
16459
- readQueueRaw: () => {
16460
- try {
16461
- return (0, import_node_fs19.readFileSync)(QUEUE_FILE, "utf8");
16462
- } catch {
16463
- return null;
16464
- }
16465
- },
16466
- writeQueueRaw: (raw) => {
16467
- ensureDir();
16468
- atomicWriteFileSync(QUEUE_FILE, raw);
16469
- },
16470
- detachSync: detachPlanSync,
16471
- log: (m) => io.log(m),
16472
- err: (m) => io.err(m),
16473
- now: () => (/* @__PURE__ */ new Date()).toISOString()
16474
- };
16475
- }
16476
- function openInEditor(path2) {
16477
- const editor = process.env.EDITOR || process.env.VISUAL;
16478
- if (!editor) {
16479
- console.log(`plan at ${path2} (set $EDITOR to open it automatically)`);
16480
- return;
17466
+ } catch (e) {
17467
+ await swapped.cleanup();
17468
+ const err = e;
17469
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
17470
+ fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
17471
+ } finally {
17472
+ await swapped.cleanup();
16481
17473
  }
17474
+ }
17475
+ async function ghJson(args, timeout = 1e4) {
17476
+ const { stdout } = await execFileP2("gh", args, { timeout });
17477
+ return JSON.parse(stdout);
17478
+ }
17479
+ async function resolveRepo(repo) {
17480
+ if (repo) return repo;
17481
+ const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
17482
+ if (fromOrigin) return fromOrigin;
16482
17483
  try {
16483
- (0, import_node_child_process10.spawn)(editor, [path2], { stdio: "inherit" });
17484
+ const { stdout } = await execFileP2("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
17485
+ return stdout.trim() || void 0;
16484
17486
  } catch {
16485
- console.log(`open ${path2} manually`);
17487
+ return void 0;
16486
17488
  }
16487
17489
  }
16488
- async function withPlan(quiet, run, io = consoleIo) {
16489
- const cfg = await loadConfig();
16490
- if (!cfg.sagaApiUrl) {
16491
- if (!quiet) fail("plan: Hub API URL not configured");
16492
- return;
17490
+ async function attachToProject(issueNumber, repo, priority) {
17491
+ const targetRepo2 = await resolveRepo(repo);
17492
+ let cfg;
17493
+ try {
17494
+ cfg = await loadConfigForRepo(targetRepo2);
17495
+ } catch (e) {
17496
+ console.error(`issue create: board attach skipped \u2014 ${e.message}`);
17497
+ return void 0;
16493
17498
  }
16494
- await run(makePlanDeps(cfg, io));
16495
- }
16496
- async function gatherRelevanceSignals() {
16497
- const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
16498
- const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
16499
- const signals = { branch, changedFiles: changed.length ? changed : void 0 };
16500
- const issueNum = branch?.match(/\b(\d{2,})\b/)?.[1];
16501
- if (issueNum) {
16502
- try {
16503
- const { stdout } = await execFileP2(
16504
- "gh",
16505
- ["issue", "view", issueNum, "--json", "title,labels", "--jq", "{title:.title,labels:[.labels[].name]}"],
16506
- { timeout: 1e4 }
16507
- );
16508
- const j = JSON.parse(stdout);
16509
- if (j.title) signals.issueTitle = j.title;
16510
- if (j.labels?.length) signals.issueLabels = j.labels;
16511
- } catch {
16512
- }
17499
+ if (!cfg.projectId) {
17500
+ console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
17501
+ return void 0;
16513
17502
  }
16514
- return signals;
16515
- }
16516
- function registerNorthStarCommands(cmd) {
16517
- cmd.command("push <slug>").description("push a North Star plan to the server (from plans/<slug>.md or --body-file)").option("--project <name>", "override the project key").option("--body-file <path|->", "read plan markdown from a UTF-8 file, or from stdin with -").option("--force", "overwrite the remote even if it changed since your last sync").option("--wait", "push synchronously (block until the server write lands)").action(async (slug, o) => {
16518
- let content;
16519
- if (o.bodyFile) {
17503
+ try {
17504
+ const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
17505
+ if (targetRepo2) viewArgs.push("--repo", targetRepo2);
17506
+ const { stdout: idOut } = await execFileP2("gh", viewArgs, { timeout: 1e4 });
17507
+ const contentId = idOut.trim();
17508
+ if (!contentId) throw new Error("could not resolve issue node id");
17509
+ const { stdout } = await execFileP2("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: GH_MUTATION_TIMEOUT_MS });
17510
+ const projectItemId = parseAddedItemId(stdout);
17511
+ if (projectItemId && priority) {
16520
17512
  try {
16521
- content = await resolveTextArg({ file: o.bodyFile }, { readFile: import_promises6.readFile, readStdin }, {
16522
- value: "inline content",
16523
- file: "--body-file",
16524
- noun: "plan"
16525
- });
17513
+ await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
16526
17514
  } catch (e) {
16527
- console.error(e.message);
16528
- process.exitCode = 1;
16529
- return;
17515
+ const err = e;
17516
+ process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
17517
+ `);
16530
17518
  }
16531
17519
  }
16532
- return withPlan(false, async (d) => {
16533
- const ok = await planPush(d, slug, { project: o.project, force: o.force, wait: o.wait, content });
16534
- if (!ok) process.exitCode = 1;
16535
- });
16536
- });
16537
- cmd.command("pull <slug>").description("pull a North Star plan from the server into plans/<slug>.md").option("--project <name>", "override the project key").option("--force", "overwrite local even if it has unpushed edits").action((slug, o) => withPlan(false, async (d) => {
16538
- const ok = await planPull(d, slug, o);
16539
- if (!ok) process.exitCode = 1;
16540
- }));
16541
- cmd.command("show <slug>").alias("get").description("print a North Star plan to stdout, read-only (no local copy written \u2014 mirrors `kb get`)").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, async (d) => {
16542
- const ok = await planShow(d, slug, o);
16543
- if (!ok) process.exitCode = 1;
16544
- }));
16545
- cmd.command("list").description("list your North Star plans (cross-device)").option("--project <name>", "filter by project").option("--json", "machine-readable output").option("--quiet", "silent when unconfigured/empty/unreachable (SessionStart hook)").option("--fresh", "bypass the local cache and read from the server").action((o) => withPlan(o.quiet ?? false, (d) => planList(d, o)));
16546
- cmd.command("relevant").description("list North Stars relevant to your current task (branch + claimed issue + changed files)").option("--project <name>", "override the project key").option("--all", "include superseded/graduated and recent non-matching plans").option("--limit <n>", "max results (default 5)").option("--fresh", "bypass the local cache and read from the server").option("--json", "machine-readable output").action((o) => withPlan(false, async (d) => {
16547
- const signals = await gatherRelevanceSignals();
16548
- await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh, json: o.json });
16549
- }));
16550
- cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").option("--wait", "durable confirmation: drain, then report unresolved pushes and exit non-zero if any remain").action((o) => withPlan(o.quiet ?? false, async (d) => {
16551
- const unresolved = await planSync(d, o);
16552
- if (!o.wait) return;
16553
- if (unresolved.length) {
16554
- for (const e of unresolved) d.err(`${e.slug}: ${e.conflict ?? e.deadLettered ?? "still pending"}`);
16555
- process.exitCode = 1;
16556
- } else if (!o.quiet) {
16557
- d.log("north star: all queued pushes landed");
16558
- }
16559
- }));
16560
- cmd.command("status").description("show pending/conflicted background pushes and the plan-cache age").option("--json", "machine-readable output").action((o) => withPlan(false, (d) => planStatus(d, o)));
16561
- cmd.command("reconcile").description("refresh stale local etags from the server without --force (recovers from an object-store re-stamp)").action(() => withPlan(false, (d) => planReconcile(d)));
16562
- cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
16563
- (slug, o) => withPlan(false, async (d) => {
16564
- const ok = await planPull(d, slug, { project: o.project });
16565
- if (!ok) {
16566
- process.exitCode = 1;
16567
- return;
16568
- }
16569
- openInEditor(planPath(slug));
16570
- })
16571
- );
16572
- cmd.command("delete <slug>").description("delete a North Star plan from the server and the local copy").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, (d) => planDelete(d, slug, o)));
16573
- cmd.command("graduate <slug>").description("mark a built-and-merged North Star plan as org-visible and push it").requiredOption("--merged-pr <url|number>", "merged PR URL or number proving the plan shipped").option("--org-visible", "confirm this plan is safe to queue for org KB curation").option("--project <name>", "override the project key").option("--force", "overwrite the remote even if it changed since your last sync").action(
16574
- (slug, o) => withPlan(false, (d) => planGraduate(d, slug, o))
16575
- );
16576
- }
16577
- var northstar = program2.command("northstar").description("North Star \u2014 your cross-device plans/SSOTs (S3-backed, git-clean)");
16578
- registerNorthStarCommands(northstar);
16579
- var plan = program2.command("plan").description("Alias for `northstar` (kept for compatibility)");
16580
- registerNorthStarCommands(plan);
16581
- async function readSecretStdin() {
16582
- if (process.stdin.isTTY) {
16583
- process.stderr.write(
16584
- 'secrets set: pipe the value on stdin (it is never an argument) \u2014 e.g.\n printf %s "$VALUE" | mmi-cli secrets set <KEY>\n'
16585
- );
16586
- return "";
17520
+ return projectItemId;
17521
+ } catch (e) {
17522
+ const err = e;
17523
+ process.stderr.write(`warning: issue #${issueNumber} created but NOT added to the project board: ${(err.stderr || err.message || String(e)).trim()}
17524
+ `);
17525
+ return void 0;
16587
17526
  }
16588
- const chunks = [];
16589
- for await (const chunk of process.stdin) chunks.push(chunk);
16590
- return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
16591
- }
16592
- function makeSecretsDeps(cfg) {
16593
- return {
16594
- apiUrl: cfg.sagaApiUrl,
16595
- fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
16596
- headers: (extra) => hubHeaders(extra),
16597
- // Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
16598
- // casing, which leaked mixed-case into `secrets where` output (#681).
16599
- slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
16600
- readSecretValue: () => readSecretStdin(),
16601
- log: (m) => console.log(m),
16602
- err: (m) => console.error(m)
16603
- };
16604
17527
  }
16605
- async function withSecrets(run) {
16606
- const cfg = await loadConfig();
16607
- if (!cfg.sagaApiUrl) {
16608
- fail("secrets: Hub API URL not configured");
16609
- return;
17528
+ var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
17529
+ function scheduleRelatedDiscovery(o) {
17530
+ try {
17531
+ const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
17532
+ if (o.repo) args.push("--repo", o.repo);
17533
+ (0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], ...args], {
17534
+ detached: true,
17535
+ stdio: "ignore",
17536
+ windowsHide: true,
17537
+ cwd: process.cwd()
17538
+ }).unref();
17539
+ } catch {
16610
17540
  }
16611
- await run(makeSecretsDeps(cfg));
16612
17541
  }
16613
- var secrets = program2.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
16614
- secrets.command("where").description("print where this repo\u2019s secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsWhere(d, o)));
16615
- secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsList(d, o)));
16616
- secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").action(async (o) => {
16617
- if (!["dev", "rc", "main"].includes(o.stage)) {
16618
- return fail("secrets preflight: --stage must be dev, rc, or main");
16619
- }
16620
- const cfg = await loadConfig();
16621
- if (!cfg.sagaApiUrl) {
16622
- fail("secrets: Hub API URL not configured");
16623
- return;
16624
- }
16625
- const d = makeSecretsDeps(cfg);
16626
- const repo = o.repo ?? `mutmutco/${await d.slug()}`;
16627
- const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(cfg));
16628
- const required = o.required?.length ? o.required : requiredRuntimeSecretNames(o.stage, meta?.requiredRuntimeSecrets, {
16629
- includeGoogleOAuth: projectRequiresGoogleOAuth(meta, meta?.deployModel)
16630
- });
16631
- const centralContainer = meta?.deployModel === "tenant-container" || meta?.deployModel === "solo-container";
16632
- if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
16633
- d.err("secrets preflight: requiredRuntimeSecrets is unset for this deployable tenant \u2014 declare the per-stage contract in registry META (or an explicit empty map) before promoting");
16634
- process.exitCode = 1;
16635
- return;
16636
- }
16637
- const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
16638
- if (!ok) process.exitCode = 1;
17542
+ var northstar = program2.command("northstar").description("North Star \u2014 your cross-device plans/SSOTs (S3-backed, git-clean)");
17543
+ registerNorthStarCommands(northstar);
17544
+ var plan = program2.command("plan").description("Alias for `northstar` (deprecated \u2014 use `northstar`)");
17545
+ plan.hook("preAction", () => {
17546
+ process.stderr.write("warning: `plan` is deprecated; use `northstar` instead\n");
16639
17547
  });
16640
- secrets.command("get <key>").description("print one secret value over TLS (prints once, raw \u2014 do not log/paste it)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
16641
- const ok = await secretsGet(d, key, o);
16642
- if (!ok) process.exitCode = 1;
16643
- }));
16644
- secrets.command("request <key>").description("approved escalation: create a Hub issue + admin DM for a missing secret (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--priority <priority>", "urgent | high | medium | low (default: medium)").option("--reason <text>", "why the secret is needed; safe metadata only").option("--context <text>", "lookup command/context; safe metadata only").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
16645
- const ok = await secretsRequest(d, key, o);
16646
- if (!ok) process.exitCode = 1;
16647
- }));
16648
- secrets.command("verify <key>").description("validate a known provider secret without printing its value").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
16649
- const ok = await secretsVerify(d, key, o);
16650
- if (!ok) process.exitCode = 1;
16651
- }));
16652
- secrets.command("set <key>").description("write/rotate a secret; value is read from stdin (never an argument)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
16653
- const ok = await secretsSet(d, key, o);
16654
- if (!ok) process.exitCode = 1;
16655
- }));
16656
- secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
16657
- const ok = await secretsEdit(d, key, o);
16658
- if (!ok) process.exitCode = 1;
16659
- }));
16660
- secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
16661
- const stages = ["dev", "rc", "main"];
16662
- if (!stages.includes(o.from) || !stages.includes(o.to)) {
16663
- return fail("secrets copy: --from and --to must be dev, rc, or main");
16664
- }
16665
- const ok = await secretsCopy(d, {
16666
- repo: o.repo,
16667
- from: o.from,
16668
- to: o.to,
16669
- keys: o.keys.split(","),
16670
- dryRun: o.dryRun
16671
- });
16672
- if (!ok) process.exitCode = 1;
16673
- }));
16674
- secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
16675
- secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
16676
- secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
16677
- secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-tier secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
17548
+ registerNorthStarCommands(plan);
17549
+ registerSecretsCommands(program2);
16678
17550
  function registryClientDeps(cfg) {
16679
17551
  return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
16680
17552
  }
@@ -16691,23 +17563,23 @@ async function reportWrite(label, res) {
16691
17563
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
16692
17564
  }
16693
17565
  var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
16694
- tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, plus rc-only retire and read-only verify-secrets) for a tenant; project-admin dev/rc, master main").option("--json", "machine-readable output").action(async (repo, stage2, action, o) => {
16695
- const cfg = await loadConfig();
16696
- const wait = tenantControlWait(action);
16697
- const res = await tenantControl({ repo, stage: stage2, action, wait }, registryClientDeps(cfg));
16698
- const body = res.body;
16699
- if (!res.ok && body?.category) {
16700
- console.log(JSON.stringify(body));
16701
- return failGraceful(`tenant control ${stage2} ${action}: ${body.category}`);
16702
- }
16703
- if (res.ok && action === "verify-secrets") {
16704
- const { lines, failure } = renderVerifySecrets(res.body);
16705
- if (o.json) console.log(JSON.stringify(res.body));
16706
- else lines.forEach((l) => console.log(l));
16707
- if (failure) return failGraceful(`tenant control ${stage2} verify-secrets: ${failure}`);
16708
- return;
17566
+ tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, rc-only retire, read-only verify-secrets) for a tenant via the central tenant-control.yml workflow; project-admin dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets watch by default)").option("--json", "machine-readable output").action(async (repo, stage2, action, o) => {
17567
+ try {
17568
+ const result = await runTenantControl(trainApplyDeps(), { repo, stage: stage2, action, watch: o.watch });
17569
+ if (!o.json && action === "verify-secrets" && result.secrets) {
17570
+ const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed" };
17571
+ const { lines, failure } = renderVerifySecrets(body);
17572
+ for (const line of lines) printLine(line);
17573
+ if (failure) return failGraceful(`tenant control ${stage2} verify-secrets: ${failure}`);
17574
+ } else {
17575
+ printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
17576
+ }
17577
+ if (result.conclusion === "failure") {
17578
+ return failGraceful(`tenant control ${stage2} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`);
17579
+ }
17580
+ } catch (e) {
17581
+ return failGraceful(`tenant control: ${e.message}`);
16709
17582
  }
16710
- return reportWrite("tenant control", res);
16711
17583
  });
16712
17584
  tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
16713
17585
  if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
@@ -16724,18 +17596,18 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
16724
17596
  }
16725
17597
  const cfg = await loadConfig();
16726
17598
  const cdeps = registryClientDeps(cfg);
17599
+ const tdeps = trainApplyDeps();
16727
17600
  try {
16728
17601
  const result = await sweepRcOrphans({
16729
17602
  listProjects: () => fetchProjectsList(cdeps),
16730
17603
  status: async (repo) => {
16731
- const res = await tenantControl({ repo, stage: "rc", action: "status", wait: true }, cdeps);
16732
- const b = res.body;
16733
- return { serviceState: b?.serviceState ?? "unknown" };
17604
+ const r = await runTenantControl(tdeps, { repo, stage: "rc", action: "status", watch: true });
17605
+ const serviceState = r.conclusion === "success" ? r.serviceState ?? "unknown" : "error";
17606
+ return { serviceState };
16734
17607
  },
16735
17608
  retire: async (repo) => {
16736
- const res = await tenantControl({ repo, stage: "rc", action: "retire", wait: true }, cdeps);
16737
- const b = res.body;
16738
- return { ok: res.ok, category: b?.category, reason: b?.reason };
17609
+ const r = await runTenantControl(tdeps, { repo, stage: "rc", action: "retire", watch: true });
17610
+ return { ok: r.category === "retired", category: r.category };
16739
17611
  }
16740
17612
  }, { retire: !!o.retire });
16741
17613
  return printLine(o.json ? JSON.stringify(result) : renderSweep(result));
@@ -16922,7 +17794,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
16922
17794
  const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
16923
17795
  return reportWrite("project attest", res);
16924
17796
  });
16925
- project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project META (idempotent merge; defaults to the current repo; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
17797
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master for most fields; project-admin may set/unset dashboard only)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired, dashboard").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
16926
17798
  const cfg = await loadConfig();
16927
17799
  let target;
16928
17800
  try {
@@ -16931,6 +17803,7 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
16931
17803
  return fail(e.message);
16932
17804
  }
16933
17805
  const slug = slugOf(target);
17806
+ const repo = target.includes("/") ? target : `mutmutco/${slug}`;
16934
17807
  let patch;
16935
17808
  try {
16936
17809
  patch = buildProjectSetPatch({
@@ -16948,7 +17821,7 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
16948
17821
  const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
16949
17822
  const boardError = boardLinkWriteError(patch, existing);
16950
17823
  if (boardError) return fail(`project set: ${boardError}`);
16951
- const res = await upsertProject(slug, patch, registryClientDeps(cfg));
17824
+ const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
16952
17825
  return reportWrite("project set", res);
16953
17826
  });
16954
17827
  var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
@@ -17053,8 +17926,8 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
17053
17926
  let body;
17054
17927
  let title;
17055
17928
  try {
17056
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
17057
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
17929
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
17930
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
17058
17931
  if (o.priority === void 0) throw new Error("missing --priority <priority> \u2014 expected one of: urgent, high, medium, low");
17059
17932
  priority = normalizePriority(o.priority);
17060
17933
  args = buildIssueArgs({ type: o.type, title, body, priority, repo: o.repo, labels: o.label });
@@ -17141,8 +18014,8 @@ program2.command("report").description("file a friction report on the Hub board
17141
18014
  const targetRepo2 = o.repo ?? HUB_REPO2;
17142
18015
  const sourceRepo = await resolveRepo(void 0);
17143
18016
  try {
17144
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
17145
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
18017
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
18018
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
17146
18019
  priority = normalizePriority(o.priority);
17147
18020
  args = buildIssueArgs({
17148
18021
  type: o.type,
@@ -17208,8 +18081,8 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
17208
18081
  try {
17209
18082
  const routing = assertVerifyRouting(o.routing);
17210
18083
  const lenses = o.lenses.split(",").map((s) => assertGrindLens(s.trim()));
17211
- const criteria = await (0, import_promises6.readFile)(o.criteriaFile, "utf8");
17212
- const diff = await (0, import_promises6.readFile)(o.diffFile, "utf8");
18084
+ const criteria = await (0, import_promises7.readFile)(o.criteriaFile, "utf8");
18085
+ const diff = await (0, import_promises7.readFile)(o.diffFile, "utf8");
17213
18086
  const plan2 = buildPanelPlan({ routing, lenses, criteria, diff });
17214
18087
  console.log(JSON.stringify(plan2));
17215
18088
  } catch (e) {
@@ -17218,7 +18091,7 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
17218
18091
  });
17219
18092
  verify.command("synthesize").description("merge lens JSON array into a PanelReport").option("--input-file <path|->", "JSON lens array (use - for stdin)", "-").action(async (o) => {
17220
18093
  try {
17221
- const raw = o.inputFile === "-" ? await readStdin() : await (0, import_promises6.readFile)(o.inputFile, "utf8");
18094
+ const raw = o.inputFile === "-" ? await readStdin() : await (0, import_promises7.readFile)(o.inputFile, "utf8");
17222
18095
  const lenses = parseLensResults(JSON.parse(raw));
17223
18096
  console.log(JSON.stringify(synthesizePanelReport(lenses)));
17224
18097
  } catch (e) {
@@ -17291,7 +18164,7 @@ build.command("frontier").description("Evaluate external frontier exhaustion + L
17291
18164
  iterationCapOverride: opts.iterationCap
17292
18165
  };
17293
18166
  if (opts.jsonFile) {
17294
- const raw = await (0, import_promises6.readFile)(opts.jsonFile, "utf8");
18167
+ const raw = await (0, import_promises7.readFile)(opts.jsonFile, "utf8");
17295
18168
  state = { ...state, ...JSON.parse(raw) };
17296
18169
  }
17297
18170
  const result = evaluateBuildFrontier(state);
@@ -17345,8 +18218,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
17345
18218
  let args;
17346
18219
  try {
17347
18220
  skill = assertSkillName(o.skill);
17348
- rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
17349
- const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
18221
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
18222
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
17350
18223
  title = buildSkillLessonTitle(skill, rawTitle);
17351
18224
  priority = normalizePriority(o.priority);
17352
18225
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -17397,8 +18270,8 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
17397
18270
  let body;
17398
18271
  let title;
17399
18272
  try {
17400
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
17401
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
18273
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
18274
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
17402
18275
  } catch (e) {
17403
18276
  return fail(`pr create: ${e.message}`);
17404
18277
  }
@@ -17406,9 +18279,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
17406
18279
  console.log(JSON.stringify(created));
17407
18280
  });
17408
18281
  async function listCiWorkflowPaths(cwd = process.cwd()) {
17409
- const wfDir = (0, import_node_path17.join)(cwd, ".github", "workflows");
17410
- if (!(0, import_node_fs19.existsSync)(wfDir)) return [];
17411
- return (0, import_node_fs19.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
18282
+ const wfDir = (0, import_node_path19.join)(cwd, ".github", "workflows");
18283
+ if (!(0, import_node_fs22.existsSync)(wfDir)) return [];
18284
+ return (0, import_node_fs22.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
17412
18285
  }
17413
18286
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
17414
18287
  const repo = repoOpt ?? await resolveRepo();
@@ -17427,7 +18300,7 @@ function ciAuditDeps() {
17427
18300
  // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
17428
18301
  // reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
17429
18302
  // the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
17430
- readSeedFile: (path2) => (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null
18303
+ readSeedFile: (path2) => (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null
17431
18304
  };
17432
18305
  }
17433
18306
  pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
@@ -17467,12 +18340,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
17467
18340
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
17468
18341
  const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
17469
18342
  resolveRepo: async (prNumber, repoOpt) => {
17470
- if (repoOpt) return repoOpt;
17471
- const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
17472
- const [repo, base2] = viewed.split(/\s+/);
18343
+ const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
18344
+ const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
18345
+ const [repoFromGh, base2] = viewed.split(/\s+/);
17473
18346
  if (base2 && base2 !== "development") {
17474
18347
  throw new Error(`pr land: base branch must be development (got ${base2}) \u2014 promotion merges stay human-only`);
17475
18348
  }
18349
+ const repo = repoOpt ?? repoFromGh;
17476
18350
  if (!repo) throw new Error("pr land: could not resolve PR repo");
17477
18351
  return repo;
17478
18352
  },
@@ -17497,32 +18371,50 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
17497
18371
  }
17498
18372
  return { mergeStatus: "failed", error: `merge blocked: ${message.split("\n")[0]} \u2014 ensure checks are green` };
17499
18373
  }
17500
- const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
17501
- return { mergeStatus: state === "MERGED" ? "merged" : "auto-merge-enqueued" };
18374
+ const stateRead = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS })).stdout);
18375
+ if (!stateRead.ok) {
18376
+ return { mergeStatus: "failed", error: `could not read PR state after merge: ${stateRead.error}` };
18377
+ }
18378
+ return { mergeStatus: stateRead.state === "MERGED" ? "merged" : "auto-merge-enqueued" };
17502
18379
  },
17503
18380
  pollMerged: async (prNumber, repo, deadlineMs) => {
17504
18381
  const args = repo ? ["--repo", repo] : [];
17505
18382
  while (Date.now() < deadlineMs) {
17506
- const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
17507
- if (state === "MERGED") return true;
18383
+ const stateRead = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS })).stdout, { retries: 2, delayMs: 1e3 });
18384
+ if (stateRead.ok && stateRead.state === "MERGED") return true;
17508
18385
  await new Promise((resolve) => setTimeout(resolve, PR_LAND_POLL_MS));
17509
18386
  }
17510
18387
  return false;
17511
18388
  }
17512
18389
  });
18390
+ if (result.status !== "failed") {
18391
+ try {
18392
+ const { stdout } = await execFileP2(process.execPath, [
18393
+ process.argv[1],
18394
+ "pr",
18395
+ "merge",
18396
+ number,
18397
+ ...o.repo ? ["--repo", o.repo] : [],
18398
+ "--squash"
18399
+ ], { timeout: GH_MUTATION_TIMEOUT_MS });
18400
+ const trimmed = stdout.trim();
18401
+ if (trimmed) {
18402
+ try {
18403
+ result.cleanup = JSON.parse(trimmed);
18404
+ } catch {
18405
+ result.cleanupError = "cleanup output was not JSON";
18406
+ }
18407
+ }
18408
+ } catch (e) {
18409
+ result.cleanupError = String(e.message || "pr merge cleanup failed");
18410
+ }
18411
+ }
17513
18412
  if (o.json) printLine(JSON.stringify(result));
17514
- else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
17515
- if (result.status === "failed") process.exitCode = 1;
17516
18413
  else {
17517
- await execFileP2(process.execPath, [
17518
- process.argv[1],
17519
- "pr",
17520
- "merge",
17521
- number,
17522
- ...o.repo ? ["--repo", o.repo] : [],
17523
- "--squash"
17524
- ], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
18414
+ printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
18415
+ if (result.cleanupError) printLine(`pr land cleanup: ${result.cleanupError}`);
17525
18416
  }
18417
+ if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
17526
18418
  });
17527
18419
  async function remoteBranchExists2(branch, options = {}) {
17528
18420
  return checkRemoteBranchExists(branch, {
@@ -17537,15 +18429,15 @@ async function createDeferredWorktreeStore() {
17537
18429
  return {
17538
18430
  read: async () => {
17539
18431
  try {
17540
- return parseDeferredWorktreesFile(await (0, import_promises6.readFile)(registryPath, "utf8"));
18432
+ return parseDeferredWorktreesFile(await (0, import_promises7.readFile)(registryPath, "utf8"));
17541
18433
  } catch {
17542
18434
  return [];
17543
18435
  }
17544
18436
  },
17545
18437
  write: async (entries) => {
17546
18438
  try {
17547
- await (0, import_promises6.mkdir)((0, import_node_path17.dirname)(registryPath), { recursive: true });
17548
- await (0, import_promises6.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
18439
+ await (0, import_promises7.mkdir)((0, import_node_path19.dirname)(registryPath), { recursive: true });
18440
+ await (0, import_promises7.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
17549
18441
  } catch {
17550
18442
  }
17551
18443
  }
@@ -17558,13 +18450,13 @@ var realWorktreeDirRemover = {
17558
18450
  probe: (p) => {
17559
18451
  let st;
17560
18452
  try {
17561
- st = (0, import_node_fs19.lstatSync)(p);
18453
+ st = (0, import_node_fs22.lstatSync)(p);
17562
18454
  } catch {
17563
18455
  return null;
17564
18456
  }
17565
18457
  if (st.isSymbolicLink()) return "link";
17566
18458
  try {
17567
- (0, import_node_fs19.readlinkSync)(p);
18459
+ (0, import_node_fs22.readlinkSync)(p);
17568
18460
  return "link";
17569
18461
  } catch {
17570
18462
  }
@@ -17572,7 +18464,7 @@ var realWorktreeDirRemover = {
17572
18464
  },
17573
18465
  readdir: (p) => {
17574
18466
  try {
17575
- return (0, import_node_fs19.readdirSync)(p);
18467
+ return (0, import_node_fs22.readdirSync)(p);
17576
18468
  } catch {
17577
18469
  return [];
17578
18470
  }
@@ -17581,12 +18473,12 @@ var realWorktreeDirRemover = {
17581
18473
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
17582
18474
  detachLink: (p) => {
17583
18475
  try {
17584
- (0, import_node_fs19.rmdirSync)(p);
18476
+ (0, import_node_fs22.rmdirSync)(p);
17585
18477
  } catch {
17586
- (0, import_node_fs19.unlinkSync)(p);
18478
+ (0, import_node_fs22.unlinkSync)(p);
17587
18479
  }
17588
18480
  },
17589
- removeTree: (p) => (0, import_promises6.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
18481
+ removeTree: (p) => (0, import_promises7.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
17590
18482
  };
17591
18483
  async function resolvePrimaryCheckout(execGit) {
17592
18484
  try {
@@ -17604,7 +18496,7 @@ function worktreeRemoveDeps(execGit) {
17604
18496
  }
17605
18497
  function teardownWorktreeStage(worktreePath) {
17606
18498
  return runWorktreeStageTeardown(worktreePath, {
17607
- hasStageState: (wt) => (0, import_node_fs19.existsSync)(stageStatePath(wt)),
18499
+ hasStageState: (wt) => (0, import_node_fs22.existsSync)(stageStatePath(wt)),
17608
18500
  stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
17609
18501
  listComposeProjects: async () => {
17610
18502
  const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
@@ -17618,7 +18510,7 @@ function teardownWorktreeStage(worktreePath) {
17618
18510
  pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").action(async (number, o) => {
17619
18511
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
17620
18512
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
17621
- const headRef = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
18513
+ const [headRef, baseRef] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName", "--jq", '.headRefName + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim().split(/\s+/);
17622
18514
  const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
17623
18515
  const beforeWorktrees = parseWorktreePorcelain(
17624
18516
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
@@ -17667,11 +18559,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
17667
18559
  } : await cleanupPrMergeLocalBranch(headRef, {
17668
18560
  beforeWorktrees,
17669
18561
  startingPath,
17670
- pathExists: (p) => (0, import_node_fs19.existsSync)(p),
18562
+ pathExists: (p) => (0, import_node_fs22.existsSync)(p),
17671
18563
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
17672
18564
  teardownWorktreeStage,
17673
18565
  deferredStore,
17674
- removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir
18566
+ removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir,
18567
+ // After merge, return the local checkout to the (fast-forwarded) branch the PR merged into so
18568
+ // grind/build never leave the primary parked on a dead feature branch (#1606).
18569
+ returnToBranch: baseRef
17675
18570
  });
17676
18571
  } catch (e) {
17677
18572
  localCleanup = {
@@ -17839,7 +18734,7 @@ function rawValues(flag) {
17839
18734
  return out;
17840
18735
  }
17841
18736
  function printLine(value) {
17842
- (0, import_node_fs19.writeSync)(1, `${value}
18737
+ (0, import_node_fs22.writeSync)(1, `${value}
17843
18738
  `);
17844
18739
  }
17845
18740
  function stageKeepAlive() {
@@ -17856,8 +18751,8 @@ async function resolveStage() {
17856
18751
  local,
17857
18752
  shell: shellFor(),
17858
18753
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
17859
- hasCompose: (0, import_node_fs19.existsSync)((0, import_node_path17.join)(process.cwd(), "docker-compose.yml")),
17860
- hasEnvExample: (0, import_node_fs19.existsSync)((0, import_node_path17.join)(process.cwd(), ".env.example"))
18754
+ hasCompose: (0, import_node_fs22.existsSync)((0, import_node_path19.join)(process.cwd(), "docker-compose.yml")),
18755
+ hasEnvExample: (0, import_node_fs22.existsSync)((0, import_node_path19.join)(process.cwd(), ".env.example"))
17861
18756
  });
17862
18757
  }
17863
18758
  async function fetchStageVaultEnvMerge() {
@@ -17909,9 +18804,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
17909
18804
  printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
17910
18805
  return;
17911
18806
  }
17912
- const path2 = (0, import_node_path17.join)(process.cwd(), "infra", "port-ranges.json");
18807
+ const path2 = (0, import_node_path19.join)(process.cwd(), "infra", "port-ranges.json");
17913
18808
  const allocate = async (seed) => {
17914
- const { stdout } = await execFileP2("node", [(0, import_node_path17.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
18809
+ const { stdout } = await execFileP2("node", [(0, import_node_path19.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
17915
18810
  const parsed = JSON.parse(stdout);
17916
18811
  if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
17917
18812
  return parsed.range;
@@ -18097,6 +18992,15 @@ function trainApplyDeps() {
18097
18992
  throw new Error(`tenant deploy dispatch failed: ${detail}`);
18098
18993
  }
18099
18994
  },
18995
+ // Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
18996
+ // workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
18997
+ // outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
18998
+ dispatchTenantControl: async ({ repo, stage: stage2, action }) => {
18999
+ const res = await tenantControl({ repo, stage: stage2, action }, registryClientDeps(await loadConfig()));
19000
+ if (res.ok) return { ok: true };
19001
+ const body = res.body;
19002
+ return { ok: false, category: body?.category, error: body?.error ?? res.error };
19003
+ },
18100
19004
  // Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
18101
19005
  // release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
18102
19006
  // fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
@@ -18105,7 +19009,7 @@ function trainApplyDeps() {
18105
19009
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
18106
19010
  announce: (args) => announceRelease({
18107
19011
  run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
18108
- readFile: (path2) => (0, import_promises6.readFile)(path2, "utf8")
19012
+ readFile: (path2) => (0, import_promises7.readFile)(path2, "utf8")
18109
19013
  }, args),
18110
19014
  fetchEdgeDomains: async (slug) => {
18111
19015
  const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
@@ -18215,7 +19119,8 @@ function renderHotfixStatus(r) {
18215
19119
  ` - branch: ${r.branchExists ? "pushed" : "absent"} \xB7 PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} \xB7 tag: ${r.tagPushed ? "pushed" : "absent"} \xB7 Release: ${r.releaseExists ? "exists" : "absent"}`,
18216
19120
  ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
18217
19121
  ` - npm @mutmutco/cli: ${r.npmVersion}`,
18218
- ` - next: ${r.next}`
19122
+ ` - next: ${r.next}`,
19123
+ ...r.warnings.map((w) => ` - warning: ${w}`)
18219
19124
  ].join("\n");
18220
19125
  }
18221
19126
  async function runHotfixSub(sub, body, json, render) {
@@ -18273,12 +19178,12 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
18273
19178
  }
18274
19179
  if (!audit.ok) process.exitCode = 1;
18275
19180
  });
18276
- var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
19181
+ var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--dashboard", "dashboard repo \u2014 also seeds components.json wired to the @mutmutco registry (#1452)").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
18277
19182
  if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
18278
19183
  if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
18279
19184
  if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap: --class must be deployable or content");
18280
- const steps = bootstrapPlan(o.repo, o.class);
18281
- console.log(o.json ? JSON.stringify({ command: "bootstrap", repo: o.repo, class: o.class, steps }, null, 2) : renderSteps(`mmi-cli bootstrap: dry-run plan for ${o.repo}`, steps));
19185
+ const steps = bootstrapPlan(o.repo, o.class, { dashboard: o.dashboard });
19186
+ console.log(o.json ? JSON.stringify({ command: "bootstrap", repo: o.repo, class: o.class, dashboard: o.dashboard === true, steps }, null, 2) : renderSteps(`mmi-cli bootstrap: dry-run plan for ${o.repo}`, steps));
18282
19187
  });
18283
19188
  bootstrap.command("verify <repo>").description("audit whether an existing repo is bootstrapped correctly; no mutations").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").action(async (repo) => {
18284
19189
  const o = { class: rawValue("--class", "deployable"), json: rawFlag("--json") };
@@ -18292,7 +19197,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
18292
19197
  client: defaultGitHubClient(),
18293
19198
  projectMeta: meta,
18294
19199
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
18295
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null,
19200
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
18296
19201
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
18297
19202
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
18298
19203
  requiredGcpApis: (() => {
@@ -18317,12 +19222,13 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
18317
19222
  console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
18318
19223
  if (!report.ok) process.exitCode = 1;
18319
19224
  });
18320
- 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) => {
19225
+ 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("--dashboard", "dashboard repo \u2014 seeds components.json wired to the @mutmutco registry (#1452); changes no other axis").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) => {
18321
19226
  const o = {
18322
19227
  class: rawValue("--class", "deployable"),
18323
19228
  projectType: rawValue("--project-type", ""),
18324
19229
  deployModel: rawValue("--deploy-model", ""),
18325
19230
  releaseTrack: rawValue("--release-track", ""),
19231
+ dashboard: rawFlag("--dashboard"),
18326
19232
  execute: rawFlag("--execute"),
18327
19233
  json: rawFlag("--json")
18328
19234
  };
@@ -18335,20 +19241,22 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18335
19241
  return fail(`bootstrap apply: ${e.message}`);
18336
19242
  }
18337
19243
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
18338
- if (!(0, import_node_fs19.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
18339
- const manifest = loadBootstrapSeeds((0, import_node_fs19.readFileSync)(manifestPath, "utf8"));
19244
+ if (!(0, import_node_fs22.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
19245
+ const manifest = loadBootstrapSeeds((0, import_node_fs22.readFileSync)(manifestPath, "utf8"));
18340
19246
  const baseBranch = o.class === "content" ? "main" : "development";
18341
19247
  const slug = parsedRepo.slug;
18342
19248
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
18343
- const readFile6 = (p) => (0, import_node_fs19.existsSync)(p) ? (0, import_node_fs19.readFileSync)(p, "utf8") : null;
19249
+ const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
18344
19250
  const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
18345
19251
  const rawVars = {};
18346
19252
  for (const value of rawValues("--var")) {
18347
19253
  const eq = value.indexOf("=");
18348
19254
  if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
18349
19255
  }
19256
+ let registryMetaDashboard = false;
18350
19257
  try {
18351
19258
  const meta = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
19259
+ registryMetaDashboard = meta?.dashboard === true;
18352
19260
  for (const [k, v] of Object.entries(gateConfigToVars(meta?.gate))) if (rawVars[k] == null) rawVars[k] = v;
18353
19261
  } catch {
18354
19262
  }
@@ -18365,16 +19273,20 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18365
19273
  const applied = [];
18366
19274
  let applyDeployModel;
18367
19275
  try {
18368
- applyDeployModel = buildRegisterPayload(repo, o.class, vars, {
19276
+ const payload = buildRegisterPayload(repo, o.class, vars, {
18369
19277
  projectType: o.projectType || void 0,
18370
19278
  deployModel: o.deployModel || void 0,
18371
- releaseTrack: o.releaseTrack || void 0
18372
- }).deployModel;
19279
+ releaseTrack: o.releaseTrack || void 0,
19280
+ dashboard: o.dashboard
19281
+ });
19282
+ applyDeployModel = payload.deployModel;
18373
19283
  } catch {
18374
19284
  }
19285
+ const applyDashboard = o.dashboard === true || !rawFlag("--dashboard") && registryMetaDashboard;
18375
19286
  for (const seed of manifest.seeds) {
18376
19287
  if (!seed.classes.includes(o.class)) continue;
18377
19288
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
19289
+ if (!seedMatchesDashboard(seed, applyDashboard)) continue;
18378
19290
  const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
18379
19291
  let exists = false;
18380
19292
  let sha;
@@ -18397,7 +19309,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18397
19309
  }
18398
19310
  const planned = planSeedAction(resolved, exists);
18399
19311
  const isBlock = resolved.source === "managed-block";
18400
- const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile6) : null;
19312
+ const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
18401
19313
  const action = reconcileSeedAction(planned, content, isBlock);
18402
19314
  actions.push(action);
18403
19315
  if (o.execute && (action.action === "create" || action.action === "update")) {
@@ -18414,7 +19326,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18414
19326
  }
18415
19327
  const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
18416
19328
  if (rulesetSeed) {
18417
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile6);
19329
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
18418
19330
  if (rulesetContent) {
18419
19331
  try {
18420
19332
  const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
@@ -18450,7 +19362,8 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18450
19362
  registerPayload = buildRegisterPayload(repo, o.class, vars, {
18451
19363
  projectType: o.projectType || void 0,
18452
19364
  deployModel: o.deployModel || void 0,
18453
- releaseTrack: bootstrapReleaseTrack
19365
+ releaseTrack: bootstrapReleaseTrack,
19366
+ dashboard: o.dashboard
18454
19367
  });
18455
19368
  } catch (e) {
18456
19369
  return fail(`bootstrap apply: ${e.message}`);
@@ -18584,38 +19497,39 @@ access.command("audit").description("audit collaborator roles + train-branch pus
18584
19497
  if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
18585
19498
  targets = [{ repo: o.repo, class: o.class }];
18586
19499
  } else {
18587
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs19.existsSync)("projects.json") ? (0, import_node_fs19.readFileSync)("projects.json", "utf8") : null;
19500
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs22.existsSync)("projects.json") ? (0, import_node_fs22.readFileSync)("projects.json", "utf8") : null;
18588
19501
  if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
18589
- const fanoutJson = (0, import_node_fs19.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs19.readFileSync)(".github/fanout-targets.json", "utf8") : null;
19502
+ const fanoutJson = (0, import_node_fs22.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs22.readFileSync)(".github/fanout-targets.json", "utf8") : null;
18590
19503
  targets = loadAccessTargets(projectsJson, fanoutJson);
18591
19504
  }
18592
19505
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
18593
- const fileMatrix = (0, import_node_fs19.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs19.readFileSync)("access-matrix.json", "utf8")) : {};
19506
+ const fileMatrix = (0, import_node_fs22.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs22.readFileSync)("access-matrix.json", "utf8")) : {};
18594
19507
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
18595
19508
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
18596
- const fileContracts = (0, import_node_fs19.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs19.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
19509
+ const fileContracts = (0, import_node_fs22.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs22.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
18597
19510
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
18598
19511
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
18599
19512
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
18600
19513
  if (!report.ok) process.exitCode = 1;
18601
19514
  });
19515
+ access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
18602
19516
  var isWin = process.platform === "win32";
18603
19517
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
18604
19518
  const homeDir = surface === "codex" ? ".codex" : ".claude";
18605
- return (0, import_node_path17.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
19519
+ return (0, import_node_path19.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
18606
19520
  };
18607
19521
  function readInstalledPlugins() {
18608
19522
  try {
18609
- return JSON.parse((0, import_node_fs19.readFileSync)(installedPluginsPath(), "utf8"));
19523
+ return JSON.parse((0, import_node_fs22.readFileSync)(installedPluginsPath(), "utf8"));
18610
19524
  } catch {
18611
19525
  return null;
18612
19526
  }
18613
19527
  }
18614
19528
  function installedPluginSources() {
18615
19529
  return ["claude", "codex"].map((surface) => {
18616
- const recordPath = (0, import_node_path17.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
19530
+ const recordPath = (0, import_node_path19.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
18617
19531
  try {
18618
- return { surface, installed: JSON.parse((0, import_node_fs19.readFileSync)(recordPath, "utf8")), recordPath };
19532
+ return { surface, installed: JSON.parse((0, import_node_fs22.readFileSync)(recordPath, "utf8")), recordPath };
18619
19533
  } catch {
18620
19534
  return { surface, installed: null, recordPath };
18621
19535
  }
@@ -18623,7 +19537,7 @@ function installedPluginSources() {
18623
19537
  }
18624
19538
  function readClaudeSettings() {
18625
19539
  try {
18626
- return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
19540
+ return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
18627
19541
  } catch {
18628
19542
  return null;
18629
19543
  }
@@ -18645,7 +19559,7 @@ function writeProjectInstallRecord(record) {
18645
19559
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
18646
19560
  list.push(record);
18647
19561
  file.plugins[MMI_PLUGIN_ID] = list;
18648
- (0, import_node_fs19.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
19562
+ (0, import_node_fs22.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
18649
19563
  `, "utf8");
18650
19564
  return true;
18651
19565
  } catch {
@@ -18658,9 +19572,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
18658
19572
  if (!file) return false;
18659
19573
  if (!file.plugins) file.plugins = {};
18660
19574
  const path2 = installedPluginsPath();
18661
- (0, import_node_fs19.copyFileSync)(path2, `${path2}.bak`);
19575
+ (0, import_node_fs22.copyFileSync)(path2, `${path2}.bak`);
18662
19576
  file.plugins[pluginId] = records;
18663
- (0, import_node_fs19.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
19577
+ (0, import_node_fs22.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
18664
19578
  `, "utf8");
18665
19579
  return true;
18666
19580
  } catch {
@@ -18668,35 +19582,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
18668
19582
  }
18669
19583
  }
18670
19584
  function cursorPluginCacheRoot() {
18671
- return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
19585
+ return (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
18672
19586
  }
18673
19587
  function cursorPluginCachePinSnapshots() {
18674
19588
  const root = cursorPluginCacheRoot();
18675
19589
  try {
18676
- return (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
18677
- const path2 = (0, import_node_path17.join)(root, entry.name);
18678
- const pluginJson = (0, import_node_path17.join)(path2, ".cursor-plugin", "plugin.json");
18679
- const hooksJson = (0, import_node_path17.join)(path2, "hooks", "hooks.json");
18680
- const cliBundle = (0, import_node_path17.join)(path2, "cli", "dist", "index.cjs");
19590
+ return (0, import_node_fs22.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
19591
+ const path2 = (0, import_node_path19.join)(root, entry.name);
19592
+ const pluginJson = (0, import_node_path19.join)(path2, ".cursor-plugin", "plugin.json");
19593
+ const hooksJson = (0, import_node_path19.join)(path2, "hooks", "hooks.json");
19594
+ const cliBundle = (0, import_node_path19.join)(path2, "cli", "dist", "index.cjs");
18681
19595
  let version;
18682
19596
  try {
18683
- const raw = JSON.parse((0, import_node_fs19.readFileSync)(pluginJson, "utf8"));
19597
+ const raw = JSON.parse((0, import_node_fs22.readFileSync)(pluginJson, "utf8"));
18684
19598
  version = typeof raw.version === "string" ? raw.version : void 0;
18685
19599
  } catch {
18686
19600
  version = void 0;
18687
19601
  }
18688
19602
  let isEmpty = true;
18689
19603
  try {
18690
- isEmpty = (0, import_node_fs19.readdirSync)(path2).length === 0;
19604
+ isEmpty = (0, import_node_fs22.readdirSync)(path2).length === 0;
18691
19605
  } catch {
18692
19606
  isEmpty = true;
18693
19607
  }
18694
19608
  return {
18695
19609
  name: entry.name,
18696
19610
  path: path2,
18697
- hasPluginJson: (0, import_node_fs19.existsSync)(pluginJson),
18698
- hasHooksJson: (0, import_node_fs19.existsSync)(hooksJson),
18699
- hasCliBundle: (0, import_node_fs19.existsSync)(cliBundle),
19611
+ hasPluginJson: (0, import_node_fs22.existsSync)(pluginJson),
19612
+ hasHooksJson: (0, import_node_fs22.existsSync)(hooksJson),
19613
+ hasCliBundle: (0, import_node_fs22.existsSync)(cliBundle),
18700
19614
  isEmpty,
18701
19615
  version
18702
19616
  };
@@ -18706,19 +19620,19 @@ function cursorPluginCachePinSnapshots() {
18706
19620
  }
18707
19621
  }
18708
19622
  function hubCheckoutForCursorSeed() {
18709
- const manifest = (0, import_node_path17.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
18710
- return (0, import_node_fs19.existsSync)(manifest) ? process.cwd() : void 0;
19623
+ const manifest = (0, import_node_path19.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
19624
+ return (0, import_node_fs22.existsSync)(manifest) ? process.cwd() : void 0;
18711
19625
  }
18712
19626
  function mmiPluginCacheRootSnapshots() {
18713
19627
  const roots = [
18714
- { surface: "claude", root: (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
18715
- { surface: "codex", root: (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
19628
+ { surface: "claude", root: (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
19629
+ { surface: "codex", root: (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
18716
19630
  ];
18717
19631
  return roots.flatMap(({ surface, root }) => {
18718
19632
  try {
18719
- const entries = (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
19633
+ const entries = (0, import_node_fs22.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
18720
19634
  name: entry.name,
18721
- path: (0, import_node_path17.join)(root, entry.name),
19635
+ path: (0, import_node_path19.join)(root, entry.name),
18722
19636
  isDirectory: entry.isDirectory()
18723
19637
  }));
18724
19638
  return [{ surface, root, entries }];
@@ -18729,7 +19643,7 @@ function mmiPluginCacheRootSnapshots() {
18729
19643
  }
18730
19644
  function hasNestedMmiChild(versionDir) {
18731
19645
  try {
18732
- return (0, import_node_fs19.statSync)((0, import_node_path17.join)(versionDir, "mmi")).isDirectory();
19646
+ return (0, import_node_fs22.statSync)((0, import_node_path19.join)(versionDir, "mmi")).isDirectory();
18733
19647
  } catch {
18734
19648
  return false;
18735
19649
  }
@@ -18740,10 +19654,10 @@ function nestedPluginTreeSnapshot() {
18740
19654
  );
18741
19655
  }
18742
19656
  function uniqueQuarantineTarget(path2) {
18743
- if (!(0, import_node_fs19.existsSync)(path2)) return path2;
19657
+ if (!(0, import_node_fs22.existsSync)(path2)) return path2;
18744
19658
  for (let i = 1; i < 100; i += 1) {
18745
19659
  const candidate = `${path2}-${i}`;
18746
- if (!(0, import_node_fs19.existsSync)(candidate)) return candidate;
19660
+ if (!(0, import_node_fs22.existsSync)(candidate)) return candidate;
18747
19661
  }
18748
19662
  return `${path2}-${Date.now()}`;
18749
19663
  }
@@ -18752,10 +19666,10 @@ function quarantinePluginCacheDirs(plan2) {
18752
19666
  const failed = [];
18753
19667
  for (const move of plan2) {
18754
19668
  try {
18755
- if (!(0, import_node_fs19.existsSync)(move.from)) continue;
19669
+ if (!(0, import_node_fs22.existsSync)(move.from)) continue;
18756
19670
  const target = uniqueQuarantineTarget(move.to);
18757
- (0, import_node_fs19.mkdirSync)((0, import_node_path17.dirname)(target), { recursive: true });
18758
- (0, import_node_fs19.renameSync)(move.from, target);
19671
+ (0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(target), { recursive: true });
19672
+ (0, import_node_fs22.renameSync)(move.from, target);
18759
19673
  moved += 1;
18760
19674
  } catch {
18761
19675
  failed.push(move);
@@ -18774,23 +19688,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
18774
19688
  }
18775
19689
  async function clearNestedPluginTreeDir(targetPath) {
18776
19690
  try {
18777
- if (!(0, import_node_fs19.existsSync)(targetPath)) return true;
19691
+ if (!(0, import_node_fs22.existsSync)(targetPath)) return true;
18778
19692
  if (isWin) {
18779
- const emptyDir = (0, import_node_path17.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
18780
- (0, import_node_fs19.mkdirSync)(emptyDir, { recursive: true });
19693
+ const emptyDir = (0, import_node_path19.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
19694
+ (0, import_node_fs22.mkdirSync)(emptyDir, { recursive: true });
18781
19695
  try {
18782
19696
  await robocopyMirrorEmpty(emptyDir, targetPath);
18783
- (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
19697
+ (0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
18784
19698
  } finally {
18785
19699
  try {
18786
- (0, import_node_fs19.rmSync)(emptyDir, { recursive: true, force: true });
19700
+ (0, import_node_fs22.rmSync)(emptyDir, { recursive: true, force: true });
18787
19701
  } catch {
18788
19702
  }
18789
19703
  }
18790
- return !(0, import_node_fs19.existsSync)(targetPath);
19704
+ return !(0, import_node_fs22.existsSync)(targetPath);
18791
19705
  }
18792
- (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
18793
- return !(0, import_node_fs19.existsSync)(targetPath);
19706
+ (0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
19707
+ return !(0, import_node_fs22.existsSync)(targetPath);
18794
19708
  } catch {
18795
19709
  return false;
18796
19710
  }
@@ -18803,11 +19717,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
18803
19717
  }
18804
19718
  return true;
18805
19719
  }
18806
- var gitignorePath = () => (0, import_node_path17.join)(process.cwd(), ".gitignore");
19720
+ var gitignorePath = () => (0, import_node_path19.join)(process.cwd(), ".gitignore");
18807
19721
  function readTextFile(path2) {
18808
19722
  try {
18809
- if (!(0, import_node_fs19.existsSync)(path2)) return null;
18810
- return (0, import_node_fs19.readFileSync)(path2, "utf8");
19723
+ if (!(0, import_node_fs22.existsSync)(path2)) return null;
19724
+ return (0, import_node_fs22.readFileSync)(path2, "utf8");
18811
19725
  } catch {
18812
19726
  return null;
18813
19727
  }
@@ -18816,9 +19730,9 @@ function playwrightMcpConfigSnapshots() {
18816
19730
  const cwd = process.cwd();
18817
19731
  const home = (0, import_node_os6.homedir)();
18818
19732
  const candidates = [
18819
- (0, import_node_path17.join)(cwd, ".cursor", "mcp.json"),
18820
- (0, import_node_path17.join)(home, ".cursor", "mcp.json"),
18821
- (0, import_node_path17.join)(home, ".codex", "config.toml")
19733
+ (0, import_node_path19.join)(cwd, ".cursor", "mcp.json"),
19734
+ (0, import_node_path19.join)(home, ".cursor", "mcp.json"),
19735
+ (0, import_node_path19.join)(home, ".codex", "config.toml")
18822
19736
  ];
18823
19737
  const out = [];
18824
19738
  for (const path2 of candidates) {
@@ -18831,7 +19745,7 @@ function strayBrowserArtifactPaths() {
18831
19745
  const cwd = process.cwd();
18832
19746
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
18833
19747
  try {
18834
- return (0, import_node_fs19.existsSync)((0, import_node_path17.join)(cwd, rel));
19748
+ return (0, import_node_fs22.existsSync)((0, import_node_path19.join)(cwd, rel));
18835
19749
  } catch {
18836
19750
  return false;
18837
19751
  }
@@ -18839,14 +19753,14 @@ function strayBrowserArtifactPaths() {
18839
19753
  }
18840
19754
  function readGitignore() {
18841
19755
  try {
18842
- return (0, import_node_fs19.readFileSync)(gitignorePath(), "utf8");
19756
+ return (0, import_node_fs22.readFileSync)(gitignorePath(), "utf8");
18843
19757
  } catch {
18844
19758
  return null;
18845
19759
  }
18846
19760
  }
18847
19761
  function writeGitignore(content) {
18848
19762
  try {
18849
- (0, import_node_fs19.writeFileSync)(gitignorePath(), content, "utf8");
19763
+ (0, import_node_fs22.writeFileSync)(gitignorePath(), content, "utf8");
18850
19764
  return true;
18851
19765
  } catch {
18852
19766
  return false;
@@ -18885,7 +19799,7 @@ async function runDoctor(opts, io = consoleIo) {
18885
19799
  let onPath = pathProbe;
18886
19800
  if (!onPath) {
18887
19801
  const root = process.env.CLAUDE_PLUGIN_ROOT;
18888
- if (root && (0, import_node_fs19.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
19802
+ if (root && (0, import_node_fs22.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18889
19803
  }
18890
19804
  checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
18891
19805
  const surface = detectSurface(process.env);
@@ -18929,10 +19843,40 @@ async function runDoctor(opts, io = consoleIo) {
18929
19843
  if (!pluginCheck.ok && pluginCheck.recordToInsert && repairLocal) {
18930
19844
  if (writeProjectInstallRecord(pluginCheck.recordToInsert)) {
18931
19845
  pluginCheck = { ...pluginCheck, ok: true };
18932
- io.err(` \u21BB repaired: registered mmi@mmi project install record \u2014 ${reloadHint} to load MMI commands`);
19846
+ io.err(` \u21BB repaired: registered mmi@mutmutco project install record \u2014 ${reloadHint} to load MMI commands`);
18933
19847
  }
18934
19848
  }
18935
19849
  checks.push(pluginCheck);
19850
+ let legacyPluginCheck = buildLegacyPluginInstallCheck({
19851
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19852
+ sources: installedPluginSources(),
19853
+ surface
19854
+ });
19855
+ if (!legacyPluginCheck.ok && repairLocal) {
19856
+ const claudeLegacy = legacyPluginCheck.staleSurfaces?.includes("claude") ?? false;
19857
+ const codexLegacy = legacyPluginCheck.staleSurfaces?.includes("codex") ?? false;
19858
+ if (claudeLegacy && await applyClaudePluginHeal(surface, (m) => io.err(m), { force: true })) {
19859
+ legacyPluginCheck = buildLegacyPluginInstallCheck({
19860
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19861
+ sources: installedPluginSources(),
19862
+ surface
19863
+ });
19864
+ if (legacyPluginCheck.ok) {
19865
+ io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via claude plugin \u2014 ${reloadHint} to load MMI commands`);
19866
+ }
19867
+ }
19868
+ if (!legacyPluginCheck.ok && codexLegacy && await applyCodexPluginHeal(surface, (m) => io.err(m), { force: true })) {
19869
+ legacyPluginCheck = buildLegacyPluginInstallCheck({
19870
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19871
+ sources: installedPluginSources(),
19872
+ surface
19873
+ });
19874
+ if (legacyPluginCheck.ok) {
19875
+ io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via codex plugin \u2014 ${reloadHint} to load MMI commands`);
19876
+ }
19877
+ }
19878
+ }
19879
+ checks.push(legacyPluginCheck);
18936
19880
  let gitignoreCheck = buildGitignoreManagedBlockCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), content: readGitignore() });
18937
19881
  const gitignoreDecision = decideGitignoreRepair(gitignoreCheck, { repoWritesAllowed, repairFull });
18938
19882
  gitignoreCheck = gitignoreDecision.check;
@@ -18955,7 +19899,7 @@ async function runDoctor(opts, io = consoleIo) {
18955
19899
  if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
18956
19900
  if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
18957
19901
  driftCheck = { ...driftCheck, ok: true };
18958
- io.err(` \u21BB repaired: collapsed mmi@mmi to one user-scope entry (backup at installed_plugins.json.bak) \u2014 ${reloadHint} to load MMI commands`);
19902
+ io.err(` \u21BB repaired: collapsed mmi@mutmutco to one user-scope entry (backup at installed_plugins.json.bak) \u2014 ${reloadHint} to load MMI commands`);
18959
19903
  }
18960
19904
  }
18961
19905
  checks.push(driftCheck);
@@ -19057,7 +20001,7 @@ async function runDoctor(opts, io = consoleIo) {
19057
20001
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19058
20002
  surface,
19059
20003
  cacheRoot: cursorCacheRoot,
19060
- cacheRootExists: (0, import_node_fs19.existsSync)(cursorCacheRoot),
20004
+ cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
19061
20005
  pins: cursorPins,
19062
20006
  hubCheckout: hubCheckoutForCursorSeed(),
19063
20007
  releasedVersion
@@ -19068,7 +20012,7 @@ async function runDoctor(opts, io = consoleIo) {
19068
20012
  releasedVersion,
19069
20013
  hubCheckout: hubCheckoutForCursorSeed(),
19070
20014
  execFileP: execFileP2,
19071
- mkdtemp: (prefix) => (0, import_promises6.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
20015
+ mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path19.join)((0, import_node_os6.tmpdir)(), prefix)),
19072
20016
  log: (m) => io.err(m)
19073
20017
  });
19074
20018
  if (seeded) {
@@ -19077,7 +20021,7 @@ async function runDoctor(opts, io = consoleIo) {
19077
20021
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19078
20022
  surface,
19079
20023
  cacheRoot: cursorCacheRoot,
19080
- cacheRootExists: (0, import_node_fs19.existsSync)(cursorCacheRoot),
20024
+ cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
19081
20025
  pins: cursorPins,
19082
20026
  hubCheckout: hubCheckoutForCursorSeed(),
19083
20027
  releasedVersion
@@ -19116,6 +20060,38 @@ async function runDoctor(opts, io = consoleIo) {
19116
20060
  strayPaths: strayBrowserArtifactPaths()
19117
20061
  })
19118
20062
  );
20063
+ const dashboardConsumer = await resolveDashboardConsumer(cfg);
20064
+ const isDashboardConsumer = dashboardConsumer.isConsumer;
20065
+ const uiSnapshot = designSystemSnapshot(process.cwd());
20066
+ const uiLatestVersion = isDashboardConsumer && uiSnapshot.packageName ? await fetchUiPackageLatestVersion(uiSnapshot.packageName) : void 0;
20067
+ let designSystemCheck = dashboardConsumer.registryReadFailed ? buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildDesignSystemVersionCheck({
20068
+ ...uiSnapshot,
20069
+ isConsumerRepo: isDashboardConsumer,
20070
+ latestVersion: uiLatestVersion
20071
+ });
20072
+ if (!designSystemCheck.ok && (repairFull || repairLocal) && designSystemCheck.packageName) {
20073
+ designSystemCheck = await applyDesignSystemUpdate(designSystemCheck, (m) => io.err(m));
20074
+ if (designSystemCheck.ok) {
20075
+ io.err(` \u21BB updated ${designSystemCheck.packageName} \u2192 ${designSystemCheck.installedVersion ?? designSystemCheck.latestVersion ?? "latest"}`);
20076
+ }
20077
+ }
20078
+ checks.push(designSystemCheck);
20079
+ const registryTargetVersion = designSystemCheck.latestVersion ?? designSystemCheck.installedVersion ?? uiLatestVersion;
20080
+ let registryComponentsCheck = dashboardConsumer.registryReadFailed ? buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildRegistryComponentsCheck({
20081
+ ...await gatherRegistryComponentsState(process.cwd(), registryTargetVersion, { fetch }),
20082
+ isConsumerRepo: isDashboardConsumer
20083
+ });
20084
+ if (!registryComponentsCheck.ok && (repairFull || repairLocal) && repoWritesAllowed && registryComponentsCheck.components?.length) {
20085
+ registryComponentsCheck = await applyRegistryComponentsSyncCheck(
20086
+ registryComponentsCheck,
20087
+ registryTargetVersion,
20088
+ (m) => io.err(m)
20089
+ );
20090
+ if (registryComponentsCheck.ok) {
20091
+ io.err(` \u21BB synced ${registryComponentsCheck.components?.length ?? 0} registry component(s) \u2192 .mmi/design-system/components`);
20092
+ }
20093
+ }
20094
+ checks.push(registryComponentsCheck);
19119
20095
  const gaps = checks.filter((c) => !c.ok);
19120
20096
  if (opts.banner) {
19121
20097
  if (gaps.length) io.log(`\u26A0 MMI setup needed \u2014 ${gaps.map((g) => g.fix).join(" \xB7 ")} \xB7 guide: ${MMI_AGENTIC_ONBOARDING_GUIDE.url}`);
@@ -19144,7 +20120,82 @@ async function runDoctor(opts, io = consoleIo) {
19144
20120
  io.log(gaps.length ? `
19145
20121
  ${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
19146
20122
  }
19147
- program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install, Playwright MCP vision caps, browser artifact hygiene), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
20123
+ var designSystem = program2.command("design-system").description("@mutmutco UI npm package + registry component freshness for dashboard consumers (#1633, #1635)");
20124
+ designSystem.command("status").description("compare the installed @mutmutco/ui-dashboard (or legacy @mutmutco/ui) semver in this repo vs npm @latest").option("--json", "machine-readable output").option("--apply", "run npm update when behind (same as doctor --apply for this check)").action(async (opts) => {
20125
+ const cfg = await loadConfig();
20126
+ const dashboardConsumer = await resolveDashboardConsumer(cfg);
20127
+ if (dashboardConsumer.registryReadFailed) {
20128
+ const check2 = buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed);
20129
+ if (opts.json) {
20130
+ console.log(JSON.stringify(check2, null, 2));
20131
+ } else {
20132
+ console.log(`\u2717 ${check2.label}`);
20133
+ console.log(` fix: ${check2.fix}`);
20134
+ }
20135
+ process.exitCode = 1;
20136
+ return;
20137
+ }
20138
+ const isDashboardConsumer = dashboardConsumer.isConsumer;
20139
+ const snapshot = designSystemSnapshot(process.cwd());
20140
+ let check = buildDesignSystemVersionCheck({
20141
+ ...snapshot,
20142
+ isConsumerRepo: isDashboardConsumer,
20143
+ latestVersion: isDashboardConsumer && snapshot.packageName ? await fetchUiPackageLatestVersion(snapshot.packageName) : void 0
20144
+ });
20145
+ if (!check.ok && opts.apply && check.packageName) {
20146
+ check = await applyDesignSystemUpdate(check, (m) => console.error(m));
20147
+ }
20148
+ if (opts.json) {
20149
+ console.log(JSON.stringify(check, null, 2));
20150
+ process.exitCode = check.ok ? 0 : 1;
20151
+ return;
20152
+ }
20153
+ console.log(check.ok ? `\u2713 ${check.label}` : `\u2717 ${check.label}`);
20154
+ if (check.packageName) console.log(` package: ${check.packageName}`);
20155
+ if (check.installedVersion) console.log(` installed: ${check.installedVersion}`);
20156
+ if (check.latestVersion) console.log(` latest: ${check.latestVersion}`);
20157
+ if (!check.ok) console.log(` fix: ${check.fix}`);
20158
+ process.exitCode = check.ok ? 0 : 1;
20159
+ });
20160
+ designSystem.command("registry").description("compare .mmi/design-system/components cache vs the live @mutmutco registry").option("--json", "machine-readable output").option("--apply", "pull registry components into .mmi/ (same as doctor --apply for this check)").action(async (opts) => {
20161
+ const cfg = await loadConfig();
20162
+ const dashboardConsumer = await resolveDashboardConsumer(cfg);
20163
+ if (dashboardConsumer.registryReadFailed) {
20164
+ const check2 = buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed);
20165
+ if (opts.json) {
20166
+ console.log(JSON.stringify(check2, null, 2));
20167
+ } else {
20168
+ console.log(`\u2717 ${check2.label}`);
20169
+ console.log(` fix: ${check2.fix}`);
20170
+ }
20171
+ process.exitCode = 1;
20172
+ return;
20173
+ }
20174
+ const isDashboardConsumer = dashboardConsumer.isConsumer;
20175
+ const snapshot = designSystemSnapshot(process.cwd());
20176
+ const targetVersion = isDashboardConsumer && snapshot.packageName ? await fetchUiPackageLatestVersion(snapshot.packageName) : void 0;
20177
+ const state = await gatherRegistryComponentsState(process.cwd(), targetVersion, { fetch });
20178
+ let check = buildRegistryComponentsCheck({
20179
+ ...state,
20180
+ isConsumerRepo: isDashboardConsumer
20181
+ });
20182
+ if (!check.ok && opts.apply && check.components?.length) {
20183
+ check = await applyRegistryComponentsSyncCheck(check, targetVersion, (m) => console.error(m));
20184
+ }
20185
+ if (opts.json) {
20186
+ console.log(JSON.stringify(check, null, 2));
20187
+ process.exitCode = check.ok ? 0 : 1;
20188
+ return;
20189
+ }
20190
+ console.log(check.ok ? `\u2713 ${check.label}` : `\u2717 ${check.label}`);
20191
+ if (check.components?.length) console.log(` components: ${check.components.join(", ")}`);
20192
+ if (check.cacheVersion) console.log(` cache version: ${check.cacheVersion}`);
20193
+ if (check.targetVersion) console.log(` target version: ${check.targetVersion}`);
20194
+ if (check.staleComponents?.length) console.log(` stale: ${check.staleComponents.join(", ")}`);
20195
+ if (!check.ok) console.log(` fix: ${check.fix}`);
20196
+ process.exitCode = check.ok ? 0 : 1;
20197
+ });
20198
+ program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install, @mutmutco UI npm package freshness, @mutmutco registry component cache, Playwright MCP vision caps, browser artifact hygiene), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
19148
20199
  // Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
19149
20200
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
19150
20201
  ));
@@ -19159,7 +20210,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
19159
20210
  } catch (e) {
19160
20211
  console.error(`[mmi-hook] saga session failed: ${e.message}`);
19161
20212
  }
19162
- spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
20213
+ spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19163
20214
  let northstarInjected = false;
19164
20215
  const { parallel, sequential } = buildSessionStartPlan({
19165
20216
  rulesSync: (io) => runRulesSync({ quiet: true }, io),
@@ -19201,7 +20252,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
19201
20252
  for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
19202
20253
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
19203
20254
  if (worktreeBanner) {
19204
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
20255
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19205
20256
  consoleIo.log(worktreeBanner);
19206
20257
  }
19207
20258
  });