@mutmutco/cli 2.54.2 → 2.56.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
@@ -3408,7 +3408,7 @@ var program = new Command();
3408
3408
 
3409
3409
  // src/index.ts
3410
3410
  var import_promises8 = require("node:fs/promises");
3411
- var import_node_fs28 = require("node:fs");
3411
+ var import_node_fs29 = require("node:fs");
3412
3412
 
3413
3413
  // src/rules-sync.ts
3414
3414
  function normalizeEol(s) {
@@ -5594,7 +5594,7 @@ async function restoreDirtyOrgSpineToHead(deps, options = {}) {
5594
5594
  }
5595
5595
 
5596
5596
  // src/index.ts
5597
- var import_node_child_process13 = require("node:child_process");
5597
+ var import_node_child_process15 = require("node:child_process");
5598
5598
 
5599
5599
  // src/cli-shared.ts
5600
5600
  var import_promises = require("node:fs/promises");
@@ -9862,7 +9862,7 @@ async function runCoopDeliver(coopId, io, quiet = false) {
9862
9862
  if (!quiet) {
9863
9863
  io.log(`[coop deliver] ${coopId} \u2014 issue ${match.meta.issueUrl}`);
9864
9864
  for (const m of match.messages) io.log(m.envelope);
9865
- io.log("[coop deliver] Read the issue, reply via `mmi-cli coop say`, then mark delivered.");
9865
+ io.log("[coop deliver] Coordinate in `#mmi-agents` via `mmi-cli coop say`; the issue keeps proof/context.");
9866
9866
  }
9867
9867
  await postJson("/coop/delivered", { coopId });
9868
9868
  }
@@ -9874,7 +9874,7 @@ function registerCoopCommands(program3) {
9874
9874
  { file: opts.messageFile },
9875
9875
  { readFile: import_promises4.readFile, readStdin },
9876
9876
  { value: "a message argument", file: "--message-file", noun: "message" }
9877
- ) : "Coop session started \u2014 handshake on the linked issue.";
9877
+ ) : "Coop session started - coordinate in #mmi-agents; the issue keeps proof/context.";
9878
9878
  const payload = {
9879
9879
  repo: opts.repo ?? ctx.repo,
9880
9880
  branch: ctx.branch,
@@ -9955,14 +9955,856 @@ function registerCoopCommands(program3) {
9955
9955
  });
9956
9956
  }
9957
9957
 
9958
- // src/throttle-commands.ts
9958
+ // src/overlord.ts
9959
9959
  var import_node_child_process8 = require("node:child_process");
9960
- var import_node_fs14 = require("node:fs");
9960
+ var import_node_fs15 = require("node:fs");
9961
+ var import_node_os3 = require("node:os");
9961
9962
  var import_node_path13 = require("node:path");
