@mutmutco/cli 2.35.0 → 2.37.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.
package/dist/main.cjs CHANGED
@@ -2160,10 +2160,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
2160
2160
  _executeSubCommand(subcommand, args) {
2161
2161
  args = args.slice();
2162
2162
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2163
- function findFile(baseDir, baseName) {
2164
- const localBin = import_node_path.default.resolve(baseDir, baseName);
2163
+ function findFile(baseDir, baseName2) {
2164
+ const localBin = import_node_path.default.resolve(baseDir, baseName2);
2165
2165
  if (import_node_fs.default.existsSync(localBin)) return localBin;
2166
- if (sourceExt.includes(import_node_path.default.extname(baseName))) return void 0;
2166
+ if (sourceExt.includes(import_node_path.default.extname(baseName2))) return void 0;
2167
2167
  const foundExt = sourceExt.find(
2168
2168
  (ext) => import_node_fs.default.existsSync(`${localBin}${ext}`)
2169
2169
  );
@@ -3391,8 +3391,8 @@ function useColor() {
3391
3391
  var program = new Command();
3392
3392
 
3393
3393
  // src/index.ts
3394
- var import_promises5 = require("node:fs/promises");
3395
- var import_node_fs16 = require("node:fs");
3394
+ var import_promises6 = require("node:fs/promises");
3395
+ var import_node_fs19 = require("node:fs");
3396
3396
 
3397
3397
  // src/rules-sync.ts
3398
3398
  function normalizeEol(s) {
@@ -3833,6 +3833,7 @@ function createGitHubClient(options = {}) {
3833
3833
  const token = options.token ?? githubToken;
3834
3834
  const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
3835
3835
  const fetchImpl = options.fetchImpl ?? fetch;
3836
+ const externalSignal = options.signal;
3836
3837
  async function request2(method, url, init = {}) {
3837
3838
  const t = await token();
3838
3839
  const headers = {
@@ -3843,11 +3844,12 @@ function createGitHubClient(options = {}) {
3843
3844
  ...init.body !== void 0 ? { "Content-Type": "application/json" } : {},
3844
3845
  ...init.headers
3845
3846
  };
3847
+ const timeoutSignal = AbortSignal.timeout(init.timeoutMs ?? defaultTimeoutMs);
3846
3848
  const res = await fetchImpl(url, {
3847
3849
  method,
3848
3850
  headers,
3849
3851
  body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
3850
- signal: AbortSignal.timeout(init.timeoutMs ?? defaultTimeoutMs)
3852
+ signal: externalSignal ? AbortSignal.any([externalSignal, timeoutSignal]) : timeoutSignal
3851
3853
  });
3852
3854
  if (!res.ok) throw await errorFromResponse(res);
3853
3855
  return res;
@@ -3905,9 +3907,9 @@ var HEAD_MIN_INTERVAL_MS = 5 * 60 * 1e3;
3905
3907
  var HEAD_ENGINE_TIMEOUT_MS = 15e3;
3906
3908
  var HEAD_PROMPT_ACTION_LIMIT = 50;
3907
3909
  var HEAD_PROMPT_DECISION_LIMIT = 80;
3908
- function resolveEngine(platform, custom) {
3910
+ function resolveEngine(platform2, custom) {
3909
3911
  if (custom) return { cmd: custom, args: [], shell: true };
3910
- return { cmd: "claude", args: ["-p", "--no-session-persistence"], shell: platform === "win32" };
3912
+ return { cmd: "claude", args: ["-p", "--no-session-persistence"], shell: platform2 === "win32" };
3911
3913
  }
3912
3914
  function headTsPath(key) {
3913
3915
  const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
@@ -4263,6 +4265,13 @@ var import_node_crypto3 = require("node:crypto");
4263
4265
  var import_promises2 = require("node:fs/promises");
4264
4266
 
4265
4267
  // src/issue-body.ts
4268
+ var import_node_os2 = require("node:os");
4269
+ function emptyStdinMessage(fileFlag) {
4270
+ if ((0, import_node_os2.platform)() === "win32") {
4271
+ return `${fileFlag} - read empty stdin (on Windows, ${fileFlag} - is unreliable through the npm .cmd shim \u2014 use ${fileFlag} <path>, or pipe to \`node cli/dist/index.cjs\` directly)`;
4272
+ }
4273
+ return `${fileFlag} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${fileFlag} <path>)`;
4274
+ }
4266
4275
  async function resolveTextArg(input, deps, labels) {
4267
4276
  const hasValue = input.value !== void 0;
4268
4277
  const hasFile = input.file !== void 0;
@@ -4277,7 +4286,7 @@ async function resolveTextArg(input, deps, labels) {
4277
4286
  const text = source === "-" ? await deps.readStdin() : await deps.readFile(source, "utf8");
4278
4287
  if (text.trim().length === 0) {
4279
4288
  throw new Error(
4280
- source === "-" ? `${labels.file} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${labels.file} <path>)` : `${labels.file} produced an empty ${labels.noun}`
4289
+ source === "-" ? emptyStdinMessage(labels.file) : `${labels.file} produced an empty ${labels.noun}`
4281
4290
  );
4282
4291
  }
4283
4292
  return text;
@@ -4299,7 +4308,7 @@ function resolveIssueTitle(input, deps) {
4299
4308
 
4300
4309
  // src/saga-capture.ts
4301
4310
  var import_node_fs8 = require("node:fs");
4302
- var import_node_os2 = require("node:os");
4311
+ var import_node_os3 = require("node:os");
4303
4312
  var import_node_path6 = require("node:path");
4304
4313
  function parseHookInput(stdin) {
4305
4314
  try {
@@ -4315,7 +4324,17 @@ function cursorProjectSlug(workspaceRoot) {
4315
4324
  return p.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
4316
4325
  }
4317
4326
  function cursorProjectsRoot(env) {
4318
- return (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0, import_node_os2.homedir)(), ".cursor", "projects");
4327
+ return (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0, import_node_os3.homedir)(), ".cursor", "projects");
4328
+ }
4329
+ function resolveClaudeTranscriptPath(hook, env = process.env) {
4330
+ const sessionId = hook.session_id?.trim();
4331
+ if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return void 0;
4332
+ const cwd = hook.cwd?.trim() || env.CLAUDE_PROJECT_DIR?.trim();
4333
+ if (!cwd) return void 0;
4334
+ const encoded = cwd.replace(/[^A-Za-z0-9]/g, "-");
4335
+ const root = (0, import_node_path6.join)(env.USERPROFILE ?? env.HOME ?? (0, import_node_os3.homedir)(), ".claude", "projects");
4336
+ const candidate = (0, import_node_path6.join)(root, encoded, `${sessionId}.jsonl`);
4337
+ return (0, import_node_fs8.existsSync)(candidate) ? candidate : void 0;
4319
4338
  }
4320
4339
  function resolveCursorTranscriptPath(hook, env = process.env) {
4321
4340
  const conversationId = (hook.conversation_id ?? hook.conversationId ?? hook.session_id)?.trim();
@@ -4339,7 +4358,7 @@ function resolveTranscriptPath(hook, env = process.env) {
4339
4358
  if (surface === "cursor" || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) {
4340
4359
  return resolveCursorTranscriptPath(hook, env);
4341
4360
  }
4342
- return void 0;
4361
+ return resolveClaudeTranscriptPath(hook, env);
4343
4362
  }
4344
4363
  function ingestTranscriptFallbackHint(surface) {
4345
4364
  if (surface === "cursor") {
@@ -4348,6 +4367,9 @@ function ingestTranscriptFallbackHint(surface) {
4348
4367
  if (surface === "codex") {
4349
4368
  return 'Codex Stop hook may omit transcript_path \u2014 run: mmi-cli honcho ingest --source note --summary "<turn summary>"';
4350
4369
  }
4370
+ if (surface === "claude") {
4371
+ return 'Claude Stop hook omitted transcript_path and no transcript file was found \u2014 run: mmi-cli honcho ingest --source note --summary "<turn summary>"';
4372
+ }
4351
4373
  return void 0;
4352
4374
  }
4353
4375
 
@@ -4447,6 +4469,7 @@ function buildNoteCapture(summary, o, id, evidence) {
4447
4469
  next: o.next,
4448
4470
  decision: o.decision,
4449
4471
  queueOp,
4472
+ handoffClose: o.handoffClose,
4450
4473
  state,
4451
4474
  source,
4452
4475
  evidence: Object.keys(ev).length ? ev : void 0,
@@ -5080,9 +5103,276 @@ function registerSagaCommands(program3) {
5080
5103
  });
5081
5104
  }
5082
5105
 
5106
+ // src/handoff-commands.ts
5107
+ var import_node_crypto4 = require("node:crypto");
5108
+ var import_promises3 = require("node:fs/promises");
5109
+
5110
+ // src/handoff.ts
5111
+ var HANDOFF_PREFIX = "handoff:";
5112
+ function clean(value) {
5113
+ return value?.trim() ?? "";
5114
+ }
5115
+ function normalizeHandoffKey(value) {
5116
+ const key = clean(value);
5117
+ if (!key) throw new Error("handoff key is required");
5118
+ return key;
5119
+ }
5120
+ function serializeHandoff(record) {
5121
+ return `${HANDOFF_PREFIX}${JSON.stringify(record)}`;
5122
+ }
5123
+ function parseHandoffText(text) {
5124
+ if (!text.startsWith(HANDOFF_PREFIX)) return null;
5125
+ try {
5126
+ const raw = JSON.parse(text.slice(HANDOFF_PREFIX.length));
5127
+ const state = raw.state;
5128
+ const key = clean(raw.key);
5129
+ const northStarSlug = clean(raw.northStarSlug);
5130
+ const summary = clean(raw.summary);
5131
+ const sourceSessionId = clean(raw.sourceSessionId);
5132
+ const createdAt = clean(raw.createdAt);
5133
+ if (state !== "open" && state !== "claimed" && state !== "cancelled") return null;
5134
+ if (!key || !northStarSlug || !summary || !sourceSessionId || !createdAt) return null;
5135
+ return {
5136
+ state,
5137
+ key,
5138
+ northStarSlug,
5139
+ summary,
5140
+ sourceSessionId,
5141
+ createdAt,
5142
+ claimedBySessionId: clean(raw.claimedBySessionId) || void 0,
5143
+ closedAt: clean(raw.closedAt) || void 0
5144
+ };
5145
+ } catch {
5146
+ return null;
5147
+ }
5148
+ }
5149
+ function listHandoffs(head, opts = {}) {
5150
+ const parsed = (head.queued ?? []).map((item, index) => ({ done: item.done, index, record: parseHandoffText(item.text) })).filter((x) => x.record !== null);
5151
+ return parsed.filter((x) => opts.includeClosed || x.record.state === "open" && !x.done).map((x) => ({ index: x.index, done: x.done, record: x.record }));
5152
+ }
5153
+ function findOpenHandoff(head, key) {
5154
+ const normalized = normalizeHandoffKey(key).toLowerCase();
5155
+ return listHandoffs(head).find((item) => item.record.key.toLowerCase() === normalized || item.record.northStarSlug.toLowerCase() === normalized);
5156
+ }
5157
+ function planOpenHandoff(input) {
5158
+ const key = normalizeHandoffKey(input.key);
5159
+ const northStarSlug = clean(input.northStarSlug);
5160
+ const summary = clean(input.summary);
5161
+ const sourceSessionId = clean(input.sourceSessionId);
5162
+ if (!northStarSlug) throw new Error("north star slug is required");
5163
+ if (!summary) throw new Error("handoff summary is required");
5164
+ if (!sourceSessionId) throw new Error("source session id is required");
5165
+ return {
5166
+ state: "open",
5167
+ key,
5168
+ northStarSlug,
5169
+ summary,
5170
+ sourceSessionId,
5171
+ createdAt: input.createdAt
5172
+ };
5173
+ }
5174
+ function closeHandoff(record, state, at, claimedBySessionId) {
5175
+ return {
5176
+ ...record,
5177
+ state,
5178
+ closedAt: at,
5179
+ claimedBySessionId: claimedBySessionId || record.claimedBySessionId
5180
+ };
5181
+ }
5182
+ function formatHandoffLine(item) {
5183
+ const r = item.record;
5184
+ const suffix = r.state === "claimed" && r.claimedBySessionId ? ` -> ${r.claimedBySessionId}` : "";
5185
+ return `${r.key} [${r.state}] northstar:${r.northStarSlug} source:${r.sourceSessionId}${suffix} - ${r.summary}`;
5186
+ }
5187
+
5188
+ // src/handoff-commands.ts
5189
+ var FOREGROUND_FETCH = { attempts: 2, timeoutMs: 8e3 };
5190
+ var SESSION_START_FETCH = { attempts: 1, timeoutMs: 3e3 };
5191
+ async function fetchState(url, qs, retry = FOREGROUND_FETCH) {
5192
+ const res = await fetchWithRetry(fetch, `${url}/saga/state?${qs}`, { headers: await hubHeaders() }, retry);
5193
+ if (!res.ok) return null;
5194
+ const body = await res.json();
5195
+ if ("state" in body) return body;
5196
+ return { key: null, state: { head: body.head } };
5197
+ }
5198
+ async function fetchScopedSessions(url, project2, branch, retry = FOREGROUND_FETCH) {
5199
+ const qs = new URLSearchParams({ project: project2, branch });
5200
+ const res = await fetchWithRetry(fetch, `${url}/saga/sessions?${qs}`, { headers: await hubHeaders() }, retry);
5201
+ if (!res.ok) return [];
5202
+ const body = await res.json();
5203
+ return body.sessions ?? [];
5204
+ }
5205
+ async function fetchSessionHead(url, key, retry = FOREGROUND_FETCH) {
5206
+ const qs = new URLSearchParams(key);
5207
+ const got = await fetchState(url, qs, retry);
5208
+ return got?.state?.head ?? null;
5209
+ }
5210
+ async function fetchCurrentState() {
5211
+ const cfg = await loadConfig();
5212
+ if (!cfg.sagaApiUrl) return null;
5213
+ const key = await sagaKey(cfg);
5214
+ const got = await fetchState(cfg.sagaApiUrl, new URLSearchParams(key));
5215
+ return got ? { key, head: got.state?.head ?? {} } : null;
5216
+ }
5217
+ async function collectScopedHandoffs(opts = {}) {
5218
+ const cfg = await loadConfig();
5219
+ if (!cfg.sagaApiUrl) return { handoffs: [], sessions: [] };
5220
+ const key = await sagaKey(cfg);
5221
+ const retry = opts.retry ?? FOREGROUND_FETCH;
5222
+ const sessions = await fetchScopedSessions(cfg.sagaApiUrl, key.project, key.branch, retry);
5223
+ const seen = /* @__PURE__ */ new Set();
5224
+ const handoffs = [];
5225
+ for (const session of sessions) {
5226
+ const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
5227
+ if (!head) continue;
5228
+ for (const item of listHandoffs(head, { includeClosed: opts.includeClosed })) {
5229
+ const dedupe = `${item.record.sourceSessionId}:${item.record.key}:${item.record.state}`;
5230
+ if (seen.has(dedupe)) continue;
5231
+ seen.add(dedupe);
5232
+ handoffs.push(item);
5233
+ }
5234
+ }
5235
+ return { handoffs, sessions };
5236
+ }
5237
+ async function locateOpenHandoff(key, retry = FOREGROUND_FETCH) {
5238
+ const cfg = await loadConfig();
5239
+ if (!cfg.sagaApiUrl) return null;
5240
+ const scopeKey = await sagaKey(cfg);
5241
+ const sessions = await fetchScopedSessions(cfg.sagaApiUrl, scopeKey.project, scopeKey.branch, retry);
5242
+ for (const session of sessions) {
5243
+ const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
5244
+ if (!head) continue;
5245
+ const item = findOpenHandoff(head, key);
5246
+ if (item) {
5247
+ return {
5248
+ key: { project: session.project, branch: session.branch, sessionId: session.sessionId },
5249
+ item
5250
+ };
5251
+ }
5252
+ }
5253
+ return null;
5254
+ }
5255
+ async function locateClaimedHandoff(key, claimedBySessionId, retry = FOREGROUND_FETCH) {
5256
+ const { handoffs } = await collectScopedHandoffs({ includeClosed: true, retry });
5257
+ const normalized = key.toLowerCase();
5258
+ return handoffs.find((item) => {
5259
+ const r = item.record;
5260
+ return r.state === "claimed" && r.claimedBySessionId === claimedBySessionId && (r.key.toLowerCase() === normalized || r.northStarSlug.toLowerCase() === normalized);
5261
+ }) ?? null;
5262
+ }
5263
+ async function postKeyedNote(key, summary, options) {
5264
+ const cfg = await loadConfig();
5265
+ if (!cfg.sagaApiUrl) fail("handoff: Hub API URL not configured");
5266
+ const sha = await gitOut(["rev-parse", "--short", "HEAD"]);
5267
+ const capture = buildNoteCapture(summary, options, (0, import_node_crypto4.randomUUID)(), { sha: sha || void 0, branch: key.branch });
5268
+ const result = await postCaptureOnce(cfg.sagaApiUrl, { ...capture, ...key });
5269
+ if (!result.ok) fail(`handoff: write failed${result.status ? ` (HTTP ${result.status})` : ""}${result.message ? `: ${result.message}` : ""}`);
5270
+ }
5271
+ function deriveOpenFields(summary, opts, head, key) {
5272
+ const northStarSlug = opts.northStarSlug ?? head.anchor?.slug;
5273
+ const handoffKey = opts.key ?? northStarSlug;
5274
+ const text = summary?.trim() || head.next?.trim() || head.anchor?.intent?.trim();
5275
+ return planOpenHandoff({
5276
+ key: handoffKey ?? "",
5277
+ northStarSlug: northStarSlug ?? "",
5278
+ summary: text ?? "",
5279
+ sourceSessionId: key.sessionId,
5280
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5281
+ });
5282
+ }
5283
+ async function runHandoffOpen(summary, opts, io = consoleIo) {
5284
+ let resolvedSummary = summary;
5285
+ if (summary !== void 0 || opts.messageFile !== void 0) {
5286
+ try {
5287
+ resolvedSummary = await resolveTextArg({ value: summary, file: opts.messageFile }, { readFile: import_promises3.readFile, readStdin }, {
5288
+ value: "a summary argument",
5289
+ file: "--message-file",
5290
+ noun: "message"
5291
+ });
5292
+ } catch (e) {
5293
+ return fail(`handoff open: ${e.message}`);
5294
+ }
5295
+ }
5296
+ const current = await fetchCurrentState();
5297
+ if (!current) return fail("handoff open: saga state unavailable");
5298
+ let record;
5299
+ try {
5300
+ record = deriveOpenFields(resolvedSummary, opts, current.head, current.key);
5301
+ } catch (e) {
5302
+ return fail(`handoff open: ${e.message}`);
5303
+ }
5304
+ await postKeyedNote(current.key, `handoff opened ${record.key}`, { queueAdd: serializeHandoff(record), anchorSlug: record.northStarSlug });
5305
+ if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: record }));
5306
+ io.log(`handoff open: ${formatHandoffLine({ index: -1, done: false, record })}`);
5307
+ }
5308
+ async function runHandoffList(opts, io = consoleIo) {
5309
+ const { handoffs } = await collectScopedHandoffs({ includeClosed: opts.all });
5310
+ if (opts.json) {
5311
+ io.log(JSON.stringify({ handoffs }, null, 2));
5312
+ } else if (!handoffs.length) {
5313
+ io.log("handoff list: none");
5314
+ } else {
5315
+ for (const h of handoffs) io.log(formatHandoffLine(h));
5316
+ }
5317
+ return handoffs;
5318
+ }
5319
+ async function runHandoffOffer(io = consoleIo, opts = {}) {
5320
+ const retry = opts.fast ? SESSION_START_FETCH : FOREGROUND_FETCH;
5321
+ const { handoffs } = await collectScopedHandoffs({ retry });
5322
+ if (!handoffs.length) return;
5323
+ io.log("Open handoffs:");
5324
+ for (const h of handoffs) io.log(`- ${formatHandoffLine(h)}`);
5325
+ io.log("Use `mmi-cli handoff accept <key>` to claim, `mmi-cli handoff cancel <key>` to close, or decline in chat to leave it open.");
5326
+ }
5327
+ async function closeSourceHandoff(key, state, opts = {}, io = consoleIo) {
5328
+ const current = await sagaKey(await loadConfig(), resolveSessionId());
5329
+ const located = await locateOpenHandoff(key);
5330
+ if (!located) {
5331
+ if (state === "claimed") {
5332
+ const prior = await locateClaimedHandoff(key, current.sessionId);
5333
+ if (prior) {
5334
+ if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: prior.record, idempotent: true }));
5335
+ return io.log(`handoff claimed: ${formatHandoffLine(prior)} (already accepted)`);
5336
+ }
5337
+ }
5338
+ return fail(`handoff ${state}: no open handoff matching ${key}`);
5339
+ }
5340
+ const closed = closeHandoff(located.item.record, state, (/* @__PURE__ */ new Date()).toISOString(), state === "claimed" ? current.sessionId : void 0);
5341
+ await postKeyedNote(located.key, `handoff ${state} ${located.item.record.key}`, {
5342
+ handoffClose: { index: located.item.index, closedText: serializeHandoff(closed) }
5343
+ });
5344
+ if (state === "claimed") {
5345
+ await postKeyedNote(current, `handoff accepted ${located.item.record.key}`, {
5346
+ anchor: located.item.record.summary,
5347
+ anchorSlug: located.item.record.northStarSlug,
5348
+ anchorForce: true,
5349
+ decision: `accepted handoff ${located.item.record.key} from session ${located.item.record.sourceSessionId}; northstar ${located.item.record.northStarSlug}`,
5350
+ verified: true
5351
+ });
5352
+ }
5353
+ if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: closed }));
5354
+ io.log(`handoff ${state}: ${formatHandoffLine({ index: located.item.index, done: true, record: closed })}`);
5355
+ }
5356
+ async function runHandoffDecline(key, opts = {}, io = consoleIo) {
5357
+ const located = await locateOpenHandoff(key);
5358
+ if (!located) return fail(`handoff decline: no open handoff matching ${key}`);
5359
+ if (opts.json) return io.log(JSON.stringify({ ok: true, unchanged: true, handoff: located.item.record }));
5360
+ io.log(`handoff decline: left open for re-offer \u2014 ${formatHandoffLine(located.item)}`);
5361
+ }
5362
+ function registerHandoffCommands(program3) {
5363
+ const handoff = program3.command("handoff").description("explicit saga + North Star handoff lifecycle");
5364
+ 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));
5365
+ handoff.command("list").description("list open handoffs for the current repo/branch").option("--all", "include claimed/cancelled records").option("--json", "machine-readable output").action(async (opts) => {
5366
+ await runHandoffList(opts);
5367
+ });
5368
+ handoff.command("accept <key>").description("claim an open handoff and bind this session to its North Star").option("--json", "machine-readable output").action((key, opts) => closeSourceHandoff(key, "claimed", opts));
5369
+ handoff.command("cancel <key>").description("close an open handoff without claiming it").option("--json", "machine-readable output").action((key, opts) => closeSourceHandoff(key, "cancelled", opts));
5370
+ handoff.command("decline <key>").description("leave an open handoff unchanged so it is re-offered later").option("--json", "machine-readable output").action((key, opts) => runHandoffDecline(key, opts));
5371
+ }
5372
+
5083
5373
  // src/honcho-commands.ts
5084
5374
  var import_node_child_process5 = require("node:child_process");
5085
- var import_promises3 = require("node:fs/promises");
5375
+ var import_promises4 = require("node:fs/promises");
5086
5376
  var import_node_fs10 = require("node:fs");
5087
5377
  var import_node_path8 = require("node:path");
5088
5378
 
@@ -5631,6 +5921,28 @@ async function honchoApiKey(env = process.env, fetcher = vaultKeyFetcher) {
5631
5921
  cachedVaultKey ??= fetcher().catch(() => null);
5632
5922
  return cachedVaultKey;
5633
5923
  }
5924
+ function honchoKeyReasonFromStatus(status) {
5925
+ if (status >= 200 && status < 300) return "ok";
5926
+ if (status === 401 || status === 403) return "forbidden";
5927
+ if (status === 404) return "missing";
5928
+ return "unreachable";
5929
+ }
5930
+ async function diagnoseHonchoKey(env = process.env, fetchImpl = fetch) {
5931
+ if ((env.HONCHO_API_KEY || "").trim()) return { reason: "ok" };
5932
+ const cfg = await loadConfig();
5933
+ if (!cfg.sagaApiUrl) return { reason: "unreachable" };
5934
+ try {
5935
+ const res = await fetchImpl(`${cfg.sagaApiUrl}/secrets/get`, {
5936
+ method: "POST",
5937
+ headers: await hubHeaders({ "content-type": "application/json" }),
5938
+ body: JSON.stringify({ repo: HONCHO_SECRET_REPO, key: HONCHO_SECRET_KEY }),
5939
+ signal: AbortSignal.timeout(8e3)
5940
+ });
5941
+ return { reason: honchoKeyReasonFromStatus(res.status), httpStatus: res.status };
5942
+ } catch {
5943
+ return { reason: "unreachable" };
5944
+ }
5945
+ }
5634
5946
  async function resolveHonchoConfig(cfg = {}, opts = {}) {
5635
5947
  const { apiUrl, workspace } = honchoEndpoint(cfg, opts.env);
5636
5948
  if (!apiUrl) return null;
@@ -5640,7 +5952,7 @@ async function resolveHonchoConfig(cfg = {}, opts = {}) {
5640
5952
  }
5641
5953
 
5642
5954
  // src/honcho-ingest.ts
5643
- var import_node_crypto4 = require("node:crypto");
5955
+ var import_node_crypto5 = require("node:crypto");
5644
5956
  var DEFAULT_HONCHO_MAX_CHARS = 4e3;
5645
5957
  var DEFAULT_HONCHO_CARD_MAX_CHARS = 1200;
5646
5958
  var REDACTED = "[REDACTED]";
@@ -5726,7 +6038,7 @@ function buildIngestPayload(args) {
5726
6038
  const maxChars = args.maxChars ?? DEFAULT_HONCHO_MAX_CHARS;
5727
6039
  const messages = args.messages.map((m) => ({ role: m.role, content: capContent(redactSecrets(m.content), maxChars) })).filter((m) => m.content.trim().length > 0);
5728
6040
  return {
5729
- id: args.id ?? (0, import_node_crypto4.randomUUID)(),
6041
+ id: args.id ?? (0, import_node_crypto5.randomUUID)(),
5730
6042
  workspace: args.workspace,
5731
6043
  peer: args.peer,
5732
6044
  session: args.session,
@@ -5950,7 +6262,7 @@ async function resolveSummaryText(summary, messageFile) {
5950
6262
  if (summary && summary.trim()) return summary.trim();
5951
6263
  if (messageFile) {
5952
6264
  try {
5953
- return (messageFile === "-" ? await readStdin() : await (0, import_promises3.readFile)(messageFile, "utf8")).trim();
6265
+ return (messageFile === "-" ? await readStdin() : await (0, import_promises4.readFile)(messageFile, "utf8")).trim();
5954
6266
  } catch {
5955
6267
  return "";
5956
6268
  }
@@ -5978,7 +6290,7 @@ async function runHonchoIngest(opts) {
5978
6290
  const transcriptPath = resolveTranscriptPath(hook);
5979
6291
  if (transcriptPath) {
5980
6292
  try {
5981
- messages = extractTurnMessages(await (0, import_promises3.readFile)(transcriptPath, "utf8"));
6293
+ messages = extractTurnMessages(await (0, import_promises4.readFile)(transcriptPath, "utf8"));
5982
6294
  } catch {
5983
6295
  recordIngestSkip({
5984
6296
  reason: "transcript-unreadable",
@@ -6127,6 +6439,7 @@ async function runHonchoHealth(o, io = consoleIo) {
6127
6439
  const { apiUrl, workspace } = honchoEndpoint(cfg);
6128
6440
  const apiKey = await honchoApiKey();
6129
6441
  const hc = apiKey ? { apiUrl, apiKey, workspace } : null;
6442
+ const keyReason = apiKey ? "ok" : (await diagnoseHonchoKey()).reason;
6130
6443
  const peer = honchoPeerId(await honchoLogin(cfg), cfg);
6131
6444
  const liveness = hc ? await probeHoncho(hc, fetch, 3e3, { peer: peer ?? void 0 }) : {
6132
6445
  reachable: false,
@@ -6144,6 +6457,7 @@ async function runHonchoHealth(o, io = consoleIo) {
6144
6457
  configured: !!hc,
6145
6458
  apiUrl,
6146
6459
  apiKeyConfigured: !!apiKey,
6460
+ keyReason,
6147
6461
  reachable: liveness.reachable,
6148
6462
  authOk: liveness.authOk,
6149
6463
  status: liveness.status,
@@ -6168,7 +6482,11 @@ async function runHonchoHealth(o, io = consoleIo) {
6168
6482
  return;
6169
6483
  }
6170
6484
  io.log(`honcho health: ${report.ok ? "OK" : "NOT OK"}`);
6171
- if (!apiKey) io.log(" - no client key available from the vault \u2014 honcho is off");
6485
+ if (!apiKey) {
6486
+ if (keyReason === "forbidden") io.log(" - off: client key forbidden (401/403) \u2014 your account lacks Honcho access");
6487
+ else if (keyReason === "missing") io.log(" - off: client key not found in the vault (404)");
6488
+ else io.log(" - off: vault unreachable \u2014 could not fetch the client key");
6489
+ }
6172
6490
  if (report.configured && !report.reachable) io.log(" - service unreachable");
6173
6491
  if (report.configured && report.reachable && !report.authOk) io.log(" - API key rejected (401/403) \u2014 rotate or fix vault path");
6174
6492
  if (!report.peer) io.log(" - no peer identity resolved (gh login) \u2014 ingest will be skipped");
@@ -6203,6 +6521,89 @@ function registerHonchoCommands(program3) {
6203
6521
  honcho.command("key").option("--json", "machine-readable output").description("print the resolved honcho identity (workspace, peer, session) \u2014 no write, no key").action((o) => runHonchoKey(o));
6204
6522
  }
6205
6523
 
6524
+ // src/scrooge-commands.ts
6525
+ var import_node_fs11 = require("node:fs");
6526
+ var import_node_path9 = require("node:path");
6527
+ var SCROOGE_TRACE_PATH = (0, import_node_path9.join)(".mmi", "scrooge", "trace.jsonl");
6528
+ function resolveModeFromEnv() {
6529
+ const v = String(process.env.MMI_SCROOGE ?? "conservative").trim().toLowerCase();
6530
+ if (!v || v === "conservative") return "conservative";
6531
+ if (v === "0" || v === "off" || v === "false") return "off";
6532
+ if (v === "normal") return "normal";
6533
+ if (v === "spike") return "spike";
6534
+ return "conservative";
6535
+ }
6536
+ function parseTraceLines(raw) {
6537
+ const out = [];
6538
+ for (const line of raw.split(/\r?\n/)) {
6539
+ const t = line.trim();
6540
+ if (!t) continue;
6541
+ try {
6542
+ const o = JSON.parse(t);
6543
+ if (o && typeof o === "object") out.push(o);
6544
+ } catch {
6545
+ }
6546
+ }
6547
+ return out;
6548
+ }
6549
+ function summarizeTrace(entries) {
6550
+ let tools = 0;
6551
+ let charsIn = 0;
6552
+ let charsOut = 0;
6553
+ const passCounts = {};
6554
+ for (const e of entries) {
6555
+ tools += 1;
6556
+ charsIn += Number(e.charsIn) || 0;
6557
+ charsOut += Number(e.charsOut) || 0;
6558
+ for (const p of e.passes ?? []) passCounts[p] = (passCounts[p] ?? 0) + 1;
6559
+ }
6560
+ const saved = Math.max(0, charsIn - charsOut);
6561
+ return {
6562
+ tools,
6563
+ charsIn,
6564
+ charsOut,
6565
+ charsSaved: saved,
6566
+ estTokensSaved: Math.round(saved / 4),
6567
+ passCounts
6568
+ };
6569
+ }
6570
+ function runScroogeReport(io, tracePath = SCROOGE_TRACE_PATH) {
6571
+ const mode = resolveModeFromEnv();
6572
+ if (!(0, import_node_fs11.existsSync)(tracePath)) {
6573
+ io.log(`Scrooge: no trace at ${tracePath} (hook has not compacted anything yet).`);
6574
+ io.log(`Active mode: ${mode} (MMI_SCROOGE env; default conservative).`);
6575
+ return 0;
6576
+ }
6577
+ let raw = "";
6578
+ try {
6579
+ raw = (0, import_node_fs11.readFileSync)(tracePath, "utf8");
6580
+ } catch (e) {
6581
+ io.err(`Scrooge: could not read trace: ${e.message}`);
6582
+ return 1;
6583
+ }
6584
+ const entries = parseTraceLines(raw);
6585
+ const s = summarizeTrace(entries);
6586
+ io.log("Scrooge session summary");
6587
+ io.log(` mode (current env): ${mode}`);
6588
+ io.log(` compacted tool results: ${s.tools}`);
6589
+ io.log(` chars in / out: ${s.charsIn} / ${s.charsOut}`);
6590
+ io.log(` chars saved: ${s.charsSaved}`);
6591
+ io.log(` est tokens saved (chars/4): ~${s.estTokensSaved}`);
6592
+ const passes = Object.entries(s.passCounts).sort((a, b) => b[1] - a[1]);
6593
+ if (passes.length) {
6594
+ io.log(" passes:");
6595
+ for (const [name, count] of passes) io.log(` ${name}: ${count}`);
6596
+ }
6597
+ return 0;
6598
+ }
6599
+ function registerScroogeCommands(program3) {
6600
+ const scrooge = program3.command("scrooge").description("Scrooge \u2014 org tool-output compaction (trace + report)");
6601
+ scrooge.command("report").description("print char/token savings from .mmi/scrooge/trace.jsonl").action(() => {
6602
+ const code = runScroogeReport({ log: (s) => console.log(s), err: (s) => console.error(s) });
6603
+ process.exit(code);
6604
+ });
6605
+ }
6606
+
6206
6607
  // src/docs-sync.ts
6207
6608
  var SYNCED_DOCS = ["README.md", "architecture.md"];
6208
6609
  async function syncDocs(deps, docs2 = SYNCED_DOCS) {
@@ -6225,8 +6626,205 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
6225
6626
  }
6226
6627
 
6227
6628
  // src/session-start.ts
6228
- var import_node_fs11 = require("node:fs");
6229
- var import_node_path9 = require("node:path");
6629
+ var import_node_fs13 = require("node:fs");
6630
+ var import_node_path11 = require("node:path");
6631
+
6632
+ // src/scratch-gc.ts
6633
+ var import_node_fs12 = require("node:fs");
6634
+ var import_node_path10 = require("node:path");
6635
+ var TMP_STALE_MS = 6e4;
6636
+ var FLUSH_LOCK_GC_MS = 6e5;
6637
+ var HEAD_TS_STALE_MS = 48 * 36e5;
6638
+ var LAST_SKIP_STALE_MS = 24 * 36e5;
6639
+ var CONFLICT_COPY_STALE_MS = 24 * 36e5;
6640
+ var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
6641
+ var SCRATCH_GC_THROTTLE_MS = 24 * 36e5;
6642
+ function scratchGcThrottlePath(mmiRoot) {
6643
+ return (0, import_node_path10.join)(mmiRoot, "head-ts", ".scratch-gc-last");
6644
+ }
6645
+ function scratchGcDue(stampPath, now = Date.now(), read = import_node_fs12.readFileSync) {
6646
+ try {
6647
+ return now - (Number(read(stampPath, "utf8").trim()) || 0) >= SCRATCH_GC_THROTTLE_MS;
6648
+ } catch {
6649
+ return true;
6650
+ }
6651
+ }
6652
+ function markScratchGcRun(stampPath, now = Date.now()) {
6653
+ try {
6654
+ (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(stampPath), { recursive: true });
6655
+ (0, import_node_fs12.writeFileSync)(stampPath, String(now), "utf8");
6656
+ } catch {
6657
+ }
6658
+ }
6659
+ function executeScratchGc(repoRoot, opts, now = Date.now()) {
6660
+ const snap = collectScratchSnapshot(repoRoot);
6661
+ const plan2 = planScratchGc(snap, now);
6662
+ if (!opts.apply) return { plan: plan2 };
6663
+ return { plan: plan2, applied: applyScratchGc(plan2, snap.mmiRoot, now) };
6664
+ }
6665
+ var NEVER_BASENAMES = /* @__PURE__ */ new Set([".session", "config.json", "saga-pending.jsonl"]);
6666
+ var TMP_SIDECAR_BASES = ["saga-pending.jsonl", "last-skip.json"];
6667
+ var CONFLICT_COPY_ALLOWLIST = /* @__PURE__ */ new Set(["saga-pending.jsonl", "last-skip.json"]);
6668
+ function conflictCopyOriginal(name) {
6669
+ const m = /^(.+) \d+(\.[^.]+)$/.exec(name);
6670
+ return m ? `${m[1]}${m[2]}` : null;
6671
+ }
6672
+ function isTmpSidecar(name) {
6673
+ return name.endsWith(".tmp") && TMP_SIDECAR_BASES.some((b) => name.startsWith(`${b}.`));
6674
+ }
6675
+ function planScratchGc(snap, now = Date.now()) {
6676
+ const candidates = [];
6677
+ const normalizePath = (p) => p.replace(/\\/g, "/");
6678
+ const mmiPaths = new Set(snap.mmiFiles.map((f) => normalizePath(f.path)));
6679
+ const headTsPrefix = `${snap.mmiRoot.replace(/[\\/]+$/, "")}/head-ts/`.replace(/\\/g, "/");
6680
+ const days = (ms) => `${Math.floor(ms / 864e5)}d`;
6681
+ for (const f of snap.mmiFiles) {
6682
+ const age = now - f.mtimeMs;
6683
+ const add = (family, reason) => candidates.push({ path: f.path, family, tier: "safe-auto", reason, bytes: f.bytes });
6684
+ if (isTmpSidecar(f.name)) {
6685
+ if (age > TMP_STALE_MS) add("tmp-sidecar", `crashed atomic-write sidecar (${days(age)} old)`);
6686
+ continue;
6687
+ }
6688
+ if (f.name === "saga-flush.lock") {
6689
+ if (age > FLUSH_LOCK_GC_MS) add("flush-lock", `abandoned flush lock (${days(age)} old)`);
6690
+ continue;
6691
+ }
6692
+ if (NEVER_BASENAMES.has(f.name)) continue;
6693
+ if (f.path.replace(/\\/g, "/").startsWith(headTsPrefix) && !f.name.startsWith(".")) {
6694
+ if (age > HEAD_TS_STALE_MS) add("head-ts", `stale throttle stamp (${days(age)} old)`);
6695
+ continue;
6696
+ }
6697
+ if (f.name === "last-skip.json") {
6698
+ if (age > LAST_SKIP_STALE_MS) add("last-skip", `stale ingest-skip hint (${days(age)} old)`);
6699
+ continue;
6700
+ }
6701
+ const orig = conflictCopyOriginal(f.name);
6702
+ if (orig && CONFLICT_COPY_ALLOWLIST.has(orig) && mmiPaths.has(normalizePath((0, import_node_path10.join)(f.dir, orig))) && age > CONFLICT_COPY_STALE_MS) {
6703
+ add("conflict-copy", `cloud-sync conflict copy of ${orig} (${days(age)} old)`);
6704
+ }
6705
+ }
6706
+ if (snap.syncQueueSlugs) {
6707
+ for (const f of snap.planMdFiles) {
6708
+ const slug = f.name.replace(/\.md$/, "");
6709
+ if (snap.syncQueueSlugs.has(slug)) continue;
6710
+ if (now - f.mtimeMs > PLAN_ADVISORY_AGE_MS) {
6711
+ candidates.push({
6712
+ path: f.path,
6713
+ family: "plan",
6714
+ tier: "advisory",
6715
+ reason: `synced plan older than ${Math.floor(PLAN_ADVISORY_AGE_MS / 864e5)}d`,
6716
+ bytes: f.bytes
6717
+ });
6718
+ }
6719
+ }
6720
+ }
6721
+ return {
6722
+ candidates,
6723
+ safeAuto: candidates.filter((c) => c.tier === "safe-auto"),
6724
+ advisory: candidates.filter((c) => c.tier === "advisory")
6725
+ };
6726
+ }
6727
+ function formatScratchGcPlan(plan2, applied) {
6728
+ if (plan2.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local saga/plan/honcho scratch is clean.";
6729
+ const lines = [];
6730
+ const bytes = plan2.safeAuto.reduce((n, c) => n + c.bytes, 0);
6731
+ lines.push(
6732
+ `scratch GC: ${applied ? "pruned" : "would prune"} ${plan2.safeAuto.length} stale file(s) (${bytes} bytes)` + (plan2.advisory.length ? `; ${plan2.advisory.length} advisory (kept):` : ":")
6733
+ );
6734
+ for (const c of plan2.safeAuto.slice(0, 40)) lines.push(` - [${c.family}] ${c.path} \u2014 ${c.reason}`);
6735
+ if (plan2.safeAuto.length > 40) lines.push(` ... +${plan2.safeAuto.length - 40} more`);
6736
+ for (const c of plan2.advisory.slice(0, 20)) lines.push(` \xB7 [advisory] ${c.path} \u2014 ${c.reason} (kept; review manually)`);
6737
+ return lines.join("\n");
6738
+ }
6739
+ function scratchGcBannerLine(safeAutoPruned, advisory) {
6740
+ const parts = [];
6741
+ if (safeAutoPruned > 0) parts.push(`pruned ${safeAutoPruned} stale local file(s)`);
6742
+ if (advisory > 0) parts.push(`${advisory} old synced plan(s) prunable \u2014 \`mmi-cli gc --scratch --dry-run\` to review`);
6743
+ return parts.length ? `[cleanup] ${parts.join("; ")}` : void 0;
6744
+ }
6745
+ function applyScratchGc(plan2, mmiRoot, now = Date.now()) {
6746
+ const result = { pruned: [], skipped: 0, bytes: 0 };
6747
+ let anchor;
6748
+ try {
6749
+ anchor = (0, import_node_fs12.realpathSync)(mmiRoot).replace(/\\/g, "/").replace(/\/+$/, "");
6750
+ } catch {
6751
+ return result;
6752
+ }
6753
+ for (const c of plan2.safeAuto) {
6754
+ try {
6755
+ const real = (0, import_node_fs12.realpathSync)(c.path).replace(/\\/g, "/");
6756
+ if (real !== anchor && !real.startsWith(`${anchor}/`)) {
6757
+ result.skipped += 1;
6758
+ continue;
6759
+ }
6760
+ const st = (0, import_node_fs12.statSync)(c.path);
6761
+ const floor = c.family === "flush-lock" ? FLUSH_LOCK_GC_MS : c.family === "tmp-sidecar" ? TMP_STALE_MS : c.family === "head-ts" ? HEAD_TS_STALE_MS : c.family === "last-skip" ? LAST_SKIP_STALE_MS : CONFLICT_COPY_STALE_MS;
6762
+ if (now - st.mtimeMs <= floor) {
6763
+ result.skipped += 1;
6764
+ continue;
6765
+ }
6766
+ (0, import_node_fs12.unlinkSync)(c.path);
6767
+ result.pruned.push(c.path);
6768
+ result.bytes += c.bytes;
6769
+ } catch {
6770
+ result.skipped += 1;
6771
+ }
6772
+ }
6773
+ return result;
6774
+ }
6775
+ function collectScratchSnapshot(repoRoot, deps = {}) {
6776
+ const readdir = deps.readdir ?? import_node_fs12.readdirSync;
6777
+ const stat = deps.stat ?? import_node_fs12.statSync;
6778
+ const readFile6 = deps.readFile ?? import_node_fs12.readFileSync;
6779
+ const mmiRoot = (0, import_node_path10.join)(repoRoot, ".mmi");
6780
+ const plansRoot = (0, import_node_path10.join)(repoRoot, "plans");
6781
+ const mmiFiles = [];
6782
+ try {
6783
+ for (const ent of readdir(mmiRoot, { recursive: true, withFileTypes: true })) {
6784
+ if (!ent.isFile()) continue;
6785
+ const dir = ent.parentPath ?? ent.path ?? mmiRoot;
6786
+ const full = (0, import_node_path10.join)(dir, ent.name);
6787
+ try {
6788
+ const st = stat(full);
6789
+ mmiFiles.push({ path: full, dir, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
6790
+ } catch {
6791
+ }
6792
+ }
6793
+ } catch {
6794
+ }
6795
+ const planMdFiles = [];
6796
+ try {
6797
+ for (const ent of readdir(plansRoot, { withFileTypes: true })) {
6798
+ if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
6799
+ const full = (0, import_node_path10.join)(plansRoot, ent.name);
6800
+ try {
6801
+ const st = stat(full);
6802
+ planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
6803
+ } catch {
6804
+ }
6805
+ }
6806
+ } catch {
6807
+ }
6808
+ let syncQueueSlugs = null;
6809
+ let queueRaw;
6810
+ try {
6811
+ queueRaw = readFile6((0, import_node_path10.join)(plansRoot, ".sync-queue.json"), "utf8");
6812
+ } catch {
6813
+ syncQueueSlugs = /* @__PURE__ */ new Set();
6814
+ }
6815
+ if (queueRaw !== void 0) {
6816
+ try {
6817
+ const parsed = JSON.parse(queueRaw);
6818
+ const entries = Array.isArray(parsed) ? parsed : parsed.entries ?? [];
6819
+ syncQueueSlugs = new Set(entries.map((e) => e.slug).filter((s) => typeof s === "string"));
6820
+ } catch {
6821
+ syncQueueSlugs = null;
6822
+ }
6823
+ }
6824
+ return { mmiRoot, mmiFiles, planMdFiles, syncQueueSlugs };
6825
+ }
6826
+
6827
+ // src/session-start.ts
6230
6828
  async function runBufferedStep(step) {
6231
6829
  const lines = [];
6232
6830
  const io = {
@@ -6254,6 +6852,7 @@ function buildSessionStartPlan(verbs) {
6254
6852
  parallel: [
6255
6853
  { name: "rules sync", run: verbs.rulesSync },
6256
6854
  { name: "saga show", run: verbs.sagaShow },
6855
+ { name: "handoff offer", run: verbs.handoffOffer },
6257
6856
  // honcho profile (#1162): the behavioral-memory prior, flushed right after the saga resume. A fast
6258
6857
  // peer-card GET, fail-soft + silent when off/empty — it never hangs or noises the banner.
6259
6858
  { name: "honcho profile", run: verbs.honchoContext },
@@ -6275,14 +6874,24 @@ function spawnDetachedSelf(args, deps) {
6275
6874
  } catch {
6276
6875
  }
6277
6876
  }
6877
+ function isInsideRepoSubdir(cwd, exists = import_node_fs13.existsSync) {
6878
+ if (exists((0, import_node_path11.join)(cwd, ".git"))) return false;
6879
+ let dir = cwd;
6880
+ for (; ; ) {
6881
+ const parent = (0, import_node_path11.dirname)(dir);
6882
+ if (parent === dir) return false;
6883
+ if (exists((0, import_node_path11.join)(parent, ".git"))) return true;
6884
+ dir = parent;
6885
+ }
6886
+ }
6278
6887
  function planStoreLines(cwd) {
6279
6888
  const mdFiles = (dir, minSize = 0) => {
6280
- const p = (0, import_node_path9.join)(cwd, dir);
6281
- if (!(0, import_node_fs11.existsSync)(p)) return [];
6889
+ const p = (0, import_node_path11.join)(cwd, dir);
6890
+ if (!(0, import_node_fs13.existsSync)(p)) return [];
6282
6891
  try {
6283
- return (0, import_node_fs11.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
6892
+ return (0, import_node_fs13.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
6284
6893
  try {
6285
- return (0, import_node_fs11.statSync)((0, import_node_path9.join)(p, f)).size >= minSize;
6894
+ return (0, import_node_fs13.statSync)((0, import_node_path11.join)(p, f)).size >= minSize;
6286
6895
  } catch {
6287
6896
  return false;
6288
6897
  }
@@ -6300,6 +6909,19 @@ function planStoreLines(cwd) {
6300
6909
  out.push(`[plan-store] ${localPlans.length} local plan(s) in plans/ \u2014 ensure new/changed ones are \`mmi-cli plan push\`ed (S3-backed, not git).`);
6301
6910
  return out;
6302
6911
  }
6912
+ function scratchGcLines(cwd, env = process.env, now = Date.now()) {
6913
+ if (env.MMI_NO_AUTO_GC) return [];
6914
+ try {
6915
+ const stamp = scratchGcThrottlePath((0, import_node_path11.join)(cwd, ".mmi"));
6916
+ if (!scratchGcDue(stamp, now)) return [];
6917
+ const run = executeScratchGc(cwd, { apply: true }, now);
6918
+ markScratchGcRun(stamp, now);
6919
+ const line = scratchGcBannerLine(run.applied?.pruned.length ?? 0, run.plan.advisory.length);
6920
+ return line ? [line] : [];
6921
+ } catch {
6922
+ return [];
6923
+ }
6924
+ }
6303
6925
  function northstarPointer(injected = false) {
6304
6926
  if (injected) {
6305
6927
  return "North Stars: `mmi-cli northstar relevant` for more matches; `northstar pull <slug>` for the full SSOT.";
@@ -6347,6 +6969,71 @@ function recoverPriorityFromEvents(events) {
6347
6969
  return found;
6348
6970
  }
6349
6971
 
6972
+ // src/board-dependency.ts
6973
+ var DEPENDS_ON_LINE = /^\s*[-*]?\s*\*\*Depends on:\*\*\s*(.+)$/im;
6974
+ var ISSUE_REF = /([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)#(\d+)/g;
6975
+ function parseDependsOnRefs(body) {
6976
+ const match = body.match(DEPENDS_ON_LINE);
6977
+ if (!match) return [];
6978
+ const refs = [];
6979
+ for (const m of match[1].matchAll(ISSUE_REF)) {
6980
+ refs.push({ repo: m[1], number: Number(m[2]) });
6981
+ }
6982
+ return refs;
6983
+ }
6984
+ async function issueOpen(client, repo, number) {
6985
+ try {
6986
+ const data = await client.rest("GET", `repos/${repo}/issues/${number}`);
6987
+ return data?.state === "OPEN";
6988
+ } catch {
6989
+ return void 0;
6990
+ }
6991
+ }
6992
+ async function dependencyBlocksClaim(client, body) {
6993
+ const refs = parseDependsOnRefs(body);
6994
+ if (!refs.length) return { blocked: false, openDependencies: [] };
6995
+ const openDependencies = [];
6996
+ for (const ref of refs) {
6997
+ const open = await issueOpen(client, ref.repo, ref.number);
6998
+ if (open === true) openDependencies.push(`${ref.repo}#${ref.number}`);
6999
+ }
7000
+ return { blocked: openDependencies.length > 0, openDependencies };
7001
+ }
7002
+ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
7003
+ const claimable = [];
7004
+ const blocked = [];
7005
+ const warnings = [];
7006
+ for (const item of items) {
7007
+ if (item.contentType !== "Issue") {
7008
+ claimable.push(item);
7009
+ continue;
7010
+ }
7011
+ let body = item.details?.body;
7012
+ if (body === void 0) {
7013
+ if (opts.requireBundledBody) {
7014
+ claimable.push(item);
7015
+ continue;
7016
+ }
7017
+ try {
7018
+ const data = await client.rest("GET", `repos/${item.repository}/issues/${item.number}`);
7019
+ body = data?.body ?? "";
7020
+ } catch (e) {
7021
+ warnings.push(`dependency check skipped for ${item.ref}: ${e.message}`);
7022
+ claimable.push(item);
7023
+ continue;
7024
+ }
7025
+ }
7026
+ const gate = await dependencyBlocksClaim(client, body);
7027
+ if (gate.blocked) {
7028
+ blocked.push(item);
7029
+ warnings.push(`${item.ref} blocked on open ${gate.openDependencies.join(", ")}`);
7030
+ } else {
7031
+ claimable.push(item);
7032
+ }
7033
+ }
7034
+ return { claimable, blocked, warnings };
7035
+ }
7036
+
6350
7037
  // ../infra/board-vocab.mjs
6351
7038
  var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
6352
7039
 
@@ -6670,6 +7357,13 @@ async function readBoard(options, deps = {}) {
6670
7357
  if (options.includeBundleDetails) {
6671
7358
  await attachBundleDetails(report, client, options.allowPartial ?? false);
6672
7359
  }
7360
+ for (const scope of ["primary", "secondary"]) {
7361
+ const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client, {
7362
+ requireBundledBody: Boolean(options.includeBundleDetails)
7363
+ });
7364
+ report[scope].claimable = filtered.claimable;
7365
+ report.warnings.push(...filtered.warnings);
7366
+ }
6673
7367
  return report;
6674
7368
  }
6675
7369
  function findBoardItem(items, selector) {
@@ -6751,6 +7445,11 @@ async function prepareClaimContext(options, selectors, deps, collected) {
6751
7445
  warnings: collected.warnings,
6752
7446
  partial: collected.partial
6753
7447
  };
7448
+ for (const scope of ["primary", "secondary"]) {
7449
+ const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client);
7450
+ report[scope].claimable = filtered.claimable;
7451
+ report.warnings.push(...filtered.warnings);
7452
+ }
6754
7453
  return { cfg, client, items: collected.items, writable: writable.repos, report };
6755
7454
  }
6756
7455
  async function claimOneBoardItem(ctx, selector, options) {
@@ -6759,6 +7458,21 @@ async function claimOneBoardItem(ctx, selector, options) {
6759
7458
  if (flatItem.status === "Todo" && flatItem.assignees.length === 0 && !ctx.writable.has(flatItem.repository.toLowerCase())) {
6760
7459
  throw new Error(`${flatItem.ref} is not claimable: viewer does not have write access to ${flatItem.repository}`);
6761
7460
  }
7461
+ if (flatItem.contentType === "Issue") {
7462
+ let body = flatItem.details?.body;
7463
+ if (body === void 0) {
7464
+ try {
7465
+ const data = await client.rest("GET", `repos/${flatItem.repository}/issues/${flatItem.number}`);
7466
+ body = data?.body ?? "";
7467
+ } catch {
7468
+ body = "";
7469
+ }
7470
+ }
7471
+ const gate = await dependencyBlocksClaim(client, body);
7472
+ if (gate.blocked) {
7473
+ throw new Error(`${flatItem.ref} is not claimable: blocked on open ${gate.openDependencies.join(", ")}`);
7474
+ }
7475
+ }
6762
7476
  let item = findClaimableItem(report, selector);
6763
7477
  if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
6764
7478
  const fresh = (await fetchIssueProjectItem(client, cfg, { repo: item.repository, number: item.number })).item;
@@ -7195,33 +7909,142 @@ async function withTimeout(promise, ms) {
7195
7909
  }
7196
7910
  }
7197
7911
  async function runBoardSlice(io, deps, opts) {
7912
+ const budget = deps.timeoutMs ?? SESSION_START_BOARD_TIMEOUT_MS;
7913
+ const controller = new AbortController();
7914
+ const deadline = setTimeout(() => controller.abort(), budget);
7198
7915
  try {
7199
7916
  const cfg = await deps.loadConfig();
7200
7917
  if (!boardMetaConfigured(cfg)) return;
7918
+ const client = deps.client ?? createGitHubClient({ defaultTimeoutMs: budget, signal: controller.signal });
7201
7919
  const report = await withTimeout(
7202
- deps.readBoard({ config: cfg, allowPartial: true }),
7203
- deps.timeoutMs ?? SESSION_START_BOARD_TIMEOUT_MS
7920
+ deps.readBoard({ config: cfg, allowPartial: true }, { client }),
7921
+ budget
7204
7922
  );
7205
7923
  if (opts?.expectedLogin && report.viewer && opts.expectedLogin !== report.viewer) return;
7206
7924
  const block = renderBoardSlice(report);
7207
7925
  if (block) io.log(block);
7208
7926
  } catch {
7927
+ } finally {
7928
+ clearTimeout(deadline);
7209
7929
  }
7210
7930
  }
7211
7931
 
7212
- // src/frontmatter.ts
7213
- function splitFrontmatter(content) {
7214
- const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
7215
- if (!match) return { entries: [], body: content };
7216
- return { entries: match[1].split(/\r?\n/).filter((line) => line.trim()), body: content.slice(match[0].length) };
7217
- }
7218
- function entryKeyValue(line) {
7219
- const i = line.indexOf(":");
7220
- if (i < 0) return null;
7221
- return { key: line.slice(0, i).trim().toLowerCase(), value: line.slice(i + 1).trim() };
7222
- }
7223
- function frontmatterValue(content, key) {
7224
- const want = key.trim().toLowerCase();
7932
+ // src/worktree.ts
7933
+ var import_node_fs14 = require("node:fs");
7934
+ var import_node_path12 = require("node:path");
7935
+ var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
7936
+ var PKG = "package.json";
7937
+ var LOCKFILE = "package-lock.json";
7938
+ var NODE_MODULES = "node_modules";
7939
+ var realFsProbe = {
7940
+ isDir: (p) => {
7941
+ try {
7942
+ return (0, import_node_fs14.statSync)(p).isDirectory();
7943
+ } catch {
7944
+ return false;
7945
+ }
7946
+ },
7947
+ isFile: (p) => {
7948
+ try {
7949
+ return (0, import_node_fs14.statSync)(p).isFile();
7950
+ } catch {
7951
+ return false;
7952
+ }
7953
+ },
7954
+ listDirs: (p) => {
7955
+ try {
7956
+ return (0, import_node_fs14.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7957
+ } catch {
7958
+ return [];
7959
+ }
7960
+ }
7961
+ };
7962
+ function scanInstallDirs(root, fs2 = realFsProbe) {
7963
+ const factsFor = (dir) => {
7964
+ const abs = dir ? (0, import_node_path12.join)(root, dir) : root;
7965
+ return {
7966
+ dir,
7967
+ hasPackageJson: fs2.isFile((0, import_node_path12.join)(abs, PKG)),
7968
+ hasLockfile: fs2.isFile((0, import_node_path12.join)(abs, LOCKFILE)),
7969
+ hasNodeModules: fs2.isDir((0, import_node_path12.join)(abs, NODE_MODULES))
7970
+ };
7971
+ };
7972
+ const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
7973
+ return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
7974
+ }
7975
+ function npmInstallTargets(dirs) {
7976
+ return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
7977
+ }
7978
+ function isLinkedWorktree(root, fs2 = realFsProbe) {
7979
+ return fs2.isFile((0, import_node_path12.join)(root, ".git"));
7980
+ }
7981
+ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
7982
+ if (!isLinkedWorktree(root, fs2)) return null;
7983
+ const pending = npmInstallTargets(scanInstallDirs(root, fs2));
7984
+ if (!pending.length) return null;
7985
+ const where = pending.map((t) => t.dir || ".").join(", ");
7986
+ return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
7987
+ }
7988
+ function defaultCopyFile(from, to) {
7989
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(to), { recursive: true });
7990
+ (0, import_node_fs14.copyFileSync)(from, to);
7991
+ }
7992
+ async function provisionWorktree(worktreeRoot, deps) {
7993
+ const fs2 = deps.fs ?? realFsProbe;
7994
+ const copyFile = deps.copyFile ?? defaultCopyFile;
7995
+ const log = deps.log ?? (() => {
7996
+ });
7997
+ const allDirs = scanInstallDirs(worktreeRoot, fs2);
7998
+ const targets = npmInstallTargets(allDirs);
7999
+ const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
8000
+ const installed = [];
8001
+ for (const target of targets) {
8002
+ const cwd = target.dir ? (0, import_node_path12.join)(worktreeRoot, target.dir) : worktreeRoot;
8003
+ log(`installing deps: ${target.command} in ${target.dir || "."}`);
8004
+ await deps.runInstall(target.command, cwd);
8005
+ installed.push(target);
8006
+ }
8007
+ const copied = [];
8008
+ const copySkipped = [];
8009
+ const primary = await deps.primaryCheckout();
8010
+ for (const rel of LOCAL_ONLY_FILES) {
8011
+ const dest = (0, import_node_path12.join)(worktreeRoot, rel);
8012
+ if (fs2.isFile(dest)) {
8013
+ copySkipped.push({ file: rel, reason: "already-present" });
8014
+ continue;
8015
+ }
8016
+ if (!primary) {
8017
+ copySkipped.push({ file: rel, reason: "no-primary" });
8018
+ continue;
8019
+ }
8020
+ if (!fs2.isFile((0, import_node_path12.join)(primary, rel))) {
8021
+ copySkipped.push({ file: rel, reason: "absent-in-primary" });
8022
+ continue;
8023
+ }
8024
+ copyFile((0, import_node_path12.join)(primary, rel), dest);
8025
+ copied.push(rel);
8026
+ log(`copied local config: ${rel}`);
8027
+ }
8028
+ return { worktree: worktreeRoot, installed, skippedInstall, copied, copySkipped };
8029
+ }
8030
+ function defaultWorktreePath(repoRoot, branch) {
8031
+ const safe = branch.replace(/[/\\]+/g, "-");
8032
+ return (0, import_node_path12.join)((0, import_node_path12.dirname)(repoRoot), "mmi-worktrees", safe);
8033
+ }
8034
+
8035
+ // src/frontmatter.ts
8036
+ function splitFrontmatter(content) {
8037
+ const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
8038
+ if (!match) return { entries: [], body: content };
8039
+ return { entries: match[1].split(/\r?\n/).filter((line) => line.trim()), body: content.slice(match[0].length) };
8040
+ }
8041
+ function entryKeyValue(line) {
8042
+ const i = line.indexOf(":");
8043
+ if (i < 0) return null;
8044
+ return { key: line.slice(0, i).trim().toLowerCase(), value: line.slice(i + 1).trim() };
8045
+ }
8046
+ function frontmatterValue(content, key) {
8047
+ const want = key.trim().toLowerCase();
7225
8048
  for (const line of splitFrontmatter(content).entries) {
7226
8049
  const kv = entryKeyValue(line);
7227
8050
  if (kv && kv.key === want) return kv.value || void 0;
@@ -7404,7 +8227,7 @@ async function runNorthstarContext(io, deps) {
7404
8227
  }
7405
8228
 
7406
8229
  // src/index.ts
7407
- var import_node_path14 = require("node:path");
8230
+ var import_node_path17 = require("node:path");
7408
8231
 
7409
8232
  // src/merge-ci-policy.ts
7410
8233
  function resolveMergeCiPolicy(input) {
@@ -7441,6 +8264,13 @@ function parseGhPrChecksOutput(stdout) {
7441
8264
  }
7442
8265
  return anyPending ? "pending" : "success";
7443
8266
  }
8267
+ function parseGhPrChecksResult(stdout, stderr) {
8268
+ if (stdout.trim()) return parseGhPrChecksOutput(stdout);
8269
+ const err = (stderr ?? "").trim();
8270
+ if (/no checks reported/i.test(err)) return "no-checks-reported";
8271
+ if (!err) return "no-checks-reported";
8272
+ return "error";
8273
+ }
7444
8274
  var PR_CHECKS_POLL_MS = 3e4;
7445
8275
  var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
7446
8276
  async function waitForPrChecks(deps) {
@@ -7501,12 +8331,500 @@ async function activateProductRuleset(repo, rulesetBody, client) {
7501
8331
  return { action: "created" };
7502
8332
  }
7503
8333
 
8334
+ // src/bootstrap-seeds.ts
8335
+ var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
8336
+ function loadBootstrapSeeds(manifestJson) {
8337
+ let parsed;
8338
+ try {
8339
+ parsed = JSON.parse(manifestJson);
8340
+ } catch {
8341
+ throw new Error("bootstrap seed manifest is not valid JSON");
8342
+ }
8343
+ const obj = parsed ?? {};
8344
+ const seeds = obj.seeds ?? [];
8345
+ for (const s of seeds) {
8346
+ if (!s || !s.target || !s.source || !Array.isArray(s.classes)) {
8347
+ throw new Error(`invalid seed entry (needs target, source, classes): ${JSON.stringify(s)}`);
8348
+ }
8349
+ if (s.ownership !== "org" && s.ownership !== "repo") {
8350
+ throw new Error(`invalid seed ownership '${s.ownership}' for ${s.target} (must be 'org' or 'repo')`);
8351
+ }
8352
+ }
8353
+ return {
8354
+ seeds,
8355
+ labels: obj.labels ?? [],
8356
+ placeholders: obj.placeholders ?? []
8357
+ };
8358
+ }
8359
+ function renderSeed(template, vars) {
8360
+ return template.replace(PLACEHOLDER_RE, (match, key) => key in vars ? vars[key] : match);
8361
+ }
8362
+ function missingPlaceholders(rendered) {
8363
+ const out = /* @__PURE__ */ new Set();
8364
+ for (const m of rendered.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
8365
+ return [...out];
8366
+ }
8367
+ var GITIGNORE_MANAGED_BEGIN = "# >>> mmi-managed >>>";
8368
+ var GITIGNORE_MANAGED_END = "# <<< mmi-managed <<<";
8369
+ var MANAGED_GITIGNORE_LINES = [
8370
+ '# Org-wide cleanliness (AGENTS.md "Repo cleanliness") \u2014 enforced by `mmi-cli doctor`.',
8371
+ "# Do not edit inside these markers; this block is regenerated on the next doctor run.",
8372
+ "/tmp/",
8373
+ // Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are S3-synced
8374
+ // via `mmi-cli plan push`, never git-tracked (AGENTS.md "Repo cleanliness", #1550).
8375
+ "**/plans/",
8376
+ ".playwright-mcp/",
8377
+ ".claude/worktrees/",
8378
+ // .mmi is agent/CI scratch — ignore the WHOLE tree at any depth (root + cli/.mmi, .github/workflows/.mmi,
8379
+ // and any future feature's subdir) so new scratch NEVER needs a new ignore line. `**/.mmi/*` ignores the
8380
+ // dir's contents (not the dir itself), so the one tracked file is re-included on the next line (#1550).
8381
+ // This replaces the brittle per-path enumeration (saga/honcho/head-ts/scrooge/… — #1472 was one such miss).
8382
+ "**/.mmi/*",
8383
+ // The single tracked .mmi file. A NEW tracked .mmi file would add its own `!` line here; everything else
8384
+ // under .mmi stays ignored automatically.
8385
+ "!**/.mmi/config.json",
8386
+ ".aws-sam/",
8387
+ "/*.png",
8388
+ // Runtime secrets/config are vault-only (AGENTS.md "Privileged ops") — never commit a local .env. The
8389
+ // .env.example reference file stays tracked.
8390
+ ".env",
8391
+ ".env.*",
8392
+ "!.env.example"
8393
+ ];
8394
+ function renderManagedGitignoreBlock() {
8395
+ return [GITIGNORE_MANAGED_BEGIN, ...MANAGED_GITIGNORE_LINES, GITIGNORE_MANAGED_END].join("\n");
8396
+ }
8397
+ function upsertManagedGitignoreBlock(current) {
8398
+ const block = renderManagedGitignoreBlock();
8399
+ const src = (current ?? "").replace(/\r\n/g, "\n");
8400
+ if (src.trim() === "") {
8401
+ const next2 = `${block}
8402
+ `;
8403
+ return { content: next2, changed: src !== next2 };
8404
+ }
8405
+ const managed = /* @__PURE__ */ new Set([GITIGNORE_MANAGED_BEGIN, GITIGNORE_MANAGED_END, ...MANAGED_GITIGNORE_LINES]);
8406
+ const lines = src.split("\n");
8407
+ const beginAt = lines.findIndex((l) => l === GITIGNORE_MANAGED_BEGIN);
8408
+ const endAt = beginAt === -1 ? -1 : lines.findIndex((l, i) => i > beginAt && l === GITIGNORE_MANAGED_END);
8409
+ let next;
8410
+ if (beginAt !== -1 && endAt !== -1) {
8411
+ const before = lines.slice(0, beginAt).filter((l) => !managed.has(l.trim()));
8412
+ const after = lines.slice(endAt + 1).filter((l) => !managed.has(l.trim()));
8413
+ next = `${[...before, ...block.split("\n"), ...after].join("\n").replace(/\n+$/, "")}
8414
+ `;
8415
+ } else {
8416
+ const kept = lines.filter((l) => !managed.has(l.trim())).join("\n").replace(/\n+$/, "");
8417
+ next = kept === "" ? `${block}
8418
+ ` : `${kept}
8419
+
8420
+ ${block}
8421
+ `;
8422
+ }
8423
+ return { content: next, changed: src !== next };
8424
+ }
8425
+ function diffManagedGitignoreBlock(current) {
8426
+ const lines = (current ?? "").replace(/\r\n/g, "\n").split("\n");
8427
+ const beginAt = lines.findIndex((l) => l === GITIGNORE_MANAGED_BEGIN);
8428
+ const endAt = beginAt === -1 ? -1 : lines.findIndex((l, i) => i > beginAt && l === GITIGNORE_MANAGED_END);
8429
+ const hasMarker = beginAt !== -1 || lines.some((l) => l === GITIGNORE_MANAGED_END);
8430
+ if (!hasMarker) return { added: [], removed: [], seeded: true };
8431
+ const wellOrdered = beginAt !== -1 && endAt !== -1;
8432
+ if (!wellOrdered) return { added: [], removed: [], seeded: false };
8433
+ const isRule = (l) => l.trim() !== "" && !l.trim().startsWith("#");
8434
+ const oldBody = lines.slice(beginAt + 1, endAt).filter(isRule);
8435
+ const newBody = MANAGED_GITIGNORE_LINES.filter(isRule);
8436
+ return {
8437
+ added: newBody.filter((l) => !oldBody.includes(l)),
8438
+ removed: oldBody.filter((l) => !newBody.includes(l)),
8439
+ seeded: false
8440
+ };
8441
+ }
8442
+
8443
+ // src/project-model.ts
8444
+ var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable", "cli-tool", "worker"];
8445
+ var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "solo-container", "registry-publish", "content", "none"];
8446
+ var RELEASE_TRACKS = ["full", "direct", "trunk"];
8447
+ var PROJECT_TYPE_SET = new Set(PROJECT_TYPES);
8448
+ var DEPLOY_MODEL_SET = new Set(DEPLOY_MODELS);
8449
+ var RELEASE_TRACK_SET = new Set(RELEASE_TRACKS);
8450
+ function isProjectType(value) {
8451
+ return Boolean(value && PROJECT_TYPE_SET.has(value));
8452
+ }
8453
+ function isDeployModel(value) {
8454
+ return Boolean(value && DEPLOY_MODEL_SET.has(value));
8455
+ }
8456
+ function isReleaseTrack(value) {
8457
+ return Boolean(value && RELEASE_TRACK_SET.has(value));
8458
+ }
8459
+ function repoIsHub(repo) {
8460
+ return repo.toLowerCase().endsWith("/mmi-hub") || repo.toLowerCase() === "mmi-hub";
8461
+ }
8462
+ function resolveProjectTypeConfident(meta, repo) {
8463
+ const rawType = typeof meta?.projectType === "string" ? meta.projectType : void 0;
8464
+ if (isProjectType(rawType)) return rawType;
8465
+ if (meta?.class === "content" || meta?.deployModel === "content") return "content";
8466
+ if (meta?.deployModel === "hub-serverless" || repoIsHub(repo)) return "hub-service";
8467
+ if (meta?.deployModel === "registry-publish") return "cli-tool";
8468
+ if (meta?.deployModel === "solo-container") return "worker";
8469
+ if (meta?.deployModel === "none") return "non-deployable";
8470
+ return void 0;
8471
+ }
8472
+ function resolveProjectType(meta, repo) {
8473
+ return resolveProjectTypeConfident(meta, repo) ?? "web-app";
8474
+ }
8475
+ function resolveDeployModel(meta, repo) {
8476
+ const rawModel = typeof meta?.deployModel === "string" ? meta.deployModel : void 0;
8477
+ if (isDeployModel(rawModel)) return rawModel;
8478
+ const projectType = resolveProjectType(meta, repo);
8479
+ if (projectType === "content" || meta?.class === "content") return "content";
8480
+ if (projectType === "hub-service" || repoIsHub(repo)) return "hub-serverless";
8481
+ if (projectType === "desktop-game" || projectType === "non-deployable") return "none";
8482
+ if (projectType === "cli-tool") return "registry-publish";
8483
+ return "tenant-container";
8484
+ }
8485
+ function projectTypeClearsWebProfile(projectType, deployModel) {
8486
+ return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
8487
+ }
8488
+ function inferReleaseTrackFromBranches(hints) {
8489
+ if (hints.hasRcBranch) return void 0;
8490
+ if (hints.hasDevelopmentBranch && hints.hasMainBranch) return "direct";
8491
+ return void 0;
8492
+ }
8493
+ function resolveBootstrapReleaseTrack(cls, explicit) {
8494
+ if (isReleaseTrack(explicit)) return explicit;
8495
+ if (cls === "content") return "trunk";
8496
+ return "full";
8497
+ }
8498
+ function resolveReleaseTrack(meta, hints) {
8499
+ const raw = typeof meta?.releaseTrack === "string" ? meta.releaseTrack : void 0;
8500
+ if (isReleaseTrack(raw)) return raw;
8501
+ if (meta?.class === "content" || meta?.deployModel === "content") return "trunk";
8502
+ const inferred = hints ? inferReleaseTrackFromBranches(hints) : void 0;
8503
+ if (inferred) return inferred;
8504
+ return "full";
8505
+ }
8506
+ function branchesForTrack(track) {
8507
+ if (track === "trunk") return ["main"];
8508
+ if (track === "direct") return ["development", "main"];
8509
+ return ["development", "rc", "main"];
8510
+ }
8511
+
8512
+ // src/bootstrap-apply.ts
8513
+ function parseOwnerRepo(repo) {
8514
+ const trimmed = repo.trim();
8515
+ const parts = trimmed.split("/");
8516
+ if (parts.length !== 2 || !parts[0]?.trim() || !parts[1]?.trim()) {
8517
+ throw new Error("repo must be owner/repo");
8518
+ }
8519
+ const owner = parts[0].trim();
8520
+ const name = parts[1].trim();
8521
+ if (owner.includes("\\") || name.includes("\\")) throw new Error("repo must be owner/repo");
8522
+ return { owner, name, slug: name.toLowerCase(), fullName: `${owner}/${name}` };
8523
+ }
8524
+ var DEFAULT_INSTALL_CMD = "npm ci";
8525
+ var DEFAULT_GATE_CMD = "npm run check";
8526
+ var DEFAULT_GATE_PY_VERSION = "3.11";
8527
+ var GATE_RUNTIME_DEFAULTS = {
8528
+ node: { cmd: DEFAULT_GATE_CMD, install: DEFAULT_INSTALL_CMD },
8529
+ python: { cmd: "pytest", install: 'pip install -e ".[dev]"' }
8530
+ };
8531
+ function gateSeedVars(cls, releaseTrack, runtime = "node") {
8532
+ const rt = GATE_RUNTIME_DEFAULTS[runtime] ?? GATE_RUNTIME_DEFAULTS.node;
8533
+ const runtimeVars = {
8534
+ GATE_RUNTIME: runtime,
8535
+ GATE_CMD: rt.cmd,
8536
+ GATE_INSTALL_CMD: rt.install,
8537
+ GATE_WORKDIR: ".",
8538
+ GATE_CACHE_DEP_PATH: "package-lock.json",
8539
+ GATE_PY_VERSION: DEFAULT_GATE_PY_VERSION
8540
+ };
8541
+ const track = releaseTrack ?? (cls === "content" ? "trunk" : "full");
8542
+ if (track === "trunk") {
8543
+ return {
8544
+ ...runtimeVars,
8545
+ GATE_PUSH_BRANCHES_YAML: "[main]",
8546
+ GATE_FULL_RUN_BRANCH: "main",
8547
+ GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/main"]'
8548
+ };
8549
+ }
8550
+ if (track === "direct") {
8551
+ return {
8552
+ ...runtimeVars,
8553
+ GATE_PUSH_BRANCHES_YAML: "[development, main]",
8554
+ GATE_FULL_RUN_BRANCH: "development",
8555
+ GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/main"]'
8556
+ };
8557
+ }
8558
+ return {
8559
+ ...runtimeVars,
8560
+ GATE_PUSH_BRANCHES_YAML: "[development, rc, main]",
8561
+ GATE_FULL_RUN_BRANCH: "development",
8562
+ GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/rc", "refs/heads/main"]'
8563
+ };
8564
+ }
8565
+ function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
8566
+ const out = { ...vars };
8567
+ out.REPO_NAME ??= parsed.name;
8568
+ out.REPO_SLUG ??= parsed.slug;
8569
+ out.CLASS ??= cls;
8570
+ out.INSTALL_CMD ??= DEFAULT_INSTALL_CMD;
8571
+ const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
8572
+ const runtime = out.GATE_RUNTIME === "python" ? "python" : "node";
8573
+ for (const [key, value] of Object.entries(gateSeedVars(cls, track, runtime))) {
8574
+ out[key] ??= value;
8575
+ }
8576
+ return out;
8577
+ }
8578
+ function gateConfigToVars(gate) {
8579
+ const out = {};
8580
+ if (!gate || typeof gate !== "object") return out;
8581
+ if (gate.runtime === "node" || gate.runtime === "python") out.GATE_RUNTIME = gate.runtime;
8582
+ if (typeof gate.cmd === "string" && gate.cmd.trim()) out.GATE_CMD = gate.cmd;
8583
+ if (typeof gate.workdir === "string" && gate.workdir.trim()) out.GATE_WORKDIR = gate.workdir;
8584
+ if (typeof gate.cacheDepPath === "string" && gate.cacheDepPath.trim()) out.GATE_CACHE_DEP_PATH = gate.cacheDepPath;
8585
+ if (typeof gate.pyVersion === "string" && gate.pyVersion.trim()) out.GATE_PY_VERSION = gate.pyVersion;
8586
+ return out;
8587
+ }
8588
+ function planSeedAction(seed, exists) {
8589
+ if (seed.source === "fanout") {
8590
+ return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
8591
+ }
8592
+ if (seed.source === "managed-block") {
8593
+ return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-managed block merged in-place (repo-owned lines preserved)" } : { target: seed.target, action: "create", ownership: "org", reason: "org-managed block; .gitignore absent, created" };
8594
+ }
8595
+ if (seed.ownership === "repo") {
8596
+ return exists ? { target: seed.target, action: "skip", ownership: "repo", reason: "repo-owned, already present (never clobbered)" } : { target: seed.target, action: "create", ownership: "repo", reason: "repo-owned, missing" };
8597
+ }
8598
+ return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
8599
+ }
8600
+ function reconcileSeedAction(action, content, isManagedBlock) {
8601
+ if (action.action === "skip") return action;
8602
+ if (content == null) {
8603
+ return { ...action, action: "skip", reason: "no resolvable content" };
8604
+ }
8605
+ if (isManagedBlock) return action;
8606
+ const unfilled = missingPlaceholders(content);
8607
+ if (unfilled.length) {
8608
+ return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
8609
+ }
8610
+ return action;
8611
+ }
8612
+ function renderSeedPlan(actions) {
8613
+ const lines = ["bootstrap apply \u2014 seed plan (dry-run; no mutations):"];
8614
+ for (const a of actions) {
8615
+ lines.push(` ${a.action.toUpperCase().padEnd(6)} ${a.target} (${a.ownership}: ${a.reason})`);
8616
+ }
8617
+ const order = ["create", "update", "skip"];
8618
+ lines.push(` \u2014 ${order.map((k) => `${actions.filter((a) => a.action === k).length} ${k}`).join(", ")}`);
8619
+ return lines.join("\n");
8620
+ }
8621
+ var GITHUB_DEFAULT_LABELS = [
8622
+ "documentation",
8623
+ "duplicate",
8624
+ "enhancement",
8625
+ "good first issue",
8626
+ "help wanted",
8627
+ "invalid",
8628
+ "question",
8629
+ "wontfix"
8630
+ ];
8631
+ function labelsToPrune(orgLabelNames) {
8632
+ const org = new Set(orgLabelNames);
8633
+ return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
8634
+ }
8635
+ function resolveSeedContent(seed, vars, readFile6) {
8636
+ if (seed.source === "self") return readFile6(seed.target);
8637
+ if (seed.source.startsWith("seed:")) {
8638
+ const tmpl = readFile6(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
8639
+ return tmpl == null ? null : renderSeed(tmpl, vars);
8640
+ }
8641
+ return null;
8642
+ }
8643
+ function buildRegisterPayload(repo, cls, vars, options = {}) {
8644
+ const parsedRepo = parseOwnerRepo(repo);
8645
+ const slug = parsedRepo.slug;
8646
+ if (options.projectType && !isProjectType(options.projectType)) {
8647
+ throw new Error(`projectType must be one of: ${PROJECT_TYPES.join(", ")}`);
8648
+ }
8649
+ if (options.deployModel && !isDeployModel(options.deployModel)) {
8650
+ throw new Error(`deployModel must be one of: ${DEPLOY_MODELS.join(", ")}`);
8651
+ }
8652
+ if (options.releaseTrack && !isReleaseTrack(options.releaseTrack)) {
8653
+ throw new Error("releaseTrack must be full, direct, or trunk");
8654
+ }
8655
+ const shape = {
8656
+ class: cls,
8657
+ projectType: options.projectType,
8658
+ deployModel: options.deployModel
8659
+ };
8660
+ const projectType = resolveProjectTypeConfident(shape, parsedRepo.fullName);
8661
+ if (!projectType) {
8662
+ throw new Error(
8663
+ `Project type for ${parsedRepo.fullName} is unset and not derivable \u2014 pass --project-type <${PROJECT_TYPES.join("|")}> and --deploy-model <${DEPLOY_MODELS.join("|")}> (prevents defaulting a non-web repo to tenant-container).`
8664
+ );
8665
+ }
8666
+ const deployModel = resolveDeployModel({ ...shape, projectType }, parsedRepo.fullName);
8667
+ const num = (v) => {
8668
+ if (v == null || v === "") return void 0;
8669
+ const n = Number(v);
8670
+ return Number.isFinite(n) ? n : void 0;
8671
+ };
8672
+ const statusOptions = vars.STATUS_TODO || vars.STATUS_IN_PROGRESS || vars.STATUS_IN_REVIEW || vars.STATUS_DONE ? {
8673
+ Todo: vars.STATUS_TODO,
8674
+ "In Progress": vars.STATUS_IN_PROGRESS,
8675
+ "In Review": vars.STATUS_IN_REVIEW,
8676
+ Done: vars.STATUS_DONE
8677
+ } : void 0;
8678
+ const priorityOptions = vars.PRIORITY_URGENT || vars.PRIORITY_HIGH || vars.PRIORITY_MEDIUM || vars.PRIORITY_LOW ? {
8679
+ Urgent: vars.PRIORITY_URGENT,
8680
+ High: vars.PRIORITY_HIGH,
8681
+ Medium: vars.PRIORITY_MEDIUM,
8682
+ Low: vars.PRIORITY_LOW
8683
+ } : void 0;
8684
+ const payload = {
8685
+ slug,
8686
+ // Identity. name/division default off the repo name when the skill didn't pass them.
8687
+ name: vars.NAME || parsedRepo.name,
8688
+ division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
8689
+ repos: [`${parsedRepo.owner}/${slug}`],
8690
+ wikiRepo: vars.WIKI_REPO || parsedRepo.fullName,
8691
+ branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
8692
+ class: cls,
8693
+ projectType,
8694
+ deployModel,
8695
+ // #1359: always persist an explicit track so release tooling never guesses from absence alone.
8696
+ releaseTrack: resolveBootstrapReleaseTrack(cls, options.releaseTrack),
8697
+ // Board coords (from GraphQL at bootstrap, passed as --var by the skill).
8698
+ projectOwner: vars.PROJECT_OWNER || void 0,
8699
+ projectNumber: num(vars.PROJECT_NUMBER),
8700
+ projectId: vars.PROJECT_ID || void 0,
8701
+ statusFieldId: vars.STATUS_FIELD_ID || void 0,
8702
+ statusOptions,
8703
+ priorityFieldId: vars.PRIORITY_FIELD_ID || void 0,
8704
+ priorityOptions,
8705
+ // Pointers. vaultPath is explicit + canonical; kbPointer is the per-project KB doc path.
8706
+ vaultPath: `/mmi-future/${slug}`,
8707
+ kbPointer: `kb/projects/${slug}.md`
8708
+ };
8709
+ for (const k of Object.keys(payload)) if (payload[k] === void 0) delete payload[k];
8710
+ if (payload.projectId && payload.projectNumber == null) {
8711
+ throw new Error(
8712
+ "bootstrap apply: PROJECT_ID is set but PROJECT_NUMBER is missing \u2014 pass --var PROJECT_NUMBER=<N> or ensure the live board GraphQL query succeeds"
8713
+ );
8714
+ }
8715
+ return payload;
8716
+ }
8717
+ var BOARD_FIELD_VAR_MAP = {
8718
+ Status: {
8719
+ fieldIdVar: "STATUS_FIELD_ID",
8720
+ options: { Todo: "STATUS_TODO", "In Progress": "STATUS_IN_PROGRESS", "In Review": "STATUS_IN_REVIEW", Done: "STATUS_DONE" }
8721
+ },
8722
+ Priority: {
8723
+ fieldIdVar: "PRIORITY_FIELD_ID",
8724
+ options: { Urgent: "PRIORITY_URGENT", High: "PRIORITY_HIGH", Medium: "PRIORITY_MEDIUM", Low: "PRIORITY_LOW" }
8725
+ }
8726
+ };
8727
+ function projectV2BoardNode(fieldsJson) {
8728
+ if (Array.isArray(fieldsJson)) return { fields: { nodes: fieldsJson } };
8729
+ const wrapped = fieldsJson;
8730
+ return wrapped?.data?.node ?? wrapped?.node;
8731
+ }
8732
+ function extractBoardFieldVars(fieldsJson) {
8733
+ const out = {};
8734
+ const projectNode = projectV2BoardNode(fieldsJson);
8735
+ if (typeof projectNode?.number === "number" && Number.isFinite(projectNode.number)) {
8736
+ out.PROJECT_NUMBER = String(projectNode.number);
8737
+ }
8738
+ const nodes = projectNode?.fields?.nodes;
8739
+ if (!Array.isArray(nodes)) return out;
8740
+ for (const node of nodes) {
8741
+ const field = node;
8742
+ const name = typeof field.name === "string" ? field.name : void 0;
8743
+ const map = name ? BOARD_FIELD_VAR_MAP[name] : void 0;
8744
+ if (!map || typeof field.id !== "string" || !field.id) continue;
8745
+ out[map.fieldIdVar] = field.id;
8746
+ const options = Array.isArray(field.options) ? field.options : [];
8747
+ for (const opt of options) {
8748
+ const o = opt;
8749
+ const varName = typeof o.name === "string" ? map.options[o.name] : void 0;
8750
+ if (varName && typeof o.id === "string" && o.id) out[varName] = o.id;
8751
+ }
8752
+ }
8753
+ return out;
8754
+ }
8755
+ function boardFieldsQueryArgs(projectId) {
8756
+ const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
8757
+ return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
8758
+ }
8759
+ function serializeRegistry(obj) {
8760
+ return `${JSON.stringify(obj, null, 2)}
8761
+ `;
8762
+ }
8763
+ function planFanoutRegistration(fanoutTargetsRaw, projectsRaw, entry) {
8764
+ const fanout = JSON.parse(fanoutTargetsRaw);
8765
+ const projects = JSON.parse(projectsRaw);
8766
+ const fanoutRepos = Array.isArray(fanout.repos) ? fanout.repos : [];
8767
+ const projectEntries = Array.isArray(projects.projects) ? projects.projects : [];
8768
+ const name = entry.name ?? entry.repo;
8769
+ const canonName = entry.repo.toLowerCase();
8770
+ const inFanout = fanoutRepos.some((r) => typeof r.repo === "string" && r.repo.toLowerCase() === canonName);
8771
+ const inProjects = projectEntries.some(
8772
+ (p) => p.slug === entry.slug || Array.isArray(p.repos) && p.repos.some((full) => String(full).split("/").pop()?.toLowerCase() === canonName)
8773
+ );
8774
+ if (inFanout || inProjects) {
8775
+ return { changed: false, fanoutTargets: fanoutTargetsRaw, projects: projectsRaw };
8776
+ }
8777
+ const projectEntry = {
8778
+ name,
8779
+ slug: entry.slug,
8780
+ projectId: entry.projectId,
8781
+ wikiRepo: entry.wikiRepo,
8782
+ repos: [`mutmutco/${entry.repo}`]
8783
+ };
8784
+ if (entry.cls === "content") projectEntry.branch = "main";
8785
+ for (const k of Object.keys(projectEntry)) if (projectEntry[k] === void 0) delete projectEntry[k];
8786
+ const nextProjects = { ...projects, projects: [...projectEntries, projectEntry] };
8787
+ const fanoutEntry = { repo: entry.repo, branch: entry.branch, class: entry.cls };
8788
+ const nextFanout = { ...fanout, repos: [...fanoutRepos, fanoutEntry] };
8789
+ return {
8790
+ changed: true,
8791
+ fanoutTargets: serializeRegistry(nextFanout),
8792
+ projects: serializeRegistry(nextProjects)
8793
+ };
8794
+ }
8795
+ function decideFanoutPrAction(openPrs) {
8796
+ const list = Array.isArray(openPrs) ? openPrs : [];
8797
+ for (const pr2 of list) {
8798
+ const url = pr2?.url;
8799
+ if (typeof url === "string" && url.trim()) return { action: "reuse", url: url.trim() };
8800
+ }
8801
+ return { action: "create" };
8802
+ }
8803
+ function contentPutArgs(repo, path2, content, branch, sha) {
8804
+ const args = [
8805
+ "api",
8806
+ "-X",
8807
+ "PUT",
8808
+ `repos/${repo}/contents/${path2.split("/").map(encodeURIComponent).join("/")}`,
8809
+ "-f",
8810
+ `message=bootstrap: seed ${path2}`,
8811
+ "-f",
8812
+ `content=${Buffer.from(content, "utf8").toString("base64")}`,
8813
+ "-f",
8814
+ `branch=${branch}`
8815
+ ];
8816
+ if (sha) args.push("-f", `sha=${sha}`);
8817
+ return args;
8818
+ }
8819
+
7504
8820
  // src/ci-audit.ts
7505
8821
  var HUB_REPO = "mutmutco/MMI-Hub";
7506
8822
  var PRODUCT_GATE_CONTEXT2 = "gate";
7507
8823
  var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
7508
8824
  var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
7509
8825
  var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
8826
+ var GATE_TEMPLATE_SEED = "seed:gate.template.yml";
8827
+ var RULESET_TEMPLATE_SEED = "seed:mmi-product-required-checks.template.json";
7510
8828
  function slugFromRepo(repo) {
7511
8829
  return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
7512
8830
  }
@@ -7737,10 +9055,76 @@ async function fetchRulesetSeedBody(deps, repo) {
7737
9055
  return null;
7738
9056
  }
7739
9057
  }
9058
+ async function contentSha(deps, repo, branch, path2) {
9059
+ try {
9060
+ const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
9061
+ const file = await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
9062
+ return typeof file?.sha === "string" ? file.sha : void 0;
9063
+ } catch {
9064
+ return void 0;
9065
+ }
9066
+ }
9067
+ function renderSeedBody(deps, seedSource, target, vars) {
9068
+ if (deps.renderSeed) return deps.renderSeed(seedSource, target, vars);
9069
+ if (!deps.readSeedFile) return null;
9070
+ return resolveSeedContent({ target, source: seedSource, ownership: "org", classes: ["deployable"] }, vars, deps.readSeedFile);
9071
+ }
9072
+ async function putSeedFile(deps, repo, path2, content, branch) {
9073
+ const sha = await contentSha(deps, repo, branch, path2);
9074
+ const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
9075
+ const body = {
9076
+ message: `ci reconcile: re-seed ${path2} (#1550)`,
9077
+ content: Buffer.from(content, "utf8").toString("base64"),
9078
+ branch
9079
+ };
9080
+ if (sha) body.sha = sha;
9081
+ await deps.client.rest("PUT", `repos/${repo}/contents/${encodedPath}`, { body });
9082
+ }
9083
+ async function seedGateYml(repo, deps, meta, result) {
9084
+ const baseBranch = "development";
9085
+ if (await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH)) return;
9086
+ if (!deps.readSeedFile && !deps.renderSeed) {
9087
+ result.skipped.push(`gate.yml missing but no seed-template source wired \u2014 run bootstrap apply`);
9088
+ return;
9089
+ }
9090
+ const gate = meta?.gate;
9091
+ if (!gate || typeof gate === "object" && Object.keys(gate).length === 0) {
9092
+ result.skipped.push(`gate.yml missing \u2014 no registry gate config; set \`mmi-cli project set ${repo} --var gate={...}\` first, then re-run reconcile`);
9093
+ return;
9094
+ }
9095
+ const releaseTrack = isReleaseTrack(meta?.releaseTrack) ? meta?.releaseTrack : void 0;
9096
+ const parsed = parseOwnerRepo(repo);
9097
+ const derivedVars = withDerivedRepoVars({ ...gateConfigToVars(gate), REPO_SLUG: parsed.slug }, parsed, "deployable", releaseTrack);
9098
+ const rendered = renderSeedBody(deps, GATE_TEMPLATE_SEED, PRODUCT_GATE_PATH, derivedVars);
9099
+ if (rendered == null) {
9100
+ result.errors.push(`gate.yml re-seed: could not render ${GATE_TEMPLATE_SEED} (template unreadable or unfilled)`);
9101
+ return;
9102
+ }
9103
+ try {
9104
+ await putSeedFile(deps, repo, PRODUCT_GATE_PATH, rendered, baseBranch);
9105
+ result.applied.push(`seeded ${PRODUCT_GATE_PATH}`);
9106
+ } catch (e) {
9107
+ result.errors.push(`gate.yml re-seed failed: ${e.message}`);
9108
+ return;
9109
+ }
9110
+ if (!await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF)) {
9111
+ const rulesetBody = renderSeedBody(deps, RULESET_TEMPLATE_SEED, PRODUCT_RULESET_REF, derivedVars);
9112
+ if (rulesetBody != null) {
9113
+ try {
9114
+ await putSeedFile(deps, repo, PRODUCT_RULESET_REF, rulesetBody, baseBranch);
9115
+ result.applied.push(`seeded ${PRODUCT_RULESET_REF}`);
9116
+ } catch (e) {
9117
+ result.errors.push(`ruleset reference re-seed failed: ${e.message}`);
9118
+ }
9119
+ }
9120
+ }
9121
+ }
7740
9122
  async function applyCiReconcileRepo(repo, deps) {
7741
9123
  const merge = await applyCiReconcileMergeSettings(repo, deps);
9124
+ const meta = await deps.getProjectMeta(slugFromRepo(repo));
7742
9125
  const report = await auditRepoCi(repo, deps);
7743
9126
  if (report.class !== "deployable") return merge;
9127
+ await seedGateYml(repo, deps, meta, merge);
7744
9128
  const gateCheck = report.checks.find((c) => c.label === "product gate status check active");
7745
9129
  if (gateCheck?.ok) {
7746
9130
  merge.skipped.push("product ruleset already active");
@@ -7811,13 +9195,13 @@ async function runPrLand(prNumber, options, deps) {
7811
9195
  }
7812
9196
 
7813
9197
  // src/index.ts
7814
- var import_node_os5 = require("node:os");
9198
+ var import_node_os6 = require("node:os");
7815
9199
 
7816
9200
  // src/gh-create.ts
7817
- var import_promises4 = require("node:fs/promises");
7818
- var import_node_os3 = require("node:os");
7819
- var import_node_path10 = require("node:path");
7820
- var import_node_crypto5 = require("node:crypto");
9201
+ var import_promises5 = require("node:fs/promises");
9202
+ var import_node_os4 = require("node:os");
9203
+ var import_node_path13 = require("node:path");
9204
+ var import_node_crypto6 = require("node:crypto");
7821
9205
  var ISSUE_TYPES = ["bug", "feature", "task"];
7822
9206
  var GH_MUTATION_TIMEOUT_MS = 12e4;
7823
9207
  function timeoutKillNote(err, timeoutMs) {
@@ -7855,9 +9239,9 @@ async function bodyArgsViaFile(args, deps = {}) {
7855
9239
  const i = args.indexOf("--body");
7856
9240
  if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
7857
9241
  } };
7858
- const write = deps.write ?? import_promises4.writeFile;
7859
- const remove = deps.remove ?? import_promises4.unlink;
7860
- const file = (0, import_node_path10.join)(deps.dir ?? (0, import_node_os3.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.md`);
9242
+ const write = deps.write ?? import_promises5.writeFile;
9243
+ const remove = deps.remove ?? import_promises5.unlink;
9244
+ 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`);
7861
9245
  await write(file, args[i + 1], "utf8");
7862
9246
  return {
7863
9247
  args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
@@ -7899,6 +9283,76 @@ function buildPrArgs({ title, body, base: base2, head, repo }) {
7899
9283
  return args;
7900
9284
  }
7901
9285
 
9286
+ // src/command-manifest.ts
9287
+ function buildArgument(arg) {
9288
+ const out = { name: arg.name(), required: arg.required, variadic: arg.variadic };
9289
+ if (arg.description) out.description = arg.description;
9290
+ return out;
9291
+ }
9292
+ function buildOption(opt) {
9293
+ const out = {
9294
+ flags: opt.flags,
9295
+ mandatory: Boolean(opt.mandatory),
9296
+ valueRequired: Boolean(opt.required),
9297
+ optional: Boolean(opt.optional),
9298
+ takesValue: Boolean(opt.required || opt.optional),
9299
+ variadic: Boolean(opt.variadic),
9300
+ negate: Boolean(opt.negate)
9301
+ };
9302
+ if (opt.description) out.description = opt.description;
9303
+ if (opt.defaultValue !== void 0) out.default = opt.defaultValue;
9304
+ return out;
9305
+ }
9306
+ function buildCommand(cmd, path2) {
9307
+ const out = {
9308
+ name: cmd.name(),
9309
+ path: path2,
9310
+ arguments: cmd.registeredArguments.map(buildArgument),
9311
+ options: cmd.options.map(buildOption),
9312
+ subcommands: cmd.commands.map(
9313
+ (child) => buildCommand(child, path2 ? `${path2} ${child.name()}` : child.name())
9314
+ )
9315
+ };
9316
+ const description = cmd.description();
9317
+ if (description) out.description = description;
9318
+ return out;
9319
+ }
9320
+ function collectLeaves(node, acc) {
9321
+ if (node.subcommands.length === 0) {
9322
+ if (node.path) acc.push(node);
9323
+ return;
9324
+ }
9325
+ for (const child of node.subcommands) collectLeaves(child, acc);
9326
+ }
9327
+ function buildCommandManifest(program3) {
9328
+ const tree = buildCommand(program3, "");
9329
+ const index = [];
9330
+ collectLeaves(tree, index);
9331
+ const manifest = { name: tree.name, tree, index };
9332
+ const version = program3.version();
9333
+ if (version) manifest.version = version;
9334
+ return manifest;
9335
+ }
9336
+ function argSignature(arg) {
9337
+ const inner = arg.variadic ? `${arg.name}...` : arg.name;
9338
+ return arg.required ? `<${inner}>` : `[${inner}]`;
9339
+ }
9340
+ function formatManifestHuman(manifest) {
9341
+ const lines = [];
9342
+ const render = (node, depth) => {
9343
+ const indent = " ".repeat(depth);
9344
+ const args = node.arguments.map(argSignature).join(" ");
9345
+ const head = depth === 0 ? `${node.name}${manifest.version ? ` v${manifest.version}` : ""}` : node.name + (args ? ` ${args}` : "");
9346
+ lines.push(node.description ? `${indent}${head} \u2014 ${node.description}` : `${indent}${head}`);
9347
+ for (const opt of node.options) {
9348
+ lines.push(`${indent} ${opt.flags}${opt.description ? ` ${opt.description}` : ""}`);
9349
+ }
9350
+ for (const child of node.subcommands) render(child, depth + 1);
9351
+ };
9352
+ render(manifest.tree, 0);
9353
+ return lines.join("\n");
9354
+ }
9355
+
7902
9356
  // src/config-discovery.ts
7903
9357
  function stripMutableBoardConfig(cfg) {
7904
9358
  const {
@@ -8182,7 +9636,7 @@ ${buildReportBody(body, sourceRepo)}`;
8182
9636
 
8183
9637
  // src/skill-lesson.ts
8184
9638
  var SKILL_LESSON_LABEL = "skill-lesson";
8185
- var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
9639
+ var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "handoff", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
8186
9640
  function assertSkillName(name) {
8187
9641
  const match = SKILL_NAMES.find((skill) => skill === name);
8188
9642
  if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
@@ -8563,6 +10017,36 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
8563
10017
  }
8564
10018
  return { status: "failed", attempts, error: errorMessage(lastError) };
8565
10019
  }
10020
+ var NO_NESTED_LINKS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
10021
+ function baseName(p) {
10022
+ return p.replace(/[/\\]+$/, "").split(/[/\\]/).pop() ?? "";
10023
+ }
10024
+ function assertSafeWorktreeTarget(worktreePath, primaryCheckout) {
10025
+ const wt = normPath(worktreePath);
10026
+ if (!wt) throw new Error("worktree teardown refused: empty target path");
10027
+ if (!primaryCheckout) return;
10028
+ const primary = normPath(primaryCheckout);
10029
+ if (wt === primary) throw new Error(`worktree teardown refused: target is the primary checkout (${worktreePath})`);
10030
+ if (primary.startsWith(`${wt}/`)) {
10031
+ throw new Error(`worktree teardown refused: target ${worktreePath} contains the primary checkout`);
10032
+ }
10033
+ }
10034
+ function detachReparsePoints(path2, rmv) {
10035
+ const kind = rmv.probe(path2);
10036
+ if (kind === "link") {
10037
+ rmv.detachLink(path2);
10038
+ return;
10039
+ }
10040
+ if (kind !== "dir") return;
10041
+ if (NO_NESTED_LINKS.has(baseName(path2))) return;
10042
+ const dir = path2.replace(/[/\\]+$/, "");
10043
+ for (const name of rmv.readdir(path2)) detachReparsePoints(`${dir}/${name}`, rmv);
10044
+ }
10045
+ async function removeWorktreeTree(worktreePath, primaryCheckout, rmv) {
10046
+ assertSafeWorktreeTarget(worktreePath, primaryCheckout);
10047
+ detachReparsePoints(worktreePath, rmv);
10048
+ await rmv.removeTree(worktreePath);
10049
+ }
8566
10050
  function buildRemoteBranchCleanupReport(branch, input) {
8567
10051
  if (!input.attempted) return { name: branch, status: "not-attempted", reason: input.reason };
8568
10052
  if (input.existsAfter === true) return { name: branch, status: "failed", reason: "still-present-after-delete" };
@@ -8682,12 +10166,12 @@ function buildGcPlan(inputs) {
8682
10166
  skipped.push({ branch, reason: "current-branch" });
8683
10167
  continue;
8684
10168
  }
8685
- const worktree = worktrees.get(branch);
8686
- if (worktree?.dirty) {
8687
- skipped.push({ branch, reason: "dirty-worktree", detail: worktree.path });
10169
+ const worktree2 = worktrees.get(branch);
10170
+ if (worktree2?.dirty) {
10171
+ skipped.push({ branch, reason: "dirty-worktree", detail: worktree2.path });
8688
10172
  continue;
8689
10173
  }
8690
- branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath: worktree?.path });
10174
+ branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath: worktree2?.path });
8691
10175
  }
8692
10176
  const trackingRefs = [...new Set(inputs.staleTrackingRefs ?? [])].map((ref) => {
8693
10177
  const branch = branchForTrackingRef(ref, remote);
@@ -9050,8 +10534,8 @@ function stagePlan(stage2 = {}, stops = true) {
9050
10534
  }
9051
10535
  function derivedStagePlan(derived, shell2, stops = true) {
9052
10536
  const { port } = derived;
9053
- const envOrder = ["MMI_STAGE", "MMI_PORT", "PORT", "COMPOSE_PROFILES"];
9054
- const resolved = { MMI_STAGE: "development", MMI_PORT: String(port), PORT: String(port), COMPOSE_PROFILES: "local" };
10537
+ const envOrder = ["MMI_STAGE", "MMI_PORT", "COMPOSE_PROFILES"];
10538
+ const resolved = { MMI_STAGE: "development", MMI_PORT: String(port), COMPOSE_PROFILES: "local" };
9055
10539
  const ensureEnv = shell2 === "powershell" ? `if (-not (Test-Path -LiteralPath '.env')) { Copy-Item -LiteralPath '.env.example' -Destination '.env' }` : `[ -f .env ] || cp .env.example .env`;
9056
10540
  const up = shell2 === "powershell" ? `${envOrder.map((k) => `$env:${k}='${resolved[k]}'`).join("; ")}; docker compose up -d --build` : `${envOrder.map((k) => `${k}=${resolved[k]}`).join(" ")} docker compose up -d --build`;
9057
10541
  const health = shell2 === "powershell" ? `curl.exe --fail ${derived.url}` : `curl --fail ${derived.url}`;
@@ -9162,8 +10646,8 @@ function bootstrapPlan(repo, repoClass) {
9162
10646
  }
9163
10647
 
9164
10648
  // src/stage-default.ts
9165
- function shellFor(platform = process.platform) {
9166
- return platform === "win32" ? "powershell" : "bash";
10649
+ function shellFor(platform2 = process.platform) {
10650
+ return platform2 === "win32" ? "powershell" : "bash";
9167
10651
  }
9168
10652
  function isCentralContainerModel(model) {
9169
10653
  return model === "tenant-container" || model === "solo-container";
@@ -9189,10 +10673,11 @@ function deriveStage(inputs) {
9189
10673
  healthAnyStatus: true,
9190
10674
  portRange: inputs.portRange,
9191
10675
  ensureEnv: { example: ".env.example", target: ".env" },
10676
+ // MMI_PORT is the host-published port (the free STAGE_PORT); PORT is intentionally absent so compose
10677
+ // reads the container's bind port from .env rather than this overriding it to the publish port (#1505).
9192
10678
  env: {
9193
10679
  MMI_STAGE: "development",
9194
10680
  MMI_PORT: "$STAGE_PORT",
9195
- PORT: "$STAGE_PORT",
9196
10681
  COMPOSE_PROFILES: "local"
9197
10682
  }
9198
10683
  };
@@ -9226,9 +10711,9 @@ function stalePosixFields(config, shell2) {
9226
10711
  }
9227
10712
  function sanitizeLocalStage(local, stale) {
9228
10713
  if (!stale.length) return local;
9229
- const clean3 = { ...local };
9230
- for (const field of stale) delete clean3[field];
9231
- return clean3;
10714
+ const clean4 = { ...local };
10715
+ for (const field of stale) delete clean4[field];
10716
+ return clean4;
9232
10717
  }
9233
10718
  function staleNote(staleFields, outcome) {
9234
10719
  const list = staleFields.join(", ");
@@ -9274,9 +10759,9 @@ function decideStage(inputs) {
9274
10759
 
9275
10760
  // src/cursor-plugin-seed.ts
9276
10761
  var import_node_child_process7 = require("node:child_process");
9277
- var import_node_fs12 = require("node:fs");
9278
- var import_node_os4 = require("node:os");
9279
- var import_node_path11 = require("node:path");
10762
+ var import_node_fs15 = require("node:fs");
10763
+ var import_node_os5 = require("node:os");
10764
+ var import_node_path14 = require("node:path");
9280
10765
  var import_node_util6 = require("node:util");
9281
10766
  function isSemverVersion(v) {
9282
10767
  return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
@@ -9293,17 +10778,17 @@ function ghReleaseTarballApiArgs(tag) {
9293
10778
  }
9294
10779
  function cursorUserGlobalStatePath() {
9295
10780
  if (process.platform === "win32") {
9296
- const base2 = process.env.APPDATA || (0, import_node_path11.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
9297
- return (0, import_node_path11.join)(base2, "Cursor", "User", "globalStorage", "state.vscdb");
10781
+ const base2 = process.env.APPDATA || (0, import_node_path14.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
10782
+ return (0, import_node_path14.join)(base2, "Cursor", "User", "globalStorage", "state.vscdb");
9298
10783
  }
9299
10784
  if (process.platform === "darwin") {
9300
- return (0, import_node_path11.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
10785
+ return (0, import_node_path14.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
9301
10786
  }
9302
- return (0, import_node_path11.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
10787
+ return (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
9303
10788
  }
9304
10789
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
9305
10790
  const dbPath = cursorUserGlobalStatePath();
9306
- if (!(0, import_node_fs12.existsSync)(dbPath)) return void 0;
10791
+ if (!(0, import_node_fs15.existsSync)(dbPath)) return void 0;
9307
10792
  try {
9308
10793
  const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
9309
10794
  timeout: 5e3
@@ -9317,57 +10802,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
9317
10802
  }
9318
10803
  }
9319
10804
  function syncDirContents(src, dest) {
9320
- (0, import_node_fs12.mkdirSync)(dest, { recursive: true });
9321
- for (const name of (0, import_node_fs12.readdirSync)(dest)) {
9322
- (0, import_node_fs12.rmSync)((0, import_node_path11.join)(dest, name), { recursive: true, force: true });
10805
+ (0, import_node_fs15.mkdirSync)(dest, { recursive: true });
10806
+ for (const name of (0, import_node_fs15.readdirSync)(dest)) {
10807
+ (0, import_node_fs15.rmSync)((0, import_node_path14.join)(dest, name), { recursive: true, force: true });
9323
10808
  }
9324
- (0, import_node_fs12.cpSync)(src, dest, { recursive: true });
10809
+ (0, import_node_fs15.cpSync)(src, dest, { recursive: true });
9325
10810
  }
9326
10811
  function releaseTag(releasedVersion) {
9327
10812
  return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
9328
10813
  }
9329
10814
  async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
9330
- const tarFile = (0, import_node_path11.join)(tmpRoot, "archive.tar");
10815
+ const tarFile = (0, import_node_path14.join)(tmpRoot, "archive.tar");
9331
10816
  try {
9332
10817
  await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
9333
10818
  await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
9334
10819
  timeout: 6e4
9335
10820
  });
9336
10821
  await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
9337
- const pluginMmi = (0, import_node_path11.join)(tmpRoot, "plugins", "mmi");
9338
- return (0, import_node_fs12.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
10822
+ const pluginMmi = (0, import_node_path14.join)(tmpRoot, "plugins", "mmi");
10823
+ return (0, import_node_fs15.existsSync)((0, import_node_path14.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
9339
10824
  } catch {
9340
10825
  return void 0;
9341
10826
  }
9342
10827
  }
9343
10828
  async function downloadPluginMmiViaGh(tag, tmpRoot) {
9344
- const tarPath = (0, import_node_path11.join)(tmpRoot, "repo.tgz");
10829
+ const tarPath = (0, import_node_path14.join)(tmpRoot, "repo.tgz");
9345
10830
  try {
9346
- (0, import_node_fs12.mkdirSync)(tmpRoot, { recursive: true });
10831
+ (0, import_node_fs15.mkdirSync)(tmpRoot, { recursive: true });
9347
10832
  const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
9348
10833
  timeout: 12e4,
9349
10834
  maxBuffer: 100 * 1024 * 1024,
9350
10835
  encoding: "buffer",
9351
10836
  windowsHide: true
9352
10837
  });
9353
- (0, import_node_fs12.writeFileSync)(tarPath, stdout);
10838
+ (0, import_node_fs15.writeFileSync)(tarPath, stdout);
9354
10839
  await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
9355
- const top = (0, import_node_fs12.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
10840
+ const top = (0, import_node_fs15.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
9356
10841
  if (!top) return void 0;
9357
- const pluginMmi = (0, import_node_path11.join)(tmpRoot, top, "plugins", "mmi");
9358
- return (0, import_node_fs12.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
10842
+ const pluginMmi = (0, import_node_path14.join)(tmpRoot, top, "plugins", "mmi");
10843
+ return (0, import_node_fs15.existsSync)((0, import_node_path14.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
9359
10844
  } catch {
9360
10845
  return void 0;
9361
10846
  }
9362
10847
  }
9363
10848
  async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
9364
- (0, import_node_fs12.mkdirSync)(tmpRoot, { recursive: true });
10849
+ (0, import_node_fs15.mkdirSync)(tmpRoot, { recursive: true });
9365
10850
  const tag = releaseTag(releasedVersion);
9366
10851
  if (hubCheckout) {
9367
10852
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
9368
10853
  if (fromHub) return fromHub;
9369
10854
  }
9370
- return downloadPluginMmiViaGh(tag, (0, import_node_path11.join)(tmpRoot, "gh"));
10855
+ return downloadPluginMmiViaGh(tag, (0, import_node_path14.join)(tmpRoot, "gh"));
9371
10856
  }
9372
10857
  function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
9373
10858
  if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
@@ -9388,115 +10873,10 @@ async function applyCursorPluginCacheSeed(input) {
9388
10873
  for (const pin of pinsToSeed) {
9389
10874
  syncDirContents(source, pin.path);
9390
10875
  }
9391
- (0, import_node_fs12.rmSync)(tmpRoot, { recursive: true, force: true });
10876
+ (0, import_node_fs15.rmSync)(tmpRoot, { recursive: true, force: true });
9392
10877
  return true;
9393
10878
  }
9394
10879
 
9395
- // src/bootstrap-seeds.ts
9396
- var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
9397
- function loadBootstrapSeeds(manifestJson) {
9398
- let parsed;
9399
- try {
9400
- parsed = JSON.parse(manifestJson);
9401
- } catch {
9402
- throw new Error("bootstrap seed manifest is not valid JSON");
9403
- }
9404
- const obj = parsed ?? {};
9405
- const seeds = obj.seeds ?? [];
9406
- for (const s of seeds) {
9407
- if (!s || !s.target || !s.source || !Array.isArray(s.classes)) {
9408
- throw new Error(`invalid seed entry (needs target, source, classes): ${JSON.stringify(s)}`);
9409
- }
9410
- if (s.ownership !== "org" && s.ownership !== "repo") {
9411
- throw new Error(`invalid seed ownership '${s.ownership}' for ${s.target} (must be 'org' or 'repo')`);
9412
- }
9413
- }
9414
- return {
9415
- seeds,
9416
- labels: obj.labels ?? [],
9417
- placeholders: obj.placeholders ?? []
9418
- };
9419
- }
9420
- function renderSeed(template, vars) {
9421
- return template.replace(PLACEHOLDER_RE, (match, key) => key in vars ? vars[key] : match);
9422
- }
9423
- function missingPlaceholders(rendered) {
9424
- const out = /* @__PURE__ */ new Set();
9425
- for (const m of rendered.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
9426
- return [...out];
9427
- }
9428
- var GITIGNORE_MANAGED_BEGIN = "# >>> mmi-managed >>>";
9429
- var GITIGNORE_MANAGED_END = "# <<< mmi-managed <<<";
9430
- var MANAGED_GITIGNORE_LINES = [
9431
- '# Org-wide cleanliness (AGENTS.md "Repo cleanliness") \u2014 enforced by `mmi-cli doctor`.',
9432
- "# Do not edit inside these markers; this block is regenerated on the next doctor run.",
9433
- "/tmp/",
9434
- "/plans/",
9435
- ".playwright-mcp/",
9436
- ".claude/worktrees/",
9437
- ".mmi/.session",
9438
- ".mmi/head-ts/",
9439
- // Recursive so any reuse of the parameterized queue engine in a new .mmi subdir (honcho today, others
9440
- // later) is ignored too — `**` matches zero dirs, so this still covers the root saga queue.
9441
- ".mmi/**/saga-pending.jsonl*",
9442
- ".mmi/**/saga-flush.lock",
9443
- ".aws-sam/",
9444
- "/*.png",
9445
- // Runtime secrets/config are vault-only (AGENTS.md "Privileged ops") — never commit a local .env. The
9446
- // .env.example reference file stays tracked.
9447
- ".env",
9448
- ".env.*",
9449
- "!.env.example"
9450
- ];
9451
- function renderManagedGitignoreBlock() {
9452
- return [GITIGNORE_MANAGED_BEGIN, ...MANAGED_GITIGNORE_LINES, GITIGNORE_MANAGED_END].join("\n");
9453
- }
9454
- function upsertManagedGitignoreBlock(current) {
9455
- const block = renderManagedGitignoreBlock();
9456
- const src = (current ?? "").replace(/\r\n/g, "\n");
9457
- if (src.trim() === "") {
9458
- const next2 = `${block}
9459
- `;
9460
- return { content: next2, changed: src !== next2 };
9461
- }
9462
- const managed = /* @__PURE__ */ new Set([GITIGNORE_MANAGED_BEGIN, GITIGNORE_MANAGED_END, ...MANAGED_GITIGNORE_LINES]);
9463
- const lines = src.split("\n");
9464
- const beginAt = lines.findIndex((l) => l === GITIGNORE_MANAGED_BEGIN);
9465
- const endAt = beginAt === -1 ? -1 : lines.findIndex((l, i) => i > beginAt && l === GITIGNORE_MANAGED_END);
9466
- let next;
9467
- if (beginAt !== -1 && endAt !== -1) {
9468
- const before = lines.slice(0, beginAt).filter((l) => !managed.has(l.trim()));
9469
- const after = lines.slice(endAt + 1).filter((l) => !managed.has(l.trim()));
9470
- next = `${[...before, ...block.split("\n"), ...after].join("\n").replace(/\n+$/, "")}
9471
- `;
9472
- } else {
9473
- const kept = lines.filter((l) => !managed.has(l.trim())).join("\n").replace(/\n+$/, "");
9474
- next = kept === "" ? `${block}
9475
- ` : `${kept}
9476
-
9477
- ${block}
9478
- `;
9479
- }
9480
- return { content: next, changed: src !== next };
9481
- }
9482
- function diffManagedGitignoreBlock(current) {
9483
- const lines = (current ?? "").replace(/\r\n/g, "\n").split("\n");
9484
- const beginAt = lines.findIndex((l) => l === GITIGNORE_MANAGED_BEGIN);
9485
- const endAt = beginAt === -1 ? -1 : lines.findIndex((l, i) => i > beginAt && l === GITIGNORE_MANAGED_END);
9486
- const hasMarker = beginAt !== -1 || lines.some((l) => l === GITIGNORE_MANAGED_END);
9487
- if (!hasMarker) return { added: [], removed: [], seeded: true };
9488
- const wellOrdered = beginAt !== -1 && endAt !== -1;
9489
- if (!wellOrdered) return { added: [], removed: [], seeded: false };
9490
- const isRule = (l) => l.trim() !== "" && !l.trim().startsWith("#");
9491
- const oldBody = lines.slice(beginAt + 1, endAt).filter(isRule);
9492
- const newBody = MANAGED_GITIGNORE_LINES.filter(isRule);
9493
- return {
9494
- added: newBody.filter((l) => !oldBody.includes(l)),
9495
- removed: oldBody.filter((l) => !newBody.includes(l)),
9496
- seeded: false
9497
- };
9498
- }
9499
-
9500
10880
  // src/doctor.ts
9501
10881
  var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
9502
10882
  var AWS_CROSS_ACCOUNT_FIX = "use a non-root IAM user/session profile for master-agent AWS checks; set AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (plus AWS_SESSION_TOKEN for temporary credentials), then verify `aws sts get-caller-identity` does not end in :root";
@@ -10106,8 +11486,8 @@ async function runStageLiveDown(deps, t) {
10106
11486
 
10107
11487
  // src/stage-runner.ts
10108
11488
  var import_node_child_process8 = require("node:child_process");
10109
- var import_node_fs13 = require("node:fs");
10110
- var import_node_path12 = require("node:path");
11489
+ var import_node_fs16 = require("node:fs");
11490
+ var import_node_path15 = require("node:path");
10111
11491
  var import_node_net2 = require("node:net");
10112
11492
  var import_node_util7 = require("node:util");
10113
11493
  var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process8.execFile);
@@ -10152,7 +11532,7 @@ function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
10152
11532
  return void 0;
10153
11533
  }
10154
11534
  function stageStatePath(cwd = process.cwd()) {
10155
- return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
11535
+ return (0, import_node_path15.join)(cwd, "tmp", "stage", "state.json");
10156
11536
  }
10157
11537
  function mergeEnvSecretsIntoFile(content, secrets2) {
10158
11538
  const lines = content.split(/\r?\n/);
@@ -10176,8 +11556,8 @@ function mergeEnvSecretsIntoFile(content, secrets2) {
10176
11556
  `;
10177
11557
  }
10178
11558
  var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
10179
- function posixOnlyShellProblems(command, field, platform = process.platform) {
10180
- if (platform !== "win32" || !command?.trim()) return [];
11559
+ function posixOnlyShellProblems(command, field, platform2 = process.platform) {
11560
+ if (platform2 !== "win32" || !command?.trim()) return [];
10181
11561
  const problems = [];
10182
11562
  if (/(^|&&|\||;)\s*[A-Za-z_][A-Za-z0-9_]*=\S/.test(command)) {
10183
11563
  problems.push(
@@ -10235,9 +11615,9 @@ async function shell(command, cwd, timeoutMs) {
10235
11615
  });
10236
11616
  }
10237
11617
  function readState(path2) {
10238
- if (!(0, import_node_fs13.existsSync)(path2)) return null;
11618
+ if (!(0, import_node_fs16.existsSync)(path2)) return null;
10239
11619
  try {
10240
- return JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
11620
+ return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
10241
11621
  } catch {
10242
11622
  return null;
10243
11623
  }
@@ -10289,7 +11669,7 @@ async function stopStage(opts = {}) {
10289
11669
  return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
10290
11670
  }
10291
11671
  await killTree(state.pid);
10292
- (0, import_node_fs13.rmSync)(statePath, { force: true });
11672
+ (0, import_node_fs16.rmSync)(statePath, { force: true });
10293
11673
  return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
10294
11674
  }
10295
11675
  async function startStage(config = {}, opts = {}) {
@@ -10298,7 +11678,7 @@ async function startStage(config = {}, opts = {}) {
10298
11678
  const cwd = opts.cwd ?? process.cwd();
10299
11679
  const statePath = opts.statePath ?? stageStatePath(cwd);
10300
11680
  const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
10301
- (0, import_node_fs13.mkdirSync)(dir, { recursive: true });
11681
+ (0, import_node_fs16.mkdirSync)(dir, { recursive: true });
10302
11682
  let stagePort;
10303
11683
  if (config.portRange) {
10304
11684
  const [s, e] = config.portRange;
@@ -10308,14 +11688,14 @@ async function startStage(config = {}, opts = {}) {
10308
11688
  }
10309
11689
  const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
10310
11690
  if (config.ensureEnv) {
10311
- const target = (0, import_node_path12.join)(cwd, config.ensureEnv.target);
10312
- const example = (0, import_node_path12.join)(cwd, config.ensureEnv.example);
10313
- if (!(0, import_node_fs13.existsSync)(target) && (0, import_node_fs13.existsSync)(example)) {
10314
- (0, import_node_fs13.copyFileSync)(example, target);
10315
- } else if ((0, import_node_fs13.existsSync)(target) && (0, import_node_fs13.existsSync)(example)) {
10316
- const stale = detectStaleEnvFile((0, import_node_fs13.readFileSync)(example, "utf8"), (0, import_node_fs13.readFileSync)(target, "utf8"), {
10317
- exampleMtimeMs: (0, import_node_fs13.statSync)(example).mtimeMs,
10318
- targetMtimeMs: (0, import_node_fs13.statSync)(target).mtimeMs
11691
+ const target = (0, import_node_path15.join)(cwd, config.ensureEnv.target);
11692
+ const example = (0, import_node_path15.join)(cwd, config.ensureEnv.example);
11693
+ if (!(0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11694
+ (0, import_node_fs16.copyFileSync)(example, target);
11695
+ } else if ((0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11696
+ const stale = detectStaleEnvFile((0, import_node_fs16.readFileSync)(example, "utf8"), (0, import_node_fs16.readFileSync)(target, "utf8"), {
11697
+ exampleMtimeMs: (0, import_node_fs16.statSync)(example).mtimeMs,
11698
+ targetMtimeMs: (0, import_node_fs16.statSync)(target).mtimeMs
10319
11699
  });
10320
11700
  if (stale) {
10321
11701
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -10323,8 +11703,8 @@ async function startStage(config = {}, opts = {}) {
10323
11703
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
10324
11704
  }
10325
11705
  }
10326
- if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs13.existsSync)(target)) {
10327
- (0, import_node_fs13.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs13.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
11706
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs16.existsSync)(target)) {
11707
+ (0, import_node_fs16.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs16.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
10328
11708
  }
10329
11709
  }
10330
11710
  const extraEnv = {};
@@ -10349,13 +11729,13 @@ async function startStage(config = {}, opts = {}) {
10349
11729
  healthUrl: sub(config.healthUrl?.trim()) || void 0,
10350
11730
  port: stagePort
10351
11731
  };
10352
- (0, import_node_fs13.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
11732
+ (0, import_node_fs16.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
10353
11733
  try {
10354
11734
  if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
10355
11735
  else await waitForProcessStability(child);
10356
11736
  } catch (e) {
10357
11737
  await killTree(state.pid);
10358
- (0, import_node_fs13.rmSync)(statePath, { force: true });
11738
+ (0, import_node_fs16.rmSync)(statePath, { force: true });
10359
11739
  throw e;
10360
11740
  }
10361
11741
  const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
@@ -10374,75 +11754,6 @@ async function runStage(config = {}, opts = {}) {
10374
11754
  return { ...started, action: "run", message: `built and ${started.message}` };
10375
11755
  }
10376
11756
 
10377
- // src/project-model.ts
10378
- var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable", "cli-tool", "worker"];
10379
- var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "solo-container", "registry-publish", "content", "none"];
10380
- var RELEASE_TRACKS = ["full", "direct", "trunk"];
10381
- var PROJECT_TYPE_SET = new Set(PROJECT_TYPES);
10382
- var DEPLOY_MODEL_SET = new Set(DEPLOY_MODELS);
10383
- var RELEASE_TRACK_SET = new Set(RELEASE_TRACKS);
10384
- function isProjectType(value) {
10385
- return Boolean(value && PROJECT_TYPE_SET.has(value));
10386
- }
10387
- function isDeployModel(value) {
10388
- return Boolean(value && DEPLOY_MODEL_SET.has(value));
10389
- }
10390
- function isReleaseTrack(value) {
10391
- return Boolean(value && RELEASE_TRACK_SET.has(value));
10392
- }
10393
- function repoIsHub(repo) {
10394
- return repo.toLowerCase().endsWith("/mmi-hub") || repo.toLowerCase() === "mmi-hub";
10395
- }
10396
- function resolveProjectTypeConfident(meta, repo) {
10397
- const rawType = typeof meta?.projectType === "string" ? meta.projectType : void 0;
10398
- if (isProjectType(rawType)) return rawType;
10399
- if (meta?.class === "content" || meta?.deployModel === "content") return "content";
10400
- if (meta?.deployModel === "hub-serverless" || repoIsHub(repo)) return "hub-service";
10401
- if (meta?.deployModel === "registry-publish") return "cli-tool";
10402
- if (meta?.deployModel === "solo-container") return "worker";
10403
- if (meta?.deployModel === "none") return "non-deployable";
10404
- return void 0;
10405
- }
10406
- function resolveProjectType(meta, repo) {
10407
- return resolveProjectTypeConfident(meta, repo) ?? "web-app";
10408
- }
10409
- function resolveDeployModel(meta, repo) {
10410
- const rawModel = typeof meta?.deployModel === "string" ? meta.deployModel : void 0;
10411
- if (isDeployModel(rawModel)) return rawModel;
10412
- const projectType = resolveProjectType(meta, repo);
10413
- if (projectType === "content" || meta?.class === "content") return "content";
10414
- if (projectType === "hub-service" || repoIsHub(repo)) return "hub-serverless";
10415
- if (projectType === "desktop-game" || projectType === "non-deployable") return "none";
10416
- if (projectType === "cli-tool") return "registry-publish";
10417
- return "tenant-container";
10418
- }
10419
- function projectTypeClearsWebProfile(projectType, deployModel) {
10420
- return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
10421
- }
10422
- function inferReleaseTrackFromBranches(hints) {
10423
- if (hints.hasRcBranch) return void 0;
10424
- if (hints.hasDevelopmentBranch && hints.hasMainBranch) return "direct";
10425
- return void 0;
10426
- }
10427
- function resolveBootstrapReleaseTrack(cls, explicit) {
10428
- if (isReleaseTrack(explicit)) return explicit;
10429
- if (cls === "content") return "trunk";
10430
- return "full";
10431
- }
10432
- function resolveReleaseTrack(meta, hints) {
10433
- const raw = typeof meta?.releaseTrack === "string" ? meta.releaseTrack : void 0;
10434
- if (isReleaseTrack(raw)) return raw;
10435
- if (meta?.class === "content" || meta?.deployModel === "content") return "trunk";
10436
- const inferred = hints ? inferReleaseTrackFromBranches(hints) : void 0;
10437
- if (inferred) return inferred;
10438
- return "full";
10439
- }
10440
- function branchesForTrack(track) {
10441
- if (track === "trunk") return ["main"];
10442
- if (track === "direct") return ["development", "main"];
10443
- return ["development", "rc", "main"];
10444
- }
10445
-
10446
11757
  // src/org-spine-pull.ts
10447
11758
  async function reconcileDirtyOrgSpineBeforePull(deps, branch, options = {}) {
10448
11759
  await deps.run("git", ["fetch", "origin", branch]).catch(() => "");
@@ -10471,7 +11782,7 @@ function resolveDeployModel2(meta, repo) {
10471
11782
  if (isDeployModel(m)) return m;
10472
11783
  return resolveDeployModel(meta, repo);
10473
11784
  }
10474
- function clean(out) {
11785
+ function clean2(out) {
10475
11786
  return out.trim();
10476
11787
  }
10477
11788
  function requireValue(value, label) {
@@ -10522,11 +11833,11 @@ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedS
10522
11833
  if (release.targetCommitish !== expectedTarget) {
10523
11834
  throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
10524
11835
  }
10525
- const tagSha = requireValue(clean(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
11836
+ const tagSha = requireValue(clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
10526
11837
  if (tagSha !== expectedSha) {
10527
11838
  throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
10528
11839
  }
10529
- const branchOut = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
11840
+ const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
10530
11841
  const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
10531
11842
  if (branchSha !== expectedSha) {
10532
11843
  throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
@@ -10586,7 +11897,7 @@ function ensurePositiveCount(out, emptyMessage) {
10586
11897
  if (count <= 0) throw new Error(emptyMessage);
10587
11898
  }
10588
11899
  async function remoteBranchExists(deps, branch) {
10589
- const out = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
11900
+ const out = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
10590
11901
  return out.length > 0;
10591
11902
  }
10592
11903
  async function loadReleaseTrackBranchHints(deps) {
@@ -10598,10 +11909,10 @@ async function loadReleaseTrackBranchHints(deps) {
10598
11909
  return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
10599
11910
  }
10600
11911
  async function buildTrainApplyContext(deps) {
10601
- const repo = requireValue(clean(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
11912
+ const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
10602
11913
  const [owner, name] = repo.split("/");
10603
11914
  if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
10604
- const login = requireValue(clean(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
11915
+ const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
10605
11916
  const verdict = await deps.trainAuthority(repo);
10606
11917
  if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
10607
11918
  if (!verdict.train) {
@@ -10622,7 +11933,7 @@ async function requireCleanTree(deps) {
10622
11933
  if (status.trim()) throw new Error("working tree must be clean before train apply");
10623
11934
  }
10624
11935
  async function requireBranch(deps, branch) {
10625
- const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
11936
+ const current = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
10626
11937
  if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
10627
11938
  }
10628
11939
  var HUB_REPO3 = "mutmutco/MMI-Hub";
@@ -10791,7 +12102,7 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
10791
12102
  return [...contexts];
10792
12103
  }
10793
12104
  async function findOpenAlignmentPr(deps, ctx) {
10794
- const out = clean(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
12105
+ const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
10795
12106
  if (!out) return void 0;
10796
12107
  const rows = JSON.parse(out);
10797
12108
  const row = rows.find((r) => typeof r.number === "number" && typeof r.url === "string");
@@ -10819,14 +12130,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
10819
12130
  note: `alignment PR already open: ${existing.url} \u2014 land it with \`gh pr merge ${existing.number} --merge\``
10820
12131
  };
10821
12132
  }
10822
- const ahead = clean(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
12133
+ const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
10823
12134
  if (ahead === "0") {
10824
12135
  return { status: "aligned", note: "development already contains the released main; nothing to roll forward" };
10825
12136
  }
10826
12137
  const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
10827
12138
 
10828
12139
  \`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** (\`gh pr merge --merge\`, not squash) so the merge parentage survives and the misalignment guard stays satisfied.`;
10829
- const url = clean(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
12140
+ const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
10830
12141
  const number = parsePrNumber(url);
10831
12142
  return {
10832
12143
  status: "pr-pending",
@@ -10892,10 +12203,10 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
10892
12203
  }
10893
12204
  async function ensureTagPushed(deps, tag, sha) {
10894
12205
  const remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]);
10895
- const remoteSha = clean(remoteOut).split(/\s+/)[0] || "";
12206
+ const remoteSha = clean2(remoteOut).split(/\s+/)[0] || "";
10896
12207
  let localSha = "";
10897
12208
  try {
10898
- localSha = clean(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
12209
+ localSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
10899
12210
  } catch {
10900
12211
  }
10901
12212
  if (remoteSha) {
@@ -10918,7 +12229,7 @@ async function ensureTagPushed(deps, tag, sha) {
10918
12229
  }
10919
12230
  async function resolveRcResumeTag(deps, base2, sha) {
10920
12231
  const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base2}-rc.*`]);
10921
- const onSha = clean(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base2.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
12232
+ const onSha = clean2(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base2.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
10922
12233
  const unique = [...new Set(onSha)];
10923
12234
  if (unique.length === 0) return { tag: null };
10924
12235
  const rcNum = (t) => Number.parseInt(t.replace(/^.*-rc\./, ""), 10);
@@ -11043,13 +12354,13 @@ async function runTrainApply(command, deps, options = {}) {
11043
12354
  "nothing to promote: origin/development is not ahead of origin/rc"
11044
12355
  );
11045
12356
  const deployModel2 = await preflight(deps, ctx, "rc", meta);
11046
- const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
12357
+ const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
11047
12358
  await deps.run("git", ["checkout", "rc"]);
11048
12359
  await ffOnlyPull(deps, "rc");
11049
12360
  await deps.run("git", ["merge", "development", "--no-edit"]);
11050
- const rcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
12361
+ const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
11051
12362
  const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
11052
- const tag2 = resume.tag ?? requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
12363
+ const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
11053
12364
  const resumeNote = resume.tag ? resume.note : void 0;
11054
12365
  await ensureTagPushed(deps, tag2, rcSha);
11055
12366
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
@@ -11067,7 +12378,7 @@ async function runTrainApply(command, deps, options = {}) {
11067
12378
  "nothing to release: origin/development is not ahead of origin/main"
11068
12379
  );
11069
12380
  const deployModel2 = await preflight(deps, ctx, "main", meta);
11070
- const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
12381
+ const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
11071
12382
  const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
11072
12383
  const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
11073
12384
  const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
@@ -11085,12 +12396,12 @@ async function runTrainApply(command, deps, options = {}) {
11085
12396
  await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
11086
12397
  }
11087
12398
  const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
11088
- const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
12399
+ const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
11089
12400
  await ensureTagPushed(deps, tag2, releaseSha2);
11090
12401
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
11091
12402
  const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
11092
12403
  await deps.run("git", ["push", "origin", "main"]);
11093
- const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
12404
+ const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
11094
12405
  await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
11095
12406
  const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
11096
12407
  const autoRunSince2 = (deps.now ?? Date.now)();
@@ -11139,8 +12450,8 @@ async function runTrainApply(command, deps, options = {}) {
11139
12450
  "nothing to release: origin/development is not ahead of origin/main"
11140
12451
  );
11141
12452
  const deployModel2 = await preflight(deps, ctx, "main", meta);
11142
- const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
11143
- const rcShaAtRelease = hasRcBranch ? clean(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
12453
+ const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
12454
+ const rcShaAtRelease = hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
11144
12455
  const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
11145
12456
  const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
11146
12457
  const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
@@ -11158,12 +12469,12 @@ async function runTrainApply(command, deps, options = {}) {
11158
12469
  await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
11159
12470
  }
11160
12471
  const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
11161
- const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
12472
+ const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
11162
12473
  await ensureTagPushed(deps, tag2, releaseSha2);
11163
12474
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
11164
12475
  const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
11165
12476
  await deps.run("git", ["push", "origin", "main"]);
11166
- const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
12477
+ const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
11167
12478
  await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
11168
12479
  const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
11169
12480
  const autoRunSince2 = (deps.now ?? Date.now)();
@@ -11230,7 +12541,7 @@ async function runTrainApply(command, deps, options = {}) {
11230
12541
  `hotfix-coverage: ${coverage.uncovered.length} main-only commit(s) not proven in origin/rc \u2014 the candidate would revert a prod hotfix: ${list}. Re-cut /rcand from development, or have the authorized human verify the content is in the candidate and rerun release with --ack <sha>[,<sha>\u2026].`
11231
12542
  );
11232
12543
  }
11233
- const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
12544
+ const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
11234
12545
  await deps.run("git", ["checkout", "main"]);
11235
12546
  await ffOnlyPull(deps, "main");
11236
12547
  if (predicted.length === 0) {
@@ -11238,15 +12549,15 @@ async function runTrainApply(command, deps, options = {}) {
11238
12549
  } else {
11239
12550
  await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs", tolerated);
11240
12551
  }
11241
- const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
12552
+ const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
11242
12553
  const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
11243
- const releaseSha = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
12554
+ const releaseSha = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
11244
12555
  await ensureTagPushed(deps, tag, releaseSha);
11245
12556
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
11246
12557
  const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
11247
12558
  await deps.run("git", ["push", "origin", "main"]);
11248
12559
  const autoRunSince = (deps.now ?? Date.now)();
11249
- const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
12560
+ const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
11250
12561
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
11251
12562
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
11252
12563
  const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report");
@@ -11374,7 +12685,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
11374
12685
  }
11375
12686
  try {
11376
12687
  await deps.run("git", ["fetch", "origin", "rc"]);
11377
- const rcNow = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
12688
+ const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
11378
12689
  if (rcNow !== releasedRcSha) {
11379
12690
  return {
11380
12691
  status: "skipped",
@@ -11406,7 +12717,7 @@ async function runTenantRedeploy(deps, options) {
11406
12717
  const repo = options.repo;
11407
12718
  const [owner, name] = repo.split("/");
11408
12719
  if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
11409
- const login = requireValue(clean(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
12720
+ const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
11410
12721
  const verdict = await deps.trainAuthority(repo);
11411
12722
  if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
11412
12723
  if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
@@ -11562,7 +12873,7 @@ var HOTFIX_RUN_FIND_ATTEMPTS = 10;
11562
12873
  var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
11563
12874
  var HOTFIX_VERIFY_ATTEMPTS = 5;
11564
12875
  var HOTFIX_VERIFY_RETRY_MS = 6e3;
11565
- function clean2(out) {
12876
+ function clean3(out) {
11566
12877
  return out.trim();
11567
12878
  }
11568
12879
  function sleeper(deps) {
@@ -11620,7 +12931,7 @@ async function resolveHotfixSource(deps, ctx, from) {
11620
12931
  if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
11621
12932
  return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
11622
12933
  }
11623
- const sha = clean2(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
12934
+ const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
11624
12935
  if (!sha) throw new Error(`could not resolve commit ${from}`);
11625
12936
  return { sha, label: sha.slice(0, 7) };
11626
12937
  }
@@ -11648,7 +12959,7 @@ async function runHotfixStart(deps, options) {
11648
12959
  };
11649
12960
  }
11650
12961
  const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
11651
- const remoteBranch = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
12962
+ const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
11652
12963
  if (remoteBranch) {
11653
12964
  await deps.run("git", ["checkout", branch]);
11654
12965
  const isRulesSource2 = deps.isRulesSource ? await deps.isRulesSource() : false;
@@ -11681,7 +12992,7 @@ async function runHotfixStart(deps, options) {
11681
12992
  await deps.run("git", ["push", "-u", "origin", branch]);
11682
12993
  }
11683
12994
  const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : "";
11684
- const prUrl = clean2(await deps.run("gh", [
12995
+ const prUrl = clean3(await deps.run("gh", [
11685
12996
  "pr",
11686
12997
  "create",
11687
12998
  "--repo",
@@ -11801,7 +13112,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
11801
13112
  }
11802
13113
  let verifyNote;
11803
13114
  if (deployModel === "hub-serverless") {
11804
- const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
13115
+ const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
11805
13116
  const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
11806
13117
  try {
11807
13118
  await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
@@ -11886,9 +13197,9 @@ async function runHotfixStatus(deps, versionInput) {
11886
13197
  return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
11887
13198
  }
11888
13199
  async function gatherHotfixFacts(deps, ctx, tag, version) {
11889
- const branchExists = Boolean(clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
13200
+ const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
11890
13201
  const pr2 = await findHotfixPr(deps, ctx, tag);
11891
- const remoteTag = clean2(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
13202
+ const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
11892
13203
  const tagPushed = Boolean(remoteTag);
11893
13204
  const tagSha = remoteTag.split(/\s+/)[0] || "";
11894
13205
  let releaseExists = false;
@@ -11922,7 +13233,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
11922
13233
  }
11923
13234
  }
11924
13235
  }
11925
- const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean2, () => "unknown");
13236
+ const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
11926
13237
  return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
11927
13238
  }
11928
13239
  async function findInFlightHotfixVersion(deps, ctx) {
@@ -11945,7 +13256,7 @@ async function findInFlightHotfixVersion(deps, ctx) {
11945
13256
  const m = typeof row.headRefName === "string" && /^hotfix\/(v\d+\.\d+\.\d+)/.exec(row.headRefName);
11946
13257
  if (m) tags.add(m[1]);
11947
13258
  }
11948
- const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
13259
+ const branchOut = clean3(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
11949
13260
  for (const line of branchOut.split("\n").filter(Boolean)) {
11950
13261
  const ref = line.split(/\s+/)[1] ?? "";
11951
13262
  const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
@@ -12094,7 +13405,7 @@ async function announceRelease(deps, args) {
12094
13405
  }
12095
13406
 
12096
13407
  // src/port-registry.ts
12097
- var import_node_fs14 = require("node:fs");
13408
+ var import_node_fs17 = require("node:fs");
12098
13409
 
12099
13410
  // ../infra/port-geometry.mjs
12100
13411
  var PORT_BLOCK = 100;
@@ -12108,8 +13419,8 @@ function nextPortBlock(registry2) {
12108
13419
  return [base2, base2 + PORT_SPAN];
12109
13420
  }
12110
13421
  function loadPortRegistry(path2) {
12111
- if (!(0, import_node_fs14.existsSync)(path2)) return {};
12112
- const raw = JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
13422
+ if (!(0, import_node_fs17.existsSync)(path2)) return {};
13423
+ const raw = JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8"));
12113
13424
  const out = {};
12114
13425
  for (const [key, value] of Object.entries(raw)) {
12115
13426
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -12123,9 +13434,9 @@ function ensurePortRange(repo, path2) {
12123
13434
  const existing = registry2[repo];
12124
13435
  if (existing) return existing;
12125
13436
  const range = nextPortBlock(registry2);
12126
- const raw = (0, import_node_fs14.existsSync)(path2) ? JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8")) : {};
13437
+ const raw = (0, import_node_fs17.existsSync)(path2) ? JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8")) : {};
12127
13438
  raw[repo] = range;
12128
- (0, import_node_fs14.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
13439
+ (0, import_node_fs17.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
12129
13440
  return range;
12130
13441
  }
12131
13442
  function portCursorSeed(registry2) {
@@ -12417,7 +13728,13 @@ var HUB_REPO4 = "mutmutco/MMI-Hub";
12417
13728
  var requiredLabels = ["bug", "feature", "task"];
12418
13729
  var requiredPriorityOptions = ["Urgent", "High", "Medium", "Low"];
12419
13730
  var strayDefaultLabels = ["documentation", "duplicate", "enhancement", "good first issue", "help wanted", "invalid", "question", "wontfix"];
13731
+ var retiredPriorityLabels = ["priority:urgent", "priority:high", "priority:medium", "priority:low"];
12420
13732
  var requiredStatusOptions = ["Todo", "In Progress", "In Review", "Done"];
13733
+ var canonicalStatusColors = { Todo: "GRAY", "In Progress": "YELLOW", "In Review": "ORANGE", Done: "GREEN" };
13734
+ var canonicalPriorityColors = { Urgent: "RED", High: "ORANGE", Medium: "YELLOW", Low: "GRAY" };
13735
+ function miscoloredOptions(options, canon) {
13736
+ return (options ?? []).filter((o) => canon[o.name] != null && o.color != null && o.color !== canon[o.name]).map((o) => `${o.name}=${o.color} (want ${canon[o.name]})`);
13737
+ }
12421
13738
  var requiredProjectWorkflows = [
12422
13739
  "Auto-add sub-issues to project",
12423
13740
  "Auto-archive items",
@@ -12537,6 +13854,23 @@ function localRegistryCheck(deps, path2, predicate) {
12537
13854
  if (text == null) return null;
12538
13855
  return predicate(safeJson2(text, null));
12539
13856
  }
13857
+ var DOC_PLACEHOLDER_PROMPTS = [
13858
+ "languages, frameworks, major services",
13859
+ "install, dev server",
13860
+ "lint, typecheck, repo gate script",
13861
+ "ports, env from vault not files",
13862
+ "Describe the product/service",
13863
+ "How to run, build, and test locally",
13864
+ "System shape: components, data flow, deploy model",
13865
+ "Build/test commands, CI gate, deploy target"
13866
+ ];
13867
+ function unfilledDocPlaceholders(text) {
13868
+ if (!text) return [];
13869
+ const found = /* @__PURE__ */ new Set();
13870
+ for (const m of text.match(/\{\{[A-Za-z0-9_]+\}\}/g) ?? []) found.add(m);
13871
+ for (const prompt of DOC_PLACEHOLDER_PROMPTS) if (text.includes(prompt)) found.add(prompt);
13872
+ return [...found];
13873
+ }
12540
13874
  async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12541
13875
  const branchesWanted = expectedBranches(repoClass, releaseTrack);
12542
13876
  const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
@@ -12600,6 +13934,19 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12600
13934
  label: "README has Agent context section",
12601
13935
  detail: readme === null ? "README.md not readable via API" : void 0
12602
13936
  });
13937
+ const readmePlaceholders = unfilledDocPlaceholders(readme);
13938
+ checks.push({
13939
+ ok: readmePlaceholders.length === 0,
13940
+ label: "README Agent context is filled (no template placeholders)",
13941
+ detail: readmePlaceholders.length ? `unfilled: ${readmePlaceholders.join(", ")}` : void 0
13942
+ });
13943
+ const architecture = await contentText(deps, repo, baseBranch, "architecture.md");
13944
+ const archPlaceholders = unfilledDocPlaceholders(architecture);
13945
+ checks.push({
13946
+ ok: archPlaceholders.length === 0,
13947
+ label: "architecture.md is filled (no template placeholders)",
13948
+ detail: archPlaceholders.length ? `unfilled: ${archPlaceholders.join(", ")}` : void 0
13949
+ });
12603
13950
  const agentRulesPath = `.cursor/rules/${repoSlugFromFullName(repo)}.mdc`;
12604
13951
  const agentRulesOk = await contentExists2(deps, repo, baseBranch, agentRulesPath);
12605
13952
  checks.push({
@@ -12614,6 +13961,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12614
13961
  }
12615
13962
  const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
12616
13963
  checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
13964
+ const priorityStrays = retiredPriorityLabels.filter((l) => labelNames.has(l));
13965
+ checks.push({ ok: priorityStrays.length === 0, label: "no retired priority:* labels (#416)", detail: presentDetail(priorityStrays) });
12617
13966
  const actions = await restJson3(deps, `repos/${repo}/actions/permissions`, {});
12618
13967
  checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
12619
13968
  const config = deps.projectMeta ?? null;
@@ -12629,7 +13978,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12629
13978
  });
12630
13979
  }
12631
13980
  if (config?.projectOwner && config.projectNumber != null) {
12632
- const fieldsQuery = `query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name } ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }`;
13981
+ const fieldsQuery = `query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name } ... on ProjectV2SingleSelectField { id name options { id name color } } } } } } }`;
12633
13982
  const fields = await (async () => {
12634
13983
  try {
12635
13984
  const data = await deps.client.graphql(fieldsQuery, {
@@ -12670,6 +14019,12 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12670
14019
  label: `registry status option matches project: ${optionName}`
12671
14020
  });
12672
14021
  }
14022
+ const statusColorDrift = miscoloredOptions(statusField.options, canonicalStatusColors);
14023
+ checks.push({
14024
+ ok: statusColorDrift.length === 0,
14025
+ label: "Project Status option colors canonical (#1543)",
14026
+ detail: statusColorDrift.length ? `drifted: ${statusColorDrift.join(", ")}` : void 0
14027
+ });
12673
14028
  }
12674
14029
  const priorityField = fields.find((field) => field.name === "Priority" && (field.options?.length ?? 0) > 0);
12675
14030
  checks.push({
@@ -12695,6 +14050,12 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12695
14050
  label: `registry priority option matches project: ${optionName}`
12696
14051
  });
12697
14052
  }
14053
+ const priorityColorDrift = miscoloredOptions(priorityField.options, canonicalPriorityColors);
14054
+ checks.push({
14055
+ ok: priorityColorDrift.length === 0,
14056
+ label: "Project Priority option colors canonical (#1543)",
14057
+ detail: priorityColorDrift.length ? `drifted: ${priorityColorDrift.join(", ")}` : void 0
14058
+ });
12698
14059
  }
12699
14060
  const workflowQuery = "query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { workflows(first: 30) { nodes { name enabled } } } } }";
12700
14061
  const workflowResponse = await (async () => {
@@ -12732,340 +14093,57 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12732
14093
  detail: orgRuleset ? void 0 : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
12733
14094
  });
12734
14095
  if (repo === HUB_REPO4) {
12735
- const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
12736
- const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
12737
- checks.push({
12738
- ok: missing.length === 0,
12739
- label: "Hub required status checks configured",
12740
- detail: optionDetail(missing)
12741
- });
12742
- } else if (repoClass === "deployable") {
12743
- const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
12744
- const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
12745
- checks.push({
12746
- ok: missing.length === 0,
12747
- label: "product required status checks configured",
12748
- detail: missing.length ? `missing contexts: ${missing.join(", ")} \u2014 apply ${requiredProductRulesetRef} as an active repo ruleset` : void 0
12749
- });
12750
- }
12751
- const declaredApis = (deps.requiredGcpApis ?? []).filter((a) => a && a.trim());
12752
- if (declaredApis.length > 0) {
12753
- const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
12754
- const gcpProject = gcpProjectForSlug(slug);
12755
- const enabled = deps.listEnabledGcpApis ? await deps.listEnabledGcpApis(gcpProject) : null;
12756
- if (enabled == null) {
12757
- checks.push({
12758
- ok: false,
12759
- label: `required GCP APIs enabled in ${gcpProject}`,
12760
- detail: `could not list enabled APIs (gcloud unavailable, not authed, or no project ${gcpProject})`
12761
- });
12762
- } else {
12763
- const missing = findMissingGcpApis(declaredApis, enabled);
12764
- checks.push({
12765
- ok: missing.length === 0,
12766
- label: `required GCP APIs enabled in ${gcpProject}`,
12767
- detail: missing.length ? `disabled: ${missing.join(", ")}` : void 0
12768
- });
12769
- }
12770
- }
12771
- const waived = applyWaivers(checks, config?.verifyWaivers ?? []);
12772
- return { ok: waived.every((c) => c.ok || c.waived), repo, class: repoClass, baseBranch, checks: waived };
12773
- }
12774
- function applyWaivers(checks, waivers) {
12775
- if (!waivers?.length) return checks;
12776
- const set = new Set(waivers);
12777
- return checks.map((c) => !c.ok && set.has(c.label) ? { ...c, waived: true } : c);
12778
- }
12779
- function renderBootstrapVerifyReport(report) {
12780
- const lines = [`mmi-cli bootstrap verify: ${report.ok ? "OK" : "CHECK"} ${report.repo} (${report.class}, ${report.baseBranch})`];
12781
- for (const check of report.checks) {
12782
- const status = check.ok ? "OK" : check.waived ? "WAIVE" : "FAIL";
12783
- lines.push(`${status} ${check.label}${check.detail ? ` - ${check.detail}` : ""}`);
12784
- }
12785
- return lines.join("\n");
12786
- }
12787
-
12788
- // src/bootstrap-apply.ts
12789
- function parseOwnerRepo(repo) {
12790
- const trimmed = repo.trim();
12791
- const parts = trimmed.split("/");
12792
- if (parts.length !== 2 || !parts[0]?.trim() || !parts[1]?.trim()) {
12793
- throw new Error("repo must be owner/repo");
12794
- }
12795
- const owner = parts[0].trim();
12796
- const name = parts[1].trim();
12797
- if (owner.includes("\\") || name.includes("\\")) throw new Error("repo must be owner/repo");
12798
- return { owner, name, slug: name.toLowerCase(), fullName: `${owner}/${name}` };
12799
- }
12800
- var DEFAULT_INSTALL_CMD = "npm ci";
12801
- var DEFAULT_GATE_CMD = "npm run check";
12802
- function gateSeedVars(cls, releaseTrack) {
12803
- const track = releaseTrack ?? (cls === "content" ? "trunk" : "full");
12804
- if (track === "trunk") {
12805
- return {
12806
- GATE_CMD: DEFAULT_GATE_CMD,
12807
- GATE_PUSH_BRANCHES_YAML: "[main]",
12808
- GATE_FULL_RUN_BRANCH: "main",
12809
- GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/main"]'
12810
- };
12811
- }
12812
- if (track === "direct") {
12813
- return {
12814
- GATE_CMD: DEFAULT_GATE_CMD,
12815
- GATE_PUSH_BRANCHES_YAML: "[development, main]",
12816
- GATE_FULL_RUN_BRANCH: "development",
12817
- GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/main"]'
12818
- };
12819
- }
12820
- return {
12821
- GATE_CMD: DEFAULT_GATE_CMD,
12822
- GATE_PUSH_BRANCHES_YAML: "[development, rc, main]",
12823
- GATE_FULL_RUN_BRANCH: "development",
12824
- GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/rc", "refs/heads/main"]'
12825
- };
12826
- }
12827
- function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
12828
- const out = { ...vars };
12829
- out.REPO_NAME ??= parsed.name;
12830
- out.REPO_SLUG ??= parsed.slug;
12831
- out.CLASS ??= cls;
12832
- out.INSTALL_CMD ??= DEFAULT_INSTALL_CMD;
12833
- const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
12834
- for (const [key, value] of Object.entries(gateSeedVars(cls, track))) {
12835
- out[key] ??= value;
12836
- }
12837
- return out;
12838
- }
12839
- function planSeedAction(seed, exists) {
12840
- if (seed.source === "fanout") {
12841
- return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
12842
- }
12843
- if (seed.source === "managed-block") {
12844
- return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-managed block merged in-place (repo-owned lines preserved)" } : { target: seed.target, action: "create", ownership: "org", reason: "org-managed block; .gitignore absent, created" };
12845
- }
12846
- if (seed.ownership === "repo") {
12847
- return exists ? { target: seed.target, action: "skip", ownership: "repo", reason: "repo-owned, already present (never clobbered)" } : { target: seed.target, action: "create", ownership: "repo", reason: "repo-owned, missing" };
12848
- }
12849
- return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
12850
- }
12851
- function reconcileSeedAction(action, content, isManagedBlock) {
12852
- if (action.action === "skip") return action;
12853
- if (content == null) {
12854
- return { ...action, action: "skip", reason: "no resolvable content" };
12855
- }
12856
- if (isManagedBlock) return action;
12857
- const unfilled = missingPlaceholders(content);
12858
- if (unfilled.length) {
12859
- return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
12860
- }
12861
- return action;
12862
- }
12863
- function renderSeedPlan(actions) {
12864
- const lines = ["bootstrap apply \u2014 seed plan (dry-run; no mutations):"];
12865
- for (const a of actions) {
12866
- lines.push(` ${a.action.toUpperCase().padEnd(6)} ${a.target} (${a.ownership}: ${a.reason})`);
12867
- }
12868
- const order = ["create", "update", "skip"];
12869
- lines.push(` \u2014 ${order.map((k) => `${actions.filter((a) => a.action === k).length} ${k}`).join(", ")}`);
12870
- return lines.join("\n");
12871
- }
12872
- var GITHUB_DEFAULT_LABELS = [
12873
- "documentation",
12874
- "duplicate",
12875
- "enhancement",
12876
- "good first issue",
12877
- "help wanted",
12878
- "invalid",
12879
- "question",
12880
- "wontfix"
12881
- ];
12882
- function labelsToPrune(orgLabelNames) {
12883
- const org = new Set(orgLabelNames);
12884
- return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
12885
- }
12886
- function resolveSeedContent(seed, vars, readFile5) {
12887
- if (seed.source === "self") return readFile5(seed.target);
12888
- if (seed.source.startsWith("seed:")) {
12889
- const tmpl = readFile5(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
12890
- return tmpl == null ? null : renderSeed(tmpl, vars);
12891
- }
12892
- return null;
12893
- }
12894
- function buildRegisterPayload(repo, cls, vars, options = {}) {
12895
- const parsedRepo = parseOwnerRepo(repo);
12896
- const slug = parsedRepo.slug;
12897
- if (options.projectType && !isProjectType(options.projectType)) {
12898
- throw new Error(`projectType must be one of: ${PROJECT_TYPES.join(", ")}`);
12899
- }
12900
- if (options.deployModel && !isDeployModel(options.deployModel)) {
12901
- throw new Error(`deployModel must be one of: ${DEPLOY_MODELS.join(", ")}`);
12902
- }
12903
- if (options.releaseTrack && !isReleaseTrack(options.releaseTrack)) {
12904
- throw new Error("releaseTrack must be full, direct, or trunk");
12905
- }
12906
- const shape = {
12907
- class: cls,
12908
- projectType: options.projectType,
12909
- deployModel: options.deployModel
12910
- };
12911
- const projectType = resolveProjectTypeConfident(shape, parsedRepo.fullName);
12912
- if (!projectType) {
12913
- throw new Error(
12914
- `Project type for ${parsedRepo.fullName} is unset and not derivable \u2014 pass --project-type <${PROJECT_TYPES.join("|")}> and --deploy-model <${DEPLOY_MODELS.join("|")}> (prevents defaulting a non-web repo to tenant-container).`
12915
- );
12916
- }
12917
- const deployModel = resolveDeployModel({ ...shape, projectType }, parsedRepo.fullName);
12918
- const num = (v) => {
12919
- if (v == null || v === "") return void 0;
12920
- const n = Number(v);
12921
- return Number.isFinite(n) ? n : void 0;
12922
- };
12923
- const statusOptions = vars.STATUS_TODO || vars.STATUS_IN_PROGRESS || vars.STATUS_IN_REVIEW || vars.STATUS_DONE ? {
12924
- Todo: vars.STATUS_TODO,
12925
- "In Progress": vars.STATUS_IN_PROGRESS,
12926
- "In Review": vars.STATUS_IN_REVIEW,
12927
- Done: vars.STATUS_DONE
12928
- } : void 0;
12929
- const priorityOptions = vars.PRIORITY_URGENT || vars.PRIORITY_HIGH || vars.PRIORITY_MEDIUM || vars.PRIORITY_LOW ? {
12930
- Urgent: vars.PRIORITY_URGENT,
12931
- High: vars.PRIORITY_HIGH,
12932
- Medium: vars.PRIORITY_MEDIUM,
12933
- Low: vars.PRIORITY_LOW
12934
- } : void 0;
12935
- const payload = {
12936
- slug,
12937
- // Identity. name/division default off the repo name when the skill didn't pass them.
12938
- name: vars.NAME || parsedRepo.name,
12939
- division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
12940
- repos: [`${parsedRepo.owner}/${slug}`],
12941
- wikiRepo: vars.WIKI_REPO || parsedRepo.fullName,
12942
- branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
12943
- class: cls,
12944
- projectType,
12945
- deployModel,
12946
- // #1359: always persist an explicit track so release tooling never guesses from absence alone.
12947
- releaseTrack: resolveBootstrapReleaseTrack(cls, options.releaseTrack),
12948
- // Board coords (from GraphQL at bootstrap, passed as --var by the skill).
12949
- projectOwner: vars.PROJECT_OWNER || void 0,
12950
- projectNumber: num(vars.PROJECT_NUMBER),
12951
- projectId: vars.PROJECT_ID || void 0,
12952
- statusFieldId: vars.STATUS_FIELD_ID || void 0,
12953
- statusOptions,
12954
- priorityFieldId: vars.PRIORITY_FIELD_ID || void 0,
12955
- priorityOptions,
12956
- // Pointers. vaultPath is explicit + canonical; kbPointer is the per-project KB doc path.
12957
- vaultPath: `/mmi-future/${slug}`,
12958
- kbPointer: `kb/projects/${slug}.md`
12959
- };
12960
- for (const k of Object.keys(payload)) if (payload[k] === void 0) delete payload[k];
12961
- if (payload.projectId && payload.projectNumber == null) {
12962
- throw new Error(
12963
- "bootstrap apply: PROJECT_ID is set but PROJECT_NUMBER is missing \u2014 pass --var PROJECT_NUMBER=<N> or ensure the live board GraphQL query succeeds"
12964
- );
12965
- }
12966
- return payload;
12967
- }
12968
- var BOARD_FIELD_VAR_MAP = {
12969
- Status: {
12970
- fieldIdVar: "STATUS_FIELD_ID",
12971
- options: { Todo: "STATUS_TODO", "In Progress": "STATUS_IN_PROGRESS", "In Review": "STATUS_IN_REVIEW", Done: "STATUS_DONE" }
12972
- },
12973
- Priority: {
12974
- fieldIdVar: "PRIORITY_FIELD_ID",
12975
- options: { Urgent: "PRIORITY_URGENT", High: "PRIORITY_HIGH", Medium: "PRIORITY_MEDIUM", Low: "PRIORITY_LOW" }
12976
- }
12977
- };
12978
- function projectV2BoardNode(fieldsJson) {
12979
- if (Array.isArray(fieldsJson)) return { fields: { nodes: fieldsJson } };
12980
- const wrapped = fieldsJson;
12981
- return wrapped?.data?.node ?? wrapped?.node;
12982
- }
12983
- function extractBoardFieldVars(fieldsJson) {
12984
- const out = {};
12985
- const projectNode = projectV2BoardNode(fieldsJson);
12986
- if (typeof projectNode?.number === "number" && Number.isFinite(projectNode.number)) {
12987
- out.PROJECT_NUMBER = String(projectNode.number);
14096
+ const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
14097
+ const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
14098
+ checks.push({
14099
+ ok: missing.length === 0,
14100
+ label: "Hub required status checks configured",
14101
+ detail: optionDetail(missing)
14102
+ });
14103
+ } else if (repoClass === "deployable") {
14104
+ const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
14105
+ const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
14106
+ checks.push({
14107
+ ok: missing.length === 0,
14108
+ label: "product required status checks configured",
14109
+ detail: missing.length ? `missing contexts: ${missing.join(", ")} \u2014 apply ${requiredProductRulesetRef} as an active repo ruleset` : void 0
14110
+ });
12988
14111
  }
12989
- const nodes = projectNode?.fields?.nodes;
12990
- if (!Array.isArray(nodes)) return out;
12991
- for (const node of nodes) {
12992
- const field = node;
12993
- const name = typeof field.name === "string" ? field.name : void 0;
12994
- const map = name ? BOARD_FIELD_VAR_MAP[name] : void 0;
12995
- if (!map || typeof field.id !== "string" || !field.id) continue;
12996
- out[map.fieldIdVar] = field.id;
12997
- const options = Array.isArray(field.options) ? field.options : [];
12998
- for (const opt of options) {
12999
- const o = opt;
13000
- const varName = typeof o.name === "string" ? map.options[o.name] : void 0;
13001
- if (varName && typeof o.id === "string" && o.id) out[varName] = o.id;
14112
+ const declaredApis = (deps.requiredGcpApis ?? []).filter((a) => a && a.trim());
14113
+ if (declaredApis.length > 0) {
14114
+ const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
14115
+ const gcpProject = gcpProjectForSlug(slug);
14116
+ const enabled = deps.listEnabledGcpApis ? await deps.listEnabledGcpApis(gcpProject) : null;
14117
+ if (enabled == null) {
14118
+ checks.push({
14119
+ ok: false,
14120
+ label: `required GCP APIs enabled in ${gcpProject}`,
14121
+ detail: `could not list enabled APIs (gcloud unavailable, not authed, or no project ${gcpProject})`
14122
+ });
14123
+ } else {
14124
+ const missing = findMissingGcpApis(declaredApis, enabled);
14125
+ checks.push({
14126
+ ok: missing.length === 0,
14127
+ label: `required GCP APIs enabled in ${gcpProject}`,
14128
+ detail: missing.length ? `disabled: ${missing.join(", ")}` : void 0
14129
+ });
13002
14130
  }
13003
14131
  }
13004
- return out;
13005
- }
13006
- function boardFieldsQueryArgs(projectId) {
13007
- const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
13008
- return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
13009
- }
13010
- function serializeRegistry(obj) {
13011
- return `${JSON.stringify(obj, null, 2)}
13012
- `;
14132
+ const waived = applyWaivers(checks, config?.verifyWaivers ?? []);
14133
+ return { ok: waived.every((c) => c.ok || c.waived), repo, class: repoClass, baseBranch, checks: waived };
13013
14134
  }
13014
- function planFanoutRegistration(fanoutTargetsRaw, projectsRaw, entry) {
13015
- const fanout = JSON.parse(fanoutTargetsRaw);
13016
- const projects = JSON.parse(projectsRaw);
13017
- const fanoutRepos = Array.isArray(fanout.repos) ? fanout.repos : [];
13018
- const projectEntries = Array.isArray(projects.projects) ? projects.projects : [];
13019
- const name = entry.name ?? entry.repo;
13020
- const canonName = entry.repo.toLowerCase();
13021
- const inFanout = fanoutRepos.some((r) => typeof r.repo === "string" && r.repo.toLowerCase() === canonName);
13022
- const inProjects = projectEntries.some(
13023
- (p) => p.slug === entry.slug || Array.isArray(p.repos) && p.repos.some((full) => String(full).split("/").pop()?.toLowerCase() === canonName)
13024
- );
13025
- if (inFanout || inProjects) {
13026
- return { changed: false, fanoutTargets: fanoutTargetsRaw, projects: projectsRaw };
13027
- }
13028
- const projectEntry = {
13029
- name,
13030
- slug: entry.slug,
13031
- projectId: entry.projectId,
13032
- wikiRepo: entry.wikiRepo,
13033
- repos: [`mutmutco/${entry.repo}`]
13034
- };
13035
- if (entry.cls === "content") projectEntry.branch = "main";
13036
- for (const k of Object.keys(projectEntry)) if (projectEntry[k] === void 0) delete projectEntry[k];
13037
- const nextProjects = { ...projects, projects: [...projectEntries, projectEntry] };
13038
- const fanoutEntry = { repo: entry.repo, branch: entry.branch, class: entry.cls };
13039
- const nextFanout = { ...fanout, repos: [...fanoutRepos, fanoutEntry] };
13040
- return {
13041
- changed: true,
13042
- fanoutTargets: serializeRegistry(nextFanout),
13043
- projects: serializeRegistry(nextProjects)
13044
- };
14135
+ function applyWaivers(checks, waivers) {
14136
+ if (!waivers?.length) return checks;
14137
+ const set = new Set(waivers);
14138
+ return checks.map((c) => !c.ok && set.has(c.label) ? { ...c, waived: true } : c);
13045
14139
  }
13046
- function decideFanoutPrAction(openPrs) {
13047
- const list = Array.isArray(openPrs) ? openPrs : [];
13048
- for (const pr2 of list) {
13049
- const url = pr2?.url;
13050
- if (typeof url === "string" && url.trim()) return { action: "reuse", url: url.trim() };
14140
+ function renderBootstrapVerifyReport(report) {
14141
+ const lines = [`mmi-cli bootstrap verify: ${report.ok ? "OK" : "CHECK"} ${report.repo} (${report.class}, ${report.baseBranch})`];
14142
+ for (const check of report.checks) {
14143
+ const status = check.ok ? "OK" : check.waived ? "WAIVE" : "FAIL";
14144
+ lines.push(`${status} ${check.label}${check.detail ? ` - ${check.detail}` : ""}`);
13051
14145
  }
13052
- return { action: "create" };
13053
- }
13054
- function contentPutArgs(repo, path2, content, branch, sha) {
13055
- const args = [
13056
- "api",
13057
- "-X",
13058
- "PUT",
13059
- `repos/${repo}/contents/${path2.split("/").map(encodeURIComponent).join("/")}`,
13060
- "-f",
13061
- `message=bootstrap: seed ${path2}`,
13062
- "-f",
13063
- `content=${Buffer.from(content, "utf8").toString("base64")}`,
13064
- "-f",
13065
- `branch=${branch}`
13066
- ];
13067
- if (sha) args.push("-f", `sha=${sha}`);
13068
- return args;
14146
+ return lines.join("\n");
13069
14147
  }
13070
14148
 
13071
14149
  // ../infra/registry-endpoints.mjs
@@ -13244,6 +14322,12 @@ function renderVerifySecrets(body) {
13244
14322
  }
13245
14323
 
13246
14324
  // src/project-readiness.ts
14325
+ function stagesForTrack(meta) {
14326
+ return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
14327
+ }
14328
+ function stageInTrack(meta, stage2) {
14329
+ return stagesForTrack(meta).includes(stage2);
14330
+ }
13247
14331
  function dnsErrorToResolution(code) {
13248
14332
  return code === "ENOTFOUND" || code === "EAI_NONAME" ? false : void 0;
13249
14333
  }
@@ -13551,15 +14635,15 @@ async function buildV2Doctor(repoOrSlug, deps) {
13551
14635
  secretsError = e?.message || "secrets list failed";
13552
14636
  }
13553
14637
  const deployCoords = Object.fromEntries(await Promise.all(STAGES.map(async (stage2) => {
13554
- const required = projectRequiresDeployCoords(model, stage2, meta);
14638
+ const required = stageInTrack(meta, stage2) && projectRequiresDeployCoords(model, stage2, meta);
13555
14639
  return [stage2, { required, ok: required ? await deps.hasDeployCoords(slug, stage2) : true }];
13556
14640
  })));
13557
14641
  const deployState = Object.fromEntries(await Promise.all(STAGES.map(async (stage2) => {
13558
- const required = projectRequiresDeployState(model, stage2);
14642
+ const required = stageInTrack(meta, stage2) && projectRequiresDeployState(model, stage2);
13559
14643
  return [stage2, { required, ok: required ? await deps.hasDeployState(slug, stage2) : true }];
13560
14644
  })));
13561
14645
  const secrets2 = Object.fromEntries(STAGES.map((stage2) => {
13562
- const required = stageRequiredSecrets(stage2, meta).map((key) => stageKey2(stage2, key));
14646
+ const required = stageInTrack(meta, stage2) ? stageRequiredSecrets(stage2, meta).map((key) => stageKey2(stage2, key)) : [];
13563
14647
  const present = required.filter((key) => presentSecrets.has(key));
13564
14648
  const missing = required.filter((key) => !presentSecrets.has(key));
13565
14649
  return [stage2, { required, present, missing }];
@@ -13624,7 +14708,7 @@ ${section}`.trim();
13624
14708
  }
13625
14709
 
13626
14710
  // src/project-set.ts
13627
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired"];
14711
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "ci", "requiredChecks", "gate"];
13628
14712
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
13629
14713
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
13630
14714
  function parseRuntimeSecretsVar(raw) {
@@ -13783,6 +14867,43 @@ function parsePublishRequiredVar(raw) {
13783
14867
  if (raw === "false") return false;
13784
14868
  throw new Error("project set: publishRequired must be true or false");
13785
14869
  }
14870
+ function parseRequiredChecksVar(raw) {
14871
+ let parsed;
14872
+ try {
14873
+ parsed = JSON.parse(raw);
14874
+ } catch {
14875
+ throw new Error('project set: requiredChecks must be a JSON array, e.g. ["gate"] or [] for an intentional no-ci repo');
14876
+ }
14877
+ if (!Array.isArray(parsed) || parsed.some((c) => typeof c !== "string" || !c.trim())) {
14878
+ throw new Error("project set: requiredChecks must be a JSON array of non-empty check-context strings (use [] for none)");
14879
+ }
14880
+ return parsed.map((c) => c.trim());
14881
+ }
14882
+ function parseGateVar(raw) {
14883
+ let parsed;
14884
+ try {
14885
+ parsed = JSON.parse(raw);
14886
+ } catch {
14887
+ throw new Error('project set: gate must be JSON, e.g. {"runtime":"python","cmd":"pytest","workdir":"app"}');
14888
+ }
14889
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
14890
+ throw new Error("project set: gate must be a {runtime,cmd,workdir,cacheDepPath,pyVersion} object");
14891
+ }
14892
+ const map = parsed;
14893
+ const out = {};
14894
+ for (const [key, value] of Object.entries(map)) {
14895
+ if (key === "runtime") {
14896
+ if (value !== "node" && value !== "python") throw new Error('project set: gate.runtime must be "node" or "python"');
14897
+ out.runtime = value;
14898
+ } else if (key === "cmd" || key === "workdir" || key === "cacheDepPath" || key === "pyVersion") {
14899
+ if (typeof value !== "string" || !value.trim()) throw new Error(`project set: gate.${key} must be a non-empty string`);
14900
+ out[key] = value.trim();
14901
+ } else {
14902
+ throw new Error(`project set: gate key "${key}" \u2014 expected only runtime/cmd/workdir/cacheDepPath/pyVersion`);
14903
+ }
14904
+ }
14905
+ return out;
14906
+ }
13786
14907
  var SETTABLE_VAR_KEYS = [
13787
14908
  "name",
13788
14909
  "division",
@@ -13803,7 +14924,10 @@ var SETTABLE_VAR_KEYS = [
13803
14924
  "statusOptions",
13804
14925
  "priorityFieldId",
13805
14926
  "priorityOptions",
13806
- "portRange"
14927
+ "portRange",
14928
+ "ci",
14929
+ "requiredChecks",
14930
+ "gate"
13807
14931
  ];
13808
14932
  var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
13809
14933
  var SETTABLE_VAR_HINTS = {
@@ -13816,7 +14940,10 @@ var SETTABLE_VAR_HINTS = {
13816
14940
  edgeDomains: "JSON {dev,rc,main} domain map",
13817
14941
  statusOptions: "JSON name\u2192id map",
13818
14942
  priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
13819
- portRange: "JSON {start,end} or [start,end]"
14943
+ portRange: "JSON {start,end} or [start,end]",
14944
+ ci: "none \u2014 declare intentional no-ci",
14945
+ requiredChecks: 'JSON array, e.g. ["gate"] or [] for no-ci',
14946
+ gate: "JSON {runtime,cmd,workdir,cacheDepPath,pyVersion}"
13820
14947
  };
13821
14948
  function settableVarHelp() {
13822
14949
  const keys = SETTABLE_VAR_KEYS.map((k) => {
@@ -13879,6 +15006,13 @@ function buildProjectSetPatch(input) {
13879
15006
  patch[key] = parseReposVar(raw);
13880
15007
  } else if (key === "publishRequired") {
13881
15008
  patch[key] = parsePublishRequiredVar(raw);
15009
+ } else if (key === "ci") {
15010
+ if (raw !== "none") throw new Error('project set: ci must be "none" (or use --unset ci to require checks)');
15011
+ patch[key] = raw;
15012
+ } else if (key === "requiredChecks") {
15013
+ patch[key] = parseRequiredChecksVar(raw);
15014
+ } else if (key === "gate") {
15015
+ patch[key] = parseGateVar(raw);
13882
15016
  } else {
13883
15017
  patch[key] = raw;
13884
15018
  }
@@ -13919,8 +15053,8 @@ function resolveKbSource(rawBase) {
13919
15053
  return { owner: m[1], repo: m[2], ref: m[3] };
13920
15054
  }
13921
15055
  function buildKbGetArgs(src, path2) {
13922
- const clean3 = path2.replace(/^\/+/, "");
13923
- return ["api", `repos/${src.owner}/${src.repo}/contents/${clean3}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
15056
+ const clean4 = path2.replace(/^\/+/, "");
15057
+ return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
13924
15058
  }
13925
15059
  function buildKbTreeArgs(src) {
13926
15060
  return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
@@ -13937,10 +15071,10 @@ function parseKbTree(stdout, prefix) {
13937
15071
  }
13938
15072
 
13939
15073
  // src/plan.ts
13940
- var import_node_path13 = require("node:path");
15074
+ var import_node_path16 = require("node:path");
13941
15075
  var PLANS_DIR = "plans";
13942
- var META_FILE = (0, import_node_path13.join)(PLANS_DIR, ".plan-meta.json");
13943
- var planPath = (slug) => (0, import_node_path13.join)(PLANS_DIR, `${slug}.md`);
15076
+ var META_FILE = (0, import_node_path16.join)(PLANS_DIR, ".plan-meta.json");
15077
+ var planPath = (slug) => (0, import_node_path16.join)(PLANS_DIR, `${slug}.md`);
13944
15078
  var metaKey = (project2, slug) => `${project2}/${slug}`;
13945
15079
  function parseMeta(raw) {
13946
15080
  if (!raw) return {};
@@ -13965,7 +15099,7 @@ function hashContent(s) {
13965
15099
  function staleHint(slug) {
13966
15100
  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`;
13967
15101
  }
13968
- var INDEX_FILE = (0, import_node_path13.join)(PLANS_DIR, ".index.json");
15102
+ var INDEX_FILE = (0, import_node_path16.join)(PLANS_DIR, ".index.json");
13969
15103
  var INDEX_TTL_MS = 6e4;
13970
15104
  function parseIndex(raw) {
13971
15105
  if (!raw) return null;
@@ -13994,7 +15128,7 @@ function mergeIndex(idx, scope, plans, now) {
13994
15128
  const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
13995
15129
  return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
13996
15130
  }
13997
- var QUEUE_FILE = (0, import_node_path13.join)(PLANS_DIR, ".sync-queue.json");
15131
+ var QUEUE_FILE = (0, import_node_path16.join)(PLANS_DIR, ".sync-queue.json");
13998
15132
  var QUEUE_MAX_ATTEMPTS = 10;
13999
15133
  function isValidQueueEntry(e) {
14000
15134
  if (!e || typeof e !== "object") return false;
@@ -14163,6 +15297,16 @@ async function planPull(deps, slug, opts = {}) {
14163
15297
  deps.err(`local ${planPath(slug)} has unpushed edits \u2014 push it, or pull with --force to overwrite`);
14164
15298
  return false;
14165
15299
  }
15300
+ if (local != null && !opts.force) {
15301
+ const queued = parseQueue(deps.readQueueRaw()).find((e) => e.project === project2 && e.slug === slug);
15302
+ if (queued) {
15303
+ const state = queued.conflict ?? queued.deadLettered ?? "still pending";
15304
+ deps.err(
15305
+ `local ${planPath(slug)} has an unpushed edit queued (${state}) \u2014 land it with \`mmi-cli northstar push ${slug} --wait\`, or pull with --force to discard your local edit`
15306
+ );
15307
+ return false;
15308
+ }
15309
+ }
14166
15310
  const qs = new URLSearchParams({ project: project2, slug }).toString();
14167
15311
  const res = await deps.fetch(`${deps.apiUrl}/plan/get?${qs}`, {
14168
15312
  method: "GET",
@@ -14372,6 +15516,7 @@ async function planSync(deps, opts = {}) {
14372
15516
  } catch (e) {
14373
15517
  if (!opts.quiet) deps.err(`northstar sync: list refresh failed: ${e.message}`);
14374
15518
  }
15519
+ return kept;
14375
15520
  }
14376
15521
  async function planStatus(deps, opts = {}) {
14377
15522
  const queue = parseQueue(deps.readQueueRaw());
@@ -14443,11 +15588,11 @@ async function planGraduate(deps, slug, opts = {}) {
14443
15588
  }
14444
15589
 
14445
15590
  // src/atomic-write.ts
14446
- var import_node_fs15 = require("node:fs");
15591
+ var import_node_fs18 = require("node:fs");
14447
15592
  function atomicWriteFileSync(path2, content) {
14448
15593
  const tmp = `${path2}.${process.pid}.tmp`;
14449
- (0, import_node_fs15.writeFileSync)(tmp, content, "utf8");
14450
- (0, import_node_fs15.renameSync)(tmp, path2);
15594
+ (0, import_node_fs18.writeFileSync)(tmp, content, "utf8");
15595
+ (0, import_node_fs18.renameSync)(tmp, path2);
14451
15596
  }
14452
15597
 
14453
15598
  // src/oauth.ts
@@ -14678,7 +15823,7 @@ async function fetchHubVersionInfo(baseUrl) {
14678
15823
  }
14679
15824
  function readRepoVersion() {
14680
15825
  try {
14681
- return JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
15826
+ return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
14682
15827
  } catch {
14683
15828
  return void 0;
14684
15829
  }
@@ -14770,7 +15915,7 @@ async function applyCodexPluginHeal(surface, log) {
14770
15915
  return true;
14771
15916
  }
14772
15917
  var program2 = new Command();
14773
- program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion());
15918
+ program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
14774
15919
  async function runRulesSync(opts, io = consoleIo) {
14775
15920
  const cfg = await loadConfig();
14776
15921
  if (isRulesSource(cfg.orgRulesSource)) {
@@ -14804,11 +15949,11 @@ async function runRulesSync(opts, io = consoleIo) {
14804
15949
  for (const entry of fetched) {
14805
15950
  if ("error" in entry) continue;
14806
15951
  const { file, source } = entry;
14807
- const current = (0, import_node_fs16.existsSync)(file) ? await (0, import_promises5.readFile)(file, "utf8") : null;
15952
+ const current = (0, import_node_fs19.existsSync)(file) ? await (0, import_promises6.readFile)(file, "utf8") : null;
14808
15953
  if (needsUpdate(source, current)) {
14809
15954
  const slash = file.lastIndexOf("/");
14810
- if (slash > 0) (0, import_node_fs16.mkdirSync)(file.slice(0, slash), { recursive: true });
14811
- await (0, import_promises5.writeFile)(file, normalizeEol(source), "utf8");
15955
+ if (slash > 0) (0, import_node_fs19.mkdirSync)(file.slice(0, slash), { recursive: true });
15956
+ await (0, import_promises6.writeFile)(file, normalizeEol(source), "utf8");
14812
15957
  changed++;
14813
15958
  if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
14814
15959
  }
@@ -14833,9 +15978,9 @@ async function runDocsSync(opts, io = consoleIo) {
14833
15978
  return null;
14834
15979
  }
14835
15980
  },
14836
- localContent: async (f) => (0, import_node_fs16.existsSync)(f) ? await (0, import_promises5.readFile)(f, "utf8") : null,
15981
+ localContent: async (f) => (0, import_node_fs19.existsSync)(f) ? await (0, import_promises6.readFile)(f, "utf8") : null,
14837
15982
  writeDoc: async (f, c) => {
14838
- await (0, import_promises5.writeFile)(f, c, "utf8");
15983
+ await (0, import_promises6.writeFile)(f, c, "utf8");
14839
15984
  }
14840
15985
  });
14841
15986
  for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
@@ -14845,7 +15990,13 @@ async function runDocsSync(opts, io = consoleIo) {
14845
15990
  var docs = program2.command("docs").description("repo-owned authoritative docs");
14846
15991
  docs.command("sync").option("--quiet", "stay silent unless something changed or errored").description("refresh README.md / architecture.md from the repo default branch (keeper-authored); never clobbers uncommitted edits").action((opts) => runDocsSync(opts));
14847
15992
  registerSagaCommands(program2);
15993
+ registerHandoffCommands(program2);
14848
15994
  registerHonchoCommands(program2);
15995
+ registerScroogeCommands(program2);
15996
+ program2.command("commands").description("print the command manifest \u2014 every subcommand + its flags (ground against this instead of guessing)").option("--json", "machine-readable JSON: { name, version, tree, index } \u2014 index is a flat list of every leaf command path").action((o) => {
15997
+ const manifest = buildCommandManifest(program2);
15998
+ consoleIo.log(o.json ? JSON.stringify(manifest, null, 2) : formatManifestHuman(manifest));
15999
+ });
14849
16000
  async function runWhoami(io = consoleIo) {
14850
16001
  const cfg = await loadConfig();
14851
16002
  const report = await resolveWhoami({
@@ -14858,8 +16009,18 @@ async function runWhoami(io = consoleIo) {
14858
16009
  program2.command("whoami").description('resolve the logged-in human: {login, source, sessionExpiresAt} JSON; source "unknown" (exit 0) when neither the Hub session nor gh can name them').option("--json", "machine-readable output (default)").action(async () => {
14859
16010
  await runWhoami();
14860
16011
  });
14861
- program2.command("gc").description("dry-run cleanup for merged/closed PR branches and stale tracking refs").option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR branches and stale tracking refs").option("--json", "machine-readable output").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
16012
+ program2.command("gc").description("dry-run cleanup for merged/closed PR branches and stale tracking refs (--scratch: stale local saga/plan/honcho files)").option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR branches and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune stale local saga/plan/honcho scratch already recorded remotely (#1474) instead of git branches/refs").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
14862
16013
  if (o.apply && o.dryRun) return fail("gc: choose either --dry-run or --apply");
16014
+ if (o.scratch) {
16015
+ try {
16016
+ const root = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
16017
+ const run = executeScratchGc(root, { apply: Boolean(o.apply) });
16018
+ if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, ...run }, null, 2));
16019
+ return console.log(formatScratchGcPlan(run.plan, Boolean(o.apply)));
16020
+ } catch (e) {
16021
+ return fail(`gc --scratch: ${e.message}`);
16022
+ }
16023
+ }
14863
16024
  const limit = Number.parseInt(o.limit, 10);
14864
16025
  if (!Number.isFinite(limit) || limit < 1) return fail("gc: --limit must be a positive integer");
14865
16026
  try {
@@ -14887,6 +16048,120 @@ ${renderGcApplyResult(applyResult)}`);
14887
16048
  fail(`gc: ${e.message}`);
14888
16049
  }
14889
16050
  });
16051
+ var NPM_PROVISION_TIMEOUT_MS = 3e5;
16052
+ var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
16053
+ function runWorktreeInstall(command, cwd, quiet) {
16054
+ const [bin, ...args] = command.split(" ");
16055
+ const file = isWin ? "cmd.exe" : bin;
16056
+ const spawnArgs = isWin ? ["/c", bin, ...args] : args;
16057
+ return new Promise((resolve, reject) => {
16058
+ const child = (0, import_node_child_process10.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
16059
+ const timer = setTimeout(() => {
16060
+ try {
16061
+ child.kill();
16062
+ } catch {
16063
+ }
16064
+ reject(new Error(`${command} timed out after ${NPM_PROVISION_TIMEOUT_MS}ms in ${cwd}`));
16065
+ }, NPM_PROVISION_TIMEOUT_MS);
16066
+ child.on("error", (e) => {
16067
+ clearTimeout(timer);
16068
+ reject(e);
16069
+ });
16070
+ child.on("exit", (code) => {
16071
+ clearTimeout(timer);
16072
+ if (code === 0) resolve();
16073
+ else reject(new Error(`${command} exited ${code} in ${cwd}`));
16074
+ });
16075
+ });
16076
+ }
16077
+ async function primaryCheckoutRoot(worktreeRoot) {
16078
+ try {
16079
+ const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
16080
+ return out ? (0, import_node_path17.dirname)(out) : void 0;
16081
+ } catch {
16082
+ return void 0;
16083
+ }
16084
+ }
16085
+ function makeProvisionDeps(worktreeRoot, quiet, log) {
16086
+ return {
16087
+ runInstall: (command, cwd) => runWorktreeInstall(command, cwd, quiet),
16088
+ primaryCheckout: () => primaryCheckoutRoot(worktreeRoot),
16089
+ log
16090
+ };
16091
+ }
16092
+ function acquireWorktreeSetupLock(worktreeRoot) {
16093
+ const lockPath = (0, import_node_path17.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
16094
+ const take = () => {
16095
+ const fd = (0, import_node_fs19.openSync)(lockPath, "wx");
16096
+ try {
16097
+ (0, import_node_fs19.writeSync)(fd, String(Date.now()));
16098
+ } finally {
16099
+ (0, import_node_fs19.closeSync)(fd);
16100
+ }
16101
+ return () => {
16102
+ try {
16103
+ (0, import_node_fs19.rmSync)(lockPath, { force: true });
16104
+ } catch {
16105
+ }
16106
+ };
16107
+ };
16108
+ try {
16109
+ (0, import_node_fs19.mkdirSync)((0, import_node_path17.dirname)(lockPath), { recursive: true });
16110
+ return take();
16111
+ } catch {
16112
+ try {
16113
+ if (Date.now() - (0, import_node_fs19.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
16114
+ (0, import_node_fs19.rmSync)(lockPath, { force: true });
16115
+ return take();
16116
+ }
16117
+ } catch {
16118
+ }
16119
+ return null;
16120
+ }
16121
+ }
16122
+ var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
16123
+ worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch the base from", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
16124
+ try {
16125
+ const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
16126
+ const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
16127
+ const baseBranch = o.from.startsWith(`${o.remote}/`) ? o.from.slice(o.remote.length + 1) : void 0;
16128
+ if (baseBranch) await execFileP2("git", ["fetch", o.remote, baseBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
16129
+ await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, o.from], { timeout: GH_MUTATION_TIMEOUT_MS });
16130
+ const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
16131
+ if (!o.json) console.error(` ${m}`);
16132
+ }));
16133
+ if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base: o.from, ...report }, null, 2));
16134
+ console.log(`worktree ready: ${wtPath} (branch ${branch} from ${o.from})`);
16135
+ console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
16136
+ console.log(` copied: ${report.copied.join(", ") || "none"}`);
16137
+ } catch (e) {
16138
+ fail(`worktree create: ${e.message}`);
16139
+ }
16140
+ });
16141
+ worktree.command("setup [path]").description("provision an existing worktree (install missing deps + copy local-only config); fired automatically by SessionStart").option("--quiet", "no output on success (for the detached SessionStart auto-fire worker)").option("--json", "machine-readable output").action(async (path2, o) => {
16142
+ const root = path2 ?? process.cwd();
16143
+ const release = acquireWorktreeSetupLock(root);
16144
+ if (!release) {
16145
+ if (!o.quiet && !o.json) console.log("worktree setup: another provision is in progress \u2014 skipping");
16146
+ return;
16147
+ }
16148
+ try {
16149
+ const report = await provisionWorktree(root, makeProvisionDeps(root, Boolean(o.quiet), (m) => {
16150
+ if (!o.quiet && !o.json) console.error(` ${m}`);
16151
+ }));
16152
+ if (o.json) return console.log(JSON.stringify({ path: root, ...report }, null, 2));
16153
+ if (!o.quiet) {
16154
+ console.log(`worktree provisioned: ${root}`);
16155
+ console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
16156
+ console.log(` copied: ${report.copied.join(", ") || "none"}`);
16157
+ }
16158
+ } catch (e) {
16159
+ if (o.quiet) return void console.error(`[worktree-setup] ${e.message}`);
16160
+ fail(`worktree setup: ${e.message}`);
16161
+ } finally {
16162
+ release();
16163
+ }
16164
+ });
14890
16165
  var kb = program2.command("kb").description("org knowledgebase (read-only)");
14891
16166
  kb.command("get <path>").description("print a KB document by path").action(async (path2) => {
14892
16167
  const src = resolveKbSource((await loadConfig()).kbSource);
@@ -15006,7 +16281,7 @@ function detachPlanSync() {
15006
16281
  }
15007
16282
  }
15008
16283
  function makePlanDeps(cfg, io = consoleIo) {
15009
- const ensureDir = () => (0, import_node_fs16.mkdirSync)(PLANS_DIR, { recursive: true });
16284
+ const ensureDir = () => (0, import_node_fs19.mkdirSync)(PLANS_DIR, { recursive: true });
15010
16285
  return {
15011
16286
  apiUrl: cfg.sagaApiUrl,
15012
16287
  fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
@@ -15014,24 +16289,24 @@ function makePlanDeps(cfg, io = consoleIo) {
15014
16289
  project: async () => (await sagaKey(cfg)).project,
15015
16290
  readLocal: (slug) => {
15016
16291
  try {
15017
- return (0, import_node_fs16.readFileSync)(planPath(slug), "utf8");
16292
+ return (0, import_node_fs19.readFileSync)(planPath(slug), "utf8");
15018
16293
  } catch {
15019
16294
  return null;
15020
16295
  }
15021
16296
  },
15022
16297
  writeLocal: (slug, content) => {
15023
16298
  ensureDir();
15024
- (0, import_node_fs16.writeFileSync)(planPath(slug), content, "utf8");
16299
+ (0, import_node_fs19.writeFileSync)(planPath(slug), content, "utf8");
15025
16300
  },
15026
16301
  removeLocal: (slug) => {
15027
16302
  try {
15028
- (0, import_node_fs16.rmSync)(planPath(slug));
16303
+ (0, import_node_fs19.rmSync)(planPath(slug));
15029
16304
  } catch {
15030
16305
  }
15031
16306
  },
15032
16307
  readMetaRaw: () => {
15033
16308
  try {
15034
- return (0, import_node_fs16.readFileSync)(META_FILE, "utf8");
16309
+ return (0, import_node_fs19.readFileSync)(META_FILE, "utf8");
15035
16310
  } catch {
15036
16311
  return null;
15037
16312
  }
@@ -15042,7 +16317,7 @@ function makePlanDeps(cfg, io = consoleIo) {
15042
16317
  },
15043
16318
  readIndexRaw: () => {
15044
16319
  try {
15045
- return (0, import_node_fs16.readFileSync)(INDEX_FILE, "utf8");
16320
+ return (0, import_node_fs19.readFileSync)(INDEX_FILE, "utf8");
15046
16321
  } catch {
15047
16322
  return null;
15048
16323
  }
@@ -15053,7 +16328,7 @@ function makePlanDeps(cfg, io = consoleIo) {
15053
16328
  },
15054
16329
  readQueueRaw: () => {
15055
16330
  try {
15056
- return (0, import_node_fs16.readFileSync)(QUEUE_FILE, "utf8");
16331
+ return (0, import_node_fs19.readFileSync)(QUEUE_FILE, "utf8");
15057
16332
  } catch {
15058
16333
  return null;
15059
16334
  }
@@ -15113,7 +16388,7 @@ function registerNorthStarCommands(cmd) {
15113
16388
  let content;
15114
16389
  if (o.bodyFile) {
15115
16390
  try {
15116
- content = await resolveTextArg({ file: o.bodyFile }, { readFile: import_promises5.readFile, readStdin }, {
16391
+ content = await resolveTextArg({ file: o.bodyFile }, { readFile: import_promises6.readFile, readStdin }, {
15117
16392
  value: "inline content",
15118
16393
  file: "--body-file",
15119
16394
  noun: "plan"
@@ -15142,7 +16417,16 @@ function registerNorthStarCommands(cmd) {
15142
16417
  const signals = await gatherRelevanceSignals();
15143
16418
  await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0, fresh: o.fresh, json: o.json });
15144
16419
  }));
15145
- cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").action((o) => withPlan(o.quiet ?? false, (d) => planSync(d, o)));
16420
+ cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").option("--wait", "durable confirmation: drain, then report unresolved pushes and exit non-zero if any remain").action((o) => withPlan(o.quiet ?? false, async (d) => {
16421
+ const unresolved = await planSync(d, o);
16422
+ if (!o.wait) return;
16423
+ if (unresolved.length) {
16424
+ for (const e of unresolved) d.err(`${e.slug}: ${e.conflict ?? e.deadLettered ?? "still pending"}`);
16425
+ process.exitCode = 1;
16426
+ } else if (!o.quiet) {
16427
+ d.log("north star: all queued pushes landed");
16428
+ }
16429
+ }));
15146
16430
  cmd.command("status").description("show pending/conflicted background pushes and the plan-cache age").option("--json", "machine-readable output").action((o) => withPlan(false, (d) => planStatus(d, o)));
15147
16431
  cmd.command("reconcile").description("refresh stale local etags from the server without --force (recovers from an object-store re-stamp)").action(() => withPlan(false, (d) => planReconcile(d)));
15148
16432
  cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
@@ -15425,6 +16709,17 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
15425
16709
  return failGraceful(`project get: no registry META for ${target} (unknown or unbootstrapped)`);
15426
16710
  }
15427
16711
  console.log(JSON.stringify(read.project));
16712
+ if (!o.json) {
16713
+ const m = read.project;
16714
+ const track = resolveReleaseTrack(m);
16715
+ const stages = branchesForTrack(track).join(" -> ");
16716
+ const note = track === "direct" ? " (direct \u2014 no rc; /rcand refuses, /release ships development -> main)" : track === "trunk" ? " (trunk \u2014 main only)" : "";
16717
+ console.error(
16718
+ `${m.name ?? target} \xB7 class ${m.class ?? "?"} \xB7 deploy ${m.deployModel ?? "?"}
16719
+ release track: ${track} \u2014 stages: ${stages}${note}
16720
+ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files. Deploy coords are OIDC-gated \u2014 see \`project resolve\`.`
16721
+ );
16722
+ }
15428
16723
  });
15429
16724
  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) => {
15430
16725
  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.";
@@ -15622,14 +16917,15 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
15622
16917
  if (mismatch) process.exitCode = 1;
15623
16918
  });
15624
16919
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
15625
- issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").requiredOption("--type <type>", "bug | feature | task (sets the matching label)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").requiredOption("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment").action(async (o) => {
16920
+ issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").requiredOption("--type <type>", "bug | feature | task (sets the matching label)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment").action(async (o) => {
15626
16921
  let args;
15627
16922
  let priority;
15628
16923
  let body;
15629
16924
  let title;
15630
16925
  try {
15631
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
15632
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
16926
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
16927
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
16928
+ if (o.priority === void 0) throw new Error("missing --priority <priority> \u2014 expected one of: urgent, high, medium, low");
15633
16929
  priority = normalizePriority(o.priority);
15634
16930
  args = buildIssueArgs({ type: o.type, title, body, priority, repo: o.repo, labels: o.label });
15635
16931
  if (o.parent !== void 0) parseIssueRef(o.parent);
@@ -15715,8 +17011,8 @@ program2.command("report").description("file a friction report on the Hub board
15715
17011
  const targetRepo2 = o.repo ?? HUB_REPO2;
15716
17012
  const sourceRepo = await resolveRepo(void 0);
15717
17013
  try {
15718
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
15719
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
17014
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
17015
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
15720
17016
  priority = normalizePriority(o.priority);
15721
17017
  args = buildIssueArgs({
15722
17018
  type: o.type,
@@ -15782,8 +17078,8 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
15782
17078
  try {
15783
17079
  const routing = assertVerifyRouting(o.routing);
15784
17080
  const lenses = o.lenses.split(",").map((s) => assertGrindLens(s.trim()));
15785
- const criteria = await (0, import_promises5.readFile)(o.criteriaFile, "utf8");
15786
- const diff = await (0, import_promises5.readFile)(o.diffFile, "utf8");
17081
+ const criteria = await (0, import_promises6.readFile)(o.criteriaFile, "utf8");
17082
+ const diff = await (0, import_promises6.readFile)(o.diffFile, "utf8");
15787
17083
  const plan2 = buildPanelPlan({ routing, lenses, criteria, diff });
15788
17084
  console.log(JSON.stringify(plan2));
15789
17085
  } catch (e) {
@@ -15792,7 +17088,7 @@ verify.command("panel").description("plan a fresh-eyes lens panel \u2014 print j
15792
17088
  });
15793
17089
  verify.command("synthesize").description("merge lens JSON array into a PanelReport").option("--input-file <path|->", "JSON lens array (use - for stdin)", "-").action(async (o) => {
15794
17090
  try {
15795
- const raw = o.inputFile === "-" ? await readStdin() : await (0, import_promises5.readFile)(o.inputFile, "utf8");
17091
+ const raw = o.inputFile === "-" ? await readStdin() : await (0, import_promises6.readFile)(o.inputFile, "utf8");
15796
17092
  const lenses = parseLensResults(JSON.parse(raw));
15797
17093
  console.log(JSON.stringify(synthesizePanelReport(lenses)));
15798
17094
  } catch (e) {
@@ -15865,7 +17161,7 @@ build.command("frontier").description("Evaluate external frontier exhaustion + L
15865
17161
  iterationCapOverride: opts.iterationCap
15866
17162
  };
15867
17163
  if (opts.jsonFile) {
15868
- const raw = await (0, import_promises5.readFile)(opts.jsonFile, "utf8");
17164
+ const raw = await (0, import_promises6.readFile)(opts.jsonFile, "utf8");
15869
17165
  state = { ...state, ...JSON.parse(raw) };
15870
17166
  }
15871
17167
  const result = evaluateBuildFrontier(state);
@@ -15919,8 +17215,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
15919
17215
  let args;
15920
17216
  try {
15921
17217
  skill = assertSkillName(o.skill);
15922
- rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
15923
- const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
17218
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
17219
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
15924
17220
  title = buildSkillLessonTitle(skill, rawTitle);
15925
17221
  priority = normalizePriority(o.priority);
15926
17222
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -15971,8 +17267,8 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
15971
17267
  let body;
15972
17268
  let title;
15973
17269
  try {
15974
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
15975
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
17270
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
17271
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
15976
17272
  } catch (e) {
15977
17273
  return fail(`pr create: ${e.message}`);
15978
17274
  }
@@ -15980,9 +17276,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
15980
17276
  console.log(JSON.stringify(created));
15981
17277
  });
15982
17278
  async function listCiWorkflowPaths(cwd = process.cwd()) {
15983
- const wfDir = (0, import_node_path14.join)(cwd, ".github", "workflows");
15984
- if (!(0, import_node_fs16.existsSync)(wfDir)) return [];
15985
- return (0, import_node_fs16.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
17279
+ const wfDir = (0, import_node_path17.join)(cwd, ".github", "workflows");
17280
+ if (!(0, import_node_fs19.existsSync)(wfDir)) return [];
17281
+ return (0, import_node_fs19.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
15986
17282
  }
15987
17283
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
15988
17284
  const repo = repoOpt ?? await resolveRepo();
@@ -15997,7 +17293,11 @@ function ciAuditDeps() {
15997
17293
  return {
15998
17294
  client: defaultGitHubClient(),
15999
17295
  listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
16000
- getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise))
17296
+ getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
17297
+ // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
17298
+ // reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
17299
+ // the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
17300
+ readSeedFile: (path2) => (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null
16001
17301
  };
16002
17302
  }
16003
17303
  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) => {
@@ -16005,14 +17305,28 @@ pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs
16005
17305
  if (o.json) return printLine(JSON.stringify(result));
16006
17306
  printLine(`merge CI policy: ${result.policy} (${result.reason})`);
16007
17307
  });
17308
+ async function pollGhPrChecks(prNumber, repoArgs) {
17309
+ let stdout = "";
17310
+ let stderr = "";
17311
+ try {
17312
+ ({ stdout, stderr } = await execFileP2("gh", ["pr", "checks", prNumber, ...repoArgs], { timeout: GC_GH_TIMEOUT_MS }));
17313
+ } catch (e) {
17314
+ const err = e;
17315
+ if (err.killed || err.stdout == null && err.stderr == null) throw e;
17316
+ stdout = err.stdout ?? "";
17317
+ stderr = err.stderr ?? "";
17318
+ }
17319
+ const state = parseGhPrChecksResult(stdout, stderr);
17320
+ if (state === "error") {
17321
+ throw new Error(`gh pr checks ${prNumber}: ${(stderr || stdout).trim() || "unknown error"}`);
17322
+ }
17323
+ return state;
17324
+ }
16008
17325
  pr.command("checks-wait <number>").description("bounded wait for PR checks; skips immediately on no-ci repos (#1432)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
16009
17326
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
16010
17327
  const result = await waitForPrChecks({
16011
17328
  resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
16012
- pollChecks: async () => {
16013
- const { stdout } = await execFileP2("gh", ["pr", "checks", number, ...repoArgs], { timeout: GC_GH_TIMEOUT_MS });
16014
- return parseGhPrChecksOutput(stdout);
16015
- },
17329
+ pollChecks: () => pollGhPrChecks(number, repoArgs),
16016
17330
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
16017
17331
  });
16018
17332
  if (o.json) printLine(JSON.stringify(result));
@@ -16036,11 +17350,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
16036
17350
  resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
16037
17351
  waitForChecks: (prNumber, repo) => waitForPrChecks({
16038
17352
  resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
16039
- pollChecks: async () => {
16040
- const args = repo ? ["--repo", repo] : [];
16041
- const { stdout } = await execFileP2("gh", ["pr", "checks", prNumber, ...args], { timeout: GC_GH_TIMEOUT_MS });
16042
- return parseGhPrChecksOutput(stdout);
16043
- },
17353
+ pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
16044
17354
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
16045
17355
  }),
16046
17356
  mergeAuto: async (prNumber, repo) => {
@@ -16097,15 +17407,15 @@ async function createDeferredWorktreeStore() {
16097
17407
  return {
16098
17408
  read: async () => {
16099
17409
  try {
16100
- return parseDeferredWorktreesFile(await (0, import_promises5.readFile)(registryPath, "utf8"));
17410
+ return parseDeferredWorktreesFile(await (0, import_promises6.readFile)(registryPath, "utf8"));
16101
17411
  } catch {
16102
17412
  return [];
16103
17413
  }
16104
17414
  },
16105
17415
  write: async (entries) => {
16106
17416
  try {
16107
- await (0, import_promises5.mkdir)((0, import_node_path14.dirname)(registryPath), { recursive: true });
16108
- await (0, import_promises5.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
17417
+ await (0, import_promises6.mkdir)((0, import_node_path17.dirname)(registryPath), { recursive: true });
17418
+ await (0, import_promises6.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
16109
17419
  } catch {
16110
17420
  }
16111
17421
  }
@@ -16114,16 +17424,57 @@ async function createDeferredWorktreeStore() {
16114
17424
  return void 0;
16115
17425
  }
16116
17426
  }
17427
+ var realWorktreeDirRemover = {
17428
+ probe: (p) => {
17429
+ let st;
17430
+ try {
17431
+ st = (0, import_node_fs19.lstatSync)(p);
17432
+ } catch {
17433
+ return null;
17434
+ }
17435
+ if (st.isSymbolicLink()) return "link";
17436
+ try {
17437
+ (0, import_node_fs19.readlinkSync)(p);
17438
+ return "link";
17439
+ } catch {
17440
+ }
17441
+ return st.isDirectory() ? "dir" : "file";
17442
+ },
17443
+ readdir: (p) => {
17444
+ try {
17445
+ return (0, import_node_fs19.readdirSync)(p);
17446
+ } catch {
17447
+ return [];
17448
+ }
17449
+ },
17450
+ // A directory reparse point (junction / dir-symlink) is detached with rmdir (unlinks the mount point,
17451
+ // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
17452
+ detachLink: (p) => {
17453
+ try {
17454
+ (0, import_node_fs19.rmdirSync)(p);
17455
+ } catch {
17456
+ (0, import_node_fs19.unlinkSync)(p);
17457
+ }
17458
+ },
17459
+ removeTree: (p) => (0, import_promises6.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
17460
+ };
17461
+ async function resolvePrimaryCheckout(execGit) {
17462
+ try {
17463
+ return parseWorktreePorcelain(await execGit(["worktree", "list", "--porcelain"]))[0]?.path;
17464
+ } catch {
17465
+ return void 0;
17466
+ }
17467
+ }
16117
17468
  function worktreeRemoveDeps(execGit) {
16118
17469
  return {
16119
17470
  git: execGit,
16120
17471
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
16121
- removeWorktreeDir: async (worktreePath) => (0, import_promises5.rm)(worktreePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
17472
+ removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover)
16122
17473
  };
16123
17474
  }
16124
17475
  function teardownWorktreeStage(worktreePath) {
16125
17476
  return runWorktreeStageTeardown(worktreePath, {
16126
- hasStageState: (wt) => (0, import_node_fs16.existsSync)(stageStatePath(wt)),
17477
+ hasStageState: (wt) => (0, import_node_fs19.existsSync)(stageStatePath(wt)),
16127
17478
  stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
16128
17479
  listComposeProjects: async () => {
16129
17480
  const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
@@ -16186,7 +17537,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
16186
17537
  } : await cleanupPrMergeLocalBranch(headRef, {
16187
17538
  beforeWorktrees,
16188
17539
  startingPath,
16189
- pathExists: (p) => (0, import_node_fs16.existsSync)(p),
17540
+ pathExists: (p) => (0, import_node_fs19.existsSync)(p),
16190
17541
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
16191
17542
  teardownWorktreeStage,
16192
17543
  deferredStore,
@@ -16358,7 +17709,7 @@ function rawValues(flag) {
16358
17709
  return out;
16359
17710
  }
16360
17711
  function printLine(value) {
16361
- (0, import_node_fs16.writeSync)(1, `${value}
17712
+ (0, import_node_fs19.writeSync)(1, `${value}
16362
17713
  `);
16363
17714
  }
16364
17715
  function stageKeepAlive() {
@@ -16375,8 +17726,8 @@ async function resolveStage() {
16375
17726
  local,
16376
17727
  shell: shellFor(),
16377
17728
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
16378
- hasCompose: (0, import_node_fs16.existsSync)((0, import_node_path14.join)(process.cwd(), "docker-compose.yml")),
16379
- hasEnvExample: (0, import_node_fs16.existsSync)((0, import_node_path14.join)(process.cwd(), ".env.example"))
17729
+ hasCompose: (0, import_node_fs19.existsSync)((0, import_node_path17.join)(process.cwd(), "docker-compose.yml")),
17730
+ hasEnvExample: (0, import_node_fs19.existsSync)((0, import_node_path17.join)(process.cwd(), ".env.example"))
16380
17731
  });
16381
17732
  }
16382
17733
  async function fetchStageVaultEnvMerge() {
@@ -16428,9 +17779,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
16428
17779
  printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
16429
17780
  return;
16430
17781
  }
16431
- const path2 = (0, import_node_path14.join)(process.cwd(), "infra", "port-ranges.json");
17782
+ const path2 = (0, import_node_path17.join)(process.cwd(), "infra", "port-ranges.json");
16432
17783
  const allocate = async (seed) => {
16433
- const { stdout } = await execFileP2("node", [(0, import_node_path14.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
17784
+ const { stdout } = await execFileP2("node", [(0, import_node_path17.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
16434
17785
  const parsed = JSON.parse(stdout);
16435
17786
  if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
16436
17787
  return parsed.range;
@@ -16624,7 +17975,7 @@ function trainApplyDeps() {
16624
17975
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
16625
17976
  announce: (args) => announceRelease({
16626
17977
  run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
16627
- readFile: (path2) => (0, import_promises5.readFile)(path2, "utf8")
17978
+ readFile: (path2) => (0, import_promises6.readFile)(path2, "utf8")
16628
17979
  }, args),
16629
17980
  fetchEdgeDomains: async (slug) => {
16630
17981
  const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
@@ -16697,7 +18048,16 @@ for (const commandName of ["rcand", "release"]) {
16697
18048
  }
16698
18049
  const repo = await resolveRepo();
16699
18050
  const targets = commandName === "rcand" ? await resolveRcandPlanTargets() : void 0;
16700
- const steps = trainPlan(commandName, { ...targets ?? {}, repo, dev: o.dev });
18051
+ let releaseTrack;
18052
+ if (repo) {
18053
+ try {
18054
+ const read = await fetchProjectBySlugChecked(slugOf(repo), registryClientDeps(await loadConfig()));
18055
+ const raw = read.ok ? read.project?.releaseTrack : void 0;
18056
+ if (isReleaseTrack(raw)) releaseTrack = raw;
18057
+ } catch {
18058
+ }
18059
+ }
18060
+ const steps = trainPlan(commandName, { ...targets ?? {}, repo, dev: o.dev, releaseTrack });
16701
18061
  console.log(
16702
18062
  o.json ? JSON.stringify({ command: commandName, ...targets ?? {}, repo, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps)
16703
18063
  );
@@ -16801,7 +18161,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
16801
18161
  const report = await verifyBootstrap(repo, o.class, {
16802
18162
  client: defaultGitHubClient(),
16803
18163
  projectMeta: meta,
16804
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs16.existsSync)(path2) ? (0, import_node_fs16.readFileSync)(path2, "utf8") : null,
18164
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null,
16805
18165
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
16806
18166
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
16807
18167
  requiredGcpApis: (() => {
@@ -16844,18 +18204,23 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16844
18204
  return fail(`bootstrap apply: ${e.message}`);
16845
18205
  }
16846
18206
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
16847
- if (!(0, import_node_fs16.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
16848
- const manifest = loadBootstrapSeeds((0, import_node_fs16.readFileSync)(manifestPath, "utf8"));
18207
+ if (!(0, import_node_fs19.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
18208
+ const manifest = loadBootstrapSeeds((0, import_node_fs19.readFileSync)(manifestPath, "utf8"));
16849
18209
  const baseBranch = o.class === "content" ? "main" : "development";
16850
18210
  const slug = parsedRepo.slug;
16851
18211
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
16852
- const readFile5 = (p) => (0, import_node_fs16.existsSync)(p) ? (0, import_node_fs16.readFileSync)(p, "utf8") : null;
18212
+ const readFile6 = (p) => (0, import_node_fs19.existsSync)(p) ? (0, import_node_fs19.readFileSync)(p, "utf8") : null;
16853
18213
  const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
16854
18214
  const rawVars = {};
16855
18215
  for (const value of rawValues("--var")) {
16856
18216
  const eq = value.indexOf("=");
16857
18217
  if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
16858
18218
  }
18219
+ try {
18220
+ const meta = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
18221
+ for (const [k, v] of Object.entries(gateConfigToVars(meta?.gate))) if (rawVars[k] == null) rawVars[k] = v;
18222
+ } catch {
18223
+ }
16859
18224
  const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class, bootstrapReleaseTrack);
16860
18225
  if (vars.PROJECT_ID) {
16861
18226
  try {
@@ -16891,7 +18256,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16891
18256
  }
16892
18257
  const planned = planSeedAction(resolved, exists);
16893
18258
  const isBlock = resolved.source === "managed-block";
16894
- const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile5) : null;
18259
+ const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile6) : null;
16895
18260
  const action = reconcileSeedAction(planned, content, isBlock);
16896
18261
  actions.push(action);
16897
18262
  if (o.execute && (action.action === "create" || action.action === "update")) {
@@ -16908,7 +18273,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16908
18273
  }
16909
18274
  const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
16910
18275
  if (rulesetSeed) {
16911
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile5);
18276
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile6);
16912
18277
  if (rulesetContent) {
16913
18278
  try {
16914
18279
  const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
@@ -17078,16 +18443,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
17078
18443
  if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
17079
18444
  targets = [{ repo: o.repo, class: o.class }];
17080
18445
  } else {
17081
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs16.existsSync)("projects.json") ? (0, import_node_fs16.readFileSync)("projects.json", "utf8") : null;
18446
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs19.existsSync)("projects.json") ? (0, import_node_fs19.readFileSync)("projects.json", "utf8") : null;
17082
18447
  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>");
17083
- const fanoutJson = (0, import_node_fs16.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs16.readFileSync)(".github/fanout-targets.json", "utf8") : null;
18448
+ const fanoutJson = (0, import_node_fs19.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs19.readFileSync)(".github/fanout-targets.json", "utf8") : null;
17084
18449
  targets = loadAccessTargets(projectsJson, fanoutJson);
17085
18450
  }
17086
18451
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
17087
- const fileMatrix = (0, import_node_fs16.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs16.readFileSync)("access-matrix.json", "utf8")) : {};
18452
+ const fileMatrix = (0, import_node_fs19.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs19.readFileSync)("access-matrix.json", "utf8")) : {};
17088
18453
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
17089
18454
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
17090
- const fileContracts = (0, import_node_fs16.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs16.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
18455
+ const fileContracts = (0, import_node_fs19.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs19.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
17091
18456
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
17092
18457
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
17093
18458
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -17096,20 +18461,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
17096
18461
  var isWin = process.platform === "win32";
17097
18462
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
17098
18463
  const homeDir = surface === "codex" ? ".codex" : ".claude";
17099
- return (0, import_node_path14.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
18464
+ return (0, import_node_path17.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
17100
18465
  };
17101
18466
  function readInstalledPlugins() {
17102
18467
  try {
17103
- return JSON.parse((0, import_node_fs16.readFileSync)(installedPluginsPath(), "utf8"));
18468
+ return JSON.parse((0, import_node_fs19.readFileSync)(installedPluginsPath(), "utf8"));
17104
18469
  } catch {
17105
18470
  return null;
17106
18471
  }
17107
18472
  }
17108
18473
  function installedPluginSources() {
17109
18474
  return ["claude", "codex"].map((surface) => {
17110
- const recordPath = (0, import_node_path14.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
18475
+ const recordPath = (0, import_node_path17.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
17111
18476
  try {
17112
- return { surface, installed: JSON.parse((0, import_node_fs16.readFileSync)(recordPath, "utf8")), recordPath };
18477
+ return { surface, installed: JSON.parse((0, import_node_fs19.readFileSync)(recordPath, "utf8")), recordPath };
17113
18478
  } catch {
17114
18479
  return { surface, installed: null, recordPath };
17115
18480
  }
@@ -17117,7 +18482,7 @@ function installedPluginSources() {
17117
18482
  }
17118
18483
  function readClaudeSettings() {
17119
18484
  try {
17120
- return JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
18485
+ return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
17121
18486
  } catch {
17122
18487
  return null;
17123
18488
  }
@@ -17139,7 +18504,7 @@ function writeProjectInstallRecord(record) {
17139
18504
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
17140
18505
  list.push(record);
17141
18506
  file.plugins[MMI_PLUGIN_ID] = list;
17142
- (0, import_node_fs16.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
18507
+ (0, import_node_fs19.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
17143
18508
  `, "utf8");
17144
18509
  return true;
17145
18510
  } catch {
@@ -17152,9 +18517,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
17152
18517
  if (!file) return false;
17153
18518
  if (!file.plugins) file.plugins = {};
17154
18519
  const path2 = installedPluginsPath();
17155
- (0, import_node_fs16.copyFileSync)(path2, `${path2}.bak`);
18520
+ (0, import_node_fs19.copyFileSync)(path2, `${path2}.bak`);
17156
18521
  file.plugins[pluginId] = records;
17157
- (0, import_node_fs16.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
18522
+ (0, import_node_fs19.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
17158
18523
  `, "utf8");
17159
18524
  return true;
17160
18525
  } catch {
@@ -17162,35 +18527,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
17162
18527
  }
17163
18528
  }
17164
18529
  function cursorPluginCacheRoot() {
17165
- return (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
18530
+ return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
17166
18531
  }
17167
18532
  function cursorPluginCachePinSnapshots() {
17168
18533
  const root = cursorPluginCacheRoot();
17169
18534
  try {
17170
- return (0, import_node_fs16.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
17171
- const path2 = (0, import_node_path14.join)(root, entry.name);
17172
- const pluginJson = (0, import_node_path14.join)(path2, ".cursor-plugin", "plugin.json");
17173
- const hooksJson = (0, import_node_path14.join)(path2, "hooks", "hooks.json");
17174
- const cliBundle = (0, import_node_path14.join)(path2, "cli", "dist", "index.cjs");
18535
+ return (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
18536
+ const path2 = (0, import_node_path17.join)(root, entry.name);
18537
+ const pluginJson = (0, import_node_path17.join)(path2, ".cursor-plugin", "plugin.json");
18538
+ const hooksJson = (0, import_node_path17.join)(path2, "hooks", "hooks.json");
18539
+ const cliBundle = (0, import_node_path17.join)(path2, "cli", "dist", "index.cjs");
17175
18540
  let version;
17176
18541
  try {
17177
- const raw = JSON.parse((0, import_node_fs16.readFileSync)(pluginJson, "utf8"));
18542
+ const raw = JSON.parse((0, import_node_fs19.readFileSync)(pluginJson, "utf8"));
17178
18543
  version = typeof raw.version === "string" ? raw.version : void 0;
17179
18544
  } catch {
17180
18545
  version = void 0;
17181
18546
  }
17182
18547
  let isEmpty = true;
17183
18548
  try {
17184
- isEmpty = (0, import_node_fs16.readdirSync)(path2).length === 0;
18549
+ isEmpty = (0, import_node_fs19.readdirSync)(path2).length === 0;
17185
18550
  } catch {
17186
18551
  isEmpty = true;
17187
18552
  }
17188
18553
  return {
17189
18554
  name: entry.name,
17190
18555
  path: path2,
17191
- hasPluginJson: (0, import_node_fs16.existsSync)(pluginJson),
17192
- hasHooksJson: (0, import_node_fs16.existsSync)(hooksJson),
17193
- hasCliBundle: (0, import_node_fs16.existsSync)(cliBundle),
18556
+ hasPluginJson: (0, import_node_fs19.existsSync)(pluginJson),
18557
+ hasHooksJson: (0, import_node_fs19.existsSync)(hooksJson),
18558
+ hasCliBundle: (0, import_node_fs19.existsSync)(cliBundle),
17194
18559
  isEmpty,
17195
18560
  version
17196
18561
  };
@@ -17200,19 +18565,19 @@ function cursorPluginCachePinSnapshots() {
17200
18565
  }
17201
18566
  }
17202
18567
  function hubCheckoutForCursorSeed() {
17203
- const manifest = (0, import_node_path14.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
17204
- return (0, import_node_fs16.existsSync)(manifest) ? process.cwd() : void 0;
18568
+ const manifest = (0, import_node_path17.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
18569
+ return (0, import_node_fs19.existsSync)(manifest) ? process.cwd() : void 0;
17205
18570
  }
17206
18571
  function mmiPluginCacheRootSnapshots() {
17207
18572
  const roots = [
17208
- { surface: "claude", root: (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
17209
- { surface: "codex", root: (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
18573
+ { surface: "claude", root: (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
18574
+ { surface: "codex", root: (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
17210
18575
  ];
17211
18576
  return roots.flatMap(({ surface, root }) => {
17212
18577
  try {
17213
- const entries = (0, import_node_fs16.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
18578
+ const entries = (0, import_node_fs19.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
17214
18579
  name: entry.name,
17215
- path: (0, import_node_path14.join)(root, entry.name),
18580
+ path: (0, import_node_path17.join)(root, entry.name),
17216
18581
  isDirectory: entry.isDirectory()
17217
18582
  }));
17218
18583
  return [{ surface, root, entries }];
@@ -17223,7 +18588,7 @@ function mmiPluginCacheRootSnapshots() {
17223
18588
  }
17224
18589
  function hasNestedMmiChild(versionDir) {
17225
18590
  try {
17226
- return (0, import_node_fs16.statSync)((0, import_node_path14.join)(versionDir, "mmi")).isDirectory();
18591
+ return (0, import_node_fs19.statSync)((0, import_node_path17.join)(versionDir, "mmi")).isDirectory();
17227
18592
  } catch {
17228
18593
  return false;
17229
18594
  }
@@ -17234,26 +18599,28 @@ function nestedPluginTreeSnapshot() {
17234
18599
  );
17235
18600
  }
17236
18601
  function uniqueQuarantineTarget(path2) {
17237
- if (!(0, import_node_fs16.existsSync)(path2)) return path2;
18602
+ if (!(0, import_node_fs19.existsSync)(path2)) return path2;
17238
18603
  for (let i = 1; i < 100; i += 1) {
17239
18604
  const candidate = `${path2}-${i}`;
17240
- if (!(0, import_node_fs16.existsSync)(candidate)) return candidate;
18605
+ if (!(0, import_node_fs19.existsSync)(candidate)) return candidate;
17241
18606
  }
17242
18607
  return `${path2}-${Date.now()}`;
17243
18608
  }
17244
18609
  function quarantinePluginCacheDirs(plan2) {
17245
18610
  let moved = 0;
18611
+ const failed = [];
17246
18612
  for (const move of plan2) {
17247
18613
  try {
17248
- if (!(0, import_node_fs16.existsSync)(move.from)) continue;
18614
+ if (!(0, import_node_fs19.existsSync)(move.from)) continue;
17249
18615
  const target = uniqueQuarantineTarget(move.to);
17250
- (0, import_node_fs16.mkdirSync)((0, import_node_path14.dirname)(target), { recursive: true });
17251
- (0, import_node_fs16.renameSync)(move.from, target);
18616
+ (0, import_node_fs19.mkdirSync)((0, import_node_path17.dirname)(target), { recursive: true });
18617
+ (0, import_node_fs19.renameSync)(move.from, target);
17252
18618
  moved += 1;
17253
18619
  } catch {
18620
+ failed.push(move);
17254
18621
  }
17255
18622
  }
17256
- return moved;
18623
+ return { moved, failed };
17257
18624
  }
17258
18625
  async function robocopyMirrorEmpty(emptyDir, target) {
17259
18626
  try {
@@ -17266,23 +18633,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
17266
18633
  }
17267
18634
  async function clearNestedPluginTreeDir(targetPath) {
17268
18635
  try {
17269
- if (!(0, import_node_fs16.existsSync)(targetPath)) return true;
18636
+ if (!(0, import_node_fs19.existsSync)(targetPath)) return true;
17270
18637
  if (isWin) {
17271
- const emptyDir = (0, import_node_path14.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
17272
- (0, import_node_fs16.mkdirSync)(emptyDir, { recursive: true });
18638
+ const emptyDir = (0, import_node_path17.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
18639
+ (0, import_node_fs19.mkdirSync)(emptyDir, { recursive: true });
17273
18640
  try {
17274
18641
  await robocopyMirrorEmpty(emptyDir, targetPath);
17275
- (0, import_node_fs16.rmSync)(targetPath, { recursive: true, force: true });
18642
+ (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
17276
18643
  } finally {
17277
18644
  try {
17278
- (0, import_node_fs16.rmSync)(emptyDir, { recursive: true, force: true });
18645
+ (0, import_node_fs19.rmSync)(emptyDir, { recursive: true, force: true });
17279
18646
  } catch {
17280
18647
  }
17281
18648
  }
17282
- return !(0, import_node_fs16.existsSync)(targetPath);
18649
+ return !(0, import_node_fs19.existsSync)(targetPath);
17283
18650
  }
17284
- (0, import_node_fs16.rmSync)(targetPath, { recursive: true, force: true });
17285
- return !(0, import_node_fs16.existsSync)(targetPath);
18651
+ (0, import_node_fs19.rmSync)(targetPath, { recursive: true, force: true });
18652
+ return !(0, import_node_fs19.existsSync)(targetPath);
17286
18653
  } catch {
17287
18654
  return false;
17288
18655
  }
@@ -17295,22 +18662,22 @@ async function applyNestedPluginTreeCleanup(paths, log) {
17295
18662
  }
17296
18663
  return true;
17297
18664
  }
17298
- var gitignorePath = () => (0, import_node_path14.join)(process.cwd(), ".gitignore");
18665
+ var gitignorePath = () => (0, import_node_path17.join)(process.cwd(), ".gitignore");
17299
18666
  function readTextFile(path2) {
17300
18667
  try {
17301
- if (!(0, import_node_fs16.existsSync)(path2)) return null;
17302
- return (0, import_node_fs16.readFileSync)(path2, "utf8");
18668
+ if (!(0, import_node_fs19.existsSync)(path2)) return null;
18669
+ return (0, import_node_fs19.readFileSync)(path2, "utf8");
17303
18670
  } catch {
17304
18671
  return null;
17305
18672
  }
17306
18673
  }
17307
18674
  function playwrightMcpConfigSnapshots() {
17308
18675
  const cwd = process.cwd();
17309
- const home = (0, import_node_os5.homedir)();
18676
+ const home = (0, import_node_os6.homedir)();
17310
18677
  const candidates = [
17311
- (0, import_node_path14.join)(cwd, ".cursor", "mcp.json"),
17312
- (0, import_node_path14.join)(home, ".cursor", "mcp.json"),
17313
- (0, import_node_path14.join)(home, ".codex", "config.toml")
18678
+ (0, import_node_path17.join)(cwd, ".cursor", "mcp.json"),
18679
+ (0, import_node_path17.join)(home, ".cursor", "mcp.json"),
18680
+ (0, import_node_path17.join)(home, ".codex", "config.toml")
17314
18681
  ];
17315
18682
  const out = [];
17316
18683
  for (const path2 of candidates) {
@@ -17323,7 +18690,7 @@ function strayBrowserArtifactPaths() {
17323
18690
  const cwd = process.cwd();
17324
18691
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
17325
18692
  try {
17326
- return (0, import_node_fs16.existsSync)((0, import_node_path14.join)(cwd, rel));
18693
+ return (0, import_node_fs19.existsSync)((0, import_node_path17.join)(cwd, rel));
17327
18694
  } catch {
17328
18695
  return false;
17329
18696
  }
@@ -17331,14 +18698,14 @@ function strayBrowserArtifactPaths() {
17331
18698
  }
17332
18699
  function readGitignore() {
17333
18700
  try {
17334
- return (0, import_node_fs16.readFileSync)(gitignorePath(), "utf8");
18701
+ return (0, import_node_fs19.readFileSync)(gitignorePath(), "utf8");
17335
18702
  } catch {
17336
18703
  return null;
17337
18704
  }
17338
18705
  }
17339
18706
  function writeGitignore(content) {
17340
18707
  try {
17341
- (0, import_node_fs16.writeFileSync)(gitignorePath(), content, "utf8");
18708
+ (0, import_node_fs19.writeFileSync)(gitignorePath(), content, "utf8");
17342
18709
  return true;
17343
18710
  } catch {
17344
18711
  return false;
@@ -17377,7 +18744,7 @@ async function runDoctor(opts, io = consoleIo) {
17377
18744
  let onPath = pathProbe;
17378
18745
  if (!onPath) {
17379
18746
  const root = process.env.CLAUDE_PLUGIN_ROOT;
17380
- if (root && (0, import_node_fs16.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18747
+ if (root && (0, import_node_fs19.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
17381
18748
  }
17382
18749
  checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
17383
18750
  const surface = detectSurface(process.env);
@@ -17494,7 +18861,8 @@ async function runDoctor(opts, io = consoleIo) {
17494
18861
  installedVersions: installedPluginVersions(installed)
17495
18862
  });
17496
18863
  if (!cacheCleanupCheck.ok && cacheCleanupCheck.quarantinePlan && repairLocal) {
17497
- const moved = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
18864
+ const { moved, failed } = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
18865
+ const attempted = moved + failed.length;
17498
18866
  if (moved > 0) {
17499
18867
  const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
17500
18868
  const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
@@ -17505,8 +18873,12 @@ async function runDoctor(opts, io = consoleIo) {
17505
18873
  isOrgRepo: Boolean(cfg.sagaApiUrl),
17506
18874
  roots: mmiPluginCacheRootSnapshots(),
17507
18875
  activeVersion: resolveClientVersion(),
17508
- releasedVersion
18876
+ releasedVersion,
18877
+ installedVersions: installedPluginVersions(installed)
17509
18878
  }),
18879
+ attemptedCount: attempted,
18880
+ failedCount: failed.length,
18881
+ ...failed.length > 0 ? { failedMoves: failed } : {},
17510
18882
  ...moved > 0 ? { cleanedCount: moved } : {}
17511
18883
  };
17512
18884
  }
@@ -17544,7 +18916,7 @@ async function runDoctor(opts, io = consoleIo) {
17544
18916
  isOrgRepo: Boolean(cfg.sagaApiUrl),
17545
18917
  surface,
17546
18918
  cacheRoot: cursorCacheRoot,
17547
- cacheRootExists: (0, import_node_fs16.existsSync)(cursorCacheRoot),
18919
+ cacheRootExists: (0, import_node_fs19.existsSync)(cursorCacheRoot),
17548
18920
  pins: cursorPins,
17549
18921
  hubCheckout: hubCheckoutForCursorSeed(),
17550
18922
  releasedVersion
@@ -17555,7 +18927,7 @@ async function runDoctor(opts, io = consoleIo) {
17555
18927
  releasedVersion,
17556
18928
  hubCheckout: hubCheckoutForCursorSeed(),
17557
18929
  execFileP: execFileP2,
17558
- mkdtemp: (prefix) => (0, import_promises5.mkdtemp)((0, import_node_path14.join)((0, import_node_os5.tmpdir)(), prefix)),
18930
+ mkdtemp: (prefix) => (0, import_promises6.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
17559
18931
  log: (m) => io.err(m)
17560
18932
  });
17561
18933
  if (seeded) {
@@ -17564,7 +18936,7 @@ async function runDoctor(opts, io = consoleIo) {
17564
18936
  isOrgRepo: Boolean(cfg.sagaApiUrl),
17565
18937
  surface,
17566
18938
  cacheRoot: cursorCacheRoot,
17567
- cacheRootExists: (0, import_node_fs16.existsSync)(cursorCacheRoot),
18939
+ cacheRootExists: (0, import_node_fs19.existsSync)(cursorCacheRoot),
17568
18940
  pins: cursorPins,
17569
18941
  hubCheckout: hubCheckoutForCursorSeed(),
17570
18942
  releasedVersion
@@ -17636,6 +19008,10 @@ program2.command("doctor").description("check onboarding gates (GitHub auth, mmi
17636
19008
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
17637
19009
  ));
17638
19010
  program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, whoami, doctor, plan-store check) in one process; docs sync runs detached").action(async () => {
19011
+ if (isInsideRepoSubdir(process.cwd())) {
19012
+ console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
19013
+ return;
19014
+ }
17639
19015
  try {
17640
19016
  const hook = parseHookInput(await readStdin());
17641
19017
  if (hook.session_id) persistSession(hook.session_id);
@@ -17647,6 +19023,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
17647
19023
  const { parallel, sequential } = buildSessionStartPlan({
17648
19024
  rulesSync: (io) => runRulesSync({ quiet: true }, io),
17649
19025
  sagaShow: (io) => runSagaShow({ quiet: true }, io),
19026
+ handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
17650
19027
  // honcho profile (#1162): inject the behavioral-memory prior (peer card). Bounded + fail-soft +
17651
19028
  // silent when off/empty, so it never delays or noises the session banner.
17652
19029
  honchoContext: (io) => runHonchoContext({ quiet: true, banner: true }, io),
@@ -17680,6 +19057,12 @@ program2.command("session-start").description("run the SessionStart verbs (rules
17680
19057
  await runSessionStart(parallel, sequential, consoleIo);
17681
19058
  consoleIo.log(northstarPointer(northstarInjected));
17682
19059
  for (const line of planStoreLines(process.cwd())) consoleIo.log(line);
19060
+ for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
19061
+ const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
19062
+ if (worktreeBanner) {
19063
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19064
+ consoleIo.log(worktreeBanner);
19065
+ }
17683
19066
  });
17684
19067
  installProcessBackstop();
17685
19068
  program2.parseAsync().catch((e) => failGraceful(e.message));