@mutmutco/cli 2.55.0 → 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
@@ -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,
@@ -9973,6 +9973,7 @@ function atomicWriteFileSync(path2, content) {
9973
9973
  var OVERLORD_DEFAULT_COUNT = 3;
9974
9974
  var OVERLORD_MIN_COUNT = 3;
9975
9975
  var OVERLORD_MAX_COUNT = 6;
9976
+ var OVERLORD_HANDOFF_TIMEOUT_MS = 12e4;
9976
9977
  var GENERIC_STOP_NAMES = /* @__PURE__ */ new Set([
9977
9978
  "windowsterminal",
9978
9979
  "windows terminal",
@@ -10001,6 +10002,17 @@ function defaultMessageId() {
10001
10002
  function servantSlotId(slot) {
10002
10003
  return slot.name.replace(/\s+/g, "-").toLowerCase();
10003
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
+ }
10004
10016
  function parseOverlordCount(args) {
10005
10017
  const counts = args.map(numericCountArg).filter((n) => n !== void 0);
10006
10018
  if (counts.length === 0) return OVERLORD_DEFAULT_COUNT;
@@ -10057,6 +10069,64 @@ ${modelCatalog}`)) problems.push("fugu-ultra is not available");
10057
10069
  }
10058
10070
  return { ok: problems.length === 0, problems };
10059
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
+ }
10060
10130
  function defaultOverlordStatePath(cwd) {
10061
10131
  return (0, import_node_path13.join)(cwd, "tmp", "overlord", "runs.json");
10062
10132
  }
@@ -10079,6 +10149,7 @@ function buildOverlordRun(options) {
10079
10149
  const runToken = options.runToken?.() ?? defaultRunToken();
10080
10150
  const statePath = defaultOverlordStatePath(options.cwd);
10081
10151
  const journalDir = (0, import_node_path13.join)(options.cwd, "tmp", "overlord", runId);
10152
+ const engine = options.engine ?? "codex-fugu";
10082
10153
  const timestamp = isoNow(options.now);
10083
10154
  return {
10084
10155
  runId,
@@ -10090,13 +10161,21 @@ function buildOverlordRun(options) {
10090
10161
  worktree: options.cwd,
10091
10162
  statePath,
10092
10163
  journalDir,
10164
+ ledgerPath: (0, import_node_path13.join)(journalDir, "ledger.jsonl"),
10165
+ engine,
10166
+ provider: engine === "opencode" ? "opencode" : "codex",
10167
+ opencodeVersion: options.opencodeVersion,
10093
10168
  servants: buildServantLayout(options.count).map((slot) => ({
10094
10169
  ...slot,
10095
10170
  slotId: servantSlotId(slot),
10096
10171
  profile: "consultation",
10097
10172
  state: "planned",
10098
10173
  journalPath: (0, import_node_path13.join)(journalDir, `${servantSlotId(slot)}.log`),
10099
- composerSubmitMode: "unknown"
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
10100
10179
  })),
10101
10180
  ownedResources: []
10102
10181
  };
@@ -10145,14 +10224,18 @@ function planStopResource(resource, context) {
10145
10224
  }
10146
10225
  function summarizeOverlordRun(run, probe) {
10147
10226
  const controller = run.controllerPid == null ? "not-started" : probe.isPidAlive(run.controllerPid) ? "alive" : "lost";
10227
+ const now = probe.now?.() ?? /* @__PURE__ */ new Date();
10148
10228
  return {
10149
10229
  active: run.state !== "stopped" && run.state !== "failed",
10150
10230
  runId: run.runId,
10151
10231
  state: run.state,
10152
10232
  controller,
10153
10233
  servants: run.servants.map((servant) => {
10234
+ if (run.engine === "opencode" || servant.engine === "opencode") return { name: servant.name, state: servantProgress(run, servant, now) };
10154
10235
  if (servant.pid == null) return { name: servant.name, state: "not-started" };
10155
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 };
10156
10239
  if (servant.state === "ready" && servant.lastAckAt && servant.composerSubmitMode !== "unknown") {
10157
10240
  return { name: servant.name, state: "ready" };
10158
10241
  }
@@ -10185,6 +10268,37 @@ function defaultStartController(run) {
10185
10268
  child.unref();
10186
10269
  return { pid: child.pid, fingerprint };
10187
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
+ }
10188
10302
  function defaultIsPidAlive(pid) {
10189
10303
  if (!Number.isFinite(pid) || pid <= 0) return false;
10190
10304
  try {
@@ -10232,6 +10346,15 @@ function hasCodexAuthEvidence() {
10232
10346
  const codexHome = process.env.CODEX_HOME ?? (0, import_node_path13.join)((0, import_node_os3.homedir)(), ".codex");
10233
10347
  return (0, import_node_fs15.existsSync)((0, import_node_path13.join)(codexHome, "auth.json"));
10234
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
+ }
10235
10358
  function collectCodexFuguPreflight() {
10236
10359
  const codexHelp = boundedCommandText("codex", ["--help"]);
10237
10360
  const fuguHelp = boundedCommandText("codex-fugu", ["--help"]);
@@ -10276,6 +10399,57 @@ function renderCodexFuguPreflightFailure(report) {
10276
10399
  }
10277
10400
  return lines.join("\n");
10278
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
+ }
10279
10453
  function findActiveRun(registry2) {
10280
10454
  const runId = registry2.activeRunId;
10281
10455
  if (!runId) return void 0;
@@ -10290,17 +10464,42 @@ function hasServantTarget(run, target) {
10290
10464
  (servant) => servant.slotId === normalized || normalizeServantTarget(servant.name) === normalized
10291
10465
  );
10292
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
+ }
10293
10479
  function renderOverlordStatus(summary, run) {
10294
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");
10295
10482
  return [
10296
10483
  `Overlord run ${summary.runId}`,
10297
10484
  `State: ${summary.state}`,
10298
10485
  `Task: ${run.task || "not provided yet"}`,
10299
10486
  `Controller: ${summary.controller}`,
10300
10487
  "Servants:",
10301
- servants
10488
+ servants,
10489
+ ...messages ? ["Messages:", messages] : []
10302
10490
  ].join("\n");
10303
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
+ }
10304
10503
  function runJson(run, extra = {}) {
10305
10504
  return {
10306
10505
  ok: true,
@@ -10308,15 +10507,19 @@ function runJson(run, extra = {}) {
10308
10507
  state: run.state,
10309
10508
  task: run.task,
10310
10509
  count: run.servants.length,
10510
+ engine: run.engine,
10311
10511
  controllerPid: run.controllerPid,
10312
10512
  statePath: run.statePath,
10513
+ ledgerPath: run.ledgerPath,
10313
10514
  servants: run.servants.map((servant) => ({
10314
10515
  name: servant.name,
10315
10516
  role: servant.role,
10316
10517
  model: servant.model,
10317
10518
  state: servant.state,
10318
10519
  pid: servant.pid,
10319
- journalPath: servant.journalPath
10520
+ journalPath: servant.journalPath,
10521
+ engine: servant.engine,
10522
+ opencodeSessionId: servant.opencodeSessionId
10320
10523
  })),
10321
10524
  ...extra
10322
10525
  };
@@ -10333,15 +10536,19 @@ function registerOverlordCommands(program3, deps = {}) {
10333
10536
  const cwd = deps.cwd ?? (() => process.cwd());
10334
10537
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
10335
10538
  const preflight2 = deps.preflight ?? collectCodexFuguPreflight;
10539
+ const opencodePreflight = deps.opencodePreflight ?? collectOpenCodePreflight;
10336
10540
  const startController = deps.startController ?? defaultStartController;
10541
+ const runOpenCode = deps.runOpenCode ?? defaultRunOpenCode;
10337
10542
  const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
10338
10543
  const killPid = deps.killPid ?? defaultKillPid;
10339
- 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("--json", "print machine-readable output").action((task = [], options) => {
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) => {
10340
10545
  try {
10341
10546
  const args = [...countArgsFromOptions(options), ...task];
10342
10547
  const root = cwd();
10343
10548
  const plan2 = buildOverlordStartupPlan(args, root);
10344
- const preflightReport = preflight2();
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();
10345
10552
  if (!preflightReport.ok) {
10346
10553
  err(`${renderCodexFuguPreflightFailure(preflightReport)}
10347
10554
  `);
@@ -10357,19 +10564,25 @@ function registerOverlordCommands(program3, deps = {}) {
10357
10564
  task: plan2.task,
10358
10565
  cwd: root,
10359
10566
  count: plan2.count,
10567
+ engine,
10360
10568
  now,
10361
10569
  runId: deps.runId,
10362
10570
  runToken: deps.runToken
10363
10571
  });
10364
10572
  writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
10365
- const controller = startController(run);
10366
- if (controller.pid != null) {
10367
- run = recordOverlordHeartbeat(run, {
10368
- controllerPid: controller.pid,
10369
- fingerprint: controller.fingerprint,
10370
- now
10371
- });
10573
+ if (engine === "opencode") {
10574
+ run = startOpenCodeServants(run, runOpenCode, now);
10372
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
+ }
10373
10586
  }
10374
10587
  if (options.json) out(`${JSON.stringify(runJson(run, { nextPhase: "consult-servants" }), null, 2)}
10375
10588
  `);
@@ -10404,15 +10617,32 @@ function registerOverlordCommands(program3, deps = {}) {
10404
10617
  if (!hasServantTarget(run, normalized)) throw new Error(`unknown Overlord servant target: ${target}`);
10405
10618
  const text = message.join(" ").trim();
10406
10619
  if (!text) throw new Error("message is required");
10620
+ const timestamp = isoNow(now);
10407
10621
  const queued = {
10408
10622
  id: deps.runId?.() ?? defaultMessageId(),
10409
10623
  target: normalized,
10410
10624
  text,
10411
- createdAt: isoNow(now)
10625
+ createdAt: timestamp,
10626
+ queuedAt: timestamp,
10627
+ state: "queued"
10412
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
+ }
10413
10643
  const next = {
10414
10644
  ...run,
10415
- updatedAt: isoNow(now),
10645
+ updatedAt: timestamp,
10416
10646
  messages: [...run.messages ?? [], queued]
10417
10647
  };
10418
10648
  writeOverlordRegistry(statePath, {
@@ -10448,12 +10678,74 @@ State: ${payload2.statePath}
10448
10678
  `);
10449
10679
  return;
10450
10680
  }
10451
- const summary = summarizeOverlordRun(run, { isPidAlive });
10452
- const payload = { ...summary, statePath, task: run.task };
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
+ };
10453
10712
  if (json) out(`${JSON.stringify(payload, null, 2)}
10454
10713
  `);
10455
10714
  else out(`${renderOverlordStatus(summary, run)}
10456
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}
10457
10749
  `);
10458
10750
  });
