@mutmutco/cli 2.34.1 → 2.36.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
@@ -3392,7 +3392,7 @@ var program = new Command();
3392
3392
 
3393
3393
  // src/index.ts
3394
3394
  var import_promises5 = require("node:fs/promises");
3395
- var import_node_fs15 = require("node:fs");
3395
+ var import_node_fs18 = require("node:fs");
3396
3396
 
3397
3397
  // src/rules-sync.ts
3398
3398
  function normalizeEol(s) {
@@ -3427,7 +3427,7 @@ var import_node_child_process10 = require("node:child_process");
3427
3427
 
3428
3428
  // src/cli-shared.ts
3429
3429
  var import_promises = require("node:fs/promises");
3430
- var import_node_fs6 = require("node:fs");
3430
+ var import_node_fs7 = require("node:fs");
3431
3431
  var import_node_crypto2 = require("node:crypto");
3432
3432
  var import_node_child_process4 = require("node:child_process");
3433
3433
  var import_node_util4 = require("node:util");
@@ -4109,10 +4109,19 @@ async function hubAuthToken(deps) {
4109
4109
  }
4110
4110
 
4111
4111
  // src/stdin-inject.ts
4112
+ var import_node_fs6 = require("node:fs");
4112
4113
  var injectedStdin;
4114
+ function stdinHasPipedInput(statFd = () => (0, import_node_fs6.fstatSync)(0)) {
4115
+ try {
4116
+ const stat = statFd();
4117
+ return stat.isFIFO() || stat.isFile();
4118
+ } catch {
4119
+ return false;
4120
+ }
4121
+ }
4113
4122
  async function readStdin() {
4114
4123
  if (injectedStdin !== void 0) return injectedStdin;
4115
- if (process.stdin.isTTY !== false) return "";
4124
+ if (!stdinHasPipedInput()) return "";
4116
4125
  const chunks = [];
4117
4126
  for await (const chunk of process.stdin) chunks.push(chunk);
4118
4127
  return Buffer.concat(chunks).toString("utf8");
@@ -4161,7 +4170,7 @@ function sessionDeps() {
4161
4170
  env: process.env,
4162
4171
  readPersisted: () => {
4163
4172
  try {
4164
- return (0, import_node_fs6.readFileSync)(SESSION_FILE, "utf8");
4173
+ return (0, import_node_fs7.readFileSync)(SESSION_FILE, "utf8");
4165
4174
  } catch {
4166
4175
  return null;
4167
4176
  }
@@ -4174,8 +4183,8 @@ function sessionDeps() {
4174
4183
  var resolveSessionId = () => resolveSession(sessionDeps());
4175
4184
  function persistSession(id) {
4176
4185
  try {
4177
- (0, import_node_fs6.mkdirSync)(".mmi", { recursive: true });
4178
- (0, import_node_fs6.writeFileSync)(SESSION_FILE, id, "utf8");
4186
+ (0, import_node_fs7.mkdirSync)(".mmi", { recursive: true });
4187
+ (0, import_node_fs7.writeFileSync)(SESSION_FILE, id, "utf8");
4179
4188
  } catch {
4180
4189
  }
4181
4190
  }
@@ -4267,7 +4276,9 @@ async function resolveTextArg(input, deps, labels) {
4267
4276
  const source = input.file ?? "";
4268
4277
  const text = source === "-" ? await deps.readStdin() : await deps.readFile(source, "utf8");
4269
4278
  if (text.trim().length === 0) {
4270
- throw new Error(`${labels.file} produced an empty ${labels.noun}`);
4279
+ 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}`
4281
+ );
4271
4282
  }
4272
4283
  return text;
4273
4284
  }
@@ -4287,7 +4298,7 @@ function resolveIssueTitle(input, deps) {
4287
4298
  }
4288
4299
 
4289
4300
  // src/saga-capture.ts
4290
- var import_node_fs7 = require("node:fs");
4301
+ var import_node_fs8 = require("node:fs");
4291
4302
  var import_node_os2 = require("node:os");
4292
4303
  var import_node_path6 = require("node:path");
4293
4304
  function parseHookInput(stdin) {
@@ -4314,16 +4325,16 @@ function resolveCursorTranscriptPath(hook, env = process.env) {
4314
4325
  const slug = cursorProjectSlug(workspaceRoot);
4315
4326
  const transcriptsDir = (0, import_node_path6.join)(cursorProjectsRoot(env), slug, "agent-transcripts");
4316
4327
  const nested = (0, import_node_path6.join)(transcriptsDir, conversationId, `${conversationId}.jsonl`);
4317
- if ((0, import_node_fs7.existsSync)(nested)) return nested;
4328
+ if ((0, import_node_fs8.existsSync)(nested)) return nested;
4318
4329
  const flat = (0, import_node_path6.join)(transcriptsDir, `${conversationId}.jsonl`);
4319
- if ((0, import_node_fs7.existsSync)(flat)) return flat;
4330
+ if ((0, import_node_fs8.existsSync)(flat)) return flat;
4320
4331
  return void 0;
4321
4332
  }
4322
4333
  function resolveTranscriptPath(hook, env = process.env) {
4323
4334
  const fromHook = (hook.transcript_path ?? hook.transcriptPath)?.trim();
4324
4335
  if (fromHook) return fromHook;
4325
4336
  const fromEnv = env.CURSOR_TRANSCRIPT_PATH?.trim();
4326
- if (fromEnv && (0, import_node_fs7.existsSync)(fromEnv)) return fromEnv;
4337
+ if (fromEnv && (0, import_node_fs8.existsSync)(fromEnv)) return fromEnv;
4327
4338
  const surface = env.MMI_AGENT_SURFACE?.trim();
4328
4339
  if (surface === "cursor" || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) {
4329
4340
  return resolveCursorTranscriptPath(hook, env);
@@ -4436,6 +4447,7 @@ function buildNoteCapture(summary, o, id, evidence) {
4436
4447
  next: o.next,
4437
4448
  decision: o.decision,
4438
4449
  queueOp,
4450
+ handoffClose: o.handoffClose,
4439
4451
  state,
4440
4452
  source,
4441
4453
  evidence: Object.keys(ev).length ? ev : void 0,
@@ -5069,27 +5081,281 @@ function registerSagaCommands(program3) {
5069
5081
  });
5070
5082
  }
5071
5083
 
5084
+ // src/handoff-commands.ts
5085
+ var import_node_crypto4 = require("node:crypto");
5086
+
5087
+ // src/handoff.ts
5088
+ var HANDOFF_PREFIX = "handoff:";
5089
+ function clean(value) {
5090
+ return value?.trim() ?? "";
5091
+ }
5092
+ function normalizeHandoffKey(value) {
5093
+ const key = clean(value);
5094
+ if (!key) throw new Error("handoff key is required");
5095
+ return key;
5096
+ }
5097
+ function serializeHandoff(record) {
5098
+ return `${HANDOFF_PREFIX}${JSON.stringify(record)}`;
5099
+ }
5100
+ function parseHandoffText(text) {
5101
+ if (!text.startsWith(HANDOFF_PREFIX)) return null;
5102
+ try {
5103
+ const raw = JSON.parse(text.slice(HANDOFF_PREFIX.length));
5104
+ const state = raw.state;
5105
+ const key = clean(raw.key);
5106
+ const northStarSlug = clean(raw.northStarSlug);
5107
+ const summary = clean(raw.summary);
5108
+ const sourceSessionId = clean(raw.sourceSessionId);
5109
+ const createdAt = clean(raw.createdAt);
5110
+ if (state !== "open" && state !== "claimed" && state !== "cancelled") return null;
5111
+ if (!key || !northStarSlug || !summary || !sourceSessionId || !createdAt) return null;
5112
+ return {
5113
+ state,
5114
+ key,
5115
+ northStarSlug,
5116
+ summary,
5117
+ sourceSessionId,
5118
+ createdAt,
5119
+ claimedBySessionId: clean(raw.claimedBySessionId) || void 0,
5120
+ closedAt: clean(raw.closedAt) || void 0
5121
+ };
5122
+ } catch {
5123
+ return null;
5124
+ }
5125
+ }
5126
+ function listHandoffs(head, opts = {}) {
5127
+ const parsed = (head.queued ?? []).map((item, index) => ({ done: item.done, index, record: parseHandoffText(item.text) })).filter((x) => x.record !== null);
5128
+ return parsed.filter((x) => opts.includeClosed || x.record.state === "open" && !x.done).map((x) => ({ index: x.index, done: x.done, record: x.record }));
5129
+ }
5130
+ function findOpenHandoff(head, key) {
5131
+ const normalized = normalizeHandoffKey(key).toLowerCase();
5132
+ return listHandoffs(head).find((item) => item.record.key.toLowerCase() === normalized || item.record.northStarSlug.toLowerCase() === normalized);
5133
+ }
5134
+ function planOpenHandoff(input) {
5135
+ const key = normalizeHandoffKey(input.key);
5136
+ const northStarSlug = clean(input.northStarSlug);
5137
+ const summary = clean(input.summary);
5138
+ const sourceSessionId = clean(input.sourceSessionId);
5139
+ if (!northStarSlug) throw new Error("north star slug is required");
5140
+ if (!summary) throw new Error("handoff summary is required");
5141
+ if (!sourceSessionId) throw new Error("source session id is required");
5142
+ return {
5143
+ state: "open",
5144
+ key,
5145
+ northStarSlug,
5146
+ summary,
5147
+ sourceSessionId,
5148
+ createdAt: input.createdAt
5149
+ };
5150
+ }
5151
+ function closeHandoff(record, state, at, claimedBySessionId) {
5152
+ return {
5153
+ ...record,
5154
+ state,
5155
+ closedAt: at,
5156
+ claimedBySessionId: claimedBySessionId || record.claimedBySessionId
5157
+ };
5158
+ }
5159
+ function formatHandoffLine(item) {
5160
+ const r = item.record;
5161
+ const suffix = r.state === "claimed" && r.claimedBySessionId ? ` -> ${r.claimedBySessionId}` : "";
5162
+ return `${r.key} [${r.state}] northstar:${r.northStarSlug} source:${r.sourceSessionId}${suffix} - ${r.summary}`;
5163
+ }
5164
+
5165
+ // src/handoff-commands.ts
5166
+ var FOREGROUND_FETCH = { attempts: 2, timeoutMs: 8e3 };
5167
+ var SESSION_START_FETCH = { attempts: 1, timeoutMs: 3e3 };
5168
+ async function fetchState(url, qs, retry = FOREGROUND_FETCH) {
5169
+ const res = await fetchWithRetry(fetch, `${url}/saga/state?${qs}`, { headers: await hubHeaders() }, retry);
5170
+ if (!res.ok) return null;
5171
+ const body = await res.json();
5172
+ if ("state" in body) return body;
5173
+ return { key: null, state: { head: body.head } };
5174
+ }
5175
+ async function fetchScopedSessions(url, project2, branch, retry = FOREGROUND_FETCH) {
5176
+ const qs = new URLSearchParams({ project: project2, branch });
5177
+ const res = await fetchWithRetry(fetch, `${url}/saga/sessions?${qs}`, { headers: await hubHeaders() }, retry);
5178
+ if (!res.ok) return [];
5179
+ const body = await res.json();
5180
+ return body.sessions ?? [];
5181
+ }
5182
+ async function fetchSessionHead(url, key, retry = FOREGROUND_FETCH) {
5183
+ const qs = new URLSearchParams(key);
5184
+ const got = await fetchState(url, qs, retry);
5185
+ return got?.state?.head ?? null;
5186
+ }
5187
+ async function fetchCurrentState() {
5188
+ const cfg = await loadConfig();
5189
+ if (!cfg.sagaApiUrl) return null;
5190
+ const key = await sagaKey(cfg);
5191
+ const got = await fetchState(cfg.sagaApiUrl, new URLSearchParams(key));
5192
+ return got ? { key, head: got.state?.head ?? {} } : null;
5193
+ }
5194
+ async function collectScopedHandoffs(opts = {}) {
5195
+ const cfg = await loadConfig();
5196
+ if (!cfg.sagaApiUrl) return { handoffs: [], sessions: [] };
5197
+ const key = await sagaKey(cfg);
5198
+ const retry = opts.retry ?? FOREGROUND_FETCH;
5199
+ const sessions = await fetchScopedSessions(cfg.sagaApiUrl, key.project, key.branch, retry);
5200
+ const seen = /* @__PURE__ */ new Set();
5201
+ const handoffs = [];
5202
+ for (const session of sessions) {
5203
+ const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
5204
+ if (!head) continue;
5205
+ for (const item of listHandoffs(head, { includeClosed: opts.includeClosed })) {
5206
+ const dedupe = `${item.record.sourceSessionId}:${item.record.key}:${item.record.state}`;
5207
+ if (seen.has(dedupe)) continue;
5208
+ seen.add(dedupe);
5209
+ handoffs.push(item);
5210
+ }
5211
+ }
5212
+ return { handoffs, sessions };
5213
+ }
5214
+ async function locateOpenHandoff(key, retry = FOREGROUND_FETCH) {
5215
+ const cfg = await loadConfig();
5216
+ if (!cfg.sagaApiUrl) return null;
5217
+ const scopeKey = await sagaKey(cfg);
5218
+ const sessions = await fetchScopedSessions(cfg.sagaApiUrl, scopeKey.project, scopeKey.branch, retry);
5219
+ for (const session of sessions) {
5220
+ const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
5221
+ if (!head) continue;
5222
+ const item = findOpenHandoff(head, key);
5223
+ if (item) {
5224
+ return {
5225
+ key: { project: session.project, branch: session.branch, sessionId: session.sessionId },
5226
+ item
5227
+ };
5228
+ }
5229
+ }
5230
+ return null;
5231
+ }
5232
+ async function locateClaimedHandoff(key, claimedBySessionId, retry = FOREGROUND_FETCH) {
5233
+ const { handoffs } = await collectScopedHandoffs({ includeClosed: true, retry });
5234
+ const normalized = key.toLowerCase();
5235
+ return handoffs.find((item) => {
5236
+ const r = item.record;
5237
+ return r.state === "claimed" && r.claimedBySessionId === claimedBySessionId && (r.key.toLowerCase() === normalized || r.northStarSlug.toLowerCase() === normalized);
5238
+ }) ?? null;
5239
+ }
5240
+ async function postKeyedNote(key, summary, options) {
5241
+ const cfg = await loadConfig();
5242
+ if (!cfg.sagaApiUrl) fail("handoff: Hub API URL not configured");
5243
+ const sha = await gitOut(["rev-parse", "--short", "HEAD"]);
5244
+ const capture = buildNoteCapture(summary, options, (0, import_node_crypto4.randomUUID)(), { sha: sha || void 0, branch: key.branch });
5245
+ const result = await postCaptureOnce(cfg.sagaApiUrl, { ...capture, ...key });
5246
+ if (!result.ok) fail(`handoff: write failed${result.status ? ` (HTTP ${result.status})` : ""}${result.message ? `: ${result.message}` : ""}`);
5247
+ }
5248
+ function deriveOpenFields(summary, opts, head, key) {
5249
+ const northStarSlug = opts.northStarSlug ?? head.anchor?.slug;
5250
+ const handoffKey = opts.key ?? northStarSlug;
5251
+ const text = summary?.trim() || head.next?.trim() || head.anchor?.intent?.trim();
5252
+ return planOpenHandoff({
5253
+ key: handoffKey ?? "",
5254
+ northStarSlug: northStarSlug ?? "",
5255
+ summary: text ?? "",
5256
+ sourceSessionId: key.sessionId,
5257
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5258
+ });
5259
+ }
5260
+ async function runHandoffOpen(summary, opts, io = consoleIo) {
5261
+ const current = await fetchCurrentState();
5262
+ if (!current) return fail("handoff open: saga state unavailable");
5263
+ let record;
5264
+ try {
5265
+ record = deriveOpenFields(summary, opts, current.head, current.key);
5266
+ } catch (e) {
5267
+ return fail(`handoff open: ${e.message}`);
5268
+ }
5269
+ await postKeyedNote(current.key, `handoff opened ${record.key}`, { queueAdd: serializeHandoff(record), anchorSlug: record.northStarSlug });
5270
+ if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: record }));
5271
+ io.log(`handoff open: ${formatHandoffLine({ index: -1, done: false, record })}`);
5272
+ }
5273
+ async function runHandoffList(opts, io = consoleIo) {
5274
+ const { handoffs } = await collectScopedHandoffs({ includeClosed: opts.all });
5275
+ if (opts.json) {
5276
+ io.log(JSON.stringify({ handoffs }, null, 2));
5277
+ } else if (!handoffs.length) {
5278
+ io.log("handoff list: none");
5279
+ } else {
5280
+ for (const h of handoffs) io.log(formatHandoffLine(h));
5281
+ }
5282
+ return handoffs;
5283
+ }
5284
+ async function runHandoffOffer(io = consoleIo, opts = {}) {
5285
+ const retry = opts.fast ? SESSION_START_FETCH : FOREGROUND_FETCH;
5286
+ const { handoffs } = await collectScopedHandoffs({ retry });
5287
+ if (!handoffs.length) return;
5288
+ io.log("Open handoffs:");
5289
+ for (const h of handoffs) io.log(`- ${formatHandoffLine(h)}`);
5290
+ 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.");
5291
+ }
5292
+ async function closeSourceHandoff(key, state, opts = {}, io = consoleIo) {
5293
+ const current = await sagaKey(await loadConfig(), resolveSessionId());
5294
+ const located = await locateOpenHandoff(key);
5295
+ if (!located) {
5296
+ if (state === "claimed") {
5297
+ const prior = await locateClaimedHandoff(key, current.sessionId);
5298
+ if (prior) {
5299
+ if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: prior.record, idempotent: true }));
5300
+ return io.log(`handoff claimed: ${formatHandoffLine(prior)} (already accepted)`);
5301
+ }
5302
+ }
5303
+ return fail(`handoff ${state}: no open handoff matching ${key}`);
5304
+ }
5305
+ const closed = closeHandoff(located.item.record, state, (/* @__PURE__ */ new Date()).toISOString(), state === "claimed" ? current.sessionId : void 0);
5306
+ await postKeyedNote(located.key, `handoff ${state} ${located.item.record.key}`, {
5307
+ handoffClose: { index: located.item.index, closedText: serializeHandoff(closed) }
5308
+ });
5309
+ if (state === "claimed") {
5310
+ await postKeyedNote(current, `handoff accepted ${located.item.record.key}`, {
5311
+ anchor: located.item.record.summary,
5312
+ anchorSlug: located.item.record.northStarSlug,
5313
+ anchorForce: true,
5314
+ decision: `accepted handoff ${located.item.record.key} from session ${located.item.record.sourceSessionId}; northstar ${located.item.record.northStarSlug}`,
5315
+ verified: true
5316
+ });
5317
+ }
5318
+ if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: closed }));
5319
+ io.log(`handoff ${state}: ${formatHandoffLine({ index: located.item.index, done: true, record: closed })}`);
5320
+ }
5321
+ async function runHandoffDecline(key, opts = {}, io = consoleIo) {
5322
+ const located = await locateOpenHandoff(key);
5323
+ if (!located) return fail(`handoff decline: no open handoff matching ${key}`);
5324
+ if (opts.json) return io.log(JSON.stringify({ ok: true, unchanged: true, handoff: located.item.record }));
5325
+ io.log(`handoff decline: left open for re-offer \u2014 ${formatHandoffLine(located.item)}`);
5326
+ }
5327
+ function registerHandoffCommands(program3) {
5328
+ const handoff = program3.command("handoff").description("explicit saga + North Star handoff lifecycle");
5329
+ 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("--json", "machine-readable output").action((summary, opts) => runHandoffOpen(summary, opts));
5330
+ 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) => {
5331
+ await runHandoffList(opts);
5332
+ });
5333
+ 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));
5334
+ handoff.command("cancel <key>").description("close an open handoff without claiming it").option("--json", "machine-readable output").action((key, opts) => closeSourceHandoff(key, "cancelled", opts));
5335
+ 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));
5336
+ }
5337
+
5072
5338
  // src/honcho-commands.ts
5073
5339
  var import_node_child_process5 = require("node:child_process");
5074
5340
  var import_promises3 = require("node:fs/promises");
5075
- var import_node_fs9 = require("node:fs");
5341
+ var import_node_fs10 = require("node:fs");
5076
5342
  var import_node_path8 = require("node:path");
5077
5343
 
5078
5344
  // src/honcho-ingest-skip.ts
5079
- var import_node_fs8 = require("node:fs");
5345
+ var import_node_fs9 = require("node:fs");
5080
5346
  var import_node_path7 = require("node:path");
5081
5347
  var INGEST_SKIP_FILE = ".mmi/honcho/last-skip.json";
5082
5348
  function recordIngestSkip(record) {
5083
5349
  try {
5084
- (0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(INGEST_SKIP_FILE), { recursive: true });
5085
- (0, import_node_fs8.writeFileSync)(INGEST_SKIP_FILE, JSON.stringify(record), "utf8");
5350
+ (0, import_node_fs9.mkdirSync)((0, import_node_path7.dirname)(INGEST_SKIP_FILE), { recursive: true });
5351
+ (0, import_node_fs9.writeFileSync)(INGEST_SKIP_FILE, JSON.stringify(record), "utf8");
5086
5352
  } catch {
5087
5353
  }
5088
5354
  }
5089
5355
  function readIngestSkip() {
5090
5356
  try {
5091
- if (!(0, import_node_fs8.existsSync)(INGEST_SKIP_FILE)) return null;
5092
- const o = JSON.parse((0, import_node_fs8.readFileSync)(INGEST_SKIP_FILE, "utf8"));
5357
+ if (!(0, import_node_fs9.existsSync)(INGEST_SKIP_FILE)) return null;
5358
+ const o = JSON.parse((0, import_node_fs9.readFileSync)(INGEST_SKIP_FILE, "utf8"));
5093
5359
  return o?.reason && o?.surface && o?.ts ? o : null;
5094
5360
  } catch {
5095
5361
  return null;
@@ -5097,7 +5363,7 @@ function readIngestSkip() {
5097
5363
  }
5098
5364
  function clearIngestSkip() {
5099
5365
  try {
5100
- if ((0, import_node_fs8.existsSync)(INGEST_SKIP_FILE)) (0, import_node_fs8.unlinkSync)(INGEST_SKIP_FILE);
5366
+ if ((0, import_node_fs9.existsSync)(INGEST_SKIP_FILE)) (0, import_node_fs9.unlinkSync)(INGEST_SKIP_FILE);
5101
5367
  } catch {
5102
5368
  }
5103
5369
  }
@@ -5159,7 +5425,8 @@ function formatVaultPointer(p) {
5159
5425
  ``,
5160
5426
  `enumerate actual keys: mmi-cli secrets list`,
5161
5427
  `read one: mmi-cli secrets get <stage>/<KEY> (e.g. main/GOOGLE_CLIENT_ID)`,
5162
- `set a project-tier key: mmi-cli secrets set <KEY> (value via stdin; org tier needs a master grant)`
5428
+ `set a project-tier key: mmi-cli secrets set <KEY> (value via stdin; org tier needs a master grant)`,
5429
+ `copy provider keys: mmi-cli secrets copy --from rc --to dev --keys RECALL_API_KEY,GEMINI_API_KEY`
5163
5430
  ];
5164
5431
  return lines.join("\n");
5165
5432
  }
@@ -5503,6 +5770,54 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
5503
5770
  }
5504
5771
  deps.log(`revoked @${login}'s access to ${key} in ${repo}`);
5505
5772
  }
5773
+ var SECRET_COPY_BLOCKED_RE = /(?:ENC_KEY|ENCRYPTION_KEY|SECRET_KEY_BASE)/i;
5774
+ function isSecretCopyBlocked(key) {
5775
+ const slash = key.indexOf("/");
5776
+ const leaf = slash === -1 ? key : key.slice(slash + 1);
5777
+ return SECRET_COPY_BLOCKED_RE.test(leaf);
5778
+ }
5779
+ function copyTierKey(stage2, leaf) {
5780
+ return `${stage2}/${leaf}`;
5781
+ }
5782
+ async function secretsCopy(deps, opts) {
5783
+ if (opts.from === opts.to) {
5784
+ deps.err("secrets copy: --from and --to must differ");
5785
+ return false;
5786
+ }
5787
+ const keys = [...new Set(opts.keys.map((k) => k.trim()).filter(Boolean))];
5788
+ if (!keys.length) {
5789
+ deps.err("secrets copy: --keys required (comma-separated allowlist)");
5790
+ return false;
5791
+ }
5792
+ for (const key of keys) {
5793
+ if (!isValidSecretKey(key)) {
5794
+ deps.err(`invalid secret key ${JSON.stringify(key)}`);
5795
+ return false;
5796
+ }
5797
+ if (isSecretCopyBlocked(key)) {
5798
+ deps.err(`secrets copy: ${key} is not copyable \u2014 generate encryption/stage-distinct keys per stage`);
5799
+ return false;
5800
+ }
5801
+ }
5802
+ for (const key of keys) {
5803
+ const leaf = secretLeafName(key);
5804
+ const srcKey = copyTierKey(opts.from, leaf);
5805
+ const dstKey = copyTierKey(opts.to, leaf);
5806
+ const value = await fetchSecretValue(deps, srcKey, opts);
5807
+ if (!value) {
5808
+ deps.err(`secrets copy: could not read source ${srcKey}`);
5809
+ return false;
5810
+ }
5811
+ if (opts.dryRun) {
5812
+ deps.log(`would copy ${srcKey} \u2192 ${dstKey}`);
5813
+ continue;
5814
+ }
5815
+ const ok = await putSecret(deps, dstKey, value, opts);
5816
+ if (!ok) return false;
5817
+ deps.log(`copied ${srcKey} \u2192 ${dstKey}`);
5818
+ }
5819
+ return true;
5820
+ }
5506
5821
  async function secretsUse(deps, key, opts) {
5507
5822
  const slug = await vaultSlug(deps, opts);
5508
5823
  const tier = classifyTier(slug, key);
@@ -5580,7 +5895,7 @@ async function resolveHonchoConfig(cfg = {}, opts = {}) {
5580
5895
  }
5581
5896
 
5582
5897
  // src/honcho-ingest.ts
5583
- var import_node_crypto4 = require("node:crypto");
5898
+ var import_node_crypto5 = require("node:crypto");
5584
5899
  var DEFAULT_HONCHO_MAX_CHARS = 4e3;
5585
5900
  var DEFAULT_HONCHO_CARD_MAX_CHARS = 1200;
5586
5901
  var REDACTED = "[REDACTED]";
@@ -5666,7 +5981,7 @@ function buildIngestPayload(args) {
5666
5981
  const maxChars = args.maxChars ?? DEFAULT_HONCHO_MAX_CHARS;
5667
5982
  const messages = args.messages.map((m) => ({ role: m.role, content: capContent(redactSecrets(m.content), maxChars) })).filter((m) => m.content.trim().length > 0);
5668
5983
  return {
5669
- id: args.id ?? (0, import_node_crypto4.randomUUID)(),
5984
+ id: args.id ?? (0, import_node_crypto5.randomUUID)(),
5670
5985
  workspace: args.workspace,
5671
5986
  peer: args.peer,
5672
5987
  session: args.session,
@@ -5873,7 +6188,7 @@ function honchoThrottlePath(key) {
5873
6188
  function honchoIngestDue(path2, intervalMs, now = Date.now()) {
5874
6189
  if (intervalMs <= 0) return true;
5875
6190
  try {
5876
- const last = Number((0, import_node_fs9.readFileSync)(path2, "utf8").trim()) || 0;
6191
+ const last = Number((0, import_node_fs10.readFileSync)(path2, "utf8").trim()) || 0;
5877
6192
  return now - last >= intervalMs;
5878
6193
  } catch {
5879
6194
  return true;
@@ -5881,8 +6196,8 @@ function honchoIngestDue(path2, intervalMs, now = Date.now()) {
5881
6196
  }
5882
6197
  function markHonchoIngest(path2, now = Date.now()) {
5883
6198
  try {
5884
- (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(path2), { recursive: true });
5885
- (0, import_node_fs9.writeFileSync)(path2, String(now), "utf8");
6199
+ (0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(path2), { recursive: true });
6200
+ (0, import_node_fs10.writeFileSync)(path2, String(now), "utf8");
5886
6201
  } catch {
5887
6202
  }
5888
6203
  }
@@ -6165,8 +6480,205 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
6165
6480
  }
6166
6481
 
6167
6482
  // src/session-start.ts
6168
- var import_node_fs10 = require("node:fs");
6483
+ var import_node_fs12 = require("node:fs");
6484
+ var import_node_path10 = require("node:path");
6485
+
6486
+ // src/scratch-gc.ts
6487
+ var import_node_fs11 = require("node:fs");
6169
6488
  var import_node_path9 = require("node:path");
6489
+ var TMP_STALE_MS = 6e4;
6490
+ var FLUSH_LOCK_GC_MS = 6e5;
6491
+ var HEAD_TS_STALE_MS = 48 * 36e5;
6492
+ var LAST_SKIP_STALE_MS = 24 * 36e5;
6493
+ var CONFLICT_COPY_STALE_MS = 24 * 36e5;
6494
+ var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
6495
+ var SCRATCH_GC_THROTTLE_MS = 24 * 36e5;
6496
+ function scratchGcThrottlePath(mmiRoot) {
6497
+ return (0, import_node_path9.join)(mmiRoot, "head-ts", ".scratch-gc-last");
6498
+ }
6499
+ function scratchGcDue(stampPath, now = Date.now(), read = import_node_fs11.readFileSync) {
6500
+ try {
6501
+ return now - (Number(read(stampPath, "utf8").trim()) || 0) >= SCRATCH_GC_THROTTLE_MS;
6502
+ } catch {
6503
+ return true;
6504
+ }
6505
+ }
6506
+ function markScratchGcRun(stampPath, now = Date.now()) {
6507
+ try {
6508
+ (0, import_node_fs11.mkdirSync)((0, import_node_path9.dirname)(stampPath), { recursive: true });
6509
+ (0, import_node_fs11.writeFileSync)(stampPath, String(now), "utf8");
6510
+ } catch {
6511
+ }
6512
+ }
6513
+ function executeScratchGc(repoRoot, opts, now = Date.now()) {
6514
+ const snap = collectScratchSnapshot(repoRoot);
6515
+ const plan2 = planScratchGc(snap, now);
6516
+ if (!opts.apply) return { plan: plan2 };
6517
+ return { plan: plan2, applied: applyScratchGc(plan2, snap.mmiRoot, now) };
6518
+ }
6519
+ var NEVER_BASENAMES = /* @__PURE__ */ new Set([".session", "config.json", "saga-pending.jsonl"]);
6520
+ var TMP_SIDECAR_BASES = ["saga-pending.jsonl", "last-skip.json"];
6521
+ var CONFLICT_COPY_ALLOWLIST = /* @__PURE__ */ new Set(["saga-pending.jsonl", "last-skip.json"]);
6522
+ function conflictCopyOriginal(name) {
6523
+ const m = /^(.+) \d+(\.[^.]+)$/.exec(name);
6524
+ return m ? `${m[1]}${m[2]}` : null;
6525
+ }
6526
+ function isTmpSidecar(name) {
6527
+ return name.endsWith(".tmp") && TMP_SIDECAR_BASES.some((b) => name.startsWith(`${b}.`));
6528
+ }
6529
+ function planScratchGc(snap, now = Date.now()) {
6530
+ const candidates = [];
6531
+ const normalizePath = (p) => p.replace(/\\/g, "/");
6532
+ const mmiPaths = new Set(snap.mmiFiles.map((f) => normalizePath(f.path)));
6533
+ const headTsPrefix = `${snap.mmiRoot.replace(/[\\/]+$/, "")}/head-ts/`.replace(/\\/g, "/");
6534
+ const days = (ms) => `${Math.floor(ms / 864e5)}d`;
6535
+ for (const f of snap.mmiFiles) {
6536
+ const age = now - f.mtimeMs;
6537
+ const add = (family, reason) => candidates.push({ path: f.path, family, tier: "safe-auto", reason, bytes: f.bytes });
6538
+ if (isTmpSidecar(f.name)) {
6539
+ if (age > TMP_STALE_MS) add("tmp-sidecar", `crashed atomic-write sidecar (${days(age)} old)`);
6540
+ continue;
6541
+ }
6542
+ if (f.name === "saga-flush.lock") {
6543
+ if (age > FLUSH_LOCK_GC_MS) add("flush-lock", `abandoned flush lock (${days(age)} old)`);
6544
+ continue;
6545
+ }
6546
+ if (NEVER_BASENAMES.has(f.name)) continue;
6547
+ if (f.path.replace(/\\/g, "/").startsWith(headTsPrefix) && !f.name.startsWith(".")) {
6548
+ if (age > HEAD_TS_STALE_MS) add("head-ts", `stale throttle stamp (${days(age)} old)`);
6549
+ continue;
6550
+ }
6551
+ if (f.name === "last-skip.json") {
6552
+ if (age > LAST_SKIP_STALE_MS) add("last-skip", `stale ingest-skip hint (${days(age)} old)`);
6553
+ continue;
6554
+ }
6555
+ const orig = conflictCopyOriginal(f.name);
6556
+ if (orig && CONFLICT_COPY_ALLOWLIST.has(orig) && mmiPaths.has(normalizePath((0, import_node_path9.join)(f.dir, orig))) && age > CONFLICT_COPY_STALE_MS) {
6557
+ add("conflict-copy", `cloud-sync conflict copy of ${orig} (${days(age)} old)`);
6558
+ }
6559
+ }
6560
+ if (snap.syncQueueSlugs) {
6561
+ for (const f of snap.planMdFiles) {
6562
+ const slug = f.name.replace(/\.md$/, "");
6563
+ if (snap.syncQueueSlugs.has(slug)) continue;
6564
+ if (now - f.mtimeMs > PLAN_ADVISORY_AGE_MS) {
6565
+ candidates.push({
6566
+ path: f.path,
6567
+ family: "plan",
6568
+ tier: "advisory",
6569
+ reason: `synced plan older than ${Math.floor(PLAN_ADVISORY_AGE_MS / 864e5)}d`,
6570
+ bytes: f.bytes
6571
+ });
6572
+ }
6573
+ }
6574
+ }
6575
+ return {
6576
+ candidates,
6577
+ safeAuto: candidates.filter((c) => c.tier === "safe-auto"),
6578
+ advisory: candidates.filter((c) => c.tier === "advisory")
6579
+ };
6580
+ }
6581
+ function formatScratchGcPlan(plan2, applied) {
6582
+ if (plan2.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local saga/plan/honcho scratch is clean.";
6583
+ const lines = [];
6584
+ const bytes = plan2.safeAuto.reduce((n, c) => n + c.bytes, 0);
6585
+ lines.push(
6586
+ `scratch GC: ${applied ? "pruned" : "would prune"} ${plan2.safeAuto.length} stale file(s) (${bytes} bytes)` + (plan2.advisory.length ? `; ${plan2.advisory.length} advisory (kept):` : ":")
6587
+ );
6588
+ for (const c of plan2.safeAuto.slice(0, 40)) lines.push(` - [${c.family}] ${c.path} \u2014 ${c.reason}`);
6589
+ if (plan2.safeAuto.length > 40) lines.push(` ... +${plan2.safeAuto.length - 40} more`);
6590
+ for (const c of plan2.advisory.slice(0, 20)) lines.push(` \xB7 [advisory] ${c.path} \u2014 ${c.reason} (kept; review manually)`);
6591
+ return lines.join("\n");
6592
+ }
6593
+ function scratchGcBannerLine(safeAutoPruned, advisory) {
6594
+ const parts = [];
6595
+ if (safeAutoPruned > 0) parts.push(`pruned ${safeAutoPruned} stale local file(s)`);
6596
+ if (advisory > 0) parts.push(`${advisory} old synced plan(s) prunable \u2014 \`mmi-cli gc --scratch --dry-run\` to review`);
6597
+ return parts.length ? `[cleanup] ${parts.join("; ")}` : void 0;
6598
+ }
6599
+ function applyScratchGc(plan2, mmiRoot, now = Date.now()) {
6600
+ const result = { pruned: [], skipped: 0, bytes: 0 };
6601
+ let anchor;
6602
+ try {
6603
+ anchor = (0, import_node_fs11.realpathSync)(mmiRoot).replace(/\\/g, "/").replace(/\/+$/, "");
6604
+ } catch {
6605
+ return result;
6606
+ }
6607
+ for (const c of plan2.safeAuto) {
6608
+ try {
6609
+ const real = (0, import_node_fs11.realpathSync)(c.path).replace(/\\/g, "/");
6610
+ if (real !== anchor && !real.startsWith(`${anchor}/`)) {
6611
+ result.skipped += 1;
6612
+ continue;
6613
+ }
6614
+ const st = (0, import_node_fs11.statSync)(c.path);
6615
+ 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;
6616
+ if (now - st.mtimeMs <= floor) {
6617
+ result.skipped += 1;
6618
+ continue;
6619
+ }
6620
+ (0, import_node_fs11.unlinkSync)(c.path);
6621
+ result.pruned.push(c.path);
6622
+ result.bytes += c.bytes;
6623
+ } catch {
6624
+ result.skipped += 1;
6625
+ }
6626
+ }
6627
+ return result;
6628
+ }
6629
+ function collectScratchSnapshot(repoRoot, deps = {}) {
6630
+ const readdir = deps.readdir ?? import_node_fs11.readdirSync;
6631
+ const stat = deps.stat ?? import_node_fs11.statSync;
6632
+ const readFile5 = deps.readFile ?? import_node_fs11.readFileSync;
6633
+ const mmiRoot = (0, import_node_path9.join)(repoRoot, ".mmi");
6634
+ const plansRoot = (0, import_node_path9.join)(repoRoot, "plans");
6635
+ const mmiFiles = [];
6636
+ try {
6637
+ for (const ent of readdir(mmiRoot, { recursive: true, withFileTypes: true })) {
6638
+ if (!ent.isFile()) continue;
6639
+ const dir = ent.parentPath ?? ent.path ?? mmiRoot;
6640
+ const full = (0, import_node_path9.join)(dir, ent.name);
6641
+ try {
6642
+ const st = stat(full);
6643
+ mmiFiles.push({ path: full, dir, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
6644
+ } catch {
6645
+ }
6646
+ }
6647
+ } catch {
6648
+ }
6649
+ const planMdFiles = [];
6650
+ try {
6651
+ for (const ent of readdir(plansRoot, { withFileTypes: true })) {
6652
+ if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
6653
+ const full = (0, import_node_path9.join)(plansRoot, ent.name);
6654
+ try {
6655
+ const st = stat(full);
6656
+ planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
6657
+ } catch {
6658
+ }
6659
+ }
6660
+ } catch {
6661
+ }
6662
+ let syncQueueSlugs = null;
6663
+ let queueRaw;
6664
+ try {
6665
+ queueRaw = readFile5((0, import_node_path9.join)(plansRoot, ".sync-queue.json"), "utf8");
6666
+ } catch {
6667
+ syncQueueSlugs = /* @__PURE__ */ new Set();
6668
+ }
6669
+ if (queueRaw !== void 0) {
6670
+ try {
6671
+ const parsed = JSON.parse(queueRaw);
6672
+ const entries = Array.isArray(parsed) ? parsed : parsed.entries ?? [];
6673
+ syncQueueSlugs = new Set(entries.map((e) => e.slug).filter((s) => typeof s === "string"));
6674
+ } catch {
6675
+ syncQueueSlugs = null;
6676
+ }
6677
+ }
6678
+ return { mmiRoot, mmiFiles, planMdFiles, syncQueueSlugs };
6679
+ }
6680
+
6681
+ // src/session-start.ts
6170
6682
  async function runBufferedStep(step) {
6171
6683
  const lines = [];
6172
6684
  const io = {
@@ -6194,6 +6706,7 @@ function buildSessionStartPlan(verbs) {
6194
6706
  parallel: [
6195
6707
  { name: "rules sync", run: verbs.rulesSync },
6196
6708
  { name: "saga show", run: verbs.sagaShow },
6709
+ { name: "handoff offer", run: verbs.handoffOffer },
6197
6710
  // honcho profile (#1162): the behavioral-memory prior, flushed right after the saga resume. A fast
6198
6711
  // peer-card GET, fail-soft + silent when off/empty — it never hangs or noises the banner.
6199
6712
  { name: "honcho profile", run: verbs.honchoContext },
@@ -6217,12 +6730,12 @@ function spawnDetachedSelf(args, deps) {
6217
6730
  }
6218
6731
  function planStoreLines(cwd) {
6219
6732
  const mdFiles = (dir, minSize = 0) => {
6220
- const p = (0, import_node_path9.join)(cwd, dir);
6221
- if (!(0, import_node_fs10.existsSync)(p)) return [];
6733
+ const p = (0, import_node_path10.join)(cwd, dir);
6734
+ if (!(0, import_node_fs12.existsSync)(p)) return [];
6222
6735
  try {
6223
- return (0, import_node_fs10.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
6736
+ return (0, import_node_fs12.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
6224
6737
  try {
6225
- return (0, import_node_fs10.statSync)((0, import_node_path9.join)(p, f)).size >= minSize;
6738
+ return (0, import_node_fs12.statSync)((0, import_node_path10.join)(p, f)).size >= minSize;
6226
6739
  } catch {
6227
6740
  return false;
6228
6741
  }
@@ -6240,6 +6753,19 @@ function planStoreLines(cwd) {
6240
6753
  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).`);
6241
6754
  return out;
6242
6755
  }
6756
+ function scratchGcLines(cwd, env = process.env, now = Date.now()) {
6757
+ if (env.MMI_NO_AUTO_GC) return [];
6758
+ try {
6759
+ const stamp = scratchGcThrottlePath((0, import_node_path10.join)(cwd, ".mmi"));
6760
+ if (!scratchGcDue(stamp, now)) return [];
6761
+ const run = executeScratchGc(cwd, { apply: true }, now);
6762
+ markScratchGcRun(stamp, now);
6763
+ const line = scratchGcBannerLine(run.applied?.pruned.length ?? 0, run.plan.advisory.length);
6764
+ return line ? [line] : [];
6765
+ } catch {
6766
+ return [];
6767
+ }
6768
+ }
6243
6769
  function northstarPointer(injected = false) {
6244
6770
  if (injected) {
6245
6771
  return "North Stars: `mmi-cli northstar relevant` for more matches; `northstar pull <slug>` for the full SSOT.";
@@ -6287,6 +6813,71 @@ function recoverPriorityFromEvents(events) {
6287
6813
  return found;
6288
6814
  }
6289
6815
 
6816
+ // src/board-dependency.ts
6817
+ var DEPENDS_ON_LINE = /^\s*[-*]?\s*\*\*Depends on:\*\*\s*(.+)$/im;
6818
+ var ISSUE_REF = /([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)#(\d+)/g;
6819
+ function parseDependsOnRefs(body) {
6820
+ const match = body.match(DEPENDS_ON_LINE);
6821
+ if (!match) return [];
6822
+ const refs = [];
6823
+ for (const m of match[1].matchAll(ISSUE_REF)) {
6824
+ refs.push({ repo: m[1], number: Number(m[2]) });
6825
+ }
6826
+ return refs;
6827
+ }
6828
+ async function issueOpen(client, repo, number) {
6829
+ try {
6830
+ const data = await client.rest("GET", `repos/${repo}/issues/${number}`);
6831
+ return data?.state === "OPEN";
6832
+ } catch {
6833
+ return void 0;
6834
+ }
6835
+ }
6836
+ async function dependencyBlocksClaim(client, body) {
6837
+ const refs = parseDependsOnRefs(body);
6838
+ if (!refs.length) return { blocked: false, openDependencies: [] };
6839
+ const openDependencies = [];
6840
+ for (const ref of refs) {
6841
+ const open = await issueOpen(client, ref.repo, ref.number);
6842
+ if (open === true) openDependencies.push(`${ref.repo}#${ref.number}`);
6843
+ }
6844
+ return { blocked: openDependencies.length > 0, openDependencies };
6845
+ }
6846
+ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
6847
+ const claimable = [];
6848
+ const blocked = [];
6849
+ const warnings = [];
6850
+ for (const item of items) {
6851
+ if (item.contentType !== "Issue") {
6852
+ claimable.push(item);
6853
+ continue;
6854
+ }
6855
+ let body = item.details?.body;
6856
+ if (body === void 0) {
6857
+ if (opts.requireBundledBody) {
6858
+ claimable.push(item);
6859
+ continue;
6860
+ }
6861
+ try {
6862
+ const data = await client.rest("GET", `repos/${item.repository}/issues/${item.number}`);
6863
+ body = data?.body ?? "";
6864
+ } catch (e) {
6865
+ warnings.push(`dependency check skipped for ${item.ref}: ${e.message}`);
6866
+ claimable.push(item);
6867
+ continue;
6868
+ }
6869
+ }
6870
+ const gate = await dependencyBlocksClaim(client, body);
6871
+ if (gate.blocked) {
6872
+ blocked.push(item);
6873
+ warnings.push(`${item.ref} blocked on open ${gate.openDependencies.join(", ")}`);
6874
+ } else {
6875
+ claimable.push(item);
6876
+ }
6877
+ }
6878
+ return { claimable, blocked, warnings };
6879
+ }
6880
+
6290
6881
  // ../infra/board-vocab.mjs
6291
6882
  var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
6292
6883
 
@@ -6610,6 +7201,13 @@ async function readBoard(options, deps = {}) {
6610
7201
  if (options.includeBundleDetails) {
6611
7202
  await attachBundleDetails(report, client, options.allowPartial ?? false);
6612
7203
  }
7204
+ for (const scope of ["primary", "secondary"]) {
7205
+ const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client, {
7206
+ requireBundledBody: Boolean(options.includeBundleDetails)
7207
+ });
7208
+ report[scope].claimable = filtered.claimable;
7209
+ report.warnings.push(...filtered.warnings);
7210
+ }
6613
7211
  return report;
6614
7212
  }
6615
7213
  function findBoardItem(items, selector) {
@@ -6691,6 +7289,11 @@ async function prepareClaimContext(options, selectors, deps, collected) {
6691
7289
  warnings: collected.warnings,
6692
7290
  partial: collected.partial
6693
7291
  };
7292
+ for (const scope of ["primary", "secondary"]) {
7293
+ const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client);
7294
+ report[scope].claimable = filtered.claimable;
7295
+ report.warnings.push(...filtered.warnings);
7296
+ }
6694
7297
  return { cfg, client, items: collected.items, writable: writable.repos, report };
6695
7298
  }
6696
7299
  async function claimOneBoardItem(ctx, selector, options) {
@@ -6699,6 +7302,21 @@ async function claimOneBoardItem(ctx, selector, options) {
6699
7302
  if (flatItem.status === "Todo" && flatItem.assignees.length === 0 && !ctx.writable.has(flatItem.repository.toLowerCase())) {
6700
7303
  throw new Error(`${flatItem.ref} is not claimable: viewer does not have write access to ${flatItem.repository}`);
6701
7304
  }
7305
+ if (flatItem.contentType === "Issue") {
7306
+ let body = flatItem.details?.body;
7307
+ if (body === void 0) {
7308
+ try {
7309
+ const data = await client.rest("GET", `repos/${flatItem.repository}/issues/${flatItem.number}`);
7310
+ body = data?.body ?? "";
7311
+ } catch {
7312
+ body = "";
7313
+ }
7314
+ }
7315
+ const gate = await dependencyBlocksClaim(client, body);
7316
+ if (gate.blocked) {
7317
+ throw new Error(`${flatItem.ref} is not claimable: blocked on open ${gate.openDependencies.join(", ")}`);
7318
+ }
7319
+ }
6702
7320
  let item = findClaimableItem(report, selector);
6703
7321
  if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
6704
7322
  const fresh = (await fetchIssueProjectItem(client, cfg, { repo: item.repository, number: item.number })).item;
@@ -6824,6 +7442,73 @@ async function backfillBoardPriorities(options, deps = {}) {
6824
7442
  }
6825
7443
  return result;
6826
7444
  }
7445
+ async function prunePriorityLabels(options, deps = {}) {
7446
+ const cfg = resolveBoardConfig(options.config);
7447
+ const client = deps.client ?? defaultGitHubClient();
7448
+ const collected = await collectBoardItems(cfg, { repo: options.repo }, deps);
7449
+ const issues = collected.items.filter(
7450
+ (item) => item.contentType === "Issue" && item.labels.some((l) => labelToFieldPriority(l))
7451
+ );
7452
+ const concurrency = Math.max(1, options.concurrency ?? 8);
7453
+ const result = {
7454
+ scanned: issues.length,
7455
+ pruned: 0,
7456
+ removedLabels: 0,
7457
+ skippedNoField: 0,
7458
+ failed: 0,
7459
+ details: []
7460
+ };
7461
+ async function work(item) {
7462
+ const priorityLabels = item.labels.filter((l) => labelToFieldPriority(l));
7463
+ if (!priorityLabels.length) return;
7464
+ if (!item.priority) {
7465
+ result.skippedNoField += 1;
7466
+ result.details.push(
7467
+ `${item.ref}: Priority field unset \u2014 kept ${priorityLabels.join(", ")} (run \`mmi-cli board backfill-priority\` first)`
7468
+ );
7469
+ return;
7470
+ }
7471
+ if (options.dryRun) {
7472
+ result.pruned += 1;
7473
+ result.removedLabels += priorityLabels.length;
7474
+ result.details.push(`${item.ref} \u2192 remove ${priorityLabels.join(", ")} (field=${item.priority}) (dry-run)`);
7475
+ return;
7476
+ }
7477
+ try {
7478
+ const removed = [];
7479
+ const alreadyAbsent = [];
7480
+ for (const label of priorityLabels) {
7481
+ try {
7482
+ await client.rest(
7483
+ "DELETE",
7484
+ `repos/${item.repository}/issues/${item.number}/labels/${encodeURIComponent(label)}`
7485
+ );
7486
+ removed.push(label);
7487
+ result.removedLabels += 1;
7488
+ } catch (e) {
7489
+ if (e instanceof GitHubApiError && e.status === 404) {
7490
+ alreadyAbsent.push(label);
7491
+ continue;
7492
+ }
7493
+ throw e;
7494
+ }
7495
+ }
7496
+ result.pruned += 1;
7497
+ const parts = [
7498
+ ...removed.length ? [`removed ${removed.join(", ")}`] : [],
7499
+ ...alreadyAbsent.length ? [`already absent ${alreadyAbsent.join(", ")}`] : []
7500
+ ];
7501
+ result.details.push(`${item.ref} \u2192 ${parts.join("; ")} (field=${item.priority})`);
7502
+ } catch (e) {
7503
+ result.failed += 1;
7504
+ result.details.push(`${item.ref}: ${ghError(e)}`);
7505
+ }
7506
+ }
7507
+ for (let i = 0; i < issues.length; i += concurrency) {
7508
+ await Promise.all(issues.slice(i, i + concurrency).map(work));
7509
+ }
7510
+ return result;
7511
+ }
6827
7512
  async function recoverIssuePriority(client, item) {
6828
7513
  for (const label of item.labels) {
6829
7514
  const fromLabel = labelToFieldPriority(label);
@@ -7082,6 +7767,109 @@ async function runBoardSlice(io, deps, opts) {
7082
7767
  }
7083
7768
  }
7084
7769
 
7770
+ // src/worktree.ts
7771
+ var import_node_fs13 = require("node:fs");
7772
+ var import_node_path11 = require("node:path");
7773
+ var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
7774
+ var PKG = "package.json";
7775
+ var LOCKFILE = "package-lock.json";
7776
+ var NODE_MODULES = "node_modules";
7777
+ var realFsProbe = {
7778
+ isDir: (p) => {
7779
+ try {
7780
+ return (0, import_node_fs13.statSync)(p).isDirectory();
7781
+ } catch {
7782
+ return false;
7783
+ }
7784
+ },
7785
+ isFile: (p) => {
7786
+ try {
7787
+ return (0, import_node_fs13.statSync)(p).isFile();
7788
+ } catch {
7789
+ return false;
7790
+ }
7791
+ },
7792
+ listDirs: (p) => {
7793
+ try {
7794
+ return (0, import_node_fs13.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7795
+ } catch {
7796
+ return [];
7797
+ }
7798
+ }
7799
+ };
7800
+ function scanInstallDirs(root, fs2 = realFsProbe) {
7801
+ const factsFor = (dir) => {
7802
+ const abs = dir ? (0, import_node_path11.join)(root, dir) : root;
7803
+ return {
7804
+ dir,
7805
+ hasPackageJson: fs2.isFile((0, import_node_path11.join)(abs, PKG)),
7806
+ hasLockfile: fs2.isFile((0, import_node_path11.join)(abs, LOCKFILE)),
7807
+ hasNodeModules: fs2.isDir((0, import_node_path11.join)(abs, NODE_MODULES))
7808
+ };
7809
+ };
7810
+ const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
7811
+ return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
7812
+ }
7813
+ function npmInstallTargets(dirs) {
7814
+ return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
7815
+ }
7816
+ function isLinkedWorktree(root, fs2 = realFsProbe) {
7817
+ return fs2.isFile((0, import_node_path11.join)(root, ".git"));
7818
+ }
7819
+ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
7820
+ if (!isLinkedWorktree(root, fs2)) return null;
7821
+ const pending = npmInstallTargets(scanInstallDirs(root, fs2));
7822
+ if (!pending.length) return null;
7823
+ const where = pending.map((t) => t.dir || ".").join(", ");
7824
+ return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
7825
+ }
7826
+ function defaultCopyFile(from, to) {
7827
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(to), { recursive: true });
7828
+ (0, import_node_fs13.copyFileSync)(from, to);
7829
+ }
7830
+ async function provisionWorktree(worktreeRoot, deps) {
7831
+ const fs2 = deps.fs ?? realFsProbe;
7832
+ const copyFile = deps.copyFile ?? defaultCopyFile;
7833
+ const log = deps.log ?? (() => {
7834
+ });
7835
+ const allDirs = scanInstallDirs(worktreeRoot, fs2);
7836
+ const targets = npmInstallTargets(allDirs);
7837
+ const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
7838
+ const installed = [];
7839
+ for (const target of targets) {
7840
+ const cwd = target.dir ? (0, import_node_path11.join)(worktreeRoot, target.dir) : worktreeRoot;
7841
+ log(`installing deps: ${target.command} in ${target.dir || "."}`);
7842
+ await deps.runInstall(target.command, cwd);
7843
+ installed.push(target);
7844
+ }
7845
+ const copied = [];
7846
+ const copySkipped = [];
7847
+ const primary = await deps.primaryCheckout();
7848
+ for (const rel of LOCAL_ONLY_FILES) {
7849
+ const dest = (0, import_node_path11.join)(worktreeRoot, rel);
7850
+ if (fs2.isFile(dest)) {
7851
+ copySkipped.push({ file: rel, reason: "already-present" });
7852
+ continue;
7853
+ }
7854
+ if (!primary) {
7855
+ copySkipped.push({ file: rel, reason: "no-primary" });
7856
+ continue;
7857
+ }
7858
+ if (!fs2.isFile((0, import_node_path11.join)(primary, rel))) {
7859
+ copySkipped.push({ file: rel, reason: "absent-in-primary" });
7860
+ continue;
7861
+ }
7862
+ copyFile((0, import_node_path11.join)(primary, rel), dest);
7863
+ copied.push(rel);
7864
+ log(`copied local config: ${rel}`);
7865
+ }
7866
+ return { worktree: worktreeRoot, installed, skippedInstall, copied, copySkipped };
7867
+ }
7868
+ function defaultWorktreePath(repoRoot, branch) {
7869
+ const safe = branch.replace(/[/\\]+/g, "-");
7870
+ return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot), "mmi-worktrees", safe);
7871
+ }
7872
+
7085
7873
  // src/frontmatter.ts
7086
7874
  function splitFrontmatter(content) {
7087
7875
  const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
@@ -7237,54 +8025,460 @@ function buildNorthstarContextBlock(ranked, signals, readLocal, opts = {}) {
7237
8025
  cards.push(card);
7238
8026
  used += card.length;
7239
8027
  }
7240
- if (!cards.length) return null;
7241
- return [NORTHSTAR_CONTEXT_FRAMING, ...cards].join("\n");
8028
+ if (!cards.length) return null;
8029
+ return [NORTHSTAR_CONTEXT_FRAMING, ...cards].join("\n");
8030
+ }
8031
+ async function withTimeout2(promise, ms) {
8032
+ let timer;
8033
+ try {
8034
+ return await Promise.race([
8035
+ promise,
8036
+ new Promise((_, reject) => {
8037
+ timer = setTimeout(() => reject(new Error("northstar context timeout")), ms);
8038
+ })
8039
+ ]);
8040
+ } finally {
8041
+ if (timer) clearTimeout(timer);
8042
+ }
8043
+ }
8044
+ async function runNorthstarContext(io, deps) {
8045
+ try {
8046
+ const injected = await withTimeout2(
8047
+ (async () => {
8048
+ const plans = await deps.loadPlans();
8049
+ if (!plans.length) return false;
8050
+ const signals = await deps.gatherSignals();
8051
+ const ranked = rankPlansByRelevance(plans, signals);
8052
+ const block = buildNorthstarContextBlock(ranked, signals, deps.readLocal, {
8053
+ anchorSlug: signals.anchorSlug
8054
+ });
8055
+ if (!block) return false;
8056
+ io.log(block);
8057
+ return true;
8058
+ })(),
8059
+ deps.timeoutMs ?? SESSION_START_NORTHSTAR_TIMEOUT_MS
8060
+ );
8061
+ return injected;
8062
+ } catch {
8063
+ return false;
8064
+ }
8065
+ }
8066
+
8067
+ // src/index.ts
8068
+ var import_node_path16 = require("node:path");
8069
+
8070
+ // src/merge-ci-policy.ts
8071
+ function resolveMergeCiPolicy(input) {
8072
+ if (input.registryCi === "none") {
8073
+ return { policy: "no-ci", reason: "registry META ci:none" };
8074
+ }
8075
+ if (input.registryRequiredChecks === null) {
8076
+ return { policy: "no-ci", reason: "registry META requiredChecks:null" };
8077
+ }
8078
+ if (Array.isArray(input.registryRequiredChecks) && input.registryRequiredChecks.length === 0) {
8079
+ return { policy: "no-ci", reason: "registry META requiredChecks:[]" };
8080
+ }
8081
+ const ciWorkflows = input.workflowPaths.filter(
8082
+ (p) => /^\.github\/workflows\/[^/]+\.(ya?ml)$/i.test(p.replace(/\\/g, "/"))
8083
+ );
8084
+ if (!ciWorkflows.length) {
8085
+ return { policy: "no-ci", reason: "no .github/workflows CI" };
8086
+ }
8087
+ return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
8088
+ }
8089
+ function parseGhPrChecksOutput(stdout) {
8090
+ const text = stdout.trim();
8091
+ if (!text) return "no-checks-reported";
8092
+ if (/^no checks reported/i.test(text)) return "no-checks-reported";
8093
+ const lines = text.split(/\r?\n/).filter((l) => l.trim());
8094
+ if (!lines.length) return "no-checks-reported";
8095
+ let anyPending = false;
8096
+ for (const line of lines) {
8097
+ const parts = line.split(/\s+/);
8098
+ const state = (parts[1] ?? "").toLowerCase();
8099
+ if (state === "fail" || state === "failure" || state === "error") return "failure";
8100
+ if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
8101
+ anyPending = true;
8102
+ }
8103
+ return anyPending ? "pending" : "success";
8104
+ }
8105
+ var PR_CHECKS_POLL_MS = 3e4;
8106
+ var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
8107
+ async function waitForPrChecks(deps) {
8108
+ const { policy, reason } = await deps.resolvePolicy();
8109
+ if (policy === "no-ci") {
8110
+ return { policy, status: "skipped", reason };
8111
+ }
8112
+ const now = deps.now ?? (() => Date.now());
8113
+ const deadline = now() + PR_CHECKS_TIMEOUT_MS;
8114
+ let lastDetail = "pending";
8115
+ while (now() < deadline) {
8116
+ const state = await deps.pollChecks();
8117
+ if (state === "success") return { policy, status: "success", detail: lastDetail };
8118
+ if (state === "failure") return { policy, status: "failure", detail: lastDetail };
8119
+ if (state === "no-checks-reported") {
8120
+ lastDetail = "no-checks-reported (waiting for workflow to queue)";
8121
+ await deps.sleep(PR_CHECKS_POLL_MS);
8122
+ continue;
8123
+ }
8124
+ lastDetail = state;
8125
+ await deps.sleep(PR_CHECKS_POLL_MS);
8126
+ }
8127
+ return { policy, status: "timeout", detail: lastDetail };
8128
+ }
8129
+
8130
+ // src/bootstrap-ruleset.ts
8131
+ var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
8132
+ var PRODUCT_GATE_CONTEXT = "gate";
8133
+ function stripRulesetComment(raw) {
8134
+ const parsed = JSON.parse(raw);
8135
+ delete parsed._comment;
8136
+ return parsed;
8137
+ }
8138
+ function rulesetHasGateContext(ruleset, context = PRODUCT_GATE_CONTEXT) {
8139
+ for (const rule of ruleset.rules ?? []) {
8140
+ if (rule.type !== "required_status_checks") continue;
8141
+ for (const check of rule.parameters?.required_status_checks ?? []) {
8142
+ if (check.context === context) return true;
8143
+ }
8144
+ }
8145
+ return false;
8146
+ }
8147
+ function findProductRuleset(rulesets) {
8148
+ return rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
8149
+ }
8150
+ async function activateProductRuleset(repo, rulesetBody, client) {
8151
+ const list = await client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
8152
+ const existing = findProductRuleset(list ?? []);
8153
+ if (existing?.id != null) {
8154
+ const detail = await client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
8155
+ if (detail.enforcement === "active" && rulesetHasGateContext(detail)) {
8156
+ return { action: "skipped", detail: "active ruleset already requires gate" };
8157
+ }
8158
+ await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: rulesetBody, timeoutMs: 2e4 });
8159
+ return { action: "updated", detail: `ruleset ${existing.id}` };
8160
+ }
8161
+ await client.rest("POST", `repos/${repo}/rulesets`, { body: rulesetBody, timeoutMs: 2e4 });
8162
+ return { action: "created" };
8163
+ }
8164
+
8165
+ // src/ci-audit.ts
8166
+ var HUB_REPO = "mutmutco/MMI-Hub";
8167
+ var PRODUCT_GATE_CONTEXT2 = "gate";
8168
+ var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
8169
+ var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
8170
+ var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
8171
+ function slugFromRepo(repo) {
8172
+ return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
8173
+ }
8174
+ function classifyRepo(repo, meta) {
8175
+ if (repo.toLowerCase() === HUB_REPO.toLowerCase()) return "hub";
8176
+ if (meta?.class === "content") return "content";
8177
+ if (meta?.class === "deployable" || meta?.deployModel && meta.deployModel !== "content" && meta.deployModel !== "none") return "deployable";
8178
+ if (meta) return "deployable";
8179
+ return "unknown";
8180
+ }
8181
+ function rulesetStatusChecks(rulesets) {
8182
+ return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
8183
+ }
8184
+ async function restJson(deps, path2, fallback) {
8185
+ try {
8186
+ return await deps.client.rest("GET", path2) ?? fallback;
8187
+ } catch {
8188
+ return fallback;
8189
+ }
8190
+ }
8191
+ async function rulesetDetails(deps, repo, list) {
8192
+ const details = [];
8193
+ for (const ruleset of list) {
8194
+ if (ruleset.id == null || ruleset.rules != null) {
8195
+ details.push(ruleset);
8196
+ continue;
8197
+ }
8198
+ details.push(await restJson(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
8199
+ }
8200
+ return details;
8201
+ }
8202
+ async function contentExists(deps, repo, branch, path2) {
8203
+ try {
8204
+ const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
8205
+ await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
8206
+ return true;
8207
+ } catch {
8208
+ return false;
8209
+ }
8210
+ }
8211
+ function collectRegistryRepos(projects) {
8212
+ const seen = /* @__PURE__ */ new Set();
8213
+ for (const project2 of projects) {
8214
+ for (const raw of project2.repos ?? []) {
8215
+ const repo = raw.includes("/") ? raw : `mutmutco/${raw}`;
8216
+ seen.add(repo);
8217
+ }
8218
+ }
8219
+ if (!seen.has(HUB_REPO)) seen.add(HUB_REPO);
8220
+ return [...seen].sort((a, b) => a.localeCompare(b));
8221
+ }
8222
+ async function resolveRepoMergeCiPolicy(repo, deps) {
8223
+ const meta = await deps.getProjectMeta(slugFromRepo(repo));
8224
+ const repoClass = classifyRepo(repo, meta);
8225
+ if (repoClass === "content") {
8226
+ return resolveMergeCiPolicy({
8227
+ workflowPaths: [],
8228
+ registryCi: meta?.ci ?? "none",
8229
+ registryRequiredChecks: meta?.requiredChecks ?? []
8230
+ });
8231
+ }
8232
+ const baseBranch = "development";
8233
+ const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
8234
+ return resolveMergeCiPolicy({
8235
+ workflowPaths: hasGate ? repoClass === "hub" ? [".github/workflows/gate.yml"] : [PRODUCT_GATE_PATH] : [],
8236
+ registryCi: meta?.ci,
8237
+ registryRequiredChecks: meta?.requiredChecks
8238
+ });
8239
+ }
8240
+ async function auditRepoCi(repo, deps) {
8241
+ const meta = await deps.getProjectMeta(slugFromRepo(repo));
8242
+ const repoClass = classifyRepo(repo, meta);
8243
+ const checks = [];
8244
+ const info = await restJson(deps, `repos/${repo}`, {});
8245
+ const baseBranch = repoClass === "content" ? "main" : "development";
8246
+ checks.push({
8247
+ ok: info.allow_auto_merge === true,
8248
+ label: "allow_auto_merge enabled",
8249
+ detail: info.allow_auto_merge === true ? void 0 : "false or unavailable",
8250
+ remediation: `gh api -X PATCH repos/${repo} -f allow_auto_merge=true -f allow_squash_merge=true -f delete_branch_on_merge=true`
8251
+ });
8252
+ checks.push({
8253
+ ok: info.allow_squash_merge === true,
8254
+ label: "allow_squash_merge enabled",
8255
+ detail: info.allow_squash_merge === true ? void 0 : "false or unavailable"
8256
+ });
8257
+ checks.push({
8258
+ ok: info.delete_branch_on_merge === true,
8259
+ label: "delete_branch_on_merge enabled",
8260
+ detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
8261
+ });
8262
+ const hasGateWorkflow = repoClass === "hub" ? true : repoClass === "content" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
8263
+ if (repoClass === "deployable") {
8264
+ checks.push({
8265
+ ok: hasGateWorkflow,
8266
+ label: "gate workflow committed",
8267
+ detail: hasGateWorkflow ? void 0 : `missing ${PRODUCT_GATE_PATH}`,
8268
+ remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute (seeds gate.yml)`
8269
+ });
8270
+ checks.push({
8271
+ ok: await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF),
8272
+ label: "product ruleset reference committed",
8273
+ detail: `expected ${PRODUCT_RULESET_REF}`,
8274
+ remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute`
8275
+ });
8276
+ }
8277
+ const rulesetList = await restJson(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
8278
+ const rulesets = await rulesetDetails(deps, repo, rulesetList);
8279
+ const activeBranchRulesets = rulesets.filter((r) => r.target === "branch" && r.enforcement === "active");
8280
+ const statusChecks = rulesetStatusChecks(activeBranchRulesets);
8281
+ if (repoClass === "hub") {
8282
+ const missing = HUB_GATE_CONTEXTS.filter((c) => !statusChecks.has(c));
8283
+ checks.push({
8284
+ ok: missing.length === 0,
8285
+ label: "Hub required status checks active",
8286
+ detail: missing.length ? `missing: ${missing.join(", ")}` : void 0
8287
+ });
8288
+ } else if (repoClass === "deployable") {
8289
+ const missing = [PRODUCT_GATE_CONTEXT2].filter((c) => !statusChecks.has(c));
8290
+ checks.push({
8291
+ ok: missing.length === 0,
8292
+ label: "product gate status check active",
8293
+ detail: missing.length ? `missing context: ${PRODUCT_GATE_CONTEXT2} \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset` : void 0,
8294
+ remediation: missing.length ? `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)` : void 0
8295
+ });
8296
+ }
8297
+ const registryCi = meta?.ci;
8298
+ const registryRequiredChecks = meta?.requiredChecks;
8299
+ const workflowPaths = hasGateWorkflow && repoClass === "deployable" ? [PRODUCT_GATE_PATH] : [];
8300
+ const { policy, reason } = resolveMergeCiPolicy({
8301
+ workflowPaths,
8302
+ registryCi,
8303
+ registryRequiredChecks
8304
+ });
8305
+ if (repoClass === "content") {
8306
+ const explicitNoCi = registryCi === "none" || registryRequiredChecks === null || Array.isArray(registryRequiredChecks) && registryRequiredChecks.length === 0;
8307
+ checks.push({
8308
+ ok: explicitNoCi,
8309
+ label: "registry META declares intentional no-ci",
8310
+ detail: explicitNoCi ? void 0 : "set ci:none and requiredChecks:[] in registry META",
8311
+ remediation: `mmi-cli project set ${repo} --var ci=none --var requiredChecks=[]`
8312
+ });
8313
+ } else if (repoClass === "deployable") {
8314
+ checks.push({
8315
+ ok: policy === "wait-for-checks",
8316
+ label: "merge CI policy is wait-for-checks",
8317
+ detail: `${policy} (${reason})`
8318
+ });
8319
+ }
8320
+ const ok = checks.every((c) => c.ok);
8321
+ return { repo, class: repoClass, mergePolicy: policy, ok, checks };
8322
+ }
8323
+ async function auditOrgCi(deps, repoFilter) {
8324
+ const projects = await deps.listProjects();
8325
+ if (!projects) {
8326
+ const single = repoFilter ?? HUB_REPO;
8327
+ const report = await auditRepoCi(single, deps);
8328
+ return { ok: report.ok, repos: [report] };
8329
+ }
8330
+ const targets = repoFilter ? [repoFilter] : collectRegistryRepos(projects);
8331
+ const repos = [];
8332
+ for (const repo of targets) {
8333
+ repos.push(await auditRepoCi(repo, deps));
8334
+ }
8335
+ return { ok: repos.every((r) => r.ok), repos };
8336
+ }
8337
+ function renderCiAuditMarkdown(report) {
8338
+ const lines = [
8339
+ `# CI merge-readiness audit`,
8340
+ "",
8341
+ `Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok).length}/${report.repos.length} repos ready)`,
8342
+ "",
8343
+ "| Repo | Class | Policy | OK | Top gap |",
8344
+ "|------|-------|--------|----|---------|"
8345
+ ];
8346
+ for (const r of report.repos) {
8347
+ const gap = r.checks.find((c) => !c.ok);
8348
+ lines.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap?.label ?? "\u2014"} |`);
8349
+ }
8350
+ return lines.join("\n");
8351
+ }
8352
+ function renderCiAuditText(report) {
8353
+ const lines = [`mmi-cli ci audit: ${report.ok ? "OK" : "GAPS"} (${report.repos.length} repos)`];
8354
+ for (const r of report.repos) {
8355
+ lines.push(`
8356
+ ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${r.ok ? "OK" : "GAP"}`);
8357
+ for (const c of r.checks) {
8358
+ lines.push(` ${c.ok ? "OK" : "FAIL"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
8359
+ }
8360
+ }
8361
+ return lines.join("\n");
7242
8362
  }
7243
- async function withTimeout2(promise, ms) {
7244
- let timer;
8363
+ async function applyCiReconcileMergeSettings(repo, deps) {
8364
+ const report = await auditRepoCi(repo, deps);
8365
+ const applied = [];
8366
+ const skipped = [];
8367
+ const errors = [];
8368
+ const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch"));
8369
+ const needsPatch = mergeChecks.some((c) => !c.ok);
8370
+ if (!needsPatch) {
8371
+ skipped.push("merge settings already canonical");
8372
+ return { repo, applied, skipped, errors };
8373
+ }
7245
8374
  try {
7246
- return await Promise.race([
7247
- promise,
7248
- new Promise((_, reject) => {
7249
- timer = setTimeout(() => reject(new Error("northstar context timeout")), ms);
7250
- })
7251
- ]);
7252
- } finally {
7253
- if (timer) clearTimeout(timer);
8375
+ await deps.client.rest("PATCH", `repos/${repo}`, {
8376
+ body: {
8377
+ allow_auto_merge: true,
8378
+ allow_squash_merge: true,
8379
+ delete_branch_on_merge: true
8380
+ }
8381
+ });
8382
+ applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge");
8383
+ } catch (e) {
8384
+ errors.push(e.message);
7254
8385
  }
8386
+ return { repo, applied, skipped, errors };
7255
8387
  }
7256
- async function runNorthstarContext(io, deps) {
8388
+ async function fetchRulesetSeedBody(deps, repo) {
7257
8389
  try {
7258
- const injected = await withTimeout2(
7259
- (async () => {
7260
- const plans = await deps.loadPlans();
7261
- if (!plans.length) return false;
7262
- const signals = await deps.gatherSignals();
7263
- const ranked = rankPlansByRelevance(plans, signals);
7264
- const block = buildNorthstarContextBlock(ranked, signals, deps.readLocal, {
7265
- anchorSlug: signals.anchorSlug
7266
- });
7267
- if (!block) return false;
7268
- io.log(block);
7269
- return true;
7270
- })(),
7271
- deps.timeoutMs ?? SESSION_START_NORTHSTAR_TIMEOUT_MS
8390
+ const encodedPath = PRODUCT_RULESET_REF.split("/").map(encodeURIComponent).join("/");
8391
+ const file = await deps.client.rest(
8392
+ "GET",
8393
+ `repos/${repo}/contents/${encodedPath}?ref=development`
7272
8394
  );
7273
- return injected;
8395
+ if (file.encoding !== "base64" || typeof file.content !== "string") return null;
8396
+ return Buffer.from(file.content, "base64").toString("utf8");
7274
8397
  } catch {
7275
- return false;
8398
+ return null;
8399
+ }
8400
+ }
8401
+ async function applyCiReconcileRepo(repo, deps) {
8402
+ const merge = await applyCiReconcileMergeSettings(repo, deps);
8403
+ const report = await auditRepoCi(repo, deps);
8404
+ if (report.class !== "deployable") return merge;
8405
+ const gateCheck = report.checks.find((c) => c.label === "product gate status check active");
8406
+ if (gateCheck?.ok) {
8407
+ merge.skipped.push("product ruleset already active");
8408
+ return merge;
8409
+ }
8410
+ const raw = await fetchRulesetSeedBody(deps, repo);
8411
+ if (!raw) {
8412
+ merge.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
8413
+ return merge;
8414
+ }
8415
+ try {
8416
+ const body = stripRulesetComment(raw);
8417
+ const activation = await activateProductRuleset(repo, body, deps.client);
8418
+ if (activation.action === "skipped") merge.skipped.push(activation.detail ?? "product ruleset");
8419
+ else merge.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
8420
+ } catch (e) {
8421
+ merge.errors.push(e.message);
8422
+ }
8423
+ return merge;
8424
+ }
8425
+
8426
+ // src/pr-land.ts
8427
+ var PR_LAND_POLL_MS = 3e4;
8428
+ var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
8429
+ async function runPrLand(prNumber, options, deps) {
8430
+ const repo = await deps.resolveRepo(prNumber, options.repo);
8431
+ const base2 = { status: "failed", repo, pr: prNumber };
8432
+ if (options.requireTrain !== false) {
8433
+ const verdict = await deps.fetchTrainAuthority(repo);
8434
+ if (!verdict.ok) {
8435
+ return { ...base2, error: `train authority unavailable: ${verdict.error}` };
8436
+ }
8437
+ base2.train = { role: verdict.authority.role, train: verdict.authority.train };
8438
+ if (!verdict.authority.train) {
8439
+ return {
8440
+ ...base2,
8441
+ error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
8442
+ };
8443
+ }
8444
+ }
8445
+ const ciPolicy = await deps.resolveCiPolicy(repo);
8446
+ base2.ciPolicy = ciPolicy;
8447
+ const checksWait = await deps.waitForChecks(prNumber, repo);
8448
+ base2.checksWait = checksWait;
8449
+ if (checksWait.status === "failure" || checksWait.status === "timeout") {
8450
+ return {
8451
+ ...base2,
8452
+ error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
8453
+ };
8454
+ }
8455
+ const merge = await deps.mergeAuto(prNumber, repo);
8456
+ base2.mergeStatus = merge.mergeStatus;
8457
+ if (merge.mergeStatus === "failed") {
8458
+ return { ...base2, error: merge.error ?? "merge failed" };
8459
+ }
8460
+ if (merge.mergeStatus === "merged") {
8461
+ return { ...base2, status: "merged" };
8462
+ }
8463
+ const now = deps.now ?? (() => Date.now());
8464
+ const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
8465
+ if (merged) {
8466
+ return { ...base2, status: "auto-merge-enqueued-then-merged", mergeStatus: "merged" };
7276
8467
  }
8468
+ return {
8469
+ ...base2,
8470
+ error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
8471
+ };
7277
8472
  }
7278
8473
 
7279
8474
  // src/index.ts
7280
- var import_node_path14 = require("node:path");
7281
8475
  var import_node_os5 = require("node:os");
7282
8476
 
7283
8477
  // src/gh-create.ts
7284
8478
  var import_promises4 = require("node:fs/promises");
7285
8479
  var import_node_os3 = require("node:os");
7286
- var import_node_path10 = require("node:path");
7287
- var import_node_crypto5 = require("node:crypto");
8480
+ var import_node_path12 = require("node:path");
8481
+ var import_node_crypto6 = require("node:crypto");
7288
8482
  var ISSUE_TYPES = ["bug", "feature", "task"];
7289
8483
  var GH_MUTATION_TIMEOUT_MS = 12e4;
7290
8484
  function timeoutKillNote(err, timeoutMs) {
@@ -7324,7 +8518,7 @@ async function bodyArgsViaFile(args, deps = {}) {
7324
8518
  } };
7325
8519
  const write = deps.write ?? import_promises4.writeFile;
7326
8520
  const remove = deps.remove ?? import_promises4.unlink;
7327
- 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`);
8521
+ const file = (0, import_node_path12.join)(deps.dir ?? (0, import_node_os3.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.md`);
7328
8522
  await write(file, args[i + 1], "utf8");
7329
8523
  return {
7330
8524
  args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
@@ -7614,7 +8808,7 @@ function parentLinkFields(result, error) {
7614
8808
  }
7615
8809
 
7616
8810
  // src/report.ts
7617
- var HUB_REPO = "mutmutco/MMI-Hub";
8811
+ var HUB_REPO2 = "mutmutco/MMI-Hub";
7618
8812
  var REPORT_LABEL = "report";
7619
8813
  function findDuplicateReport(source, openReports, threshold = 0.6) {
7620
8814
  const normalizedTitle = normalizeTitle(source.title);
@@ -7649,7 +8843,7 @@ ${buildReportBody(body, sourceRepo)}`;
7649
8843
 
7650
8844
  // src/skill-lesson.ts
7651
8845
  var SKILL_LESSON_LABEL = "skill-lesson";
7652
- var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
8846
+ var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "handoff", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
7653
8847
  function assertSkillName(name) {
7654
8848
  const match = SKILL_NAMES.find((skill) => skill === name);
7655
8849
  if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
@@ -8149,12 +9343,12 @@ function buildGcPlan(inputs) {
8149
9343
  skipped.push({ branch, reason: "current-branch" });
8150
9344
  continue;
8151
9345
  }
8152
- const worktree = worktrees.get(branch);
8153
- if (worktree?.dirty) {
8154
- skipped.push({ branch, reason: "dirty-worktree", detail: worktree.path });
9346
+ const worktree2 = worktrees.get(branch);
9347
+ if (worktree2?.dirty) {
9348
+ skipped.push({ branch, reason: "dirty-worktree", detail: worktree2.path });
8155
9349
  continue;
8156
9350
  }
8157
- branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath: worktree?.path });
9351
+ branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath: worktree2?.path });
8158
9352
  }
8159
9353
  const trackingRefs = [...new Set(inputs.staleTrackingRefs ?? [])].map((ref) => {
8160
9354
  const branch = branchForTrackingRef(ref, remote);
@@ -8298,6 +9492,28 @@ function selectSafeWorktreeCwd(worktrees, targetPath, options) {
8298
9492
  const exists = options?.pathExists ?? (() => true);
8299
9493
  return worktrees.find((w) => !samePath(w.path, targetPath) && exists(w.path))?.path;
8300
9494
  }
9495
+ function isPathUnderDirectory(childPath, parentPath) {
9496
+ const child = normPath(childPath);
9497
+ const parent = normPath(parentPath);
9498
+ if (!child || !parent) return false;
9499
+ if (child === parent) return true;
9500
+ return child.startsWith(`${parent}/`);
9501
+ }
9502
+ function planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, currentCwd) {
9503
+ if (!safeCwd || !isPathUnderDirectory(currentCwd, targetPath)) return void 0;
9504
+ if (samePath(currentCwd, safeCwd)) return void 0;
9505
+ return safeCwd;
9506
+ }
9507
+ function releaseCwdIfUnderWorktree(targetPath, safeCwd, options) {
9508
+ const getCwd = options?.getCwd ?? (() => process.cwd());
9509
+ const chdir = options?.chdir ?? ((path2) => {
9510
+ process.chdir(path2);
9511
+ });
9512
+ const releaseTo = planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, getCwd());
9513
+ if (!releaseTo) return false;
9514
+ chdir(releaseTo);
9515
+ return true;
9516
+ }
8301
9517
  function branchMissingFromList(branch, stdout) {
8302
9518
  const names = stdout.split(/\r?\n/).map((line) => line.replace(/^\*\s*/, "").trim()).filter(Boolean);
8303
9519
  return !names.includes(branch);
@@ -8350,6 +9566,10 @@ async function cleanupPrMergeLocalBranch(branch, options) {
8350
9566
  stageTeardown = { status: "failed", error: errorMessage(e) };
8351
9567
  }
8352
9568
  }
9569
+ releaseCwdIfUnderWorktree(wtPath, safeCwd, {
9570
+ getCwd: options.getCwd,
9571
+ chdir: options.chdir
9572
+ });
8353
9573
  const outcome = await removeWorktreeWithRecovery(wtPath, {
8354
9574
  git,
8355
9575
  sleep: options.sleep ?? defaultSleep,
@@ -8667,9 +9887,9 @@ function stalePosixFields(config, shell2) {
8667
9887
  }
8668
9888
  function sanitizeLocalStage(local, stale) {
8669
9889
  if (!stale.length) return local;
8670
- const clean3 = { ...local };
8671
- for (const field of stale) delete clean3[field];
8672
- return clean3;
9890
+ const clean4 = { ...local };
9891
+ for (const field of stale) delete clean4[field];
9892
+ return clean4;
8673
9893
  }
8674
9894
  function staleNote(staleFields, outcome) {
8675
9895
  const list = staleFields.join(", ");
@@ -8715,9 +9935,9 @@ function decideStage(inputs) {
8715
9935
 
8716
9936
  // src/cursor-plugin-seed.ts
8717
9937
  var import_node_child_process7 = require("node:child_process");
8718
- var import_node_fs11 = require("node:fs");
9938
+ var import_node_fs14 = require("node:fs");
8719
9939
  var import_node_os4 = require("node:os");
8720
- var import_node_path11 = require("node:path");
9940
+ var import_node_path13 = require("node:path");
8721
9941
  var import_node_util6 = require("node:util");
8722
9942
  function isSemverVersion(v) {
8723
9943
  return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
@@ -8734,17 +9954,17 @@ function ghReleaseTarballApiArgs(tag) {
8734
9954
  }
8735
9955
  function cursorUserGlobalStatePath() {
8736
9956
  if (process.platform === "win32") {
8737
- const base2 = process.env.APPDATA || (0, import_node_path11.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
8738
- return (0, import_node_path11.join)(base2, "Cursor", "User", "globalStorage", "state.vscdb");
9957
+ const base2 = process.env.APPDATA || (0, import_node_path13.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
9958
+ return (0, import_node_path13.join)(base2, "Cursor", "User", "globalStorage", "state.vscdb");
8739
9959
  }
8740
9960
  if (process.platform === "darwin") {
8741
- return (0, import_node_path11.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
9961
+ return (0, import_node_path13.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
8742
9962
  }
8743
- return (0, import_node_path11.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
9963
+ return (0, import_node_path13.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
8744
9964
  }
8745
9965
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
8746
9966
  const dbPath = cursorUserGlobalStatePath();
8747
- if (!(0, import_node_fs11.existsSync)(dbPath)) return void 0;
9967
+ if (!(0, import_node_fs14.existsSync)(dbPath)) return void 0;
8748
9968
  try {
8749
9969
  const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
8750
9970
  timeout: 5e3
@@ -8758,57 +9978,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
8758
9978
  }
8759
9979
  }
8760
9980
  function syncDirContents(src, dest) {
8761
- (0, import_node_fs11.mkdirSync)(dest, { recursive: true });
8762
- for (const name of (0, import_node_fs11.readdirSync)(dest)) {
8763
- (0, import_node_fs11.rmSync)((0, import_node_path11.join)(dest, name), { recursive: true, force: true });
9981
+ (0, import_node_fs14.mkdirSync)(dest, { recursive: true });
9982
+ for (const name of (0, import_node_fs14.readdirSync)(dest)) {
9983
+ (0, import_node_fs14.rmSync)((0, import_node_path13.join)(dest, name), { recursive: true, force: true });
8764
9984
  }
8765
- (0, import_node_fs11.cpSync)(src, dest, { recursive: true });
9985
+ (0, import_node_fs14.cpSync)(src, dest, { recursive: true });
8766
9986
  }
8767
9987
  function releaseTag(releasedVersion) {
8768
9988
  return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
8769
9989
  }
8770
9990
  async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
8771
- const tarFile = (0, import_node_path11.join)(tmpRoot, "archive.tar");
9991
+ const tarFile = (0, import_node_path13.join)(tmpRoot, "archive.tar");
8772
9992
  try {
8773
9993
  await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
8774
9994
  await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
8775
9995
  timeout: 6e4
8776
9996
  });
8777
9997
  await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
8778
- const pluginMmi = (0, import_node_path11.join)(tmpRoot, "plugins", "mmi");
8779
- return (0, import_node_fs11.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
9998
+ const pluginMmi = (0, import_node_path13.join)(tmpRoot, "plugins", "mmi");
9999
+ return (0, import_node_fs14.existsSync)((0, import_node_path13.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
8780
10000
  } catch {
8781
10001
  return void 0;
8782
10002
  }
8783
10003
  }
8784
10004
  async function downloadPluginMmiViaGh(tag, tmpRoot) {
8785
- const tarPath = (0, import_node_path11.join)(tmpRoot, "repo.tgz");
10005
+ const tarPath = (0, import_node_path13.join)(tmpRoot, "repo.tgz");
8786
10006
  try {
8787
- (0, import_node_fs11.mkdirSync)(tmpRoot, { recursive: true });
10007
+ (0, import_node_fs14.mkdirSync)(tmpRoot, { recursive: true });
8788
10008
  const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
8789
10009
  timeout: 12e4,
8790
10010
  maxBuffer: 100 * 1024 * 1024,
8791
10011
  encoding: "buffer",
8792
10012
  windowsHide: true
8793
10013
  });
8794
- (0, import_node_fs11.writeFileSync)(tarPath, stdout);
10014
+ (0, import_node_fs14.writeFileSync)(tarPath, stdout);
8795
10015
  await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
8796
- const top = (0, import_node_fs11.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
10016
+ const top = (0, import_node_fs14.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
8797
10017
  if (!top) return void 0;
8798
- const pluginMmi = (0, import_node_path11.join)(tmpRoot, top, "plugins", "mmi");
8799
- return (0, import_node_fs11.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
10018
+ const pluginMmi = (0, import_node_path13.join)(tmpRoot, top, "plugins", "mmi");
10019
+ return (0, import_node_fs14.existsSync)((0, import_node_path13.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
8800
10020
  } catch {
8801
10021
  return void 0;
8802
10022
  }
8803
10023
  }
8804
10024
  async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
8805
- (0, import_node_fs11.mkdirSync)(tmpRoot, { recursive: true });
10025
+ (0, import_node_fs14.mkdirSync)(tmpRoot, { recursive: true });
8806
10026
  const tag = releaseTag(releasedVersion);
8807
10027
  if (hubCheckout) {
8808
10028
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
8809
10029
  if (fromHub) return fromHub;
8810
10030
  }
8811
- return downloadPluginMmiViaGh(tag, (0, import_node_path11.join)(tmpRoot, "gh"));
10031
+ return downloadPluginMmiViaGh(tag, (0, import_node_path13.join)(tmpRoot, "gh"));
8812
10032
  }
8813
10033
  function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
8814
10034
  if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
@@ -8829,7 +10049,7 @@ async function applyCursorPluginCacheSeed(input) {
8829
10049
  for (const pin of pinsToSeed) {
8830
10050
  syncDirContents(source, pin.path);
8831
10051
  }
8832
- (0, import_node_fs11.rmSync)(tmpRoot, { recursive: true, force: true });
10052
+ (0, import_node_fs14.rmSync)(tmpRoot, { recursive: true, force: true });
8833
10053
  return true;
8834
10054
  }
8835
10055
 
@@ -8877,8 +10097,12 @@ var MANAGED_GITIGNORE_LINES = [
8877
10097
  ".claude/worktrees/",
8878
10098
  ".mmi/.session",
8879
10099
  ".mmi/head-ts/",
8880
- // Recursive so any reuse of the parameterized queue engine in a new .mmi subdir (honcho today, others
8881
- // later) is ignored too `**` matches zero dirs, so this still covers the root saga queue.
10100
+ // Honcho's whole runtime dir its ingest-throttle stamp (`last-skip.json`) is NOT a queue file, so the
10101
+ // recursive patterns below miss it and it dirtied the working tree (blocked a release clean-tree check,
10102
+ // #1472). The dir is pure scratch, so ignore it wholesale, like `.mmi/head-ts/`.
10103
+ ".mmi/honcho/",
10104
+ // Recursive so any reuse of the parameterized queue engine in a new .mmi subdir is ignored too —
10105
+ // `**` matches zero dirs, so this still covers the root saga queue.
8882
10106
  ".mmi/**/saga-pending.jsonl*",
8883
10107
  ".mmi/**/saga-flush.lock",
8884
10108
  ".aws-sam/",
@@ -9092,6 +10316,12 @@ function cachePathJoin(root, ...parts) {
9092
10316
  const sep = root.includes("\\") ? "\\" : "/";
9093
10317
  return [root.replace(/[\\/]+$/, ""), ...parts].join(sep);
9094
10318
  }
10319
+ function cacheParentDir(root) {
10320
+ const sep = root.includes("\\") ? "\\" : "/";
10321
+ const trimmed = root.replace(/[\\/]+$/, "");
10322
+ const idx = trimmed.lastIndexOf(sep);
10323
+ return idx <= 0 ? root : trimmed.slice(0, idx);
10324
+ }
9095
10325
  function isProtectedCacheDir(name, protectedVersions) {
9096
10326
  const normalized = normalizeVersion(name);
9097
10327
  return name.startsWith(".") || normalized !== void 0 && protectedVersions.has(normalized);
@@ -9122,7 +10352,7 @@ function buildMmiPluginCacheCleanupCheck(input) {
9122
10352
  plannedCount: leftovers.length,
9123
10353
  quarantinePlan: leftovers.map((entry) => ({
9124
10354
  from: entry.path,
9125
- to: cachePathJoin(entry.root, ".mmi-quarantine", stamp, entry.name)
10355
+ to: cachePathJoin(cacheParentDir(entry.root), ".mmi-quarantine", stamp, entry.name)
9126
10356
  }))
9127
10357
  };
9128
10358
  }
@@ -9183,6 +10413,12 @@ var CLAUDE_PLUGIN_HEAL_STEPS = [
9183
10413
  { args: ["plugin", "install", "mmi@mmi"], gated: true },
9184
10414
  { args: ["plugin", "enable", "mmi@mmi"], gated: false }
9185
10415
  ];
10416
+ var CODEX_PLUGIN_RECOVERY = "codex plugin marketplace remove mmi && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mmi";
10417
+ var CODEX_PLUGIN_HEAL_STEPS = [
10418
+ { args: ["plugin", "marketplace", "remove", "mmi"], gated: false },
10419
+ { args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
10420
+ { args: ["plugin", "add", "mmi@mmi"], gated: true }
10421
+ ];
9186
10422
  function healStepAborts(step, ok) {
9187
10423
  return !ok && step.gated;
9188
10424
  }
@@ -9193,7 +10429,7 @@ function pluginRecoveryFix(surface) {
9193
10429
  case "claude-cli":
9194
10430
  return `${claude} # then ${reloadAction(surface)} to reload MMI commands`;
9195
10431
  case "codex":
9196
- return "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi # then restart Codex";
10432
+ return `${CODEX_PLUGIN_RECOVERY} # then ${reloadAction(surface)}`;
9197
10433
  case "cursor":
9198
10434
  return `in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace; then ${reloadAction(surface)} to reload MMI skills + hooks`;
9199
10435
  case "shell":
@@ -9203,7 +10439,7 @@ function pluginRecoveryFix(surface) {
9203
10439
  }
9204
10440
  var PLUGIN_UPDATE_RECIPES = {
9205
10441
  claude: [CLAUDE_PLUGIN_RECOVERY],
9206
- codex: ["codex plugin marketplace upgrade mmi", "codex plugin list # verify mmi@mmi shows the new version"],
10442
+ codex: [CODEX_PLUGIN_RECOVERY, "codex plugin list # verify mmi@mmi shows the released version"],
9207
10443
  cli: ["npm install -g @mutmutco/cli@latest"]
9208
10444
  };
9209
10445
  function highestSemver(versions) {
@@ -9257,7 +10493,7 @@ function isSemverVersion2(v) {
9257
10493
  return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
9258
10494
  }
9259
10495
  function staleRecordCommand(surface) {
9260
- return surface === "codex" ? "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi" : CLAUDE_PLUGIN_RECOVERY;
10496
+ return surface === "codex" ? CODEX_PLUGIN_RECOVERY : CLAUDE_PLUGIN_RECOVERY;
9261
10497
  }
9262
10498
  function staleSurfacesFix(stale, releasedVersion) {
9263
10499
  const parts = stale.map((s) => {
@@ -9535,8 +10771,8 @@ async function runStageLiveDown(deps, t) {
9535
10771
 
9536
10772
  // src/stage-runner.ts
9537
10773
  var import_node_child_process8 = require("node:child_process");
9538
- var import_node_fs12 = require("node:fs");
9539
- var import_node_path12 = require("node:path");
10774
+ var import_node_fs15 = require("node:fs");
10775
+ var import_node_path14 = require("node:path");
9540
10776
  var import_node_net2 = require("node:net");
9541
10777
  var import_node_util7 = require("node:util");
9542
10778
  var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process8.execFile);
@@ -9581,7 +10817,28 @@ function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
9581
10817
  return void 0;
9582
10818
  }
9583
10819
  function stageStatePath(cwd = process.cwd()) {
9584
- return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
10820
+ return (0, import_node_path14.join)(cwd, "tmp", "stage", "state.json");
10821
+ }
10822
+ function mergeEnvSecretsIntoFile(content, secrets2) {
10823
+ const lines = content.split(/\r?\n/);
10824
+ const indexByKey = /* @__PURE__ */ new Map();
10825
+ for (let i = 0; i < lines.length; i++) {
10826
+ const trimmed = lines[i].trim();
10827
+ if (!trimmed || trimmed.startsWith("#")) continue;
10828
+ const eq = trimmed.indexOf("=");
10829
+ if (eq === -1) continue;
10830
+ indexByKey.set(trimmed.slice(0, eq).trim(), i);
10831
+ }
10832
+ for (const [key, value] of Object.entries(secrets2)) {
10833
+ const escaped = /[\s#"'\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
10834
+ const line = `${key}=${escaped}`;
10835
+ const idx = indexByKey.get(key);
10836
+ if (idx != null) lines[idx] = line;
10837
+ else lines.push(line);
10838
+ }
10839
+ const body = lines.join("\n");
10840
+ return body.endsWith("\n") ? body : `${body}
10841
+ `;
9585
10842
  }
9586
10843
  var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
9587
10844
  function posixOnlyShellProblems(command, field, platform = process.platform) {
@@ -9643,9 +10900,9 @@ async function shell(command, cwd, timeoutMs) {
9643
10900
  });
9644
10901
  }
9645
10902
  function readState(path2) {
9646
- if (!(0, import_node_fs12.existsSync)(path2)) return null;
10903
+ if (!(0, import_node_fs15.existsSync)(path2)) return null;
9647
10904
  try {
9648
- return JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
10905
+ return JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf8"));
9649
10906
  } catch {
9650
10907
  return null;
9651
10908
  }
@@ -9697,7 +10954,7 @@ async function stopStage(opts = {}) {
9697
10954
  return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
9698
10955
  }
9699
10956
  await killTree(state.pid);
9700
- (0, import_node_fs12.rmSync)(statePath, { force: true });
10957
+ (0, import_node_fs15.rmSync)(statePath, { force: true });
9701
10958
  return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
9702
10959
  }
9703
10960
  async function startStage(config = {}, opts = {}) {
@@ -9706,7 +10963,7 @@ async function startStage(config = {}, opts = {}) {
9706
10963
  const cwd = opts.cwd ?? process.cwd();
9707
10964
  const statePath = opts.statePath ?? stageStatePath(cwd);
9708
10965
  const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
9709
- (0, import_node_fs12.mkdirSync)(dir, { recursive: true });
10966
+ (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
9710
10967
  let stagePort;
9711
10968
  if (config.portRange) {
9712
10969
  const [s, e] = config.portRange;
@@ -9716,14 +10973,14 @@ async function startStage(config = {}, opts = {}) {
9716
10973
  }
9717
10974
  const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
9718
10975
  if (config.ensureEnv) {
9719
- const target = (0, import_node_path12.join)(cwd, config.ensureEnv.target);
9720
- const example = (0, import_node_path12.join)(cwd, config.ensureEnv.example);
9721
- if (!(0, import_node_fs12.existsSync)(target) && (0, import_node_fs12.existsSync)(example)) {
9722
- (0, import_node_fs12.copyFileSync)(example, target);
9723
- } else if ((0, import_node_fs12.existsSync)(target) && (0, import_node_fs12.existsSync)(example)) {
9724
- const stale = detectStaleEnvFile((0, import_node_fs12.readFileSync)(example, "utf8"), (0, import_node_fs12.readFileSync)(target, "utf8"), {
9725
- exampleMtimeMs: (0, import_node_fs12.statSync)(example).mtimeMs,
9726
- targetMtimeMs: (0, import_node_fs12.statSync)(target).mtimeMs
10976
+ const target = (0, import_node_path14.join)(cwd, config.ensureEnv.target);
10977
+ const example = (0, import_node_path14.join)(cwd, config.ensureEnv.example);
10978
+ if (!(0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
10979
+ (0, import_node_fs15.copyFileSync)(example, target);
10980
+ } else if ((0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
10981
+ const stale = detectStaleEnvFile((0, import_node_fs15.readFileSync)(example, "utf8"), (0, import_node_fs15.readFileSync)(target, "utf8"), {
10982
+ exampleMtimeMs: (0, import_node_fs15.statSync)(example).mtimeMs,
10983
+ targetMtimeMs: (0, import_node_fs15.statSync)(target).mtimeMs
9727
10984
  });
9728
10985
  if (stale) {
9729
10986
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -9731,6 +10988,9 @@ async function startStage(config = {}, opts = {}) {
9731
10988
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
9732
10989
  }
9733
10990
  }
10991
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs15.existsSync)(target)) {
10992
+ (0, import_node_fs15.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs15.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
10993
+ }
9734
10994
  }
9735
10995
  const extraEnv = {};
9736
10996
  for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
@@ -9754,13 +11014,13 @@ async function startStage(config = {}, opts = {}) {
9754
11014
  healthUrl: sub(config.healthUrl?.trim()) || void 0,
9755
11015
  port: stagePort
9756
11016
  };
9757
- (0, import_node_fs12.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
11017
+ (0, import_node_fs15.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
9758
11018
  try {
9759
11019
  if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
9760
11020
  else await waitForProcessStability(child);
9761
11021
  } catch (e) {
9762
11022
  await killTree(state.pid);
9763
- (0, import_node_fs12.rmSync)(statePath, { force: true });
11023
+ (0, import_node_fs15.rmSync)(statePath, { force: true });
9764
11024
  throw e;
9765
11025
  }
9766
11026
  const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
@@ -9876,7 +11136,7 @@ function resolveDeployModel2(meta, repo) {
9876
11136
  if (isDeployModel(m)) return m;
9877
11137
  return resolveDeployModel(meta, repo);
9878
11138
  }
9879
- function clean(out) {
11139
+ function clean2(out) {
9880
11140
  return out.trim();
9881
11141
  }
9882
11142
  function requireValue(value, label) {
@@ -9927,11 +11187,11 @@ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedS
9927
11187
  if (release.targetCommitish !== expectedTarget) {
9928
11188
  throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
9929
11189
  }
9930
- const tagSha = requireValue(clean(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
11190
+ const tagSha = requireValue(clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
9931
11191
  if (tagSha !== expectedSha) {
9932
11192
  throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
9933
11193
  }
9934
- const branchOut = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
11194
+ const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
9935
11195
  const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
9936
11196
  if (branchSha !== expectedSha) {
9937
11197
  throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
@@ -9991,7 +11251,7 @@ function ensurePositiveCount(out, emptyMessage) {
9991
11251
  if (count <= 0) throw new Error(emptyMessage);
9992
11252
  }
9993
11253
  async function remoteBranchExists(deps, branch) {
9994
- const out = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
11254
+ const out = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
9995
11255
  return out.length > 0;
9996
11256
  }
9997
11257
  async function loadReleaseTrackBranchHints(deps) {
@@ -10003,10 +11263,10 @@ async function loadReleaseTrackBranchHints(deps) {
10003
11263
  return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
10004
11264
  }
10005
11265
  async function buildTrainApplyContext(deps) {
10006
- const repo = requireValue(clean(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
11266
+ const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
10007
11267
  const [owner, name] = repo.split("/");
10008
11268
  if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
10009
- const login = requireValue(clean(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
11269
+ const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
10010
11270
  const verdict = await deps.trainAuthority(repo);
10011
11271
  if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
10012
11272
  if (!verdict.train) {
@@ -10027,12 +11287,12 @@ async function requireCleanTree(deps) {
10027
11287
  if (status.trim()) throw new Error("working tree must be clean before train apply");
10028
11288
  }
10029
11289
  async function requireBranch(deps, branch) {
10030
- const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
11290
+ const current = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
10031
11291
  if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
10032
11292
  }
10033
- var HUB_REPO2 = "mutmutco/MMI-Hub";
11293
+ var HUB_REPO3 = "mutmutco/MMI-Hub";
10034
11294
  function isHubControlRepo(repo) {
10035
- return repo.toLowerCase() === HUB_REPO2.toLowerCase();
11295
+ return repo.toLowerCase() === HUB_REPO3.toLowerCase();
10036
11296
  }
10037
11297
  function projectGetFailureText(e) {
10038
11298
  const err = e;
@@ -10092,7 +11352,7 @@ async function correlateDispatchedRun(deps, workflow, since) {
10092
11352
  "run",
10093
11353
  "list",
10094
11354
  "--repo",
10095
- HUB_REPO2,
11355
+ HUB_REPO3,
10096
11356
  "--workflow",
10097
11357
  workflow,
10098
11358
  "--limit",
@@ -10126,7 +11386,7 @@ async function correlateWorkflowRun(deps, args) {
10126
11386
  "run",
10127
11387
  "list",
10128
11388
  "--repo",
10129
- HUB_REPO2,
11389
+ HUB_REPO3,
10130
11390
  "--workflow",
10131
11391
  args.workflow,
10132
11392
  "--event",
@@ -10154,7 +11414,7 @@ async function correlateWorkflowRun(deps, args) {
10154
11414
  async function watchTenantRun(deps, runId) {
10155
11415
  if (runId == null) return "pending";
10156
11416
  try {
10157
- await deps.run("gh", ["run", "watch", String(runId), "--repo", HUB_REPO2, "--exit-status"]);
11417
+ await deps.run("gh", ["run", "watch", String(runId), "--repo", HUB_REPO3, "--exit-status"]);
10158
11418
  return "success";
10159
11419
  } catch {
10160
11420
  return "failure";
@@ -10196,7 +11456,7 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
10196
11456
  return [...contexts];
10197
11457
  }
10198
11458
  async function findOpenAlignmentPr(deps, ctx) {
10199
- const out = clean(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
11459
+ const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
10200
11460
  if (!out) return void 0;
10201
11461
  const rows = JSON.parse(out);
10202
11462
  const row = rows.find((r) => typeof r.number === "number" && typeof r.url === "string");
@@ -10224,14 +11484,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
10224
11484
  note: `alignment PR already open: ${existing.url} \u2014 land it with \`gh pr merge ${existing.number} --merge\``
10225
11485
  };
10226
11486
  }
10227
- const ahead = clean(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
11487
+ const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
10228
11488
  if (ahead === "0") {
10229
11489
  return { status: "aligned", note: "development already contains the released main; nothing to roll forward" };
10230
11490
  }
10231
11491
  const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
10232
11492
 
10233
11493
  \`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.`;
10234
- 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]));
11494
+ 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]));
10235
11495
  const number = parsePrNumber(url);
10236
11496
  return {
10237
11497
  status: "pr-pending",
@@ -10297,10 +11557,10 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
10297
11557
  }
10298
11558
  async function ensureTagPushed(deps, tag, sha) {
10299
11559
  const remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]);
10300
- const remoteSha = clean(remoteOut).split(/\s+/)[0] || "";
11560
+ const remoteSha = clean2(remoteOut).split(/\s+/)[0] || "";
10301
11561
  let localSha = "";
10302
11562
  try {
10303
- localSha = clean(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
11563
+ localSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
10304
11564
  } catch {
10305
11565
  }
10306
11566
  if (remoteSha) {
@@ -10323,7 +11583,7 @@ async function ensureTagPushed(deps, tag, sha) {
10323
11583
  }
10324
11584
  async function resolveRcResumeTag(deps, base2, sha) {
10325
11585
  const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base2}-rc.*`]);
10326
- 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));
11586
+ 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));
10327
11587
  const unique = [...new Set(onSha)];
10328
11588
  if (unique.length === 0) return { tag: null };
10329
11589
  const rcNum = (t) => Number.parseInt(t.replace(/^.*-rc\./, ""), 10);
@@ -10375,7 +11635,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
10375
11635
  "run",
10376
11636
  "tenant-publish.yml",
10377
11637
  "--repo",
10378
- HUB_REPO2,
11638
+ HUB_REPO3,
10379
11639
  "-f",
10380
11640
  `slug=${ctx.slug}`,
10381
11641
  "-f",
@@ -10448,13 +11708,13 @@ async function runTrainApply(command, deps, options = {}) {
10448
11708
  "nothing to promote: origin/development is not ahead of origin/rc"
10449
11709
  );
10450
11710
  const deployModel2 = await preflight(deps, ctx, "rc", meta);
10451
- const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
11711
+ const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
10452
11712
  await deps.run("git", ["checkout", "rc"]);
10453
11713
  await ffOnlyPull(deps, "rc");
10454
11714
  await deps.run("git", ["merge", "development", "--no-edit"]);
10455
- const rcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
11715
+ const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
10456
11716
  const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
10457
- const tag2 = resume.tag ?? requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
11717
+ const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
10458
11718
  const resumeNote = resume.tag ? resume.note : void 0;
10459
11719
  await ensureTagPushed(deps, tag2, rcSha);
10460
11720
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
@@ -10472,7 +11732,7 @@ async function runTrainApply(command, deps, options = {}) {
10472
11732
  "nothing to release: origin/development is not ahead of origin/main"
10473
11733
  );
10474
11734
  const deployModel2 = await preflight(deps, ctx, "main", meta);
10475
- const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
11735
+ const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
10476
11736
  const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
10477
11737
  const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
10478
11738
  const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
@@ -10490,12 +11750,12 @@ async function runTrainApply(command, deps, options = {}) {
10490
11750
  await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
10491
11751
  }
10492
11752
  const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
10493
- const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
11753
+ const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
10494
11754
  await ensureTagPushed(deps, tag2, releaseSha2);
10495
11755
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
10496
11756
  const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
10497
11757
  await deps.run("git", ["push", "origin", "main"]);
10498
- const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
11758
+ const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
10499
11759
  await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
10500
11760
  const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
10501
11761
  const autoRunSince2 = (deps.now ?? Date.now)();
@@ -10544,8 +11804,8 @@ async function runTrainApply(command, deps, options = {}) {
10544
11804
  "nothing to release: origin/development is not ahead of origin/main"
10545
11805
  );
10546
11806
  const deployModel2 = await preflight(deps, ctx, "main", meta);
10547
- const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
10548
- const rcShaAtRelease = hasRcBranch ? clean(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
11807
+ const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
11808
+ const rcShaAtRelease = hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
10549
11809
  const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
10550
11810
  const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
10551
11811
  const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
@@ -10563,12 +11823,12 @@ async function runTrainApply(command, deps, options = {}) {
10563
11823
  await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
10564
11824
  }
10565
11825
  const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
10566
- const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
11826
+ const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
10567
11827
  await ensureTagPushed(deps, tag2, releaseSha2);
10568
11828
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
10569
11829
  const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
10570
11830
  await deps.run("git", ["push", "origin", "main"]);
10571
- const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
11831
+ const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
10572
11832
  await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
10573
11833
  const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
10574
11834
  const autoRunSince2 = (deps.now ?? Date.now)();
@@ -10635,7 +11895,7 @@ async function runTrainApply(command, deps, options = {}) {
10635
11895
  `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].`
10636
11896
  );
10637
11897
  }
10638
- const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
11898
+ const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
10639
11899
  await deps.run("git", ["checkout", "main"]);
10640
11900
  await ffOnlyPull(deps, "main");
10641
11901
  if (predicted.length === 0) {
@@ -10643,15 +11903,15 @@ async function runTrainApply(command, deps, options = {}) {
10643
11903
  } else {
10644
11904
  await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs", tolerated);
10645
11905
  }
10646
- const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
11906
+ const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
10647
11907
  const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
10648
- const releaseSha = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
11908
+ const releaseSha = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
10649
11909
  await ensureTagPushed(deps, tag, releaseSha);
10650
11910
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
10651
11911
  const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
10652
11912
  await deps.run("git", ["push", "origin", "main"]);
10653
11913
  const autoRunSince = (deps.now ?? Date.now)();
10654
- const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
11914
+ const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
10655
11915
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
10656
11916
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
10657
11917
  const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report");
@@ -10779,7 +12039,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
10779
12039
  }
10780
12040
  try {
10781
12041
  await deps.run("git", ["fetch", "origin", "rc"]);
10782
- const rcNow = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
12042
+ const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
10783
12043
  if (rcNow !== releasedRcSha) {
10784
12044
  return {
10785
12045
  status: "skipped",
@@ -10811,7 +12071,7 @@ async function runTenantRedeploy(deps, options) {
10811
12071
  const repo = options.repo;
10812
12072
  const [owner, name] = repo.split("/");
10813
12073
  if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
10814
- const login = requireValue(clean(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
12074
+ const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
10815
12075
  const verdict = await deps.trainAuthority(repo);
10816
12076
  if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
10817
12077
  if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
@@ -10967,7 +12227,7 @@ var HOTFIX_RUN_FIND_ATTEMPTS = 10;
10967
12227
  var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
10968
12228
  var HOTFIX_VERIFY_ATTEMPTS = 5;
10969
12229
  var HOTFIX_VERIFY_RETRY_MS = 6e3;
10970
- function clean2(out) {
12230
+ function clean3(out) {
10971
12231
  return out.trim();
10972
12232
  }
10973
12233
  function sleeper(deps) {
@@ -11025,7 +12285,7 @@ async function resolveHotfixSource(deps, ctx, from) {
11025
12285
  if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
11026
12286
  return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
11027
12287
  }
11028
- const sha = clean2(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
12288
+ const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
11029
12289
  if (!sha) throw new Error(`could not resolve commit ${from}`);
11030
12290
  return { sha, label: sha.slice(0, 7) };
11031
12291
  }
@@ -11053,7 +12313,7 @@ async function runHotfixStart(deps, options) {
11053
12313
  };
11054
12314
  }
11055
12315
  const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
11056
- const remoteBranch = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
12316
+ const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
11057
12317
  if (remoteBranch) {
11058
12318
  await deps.run("git", ["checkout", branch]);
11059
12319
  const isRulesSource2 = deps.isRulesSource ? await deps.isRulesSource() : false;
@@ -11086,7 +12346,7 @@ async function runHotfixStart(deps, options) {
11086
12346
  await deps.run("git", ["push", "-u", "origin", branch]);
11087
12347
  }
11088
12348
  const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : "";
11089
- const prUrl = clean2(await deps.run("gh", [
12349
+ const prUrl = clean3(await deps.run("gh", [
11090
12350
  "pr",
11091
12351
  "create",
11092
12352
  "--repo",
@@ -11206,7 +12466,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
11206
12466
  }
11207
12467
  let verifyNote;
11208
12468
  if (deployModel === "hub-serverless") {
11209
- const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
12469
+ const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
11210
12470
  const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
11211
12471
  try {
11212
12472
  await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
@@ -11291,9 +12551,9 @@ async function runHotfixStatus(deps, versionInput) {
11291
12551
  return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
11292
12552
  }
11293
12553
  async function gatherHotfixFacts(deps, ctx, tag, version) {
11294
- const branchExists = Boolean(clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
12554
+ const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
11295
12555
  const pr2 = await findHotfixPr(deps, ctx, tag);
11296
- const remoteTag = clean2(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
12556
+ const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
11297
12557
  const tagPushed = Boolean(remoteTag);
11298
12558
  const tagSha = remoteTag.split(/\s+/)[0] || "";
11299
12559
  let releaseExists = false;
@@ -11327,7 +12587,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
11327
12587
  }
11328
12588
  }
11329
12589
  }
11330
- const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean2, () => "unknown");
12590
+ const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
11331
12591
  return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
11332
12592
  }
11333
12593
  async function findInFlightHotfixVersion(deps, ctx) {
@@ -11350,7 +12610,7 @@ async function findInFlightHotfixVersion(deps, ctx) {
11350
12610
  const m = typeof row.headRefName === "string" && /^hotfix\/(v\d+\.\d+\.\d+)/.exec(row.headRefName);
11351
12611
  if (m) tags.add(m[1]);
11352
12612
  }
11353
- const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
12613
+ const branchOut = clean3(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
11354
12614
  for (const line of branchOut.split("\n").filter(Boolean)) {
11355
12615
  const ref = line.split(/\s+/)[1] ?? "";
11356
12616
  const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
@@ -11499,7 +12759,7 @@ async function announceRelease(deps, args) {
11499
12759
  }
11500
12760
 
11501
12761
  // src/port-registry.ts
11502
- var import_node_fs13 = require("node:fs");
12762
+ var import_node_fs16 = require("node:fs");
11503
12763
 
11504
12764
  // ../infra/port-geometry.mjs
11505
12765
  var PORT_BLOCK = 100;
@@ -11513,8 +12773,8 @@ function nextPortBlock(registry2) {
11513
12773
  return [base2, base2 + PORT_SPAN];
11514
12774
  }
11515
12775
  function loadPortRegistry(path2) {
11516
- if (!(0, import_node_fs13.existsSync)(path2)) return {};
11517
- const raw = JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
12776
+ if (!(0, import_node_fs16.existsSync)(path2)) return {};
12777
+ const raw = JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
11518
12778
  const out = {};
11519
12779
  for (const [key, value] of Object.entries(raw)) {
11520
12780
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -11528,9 +12788,9 @@ function ensurePortRange(repo, path2) {
11528
12788
  const existing = registry2[repo];
11529
12789
  if (existing) return existing;
11530
12790
  const range = nextPortBlock(registry2);
11531
- const raw = (0, import_node_fs13.existsSync)(path2) ? JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8")) : {};
12791
+ const raw = (0, import_node_fs16.existsSync)(path2) ? JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8")) : {};
11532
12792
  raw[repo] = range;
11533
- (0, import_node_fs13.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
12793
+ (0, import_node_fs16.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
11534
12794
  return range;
11535
12795
  }
11536
12796
  function portCursorSeed(registry2) {
@@ -11582,7 +12842,7 @@ function safeJson(text, fallback) {
11582
12842
  return fallback;
11583
12843
  }
11584
12844
  }
11585
- async function restJson(deps, path2, fallback) {
12845
+ async function restJson2(deps, path2, fallback) {
11586
12846
  try {
11587
12847
  return await deps.client.rest("GET", path2) ?? fallback;
11588
12848
  } catch {
@@ -11709,7 +12969,7 @@ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /*
11709
12969
  return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
11710
12970
  }
11711
12971
  async function auditOrgBasePermission(deps) {
11712
- const org = await restJson(deps, `orgs/${OWNER2}`, {});
12972
+ const org = await restJson2(deps, `orgs/${OWNER2}`, {});
11713
12973
  const perm = org.default_repository_permission;
11714
12974
  if (perm && perm !== "read" && perm !== "none") {
11715
12975
  return [{
@@ -11818,8 +13078,8 @@ var requiredIssueTemplates = [
11818
13078
  var requiredWorkflows = [];
11819
13079
  var requiredProductWorkflows = [".github/workflows/gate.yml"];
11820
13080
  var requiredProductRulesetRef = ".github/rulesets/mmi-product-required-checks.json";
11821
- var HUB_REPO3 = "mutmutco/MMI-Hub";
11822
- var requiredLabels = ["bug", "feature", "task", "priority:urgent", "priority:high", "priority:medium", "priority:low"];
13081
+ var HUB_REPO4 = "mutmutco/MMI-Hub";
13082
+ var requiredLabels = ["bug", "feature", "task"];
11823
13083
  var requiredPriorityOptions = ["Urgent", "High", "Medium", "Low"];
11824
13084
  var strayDefaultLabels = ["documentation", "duplicate", "enhancement", "good first issue", "help wanted", "invalid", "question", "wontfix"];
11825
13085
  var requiredStatusOptions = ["Todo", "In Progress", "In Review", "Done"];
@@ -11858,7 +13118,7 @@ function safeJson2(text, fallback) {
11858
13118
  return fallback;
11859
13119
  }
11860
13120
  }
11861
- async function restJson2(deps, path2, fallback) {
13121
+ async function restJson3(deps, path2, fallback) {
11862
13122
  try {
11863
13123
  return await deps.client.rest("GET", path2) ?? fallback;
11864
13124
  } catch {
@@ -11872,7 +13132,7 @@ async function restPagedJson2(deps, path2, fallback) {
11872
13132
  return fallback;
11873
13133
  }
11874
13134
  }
11875
- async function contentExists(deps, repo, branch, path2) {
13135
+ async function contentExists2(deps, repo, branch, path2) {
11876
13136
  try {
11877
13137
  const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
11878
13138
  await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
@@ -11917,17 +13177,17 @@ function missingRuleTypes(ruleset, required) {
11917
13177
  const types = new Set((ruleset.rules || []).map((rule) => rule.type).filter(Boolean));
11918
13178
  return required.filter((type) => !types.has(type));
11919
13179
  }
11920
- function rulesetStatusChecks(rulesets) {
13180
+ function rulesetStatusChecks2(rulesets) {
11921
13181
  return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
11922
13182
  }
11923
- async function rulesetDetails(deps, repo, list) {
13183
+ async function rulesetDetails2(deps, repo, list) {
11924
13184
  const details = [];
11925
13185
  for (const ruleset of list) {
11926
13186
  if (ruleset.id == null || ruleset.rules != null) {
11927
13187
  details.push(ruleset);
11928
13188
  continue;
11929
13189
  }
11930
- details.push(await restJson2(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
13190
+ details.push(await restJson3(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
11931
13191
  }
11932
13192
  return details;
11933
13193
  }
@@ -11946,7 +13206,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
11946
13206
  const branchesWanted = expectedBranches(repoClass, releaseTrack);
11947
13207
  const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
11948
13208
  const checks = [];
11949
- const repoInfo = await restJson2(deps, `repos/${repo}`, {});
13209
+ const repoInfo = await restJson3(deps, `repos/${repo}`, {});
11950
13210
  checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
11951
13211
  checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
11952
13212
  checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
@@ -11976,29 +13236,29 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
11976
13236
  detail: overgrants.length ? `over-granted: ${overgrants.map((f) => f.actor).join(", ")}` : void 0
11977
13237
  });
11978
13238
  for (const path2 of requiredDocs) {
11979
- checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `bootstrap artifact exists: ${path2}` });
13239
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `bootstrap artifact exists: ${path2}` });
11980
13240
  }
11981
13241
  for (const path2 of requiredIssueTemplates) {
11982
- checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `issue template exists: ${path2}` });
13242
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `issue template exists: ${path2}` });
11983
13243
  }
11984
13244
  for (const path2 of requiredWorkflows) {
11985
- checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `automation workflow exists: ${path2}` });
13245
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `automation workflow exists: ${path2}` });
11986
13246
  }
11987
- if (repo !== HUB_REPO3) {
13247
+ if (repo !== HUB_REPO4 && repoClass === "deployable") {
11988
13248
  for (const path2 of requiredProductWorkflows) {
11989
- checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `gate workflow exists: ${path2}` });
13249
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `gate workflow exists: ${path2}` });
11990
13250
  }
11991
13251
  checks.push({
11992
- ok: await contentExists(deps, repo, baseBranch, requiredProductRulesetRef),
13252
+ ok: await contentExists2(deps, repo, baseBranch, requiredProductRulesetRef),
11993
13253
  label: "product required-check ruleset reference exists",
11994
13254
  detail: `expected: ${requiredProductRulesetRef} (apply as an active repo ruleset after bootstrap)`
11995
13255
  });
11996
13256
  }
11997
13257
  if (repoClass === "deployable") {
11998
13258
  const trainScript = "scripts/next-version.mjs";
11999
- checks.push({ ok: await contentExists(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
13259
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
12000
13260
  }
12001
- checks.push({ ok: await contentExists(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
13261
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
12002
13262
  const readme = await contentText(deps, repo, baseBranch, "README.md");
12003
13263
  checks.push({
12004
13264
  ok: readme !== null && readme.includes("## Agent context"),
@@ -12006,7 +13266,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12006
13266
  detail: readme === null ? "README.md not readable via API" : void 0
12007
13267
  });
12008
13268
  const agentRulesPath = `.cursor/rules/${repoSlugFromFullName(repo)}.mdc`;
12009
- const agentRulesOk = await contentExists(deps, repo, baseBranch, agentRulesPath);
13269
+ const agentRulesOk = await contentExists2(deps, repo, baseBranch, agentRulesPath);
12010
13270
  checks.push({
12011
13271
  ok: agentRulesOk,
12012
13272
  label: "Cursor agent rules file exists",
@@ -12019,7 +13279,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12019
13279
  }
12020
13280
  const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
12021
13281
  checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
12022
- const actions = await restJson2(deps, `repos/${repo}/actions/permissions`, {});
13282
+ const actions = await restJson3(deps, `repos/${repo}/actions/permissions`, {});
12023
13283
  checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
12024
13284
  const config = deps.projectMeta ?? null;
12025
13285
  checks.push({
@@ -12124,8 +13384,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12124
13384
  if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
12125
13385
  const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && projectRegistryIncludesRepo(json.projects, repo));
12126
13386
  if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "cloud-agent project registry includes repo" });
12127
- const rulesetList = await restJson2(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
12128
- const rulesets = await rulesetDetails(deps, repo, rulesetList);
13387
+ const rulesetList = await restJson3(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
13388
+ const rulesets = await rulesetDetails2(deps, repo, rulesetList);
12129
13389
  const activeOrgRulesets = rulesets.filter(
12130
13390
  (r) => r.source_type === "Organization" && r.target === "branch" && r.enforcement === "active"
12131
13391
  );
@@ -12136,16 +13396,16 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12136
13396
  label: "covered by an active org ruleset",
12137
13397
  detail: orgRuleset ? void 0 : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
12138
13398
  });
12139
- if (repo === HUB_REPO3) {
12140
- const statusChecks = rulesetStatusChecks(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
13399
+ if (repo === HUB_REPO4) {
13400
+ const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
12141
13401
  const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
12142
13402
  checks.push({
12143
13403
  ok: missing.length === 0,
12144
13404
  label: "Hub required status checks configured",
12145
13405
  detail: optionDetail(missing)
12146
13406
  });
12147
- } else {
12148
- const statusChecks = rulesetStatusChecks(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
13407
+ } else if (repoClass === "deployable") {
13408
+ const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
12149
13409
  const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
12150
13410
  checks.push({
12151
13411
  ok: missing.length === 0,
@@ -13324,8 +14584,8 @@ function resolveKbSource(rawBase) {
13324
14584
  return { owner: m[1], repo: m[2], ref: m[3] };
13325
14585
  }
13326
14586
  function buildKbGetArgs(src, path2) {
13327
- const clean3 = path2.replace(/^\/+/, "");
13328
- return ["api", `repos/${src.owner}/${src.repo}/contents/${clean3}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
14587
+ const clean4 = path2.replace(/^\/+/, "");
14588
+ return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
13329
14589
  }
13330
14590
  function buildKbTreeArgs(src) {
13331
14591
  return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
@@ -13342,10 +14602,10 @@ function parseKbTree(stdout, prefix) {
13342
14602
  }
13343
14603
 
13344
14604
  // src/plan.ts
13345
- var import_node_path13 = require("node:path");
14605
+ var import_node_path15 = require("node:path");
13346
14606
  var PLANS_DIR = "plans";
13347
- var META_FILE = (0, import_node_path13.join)(PLANS_DIR, ".plan-meta.json");
13348
- var planPath = (slug) => (0, import_node_path13.join)(PLANS_DIR, `${slug}.md`);
14607
+ var META_FILE = (0, import_node_path15.join)(PLANS_DIR, ".plan-meta.json");
14608
+ var planPath = (slug) => (0, import_node_path15.join)(PLANS_DIR, `${slug}.md`);
13349
14609
  var metaKey = (project2, slug) => `${project2}/${slug}`;
13350
14610
  function parseMeta(raw) {
13351
14611
  if (!raw) return {};
@@ -13370,7 +14630,7 @@ function hashContent(s) {
13370
14630
  function staleHint(slug) {
13371
14631
  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`;
13372
14632
  }
13373
- var INDEX_FILE = (0, import_node_path13.join)(PLANS_DIR, ".index.json");
14633
+ var INDEX_FILE = (0, import_node_path15.join)(PLANS_DIR, ".index.json");
13374
14634
  var INDEX_TTL_MS = 6e4;
13375
14635
  function parseIndex(raw) {
13376
14636
  if (!raw) return null;
@@ -13399,7 +14659,7 @@ function mergeIndex(idx, scope, plans, now) {
13399
14659
  const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
13400
14660
  return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
13401
14661
  }
13402
- var QUEUE_FILE = (0, import_node_path13.join)(PLANS_DIR, ".sync-queue.json");
14662
+ var QUEUE_FILE = (0, import_node_path15.join)(PLANS_DIR, ".sync-queue.json");
13403
14663
  var QUEUE_MAX_ATTEMPTS = 10;
13404
14664
  function isValidQueueEntry(e) {
13405
14665
  if (!e || typeof e !== "object") return false;
@@ -13848,11 +15108,11 @@ async function planGraduate(deps, slug, opts = {}) {
13848
15108
  }
13849
15109
 
13850
15110
  // src/atomic-write.ts
13851
- var import_node_fs14 = require("node:fs");
15111
+ var import_node_fs17 = require("node:fs");
13852
15112
  function atomicWriteFileSync(path2, content) {
13853
15113
  const tmp = `${path2}.${process.pid}.tmp`;
13854
- (0, import_node_fs14.writeFileSync)(tmp, content, "utf8");
13855
- (0, import_node_fs14.renameSync)(tmp, path2);
15114
+ (0, import_node_fs17.writeFileSync)(tmp, content, "utf8");
15115
+ (0, import_node_fs17.renameSync)(tmp, path2);
13856
15116
  }
13857
15117
 
13858
15118
  // src/oauth.ts
@@ -14083,7 +15343,7 @@ async function fetchHubVersionInfo(baseUrl) {
14083
15343
  }
14084
15344
  function readRepoVersion() {
14085
15345
  try {
14086
- return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
15346
+ return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
14087
15347
  } catch {
14088
15348
  return void 0;
14089
15349
  }
@@ -14158,6 +15418,22 @@ async function applyClaudePluginHeal(surface, log) {
14158
15418
  }
14159
15419
  return true;
14160
15420
  }
15421
+ async function runCodexPlugin(args) {
15422
+ try {
15423
+ await runHostBin("codex", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
15424
+ return true;
15425
+ } catch {
15426
+ return false;
15427
+ }
15428
+ }
15429
+ async function applyCodexPluginHeal(surface, log) {
15430
+ if (surface !== "codex") return false;
15431
+ log(" \u21BB reinstalling the MMI plugin via `codex plugin` (marketplace remove \u2192 add --ref main \u2192 add)\u2026");
15432
+ for (const step of CODEX_PLUGIN_HEAL_STEPS) {
15433
+ if (healStepAborts(step, await runCodexPlugin([...step.args]))) return false;
15434
+ }
15435
+ return true;
15436
+ }
14161
15437
  var program2 = new Command();
14162
15438
  program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion());
14163
15439
  async function runRulesSync(opts, io = consoleIo) {
@@ -14193,10 +15469,10 @@ async function runRulesSync(opts, io = consoleIo) {
14193
15469
  for (const entry of fetched) {
14194
15470
  if ("error" in entry) continue;
14195
15471
  const { file, source } = entry;
14196
- const current = (0, import_node_fs15.existsSync)(file) ? await (0, import_promises5.readFile)(file, "utf8") : null;
15472
+ const current = (0, import_node_fs18.existsSync)(file) ? await (0, import_promises5.readFile)(file, "utf8") : null;
14197
15473
  if (needsUpdate(source, current)) {
14198
15474
  const slash = file.lastIndexOf("/");
14199
- if (slash > 0) (0, import_node_fs15.mkdirSync)(file.slice(0, slash), { recursive: true });
15475
+ if (slash > 0) (0, import_node_fs18.mkdirSync)(file.slice(0, slash), { recursive: true });
14200
15476
  await (0, import_promises5.writeFile)(file, normalizeEol(source), "utf8");
14201
15477
  changed++;
14202
15478
  if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
@@ -14222,7 +15498,7 @@ async function runDocsSync(opts, io = consoleIo) {
14222
15498
  return null;
14223
15499
  }
14224
15500
  },
14225
- localContent: async (f) => (0, import_node_fs15.existsSync)(f) ? await (0, import_promises5.readFile)(f, "utf8") : null,
15501
+ localContent: async (f) => (0, import_node_fs18.existsSync)(f) ? await (0, import_promises5.readFile)(f, "utf8") : null,
14226
15502
  writeDoc: async (f, c) => {
14227
15503
  await (0, import_promises5.writeFile)(f, c, "utf8");
14228
15504
  }
@@ -14234,6 +15510,7 @@ async function runDocsSync(opts, io = consoleIo) {
14234
15510
  var docs = program2.command("docs").description("repo-owned authoritative docs");
14235
15511
  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));
14236
15512
  registerSagaCommands(program2);
15513
+ registerHandoffCommands(program2);
14237
15514
  registerHonchoCommands(program2);
14238
15515
  async function runWhoami(io = consoleIo) {
14239
15516
  const cfg = await loadConfig();
@@ -14247,8 +15524,18 @@ async function runWhoami(io = consoleIo) {
14247
15524
  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 () => {
14248
15525
  await runWhoami();
14249
15526
  });
14250
- 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) => {
15527
+ 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) => {
14251
15528
  if (o.apply && o.dryRun) return fail("gc: choose either --dry-run or --apply");
15529
+ if (o.scratch) {
15530
+ try {
15531
+ const root = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
15532
+ const run = executeScratchGc(root, { apply: Boolean(o.apply) });
15533
+ if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, ...run }, null, 2));
15534
+ return console.log(formatScratchGcPlan(run.plan, Boolean(o.apply)));
15535
+ } catch (e) {
15536
+ return fail(`gc --scratch: ${e.message}`);
15537
+ }
15538
+ }
14252
15539
  const limit = Number.parseInt(o.limit, 10);
14253
15540
  if (!Number.isFinite(limit) || limit < 1) return fail("gc: --limit must be a positive integer");
14254
15541
  try {
@@ -14276,6 +15563,120 @@ ${renderGcApplyResult(applyResult)}`);
14276
15563
  fail(`gc: ${e.message}`);
14277
15564
  }
14278
15565
  });
15566
+ var NPM_PROVISION_TIMEOUT_MS = 3e5;
15567
+ var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
15568
+ function runWorktreeInstall(command, cwd, quiet) {
15569
+ const [bin, ...args] = command.split(" ");
15570
+ const file = isWin ? "cmd.exe" : bin;
15571
+ const spawnArgs = isWin ? ["/c", bin, ...args] : args;
15572
+ return new Promise((resolve, reject) => {
15573
+ const child = (0, import_node_child_process10.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
15574
+ const timer = setTimeout(() => {
15575
+ try {
15576
+ child.kill();
15577
+ } catch {
15578
+ }
15579
+ reject(new Error(`${command} timed out after ${NPM_PROVISION_TIMEOUT_MS}ms in ${cwd}`));
15580
+ }, NPM_PROVISION_TIMEOUT_MS);
15581
+ child.on("error", (e) => {
15582
+ clearTimeout(timer);
15583
+ reject(e);
15584
+ });
15585
+ child.on("exit", (code) => {
15586
+ clearTimeout(timer);
15587
+ if (code === 0) resolve();
15588
+ else reject(new Error(`${command} exited ${code} in ${cwd}`));
15589
+ });
15590
+ });
15591
+ }
15592
+ async function primaryCheckoutRoot(worktreeRoot) {
15593
+ try {
15594
+ const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
15595
+ return out ? (0, import_node_path16.dirname)(out) : void 0;
15596
+ } catch {
15597
+ return void 0;
15598
+ }
15599
+ }
15600
+ function makeProvisionDeps(worktreeRoot, quiet, log) {
15601
+ return {
15602
+ runInstall: (command, cwd) => runWorktreeInstall(command, cwd, quiet),
15603
+ primaryCheckout: () => primaryCheckoutRoot(worktreeRoot),
15604
+ log
15605
+ };
15606
+ }
15607
+ function acquireWorktreeSetupLock(worktreeRoot) {
15608
+ const lockPath = (0, import_node_path16.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
15609
+ const take = () => {
15610
+ const fd = (0, import_node_fs18.openSync)(lockPath, "wx");
15611
+ try {
15612
+ (0, import_node_fs18.writeSync)(fd, String(Date.now()));
15613
+ } finally {
15614
+ (0, import_node_fs18.closeSync)(fd);
15615
+ }
15616
+ return () => {
15617
+ try {
15618
+ (0, import_node_fs18.rmSync)(lockPath, { force: true });
15619
+ } catch {
15620
+ }
15621
+ };
15622
+ };
15623
+ try {
15624
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)(lockPath), { recursive: true });
15625
+ return take();
15626
+ } catch {
15627
+ try {
15628
+ if (Date.now() - (0, import_node_fs18.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
15629
+ (0, import_node_fs18.rmSync)(lockPath, { force: true });
15630
+ return take();
15631
+ }
15632
+ } catch {
15633
+ }
15634
+ return null;
15635
+ }
15636
+ }
15637
+ var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
15638
+ 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) => {
15639
+ try {
15640
+ const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
15641
+ const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
15642
+ const baseBranch = o.from.startsWith(`${o.remote}/`) ? o.from.slice(o.remote.length + 1) : void 0;
15643
+ if (baseBranch) await execFileP2("git", ["fetch", o.remote, baseBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
15644
+ await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, o.from], { timeout: GH_MUTATION_TIMEOUT_MS });
15645
+ const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
15646
+ if (!o.json) console.error(` ${m}`);
15647
+ }));
15648
+ if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base: o.from, ...report }, null, 2));
15649
+ console.log(`worktree ready: ${wtPath} (branch ${branch} from ${o.from})`);
15650
+ console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
15651
+ console.log(` copied: ${report.copied.join(", ") || "none"}`);
15652
+ } catch (e) {
15653
+ fail(`worktree create: ${e.message}`);
15654
+ }
15655
+ });
15656
+ 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) => {
15657
+ const root = path2 ?? process.cwd();
15658
+ const release = acquireWorktreeSetupLock(root);
15659
+ if (!release) {
15660
+ if (!o.quiet && !o.json) console.log("worktree setup: another provision is in progress \u2014 skipping");
15661
+ return;
15662
+ }
15663
+ try {
15664
+ const report = await provisionWorktree(root, makeProvisionDeps(root, Boolean(o.quiet), (m) => {
15665
+ if (!o.quiet && !o.json) console.error(` ${m}`);
15666
+ }));
15667
+ if (o.json) return console.log(JSON.stringify({ path: root, ...report }, null, 2));
15668
+ if (!o.quiet) {
15669
+ console.log(`worktree provisioned: ${root}`);
15670
+ console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
15671
+ console.log(` copied: ${report.copied.join(", ") || "none"}`);
15672
+ }
15673
+ } catch (e) {
15674
+ if (o.quiet) return void console.error(`[worktree-setup] ${e.message}`);
15675
+ fail(`worktree setup: ${e.message}`);
15676
+ } finally {
15677
+ release();
15678
+ }
15679
+ });
14279
15680
  var kb = program2.command("kb").description("org knowledgebase (read-only)");
14280
15681
  kb.command("get <path>").description("print a KB document by path").action(async (path2) => {
14281
15682
  const src = resolveKbSource((await loadConfig()).kbSource);
@@ -14395,7 +15796,7 @@ function detachPlanSync() {
14395
15796
  }
14396
15797
  }
14397
15798
  function makePlanDeps(cfg, io = consoleIo) {
14398
- const ensureDir = () => (0, import_node_fs15.mkdirSync)(PLANS_DIR, { recursive: true });
15799
+ const ensureDir = () => (0, import_node_fs18.mkdirSync)(PLANS_DIR, { recursive: true });
14399
15800
  return {
14400
15801
  apiUrl: cfg.sagaApiUrl,
14401
15802
  fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
@@ -14403,24 +15804,24 @@ function makePlanDeps(cfg, io = consoleIo) {
14403
15804
  project: async () => (await sagaKey(cfg)).project,
14404
15805
  readLocal: (slug) => {
14405
15806
  try {
14406
- return (0, import_node_fs15.readFileSync)(planPath(slug), "utf8");
15807
+ return (0, import_node_fs18.readFileSync)(planPath(slug), "utf8");
14407
15808
  } catch {
14408
15809
  return null;
14409
15810
  }
14410
15811
  },
14411
15812
  writeLocal: (slug, content) => {
14412
15813
  ensureDir();
14413
- (0, import_node_fs15.writeFileSync)(planPath(slug), content, "utf8");
15814
+ (0, import_node_fs18.writeFileSync)(planPath(slug), content, "utf8");
14414
15815
  },
14415
15816
  removeLocal: (slug) => {
14416
15817
  try {
14417
- (0, import_node_fs15.rmSync)(planPath(slug));
15818
+ (0, import_node_fs18.rmSync)(planPath(slug));
14418
15819
  } catch {
14419
15820
  }
14420
15821
  },
14421
15822
  readMetaRaw: () => {
14422
15823
  try {
14423
- return (0, import_node_fs15.readFileSync)(META_FILE, "utf8");
15824
+ return (0, import_node_fs18.readFileSync)(META_FILE, "utf8");
14424
15825
  } catch {
14425
15826
  return null;
14426
15827
  }
@@ -14431,7 +15832,7 @@ function makePlanDeps(cfg, io = consoleIo) {
14431
15832
  },
14432
15833
  readIndexRaw: () => {
14433
15834
  try {
14434
- return (0, import_node_fs15.readFileSync)(INDEX_FILE, "utf8");
15835
+ return (0, import_node_fs18.readFileSync)(INDEX_FILE, "utf8");
14435
15836
  } catch {
14436
15837
  return null;
14437
15838
  }
@@ -14442,7 +15843,7 @@ function makePlanDeps(cfg, io = consoleIo) {
14442
15843
  },
14443
15844
  readQueueRaw: () => {
14444
15845
  try {
14445
- return (0, import_node_fs15.readFileSync)(QUEUE_FILE, "utf8");
15846
+ return (0, import_node_fs18.readFileSync)(QUEUE_FILE, "utf8");
14446
15847
  } catch {
14447
15848
  return null;
14448
15849
  }
@@ -14632,6 +16033,20 @@ secrets.command("edit <key>").description("alias for set \u2014 replace a secret
14632
16033
  const ok = await secretsEdit(d, key, o);
14633
16034
  if (!ok) process.exitCode = 1;
14634
16035
  }));
16036
+ secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
16037
+ const stages = ["dev", "rc", "main"];
16038
+ if (!stages.includes(o.from) || !stages.includes(o.to)) {
16039
+ return fail("secrets copy: --from and --to must be dev, rc, or main");
16040
+ }
16041
+ const ok = await secretsCopy(d, {
16042
+ repo: o.repo,
16043
+ from: o.from,
16044
+ to: o.to,
16045
+ keys: o.keys.split(","),
16046
+ dryRun: o.dryRun
16047
+ });
16048
+ if (!ok) process.exitCode = 1;
16049
+ }));
14635
16050
  secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
14636
16051
  secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
14637
16052
  secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
@@ -14997,7 +16412,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
14997
16412
  if (mismatch) process.exitCode = 1;
14998
16413
  });
14999
16414
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
15000
- 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 (label + board Priority field when configured)").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) => {
16415
+ 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) => {
15001
16416
  let args;
15002
16417
  let priority;
15003
16418
  let body;
@@ -15005,6 +16420,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
15005
16420
  try {
15006
16421
  title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
15007
16422
  body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
16423
+ if (o.priority === void 0) throw new Error("missing --priority <priority> \u2014 expected one of: urgent, high, medium, low");
15008
16424
  priority = normalizePriority(o.priority);
15009
16425
  args = buildIssueArgs({ type: o.type, title, body, priority, repo: o.repo, labels: o.label });
15010
16426
  if (o.parent !== void 0) parseIssueRef(o.parent);
@@ -15082,12 +16498,12 @@ issue.command("link-child <parent> <child>").description("link an existing issue
15082
16498
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
15083
16499
  }
15084
16500
  });
15085
- program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
16501
+ program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
15086
16502
  let body;
15087
16503
  let priority;
15088
16504
  let args;
15089
16505
  let title;
15090
- const targetRepo2 = o.repo ?? HUB_REPO;
16506
+ const targetRepo2 = o.repo ?? HUB_REPO2;
15091
16507
  const sourceRepo = await resolveRepo(void 0);
15092
16508
  try {
15093
16509
  title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
@@ -15282,8 +16698,8 @@ grindCmd.command("estimate").description("Worst-case cost proxy (agent-call unit
15282
16698
  console.log(`ceiling: ${GRIND_COST_CEILING} units \u2014 ${estimate.exceedsCeiling ? "EXCEEDS \u2192 ask human (cap/stuck path)" : "within"}`);
15283
16699
  }
15284
16700
  });
15285
- program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
15286
- const targetRepo2 = o.repo ?? HUB_REPO;
16701
+ program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
16702
+ const targetRepo2 = o.repo ?? HUB_REPO2;
15287
16703
  const sourceRepo = await resolveRepo(void 0);
15288
16704
  const pluginSha = await resolvePluginSha();
15289
16705
  let skill;
@@ -15354,6 +16770,111 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
15354
16770
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
15355
16771
  console.log(JSON.stringify(created));
15356
16772
  });
16773
+ async function listCiWorkflowPaths(cwd = process.cwd()) {
16774
+ const wfDir = (0, import_node_path16.join)(cwd, ".github", "workflows");
16775
+ if (!(0, import_node_fs18.existsSync)(wfDir)) return [];
16776
+ return (0, import_node_fs18.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
16777
+ }
16778
+ async function resolveMergeCiPolicyForCheckout(repoOpt) {
16779
+ const repo = repoOpt ?? await resolveRepo();
16780
+ if (repo) {
16781
+ return resolveRepoMergeCiPolicy(repo, ciAuditDeps());
16782
+ }
16783
+ const workflowPaths = await listCiWorkflowPaths();
16784
+ return resolveMergeCiPolicy({ workflowPaths });
16785
+ }
16786
+ function ciAuditDeps() {
16787
+ const cfgPromise = loadConfig();
16788
+ return {
16789
+ client: defaultGitHubClient(),
16790
+ listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
16791
+ getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise))
16792
+ };
16793
+ }
16794
+ 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) => {
16795
+ const result = await resolveMergeCiPolicyForCheckout(o.repo);
16796
+ if (o.json) return printLine(JSON.stringify(result));
16797
+ printLine(`merge CI policy: ${result.policy} (${result.reason})`);
16798
+ });
16799
+ 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) => {
16800
+ const repoArgs = o.repo ? ["--repo", o.repo] : [];
16801
+ const result = await waitForPrChecks({
16802
+ resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
16803
+ pollChecks: async () => {
16804
+ const { stdout } = await execFileP2("gh", ["pr", "checks", number, ...repoArgs], { timeout: GC_GH_TIMEOUT_MS });
16805
+ return parseGhPrChecksOutput(stdout);
16806
+ },
16807
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
16808
+ });
16809
+ if (o.json) printLine(JSON.stringify(result));
16810
+ else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
16811
+ if (result.status === "failure" || result.status === "timeout") process.exitCode = 1;
16812
+ });
16813
+ pr.command("land <number>").description("agent merge path (#1440): train probe \u2192 checks-wait \u2192 merge --auto \u2192 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").action(async (number, o) => {
16814
+ const repoArgs = o.repo ? ["--repo", o.repo] : [];
16815
+ const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
16816
+ resolveRepo: async (prNumber, repoOpt) => {
16817
+ if (repoOpt) return repoOpt;
16818
+ const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
16819
+ const [repo, base2] = viewed.split(/\s+/);
16820
+ if (base2 && base2 !== "development") {
16821
+ throw new Error(`pr land: base branch must be development (got ${base2}) \u2014 promotion merges stay human-only`);
16822
+ }
16823
+ if (!repo) throw new Error("pr land: could not resolve PR repo");
16824
+ return repo;
16825
+ },
16826
+ fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
16827
+ resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
16828
+ waitForChecks: (prNumber, repo) => waitForPrChecks({
16829
+ resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
16830
+ pollChecks: async () => {
16831
+ const args = repo ? ["--repo", repo] : [];
16832
+ const { stdout } = await execFileP2("gh", ["pr", "checks", prNumber, ...args], { timeout: GC_GH_TIMEOUT_MS });
16833
+ return parseGhPrChecksOutput(stdout);
16834
+ },
16835
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
16836
+ }),
16837
+ mergeAuto: async (prNumber, repo) => {
16838
+ const args = repo ? ["--repo", repo] : [];
16839
+ try {
16840
+ await execFileP2("gh", buildPrMergeArgs({ number: prNumber, repoArgs: args, method: "--squash", auto: true }), { timeout: GH_MUTATION_TIMEOUT_MS });
16841
+ } catch (e) {
16842
+ const message = String(e.message || "");
16843
+ if (/already been merged/i.test(message)) return { mergeStatus: "merged" };
16844
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
16845
+ if (note) return { mergeStatus: "failed", error: note };
16846
+ if (!basePolicyBlocksImmediateMerge(message)) {
16847
+ return { mergeStatus: "failed", error: message.split("\n")[0] };
16848
+ }
16849
+ return { mergeStatus: "failed", error: `merge blocked: ${message.split("\n")[0]} \u2014 ensure checks are green` };
16850
+ }
16851
+ const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
16852
+ return { mergeStatus: state === "MERGED" ? "merged" : "auto-merge-enqueued" };
16853
+ },
16854
+ pollMerged: async (prNumber, repo, deadlineMs) => {
16855
+ const args = repo ? ["--repo", repo] : [];
16856
+ while (Date.now() < deadlineMs) {
16857
+ const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
16858
+ if (state === "MERGED") return true;
16859
+ await new Promise((resolve) => setTimeout(resolve, PR_LAND_POLL_MS));
16860
+ }
16861
+ return false;
16862
+ }
16863
+ });
16864
+ if (o.json) printLine(JSON.stringify(result));
16865
+ else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
16866
+ if (result.status === "failed") process.exitCode = 1;
16867
+ else {
16868
+ await execFileP2(process.execPath, [
16869
+ process.argv[1],
16870
+ "pr",
16871
+ "merge",
16872
+ number,
16873
+ ...o.repo ? ["--repo", o.repo] : [],
16874
+ "--squash"
16875
+ ], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
16876
+ }
16877
+ });
15357
16878
  async function remoteBranchExists2(branch, options = {}) {
15358
16879
  return checkRemoteBranchExists(branch, {
15359
16880
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
@@ -15362,7 +16883,7 @@ async function remoteBranchExists2(branch, options = {}) {
15362
16883
  var COMPOSE_TIMEOUT_MS = 12e4;
15363
16884
  async function createDeferredWorktreeStore() {
15364
16885
  try {
15365
- const { stdout } = await execFileP2("git", ["rev-parse", "--git-dir"], { timeout: GIT_TIMEOUT_MS });
16886
+ const { stdout } = await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS });
15366
16887
  const registryPath = deferredWorktreesRegistryPath(stdout.trim());
15367
16888
  return {
15368
16889
  read: async () => {
@@ -15374,7 +16895,7 @@ async function createDeferredWorktreeStore() {
15374
16895
  },
15375
16896
  write: async (entries) => {
15376
16897
  try {
15377
- await (0, import_promises5.mkdir)((0, import_node_path14.dirname)(registryPath), { recursive: true });
16898
+ await (0, import_promises5.mkdir)((0, import_node_path16.dirname)(registryPath), { recursive: true });
15378
16899
  await (0, import_promises5.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
15379
16900
  } catch {
15380
16901
  }
@@ -15393,7 +16914,7 @@ function worktreeRemoveDeps(execGit) {
15393
16914
  }
15394
16915
  function teardownWorktreeStage(worktreePath) {
15395
16916
  return runWorktreeStageTeardown(worktreePath, {
15396
- hasStageState: (wt) => (0, import_node_fs15.existsSync)(stageStatePath(wt)),
16917
+ hasStageState: (wt) => (0, import_node_fs18.existsSync)(stageStatePath(wt)),
15397
16918
  stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
15398
16919
  listComposeProjects: async () => {
15399
16920
  const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
@@ -15404,7 +16925,7 @@ function teardownWorktreeStage(worktreePath) {
15404
16925
  }
15405
16926
  });
15406
16927
  }
15407
- pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").action(async (number, o) => {
16928
+ pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").action(async (number, o) => {
15408
16929
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
15409
16930
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
15410
16931
  const headRef = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
@@ -15456,7 +16977,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
15456
16977
  } : await cleanupPrMergeLocalBranch(headRef, {
15457
16978
  beforeWorktrees,
15458
16979
  startingPath,
15459
- pathExists: (p) => (0, import_node_fs15.existsSync)(p),
16980
+ pathExists: (p) => (0, import_node_fs18.existsSync)(p),
15460
16981
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
15461
16982
  teardownWorktreeStage,
15462
16983
  deferredStore,
@@ -15571,6 +17092,25 @@ board.command("backfill-priority").description("set board Priority from priority
15571
17092
  return failGraceful(`board backfill-priority failed: ${e.message}`);
15572
17093
  }
15573
17094
  });
17095
+ board.command("prune-priority-labels").description("remove retired priority:* labels (#416) from issues whose board Priority field is already set").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--dry-run", "report what would be removed without writing").option("--concurrency <n>", "parallel issue edits (default 8)", "8").action(async (o) => {
17096
+ try {
17097
+ const result = await prunePriorityLabels({
17098
+ config: await loadConfigForRepo(o.repo),
17099
+ repo: o.repo,
17100
+ dryRun: o.dryRun,
17101
+ concurrency: Number(o.concurrency) || 8
17102
+ });
17103
+ if (o.json) return console.log(JSON.stringify(result));
17104
+ console.log(
17105
+ `prune-priority-labels: scanned ${result.scanned}, pruned ${result.pruned} (${result.removedLabels} labels), skipped ${result.skippedNoField} (field unset), failed ${result.failed}`
17106
+ );
17107
+ for (const line of result.details.slice(0, 30)) console.log(` ${line}`);
17108
+ if (result.details.length > 30) console.log(` ... +${result.details.length - 30} more`);
17109
+ if (result.failed) process.exitCode = 1;
17110
+ } catch (e) {
17111
+ return failGraceful(`board prune-priority-labels failed: ${e.message}`);
17112
+ }
17113
+ });
15574
17114
  board.command("done <issue>").description("set a board item's Status to Done (does not close the GitHub issue; use `gh issue close`)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails").action(async (issueRef, o) => {
15575
17115
  try {
15576
17116
  const result = await moveBoardItem({ config: await loadConfigOrDiscover(), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
@@ -15609,7 +17149,7 @@ function rawValues(flag) {
15609
17149
  return out;
15610
17150
  }
15611
17151
  function printLine(value) {
15612
- (0, import_node_fs15.writeSync)(1, `${value}
17152
+ (0, import_node_fs18.writeSync)(1, `${value}
15613
17153
  `);
15614
17154
  }
15615
17155
  function stageKeepAlive() {
@@ -15626,10 +17166,26 @@ async function resolveStage() {
15626
17166
  local,
15627
17167
  shell: shellFor(),
15628
17168
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
15629
- hasCompose: (0, import_node_fs15.existsSync)((0, import_node_path14.join)(process.cwd(), "docker-compose.yml")),
15630
- hasEnvExample: (0, import_node_fs15.existsSync)((0, import_node_path14.join)(process.cwd(), ".env.example"))
17169
+ hasCompose: (0, import_node_fs18.existsSync)((0, import_node_path16.join)(process.cwd(), "docker-compose.yml")),
17170
+ hasEnvExample: (0, import_node_fs18.existsSync)((0, import_node_path16.join)(process.cwd(), ".env.example"))
15631
17171
  });
15632
17172
  }
17173
+ async function fetchStageVaultEnvMerge() {
17174
+ const cfg = await loadConfig();
17175
+ if (!cfg.sagaApiUrl) return void 0;
17176
+ const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch(() => null);
17177
+ if (!read?.ok || !read.project) return void 0;
17178
+ const names = requiredRuntimeSecretNames("dev", read.project.requiredRuntimeSecrets, { includeGoogleOAuth: false });
17179
+ if (!names.length) return void 0;
17180
+ const d = makeSecretsDeps(cfg);
17181
+ const merge = {};
17182
+ for (const name of names) {
17183
+ const key = name.includes("/") ? name : `dev/${name}`;
17184
+ const value = await fetchSecretValue(d, key, {});
17185
+ if (value != null) merge[name.includes("/") ? name.split("/").pop() : name] = value;
17186
+ }
17187
+ return Object.keys(merge).length ? merge : void 0;
17188
+ }
15633
17189
  function stageStepsFor(res, stops = true) {
15634
17190
  if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
15635
17191
  if (res.source === "local") return stagePlan(res.config ?? {}, stops);
@@ -15663,9 +17219,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
15663
17219
  printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
15664
17220
  return;
15665
17221
  }
15666
- const path2 = (0, import_node_path14.join)(process.cwd(), "infra", "port-ranges.json");
17222
+ const path2 = (0, import_node_path16.join)(process.cwd(), "infra", "port-ranges.json");
15667
17223
  const allocate = async (seed) => {
15668
- const { stdout } = await execFileP2("node", [(0, import_node_path14.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
17224
+ const { stdout } = await execFileP2("node", [(0, import_node_path16.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
15669
17225
  const parsed = JSON.parse(stdout);
15670
17226
  if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
15671
17227
  return parsed.range;
@@ -15761,6 +17317,7 @@ stage.command("start").description("start the configured local stage process and
15761
17317
  }
15762
17318
  if (res.source === "none") return failGraceful(`stage start: ${res.gap}`);
15763
17319
  const cfg = res.config ?? res.derived.config;
17320
+ const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
15764
17321
  try {
15765
17322
  const hold = stageKeepAlive();
15766
17323
  let printed = false;
@@ -15768,6 +17325,7 @@ stage.command("start").description("start the configured local stage process and
15768
17325
  const result = await startStage(cfg, {
15769
17326
  timeoutMs: Number(o.timeoutMs || 6e4),
15770
17327
  allowStaleEnv: o.allowStaleEnv,
17328
+ vaultEnvMerge,
15771
17329
  onReady: (ready) => {
15772
17330
  printed = true;
15773
17331
  const reportUrl = reportedStageUrl(res, ready);
@@ -15795,6 +17353,7 @@ stage.command("run").description("force-stop previous stage, build, start, and h
15795
17353
  }
15796
17354
  if (res.source === "none") return failGraceful(`stage run: ${res.gap}`);
15797
17355
  const cfg = res.config ?? res.derived.config;
17356
+ const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
15798
17357
  try {
15799
17358
  const hold = stageKeepAlive();
15800
17359
  let printed = false;
@@ -15802,6 +17361,7 @@ stage.command("run").description("force-stop previous stage, build, start, and h
15802
17361
  const result = await runStage(cfg, {
15803
17362
  timeoutMs: Number(o.timeoutMs || 6e4),
15804
17363
  allowStaleEnv: o.allowStaleEnv,
17364
+ vaultEnvMerge,
15805
17365
  onReady: (ready) => {
15806
17366
  const reportUrl = reportedStageUrl(res, ready);
15807
17367
  const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
@@ -15981,6 +17541,39 @@ var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestr
15981
17541
  hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
15982
17542
  hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile }), o.json, renderHotfixRelease));
15983
17543
  hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
17544
+ var ci = program2.command("ci").description("org CI + merge-readiness audit and reconcile");
17545
+ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset contexts, auto-merge, registry META (#1440)").option("--json", "machine-readable output").option("--markdown", "fleet summary table for issue comments").option("--repo <owner/repo>", "audit one repo instead of the full registry").action(async (o) => {
17546
+ const report = await auditOrgCi(ciAuditDeps(), o.repo);
17547
+ if (o.json) console.log(JSON.stringify(report, null, 2));
17548
+ else if (o.markdown) console.log(renderCiAuditMarkdown(report));
17549
+ else console.log(renderCiAuditText(report));
17550
+ if (!report.ok) process.exitCode = 1;
17551
+ });
17552
+ ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required)").action(async (o) => {
17553
+ if (o.apply) {
17554
+ const verdict = await fetchTrainAuthority(HUB_REPO2, registryClientDeps(await loadConfig()));
17555
+ if (!verdict.ok || verdict.authority.role !== "master") {
17556
+ return fail("ci reconcile --apply: master-admin required");
17557
+ }
17558
+ }
17559
+ const deps = ciAuditDeps();
17560
+ const audit = await auditOrgCi(deps, o.repo);
17561
+ const applyResults = o.apply ? await Promise.all(audit.repos.map((r) => applyCiReconcileRepo(r.repo, deps))) : [];
17562
+ const payload = { audit, apply: applyResults };
17563
+ if (o.json) console.log(JSON.stringify(payload, null, 2));
17564
+ else {
17565
+ console.log(renderCiAuditText(audit));
17566
+ if (o.apply) {
17567
+ for (const r of applyResults) {
17568
+ console.log(`
17569
+ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${r.errors.length ? ` errors=[${r.errors.join("; ")}]` : ""}`);
17570
+ }
17571
+ } else {
17572
+ console.log("\nDry-run \u2014 re-run with --apply to patch merge settings and activate product rulesets (master-admin).");
17573
+ }
17574
+ }
17575
+ if (!audit.ok) process.exitCode = 1;
17576
+ });
15984
17577
  var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
15985
17578
  if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
15986
17579
  if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
@@ -15999,7 +17592,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
15999
17592
  const report = await verifyBootstrap(repo, o.class, {
16000
17593
  client: defaultGitHubClient(),
16001
17594
  projectMeta: meta,
16002
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs15.existsSync)(path2) ? (0, import_node_fs15.readFileSync)(path2, "utf8") : null,
17595
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null,
16003
17596
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
16004
17597
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
16005
17598
  requiredGcpApis: (() => {
@@ -16042,12 +17635,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16042
17635
  return fail(`bootstrap apply: ${e.message}`);
16043
17636
  }
16044
17637
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
16045
- if (!(0, import_node_fs15.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
16046
- const manifest = loadBootstrapSeeds((0, import_node_fs15.readFileSync)(manifestPath, "utf8"));
17638
+ if (!(0, import_node_fs18.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
17639
+ const manifest = loadBootstrapSeeds((0, import_node_fs18.readFileSync)(manifestPath, "utf8"));
16047
17640
  const baseBranch = o.class === "content" ? "main" : "development";
16048
17641
  const slug = parsedRepo.slug;
16049
17642
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
16050
- const readFile5 = (p) => (0, import_node_fs15.existsSync)(p) ? (0, import_node_fs15.readFileSync)(p, "utf8") : null;
17643
+ const readFile5 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
16051
17644
  const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
16052
17645
  const rawVars = {};
16053
17646
  for (const value of rawValues("--var")) {
@@ -16097,6 +17690,26 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16097
17690
  applied.push(`${action.action} ${resolved.target}`);
16098
17691
  }
16099
17692
  }
17693
+ if (o.execute && o.class === "deployable") {
17694
+ try {
17695
+ await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
17696
+ applied.push("merge settings: allow_auto_merge, squash, delete-branch-on-merge");
17697
+ } catch (e) {
17698
+ applied.push(`merge settings (failed: ${e.message})`);
17699
+ }
17700
+ const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
17701
+ if (rulesetSeed) {
17702
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile5);
17703
+ if (rulesetContent) {
17704
+ try {
17705
+ const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
17706
+ applied.push(`product ruleset: ${activation.action}${activation.detail ? ` (${activation.detail})` : ""}`);
17707
+ } catch (e) {
17708
+ return failGraceful(`bootstrap apply: product ruleset activation failed: ${e.message}`);
17709
+ }
17710
+ }
17711
+ }
17712
+ }
16100
17713
  if (o.execute) {
16101
17714
  for (const l of manifest.labels) {
16102
17715
  try {
@@ -16150,10 +17763,10 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16150
17763
  name: vars.NAME || parsedRepo.name
16151
17764
  };
16152
17765
  const readHubFile = async (path2) => {
16153
- const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc2(path2)}?ref=development`]);
17766
+ const r = await gh(["api", `repos/${HUB_REPO2}/contents/${enc2(path2)}?ref=development`]);
16154
17767
  const parsed = JSON.parse(r.stdout);
16155
17768
  if (parsed.encoding !== "base64" || typeof parsed.content !== "string" || !parsed.sha) {
16156
- throw new Error(`could not read ${HUB_REPO}/${path2}`);
17769
+ throw new Error(`could not read ${HUB_REPO2}/${path2}`);
16157
17770
  }
16158
17771
  return { content: Buffer.from(parsed.content, "base64").toString("utf8"), sha: parsed.sha };
16159
17772
  };
@@ -16165,15 +17778,15 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16165
17778
  applied.push(`fanout: already registered (${parsedRepo.name})`);
16166
17779
  } else {
16167
17780
  const branchName = `bootstrap-register-fanout-${slug}`;
16168
- const headSha = (await gh(["api", `repos/${HUB_REPO}/git/ref/heads/development`, "--jq", ".object.sha"])).stdout.trim();
17781
+ const headSha = (await gh(["api", `repos/${HUB_REPO2}/git/ref/heads/development`, "--jq", ".object.sha"])).stdout.trim();
16169
17782
  try {
16170
- await gh(["api", "-X", "POST", `repos/${HUB_REPO}/git/refs`, "-f", `ref=refs/heads/${branchName}`, "-f", `sha=${headSha}`]);
17783
+ await gh(["api", "-X", "POST", `repos/${HUB_REPO2}/git/refs`, "-f", `ref=refs/heads/${branchName}`, "-f", `sha=${headSha}`]);
16171
17784
  } catch (e) {
16172
17785
  if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
16173
17786
  }
16174
17787
  const branchFileSha = async (path2) => {
16175
17788
  try {
16176
- const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc2(path2)}?ref=${branchName}`, "--jq", ".sha"]);
17789
+ const r = await gh(["api", `repos/${HUB_REPO2}/contents/${enc2(path2)}?ref=${branchName}`, "--jq", ".sha"]);
16177
17790
  return r.stdout.trim() || void 0;
16178
17791
  } catch (e) {
16179
17792
  if (/404|Not Found/i.test(String(e.message ?? ""))) return void 0;
@@ -16182,9 +17795,9 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16182
17795
  };
16183
17796
  const fanoutBranchSha = await branchFileSha(".github/fanout-targets.json");
16184
17797
  const projectsBranchSha = await branchFileSha("projects.json");
16185
- await gh(contentPutArgs(HUB_REPO, ".github/fanout-targets.json", plan2.fanoutTargets, branchName, fanoutBranchSha));
16186
- await gh(contentPutArgs(HUB_REPO, "projects.json", plan2.projects, branchName, projectsBranchSha));
16187
- const openPrs = await gh(["pr", "list", "--repo", HUB_REPO, "--head", branchName, "--base", "development", "--state", "open", "--json", "number,url"]);
17798
+ await gh(contentPutArgs(HUB_REPO2, ".github/fanout-targets.json", plan2.fanoutTargets, branchName, fanoutBranchSha));
17799
+ await gh(contentPutArgs(HUB_REPO2, "projects.json", plan2.projects, branchName, projectsBranchSha));
17800
+ const openPrs = await gh(["pr", "list", "--repo", HUB_REPO2, "--head", branchName, "--base", "development", "--state", "open", "--json", "number,url"]);
16188
17801
  const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
16189
17802
  if (prDecision.action === "reuse") {
16190
17803
  fanoutPrUrl = prDecision.url;
@@ -16193,7 +17806,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16193
17806
  "pr",
16194
17807
  "create",
16195
17808
  "--repo",
16196
- HUB_REPO,
17809
+ HUB_REPO2,
16197
17810
  "--base",
16198
17811
  "development",
16199
17812
  "--head",
@@ -16205,7 +17818,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16205
17818
  ]);
16206
17819
  fanoutPrUrl = created.url;
16207
17820
  }
16208
- await gh(["pr", "merge", fanoutPrUrl, "--repo", HUB_REPO, "--auto", "--squash"]).catch((e) => {
17821
+ await gh(["pr", "merge", fanoutPrUrl, "--repo", HUB_REPO2, "--auto", "--squash"]).catch((e) => {
16209
17822
  if (!/already/i.test(String(e.message ?? ""))) throw e;
16210
17823
  });
16211
17824
  applied.push(`fanout: PR ${fanoutPrUrl} (auto-merge enabled)`);
@@ -16256,16 +17869,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
16256
17869
  if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
16257
17870
  targets = [{ repo: o.repo, class: o.class }];
16258
17871
  } else {
16259
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs15.existsSync)("projects.json") ? (0, import_node_fs15.readFileSync)("projects.json", "utf8") : null;
17872
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs18.existsSync)("projects.json") ? (0, import_node_fs18.readFileSync)("projects.json", "utf8") : null;
16260
17873
  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>");
16261
- const fanoutJson = (0, import_node_fs15.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs15.readFileSync)(".github/fanout-targets.json", "utf8") : null;
17874
+ const fanoutJson = (0, import_node_fs18.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs18.readFileSync)(".github/fanout-targets.json", "utf8") : null;
16262
17875
  targets = loadAccessTargets(projectsJson, fanoutJson);
16263
17876
  }
16264
17877
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
16265
- const fileMatrix = (0, import_node_fs15.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs15.readFileSync)("access-matrix.json", "utf8")) : {};
17878
+ const fileMatrix = (0, import_node_fs18.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs18.readFileSync)("access-matrix.json", "utf8")) : {};
16266
17879
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
16267
17880
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
16268
- const fileContracts = (0, import_node_fs15.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs15.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
17881
+ const fileContracts = (0, import_node_fs18.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs18.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
16269
17882
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
16270
17883
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
16271
17884
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -16274,20 +17887,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
16274
17887
  var isWin = process.platform === "win32";
16275
17888
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
16276
17889
  const homeDir = surface === "codex" ? ".codex" : ".claude";
16277
- return (0, import_node_path14.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
17890
+ return (0, import_node_path16.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
16278
17891
  };
16279
17892
  function readInstalledPlugins() {
16280
17893
  try {
16281
- return JSON.parse((0, import_node_fs15.readFileSync)(installedPluginsPath(), "utf8"));
17894
+ return JSON.parse((0, import_node_fs18.readFileSync)(installedPluginsPath(), "utf8"));
16282
17895
  } catch {
16283
17896
  return null;
16284
17897
  }
16285
17898
  }
16286
17899
  function installedPluginSources() {
16287
17900
  return ["claude", "codex"].map((surface) => {
16288
- const recordPath = (0, import_node_path14.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
17901
+ const recordPath = (0, import_node_path16.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
16289
17902
  try {
16290
- return { surface, installed: JSON.parse((0, import_node_fs15.readFileSync)(recordPath, "utf8")), recordPath };
17903
+ return { surface, installed: JSON.parse((0, import_node_fs18.readFileSync)(recordPath, "utf8")), recordPath };
16291
17904
  } catch {
16292
17905
  return { surface, installed: null, recordPath };
16293
17906
  }
@@ -16295,7 +17908,7 @@ function installedPluginSources() {
16295
17908
  }
16296
17909
  function readClaudeSettings() {
16297
17910
  try {
16298
- return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
17911
+ return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
16299
17912
  } catch {
16300
17913
  return null;
16301
17914
  }
@@ -16317,7 +17930,7 @@ function writeProjectInstallRecord(record) {
16317
17930
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
16318
17931
  list.push(record);
16319
17932
  file.plugins[MMI_PLUGIN_ID] = list;
16320
- (0, import_node_fs15.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
17933
+ (0, import_node_fs18.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
16321
17934
  `, "utf8");
16322
17935
  return true;
16323
17936
  } catch {
@@ -16330,9 +17943,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
16330
17943
  if (!file) return false;
16331
17944
  if (!file.plugins) file.plugins = {};
16332
17945
  const path2 = installedPluginsPath();
16333
- (0, import_node_fs15.copyFileSync)(path2, `${path2}.bak`);
17946
+ (0, import_node_fs18.copyFileSync)(path2, `${path2}.bak`);
16334
17947
  file.plugins[pluginId] = records;
16335
- (0, import_node_fs15.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
17948
+ (0, import_node_fs18.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
16336
17949
  `, "utf8");
16337
17950
  return true;
16338
17951
  } catch {
@@ -16340,35 +17953,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
16340
17953
  }
16341
17954
  }
16342
17955
  function cursorPluginCacheRoot() {
16343
- return (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
17956
+ return (0, import_node_path16.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
16344
17957
  }
16345
17958
  function cursorPluginCachePinSnapshots() {
16346
17959
  const root = cursorPluginCacheRoot();
16347
17960
  try {
16348
- return (0, import_node_fs15.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
16349
- const path2 = (0, import_node_path14.join)(root, entry.name);
16350
- const pluginJson = (0, import_node_path14.join)(path2, ".cursor-plugin", "plugin.json");
16351
- const hooksJson = (0, import_node_path14.join)(path2, "hooks", "hooks.json");
16352
- const cliBundle = (0, import_node_path14.join)(path2, "cli", "dist", "index.cjs");
17961
+ return (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
17962
+ const path2 = (0, import_node_path16.join)(root, entry.name);
17963
+ const pluginJson = (0, import_node_path16.join)(path2, ".cursor-plugin", "plugin.json");
17964
+ const hooksJson = (0, import_node_path16.join)(path2, "hooks", "hooks.json");
17965
+ const cliBundle = (0, import_node_path16.join)(path2, "cli", "dist", "index.cjs");
16353
17966
  let version;
16354
17967
  try {
16355
- const raw = JSON.parse((0, import_node_fs15.readFileSync)(pluginJson, "utf8"));
17968
+ const raw = JSON.parse((0, import_node_fs18.readFileSync)(pluginJson, "utf8"));
16356
17969
  version = typeof raw.version === "string" ? raw.version : void 0;
16357
17970
  } catch {
16358
17971
  version = void 0;
16359
17972
  }
16360
17973
  let isEmpty = true;
16361
17974
  try {
16362
- isEmpty = (0, import_node_fs15.readdirSync)(path2).length === 0;
17975
+ isEmpty = (0, import_node_fs18.readdirSync)(path2).length === 0;
16363
17976
  } catch {
16364
17977
  isEmpty = true;
16365
17978
  }
16366
17979
  return {
16367
17980
  name: entry.name,
16368
17981
  path: path2,
16369
- hasPluginJson: (0, import_node_fs15.existsSync)(pluginJson),
16370
- hasHooksJson: (0, import_node_fs15.existsSync)(hooksJson),
16371
- hasCliBundle: (0, import_node_fs15.existsSync)(cliBundle),
17982
+ hasPluginJson: (0, import_node_fs18.existsSync)(pluginJson),
17983
+ hasHooksJson: (0, import_node_fs18.existsSync)(hooksJson),
17984
+ hasCliBundle: (0, import_node_fs18.existsSync)(cliBundle),
16372
17985
  isEmpty,
16373
17986
  version
16374
17987
  };
@@ -16378,19 +17991,19 @@ function cursorPluginCachePinSnapshots() {
16378
17991
  }
16379
17992
  }
16380
17993
  function hubCheckoutForCursorSeed() {
16381
- const manifest = (0, import_node_path14.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
16382
- return (0, import_node_fs15.existsSync)(manifest) ? process.cwd() : void 0;
17994
+ const manifest = (0, import_node_path16.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
17995
+ return (0, import_node_fs18.existsSync)(manifest) ? process.cwd() : void 0;
16383
17996
  }
16384
17997
  function mmiPluginCacheRootSnapshots() {
16385
17998
  const roots = [
16386
- { surface: "claude", root: (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
16387
- { surface: "codex", root: (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
17999
+ { surface: "claude", root: (0, import_node_path16.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
18000
+ { surface: "codex", root: (0, import_node_path16.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
16388
18001
  ];
16389
18002
  return roots.flatMap(({ surface, root }) => {
16390
18003
  try {
16391
- const entries = (0, import_node_fs15.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
18004
+ const entries = (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
16392
18005
  name: entry.name,
16393
- path: (0, import_node_path14.join)(root, entry.name),
18006
+ path: (0, import_node_path16.join)(root, entry.name),
16394
18007
  isDirectory: entry.isDirectory()
16395
18008
  }));
16396
18009
  return [{ surface, root, entries }];
@@ -16401,7 +18014,7 @@ function mmiPluginCacheRootSnapshots() {
16401
18014
  }
16402
18015
  function hasNestedMmiChild(versionDir) {
16403
18016
  try {
16404
- return (0, import_node_fs15.statSync)((0, import_node_path14.join)(versionDir, "mmi")).isDirectory();
18017
+ return (0, import_node_fs18.statSync)((0, import_node_path16.join)(versionDir, "mmi")).isDirectory();
16405
18018
  } catch {
16406
18019
  return false;
16407
18020
  }
@@ -16412,26 +18025,28 @@ function nestedPluginTreeSnapshot() {
16412
18025
  );
16413
18026
  }
16414
18027
  function uniqueQuarantineTarget(path2) {
16415
- if (!(0, import_node_fs15.existsSync)(path2)) return path2;
18028
+ if (!(0, import_node_fs18.existsSync)(path2)) return path2;
16416
18029
  for (let i = 1; i < 100; i += 1) {
16417
18030
  const candidate = `${path2}-${i}`;
16418
- if (!(0, import_node_fs15.existsSync)(candidate)) return candidate;
18031
+ if (!(0, import_node_fs18.existsSync)(candidate)) return candidate;
16419
18032
  }
16420
18033
  return `${path2}-${Date.now()}`;
16421
18034
  }
16422
18035
  function quarantinePluginCacheDirs(plan2) {
16423
18036
  let moved = 0;
18037
+ const failed = [];
16424
18038
  for (const move of plan2) {
16425
18039
  try {
16426
- if (!(0, import_node_fs15.existsSync)(move.from)) continue;
18040
+ if (!(0, import_node_fs18.existsSync)(move.from)) continue;
16427
18041
  const target = uniqueQuarantineTarget(move.to);
16428
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(target), { recursive: true });
16429
- (0, import_node_fs15.renameSync)(move.from, target);
18042
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)(target), { recursive: true });
18043
+ (0, import_node_fs18.renameSync)(move.from, target);
16430
18044
  moved += 1;
16431
18045
  } catch {
18046
+ failed.push(move);
16432
18047
  }
16433
18048
  }
16434
- return moved;
18049
+ return { moved, failed };
16435
18050
  }
16436
18051
  async function robocopyMirrorEmpty(emptyDir, target) {
16437
18052
  try {
@@ -16444,23 +18059,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
16444
18059
  }
16445
18060
  async function clearNestedPluginTreeDir(targetPath) {
16446
18061
  try {
16447
- if (!(0, import_node_fs15.existsSync)(targetPath)) return true;
18062
+ if (!(0, import_node_fs18.existsSync)(targetPath)) return true;
16448
18063
  if (isWin) {
16449
- const emptyDir = (0, import_node_path14.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
16450
- (0, import_node_fs15.mkdirSync)(emptyDir, { recursive: true });
18064
+ const emptyDir = (0, import_node_path16.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
18065
+ (0, import_node_fs18.mkdirSync)(emptyDir, { recursive: true });
16451
18066
  try {
16452
18067
  await robocopyMirrorEmpty(emptyDir, targetPath);
16453
- (0, import_node_fs15.rmSync)(targetPath, { recursive: true, force: true });
18068
+ (0, import_node_fs18.rmSync)(targetPath, { recursive: true, force: true });
16454
18069
  } finally {
16455
18070
  try {
16456
- (0, import_node_fs15.rmSync)(emptyDir, { recursive: true, force: true });
18071
+ (0, import_node_fs18.rmSync)(emptyDir, { recursive: true, force: true });
16457
18072
  } catch {
16458
18073
  }
16459
18074
  }
16460
- return !(0, import_node_fs15.existsSync)(targetPath);
18075
+ return !(0, import_node_fs18.existsSync)(targetPath);
16461
18076
  }
16462
- (0, import_node_fs15.rmSync)(targetPath, { recursive: true, force: true });
16463
- return !(0, import_node_fs15.existsSync)(targetPath);
18077
+ (0, import_node_fs18.rmSync)(targetPath, { recursive: true, force: true });
18078
+ return !(0, import_node_fs18.existsSync)(targetPath);
16464
18079
  } catch {
16465
18080
  return false;
16466
18081
  }
@@ -16473,11 +18088,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
16473
18088
  }
16474
18089
  return true;
16475
18090
  }
16476
- var gitignorePath = () => (0, import_node_path14.join)(process.cwd(), ".gitignore");
18091
+ var gitignorePath = () => (0, import_node_path16.join)(process.cwd(), ".gitignore");
16477
18092
  function readTextFile(path2) {
16478
18093
  try {
16479
- if (!(0, import_node_fs15.existsSync)(path2)) return null;
16480
- return (0, import_node_fs15.readFileSync)(path2, "utf8");
18094
+ if (!(0, import_node_fs18.existsSync)(path2)) return null;
18095
+ return (0, import_node_fs18.readFileSync)(path2, "utf8");
16481
18096
  } catch {
16482
18097
  return null;
16483
18098
  }
@@ -16486,9 +18101,9 @@ function playwrightMcpConfigSnapshots() {
16486
18101
  const cwd = process.cwd();
16487
18102
  const home = (0, import_node_os5.homedir)();
16488
18103
  const candidates = [
16489
- (0, import_node_path14.join)(cwd, ".cursor", "mcp.json"),
16490
- (0, import_node_path14.join)(home, ".cursor", "mcp.json"),
16491
- (0, import_node_path14.join)(home, ".codex", "config.toml")
18104
+ (0, import_node_path16.join)(cwd, ".cursor", "mcp.json"),
18105
+ (0, import_node_path16.join)(home, ".cursor", "mcp.json"),
18106
+ (0, import_node_path16.join)(home, ".codex", "config.toml")
16492
18107
  ];
16493
18108
  const out = [];
16494
18109
  for (const path2 of candidates) {
@@ -16501,7 +18116,7 @@ function strayBrowserArtifactPaths() {
16501
18116
  const cwd = process.cwd();
16502
18117
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
16503
18118
  try {
16504
- return (0, import_node_fs15.existsSync)((0, import_node_path14.join)(cwd, rel));
18119
+ return (0, import_node_fs18.existsSync)((0, import_node_path16.join)(cwd, rel));
16505
18120
  } catch {
16506
18121
  return false;
16507
18122
  }
@@ -16509,14 +18124,14 @@ function strayBrowserArtifactPaths() {
16509
18124
  }
16510
18125
  function readGitignore() {
16511
18126
  try {
16512
- return (0, import_node_fs15.readFileSync)(gitignorePath(), "utf8");
18127
+ return (0, import_node_fs18.readFileSync)(gitignorePath(), "utf8");
16513
18128
  } catch {
16514
18129
  return null;
16515
18130
  }
16516
18131
  }
16517
18132
  function writeGitignore(content) {
16518
18133
  try {
16519
- (0, import_node_fs15.writeFileSync)(gitignorePath(), content, "utf8");
18134
+ (0, import_node_fs18.writeFileSync)(gitignorePath(), content, "utf8");
16520
18135
  return true;
16521
18136
  } catch {
16522
18137
  return false;
@@ -16555,7 +18170,7 @@ async function runDoctor(opts, io = consoleIo) {
16555
18170
  let onPath = pathProbe;
16556
18171
  if (!onPath) {
16557
18172
  const root = process.env.CLAUDE_PLUGIN_ROOT;
16558
- if (root && (0, import_node_fs15.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
18173
+ if (root && (0, import_node_fs18.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
16559
18174
  }
16560
18175
  checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
16561
18176
  const surface = detectSurface(process.env);
@@ -16649,6 +18264,19 @@ async function runDoctor(opts, io = consoleIo) {
16649
18264
  io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reloadAction(surface)} to load the new commands`);
16650
18265
  }
16651
18266
  }
18267
+ const codexStale = installedVersionCheck.staleSurfaces?.some((s) => s.surface === "codex") ?? false;
18268
+ if (!installedVersionCheck.ok && codexStale && await applyCodexPluginHeal(surface, (m) => io.err(m))) {
18269
+ const healed = buildInstalledPluginVersionCheck({
18270
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
18271
+ sources: installedPluginSources(),
18272
+ releasedVersion,
18273
+ surface
18274
+ });
18275
+ installedVersionCheck = healed;
18276
+ if (healed.ok) {
18277
+ io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction(surface)} to load the new commands`);
18278
+ }
18279
+ }
16652
18280
  }
16653
18281
  checks.push(installedVersionCheck);
16654
18282
  let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
@@ -16659,7 +18287,8 @@ async function runDoctor(opts, io = consoleIo) {
16659
18287
  installedVersions: installedPluginVersions(installed)
16660
18288
  });
16661
18289
  if (!cacheCleanupCheck.ok && cacheCleanupCheck.quarantinePlan && repairLocal) {
16662
- const moved = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
18290
+ const { moved, failed } = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
18291
+ const attempted = moved + failed.length;
16663
18292
  if (moved > 0) {
16664
18293
  const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
16665
18294
  const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
@@ -16670,8 +18299,12 @@ async function runDoctor(opts, io = consoleIo) {
16670
18299
  isOrgRepo: Boolean(cfg.sagaApiUrl),
16671
18300
  roots: mmiPluginCacheRootSnapshots(),
16672
18301
  activeVersion: resolveClientVersion(),
16673
- releasedVersion
18302
+ releasedVersion,
18303
+ installedVersions: installedPluginVersions(installed)
16674
18304
  }),
18305
+ attemptedCount: attempted,
18306
+ failedCount: failed.length,
18307
+ ...failed.length > 0 ? { failedMoves: failed } : {},
16675
18308
  ...moved > 0 ? { cleanedCount: moved } : {}
16676
18309
  };
16677
18310
  }
@@ -16709,7 +18342,7 @@ async function runDoctor(opts, io = consoleIo) {
16709
18342
  isOrgRepo: Boolean(cfg.sagaApiUrl),
16710
18343
  surface,
16711
18344
  cacheRoot: cursorCacheRoot,
16712
- cacheRootExists: (0, import_node_fs15.existsSync)(cursorCacheRoot),
18345
+ cacheRootExists: (0, import_node_fs18.existsSync)(cursorCacheRoot),
16713
18346
  pins: cursorPins,
16714
18347
  hubCheckout: hubCheckoutForCursorSeed(),
16715
18348
  releasedVersion
@@ -16720,7 +18353,7 @@ async function runDoctor(opts, io = consoleIo) {
16720
18353
  releasedVersion,
16721
18354
  hubCheckout: hubCheckoutForCursorSeed(),
16722
18355
  execFileP: execFileP2,
16723
- mkdtemp: (prefix) => (0, import_promises5.mkdtemp)((0, import_node_path14.join)((0, import_node_os5.tmpdir)(), prefix)),
18356
+ mkdtemp: (prefix) => (0, import_promises5.mkdtemp)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), prefix)),
16724
18357
  log: (m) => io.err(m)
16725
18358
  });
16726
18359
  if (seeded) {
@@ -16729,7 +18362,7 @@ async function runDoctor(opts, io = consoleIo) {
16729
18362
  isOrgRepo: Boolean(cfg.sagaApiUrl),
16730
18363
  surface,
16731
18364
  cacheRoot: cursorCacheRoot,
16732
- cacheRootExists: (0, import_node_fs15.existsSync)(cursorCacheRoot),
18365
+ cacheRootExists: (0, import_node_fs18.existsSync)(cursorCacheRoot),
16733
18366
  pins: cursorPins,
16734
18367
  hubCheckout: hubCheckoutForCursorSeed(),
16735
18368
  releasedVersion
@@ -16812,6 +18445,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
16812
18445
  const { parallel, sequential } = buildSessionStartPlan({
16813
18446
  rulesSync: (io) => runRulesSync({ quiet: true }, io),
16814
18447
  sagaShow: (io) => runSagaShow({ quiet: true }, io),
18448
+ handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
16815
18449
  // honcho profile (#1162): inject the behavioral-memory prior (peer card). Bounded + fail-soft +
16816
18450
  // silent when off/empty, so it never delays or noises the session banner.
16817
18451
  honchoContext: (io) => runHonchoContext({ quiet: true, banner: true }, io),
@@ -16845,6 +18479,12 @@ program2.command("session-start").description("run the SessionStart verbs (rules
16845
18479
  await runSessionStart(parallel, sequential, consoleIo);
16846
18480
  consoleIo.log(northstarPointer(northstarInjected));
16847
18481
  for (const line of planStoreLines(process.cwd())) consoleIo.log(line);
18482
+ for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
18483
+ const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
18484
+ if (worktreeBanner) {
18485
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
18486
+ consoleIo.log(worktreeBanner);
18487
+ }
16848
18488
  });
16849
18489
  installProcessBackstop();
16850
18490
  program2.parseAsync().catch((e) => failGraceful(e.message));