9962
- var THROTTLE_TRACE_REL = (0, import_node_path13.join)(".mmi", "throttle", "trace.jsonl");
9963
+
9964
+ // src/atomic-write.ts
9965
+ var import_node_fs14 = require("node:fs");
9966
+ function atomicWriteFileSync(path2, content) {
9967
+ const tmp = `${path2}.${process.pid}.tmp`;
9968
+ (0, import_node_fs14.writeFileSync)(tmp, content, "utf8");
9969
+ (0, import_node_fs14.renameSync)(tmp, path2);
9970
+ }
9971
+
9972
+ // src/overlord.ts
9973
+ var OVERLORD_DEFAULT_COUNT = 3;
9974
+ var OVERLORD_MIN_COUNT = 3;
9975
+ var OVERLORD_MAX_COUNT = 6;
9976
+ var OVERLORD_HANDOFF_TIMEOUT_MS = 12e4;
9977
+ var GENERIC_STOP_NAMES = /* @__PURE__ */ new Set([
9978
+ "windowsterminal",
9979
+ "windows terminal",
9980
+ "pwsh",
9981
+ "powershell",
9982
+ "opencode",
9983
+ "codex",
9984
+ "codex-fugu"
9985
+ ]);
9986
+ function numericCountArg(arg) {
9987
+ const match = /^--([0-9]+)$/.exec(arg);
9988
+ return match ? Number(match[1]) : void 0;
9989
+ }
9990
+ function isoNow(now = () => /* @__PURE__ */ new Date()) {
9991
+ return now().toISOString();
9992
+ }
9993
+ function defaultRunId() {
9994
+ return `overlord-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
9995
+ }
9996
+ function defaultRunToken() {
9997
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
9998
+ }
9999
+ function defaultMessageId() {
10000
+ return `msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
10001
+ }
10002
+ function servantSlotId(slot) {
10003
+ return slot.name.replace(/\s+/g, "-").toLowerCase();
10004
+ }
10005
+ function overlordServantPrompt(servant, run) {
10006
+ const roleLine = servant.role === "ultra" ? "You are the single Ultra Fugu: take the hardest, highest-uncertainty questions and report calibrated judgment." : "You are a normal Fugu servant: take one bounded mission at a time and report concise evidence.";
10007
+ return [
10008
+ `You are ${servant.name} in Overlord run ${run.runId}.`,
10009
+ roleLine,
10010
+ "First respond with exactly: ACK " + servant.name + " ready",
10011
+ "After the ACK, wait for the Overlord to assign bounded work.",
10012
+ "Do not start dev servers, browsers, Playwright, PRs, merges, releases, or worktree changes unless the Overlord explicitly assigns that scope.",
10013
+ "When assigned work, gather evidence before editing, verify before claiming done, and escalate blockers instead of looping."
10014
+ ].join("\n");
10015
+ }
10016
+ function parseOverlordCount(args) {
10017
+ const counts = args.map(numericCountArg).filter((n) => n !== void 0);
10018
+ if (counts.length === 0) return OVERLORD_DEFAULT_COUNT;
10019
+ if (counts.length > 1) throw new Error("provide only one overlord servant count");
10020
+ const count = counts[0];
10021
+ if (count < OVERLORD_MIN_COUNT || count > OVERLORD_MAX_COUNT) {
10022
+ throw new Error(`overlord servant count must be between ${OVERLORD_MIN_COUNT} and ${OVERLORD_MAX_COUNT}`);
10023
+ }
10024
+ return count;
10025
+ }
10026
+ function buildServantLayout(count) {
10027
+ if (count < OVERLORD_MIN_COUNT || count > OVERLORD_MAX_COUNT) {
10028
+ throw new Error(`overlord servant count must be between ${OVERLORD_MIN_COUNT} and ${OVERLORD_MAX_COUNT}`);
10029
+ }
10030
+ const slots = [{
10031
+ name: "fugu ultra 1",
10032
+ role: "ultra",
10033
+ model: "fugu-ultra",
10034
+ index: 1
10035
+ }];
10036
+ for (let i = 1; i < count; i++) {
10037
+ slots.push({
10038
+ name: `fugu normal ${i}`,
10039
+ role: "normal",
10040
+ model: "fugu",
10041
+ index: i
10042
+ });
10043
+ }
10044
+ return slots;
10045
+ }
10046
+ function validateCodexFuguHelp(helpText) {
10047
+ const problems = [];
10048
+ if (!/(?:^|\s)-a(?:,|\s)|--ask-for-approval/.test(helpText)) problems.push("missing approval flag");
10049
+ if (!/(?:^|\s)-s(?:,|\s)|--sandbox/.test(helpText)) problems.push("missing sandbox flag");
10050
+ if (!/(?:^|\s)-c(?:,|\s)|--config/.test(helpText)) problems.push("missing config override flag");
10051
+ if (!/--no-alt-screen/.test(helpText)) problems.push("missing no-alt-screen flag");
10052
+ if (!/(?:^|\s)-C(?:,|\s)|--cwd|--cd|--workdir/.test(helpText)) problems.push("missing cwd flag");
10053
+ return problems;
10054
+ }
10055
+ function evaluateCodexFuguPreflight(input) {
10056
+ const problems = [];
10057
+ if (!input.codexFound) problems.push("codex is not installed or not on PATH");
10058
+ if (!input.fuguFound) problems.push("codex-fugu is not installed or not on PATH");
10059
+ if (!input.authConfigured && !input.envNames.includes("OPENAI_API_KEY") && !input.envNames.includes("CODEX_API_KEY")) {
10060
+ problems.push("missing API key environment: OPENAI_API_KEY or CODEX_API_KEY");
10061
+ }
10062
+ problems.push(...validateCodexFuguHelp(input.helpText ?? ""));
10063
+ const status = input.statusText ?? "";
10064
+ const modelCatalog = input.modelCatalogText ?? "";
10065
+ if (!/\bfugu-ultra\b/i.test(`${status}
10066
+ ${modelCatalog}`)) problems.push("fugu-ultra is not available");
10067
+ if (/native windows codex[^\n]*\/c\/users|\/c\/users[^\n]*native windows codex/i.test(status)) {
10068
+ problems.push("native Windows Codex is configured with a Git Bash /c/Users path");
10069
+ }
10070
+ return { ok: problems.length === 0, problems };
10071
+ }
10072
+ function evaluateOpenCodePreflight(input) {
10073
+ const problems = [];
10074
+ if (!input.found) problems.push("opencode is not installed or not on PATH");
10075
+ if (!/sakana\/fugu\b/i.test(input.modelsText ?? "")) problems.push("sakana/fugu is not available");
10076
+ if (!/sakana\/fugu-ultra\b/i.test(input.modelsText ?? "")) problems.push("sakana/fugu-ultra is not available");
10077
+ const probe = input.jsonProbeText ?? "";
10078
+ if (probe && !/(sessionID|sessionId|step_finish|finish)/i.test(probe)) problems.push("opencode run --format json did not emit session or finish events");
10079
+ return { ok: problems.length === 0, problems };
10080
+ }
10081
+ function buildOpenCodeLaunch(slot, message, sessionId) {
10082
+ const model = slot.role === "ultra" ? "sakana/fugu-ultra" : "sakana/fugu";
10083
+ const args = ["run", "-m", model, "--format", "json"];
10084
+ if (sessionId) args.push("--session", sessionId);
10085
+ args.push(message);
10086
+ return { command: "opencode", args };
10087
+ }
10088
+ function parseOpenCodeEvents(raw) {
10089
+ const events = [];
10090
+ const trimmed = (raw ?? "").trim();
10091
+ if (!trimmed) return { text: "", finished: false, events };
10092
+ const pushParsed = (chunk) => {
10093
+ const t = chunk.trim();
10094
+ if (!t) return;
10095
+ try {
10096
+ events.push(JSON.parse(t));
10097
+ } catch {
10098
+ }
10099
+ };
10100
+ try {
10101
+ const whole = JSON.parse(trimmed);
10102
+ if (Array.isArray(whole)) events.push(...whole);
10103
+ else events.push(whole);
10104
+ } catch {
10105
+ for (const line of trimmed.split(/\r?\n/)) pushParsed(line);
10106
+ }
10107
+ let sessionId;
10108
+ let text = "";
10109
+ let finished = false;
10110
+ for (const ev of events) {
10111
+ if (!ev || typeof ev !== "object") continue;
10112
+ const e = ev;
10113
+ const sid = e.sessionID ?? e.sessionId ?? e.session?.id;
10114
+ if (typeof sid === "string" && sid) sessionId = sid;
10115
+ const type = typeof e.type === "string" ? e.type : void 0;
10116
+ if (typeof e.text === "string") text += e.text;
10117
+ else if (type === "text" && typeof e.content === "string") text += e.content;
10118
+ if (type === "step_finish" || type === "finish" || e.finishReason || e.finish_reason) finished = true;
10119
+ }
10120
+ return { sessionId, text: text.trim(), finished, events };
10121
+ }
10122
+ function appendOverlordLedger(ledgerPath, entry) {
10123
+ try {
10124
+ (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(ledgerPath), { recursive: true });
10125
+ (0, import_node_fs15.appendFileSync)(ledgerPath, `${JSON.stringify(entry)}
10126
+ `, "utf8");
10127
+ } catch {
10128
+ }
10129
+ }
10130
+ function defaultOverlordStatePath(cwd) {
10131
+ return (0, import_node_path13.join)(cwd, "tmp", "overlord", "runs.json");
10132
+ }
10133
+ function readOverlordRegistry(statePath) {
10134
+ if (!(0, import_node_fs15.existsSync)(statePath)) return { runs: {} };
10135
+ try {
10136
+ const parsed = JSON.parse((0, import_node_fs15.readFileSync)(statePath, "utf8"));
10137
+ return { activeRunId: parsed.activeRunId, runs: parsed.runs ?? {} };
10138
+ } catch {
10139
+ return { runs: {} };
10140
+ }
10141
+ }
10142
+ function writeOverlordRegistry(statePath, registry2) {
10143
+ (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(statePath), { recursive: true });
10144
+ atomicWriteFileSync(statePath, `${JSON.stringify(registry2, null, 2)}
10145
+ `);
10146
+ }
10147
+ function buildOverlordRun(options) {
10148
+ const runId = options.runId?.() ?? defaultRunId();
10149
+ const runToken = options.runToken?.() ?? defaultRunToken();
10150
+ const statePath = defaultOverlordStatePath(options.cwd);
10151
+ const journalDir = (0, import_node_path13.join)(options.cwd, "tmp", "overlord", runId);
10152
+ const engine = options.engine ?? "codex-fugu";
10153
+ const timestamp = isoNow(options.now);
10154
+ return {
10155
+ runId,
10156
+ runToken,
10157
+ task: options.task,
10158
+ state: "starting",
10159
+ createdAt: timestamp,
10160
+ updatedAt: timestamp,
10161
+ worktree: options.cwd,
10162
+ statePath,
10163
+ journalDir,
10164
+ ledgerPath: (0, import_node_path13.join)(journalDir, "ledger.jsonl"),
10165
+ engine,
10166
+ provider: engine === "opencode" ? "opencode" : "codex",
10167
+ opencodeVersion: options.opencodeVersion,
10168
+ servants: buildServantLayout(options.count).map((slot) => ({
10169
+ ...slot,
10170
+ slotId: servantSlotId(slot),
10171
+ profile: "consultation",
10172
+ state: "planned",
10173
+ journalPath: (0, import_node_path13.join)(journalDir, `${servantSlotId(slot)}.log`),
10174
+ composerSubmitMode: engine === "opencode" ? "surface-api" : "unknown",
10175
+ engine,
10176
+ provider: engine === "opencode" ? "opencode" : "codex",
10177
+ opencodeVersion: options.opencodeVersion,
10178
+ eventJournalPath: engine === "opencode" ? (0, import_node_path13.join)(journalDir, `${servantSlotId(slot)}.events.jsonl`) : void 0
10179
+ })),
10180
+ ownedResources: []
10181
+ };
10182
+ }
10183
+ function recordOverlordHeartbeat(run, options) {
10184
+ const timestamp = isoNow(options.now);
10185
+ const controllerResource = {
10186
+ kind: "process",
10187
+ pid: options.controllerPid,
10188
+ commandName: "mmi-cli overlord controller",
10189
+ runId: run.runId,
10190
+ runToken: run.runToken,
10191
+ fingerprint: options.fingerprint
10192
+ };
10193
+ const others = run.ownedResources.filter((resource) => resource.commandName !== "mmi-cli overlord controller");
10194
+ return {
10195
+ ...run,
10196
+ state: run.state === "starting" ? "active" : run.state,
10197
+ updatedAt: timestamp,
10198
+ controllerPid: options.controllerPid,
10199
+ controllerFingerprint: options.fingerprint,
10200
+ lastControllerHeartbeatAt: timestamp,
10201
+ ownedResources: [controllerResource, ...others]
10202
+ };
10203
+ }
10204
+ function buildOverlordStartupPlan(args, cwd) {
10205
+ const count = parseOverlordCount(args);
10206
+ const task = args.filter((arg) => numericCountArg(arg) === void 0).join(" ").trim();
10207
+ return {
10208
+ count,
10209
+ servants: buildServantLayout(count),
10210
+ task,
10211
+ statePath: defaultOverlordStatePath(cwd),
10212
+ nextPhase: "preflight"
10213
+ };
10214
+ }
10215
+ function normalizedCommandName(resource) {
10216
+ return (resource.commandName ?? resource.title ?? "").trim().toLowerCase();
10217
+ }
10218
+ function planStopResource(resource, context) {
10219
+ const exactOwner = resource.runId === context.runId && resource.runToken === context.runToken && Boolean(resource.fingerprint);
10220
+ if (exactOwner) return { action: "stop", reason: "owned" };
10221
+ const name = normalizedCommandName(resource);
10222
+ if (!name || GENERIC_STOP_NAMES.has(name)) return { action: "refuse", reason: "ownership not proven" };
10223
+ return { action: "refuse", reason: "ownership not proven" };
10224
+ }
10225
+ function summarizeOverlordRun(run, probe) {
10226
+ const controller = run.controllerPid == null ? "not-started" : probe.isPidAlive(run.controllerPid) ? "alive" : "lost";
10227
+ const now = probe.now?.() ?? /* @__PURE__ */ new Date();
10228
+ return {
10229
+ active: run.state !== "stopped" && run.state !== "failed",
10230
+ runId: run.runId,
10231
+ state: run.state,
10232
+ controller,
10233
+ servants: run.servants.map((servant) => {
10234
+ if (run.engine === "opencode" || servant.engine === "opencode") return { name: servant.name, state: servantProgress(run, servant, now) };
10235
+ if (servant.pid == null) return { name: servant.name, state: "not-started" };
10236
+ if (!probe.isPidAlive(servant.pid)) return { name: servant.name, state: "lost" };
10237
+ const progress = servantProgress(run, servant, now);
10238
+ if (progress !== servant.state) return { name: servant.name, state: progress };
10239
+ if (servant.state === "ready" && servant.lastAckAt && servant.composerSubmitMode !== "unknown") {
10240
+ return { name: servant.name, state: "ready" };
10241
+ }
10242
+ return { name: servant.name, state: servant.state };
10243
+ })
10244
+ };
10245
+ }
10246
+ function planOverlordRunStop(run) {
10247
+ const killPids = [];
10248
+ const uncertain = [];
10249
+ for (const resource of run.ownedResources) {
10250
+ const plan2 = planStopResource(resource, { runId: run.runId, runToken: run.runToken });
10251
+ if (plan2.action === "stop" && resource.pid != null) killPids.push(resource.pid);
10252
+ else uncertain.push(resource);
10253
+ }
10254
+ return { killPids, uncertain };
10255
+ }
10256
+ function controllerFingerprint(run) {
10257
+ return `mmi-overlord-controller:${run.runId}:${run.worktree}`;
10258
+ }
10259
+ function defaultStartController(run) {
10260
+ const scriptPath = (0, import_node_path13.join)(__dirname, "overlord-controller.cjs");
10261
+ const fingerprint = controllerFingerprint(run);
10262
+ const child = (0, import_node_child_process8.spawn)(process.execPath, [scriptPath, run.runId, run.statePath, fingerprint], {
10263
+ detached: true,
10264
+ stdio: "ignore",
10265
+ windowsHide: true,
10266
+ cwd: run.worktree
10267
+ });
10268
+ child.unref();
10269
+ return { pid: child.pid, fingerprint };
10270
+ }
10271
+ function defaultRunOpenCode(run, servant, message, sessionId) {
10272
+ const launch = buildOpenCodeLaunch(servant, message, sessionId ?? servant.opencodeSessionId);
10273
+ const shell2 = process.platform === "win32";
10274
+ const file = shell2 ? [launch.command, ...launch.args].map(shellQuote2).join(" ") : launch.command;
10275
+ const result = (0, import_node_child_process8.spawnSync)(file, shell2 ? [] : launch.args, {
10276
+ encoding: "utf8",
10277
+ shell: shell2,
10278
+ cwd: run.worktree,
10279
+ timeout: 6e5,
10280
+ windowsHide: true,
10281
+ env: { ...process.env }
10282
+ });
10283
+ if (result.error && result.error.code === "ENOENT") {
10284
+ return { ok: false, error: "opencode is not installed or not on PATH" };
10285
+ }
10286
+ const raw = `${result.stdout ?? ""}
10287
+ ${result.stderr ?? ""}`;
10288
+ if (servant.eventJournalPath) {
10289
+ try {
10290
+ (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(servant.eventJournalPath), { recursive: true });
10291
+ (0, import_node_fs15.appendFileSync)(servant.eventJournalPath, `${raw.trim()}
10292
+ `, "utf8");
10293
+ } catch {
10294
+ }
10295
+ }
10296
+ const parsed = parseOpenCodeEvents(raw);
10297
+ if (result.status !== 0 && !parsed.finished) {
10298
+ return { ok: false, sessionId: parsed.sessionId, text: parsed.text, error: `opencode run exited ${result.status ?? "with error"}`, events: parsed.events };
10299
+ }
10300
+ return { ok: true, sessionId: parsed.sessionId, text: parsed.text, events: parsed.events };
10301
+ }
10302
+ function defaultIsPidAlive(pid) {
10303
+ if (!Number.isFinite(pid) || pid <= 0) return false;
10304
+ try {
10305
+ process.kill(pid, 0);
10306
+ return true;
10307
+ } catch {
10308
+ return false;
10309
+ }
10310
+ }
10311
+ function defaultKillPid(pid) {
10312
+ process.kill(pid);
10313
+ }
10314
+ function shellQuote2(arg) {
10315
+ return /^[A-Za-z0-9_./:-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`;
10316
+ }
10317
+ function boundedCommandText(command, args) {
10318
+ const shell2 = process.platform === "win32";
10319
+ const file = shell2 ? [command, ...args].map(shellQuote2).join(" ") : command;
10320
+ const result = (0, import_node_child_process8.spawnSync)(file, shell2 ? [] : args, {
10321
+ encoding: "utf8",
10322
+ shell: shell2,
10323
+ timeout: 5e3,
10324
+ windowsHide: true,
10325
+ env: {
10326
+ ...process.env,
10327
+ CODEX_FUGU_NO_NOTICE: "1",
10328
+ CODEX_FUGU_NO_UPDATE: "1"
10329
+ }
10330
+ });
10331
+ const text = `${result.stdout ?? ""}
10332
+ ${result.stderr ?? ""}`.trim();
10333
+ if (result.error && result.error.code === "ENOENT") return { found: false, text };
10334
+ return { found: !result.error, text };
10335
+ }
10336
+ function readFuguModelCatalogText() {
10337
+ const codexHome = process.env.CODEX_HOME ?? (0, import_node_path13.join)((0, import_node_os3.homedir)(), ".codex");
10338
+ const catalogPath = (0, import_node_path13.join)(codexHome, "fugu.json");
10339
+ try {
10340
+ return (0, import_node_fs15.existsSync)(catalogPath) ? (0, import_node_fs15.readFileSync)(catalogPath, "utf8") : "";
10341
+ } catch {
10342
+ return "";
10343
+ }
10344
+ }
10345
+ function hasCodexAuthEvidence() {
10346
+ const codexHome = process.env.CODEX_HOME ?? (0, import_node_path13.join)((0, import_node_os3.homedir)(), ".codex");
10347
+ return (0, import_node_fs15.existsSync)((0, import_node_path13.join)(codexHome, "auth.json"));
10348
+ }
10349
+ function collectOpenCodePreflight() {
10350
+ const version = boundedCommandText("opencode", ["--version"]);
10351
+ const models = boundedCommandText("opencode", ["models"]);
10352
+ return evaluateOpenCodePreflight({
10353
+ found: version.found,
10354
+ versionText: version.text,
10355
+ modelsText: models.text
10356
+ });
10357
+ }
10358
+ function collectCodexFuguPreflight() {
10359
+ const codexHelp = boundedCommandText("codex", ["--help"]);
10360
+ const fuguHelp = boundedCommandText("codex-fugu", ["--help"]);
10361
+ const fuguStatus = boundedCommandText("codex-fugu", ["--status"]);
10362
+ return evaluateCodexFuguPreflight({
10363
+ codexFound: codexHelp.found,
10364
+ fuguFound: fuguHelp.found,
10365
+ helpText: fuguHelp.text || codexHelp.text,
10366
+ statusText: fuguStatus.text,
10367
+ modelCatalogText: readFuguModelCatalogText(),
10368
+ authConfigured: hasCodexAuthEvidence(),
10369
+ envNames: Object.keys(process.env)
10370
+ });
10371
+ }
10372
+ function renderCodexFuguPreflightFailure(report) {
10373
+ const lines = [
10374
+ "Overlord setup needed",
10375
+ "The servant pool was not started because Codex/Fugu preflight failed.",
10376
+ "",
10377
+ "Problems:",
10378
+ ...report.problems.map((problem) => `- ${problem}`),
10379
+ "",
10380
+ "Fixes:"
10381
+ ];
10382
+ if (report.problems.some((problem) => problem.includes("codex is not installed"))) {
10383
+ lines.push("- Install or update Codex, then confirm with `codex --version`.");
10384
+ }
10385
+ if (report.problems.some((problem) => problem.includes("codex-fugu is not installed"))) {
10386
+ lines.push("- Install or repair codex-fugu, then confirm with `codex-fugu --status`.");
10387
+ }
10388
+ if (report.problems.some((problem) => problem.includes("missing API key"))) {
10389
+ lines.push("- Set OPENAI_API_KEY or CODEX_API_KEY in the launching shell; do not paste key values into chat.");
10390
+ }
10391
+ if (report.problems.some((problem) => problem.includes("fugu-ultra"))) {
10392
+ lines.push("- Recheck or update Fugu configs so `fugu-ultra` appears in the Fugu model catalog.");
10393
+ }
10394
+ if (report.problems.some((problem) => problem.includes("missing ") && problem.includes("flag"))) {
10395
+ lines.push("- Update Codex/Fugu until `codex-fugu --help` exposes approval, sandbox, config, cwd, and no-alt-screen flags.");
10396
+ }
10397
+ if (report.problems.some((problem) => problem.includes("/c/Users"))) {
10398
+ lines.push("- Launch from a native shell adapter or repair Fugu path configuration so Windows paths stay native.");
10399
+ }
10400
+ return lines.join("\n");
10401
+ }
10402
+ function updateOpenCodeServant(run, servant, result, timestamp) {
10403
+ return {
10404
+ ...servant,
10405
+ state: result.ok ? "ready" : "blocked",
10406
+ opencodeSessionId: result.sessionId ?? servant.opencodeSessionId,
10407
+ lastAckAt: result.ok ? timestamp : servant.lastAckAt,
10408
+ lastUsefulSignalAt: result.ok ? timestamp : servant.lastUsefulSignalAt,
10409
+ lastEventAt: timestamp,
10410
+ lastMessageCompletedAt: result.ok ? timestamp : servant.lastMessageCompletedAt
10411
+ };
10412
+ }
10413
+ function startOpenCodeServants(run, runOpenCode, now) {
10414
+ const timestamp = isoNow(now);
10415
+ const servants = run.servants.map((servant) => {
10416
+ const result = runOpenCode(run, servant, overlordServantPrompt(servant, run));
10417
+ if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: timestamp, kind: "servant-start", ownerSlotId: servant.slotId, ok: result.ok, sessionId: result.sessionId, responseText: result.text, error: result.error });
10418
+ return updateOpenCodeServant(run, servant, result, timestamp);
10419
+ });
10420
+ return { ...run, state: "active", updatedAt: timestamp, servants };
10421
+ }
10422
+ function dispatchOpenCodeMessage(run, message, runOpenCode, now) {
10423
+ const timestamp = isoNow(now);
10424
+ const targets = run.servants.filter((servant) => message.target === "all" || servant.slotId === message.target || normalizeServantTarget(servant.name) === message.target);
10425
+ const responses = [];
10426
+ let ok = targets.length > 0;
10427
+ const nextServants = run.servants.map((servant) => {
10428
+ if (!targets.some((target) => target.slotId === servant.slotId)) return servant;
10429
+ const result = runOpenCode(run, servant, message.text, servant.opencodeSessionId);
10430
+ if (!result.ok) ok = false;
10431
+ if (result.text) responses.push(`${servant.slotId}: ${result.text}`);
10432
+ if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: timestamp, kind: "message-response", messageId: message.id, ownerSlotId: servant.slotId, ok: result.ok, sessionId: result.sessionId, responseText: result.text, error: result.error });
10433
+ return updateOpenCodeServant(run, servant, result, timestamp);
10434
+ });
10435
+ const nextMessage = {
10436
+ ...message,
10437
+ state: ok ? "completed" : "failed",
10438
+ startedAt: timestamp,
10439
+ completedAt: ok ? timestamp : void 0,
10440
+ failedAt: ok ? void 0 : timestamp,
10441
+ ackText: ok ? "opencode response captured" : void 0,
10442
+ responseText: responses.join("\n"),
10443
+ failureReason: ok ? void 0 : "one or more OpenCode servant calls failed",
10444
+ eventJournalPath: targets.map((target) => target.eventJournalPath).filter(Boolean).join(",")
10445
+ };
10446
+ return {
10447
+ ...run,
10448
+ updatedAt: timestamp,
10449
+ servants: nextServants,
10450
+ messages: [...(run.messages ?? []).filter((m) => m.id !== message.id), nextMessage]
10451
+ };
10452
+ }
10453
+ function findActiveRun(registry2) {
10454
+ const runId = registry2.activeRunId;
10455
+ if (!runId) return void 0;
10456
+ return registry2.runs[runId];
10457
+ }
10458
+ function normalizeServantTarget(target) {
10459
+ return target.trim().toLowerCase().replace(/\s+/g, "-");
10460
+ }
10461
+ function hasServantTarget(run, target) {
10462
+ const normalized = normalizeServantTarget(target);
10463
+ return normalized === "all" || run.servants.some(
10464
+ (servant) => servant.slotId === normalized || normalizeServantTarget(servant.name) === normalized
10465
+ );
10466
+ }
10467
+ function messageProgress(message, now = /* @__PURE__ */ new Date(), timeoutMs = OVERLORD_HANDOFF_TIMEOUT_MS) {
10468
+ if (message.completedAt || message.state === "completed") return "completed";
10469
+ if (message.failedAt || message.state === "failed") return "failed";
10470
+ const startedAt = message.startedAt ?? message.deliveredAt;
10471
+ if (!startedAt) return "queued";
10472
+ const elapsed = now.getTime() - new Date(startedAt).getTime();
10473
+ return Number.isFinite(elapsed) && elapsed >= timeoutMs ? "stalled" : "started";
10474
+ }
10475
+ function servantProgress(run, servant, now = /* @__PURE__ */ new Date()) {
10476
+ const relevant = (run.messages ?? []).filter((message) => message.target === "all" || servant.slotId === message.target || normalizeServantTarget(servant.name) === message.target);
10477
+ return relevant.some((message) => messageProgress(message, now) === "stalled") ? "stalled-after-delivery" : servant.state;
10478
+ }
10479
+ function renderOverlordStatus(summary, run) {
10480
+ const servants = summary.servants.map((servant) => `- ${servant.name}: ${servant.state}`).join("\n");
10481
+ const messages = (run.messages ?? []).map((message) => `- ${message.id}: ${message.target} ${messageProgress(message)}`).join("\n");
10482
+ return [
10483
+ `Overlord run ${summary.runId}`,
10484
+ `State: ${summary.state}`,
10485
+ `Task: ${run.task || "not provided yet"}`,
10486
+ `Controller: ${summary.controller}`,
10487
+ "Servants:",
10488
+ servants,
10489
+ ...messages ? ["Messages:", messages] : []
10490
+ ].join("\n");
10491
+ }
10492
+ function usefulJournalLines(text) {
10493
+ return text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !/^fugu\s/i.test(line) && !/^›/.test(line) && !/^\[overlord\] launched/.test(line)).slice(-20);
10494
+ }
10495
+ function servantJournalSummary(servant) {
10496
+ try {
10497
+ const lines = usefulJournalLines((0, import_node_fs15.readFileSync)(servant.journalPath, "utf8"));
10498
+ return { lines, hasHandoff: lines.some((line) => /\b(handoff|evidence|recommended|recommendation)\b/i.test(line)) };
10499
+ } catch {
10500
+ return { lines: [], hasHandoff: false };
10501
+ }
10502
+ }
10503
+ function runJson(run, extra = {}) {
10504
+ return {
10505
+ ok: true,
10506
+ runId: run.runId,
10507
+ state: run.state,
10508
+ task: run.task,
10509
+ count: run.servants.length,
10510
+ engine: run.engine,
10511
+ controllerPid: run.controllerPid,
10512
+ statePath: run.statePath,
10513
+ ledgerPath: run.ledgerPath,
10514
+ servants: run.servants.map((servant) => ({
10515
+ name: servant.name,
10516
+ role: servant.role,
10517
+ model: servant.model,
10518
+ state: servant.state,
10519
+ pid: servant.pid,
10520
+ journalPath: servant.journalPath,
10521
+ engine: servant.engine,
10522
+ opencodeSessionId: servant.opencodeSessionId
10523
+ })),
10524
+ ...extra
10525
+ };
10526
+ }
10527
+ function wantsJson(options, command) {
10528
+ return Boolean(options.json || command?.parent?.opts?.().json);
10529
+ }
10530
+ function countArgsFromOptions(options) {
10531
+ return ["3", "4", "5", "6"].filter((key) => options[key]).map((key) => `--${key}`);
10532
+ }
10533
+ function registerOverlordCommands(program3, deps = {}) {
10534
+ const out = deps.out ?? ((text) => process.stdout.write(text));
10535
+ const err = deps.err ?? ((text) => process.stderr.write(text));
10536
+ const cwd = deps.cwd ?? (() => process.cwd());
10537
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
10538
+ const preflight2 = deps.preflight ?? collectCodexFuguPreflight;
10539
+ const opencodePreflight = deps.opencodePreflight ?? collectOpenCodePreflight;
10540
+ const startController = deps.startController ?? defaultStartController;
10541
+ const runOpenCode = deps.runOpenCode ?? defaultRunOpenCode;
10542
+ const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
10543
+ const killPid = deps.killPid ?? defaultKillPid;
10544
+ const overlord = program3.command("overlord").description("coordinate one Ultra and normal Fugu servants for hard org work").allowUnknownOption(true).argument("[task...]", "task for the Overlord system").option("--3", "run one Ultra and two normal Fugus").option("--4", "run one Ultra and three normal Fugus").option("--5", "run one Ultra and four normal Fugus").option("--6", "run one Ultra and five normal Fugus").option("--engine <engine>", "servant engine: opencode or codex-fugu", "codex-fugu").option("--json", "print machine-readable output").action((task = [], options) => {
10545
+ try {
10546
+ const args = [...countArgsFromOptions(options), ...task];
10547
+ const root = cwd();
10548
+ const plan2 = buildOverlordStartupPlan(args, root);
10549
+ const engine = `${options.engine ?? "codex-fugu"}`;
10550
+ if (engine !== "codex-fugu" && engine !== "opencode") throw new Error("--engine must be opencode or codex-fugu");
10551
+ const preflightReport = engine === "opencode" ? opencodePreflight() : preflight2();
10552
+ if (!preflightReport.ok) {
10553
+ err(`${renderCodexFuguPreflightFailure(preflightReport)}
10554
+ `);
10555
+ process.exitCode = 1;
10556
+ return;
10557
+ }
10558
+ const registry2 = readOverlordRegistry(plan2.statePath);
10559
+ const active = findActiveRun(registry2);
10560
+ if (active && active.state !== "stopped" && active.state !== "failed") {
10561
+ throw new Error(`active Overlord run already exists: ${active.runId}`);
10562
+ }
10563
+ let run = buildOverlordRun({
10564
+ task: plan2.task,
10565
+ cwd: root,
10566
+ count: plan2.count,
10567
+ engine,
10568
+ now,
10569
+ runId: deps.runId,
10570
+ runToken: deps.runToken
10571
+ });
10572
+ writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
10573
+ if (engine === "opencode") {
10574
+ run = startOpenCodeServants(run, runOpenCode, now);
10575
+ writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
10576
+ } else {
10577
+ const controller = startController(run);
10578
+ if (controller.pid != null) {
10579
+ run = recordOverlordHeartbeat(run, {
10580
+ controllerPid: controller.pid,
10581
+ fingerprint: controller.fingerprint,
10582
+ now
10583
+ });
10584
+ writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
10585
+ }
10586
+ }
10587
+ if (options.json) out(`${JSON.stringify(runJson(run, { nextPhase: "consult-servants" }), null, 2)}
10588
+ `);
10589
+ else {
10590
+ out(`${[
10591
+ "Overlord startup",
10592
+ `Task: ${run.task || "not provided yet"}`,
10593
+ `Run: ${run.runId}`,
10594
+ `Servants: ${run.servants.length} total`,
10595
+ `Controller: ${run.controllerPid ? `started (pid ${run.controllerPid})` : "launch requested"}`,
10596
+ "Next: consult servants, interview the human, then print an approved todo list."
10597
+ ].join("\n")}
10598
+ `);
10599
+ }
10600
+ } catch (e) {
10601
+ const message = e instanceof Error ? e.message : String(e);
10602
+ if (options.json) out(`${JSON.stringify({ ok: false, error: message }, null, 2)}
10603
+ `);
10604
+ else err(`overlord: ${message}
10605
+ `);
10606
+ process.exitCode = 1;
10607
+ }
10608
+ });
10609
+ overlord.command("send").description("queue an assignment or redirect for the active Overlord servant controller").argument("<target>", "servant slot id/name, or all").argument("[message...]", "message to deliver to the servant PTY").option("--json", "print machine-readable output").action((target, message = [], options, command) => {
10610
+ const json = wantsJson(options, command);
10611
+ try {
10612
+ const statePath = defaultOverlordStatePath(cwd());
10613
+ const registry2 = readOverlordRegistry(statePath);
10614
+ const run = findActiveRun(registry2);
10615
+ if (!run) throw new Error("no active Overlord run found");
10616
+ const normalized = normalizeServantTarget(target);
10617
+ if (!hasServantTarget(run, normalized)) throw new Error(`unknown Overlord servant target: ${target}`);
10618
+ const text = message.join(" ").trim();
10619
+ if (!text) throw new Error("message is required");
10620
+ const timestamp = isoNow(now);
10621
+ const queued = {
10622
+ id: deps.runId?.() ?? defaultMessageId(),
10623
+ target: normalized,
10624
+ text,
10625
+ createdAt: timestamp,
10626
+ queuedAt: timestamp,
10627
+ state: "queued"
10628
+ };
10629
+ if (run.engine === "opencode") {
10630
+ const dispatched = dispatchOpenCodeMessage(run, queued, runOpenCode, now);
10631
+ writeOverlordRegistry(statePath, { ...registry2, runs: { ...registry2.runs, [run.runId]: dispatched } });
10632
+ const settled = (dispatched.messages ?? []).find((m) => m.id === queued.id);
10633
+ const ok = settled?.state === "completed";
10634
+ const payload2 = { ok, runId: run.runId, target: normalized, messageId: queued.id, state: settled?.state, responseText: settled?.responseText, statePath };
10635
+ if (json) out(`${JSON.stringify(payload2, null, 2)}
10636
+ `);
10637
+ else out(`Message ${queued.id} ${ok ? "completed" : "failed"} for ${normalized}.
10638
+ State: ${statePath}
10639
+ `);
10640
+ if (!ok) process.exitCode = 1;
10641
+ return;
10642
+ }
10643
+ const next = {
10644
+ ...run,
10645
+ updatedAt: timestamp,
10646
+ messages: [...run.messages ?? [], queued]
10647
+ };
10648
+ writeOverlordRegistry(statePath, {
10649
+ ...registry2,
10650
+ runs: { ...registry2.runs, [run.runId]: next }
10651
+ });
10652
+ const payload = { queued: 1, runId: run.runId, target: normalized, messageId: queued.id, statePath };
10653
+ if (json) out(`${JSON.stringify(payload, null, 2)}
10654
+ `);
10655
+ else out(`Queued message ${queued.id} for ${normalized}.
10656
+ State: ${statePath}
10657
+ `);
10658
+ } catch (e) {
10659
+ const messageText = e instanceof Error ? e.message : String(e);
10660
+ if (json) out(`${JSON.stringify({ ok: false, error: messageText }, null, 2)}
10661
+ `);
10662
+ else err(`overlord send: ${messageText}
10663
+ `);
10664
+ process.exitCode = 1;
10665
+ }
10666
+ });
10667
+ overlord.command("status").description("show the active Overlord run and servant liveness").option("--json", "print machine-readable output").action((options, command) => {
10668
+ const json = wantsJson(options, command);
10669
+ const statePath = defaultOverlordStatePath(cwd());
10670
+ const registry2 = readOverlordRegistry(statePath);
10671
+ const run = findActiveRun(registry2);
10672
+ if (!run) {
10673
+ const payload2 = { active: false, statePath, message: "no active Overlord run found" };
10674
+ if (json) out(`${JSON.stringify(payload2, null, 2)}
10675
+ `);
10676
+ else out(`No active Overlord run found.
10677
+ State: ${payload2.statePath}
10678
+ `);
10679
+ return;
10680
+ }
10681
+ const summary = summarizeOverlordRun(run, { isPidAlive, now });
10682
+ const current = now();
10683
+ const payload = {
10684
+ ...summary,
10685
+ statePath,
10686
+ task: run.task,
10687
+ engine: run.engine,
10688
+ ledgerPath: run.ledgerPath,
10689
+ sessions: run.servants.map((servant) => ({
10690
+ name: servant.name,
10691
+ slotId: servant.slotId,
10692
+ engine: servant.engine,
10693
+ opencodeSessionId: servant.opencodeSessionId,
10694
+ eventJournalPath: servant.eventJournalPath,
10695
+ lastEventAt: servant.lastEventAt,
10696
+ lastMessageCompletedAt: servant.lastMessageCompletedAt
10697
+ })),
10698
+ messages: (run.messages ?? []).map((message) => ({
10699
+ id: message.id,
10700
+ target: message.target,
10701
+ state: messageProgress(message, current),
10702
+ queuedAt: message.queuedAt ?? message.createdAt,
10703
+ startedAt: message.startedAt ?? message.deliveredAt,
10704
+ completedAt: message.completedAt,
10705
+ failedAt: message.failedAt,
10706
+ ackText: message.ackText,
10707
+ responseText: message.responseText,
10708
+ eventJournalPath: message.eventJournalPath,
10709
+ failureReason: message.failureReason
10710
+ }))
10711
+ };
10712
+ if (json) out(`${JSON.stringify(payload, null, 2)}
10713
+ `);
10714
+ else out(`${renderOverlordStatus(summary, run)}
10715
+ State: ${statePath}
10716
+ `);
10717
+ });
10718
+ overlord.command("collect").description("summarize servant handoff/liveness evidence from the active Overlord journals").option("--json", "print machine-readable output").action((options, command) => {
10719
+ const json = wantsJson(options, command);
10720
+ const statePath = defaultOverlordStatePath(cwd());
10721
+ const registry2 = readOverlordRegistry(statePath);
10722
+ const run = findActiveRun(registry2);
10723
+ if (!run) {
10724
+ const payload2 = { active: false, statePath, message: "no active Overlord run found" };
10725
+ if (json) out(`${JSON.stringify(payload2, null, 2)}
10726
+ `);
10727
+ else out(`No active Overlord run found.
10728
+ State: ${payload2.statePath}
10729
+ `);
10730
+ return;
10731
+ }
10732
+ const current = now();
10733
+ const servants = run.servants.map((servant) => {
10734
+ const journal = servantJournalSummary(servant);
10735
+ return {
10736
+ slotId: servant.slotId,
10737
+ name: servant.name,
10738
+ state: servantProgress(run, servant, current),
10739
+ hasHandoff: journal.hasHandoff,
10740
+ journalPath: servant.journalPath,
10741
+ lines: journal.lines
10742
+ };
10743
+ });
10744
+ const payload = { active: true, runId: run.runId, statePath, servants };
10745
+ if (json) out(`${JSON.stringify(payload, null, 2)}
10746
+ `);
10747
+ else out(`${servants.map((servant) => `${servant.name}: ${servant.state}; handoff=${servant.hasHandoff ? "yes" : "no"}; ${servant.journalPath}`).join("\n")}
10748
+ State: ${statePath}
10749
+ `);
10750
+ });
10751
+ overlord.command("stop").description("stop only exact run-owned Overlord resources").option("--json", "print machine-readable output").action((options, command) => {
10752
+ const json = wantsJson(options, command);
10753
+ const statePath = defaultOverlordStatePath(cwd());
10754
+ const registry2 = readOverlordRegistry(statePath);
10755
+ const run = findActiveRun(registry2);
10756
+ if (!run) {
10757
+ const payload2 = { stopped: 0, uncertain: 0, statePath, message: "no active Overlord run found" };
10758
+ if (json) out(`${JSON.stringify(payload2, null, 2)}
10759
+ `);
10760
+ else out(`No active Overlord run found. Nothing stopped.
10761
+ State: ${payload2.statePath}
10762
+ `);
10763
+ return;
10764
+ }
10765
+ const stopPlan = planOverlordRunStop(run);
10766
+ for (const pid of stopPlan.killPids) {
10767
+ try {
10768
+ killPid(pid);
10769
+ } catch {
10770
+ }
10771
+ }
10772
+ const stoppedRun = {
10773
+ ...run,
10774
+ state: "stopped",
10775
+ updatedAt: isoNow(now)
10776
+ };
10777
+ writeOverlordRegistry(statePath, {
10778
+ runs: { ...registry2.runs, [run.runId]: stoppedRun }
10779
+ });
10780
+ const payload = {
10781
+ runId: run.runId,
10782
+ stopped: stopPlan.killPids.length,
10783
+ uncertain: stopPlan.uncertain.length,
10784
+ statePath
10785
+ };
10786
+ if (json) out(`${JSON.stringify(payload, null, 2)}
10787
+ `);
10788
+ else {
10789
+ out(`${[
10790
+ `Stopped ${payload.stopped} Overlord-owned process(es).`,
10791
+ payload.uncertain ? `Left ${payload.uncertain} uncertain resource(s) untouched.` : "No uncertain resources.",
10792
+ `State: ${statePath}`
10793
+ ].join("\n")}
10794
+ `);
10795
+ }
10796
+ });
10797
+ return overlord;
10798
+ }
10799
+
10800
+ // src/throttle-commands.ts
10801
+ var import_node_child_process9 = require("node:child_process");
10802
+ var import_node_fs16 = require("node:fs");
10803
+ var import_node_path14 = require("node:path");
10804
+ var THROTTLE_TRACE_REL = (0, import_node_path14.join)(".mmi", "throttle", "trace.jsonl");
9963
10805
  function resolveRepoGitRoot(cwd = process.cwd()) {
9964
10806
  try {
9965
- const root = (0, import_node_child_process8.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
10807
+ const root = (0, import_node_child_process9.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
9966
10808
  encoding: "utf8",
9967
10809
  timeout: 5e3
9968
10810
  }).trim();
@@ -9972,7 +10814,7 @@ function resolveRepoGitRoot(cwd = process.cwd()) {
9972
10814
  }
9973
10815
  }
9974
10816
  function resolveThrottleTracePath(cwd = process.cwd()) {
9975
- return (0, import_node_path13.join)(resolveRepoGitRoot(cwd), THROTTLE_TRACE_REL);
10817
+ return (0, import_node_path14.join)(resolveRepoGitRoot(cwd), THROTTLE_TRACE_REL);
9976
10818
  }
9977
10819
  function resolveModeFromEnv() {
9978
10820
  const v = String(process.env.MMI_THROTTLE_MODE ?? "block").trim().toLowerCase();
@@ -9992,6 +10834,12 @@ function parseTraceLines(raw) {
9992
10834
  }
9993
10835
  return out;
9994
10836
  }
10837
+ function commandSafetyClass(reasonId) {
10838
+ if (reasonId === "shell_dialect_redirect") return "dialect/redirect mismatch";
10839
+ if (reasonId === "shell_log_dump") return "unbounded log/text dump";
10840
+ if (reasonId.startsWith("shell_")) return "shell command safety";
10841
+ return void 0;
10842
+ }
9995
10843
  function summarizeTrace(entries) {
9996
10844
  let denials = 0;
9997
10845
  let readBytesWouldBlock = 0;
@@ -9999,6 +10847,7 @@ function summarizeTrace(entries) {
9999
10847
  const byReason = {};
10000
10848
  const bySurface = {};
10001
10849
  const byMode = {};
10850
+ const commandSafetyBySurface = {};
10002
10851
  for (const e of entries) {
10003
10852
  denials += 1;
10004
10853
  const tool = e.tool ?? "unknown";
@@ -10009,22 +10858,27 @@ function summarizeTrace(entries) {
10009
10858
  bySurface[surface] = (bySurface[surface] ?? 0) + 1;
10010
10859
  const mode = e.mode ?? "unknown";
10011
10860
  byMode[mode] = (byMode[mode] ?? 0) + 1;
10861
+ const safetyClass = commandSafetyClass(reason);
10862
+ if (safetyClass) {
10863
+ commandSafetyBySurface[surface] = commandSafetyBySurface[surface] ?? {};
10864
+ commandSafetyBySurface[surface][safetyClass] = (commandSafetyBySurface[surface][safetyClass] ?? 0) + 1;
10865
+ }
10012
10866
  if (reason === "read_unbounded_large") {
10013
10867
  readBytesWouldBlock += Number(e.fileBytes) || 0;
10014
10868
  }
10015
10869
  }
10016
- return { denials, readBytesWouldBlock, byTool, byReason, bySurface, byMode };
10870
+ return { denials, readBytesWouldBlock, byTool, byReason, bySurface, byMode, commandSafetyBySurface };
10017
10871
  }
10018
10872
  function runThrottleReport(io, tracePath = resolveThrottleTracePath()) {
10019
10873
  const mode = resolveModeFromEnv();
10020
- if (!(0, import_node_fs14.existsSync)(tracePath)) {
10874
+ if (!(0, import_node_fs16.existsSync)(tracePath)) {
10021
10875
  io.log(`Throttle: no trace at ${tracePath} (gates have not denied anything yet).`);
10022
10876
  io.log(`Active mode: ${mode} (MMI_THROTTLE_MODE env; default block).`);
10023
10877
  return 0;
10024
10878
  }
10025
10879
  let raw = "";
10026
10880
  try {
10027
- raw = (0, import_node_fs14.readFileSync)(tracePath, "utf8");
10881
+ raw = (0, import_node_fs16.readFileSync)(tracePath, "utf8");
10028
10882
  } catch (e) {
10029
10883
  io.err(`Throttle: could not read trace: ${e.message}`);
10030
10884
  return 1;
@@ -10059,6 +10913,14 @@ function runThrottleReport(io, tracePath = resolveThrottleTracePath()) {
10059
10913
  io.log(" by surface:");
10060
10914
  for (const [name, count] of surfaces) io.log(` ${name}: ${count}`);
10061
10915
  }
10916
+ const safetySurfaces = Object.entries(s.commandSafetyBySurface).sort((a, b) => a[0].localeCompare(b[0]));
10917
+ if (safetySurfaces.length) {
10918
+ io.log(" command safety (quoting/dialect/shell) by surface:");
10919
+ for (const [surface, classes] of safetySurfaces) {
10920
+ const detail = Object.entries(classes).sort((a, b) => b[1] - a[1]).map(([cls, count]) => `${cls}: ${count}`).join(", ");
10921
+ io.log(` ${surface}: ${detail}`);
10922
+ }
10923
+ }
10062
10924
  return 0;
10063
10925
  }
10064
10926
  function registerThrottleCommands(program3) {
@@ -10091,7 +10953,7 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
10091
10953
  }
10092
10954
 
10093
10955
  // src/board.ts
10094
- var import_node_child_process9 = require("node:child_process");
10956
+ var import_node_child_process10 = require("node:child_process");
10095
10957
  var import_node_util6 = require("node:util");
10096
10958
 
10097
10959
  // src/board-priority.ts
@@ -10199,7 +11061,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
10199
11061
  var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
10200
11062
 
10201
11063
  // src/board.ts
10202
- var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process9.execFile);
11064
+ var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process10.execFile);
10203
11065
  var BOARD_GIT_TIMEOUT_MS = 1e4;
10204
11066
  var WRITE_PROBE_CONCURRENCY = 8;
10205
11067
  var CLAIM_CONCURRENCY = 5;
@@ -11028,16 +11890,16 @@ function ghError(e) {
11028
11890
  }
11029
11891
 
11030
11892
  // src/board-slice-cache.ts
11031
- var import_node_fs15 = require("node:fs");
11032
- var import_node_path14 = require("node:path");
11033
- var BOARD_SLICE_CACHE_FILE = (0, import_node_path14.join)(".mmi", "board-slice.json");
11893
+ var import_node_fs17 = require("node:fs");
11894
+ var import_node_path15 = require("node:path");
11895
+ var BOARD_SLICE_CACHE_FILE = (0, import_node_path15.join)(".mmi", "board-slice.json");
11034
11896
  var BOARD_SLICE_CACHE_TTL_MS = 10 * 60 * 1e3;
11035
11897
  function boardSliceCachePath(cwd) {
11036
- return (0, import_node_path14.join)(cwd, BOARD_SLICE_CACHE_FILE);
11898
+ return (0, import_node_path15.join)(cwd, BOARD_SLICE_CACHE_FILE);
11037
11899
  }
11038
11900
  function readCachedBoardSlice(cwd) {
11039
11901
  try {
11040
- const parsed = JSON.parse((0, import_node_fs15.readFileSync)(boardSliceCachePath(cwd), "utf8"));
11902
+ const parsed = JSON.parse((0, import_node_fs17.readFileSync)(boardSliceCachePath(cwd), "utf8"));
11041
11903
  if (typeof parsed.ts !== "number") return null;
11042
11904
  return { block: typeof parsed.block === "string" ? parsed.block : null, ts: parsed.ts };
11043
11905
  } catch {
@@ -11048,12 +11910,12 @@ function writeCachedBoardSlice(cwd, slice) {
11048
11910
  const path2 = boardSliceCachePath(cwd);
11049
11911
  const tmp = `${path2}.${process.pid}.tmp`;
11050
11912
  try {
11051
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(path2), { recursive: true });
11052
- (0, import_node_fs15.writeFileSync)(tmp, JSON.stringify(slice));
11053
- (0, import_node_fs15.renameSync)(tmp, path2);
11913
+ (0, import_node_fs17.mkdirSync)((0, import_node_path15.dirname)(path2), { recursive: true });
11914
+ (0, import_node_fs17.writeFileSync)(tmp, JSON.stringify(slice));
11915
+ (0, import_node_fs17.renameSync)(tmp, path2);
11054
11916
  } catch {
11055
11917
  try {
11056
- (0, import_node_fs15.rmSync)(tmp, { force: true });
11918
+ (0, import_node_fs17.rmSync)(tmp, { force: true });
11057
11919
  } catch {
11058
11920
  }
11059
11921
  }
@@ -11181,8 +12043,8 @@ async function refreshBoardSliceCache(deps) {
11181
12043
  }
11182
12044
 
11183
12045
  // src/worktree.ts
11184
- var import_node_fs16 = require("node:fs");
11185
- var import_node_path15 = require("node:path");
12046
+ var import_node_fs18 = require("node:fs");
12047
+ var import_node_path16 = require("node:path");
11186
12048
  var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
11187
12049
  var PKG = "package.json";
11188
12050
  var LOCKFILE = "package-lock.json";
@@ -11190,21 +12052,21 @@ var NODE_MODULES = "node_modules";
11190
12052
  var realFsProbe = {
11191
12053
  isDir: (p) => {
11192
12054
  try {
11193
- return (0, import_node_fs16.statSync)(p).isDirectory();
12055
+ return (0, import_node_fs18.statSync)(p).isDirectory();
11194
12056
  } catch {
11195
12057
  return false;
11196
12058
  }
11197
12059
  },
11198
12060
  isFile: (p) => {
11199
12061
  try {
11200
- return (0, import_node_fs16.statSync)(p).isFile();
12062
+ return (0, import_node_fs18.statSync)(p).isFile();
11201
12063
  } catch {
11202
12064
  return false;
11203
12065
  }
11204
12066
  },
11205
12067
  listDirs: (p) => {
11206
12068
  try {
11207
- return (0, import_node_fs16.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
12069
+ return (0, import_node_fs18.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
11208
12070
  } catch {
11209
12071
  return [];
11210
12072
  }
@@ -11212,12 +12074,12 @@ var realFsProbe = {
11212
12074
  };
11213
12075
  function scanInstallDirs(root, fs2 = realFsProbe) {
11214
12076
  const factsFor = (dir) => {
11215
- const abs = dir ? (0, import_node_path15.join)(root, dir) : root;
12077
+ const abs = dir ? (0, import_node_path16.join)(root, dir) : root;
11216
12078
  return {
11217
12079
  dir,
11218
- hasPackageJson: fs2.isFile((0, import_node_path15.join)(abs, PKG)),
11219
- hasLockfile: fs2.isFile((0, import_node_path15.join)(abs, LOCKFILE)),
11220
- hasNodeModules: fs2.isDir((0, import_node_path15.join)(abs, NODE_MODULES))
12080
+ hasPackageJson: fs2.isFile((0, import_node_path16.join)(abs, PKG)),
12081
+ hasLockfile: fs2.isFile((0, import_node_path16.join)(abs, LOCKFILE)),
12082
+ hasNodeModules: fs2.isDir((0, import_node_path16.join)(abs, NODE_MODULES))
11221
12083
  };
11222
12084
  };
11223
12085
  const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
@@ -11227,7 +12089,7 @@ function npmInstallTargets(dirs) {
11227
12089
  return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
11228
12090
  }
11229
12091
  function isLinkedWorktree(root, fs2 = realFsProbe) {
11230
- return fs2.isFile((0, import_node_path15.join)(root, ".git"));
12092
+ return fs2.isFile((0, import_node_path16.join)(root, ".git"));
11231
12093
  }
11232
12094
  function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
11233
12095
  if (!isLinkedWorktree(root, fs2)) return null;
@@ -11237,8 +12099,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
11237
12099
  return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
11238
12100
  }
11239
12101
  function defaultCopyFile(from, to) {
11240
- (0, import_node_fs16.mkdirSync)((0, import_node_path15.dirname)(to), { recursive: true });
11241
- (0, import_node_fs16.copyFileSync)(from, to);
12102
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)(to), { recursive: true });
12103
+ (0, import_node_fs18.copyFileSync)(from, to);
11242
12104
  }
11243
12105
  async function provisionWorktree(worktreeRoot, deps) {
11244
12106
  const fs2 = deps.fs ?? realFsProbe;
@@ -11250,7 +12112,7 @@ async function provisionWorktree(worktreeRoot, deps) {
11250
12112
  const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
11251
12113
  const installed = [];
11252
12114
  for (const target of targets) {
11253
- const cwd = target.dir ? (0, import_node_path15.join)(worktreeRoot, target.dir) : worktreeRoot;
12115
+ const cwd = target.dir ? (0, import_node_path16.join)(worktreeRoot, target.dir) : worktreeRoot;
11254
12116
  log(`installing deps: ${target.command} in ${target.dir || "."}`);
11255
12117
  await deps.runInstall(target.command, cwd);
11256
12118
  installed.push(target);
@@ -11259,7 +12121,7 @@ async function provisionWorktree(worktreeRoot, deps) {
11259
12121
  const copySkipped = [];
11260
12122
  const primary = await deps.primaryCheckout();
11261
12123
  for (const rel of LOCAL_ONLY_FILES) {
11262
- const dest = (0, import_node_path15.join)(worktreeRoot, rel);
12124
+ const dest = (0, import_node_path16.join)(worktreeRoot, rel);
11263
12125
  if (fs2.isFile(dest)) {
11264
12126
  copySkipped.push({ file: rel, reason: "already-present" });
11265
12127
  continue;
@@ -11268,11 +12130,11 @@ async function provisionWorktree(worktreeRoot, deps) {
11268
12130
  copySkipped.push({ file: rel, reason: "no-primary" });
11269
12131
  continue;
11270
12132
  }
11271
- if (!fs2.isFile((0, import_node_path15.join)(primary, rel))) {
12133
+ if (!fs2.isFile((0, import_node_path16.join)(primary, rel))) {
11272
12134
  copySkipped.push({ file: rel, reason: "absent-in-primary" });
11273
12135
  continue;
11274
12136
  }
11275
- copyFile((0, import_node_path15.join)(primary, rel), dest);
12137
+ copyFile((0, import_node_path16.join)(primary, rel), dest);
11276
12138
  copied.push(rel);
11277
12139
  log(`copied local config: ${rel}`);
11278
12140
  }
@@ -11280,7 +12142,7 @@ async function provisionWorktree(worktreeRoot, deps) {
11280
12142
  }
11281
12143
  function defaultWorktreePath(repoRoot, branch) {
11282
12144
  const safe = branch.replace(/[/\\]+/g, "-");
11283
- return (0, import_node_path15.join)((0, import_node_path15.dirname)(repoRoot), "mmi-worktrees", safe);
12145
+ return (0, import_node_path16.join)((0, import_node_path16.dirname)(repoRoot), "mmi-worktrees", safe);
11284
12146
  }
11285
12147
  function resolveWorktreeBase(from, remote) {
11286
12148
  const remotePrefix = `${remote}/`;
@@ -11427,7 +12289,7 @@ function whoamiLine(report) {
11427
12289
  }
11428
12290
 
11429
12291
  // src/index.ts
11430
- var import_node_path25 = require("node:path");
12292
+ var import_node_path26 = require("node:path");
11431
12293
 
11432
12294
  // src/merge-ci-policy.ts
11433
12295
  function resolveMergeCiPolicy(input) {
@@ -12206,14 +13068,14 @@ async function auditRepoCi(repo, deps) {
12206
13068
  if (deployableGated) {
12207
13069
  checks.push({
12208
13070
  ok: hasGateWorkflow,
12209
- label: "gate workflow committed",
12210
- detail: hasGateWorkflow ? void 0 : `missing ${PRODUCT_GATE_PATH}`,
13071
+ label: `gate workflow committed on ${baseBranch}`,
13072
+ detail: hasGateWorkflow ? `read ${PRODUCT_GATE_PATH} at refs/heads/${baseBranch}` : `missing ${PRODUCT_GATE_PATH} at refs/heads/${baseBranch}`,
12211
13073
  remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute (seeds gate.yml)`
12212
13074
  });