10459
10751
  overlord.command("stop").description("stop only exact run-owned Overlord resources").option("--json", "print machine-readable output").action((options, command) => {
@@ -12776,14 +13068,14 @@ async function auditRepoCi(repo, deps) {
12776
13068
  if (deployableGated) {
12777
13069
  checks.push({
12778
13070
  ok: hasGateWorkflow,
12779
- label: "gate workflow committed",
12780
- 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}`,
12781
13073
  remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute (seeds gate.yml)`
12782
13074
  });
12783
13075
  checks.push({
12784
13076
  ok: await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF),
12785
- label: "product ruleset reference committed",
12786
- detail: `expected ${PRODUCT_RULESET_REF}`,
13077
+ label: `product ruleset reference committed on ${baseBranch}`,
13078
+ detail: `read ${PRODUCT_RULESET_REF} at refs/heads/${baseBranch}`,
12787
13079
  remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute`
12788
13080
  });
12789
13081
  }
@@ -15483,7 +15775,8 @@ var OVERGRANT_ROLES = /* @__PURE__ */ new Set(["admin", "maintain"]);
15483
15775
  var REQUIRED_DATA_ACCESS = {
15484
15776
  "mutmutco/MM-Chat": [{ name: "kb-projection-reader", dbRole: "kb_reader", vaultParamNeedle: "KB_READ_DB_URL" }]
15485
15777
  };
15486
- function lockedBranches(repoClass) {
15778
+ function lockedBranches(repoClass, releaseTrack) {
15779
+ if (releaseTrack) return branchesForTrack(releaseTrack);
15487
15780
  return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
15488
15781
  }
15489
15782
  function safeJson(text, fallback) {
@@ -15610,14 +15903,15 @@ function auditDataAccessContracts(repo, contracts = { consumers: {} }) {
15610
15903
  }
15611
15904
  return findings;
15612
15905
  }
15613
- 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) {
15614
15907
  const findings = [];
15615
15908
  findings.push(...await auditRepoCollaborators(repo, owners, deps));
15616
15909
  if (dataAccess) findings.push(...auditDataAccessContracts(repo, dataAccess));
15617
- for (const branch of lockedBranches(repoClass)) {
15910
+ const track = releaseTrack ?? (repoClass === "content" ? "trunk" : void 0);
15911
+ for (const branch of lockedBranches(repoClass, track)) {
15618
15912
  findings.push(...await auditTrainBranch(repo, branch, owners, deps, projectAdmins));
15619
15913
  }
15620
- 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 };
15621
15915
  }
15622
15916
  async function auditOrgBasePermission(deps) {
15623
15917
  const org = await restJson2(deps, `orgs/${OWNER}`, {});
@@ -15638,7 +15932,7 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
15638
15932
  const orgFindings = await auditOrgBasePermission(deps);
15639
15933
  const repos = [];
15640
15934
  for (const target of targets) {
15641
- 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));
15642
15936
  }
15643
15937
  const ok = orgFindings.every((f) => f.severity !== "high") && repos.every((r) => r.ok);
15644
15938
  return { ok, owners: [...owners], orgFindings, repos };
@@ -15658,7 +15952,8 @@ function loadAccessTargets(projectsJson, fanoutJson) {
15658
15952
  seen.add(repo);
15659
15953
  const repoName = repo.split("/").pop()?.toLowerCase() ?? repo.toLowerCase();
15660
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";
15661
- targets.push({ repo, class: cls });
15955
+ const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
15956
+ targets.push({ repo, class: cls, releaseTrack });
15662
15957
  }
15663
15958
  }
15664
15959
  return targets;
@@ -15708,7 +16003,7 @@ function renderAccessReport(report) {
15708
16003
  if (finding.remediation) lines.push(` ${finding.remediation}`);
15709
16004
  }
15710
16005
  for (const repo of report.repos) {
15711
- 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}` : ""})`);
15712
16007
  for (const finding of repo.findings) {
15713
16008
  lines.push(` [${finding.severity}] ${finding.kind}${finding.branch ? ` @${finding.branch}` : ""}: ${finding.detail}`);
15714
16009
  if (finding.remediation) lines.push(` ${finding.remediation}`);
@@ -16254,6 +16549,23 @@ async function fetchDeployStatusBySlug(slug, deps) {
16254
16549
  return null;
16255
16550
  }
16256
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
+ }
16257
16569
  async function fetchOrgConfig(deps) {
16258
16570
  if (!deps.baseUrl) return null;
16259
16571
  const token = await deps.token();
@@ -16427,7 +16739,7 @@ function attestedLine(att) {
16427
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.`;
16428
16740
  }
16429
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: [] }).";
16430
- function appGapsFor(meta, model, slug, projectType) {
16742
+ function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
16431
16743
  const attested = appAttestationOf(meta);
16432
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";
16433
16745
  const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
@@ -16479,8 +16791,12 @@ function appGapsFor(meta, model, slug, projectType) {
16479
16791
  if (slug === "mmi-katip") {
16480
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.");
16481
16793
  }
16482
- if ((model === "solo-container" || model === "tenant-container") && typeof meta?.portRange?.start === "number") {
16483
- 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
+ }
16484
16800
  }
16485
16801
  if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
16486
16802
  return gaps;
@@ -16525,7 +16841,7 @@ function sameNames(a, b) {
16525
16841
  function sameStageContract(a, b) {
16526
16842
  return STAGES.every((stage2) => sameNames(a[stage2], b[stage2]));
16527
16843
  }
16528
- function buildV2HealPatch(repoOrSlug, meta) {
16844
+ function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
16529
16845
  const slug = slugOfRepo(repoOrSlug);
16530
16846
  const repo = repoFrom(repoOrSlug, slug);
16531
16847
  const sub = defaultSubdomain(slug);
@@ -16563,7 +16879,7 @@ function buildV2HealPatch(repoOrSlug, meta) {
16563
16879
  patch.requiredRuntimeSecrets = next;
16564
16880
  }
16565
16881
  }
16566
- 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).`];
16567
16883
  if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
16568
16884
  return { slug, patch, appOwnedGaps };
16569
16885
  }
@@ -16608,7 +16924,8 @@ async function buildV2Doctor(repoOrSlug, deps) {
16608
16924
  const meta = read.project;
16609
16925
  const projectType = resolveProjectType(meta, repo);
16610
16926
  const model = resolveDeployModel(meta, repo);
16611
- const autoHeal = buildV2HealPatch(repo, meta);
16927
+ const mainDeployFact = meta && deps.getDeployFact ? await deps.getDeployFact(slug, "main") : null;
16928
+ const autoHeal = buildV2HealPatch(repo, meta, mainDeployFact);
16612
16929
  if (!meta) {
16613
16930
  const emptySecrets = {
16614
16931
  dev: { required: [], present: [], missing: [] },
@@ -16722,6 +17039,152 @@ function renderReadinessIssueBody(existingBody, report, opts = {}) {
16722
17039
  ${section}`.trim();
16723
17040
  }
