@mutmutco/cli 2.39.0 → 2.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/main.cjs +2119 -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,40 @@ 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 logLinePayload(line) {
12596
+ const z = line.lastIndexOf("Z ");
12597
+ if (z >= 0) return line.slice(z + 2);
12598
+ return line;
12599
+ }
12600
+ function extractControlOutputFromLog(log) {
12601
+ const lines = log.split(/\r?\n/);
12602
+ const start = lines.findIndex((l) => logLinePayload(l).trim() === OUTPUT_BEGIN);
12603
+ if (start < 0) return "";
12604
+ const end = lines.findIndex((l, i) => i > start && logLinePayload(l).trim() === OUTPUT_END);
12605
+ const slice = end < 0 ? lines.slice(start + 1) : lines.slice(start + 1, end);
12606
+ return slice.map(logLinePayload).join("\n").trim();
12607
+ }
12608
+ function parseStatusSnippet(stdout) {
12609
+ const t = stdout.toLowerCase();
12610
+ const m = t.match(/service[:=]\s*(running|stopped|missing|up|down|absent)/);
12611
+ if (!m) return { serviceState: "unknown" };
12612
+ const token = m[1];
12613
+ if (token === "running" || token === "up") return { serviceState: "running" };
12614
+ if (token === "stopped" || token === "down") return { serviceState: "stopped" };
12615
+ return { serviceState: "missing" };
12616
+ }
12617
+ function parseVerifySecrets(stdout) {
12618
+ const out = [];
12619
+ for (const line of stdout.split("\n")) {
12620
+ const m = /^(\S+):\s*(match|mismatch|missing)\b/.exec(line.trim());
12621
+ if (m) out.push({ key: m[1], status: m[2] });
12622
+ }
12623
+ return out;
12624
+ }
12625
+
11898
12626
  // src/train-apply.ts
11899
12627
  function resolveDeployModel2(meta, repo) {
11900
12628
  const m = meta?.deployModel;
@@ -11969,7 +12697,8 @@ var ORG_SPINE_FILES = [
11969
12697
  ".claude/settings.json",
11970
12698
  ".claude/output-styles/mmi-plain.md",
11971
12699
  ".cursor/rules/mmi-plain-language.mdc",
11972
- ".cursor/rules/mmi-tool-economy.mdc"
12700
+ ".cursor/rules/mmi-tool-economy.mdc",
12701
+ ".cursor/rules/mmi-code-economy.mdc"
11973
12702
  ];
11974
12703
  function isSpinePath(path2) {
11975
12704
  return ORG_SPINE_FILES.includes(path2);
@@ -12101,53 +12830,23 @@ function requireProjectMetaForTrain(load, repo) {
12101
12830
  var CORRELATE_ATTEMPTS = 5;
12102
12831
  var CORRELATE_DELAY_MS = 1500;
12103
12832
  var CORRELATE_SKEW_SLACK_MS = 1e4;
12833
+ var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12834
+ function resolveSleep(deps) {
12835
+ return deps.sleep ?? defaultSleep2;
12836
+ }
12104
12837
  var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion}]";
12105
12838
  var TRAIN_COMMIT_STATUS_JQ = "[.statuses[]|{context:.context,state:.state}]";
12106
12839
  var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
12107
12840
  var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
12108
12841
  var TRAIN_CHECK_ATTEMPTS = 40;
12109
12842
  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)));
12843
+ async function correlateRun(deps, args) {
12844
+ const sleep2 = resolveSleep(deps);
12146
12845
  const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
12147
12846
  let lastError;
12148
12847
  let parsedAnyResponse = false;
12149
12848
  for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
12150
- if (attempt > 0) await sleep(CORRELATE_DELAY_MS);
12849
+ if (attempt > 0) await sleep2(CORRELATE_DELAY_MS);
12151
12850
  const listArgs = [
12152
12851
  "run",
12153
12852
  "list",
@@ -12155,28 +12854,43 @@ async function correlateWorkflowRun(deps, args) {
12155
12854
  HUB_REPO3,
12156
12855
  "--workflow",
12157
12856
  args.workflow,
12158
- "--event",
12159
- args.event,
12160
- ...args.branch ? ["--branch", args.branch] : [],
12857
+ ...args.mode === "workflow" ? ["--event", args.event] : [],
12858
+ ...args.mode === "workflow" && args.branch ? ["--branch", args.branch] : [],
12161
12859
  "--limit",
12162
12860
  "10",
12163
12861
  "--json",
12164
- "databaseId,url,event,createdAt,status,conclusion,headSha"
12862
+ args.mode === "dispatch" ? "databaseId,url,event,createdAt" : "databaseId,url,event,createdAt,status,conclusion,headSha"
12165
12863
  ];
12166
12864
  let rows;
12167
12865
  try {
12168
12866
  rows = JSON.parse(await deps.run("gh", listArgs));
12169
12867
  parsedAnyResponse = true;
12170
12868
  } catch {
12171
- lastError = new Error(`could not list ${args.workflow} runs`);
12869
+ if (args.mode === "workflow") lastError = new Error(`could not list ${args.workflow} runs`);
12172
12870
  continue;
12173
12871
  }
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];
12872
+ const match = rows.filter((r) => {
12873
+ if (typeof r.databaseId !== "number") return false;
12874
+ if (args.mode === "dispatch") return r.event === "workflow_dispatch";
12875
+ return r.event === args.event && r.headSha === args.headSha;
12876
+ }).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
12877
  if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
12176
12878
  }
12177
- if (!parsedAnyResponse && lastError) throw lastError;
12879
+ if (args.mode === "workflow" && !parsedAnyResponse && lastError) throw lastError;
12178
12880
  return {};
12179
12881
  }
12882
+ function correlateTenantRun(deps, since) {
12883
+ return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch" });
12884
+ }
12885
+ function correlatePublishRun(deps, since) {
12886
+ return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
12887
+ }
12888
+ function correlateControlRun(deps, since) {
12889
+ return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch" });
12890
+ }
12891
+ async function correlateWorkflowRun(deps, args) {
12892
+ return correlateRun(deps, { ...args, mode: "workflow" });
12893
+ }
12180
12894
  async function watchTenantRun(deps, runId) {
12181
12895
  if (runId == null) return "pending";
12182
12896
  try {
@@ -12186,6 +12900,13 @@ async function watchTenantRun(deps, runId) {
12186
12900
  return "failure";
12187
12901
  }
12188
12902
  }
12903
+ async function fetchControlRunLog(deps, runId) {
12904
+ try {
12905
+ return await deps.run("gh", ["run", "view", String(runId), "--repo", HUB_REPO3, "--log"]);
12906
+ } catch {
12907
+ return "";
12908
+ }
12909
+ }
12189
12910
  async function watchWorkflowRun(deps, workflow, run) {
12190
12911
  if (run.runId == null) return { workflow, conclusion: "pending" };
12191
12912
  const conclusion = await watchTenantRun(deps, run.runId);
@@ -12286,11 +13007,11 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
12286
13007
  if (required.length === 0) {
12287
13008
  return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
12288
13009
  }
12289
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
13010
+ const sleep2 = resolveSleep(deps);
12290
13011
  let lastStatus = "not checked";
12291
13012
  let lastError;
12292
13013
  for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
12293
- if (attempt > 0) await sleep(TRAIN_CHECK_DELAY_MS);
13014
+ if (attempt > 0) await sleep2(TRAIN_CHECK_DELAY_MS);
12294
13015
  let checkRuns;
12295
13016
  let statuses;
12296
13017
  try {
@@ -12366,18 +13087,77 @@ function isTransientDispatchFailure(e) {
12366
13087
  return /timed? ?out|timeout|aborted|network|fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(msg);
12367
13088
  }
12368
13089
  async function dispatchTenantDeployWithRetry(deps, input) {
12369
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
13090
+ const sleep2 = resolveSleep(deps);
12370
13091
  for (let attempt = 1; ; attempt++) {
12371
13092
  try {
12372
13093
  await deps.dispatchTenantDeploy(input);
12373
13094
  return;
12374
13095
  } catch (e) {
12375
13096
  if (attempt >= DISPATCH_ATTEMPTS || !isTransientDispatchFailure(e)) throw e;
12376
- await sleep(DISPATCH_RETRY_DELAY_MS * attempt);
13097
+ await sleep2(DISPATCH_RETRY_DELAY_MS * attempt);
12377
13098
  }
12378
13099
  }
12379
13100
  }
12380
- async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw") {
13101
+ function tenantPublishRecoveryCommand(slug, repo, ref, stage2, publishDir) {
13102
+ const parts = [
13103
+ `gh workflow run tenant-publish.yml --repo ${HUB_REPO3}`,
13104
+ `-f slug=${slug}`,
13105
+ `-f repo=${repo}`,
13106
+ `-f ref=${ref}`,
13107
+ `-f stage=${stage2}`
13108
+ ];
13109
+ if (publishDir && publishDir !== ".") parts.push(`-f publishDir=${publishDir}`);
13110
+ return parts.join(" ");
13111
+ }
13112
+ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure = "throw", publishDir) {
13113
+ const since = (deps.now ?? Date.now)();
13114
+ const dispatchArgs = [
13115
+ "workflow",
13116
+ "run",
13117
+ "tenant-publish.yml",
13118
+ "--repo",
13119
+ HUB_REPO3,
13120
+ "-f",
13121
+ `slug=${ctx.slug}`,
13122
+ "-f",
13123
+ `repo=${ctx.repo}`,
13124
+ "-f",
13125
+ `ref=${ref}`,
13126
+ "-f",
13127
+ `stage=${stage2}`
13128
+ ];
13129
+ if (publishDir && publishDir !== ".") dispatchArgs.push("-f", `publishDir=${publishDir}`);
13130
+ try {
13131
+ await deps.run("gh", dispatchArgs);
13132
+ } catch (e) {
13133
+ if (dispatchFailure === "throw") throw e;
13134
+ const msg = e instanceof Error ? e.message : String(e);
13135
+ const recovery = tenantPublishRecoveryCommand(ctx.slug, ctx.repo, ref, stage2, publishDir);
13136
+ return {
13137
+ note: `tenant-publish dispatch FAILED: ${msg}. The promotion itself landed \u2014 recover with \`${recovery}\``,
13138
+ deployStatus: "failure"
13139
+ };
13140
+ }
13141
+ const { runId, runUrl } = await correlatePublishRun(deps, since);
13142
+ const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
13143
+ return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
13144
+ }
13145
+ async function dispatchPublishIfRequired(deps, ctx, meta, model, stage2, publishRef, watch, dispatchFailure) {
13146
+ if (!meta.publishRequired || stage2 !== "main") return null;
13147
+ if (model !== "tenant-container" && model !== "solo-container") return null;
13148
+ return dispatchTenantPublish(deps, ctx, stage2, publishRef, watch, dispatchFailure, meta.publishDir);
13149
+ }
13150
+ function appendPublishDispatch(deploy, publish) {
13151
+ if (!publish) return deploy;
13152
+ return {
13153
+ note: `${deploy.note}; ${publish.note}`,
13154
+ runId: deploy.runId,
13155
+ runUrl: deploy.runUrl,
13156
+ workflowRuns: [...deploy.workflowRuns ?? [], ...publish.workflowRuns ?? [{ workflow: "tenant-publish.yml", runId: publish.runId, runUrl: publish.runUrl, conclusion: publish.deployStatus }]],
13157
+ deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
13158
+ };
13159
+ }
13160
+ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
12381
13161
  if (model === "tenant-container" || model === "solo-container") {
12382
13162
  const since = (deps.now ?? Date.now)();
12383
13163
  try {
@@ -12395,25 +13175,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
12395
13175
  return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
12396
13176
  }
12397
13177
  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 };
13178
+ return dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure, publishDir);
12417
13179
  }
12418
13180
  if (model === "hub-serverless") {
12419
13181
  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 +13217,99 @@ async function preflight(deps, ctx, stage2, meta) {
12455
13217
  await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
12456
13218
  return model;
12457
13219
  }
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 };
13220
+ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix, realignMessage) {
13221
+ const foldPaths = await resolveFoldPaths(deps, deployModel);
13222
+ const tolerated = [...foldPaths, ...RELEASE_TOLERATED_PATHS];
13223
+ const predicted = await predictMergeConflicts(deps, "origin/main", remoteRef);
13224
+ const predictedBlocking = predicted.filter((f) => !isSpinePath(f) && !tolerated.includes(f));
13225
+ if (predictedBlocking.length > 0) {
13226
+ throw new Error(`${blockingPrefix}: ${predictedBlocking.join(", ")} \u2014 no merge was started. ${realignMessage}`);
12492
13227
  }
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 }
13228
+ return { foldPaths, tolerated, predicted };
13229
+ }
13230
+ async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted) {
13231
+ await deps.run("git", ["checkout", "main"]);
13232
+ await ffOnlyPull(deps, "main");
13233
+ if (predicted.length === 0) {
13234
+ await deps.run("git", ["merge", sourceRef, "--no-edit"]);
13235
+ } else {
13236
+ await mergeWithSpineResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
13237
+ }
13238
+ }
13239
+ async function mergeSourceToMain(deps, deployModel, args) {
13240
+ const { foldPaths, tolerated, predicted } = await preflightMergeToMain(
13241
+ deps,
13242
+ deployModel,
13243
+ args.remoteRef,
13244
+ args.blockingPrefix,
13245
+ args.realignMessage
13246
+ );
13247
+ await executeMergeToMain(deps, args.sourceRef, args.mergeLabel, tolerated, predicted);
13248
+ return { foldPaths };
13249
+ }
13250
+ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
13251
+ await ensureTagPushed(deps, tag, releaseSha);
13252
+ const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
13253
+ const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
13254
+ await deps.run("git", ["push", "origin", "main"]);
13255
+ const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
13256
+ await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
13257
+ const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
13258
+ const autoRunSince = (deps.now ?? Date.now)();
13259
+ const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
13260
+ const publishDispatch = deployDispatch.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report");
13261
+ let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
13262
+ if (!publishDispatch && deployDispatch.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
13263
+ dispatch = {
13264
+ ...dispatch,
13265
+ note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
12552
13266
  };
12553
13267
  }