12213
13075
  checks.push({
12214
13076
  ok: await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF),
12215
- label: "product ruleset reference committed",
12216
- detail: `expected ${PRODUCT_RULESET_REF}`,
13077
+ label: `product ruleset reference committed on ${baseBranch}`,
13078
+ detail: `read ${PRODUCT_RULESET_REF} at refs/heads/${baseBranch}`,
12217
13079
  remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute`
12218
13080
  });
12219
13081
  }
@@ -12609,8 +13471,8 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
12609
13471
 
12610
13472
  // src/gh-create.ts
12611
13473
  var import_promises5 = require("node:fs/promises");
12612
- var import_node_os3 = require("node:os");
12613
- var import_node_path16 = require("node:path");
13474
+ var import_node_os4 = require("node:os");
13475
+ var import_node_path17 = require("node:path");
12614
13476
  var import_node_crypto5 = require("node:crypto");
12615
13477
  var ISSUE_TYPES = ["bug", "feature", "task"];
12616
13478
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -12651,7 +13513,7 @@ async function bodyArgsViaFile(args, deps = {}) {
12651
13513
  } };
12652
13514
  const write = deps.write ?? import_promises5.writeFile;
12653
13515
  const remove = deps.remove ?? import_promises5.unlink;
12654
- const file = (0, import_node_path16.join)(deps.dir ?? (0, import_node_os3.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.md`);
13516
+ const file = (0, import_node_path17.join)(deps.dir ?? (0, import_node_os4.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.md`);
12655
13517
  await write(file, args[i + 1], "utf8");
12656
13518
  return {
12657
13519
  args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
@@ -12696,23 +13558,109 @@ function buildPrArgs({ title, body, base, head, repo }) {
12696
13558
  return args;
12697
13559
  }
12698
13560
 
12699
- // src/issue-check.ts
12700
- var CHECKLIST_RE = /^([ \t]*[-*+] \[)([ xX])(\] )(.*)$/gm;
12701
- function findChecklistItems(body) {
12702
- const items = [];
12703
- for (const m of body.matchAll(CHECKLIST_RE)) {
12704
- const prefix = m[1];
12705
- const marker = m[2];
12706
- const text = m[4].replace(/\r$/, "");
12707
- items.push({
12708
- markerIndex: (m.index ?? 0) + prefix.length,
12709
- checked: marker.toLowerCase() === "x",
12710
- text
12711
- });
12712
- }
12713
- return items;
12714
- }
12715
- function selectChecklistItem(items, query) {
13561
+ // src/sub-issue.ts
13562
+ function parseIssueRef(ref) {
13563
+ const trimmed = ref.trim();
13564
+ const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/i);
13565
+ if (url) return { repo: url[1], number: Number(url[2]) };
13566
+ const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
13567
+ if (qualified) return { repo: qualified[1], number: Number(qualified[2]) };
13568
+ const bare = trimmed.match(/^#?(\d+)$/);
13569
+ if (bare) return { number: Number(bare[1]) };
13570
+ throw new Error(`invalid issue reference "${ref}" \u2014 expected #123, 123, owner/repo#123, or an issue URL`);
13571
+ }
13572
+ function buildResolveIdArgs(ref) {
13573
+ const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
13574
+ if (ref.repo) args.push("--repo", ref.repo);
13575
+ return args;
13576
+ }
13577
+ function buildAddSubIssueArgs(parentId, subIssueId) {
13578
+ if (!parentId) throw new Error("addSubIssue: parentId is required");
13579
+ if (!subIssueId) throw new Error("addSubIssue: subIssueId is required");
13580
+ return [
13581
+ "api",
13582
+ "graphql",
13583
+ "-f",
13584
+ "query=mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number subIssues{totalCount}} subIssue{number}}}",
13585
+ "-f",
13586
+ `p=${parentId}`,
13587
+ "-f",
13588
+ `c=${subIssueId}`
13589
+ ];
13590
+ }
13591
+ function parseAddSubIssueResult(stdout) {
13592
+ try {
13593
+ const issue2 = JSON.parse(stdout)?.data?.addSubIssue;
13594
+ const parentNumber = issue2?.issue?.number;
13595
+ const subIssueNumber = issue2?.subIssue?.number;
13596
+ const totalCount = issue2?.issue?.subIssues?.totalCount;
13597
+ if (typeof parentNumber !== "number" || typeof subIssueNumber !== "number") return void 0;
13598
+ return { parentNumber, subIssueNumber, totalCount: typeof totalCount === "number" ? totalCount : 0 };
13599
+ } catch {
13600
+ return void 0;
13601
+ }
13602
+ }
13603
+ var RESOLVE_ID_TIMEOUT_MS = 1e4;
13604
+ async function resolveIssueNodeId(runGh, ref, fallbackRepo) {
13605
+ const resolved = ref.repo ? ref : { ...ref, repo: fallbackRepo };
13606
+ const id = (await runGh(buildResolveIdArgs(resolved), RESOLVE_ID_TIMEOUT_MS)).trim();
13607
+ if (!id) throw new Error(`could not resolve node id for issue #${ref.number}${resolved.repo ? ` in ${resolved.repo}` : ""}`);
13608
+ return id;
13609
+ }
13610
+ async function linkSubIssue(runGh, parentRef, childRef, defaultRepo) {
13611
+ const parent = parseIssueRef(parentRef);
13612
+ const child = parseIssueRef(childRef);
13613
+ const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
13614
+ const subIssueId = await resolveIssueNodeId(runGh, child, defaultRepo);
13615
+ const stdout = await runGh(buildAddSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
13616
+ const result = parseAddSubIssueResult(stdout);
13617
+ if (!result) throw new Error(`addSubIssue returned an unexpected response:
13618
+ ${stdout.trim() || "(empty)"}`);
13619
+ return result;
13620
+ }
13621
+ function parentLinkFields(result, error) {
13622
+ if (result) return { parent: result };
13623
+ if (error) return { parentLinkError: error };
13624
+ return {};
13625
+ }
13626
+
13627
+ // src/issue-comment.ts
13628
+ async function postIssueComment(client, input) {
13629
+ const parsed = parseIssueRef(input.ref);
13630
+ const repo = parsed.repo ?? input.defaultRepo;
13631
+ if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
13632
+ if (input.body.trim().length === 0) throw new Error("comment body is empty");
13633
+ const comment = await client.rest(
13634
+ "POST",
13635
+ `repos/${repo}/issues/${parsed.number}/comments`,
13636
+ { body: { body: input.body } }
13637
+ );
13638
+ if (!comment?.html_url) throw new Error("GitHub did not return a comment URL");
13639
+ return {
13640
+ number: parsed.number,
13641
+ repo,
13642
+ url: `https://github.com/${repo}/issues/${parsed.number}`,
13643
+ commentUrl: comment.html_url
13644
+ };
13645
+ }
13646
+
13647
+ // src/issue-check.ts
13648
+ var CHECKLIST_RE = /^([ \t]*[-*+] \[)([ xX])(\] )(.*)$/gm;
13649
+ function findChecklistItems(body) {
13650
+ const items = [];
13651
+ for (const m of body.matchAll(CHECKLIST_RE)) {
13652
+ const prefix = m[1];
13653
+ const marker = m[2];
13654
+ const text = m[4].replace(/\r$/, "");
13655
+ items.push({
13656
+ markerIndex: (m.index ?? 0) + prefix.length,
13657
+ checked: marker.toLowerCase() === "x",
13658
+ text
13659
+ });
13660
+ }
13661
+ return items;
13662
+ }
13663
+ function selectChecklistItem(items, query) {
12716
13664
  const q = query.trim();
12717
13665
  if (!q) return { ok: false, reason: "not-found" };
12718
13666
  const exact = items.filter((it) => it.text.trim() === q);
@@ -12978,72 +13926,6 @@ Related work discovered by mmi-cli:
12978
13926
  ${lines.join("\n")}`;
12979
13927
  }
12980
13928
 
12981
- // src/sub-issue.ts
12982
- function parseIssueRef(ref) {
12983
- const trimmed = ref.trim();
12984
- const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/i);
12985
- if (url) return { repo: url[1], number: Number(url[2]) };
12986
- const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
12987
- if (qualified) return { repo: qualified[1], number: Number(qualified[2]) };
12988
- const bare = trimmed.match(/^#?(\d+)$/);
12989
- if (bare) return { number: Number(bare[1]) };
12990
- throw new Error(`invalid issue reference "${ref}" \u2014 expected #123, 123, owner/repo#123, or an issue URL`);
12991
- }
12992
- function buildResolveIdArgs(ref) {
12993
- const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
12994
- if (ref.repo) args.push("--repo", ref.repo);
12995
- return args;
12996
- }
12997
- function buildAddSubIssueArgs(parentId, subIssueId) {
12998
- if (!parentId) throw new Error("addSubIssue: parentId is required");
12999
- if (!subIssueId) throw new Error("addSubIssue: subIssueId is required");
13000
- return [
13001
- "api",
13002
- "graphql",
13003
- "-f",
13004
- "query=mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number subIssues{totalCount}} subIssue{number}}}",
13005
- "-f",
13006
- `p=${parentId}`,
13007
- "-f",
13008
- `c=${subIssueId}`
13009
- ];
13010
- }
13011
- function parseAddSubIssueResult(stdout) {
13012
- try {
13013
- const issue2 = JSON.parse(stdout)?.data?.addSubIssue;
13014
- const parentNumber = issue2?.issue?.number;
13015
- const subIssueNumber = issue2?.subIssue?.number;
13016
- const totalCount = issue2?.issue?.subIssues?.totalCount;
13017
- if (typeof parentNumber !== "number" || typeof subIssueNumber !== "number") return void 0;
13018
- return { parentNumber, subIssueNumber, totalCount: typeof totalCount === "number" ? totalCount : 0 };
13019
- } catch {
13020
- return void 0;
13021
- }
13022
- }
13023
- var RESOLVE_ID_TIMEOUT_MS = 1e4;
13024
- async function resolveIssueNodeId(runGh, ref, fallbackRepo) {
13025
- const resolved = ref.repo ? ref : { ...ref, repo: fallbackRepo };
13026
- const id = (await runGh(buildResolveIdArgs(resolved), RESOLVE_ID_TIMEOUT_MS)).trim();
13027
- if (!id) throw new Error(`could not resolve node id for issue #${ref.number}${resolved.repo ? ` in ${resolved.repo}` : ""}`);
13028
- return id;
13029
- }
13030
- async function linkSubIssue(runGh, parentRef, childRef, defaultRepo) {
13031
- const parent = parseIssueRef(parentRef);
13032
- const child = parseIssueRef(childRef);
13033
- const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
13034
- const subIssueId = await resolveIssueNodeId(runGh, child, defaultRepo);
13035
- const stdout = await runGh(buildAddSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
13036
- const result = parseAddSubIssueResult(stdout);
13037
- if (!result) throw new Error(`addSubIssue returned an unexpected response:
13038
- ${stdout.trim() || "(empty)"}`);
13039
- return result;
13040
- }
13041
- function parentLinkFields(result, error) {
13042
- if (result) return { parent: result };
13043
- if (error) return { parentLinkError: error };
13044
- return {};
13045
- }
13046
-
13047
13929
  // src/report.ts
13048
13930
  var HUB_REPO3 = "mutmutco/MMI-Hub";
13049
13931
  var REPORT_LABEL = "report";
@@ -13080,7 +13962,7 @@ ${buildReportBody(body, sourceRepo)}`;
13080
13962
 
13081
13963
  // src/skill-lesson.ts
13082
13964
  var SKILL_LESSON_LABEL = "skill-lesson";
13083
- var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "coop", "grind", "handoff", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
13965
+ var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "coop", "grind", "handoff", "hotfix", "mmi", "overlord", "rcand", "release", "secrets", "stage"];
13084
13966
  function assertSkillName(name) {
13085
13967
  const match = SKILL_NAMES.find((skill) => skill === name);
13086
13968
  if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
@@ -13745,8 +14627,8 @@ async function runStageLiveDown(deps, t) {
13745
14627
  }
13746
14628
 
13747
14629
  // src/design-system.ts
13748
- var import_node_fs17 = require("node:fs");
13749
- var import_node_path17 = require("node:path");
14630
+ var import_node_fs19 = require("node:fs");
14631
+ var import_node_path18 = require("node:path");
13750
14632
  var UI_PACKAGE_CANDIDATES = ["@mutmutco/ui-dashboard", "@mutmutco/ui", "@mutmutco/theme"];
13751
14633
  var DESIGN_SYSTEM_VERSION_LABEL = "@mutmutco design-system npm package (vs @latest)";
13752
14634
  function dashboardConsumerRegistryFix(error) {
@@ -13795,17 +14677,17 @@ function buildDesignSystemVersionCheck(input) {
13795
14677
  }
13796
14678
  function readJsonFile(path2) {
13797
14679
  try {
13798
- return JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8"));
14680
+ return JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
13799
14681
  } catch {
13800
14682
  return void 0;
13801
14683
  }
13802
14684
  }
13803
14685
  function isUiFactoryCheckout(root) {
13804
- const pkg = readJsonFile((0, import_node_path17.join)(root, "package.json"));
14686
+ const pkg = readJsonFile((0, import_node_path18.join)(root, "package.json"));
13805
14687
  return pkg?.name === "mmd-ui" && pkg?.private === true;
13806
14688
  }
13807
14689
  function resolveDeclaredUiPackage(root) {
13808
- const pkg = readJsonFile((0, import_node_path17.join)(root, "package.json"));
14690
+ const pkg = readJsonFile((0, import_node_path18.join)(root, "package.json"));
13809
14691
  if (!pkg) return void 0;
13810
14692
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
13811
14693
  for (const name of UI_PACKAGE_CANDIDATES) {
@@ -13815,8 +14697,8 @@ function resolveDeclaredUiPackage(root) {
13815
14697
  return void 0;
13816
14698
  }
13817
14699
  function readLockfileInstalledVersion(root, packageName) {
13818
- const lockPath = (0, import_node_path17.join)(root, "package-lock.json");
13819
- if (!(0, import_node_fs17.existsSync)(lockPath)) return void 0;
14700
+ const lockPath = (0, import_node_path18.join)(root, "package-lock.json");
14701
+ if (!(0, import_node_fs19.existsSync)(lockPath)) return void 0;
13820
14702
  const lock = readJsonFile(lockPath);
13821
14703
  const node = lock?.packages?.[`node_modules/${packageName}`];
13822
14704
  const version = node?.version?.trim();
@@ -13845,18 +14727,8 @@ function designSystemSnapshot(root) {
13845
14727
 
13846
14728
  // src/design-system-registry.ts
13847
14729
  var import_node_crypto6 = require("node:crypto");
13848
- var import_node_fs19 = require("node:fs");
13849
- var import_node_path18 = require("node:path");
13850
-
13851
- // src/atomic-write.ts
13852
- var import_node_fs18 = require("node:fs");
13853
- function atomicWriteFileSync(path2, content) {
13854
- const tmp = `${path2}.${process.pid}.tmp`;
13855
- (0, import_node_fs18.writeFileSync)(tmp, content, "utf8");
13856
- (0, import_node_fs18.renameSync)(tmp, path2);
13857
- }
13858
-
13859
- // src/design-system-registry.ts
14730
+ var import_node_fs20 = require("node:fs");
14731
+ var import_node_path19 = require("node:path");
13860
14732
  var DESIGN_SYSTEM_CACHE_DIR = ".mmi/design-system/components";
13861
14733
  var DESIGN_SYSTEM_MANIFEST_PATH = ".mmi/design-system/manifest.json";
13862
14734
  var REGISTRY_COMPONENTS_LABEL = "@mutmutco registry components (.mmi cache vs live registry)";
@@ -13864,13 +14736,13 @@ var REGISTRY_FIX = "run `mmi-cli doctor --apply` to pull registry components int
13864
14736
  var REGISTRY_UNREACHABLE_FIX = "live @mutmutco registry unreachable \u2014 verify `components.json` `@mutmutco` registry URL and network, then retry `mmi-cli doctor`";
13865
14737
  function readJsonFile2(path2) {
13866
14738
  try {
13867
- return JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
14739
+ return JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
13868
14740
  } catch {
13869
14741
  return void 0;
13870
14742
  }
13871
14743
  }
13872
14744
  function readComponentsJson(root) {
13873
- return readJsonFile2((0, import_node_path18.join)(root, "components.json"));
14745
+ return readJsonFile2((0, import_node_path19.join)(root, "components.json"));
13874
14746
  }
13875
14747
  function hasMutmutcoRegistry(root) {
13876
14748
  const url = readComponentsJson(root)?.registries?.["@mutmutco"];
@@ -13878,7 +14750,7 @@ function hasMutmutcoRegistry(root) {
13878
14750
  }
13879
14751
  function resolveCacheDir(root) {
13880
14752
  const custom = readComponentsJson(root)?.mmi?.cacheDir;
13881
- return (0, import_node_path18.join)(root, custom ?? DESIGN_SYSTEM_CACHE_DIR);
14753
+ return (0, import_node_path19.join)(root, custom ?? DESIGN_SYSTEM_CACHE_DIR);
13882
14754
  }
13883
14755
  function resolveRegistryUrlTemplate(root) {
13884
14756
  return readComponentsJson(root)?.registries?.["@mutmutco"];
@@ -13887,7 +14759,7 @@ function registryItemUrl(template, name) {
13887
14759
  return template.replace("{name}", name);
13888
14760
  }
13889
14761
  function readDesignSystemManifest(root) {
13890
- const raw = readJsonFile2((0, import_node_path18.join)(root, DESIGN_SYSTEM_MANIFEST_PATH));
14762
+ const raw = readJsonFile2((0, import_node_path19.join)(root, DESIGN_SYSTEM_MANIFEST_PATH));
13891
14763
  if (!raw || !Array.isArray(raw.components)) return void 0;
13892
14764
  return raw;
13893
14765
  }
@@ -13899,11 +14771,11 @@ function listInstalledRegistryComponents(root) {
13899
14771
  return scanCachedComponentNames(resolveCacheDir(root));
13900
14772
  }
13901
14773
  function scanCachedComponentNames(cacheDir) {
13902
- if (!(0, import_node_fs19.existsSync)(cacheDir)) return [];
14774
+ if (!(0, import_node_fs20.existsSync)(cacheDir)) return [];
13903
14775
  const names = /* @__PURE__ */ new Set();
13904
14776
  const walk = (dir) => {
13905
- for (const ent of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
13906
- const p = (0, import_node_path18.join)(dir, ent.name);
14777
+ for (const ent of (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true })) {
14778
+ const p = (0, import_node_path19.join)(dir, ent.name);
13907
14779
  if (ent.isDirectory()) walk(p);
13908
14780
  else if (ent.isFile() && /\.(tsx?|jsx?)$/.test(ent.name)) {
13909
14781
  names.add(ent.name.replace(/\.(tsx|ts|jsx|js)$/, ""));
@@ -13992,13 +14864,13 @@ async function gatherRegistryComponentsState(root, targetVersion, deps) {
13992
14864
  let componentStale = false;
13993
14865
  for (const file of item.files) {
13994
14866
  if (!file.target || file.content == null) continue;
13995
- const cachePath = (0, import_node_path18.join)(cacheDir, cacheRelativePath(file.target));
13996
- if (!(0, import_node_fs19.existsSync)(cachePath)) {
14867
+ const cachePath = (0, import_node_path19.join)(cacheDir, cacheRelativePath(file.target));
14868
+ if (!(0, import_node_fs20.existsSync)(cachePath)) {
13997
14869
  componentStale = true;
13998
14870
  break;
13999
14871
  }
14000
14872
  try {
14001
- if (contentHash((0, import_node_fs19.readFileSync)(cachePath, "utf8")) !== contentHash(file.content)) {
14873
+ if (contentHash((0, import_node_fs20.readFileSync)(cachePath, "utf8")) !== contentHash(file.content)) {
14002
14874
  componentStale = true;
14003
14875
  break;
14004
14876
  }
@@ -14008,7 +14880,7 @@ async function gatherRegistryComponentsState(root, targetVersion, deps) {
14008
14880
  }
14009
14881
  }
14010
14882
  if (componentStale) {
14011
- if ((0, import_node_fs19.existsSync)((0, import_node_path18.join)(cacheDir, "ui", `${name}.tsx`)) || (0, import_node_fs19.existsSync)((0, import_node_path18.join)(cacheDir, `${name}.tsx`))) {
14883
+ if ((0, import_node_fs20.existsSync)((0, import_node_path19.join)(cacheDir, "ui", `${name}.tsx`)) || (0, import_node_fs20.existsSync)((0, import_node_path19.join)(cacheDir, `${name}.tsx`))) {
14012
14884
  stale.push(name);
14013
14885
  } else {
14014
14886
  missing.push(name);
@@ -14036,15 +14908,15 @@ async function applyRegistryComponentsSync(root, components, targetVersion, log,
14036
14908
  if (!item) return { ok: false };
14037
14909
  for (const file of item.files) {
14038
14910
  if (!file.target || file.content == null) continue;
14039
- const outPath = (0, import_node_path18.join)(cacheDir, cacheRelativePath(file.target));
14040
- deps.mkdir((0, import_node_path18.dirname)(outPath));
14911
+ const outPath = (0, import_node_path19.join)(cacheDir, cacheRelativePath(file.target));
14912
+ deps.mkdir((0, import_node_path19.dirname)(outPath));
14041
14913
  const body = file.content.endsWith("\n") ? file.content : `${file.content}
14042
14914
  `;
14043
14915
  deps.writeFile(outPath, body);
14044
14916
  }
14045
14917
  }
14046
- const manifestPath = (0, import_node_path18.join)(root, DESIGN_SYSTEM_MANIFEST_PATH);
14047
- deps.mkdir((0, import_node_path18.dirname)(manifestPath));
14918
+ const manifestPath = (0, import_node_path19.join)(root, DESIGN_SYSTEM_MANIFEST_PATH);
14919
+ deps.mkdir((0, import_node_path19.dirname)(manifestPath));
14048
14920
  const manifest = {
14049
14921
  version: targetVersion,
14050
14922
  syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14059,7 +14931,7 @@ function defaultRegistrySyncDeps() {
14059
14931
  return {
14060
14932
  fetch,
14061
14933
  writeFile: (path2, content) => atomicWriteFileSync(path2, content),
14062
- mkdir: (path2) => (0, import_node_fs19.mkdirSync)(path2, { recursive: true })
14934
+ mkdir: (path2) => (0, import_node_fs20.mkdirSync)(path2, { recursive: true })
14063
14935
  };
14064
14936
  }
14065
14937
 
@@ -14085,12 +14957,12 @@ function renderVerifySecrets(body) {
14085
14957
  }
14086
14958
 
14087
14959
  // src/hotfix-coverage.ts
14088
- var import_node_child_process10 = require("node:child_process");
14960
+ var import_node_child_process11 = require("node:child_process");
14089
14961
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
14090
14962
  function checkHotfixCoverage(options = {}) {
14091
14963
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
14092
14964
  const ack = (options.ack ?? []).filter(Boolean);
14093
- const git = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
14965
+ const git = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
14094
14966
  const revList = (range) => {
14095
14967
  const out = git(["rev-list", "--no-merges", range]).trim();
14096
14968
  return out ? out.split("\n") : [];
@@ -14813,8 +15685,8 @@ async function announceRelease(deps, args) {
14813
15685
  }
14814
15686
 
14815
15687
  // src/port-registry.ts
14816
- var import_node_fs20 = require("node:fs");
14817
- var import_node_path19 = require("node:path");
15688
+ var import_node_fs21 = require("node:fs");
15689
+ var import_node_path20 = require("node:path");
14818
15690
 
14819
15691
  // ../infra/port-geometry.mjs
14820
15692
  var PORT_BLOCK = 100;
@@ -14828,8 +15700,8 @@ function nextPortBlock(registry2) {
14828
15700
  return [base, base + PORT_SPAN];
14829
15701
  }
14830
15702
  function loadPortRegistry(path2) {
14831
- if (!(0, import_node_fs20.existsSync)(path2)) return {};
14832
- const raw = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
15703
+ if (!(0, import_node_fs21.existsSync)(path2)) return {};
15704
+ const raw = JSON.parse((0, import_node_fs21.readFileSync)(path2, "utf8"));
14833
15705
  const out = {};
14834
15706
  for (const [key, value] of Object.entries(raw)) {
14835
15707
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -14843,9 +15715,9 @@ function ensurePortRange(repo, path2) {
14843
15715
  const existing = registry2[repo];
14844
15716
  if (existing) return existing;
14845
15717
  const range = nextPortBlock(registry2);
14846
- const raw = (0, import_node_fs20.existsSync)(path2) ? JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8")) : {};
15718
+ const raw = (0, import_node_fs21.existsSync)(path2) ? JSON.parse((0, import_node_fs21.readFileSync)(path2, "utf8")) : {};
14847
15719
  raw[repo] = range;
14848
- (0, import_node_fs20.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
15720
+ (0, import_node_fs21.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
14849
15721
  return range;
14850
15722
  }
14851
15723
  function portCursorSeed(registry2) {
@@ -14867,18 +15739,18 @@ function existingPortRange(repo, registry2) {
14867
15739
  return registry2[repo] ?? null;
14868
15740
  }
14869
15741
  function portRangeInfraAt(root, source) {
14870
- const registryPath = (0, import_node_path19.join)(root, "infra", "port-ranges.json");
14871
- const ddbScriptPath = (0, import_node_path19.join)(root, "infra", "port-ddb.mjs");
14872
- if (!(0, import_node_fs20.existsSync)(registryPath) || !(0, import_node_fs20.existsSync)(ddbScriptPath)) return null;
15742
+ const registryPath = (0, import_node_path20.join)(root, "infra", "port-ranges.json");
15743
+ const ddbScriptPath = (0, import_node_path20.join)(root, "infra", "port-ddb.mjs");
15744
+ if (!(0, import_node_fs21.existsSync)(registryPath) || !(0, import_node_fs21.existsSync)(ddbScriptPath)) return null;
14873
15745
  return { root, source, registryPath, ddbScriptPath };
14874
15746
  }
14875
15747
  function resolvePortRangeInfra(cwd) {
14876
15748
  const direct = portRangeInfraAt(cwd, "cwd");
14877
15749
  if (direct) return direct;
14878
- for (let dir = cwd; ; dir = (0, import_node_path19.dirname)(dir)) {
14879
- const sibling = portRangeInfraAt((0, import_node_path19.join)(dir, "MMI-Hub"), "sibling-hub");
15750
+ for (let dir = cwd; ; dir = (0, import_node_path20.dirname)(dir)) {
15751
+ const sibling = portRangeInfraAt((0, import_node_path20.join)(dir, "MMI-Hub"), "sibling-hub");
14880
15752
  if (sibling) return sibling;
14881
- const parent = (0, import_node_path19.dirname)(dir);
15753
+ const parent = (0, import_node_path20.dirname)(dir);
14882
15754
  if (parent === dir) return null;
14883
15755
  }
14884
15756
  }
@@ -14903,7 +15775,8 @@ var OVERGRANT_ROLES = /* @__PURE__ */ new Set(["admin", "maintain"]);
14903
15775
  var REQUIRED_DATA_ACCESS = {
14904
15776
  "mutmutco/MM-Chat": [{ name: "kb-projection-reader", dbRole: "kb_reader", vaultParamNeedle: "KB_READ_DB_URL" }]
14905
15777
  };
14906
- function lockedBranches(repoClass) {
15778
+ function lockedBranches(repoClass, releaseTrack) {
15779
+ if (releaseTrack) return branchesForTrack(releaseTrack);
14907
15780
  return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
14908
15781
  }
14909
15782
  function safeJson(text, fallback) {
@@ -15030,14 +15903,15 @@ function auditDataAccessContracts(repo, contracts = { consumers: {} }) {
15030
15903
  }
15031
15904
  return findings;
15032
15905
  }
15033
- async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /* @__PURE__ */ new Set(), dataAccess) {
15906
+ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /* @__PURE__ */ new Set(), dataAccess, releaseTrack) {
15034
15907
  const findings = [];
15035
15908
  findings.push(...await auditRepoCollaborators(repo, owners, deps));
15036
15909
  if (dataAccess) findings.push(...auditDataAccessContracts(repo, dataAccess));
15037
- for (const branch of lockedBranches(repoClass)) {
15910
+ const track = releaseTrack ?? (repoClass === "content" ? "trunk" : void 0);
15911
+ for (const branch of lockedBranches(repoClass, track)) {
15038
15912
  findings.push(...await auditTrainBranch(repo, branch, owners, deps, projectAdmins));
15039
15913
  }
15040
- return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
15914
+ return { repo, class: repoClass, releaseTrack: track, ok: !findings.some((f) => f.severity === "high"), findings };
15041
15915
  }
15042
15916
  async function auditOrgBasePermission(deps) {
15043
15917
  const org = await restJson2(deps, `orgs/${OWNER}`, {});
@@ -15058,7 +15932,7 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
15058
15932
  const orgFindings = await auditOrgBasePermission(deps);
15059
15933
  const repos = [];
15060
15934
  for (const target of targets) {
15061
- repos.push(await auditRepoAccess(target.repo, target.class, owners, deps, new Set(entriesValueByCanonicalRepo(matrix, target.repo) ?? []), dataAccess));
15935
+ repos.push(await auditRepoAccess(target.repo, target.class, owners, deps, new Set(entriesValueByCanonicalRepo(matrix, target.repo) ?? []), dataAccess, target.releaseTrack));
15062
15936
  }
15063
15937
  const ok = orgFindings.every((f) => f.severity !== "high") && repos.every((r) => r.ok);
15064
15938
  return { ok, owners: [...owners], orgFindings, repos };
@@ -15078,7 +15952,8 @@ function loadAccessTargets(projectsJson, fanoutJson) {
15078
15952
  seen.add(repo);
15079
15953
  const repoName = repo.split("/").pop()?.toLowerCase() ?? repo.toLowerCase();
15080
15954
  const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) || legacyContentNames.has(repo.toLowerCase()) || legacyContentNames.has(repoName) ? "content" : "deployable";
15081
- targets.push({ repo, class: cls });
15955
+ const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
15956
+ targets.push({ repo, class: cls, releaseTrack });
15082
15957
  }
15083
15958
  }
15084
15959
  return targets;
@@ -15128,7 +16003,7 @@ function renderAccessReport(report) {
15128
16003
  if (finding.remediation) lines.push(` ${finding.remediation}`);
15129
16004
  }
15130
16005
  for (const repo of report.repos) {
15131
- lines.push(`${repo.ok ? "OK" : "FLAG"} ${repo.repo} (${repo.class})`);
16006
+ lines.push(`${repo.ok ? "OK" : "FLAG"} ${repo.repo} (${repo.class}${repo.releaseTrack ? `, ${repo.releaseTrack}` : ""})`);
15132
16007
  for (const finding of repo.findings) {
15133
16008
  lines.push(` [${finding.severity}] ${finding.kind}${finding.branch ? ` @${finding.branch}` : ""}: ${finding.detail}`);
15134
16009
  if (finding.remediation) lines.push(` ${finding.remediation}`);
@@ -15674,6 +16549,23 @@ async function fetchDeployStatusBySlug(slug, deps) {
15674
16549
  return null;
15675
16550
  }
15676
16551
  }
16552
+ async function fetchDeployFactsBySlug(slug, deps) {
16553
+ if (!deps.baseUrl || !slug) return null;
16554
+ const token = await deps.token();
16555
+ if (!token) return null;
16556
+ try {
16557
+ const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-facts`, {
16558
+ method: "GET",
16559
+ headers: { Authorization: `Bearer ${token}` }
16560
+ });
16561
+ if (!res.ok) return null;
16562
+ const body = await res.json();
16563
+ if (!body?.stages) return null;
16564
+ return { slug: body.slug ?? slug, stages: body.stages };
16565
+ } catch {
16566
+ return null;
16567
+ }
16568
+ }
15677
16569
  async function fetchOrgConfig(deps) {
15678
16570
  if (!deps.baseUrl) return null;
15679
16571
  const token = await deps.token();
@@ -15847,7 +16739,7 @@ function attestedLine(att) {
15847
16739
  return `App-owned readiness attested by @${att.by} on ${att.at.slice(0, 10)} \u2014 the static checklist is cleared (the doctor reads no product repo files); re-run \`mmi-cli project attest\` after app-owned structural changes.`;
15848
16740
  }
15849
16741
  var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
15850
- function appGapsFor(meta, model, slug, projectType) {
16742
+ function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
15851
16743
  const attested = appAttestationOf(meta);
15852
16744
  const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
15853
16745
  const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
@@ -15899,8 +16791,12 @@ function appGapsFor(meta, model, slug, projectType) {
15899
16791
  if (slug === "mmi-katip") {
15900
16792
  gaps.push("Katip-specific app plan: declare Google Workspace service-account requirements, use the service account numeric OAuth2 client ID for DWD, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
15901
16793
  }
15902
- if ((model === "solo-container" || model === "tenant-container") && typeof meta?.portRange?.start === "number") {
15903
- gaps.push(`Seed DEPLOY#main healthUrl to http://127.0.0.1:${meta.portRange.start}/health during bootstrap (tenant reconcile/control health gate \u2014 #1202).`);
16794
+ if (model === "solo-container" || model === "tenant-container") {
16795
+ if (typeof mainDeployFact?.port === "number") {
16796
+ gaps.push(`Seed DEPLOY#main healthUrl to http://127.0.0.1:${mainDeployFact.port}/health during bootstrap (tenant reconcile/control health gate \u2014 #1202; port read from DEPLOY#main).`);
16797
+ } else if (!appAttestationOf(meta)) {
16798
+ gaps.push("Seed DEPLOY#main healthUrl from the DEPLOY#main edgeVhost.port during bootstrap (tenant reconcile/control health gate \u2014 #1202); doctor has no committed DEPLOY port to cite yet.");
16799
+ }
15904
16800
  }
15905
16801
  if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
15906
16802
  return gaps;
@@ -15945,7 +16841,7 @@ function sameNames(a, b) {
15945
16841
  function sameStageContract(a, b) {
15946
16842
  return STAGES.every((stage2) => sameNames(a[stage2], b[stage2]));
15947
16843
  }
15948
- function buildV2HealPatch(repoOrSlug, meta) {
16844
+ function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
15949
16845
  const slug = slugOfRepo(repoOrSlug);
15950
16846
  const repo = repoFrom(repoOrSlug, slug);
15951
16847
  const sub = defaultSubdomain(slug);
@@ -15983,7 +16879,7 @@ function buildV2HealPatch(repoOrSlug, meta) {
15983
16879
  patch.requiredRuntimeSecrets = next;
15984
16880
  }
15985
16881
  }
15986
- const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
16882
+ const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
15987
16883
  if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
15988
16884
  return { slug, patch, appOwnedGaps };
15989
16885
  }
@@ -16028,7 +16924,8 @@ async function buildV2Doctor(repoOrSlug, deps) {
16028
16924
  const meta = read.project;
16029
16925
  const projectType = resolveProjectType(meta, repo);
16030
16926
  const model = resolveDeployModel(meta, repo);
16031
- const autoHeal = buildV2HealPatch(repo, meta);
16927
+ const mainDeployFact = meta && deps.getDeployFact ? await deps.getDeployFact(slug, "main") : null;
16928
+ const autoHeal = buildV2HealPatch(repo, meta, mainDeployFact);
16032
16929
  if (!meta) {
16033
16930
  const emptySecrets = {
16034
16931
  dev: { required: [], present: [], missing: [] },
@@ -16142,6 +17039,152 @@ function renderReadinessIssueBody(existingBody, report, opts = {}) {
16142
17039
  ${section}`.trim();
16143
17040
  }
16144
17041
 
17042
+ // src/readiness-audit.ts
17043
+ function publicUrlFromDeployFact(fact) {
17044
+ return fact?.domain ? `https://${fact.domain}` : void 0;
17045
+ }
17046
+ function tenantRuntimeHints(stage2, fact, probe) {
17047
+ const hints = [];
17048
+ if (!fact) {
17049
+ hints.push(`DEPLOY#${stage2} row missing or state-only; seed deploy coords before runtime audit can prove the endpoint.`);
17050
+ return hints;
17051
+ }
17052
+ if (!fact.sshHostPresent && fact.substrate === "hetzner-ssh") hints.push(`DEPLOY#${stage2} has hetzner-ssh substrate but no sshHost presence; tenant-deploy cannot reach the box.`);
17053
+ if (!fact.domain) hints.push(`DEPLOY#${stage2} has no edgeVhost.domain; Cloudflare/Caddy public URL cannot be derived.`);
17054
+ if (typeof fact.port !== "number") hints.push(`DEPLOY#${stage2} has no edgeVhost.port; Caddy upstream/healthUrl hints cannot be checked.`);
17055
+ if (probe?.ok === false || probe?.status != null && probe.status >= 500) hints.push("Public URL probe failed; for Cloudflare 525 check Caddy TLS/origin certificate and Cloudflare SSL mode before changing app code.");
17056
+ if (stage2 === "rc") hints.push("rc runtime is expected to be ephemeral: present between /rcand and /release, then retired after release.");
17057
+ return hints;
17058
+ }
17059
+ function buildTenantRuntimeStatus(input) {
17060
+ const publicUrl = publicUrlFromDeployFact(input.deploy);
17061
+ return {
17062
+ repo: input.repo,
17063
+ slug: input.slug,
17064
+ stage: input.stage,
17065
+ deployRowPresent: Boolean(input.deploy?.present),
17066
+ deploy: input.deploy ?? null,
17067
+ publicUrl,
17068
+ publicProbe: input.publicProbe,
17069
+ lastTenantDeployRun: input.lastTenantDeployRun,
17070
+ expectedEphemeralRc: input.stage === "rc",
17071
+ hints: tenantRuntimeHints(input.stage, input.deploy, input.publicProbe)
17072
+ };
17073
+ }
17074
+ function buildFullTrackReadinessReport(input) {
17075
+ const releaseTrack = resolveReleaseTrack(input.meta, void 0, input.repo);
17076
+ const branches = branchesForTrack(releaseTrack);
17077
+ const reasons = [];
17078
+ if (releaseTrack !== "full") reasons.push(`releaseTrack resolves ${releaseTrack}; set full for development -> rc -> main`);
17079
+ if (!input.trainAuthority?.train) reasons.push("caller does not have train authority");
17080
+ if (!input.deployFacts?.stages.rc?.present) reasons.push("DEPLOY#rc coords missing");
17081
+ const rcRuntime = input.runtime.rc;
17082
+ if (rcRuntime.publicProbe?.ok === false) reasons.push("rc public endpoint probe failed");
17083
+ return {
17084
+ repo: input.repo,
17085
+ slug: input.slug,
17086
+ releaseTrack,
17087
+ branches,
17088
+ trainAuthority: input.trainAuthority,
17089
+ deployFacts: input.deployFacts,
17090
+ rcand: { canApply: reasons.length === 0, reasons },
17091
+ runtime: input.runtime
17092
+ };
17093
+ }
17094
+
17095
+ // src/oauth.ts
17096
+ var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
17097
+ var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
17098
+ var ENV_PREFIXES = ["", "dev", "rc"];
17099
+ var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
17100
+ var SSM_ENVS = ["dev", "rc", "main"];
17101
+ var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
17102
+ var uniq = (xs) => [...new Set(xs)];
17103
+ function defaultSubdomain2(slug) {
17104
+ const i = slug.indexOf("-");
17105
+ return i === -1 ? slug : slug.slice(i + 1);
17106
+ }
17107
+ function expectedHosts(cfg) {
17108
+ const out = [];
17109
+ for (const sub of cfg.subdomains) {
17110
+ for (const domain of cfg.domains) {
17111
+ const base = sub ? `${sub}.${domain}` : domain;
17112
+ for (const env of ENV_PREFIXES) out.push(env ? `${env}.${base}` : base);
17113
+ }
17114
+ }
17115
+ if (cfg.fofuSubdomain !== void 0) {
17116
+ out.push(cfg.fofuSubdomain ? `${cfg.fofuSubdomain}.fofu.ai` : "fofu.ai");
17117
+ }
17118
+ return uniq(out);
17119
+ }
17120
+ function expectedJsOrigins(cfg) {
17121
+ return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
17122
+ }
17123
+ function expectedRedirectUris(cfg) {
17124
+ const { callbackPath } = cfg;
17125
+ return uniq([
17126
+ ...expectedHosts(cfg).map((h) => `https://${h}${callbackPath}`),
17127
+ ...LOOPBACK.map((l) => `${l}${callbackPath}`)
17128
+ ]);
17129
+ }
17130
+ function oauthSsmKeys() {
17131
+ return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
17132
+ }
17133
+ function parseOauthClientJson(input) {
17134
+ let parsed;
17135
+ try {
17136
+ parsed = JSON.parse(input);
17137
+ } catch {
17138
+ throw new Error('not valid JSON \u2014 pipe the Google client JSON (the Console "Download JSON" file)');
17139
+ }
17140
+ const root = parsed ?? {};
17141
+ const obj = root.web ?? root.installed ?? parsed;
17142
+ const clientId = typeof obj?.client_id === "string" ? obj.client_id.trim() : "";
17143
+ const clientSecret = typeof obj?.client_secret === "string" ? obj.client_secret.trim() : "";
17144
+ if (!clientId || !clientSecret) {
17145
+ throw new Error("missing client_id or client_secret in the JSON");
17146
+ }
17147
+ return { clientId, clientSecret };
17148
+ }
17149
+ function parseOauthConfig(mmiConfig, slug) {
17150
+ const rawUnknown = mmiConfig?.oauth;
17151
+ if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
17152
+ if (!rawUnknown || typeof rawUnknown !== "object" || Array.isArray(rawUnknown)) {
17153
+ throw new Error("oauth must be an object when configured");
17154
+ }
17155
+ const raw = rawUnknown;
17156
+ const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
17157
+ const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
17158
+ const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
17159
+ if (!callbackPath.startsWith("/")) {
17160
+ throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
17161
+ }
17162
+ if (callbackPath !== DEFAULT_CALLBACK_PATH) {
17163
+ throw new Error(`oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
17164
+ }
17165
+ const meta = mmiConfig ?? {};
17166
+ const rawFofuSub = raw.fofuSubdomain;
17167
+ const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
17168
+ return { subdomains, domains, callbackPath, fofuSubdomain };
17169
+ }
17170
+ function probeRedirectUri(callbackPath, port = 9123) {
17171
+ return `http://localhost:${port}${callbackPath}`;
17172
+ }
17173
+ function buildAuthorizeProbeUrl(clientId, redirectUri) {
17174
+ const qs = new URLSearchParams({
17175
+ client_id: clientId,
17176
+ redirect_uri: redirectUri,
17177
+ response_type: "code",
17178
+ scope: "openid email",
17179
+ access_type: "offline",
17180
+ prompt: "consent"
17181
+ });
17182
+ return `https://accounts.google.com/o/oauth2/v2/auth?${qs.toString()}`;
17183
+ }
17184
+ function authorizeBodyHasMismatch(body) {
17185
+ return /redirect_uri_mismatch/i.test(body);
17186
+ }
17187
+
16145
17188
  // src/project-set.ts
16146
17189
  var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "dashboard", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
16147
17190
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
@@ -16263,7 +17306,7 @@ function parseOauthVar(raw) {
16263
17306
  try {
16264
17307
  parsed = JSON.parse(raw);
16265
17308
  } catch {
16266
- throw new Error('project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"/auth/callback"}');
17309
+ throw new Error(`project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}`);
16267
17310
  }
16268
17311
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
16269
17312
  throw new Error("project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
@@ -16278,7 +17321,11 @@ function parseOauthVar(raw) {
16278
17321
  out[key] = value.map((v) => v.trim());
16279
17322
  } else if (key === "callbackPath") {
16280
17323
  if (typeof value !== "string" || !value.trim()) throw new Error("project set: oauth.callbackPath must be a non-empty string");
16281
- out.callbackPath = value.trim();
17324
+ const callbackPath = value.trim();
17325
+ if (callbackPath !== DEFAULT_CALLBACK_PATH) {
17326
+ throw new Error(`project set: oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
17327
+ }
17328
+ out.callbackPath = callbackPath;
16282
17329
  } else if (key === "fofuSubdomain") {
16283
17330
  if (typeof value !== "string") throw new Error('project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
16284
17331
  out.fofuSubdomain = value.trim();
@@ -16592,8 +17639,12 @@ function resolveKbSource(rawBase) {
16592
17639
  if (!m) return DEFAULT_KB;
16593
17640
  return { owner: m[1], repo: m[2], ref: m[3] };
16594
17641
  }
16595
- function buildKbGetArgs(src, path2) {
17642
+ function normalizeKbPath(path2) {
16596
17643
  const clean4 = path2.replace(/^\/+/, "");
17644
+ return clean4 === "kb" || clean4.startsWith("kb/") ? clean4 : `kb/${clean4}`;
17645
+ }
17646
+ function buildKbGetArgs(src, path2) {
17647
+ const clean4 = normalizeKbPath(path2);
16597
17648
  return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
16598
17649
  }
16599
17650
  function buildKbTreeArgs(src) {
@@ -16606,20 +17657,20 @@ function parseKbTree(stdout, prefix) {
16606
17657
  } catch {
16607
17658
  return [];
16608
17659
  }
16609
- const pre = prefix ? prefix.replace(/^\/+/, "") : void 0;
16610
- return tree.filter((t) => t.type === "blob" && typeof t.path === "string" && t.path.startsWith("kb/")).map((t) => t.path).filter((p) => pre ? p.startsWith(pre) : true).sort();
17660
+ const pre = prefix ? normalizeKbPath(prefix) : void 0;
17661
+ return tree.filter((t) => t.type === "blob" && typeof t.path === "string" && t.path.startsWith("kb/") && !t.path.split("/").some((s) => s.startsWith("."))).map((t) => t.path).filter((p) => pre ? p.startsWith(pre) : true).sort();
16611
17662
  }
16612
17663
 
16613
17664
  // src/northstar-commands.ts
16614
- var import_node_fs21 = require("node:fs");
16615
- var import_node_child_process11 = require("node:child_process");
17665
+ var import_node_fs22 = require("node:fs");
17666
+ var import_node_child_process12 = require("node:child_process");
16616
17667
  var import_promises6 = require("node:fs/promises");
16617
17668
  var planSyncDetached = false;
16618
17669
  function detachPlanSync() {
16619
17670
  if (planSyncDetached) return;
16620
17671
  planSyncDetached = true;
16621
17672
  try {
16622
- (0, import_node_child_process11.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
17673
+ (0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
16623
17674
  detached: true,
16624
17675
  stdio: "ignore",
16625
17676
  windowsHide: true,
@@ -16629,7 +17680,7 @@ function detachPlanSync() {
16629
17680
  }
16630
17681
  }
16631
17682
  function makePlanDeps(cfg, io = consoleIo) {
16632
- const ensureDir = () => (0, import_node_fs21.mkdirSync)(PLANS_DIR, { recursive: true });
17683
+ const ensureDir = () => (0, import_node_fs22.mkdirSync)(PLANS_DIR, { recursive: true });
16633
17684
  return {
16634
17685
  apiUrl: cfg.sagaApiUrl,
16635
17686
  fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
@@ -16637,31 +17688,31 @@ function makePlanDeps(cfg, io = consoleIo) {
16637
17688
  project: async () => (await sagaKey(cfg)).project,
16638
17689
  readLocal: (slug) => {
16639
17690
  try {
16640
- return (0, import_node_fs21.readFileSync)(planPath(slug), "utf8");
17691
+ return (0, import_node_fs22.readFileSync)(planPath(slug), "utf8");
16641
17692
  } catch {
16642
17693
  return null;
16643
17694
  }
16644
17695
  },
16645
17696
  listLocalSlugs: () => {
16646
17697
  try {
16647
- return (0, import_node_fs21.readdirSync)(PLANS_DIR, { withFileTypes: true }).filter((entry) => entry.isFile() && /^[A-Za-z0-9][A-Za-z0-9_-]*\.md$/.test(entry.name)).map((entry) => entry.name.replace(/\.md$/, ""));
17698
+ return (0, import_node_fs22.readdirSync)(PLANS_DIR, { withFileTypes: true }).filter((entry) => entry.isFile() && /^[A-Za-z0-9][A-Za-z0-9_-]*\.md$/.test(entry.name)).map((entry) => entry.name.replace(/\.md$/, ""));
16648
17699
  } catch {
16649
17700
  return [];
16650
17701
  }
16651
17702
  },
16652
17703
  writeLocal: (slug, content) => {
16653
17704
  ensureDir();
16654
- (0, import_node_fs21.writeFileSync)(planPath(slug), content, "utf8");
17705
+ (0, import_node_fs22.writeFileSync)(planPath(slug), content, "utf8");
16655
17706
  },
16656
17707
  removeLocal: (slug) => {
16657
17708
  try {
16658
- (0, import_node_fs21.rmSync)(planPath(slug));
17709
+ (0, import_node_fs22.rmSync)(planPath(slug));
16659
17710
  } catch {
16660
17711
  }
16661
17712
  },
16662
17713
  readMetaRaw: () => {
16663
17714
  try {
16664
- return (0, import_node_fs21.readFileSync)(META_FILE, "utf8");
17715
+ return (0, import_node_fs22.readFileSync)(META_FILE, "utf8");
16665
17716
  } catch {
16666
17717
  return null;
16667
17718
  }
@@ -16672,7 +17723,7 @@ function makePlanDeps(cfg, io = consoleIo) {
16672
17723
  },
16673
17724
  readIndexRaw: () => {
16674
17725
  try {
16675
- return (0, import_node_fs21.readFileSync)(INDEX_FILE, "utf8");
17726
+ return (0, import_node_fs22.readFileSync)(INDEX_FILE, "utf8");
16676
17727
  } catch {
16677
17728
  return null;
16678
17729
  }
@@ -16683,7 +17734,7 @@ function makePlanDeps(cfg, io = consoleIo) {
16683
17734
  },
16684
17735
  readQueueRaw: () => {
16685
17736
  try {
16686
- return (0, import_node_fs21.readFileSync)(QUEUE_FILE, "utf8");
17737
+ return (0, import_node_fs22.readFileSync)(QUEUE_FILE, "utf8");
16687
17738
  } catch {
16688
17739
  return null;
16689
17740
  }
@@ -16705,7 +17756,7 @@ function openInEditor(path2) {
16705
17756
  return;
16706
17757
  }
16707
17758
  try {
16708
- (0, import_node_child_process11.spawn)(editor, [path2], { stdio: "inherit" });
17759
+ (0, import_node_child_process12.spawn)(editor, [path2], { stdio: "inherit" });
16709
17760
  } catch {
16710
17761
  console.log(`open ${path2} manually`);
16711
17762
  }
@@ -16743,7 +17794,7 @@ function repoInfoFromRemote(remote) {
16743
17794
  }
16744
17795
  function readStageUrl() {
16745
17796
  try {
16746
- const state = JSON.parse((0, import_node_fs21.readFileSync)("tmp/stage/state.json", "utf8"));
17797
+ const state = JSON.parse((0, import_node_fs22.readFileSync)("tmp/stage/state.json", "utf8"));
16747
17798
  if (typeof state.url === "string" && state.url.trim()) return state.url.trim();
16748
17799
  if (typeof state.port === "number" && Number.isFinite(state.port)) return `http://127.0.0.1:${state.port}/`;
16749
17800
  if (typeof state.healthUrl === "string" && state.healthUrl.trim()) {
@@ -17056,6 +18107,11 @@ async function secretsList(deps, opts) {
17056
18107
  const { secrets } = await res.json();
17057
18108
  deps.log(formatSecretList(secrets ?? []));
17058
18109
  }
18110
+ var CAPABILITIES_TIMEOUT_MS = 2e4;
18111
+ function isTimeoutError(e) {
18112
+ const name = e?.name;
18113
+ return name === "TimeoutError" || name === "AbortError";
18114
+ }
17059
18115
  function formatCapabilities(r) {
17060
18116
  const head = `@${r.login} on ${r.repo} \u2014 ${r.role}`;
17061
18117
  const items = [...r.capabilities ?? []].sort((a, b) => a.scope.localeCompare(b.scope));
@@ -17086,10 +18142,15 @@ async function secretsCapabilities(deps, opts) {
17086
18142
  res = await deps.fetch(`${deps.apiUrl}/secrets/capabilities?${qs}`, {
17087
18143
  method: "GET",
17088
18144
  headers: await deps.headers(),
17089
- signal: AbortSignal.timeout(TIMEOUT_MS2)
18145
+ signal: AbortSignal.timeout(CAPABILITIES_TIMEOUT_MS)
17090
18146
  });
17091
18147
  } catch (e) {
17092
- deps.err(`access capabilities: ${e.message}`);
18148
+ const message = e.message;
18149
+ if (isTimeoutError(e)) {
18150
+ deps.err(`access capabilities: timed out after ${CAPABILITIES_TIMEOUT_MS}ms while aggregating vault scopes for ${repo}. No access conclusion was made; retry with a warm Hub or run scoped reads such as \`mmi-cli secrets list --repo ${repo}\` while investigating the slow source.`);
18151
+ return;
18152
+ }
18153
+ deps.err(`access capabilities: ${message}`);
17093
18154
  return;
17094
18155
  }
17095
18156
  if (!res.ok) {
@@ -17483,8 +18544,8 @@ async function secretsUse(deps, key, opts) {
17483
18544
  }
17484
18545
 
17485
18546
  // src/secrets-commands.ts
17486
- var import_node_fs22 = require("node:fs");
17487
- var import_node_path20 = require("node:path");
18547
+ var import_node_fs23 = require("node:fs");
18548
+ var import_node_path21 = require("node:path");
17488
18549
  var RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS = 3e4;
17489
18550
  var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
17490
18551
  var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
@@ -17492,18 +18553,18 @@ function collectMap(value, previous = []) {
17492
18553
  return [...previous, value];
17493
18554
  }
17494
18555
  async function decryptRailsCredentials(input) {
17495
- const appDir = (0, import_node_path20.resolve)(input.appDir ?? process.cwd());
18556
+ const appDir = (0, import_node_path21.resolve)(input.appDir ?? process.cwd());
17496
18557
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
17497
18558
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
17498
- const credentialsPath = (0, import_node_path20.resolve)(appDir, credentialsFile);
17499
- const masterKeyPath = (0, import_node_path20.resolve)(appDir, masterKeyFile);
18559
+ const credentialsPath = (0, import_node_path21.resolve)(appDir, credentialsFile);
18560
+ const masterKeyPath = (0, import_node_path21.resolve)(appDir, masterKeyFile);
17500
18561
  const env = {
17501
18562
  ...process.env,
17502
18563
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
17503
18564
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
17504
18565
  };
17505
- if ((0, import_node_fs22.existsSync)(masterKeyPath)) {
17506
- env.RAILS_MASTER_KEY = (0, import_node_fs22.readFileSync)(masterKeyPath, "utf8").trim();
18566
+ if ((0, import_node_fs23.existsSync)(masterKeyPath)) {
18567
+ env.RAILS_MASTER_KEY = (0, import_node_fs23.readFileSync)(masterKeyPath, "utf8").trim();
17507
18568
  }
17508
18569
  const script = [
17509
18570
  'require "json"',
@@ -17628,7 +18689,7 @@ function registerSecretsCommands(program3) {
17628
18689
  {
17629
18690
  ...d,
17630
18691
  decryptRailsCredentials,
17631
- removeFile: (path2) => (0, import_node_fs22.unlinkSync)((0, import_node_path20.resolve)(o.appDir ?? process.cwd(), path2))
18692
+ removeFile: (path2) => (0, import_node_fs23.unlinkSync)((0, import_node_path21.resolve)(o.appDir ?? process.cwd(), path2))
17632
18693
  },
17633
18694
  {
17634
18695
  repo: o.repo,
@@ -17713,101 +18774,12 @@ function registerEdgeCommands(program3) {
17713
18774
  });
17714
18775
  }
17715
18776
 
17716
- // src/oauth.ts
17717
- var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
17718
- var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
17719
- var ENV_PREFIXES = ["", "dev", "rc"];
17720
- var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
17721
- var SSM_ENVS = ["dev", "rc", "main"];
17722
- var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
17723
- var uniq = (xs) => [...new Set(xs)];
17724
- function defaultSubdomain2(slug) {
17725
- const i = slug.indexOf("-");
17726
- return i === -1 ? slug : slug.slice(i + 1);
17727
- }
17728
- function expectedHosts(cfg) {
17729
- const out = [];
17730
- for (const sub of cfg.subdomains) {
17731
- for (const domain of cfg.domains) {
17732
- const base = sub ? `${sub}.${domain}` : domain;
17733
- for (const env of ENV_PREFIXES) out.push(env ? `${env}.${base}` : base);
17734
- }
17735
- }
17736
- if (cfg.fofuSubdomain !== void 0) {
17737
- out.push(cfg.fofuSubdomain ? `${cfg.fofuSubdomain}.fofu.ai` : "fofu.ai");
17738
- }
17739
- return uniq(out);
17740
- }
17741
- function expectedJsOrigins(cfg) {
17742
- return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
17743
- }
17744
- function expectedRedirectUris(cfg) {
17745
- const { callbackPath } = cfg;
17746
- return uniq([
17747
- ...expectedHosts(cfg).map((h) => `https://${h}${callbackPath}`),
17748
- ...LOOPBACK.map((l) => `${l}${callbackPath}`)
17749
- ]);
17750
- }
17751
- function oauthSsmKeys() {
17752
- return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
17753
- }
17754
- function parseOauthClientJson(input) {
17755
- let parsed;
17756
- try {
17757
- parsed = JSON.parse(input);
17758
- } catch {
17759
- throw new Error('not valid JSON \u2014 pipe the Google client JSON (the Console "Download JSON" file)');
17760
- }
17761
- const root = parsed ?? {};
17762
- const obj = root.web ?? root.installed ?? parsed;
17763
- const clientId = typeof obj?.client_id === "string" ? obj.client_id.trim() : "";
17764
- const clientSecret = typeof obj?.client_secret === "string" ? obj.client_secret.trim() : "";
17765
- if (!clientId || !clientSecret) {
17766
- throw new Error("missing client_id or client_secret in the JSON");
17767
- }
17768
- return { clientId, clientSecret };
17769
- }
17770
- function parseOauthConfig(mmiConfig, slug) {
17771
- const rawUnknown = mmiConfig?.oauth;
17772
- if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
17773
- if (!rawUnknown || typeof rawUnknown !== "object" || Array.isArray(rawUnknown)) {
17774
- throw new Error("oauth must be an object when configured");
17775
- }
17776
- const raw = rawUnknown;
17777
- const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
17778
- const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
17779
- const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
17780
- if (!callbackPath.startsWith("/")) {
17781
- throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
17782
- }
17783
- const meta = mmiConfig ?? {};
17784
- const rawFofuSub = raw.fofuSubdomain;
17785
- const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
17786
- return { subdomains, domains, callbackPath, fofuSubdomain };
17787
- }
17788
- function probeRedirectUri(callbackPath, port = 9123) {
17789
- return `http://localhost:${port}${callbackPath}`;
17790
- }
17791
- function buildAuthorizeProbeUrl(clientId, redirectUri) {
17792
- const qs = new URLSearchParams({
17793
- client_id: clientId,
17794
- redirect_uri: redirectUri,
17795
- response_type: "code",
17796
- scope: "openid email",
17797
- access_type: "offline",
17798
- prompt: "consent"
17799
- });
17800
- return `https://accounts.google.com/o/oauth2/v2/auth?${qs.toString()}`;
17801
- }
17802
- function authorizeBodyHasMismatch(body) {
17803
- return /redirect_uri_mismatch/i.test(body);
17804
- }
17805
-
17806
18777
  // src/doctor-run.ts
17807
- var import_node_fs27 = require("node:fs");
18778
+ var import_node_fs28 = require("node:fs");
18779
+ var import_node_child_process14 = require("node:child_process");
17808
18780
  var import_promises7 = require("node:fs/promises");
17809
- var import_node_path24 = require("node:path");
17810
- var import_node_os5 = require("node:os");
18781
+ var import_node_path25 = require("node:path");
18782
+ var import_node_os6 = require("node:os");
17811
18783
 
17812
18784
  // src/plugin-guard.ts
17813
18785
  function buildPluginGuardDecision(i) {
@@ -17827,10 +18799,10 @@ function buildGuardSessionStartLine(state, opts = {}) {
17827
18799
  }
17828
18800
 
17829
18801
  // src/cursor-plugin-seed.ts
17830
- var import_node_child_process12 = require("node:child_process");
17831
- var import_node_fs23 = require("node:fs");
17832
- var import_node_os4 = require("node:os");
17833
- var import_node_path21 = require("node:path");
18802
+ var import_node_child_process13 = require("node:child_process");
18803
+ var import_node_fs24 = require("node:fs");
18804
+ var import_node_os5 = require("node:os");
18805
+ var import_node_path22 = require("node:path");
17834
18806
  var import_node_util7 = require("node:util");
17835
18807
  function isSemverVersion(v) {
17836
18808
  return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
@@ -17838,7 +18810,7 @@ function isSemverVersion(v) {
17838
18810
  var MMI_HUB_REPO = "mutmutco/MMI-Hub";
17839
18811
  var CURSOR_THIRD_PARTY_STATE_KEY = "cursor/thirdPartyExtensibilityEnabled";
17840
18812
  var PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
17841
- var execFileBuffer = (0, import_node_util7.promisify)(import_node_child_process12.execFile);
18813
+ var execFileBuffer = (0, import_node_util7.promisify)(import_node_child_process13.execFile);
17842
18814
  function gitFetchReleaseTagArgs(hubCheckout, tag) {
17843
18815
  return ["-C", hubCheckout, "fetch", "origin", "tag", tag, "--quiet"];
17844
18816
  }
@@ -17847,17 +18819,17 @@ function ghReleaseTarballApiArgs(tag) {
17847
18819
  }
17848
18820
  function cursorUserGlobalStatePath() {
17849
18821
  if (process.platform === "win32") {
17850
- const base = process.env.APPDATA || (0, import_node_path21.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
17851
- return (0, import_node_path21.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
18822
+ const base = process.env.APPDATA || (0, import_node_path22.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
18823
+ return (0, import_node_path22.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
17852
18824
  }
17853
18825
  if (process.platform === "darwin") {
17854
- return (0, import_node_path21.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
18826
+ return (0, import_node_path22.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
17855
18827
  }
17856
- return (0, import_node_path21.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
18828
+ return (0, import_node_path22.join)((0, import_node_os5.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
17857
18829
  }
17858
18830
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
17859
18831
  const dbPath = cursorUserGlobalStatePath();
17860
- if (!(0, import_node_fs23.existsSync)(dbPath)) return void 0;
18832
+ if (!(0, import_node_fs24.existsSync)(dbPath)) return void 0;
17861
18833
  try {
17862
18834
  const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
17863
18835
  timeout: 5e3
@@ -17871,57 +18843,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
17871
18843
  }
17872
18844
  }
17873
18845
  function syncDirContents(src, dest) {
17874
- (0, import_node_fs23.mkdirSync)(dest, { recursive: true });
17875
- for (const name of (0, import_node_fs23.readdirSync)(dest)) {
17876
- (0, import_node_fs23.rmSync)((0, import_node_path21.join)(dest, name), { recursive: true, force: true });
18846
+ (0, import_node_fs24.mkdirSync)(dest, { recursive: true });
18847
+ for (const name of (0, import_node_fs24.readdirSync)(dest)) {
18848
+ (0, import_node_fs24.rmSync)((0, import_node_path22.join)(dest, name), { recursive: true, force: true });
17877
18849
  }
17878
- (0, import_node_fs23.cpSync)(src, dest, { recursive: true });
18850
+ (0, import_node_fs24.cpSync)(src, dest, { recursive: true });
17879
18851
  }
17880
18852
  function releaseTag(releasedVersion) {
17881
18853
  return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
17882
18854
  }
17883
18855
  async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
17884
- const tarFile = (0, import_node_path21.join)(tmpRoot, "archive.tar");
18856
+ const tarFile = (0, import_node_path22.join)(tmpRoot, "archive.tar");
17885
18857
  try {
17886
18858
  await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
17887
18859
  await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
17888
18860
  timeout: 6e4
17889
18861
  });
17890
18862
  await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
17891
- const pluginMmi = (0, import_node_path21.join)(tmpRoot, "plugins", "mmi");
17892
- return (0, import_node_fs23.existsSync)((0, import_node_path21.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
18863
+ const pluginMmi = (0, import_node_path22.join)(tmpRoot, "plugins", "mmi");
18864
+ return (0, import_node_fs24.existsSync)((0, import_node_path22.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17893
18865
  } catch {
17894
18866
  return void 0;
17895
18867
  }
17896
18868
  }
17897
18869
  async function downloadPluginMmiViaGh(tag, tmpRoot) {
17898
- const tarPath = (0, import_node_path21.join)(tmpRoot, "repo.tgz");
18870
+ const tarPath = (0, import_node_path22.join)(tmpRoot, "repo.tgz");
17899
18871
  try {
17900
- (0, import_node_fs23.mkdirSync)(tmpRoot, { recursive: true });
18872
+ (0, import_node_fs24.mkdirSync)(tmpRoot, { recursive: true });
17901
18873
  const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
17902
18874
  timeout: 12e4,
17903
18875
  maxBuffer: 100 * 1024 * 1024,
17904
18876
  encoding: "buffer",
17905
18877
  windowsHide: true
17906
18878
  });
17907
- (0, import_node_fs23.writeFileSync)(tarPath, stdout);
18879
+ (0, import_node_fs24.writeFileSync)(tarPath, stdout);
17908
18880
  await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
17909
- const top = (0, import_node_fs23.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
18881
+ const top = (0, import_node_fs24.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
17910
18882
  if (!top) return void 0;
17911
- const pluginMmi = (0, import_node_path21.join)(tmpRoot, top, "plugins", "mmi");
17912
- return (0, import_node_fs23.existsSync)((0, import_node_path21.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
18883
+ const pluginMmi = (0, import_node_path22.join)(tmpRoot, top, "plugins", "mmi");
18884
+ return (0, import_node_fs24.existsSync)((0, import_node_path22.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
17913
18885
  } catch {
17914
18886
  return void 0;
17915
18887
  }
17916
18888
  }
17917
18889
  async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
17918
- (0, import_node_fs23.mkdirSync)(tmpRoot, { recursive: true });
18890
+ (0, import_node_fs24.mkdirSync)(tmpRoot, { recursive: true });
17919
18891
  const tag = releaseTag(releasedVersion);
17920
18892
  if (hubCheckout) {
17921
18893
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
17922
18894
  if (fromHub) return fromHub;
17923
18895
  }
17924
- return downloadPluginMmiViaGh(tag, (0, import_node_path21.join)(tmpRoot, "gh"));
18896
+ return downloadPluginMmiViaGh(tag, (0, import_node_path22.join)(tmpRoot, "gh"));
17925
18897
  }
17926
18898
  function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
17927
18899
  if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
@@ -17942,7 +18914,7 @@ async function applyCursorPluginCacheSeed(input) {
17942
18914
  for (const pin of pinsToSeed) {
17943
18915
  syncDirContents(source, pin.path);
17944
18916
  }
17945
- (0, import_node_fs23.rmSync)(tmpRoot, { recursive: true, force: true });
18917
+ (0, import_node_fs24.rmSync)(tmpRoot, { recursive: true, force: true });
17946
18918
  return true;
17947
18919
  }
17948
18920
 
@@ -18269,6 +19241,30 @@ function buildNestedPluginTreeCheck(input) {
18269
19241
  fix: `${nested.length} self-nested MMI plugin cache tree(s) (#1126) exceed MAX_PATH and can't self-clean \u2014 run: ${nestedPluginTreeCleanupCommand(nested.map((n) => n.path), input.isWindows)}`
18270
19242
  };
18271
19243
  }
19244
+ var CODEX_ACTIVE_CACHE_LABEL = "Codex active plugin cache (vs latest release)";
19245
+ function buildCodexActiveCacheCheck(input) {
19246
+ const base = {
19247
+ ok: true,
19248
+ label: CODEX_ACTIVE_CACHE_LABEL,
19249
+ fix: CODEX_PLUGIN_RECOVERY
19250
+ };
19251
+ if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
19252
+ if (isSemverVersion2(input.codexRecordVersion) && compareVersions(input.codexRecordVersion, input.releasedVersion) < 0) {
19253
+ return base;
19254
+ }
19255
+ const activeCacheVersion = highestSemver(input.codexCacheVersions ?? []);
19256
+ const cacheCurrent = isSemverVersion2(activeCacheVersion) && compareVersions(activeCacheVersion, input.releasedVersion) >= 0;
19257
+ if (cacheCurrent) {
19258
+ return { ...base, activeCacheVersion, releasedVersion: input.releasedVersion };
19259
+ }
19260
+ return {
19261
+ ...base,
19262
+ ok: false,
19263
+ activeCacheVersion,
19264
+ releasedVersion: input.releasedVersion,
19265
+ fix: `Codex active plugin cache is ${activeCacheVersion ?? "missing the released version"} < ${input.releasedVersion} while the installed record is current \u2014 run \`mmi-cli doctor --apply\` (forces the Codex reinstall when \`codex\` is on PATH; restart Codex after), or manually: ${CODEX_PLUGIN_RECOVERY}`
19266
+ };
19267
+ }
18272
19268
  function detectSurface(env) {
18273
19269
  const has = (k) => Boolean(env[k]?.trim());
18274
19270
  if (env.MMI_AGENT_SURFACE === "codex" || has("CODEX_HOME") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) {
@@ -18397,6 +19393,25 @@ var CODEX_PLUGIN_HEAL_STEPS = PLUGIN_SURFACE_HEAL.codex.healSteps;
18397
19393
  function healStepAborts(step, ok) {
18398
19394
  return !ok && step.gated;
18399
19395
  }
19396
+ function marketplaceAddSupportsRef(helpText) {
19397
+ if (!helpText) return false;
19398
+ return /(^|\s)--ref(\b|=)/.test(helpText);
19399
+ }
19400
+ function adaptHealStepsForRefSupport(steps, refSupported) {
19401
+ if (refSupported) return { steps: [...steps], strippedRef: false };
19402
+ let strippedRef = false;
19403
+ const adapted = steps.map((step) => {
19404
+ const refIdx = step.args.indexOf("--ref");
19405
+ const isAdd = step.args.includes("marketplace") && step.args.includes("add");
19406
+ if (!isAdd || refIdx === -1) return step;
19407
+ strippedRef = true;
19408
+ return { ...step, args: [...step.args.slice(0, refIdx), ...step.args.slice(refIdx + 2)] };
19409
+ });
19410
+ return { steps: adapted, strippedRef };
19411
+ }
19412
+ function recoveryWithoutRef(recovery) {
19413
+ return recovery.replace(/ --ref \S+/g, "");
19414
+ }
18400
19415
  function pluginRecoveryFix(surface) {
18401
19416
  const token = surfaceToken(surface);
18402
19417
  if (token) return PLUGIN_SURFACE_HEAL[token].fix(surface);
@@ -18737,7 +19752,7 @@ function cursorPluginInstallFix(input) {
18737
19752
  const cacheDir = joinCachePath(input.cacheRoot, pin);
18738
19753
  const autoSeed = "run `mmi-cli doctor --apply` to seed plugins/mmi from the latest release into the active pin";
18739
19754
  const localSeed = input.hubCheckout ? `temporary fallback: copy ${joinCachePath(input.hubCheckout, "plugins", "mmi")} to ${cacheDir}, then restart Cursor` : `temporary fallback: copy plugins/mmi from a local MMI-Hub checkout to ${cacheDir}, then restart Cursor`;
18740
- return `Cursor plugin cache at ${cacheDir} is empty or missing ${CURSOR_PLUGIN_JSON_REL} or ${CURSOR_HOOKS_JSON_REL} \u2014 ${autoSeed}; ${marketplaceRefresh}; ${authSteps}; ${localSeed}; ${logHint}; ${guide}`;
19755
+ return `Cursor plugin cache at ${cacheDir} is empty or missing ${CURSOR_PLUGIN_JSON_REL}, ${CURSOR_HOOKS_JSON_REL}, or the shell-dialect guard scripts \u2014 ${autoSeed}; ${marketplaceRefresh}; ${authSteps}; ${localSeed}; ${logHint}; ${guide}`;
18741
19756
  }
18742
19757
  function buildCursorPluginInstallCheck(input) {
18743
19758
  const base = {
@@ -18759,7 +19774,7 @@ function buildCursorPluginInstallCheck(input) {
18759
19774
  };
18760
19775
  }
18761
19776
  for (const pin of input.pins) {
18762
- if (!pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty) {
19777
+ if (!pin.hasPluginJson || !pin.hasHooksJson || pin.hasShellDialectGuard === false || pin.isEmpty) {
18763
19778
  return {
18764
19779
  ...base,
18765
19780
  ok: false,
@@ -18976,6 +19991,22 @@ function buildSelfUpdateHaltPayload(input) {
18976
19991
  checks: input.checks
18977
19992
  };
18978
19993
  }
19994
+ var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
19995
+ function buildSelfUpdateReexecArgs(opts) {
19996
+ const args = ["doctor"];
19997
+ if (opts.banner) args.push("--banner");
19998
+ if (opts.preflight) args.push("--preflight");
19999
+ if (opts.verbose) args.push("--verbose");
20000
+ if (opts.guide) args.push("--guide");
20001
+ if (opts.json) args.push("--json");
20002
+ if (opts.apply) args.push("--apply");
20003
+ if (opts.noRepoWrites) args.push("--no-repo-writes");
20004
+ return args;
20005
+ }
20006
+ function selfUpdateReexecLine(report) {
20007
+ const to = report.releasedVersion ?? "latest";
20008
+ return `\u21BB mmi-cli updated \u2192 ${to}. Re-running \`mmi-cli doctor\` under the new CLI\u2026`;
20009
+ }
18979
20010
  function preflightOutcome(input) {
18980
20011
  if (input.gaps.length) {
18981
20012
  return { healed: false, line: `\u26A0 MMI preflight: ${input.gaps.length} item(s) still need attention \u2014 ${input.gaps.map((g) => g.fix).join(" \xB7 ")}` };
@@ -19051,16 +20082,16 @@ function buildPluginResolvabilityCheck(input) {
19051
20082
  }
19052
20083
 
19053
20084
  // src/kb-drift-report.ts
19054
- var import_node_fs24 = require("node:fs");
19055
- var import_node_path22 = require("node:path");
20085
+ var import_node_fs25 = require("node:fs");
20086
+ var import_node_path23 = require("node:path");
19056
20087
  function yesterdayIso() {
19057
20088
  const d = /* @__PURE__ */ new Date();
19058
20089
  d.setUTCDate(d.getUTCDate() - 1);
19059
20090
  return d.toISOString().slice(0, 10);
19060
20091
  }
19061
20092
  async function fetchLatestKbDriftReport(execFileP5, repoRoot) {
19062
- const sagaIo = (0, import_node_path22.join)(repoRoot, "infra", "saga-io.mjs");
19063
- if (!(0, import_node_fs24.existsSync)(sagaIo)) return null;
20093
+ const sagaIo = (0, import_node_path23.join)(repoRoot, "infra", "saga-io.mjs");
20094
+ if (!(0, import_node_fs25.existsSync)(sagaIo)) return null;
19064
20095
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19065
20096
  for (const date of [today, yesterdayIso()]) {
19066
20097
  try {
@@ -19076,9 +20107,9 @@ async function fetchLatestKbDriftReport(execFileP5, repoRoot) {
19076
20107
  }
19077
20108
 
19078
20109
  // src/cli-doctor-shared.ts
19079
- var import_node_fs25 = require("node:fs");
19080
- var import_node_path23 = require("node:path");
19081
20110
  var import_node_fs26 = require("node:fs");
20111
+ var import_node_path24 = require("node:path");
20112
+ var import_node_fs27 = require("node:fs");
19082
20113
  var GC_GH_TIMEOUT_MS = 2e4;
19083
20114
  async function awsCallerArn() {
19084
20115
  try {
@@ -19124,7 +20155,7 @@ async function localBranchHeads() {
19124
20155
  }
19125
20156
  async function currentRepoWorktreeGitRoot(repoRoot) {
19126
20157
  const gitCommonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
19127
- return gitCommonDir ? (0, import_node_path23.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
20158
+ return gitCommonDir ? (0, import_node_path24.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
19128
20159
  }
19129
20160
  async function worktreeBranches() {
19130
20161
  const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
@@ -19144,18 +20175,18 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
19144
20175
  const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
19145
20176
  if (!match?.[1]) return void 0;
19146
20177
  const raw = match[1].trim();
19147
- return (0, import_node_path23.isAbsolute)(raw) ? raw : (0, import_node_path23.resolve)(worktreePath, raw);
20178
+ return (0, import_node_path24.isAbsolute)(raw) ? raw : (0, import_node_path24.resolve)(worktreePath, raw);
19148
20179
  }
19149
20180
  function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
19150
20181
  if (!worktreeGitRoot) return false;
19151
20182
  try {
19152
- const entries = (0, import_node_fs26.readdirSync)(worktreeGitRoot, { withFileTypes: true });
20183
+ const entries = (0, import_node_fs27.readdirSync)(worktreeGitRoot, { withFileTypes: true });
19153
20184
  for (const ent of entries) {
19154
20185
  if (!ent.isDirectory()) continue;
19155
20186
  try {
19156
- const gitdirPath = (0, import_node_fs25.readFileSync)((0, import_node_path23.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
19157
- const resolvedGitdir = (0, import_node_path23.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path23.resolve)(worktreeGitRoot, ent.name, gitdirPath);
19158
- if (sameWorktreeMetadataPath((0, import_node_path23.dirname)(resolvedGitdir), worktreePath)) return true;
20187
+ const gitdirPath = (0, import_node_fs26.readFileSync)((0, import_node_path24.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
20188
+ const resolvedGitdir = (0, import_node_path24.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path24.resolve)(worktreeGitRoot, ent.name, gitdirPath);
20189
+ if (sameWorktreeMetadataPath((0, import_node_path24.dirname)(resolvedGitdir), worktreePath)) return true;
19159
20190
  } catch {
19160
20191
  }
19161
20192
  }
@@ -19165,7 +20196,7 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
19165
20196
  }
19166
20197
  function pathExistsKnown(path2) {
19167
20198
  try {
19168
- (0, import_node_fs26.statSync)(path2);
20199
+ (0, import_node_fs27.statSync)(path2);
19169
20200
  return true;
19170
20201
  } catch (e) {
19171
20202
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
@@ -19174,10 +20205,10 @@ function pathExistsKnown(path2) {
19174
20205
  }
19175
20206
  }
19176
20207
  function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
19177
- const gitPath = (0, import_node_path23.join)(path2, ".git");
20208
+ const gitPath = (0, import_node_path24.join)(path2, ".git");
19178
20209
  let st;
19179
20210
  try {
19180
- st = (0, import_node_fs26.lstatSync)(gitPath);
20211
+ st = (0, import_node_fs27.lstatSync)(gitPath);
19181
20212
  } catch (e) {
19182
20213
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
19183
20214
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -19194,7 +20225,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
19194
20225
  if (st.isDirectory()) return { path: path2, gitType: "dir" };
19195
20226
  if (!st.isFile()) return { path: path2, gitType: "other" };
19196
20227
  try {
19197
- const gitFileContent = (0, import_node_fs25.readFileSync)(gitPath, "utf8");
20228
+ const gitFileContent = (0, import_node_fs26.readFileSync)(gitPath, "utf8");
19198
20229
  const gitdir = resolveGitdirForWorktreeFile(path2, gitFileContent);
19199
20230
  const gitDirExists = gitdir ? pathExistsKnown(gitdir) : false;
19200
20231
  return {
@@ -19211,7 +20242,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
19211
20242
  }
19212
20243
  function inspectDeadWorktreeDirContent(path2) {
19213
20244
  try {
19214
- return { entries: (0, import_node_fs26.readdirSync)(path2) };
20245
+ return { entries: (0, import_node_fs27.readdirSync)(path2) };
19215
20246
  } catch (e) {
19216
20247
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
19217
20248
  return { error: code ? `unable to inspect directory contents (${code})` : "unable to inspect directory contents" };
@@ -19230,8 +20261,8 @@ async function siblingWorktreeDirs() {
19230
20261
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot);
19231
20262
  const siblingRoot = siblingMmiWorktreesRoot(repoRoot);
19232
20263
  try {
19233
- const entries = (0, import_node_fs26.readdirSync)(siblingRoot, { withFileTypes: true });
19234
- return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path23.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
20264
+ const entries = (0, import_node_fs27.readdirSync)(siblingRoot, { withFileTypes: true });
20265
+ return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path24.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
19235
20266
  } catch {
19236
20267
  return [];
19237
20268
  }
@@ -19281,7 +20312,7 @@ async function fetchHubVersionInfo(baseUrl) {
19281
20312
  }
19282
20313
  function readRepoVersion() {
19283
20314
  try {
19284
- return JSON.parse((0, import_node_fs27.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
20315
+ return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
19285
20316
  } catch {
19286
20317
  return void 0;
19287
20318
  }
@@ -19382,6 +20413,21 @@ var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
19382
20413
  function runHostBin(bin, args, opts) {
19383
20414
  return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
19384
20415
  }
20416
+ function reexecMmiCli(args) {
20417
+ return new Promise((resolve6) => {
20418
+ let settled = false;
20419
+ const done = (code) => {
20420
+ if (!settled) {
20421
+ settled = true;
20422
+ resolve6(code);
20423
+ }
20424
+ };
20425
+ const env = { ...process.env, [DOCTOR_POST_SELF_UPDATE_ENV]: "1" };
20426
+ const child = isWin ? (0, import_node_child_process14.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", env }) : (0, import_node_child_process14.spawn)("mmi-cli", args, { stdio: "inherit", env });
20427
+ child.on("error", () => done(-1));
20428
+ child.on("exit", (code) => done(code ?? 0));
20429
+ });
20430
+ }
19385
20431
  function hostBinAvailable(bin) {
19386
20432
  return execFileP2(isWin ? "where" : "which", [bin]).then(() => true).catch(() => false);
19387
20433
  }
@@ -19401,15 +20447,32 @@ async function runCodexPlugin(args) {
19401
20447
  return false;
19402
20448
  }
19403
20449
  }
20450
+ async function marketplaceAddRefSupported(bin) {
20451
+ try {
20452
+ const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
20453
+ timeout: CLAUDE_PLUGIN_TIMEOUT_MS
20454
+ });
20455
+ return marketplaceAddSupportsRef(`${stdout}
20456
+ ${stderr}`);
20457
+ } catch {
20458
+ return false;
20459
+ }
20460
+ }
19404
20461
  async function applyPluginHeal(token, surface, log, opts) {
19405
20462
  if (!opts?.force && surfaceToken(surface) !== token) return false;
19406
20463
  const descriptor = PLUGIN_SURFACE_HEAL[token];
19407
- const steps = descriptor.healSteps;
19408
- if (!steps) return false;
20464
+ const tableSteps = descriptor.healSteps;
20465
+ if (!tableSteps) return false;
20466
+ const bin = descriptor.pluginRunner === "codex" ? "codex" : "claude";
19409
20467
  const runner = descriptor.pluginRunner === "codex" ? runCodexPlugin : runClaudePlugin;
20468
+ const refSupported = await marketplaceAddRefSupported(bin);
20469
+ const { steps, strippedRef } = adaptHealStepsForRefSupport(tableSteps, refSupported);
19410
20470
  log(
19411
- token === "codex" ? " \u21BB reinstalling the MMI plugin via `codex plugin` (marketplace remove \u2192 add --ref main \u2192 add)\u2026" : " \u21BB reinstalling the MMI plugin via `claude plugin` (marketplace remove \u2192 add \u2192 install)\u2026"
20471
+ refSupported ? ` \u21BB reinstalling the MMI plugin via \`${bin} plugin\` (marketplace remove \u2192 add --ref main \u2192 ${token === "codex" ? "add" : "install"})\u2026` : ` \u21BB reinstalling the MMI plugin via \`${bin} plugin\` (marketplace remove \u2192 add \u2192 ${token === "codex" ? "add" : "install"})\u2026`
19412
20472
  );
20473
+ if (strippedRef) {
20474
+ log(` \u26A0 \`${bin}\` has no \`--ref\` option \u2014 cloning the default branch; update \`${bin}\` to pin the released \`main\` branch (#2080)`);
20475
+ }
19413
20476
  for (const step of steps) {
19414
20477
  if (healStepAborts(step, await runner([...step.args]))) return false;
19415
20478
  }
@@ -19417,11 +20480,11 @@ async function applyPluginHeal(token, surface, log, opts) {
19417
20480
  }
19418
20481
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
19419
20482
  const homeDir = surface === "codex" ? ".codex" : ".claude";
19420
- return (0, import_node_path24.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
20483
+ return (0, import_node_path25.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
19421
20484
  };
19422
20485
  function readInstalledPlugins(surface = detectSurface(process.env)) {
19423
20486
  try {
19424
- return JSON.parse((0, import_node_fs27.readFileSync)(installedPluginsPath(surface), "utf8"));
20487
+ return JSON.parse((0, import_node_fs28.readFileSync)(installedPluginsPath(surface), "utf8"));
19425
20488
  } catch {
19426
20489
  return null;
19427
20490
  }
@@ -19432,15 +20495,15 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
19432
20495
  return {
19433
20496
  isOrgRepo,
19434
20497
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
19435
- marketplaceClonePresent: (0, import_node_fs27.existsSync)((0, import_node_path24.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
19436
- pluginCachePresent: (0, import_node_fs27.existsSync)((0, import_node_path24.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
20498
+ marketplaceClonePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
20499
+ pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
19437
20500
  };
19438
20501
  }
19439
20502
  function installedPluginSources() {
19440
20503
  return ["claude", "codex"].map((surface) => {
19441
- const recordPath = (0, import_node_path24.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
20504
+ const recordPath = (0, import_node_path25.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
19442
20505
  try {
19443
- return { surface, installed: JSON.parse((0, import_node_fs27.readFileSync)(recordPath, "utf8")), recordPath };
20506
+ return { surface, installed: JSON.parse((0, import_node_fs28.readFileSync)(recordPath, "utf8")), recordPath };
19444
20507
  } catch {
19445
20508
  return { surface, installed: null, recordPath };
19446
20509
  }
@@ -19448,7 +20511,7 @@ function installedPluginSources() {
19448
20511
  }
19449
20512
  function readClaudeSettings() {
19450
20513
  try {
19451
- return JSON.parse((0, import_node_fs27.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
20514
+ return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
19452
20515
  } catch {
19453
20516
  return null;
19454
20517
  }
@@ -19470,7 +20533,7 @@ function writeProjectInstallRecord(record) {
19470
20533
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
19471
20534
  list.push(record);
19472
20535
  file.plugins[MMI_PLUGIN_ID] = list;
19473
- (0, import_node_fs27.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
20536
+ (0, import_node_fs28.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
19474
20537
  `, "utf8");
19475
20538
  return true;
19476
20539
  } catch {
@@ -19483,9 +20546,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
19483
20546
  if (!file) return false;
19484
20547
  if (!file.plugins) file.plugins = {};
19485
20548
  const path2 = installedPluginsPath();
19486
- (0, import_node_fs27.copyFileSync)(path2, `${path2}.bak`);
20549
+ (0, import_node_fs28.copyFileSync)(path2, `${path2}.bak`);
19487
20550
  file.plugins[pluginId] = records;
19488
- (0, import_node_fs27.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
20551
+ (0, import_node_fs28.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
19489
20552
  `, "utf8");
19490
20553
  return true;
19491
20554
  } catch {
@@ -19493,22 +20556,22 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
19493
20556
  }
19494
20557
  }
19495
20558
  function opencodeConfigDir() {
19496
- return (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".config", "opencode");
20559
+ return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".config", "opencode");
19497
20560
  }
19498
20561
  function opencodeConfigPath() {
19499
- return (0, import_node_path24.join)(opencodeConfigDir(), "opencode.jsonc");
20562
+ return (0, import_node_path25.join)(opencodeConfigDir(), "opencode.jsonc");
19500
20563
  }
19501
20564
  function opencodeCommandsDir() {
19502
- return (0, import_node_path24.join)(opencodeConfigDir(), "commands");
20565
+ return (0, import_node_path25.join)(opencodeConfigDir(), "commands");
19503
20566
  }
19504
20567
  function opencodeSkillsPath() {
19505
- return (0, import_node_path24.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
20568
+ return (0, import_node_path25.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
19506
20569
  }
19507
20570
  function opencodeConfigSnapshot() {
19508
20571
  const path2 = opencodeConfigPath();
19509
- if (!(0, import_node_fs27.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
20572
+ if (!(0, import_node_fs28.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
19510
20573
  try {
19511
- const raw = (0, import_node_fs27.readFileSync)(path2, "utf8");
20574
+ const raw = (0, import_node_fs28.readFileSync)(path2, "utf8");
19512
20575
  const parsed = JSON.parse(stripJsonc(raw));
19513
20576
  const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
19514
20577
  const skillsPaths = Array.isArray(parsed.skills?.paths) ? parsed.skills.paths.filter((p) => typeof p === "string") : void 0;
@@ -19531,9 +20594,9 @@ function writeOpencodeConfigPlugin(snapshot) {
19531
20594
  const plan2 = planOpencodeConfigWrite(snapshot.hasConfig ? snapshot.raw : void 0);
19532
20595
  if (plan2.action === "already") return true;
19533
20596
  if (!plan2.text || plan2.action === "unsafe") return false;
19534
- (0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(path2), { recursive: true });
19535
- if (snapshot.hasConfig) (0, import_node_fs27.copyFileSync)(path2, `${path2}.bak`);
19536
- (0, import_node_fs27.writeFileSync)(path2, plan2.text, "utf8");
20597
+ (0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(path2), { recursive: true });
20598
+ if (snapshot.hasConfig) (0, import_node_fs28.copyFileSync)(path2, `${path2}.bak`);
20599
+ (0, import_node_fs28.writeFileSync)(path2, plan2.text, "utf8");
19537
20600
  return true;
19538
20601
  } catch {
19539
20602
  return false;
@@ -19548,9 +20611,9 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
19548
20611
  const normalized = skillsPath.replace(/\\/g, "/");
19549
20612
  if (!paths.some((p) => p.replace(/\\/g, "/") === normalized)) paths.push(skillsPath.replace(/\\/g, "/"));
19550
20613
  parsed.skills = { ...skills, paths };
19551
- (0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(snapshot.path), { recursive: true });
19552
- if (snapshot.hasConfig && (0, import_node_fs27.existsSync)(snapshot.path)) (0, import_node_fs27.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
19553
- (0, import_node_fs27.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
20614
+ (0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(snapshot.path), { recursive: true });
20615
+ if (snapshot.hasConfig && (0, import_node_fs28.existsSync)(snapshot.path)) (0, import_node_fs28.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
20616
+ (0, import_node_fs28.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
19554
20617
  `, "utf8");
19555
20618
  return true;
19556
20619
  } catch {
@@ -19559,7 +20622,7 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
19559
20622
  }
19560
20623
  function opencodeExistingCommands() {
19561
20624
  try {
19562
- return (0, import_node_fs27.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
20625
+ return (0, import_node_fs28.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
19563
20626
  } catch {
19564
20627
  return [];
19565
20628
  }
@@ -19567,9 +20630,9 @@ function opencodeExistingCommands() {
19567
20630
  function writeOpencodeCommandFiles() {
19568
20631
  try {
19569
20632
  const dir = opencodeCommandsDir();
19570
- (0, import_node_fs27.mkdirSync)(dir, { recursive: true });
20633
+ (0, import_node_fs28.mkdirSync)(dir, { recursive: true });
19571
20634
  for (const command of OPENCODE_WORKFLOW_COMMANDS) {
19572
- (0, import_node_fs27.writeFileSync)((0, import_node_path24.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
20635
+ (0, import_node_fs28.writeFileSync)((0, import_node_path25.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
19573
20636
  }
19574
20637
  return true;
19575
20638
  } catch {
@@ -19578,12 +20641,12 @@ function writeOpencodeCommandFiles() {
19578
20641
  }
19579
20642
  function readOpencodeAdapterDiskVersion() {
19580
20643
  const candidates = [
19581
- (0, import_node_path24.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
19582
- (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
20644
+ (0, import_node_path25.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
20645
+ (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
19583
20646
  ];
19584
20647
  for (const path2 of candidates) {
19585
20648
  try {
19586
- const parsed = JSON.parse((0, import_node_fs27.readFileSync)(path2, "utf8"));
20649
+ const parsed = JSON.parse((0, import_node_fs28.readFileSync)(path2, "utf8"));
19587
20650
  if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
19588
20651
  } catch {
19589
20652
  continue;
@@ -19599,7 +20662,7 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
19599
20662
  try {
19600
20663
  const specs = opencodeMmiPluginSpecs(snapshot);
19601
20664
  log(` \u21BB force-refreshing OpenCode MMI npm plugin(s): ${specs.join(", ")}\u2026`);
19602
- (0, import_node_fs27.mkdirSync)(opencodeConfigDir(), { recursive: true });
20665
+ (0, import_node_fs28.mkdirSync)(opencodeConfigDir(), { recursive: true });
19603
20666
  await runHostBin("npm", ["install", "--prefix", opencodeConfigDir(), "--force", ...specs], { timeout: NPM_UPDATE_TIMEOUT_MS });
19604
20667
  return true;
19605
20668
  } catch {
@@ -19614,30 +20677,30 @@ function opencodePluginVersionsForReport() {
19614
20677
  }
19615
20678
  function opencodeDesktopLogsRoot() {
19616
20679
  if (process.platform === "win32") {
19617
- const base = process.env.APPDATA || (0, import_node_path24.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
19618
- return (0, import_node_path24.join)(base, "ai.opencode.desktop", "logs");
20680
+ const base = process.env.APPDATA || (0, import_node_path25.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
20681
+ return (0, import_node_path25.join)(base, "ai.opencode.desktop", "logs");
19619
20682
  }
19620
20683
  if (process.platform === "darwin") {
19621
- return (0, import_node_path24.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
20684
+ return (0, import_node_path25.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
19622
20685
  }
19623
- return (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".config", "ai.opencode.desktop", "logs");
20686
+ return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".config", "ai.opencode.desktop", "logs");
19624
20687
  }
19625
20688
  function opencodeDesktopBootstrapSnapshot() {
19626
20689
  const root = opencodeDesktopLogsRoot();
19627
20690
  try {
19628
- const sessionDirs = (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path24.join)(root, entry.name)).sort((a, b) => (0, import_node_fs27.statSync)(b).mtimeMs - (0, import_node_fs27.statSync)(a).mtimeMs);
20691
+ const sessionDirs = (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path25.join)(root, entry.name)).sort((a, b) => (0, import_node_fs28.statSync)(b).mtimeMs - (0, import_node_fs28.statSync)(a).mtimeMs);
19629
20692
  const newest = sessionDirs[0];
19630
20693
  if (!newest) return [];
19631
- const logPath = (0, import_node_path24.join)(newest, "renderer.log");
19632
- const text = (0, import_node_fs27.readFileSync)(logPath, "utf8");
19633
- return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs27.existsSync)(directory)).map((directory) => ({ directory, logPath }));
20694
+ const logPath = (0, import_node_path25.join)(newest, "renderer.log");
20695
+ const text = (0, import_node_fs28.readFileSync)(logPath, "utf8");
20696
+ return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs28.existsSync)(directory)).map((directory) => ({ directory, logPath }));
19634
20697
  } catch {
19635
20698
  return [];
19636
20699
  }
19637
20700
  }
19638
20701
  function opencodeLegacyConfigSnapshot() {
19639
- const legacyPath = (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".opencode", "opencode.json");
19640
- if (!(0, import_node_fs27.existsSync)(legacyPath)) return {};
20702
+ const legacyPath = (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".opencode", "opencode.json");
20703
+ if (!(0, import_node_fs28.existsSync)(legacyPath)) return {};
19641
20704
  const content = readTextFile(legacyPath);
19642
20705
  if (content == null) return {};
19643
20706
  const plugins = parseOpencodeLegacyConfigPlugins(content);
@@ -19649,43 +20712,46 @@ function opencodeLegacyConfigSnapshot() {
19649
20712
  function quarantineOpencodeLegacyConfig(legacyPath) {
19650
20713
  try {
19651
20714
  const backupPath = `${legacyPath}.bak`;
19652
- if ((0, import_node_fs27.existsSync)(backupPath)) return false;
19653
- (0, import_node_fs27.renameSync)(legacyPath, backupPath);
20715
+ if ((0, import_node_fs28.existsSync)(backupPath)) return false;
20716
+ (0, import_node_fs28.renameSync)(legacyPath, backupPath);
19654
20717
  return true;
19655
20718
  } catch {
19656
20719
  return false;
19657
20720
  }
19658
20721
  }
19659
20722
  function cursorPluginCacheRoot() {
19660
- return (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
20723
+ return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
19661
20724
  }
19662
20725
  function cursorPluginCachePinSnapshots() {
19663
20726
  const root = cursorPluginCacheRoot();
19664
20727
  try {
19665
- return (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
19666
- const path2 = (0, import_node_path24.join)(root, entry.name);
19667
- const pluginJson = (0, import_node_path24.join)(path2, ".cursor-plugin", "plugin.json");
19668
- const hooksJson = (0, import_node_path24.join)(path2, "hooks", "hooks.json");
19669
- const cliBundle = (0, import_node_path24.join)(path2, "cli", "dist", "index.cjs");
20728
+ return (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
20729
+ const path2 = (0, import_node_path25.join)(root, entry.name);
20730
+ const pluginJson = (0, import_node_path25.join)(path2, ".cursor-plugin", "plugin.json");
20731
+ const hooksJson = (0, import_node_path25.join)(path2, "hooks", "hooks.json");
20732
+ const cliBundle = (0, import_node_path25.join)(path2, "cli", "dist", "index.cjs");
20733
+ const throttleGateCursor = (0, import_node_path25.join)(path2, "scripts", "throttle-gate-cursor.mjs");
20734
+ const throttleCore = (0, import_node_path25.join)(path2, "scripts", "throttle-core.mjs");
19670
20735
  let version;
19671
20736
  try {
19672
- const raw = JSON.parse((0, import_node_fs27.readFileSync)(pluginJson, "utf8"));
20737
+ const raw = JSON.parse((0, import_node_fs28.readFileSync)(pluginJson, "utf8"));
19673
20738
  version = typeof raw.version === "string" ? raw.version : void 0;
19674
20739
  } catch {
19675
20740
  version = void 0;
19676
20741
  }
19677
20742
  let isEmpty = true;
19678
20743
  try {
19679
- isEmpty = (0, import_node_fs27.readdirSync)(path2).length === 0;
20744
+ isEmpty = (0, import_node_fs28.readdirSync)(path2).length === 0;
19680
20745
  } catch {
19681
20746
  isEmpty = true;
19682
20747
  }
19683
20748
  return {
19684
20749
  name: entry.name,
19685
20750
  path: path2,
19686
- hasPluginJson: (0, import_node_fs27.existsSync)(pluginJson),
19687
- hasHooksJson: (0, import_node_fs27.existsSync)(hooksJson),
19688
- hasCliBundle: (0, import_node_fs27.existsSync)(cliBundle),
20751
+ hasPluginJson: (0, import_node_fs28.existsSync)(pluginJson),
20752
+ hasHooksJson: (0, import_node_fs28.existsSync)(hooksJson),
20753
+ hasCliBundle: (0, import_node_fs28.existsSync)(cliBundle),
20754
+ hasShellDialectGuard: (0, import_node_fs28.existsSync)(throttleGateCursor) && (0, import_node_fs28.existsSync)(throttleCore),
19689
20755
  isEmpty,
19690
20756
  version
19691
20757
  };
@@ -19695,19 +20761,19 @@ function cursorPluginCachePinSnapshots() {
19695
20761
  }
19696
20762
  }
19697
20763
  function hubCheckoutForCursorSeed() {
19698
- const manifest = (0, import_node_path24.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
19699
- return (0, import_node_fs27.existsSync)(manifest) ? process.cwd() : void 0;
20764
+ const manifest = (0, import_node_path25.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
20765
+ return (0, import_node_fs28.existsSync)(manifest) ? process.cwd() : void 0;
19700
20766
  }
19701
20767
  function mmiPluginCacheRootSnapshots() {
19702
20768
  const roots = [
19703
- { surface: "claude", root: (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
19704
- { surface: "codex", root: (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
20769
+ { surface: "claude", root: (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
20770
+ { surface: "codex", root: (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
19705
20771
  ];
19706
20772
  return roots.flatMap(({ surface, root }) => {
19707
20773
  try {
19708
- const entries = (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
20774
+ const entries = (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
19709
20775
  name: entry.name,
19710
- path: (0, import_node_path24.join)(root, entry.name),
20776
+ path: (0, import_node_path25.join)(root, entry.name),
19711
20777
  isDirectory: entry.isDirectory()
19712
20778
  }));
19713
20779
  return [{ surface, root, entries }];
@@ -19718,7 +20784,7 @@ function mmiPluginCacheRootSnapshots() {
19718
20784
  }
19719
20785
  function hasNestedMmiChild(versionDir) {
19720
20786
  try {
19721
- return (0, import_node_fs27.statSync)((0, import_node_path24.join)(versionDir, "mmi")).isDirectory();
20787
+ return (0, import_node_fs28.statSync)((0, import_node_path25.join)(versionDir, "mmi")).isDirectory();
19722
20788
  } catch {
19723
20789
  return false;
19724
20790
  }
@@ -19729,10 +20795,10 @@ function nestedPluginTreeSnapshot() {
19729
20795
  );
19730
20796
  }
19731
20797
  function uniqueQuarantineTarget(path2) {
19732
- if (!(0, import_node_fs27.existsSync)(path2)) return path2;
20798
+ if (!(0, import_node_fs28.existsSync)(path2)) return path2;
19733
20799
  for (let i = 1; i < 100; i += 1) {
19734
20800
  const candidate = `${path2}-${i}`;
19735
- if (!(0, import_node_fs27.existsSync)(candidate)) return candidate;
20801
+ if (!(0, import_node_fs28.existsSync)(candidate)) return candidate;
19736
20802
  }
19737
20803
  return `${path2}-${Date.now()}`;
19738
20804
  }
@@ -19741,10 +20807,10 @@ function quarantinePluginCacheDirs(plan2) {
19741
20807
  const failed = [];
19742
20808
  for (const move of plan2) {
19743
20809
  try {
19744
- if (!(0, import_node_fs27.existsSync)(move.from)) continue;
20810
+ if (!(0, import_node_fs28.existsSync)(move.from)) continue;
19745
20811
  const target = uniqueQuarantineTarget(move.to);
19746
- (0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(target), { recursive: true });
19747
- (0, import_node_fs27.renameSync)(move.from, target);
20812
+ (0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(target), { recursive: true });
20813
+ (0, import_node_fs28.renameSync)(move.from, target);
19748
20814
  moved += 1;
19749
20815
  } catch {
19750
20816
  failed.push(move);
@@ -19763,23 +20829,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
19763
20829
  }
19764
20830
  async function clearNestedPluginTreeDir(targetPath) {
19765
20831
  try {
19766
- if (!(0, import_node_fs27.existsSync)(targetPath)) return true;
20832
+ if (!(0, import_node_fs28.existsSync)(targetPath)) return true;
19767
20833
  if (isWin) {
19768
- const emptyDir = (0, import_node_path24.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
19769
- (0, import_node_fs27.mkdirSync)(emptyDir, { recursive: true });
20834
+ const emptyDir = (0, import_node_path25.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
20835
+ (0, import_node_fs28.mkdirSync)(emptyDir, { recursive: true });
19770
20836
  try {
19771
20837
  await robocopyMirrorEmpty(emptyDir, targetPath);
19772
- (0, import_node_fs27.rmSync)(targetPath, { recursive: true, force: true });
20838
+ (0, import_node_fs28.rmSync)(targetPath, { recursive: true, force: true });
19773
20839
  } finally {
19774
20840
  try {
19775
- (0, import_node_fs27.rmSync)(emptyDir, { recursive: true, force: true });
20841
+ (0, import_node_fs28.rmSync)(emptyDir, { recursive: true, force: true });
19776
20842
  } catch {
19777
20843
  }
19778
20844
  }
19779
- return !(0, import_node_fs27.existsSync)(targetPath);
20845
+ return !(0, import_node_fs28.existsSync)(targetPath);
19780
20846
  }
19781
- (0, import_node_fs27.rmSync)(targetPath, { recursive: true, force: true });
19782
- return !(0, import_node_fs27.existsSync)(targetPath);
20847
+ (0, import_node_fs28.rmSync)(targetPath, { recursive: true, force: true });
20848
+ return !(0, import_node_fs28.existsSync)(targetPath);
19783
20849
  } catch {
19784
20850
  return false;
19785
20851
  }
@@ -19792,23 +20858,23 @@ async function applyNestedPluginTreeCleanup(paths, log) {
19792
20858
  }
19793
20859
  return true;
19794
20860
  }
19795
- var gitignorePath = () => (0, import_node_path24.join)(process.cwd(), ".gitignore");
20861
+ var gitignorePath = () => (0, import_node_path25.join)(process.cwd(), ".gitignore");
19796
20862
  function readTextFile(path2) {
19797
20863
  try {
19798
- if (!(0, import_node_fs27.existsSync)(path2)) return null;
19799
- return (0, import_node_fs27.readFileSync)(path2, "utf8");
20864
+ if (!(0, import_node_fs28.existsSync)(path2)) return null;
20865
+ return (0, import_node_fs28.readFileSync)(path2, "utf8");
19800
20866
  } catch {
19801
20867
  return null;
19802
20868
  }
19803
20869
  }
19804
20870
  function playwrightMcpConfigSnapshots() {
19805
20871
  const cwd = process.cwd();
19806
- const home = (0, import_node_os5.homedir)();
20872
+ const home = (0, import_node_os6.homedir)();
19807
20873
  const candidates = [
19808
- (0, import_node_path24.join)(cwd, ".mcp.json"),
19809
- (0, import_node_path24.join)(cwd, ".cursor", "mcp.json"),
19810
- (0, import_node_path24.join)(home, ".cursor", "mcp.json"),
19811
- (0, import_node_path24.join)(home, ".codex", "config.toml")
20874
+ (0, import_node_path25.join)(cwd, ".mcp.json"),
20875
+ (0, import_node_path25.join)(cwd, ".cursor", "mcp.json"),
20876
+ (0, import_node_path25.join)(home, ".cursor", "mcp.json"),
20877
+ (0, import_node_path25.join)(home, ".codex", "config.toml")
19812
20878
  ];
19813
20879
  const out = [];
19814
20880
  for (const path2 of candidates) {
@@ -19821,7 +20887,7 @@ function strayBrowserArtifactPaths() {
19821
20887
  const cwd = process.cwd();
19822
20888
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
19823
20889
  try {
19824
- return (0, import_node_fs27.existsSync)((0, import_node_path24.join)(cwd, rel));
20890
+ return (0, import_node_fs28.existsSync)((0, import_node_path25.join)(cwd, rel));
19825
20891
  } catch {
19826
20892
  return false;
19827
20893
  }
@@ -19840,8 +20906,8 @@ function latestIso(values) {
19840
20906
  return best;
19841
20907
  }
19842
20908
  function latestNorthstarContinuityAt() {
19843
- const meta = parseMeta(readTextFile((0, import_node_path24.join)(process.cwd(), META_FILE)));
19844
- const queue = parseQueue(readTextFile((0, import_node_path24.join)(process.cwd(), QUEUE_FILE)));
20909
+ const meta = parseMeta(readTextFile((0, import_node_path25.join)(process.cwd(), META_FILE)));
20910
+ const queue = parseQueue(readTextFile((0, import_node_path25.join)(process.cwd(), QUEUE_FILE)));
19845
20911
  return latestIso([
19846
20912
  ...Object.values(meta).map((entry) => entry.syncedAt),
19847
20913
  ...queue.map((entry) => entry.queuedAt)
@@ -19855,14 +20921,14 @@ async function latestBranchWorkAt() {
19855
20921
  }
19856
20922
  function readGitignore() {
19857
20923
  try {
19858
- return (0, import_node_fs27.readFileSync)(gitignorePath(), "utf8");
20924
+ return (0, import_node_fs28.readFileSync)(gitignorePath(), "utf8");
19859
20925
  } catch {
19860
20926
  return null;
19861
20927
  }
19862
20928
  }
19863
20929
  function writeGitignore(content) {
19864
20930
  try {
19865
- (0, import_node_fs27.writeFileSync)(gitignorePath(), content, "utf8");
20931
+ (0, import_node_fs28.writeFileSync)(gitignorePath(), content, "utf8");
19866
20932
  return true;
19867
20933
  } catch {
19868
20934
  return false;
@@ -19901,7 +20967,7 @@ async function runDoctor(opts, io = consoleIo) {
19901
20967
  const semverPrefix = /^\d+\.\d+\.\d+/;
19902
20968
  const isBehind = (installed2, released) => Boolean(installed2 && released && semverPrefix.test(installed2) && semverPrefix.test(released) && compareVersions(installed2, released) < 0);
19903
20969
  const opencodeAdapterStale = isBehind(opencodeInstalledVersionForDoctor(), releasedVersion);
19904
- const cursorCacheStale = (0, import_node_fs27.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
20970
+ const cursorCacheStale = (0, import_node_fs28.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
19905
20971
  const healPlan = doctorHealPlan({
19906
20972
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19907
20973
  surface,
@@ -19936,7 +21002,7 @@ async function runDoctor(opts, io = consoleIo) {
19936
21002
  let onPath = pathProbe;
19937
21003
  if (!onPath) {
19938
21004
  const root = process.env.CLAUDE_PLUGIN_ROOT;
19939
- if (root && (0, import_node_fs27.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
21005
+ if (root && (0, import_node_fs28.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
19940
21006
  }
19941
21007
  checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
19942
21008
  const reloadHint = reloadAction(surface);
@@ -19950,7 +21016,7 @@ async function runDoctor(opts, io = consoleIo) {
19950
21016
  releasedVersion
19951
21017
  });
19952
21018
  let selfUpdatedCli = false;
19953
- if (repairFull) {
21019
+ if (repairFull && !process.env[DOCTOR_POST_SELF_UPDATE_ENV]) {
19954
21020
  const updated = await applyVersionAutoUpdate(versionReport, (m) => io.err(m));
19955
21021
  versionReport = updated.report;
19956
21022
  selfUpdatedCli = updated.applied === "npm";
@@ -19958,6 +21024,12 @@ async function runDoctor(opts, io = consoleIo) {
19958
21024
  if (!versionReport.ok) versionReport = { ...versionReport, fix: pluginRecoveryFix(surface) };
19959
21025
  checks.push(versionReport);
19960
21026
  if (selfUpdatedCli) {
21027
+ if (!opts.json) io.err(selfUpdateReexecLine(versionReport));
21028
+ const code = await reexecMmiCli(buildSelfUpdateReexecArgs(opts));
21029
+ if (code >= 0) {
21030
+ if (code > 0) process.exitCode = code;
21031
+ return;
21032
+ }
19961
21033
  if (opts.json) io.log(JSON.stringify(buildSelfUpdateHaltPayload({ checks, updatedTo: versionReport.releasedVersion }), null, 2));
19962
21034
  else io.err(selfUpdateHaltLine(versionReport));
19963
21035
  return;
@@ -20209,6 +21281,11 @@ async function runDoctor(opts, io = consoleIo) {
20209
21281
  }
20210
21282
  checks.push(legacyOpenCodeCheck);
20211
21283
  }
21284
+ const codexRecordVersion = () => {
21285
+ const versions = installedPluginVersions(installedPluginSources().find((src) => src.surface === "codex")?.installed ?? null).filter((v) => /^v?\d+\.\d+\.\d+/.test(v));
21286
+ return versions.sort((a, b) => compareVersions(b, a))[0];
21287
+ };
21288
+ const codexCacheVersions = () => mmiPluginCacheRootSnapshots().filter((r) => r.surface === "codex").flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
20212
21289
  let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
20213
21290
  isOrgRepo: Boolean(cfg.sagaApiUrl),
20214
21291
  roots: mmiPluginCacheRootSnapshots(),
@@ -20240,6 +21317,26 @@ async function runDoctor(opts, io = consoleIo) {
20240
21317
  };
20241
21318
  }
20242
21319
  checks.push(cacheCleanupCheck);
21320
+ let codexActiveCacheCheck = buildCodexActiveCacheCheck({
21321
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
21322
+ releasedVersion,
21323
+ codexCacheVersions: codexCacheVersions(),
21324
+ codexRecordVersion: codexRecordVersion()
21325
+ });
21326
+ if (!codexActiveCacheCheck.ok && repairFull) {
21327
+ const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
21328
+ if (canDriveCodex && await applyPluginHeal("codex", surface, (m) => io.err(m), { force: true })) {
21329
+ markPluginReloadRequired();
21330
+ io.err(` \u21BB restored Codex MMI plugin cache \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction("codex")} to load the new commands`);
21331
+ codexActiveCacheCheck = buildCodexActiveCacheCheck({
21332
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
21333
+ releasedVersion,
21334
+ codexCacheVersions: codexCacheVersions(),
21335
+ codexRecordVersion: codexRecordVersion()
21336
+ });
21337
+ }
21338
+ }
21339
+ checks.push(codexActiveCacheCheck);
20243
21340
  let nestedPluginTreeCheck = buildNestedPluginTreeCheck({
20244
21341
  isOrgRepo: Boolean(cfg.sagaApiUrl),
20245
21342
  isWindows: isWin,
@@ -20274,7 +21371,7 @@ async function runDoctor(opts, io = consoleIo) {
20274
21371
  isOrgRepo: Boolean(cfg.sagaApiUrl),
20275
21372
  surface,
20276
21373
  cacheRoot: cursorCacheRoot,
20277
- cacheRootExists: (0, import_node_fs27.existsSync)(cursorCacheRoot),
21374
+ cacheRootExists: (0, import_node_fs28.existsSync)(cursorCacheRoot),
20278
21375
  pins: cursorPins,
20279
21376
  hubCheckout: hubCheckoutForCursorSeed(),
20280
21377
  releasedVersion
@@ -20285,7 +21382,7 @@ async function runDoctor(opts, io = consoleIo) {
20285
21382
  releasedVersion,
20286
21383
  hubCheckout: hubCheckoutForCursorSeed(),
20287
21384
  execFileP: execFileP2,
20288
- mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path24.join)((0, import_node_os5.tmpdir)(), prefix)),
21385
+ mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0, import_node_os6.tmpdir)(), prefix)),
20289
21386
  log: (m) => io.err(m)
20290
21387
  });
20291
21388
  if (seeded) {
@@ -20294,7 +21391,7 @@ async function runDoctor(opts, io = consoleIo) {
20294
21391
  isOrgRepo: Boolean(cfg.sagaApiUrl),
20295
21392
  surface,
20296
21393
  cacheRoot: cursorCacheRoot,
20297
- cacheRootExists: (0, import_node_fs27.existsSync)(cursorCacheRoot),
21394
+ cacheRootExists: (0, import_node_fs28.existsSync)(cursorCacheRoot),
20298
21395
  pins: cursorPins,
20299
21396
  hubCheckout: hubCheckoutForCursorSeed(),
20300
21397
  releasedVersion
@@ -20483,23 +21580,23 @@ function mergeGuardHook(settings) {
20483
21580
  next.hooks = hooks;
20484
21581
  return next;
20485
21582
  }
20486
- var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path24.join)((0, import_node_os5.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
21583
+ var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0, import_node_os6.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
20487
21584
  function ensureUserScopeGuardHook(opts = {}) {
20488
21585
  const path2 = opts.settingsPath ?? userScopeSettingsPath();
20489
21586
  try {
20490
21587
  let current = null;
20491
- if ((0, import_node_fs27.existsSync)(path2)) {
21588
+ if ((0, import_node_fs28.existsSync)(path2)) {
20492
21589
  try {
20493
- current = JSON.parse((0, import_node_fs27.readFileSync)(path2, "utf8"));
21590
+ current = JSON.parse((0, import_node_fs28.readFileSync)(path2, "utf8"));
20494
21591
  } catch {
20495
21592
  return "failed";
20496
21593
  }
20497
21594
  }
20498
21595
  if (settingsHasGuardHook(current)) return "already";
20499
21596
  const merged = mergeGuardHook(current);
20500
- (0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(path2), { recursive: true });
20501
- if ((0, import_node_fs27.existsSync)(path2)) (0, import_node_fs27.copyFileSync)(path2, `${path2}.bak`);
20502
- (0, import_node_fs27.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
21597
+ (0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(path2), { recursive: true });
21598
+ if ((0, import_node_fs28.existsSync)(path2)) (0, import_node_fs28.copyFileSync)(path2, `${path2}.bak`);
21599
+ (0, import_node_fs28.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
20503
21600
  `, "utf8");
20504
21601
  return "written";
20505
21602
  } catch {
@@ -20533,8 +21630,13 @@ async function runPluginHeal(surface = detectSurface(process.env)) {
20533
21630
  if (healed) {
20534
21631
  console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands`);
20535
21632
  } else {
21633
+ const bin = descriptor.pluginRunner === "codex" ? "codex" : "claude";
21634
+ const refSupported = await marketplaceAddRefSupported(bin);
21635
+ const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
21636
+ const note = refSupported ? "" : `
21637
+ Note: \`${bin}\` has no \`--ref\` option here; update \`${bin}\` to pin the released \`main\` branch.`;
20536
21638
  console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
20537
- ${descriptor.recovery}`);
21639
+ ${recovery}${note}`);
20538
21640
  }
20539
21641
  }
20540
21642
 
@@ -20629,7 +21731,7 @@ async function applyGcPlan(plan2, remote) {
20629
21731
  cleanupBranch: (branch, expectedHeadOid) => cleanupPrMergeLocalBranch(branch.branch, {
20630
21732
  beforeWorktrees,
20631
21733
  startingPath: branch.worktreePath,
20632
- pathExists: (p) => (0, import_node_fs28.existsSync)(p),
21734
+ pathExists: (p) => (0, import_node_fs29.existsSync)(p),
20633
21735
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
20634
21736
  teardownWorktreeStage,
20635
21737
  deferredStore,
@@ -20652,7 +21754,7 @@ async function applyGcPlan(plan2, remote) {
20652
21754
  for (const wt of plan2.worktreeDirs) {
20653
21755
  try {
20654
21756
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
20655
- realpath: (path2) => (0, import_node_fs28.realpathSync)(path2)
21757
+ realpath: (path2) => (0, import_node_fs29.realpathSync)(path2)
20656
21758
  });
20657
21759
  if (!cleanupTarget.ok) {
20658
21760
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -20732,10 +21834,10 @@ async function runRulesSync(opts, io = consoleIo) {
20732
21834
  for (const entry of fetched) {
20733
21835
  if ("error" in entry) continue;
20734
21836
  const { file, source } = entry;
20735
- const current = (0, import_node_fs28.existsSync)(file) ? await (0, import_promises8.readFile)(file, "utf8") : null;
21837
+ const current = (0, import_node_fs29.existsSync)(file) ? await (0, import_promises8.readFile)(file, "utf8") : null;
20736
21838
  if (needsUpdate(source, current)) {
20737
21839
  const slash = file.lastIndexOf("/");
20738
- if (slash > 0) (0, import_node_fs28.mkdirSync)(file.slice(0, slash), { recursive: true });
21840
+ if (slash > 0) (0, import_node_fs29.mkdirSync)(file.slice(0, slash), { recursive: true });
20739
21841
  await (0, import_promises8.writeFile)(file, normalizeEol(source), "utf8");
20740
21842
  changed++;
20741
21843
  if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
@@ -20749,13 +21851,13 @@ rules.command("sync").option("--quiet", "stay silent unless something changed or
20749
21851
  if (!await runRulesSync(opts)) process.exitCode = 1;
20750
21852
  });
20751
21853
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
20752
- const path2 = (0, import_node_path25.join)(process.cwd(), ".gitignore");
20753
- const current = (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null;
21854
+ const path2 = (0, import_node_path26.join)(process.cwd(), ".gitignore");
21855
+ const current = (0, import_node_fs29.existsSync)(path2) ? (0, import_node_fs29.readFileSync)(path2, "utf8") : null;
20754
21856
  const plan2 = planManagedGitignore(current);
20755
21857
  const drift = [...plan2.added.map((l) => `+${l}`), ...plan2.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
20756
21858
  if (opts.write) {
20757
21859
  if (plan2.changed) {
20758
- (0, import_node_fs28.writeFileSync)(path2, plan2.content, "utf8");
21860
+ (0, import_node_fs29.writeFileSync)(path2, plan2.content, "utf8");
20759
21861
  console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
20760
21862
  } else {
20761
21863
  console.log("mmi-cli rules gitignore: up to date");
@@ -20782,7 +21884,7 @@ async function runDocsSync(opts, io = consoleIo) {
20782
21884
  return null;
20783
21885
  }
20784
21886
  },
20785
- localContent: async (f) => (0, import_node_fs28.existsSync)(f) ? await (0, import_promises8.readFile)(f, "utf8") : null,
21887
+ localContent: async (f) => (0, import_node_fs29.existsSync)(f) ? await (0, import_promises8.readFile)(f, "utf8") : null,
20786
21888
  writeDoc: async (f, c) => {
20787
21889
  await (0, import_promises8.writeFile)(f, c, "utf8");
20788
21890
  }
@@ -20796,6 +21898,7 @@ docs.command("sync").option("--quiet", "stay silent unless something changed or
20796
21898
  registerSagaCommands(program2);
20797
21899
  registerHandoffCommands(program2);
20798
21900
  registerCoopCommands(program2);
21901
+ registerOverlordCommands(program2);
20799
21902
  registerThrottleCommands(program2);
20800
21903
  program2.command("commands").description("print the command manifest \u2014 every subcommand + its flags (ground against this instead of guessing)").option("--json", "machine-readable JSON: { name, version, tree, index } \u2014 index is a flat list of every leaf command path").action((o) => {
20801
21904
  const manifest = buildCommandManifest(program2);
@@ -20934,7 +22037,7 @@ function runWorktreeInstall(command, cwd, quiet) {
20934
22037
  const file = isWin2 ? "cmd.exe" : bin;
20935
22038
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
20936
22039
  return new Promise((resolve6, reject) => {
20937
- const child = (0, import_node_child_process13.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
22040
+ const child = (0, import_node_child_process15.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
20938
22041
  const timer = setTimeout(() => {
20939
22042
  try {
20940
22043
  child.kill();
@@ -20956,7 +22059,7 @@ function runWorktreeInstall(command, cwd, quiet) {
20956
22059
  async function primaryCheckoutRoot(worktreeRoot) {
20957
22060
  try {
20958
22061
  const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
20959
- return out ? (0, import_node_path25.dirname)(out) : void 0;
22062
+ return out ? (0, import_node_path26.dirname)(out) : void 0;
20960
22063
  } catch {
20961
22064
  return void 0;
20962
22065
  }
@@ -20969,28 +22072,28 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
20969
22072
  };
20970
22073
  }
20971
22074
  function acquireWorktreeSetupLock(worktreeRoot) {
20972
- const lockPath = (0, import_node_path25.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
22075
+ const lockPath = (0, import_node_path26.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
20973
22076
  const take = () => {
20974
- const fd = (0, import_node_fs28.openSync)(lockPath, "wx");
22077
+ const fd = (0, import_node_fs29.openSync)(lockPath, "wx");
20975
22078
  try {
20976
- (0, import_node_fs28.writeSync)(fd, String(Date.now()));
22079
+ (0, import_node_fs29.writeSync)(fd, String(Date.now()));
20977
22080
  } finally {
20978
- (0, import_node_fs28.closeSync)(fd);
22081
+ (0, import_node_fs29.closeSync)(fd);
20979
22082
  }
20980
22083
  return () => {
20981
22084
  try {
20982
- (0, import_node_fs28.rmSync)(lockPath, { force: true });
22085
+ (0, import_node_fs29.rmSync)(lockPath, { force: true });
20983
22086
  } catch {
20984
22087
  }
20985
22088
  };
20986
22089
  };
20987
22090
  try {
20988
- (0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(lockPath), { recursive: true });
22091
+ (0, import_node_fs29.mkdirSync)((0, import_node_path26.dirname)(lockPath), { recursive: true });
20989
22092
  return take();
20990
22093
  } catch {
20991
22094
  try {
20992
- if (Date.now() - (0, import_node_fs28.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
20993
- (0, import_node_fs28.rmSync)(lockPath, { force: true });
22095
+ if (Date.now() - (0, import_node_fs29.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
22096
+ (0, import_node_fs29.rmSync)(lockPath, { force: true });
20994
22097
  return take();
20995
22098
  }
20996
22099
  } catch {
@@ -21152,7 +22255,7 @@ function scheduleRelatedDiscovery(o) {
21152
22255
  try {
21153
22256
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
21154
22257
  if (o.repo) args.push("--repo", o.repo);
21155
- (0, import_node_child_process13.spawn)(process.execPath, [process.argv[1], ...args], {
22258
+ (0, import_node_child_process15.spawn)(process.execPath, [process.argv[1], ...args], {
21156
22259
  detached: true,
21157
22260
  stdio: "ignore",
21158
22261
  windowsHide: true,
@@ -21201,6 +22304,20 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
21201
22304
  return failGraceful(`tenant control: ${e.message}`);
21202
22305
  }
21203
22306
  });
22307
+ tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints").option("--json", "machine-readable output").action(async (repo, stage2, _o) => {
22308
+ if (!["dev", "rc", "main"].includes(stage2)) return fail("tenant status: <stage> must be dev, rc, or main");
22309
+ const cfg = await loadConfig();
22310
+ const result = await buildTenantRuntimeStatusFor(repo, stage2, cfg);
22311
+ console.log(JSON.stringify(result, null, 2));
22312
+ if (result.publicProbe?.ok === false) process.exitCode = 1;
22313
+ });
22314
+ tenant.command("readiness <owner/repo> <stage>").description("alias for tenant status: read-only tenant runtime readiness").option("--json", "machine-readable output").action(async (repo, stage2, _o) => {
22315
+ if (!["dev", "rc", "main"].includes(stage2)) return fail("tenant readiness: <stage> must be dev, rc, or main");
22316
+ const cfg = await loadConfig();
22317
+ const result = await buildTenantRuntimeStatusFor(repo, stage2, cfg);
22318
+ console.log(JSON.stringify(result, null, 2));
22319
+ if (result.publicProbe?.ok === false) process.exitCode = 1;
22320
+ });
21204
22321
  tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
21205
22322
  if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
21206
22323
  try {
@@ -21243,6 +22360,44 @@ async function resolveDnsBounded(host, timeoutMs = 3e3) {
21243
22360
  });
21244
22361
  return Promise.race([probe, timeout]);
21245
22362
  }
22363
+ async function probeHttpBounded(url, timeoutMs = 5e3) {
22364
+ const controller = new AbortController();
22365
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
22366
+ timeout.unref?.();
22367
+ try {
22368
+ const res = await fetch(url, { method: "GET", signal: controller.signal });
22369
+ return { ok: res.ok, status: res.status };
22370
+ } catch (e) {
22371
+ return { ok: false, error: e.message };
22372
+ } finally {
22373
+ clearTimeout(timeout);
22374
+ }
22375
+ }
22376
+ async function lastWorkflowRun(workflow) {
22377
+ try {
22378
+ const out = await execFileP2("gh", ["run", "list", "--repo", HUB_REPO3, "--workflow", workflow, "--limit", "1", "--json", "databaseId,url,status,conclusion,headBranch,headSha,createdAt"], { timeout: 2e4 });
22379
+ const rows = JSON.parse(out.stdout || "[]");
22380
+ return rows[0];
22381
+ } catch {
22382
+ return void 0;
22383
+ }
22384
+ }
22385
+ async function buildTenantRuntimeStatusFor(target, stage2, cfg) {
22386
+ const slug = slugOf(target);
22387
+ const reg = registryClientDeps(cfg);
22388
+ const facts = await fetchDeployFactsBySlug(slug, reg);
22389
+ const deploy = facts?.stages[stage2] ?? null;
22390
+ const publicUrl = publicUrlFromDeployFact(deploy);
22391
+ const publicProbe = publicUrl ? await probeHttpBounded(publicUrl) : void 0;
22392
+ return buildTenantRuntimeStatus({
22393
+ repo: target,
22394
+ slug,
22395
+ stage: stage2,
22396
+ deploy,
22397
+ publicProbe,
22398
+ lastTenantDeployRun: await lastWorkflowRun("tenant-deploy.yml")
22399
+ });
22400
+ }
21246
22401
  async function v2ReadinessDeps(cfg) {
21247
22402
  const reg = registryClientDeps(cfg);
21248
22403
  return {
@@ -21257,6 +22412,11 @@ async function v2ReadinessDeps(cfg) {
21257
22412
  const status = await fetchDeployStatusBySlug(slug, reg);
21258
22413
  return Boolean(status?.deployState[stage2]);
21259
22414
  },
22415
+ getDeployFact: async (slug, stage2) => {
22416
+ const facts = await fetchDeployFactsBySlug(slug, reg);
22417
+ const fact = facts?.stages[stage2];
22418
+ return fact ? { port: fact.port, domain: fact.domain } : null;
22419
+ },
21260
22420
  listSecrets: async (targetRepo2) => {
21261
22421
  const apiUrl = cfg.sagaApiUrl;
21262
22422
  if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
@@ -21344,7 +22504,7 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
21344
22504
  }
21345
22505
  });
21346
22506
  project.command("resolve <owner/repo>").description("deploy coords for a stage \u2014 for diagnosis. NOTE: /deploy-coords is OIDC-gated (a deploy job\u2019s id-token), so a gh-token CLI cannot read it from a dev machine").option("--stage <main|rc>", "deploy stage", "main").option("--json", "machine-readable output").action((_repoOrRepo, o) => {
21347
- const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect the DEPLOY# item via a master registry (DDB) read instead.";
22507
+ const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect nonsecret DEPLOY facts with `mmi-cli project deploy get`. Full coords stay OIDC-gated.";
21348
22508
  if (o.json) {
21349
22509
  console.log(JSON.stringify({ ok: false, stage: o.stage, error: msg }));
21350
22510
  process.exitCode = 1;
@@ -21352,6 +22512,37 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
21352
22512
  }
21353
22513
  fail(msg);
21354
22514
  });
22515
+ var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
22516
+ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
22517
+ const cfg = await loadConfig();
22518
+ let target;
22519
+ try {
22520
+ target = await projectTarget("project deploy get", repoOrSlug);
22521
+ } catch (e) {
22522
+ return fail(e.message);
22523
+ }
22524
+ const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
22525
+ if (!out) return failGraceful(`project deploy get: Hub deploy facts read failed for ${target}`);
22526
+ const stage2 = o.stage?.trim();
22527
+ if (stage2 && !["dev", "rc", "main"].includes(stage2)) return fail("project deploy get: --stage must be dev, rc, or main");
22528
+ const payload = stage2 ? { slug: out.slug, stage: stage2, deploy: out.stages[stage2] ?? null } : out;
22529
+ console.log(JSON.stringify(payload));
22530
+ });
22531
+ projectDeploy.command("list [owner/repo]").description("alias for project deploy get; lists all stages by default").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
22532
+ const cfg = await loadConfig();
22533
+ let target;
22534
+ try {
22535
+ target = await projectTarget("project deploy list", repoOrSlug);
22536
+ } catch (e) {
22537
+ return fail(e.message);
22538
+ }
22539
+ const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
22540
+ if (!out) return failGraceful(`project deploy list: Hub deploy facts read failed for ${target}`);
22541
+ const stage2 = o.stage?.trim();
22542
+ if (stage2 && !["dev", "rc", "main"].includes(stage2)) return fail("project deploy list: --stage must be dev, rc, or main");
22543
+ const payload = stage2 ? { slug: out.slug, stage: stage2, deploy: out.stages[stage2] ?? null } : out;
22544
+ console.log(JSON.stringify(payload));
22545
+ });
21355
22546
  project.command("doctor [owner/repo]").description("diagnose Hub v2 readiness for a repo without reading product repo files \u2014 appOwnedGaps are advisory templates; clear them with `project attest`; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--json", "machine-readable output").action(async (repo, _o) => {
21356
22547
  const cfg = await loadConfig();
21357
22548
  let target;
@@ -21444,6 +22635,31 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
21444
22635
  const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
21445
22636
  return reportWrite("project set", res);
21446
22637
  });
22638
+ var fullTrack = program2.command("full-track").description("direct-to-full-track readiness audits");
22639
+ fullTrack.command("readiness <owner/repo>").description("aggregate branch topology, train authority, deploy facts, endpoint health, and rcand readiness for direct -> full switches").option("--json", "machine-readable output").action(async (repo, _o) => {
22640
+ const cfg = await loadConfig();
22641
+ const reg = registryClientDeps(cfg);
22642
+ const slug = slugOf(repo);
22643
+ const [metaRead, deployFacts, authority, dev, rc, main] = await Promise.all([
22644
+ fetchProjectBySlugChecked(slug, reg),
22645
+ fetchDeployFactsBySlug(slug, reg),
22646
+ fetchTrainAuthority(repo, reg),
22647
+ buildTenantRuntimeStatusFor(repo, "dev", cfg),
22648
+ buildTenantRuntimeStatusFor(repo, "rc", cfg),
22649
+ buildTenantRuntimeStatusFor(repo, "main", cfg)
22650
+ ]);
22651
+ if (!metaRead.ok) return failGraceful(`full-track readiness: Hub registry read failed (${metaRead.error})`);
22652
+ const report = buildFullTrackReadinessReport({
22653
+ repo,
22654
+ slug,
22655
+ meta: metaRead.project,
22656
+ deployFacts,
22657
+ trainAuthority: authority.ok ? authority.authority : void 0,
22658
+ runtime: { dev, rc, main }
22659
+ });
22660
+ console.log(JSON.stringify(report, null, 2));
22661
+ if (!report.rcand.canApply) process.exitCode = 1;
22662
+ });
21447
22663
  project.command("set-deploy [owner/repo]").description("write the DEPLOY#<stage> Hetzner deploy coords for a tenant (master-only) \u2014 the explicit-coords path that seeds a freshly-bootstrapped tenant; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into (required for hetzner-ssh)").option("--ssh-user <user>", "ssh user (default root)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535)").option("--substrate <substrate>", "hetzner-ssh (default)").option("--deploy-path <path>", "on-box per-stage release root (default /opt/mmi/<slug>/<stage>)").option("--service <name>", "systemd/compose service name (default the slug)").option("--domain <domain>", "canonical serving host (default the project edgeDomains[stage])").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
21448
22664
  const cfg = await loadConfig();
21449
22665
  let target;
@@ -21680,6 +22896,29 @@ issue.command("link-child <parent> <child>").description("link an existing issue
21680
22896
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
21681
22897
  }
21682
22898
  });
22899
+ issue.command("comment <ref>").description("post a Markdown comment to an issue and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
22900
+ let parsed;
22901
+ try {
22902
+ parsed = parseIssueRef(ref);
22903
+ } catch (e) {
22904
+ return fail(`issue comment: ${e.message}`);
22905
+ }
22906
+ let body;
22907
+ try {
22908
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
22909
+ } catch (e) {
22910
+ return fail(`issue comment: ${e.message}`);
22911
+ }
22912
+ const repo = await resolveRepo(parsed.repo ?? o.repo);
22913
+ if (!repo) return fail("issue comment: could not resolve repo \u2014 pass --repo owner/repo");
22914
+ try {
22915
+ const result = await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
22916
+ console.log(JSON.stringify(result));
22917
+ } catch (e) {
22918
+ const err = e;
22919
+ return fail(`issue comment: ${(err.stderr || err.message || String(e)).trim()}`);
22920
+ }
22921
+ });
21683
22922
  issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2192 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
21684
22923
  let parsed;
21685
22924
  try {
@@ -21994,9 +23233,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
21994
23233
  console.log(JSON.stringify(created));
21995
23234
  });
21996
23235
  async function listCiWorkflowPaths(cwd = process.cwd()) {
21997
- const wfDir = (0, import_node_path25.join)(cwd, ".github", "workflows");
21998
- if (!(0, import_node_fs28.existsSync)(wfDir)) return [];
21999
- return (0, import_node_fs28.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
23236
+ const wfDir = (0, import_node_path26.join)(cwd, ".github", "workflows");
23237
+ if (!(0, import_node_fs29.existsSync)(wfDir)) return [];
23238
+ return (0, import_node_fs29.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
22000
23239
  }
22001
23240
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
22002
23241
  const repo = repoOpt ?? await resolveRepo();
@@ -22015,7 +23254,7 @@ function ciAuditDeps() {
22015
23254
  // Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
22016
23255
  // reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
22017
23256
  // the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
22018
- readSeedFile: (path2) => (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null
23257
+ readSeedFile: (path2) => (0, import_node_fs29.existsSync)(path2) ? (0, import_node_fs29.readFileSync)(path2, "utf8") : null
22019
23258
  };
22020
23259
  }
22021
23260
  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) => {
@@ -22152,7 +23391,7 @@ async function remoteBranchExists2(branch, options = {}) {
22152
23391
  }
22153
23392
  var COMPOSE_TIMEOUT_MS = 12e4;
22154
23393
  function spawnDeferredGcSweep() {
22155
- spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
23394
+ spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
22156
23395
  }
22157
23396
  async function createDeferredWorktreeStore() {
22158
23397
  try {
@@ -22168,7 +23407,7 @@ async function createDeferredWorktreeStore() {
22168
23407
  },
22169
23408
  write: async (entries) => {
22170
23409
  try {
22171
- await (0, import_promises8.mkdir)((0, import_node_path25.dirname)(registryPath), { recursive: true });
23410
+ await (0, import_promises8.mkdir)((0, import_node_path26.dirname)(registryPath), { recursive: true });
22172
23411
  await (0, import_promises8.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
22173
23412
  } catch {
22174
23413
  }
@@ -22182,13 +23421,13 @@ var realWorktreeDirRemover = {
22182
23421
  probe: (p) => {
22183
23422
  let st;
22184
23423
  try {
22185
- st = (0, import_node_fs28.lstatSync)(p);
23424
+ st = (0, import_node_fs29.lstatSync)(p);
22186
23425
  } catch {
22187
23426
  return null;
22188
23427
  }
22189
23428
  if (st.isSymbolicLink()) return "link";
22190
23429
  try {
22191
- (0, import_node_fs28.readlinkSync)(p);
23430
+ (0, import_node_fs29.readlinkSync)(p);
22192
23431
  return "link";
22193
23432
  } catch {
22194
23433
  }
@@ -22196,7 +23435,7 @@ var realWorktreeDirRemover = {
22196
23435
  },
22197
23436
  readdir: (p) => {
22198
23437
  try {
22199
- return (0, import_node_fs28.readdirSync)(p);
23438
+ return (0, import_node_fs29.readdirSync)(p);
22200
23439
  } catch {
22201
23440
  return [];
22202
23441
  }
@@ -22205,9 +23444,9 @@ var realWorktreeDirRemover = {
22205
23444
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
22206
23445
  detachLink: (p) => {
22207
23446
  try {
22208
- (0, import_node_fs28.rmdirSync)(p);
23447
+ (0, import_node_fs29.rmdirSync)(p);
22209
23448
  } catch {
22210
- (0, import_node_fs28.unlinkSync)(p);
23449
+ (0, import_node_fs29.unlinkSync)(p);
22211
23450
  }
22212
23451
  },
22213
23452
  removeTree: (p) => (0, import_promises8.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -22237,9 +23476,9 @@ async function worktreeHasStageState(worktreePath) {
22237
23476
  }
22238
23477
  }
22239
23478
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
22240
- if (!(0, import_node_fs28.existsSync)(statePath)) return false;
23479
+ if (!(0, import_node_fs29.existsSync)(statePath)) return false;
22241
23480
  try {
22242
- const state = JSON.parse((0, import_node_fs28.readFileSync)(statePath, "utf8"));
23481
+ const state = JSON.parse((0, import_node_fs29.readFileSync)(statePath, "utf8"));
22243
23482
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
22244
23483
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
22245
23484
  } catch {
@@ -22312,7 +23551,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
22312
23551
  } : await cleanupPrMergeLocalBranch(headRef, {
22313
23552
  beforeWorktrees,
22314
23553
  startingPath,
22315
- pathExists: (p) => (0, import_node_fs28.existsSync)(p),
23554
+ pathExists: (p) => (0, import_node_fs29.existsSync)(p),
22316
23555
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
22317
23556
  teardownWorktreeStage,
22318
23557
  deferredStore,
@@ -22509,7 +23748,7 @@ function stageScopedRunOpts(o) {
22509
23748
  };
22510
23749
  }
22511
23750
  function printLine(value) {
22512
- (0, import_node_fs28.writeSync)(1, `${value}
23751
+ (0, import_node_fs29.writeSync)(1, `${value}
22513
23752
  `);
22514
23753
  }
22515
23754
  function stageKeepAlive() {
@@ -22526,8 +23765,8 @@ async function resolveStage() {
22526
23765
  local,
22527
23766
  shell: shellFor(),
22528
23767
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
22529
- hasCompose: (0, import_node_fs28.existsSync)((0, import_node_path25.join)(process.cwd(), "docker-compose.yml")),
22530
- hasEnvExample: (0, import_node_fs28.existsSync)((0, import_node_path25.join)(process.cwd(), ".env.example"))
23768
+ hasCompose: (0, import_node_fs29.existsSync)((0, import_node_path26.join)(process.cwd(), "docker-compose.yml")),
23769
+ hasEnvExample: (0, import_node_fs29.existsSync)((0, import_node_path26.join)(process.cwd(), ".env.example"))
22531
23770
  });
22532
23771
  }
22533
23772
  async function fetchStageVaultEnvMerge() {
@@ -22851,7 +24090,7 @@ async function resolveRcandPlanTargets() {
22851
24090
  }
22852
24091
  }
22853
24092
  for (const commandName of ["rcand", "release"]) {
22854
- program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").option("--dev", "release only: full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)").action(async (o) => {
24093
+ program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").option("--dev", "release only: full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)").option("--repo <owner/repo>", "dry-run plan for a target repo without relying on the current checkout; --apply still uses the current checkout").action(async (o) => {
22855
24094
  try {
22856
24095
  await requireFreshTrainCli(commandName);
22857
24096
  } catch (e) {
@@ -22863,6 +24102,9 @@ for (const commandName of ["rcand", "release"]) {
22863
24102
  if (o.dev && commandName !== "release") {
22864
24103
  return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
22865
24104
  }
24105
+ if (o.apply && o.repo) {
24106
+ return fail(`${commandName}: --repo is read-only for dry-run planning; --apply must run from the target repo checkout`);
24107
+ }
22866
24108
  if (o.apply) {
22867
24109
  try {
22868
24110
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -22872,8 +24114,8 @@ for (const commandName of ["rcand", "release"]) {
22872
24114
  return failGraceful(`${commandName}: ${e.message}`);
22873
24115
  }
22874
24116
  }
22875
- const repo = await resolveRepo();
22876
- const targets = commandName === "rcand" ? await resolveRcandPlanTargets() : void 0;
24117
+ const repo = o.repo ?? await resolveRepo();
24118
+ const targets = commandName === "rcand" && !o.repo ? await resolveRcandPlanTargets() : void 0;
22877
24119
  let releaseTrack;
22878
24120
  if (repo) {
22879
24121
  try {
@@ -22989,7 +24231,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
22989
24231
  client: defaultGitHubClient(),
22990
24232
  projectMeta: meta,
22991
24233
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
22992
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null,
24234
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs29.existsSync)(path2) ? (0, import_node_fs29.readFileSync)(path2, "utf8") : null,
22993
24235
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
22994
24236
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
22995
24237
  requiredGcpApis: (() => {
@@ -23033,12 +24275,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
23033
24275
  return fail(`bootstrap apply: ${e.message}`);
23034
24276
  }
23035
24277
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
23036
- if (!(0, import_node_fs28.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
23037
- const manifest = loadBootstrapSeeds((0, import_node_fs28.readFileSync)(manifestPath, "utf8"));
24278
+ if (!(0, import_node_fs29.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
24279
+ const manifest = loadBootstrapSeeds((0, import_node_fs29.readFileSync)(manifestPath, "utf8"));
23038
24280
  const baseBranch = o.class === "content" ? "main" : "development";
23039
24281
  const slug = parsedRepo.slug;
23040
24282
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
23041
- const readFile7 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
24283
+ const readFile7 = (p) => (0, import_node_fs29.existsSync)(p) ? (0, import_node_fs29.readFileSync)(p, "utf8") : null;
23042
24284
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
23043
24285
  const rawVars = {};
23044
24286
  for (const value of rawValues("--var")) {
@@ -23287,18 +24529,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
23287
24529
  const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
23288
24530
  if (o.repo) {
23289
24531
  if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
23290
- targets = [{ repo: o.repo, class: o.class }];
24532
+ const meta = registryProjects?.find((project2) => (project2.repos ?? []).some((repo) => repo.toLowerCase() === o.repo.toLowerCase())) ?? null;
24533
+ const repoClass = o.class;
24534
+ targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
23291
24535
  } else {
23292
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs28.existsSync)("projects.json") ? (0, import_node_fs28.readFileSync)("projects.json", "utf8") : null;
24536
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs29.existsSync)("projects.json") ? (0, import_node_fs29.readFileSync)("projects.json", "utf8") : null;
23293
24537
  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>");
23294
- const fanoutJson = (0, import_node_fs28.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs28.readFileSync)(".github/fanout-targets.json", "utf8") : null;
24538
+ const fanoutJson = (0, import_node_fs29.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs29.readFileSync)(".github/fanout-targets.json", "utf8") : null;
23295
24539
  targets = loadAccessTargets(projectsJson, fanoutJson);
23296
24540
  }
23297
24541
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
23298
- const fileMatrix = (0, import_node_fs28.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs28.readFileSync)("access-matrix.json", "utf8")) : {};
24542
+ const fileMatrix = (0, import_node_fs29.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs29.readFileSync)("access-matrix.json", "utf8")) : {};
23299
24543
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
23300
24544
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
23301
- const fileContracts = (0, import_node_fs28.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs28.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
24545
+ const fileContracts = (0, import_node_fs29.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs29.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
23302
24546
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
23303
24547
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
23304
24548
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -23399,7 +24643,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
23399
24643
  } catch (e) {
23400
24644
  console.error(`[mmi-hook] saga session failed: ${e.message}`);
23401
24645
  }
23402
- spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24646
+ spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
23403
24647
  spawnDeferredGcSweep();
23404
24648
  let northstarInjected = false;
23405
24649
  const { parallel, sequential } = buildSessionStartPlan({
@@ -23441,7 +24685,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
23441
24685
  readBoard,
23442
24686
  // #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
23443
24687
  // never costs banner time and next session's glance renders instantly within budget.
23444
- scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
24688
+ scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
23445
24689
  }),
23446
24690
  spineReconcile: async (io) => {
23447
24691
  const cfg = await loadConfig();
@@ -23465,7 +24709,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
23465
24709
  for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
23466
24710
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
23467
24711
  if (worktreeBanner) {
23468
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24712
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
23469
24713
  consoleIo.log(worktreeBanner);
23470
24714
  }
23471
24715
  });