16724
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
+
16725
17188
  // src/project-set.ts
16726
17189
  var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "dashboard", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
16727
17190
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
@@ -16843,7 +17306,7 @@ function parseOauthVar(raw) {
16843
17306
  try {
16844
17307
  parsed = JSON.parse(raw);
16845
17308
  } catch {
16846
- 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}"}`);
16847
17310
  }
16848
17311
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
16849
17312
  throw new Error("project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
@@ -16858,7 +17321,11 @@ function parseOauthVar(raw) {
16858
17321
  out[key] = value.map((v) => v.trim());
16859
17322
  } else if (key === "callbackPath") {
16860
17323
  if (typeof value !== "string" || !value.trim()) throw new Error("project set: oauth.callbackPath must be a non-empty string");
16861
- 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;
16862
17329
  } else if (key === "fofuSubdomain") {
16863
17330
  if (typeof value !== "string") throw new Error('project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
16864
17331
  out.fofuSubdomain = value.trim();
@@ -17640,6 +18107,11 @@ async function secretsList(deps, opts) {
17640
18107
  const { secrets } = await res.json();
17641
18108
  deps.log(formatSecretList(secrets ?? []));
17642
18109
  }
18110
+ var CAPABILITIES_TIMEOUT_MS = 2e4;
18111
+ function isTimeoutError(e) {
18112
+ const name = e?.name;
18113
+ return name === "TimeoutError" || name === "AbortError";
18114
+ }
17643
18115
  function formatCapabilities(r) {
17644
18116
  const head = `@${r.login} on ${r.repo} \u2014 ${r.role}`;
17645
18117
  const items = [...r.capabilities ?? []].sort((a, b) => a.scope.localeCompare(b.scope));
@@ -17670,10 +18142,15 @@ async function secretsCapabilities(deps, opts) {
17670
18142
  res = await deps.fetch(`${deps.apiUrl}/secrets/capabilities?${qs}`, {
17671
18143
  method: "GET",
17672
18144
  headers: await deps.headers(),
17673
- signal: AbortSignal.timeout(TIMEOUT_MS2)
18145
+ signal: AbortSignal.timeout(CAPABILITIES_TIMEOUT_MS)
17674
18146
  });
17675
18147
  } catch (e) {
17676
- 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}`);
17677
18154
  return;
17678
18155
  }
17679
18156
  if (!res.ok) {
@@ -18297,96 +18774,6 @@ function registerEdgeCommands(program3) {
18297
18774
  });
18298
18775
  }
18299
18776
 
18300
- // src/oauth.ts
18301
- var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
18302
- var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
18303
- var ENV_PREFIXES = ["", "dev", "rc"];
18304
- var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
18305
- var SSM_ENVS = ["dev", "rc", "main"];
18306
- var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
18307
- var uniq = (xs) => [...new Set(xs)];
18308
- function defaultSubdomain2(slug) {
18309
- const i = slug.indexOf("-");
18310
- return i === -1 ? slug : slug.slice(i + 1);
18311
- }
18312
- function expectedHosts(cfg) {
18313
- const out = [];
18314
- for (const sub of cfg.subdomains) {
18315
- for (const domain of cfg.domains) {
18316
- const base = sub ? `${sub}.${domain}` : domain;
18317
- for (const env of ENV_PREFIXES) out.push(env ? `${env}.${base}` : base);
18318
- }
18319
- }
18320
- if (cfg.fofuSubdomain !== void 0) {
18321
- out.push(cfg.fofuSubdomain ? `${cfg.fofuSubdomain}.fofu.ai` : "fofu.ai");
18322
- }
18323
- return uniq(out);
18324
- }
18325
- function expectedJsOrigins(cfg) {
18326
- return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
18327
- }
18328
- function expectedRedirectUris(cfg) {
18329
- const { callbackPath } = cfg;
18330
- return uniq([
18331
- ...expectedHosts(cfg).map((h) => `https://${h}${callbackPath}`),
18332
- ...LOOPBACK.map((l) => `${l}${callbackPath}`)
18333
- ]);
18334
- }
18335
- function oauthSsmKeys() {
18336
- return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
18337
- }
18338
- function parseOauthClientJson(input) {
18339
- let parsed;
18340
- try {
18341
- parsed = JSON.parse(input);
18342
- } catch {
18343
- throw new Error('not valid JSON \u2014 pipe the Google client JSON (the Console "Download JSON" file)');
18344
- }
18345
- const root = parsed ?? {};
18346
- const obj = root.web ?? root.installed ?? parsed;
18347
- const clientId = typeof obj?.client_id === "string" ? obj.client_id.trim() : "";
18348
- const clientSecret = typeof obj?.client_secret === "string" ? obj.client_secret.trim() : "";
18349
- if (!clientId || !clientSecret) {
18350
- throw new Error("missing client_id or client_secret in the JSON");
18351
- }
18352
- return { clientId, clientSecret };
18353
- }
18354
- function parseOauthConfig(mmiConfig, slug) {
18355
- const rawUnknown = mmiConfig?.oauth;
18356
- if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
18357
- if (!rawUnknown || typeof rawUnknown !== "object" || Array.isArray(rawUnknown)) {
18358
- throw new Error("oauth must be an object when configured");
18359
- }
18360
- const raw = rawUnknown;
18361
- const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
18362
- const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
18363
- const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
18364
- if (!callbackPath.startsWith("/")) {
18365
- throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
18366
- }
18367
- const meta = mmiConfig ?? {};
18368
- const rawFofuSub = raw.fofuSubdomain;
18369
- const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
18370
- return { subdomains, domains, callbackPath, fofuSubdomain };
18371
- }
18372
- function probeRedirectUri(callbackPath, port = 9123) {
18373
- return `http://localhost:${port}${callbackPath}`;
18374
- }
18375
- function buildAuthorizeProbeUrl(clientId, redirectUri) {
18376
- const qs = new URLSearchParams({
18377
- client_id: clientId,
18378
- redirect_uri: redirectUri,
18379
- response_type: "code",
18380
- scope: "openid email",
18381
- access_type: "offline",
18382
- prompt: "consent"
18383
- });
18384
- return `https://accounts.google.com/o/oauth2/v2/auth?${qs.toString()}`;
18385
- }
18386
- function authorizeBodyHasMismatch(body) {
18387
- return /redirect_uri_mismatch/i.test(body);
18388
- }
18389
-
18390
18777
  // src/doctor-run.ts
18391
18778
  var import_node_fs28 = require("node:fs");
18392
18779
  var import_node_child_process14 = require("node:child_process");
@@ -21917,6 +22304,20 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
21917
22304
  return failGraceful(`tenant control: ${e.message}`);
21918
22305
  }