12554
- if (command === "release" && options.dev) {
13268
+ return { checks, releaseUrl, announceNote, dispatch };
13269
+ }
13270
+ async function pushRcAlignment(deps) {
13271
+ try {
13272
+ await deps.run("git", ["push", "origin", "main:rc"]);
13273
+ return "origin/rc aligned to the released main";
13274
+ } catch (e) {
13275
+ return `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
13276
+ }
13277
+ }
13278
+ async function runTrainApplyPipeline(mode, input) {
13279
+ const { deps, ctx, command, meta, branchHints, watch, options } = input;
13280
+ const directTrack = input.directTrack ?? false;
13281
+ if (mode === "rcand") {
13282
+ await requireBranch(deps, "development");
13283
+ if (directTrack) {
13284
+ throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
13285
+ }
13286
+ await ffOnlyPull(deps, "development");
13287
+ ensurePositiveCount(
13288
+ await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
13289
+ "nothing to promote: origin/development is not ahead of origin/rc"
13290
+ );
13291
+ const deployModel2 = await preflight(deps, ctx, "rc", meta);
13292
+ const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
13293
+ await deps.run("git", ["checkout", "rc"]);
13294
+ await ffOnlyPull(deps, "rc");
13295
+ await deps.run("git", ["merge", "development", "--no-edit"]);
13296
+ const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
13297
+ const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
13298
+ const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
13299
+ const resumeNote = resume.tag ? resume.note : void 0;
13300
+ await ensureTagPushed(deps, tag2, rcSha);
13301
+ const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
13302
+ const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
13303
+ const autoRunSince = (deps.now ?? Date.now)();
13304
+ await deps.run("git", ["push", "origin", "rc"]);
13305
+ const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
13306
+ 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 };
13307
+ }
13308
+ if (mode === "release-dev") {
12555
13309
  await requireBranch(deps, "development");
12556
13310
  await ffOnlyPull(deps, "development");
12557
13311
  const hasRcBranch = branchHints.hasRcBranch ?? false;
12558
- if (hasRcBranch) {
13312
+ if (!directTrack && hasRcBranch) {
12559
13313
  const rcOnlyOut = (await deps.run("git", ["rev-list", "--count", "--right-only", "--cherry-pick", "--no-merges", "origin/development...origin/rc"])).trim();
12560
13314
  const rcOnly = Number.parseInt(rcOnlyOut, 10);
12561
13315
  if (!Number.isFinite(rcOnly)) throw new Error(`release --dev: could not count rc-only commits for the guard: ${rcOnlyOut || "(empty)"}`);
@@ -12571,47 +13325,44 @@ async function runTrainApply(command, deps, options = {}) {
12571
13325
  );
12572
13326
  const deployModel2 = await preflight(deps, ctx, "main", meta);
12573
13327
  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
- }
13328
+ const rcShaAtRelease = !directTrack && hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
13329
+ const { foldPaths: foldPaths2 } = await mergeSourceToMain(deps, deployModel2, {
13330
+ sourceRef: "development",
13331
+ remoteRef: "origin/development",
13332
+ mergeLabel: "development -> main",
13333
+ blockingPrefix: "development -> main merge would conflict on non-spine path(s)",
13334
+ realignMessage: "The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
13335
+ });
12591
13336
  const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
12592
13337
  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" };
13338
+ const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
12603
13339
  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";
13340
+ if (directTrack) {
13341
+ return {
13342
+ ...ctx,
13343
+ command,
13344
+ stage: "main",
13345
+ ref: "main",
13346
+ tag: tag2,
13347
+ deployModel: deployModel2,
13348
+ promoted: true,
13349
+ checks: checks2,
13350
+ versionFold: versionFold2,
13351
+ dispatch: d2.note,
13352
+ runId: d2.runId,
13353
+ runUrl: d2.runUrl,
13354
+ workflowRuns: d2.workflowRuns,
13355
+ deployStatus: d2.deployStatus,
13356
+ rcRetirement: "not-applicable",
13357
+ rcRetirementNote: "direct-track release skips rc; no rc runtime to retire",
13358
+ devRollForward: devRollForward2,
13359
+ announceNote: announceNote2,
13360
+ devNote: options.dev ? "--dev is a no-op on a direct-track repo \u2014 it already releases development -> main" : void 0,
13361
+ release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
13362
+ };
12614
13363
  }
13364
+ const retirement2 = hasRcBranch ? await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease) : { status: "not-applicable", note: "no origin/rc branch \u2014 rc runtime retirement skipped" };
13365
+ const rcAlignment2 = hasRcBranch ? await pushRcAlignment(deps) : "no origin/rc branch \u2014 rc alignment skipped";
12615
13366
  const environments2 = await buildEnvironments(deps, ctx, deployModel2, d2.deployStatus, retirement2);
12616
13367
  return {
12617
13368
  ...ctx,
@@ -12645,15 +13396,13 @@ async function runTrainApply(command, deps, options = {}) {
12645
13396
  "nothing to release: origin/rc is not ahead of origin/main"
12646
13397
  );
12647
13398
  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
- }
13399
+ const { foldPaths, tolerated, predicted } = await preflightMergeToMain(
13400
+ deps,
13401
+ deployModel,
13402
+ "origin/rc",
13403
+ "rc -> main merge would conflict on non-spine path(s)",
13404
+ "The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release."
13405
+ );
12657
13406
  const coverage = deps.hotfixCoverage({ mainRef: "origin/main", rcRef: "origin/rc", ack: options.ack ?? [] });
12658
13407
  if (!coverage.ok) {
12659
13408
  const list = coverage.uncovered.map((c) => `${c.sha.slice(0, 8)} ${c.subject}`).join("; ");
@@ -12662,34 +13411,14 @@ async function runTrainApply(command, deps, options = {}) {
12662
13411
  );
12663
13412
  }
12664
13413
  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
- }
13414
+ await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted);
12672
13415
  const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
12673
13416
  const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
12674
13417
  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");
13418
+ const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
12684
13419
  const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
12685
13420
  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
- }
13421
+ const rcAlignment = await pushRcAlignment(deps);
12693
13422
  const environments = await buildEnvironments(deps, ctx, deployModel, d.deployStatus, retirement);
12694
13423
  return {
12695
13424
  ...ctx,
@@ -12716,6 +13445,29 @@ async function runTrainApply(command, deps, options = {}) {
12716
13445
  environments
12717
13446
  };
12718
13447
  }
13448
+ async function runTrainApply(command, deps, options = {}) {
13449
+ const watch = options.watch ?? false;
13450
+ const ctx = await buildTrainApplyContext(deps);
13451
+ await requireCleanTree(deps);
13452
+ await deps.run("git", ["fetch", "origin"]);
13453
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
13454
+ const branchHints = await loadReleaseTrackBranchHints(deps);
13455
+ const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
13456
+ const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options };
13457
+ if (command === "rcand") {
13458
+ if (directTrack) {
13459
+ throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli release --apply` from development instead of rcand");
13460
+ }
13461
+ return runTrainApplyPipeline("rcand", pipelineInput);
13462
+ }
13463
+ if (directTrack) {
13464
+ return runTrainApplyPipeline("release-dev", { ...pipelineInput, directTrack: true });
13465
+ }
13466
+ if (command === "release" && options.dev) {
13467
+ return runTrainApplyPipeline("release-dev", pipelineInput);
13468
+ }
13469
+ return runTrainApplyPipeline("release-full", pipelineInput);
13470
+ }
12719
13471
  async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
12720
13472
  if (model !== "tenant-container") return void 0;
12721
13473
  const domains = deps.fetchEdgeDomains ? await deps.fetchEdgeDomains(ctx.slug).catch(() => null) : null;
@@ -12732,63 +13484,25 @@ async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
12732
13484
  if (rcDomains?.length) rc.domains = rcDomains;
12733
13485
  return { main, rc };
12734
13486
  }
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
13487
  var RETIRE_MAX_ATTEMPTS = 3;
12763
13488
  var RETIRE_BACKOFF_MS = 1500;
12764
13489
  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: {
13490
+ const r = await runTenantControl(deps, { repo: ctx.repo, stage: "rc", action: "retire", watch: true });
13491
+ if (r.category === "retired") {
13492
+ return {
12782
13493
  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 };
13494
+ category: "retired",
13495
+ note: `rc runtime retired (tenant-control.yml${r.runUrl ? `, ${r.runUrl}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
13496
+ };
13497
+ }
13498
+ if (r.category === "wait-timeout") {
13499
+ return {
13500
+ status: "failed",
13501
+ category: "wait-timeout",
13502
+ 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})` : ""}`
13503
+ };
12791
13504
  }
13505
+ return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
12792
13506
  }
12793
13507
  async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
12794
13508
  if (model !== "tenant-container") {
@@ -12812,19 +13526,18 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
12812
13526
  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
13527
  };
12814
13528
  }
12815
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
13529
+ const sleep2 = resolveSleep(deps);
12816
13530
  let last;
12817
13531
  for (let attempt = 1; attempt <= RETIRE_MAX_ATTEMPTS; attempt++) {
12818
13532
  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);
13533
+ if (last.status === "retired") return last;
13534
+ if (last.category !== "transport-failed" || attempt === RETIRE_MAX_ATTEMPTS) break;
13535
+ await sleep2(RETIRE_BACKOFF_MS * attempt);
12823
13536
  }
12824
13537
  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 };
13538
+ if (f.category === "wait-timeout") return f;
13539
+ 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)`;
13540
+ return { status: "failed", category: f.category, note };
12828
13541
  } catch (e) {
12829
13542
  const err = e;
12830
13543
  return { status: "failed", category: "transport-failed", note: `rc retirement failed (the release itself succeeded): ${err.message}` };
@@ -12850,14 +13563,82 @@ async function runTenantRedeploy(deps, options) {
12850
13563
  const d = await dispatchDeploy(deps, ctx, stage2, ref, deployModel, watch);
12851
13564
  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
13565
  }
13566
+ function tenantControlWatches(action) {
13567
+ return action === "status" || action === "retire" || action === "verify-secrets";
13568
+ }
13569
+ async function runTenantControl(deps, options) {
13570
+ const { repo, stage: stage2, action } = options;
13571
+ const watch = options.watch ?? tenantControlWatches(action);
13572
+ const base2 = { command: "tenant-control", repo, stage: stage2, action };
13573
+ const since = (deps.now ?? Date.now)();
13574
+ const d = await deps.dispatchTenantControl({ repo, stage: stage2, action });
13575
+ if (!d.ok) {
13576
+ const transport = d.category === "transport-failed";
13577
+ return {
13578
+ ...base2,
13579
+ dispatched: false,
13580
+ conclusion: "failure",
13581
+ category: action === "retire" ? transport ? "transport-failed" : "dispatch-rejected" : void 0,
13582
+ note: transport ? `tenant control ${action} dispatch failed (transport) \u2014 safe to retry` : `tenant control ${action} rejected: ${d.error ?? "request rejected by the Hub"}`
13583
+ };
13584
+ }
13585
+ const { runId, runUrl } = await correlateControlRun(deps, since);
13586
+ const conclusion = watch ? await watchTenantRun(deps, runId) : "pending";
13587
+ const result = { ...base2, dispatched: true, runId, runUrl, conclusion, note: "" };
13588
+ if (action === "retire") {
13589
+ result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
13590
+ }
13591
+ if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets")) {
13592
+ const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
13593
+ if (action === "status") {
13594
+ result.serviceState = parseStatusSnippet(output).serviceState;
13595
+ } else {
13596
+ result.secrets = parseVerifySecrets(output);
13597
+ }
13598
+ }
13599
+ 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`;
13600
+ return result;
13601
+ }
13602
+ function renderTenantControl(r) {
13603
+ const head = `tenant control ${r.repo} ${r.stage} ${r.action}: ${r.conclusion}${r.category ? ` (${r.category})` : ""}`;
13604
+ const lines = [head];
13605
+ if (r.runUrl) lines.push(` run: ${r.runUrl}`);
13606
+ if (r.serviceState) lines.push(` serviceState: ${r.serviceState}`);
13607
+ if (r.secrets?.length) {
13608
+ for (const s of r.secrets) lines.push(` ${s.key}: ${s.status}`);
13609
+ }
13610
+ lines.push(` ${r.note}`);
13611
+ return lines.join("\n");
13612
+ }
13613
+
13614
+ // src/tenant-verify-secrets.ts
13615
+ function renderVerifySecrets(body) {
13616
+ const secrets = body?.secrets ?? [];
13617
+ const counts = {
13618
+ match: secrets.filter((s) => s.status === "match").length,
13619
+ mismatch: secrets.filter((s) => s.status === "mismatch").length,
13620
+ missing: secrets.filter((s) => s.status === "missing").length
13621
+ };
13622
+ const lines = secrets.map((s) => `${s.key}: ${s.status}`);
13623
+ lines.push(`verify-secrets: ${counts.match} match, ${counts.mismatch} mismatch, ${counts.missing} missing`);
13624
+ const ssmStatus = body?.ssmStatus ?? "pending";
13625
+ if (ssmStatus !== "Success") {
13626
+ return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
13627
+ }
13628
+ const bad = counts.mismatch + counts.missing;
13629
+ if (bad > 0) {
13630
+ return { lines, failure: `${bad} of ${secrets.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
13631
+ }
13632
+ return { lines, failure: null };
13633
+ }
12853
13634
 
12854
13635
  // src/hotfix-coverage.ts
12855
- var import_node_child_process9 = require("node:child_process");
13636
+ var import_node_child_process10 = require("node:child_process");
12856
13637
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
12857
13638
  function checkHotfixCoverage(options = {}) {
12858
13639
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
12859
13640
  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"] }));
13641
+ 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
13642
  const revList = (range) => {
12862
13643
  const out = git(["rev-list", "--no-merges", range]).trim();
12863
13644
  return out ? out.split("\n") : [];
@@ -12984,6 +13765,10 @@ function renderSweep(r) {
12984
13765
  if (r.running > 0 && !r.retireAttempted) {
12985
13766
  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
13767
  }
13768
+ const undetermined = r.stages.filter((s) => s.serviceState === "unknown").length;
13769
+ if (undetermined > 0) {
13770
+ 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.`);
13771
+ }
12987
13772
  return lines.join("\n");
12988
13773
  }
12989
13774
 
@@ -13017,7 +13802,7 @@ function hotfixBranch(tag) {
13017
13802
  }
13018
13803
  async function resolveHotfixDeployModel(deps, ctx) {
13019
13804
  const load = await loadProjectMeta(deps, ctx);
13020
- const meta = load.status === "ok" ? load.meta : null;
13805
+ const meta = requireProjectMetaForTrain(load, ctx.repo);
13021
13806
  return resolveDeployModel2(meta, ctx.repo);
13022
13807
  }
13023
13808
  async function findHotfixPr(deps, ctx, tag) {
@@ -13132,9 +13917,9 @@ Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
13132
13917
  return { ...ctx, command: "hotfix-start", tag, version, branch, source: label, prUrl, reused: Boolean(remoteBranch), notes };
13133
13918
  }
