@mutmutco/cli 2.40.3 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +577 -244
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3392,7 +3392,7 @@ var program = new Command();
3392
3392
 
3393
3393
  // src/index.ts
3394
3394
  var import_promises7 = require("node:fs/promises");
3395
- var import_node_fs22 = require("node:fs");
3395
+ var import_node_fs23 = require("node:fs");
3396
3396
 
3397
3397
  // src/rules-sync.ts
3398
3398
  function normalizeEol(s) {
@@ -5168,11 +5168,11 @@ function parseHandoffText(text) {
5168
5168
  const sourceSessionId = clean(raw.sourceSessionId);
5169
5169
  const createdAt = clean(raw.createdAt);
5170
5170
  if (state !== "open" && state !== "claimed" && state !== "cancelled") return null;
5171
- if (!key || !northStarSlug || !summary || !sourceSessionId || !createdAt) return null;
5171
+ if (!key || !summary || !sourceSessionId || !createdAt) return null;
5172
5172
  return {
5173
5173
  state,
5174
5174
  key,
5175
- northStarSlug,
5175
+ northStarSlug: northStarSlug || void 0,
5176
5176
  summary,
5177
5177
  sourceSessionId,
5178
5178
  sourceBranch: clean(raw.sourceBranch) || void 0,
@@ -5190,20 +5190,19 @@ function listHandoffs(head, opts = {}) {
5190
5190
  }
5191
5191
  function findOpenHandoff(head, key) {
5192
5192
  const normalized = normalizeHandoffKey(key).toLowerCase();
5193
- return listHandoffs(head).find((item) => item.record.key.toLowerCase() === normalized || item.record.northStarSlug.toLowerCase() === normalized);
5193
+ return listHandoffs(head).find((item) => item.record.key.toLowerCase() === normalized || item.record.northStarSlug?.toLowerCase() === normalized);
5194
5194
  }
5195
5195
  function planOpenHandoff(input) {
5196
5196
  const key = normalizeHandoffKey(input.key);
5197
5197
  const northStarSlug = clean(input.northStarSlug);
5198
5198
  const summary = clean(input.summary);
5199
5199
  const sourceSessionId = clean(input.sourceSessionId);
5200
- if (!northStarSlug) throw new Error("north star slug is required");
5201
5200
  if (!summary) throw new Error("handoff summary is required");
5202
5201
  if (!sourceSessionId) throw new Error("source session id is required");
5203
5202
  return {
5204
5203
  state: "open",
5205
5204
  key,
5206
- northStarSlug,
5205
+ northStarSlug: northStarSlug || void 0,
5207
5206
  summary,
5208
5207
  sourceSessionId,
5209
5208
  sourceBranch: clean(input.sourceBranch) || void 0,
@@ -5222,7 +5221,7 @@ function formatHandoffLine(item) {
5222
5221
  const r = item.record;
5223
5222
  const branch = r.sourceBranch ? ` branch:${r.sourceBranch}` : "";
5224
5223
  const suffix = r.state === "claimed" && r.claimedBySessionId ? ` -> ${r.claimedBySessionId}` : "";
5225
- return `${r.key} [${r.state}] northstar:${r.northStarSlug}${branch} source:${r.sourceSessionId}${suffix} - ${r.summary}`;
5224
+ return `${r.key} [${r.state}] northstar:${r.northStarSlug ?? "-"}${branch} source:${r.sourceSessionId}${suffix} - ${r.summary}`;
5226
5225
  }
5227
5226
 
5228
5227
  // src/handoff-commands.ts
@@ -5298,7 +5297,7 @@ async function locateClaimedHandoff(key, claimedBySessionId, retry = FOREGROUND_
5298
5297
  const normalized = key.toLowerCase();
5299
5298
  return handoffs.find((item) => {
5300
5299
  const r = item.record;
5301
- return r.state === "claimed" && r.claimedBySessionId === claimedBySessionId && (r.key.toLowerCase() === normalized || r.northStarSlug.toLowerCase() === normalized);
5300
+ return r.state === "claimed" && r.claimedBySessionId === claimedBySessionId && (r.key.toLowerCase() === normalized || r.northStarSlug?.toLowerCase() === normalized);
5302
5301
  }) ?? null;
5303
5302
  }
5304
5303
  async function postKeyedNote(key, summary, options) {
@@ -5312,9 +5311,12 @@ async function postKeyedNote(key, summary, options) {
5312
5311
  function deriveOpenFields(summary, opts, head, key) {
5313
5312
  const northStarSlug = opts.northStarSlug ?? head.anchor?.slug;
5314
5313
  const handoffKey = opts.key ?? northStarSlug;
5314
+ if (!handoffKey?.trim()) {
5315
+ throw new Error("a handoff needs a key: pass --key <slug|#issue> (or --next), or open it on a session with a North Star anchor");
5316
+ }
5315
5317
  const text = summary?.trim() || head.next?.trim() || head.anchor?.intent?.trim();
5316
5318
  return planOpenHandoff({
5317
- key: handoffKey ?? "",
5319
+ key: handoffKey,
5318
5320
  northStarSlug: northStarSlug ?? "",
5319
5321
  summary: text ?? "",
5320
5322
  sourceSessionId: key.sessionId,
@@ -5390,7 +5392,7 @@ async function closeSourceHandoff(key, state, opts = {}, io = consoleIo) {
5390
5392
  anchor: located.item.record.summary,
5391
5393
  anchorSlug: located.item.record.northStarSlug,
5392
5394
  anchorForce: true,
5393
- decision: `accepted handoff ${located.item.record.key} from session ${located.item.record.sourceSessionId}; northstar ${located.item.record.northStarSlug}`,
5395
+ decision: `accepted handoff ${located.item.record.key} from session ${located.item.record.sourceSessionId}; northstar ${located.item.record.northStarSlug ?? "(none)"}`,
5394
5396
  verified: true
5395
5397
  });
5396
5398
  }
@@ -5412,7 +5414,7 @@ async function runHandoffDecline(key, opts = {}, io = consoleIo) {
5412
5414
  }
5413
5415
  function registerHandoffCommands(program3) {
5414
5416
  const handoff = program3.command("handoff").description("explicit saga + North Star handoff lifecycle");
5415
- handoff.command("open [summary]").description("open a handoff bound to the current North Star slug").option("--key <slug|#issue>", "handoff key (defaults to the current North Star slug)").option("--north-star-slug <slug>", "North Star slug to bind (defaults to current saga anchor slug)").option("--message-file <path|->", "read the handoff summary from a UTF-8 file, or from stdin with - (avoids cmd.exe quoting)").option("--json", "machine-readable output").action((summary, opts) => runHandoffOpen(summary, opts));
5417
+ handoff.command("open [summary]").description("open a handoff bound to a North Star slug or a board issue (--key/--next)").option("--key <slug|#issue>", "handoff key: a North Star slug or a board issue (defaults to the current North Star slug)").option("--next <slug|#issue>", "alias for --key (mirrors `saga note --next`)").option("--north-star-slug <slug>", "North Star slug to bind (optional; omit for an issue-bound handoff)").option("--message-file <path|->", "read the handoff summary from a UTF-8 file, or from stdin with - (avoids cmd.exe quoting)").option("--json", "machine-readable output").action((summary, opts) => runHandoffOpen(summary, { ...opts, key: opts.key ?? opts.next }));
5416
5418
  handoff.command("list").description("list open handoffs for the current repo (any branch)").option("--all", "include claimed/cancelled records").option("--json", "machine-readable output").action(async (opts) => {
5417
5419
  await runHandoffList(opts);
5418
5420
  });
@@ -5678,7 +5680,16 @@ async function secretsCapabilities(deps, opts) {
5678
5680
  return;
5679
5681
  }
5680
5682
  if (!res.ok) {
5681
- deps.err(await upgradeMessage(res) ?? `access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
5683
+ const upgrade = await upgradeMessage(res);
5684
+ if (upgrade) {
5685
+ deps.err(upgrade);
5686
+ } else if (res.status === 404) {
5687
+ deps.err(
5688
+ "access capabilities: the Hub API did not recognize /secrets/capabilities (HTTP 404). This endpoint ships in the Hub, so a 404 means the deployed Hub predates this command and should answer once a Hub release carrying this command is deployed \u2014 it is not an authorization or missing-credential error."
5689
+ );
5690
+ } else {
5691
+ deps.err(`access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
5692
+ }
5682
5693
  return;
5683
5694
  }
5684
5695
  const report = await res.json();
@@ -6152,7 +6163,6 @@ function buildIngestPayload(args) {
6152
6163
  }
6153
6164
 
6154
6165
  // src/honcho-client.ts
6155
- var HONCHO_ASSISTANT_PEER = "assistant";
6156
6166
  function parseHonchoQueueStatus(json) {
6157
6167
  const o = json && typeof json === "object" ? json : {};
6158
6168
  const pendingRaw = o.pending_work_units ?? o.depth ?? o.queue_depth ?? o.pending;
@@ -6205,9 +6215,9 @@ async function ingestPost(cfg, body, fetchImpl = fetch, timeoutMs = 1e4) {
6205
6215
  const session = String(body.session ?? "");
6206
6216
  const meta = body.meta && typeof body.meta === "object" ? body.meta : {};
6207
6217
  const rawMessages = Array.isArray(body.messages) ? body.messages : [];
6208
- const messages = rawMessages.filter((m) => m && typeof m.content === "string" && m.content.trim().length > 0).map((m) => ({
6218
+ const messages = rawMessages.filter((m) => m && m.role === "user" && typeof m.content === "string" && m.content.trim().length > 0).map((m) => ({
6209
6219
  content: m.content,
6210
- peer_id: m.role === "user" ? peer : HONCHO_ASSISTANT_PEER,
6220
+ peer_id: peer,
6211
6221
  metadata: { role: m.role, ...meta }
6212
6222
  }));
6213
6223
  if (!peer || !session || !messages.length) return { ok: true, threw: false, status: 204 };
@@ -6216,7 +6226,8 @@ async function ingestPost(cfg, body, fetchImpl = fetch, timeoutMs = 1e4) {
6216
6226
  if (res.status === 404) {
6217
6227
  const ensured = await request(cfg, fetchImpl, "POST", honchoRoutes.sessions(cfg.workspace), {
6218
6228
  id: session,
6219
- peers: { [peer]: {}, [HONCHO_ASSISTANT_PEER]: {} }
6229
+ peers: { [peer]: {} }
6230
+ // only the human peer — assistant turns are no longer ingested (#1775)
6220
6231
  }, timeoutMs);
6221
6232
  if (!ensured.ok && ensured.status !== 409) {
6222
6233
  if (ensured.status >= 500) return { ok: false, threw: true, message: `honcho session-ensure ${ensured.status}` };
@@ -6366,7 +6377,7 @@ function spawnHonchoFlush() {
6366
6377
  } catch {
6367
6378
  }
6368
6379
  }
6369
- var DEFAULT_INGEST_MIN_INTERVAL_SEC = 60;
6380
+ var DEFAULT_INGEST_MIN_INTERVAL_SEC = 300;
6370
6381
  function honchoThrottlePath(key) {
6371
6382
  const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
6372
6383
  return `.mmi/head-ts/honcho/${safe(key.project)}/${safe(key.branch)}`;
@@ -6398,7 +6409,12 @@ async function resolveSummaryText(summary, messageFile) {
6398
6409
  }
6399
6410
  return "";
6400
6411
  }
6412
+ function isAutomatedSession(env = process.env) {
6413
+ const v = env.GITHUB_ACTIONS;
6414
+ return !!v && v !== "false" && v !== "0";
6415
+ }
6401
6416
  async function runHonchoIngest(opts) {
6417
+ if (isAutomatedSession()) return;
6402
6418
  const cfg = await loadConfig();
6403
6419
  const peer = honchoPeerId(await honchoLogin(cfg), cfg);
6404
6420
  if (!peer) return;
@@ -7091,6 +7107,9 @@ function northstarPointer(injected = false) {
7091
7107
  }
7092
7108
  return "North Stars: run `mmi-cli northstar relevant` to load plans relevant to your task (`northstar list` for all).";
7093
7109
  }
7110
+ function kbPointer() {
7111
+ return "MM-KB (org knowledge): start at `mmi-cli kb get kb/INDEX.md`, then `kb get <path>` \u2014 consult before inventing conventions or storing long-term docs.";
7112
+ }
7094
7113
 
7095
7114
  // src/board.ts
7096
7115
  var import_node_child_process7 = require("node:child_process");
@@ -8009,9 +8028,47 @@ function ghError(e) {
8009
8028
  return (err.stderr || err.message || String(e)).trim();
8010
8029
  }
8011
8030
 
8031
+ // src/board-slice-cache.ts
8032
+ var import_node_fs14 = require("node:fs");
8033
+ var import_node_path12 = require("node:path");
8034
+ var BOARD_SLICE_CACHE_FILE = (0, import_node_path12.join)(".mmi", "board-slice.json");
8035
+ var BOARD_SLICE_CACHE_TTL_MS = 10 * 60 * 1e3;
8036
+ function boardSliceCachePath(cwd) {
8037
+ return (0, import_node_path12.join)(cwd, BOARD_SLICE_CACHE_FILE);
8038
+ }
8039
+ function readCachedBoardSlice(cwd) {
8040
+ try {
8041
+ const parsed = JSON.parse((0, import_node_fs14.readFileSync)(boardSliceCachePath(cwd), "utf8"));
8042
+ if (typeof parsed.ts !== "number") return null;
8043
+ return { block: typeof parsed.block === "string" ? parsed.block : null, ts: parsed.ts };
8044
+ } catch {
8045
+ return null;
8046
+ }
8047
+ }
8048
+ function writeCachedBoardSlice(cwd, slice) {
8049
+ const path2 = boardSliceCachePath(cwd);
8050
+ const tmp = `${path2}.${process.pid}.tmp`;
8051
+ try {
8052
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(path2), { recursive: true });
8053
+ (0, import_node_fs14.writeFileSync)(tmp, JSON.stringify(slice));
8054
+ (0, import_node_fs14.renameSync)(tmp, path2);
8055
+ } catch {
8056
+ try {
8057
+ (0, import_node_fs14.rmSync)(tmp, { force: true });
8058
+ } catch {
8059
+ }
8060
+ }
8061
+ }
8062
+ function boardSliceCacheStale(cached, now, ttlMs = BOARD_SLICE_CACHE_TTL_MS) {
8063
+ return !cached || now - cached.ts >= ttlMs;
8064
+ }
8065
+
8012
8066
  // src/board-slice.ts
8013
8067
  var SESSION_START_BOARD_TIMEOUT_MS = 3e3;
8068
+ var BOARD_SLICE_REFRESH_TIMEOUT_MS = 3e4;
8014
8069
  var BOARD_SLICE_FRAMING = "BOARD SLICE \u2014 reconcile with saga HEAD; board Status is authoritative for work lifecycle.";
8070
+ var BOARD_SLICE_EMPTY = "Board: nothing assigned or claimable right now \u2014 `mmi-cli board read` (or `/mmi`) for the full board.";
8071
+ var BOARD_SLICE_PENDING = "Board: loading your slice in the background \u2014 run `/mmi` now; it renders here next session.";
8015
8072
  var BOARD_SLICE_MAX_LINES = 5;
8016
8073
  var ACTIVE_STATUS_ORDER = {
8017
8074
  "In Progress": 0,
@@ -8072,12 +8129,25 @@ async function withTimeout(promise, ms) {
8072
8129
  }
8073
8130
  }
8074
8131
  async function runBoardSlice(io, deps, opts) {
8132
+ const cwd = deps.cwd ?? process.cwd();
8133
+ const now = deps.now ?? Date.now();
8134
+ let cfg;
8135
+ try {
8136
+ cfg = await deps.loadConfig();
8137
+ } catch {
8138
+ return;
8139
+ }
8140
+ if (!boardMetaConfigured(cfg)) return;
8141
+ const cached = readCachedBoardSlice(cwd);
8142
+ if (boardSliceCacheStale(cached, now)) deps.scheduleRefresh?.();
8143
+ if (cached) {
8144
+ io.log(cached.block ?? BOARD_SLICE_EMPTY);
8145
+ return;
8146
+ }
8075
8147
  const budget = deps.timeoutMs ?? SESSION_START_BOARD_TIMEOUT_MS;
8076
8148
  const controller = new AbortController();
8077
8149
  const deadline = setTimeout(() => controller.abort(), budget);
8078
8150
  try {
8079
- const cfg = await deps.loadConfig();
8080
- if (!boardMetaConfigured(cfg)) return;
8081
8151
  const client = deps.client ?? createGitHubClient({ defaultTimeoutMs: budget, signal: controller.signal });
8082
8152
  const report = await withTimeout(
8083
8153
  deps.readBoard({ config: cfg, allowPartial: true }, { client }),
@@ -8085,7 +8155,26 @@ async function runBoardSlice(io, deps, opts) {
8085
8155
  );
8086
8156
  if (opts?.expectedLogin && report.viewer && opts.expectedLogin !== report.viewer) return;
8087
8157
  const block = renderBoardSlice(report);
8088
- if (block) io.log(block);
8158
+ writeCachedBoardSlice(cwd, { block, ts: now });
8159
+ io.log(block ?? BOARD_SLICE_EMPTY);
8160
+ } catch {
8161
+ io.log(BOARD_SLICE_PENDING);
8162
+ } finally {
8163
+ clearTimeout(deadline);
8164
+ }
8165
+ }
8166
+ async function refreshBoardSliceCache(deps) {
8167
+ const cwd = deps.cwd ?? process.cwd();
8168
+ const now = deps.now ?? Date.now();
8169
+ const budget = deps.timeoutMs ?? BOARD_SLICE_REFRESH_TIMEOUT_MS;
8170
+ const controller = new AbortController();
8171
+ const deadline = setTimeout(() => controller.abort(), budget);
8172
+ try {
8173
+ const cfg = await deps.loadConfig();
8174
+ if (!boardMetaConfigured(cfg)) return;
8175
+ const client = deps.client ?? createGitHubClient({ defaultTimeoutMs: budget, signal: controller.signal });
8176
+ const report = await withTimeout(deps.readBoard({ config: cfg, allowPartial: true }, { client }), budget);
8177
+ writeCachedBoardSlice(cwd, { block: renderBoardSlice(report), ts: now });
8089
8178
  } catch {
8090
8179
  } finally {
8091
8180
  clearTimeout(deadline);
@@ -8093,8 +8182,8 @@ async function runBoardSlice(io, deps, opts) {
8093
8182
  }
8094
8183
 
8095
8184
  // src/worktree.ts
8096
- var import_node_fs14 = require("node:fs");
8097
- var import_node_path12 = require("node:path");
8185
+ var import_node_fs15 = require("node:fs");
8186
+ var import_node_path13 = require("node:path");
8098
8187
  var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
8099
8188
  var PKG = "package.json";
8100
8189
  var LOCKFILE = "package-lock.json";
@@ -8102,21 +8191,21 @@ var NODE_MODULES = "node_modules";
8102
8191
  var realFsProbe = {
8103
8192
  isDir: (p) => {
8104
8193
  try {
8105
- return (0, import_node_fs14.statSync)(p).isDirectory();
8194
+ return (0, import_node_fs15.statSync)(p).isDirectory();
8106
8195
  } catch {
8107
8196
  return false;
8108
8197
  }
8109
8198
  },
8110
8199
  isFile: (p) => {
8111
8200
  try {
8112
- return (0, import_node_fs14.statSync)(p).isFile();
8201
+ return (0, import_node_fs15.statSync)(p).isFile();
8113
8202
  } catch {
8114
8203
  return false;
8115
8204
  }
8116
8205
  },
8117
8206
  listDirs: (p) => {
8118
8207
  try {
8119
- return (0, import_node_fs14.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
8208
+ return (0, import_node_fs15.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
8120
8209
  } catch {
8121
8210
  return [];
8122
8211
  }
@@ -8124,12 +8213,12 @@ var realFsProbe = {
8124
8213
  };
8125
8214
  function scanInstallDirs(root, fs2 = realFsProbe) {
8126
8215
  const factsFor = (dir) => {
8127
- const abs = dir ? (0, import_node_path12.join)(root, dir) : root;
8216
+ const abs = dir ? (0, import_node_path13.join)(root, dir) : root;
8128
8217
  return {
8129
8218
  dir,
8130
- hasPackageJson: fs2.isFile((0, import_node_path12.join)(abs, PKG)),
8131
- hasLockfile: fs2.isFile((0, import_node_path12.join)(abs, LOCKFILE)),
8132
- hasNodeModules: fs2.isDir((0, import_node_path12.join)(abs, NODE_MODULES))
8219
+ hasPackageJson: fs2.isFile((0, import_node_path13.join)(abs, PKG)),
8220
+ hasLockfile: fs2.isFile((0, import_node_path13.join)(abs, LOCKFILE)),
8221
+ hasNodeModules: fs2.isDir((0, import_node_path13.join)(abs, NODE_MODULES))
8133
8222
  };
8134
8223
  };
8135
8224
  const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
@@ -8139,7 +8228,7 @@ function npmInstallTargets(dirs) {
8139
8228
  return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
8140
8229
  }
8141
8230
  function isLinkedWorktree(root, fs2 = realFsProbe) {
8142
- return fs2.isFile((0, import_node_path12.join)(root, ".git"));
8231
+ return fs2.isFile((0, import_node_path13.join)(root, ".git"));
8143
8232
  }
8144
8233
  function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
8145
8234
  if (!isLinkedWorktree(root, fs2)) return null;
@@ -8149,8 +8238,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
8149
8238
  return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
8150
8239
  }
8151
8240
  function defaultCopyFile(from, to) {
8152
- (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(to), { recursive: true });
8153
- (0, import_node_fs14.copyFileSync)(from, to);
8241
+ (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(to), { recursive: true });
8242
+ (0, import_node_fs15.copyFileSync)(from, to);
8154
8243
  }
8155
8244
  async function provisionWorktree(worktreeRoot, deps) {
8156
8245
  const fs2 = deps.fs ?? realFsProbe;
@@ -8162,7 +8251,7 @@ async function provisionWorktree(worktreeRoot, deps) {
8162
8251
  const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
8163
8252
  const installed = [];
8164
8253
  for (const target of targets) {
8165
- const cwd = target.dir ? (0, import_node_path12.join)(worktreeRoot, target.dir) : worktreeRoot;
8254
+ const cwd = target.dir ? (0, import_node_path13.join)(worktreeRoot, target.dir) : worktreeRoot;
8166
8255
  log(`installing deps: ${target.command} in ${target.dir || "."}`);
8167
8256
  await deps.runInstall(target.command, cwd);
8168
8257
  installed.push(target);
@@ -8171,7 +8260,7 @@ async function provisionWorktree(worktreeRoot, deps) {
8171
8260
  const copySkipped = [];
8172
8261
  const primary = await deps.primaryCheckout();
8173
8262
  for (const rel of LOCAL_ONLY_FILES) {
8174
- const dest = (0, import_node_path12.join)(worktreeRoot, rel);
8263
+ const dest = (0, import_node_path13.join)(worktreeRoot, rel);
8175
8264
  if (fs2.isFile(dest)) {
8176
8265
  copySkipped.push({ file: rel, reason: "already-present" });
8177
8266
  continue;
@@ -8180,11 +8269,11 @@ async function provisionWorktree(worktreeRoot, deps) {
8180
8269
  copySkipped.push({ file: rel, reason: "no-primary" });
8181
8270
  continue;
8182
8271
  }
8183
- if (!fs2.isFile((0, import_node_path12.join)(primary, rel))) {
8272
+ if (!fs2.isFile((0, import_node_path13.join)(primary, rel))) {
8184
8273
  copySkipped.push({ file: rel, reason: "absent-in-primary" });
8185
8274
  continue;
8186
8275
  }
8187
- copyFile((0, import_node_path12.join)(primary, rel), dest);
8276
+ copyFile((0, import_node_path13.join)(primary, rel), dest);
8188
8277
  copied.push(rel);
8189
8278
  log(`copied local config: ${rel}`);
8190
8279
  }
@@ -8192,7 +8281,7 @@ async function provisionWorktree(worktreeRoot, deps) {
8192
8281
  }
8193
8282
  function defaultWorktreePath(repoRoot, branch) {
8194
8283
  const safe = branch.replace(/[/\\]+/g, "-");
8195
- return (0, import_node_path12.join)((0, import_node_path12.dirname)(repoRoot), "mmi-worktrees", safe);
8284
+ return (0, import_node_path13.join)((0, import_node_path13.dirname)(repoRoot), "mmi-worktrees", safe);
8196
8285
  }
8197
8286
 
8198
8287
  // src/frontmatter.ts
@@ -8390,7 +8479,7 @@ async function runNorthstarContext(io, deps) {
8390
8479
  }
8391
8480
 
8392
8481
  // src/index.ts
8393
- var import_node_path19 = require("node:path");
8482
+ var import_node_path20 = require("node:path");
8394
8483
 
8395
8484
  // src/merge-ci-policy.ts
8396
8485
  function resolveMergeCiPolicy(input) {
@@ -8691,6 +8780,11 @@ function diffManagedGitignoreBlock(current) {
8691
8780
  seeded: false
8692
8781
  };
8693
8782
  }
8783
+ function planManagedGitignore(current) {
8784
+ const { content, changed } = upsertManagedGitignoreBlock(current);
8785
+ const { added, removed } = diffManagedGitignoreBlock(current);
8786
+ return { changed, content, added, removed };
8787
+ }
8694
8788
 
8695
8789
  // src/project-model.ts
8696
8790
  var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable", "cli-tool", "worker"];
@@ -9589,7 +9683,7 @@ var import_node_os6 = require("node:os");
9589
9683
  // src/gh-create.ts
9590
9684
  var import_promises5 = require("node:fs/promises");
9591
9685
  var import_node_os4 = require("node:os");
9592
- var import_node_path13 = require("node:path");
9686
+ var import_node_path14 = require("node:path");
9593
9687
  var import_node_crypto6 = require("node:crypto");
9594
9688
  var ISSUE_TYPES = ["bug", "feature", "task"];
9595
9689
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -9630,7 +9724,7 @@ async function bodyArgsViaFile(args, deps = {}) {
9630
9724
  } };
9631
9725
  const write = deps.write ?? import_promises5.writeFile;
9632
9726
  const remove = deps.remove ?? import_promises5.unlink;
9633
- const file = (0, import_node_path13.join)(deps.dir ?? (0, import_node_os4.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.md`);
9727
+ const file = (0, import_node_path14.join)(deps.dir ?? (0, import_node_os4.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.md`);
9634
9728
  await write(file, args[i + 1], "utf8");
9635
9729
  return {
9636
9730
  args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
@@ -9672,6 +9766,44 @@ function buildPrArgs({ title, body, base: base2, head, repo }) {
9672
9766
  return args;
9673
9767
  }
9674
9768
 
9769
+ // src/issue-check.ts
9770
+ var CHECKLIST_RE = /^([ \t]*[-*+] \[)([ xX])(\] )(.*)$/gm;
9771
+ function findChecklistItems(body) {
9772
+ const items = [];
9773
+ for (const m of body.matchAll(CHECKLIST_RE)) {
9774
+ const prefix = m[1];
9775
+ const marker = m[2];
9776
+ const text = m[4].replace(/\r$/, "");
9777
+ items.push({
9778
+ markerIndex: (m.index ?? 0) + prefix.length,
9779
+ checked: marker.toLowerCase() === "x",
9780
+ text
9781
+ });
9782
+ }
9783
+ return items;
9784
+ }
9785
+ function selectChecklistItem(items, query) {
9786
+ const q = query.trim();
9787
+ if (!q) return { ok: false, reason: "not-found" };
9788
+ const exact = items.filter((it) => it.text.trim() === q);
9789
+ if (exact.length === 1) return { ok: true, item: exact[0] };
9790
+ if (exact.length > 1) return { ok: false, reason: "ambiguous", matches: exact };
9791
+ const sub = items.filter((it) => it.text.includes(q));
9792
+ if (sub.length === 1) return { ok: true, item: sub[0] };
9793
+ if (sub.length === 0) return { ok: false, reason: "not-found" };
9794
+ return { ok: false, reason: "ambiguous", matches: sub };
9795
+ }
9796
+ function setChecklistMarker(body, item, checked) {
9797
+ if (item.checked === checked) return { body, changed: false };
9798
+ const target = checked ? "x" : " ";
9799
+ return { body: body.slice(0, item.markerIndex) + target + body.slice(item.markerIndex + 1), changed: true };
9800
+ }
9801
+ function applyChecklistCheck(body, query, checked) {
9802
+ const sel = selectChecklistItem(findChecklistItems(body), query);
9803
+ if (!sel.ok) return sel;
9804
+ return { ok: true, edit: setChecklistMarker(body, sel.item, checked), item: sel.item };
9805
+ }
9806
+
9675
9807
  // src/command-manifest.ts
9676
9808
  function buildArgument(arg) {
9677
9809
  const out = { name: arg.name(), required: arg.required, variadic: arg.variadic };
@@ -11193,9 +11325,9 @@ function decideStage(inputs) {
11193
11325
 
11194
11326
  // src/cursor-plugin-seed.ts
11195
11327
  var import_node_child_process8 = require("node:child_process");
11196
- var import_node_fs15 = require("node:fs");
11328
+ var import_node_fs16 = require("node:fs");
11197
11329
  var import_node_os5 = require("node:os");
11198
- var import_node_path14 = require("node:path");
11330
+ var import_node_path15 = require("node:path");
11199
11331
  var import_node_util6 = require("node:util");
11200
11332
  function isSemverVersion(v) {
11201
11333
  return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
@@ -11212,17 +11344,17 @@ function ghReleaseTarballApiArgs(tag) {
11212
11344
  }
11213
11345
  function cursorUserGlobalStatePath() {
11214
11346
  if (process.platform === "win32") {
11215
- const base2 = process.env.APPDATA || (0, import_node_path14.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
11216
- return (0, import_node_path14.join)(base2, "Cursor", "User", "globalStorage", "state.vscdb");
11347
+ const base2 = process.env.APPDATA || (0, import_node_path15.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
11348
+ return (0, import_node_path15.join)(base2, "Cursor", "User", "globalStorage", "state.vscdb");
11217
11349
  }
11218
11350
  if (process.platform === "darwin") {
11219
- return (0, import_node_path14.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
11351
+ return (0, import_node_path15.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
11220
11352
  }
11221
- return (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
11353
+ return (0, import_node_path15.join)((0, import_node_os5.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
11222
11354
  }
11223
11355
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
11224
11356
  const dbPath = cursorUserGlobalStatePath();
11225
- if (!(0, import_node_fs15.existsSync)(dbPath)) return void 0;
11357
+ if (!(0, import_node_fs16.existsSync)(dbPath)) return void 0;
11226
11358
  try {
11227
11359
  const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
11228
11360
  timeout: 5e3
@@ -11236,57 +11368,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
11236
11368
  }
11237
11369
  }
11238
11370
  function syncDirContents(src, dest) {
11239
- (0, import_node_fs15.mkdirSync)(dest, { recursive: true });
11240
- for (const name of (0, import_node_fs15.readdirSync)(dest)) {
11241
- (0, import_node_fs15.rmSync)((0, import_node_path14.join)(dest, name), { recursive: true, force: true });
11371
+ (0, import_node_fs16.mkdirSync)(dest, { recursive: true });
11372
+ for (const name of (0, import_node_fs16.readdirSync)(dest)) {
11373
+ (0, import_node_fs16.rmSync)((0, import_node_path15.join)(dest, name), { recursive: true, force: true });
11242
11374
  }
11243
- (0, import_node_fs15.cpSync)(src, dest, { recursive: true });
11375
+ (0, import_node_fs16.cpSync)(src, dest, { recursive: true });
11244
11376
  }
11245
11377
  function releaseTag(releasedVersion) {
11246
11378
  return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
11247
11379
  }
11248
11380
  async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
11249
- const tarFile = (0, import_node_path14.join)(tmpRoot, "archive.tar");
11381
+ const tarFile = (0, import_node_path15.join)(tmpRoot, "archive.tar");
11250
11382
  try {
11251
11383
  await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
11252
11384
  await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
11253
11385
  timeout: 6e4
11254
11386
  });
11255
11387
  await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
11256
- const pluginMmi = (0, import_node_path14.join)(tmpRoot, "plugins", "mmi");
11257
- return (0, import_node_fs15.existsSync)((0, import_node_path14.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
11388
+ const pluginMmi = (0, import_node_path15.join)(tmpRoot, "plugins", "mmi");
11389
+ return (0, import_node_fs16.existsSync)((0, import_node_path15.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
11258
11390
  } catch {
11259
11391
  return void 0;
11260
11392
  }
11261
11393
  }
11262
11394
  async function downloadPluginMmiViaGh(tag, tmpRoot) {
11263
- const tarPath = (0, import_node_path14.join)(tmpRoot, "repo.tgz");
11395
+ const tarPath = (0, import_node_path15.join)(tmpRoot, "repo.tgz");
11264
11396
  try {
11265
- (0, import_node_fs15.mkdirSync)(tmpRoot, { recursive: true });
11397
+ (0, import_node_fs16.mkdirSync)(tmpRoot, { recursive: true });
11266
11398
  const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
11267
11399
  timeout: 12e4,
11268
11400
  maxBuffer: 100 * 1024 * 1024,
11269
11401
  encoding: "buffer",
11270
11402
  windowsHide: true
11271
11403
  });
11272
- (0, import_node_fs15.writeFileSync)(tarPath, stdout);
11404
+ (0, import_node_fs16.writeFileSync)(tarPath, stdout);
11273
11405
  await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
11274
- const top = (0, import_node_fs15.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
11406
+ const top = (0, import_node_fs16.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
11275
11407
  if (!top) return void 0;
11276
- const pluginMmi = (0, import_node_path14.join)(tmpRoot, top, "plugins", "mmi");
11277
- return (0, import_node_fs15.existsSync)((0, import_node_path14.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
11408
+ const pluginMmi = (0, import_node_path15.join)(tmpRoot, top, "plugins", "mmi");
11409
+ return (0, import_node_fs16.existsSync)((0, import_node_path15.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
11278
11410
  } catch {
11279
11411
  return void 0;
11280
11412
  }
11281
11413
  }
11282
11414
  async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
11283
- (0, import_node_fs15.mkdirSync)(tmpRoot, { recursive: true });
11415
+ (0, import_node_fs16.mkdirSync)(tmpRoot, { recursive: true });
11284
11416
  const tag = releaseTag(releasedVersion);
11285
11417
  if (hubCheckout) {
11286
11418
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
11287
11419
  if (fromHub) return fromHub;
11288
11420
  }
11289
- return downloadPluginMmiViaGh(tag, (0, import_node_path14.join)(tmpRoot, "gh"));
11421
+ return downloadPluginMmiViaGh(tag, (0, import_node_path15.join)(tmpRoot, "gh"));
11290
11422
  }
11291
11423
  function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
11292
11424
  if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
@@ -11307,7 +11439,7 @@ async function applyCursorPluginCacheSeed(input) {
11307
11439
  for (const pin of pinsToSeed) {
11308
11440
  syncDirContents(source, pin.path);
11309
11441
  }
11310
- (0, import_node_fs15.rmSync)(tmpRoot, { recursive: true, force: true });
11442
+ (0, import_node_fs16.rmSync)(tmpRoot, { recursive: true, force: true });
11311
11443
  return true;
11312
11444
  }
11313
11445
 
@@ -11348,6 +11480,17 @@ function pluginInstallManualFix(projectPath, surface = "claude-cli") {
11348
11480
  function isMmiPluginEnabled(settings) {
11349
11481
  return Boolean(settings?.enabledPlugins?.[MMI_PLUGIN_ID]);
11350
11482
  }
11483
+ function buildSettingsPluginDriftCheck(input) {
11484
+ const base2 = {
11485
+ ok: true,
11486
+ label: "org plugin wiring in .claude/settings.json (mmi@mutmutco)",
11487
+ fix: "the Claude Code app pruned mmi@mutmutco from the tracked .claude/settings.json (it does this at session start when the mutmutco marketplace source does not resolve, #1805); restore it with `git checkout -- .claude/settings.json` before committing, or the whole org skill set is disabled for the branch"
11488
+ };
11489
+ const enabled = input.settings?.enabledPlugins;
11490
+ if (!input.isOrgRepo || !enabled || Object.keys(enabled).length === 0) return base2;
11491
+ if (!enabled[MMI_PLUGIN_ID]) return { ...base2, ok: false };
11492
+ return base2;
11493
+ }
11351
11494
  function hasProjectInstallRecord(file, pluginId, projectPath) {
11352
11495
  const records = file?.plugins?.[pluginId];
11353
11496
  if (!Array.isArray(records)) return false;
@@ -11913,8 +12056,8 @@ function stageLiveUpSteps(t) {
11913
12056
  command: `gh ${ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref ?? "<branch>", stage: "dev" }).join(" ")}`
11914
12057
  },
11915
12058
  {
11916
- label: "record your IP as the dev allowlist (box writes /opt/mmi/<slug>/dev/allowlist + reloads Caddy)",
11917
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip: "<your ip>" }).join(" ")}`
12059
+ label: `gate ${t.host} to your IP at the Cloudflare edge (ephemeral firewall_custom skip rule)`,
12060
+ command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-allow", host: t.host, ip: "<your ip>" }).join(" ")}`
11918
12061
  },
11919
12062
  { label: "tear down when done", command: "mmi-cli stage --live --down --apply" }
11920
12063
  ];
@@ -11926,8 +12069,8 @@ function stageLiveDownSteps(t) {
11926
12069
  command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }).join(" ")}`
11927
12070
  },
11928
12071
  {
11929
- label: "clear the dev allowlist (the stage goes dark even if restarted)",
11930
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip: "clear" }).join(" ")}`
12072
+ label: "remove the Cloudflare edge gate (the stage goes dark even if restarted)",
12073
+ command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-clear", host: t.host }).join(" ")}`
11931
12074
  }
11932
12075
  ];
11933
12076
  }
@@ -11935,8 +12078,9 @@ async function runStageLiveUp(deps, t) {
11935
12078
  if (!t.ref?.trim()) throw new Error("stage --live: cannot resolve the current branch to deploy");
11936
12079
  const ip = (await deps.detectIp()).trim();
11937
12080
  if (!validStageLiveIp(ip)) throw new Error(`stage --live: detected public IP is not a literal IPv4/IPv6 address: "${ip.slice(0, 80)}"`);
12081
+ if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
11938
12082
  await deps.run("gh", ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref, stage: "dev" }));
11939
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip }));
12083
+ await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-allow", host: t.host, ip }));
11940
12084
  return {
11941
12085
  command: "stage --live",
11942
12086
  mode: "up",
@@ -11945,25 +12089,26 @@ async function runStageLiveUp(deps, t) {
11945
12089
  ref: t.ref,
11946
12090
  ip,
11947
12091
  dispatched: ["tenant-deploy.yml", "tenant-control.yml"],
11948
- message: `dispatched the dev deploy of ${t.ref} and the allowlist update for ${ip}; watch the runs in ${STAGE_LIVE_HUB_REPO} Actions \u2014 tear down with: mmi-cli stage --live --down --apply`
12092
+ message: `dispatched the dev deploy of ${t.ref} and the Cloudflare edge gate for ${t.host} \u2192 ${ip}; watch the runs in ${STAGE_LIVE_HUB_REPO} Actions \u2014 tear down with: mmi-cli stage --live --down --apply`
11949
12093
  };
11950
12094
  }
11951
12095
  async function runStageLiveDown(deps, t) {
12096
+ if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
11952
12097
  await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }));
11953
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip: "clear" }));
12098
+ await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-clear", host: t.host }));
11954
12099
  return {
11955
12100
  command: "stage --live",
11956
12101
  mode: "down",
11957
12102
  slug: t.slug,
11958
12103
  repo: t.repo,
11959
12104
  dispatched: ["tenant-control.yml", "tenant-control.yml"],
11960
- message: `dispatched the dev stop and allowlist clear for ${t.slug}; the dev stage is dark until the next mmi-cli stage --live --apply`
12105
+ message: `dispatched the dev stop and the Cloudflare edge gate clear for ${t.host}; the dev stage is dark until the next mmi-cli stage --live --apply`
11961
12106
  };
11962
12107
  }
11963
12108
 
11964
12109
  // src/design-system.ts
11965
- var import_node_fs16 = require("node:fs");
11966
- var import_node_path15 = require("node:path");
12110
+ var import_node_fs17 = require("node:fs");
12111
+ var import_node_path16 = require("node:path");
11967
12112
  var UI_PACKAGE_CANDIDATES = ["@mutmutco/ui-dashboard", "@mutmutco/ui", "@mutmutco/theme"];
11968
12113
  var DESIGN_SYSTEM_VERSION_LABEL = "@mutmutco design-system npm package (vs @latest)";
11969
12114
  function dashboardConsumerRegistryFix(error) {
@@ -12012,17 +12157,17 @@ function buildDesignSystemVersionCheck(input) {
12012
12157
  }
12013
12158
  function readJsonFile(path2) {
12014
12159
  try {
12015
- return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
12160
+ return JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8"));
12016
12161
  } catch {
12017
12162
  return void 0;
12018
12163
  }
12019
12164
  }
12020
12165
  function isUiFactoryCheckout(root) {
12021
- const pkg = readJsonFile((0, import_node_path15.join)(root, "package.json"));
12166
+ const pkg = readJsonFile((0, import_node_path16.join)(root, "package.json"));
12022
12167
  return pkg?.name === "mmd-ui" && pkg?.private === true;
12023
12168
  }
12024
12169
  function resolveDeclaredUiPackage(root) {
12025
- const pkg = readJsonFile((0, import_node_path15.join)(root, "package.json"));
12170
+ const pkg = readJsonFile((0, import_node_path16.join)(root, "package.json"));
12026
12171
  if (!pkg) return void 0;
12027
12172
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
12028
12173
  for (const name of UI_PACKAGE_CANDIDATES) {
@@ -12032,8 +12177,8 @@ function resolveDeclaredUiPackage(root) {
12032
12177
  return void 0;
12033
12178
  }
12034
12179
  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;
12180
+ const lockPath = (0, import_node_path16.join)(root, "package-lock.json");
12181
+ if (!(0, import_node_fs17.existsSync)(lockPath)) return void 0;
12037
12182
  const lock = readJsonFile(lockPath);
12038
12183
  const node = lock?.packages?.[`node_modules/${packageName}`];
12039
12184
  const version = node?.version?.trim();
@@ -12062,15 +12207,15 @@ function designSystemSnapshot(root) {
12062
12207
 
12063
12208
  // src/design-system-registry.ts
12064
12209
  var import_node_crypto7 = require("node:crypto");
12065
- var import_node_fs18 = require("node:fs");
12066
- var import_node_path16 = require("node:path");
12210
+ var import_node_fs19 = require("node:fs");
12211
+ var import_node_path17 = require("node:path");
12067
12212
 
12068
12213
  // src/atomic-write.ts
12069
- var import_node_fs17 = require("node:fs");
12214
+ var import_node_fs18 = require("node:fs");
12070
12215
  function atomicWriteFileSync(path2, content) {
12071
12216
  const tmp = `${path2}.${process.pid}.tmp`;
12072
- (0, import_node_fs17.writeFileSync)(tmp, content, "utf8");
12073
- (0, import_node_fs17.renameSync)(tmp, path2);
12217
+ (0, import_node_fs18.writeFileSync)(tmp, content, "utf8");
12218
+ (0, import_node_fs18.renameSync)(tmp, path2);
12074
12219
  }
12075
12220
 
12076
12221
  // src/design-system-registry.ts
@@ -12081,13 +12226,13 @@ var REGISTRY_FIX = "run `mmi-cli doctor --apply` to pull registry components int
12081
12226
  var REGISTRY_UNREACHABLE_FIX = "live @mutmutco registry unreachable \u2014 verify `components.json` `@mutmutco` registry URL and network, then retry `mmi-cli doctor`";
12082
12227
  function readJsonFile2(path2) {
12083
12228
  try {
12084
- return JSON.parse((0, import_node_fs18.readFileSync)(path2, "utf8"));
12229
+ return JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
12085
12230
  } catch {
12086
12231
  return void 0;
12087
12232
  }
12088
12233
  }
12089
12234
  function readComponentsJson(root) {
12090
- return readJsonFile2((0, import_node_path16.join)(root, "components.json"));
12235
+ return readJsonFile2((0, import_node_path17.join)(root, "components.json"));
12091
12236
  }
12092
12237
  function hasMutmutcoRegistry(root) {
12093
12238
  const url = readComponentsJson(root)?.registries?.["@mutmutco"];
@@ -12095,7 +12240,7 @@ function hasMutmutcoRegistry(root) {
12095
12240
  }
12096
12241
  function resolveCacheDir(root) {
12097
12242
  const custom = readComponentsJson(root)?.mmi?.cacheDir;
12098
- return (0, import_node_path16.join)(root, custom ?? DESIGN_SYSTEM_CACHE_DIR);
12243
+ return (0, import_node_path17.join)(root, custom ?? DESIGN_SYSTEM_CACHE_DIR);
12099
12244
  }
12100
12245
  function resolveRegistryUrlTemplate(root) {
12101
12246
  return readComponentsJson(root)?.registries?.["@mutmutco"];
@@ -12104,7 +12249,7 @@ function registryItemUrl(template, name) {
12104
12249
  return template.replace("{name}", name);
12105
12250
  }
12106
12251
  function readDesignSystemManifest(root) {
12107
- const raw = readJsonFile2((0, import_node_path16.join)(root, DESIGN_SYSTEM_MANIFEST_PATH));
12252
+ const raw = readJsonFile2((0, import_node_path17.join)(root, DESIGN_SYSTEM_MANIFEST_PATH));
12108
12253
  if (!raw || !Array.isArray(raw.components)) return void 0;
12109
12254
  return raw;
12110
12255
  }
@@ -12116,11 +12261,11 @@ function listInstalledRegistryComponents(root) {
12116
12261
  return scanCachedComponentNames(resolveCacheDir(root));
12117
12262
  }
12118
12263
  function scanCachedComponentNames(cacheDir) {
12119
- if (!(0, import_node_fs18.existsSync)(cacheDir)) return [];
12264
+ if (!(0, import_node_fs19.existsSync)(cacheDir)) return [];
12120
12265
  const names = /* @__PURE__ */ new Set();
12121
12266
  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);
12267
+ for (const ent of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
12268
+ const p = (0, import_node_path17.join)(dir, ent.name);
12124
12269
  if (ent.isDirectory()) walk(p);
12125
12270
  else if (ent.isFile() && /\.(tsx?|jsx?)$/.test(ent.name)) {
12126
12271
  names.add(ent.name.replace(/\.(tsx|ts|jsx|js)$/, ""));
@@ -12209,13 +12354,13 @@ async function gatherRegistryComponentsState(root, targetVersion, deps) {
12209
12354
  let componentStale = false;
12210
12355
  for (const file of item.files) {
12211
12356
  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)) {
12357
+ const cachePath = (0, import_node_path17.join)(cacheDir, cacheRelativePath(file.target));
12358
+ if (!(0, import_node_fs19.existsSync)(cachePath)) {
12214
12359
  componentStale = true;
12215
12360
  break;
12216
12361
  }
12217
12362
  try {
12218
- if (contentHash((0, import_node_fs18.readFileSync)(cachePath, "utf8")) !== contentHash(file.content)) {
12363
+ if (contentHash((0, import_node_fs19.readFileSync)(cachePath, "utf8")) !== contentHash(file.content)) {
12219
12364
  componentStale = true;
12220
12365
  break;
12221
12366
  }
@@ -12225,7 +12370,7 @@ async function gatherRegistryComponentsState(root, targetVersion, deps) {
12225
12370
  }
12226
12371
  }
12227
12372
  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`))) {
12373
+ if ((0, import_node_fs19.existsSync)((0, import_node_path17.join)(cacheDir, "ui", `${name}.tsx`)) || (0, import_node_fs19.existsSync)((0, import_node_path17.join)(cacheDir, `${name}.tsx`))) {
12229
12374
  stale.push(name);
12230
12375
  } else {
12231
12376
  missing.push(name);
@@ -12253,15 +12398,15 @@ async function applyRegistryComponentsSync(root, components, targetVersion, log,
12253
12398
  if (!item) return { ok: false };
12254
12399
  for (const file of item.files) {
12255
12400
  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));
12401
+ const outPath = (0, import_node_path17.join)(cacheDir, cacheRelativePath(file.target));
12402
+ deps.mkdir((0, import_node_path17.dirname)(outPath));
12258
12403
  const body = file.content.endsWith("\n") ? file.content : `${file.content}
12259
12404
  `;
12260
12405
  deps.writeFile(outPath, body);
12261
12406
  }
12262
12407
  }
12263
- const manifestPath = (0, import_node_path16.join)(root, DESIGN_SYSTEM_MANIFEST_PATH);
12264
- deps.mkdir((0, import_node_path16.dirname)(manifestPath));
12408
+ const manifestPath = (0, import_node_path17.join)(root, DESIGN_SYSTEM_MANIFEST_PATH);
12409
+ deps.mkdir((0, import_node_path17.dirname)(manifestPath));
12265
12410
  const manifest = {
12266
12411
  version: targetVersion,
12267
12412
  syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12276,14 +12421,14 @@ function defaultRegistrySyncDeps() {
12276
12421
  return {
12277
12422
  fetch,
12278
12423
  writeFile: (path2, content) => atomicWriteFileSync(path2, content),
12279
- mkdir: (path2) => (0, import_node_fs18.mkdirSync)(path2, { recursive: true })
12424
+ mkdir: (path2) => (0, import_node_fs19.mkdirSync)(path2, { recursive: true })
12280
12425
  };
12281
12426
  }
12282
12427
 
12283
12428
  // src/stage-runner.ts
12284
12429
  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");
12430
+ var import_node_fs20 = require("node:fs");
12431
+ var import_node_path18 = require("node:path");
12287
12432
  var import_node_net2 = require("node:net");
12288
12433
  var import_node_util7 = require("node:util");
12289
12434
  var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process9.execFile);
@@ -12328,7 +12473,7 @@ function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
12328
12473
  return void 0;
12329
12474
  }
12330
12475
  function stageStatePath(cwd = process.cwd()) {
12331
- return (0, import_node_path17.join)(cwd, "tmp", "stage", "state.json");
12476
+ return (0, import_node_path18.join)(cwd, "tmp", "stage", "state.json");
12332
12477
  }
12333
12478
  function mergeEnvSecretsIntoFile(content, secrets) {
12334
12479
  const lines = content.split(/\r?\n/);
@@ -12411,9 +12556,9 @@ async function shell(command, cwd, timeoutMs) {
12411
12556
  });
12412
12557
  }
12413
12558
  function readState(path2) {
12414
- if (!(0, import_node_fs19.existsSync)(path2)) return null;
12559
+ if (!(0, import_node_fs20.existsSync)(path2)) return null;
12415
12560
  try {
12416
- return JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
12561
+ return JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
12417
12562
  } catch {
12418
12563
  return null;
12419
12564
  }
@@ -12465,7 +12610,7 @@ async function stopStage(opts = {}) {
12465
12610
  return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
12466
12611
  }
12467
12612
  await killTree(state.pid);
12468
- (0, import_node_fs19.rmSync)(statePath, { force: true });
12613
+ (0, import_node_fs20.rmSync)(statePath, { force: true });
12469
12614
  return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
12470
12615
  }
12471
12616
  async function startStage(config = {}, opts = {}) {
@@ -12474,7 +12619,7 @@ async function startStage(config = {}, opts = {}) {
12474
12619
  const cwd = opts.cwd ?? process.cwd();
12475
12620
  const statePath = opts.statePath ?? stageStatePath(cwd);
12476
12621
  const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
12477
- (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
12622
+ (0, import_node_fs20.mkdirSync)(dir, { recursive: true });
12478
12623
  let stagePort;
12479
12624
  if (config.portRange) {
12480
12625
  const [s, e] = config.portRange;
@@ -12484,14 +12629,14 @@ async function startStage(config = {}, opts = {}) {
12484
12629
  }
12485
12630
  const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
12486
12631
  if (config.ensureEnv) {
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
12632
+ const target = (0, import_node_path18.join)(cwd, config.ensureEnv.target);
12633
+ const example = (0, import_node_path18.join)(cwd, config.ensureEnv.example);
12634
+ if (!(0, import_node_fs20.existsSync)(target) && (0, import_node_fs20.existsSync)(example)) {
12635
+ (0, import_node_fs20.copyFileSync)(example, target);
12636
+ } else if ((0, import_node_fs20.existsSync)(target) && (0, import_node_fs20.existsSync)(example)) {
12637
+ const stale = detectStaleEnvFile((0, import_node_fs20.readFileSync)(example, "utf8"), (0, import_node_fs20.readFileSync)(target, "utf8"), {
12638
+ exampleMtimeMs: (0, import_node_fs20.statSync)(example).mtimeMs,
12639
+ targetMtimeMs: (0, import_node_fs20.statSync)(target).mtimeMs
12495
12640
  });
12496
12641
  if (stale) {
12497
12642
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -12499,8 +12644,8 @@ async function startStage(config = {}, opts = {}) {
12499
12644
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
12500
12645
  }
12501
12646
  }
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");
12647
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs20.existsSync)(target)) {
12648
+ (0, import_node_fs20.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs20.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
12504
12649
  }
12505
12650
  }
12506
12651
  const extraEnv = {};
@@ -12525,13 +12670,13 @@ async function startStage(config = {}, opts = {}) {
12525
12670
  healthUrl: sub(config.healthUrl?.trim()) || void 0,
12526
12671
  port: stagePort
12527
12672
  };
12528
- (0, import_node_fs19.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
12673
+ (0, import_node_fs20.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
12529
12674
  try {
12530
12675
  if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
12531
12676
  else await waitForProcessStability(child);
12532
12677
  } catch (e) {
12533
12678
  await killTree(state.pid);
12534
- (0, import_node_fs19.rmSync)(statePath, { force: true });
12679
+ (0, import_node_fs20.rmSync)(statePath, { force: true });
12535
12680
  throw e;
12536
12681
  }
12537
12682
  const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
@@ -12989,18 +13134,25 @@ async function rollDevelopmentForward(deps, ctx, tag) {
12989
13134
  }
12990
13135
  function resolveContextState(context, checkRuns, statuses) {
12991
13136
  let sawFailure = false;
13137
+ let sawSuccess = false;
13138
+ let sawPending = false;
12992
13139
  for (const r of checkRuns) {
12993
13140
  if (r.name !== context) continue;
12994
13141
  if (r.status === "completed") {
12995
- if (r.conclusion === "success") return "success";
12996
- if (r.conclusion !== "skipped" && r.conclusion !== "neutral") sawFailure = true;
13142
+ if (r.conclusion === "success") sawSuccess = true;
13143
+ else if (r.conclusion !== "skipped" && r.conclusion !== "neutral") sawFailure = true;
13144
+ } else {
13145
+ sawPending = true;
12997
13146
  }
12998
13147
  }
12999
13148
  for (const s of statuses) {
13000
13149
  if (s.context !== context) continue;
13001
- if (s.state === "success") return "success";
13002
- if (s.state === "failure" || s.state === "error") sawFailure = true;
13150
+ if (s.state === "success") sawSuccess = true;
13151
+ else if (s.state === "failure" || s.state === "error") sawFailure = true;
13152
+ else sawPending = true;
13003
13153
  }
13154
+ if (sawPending) return "pending";
13155
+ if (sawSuccess) return "success";
13004
13156
  return sawFailure ? "failed" : "pending";
13005
13157
  }
13006
13158
  async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
@@ -14349,7 +14501,7 @@ async function announceRelease(deps, args) {
14349
14501
  }
14350
14502
 
14351
14503
  // src/port-registry.ts
14352
- var import_node_fs20 = require("node:fs");
14504
+ var import_node_fs21 = require("node:fs");
14353
14505
 
14354
14506
  // ../infra/port-geometry.mjs
14355
14507
  var PORT_BLOCK = 100;
@@ -14363,8 +14515,8 @@ function nextPortBlock(registry2) {
14363
14515
  return [base2, base2 + PORT_SPAN];
14364
14516
  }
14365
14517
  function loadPortRegistry(path2) {
14366
- if (!(0, import_node_fs20.existsSync)(path2)) return {};
14367
- const raw = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
14518
+ if (!(0, import_node_fs21.existsSync)(path2)) return {};
14519
+ const raw = JSON.parse((0, import_node_fs21.readFileSync)(path2, "utf8"));
14368
14520
  const out = {};
14369
14521
  for (const [key, value] of Object.entries(raw)) {
14370
14522
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -14378,9 +14530,9 @@ function ensurePortRange(repo, path2) {
14378
14530
  const existing = registry2[repo];
14379
14531
  if (existing) return existing;
14380
14532
  const range = nextPortBlock(registry2);
14381
- const raw = (0, import_node_fs20.existsSync)(path2) ? JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8")) : {};
14533
+ const raw = (0, import_node_fs21.existsSync)(path2) ? JSON.parse((0, import_node_fs21.readFileSync)(path2, "utf8")) : {};
14382
14534
  raw[repo] = range;
14383
- (0, import_node_fs20.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
14535
+ (0, import_node_fs21.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
14384
14536
  return range;
14385
14537
  }
14386
14538
  function portCursorSeed(registry2) {
@@ -15240,6 +15392,9 @@ async function upsertProject(slug, patch, deps) {
15240
15392
  async function attestAppGaps(slug, repo, deps) {
15241
15393
  return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
15242
15394
  }
15395
+ async function setDeployCoords(slug, payload, deps) {
15396
+ return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
15397
+ }
15243
15398
  async function tenantControl(payload, deps) {
15244
15399
  return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
15245
15400
  }
@@ -15634,7 +15789,7 @@ ${section}`.trim();
15634
15789
  }
15635
15790
 
15636
15791
  // src/project-set.ts
15637
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dashboard", "ci", "requiredChecks", "gate"];
15792
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dashboard", "fofuEnabled", "ci", "requiredChecks", "gate"];
15638
15793
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
15639
15794
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
15640
15795
  function parseRuntimeSecretsVar(raw) {
@@ -15757,7 +15912,7 @@ function parseOauthVar(raw) {
15757
15912
  throw new Error('project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"/auth/callback"}');
15758
15913
  }
15759
15914
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15760
- throw new Error("project set: oauth must be a {subdomains,domains,callbackPath} object");
15915
+ throw new Error("project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
15761
15916
  }
15762
15917
  const map = parsed;
15763
15918
  const out = {};
@@ -15770,8 +15925,11 @@ function parseOauthVar(raw) {
15770
15925
  } else if (key === "callbackPath") {
15771
15926
  if (typeof value !== "string" || !value.trim()) throw new Error("project set: oauth.callbackPath must be a non-empty string");
15772
15927
  out.callbackPath = value.trim();
15928
+ } else if (key === "fofuSubdomain") {
15929
+ if (typeof value !== "string") throw new Error('project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
15930
+ out.fofuSubdomain = value.trim();
15773
15931
  } else {
15774
- throw new Error(`project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath`);
15932
+ throw new Error(`project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath/fofuSubdomain`);
15775
15933
  }
15776
15934
  }
15777
15935
  return out;
@@ -15798,6 +15956,11 @@ function parseDashboardVar(raw) {
15798
15956
  if (raw === "false") return false;
15799
15957
  throw new Error("project set: dashboard must be true or false");
15800
15958
  }
15959
+ function parseFofuEnabledVar(raw) {
15960
+ if (raw === "true") return true;
15961
+ if (raw === "false") return false;
15962
+ throw new Error("project set: fofuEnabled must be true or false");
15963
+ }
15801
15964
  function parsePublishDirVar(raw) {
15802
15965
  const v = raw.trim();
15803
15966
  if (v === "" || v === ".") {
@@ -15860,6 +16023,7 @@ var SETTABLE_VAR_KEYS = [
15860
16023
  "publishRequired",
15861
16024
  "publishDir",
15862
16025
  "dashboard",
16026
+ "fofuEnabled",
15863
16027
  "requiredGcpApis",
15864
16028
  "requiredRuntimeSecrets",
15865
16029
  "edgeDomains",
@@ -15878,8 +16042,9 @@ var SETTABLE_VAR_HINTS = {
15878
16042
  publishRequired: "true|false",
15879
16043
  publishDir: "relative subpath, e.g. packages/ui",
15880
16044
  dashboard: "true|false",
16045
+ fofuEnabled: "true|false",
15881
16046
  repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
15882
- oauth: "JSON {subdomains,domains,callbackPath}",
16047
+ oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
15883
16048
  requiredGcpApis: "comma-string",
15884
16049
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
15885
16050
  edgeDomains: "JSON {dev,rc,main} domain map",
@@ -15953,6 +16118,8 @@ function buildProjectSetPatch(input) {
15953
16118
  patch[key] = parsePublishRequiredVar(raw);
15954
16119
  } else if (key === "dashboard") {
15955
16120
  patch[key] = parseDashboardVar(raw);
16121
+ } else if (key === "fofuEnabled") {
16122
+ patch[key] = parseFofuEnabledVar(raw);
15956
16123
  } else if (key === "publishDir") {
15957
16124
  patch[key] = parsePublishDirVar(raw);
15958
16125
  } else if (key === "ci") {
@@ -15981,6 +16148,54 @@ function buildProjectSetPatch(input) {
15981
16148
  }
15982
16149
  return patch;
15983
16150
  }
16151
+ var DEPLOY_SUBSTRATES = ["hetzner-ssh"];
16152
+ var DEPLOY_STAGES = ["dev", "rc", "main"];
16153
+ var DEPLOY_DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
16154
+ var DEPLOY_SHELL_SAFE_RE = /^[A-Za-z0-9./:_?&=%-]*$/;
16155
+ function buildSetDeployPatch(slug, input) {
16156
+ const clean4 = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
16157
+ const stage2 = clean4(input.stage);
16158
+ if (!stage2 || !DEPLOY_STAGES.includes(stage2)) {
16159
+ throw new Error(`project set-deploy: --stage must be one of: ${DEPLOY_STAGES.join(", ")}`);
16160
+ }
16161
+ const substrate = clean4(input.substrate) ?? "hetzner-ssh";
16162
+ if (!DEPLOY_SUBSTRATES.includes(substrate)) {
16163
+ throw new Error(`project set-deploy: --substrate must be one of: ${DEPLOY_SUBSTRATES.join(", ")}`);
16164
+ }
16165
+ const sshHost = clean4(input.sshHost);
16166
+ if (substrate === "hetzner-ssh" && !sshHost) {
16167
+ throw new Error("project set-deploy: hetzner-ssh requires --ssh-host");
16168
+ }
16169
+ const sshUser = clean4(input.sshUser) ?? "root";
16170
+ const deployPath = clean4(input.deployPath) ?? `/opt/mmi/${slug}/${stage2}`;
16171
+ const serviceName = clean4(input.service) ?? slug;
16172
+ for (const [label, v] of [["--ssh-host", sshHost], ["--ssh-user", sshUser], ["--deploy-path", deployPath], ["--service", serviceName]]) {
16173
+ if (v !== void 0 && !DEPLOY_SHELL_SAFE_RE.test(v)) throw new Error(`project set-deploy: ${label} contains unsafe characters`);
16174
+ }
16175
+ let port;
16176
+ if (input.port !== void 0 && input.port !== null && `${input.port}`.trim() !== "") {
16177
+ port = Number(input.port);
16178
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("project set-deploy: --port must be an integer 1..65535");
16179
+ }
16180
+ const domain = clean4(input.domain);
16181
+ if (domain !== void 0 && !DEPLOY_DOMAIN_RE.test(domain)) throw new Error(`project set-deploy: --domain must be a DNS hostname, got ${JSON.stringify(input.domain)}`);
16182
+ const aliases = (Array.isArray(input.aliases) ? input.aliases : []).map((a) => clean4(a)).filter((a) => a !== void 0);
16183
+ for (const a of aliases) {
16184
+ if (!DEPLOY_DOMAIN_RE.test(a)) throw new Error(`project set-deploy: --alias must be a DNS hostname, got ${JSON.stringify(a)}`);
16185
+ }
16186
+ const uniqueAliases = [...new Set(aliases)];
16187
+ return {
16188
+ stage: stage2,
16189
+ substrate,
16190
+ sshHost,
16191
+ sshUser,
16192
+ deployPath,
16193
+ serviceName,
16194
+ ...domain !== void 0 ? { domain } : {},
16195
+ ...port !== void 0 ? { port } : {},
16196
+ ...uniqueAliases.length ? { aliases: uniqueAliases } : {}
16197
+ };
16198
+ }
15984
16199
  function repoFromRemoteUrl(remoteUrl) {
15985
16200
  const m = remoteUrl.trim().match(/^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@\s/]+@)?github\.com[:/]([^/\s:]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
15986
16201
  return m ? `${m[1]}/${m[2]}` : void 0;
@@ -16020,15 +16235,15 @@ function parseKbTree(stdout, prefix) {
16020
16235
  }
16021
16236
 
16022
16237
  // src/northstar-commands.ts
16023
- var import_node_fs21 = require("node:fs");
16238
+ var import_node_fs22 = require("node:fs");
16024
16239
  var import_node_child_process11 = require("node:child_process");
16025
16240
  var import_promises6 = require("node:fs/promises");
16026
16241
 
16027
16242
  // src/plan.ts
16028
- var import_node_path18 = require("node:path");
16243
+ var import_node_path19 = require("node:path");
16029
16244
  var PLANS_DIR = "plans";
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`);
16245
+ var META_FILE = (0, import_node_path19.join)(PLANS_DIR, ".plan-meta.json");
16246
+ var planPath = (slug) => (0, import_node_path19.join)(PLANS_DIR, `${slug}.md`);
16032
16247
  var metaKey = (project2, slug) => `${project2}/${slug}`;
16033
16248
  function parseMeta(raw) {
16034
16249
  if (!raw) return {};
@@ -16053,7 +16268,7 @@ function hashContent(s) {
16053
16268
  function staleHint(slug) {
16054
16269
  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`;
16055
16270
  }
16056
- var INDEX_FILE = (0, import_node_path18.join)(PLANS_DIR, ".index.json");
16271
+ var INDEX_FILE = (0, import_node_path19.join)(PLANS_DIR, ".index.json");
16057
16272
  var INDEX_TTL_MS = 6e4;
16058
16273
  function parseIndex(raw) {
16059
16274
  if (!raw) return null;
@@ -16082,7 +16297,7 @@ function mergeIndex(idx, scope, plans, now) {
16082
16297
  const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
16083
16298
  return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
16084
16299
  }
16085
- var QUEUE_FILE = (0, import_node_path18.join)(PLANS_DIR, ".sync-queue.json");
16300
+ var QUEUE_FILE = (0, import_node_path19.join)(PLANS_DIR, ".sync-queue.json");
16086
16301
  var QUEUE_MAX_ATTEMPTS = 10;
16087
16302
  function isValidQueueEntry(e) {
16088
16303
  if (!e || typeof e !== "object") return false;
@@ -16557,7 +16772,7 @@ function detachPlanSync() {
16557
16772
  }
16558
16773
  }
16559
16774
  function makePlanDeps(cfg, io = consoleIo) {
16560
- const ensureDir = () => (0, import_node_fs21.mkdirSync)(PLANS_DIR, { recursive: true });
16775
+ const ensureDir = () => (0, import_node_fs22.mkdirSync)(PLANS_DIR, { recursive: true });
16561
16776
  return {
16562
16777
  apiUrl: cfg.sagaApiUrl,
16563
16778
  fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
@@ -16565,24 +16780,24 @@ function makePlanDeps(cfg, io = consoleIo) {
16565
16780
  project: async () => (await sagaKey(cfg)).project,
16566
16781
  readLocal: (slug) => {
16567
16782
  try {
16568
- return (0, import_node_fs21.readFileSync)(planPath(slug), "utf8");
16783
+ return (0, import_node_fs22.readFileSync)(planPath(slug), "utf8");
16569
16784
  } catch {
16570
16785
  return null;
16571
16786
  }
16572
16787
  },
16573
16788
  writeLocal: (slug, content) => {
16574
16789
  ensureDir();
16575
- (0, import_node_fs21.writeFileSync)(planPath(slug), content, "utf8");
16790
+ (0, import_node_fs22.writeFileSync)(planPath(slug), content, "utf8");
16576
16791
  },
16577
16792
  removeLocal: (slug) => {
16578
16793
  try {
16579
- (0, import_node_fs21.rmSync)(planPath(slug));
16794
+ (0, import_node_fs22.rmSync)(planPath(slug));
16580
16795
  } catch {
16581
16796
  }
16582
16797
  },
16583
16798
  readMetaRaw: () => {
16584
16799
  try {
16585
- return (0, import_node_fs21.readFileSync)(META_FILE, "utf8");
16800
+ return (0, import_node_fs22.readFileSync)(META_FILE, "utf8");
16586
16801
  } catch {
16587
16802
  return null;
16588
16803
  }
@@ -16593,7 +16808,7 @@ function makePlanDeps(cfg, io = consoleIo) {
16593
16808
  },
16594
16809
  readIndexRaw: () => {
16595
16810
  try {
16596
- return (0, import_node_fs21.readFileSync)(INDEX_FILE, "utf8");
16811
+ return (0, import_node_fs22.readFileSync)(INDEX_FILE, "utf8");
16597
16812
  } catch {
16598
16813
  return null;
16599
16814
  }
@@ -16604,7 +16819,7 @@ function makePlanDeps(cfg, io = consoleIo) {
16604
16819
  },
16605
16820
  readQueueRaw: () => {
16606
16821
  try {
16607
- return (0, import_node_fs21.readFileSync)(QUEUE_FILE, "utf8");
16822
+ return (0, import_node_fs22.readFileSync)(QUEUE_FILE, "utf8");
16608
16823
  } catch {
16609
16824
  return null;
16610
16825
  }
@@ -16639,7 +16854,7 @@ async function withPlan(quiet, run, io = consoleIo) {
16639
16854
  }
16640
16855
  await run(makePlanDeps(cfg, io));
16641
16856
  }
16642
- async function gatherRelevanceSignals() {
16857
+ async function gatherRelevanceSignals(opts = {}) {
16643
16858
  const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
16644
16859
  const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
16645
16860
  const signals = { branch, changedFiles: changed.length ? changed : void 0 };
@@ -16657,6 +16872,10 @@ async function gatherRelevanceSignals() {
16657
16872
  } catch {
16658
16873
  }
16659
16874
  }
16875
+ if (opts.anchorSlug) {
16876
+ const slug = (await opts.anchorSlug().catch(() => void 0))?.trim();
16877
+ if (slug) signals.anchorSlug = slug;
16878
+ }
16660
16879
  return signals;
16661
16880
  }
16662
16881
  function registerNorthStarCommands(cmd) {
@@ -16777,9 +16996,7 @@ function registerSecretsCommands(program3) {
16777
16996
  });
16778
16997
  const centralContainer = meta?.deployModel === "tenant-container" || meta?.deployModel === "solo-container";
16779
16998
  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;
16999
+ d.err('secrets preflight: requiredRuntimeSecrets is unset: treating as an empty contract (no required keys). Declare an explicit {"dev":[],"rc":[],"main":[]} in registry META to silence this warning.');
16783
17000
  }
16784
17001
  const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
16785
17002
  if (!ok) process.exitCode = 1;
@@ -16844,6 +17061,9 @@ function expectedHosts(cfg) {
16844
17061
  for (const env of ENV_PREFIXES) out.push(env ? `${env}.${base2}` : base2);
16845
17062
  }
16846
17063
  }
17064
+ if (cfg.fofuSubdomain !== void 0) {
17065
+ out.push(cfg.fofuSubdomain ? `${cfg.fofuSubdomain}.fofu.ai` : "fofu.ai");
17066
+ }
16847
17067
  return uniq(out);
16848
17068
  }
16849
17069
  function expectedJsOrigins(cfg) {
@@ -16888,7 +17108,10 @@ function parseOauthConfig(mmiConfig, slug) {
16888
17108
  if (!callbackPath.startsWith("/")) {
16889
17109
  throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
16890
17110
  }
16891
- return { subdomains, domains, callbackPath };
17111
+ const meta = mmiConfig ?? {};
17112
+ const rawFofuSub = raw.fofuSubdomain;
17113
+ const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
17114
+ return { subdomains, domains, callbackPath, fofuSubdomain };
16892
17115
  }
16893
17116
  function probeRedirectUri(callbackPath, port = 9123) {
16894
17117
  return `http://localhost:${port}${callbackPath}`;
@@ -17052,7 +17275,7 @@ async function fetchHubVersionInfo(baseUrl) {
17052
17275
  }
17053
17276
  function readRepoVersion() {
17054
17277
  try {
17055
- return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
17278
+ return JSON.parse((0, import_node_fs23.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
17056
17279
  } catch {
17057
17280
  return void 0;
17058
17281
  }
@@ -17227,10 +17450,10 @@ async function runRulesSync(opts, io = consoleIo) {
17227
17450
  for (const entry of fetched) {
17228
17451
  if ("error" in entry) continue;
17229
17452
  const { file, source } = entry;
17230
- const current = (0, import_node_fs22.existsSync)(file) ? await (0, import_promises7.readFile)(file, "utf8") : null;
17453
+ const current = (0, import_node_fs23.existsSync)(file) ? await (0, import_promises7.readFile)(file, "utf8") : null;
17231
17454
  if (needsUpdate(source, current)) {
17232
17455
  const slash = file.lastIndexOf("/");
17233
- if (slash > 0) (0, import_node_fs22.mkdirSync)(file.slice(0, slash), { recursive: true });
17456
+ if (slash > 0) (0, import_node_fs23.mkdirSync)(file.slice(0, slash), { recursive: true });
17234
17457
  await (0, import_promises7.writeFile)(file, normalizeEol(source), "utf8");
17235
17458
  changed++;
17236
17459
  if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
@@ -17243,6 +17466,27 @@ var rules = program2.command("rules").description("org rules delivery");
17243
17466
  rules.command("sync").option("--quiet", "stay silent unless something changed or errored").description("fetch the org-delivered files (AGENTS.md / CLAUDE.md / .claude/settings.json / output style / Cursor rule) from MMI-Hub and write them verbatim (org-owned, whole-file)").action(async (opts) => {
17244
17467
  if (!await runRulesSync(opts)) process.exitCode = 1;
17245
17468
  });
17469
+ rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
17470
+ const path2 = (0, import_node_path20.join)(process.cwd(), ".gitignore");
17471
+ const current = (0, import_node_fs23.existsSync)(path2) ? (0, import_node_fs23.readFileSync)(path2, "utf8") : null;
17472
+ const plan2 = planManagedGitignore(current);
17473
+ const drift = [...plan2.added.map((l) => `+${l}`), ...plan2.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
17474
+ if (opts.write) {
17475
+ if (plan2.changed) {
17476
+ (0, import_node_fs23.writeFileSync)(path2, plan2.content, "utf8");
17477
+ console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
17478
+ } else {
17479
+ console.log("mmi-cli rules gitignore: up to date");
17480
+ }
17481
+ return;
17482
+ }
17483
+ if (plan2.changed) {
17484
+ console.error(`mmi-cli rules gitignore: managed block drift (${drift}) \u2014 run \`mmi-cli rules gitignore --write\` and commit`);
17485
+ process.exitCode = 1;
17486
+ } else {
17487
+ console.log("mmi-cli rules gitignore: up to date");
17488
+ }
17489
+ });
17246
17490
  async function runDocsSync(opts, io = consoleIo) {
17247
17491
  const ref = await gitOut(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
17248
17492
  const def = (ref.startsWith("origin/") ? ref.slice("origin/".length) : ref) || "development";
@@ -17256,7 +17500,7 @@ async function runDocsSync(opts, io = consoleIo) {
17256
17500
  return null;
17257
17501
  }
17258
17502
  },
17259
- localContent: async (f) => (0, import_node_fs22.existsSync)(f) ? await (0, import_promises7.readFile)(f, "utf8") : null,
17503
+ localContent: async (f) => (0, import_node_fs23.existsSync)(f) ? await (0, import_promises7.readFile)(f, "utf8") : null,
17260
17504
  writeDoc: async (f, c) => {
17261
17505
  await (0, import_promises7.writeFile)(f, c, "utf8");
17262
17506
  }
@@ -17355,7 +17599,7 @@ function runWorktreeInstall(command, cwd, quiet) {
17355
17599
  async function primaryCheckoutRoot(worktreeRoot) {
17356
17600
  try {
17357
17601
  const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
17358
- return out ? (0, import_node_path19.dirname)(out) : void 0;
17602
+ return out ? (0, import_node_path20.dirname)(out) : void 0;
17359
17603
  } catch {
17360
17604
  return void 0;
17361
17605
  }
@@ -17368,28 +17612,28 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
17368
17612
  };
17369
17613
  }
17370
17614
  function acquireWorktreeSetupLock(worktreeRoot) {
17371
- const lockPath = (0, import_node_path19.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
17615
+ const lockPath = (0, import_node_path20.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
17372
17616
  const take = () => {
17373
- const fd = (0, import_node_fs22.openSync)(lockPath, "wx");
17617
+ const fd = (0, import_node_fs23.openSync)(lockPath, "wx");
17374
17618
  try {
17375
- (0, import_node_fs22.writeSync)(fd, String(Date.now()));
17619
+ (0, import_node_fs23.writeSync)(fd, String(Date.now()));
17376
17620
  } finally {
17377
- (0, import_node_fs22.closeSync)(fd);
17621
+ (0, import_node_fs23.closeSync)(fd);
17378
17622
  }
17379
17623
  return () => {
17380
17624
  try {
17381
- (0, import_node_fs22.rmSync)(lockPath, { force: true });
17625
+ (0, import_node_fs23.rmSync)(lockPath, { force: true });
17382
17626
  } catch {
17383
17627
  }
17384
17628
  };
17385
17629
  };
17386
17630
  try {
17387
- (0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(lockPath), { recursive: true });
17631
+ (0, import_node_fs23.mkdirSync)((0, import_node_path20.dirname)(lockPath), { recursive: true });
17388
17632
  return take();
17389
17633
  } catch {
17390
17634
  try {
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 });
17635
+ if (Date.now() - (0, import_node_fs23.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
17636
+ (0, import_node_fs23.rmSync)(lockPath, { force: true });
17393
17637
  return take();
17394
17638
  }
17395
17639
  } catch {
@@ -17729,7 +17973,7 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
17729
17973
  }
17730
17974
  });
17731
17975
  project.command("resolve <owner/repo>").description("deploy coords for a stage \u2014 for diagnosis. NOTE: /deploy-coords is OIDC-gated (a deploy job\u2019s id-token), so a gh-token CLI cannot read it from a dev machine").option("--stage <main|rc>", "deploy stage", "main").option("--json", "machine-readable output").action((_repoOrRepo, o) => {
17732
- const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect the DEPLOY# item via the AWS console / a master DDB read instead.";
17976
+ const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect the DEPLOY# item via a master registry (DDB) read instead.";
17733
17977
  if (o.json) {
17734
17978
  console.log(JSON.stringify({ ok: false, stage: o.stage, error: msg }));
17735
17979
  process.exitCode = 1;
@@ -17829,6 +18073,34 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
17829
18073
  const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
17830
18074
  return reportWrite("project set", res);
17831
18075
  });
18076
+ project.command("set-deploy [owner/repo]").description("write the DEPLOY#<stage> Hetzner deploy coords for a tenant (master-only) \u2014 the explicit-coords path that seeds a freshly-bootstrapped tenant; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into (required for hetzner-ssh)").option("--ssh-user <user>", "ssh user (default root)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535)").option("--substrate <substrate>", "hetzner-ssh (default)").option("--deploy-path <path>", "on-box per-stage release root (default /opt/mmi/<slug>/<stage>)").option("--service <name>", "systemd/compose service name (default the slug)").option("--domain <domain>", "canonical serving host (default the project edgeDomains[stage])").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
18077
+ const cfg = await loadConfig();
18078
+ let target;
18079
+ try {
18080
+ target = await projectTarget("project set-deploy", repoOrSlug);
18081
+ } catch (e) {
18082
+ return fail(e.message);
18083
+ }
18084
+ const slug = slugOf(target);
18085
+ let body;
18086
+ try {
18087
+ body = buildSetDeployPatch(slug, {
18088
+ stage: o.stage,
18089
+ sshHost: o.sshHost,
18090
+ sshUser: o.sshUser,
18091
+ port: o.port,
18092
+ substrate: o.substrate,
18093
+ deployPath: o.deployPath,
18094
+ service: o.service,
18095
+ domain: o.domain,
18096
+ aliases: o.alias
18097
+ });
18098
+ } catch (e) {
18099
+ return fail(e.message);
18100
+ }
18101
+ const res = await setDeployCoords(slug, body, registryClientDeps(cfg));
18102
+ return reportWrite("project set-deploy", res);
18103
+ });
17832
18104
  var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
17833
18105
  registry.command("org").description("the org config (account id, region, orgProjectId, sagaApiUrl)").option("--json", "machine-readable output").action(async (_o) => {
17834
18106
  const cfg = await loadConfig();
@@ -18011,6 +18283,46 @@ issue.command("link-child <parent> <child>").description("link an existing issue
18011
18283
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
18012
18284
  }
18013
18285
  });
18286
+ issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2192 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
18287
+ let parsed;
18288
+ try {
18289
+ parsed = parseIssueRef(ref);
18290
+ } catch (e) {
18291
+ return fail(`issue check: ${e.message}`);
18292
+ }
18293
+ const repo = await resolveRepo(parsed.repo ?? o.repo);
18294
+ if (!repo) return fail("issue check: could not resolve repo \u2014 pass --repo owner/repo");
18295
+ const checked = o.off !== true;
18296
+ let body;
18297
+ try {
18298
+ const viewed = await ghJson(["issue", "view", String(parsed.number), "--repo", repo, "--json", "body"]);
18299
+ body = viewed.body ?? "";
18300
+ } catch (e) {
18301
+ return fail(`issue check: could not read ${repo}#${parsed.number}: ${e.message}`);
18302
+ }
18303
+ const result = applyChecklistCheck(body, o.item, checked);
18304
+ if (!result.ok) {
18305
+ if (result.reason === "ambiguous") {
18306
+ const list = result.matches.map((m) => ` - ${m.text}`).join("\n");
18307
+ return fail(`issue check: "${o.item}" matches ${result.matches.length} checklist items in ${repo}#${parsed.number} \u2014 narrow the text:
18308
+ ${list}`);
18309
+ }
18310
+ return fail(`issue check: no checklist item matching "${o.item}" in ${repo}#${parsed.number}`);
18311
+ }
18312
+ if (!result.edit.changed) {
18313
+ return console.log(JSON.stringify({ number: parsed.number, repo, item: result.item.text, checked, changed: false }));
18314
+ }
18315
+ const edit = await bodyArgsViaFile(["issue", "edit", String(parsed.number), "--repo", repo, "--body", result.edit.body]);
18316
+ try {
18317
+ await execFileP2("gh", edit.args, { timeout: GH_MUTATION_TIMEOUT_MS });
18318
+ } catch (e) {
18319
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
18320
+ return fail(`issue check: edit failed for ${repo}#${parsed.number}: ${e.message}${note ? ` (${note})` : ""}`);
18321
+ } finally {
18322
+ await edit.cleanup();
18323
+ }
18324
+ console.log(JSON.stringify({ number: parsed.number, repo, item: result.item.text, checked, changed: true }));
18325
+ });
18014
18326
  program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
18015
18327
  let body;
18016
18328
  let priority;
@@ -18284,9 +18596,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
18284
18596
  console.log(JSON.stringify(created));
18285
18597
  });
18286
18598
  async function listCiWorkflowPaths(cwd = process.cwd()) {
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}`);
18599
+ const wfDir = (0, import_node_path20.join)(cwd, ".github", "workflows");
18600
+ if (!(0, import_node_fs23.existsSync)(wfDir)) return [];
18601
+ return (0, import_node_fs23.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
18290
18602
  }
18291
18603
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
18292
18604
  const repo = repoOpt ?? await resolveRepo();
@@ -18305,7 +18617,7 @@ function ciAuditDeps() {
18305
18617
  // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
18306
18618
  // reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
18307
18619
  // the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
18308
- readSeedFile: (path2) => (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null
18620
+ readSeedFile: (path2) => (0, import_node_fs23.existsSync)(path2) ? (0, import_node_fs23.readFileSync)(path2, "utf8") : null
18309
18621
  };
18310
18622
  }
18311
18623
  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) => {
@@ -18441,7 +18753,7 @@ async function createDeferredWorktreeStore() {
18441
18753
  },
18442
18754
  write: async (entries) => {
18443
18755
  try {
18444
- await (0, import_promises7.mkdir)((0, import_node_path19.dirname)(registryPath), { recursive: true });
18756
+ await (0, import_promises7.mkdir)((0, import_node_path20.dirname)(registryPath), { recursive: true });
18445
18757
  await (0, import_promises7.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
18446
18758
  } catch {
18447
18759
  }
@@ -18455,13 +18767,13 @@ var realWorktreeDirRemover = {
18455
18767
  probe: (p) => {
18456
18768
  let st;
18457
18769
  try {
18458
- st = (0, import_node_fs22.lstatSync)(p);
18770
+ st = (0, import_node_fs23.lstatSync)(p);
18459
18771
  } catch {
18460
18772
  return null;
18461
18773
  }
18462
18774
  if (st.isSymbolicLink()) return "link";
18463
18775
  try {
18464
- (0, import_node_fs22.readlinkSync)(p);
18776
+ (0, import_node_fs23.readlinkSync)(p);
18465
18777
  return "link";
18466
18778
  } catch {
18467
18779
  }
@@ -18469,7 +18781,7 @@ var realWorktreeDirRemover = {
18469
18781
  },
18470
18782
  readdir: (p) => {
18471
18783
  try {
18472
- return (0, import_node_fs22.readdirSync)(p);
18784
+ return (0, import_node_fs23.readdirSync)(p);
18473
18785
  } catch {
18474
18786
  return [];
18475
18787
  }
@@ -18478,9 +18790,9 @@ var realWorktreeDirRemover = {
18478
18790
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
18479
18791
  detachLink: (p) => {
18480
18792
  try {
18481
- (0, import_node_fs22.rmdirSync)(p);
18793
+ (0, import_node_fs23.rmdirSync)(p);
18482
18794
  } catch {
18483
- (0, import_node_fs22.unlinkSync)(p);
18795
+ (0, import_node_fs23.unlinkSync)(p);
18484
18796
  }
18485
18797
  },
18486
18798
  removeTree: (p) => (0, import_promises7.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -18501,7 +18813,7 @@ function worktreeRemoveDeps(execGit) {
18501
18813
  }
18502
18814
  function teardownWorktreeStage(worktreePath) {
18503
18815
  return runWorktreeStageTeardown(worktreePath, {
18504
- hasStageState: (wt) => (0, import_node_fs22.existsSync)(stageStatePath(wt)),
18816
+ hasStageState: (wt) => (0, import_node_fs23.existsSync)(stageStatePath(wt)),
18505
18817
  stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
18506
18818
  listComposeProjects: async () => {
18507
18819
  const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
@@ -18564,7 +18876,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
18564
18876
  } : await cleanupPrMergeLocalBranch(headRef, {
18565
18877
  beforeWorktrees,
18566
18878
  startingPath,
18567
- pathExists: (p) => (0, import_node_fs22.existsSync)(p),
18879
+ pathExists: (p) => (0, import_node_fs23.existsSync)(p),
18568
18880
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
18569
18881
  teardownWorktreeStage,
18570
18882
  deferredStore,
@@ -18607,6 +18919,9 @@ async function runBoardRead(o) {
18607
18919
  }
18608
18920
  var board = program2.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
18609
18921
  board.command("read", { isDefault: true }).description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--allow-partial", "return partial board results when later page/detail reads fail").action((o) => runBoardRead(o));
18922
+ board.command("slice-refresh", { hidden: true }).description("internal: refresh the cached SessionStart board slice (background worker)").option("--quiet", "silent (background worker)").action(async () => {
18923
+ await refreshBoardSliceCache({ loadConfig: () => loadConfigForRepo(), readBoard });
18924
+ });
18610
18925
  board.command("claim <issues...>").description("assign Todo issues and move their Project v2 Status to In Progress (one or more refs)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails").action(async (issueRefs, o) => {
18611
18926
  if (issueRefs.length === 1) {
18612
18927
  const issueRef = issueRefs[0];
@@ -18739,7 +19054,7 @@ function rawValues(flag) {
18739
19054
  return out;
18740
19055
  }
18741
19056
  function printLine(value) {
18742
- (0, import_node_fs22.writeSync)(1, `${value}
19057
+ (0, import_node_fs23.writeSync)(1, `${value}
18743
19058
  `);
18744
19059
  }
18745
19060
  function stageKeepAlive() {
@@ -18756,8 +19071,8 @@ async function resolveStage() {
18756
19071
  local,
18757
19072
  shell: shellFor(),
18758
19073
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
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"))
19074
+ hasCompose: (0, import_node_fs23.existsSync)((0, import_node_path20.join)(process.cwd(), "docker-compose.yml")),
19075
+ hasEnvExample: (0, import_node_fs23.existsSync)((0, import_node_path20.join)(process.cwd(), ".env.example"))
18761
19076
  });
18762
19077
  }
18763
19078
  async function fetchStageVaultEnvMerge() {
@@ -18809,9 +19124,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
18809
19124
  printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
18810
19125
  return;
18811
19126
  }
18812
- const path2 = (0, import_node_path19.join)(process.cwd(), "infra", "port-ranges.json");
19127
+ const path2 = (0, import_node_path20.join)(process.cwd(), "infra", "port-ranges.json");
18813
19128
  const allocate = async (seed) => {
18814
- const { stdout } = await execFileP2("node", [(0, import_node_path19.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
19129
+ const { stdout } = await execFileP2("node", [(0, import_node_path20.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
18815
19130
  const parsed = JSON.parse(stdout);
18816
19131
  if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
18817
19132
  return parsed.range;
@@ -18831,7 +19146,11 @@ async function stageLiveTarget() {
18831
19146
  const repo = await resolveRepo();
18832
19147
  if (!repo) throw new Error("stage --live: cannot resolve the current repo (run inside a GitHub-remoted checkout)");
18833
19148
  const ref = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
18834
- return { slug: slugOf(repo), repo, ref };
19149
+ const slug = slugOf(repo);
19150
+ const meta = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig())).catch(() => null);
19151
+ const host = edgeDomainsByStage(meta).dev ?? `dev.${defaultSubdomain(slug)}.mutatismutandis.co`;
19152
+ if (!host.trim()) throw new Error(`stage --live: no dev edge host for ${slug} (registry edgeDomains.dev)`);
19153
+ return { slug, repo, host, ref };
18835
19154
  }
18836
19155
  async function runStageLiveCommand(o) {
18837
19156
  let target;
@@ -19202,7 +19521,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
19202
19521
  client: defaultGitHubClient(),
19203
19522
  projectMeta: meta,
19204
19523
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
19205
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
19524
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs23.existsSync)(path2) ? (0, import_node_fs23.readFileSync)(path2, "utf8") : null,
19206
19525
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
19207
19526
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
19208
19527
  requiredGcpApis: (() => {
@@ -19246,12 +19565,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
19246
19565
  return fail(`bootstrap apply: ${e.message}`);
19247
19566
  }
19248
19567
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
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"));
19568
+ if (!(0, import_node_fs23.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
19569
+ const manifest = loadBootstrapSeeds((0, import_node_fs23.readFileSync)(manifestPath, "utf8"));
19251
19570
  const baseBranch = o.class === "content" ? "main" : "development";
19252
19571
  const slug = parsedRepo.slug;
19253
19572
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
19254
- const readFile7 = (p) => (0, import_node_fs22.existsSync)(p) ? (0, import_node_fs22.readFileSync)(p, "utf8") : null;
19573
+ const readFile7 = (p) => (0, import_node_fs23.existsSync)(p) ? (0, import_node_fs23.readFileSync)(p, "utf8") : null;
19255
19574
  const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
19256
19575
  const rawVars = {};
19257
19576
  for (const value of rawValues("--var")) {
@@ -19502,16 +19821,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
19502
19821
  if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
19503
19822
  targets = [{ repo: o.repo, class: o.class }];
19504
19823
  } else {
19505
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs22.existsSync)("projects.json") ? (0, import_node_fs22.readFileSync)("projects.json", "utf8") : null;
19824
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs23.existsSync)("projects.json") ? (0, import_node_fs23.readFileSync)("projects.json", "utf8") : null;
19506
19825
  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>");
19507
- const fanoutJson = (0, import_node_fs22.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs22.readFileSync)(".github/fanout-targets.json", "utf8") : null;
19826
+ const fanoutJson = (0, import_node_fs23.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs23.readFileSync)(".github/fanout-targets.json", "utf8") : null;
19508
19827
  targets = loadAccessTargets(projectsJson, fanoutJson);
19509
19828
  }
19510
19829
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
19511
- const fileMatrix = (0, import_node_fs22.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs22.readFileSync)("access-matrix.json", "utf8")) : {};
19830
+ const fileMatrix = (0, import_node_fs23.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs23.readFileSync)("access-matrix.json", "utf8")) : {};
19512
19831
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
19513
19832
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { 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: {} };
19833
+ const fileContracts = (0, import_node_fs23.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs23.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
19515
19834
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
19516
19835
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
19517
19836
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -19521,20 +19840,20 @@ access.command("capabilities").description("enumerate your effective vault reach
19521
19840
  var isWin = process.platform === "win32";
19522
19841
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
19523
19842
  const homeDir = surface === "codex" ? ".codex" : ".claude";
19524
- return (0, import_node_path19.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
19843
+ return (0, import_node_path20.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
19525
19844
  };
19526
19845
  function readInstalledPlugins() {
19527
19846
  try {
19528
- return JSON.parse((0, import_node_fs22.readFileSync)(installedPluginsPath(), "utf8"));
19847
+ return JSON.parse((0, import_node_fs23.readFileSync)(installedPluginsPath(), "utf8"));
19529
19848
  } catch {
19530
19849
  return null;
19531
19850
  }
19532
19851
  }
19533
19852
  function installedPluginSources() {
19534
19853
  return ["claude", "codex"].map((surface) => {
19535
- const recordPath = (0, import_node_path19.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
19854
+ const recordPath = (0, import_node_path20.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
19536
19855
  try {
19537
- return { surface, installed: JSON.parse((0, import_node_fs22.readFileSync)(recordPath, "utf8")), recordPath };
19856
+ return { surface, installed: JSON.parse((0, import_node_fs23.readFileSync)(recordPath, "utf8")), recordPath };
19538
19857
  } catch {
19539
19858
  return { surface, installed: null, recordPath };
19540
19859
  }
@@ -19542,7 +19861,7 @@ function installedPluginSources() {
19542
19861
  }
19543
19862
  function readClaudeSettings() {
19544
19863
  try {
19545
- return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
19864
+ return JSON.parse((0, import_node_fs23.readFileSync)((0, import_node_path20.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
19546
19865
  } catch {
19547
19866
  return null;
19548
19867
  }
@@ -19564,7 +19883,7 @@ function writeProjectInstallRecord(record) {
19564
19883
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
19565
19884
  list.push(record);
19566
19885
  file.plugins[MMI_PLUGIN_ID] = list;
19567
- (0, import_node_fs22.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
19886
+ (0, import_node_fs23.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
19568
19887
  `, "utf8");
19569
19888
  return true;
19570
19889
  } catch {
@@ -19577,9 +19896,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
19577
19896
  if (!file) return false;
19578
19897
  if (!file.plugins) file.plugins = {};
19579
19898
  const path2 = installedPluginsPath();
19580
- (0, import_node_fs22.copyFileSync)(path2, `${path2}.bak`);
19899
+ (0, import_node_fs23.copyFileSync)(path2, `${path2}.bak`);
19581
19900
  file.plugins[pluginId] = records;
19582
- (0, import_node_fs22.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
19901
+ (0, import_node_fs23.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
19583
19902
  `, "utf8");
19584
19903
  return true;
19585
19904
  } catch {
@@ -19587,35 +19906,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
19587
19906
  }
19588
19907
  }
19589
19908
  function cursorPluginCacheRoot() {
19590
- return (0, import_node_path19.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
19909
+ return (0, import_node_path20.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
19591
19910
  }
19592
19911
  function cursorPluginCachePinSnapshots() {
19593
19912
  const root = cursorPluginCacheRoot();
19594
19913
  try {
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");
19914
+ return (0, import_node_fs23.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
19915
+ const path2 = (0, import_node_path20.join)(root, entry.name);
19916
+ const pluginJson = (0, import_node_path20.join)(path2, ".cursor-plugin", "plugin.json");
19917
+ const hooksJson = (0, import_node_path20.join)(path2, "hooks", "hooks.json");
19918
+ const cliBundle = (0, import_node_path20.join)(path2, "cli", "dist", "index.cjs");
19600
19919
  let version;
19601
19920
  try {
19602
- const raw = JSON.parse((0, import_node_fs22.readFileSync)(pluginJson, "utf8"));
19921
+ const raw = JSON.parse((0, import_node_fs23.readFileSync)(pluginJson, "utf8"));
19603
19922
  version = typeof raw.version === "string" ? raw.version : void 0;
19604
19923
  } catch {
19605
19924
  version = void 0;
19606
19925
  }
19607
19926
  let isEmpty = true;
19608
19927
  try {
19609
- isEmpty = (0, import_node_fs22.readdirSync)(path2).length === 0;
19928
+ isEmpty = (0, import_node_fs23.readdirSync)(path2).length === 0;
19610
19929
  } catch {
19611
19930
  isEmpty = true;
19612
19931
  }
19613
19932
  return {
19614
19933
  name: entry.name,
19615
19934
  path: path2,
19616
- hasPluginJson: (0, import_node_fs22.existsSync)(pluginJson),
19617
- hasHooksJson: (0, import_node_fs22.existsSync)(hooksJson),
19618
- hasCliBundle: (0, import_node_fs22.existsSync)(cliBundle),
19935
+ hasPluginJson: (0, import_node_fs23.existsSync)(pluginJson),
19936
+ hasHooksJson: (0, import_node_fs23.existsSync)(hooksJson),
19937
+ hasCliBundle: (0, import_node_fs23.existsSync)(cliBundle),
19619
19938
  isEmpty,
19620
19939
  version
19621
19940
  };
@@ -19625,19 +19944,19 @@ function cursorPluginCachePinSnapshots() {
19625
19944
  }
19626
19945
  }
19627
19946
  function hubCheckoutForCursorSeed() {
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;
19947
+ const manifest = (0, import_node_path20.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
19948
+ return (0, import_node_fs23.existsSync)(manifest) ? process.cwd() : void 0;
19630
19949
  }
19631
19950
  function mmiPluginCacheRootSnapshots() {
19632
19951
  const roots = [
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") }
19952
+ { surface: "claude", root: (0, import_node_path20.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
19953
+ { surface: "codex", root: (0, import_node_path20.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
19635
19954
  ];
19636
19955
  return roots.flatMap(({ surface, root }) => {
19637
19956
  try {
19638
- const entries = (0, import_node_fs22.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
19957
+ const entries = (0, import_node_fs23.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
19639
19958
  name: entry.name,
19640
- path: (0, import_node_path19.join)(root, entry.name),
19959
+ path: (0, import_node_path20.join)(root, entry.name),
19641
19960
  isDirectory: entry.isDirectory()
19642
19961
  }));
19643
19962
  return [{ surface, root, entries }];
@@ -19648,7 +19967,7 @@ function mmiPluginCacheRootSnapshots() {
19648
19967
  }
19649
19968
  function hasNestedMmiChild(versionDir) {
19650
19969
  try {
19651
- return (0, import_node_fs22.statSync)((0, import_node_path19.join)(versionDir, "mmi")).isDirectory();
19970
+ return (0, import_node_fs23.statSync)((0, import_node_path20.join)(versionDir, "mmi")).isDirectory();
19652
19971
  } catch {
19653
19972
  return false;
19654
19973
  }
@@ -19659,10 +19978,10 @@ function nestedPluginTreeSnapshot() {
19659
19978
  );
19660
19979
  }
19661
19980
  function uniqueQuarantineTarget(path2) {
19662
- if (!(0, import_node_fs22.existsSync)(path2)) return path2;
19981
+ if (!(0, import_node_fs23.existsSync)(path2)) return path2;
19663
19982
  for (let i = 1; i < 100; i += 1) {
19664
19983
  const candidate = `${path2}-${i}`;
19665
- if (!(0, import_node_fs22.existsSync)(candidate)) return candidate;
19984
+ if (!(0, import_node_fs23.existsSync)(candidate)) return candidate;
19666
19985
  }
19667
19986
  return `${path2}-${Date.now()}`;
19668
19987
  }
@@ -19671,10 +19990,10 @@ function quarantinePluginCacheDirs(plan2) {
19671
19990
  const failed = [];
19672
19991
  for (const move of plan2) {
19673
19992
  try {
19674
- if (!(0, import_node_fs22.existsSync)(move.from)) continue;
19993
+ if (!(0, import_node_fs23.existsSync)(move.from)) continue;
19675
19994
  const target = uniqueQuarantineTarget(move.to);
19676
- (0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(target), { recursive: true });
19677
- (0, import_node_fs22.renameSync)(move.from, target);
19995
+ (0, import_node_fs23.mkdirSync)((0, import_node_path20.dirname)(target), { recursive: true });
19996
+ (0, import_node_fs23.renameSync)(move.from, target);
19678
19997
  moved += 1;
19679
19998
  } catch {
19680
19999
  failed.push(move);
@@ -19693,23 +20012,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
19693
20012
  }
19694
20013
  async function clearNestedPluginTreeDir(targetPath) {
19695
20014
  try {
19696
- if (!(0, import_node_fs22.existsSync)(targetPath)) return true;
20015
+ if (!(0, import_node_fs23.existsSync)(targetPath)) return true;
19697
20016
  if (isWin) {
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 });
20017
+ const emptyDir = (0, import_node_path20.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
20018
+ (0, import_node_fs23.mkdirSync)(emptyDir, { recursive: true });
19700
20019
  try {
19701
20020
  await robocopyMirrorEmpty(emptyDir, targetPath);
19702
- (0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
20021
+ (0, import_node_fs23.rmSync)(targetPath, { recursive: true, force: true });
19703
20022
  } finally {
19704
20023
  try {
19705
- (0, import_node_fs22.rmSync)(emptyDir, { recursive: true, force: true });
20024
+ (0, import_node_fs23.rmSync)(emptyDir, { recursive: true, force: true });
19706
20025
  } catch {
19707
20026
  }
19708
20027
  }
19709
- return !(0, import_node_fs22.existsSync)(targetPath);
20028
+ return !(0, import_node_fs23.existsSync)(targetPath);
19710
20029
  }
19711
- (0, import_node_fs22.rmSync)(targetPath, { recursive: true, force: true });
19712
- return !(0, import_node_fs22.existsSync)(targetPath);
20030
+ (0, import_node_fs23.rmSync)(targetPath, { recursive: true, force: true });
20031
+ return !(0, import_node_fs23.existsSync)(targetPath);
19713
20032
  } catch {
19714
20033
  return false;
19715
20034
  }
@@ -19722,11 +20041,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
19722
20041
  }
19723
20042
  return true;
19724
20043
  }
19725
- var gitignorePath = () => (0, import_node_path19.join)(process.cwd(), ".gitignore");
20044
+ var gitignorePath = () => (0, import_node_path20.join)(process.cwd(), ".gitignore");
19726
20045
  function readTextFile(path2) {
19727
20046
  try {
19728
- if (!(0, import_node_fs22.existsSync)(path2)) return null;
19729
- return (0, import_node_fs22.readFileSync)(path2, "utf8");
20047
+ if (!(0, import_node_fs23.existsSync)(path2)) return null;
20048
+ return (0, import_node_fs23.readFileSync)(path2, "utf8");
19730
20049
  } catch {
19731
20050
  return null;
19732
20051
  }
@@ -19735,9 +20054,9 @@ function playwrightMcpConfigSnapshots() {
19735
20054
  const cwd = process.cwd();
19736
20055
  const home = (0, import_node_os6.homedir)();
19737
20056
  const candidates = [
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")
20057
+ (0, import_node_path20.join)(cwd, ".cursor", "mcp.json"),
20058
+ (0, import_node_path20.join)(home, ".cursor", "mcp.json"),
20059
+ (0, import_node_path20.join)(home, ".codex", "config.toml")
19741
20060
  ];
19742
20061
  const out = [];
19743
20062
  for (const path2 of candidates) {
@@ -19750,7 +20069,7 @@ function strayBrowserArtifactPaths() {
19750
20069
  const cwd = process.cwd();
19751
20070
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
19752
20071
  try {
19753
- return (0, import_node_fs22.existsSync)((0, import_node_path19.join)(cwd, rel));
20072
+ return (0, import_node_fs23.existsSync)((0, import_node_path20.join)(cwd, rel));
19754
20073
  } catch {
19755
20074
  return false;
19756
20075
  }
@@ -19758,14 +20077,14 @@ function strayBrowserArtifactPaths() {
19758
20077
  }
19759
20078
  function readGitignore() {
19760
20079
  try {
19761
- return (0, import_node_fs22.readFileSync)(gitignorePath(), "utf8");
20080
+ return (0, import_node_fs23.readFileSync)(gitignorePath(), "utf8");
19762
20081
  } catch {
19763
20082
  return null;
19764
20083
  }
19765
20084
  }
19766
20085
  function writeGitignore(content) {
19767
20086
  try {
19768
- (0, import_node_fs22.writeFileSync)(gitignorePath(), content, "utf8");
20087
+ (0, import_node_fs23.writeFileSync)(gitignorePath(), content, "utf8");
19769
20088
  return true;
19770
20089
  } catch {
19771
20090
  return false;
@@ -19804,7 +20123,7 @@ async function runDoctor(opts, io = consoleIo) {
19804
20123
  let onPath = pathProbe;
19805
20124
  if (!onPath) {
19806
20125
  const root = process.env.CLAUDE_PLUGIN_ROOT;
19807
- if (root && (0, import_node_fs22.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
20126
+ if (root && (0, import_node_fs23.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
19808
20127
  }
19809
20128
  checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
19810
20129
  const surface = detectSurface(process.env);
@@ -19837,9 +20156,10 @@ async function runDoctor(opts, io = consoleIo) {
19837
20156
  }
19838
20157
  checks.push({ ok: cloneOk, label: "plugin git clone (SSH\u2192HTTPS rewrite)", fix: CLONE_FIX });
19839
20158
  const installed = readInstalledPlugins();
20159
+ const claudeSettings = readClaudeSettings();
19840
20160
  let pluginCheck = buildPluginInstallRecordCheck({
19841
20161
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19842
- settings: readClaudeSettings(),
20162
+ settings: claudeSettings,
19843
20163
  installed,
19844
20164
  projectPath: process.cwd(),
19845
20165
  mirrorFrom: existingMirrorRecord(installed),
@@ -19852,6 +20172,7 @@ async function runDoctor(opts, io = consoleIo) {
19852
20172
  }
19853
20173
  }
19854
20174
  checks.push(pluginCheck);
20175
+ checks.push(buildSettingsPluginDriftCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), settings: claudeSettings }));
19855
20176
  let legacyPluginCheck = buildLegacyPluginInstallCheck({
19856
20177
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19857
20178
  sources: installedPluginSources(),
@@ -20006,7 +20327,7 @@ async function runDoctor(opts, io = consoleIo) {
20006
20327
  isOrgRepo: Boolean(cfg.sagaApiUrl),
20007
20328
  surface,
20008
20329
  cacheRoot: cursorCacheRoot,
20009
- cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
20330
+ cacheRootExists: (0, import_node_fs23.existsSync)(cursorCacheRoot),
20010
20331
  pins: cursorPins,
20011
20332
  hubCheckout: hubCheckoutForCursorSeed(),
20012
20333
  releasedVersion
@@ -20017,7 +20338,7 @@ async function runDoctor(opts, io = consoleIo) {
20017
20338
  releasedVersion,
20018
20339
  hubCheckout: hubCheckoutForCursorSeed(),
20019
20340
  execFileP: execFileP2,
20020
- mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path19.join)((0, import_node_os6.tmpdir)(), prefix)),
20341
+ mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path20.join)((0, import_node_os6.tmpdir)(), prefix)),
20021
20342
  log: (m) => io.err(m)
20022
20343
  });
20023
20344
  if (seeded) {
@@ -20026,7 +20347,7 @@ async function runDoctor(opts, io = consoleIo) {
20026
20347
  isOrgRepo: Boolean(cfg.sagaApiUrl),
20027
20348
  surface,
20028
20349
  cacheRoot: cursorCacheRoot,
20029
- cacheRootExists: (0, import_node_fs22.existsSync)(cursorCacheRoot),
20350
+ cacheRootExists: (0, import_node_fs23.existsSync)(cursorCacheRoot),
20030
20351
  pins: cursorPins,
20031
20352
  hubCheckout: hubCheckoutForCursorSeed(),
20032
20353
  releasedVersion
@@ -20231,7 +20552,15 @@ program2.command("session-start").description("run the SessionStart verbs (rules
20231
20552
  northstarInjected = await runNorthstarContext(io, {
20232
20553
  loadPlans: () => scopedPlanList(planDeps),
20233
20554
  readLocal: (slug) => planDeps.readLocal(slug),
20234
- gatherSignals: gatherRelevanceSignals
20555
+ // #1812: thread the saga HEAD's North Star anchor (its NEXT slug) into the relevance gate so
20556
+ // the plan the agent is actively on is force-injected even on a generic branch with no token
20557
+ // overlap. fetchSagaHead errors are swallowed via a silent io — a missing/failed HEAD just
20558
+ // falls back to token-overlap scoring, never noises or blocks the banner.
20559
+ gatherSignals: () => gatherRelevanceSignals({
20560
+ anchorSlug: () => fetchSagaHead({ log: () => {
20561
+ }, err: () => {
20562
+ } }).then((h) => h?.anchor?.slug ?? void 0)
20563
+ })
20235
20564
  });
20236
20565
  },
20237
20566
  sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
@@ -20247,12 +20576,16 @@ program2.command("session-start").description("run the SessionStart verbs (rules
20247
20576
  },
20248
20577
  boardSlice: (io) => runBoardSlice(io, {
20249
20578
  loadConfig: () => loadConfigForRepo(),
20250
- readBoard
20579
+ readBoard,
20580
+ // #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
20581
+ // never costs banner time and next session's glance renders instantly within budget.
20582
+ scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
20251
20583
  }),
20252
20584
  doctor: (io) => runDoctor({ banner: true }, io)
20253
20585
  });
20254
20586
  await runSessionStart(parallel, sequential, consoleIo);
20255
20587
  consoleIo.log(northstarPointer(northstarInjected));
20588
+ consoleIo.log(kbPointer());
20256
20589
  for (const line of planStoreLines(process.cwd())) consoleIo.log(line);
20257
20590
  for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
20258
20591
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());