21919
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
+ });
21920
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) => {
21921
22322
  if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
21922
22323
  try {
@@ -21959,6 +22360,44 @@ async function resolveDnsBounded(host, timeoutMs = 3e3) {
21959
22360
  });
21960
22361
  return Promise.race([probe, timeout]);
21961
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
+ }
21962
22401
  async function v2ReadinessDeps(cfg) {
21963
22402
  const reg = registryClientDeps(cfg);
21964
22403
  return {
@@ -21973,6 +22412,11 @@ async function v2ReadinessDeps(cfg) {
21973
22412
  const status = await fetchDeployStatusBySlug(slug, reg);
21974
22413
  return Boolean(status?.deployState[stage2]);
21975
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
+ },
21976
22420
  listSecrets: async (targetRepo2) => {
21977
22421
  const apiUrl = cfg.sagaApiUrl;
21978
22422
  if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
@@ -22060,7 +22504,7 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
22060
22504
  }
22061
22505
  });
22062
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) => {
22063
- 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.";
22064
22508
  if (o.json) {
22065
22509
  console.log(JSON.stringify({ ok: false, stage: o.stage, error: msg }));
22066
22510
  process.exitCode = 1;
@@ -22068,6 +22512,37 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
22068
22512
  }
22069
22513
  fail(msg);
22070
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
+ });
22071
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) => {
22072
22547
  const cfg = await loadConfig();
22073
22548
  let target;
@@ -22160,6 +22635,31 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
22160
22635
  const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
22161
22636
  return reportWrite("project set", res);
22162
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
+ });
22163
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) => {
22164
22664
  const cfg = await loadConfig();
22165
22665
  let target;
@@ -23590,7 +24090,7 @@ async function resolveRcandPlanTargets() {
23590
24090
  }
23591
24091
  }