13134
13919
  async function watchReleaseRun(deps, ctx, workflow, sha) {
13135
- const sleep = sleeper(deps);
13920
+ const sleep2 = sleeper(deps);
13136
13921
  for (let attempt = 0; attempt < HOTFIX_RUN_FIND_ATTEMPTS; attempt++) {
13137
- if (attempt > 0) await sleep(HOTFIX_RUN_FIND_DELAY_MS);
13922
+ if (attempt > 0) await sleep2(HOTFIX_RUN_FIND_DELAY_MS);
13138
13923
  let rows;
13139
13924
  try {
13140
13925
  const out = await deps.run("gh", [
@@ -13209,8 +13994,9 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13209
13994
  runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
13210
13995
  }
13211
13996
  deployNote = "watched release-triggered deploy.yml + publish.yml";
13212
- } else if (deployModel === "tenant-container" || deployModel === "solo-container") {
13213
- const dispatch = await dispatchDeploy(
13997
+ } else if (deployModel === "tenant-container" || deployModel === "solo-container" || deployModel === "registry-publish") {
13998
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
13999
+ const deploy = await dispatchDeploy(
13214
14000
  deps,
13215
14001
  ctx,
13216
14002
  "main",
@@ -13219,14 +14005,38 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13219
14005
  true,
13220
14006
  (deps.now ?? Date.now)(),
13221
14007
  mergedSha,
13222
- "report"
14008
+ "report",
14009
+ meta.publishDir
13223
14010
  );
14011
+ const publish = deploy.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report");
14012
+ let dispatch = appendPublishDispatch(deploy, publish);
14013
+ if (!publish && deploy.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
14014
+ dispatch = {
14015
+ ...dispatch,
14016
+ note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
14017
+ };
14018
+ }
13224
14019
  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
- });
14020
+ if (deployModel !== "registry-publish") {
14021
+ runs.push({
14022
+ workflow: "tenant-deploy.yml",
14023
+ url: deploy.runUrl,
14024
+ conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
14025
+ });
14026
+ }
14027
+ if (publish?.runUrl) {
14028
+ runs.push({
14029
+ workflow: "tenant-publish.yml",
14030
+ url: publish.runUrl,
14031
+ conclusion: publish.deployStatus === "success" ? "success" : publish.deployStatus === "failure" ? "failure" : publish.deployStatus ?? "pending"
14032
+ });
14033
+ } else if (deployModel === "registry-publish") {
14034
+ runs.push({
14035
+ workflow: "tenant-publish.yml",
14036
+ url: deploy.runUrl,
14037
+ conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
14038
+ });
14039
+ }
13230
14040
  } else {
13231
14041
  deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
13232
14042
  }
@@ -13237,7 +14047,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13237
14047
  try {
13238
14048
  await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
13239
14049
  const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
13240
- const sleep = sleeper(deps);
14050
+ const sleep2 = sleeper(deps);
13241
14051
  let attempt = 0;
13242
14052
  for (; ; ) {
13243
14053
  attempt++;
@@ -13246,7 +14056,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13246
14056
  break;
13247
14057
  } catch (err) {
13248
14058
  if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
13249
- await sleep(HOTFIX_VERIFY_RETRY_MS);
14059
+ await sleep2(HOTFIX_VERIFY_RETRY_MS);
13250
14060
  }
13251
14061
  }
13252
14062
  const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
@@ -13295,6 +14105,7 @@ async function runHotfixStatus(deps, versionInput) {
13295
14105
  await deps.run("git", ["fetch", "origin", "--tags"]);
13296
14106
  let tag;
13297
14107
  let version;
14108
+ let warnings = [];
13298
14109
  if (versionInput) {
13299
14110
  ({ tag, version } = normalizeHotfixVersion(versionInput));
13300
14111
  } else {
@@ -13303,18 +14114,19 @@ async function runHotfixStatus(deps, versionInput) {
13303
14114
  const latestFacts = await gatherHotfixFacts(deps, ctx, latest, latest.slice(1));
13304
14115
  const latestDerived = deriveHotfixState(latestFacts);
13305
14116
  if (latestDerived.state !== "complete") {
13306
- return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived };
14117
+ return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived, warnings };
13307
14118
  }
13308
14119
  }
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) };
14120
+ const found = await findInFlightHotfixVersion(deps, ctx, latest);
14121
+ warnings = supersededHotfixWarnings(found.superseded, latest);
14122
+ if (found.inFlight) {
14123
+ ({ tag, version } = found.inFlight);
14124
+ } else {
14125
+ ({ tag, version } = await deriveHotfixVersion(deps));
13313
14126
  }
13314
- ({ tag, version } = await deriveHotfixVersion(deps));
13315
14127
  }
13316
14128
  const facts = await gatherHotfixFacts(deps, ctx, tag, version);
13317
- return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
14129
+ return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts), warnings };
13318
14130
  }
13319
14131
  async function gatherHotfixFacts(deps, ctx, tag, version) {
13320
14132
  const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
@@ -13356,7 +14168,19 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
13356
14168
  const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
13357
14169
  return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
13358
14170
  }