23592
24092
  for (const commandName of ["rcand", "release"]) {
23593
- 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) => {
23594
24094
  try {
23595
24095
  await requireFreshTrainCli(commandName);
23596
24096
  } catch (e) {
@@ -23602,6 +24102,9 @@ for (const commandName of ["rcand", "release"]) {
23602
24102
  if (o.dev && commandName !== "release") {
23603
24103
  return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
23604
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
+ }
23605
24108
  if (o.apply) {
23606
24109
  try {
23607
24110
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -23611,8 +24114,8 @@ for (const commandName of ["rcand", "release"]) {
23611
24114
  return failGraceful(`${commandName}: ${e.message}`);
23612
24115
  }
23613
24116
  }
23614
- const repo = await resolveRepo();
23615
- 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;
23616
24119
  let releaseTrack;
23617
24120
  if (repo) {
23618
24121
  try {
@@ -24026,7 +24529,9 @@ access.command("audit").description("audit collaborator roles + train-branch pus
24026
24529
  const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
24027
24530
  if (o.repo) {
24028
24531
  if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
24029
- 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) }];
24030
24535
  } else {
24031
24536
  const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs29.existsSync)("projects.json") ? (0, import_node_fs29.readFileSync)("projects.json", "utf8") : null;
24032
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>");
@@ -240,7 +240,7 @@ function deliverPendingMessages() {
240
240
  if (!run?.messages?.length) return;
241
241
  let changed = false;
242
242
  const nextMessages = run.messages.map((message) => {
243
- if (message.deliveredAt) return message;
243
+ if (message.startedAt || message.completedAt || message.failedAt || message.deliveredAt) return message;
244
244
  const targets = messageTargets(run, message.target);
245
245
  if (!targets.length) return message;
246
246
  const liveTargets = targets.map((servant) => servants.get(servant.slotId));
@@ -248,7 +248,7 @@ function deliverPendingMessages() {
248
248
  for (const child of liveTargets) child?.write(`${message.text}\r
249
249
  `);
250
250
  changed = true;
251
- return { ...message, deliveredAt: (/* @__PURE__ */ new Date()).toISOString() };
251
+ return { ...message, state: "started", startedAt: (/* @__PURE__ */ new Date()).toISOString() };
252
252
  });
253
253
  if (!changed) return;
254
254
  writeOverlordRegistry(statePath, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.55.0",
3
+ "version": "2.56.0",
4
4
  "description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",