13359
- async function findInFlightHotfixVersion(deps, ctx) {
14171
+ function compareHotfixVersions(a, b) {
14172
+ const pa = a.replace(/^v/, "").split(".").map(Number);
14173
+ const pb = b.replace(/^v/, "").split(".").map(Number);
14174
+ for (let i = 0; i < 3; i++) {
14175
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
14176
+ }
14177
+ return 0;
14178
+ }
14179
+ function supersededHotfixWarnings(superseded, latestMainTag) {
14180
+ if (superseded.length === 0) return [];
14181
+ 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.`];
14182
+ }
14183
+ async function findInFlightHotfixVersion(deps, ctx, latestMainTag) {
13360
14184
  const tags = /* @__PURE__ */ new Set();
13361
14185
  const out = await deps.run("gh", [
13362
14186
  "pr",
@@ -13382,20 +14206,20 @@ async function findInFlightHotfixVersion(deps, ctx) {
13382
14206
  const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
13383
14207
  if (m) tags.add(m[1]);
13384
14208
  }
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) {
14209
+ const sorted = [...tags].sort((a, b) => compareHotfixVersions(b, a));
14210
+ const fresh = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) > 0) : sorted;
14211
+ for (const tag of fresh) {
13394
14212
  const version = tag.slice(1);
13395
14213
  const facts = await gatherHotfixFacts(deps, ctx, tag, version);
13396
- if (deriveHotfixState(facts).state !== "complete") return { tag, version };
14214
+ if (deriveHotfixState(facts).state !== "complete") return { inFlight: { tag, version }, superseded: [] };
13397
14215
  }
13398
- return null;
14216
+ const stale = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) <= 0) : [];
14217
+ const superseded = [];
14218
+ for (const tag of stale) {
14219
+ const facts = await gatherHotfixFacts(deps, ctx, tag, tag.slice(1));
14220
+ if (deriveHotfixState(facts).state !== "complete") superseded.push(tag);
14221
+ }
14222
+ return { inFlight: null, superseded };
13399
14223
  }
13400
14224
 
13401
14225
  // src/release-announce.ts
@@ -13525,7 +14349,7 @@ async function announceRelease(deps, args) {
13525
14349
  }
13526
14350
 
13527
14351
  // src/port-registry.ts
13528
- var import_node_fs17 = require("node:fs");
14352
+ var import_node_fs20 = require("node:fs");
13529
14353
 
13530
14354
  // ../infra/port-geometry.mjs
13531
14355
  var PORT_BLOCK = 100;
@@ -13539,8 +14363,8 @@ function nextPortBlock(registry2) {
13539
14363
  return [base2, base2 + PORT_SPAN];
13540
14364
  }
13541
14365
  function loadPortRegistry(path2) {
13542
- if (!(0, import_node_fs17.existsSync)(path2)) return {};
13543
- const raw = JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8"));
14366
+ if (!(0, import_node_fs20.existsSync)(path2)) return {};
14367
+ const raw = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
13544
14368
  const out = {};
13545
14369
  for (const [key, value] of Object.entries(raw)) {
13546
14370
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -13554,9 +14378,9 @@ function ensurePortRange(repo, path2) {
13554
14378
  const existing = registry2[repo];
13555
14379
  if (existing) return existing;
13556
14380
  const range = nextPortBlock(registry2);
13557
- const raw = (0, import_node_fs17.existsSync)(path2) ? JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8")) : {};
14381
+ const raw = (0, import_node_fs20.existsSync)(path2) ? JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8")) : {};
13558
14382
  raw[repo] = range;
13559
- (0, import_node_fs17.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
14383
+ (0, import_node_fs20.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
13560
14384
  return range;
13561
14385
  }
13562
14386
  function portCursorSeed(registry2) {
@@ -14279,17 +15103,16 @@ function renderBootstrapVerifyReport(report) {
14279
15103
  var PROJECTS_LIST_PATH = "/projects/list";
14280
15104
  var ORG_CONFIG_PATH = "/org/config";
14281
15105
  var PROJECTS_ENVELOPE_KEY = "projects";
15106
+ var REGISTRY_FETCH_TIMEOUT_MS = 8e3;
14282
15107
 
14283
15108
  // src/registry-client.ts
14284
- var DEFAULT_TIMEOUT_MS2 = 8e3;
14285
- var WAITED_TENANT_CONTROL_TIMEOUT_MS = 13e3;
14286
15109
  var TENANT_DEPLOY_TIMEOUT_MS = 12e4;
14287
15110
  var RETRY_ATTEMPTS = 3;
14288
15111
  function retriedFetch(deps, url, init) {
14289
15112
  const headers = { ...clientVersionHeaders(), ...init.headers };
14290
15113
  return fetchWithRetry(deps.fetch ?? fetch, url, { ...init, headers }, {
14291
15114
  attempts: RETRY_ATTEMPTS,
14292
- timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2
15115
+ timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS
14293
15116
  });
14294
15117
  }
14295
15118
  async function fetchTrainAuthority(repo, deps) {
@@ -14389,7 +15212,7 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
14389
15212
  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
15213
  const token = await deps.token();
14391
15214
  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;
15215
+ const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS;
14393
15216
  const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs });
14394
15217
  const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs });
14395
15218
  try {
@@ -14418,38 +15241,12 @@ async function attestAppGaps(slug, repo, deps) {
14418
15241
  return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
14419
15242
  }
14420
15243
  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 });
15244
+ return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
14424
15245
  }
14425
15246
  async function tenantDeploy(payload, deps) {
14426
15247
  return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
14427
15248
  }
14428
15249
 
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
15250
  // src/project-readiness.ts
14454
15251
  function stagesForTrack(meta) {
14455
15252
  return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
@@ -14771,14 +15568,14 @@ async function buildV2Doctor(repoOrSlug, deps) {
14771
15568
  const required = stageInTrack(meta, stage2) && projectRequiresDeployState(model, stage2);
14772
15569
  return [stage2, { required, ok: required ? await deps.hasDeployState(slug, stage2) : true }];
14773
15570
  })));
14774
- const secrets2 = Object.fromEntries(STAGES.map((stage2) => {
15571
+ const secrets = Object.fromEntries(STAGES.map((stage2) => {
14775
15572
  const required = stageInTrack(meta, stage2) ? stageRequiredSecrets(stage2, meta).map((key) => stageKey2(stage2, key)) : [];
14776
15573
  const present = required.filter((key) => presentSecrets.has(key));
14777
15574
  const missing = required.filter((key) => !presentSecrets.has(key));
14778
15575
  return [stage2, { required, present, missing }];
14779
15576
  }));
14780
15577
  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);
15578
+ const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets).every((v) => v.missing.length === 0);
14782
15579
  const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
14783
15580
  return {
14784
15581
  ok,
@@ -14787,7 +15584,7 @@ async function buildV2Doctor(repoOrSlug, deps) {
14787
15584
  class: meta.class,
14788
15585
  projectType,
14789
15586
  deployModel: model,
14790
- hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets: secrets2 },
15587
+ hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets },
14791
15588
  secretsError,
14792
15589
  autoHealAvailable: Object.keys(autoHeal.patch),
14793
15590
  appOwnedGaps: autoHeal.appOwnedGaps,
@@ -14837,7 +15634,7 @@ ${section}`.trim();
14837
15634
  }
14838
15635
 
14839
15636
  // src/project-set.ts
14840
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "ci", "requiredChecks", "gate"];
15637
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dashboard", "ci", "requiredChecks", "gate"];
14841
15638
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
14842
15639
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
14843
15640
  function parseRuntimeSecretsVar(raw) {
@@ -14996,6 +15793,21 @@ function parsePublishRequiredVar(raw) {
14996
15793
  if (raw === "false") return false;
14997
15794
  throw new Error("project set: publishRequired must be true or false");
14998
15795
  }
15796
+ function parseDashboardVar(raw) {
15797
+ if (raw === "true") return true;
15798
+ if (raw === "false") return false;
15799
+ throw new Error("project set: dashboard must be true or false");
15800
+ }
15801
+ function parsePublishDirVar(raw) {
15802
+ const v = raw.trim();
15803
+ if (v === "" || v === ".") {
15804
+ 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)");
15805
+ }
15806
+ if (!/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
15807
+ throw new Error('project set: publishDir must be a safe relative subpath \u2014 no leading slash, no ".." segment');
15808
+ }
15809
+ return v;
15810
+ }
14999
15811
  function parseRequiredChecksVar(raw) {
15000
15812
  let parsed;
15001
15813
  try {
@@ -15046,6 +15858,8 @@ var SETTABLE_VAR_KEYS = [
15046
15858
  "repos",
15047
15859
  "oauth",
15048
15860
  "publishRequired",
15861
+ "publishDir",
15862
+ "dashboard",
15049
15863
  "requiredGcpApis",
15050
15864
  "requiredRuntimeSecrets",
15051
15865
  "edgeDomains",
@@ -15062,6 +15876,8 @@ var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
15062
15876
  var SETTABLE_VAR_HINTS = {
15063
15877
  projectNumber: "numeric",
15064
15878
  publishRequired: "true|false",
15879
+ publishDir: "relative subpath, e.g. packages/ui",
15880
+ dashboard: "true|false",
15065
15881
  repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
15066
15882
  oauth: "JSON {subdomains,domains,callbackPath}",
15067
15883
  requiredGcpApis: "comma-string",
@@ -15135,6 +15951,10 @@ function buildProjectSetPatch(input) {
15135
15951
  patch[key] = parseReposVar(raw);
15136
15952
  } else if (key === "publishRequired") {
15137
15953
  patch[key] = parsePublishRequiredVar(raw);
15954
+ } else if (key === "dashboard") {
15955
+ patch[key] = parseDashboardVar(raw);
15956
+ } else if (key === "publishDir") {
15957
+ patch[key] = parsePublishDirVar(raw);
15138
15958
  } else if (key === "ci") {
15139
15959
  if (raw !== "none") throw new Error('project set: ci must be "none" (or use --unset ci to require checks)');
15140
15960
  patch[key] = raw;
@@ -15199,11 +16019,16 @@ function parseKbTree(stdout, prefix) {
15199
16019
  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
16020
  }
15201
16021
 
16022
+ // src/northstar-commands.ts
16023
+ var import_node_fs21 = require("node:fs");
16024
+ var import_node_child_process11 = require("node:child_process");
16025
+ var import_promises6 = require("node:fs/promises");
16026
+
15202
16027
  // src/plan.ts
15203
- var import_node_path16 = require("node:path");
16028
+ var import_node_path18 = require("node:path");
15204
16029
  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`);
16030
+ var META_FILE = (0, import_node_path18.join)(PLANS_DIR, ".plan-meta.json");
16031
+ var planPath = (slug) => (0, import_node_path18.join)(PLANS_DIR, `${slug}.md`);
15207
16032
  var metaKey = (project2, slug) => `${project2}/${slug}`;
15208
16033
  function parseMeta(raw) {
15209
16034
  if (!raw) return {};
@@ -15228,7 +16053,7 @@ function hashContent(s) {
15228
16053
  function staleHint(slug) {
15229
16054
  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
16055
  }
15231
- var INDEX_FILE = (0, import_node_path16.join)(PLANS_DIR, ".index.json");
16056
+ var INDEX_FILE = (0, import_node_path18.join)(PLANS_DIR, ".index.json");
15232
16057
  var INDEX_TTL_MS = 6e4;
15233
16058
  function parseIndex(raw) {
15234
16059
  if (!raw) return null;
@@ -15257,7 +16082,7 @@ function mergeIndex(idx, scope, plans, now) {
15257
16082
  const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
15258
16083
  return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
15259
16084
  }
15260
- var QUEUE_FILE = (0, import_node_path16.join)(PLANS_DIR, ".sync-queue.json");
16085
+ var QUEUE_FILE = (0, import_node_path18.join)(PLANS_DIR, ".sync-queue.json");
15261
16086
  var QUEUE_MAX_ATTEMPTS = 10;
15262
16087
  function isValidQueueEntry(e) {
15263
16088
  if (!e || typeof e !== "object") return false;
@@ -15716,23 +16541,298 @@ async function planGraduate(deps, slug, opts = {}) {
15716
16541
  if (pushed) deps.log(`graduated ${slug}`);
15717
16542
  }
15718
16543
 
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) {
16544
+ // src/northstar-commands.ts
16545
+ var planSyncDetached = false;
16546
+ function detachPlanSync() {
16547
+ if (planSyncDetached) return;
16548
+ planSyncDetached = true;
16549
+ try {
16550
+ (0, import_node_child_process11.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
16551
+ detached: true,
16552
+ stdio: "ignore",
16553
+ windowsHide: true,
16554
+ cwd: process.cwd()
16555
+ }).unref();
16556
+ } catch {
16557
+ }
16558
+ }
16559
+ function makePlanDeps(cfg, io = consoleIo) {
16560
+ const ensureDir = () => (0, import_node_fs21.mkdirSync)(PLANS_DIR, { recursive: true });
16561
+ return {
16562
+ apiUrl: cfg.sagaApiUrl,
16563
+ fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
16564
+ headers: (extra) => hubHeaders(extra),
16565
+ project: async () => (await sagaKey(cfg)).project,
16566
+ readLocal: (slug) => {
16567
+ try {
16568
+ return (0, import_node_fs21.readFileSync)(planPath(slug), "utf8");
16569
+ } catch {
16570
+ return null;
16571
+ }
16572
+ },
16573
+ writeLocal: (slug, content) => {
16574
+ ensureDir();
16575
+ (0, import_node_fs21.writeFileSync)(planPath(slug), content, "utf8");
16576
+ },
16577
+ removeLocal: (slug) => {
16578
+ try {
16579
+ (0, import_node_fs21.rmSync)(planPath(slug));
16580
+ } catch {
16581
+ }
16582
+ },
16583
+ readMetaRaw: () => {
16584
+ try {
16585
+ return (0, import_node_fs21.readFileSync)(META_FILE, "utf8");
16586
+ } catch {
16587
+ return null;
16588
+ }
16589
+ },
16590
+ writeMetaRaw: (raw) => {
16591
+ ensureDir();
16592
+ atomicWriteFileSync(META_FILE, raw);
16593
+ },
16594
+ readIndexRaw: () => {
16595
+ try {
16596
+ return (0, import_node_fs21.readFileSync)(INDEX_FILE, "utf8");
16597
+ } catch {
16598
+ return null;
16599
+ }
16600
+ },
16601
+ writeIndexRaw: (raw) => {
16602
+ ensureDir();
16603
+ atomicWriteFileSync(INDEX_FILE, raw);
16604
+ },
16605
+ readQueueRaw: () => {
16606
+ try {
16607
+ return (0, import_node_fs21.readFileSync)(QUEUE_FILE, "utf8");
16608
+ } catch {
16609
+ return null;
16610
+ }
16611
+ },
16612
+ writeQueueRaw: (raw) => {
16613
+ ensureDir();
16614
+ atomicWriteFileSync(QUEUE_FILE, raw);
16615
+ },
16616
+ detachSync: detachPlanSync,
16617
+ log: (m) => io.log(m),
16618
+ err: (m) => io.err(m),
16619
+ now: () => (/* @__PURE__ */ new Date()).toISOString()
16620
+ };
16621
+ }
16622
+ function openInEditor(path2) {
16623
+ const editor = process.env.EDITOR || process.env.VISUAL;
16624
+ if (!editor) {
16625
+ console.log(`plan at ${path2} (set $EDITOR to open it automatically)`);
16626
+ return;
16627
+ }
16628
+ try {
16629
+ (0, import_node_child_process11.spawn)(editor, [path2], { stdio: "inherit" });
16630
+ } catch {
16631
+ console.log(`open ${path2} manually`);
16632
+ }
16633
+ }
16634
+ async function withPlan(quiet, run, io = consoleIo) {
16635
+ const cfg = await loadConfig();
16636
+ if (!cfg.sagaApiUrl) {
16637
+ if (!quiet) fail("plan: Hub API URL not configured");
16638
+ return;
16639
+ }
16640
+ await run(makePlanDeps(cfg, io));
16641
+ }
16642
+ async function gatherRelevanceSignals() {
16643
+ const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
16644
+ const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
16645
+ const signals = { branch, changedFiles: changed.length ? changed : void 0 };
16646
+ const issueNum = branch?.match(/\b(\d{2,})\b/)?.[1];
16647
+ if (issueNum) {
16648
+ try {
16649
+ const { stdout } = await execFileP2(
16650
+ "gh",
16651
+ ["issue", "view", issueNum, "--json", "title,labels", "--jq", "{title:.title,labels:[.labels[].name]}"],
16652
+ { timeout: 1e4 }
16653
+ );
16654
+ const j = JSON.parse(stdout);
16655
+ if (j.title) signals.issueTitle = j.title;
16656
+ if (j.labels?.length) signals.issueLabels = j.labels;
16657
+ } catch {
16658
+ }
16659
+ }
16660
+ return signals;
16661
+ }
16662
+ function registerNorthStarCommands(cmd) {
16663
+ 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) => {
16664
+ let content;
16665
+ if (o.bodyFile) {
16666
+ try {
16667
+ content = await resolveTextArg({ file: o.bodyFile }, { readFile: import_promises6.readFile, readStdin }, {
16668
+ value: "inline content",
16669
+ file: "--body-file",
16670
+ noun: "plan"
16671
+ });
16672
+ } catch (e) {
16673
+ console.error(e.message);
16674
+ process.exitCode = 1;
16675
+ return;
16676
+ }
16677
+ }
16678
+ return withPlan(false, async (d) => {
16679
+ const ok = await planPush(d, slug, { project: o.project, force: o.force, wait: o.wait, content });
16680
+ if (!ok) process.exitCode = 1;
16681
+ });
16682
+ });
16683
+ 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) => {
16684
+ const ok = await planPull(d, slug, o);
16685
+ if (!ok) process.exitCode = 1;
16686
+ }));
16687
+ 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) => {
16688
+ const ok = await planShow(d, slug, o);
16689
+ if (!ok) process.exitCode = 1;
16690
+ }));
16691
+ 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)));
16692
+ 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) => {
16693
+ const signals = await gatherRelevanceSignals();
16694
+ await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh, json: o.json });
16695
+ }));
16696
+ 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) => {
16697
+ const unresolved = await planSync(d, o);
16698
+ if (!o.wait) return;
16699
+ if (unresolved.length) {
16700
+ for (const e of unresolved) d.err(`${e.slug}: ${e.conflict ?? e.deadLettered ?? "still pending"}`);
16701
+ process.exitCode = 1;
16702
+ } else if (!o.quiet) {
16703
+ d.log("north star: all queued pushes landed");
16704
+ }
16705
+ }));
16706
+ 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)));
16707
+ 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)));
16708
+ cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
16709
+ (slug, o) => withPlan(false, async (d) => {
16710
+ const ok = await planPull(d, slug, { project: o.project });
16711
+ if (!ok) {
16712
+ process.exitCode = 1;
16713
+ return;
16714
+ }
16715
+ openInEditor(planPath(slug));
16716
+ })
16717
+ );
16718
+ 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)));
16719
+ 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(
16720
+ (slug, o) => withPlan(false, (d) => planGraduate(d, slug, o))
16721
+ );
16722
+ }
16723
+
16724
+ // src/secrets-commands.ts
16725
+ async function readSecretStdin() {
16726
+ if (process.stdin.isTTY) {
16727
+ process.stderr.write(
16728
+ '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'
16729
+ );
16730
+ return "";
16731
+ }
16732
+ const chunks = [];
16733
+ for await (const chunk of process.stdin) chunks.push(chunk);
16734
+ return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
16735
+ }
16736
+ function makeSecretsDeps(cfg) {
16737
+ return {
16738
+ apiUrl: cfg.sagaApiUrl,
16739
+ fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
16740
+ headers: (extra) => hubHeaders(extra),
16741
+ // Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
16742
+ // casing, which leaked mixed-case into `secrets where` output (#681).
16743
+ slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
16744
+ readSecretValue: () => readSecretStdin(),
16745
+ log: (m) => console.log(m),
16746
+ err: (m) => console.error(m)
16747
+ };
16748
+ }
16749
+ async function withSecrets(run) {
16750
+ const cfg = await loadConfig();
16751
+ if (!cfg.sagaApiUrl) {
16752
+ fail("secrets: Hub API URL not configured");
16753
+ return;
16754
+ }
16755
+ await run(makeSecretsDeps(cfg));
16756
+ }
16757
+ function registerSecretsCommands(program3) {
16758
+ const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
16759
+ 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)));
16760
+ 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)));
16761
+ 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) => {
16762
+ if (!["dev", "rc", "main"].includes(o.stage)) {
16763
+ return fail("secrets preflight: --stage must be dev, rc, or main");
16764
+ }
16765
+ const cfg = await loadConfig();
16766
+ if (!cfg.sagaApiUrl) {
16767
+ fail("secrets: Hub API URL not configured");
16768
+ return;
16769
+ }
16770
+ const d = makeSecretsDeps(cfg);
16771
+ const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
16772
+ const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
16773
+ const repo = o.repo ?? `mutmutco/${slug}`;
16774
+ const meta = await fetchProjectBySlug(slug, regDeps);
16775
+ const required = o.required?.length ? o.required : requiredRuntimeSecretNames(o.stage, meta?.requiredRuntimeSecrets, {
16776
+ includeGoogleOAuth: projectRequiresGoogleOAuth(meta, meta?.deployModel)
16777
+ });
16778
+ const centralContainer = meta?.deployModel === "tenant-container" || meta?.deployModel === "solo-container";
16779
+ if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
16780
+ 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");
16781
+ process.exitCode = 1;
16782
+ return;
16783
+ }
16784
+ const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
16785
+ if (!ok) process.exitCode = 1;
16786
+ });
16787
+ 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) => {
16788
+ const ok = await secretsGet(d, key, o);
16789
+ if (!ok) process.exitCode = 1;
16790
+ }));
16791
+ 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) => {
16792
+ const ok = await secretsRequest(d, key, o);
16793
+ if (!ok) process.exitCode = 1;
16794
+ }));
16795
+ 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) => {
16796
+ const ok = await secretsVerify(d, key, o);
16797
+ if (!ok) process.exitCode = 1;
16798
+ }));
16799
+ 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) => {
16800
+ const ok = await secretsSet(d, key, o);
16801
+ if (!ok) process.exitCode = 1;
16802
+ }));
16803
+ 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) => {
16804
+ const ok = await secretsEdit(d, key, o);
16805
+ if (!ok) process.exitCode = 1;
16806
+ }));
16807
+ 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) => {
16808
+ const stages = ["dev", "rc", "main"];
16809
+ if (!stages.includes(o.from) || !stages.includes(o.to)) {
16810
+ return fail("secrets copy: --from and --to must be dev, rc, or main");
16811
+ }
16812
+ const ok = await secretsCopy(d, {
16813
+ repo: o.repo,
16814
+ from: o.from,
16815
+ to: o.to,
16816
+ keys: o.keys.split(","),
16817
+ dryRun: o.dryRun
16818
+ });
16819
+ if (!ok) process.exitCode = 1;
16820
+ }));
16821
+ 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)));
16822
+ 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)));
16823
+ 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, {})));
16824
+ 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, {})));
16825
+ }
16826
+
16827
+ // src/oauth.ts
16828
+ var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
16829
+ var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
16830
+ var ENV_PREFIXES = ["", "dev", "rc"];
16831
+ var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
16832
+ var SSM_ENVS = ["dev", "rc", "main"];
16833
+ var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
16834
+ var uniq = (xs) => [...new Set(xs)];
16835
+ function defaultSubdomain2(slug) {
15736
16836
  const i = slug.indexOf("-");
15737
16837
  return i === -1 ? slug : slug.slice(i + 1);
15738
16838
  }
@@ -15952,7 +17052,7 @@ async function fetchHubVersionInfo(baseUrl) {
15952
17052
  }
15953
17053
  function readRepoVersion() {
15954
17054
  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;
17055
+ return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
15956
17056
  } catch {
15957
17057
  return void 0;
15958
17058
  }
@@ -15998,6 +17098,53 @@ async function fetchNpmReleasedVersion() {
15998
17098
  return void 0;
15999
17099
  }
16000
17100
  }
17101
+ async function fetchUiPackageLatestVersion(packageName) {
17102
+ try {
17103
+ const { stdout } = await runHostBin("npm", npmUiPackageLatestArgs(packageName), { timeout: NPM_VIEW_TIMEOUT_MS });
17104
+ return parseNpmViewVersion(stdout);
17105
+ } catch {
17106
+ return void 0;
17107
+ }
17108
+ }
17109
+ async function applyDesignSystemUpdate(check, log) {
17110
+ if (check.ok || !check.packageName) return check;
17111
+ try {
17112
+ log(` \u21BB updating ${check.packageName} ${check.installedVersion ?? "(missing)"} \u2192 ${check.latestVersion ?? "latest"}\u2026`);
17113
+ await runHostBin("npm", ["update", check.packageName], { timeout: NPM_UPDATE_TIMEOUT_MS });
17114
+ const installedVersion = designSystemSnapshot(process.cwd()).installedVersion ?? check.latestVersion;
17115
+ if (check.latestVersion && installedVersion && compareVersions(installedVersion, check.latestVersion) >= 0) {
17116
+ return { ...check, ok: true, installedVersion };
17117
+ }
17118
+ return { ...check, installedVersion };
17119
+ } catch {
17120
+ return check;
17121
+ }
17122
+ }
17123
+ async function applyRegistryComponentsSyncCheck(check, targetVersion, log) {
17124
+ if (check.ok || !check.components?.length) return check;
17125
+ const result = await applyRegistryComponentsSync(
17126
+ process.cwd(),
17127
+ check.components,
17128
+ targetVersion ?? check.targetVersion,
17129
+ log,
17130
+ defaultRegistrySyncDeps()
17131
+ );
17132
+ if (!result.ok) return check;
17133
+ const state = await gatherRegistryComponentsState(process.cwd(), targetVersion ?? check.targetVersion, { fetch });
17134
+ return buildRegistryComponentsCheck({ ...state, isConsumerRepo: true });
17135
+ }
17136
+ async function resolveDashboardConsumer(cfg) {
17137
+ if (!cfg.sagaApiUrl || isUiFactoryCheckout(process.cwd())) return { isConsumer: false };
17138
+ const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg));
17139
+ if (!read.ok) return { isConsumer: false, registryReadFailed: read.error };
17140
+ return { isConsumer: isDashboardMetaConsumer(read.project) };
17141
+ }
17142
+ function buildDesignSystemRegistryReadCheck(error) {
17143
+ return { ok: false, label: DESIGN_SYSTEM_VERSION_LABEL, fix: dashboardConsumerRegistryFix(error) };
17144
+ }
17145
+ function buildRegistryComponentsRegistryReadCheck(error) {
17146
+ return { ok: false, label: REGISTRY_COMPONENTS_LABEL, fix: dashboardConsumerRegistryFix(error) };
17147
+ }
16001
17148
  async function requireFreshTrainCli(commandName) {
16002
17149
  if (process.env.MMI_TRAIN_FRESH_OVERRIDE === "1") return;
16003
17150
  const report = buildVersionLagReport({
@@ -16019,8 +17166,8 @@ async function runClaudePlugin(args) {
16019
17166
  return false;
16020
17167
  }
16021
17168
  }
16022
- async function applyClaudePluginHeal(surface, log) {
16023
- if (surface !== "claude-cli" && surface !== "claude-vscode") return false;
17169
+ async function applyClaudePluginHeal(surface, log, opts) {
17170
+ if (!opts?.force && surface !== "claude-cli" && surface !== "claude-vscode") return false;
16024
17171
  log(" \u21BB reinstalling the MMI plugin via `claude plugin` (marketplace remove \u2192 add \u2192 install)\u2026");
16025
17172
  for (const step of CLAUDE_PLUGIN_HEAL_STEPS) {
16026
17173
  if (healStepAborts(step, await runClaudePlugin([...step.args]))) return false;
@@ -16035,8 +17182,8 @@ async function runCodexPlugin(args) {
16035
17182
  return false;
16036
17183
  }
16037
17184
  }
16038
- async function applyCodexPluginHeal(surface, log) {
16039
- if (surface !== "codex") return false;
17185
+ async function applyCodexPluginHeal(surface, log, opts) {
17186
+ if (!opts?.force && surface !== "codex") return false;
16040
17187
  log(" \u21BB reinstalling the MMI plugin via `codex plugin` (marketplace remove \u2192 add --ref main \u2192 add)\u2026");
16041
17188
  for (const step of CODEX_PLUGIN_HEAL_STEPS) {
16042
17189
  if (healStepAborts(step, await runCodexPlugin([...step.args]))) return false;
@@ -16060,7 +17207,8 @@ async function runRulesSync(opts, io = consoleIo) {
16060
17207
  ".claude/settings.json",
16061
17208
  ".claude/output-styles/mmi-plain.md",
16062
17209
  ".cursor/rules/mmi-plain-language.mdc",
16063
- ".cursor/rules/mmi-tool-economy.mdc"
17210
+ ".cursor/rules/mmi-tool-economy.mdc",
17211
+ ".cursor/rules/mmi-code-economy.mdc"
16064
17212
  ];
16065
17213
  const fetched = await Promise.all(files.map(async (file) => {
16066
17214
  try {
@@ -16079,11 +17227,11 @@ async function runRulesSync(opts, io = consoleIo) {
16079
17227
  for (const entry of fetched) {
16080
17228
  if ("error" in entry) continue;
16081
17229
  const { file, source } = entry;
16082
- const current = (0, import_node_fs19.existsSync)(file) ? await (0, import_promises6.readFile)(file, "utf8") : null;
17230
+ const current = (0, import_node_fs22.existsSync)(file) ? await (0, import_promises7.readFile)(file, "utf8") : null;
16083
17231
  if (needsUpdate(source, current)) {
16084
17232
  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");
17233
+ if (slash > 0) (0, import_node_fs22.mkdirSync)(file.slice(0, slash), { recursive: true });
17234
+ await (0, import_promises7.writeFile)(file, normalizeEol(source), "utf8");
16087
17235
  changed++;
16088
17236
  if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
16089
17237
  }
@@ -16108,9 +17256,9 @@ async function runDocsSync(opts, io = consoleIo) {
16108
17256
  return null;
16109
17257
  }
16110
17258
  },
16111
- localContent: async (f) => (0, import_node_fs19.existsSync)(f) ? await (0, import_promises6.readFile)(f, "utf8") : null,
17259
+ localContent: async (f) => (0, import_node_fs22.existsSync)(f) ? await (0, import_promises7.readFile)(f, "utf8") : null,
16112
17260
  writeDoc: async (f, c) => {
16113
- await (0, import_promises6.writeFile)(f, c, "utf8");
17261
+ await (0, import_promises7.writeFile)(f, c, "utf8");
16114
17262
  }
16115
17263
  });
16116
17264
  for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
@@ -16185,7 +17333,7 @@ function runWorktreeInstall(command, cwd, quiet) {
16185
17333
  const file = isWin ? "cmd.exe" : bin;
16186
17334
  const spawnArgs = isWin ? ["/c", bin, ...args] : args;
16187
17335
  return new Promise((resolve, reject) => {
16188
- const child = (0, import_node_child_process10.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
17336
+ const child = (0, import_node_child_process12.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
16189
17337
  const timer = setTimeout(() => {
16190
17338
  try {
16191
17339
  child.kill();
@@ -16207,7 +17355,7 @@ function runWorktreeInstall(command, cwd, quiet) {
16207
17355
  async function primaryCheckoutRoot(worktreeRoot) {
16208
17356
  try {
16209
17357
  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;
17358
+ return out ? (0, import_node_path19.dirname)(out) : void 0;
16211
17359
  } catch {
16212
17360
  return void 0;
16213
17361
  }
@@ -16220,28 +17368,28 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
16220
17368
  };
16221
17369
  }
16222
17370
  function acquireWorktreeSetupLock(worktreeRoot) {
16223
- const lockPath = (0, import_node_path17.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
17371
+ const lockPath = (0, import_node_path19.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
16224
17372
  const take = () => {
16225
- const fd = (0, import_node_fs19.openSync)(lockPath, "wx");
17373
+ const fd = (0, import_node_fs22.openSync)(lockPath, "wx");
16226
17374
  try {
16227
- (0, import_node_fs19.writeSync)(fd, String(Date.now()));
17375
+ (0, import_node_fs22.writeSync)(fd, String(Date.now()));
16228
17376
  } finally {
16229
- (0, import_node_fs19.closeSync)(fd);
17377
+ (0, import_node_fs22.closeSync)(fd);
16230
17378
  }
16231
17379
  return () => {
16232
17380
  try {
16233
- (0, import_node_fs19.rmSync)(lockPath, { force: true });
17381
+ (0, import_node_fs22.rmSync)(lockPath, { force: true });
16234
17382
  } catch {
16235
17383
  }
16236
17384
  };
16237
17385
  };
16238
17386
  try {
16239
- (0, import_node_fs19.mkdirSync)((0, import_node_path17.dirname)(lockPath), { recursive: true });
17387
+ (0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(lockPath), { recursive: true });
16240
17388
  return take();
16241
17389
  } catch {
16242
17390
  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 });
17391
+ if (Date.now() - (0, import_node_fs22.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
17392
+ (0, import_node_fs22.rmSync)(lockPath, { force: true });
16245
17393
  return take();
16246
17394
  }
16247
17395
  } catch {
@@ -16320,361 +17468,90 @@ async function ghCreate(args) {
16320
17468
  try {
16321
17469
  const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
16322
17470
  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;
17471
+ } catch (e) {
17472
+ await swapped.cleanup();
17473
+ const err = e;
17474
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
17475
+ fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
17476
+ } finally {
17477
+ await swapped.cleanup();
16481
17478
  }
17479
+ }
17480
+ async function ghJson(args, timeout = 1e4) {
17481
+ const { stdout } = await execFileP2("gh", args, { timeout });
17482
+ return JSON.parse(stdout);
17483
+ }
17484
+ async function resolveRepo(repo) {
17485
+ if (repo) return repo;
17486
+ const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
17487
+ if (fromOrigin) return fromOrigin;
16482
17488
  try {
16483
- (0, import_node_child_process10.spawn)(editor, [path2], { stdio: "inherit" });
17489
+ const { stdout } = await execFileP2("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
17490
+ return stdout.trim() || void 0;
16484
17491
  } catch {
16485
- console.log(`open ${path2} manually`);
17492
+ return void 0;
16486
17493
  }
16487
17494
  }
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;
17495
+ async function attachToProject(issueNumber, repo, priority) {
17496
+ const targetRepo2 = await resolveRepo(repo);
17497
+ let cfg;
17498
+ try {
17499
+ cfg = await loadConfigForRepo(targetRepo2);
17500
+ } catch (e) {
17501
+ console.error(`issue create: board attach skipped \u2014 ${e.message}`);
17502
+ return void 0;
16493
17503
  }
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
- }
17504
+ if (!cfg.projectId) {
17505
+ 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`);
17506
+ return void 0;
16513
17507
  }
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) {
17508
+ try {
17509
+ const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
17510
+ if (targetRepo2) viewArgs.push("--repo", targetRepo2);
17511
+ const { stdout: idOut } = await execFileP2("gh", viewArgs, { timeout: 1e4 });
17512
+ const contentId = idOut.trim();
17513
+ if (!contentId) throw new Error("could not resolve issue node id");
17514
+ const { stdout } = await execFileP2("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: GH_MUTATION_TIMEOUT_MS });
17515
+ const projectItemId = parseAddedItemId(stdout);
17516
+ if (projectItemId && priority) {
16520
17517
  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
- });
17518
+ await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
16526
17519
  } catch (e) {
16527
- console.error(e.message);
16528
- process.exitCode = 1;
16529
- return;
17520
+ const err = e;
17521
+ process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
17522
+ `);
16530
17523
  }
16531
17524
  }
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 "";
17525
+ return projectItemId;
17526
+ } catch (e) {
17527
+ const err = e;
17528
+ process.stderr.write(`warning: issue #${issueNumber} created but NOT added to the project board: ${(err.stderr || err.message || String(e)).trim()}
17529
+ `);
17530
+ return void 0;
16587
17531
  }
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
17532
  }
16605
- async function withSecrets(run) {
16606
- const cfg = await loadConfig();
16607
- if (!cfg.sagaApiUrl) {
16608
- fail("secrets: Hub API URL not configured");
16609
- return;
17533
+ var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
17534
+ function scheduleRelatedDiscovery(o) {
17535
+ try {
17536
+ const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
17537
+ if (o.repo) args.push("--repo", o.repo);
17538
+ (0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], ...args], {
17539
+ detached: true,
17540
+ stdio: "ignore",
17541
+ windowsHide: true,
17542
+ cwd: process.cwd()
17543
+ }).unref();
17544
+ } catch {
16610
17545
  }
16611
- await run(makeSecretsDeps(cfg));
16612
17546
  }
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;
17547
+ var northstar = program2.command("northstar").description("North Star \u2014 your cross-device plans/SSOTs (S3-backed, git-clean)");
17548
+ registerNorthStarCommands(northstar);
17549
+ var plan = program2.command("plan").description("Alias for `northstar` (deprecated \u2014 use `northstar`)");
17550
+ plan.hook("preAction", () => {
17551
+ process.stderr.write("warning: `plan` is deprecated; use `northstar` instead\n");
16639
17552
  });
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, {})));
17553
+ registerNorthStarCommands(plan);
17554
+ registerSecretsCommands(program2);
16678
17555
  function registryClientDeps(cfg) {
16679
17556
  return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
16680
17557
  }
@@ -16691,23 +17568,23 @@ async function reportWrite(label, res) {
16691
17568
  return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
16692
17569
  }
16693
17570
  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;
17571
+ 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) => {
17572
+ try {
17573
+ const result = await runTenantControl(trainApplyDeps(), { repo, stage: stage2, action, watch: o.watch });
17574
+ if (!o.json && action === "verify-secrets" && result.secrets) {
17575
+ const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed" };
17576
+ const { lines, failure } = renderVerifySecrets(body);
17577
+ for (const line of lines) printLine(line);
17578
+ if (failure) return failGraceful(`tenant control ${stage2} verify-secrets: ${failure}`);
17579
+ } else {
17580
+ printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
17581
+ }
17582
+ if (result.conclusion === "failure") {
17583
+ return failGraceful(`tenant control ${stage2} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`);
17584
+ }
17585
+ } catch (e) {
17586
+ return failGraceful(`tenant control: ${e.message}`);
16709
17587
  }
16710
- return reportWrite("tenant control", res);
16711
17588
  });
16712
17589
  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
17590
  if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
@@ -16724,18 +17601,18 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
16724
17601
  }
16725
17602
  const cfg = await loadConfig();
16726
17603
  const cdeps = registryClientDeps(cfg);
17604
+ const tdeps = trainApplyDeps();
16727
17605
  try {
16728
17606
  const result = await sweepRcOrphans({
16729
17607
  listProjects: () => fetchProjectsList(cdeps),
16730
17608
  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" };
17609
+ const r = await runTenantControl(tdeps, { repo, stage: "rc", action: "status", watch: true });
17610
+ const serviceState = r.conclusion === "success" ? r.serviceState ?? "unknown" : "error";
17611
+ return { serviceState };
16734
17612
  },
16735
17613
  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 };
17614
+ const r = await runTenantControl(tdeps, { repo, stage: "rc", action: "retire", watch: true });
17615
+ return { ok: r.category === "retired", category: r.category };
16739
17616
  }
16740
17617
  }, { retire: !!o.retire });
16741
17618
  return printLine(o.json ? JSON.stringify(result) : renderSweep(result));
@@ -16922,7 +17799,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
16922
17799
  const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
16923
17800
  return reportWrite("project attest", res);
16924
17801
  });
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) => {
17802
+ 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
17803
  const cfg = await loadConfig();
16927
17804
  let target;
16928
17805
  try {
@@ -16931,6 +17808,7 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
16931
17808
  return fail(e.message);
16932
17809
  }
16933
17810
  const slug = slugOf(target);
17811
+ const repo = target.includes("/") ? target : `mutmutco/${slug}`;
16934
17812
  let patch;
16935
17813
  try {
16936
17814
  patch = buildProjectSetPatch({
@@ -16948,7 +17826,7 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
16948
17826
  const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
16949
17827
  const boardError = boardLinkWriteError(patch, existing);
16950
17828
  if (boardError) return fail(`project set: ${boardError}`);
16951
- const res = await upsertProject(slug, patch, registryClientDeps(cfg));
17829
+ const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
16952
17830
  return reportWrite("project set", res);
16953
17831
  });
16954
17832
  var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
@@ -17053,8 +17931,8 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
17053
17931
  let body;
17054
17932
  let title;
17055
17933
  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 });
17934
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
17935
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
17058
17936
  if (o.priority === void 0) throw new Error("missing --priority <priority> \u2014 expected one of: urgent, high, medium, low");
17059
17937
  priority = normalizePriority(o.priority);
17060
17938
  args = buildIssueArgs({ type: o.type, title, body, priority, repo: o.repo, labels: o.label });
@@ -17141,8 +18019,8 @@ program2.command("report").description("file a friction report on the Hub board
17141
18019
  const targetRepo2 = o.repo ?? HUB_REPO2;
17142
18020
  const sourceRepo = await resolveRepo(void 0);
17143
18021
  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 });
18022
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
18023
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
17146
18024
  priority = normalizePriority(o.priority);
17147
18025
  args = buildIssueArgs({
17148
18026
  type: o.type,
@@ -17208,8 +18086,8 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
17208
18086
  try {
17209
18087
  const routing = assertVerifyRouting(o.routing);
17210
18088
  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");
18089
+ const criteria = await (0, import_promises7.readFile)(o.criteriaFile, "utf8");
18090
+ const diff = await (0, import_promises7.readFile)(o.diffFile, "utf8");
17213
18091
  const plan2 = buildPanelPlan({ routing, lenses, criteria, diff });
17214
18092
  console.log(JSON.stringify(plan2));
17215
18093
  } catch (e) {
@@ -17218,7 +18096,7 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
17218
18096
  });
17219
18097
  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
18098
  try {
17221
- const raw = o.inputFile === "-" ? await readStdin() : await (0, import_promises6.readFile)(o.inputFile, "utf8");
18099
+ const raw = o.inputFile === "-" ? await readStdin() : await (0, import_promises7.readFile)(o.inputFile, "utf8");
17222
18100
  const lenses = parseLensResults(JSON.parse(raw));
17223
18101
  console.log(JSON.stringify(synthesizePanelReport(lenses)));
17224
18102
  } catch (e) {
@@ -17291,7 +18169,7 @@ build.command("frontier").description("Evaluate external frontier exhaustion + L
17291
18169
  iterationCapOverride: opts.iterationCap
17292
18170
  };
17293
18171
  if (opts.jsonFile) {
17294
- const raw = await (0, import_promises6.readFile)(opts.jsonFile, "utf8");
18172
+ const raw = await (0, import_promises7.readFile)(opts.jsonFile, "utf8");
17295
18173
  state = { ...state, ...JSON.parse(raw) };
17296
18174
  }
17297
18175
  const result = evaluateBuildFrontier(state);
@@ -17345,8 +18223,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
17345
18223
  let args;
17346
18224
  try {
17347
18225
  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 });
18226
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
18227
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
17350
18228
  title = buildSkillLessonTitle(skill, rawTitle);
17351
18229
  priority = normalizePriority(o.priority);
17352
18230
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -17397,8 +18275,8 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
17397
18275
  let body;
17398
18276
  let title;
17399
18277
  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 });
18278
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
18279
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
17402
18280
  } catch (e) {
17403
18281
  return fail(`pr create: ${e.message}`);
17404
18282
  }
@@ -17406,9 +18284,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
17406
18284
  console.log(JSON.stringify(created));
17407
18285
  });
17408
18286
  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}`);
18287
+ const wfDir = (0, import_node_path19.join)(cwd, ".github", "workflows");
18288
+ if (!(0, import_node_fs22.existsSync)(wfDir)) return [];
18289
+ return (0, import_node_fs22.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
17412
18290
  }
17413
18291
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
17414
18292
  const repo = repoOpt ?? await resolveRepo();
@@ -17427,7 +18305,7 @@ function ciAuditDeps() {
17427
18305
  // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
17428
18306
  // reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
17429
18307
  // 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
18308
+ readSeedFile: (path2) => (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null
17431
18309
  };
17432
18310
  }
17433
18311
  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 +18345,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
17467
18345
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
17468
18346
  const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
17469
18347
  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+/);
18348
+ const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
18349
+ const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
18350
+ const [repoFromGh, base2] = viewed.split(/\s+/);
17473
18351
  if (base2 && base2 !== "development") {
17474
18352
  throw new Error(`pr land: base branch must be development (got ${base2}) \u2014 promotion merges stay human-only`);
17475
18353
  }
18354
+ const repo = repoOpt ?? repoFromGh;
17476
18355
  if (!repo) throw new Error("pr land: could not resolve PR repo");
17477
18356
  return repo;
17478
18357
  },
@@ -17497,32 +18376,50 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
17497
18376
  }
17498
18377
  return { mergeStatus: "failed", error: `merge blocked: ${message.split("\n")[0]} \u2014 ensure checks are green` };
17499
18378
  }
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" };
18379
+ const stateRead = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS })).stdout);
18380
+ if (!stateRead.ok) {
18381
+ return { mergeStatus: "failed", error: `could not read PR state after merge: ${stateRead.error}` };
18382
+ }
18383
+ return { mergeStatus: stateRead.state === "MERGED" ? "merged" : "auto-merge-enqueued" };
17502
18384
  },
17503
18385
  pollMerged: async (prNumber, repo, deadlineMs) => {
17504
18386
  const args = repo ? ["--repo", repo] : [];
17505
18387
  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;
18388
+ 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 });
18389
+ if (stateRead.ok && stateRead.state === "MERGED") return true;
17508
18390
  await new Promise((resolve) => setTimeout(resolve, PR_LAND_POLL_MS));
17509
18391
  }
17510
18392
  return false;
17511
18393
  }
17512
18394
  });
18395
+ if (result.status !== "failed") {
18396
+ try {
18397
+ const { stdout } = await execFileP2(process.execPath, [
18398
+ process.argv[1],
18399
+ "pr",
18400
+ "merge",
18401
+ number,
18402
+ ...o.repo ? ["--repo", o.repo] : [],
18403
+ "--squash"
18404
+ ], { timeout: GH_MUTATION_TIMEOUT_MS });
18405
+ const trimmed = stdout.trim();
18406
+ if (trimmed) {
18407
+ try {
18408
+ result.cleanup = JSON.parse(trimmed);
18409
+ } catch {
18410
+ result.cleanupError = "cleanup output was not JSON";
18411
+ }
18412
+ }
18413
+ } catch (e) {
18414
+ result.cleanupError = String(e.message || "pr merge cleanup failed");
18415
+ }
18416
+ }
17513
18417
  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
18418
  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);
18419
+ printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
18420
+ if (result.cleanupError) printLine(`pr land cleanup: ${result.cleanupError}`);
17525
18421
  }
18422
+ if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
17526
18423
  });
17527
18424
  async function remoteBranchExists2(branch, options = {}) {
17528
18425
  return checkRemoteBranchExists(branch, {
@@ -17537,15 +18434,15 @@ async function createDeferredWorktreeStore() {
17537
18434
  return {
17538
18435
  read: async () => {
17539
18436
  try {
17540
- return parseDeferredWorktreesFile(await (0, import_promises6.readFile)(registryPath, "utf8"));
18437
+ return parseDeferredWorktreesFile(await (0, import_promises7.readFile)(registryPath, "utf8"));
17541
18438
  } catch {
17542
18439
  return [];
17543
18440
  }
17544
18441
  },
17545
18442
  write: async (entries) => {
17546
18443
  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");
18444
+ await (0, import_promises7.mkdir)((0, import_node_path19.dirname)(registryPath), { recursive: true });
18445
+ await (0, import_promises7.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
17549
18446
  } catch {
17550
18447
  }
17551
18448
  }
@@ -17558,13 +18455,13 @@ var realWorktreeDirRemover = {
17558
18455
  probe: (p) => {
17559
18456
  let st;
17560
18457
  try {
17561
- st = (0, import_node_fs19.lstatSync)(p);
18458
+ st = (0, import_node_fs22.lstatSync)(p);
17562
18459
  } catch {
17563
18460
  return null;
17564
18461
  }
17565
18462
  if (st.isSymbolicLink()) return "link";
17566
18463
  try {
17567
- (0, import_node_fs19.readlinkSync)(p);
18464
+ (0, import_node_fs22.readlinkSync)(p);
17568
18465
  return "link";
17569
18466
  } catch {
17570
18467
  }
@@ -17572,7 +18469,7 @@ var realWorktreeDirRemover = {
17572
18469
  },
17573
18470
  readdir: (p) => {
17574
18471
  try {
17575
- return (0, import_node_fs19.readdirSync)(p);
18472
+ return (0, import_node_fs22.readdirSync)(p);
17576
18473
  } catch {
17577
18474
  return [];
17578
18475
  }
@@ -17581,12 +18478,12 @@ var realWorktreeDirRemover = {
17581
18478
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
17582
18479
  detachLink: (p) => {
17583
18480
  try {
17584
- (0, import_node_fs19.rmdirSync)(p);
18481
+ (0, import_node_fs22.rmdirSync)(p);
17585
18482
  } catch {
17586
- (0, import_node_fs19.unlinkSync)(p);
18483
+ (0, import_node_fs22.unlinkSync)(p);
17587
18484
  }
17588
18485
  },
17589
- removeTree: (p) => (0, import_promises6.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
18486
+ removeTree: (p) => (0, import_promises7.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
17590
18487
  };
17591
18488
  async function resolvePrimaryCheckout(execGit) {
17592
18489
  try {
@@ -17604,7 +18501,7 @@ function worktreeRemoveDeps(execGit) {
17604
18501
  }
17605
18502
  function teardownWorktreeStage(worktreePath) {
17606
18503
  return runWorktreeStageTeardown(worktreePath, {
17607
- hasStageState: (wt) => (0, import_node_fs19.existsSync)(stageStatePath(wt)),
18504
+ hasStageState: (wt) => (0, import_node_fs22.existsSync)(stageStatePath(wt)),
17608
18505
  stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
17609
18506
  listComposeProjects: async () => {
17610
18507
  const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
@@ -17618,7 +18515,7 @@ function teardownWorktreeStage(worktreePath) {
17618
18515
  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
18516
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
17620
18517
  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();
18518
+ 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
18519
  const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
17623
18520
  const beforeWorktrees = parseWorktreePorcelain(
17624
18521
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
@@ -17667,11 +18564,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
17667
18564
  } : await cleanupPrMergeLocalBranch(headRef, {
17668
18565
  beforeWorktrees,
17669
18566
  startingPath,
17670
- pathExists: (p) => (0, import_node_fs19.existsSync)(p),
18567
+ pathExists: (p) => (0, import_node_fs22.existsSync)(p),
17671
18568
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
17672
18569
  teardownWorktreeStage,
17673
18570
  deferredStore,
17674
- removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir
18571
+ removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir,
18572
+ // After merge, return the local checkout to the (fast-forwarded) branch the PR merged into so
18573
+ // grind/build never leave the primary parked on a dead feature branch (#1606).
18574
+ returnToBranch: baseRef
17675
18575
  });
17676
18576
  } catch (e) {
17677
18577
  localCleanup = {
@@ -17839,7 +18739,7 @@ function rawValues(flag) {
17839
18739
  return out;
17840
18740
  }
17841
18741
  function printLine(value) {
17842
- (0, import_node_fs19.writeSync)(1, `${value}
18742
+ (0, import_node_fs22.writeSync)(1, `${value}
17843
18743
  `);
17844
18744
  }
17845
18745
  function stageKeepAlive() {
@@ -17856,8 +18756,8 @@ async function resolveStage() {
17856
18756
  local,
17857
18757
  shell: shellFor(),
17858
18758
  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"))
18759
+ hasCompose: (0, import_node_fs22.existsSync)((0, import_node_path19.join)(process.cwd(), "docker-compose.yml")),
18760
+ hasEnvExample: (0, import_node_fs22.existsSync)((0, import_node_path19.join)(process.cwd(), ".env.example"))
17861
18761
  });
17862
18762
  }
17863
18763
  async function fetchStageVaultEnvMerge() {
@@ -17909,9 +18809,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
17909
18809
  printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
17910
18810
  return;
17911
18811
  }
17912
- const path2 = (0, import_node_path17.join)(process.cwd(), "infra", "port-ranges.json");
18812
+ const path2 = (0, import_node_path19.join)(process.cwd(), "infra", "port-ranges.json");
17913
18813
  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 });
18814
+ const { stdout } = await execFileP2("node", [(0, import_node_path19.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
17915
18815
  const parsed = JSON.parse(stdout);
17916
18816
  if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
17917
18817
  return parsed.range;
@@ -18097,6 +18997,15 @@ function trainApplyDeps() {
18097
18997
  throw new Error(`tenant deploy dispatch failed: ${detail}`);
18098
18998
  }
18099
18999
  },
19000
+ // Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
19001
+ // workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
19002
+ // outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
19003
+ dispatchTenantControl: async ({ repo, stage: stage2, action }) => {
19004
+ const res = await tenantControl({ repo, stage: stage2, action }, registryClientDeps(await loadConfig()));
19005
+ if (res.ok) return { ok: true };
19006
+ const body = res.body;
19007
+ return { ok: false, category: body?.category, error: body?.error ?? res.error };
19008
+ },
18100
19009
  // Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
18101
19010
  // release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
18102
19011
  // fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
@@ -18105,7 +19014,7 @@ function trainApplyDeps() {
18105
19014
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
18106
19015
  announce: (args) => announceRelease({
18107
19016
  run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
18108
- readFile: (path2) => (0, import_promises6.readFile)(path2, "utf8")
19017
+ readFile: (path2) => (0, import_promises7.readFile)(path2, "utf8")
18109
19018
  }, args),
18110
19019
  fetchEdgeDomains: async (slug) => {
18111
19020
  const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
@@ -18215,7 +19124,8 @@ function renderHotfixStatus(r) {
18215
19124
  ` - 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
19125
  ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
18217
19126
  ` - npm @mutmutco/cli: ${r.npmVersion}`,
18218
- ` - next: ${r.next}`
19127
+ ` - next: ${r.next}`,
19128
+ ...r.warnings.map((w) => ` - warning: ${w}`)
18219
19129
  ].join("\n");
18220
19130
  }
18221
19131
  async function runHotfixSub(sub, body, json, render) {
@@ -18273,12 +19183,12 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
18273
19183
  }
18274
19184
  if (!audit.ok) process.exitCode = 1;
18275
19185
  });
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) => {
19186
+ 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
19187
  if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
18278
19188
  if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
18279
19189
  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));
19190
+ const steps = bootstrapPlan(o.repo, o.class, { dashboard: o.dashboard });
19191
+ 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
19192
  });
18283
19193
  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
19194
  const o = { class: rawValue("--class", "deployable"), json: rawFlag("--json") };
@@ -18292,7 +19202,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
18292
19202
  client: defaultGitHubClient(),
18293
19203
  projectMeta: meta,
18294
19204
  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,
19205
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
18296
19206
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
18297
19207
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
18298
19208
  requiredGcpApis: (() => {
@@ -18317,12 +19227,13 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
18317
19227
  console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
18318
19228
  if (!report.ok) process.exitCode = 1;
18319
19229
  });
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) => {
19230
+ 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
19231
  const o = {
18322
19232
  class: rawValue("--class", "deployable"),
18323
19233
  projectType: rawValue("--project-type", ""),
18324
19234
  deployModel: rawValue("--deploy-model", ""),
18325
19235
  releaseTrack: rawValue("--release-track", ""),
19236
+ dashboard: rawFlag("--dashboard"),
18326
19237
  execute: rawFlag("--execute"),
18327
19238
  json: rawFlag("--json")
18328
19239
  };
@@ -18335,20 +19246,22 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18335
19246
  return fail(`bootstrap apply: ${e.message}`);
18336
19247
  }
18337
19248
  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"));
19249
+ if (!(0, import_node_fs22.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
19250
+ const manifest = loadBootstrapSeeds((0, import_node_fs22.readFileSync)(manifestPath, "utf8"));
18340
19251
  const baseBranch = o.class === "content" ? "main" : "development";
18341
19252
  const slug = parsedRepo.slug;
18342
19253
  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;
19254
+ const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
18344
19255
  const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
18345
19256
  const rawVars = {};
18346
19257
  for (const value of rawValues("--var")) {
18347
19258
  const eq = value.indexOf("=");
18348
19259
  if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
18349
19260
  }
19261
+ let registryMetaDashboard = false;
18350
19262
  try {
18351
19263
  const meta = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
19264
+ registryMetaDashboard = meta?.dashboard === true;
18352
19265
  for (const [k, v] of Object.entries(gateConfigToVars(meta?.gate))) if (rawVars[k] == null) rawVars[k] = v;
18353
19266
  } catch {
18354
19267
  }
@@ -18365,16 +19278,20 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18365
19278
  const applied = [];
18366
19279
  let applyDeployModel;
18367
19280
  try {
18368
- applyDeployModel = buildRegisterPayload(repo, o.class, vars, {
19281
+ const payload = buildRegisterPayload(repo, o.class, vars, {
18369
19282
  projectType: o.projectType || void 0,
18370
19283
  deployModel: o.deployModel || void 0,
18371
- releaseTrack: o.releaseTrack || void 0
18372
- }).deployModel;
19284
+ releaseTrack: o.releaseTrack || void 0,
19285
+ dashboard: o.dashboard
19286
+ });
19287
+ applyDeployModel = payload.deployModel;
18373
19288
  } catch {
18374
19289
  }
19290
+ const applyDashboard = o.dashboard === true || !rawFlag("--dashboard") && registryMetaDashboard;
18375
19291
  for (const seed of manifest.seeds) {
18376
19292
  if (!seed.classes.includes(o.class)) continue;
18377
19293
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
19294
+ if (!seedMatchesDashboard(seed, applyDashboard)) continue;
18378
19295
  const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
18379
19296
  let exists = false;
18380
19297
  let sha;
@@ -18397,7 +19314,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18397
19314
  }
18398
19315
  const planned = planSeedAction(resolved, exists);
18399
19316
  const isBlock = resolved.source === "managed-block";
18400
- const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile6) : null;
19317
+ const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
18401
19318
  const action = reconcileSeedAction(planned, content, isBlock);
18402
19319
  actions.push(action);
18403
19320
  if (o.execute && (action.action === "create" || action.action === "update")) {
@@ -18414,7 +19331,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18414
19331
  }
18415
19332
  const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
18416
19333
  if (rulesetSeed) {
18417
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile6);
19334
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
18418
19335
  if (rulesetContent) {
18419
19336
  try {
18420
19337
  const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
@@ -18450,7 +19367,8 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18450
19367
  registerPayload = buildRegisterPayload(repo, o.class, vars, {
18451
19368
  projectType: o.projectType || void 0,
18452
19369
  deployModel: o.deployModel || void 0,
18453
- releaseTrack: bootstrapReleaseTrack
19370
+ releaseTrack: bootstrapReleaseTrack,
19371
+ dashboard: o.dashboard
18454
19372
  });
18455
19373
  } catch (e) {
18456
19374
  return fail(`bootstrap apply: ${e.message}`);
@@ -18584,38 +19502,39 @@ access.command("audit").description("audit collaborator roles + train-branch pus
18584
19502
  if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
18585
19503
  targets = [{ repo: o.repo, class: o.class }];
18586
19504
  } 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;
19505
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs22.existsSync)("projects.json") ? (0, import_node_fs22.readFileSync)("projects.json", "utf8") : null;
18588
19506
  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;
19507
+ const fanoutJson = (0, import_node_fs22.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs22.readFileSync)(".github/fanout-targets.json", "utf8") : null;
18590
19508
  targets = loadAccessTargets(projectsJson, fanoutJson);
18591
19509
  }
18592
19510
  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")) : {};
19511
+ const fileMatrix = (0, import_node_fs22.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs22.readFileSync)("access-matrix.json", "utf8")) : {};
18594
19512
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
18595
19513
  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: {} };
19514
+ const fileContracts = (0, import_node_fs22.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs22.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
18597
19515
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
18598
19516
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
18599
19517
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
18600
19518
  if (!report.ok) process.exitCode = 1;
18601
19519
  });
19520
+ 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
19521
  var isWin = process.platform === "win32";
18603
19522
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
18604
19523
  const homeDir = surface === "codex" ? ".codex" : ".claude";
18605
- return (0, import_node_path17.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
19524
+ return (0, import_node_path19.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
18606
19525
  };
18607
19526
  function readInstalledPlugins() {
18608
19527
  try {
18609
- return JSON.parse((0, import_node_fs19.readFileSync)(installedPluginsPath(), "utf8"));
19528
+ return JSON.parse((0, import_node_fs22.readFileSync)(installedPluginsPath(), "utf8"));
18610
19529
  } catch {
18611
19530
  return null;
18612
19531
  }
18613
19532
  }
18614
19533
  function installedPluginSources() {
18615
19534
  return ["claude", "codex"].map((surface) => {
18616
- const recordPath = (0, import_node_path17.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
19535
+ const recordPath = (0, import_node_path19.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
18617
19536
  try {
18618
- return { surface, installed: JSON.parse((0, import_node_fs19.readFileSync)(recordPath, "utf8")), recordPath };
19537
+ return { surface, installed: JSON.parse((0, import_node_fs22.readFileSync)(recordPath, "utf8")), recordPath };
18619
19538
  } catch {
18620
19539
  return { surface, installed: null, recordPath };
18621
19540
  }
@@ -18623,7 +19542,7 @@ function installedPluginSources() {
18623
19542
  }
18624
19543
  function readClaudeSettings() {
18625
19544
  try {
18626
- return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
19545
+ return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
18627
19546
  } catch {
18628
19547
  return null;
18629
19548
  }
@@ -18645,7 +19564,7 @@ function writeProjectInstallRecord(record) {
18645
19564
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
18646
19565
  list.push(record);
18647
19566
  file.plugins[MMI_PLUGIN_ID] = list;
18648
- (0, import_node_fs19.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
19567
+ (0, import_node_fs22.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
18649
19568
  `, "utf8");
18650
19569
  return true;
18651
19570
  } catch {
@@ -18658,9 +19577,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
18658
19577
  if (!file) return false;
18659
19578
  if (!file.plugins) file.plugins = {};
18660
19579
  const path2 = installedPluginsPath();
18661
- (0, import_node_fs19.copyFileSync)(path2, `${path2}.bak`);
19580
+ (0, import_node_fs22.copyFileSync)(path2, `${path2}.bak`);
18662
19581
  file.plugins[pluginId] = records;
18663
- (0, import_node_fs19.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
19582
+ (0, import_node_fs22.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
18664
19583
  `, "utf8");
18665
19584
  return true;
18666
19585
  } catch {
@@ -18668,35 +19587,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
18668
19587
  }
18669
19588
  }
18670
19589
  function cursorPluginCacheRoot() {
18671
- return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
19590
+ return (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
18672
19591
  }
18673
19592
  function cursorPluginCachePinSnapshots() {
18674
19593
  const root = cursorPluginCacheRoot();
18675
19594
  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");
19595
+ return (0, import_node_fs22.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
19596
+ const path2 = (0, import_node_path19.join)(root, entry.name);
19597
+ const pluginJson = (0, import_node_path19.join)(path2, ".cursor-plugin", "plugin.json");
19598
+ const hooksJson = (0, import_node_path19.join)(path2, "hooks", "hooks.json");
19599
+ const cliBundle = (0, import_node_path19.join)(path2, "cli", "dist", "index.cjs");
18681
19600
  let version;
18682
19601
  try {
18683
- const raw = JSON.parse((0, import_node_fs19.readFileSync)(pluginJson, "utf8"));
19602
+ const raw = JSON.parse((0, import_node_fs22.readFileSync)(pluginJson, "utf8"));
18684
19603
  version = typeof raw.version === "string" ? raw.version : void 0;
18685
19604
  } catch {
18686
19605
  version = void 0;
18687
19606
  }
18688
19607
  let isEmpty = true;
18689
19608
  try {
18690
- isEmpty = (0, import_node_fs19.readdirSync)(path2).length === 0;
19609
+ isEmpty = (0, import_node_fs22.readdirSync)(path2).length === 0;
18691
19610
  } catch {
18692
19611
  isEmpty = true;
18693
19612
  }
18694
19613
  return {
18695
19614
  name: entry.name,
18696
19615
  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),
19616
+ hasPluginJson: (0, import_node_fs22.existsSync)(pluginJson),
19617
+ hasHooksJson: (0, import_node_fs22.existsSync)(hooksJson),
19618
+ hasCliBundle: (0, import_node_fs22.existsSync)(cliBundle),
18700
19619
  isEmpty,
18701
19620
  version
18702
19621
  };
@@ -18706,19 +19625,19 @@ function cursorPluginCachePinSnapshots() {
18706
19625
  }
18707
19626
  }
18708
19627
  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;
19628
+ const manifest = (0, import_node_path19.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
19629
+ return (0, import_node_fs22.existsSync)(manifest) ? process.cwd() : void 0;
18711
19630
  }
18712
19631
  function mmiPluginCacheRootSnapshots() {
18713
19632
  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") }
19633
+ { surface: "claude", root: (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
19634
+ { surface: "codex", root: (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
18716
19635
  ];
18717
19636
  return roots.flatMap(({ surface, root }) => {
18718
19637
  try {
18719
- const entries = (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
19638
+ const entries = (0, import_node_fs22.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
18720
19639
  name: entry.name,
18721
- path: (0, import_node_path17.join)(root, entry.name),
19640
+ path: (0, import_node_path19.join)(root, entry.name),
18722
19641
  isDirectory: entry.isDirectory()
18723
19642
  }));
18724
19643
  return [{ surface, root, entries }];
@@ -18729,7 +19648,7 @@ function mmiPluginCacheRootSnapshots() {
18729
19648
  }
18730
19649
  function hasNestedMmiChild(versionDir) {
18731
19650
  try {
18732
- return (0, import_node_fs19.statSync)((0, import_node_path17.join)(versionDir, "mmi")).isDirectory();
19651
+ return (0, import_node_fs22.statSync)((0, import_node_path19.join)(versionDir, "mmi")).isDirectory();
18733
19652
  } catch {
18734
19653
  return false;
18735
19654
  }
@@ -18740,10 +19659,10 @@ function nestedPluginTreeSnapshot() {
18740
19659
  );
18741
19660
  }
18742
19661
  function uniqueQuarantineTarget(path2) {
18743
- if (!(0, import_node_fs19.existsSync)(path2)) return path2;
19662
+ if (!(0, import_node_fs22.existsSync)(path2)) return path2;
18744
19663
  for (let i = 1; i < 100; i += 1) {
18745
19664
  const candidate = `${path2}-${i}`;
18746
- if (!(0, import_node_fs19.existsSync)(candidate)) return candidate;
19665
+ if (!(0, import_node_fs22.existsSync)(candidate)) return candidate;
18747
19666
  }
18748
19667
  return `${path2}-${Date.now()}`;
18749
19668
  }
@@ -18752,10 +19671,10 @@ function quarantinePluginCacheDirs(plan2) {
18752
19671
  const failed = [];
18753
19672
  for (const move of plan2) {
18754
19673
  try {
18755
- if (!(0, import_node_fs19.existsSync)(move.from)) continue;
19674
+ if (!(0, import_node_fs22.existsSync)(move.from)) continue;
18756
19675
  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);
19676
+ (0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(target), { recursive: true });
19677
+ (0, import_node_fs22.renameSync)(move.from, target);
18759
19678
  moved += 1;
18760
19679
  } catch {
18761
19680
  failed.push(move);
@@ -18774,23 +19693,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
18774
19693
  }
18775
19694
  async function clearNestedPluginTreeDir(targetPath) {
18776
19695
  try {
18777
- if (!(0, import_node_fs19.existsSync)(targetPath)) return true;
19696
+ if (!(0, import_node_fs22.existsSync)(targetPath)) return true;
18778
19697
  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 });
19698
+ const emptyDir = (0, import_node_path19.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
19699
+ (0, import_node_fs22.mkdirSync)(emptyDir, { recursive: true });
18781
19700
  try {
18782
19701
  await robocopyMirrorEmpty(emptyDir, targetPath);
18783
- (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
19702
+ (0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
18784
19703
  } finally {
18785
19704
  try {
18786
- (0, import_node_fs19.rmSync)(emptyDir, { recursive: true, force: true });
19705
+ (0, import_node_fs22.rmSync)(emptyDir, { recursive: true, force: true });
18787
19706
  } catch {
18788
19707
  }
18789
19708
  }
18790
- return !(0, import_node_fs19.existsSync)(targetPath);
19709
+ return !(0, import_node_fs22.existsSync)(targetPath);
18791
19710
  }
18792
- (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
18793
- return !(0, import_node_fs19.existsSync)(targetPath);
19711
+ (0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
19712
+ return !(0, import_node_fs22.existsSync)(targetPath);
18794
19713
  } catch {
18795
19714
  return false;
18796
19715
  }
@@ -18803,11 +19722,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
18803
19722
  }
18804
19723
  return true;
18805
19724
  }
18806
- var gitignorePath = () => (0, import_node_path17.join)(process.cwd(), ".gitignore");
19725
+ var gitignorePath = () => (0, import_node_path19.join)(process.cwd(), ".gitignore");
18807
19726
  function readTextFile(path2) {
18808
19727
  try {
18809
- if (!(0, import_node_fs19.existsSync)(path2)) return null;
18810
- return (0, import_node_fs19.readFileSync)(path2, "utf8");
19728
+ if (!(0, import_node_fs22.existsSync)(path2)) return null;
19729
+ return (0, import_node_fs22.readFileSync)(path2, "utf8");
18811
19730
  } catch {
18812
19731
  return null;
18813
19732
  }
@@ -18816,9 +19735,9 @@ function playwrightMcpConfigSnapshots() {
18816
19735
  const cwd = process.cwd();
18817
19736
  const home = (0, import_node_os6.homedir)();
18818
19737
  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")
19738
+ (0, import_node_path19.join)(cwd, ".cursor", "mcp.json"),
19739
+ (0, import_node_path19.join)(home, ".cursor", "mcp.json"),
19740
+ (0, import_node_path19.join)(home, ".codex", "config.toml")
18822
19741
  ];
18823
19742
  const out = [];
18824
19743
  for (const path2 of candidates) {
@@ -18831,7 +19750,7 @@ function strayBrowserArtifactPaths() {
18831
19750
  const cwd = process.cwd();
18832
19751
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
18833
19752
  try {
18834
- return (0, import_node_fs19.existsSync)((0, import_node_path17.join)(cwd, rel));
19753
+ return (0, import_node_fs22.existsSync)((0, import_node_path19.join)(cwd, rel));
18835
19754
  } catch {
18836
19755
  return false;
18837
19756
  }
@@ -18839,14 +19758,14 @@ function strayBrowserArtifactPaths() {
18839
19758
  }
18840
19759
  function readGitignore() {
18841
19760
  try {
18842
- return (0, import_node_fs19.readFileSync)(gitignorePath(), "utf8");
19761
+ return (0, import_node_fs22.readFileSync)(gitignorePath(), "utf8");
18843
19762
  } catch {
18844
19763
  return null;
18845
19764
  }
18846
19765
  }
18847
19766
  function writeGitignore(content) {
18848
19767
  try {
18849
- (0, import_node_fs19.writeFileSync)(gitignorePath(), content, "utf8");
19768
+ (0, import_node_fs22.writeFileSync)(gitignorePath(), content, "utf8");
18850
19769
  return true;
18851
19770
  } catch {
18852
19771
  return false;
@@ -18885,7 +19804,7 @@ async function runDoctor(opts, io = consoleIo) {
18885
19804
  let onPath = pathProbe;
18886
19805
  if (!onPath) {
18887
19806
  const root = process.env.CLAUDE_PLUGIN_ROOT;
18888
- if (root && (0, import_node_fs19.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
19807
+ if (root && (0, import_node_fs22.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18889
19808
  }
18890
19809
  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
19810
  const surface = detectSurface(process.env);
@@ -18929,10 +19848,40 @@ async function runDoctor(opts, io = consoleIo) {
18929
19848
  if (!pluginCheck.ok && pluginCheck.recordToInsert && repairLocal) {
18930
19849
  if (writeProjectInstallRecord(pluginCheck.recordToInsert)) {
18931
19850
  pluginCheck = { ...pluginCheck, ok: true };
18932
- io.err(` \u21BB repaired: registered mmi@mmi project install record \u2014 ${reloadHint} to load MMI commands`);
19851
+ io.err(` \u21BB repaired: registered mmi@mutmutco project install record \u2014 ${reloadHint} to load MMI commands`);
18933
19852
  }
18934
19853
  }
18935
19854
  checks.push(pluginCheck);
19855
+ let legacyPluginCheck = buildLegacyPluginInstallCheck({
19856
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19857
+ sources: installedPluginSources(),
19858
+ surface
19859
+ });
19860
+ if (!legacyPluginCheck.ok && repairLocal) {
19861
+ const claudeLegacy = legacyPluginCheck.staleSurfaces?.includes("claude") ?? false;
19862
+ const codexLegacy = legacyPluginCheck.staleSurfaces?.includes("codex") ?? false;
19863
+ if (claudeLegacy && await applyClaudePluginHeal(surface, (m) => io.err(m), { force: true })) {
19864
+ legacyPluginCheck = buildLegacyPluginInstallCheck({
19865
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19866
+ sources: installedPluginSources(),
19867
+ surface
19868
+ });
19869
+ if (legacyPluginCheck.ok) {
19870
+ io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via claude plugin \u2014 ${reloadHint} to load MMI commands`);
19871
+ }
19872
+ }
19873
+ if (!legacyPluginCheck.ok && codexLegacy && await applyCodexPluginHeal(surface, (m) => io.err(m), { force: true })) {
19874
+ legacyPluginCheck = buildLegacyPluginInstallCheck({
19875
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19876
+ sources: installedPluginSources(),
19877
+ surface
19878
+ });
19879
+ if (legacyPluginCheck.ok) {
19880
+ io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via codex plugin \u2014 ${reloadHint} to load MMI commands`);
19881
+ }
19882
+ }
19883
+ }
19884
+ checks.push(legacyPluginCheck);
18936
19885
  let gitignoreCheck = buildGitignoreManagedBlockCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), content: readGitignore() });
18937
19886
  const gitignoreDecision = decideGitignoreRepair(gitignoreCheck, { repoWritesAllowed, repairFull });
18938
19887
  gitignoreCheck = gitignoreDecision.check;
@@ -18955,7 +19904,7 @@ async function runDoctor(opts, io = consoleIo) {
18955
19904
  if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
18956
19905
  if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
18957
19906
  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`);
19907
+ 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
19908
  }
18960
19909
  }
18961
19910
  checks.push(driftCheck);
@@ -19057,7 +20006,7 @@ async function runDoctor(opts, io = consoleIo) {
19057
20006
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19058
20007
  surface,
19059
20008
  cacheRoot: cursorCacheRoot,
19060
- cacheRootExists: (0, import_node_fs19.existsSync)(cursorCacheRoot),
20009
+ cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
19061
20010
  pins: cursorPins,
19062
20011
  hubCheckout: hubCheckoutForCursorSeed(),
19063
20012
  releasedVersion
@@ -19068,7 +20017,7 @@ async function runDoctor(opts, io = consoleIo) {
19068
20017
  releasedVersion,
19069
20018
  hubCheckout: hubCheckoutForCursorSeed(),
19070
20019
  execFileP: execFileP2,
19071
- mkdtemp: (prefix) => (0, import_promises6.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
20020
+ mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path19.join)((0, import_node_os6.tmpdir)(), prefix)),
19072
20021
  log: (m) => io.err(m)
19073
20022
  });
19074
20023
  if (seeded) {
@@ -19077,7 +20026,7 @@ async function runDoctor(opts, io = consoleIo) {
19077
20026
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19078
20027
  surface,
19079
20028
  cacheRoot: cursorCacheRoot,
19080
- cacheRootExists: (0, import_node_fs19.existsSync)(cursorCacheRoot),
20029
+ cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
19081
20030
  pins: cursorPins,
19082
20031
  hubCheckout: hubCheckoutForCursorSeed(),
19083
20032
  releasedVersion
@@ -19116,6 +20065,38 @@ async function runDoctor(opts, io = consoleIo) {
19116
20065
  strayPaths: strayBrowserArtifactPaths()
19117
20066
  })
19118
20067
  );
20068
+ const dashboardConsumer = await resolveDashboardConsumer(cfg);
20069
+ const isDashboardConsumer = dashboardConsumer.isConsumer;
20070
+ const uiSnapshot = designSystemSnapshot(process.cwd());
20071
+ const uiLatestVersion = isDashboardConsumer && uiSnapshot.packageName ? await fetchUiPackageLatestVersion(uiSnapshot.packageName) : void 0;
20072
+ let designSystemCheck = dashboardConsumer.registryReadFailed ? buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildDesignSystemVersionCheck({
20073
+ ...uiSnapshot,
20074
+ isConsumerRepo: isDashboardConsumer,
20075
+ latestVersion: uiLatestVersion
20076
+ });
20077
+ if (!designSystemCheck.ok && (repairFull || repairLocal) && designSystemCheck.packageName) {
20078
+ designSystemCheck = await applyDesignSystemUpdate(designSystemCheck, (m) => io.err(m));
20079
+ if (designSystemCheck.ok) {
20080
+ io.err(` \u21BB updated ${designSystemCheck.packageName} \u2192 ${designSystemCheck.installedVersion ?? designSystemCheck.latestVersion ?? "latest"}`);
20081
+ }
20082
+ }
20083
+ checks.push(designSystemCheck);
20084
+ const registryTargetVersion = designSystemCheck.latestVersion ?? designSystemCheck.installedVersion ?? uiLatestVersion;
20085
+ let registryComponentsCheck = dashboardConsumer.registryReadFailed ? buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildRegistryComponentsCheck({
20086
+ ...await gatherRegistryComponentsState(process.cwd(), registryTargetVersion, { fetch }),
20087
+ isConsumerRepo: isDashboardConsumer
20088
+ });
20089
+ if (!registryComponentsCheck.ok && (repairFull || repairLocal) && repoWritesAllowed && registryComponentsCheck.components?.length) {
20090
+ registryComponentsCheck = await applyRegistryComponentsSyncCheck(
20091
+ registryComponentsCheck,
20092
+ registryTargetVersion,
20093
+ (m) => io.err(m)
20094
+ );
20095
+ if (registryComponentsCheck.ok) {
20096
+ io.err(` \u21BB synced ${registryComponentsCheck.components?.length ?? 0} registry component(s) \u2192 .mmi/design-system/components`);
20097
+ }
20098
+ }
20099
+ checks.push(registryComponentsCheck);
19119
20100
  const gaps = checks.filter((c) => !c.ok);
19120
20101
  if (opts.banner) {
19121
20102
  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 +20125,82 @@ async function runDoctor(opts, io = consoleIo) {
19144
20125
  io.log(gaps.length ? `
19145
20126
  ${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
19146
20127
  }
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) => (
20128
+ var designSystem = program2.command("design-system").description("@mutmutco UI npm package + registry component freshness for dashboard consumers (#1633, #1635)");
20129
+ 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) => {
20130
+ const cfg = await loadConfig();
20131
+ const dashboardConsumer = await resolveDashboardConsumer(cfg);
20132
+ if (dashboardConsumer.registryReadFailed) {
20133
+ const check2 = buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed);
20134
+ if (opts.json) {
20135
+ console.log(JSON.stringify(check2, null, 2));
20136
+ } else {
20137
+ console.log(`\u2717 ${check2.label}`);
20138
+ console.log(` fix: ${check2.fix}`);
20139
+ }
20140
+ process.exitCode = 1;
20141
+ return;
20142
+ }
20143
+ const isDashboardConsumer = dashboardConsumer.isConsumer;
20144
+ const snapshot = designSystemSnapshot(process.cwd());
20145
+ let check = buildDesignSystemVersionCheck({
20146
+ ...snapshot,
20147
+ isConsumerRepo: isDashboardConsumer,
20148
+ latestVersion: isDashboardConsumer && snapshot.packageName ? await fetchUiPackageLatestVersion(snapshot.packageName) : void 0
20149
+ });
20150
+ if (!check.ok && opts.apply && check.packageName) {
20151
+ check = await applyDesignSystemUpdate(check, (m) => console.error(m));
20152
+ }
20153
+ if (opts.json) {
20154
+ console.log(JSON.stringify(check, null, 2));
20155
+ process.exitCode = check.ok ? 0 : 1;
20156
+ return;
20157
+ }
20158
+ console.log(check.ok ? `\u2713 ${check.label}` : `\u2717 ${check.label}`);
20159
+ if (check.packageName) console.log(` package: ${check.packageName}`);
20160
+ if (check.installedVersion) console.log(` installed: ${check.installedVersion}`);
20161
+ if (check.latestVersion) console.log(` latest: ${check.latestVersion}`);
20162
+ if (!check.ok) console.log(` fix: ${check.fix}`);
20163
+ process.exitCode = check.ok ? 0 : 1;
20164
+ });
20165
+ 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) => {
20166
+ const cfg = await loadConfig();
20167
+ const dashboardConsumer = await resolveDashboardConsumer(cfg);
20168
+ if (dashboardConsumer.registryReadFailed) {
20169
+ const check2 = buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed);
20170
+ if (opts.json) {
20171
+ console.log(JSON.stringify(check2, null, 2));
20172
+ } else {
20173
+ console.log(`\u2717 ${check2.label}`);
20174
+ console.log(` fix: ${check2.fix}`);
20175
+ }
20176
+ process.exitCode = 1;
20177
+ return;
20178
+ }
20179
+ const isDashboardConsumer = dashboardConsumer.isConsumer;
20180
+ const snapshot = designSystemSnapshot(process.cwd());
20181
+ const targetVersion = isDashboardConsumer && snapshot.packageName ? await fetchUiPackageLatestVersion(snapshot.packageName) : void 0;
20182
+ const state = await gatherRegistryComponentsState(process.cwd(), targetVersion, { fetch });
20183
+ let check = buildRegistryComponentsCheck({
20184
+ ...state,
20185
+ isConsumerRepo: isDashboardConsumer
20186
+ });
20187
+ if (!check.ok && opts.apply && check.components?.length) {
20188
+ check = await applyRegistryComponentsSyncCheck(check, targetVersion, (m) => console.error(m));
20189
+ }
20190
+ if (opts.json) {
20191
+ console.log(JSON.stringify(check, null, 2));
20192
+ process.exitCode = check.ok ? 0 : 1;
20193
+ return;
20194
+ }
20195
+ console.log(check.ok ? `\u2713 ${check.label}` : `\u2717 ${check.label}`);
20196
+ if (check.components?.length) console.log(` components: ${check.components.join(", ")}`);
20197
+ if (check.cacheVersion) console.log(` cache version: ${check.cacheVersion}`);
20198
+ if (check.targetVersion) console.log(` target version: ${check.targetVersion}`);
20199
+ if (check.staleComponents?.length) console.log(` stale: ${check.staleComponents.join(", ")}`);
20200
+ if (!check.ok) console.log(` fix: ${check.fix}`);
20201
+ process.exitCode = check.ok ? 0 : 1;
20202
+ });
20203
+ 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
20204
  // Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
19149
20205
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
19150
20206
  ));
@@ -19159,7 +20215,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
19159
20215
  } catch (e) {
19160
20216
  console.error(`[mmi-hook] saga session failed: ${e.message}`);
19161
20217
  }
19162
- spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
20218
+ spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19163
20219
  let northstarInjected = false;
19164
20220
  const { parallel, sequential } = buildSessionStartPlan({
19165
20221
  rulesSync: (io) => runRulesSync({ quiet: true }, io),
@@ -19201,7 +20257,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
19201
20257
  for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
19202
20258
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
19203
20259
  if (worktreeBanner) {
19204
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
20260
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19205
20261
  consoleIo.log(worktreeBanner);
19206
20262
  }
19207
20263
  });