@mutmutco/cli 2.56.0 → 2.58.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
@@ -4769,7 +4769,7 @@ async function correlateRun(deps, args) {
4769
4769
  "--limit",
4770
4770
  "10",
4771
4771
  "--json",
4772
- args.mode === "dispatch" ? "databaseId,url,event,createdAt" : "databaseId,url,event,createdAt,status,conclusion,headSha"
4772
+ args.mode === "dispatch" ? "databaseId,url,event,createdAt,displayTitle" : "databaseId,url,event,createdAt,status,conclusion,headSha"
4773
4773
  ];
4774
4774
  let rows;
4775
4775
  try {
@@ -4779,24 +4779,27 @@ async function correlateRun(deps, args) {
4779
4779
  if (args.mode === "workflow") lastError = new Error(`could not list ${args.workflow} runs`);
4780
4780
  continue;
4781
4781
  }
4782
- const match = rows.filter((r) => {
4782
+ const candidates = rows.filter((r) => {
4783
4783
  if (typeof r.databaseId !== "number") return false;
4784
4784
  if (args.mode === "dispatch") return r.event === "workflow_dispatch";
4785
4785
  return r.event === args.event && r.headSha === args.headSha;
4786
- }).map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
4786
+ }).map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created);
4787
+ const tokens2 = args.mode === "dispatch" ? args.titleIncludes : void 0;
4788
+ const titleMatch = tokens2 && tokens2.length ? candidates.find((c) => tokens2.every((t) => (c.row.displayTitle ?? "").includes(t))) : void 0;
4789
+ const match = titleMatch ?? candidates[0];
4787
4790
  if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
4788
4791
  }
4789
4792
  if (args.mode === "workflow" && !parsedAnyResponse && lastError) throw lastError;
4790
4793
  return {};
4791
4794
  }
4792
- function correlateTenantRun(deps, since) {
4793
- return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch" });
4795
+ function correlateTenantRun(deps, since, titleIncludes) {
4796
+ return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch", titleIncludes });
4794
4797
  }
4795
4798
  function correlatePublishRun(deps, since) {
4796
4799
  return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
4797
4800
  }
4798
- function correlateControlRun(deps, since) {
4799
- return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch" });
4801
+ function correlateControlRun(deps, since, titleIncludes) {
4802
+ return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
4800
4803
  }
4801
4804
  async function correlateWorkflowRun(deps, args) {
4802
4805
  return correlateRun(deps, { ...args, mode: "workflow" });
@@ -4927,6 +4930,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
4927
4930
  const sleep = resolveSleep(deps);
4928
4931
  let lastStatus = "not checked";
4929
4932
  let lastError;
4933
+ const everObserved = /* @__PURE__ */ new Set();
4930
4934
  for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
4931
4935
  if (attempt > 0) await sleep(TRAIN_CHECK_DELAY_MS);
4932
4936
  let checkRuns;
@@ -4945,6 +4949,9 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
4945
4949
  lastError = e.message || String(e);
4946
4950
  continue;
4947
4951
  }
4952
+ for (const c of required) {
4953
+ if (checkRuns.some((r) => r.name === c) || statuses.some((s) => s.context === c)) everObserved.add(c);
4954
+ }
4948
4955
  const states = required.map((c) => [c, resolveContextState(c, checkRuns, statuses)]);
4949
4956
  lastStatus = states.map(([c, s]) => `${c}=${s}`).join(", ");
4950
4957
  const failed = states.filter(([, s]) => s === "failed").map(([c]) => c);
@@ -4955,8 +4962,11 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
4955
4962
  return `required checks passed: ${required.join(", ")}`;
4956
4963
  }
4957
4964
  }
4965
+ const waitedMin = Math.round((TRAIN_CHECK_ATTEMPTS - 1) * TRAIN_CHECK_DELAY_MS / 6e4);
4966
+ const neverMaterialized = required.filter((c) => !everObserved.has(c));
4967
+ const neverNote = neverMaterialized.length ? ` Never materialized on ${sha} (no check-run or commit status ever appeared \u2014 the workflow that produces them was likely not triggered by the tag event): ${neverMaterialized.join(", ")}.` : "";
4958
4968
  throw new Error(
4959
- `timed out waiting for required train checks on ${sha}: ${lastError ? `last error: ${lastError}` : lastStatus}`
4969
+ `timed out after ~${waitedMin}m (${TRAIN_CHECK_ATTEMPTS} attempts) waiting for required train checks on ${sha}.${neverNote} Last observed: ${lastError ? `error: ${lastError}` : lastStatus}`
4960
4970
  );
4961
4971
  }
4962
4972
  function partialTrainRecoveryError(cause, input) {
@@ -5104,7 +5114,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
5104
5114
  deployStatus: "failure"
5105
5115
  };
5106
5116
  }
5107
- const { runId, runUrl } = await correlateTenantRun(deps, since);
5117
+ const { runId, runUrl } = await correlateTenantRun(deps, since, [ctx.slug, stage2]);
5108
5118
  const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
5109
5119
  return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
5110
5120
  }
@@ -5528,7 +5538,7 @@ async function runTenantControl(deps, options) {
5528
5538
  note: transport ? `tenant control ${action} dispatch failed (transport) \u2014 safe to retry` : `tenant control ${action} rejected: ${d.error ?? "request rejected by the Hub"}`
5529
5539
  };
5530
5540
  }
5531
- const { runId, runUrl } = await correlateControlRun(deps, since);
5541
+ const { runId, runUrl } = await correlateControlRun(deps, since, [stage2, action]);
5532
5542
  const conclusion = watch ? await watchTenantRun(deps, runId) : "pending";
5533
5543
  const result = { ...base, dispatched: true, runId, runUrl, conclusion, note: "" };
5534
5544
  if (action === "retire") {
@@ -5540,6 +5550,7 @@ async function runTenantControl(deps, options) {
5540
5550
  result.serviceState = parseStatusSnippet(output).serviceState;
5541
5551
  } else {
5542
5552
  result.secrets = parseVerifySecrets(output);
5553
+ result.secretsRaw = output;
5543
5554
  }
5544
5555
  }
5545
5556
  result.note = conclusion === "success" ? `tenant-control ${action} run succeeded` : conclusion === "failure" ? `tenant-control ${action} run failed \u2014 inspect the run` : runId == null ? `dispatched tenant-control.yml (${action}) \u2014 run not correlated; check the Actions tab` : `dispatched tenant-control.yml (${action}) \u2014 not watched`;
@@ -5594,7 +5605,7 @@ async function restoreDirtyOrgSpineToHead(deps, options = {}) {
5594
5605
  }
5595
5606
 
5596
5607
  // src/index.ts
5597
- var import_node_child_process15 = require("node:child_process");
5608
+ var import_node_child_process14 = require("node:child_process");
5598
5609
 
5599
5610
  // src/cli-shared.ts
5600
5611
  var import_promises = require("node:fs/promises");
@@ -6289,6 +6300,23 @@ async function hubAuthToken(deps) {
6289
6300
  return (await hubAuthSession(deps))?.token;
6290
6301
  }
6291
6302
 
6303
+ // src/continuity-access.ts
6304
+ var CONTINUITY_OWNER_LOGIN = "jervaise";
6305
+ function continuityAllowedForLogin(login) {
6306
+ return login?.trim().toLowerCase() === CONTINUITY_OWNER_LOGIN;
6307
+ }
6308
+ async function firstLogin(source, read) {
6309
+ const login = (await read?.().catch(() => void 0))?.trim();
6310
+ if (!login) return null;
6311
+ return { allowed: continuityAllowedForLogin(login), login, source };
6312
+ }
6313
+ async function resolveContinuityAccess(deps) {
6314
+ return await firstLogin("hub-session", deps.hubLogin) ?? await firstLogin("github", deps.ghLogin) ?? { allowed: false, source: "unknown" };
6315
+ }
6316
+ function formatContinuityAccessDenied(kind) {
6317
+ return `${kind}: continuity is restricted to ${CONTINUITY_OWNER_LOGIN}`;
6318
+ }
6319
+
6292
6320
  // src/stdin-inject.ts
6293
6321
  var import_node_fs6 = require("node:fs");
6294
6322
  var injectedStdin;
@@ -6363,6 +6391,22 @@ async function hubHeaders(extra = {}) {
6363
6391
  const base = { ...clientVersionHeaders(), ...extra };
6364
6392
  return t ? { ...base, Authorization: `Bearer ${t}` } : base;
6365
6393
  }
6394
+ async function continuityAccess() {
6395
+ const cfg = await loadConfig();
6396
+ return resolveContinuityAccess({
6397
+ hubLogin: async () => (await hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }))?.login,
6398
+ ghLogin: githubLogin
6399
+ });
6400
+ }
6401
+ async function requireContinuityAccess(kind, opts = {}, io) {
6402
+ const access2 = await continuityAccess();
6403
+ if (access2.allowed) return true;
6404
+ if (!opts.quiet) {
6405
+ (io ?? consoleIo).err(formatContinuityAccessDenied(kind));
6406
+ process.exitCode = 1;
6407
+ }
6408
+ return false;
6409
+ }
6366
6410
  var CONFIG_FILE = ".mmi/config.json";
6367
6411
  async function loadConfig() {
6368
6412
  let file = {};
@@ -6434,6 +6478,7 @@ async function postCaptureOnce(sagaApiUrl, body) {
6434
6478
  }
6435
6479
  }
6436
6480
  async function postCapture(capture, quiet = false) {
6481
+ if (!await requireContinuityAccess("saga", { quiet })) return;
6437
6482
  const cfg = await loadConfig();
6438
6483
  if (!cfg.sagaApiUrl) {
6439
6484
  if (!quiet) console.error("mmi-cli saga: Hub API URL not configured");
@@ -6456,6 +6501,7 @@ function spawnSagaFlush() {
6456
6501
  }
6457
6502
  async function maybeSpawnHeadUpdate() {
6458
6503
  try {
6504
+ if (!await requireContinuityAccess("saga", { quiet: true })) return;
6459
6505
  const gateKey = await sagaKey(await loadConfig());
6460
6506
  const tsPath = headTsPath(gateKey);
6461
6507
  if (!headGateDue(tsPath)) return;
@@ -6558,7 +6604,6 @@ function parseHookInput(stdin) {
6558
6604
  }
6559
6605
 
6560
6606
  // src/saga-health.ts
6561
- var MEMORY_STALE_DAYS = 14;
6562
6607
  var SESSION_START_LIVENESS = { attempts: 1, timeoutMs: 3e3 };
6563
6608
  function buildHealth(i) {
6564
6609
  const problems = [];
@@ -6571,9 +6616,6 @@ function buildHealth(i) {
6571
6616
  if (i.reachable && i.authorized === false) problems.push("saga backend rejected authenticated state access");
6572
6617
  if (!i.key.sessionId || i.key.sessionId === "-") problems.push("unsafe session id");
6573
6618
  const warnings = [];
6574
- if (i.memoryAgeDays !== void 0 && i.memoryAgeDays > MEMORY_STALE_DAYS) {
6575
- warnings.push(`PROJECT MEMORY is ${Math.round(i.memoryAgeDays)}d stale \u2014 the saga-keeper may have stalled`);
6576
- }
6577
6619
  const safeToWrite = problems.length === 0;
6578
6620
  return {
6579
6621
  ok: safeToWrite,
@@ -6588,8 +6630,7 @@ function buildHealth(i) {
6588
6630
  key: i.key,
6589
6631
  source: i.source,
6590
6632
  problems,
6591
- warnings,
6592
- memoryAgeDays: i.memoryAgeDays
6633
+ warnings
6593
6634
  };
6594
6635
  }
6595
6636
  function healthBanner(report) {
@@ -7662,6 +7703,7 @@ async function collectWaveStatus(deps) {
7662
7703
 
7663
7704
  // src/saga-commands.ts
7664
7705
  async function runNote(summary, o) {
7706
+ if (!await requireContinuityAccess("saga")) return;
7665
7707
  const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
7666
7708
  const capture = buildNoteCapture(summary, o, (0, import_node_crypto3.randomUUID)(), { sha: sha || void 0, branch: key.branch });
7667
7709
  await postCapture(capture);
@@ -7675,6 +7717,10 @@ function resolveSummary(summary, o) {
7675
7717
  });
7676
7718
  }
7677
7719
  async function runSagaFlush(o, io = consoleIo) {
7720
+ if (!await requireContinuityAccess("saga", { quiet: o.run }, io)) {
7721
+ if (o.json) io.log(JSON.stringify({ flushed: 0, dropped: 0, remaining: 0, restricted: true }));
7722
+ return;
7723
+ }
7678
7724
  if (o.run) {
7679
7725
  try {
7680
7726
  const cfg2 = await loadConfig();
@@ -7698,6 +7744,7 @@ async function runSagaFlush(o, io = consoleIo) {
7698
7744
  io.log(`saga flush: rolled forward ${result.flushed}, dropped ${result.dropped.length}, ${result.remaining} still pending`);
7699
7745
  }
7700
7746
  async function runSagaShow(opts, io = consoleIo) {
7747
+ if (!await requireContinuityAccess("saga", { quiet: opts.quiet }, io)) return;
7701
7748
  const cfg = await loadConfig();
7702
7749
  if (!cfg.sagaApiUrl) {
7703
7750
  if (opts.quiet) return;
@@ -7746,20 +7793,11 @@ async function probeSagaAccess(url, key) {
7746
7793
  return false;
7747
7794
  }
7748
7795
  }
7749
- async function fetchMemoryAge(url, project2) {
7750
- try {
7751
- const qs = new URLSearchParams({ project: project2 }).toString();
7752
- const res = await fetch(`${url}/saga/memory-age?${qs}`, { headers: await hubHeaders(), signal: AbortSignal.timeout(8e3) });
7753
- if (!res.ok) return void 0;
7754
- const body = await res.json();
7755
- if (!body.updatedAt) return void 0;
7756
- const ms = Date.now() - Date.parse(body.updatedAt);
7757
- return Number.isFinite(ms) && ms >= 0 ? ms / 864e5 : void 0;
7758
- } catch {
7759
- return void 0;
7760
- }
7761
- }
7762
7796
  async function runSagaHealth(o, io = consoleIo) {
7797
+ if (!await requireContinuityAccess("saga", { quiet: o.quiet || o.banner }, io)) {
7798
+ if (o.json) io.log(JSON.stringify({ ok: false, restricted: true, problems: ["continuity is restricted to jervaise"], warnings: [] }));
7799
+ return;
7800
+ }
7763
7801
  const cfg = await loadConfig();
7764
7802
  const session = resolveSessionId();
7765
7803
  const key = await sagaKey(cfg, session);
@@ -7770,7 +7808,6 @@ async function runSagaHealth(o, io = consoleIo) {
7770
7808
  cfg.sagaApiUrl ? probeBackend(cfg.sagaApiUrl, livenessOpts) : Promise.resolve({ reachable: false })
7771
7809
  ]);
7772
7810
  const authorized = o.banner ? void 0 : cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
7773
- const memoryAgeDays = o.banner ? void 0 : cfg.sagaApiUrl && liveness.reachable ? await fetchMemoryAge(cfg.sagaApiUrl, key.project) : void 0;
7774
7811
  const report = buildHealth({
7775
7812
  key,
7776
7813
  source,
@@ -7780,8 +7817,7 @@ async function runSagaHealth(o, io = consoleIo) {
7780
7817
  livenessMessage: liveness.message,
7781
7818
  authorized,
7782
7819
  sagaApiUrl: cfg.sagaApiUrl,
7783
- pendingNotes: readPending().length,
7784
- memoryAgeDays
7820
+ pendingNotes: readPending().length
7785
7821
  });
7786
7822
  if (o.json) return io.log(JSON.stringify(report));
7787
7823
  if (o.banner) {
@@ -7798,6 +7834,7 @@ async function runSagaHealth(o, io = consoleIo) {
7798
7834
  if (report.pendingNotes > 0) io.log(` - ${report.pendingNotes} note(s) queued locally \u2014 \`mmi-cli saga flush\` to roll forward`);
7799
7835
  }
7800
7836
  async function fetchSagaHead(io = consoleIo) {
7837
+ if (!await requireContinuityAccess("saga", {}, io)) return null;
7801
7838
  const cfg = await loadConfig();
7802
7839
  if (!cfg.sagaApiUrl) {
7803
7840
  io.err("saga snapshot: Hub API URL not configured");
@@ -7819,6 +7856,7 @@ async function fetchSagaHead(io = consoleIo) {
7819
7856
  }
7820
7857
  }
7821
7858
  async function postSnapshotNotes(plan2, anchorForce) {
7859
+ if (!await requireContinuityAccess("saga")) return;
7822
7860
  const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
7823
7861
  const evidence = { sha: sha || void 0, branch: key.branch };
7824
7862
  for (const idx of [...plan2.clearIndices].sort((a, b) => b - a)) {
@@ -7853,7 +7891,7 @@ async function runSagaSnapshotSet(snapshot, opts = {}, io = consoleIo) {
7853
7891
  io.log(`saga snapshot: wrote ${snapshot.kind} snapshot (retired ${plan2.clearIndices.length}, ${plan2.queueOps.length + 1} capture(s))`);
7854
7892
  }
7855
7893
  function registerSagaCommands(program3) {
7856
- const saga = program3.command("saga").description("per-session continuity");
7894
+ const saga = program3.command("saga").description("Jervaise-only per-session continuity");
7857
7895
  saga.command("note [summary]").description("record a one-line structured note into your saga (the per-turn capture)").option("--next <text>", 'set "where I left off" (NEXT)').option("--decision <text>", "append a verbatim decision").option("--queue-add <text>", "add a worklist item").option("--queue-done <n>", "mark worklist item N done").option("--verified", "mark this claim as checked against source (state: verified, else asserted)").option("--diagnostic", "isolate a probe write (state: diagnostic, source: probe) \u2014 never resume/LAST 5").option("--supersedes <key>", "retire prior decisions matching an evidence key (pr:N | file:path)").option("--anchor <intent>", "set the sprint North-Star (write-protected; needs --anchor-force to change)").option("--anchor-slug <slug>", "bind the anchor to a North Star plan slug (SSOT at plans/.../<slug>.md)").option("--anchor-force", "overwrite an existing anchor").option("--message-file <path|->", "read the summary from a UTF-8 file, or from stdin with - (avoids cmd.exe quoting)").action(async (summary, o) => {
7858
7896
  let text;
7859
7897
  try {
@@ -7873,9 +7911,10 @@ function registerSagaCommands(program3) {
7873
7911
  await runNote(text, { ...o, diagnostic: true });
7874
7912
  });
7875
7913
  saga.command("flush").option("--json", "machine-readable {flushed, dropped, remaining}").option("--run", "detached worker: drain the queue silently (spawned by note/capture)").description("roll the local pending-note queue forward (re-POST queued saga writes); reports what landed").action((o) => runSagaFlush(o));
7876
- saga.command("show").option("--quiet", "no-op silently when unconfigured/unreachable (SessionStart hook)").option("--latest-anywhere", "resume the newest saga across all repos (default: current repo)").description("print your resume block \u2014 current repo HEAD + project memory (where you left off)").action((opts) => runSagaShow(opts));
7914
+ saga.command("show").option("--quiet", "no-op silently when unconfigured/unreachable (SessionStart hook)").option("--latest-anywhere", "resume the newest saga across all repos (default: current repo)").description("print your resume block \u2014 current repo HEAD + live decisions").action((opts) => runSagaShow(opts));
7877
7915
  saga.command("capture").option("--quiet", "capture silently (for the Stop hook)").description("per-turn deterministic capture (Stop hook): turn boundary + current sha + gated HEAD-update").action(async (opts) => {
7878
7916
  if (!isOrgRepoRoot()) return;
7917
+ if (!await requireContinuityAccess("saga", { quiet: opts.quiet })) return;
7879
7918
  const hook = parseHookInput(await readStdin());
7880
7919
  if (hook.session_id) persistSession(hook.session_id);
7881
7920
  await postCapture({ event: "stop", id: (0, import_node_crypto3.randomUUID)(), source: "hook", sha: await gitOut(["rev-parse", "--short", "HEAD"]), surface: agentSurface() }, opts.quiet ?? false);
@@ -7883,10 +7922,12 @@ function registerSagaCommands(program3) {
7883
7922
  });
7884
7923
  saga.command("session").option("--quiet", "silent (for the SessionStart hook)").description("persist the harness session id for this repo (SessionStart hook)").action(async () => {
7885
7924
  if (!isOrgRepoRoot()) return;
7925
+ if (!await requireContinuityAccess("saga", { quiet: true })) return;
7886
7926
  const hook = parseHookInput(await readStdin());
7887
7927
  if (hook.session_id) persistSession(hook.session_id);
7888
7928
  });
7889
7929
  saga.command("head-update").option("--run", "detached worker: fetch state, run the engine, post the curated HEAD").option("--quiet", "silent (Stop hook)").description("curate the smart HEAD in the background (engine via SAGA_HEAD_ENGINE; default local claude)").action(async (o) => {
7930
+ if (!await requireContinuityAccess("saga", { quiet: o.quiet || o.run })) return;
7890
7931
  if (!o.run) {
7891
7932
  return maybeSpawnHeadUpdate();
7892
7933
  }
@@ -7911,6 +7952,7 @@ function registerSagaCommands(program3) {
7911
7952
  }
7912
7953
  });
7913
7954
  saga.command("key").option("--json", "machine-readable output").description("print the resolved saga key + session-id source (no write)").action(async (o) => {
7955
+ if (!await requireContinuityAccess("saga")) return;
7914
7956
  const cfg = await loadConfig();
7915
7957
  const session = resolveSessionId();
7916
7958
  const key = await sagaKey(cfg, session);
@@ -8100,7 +8142,15 @@ async function fetchScopedSessions(url, project2, branch, retry = FOREGROUND_FET
8100
8142
  const qs = new URLSearchParams({ project: project2 });
8101
8143
  if (branch) qs.set("branch", branch);
8102
8144
  const res = await fetchWithRetry(fetch, `${url}/saga/sessions?${qs}`, { headers: await hubHeaders() }, retry);
8103
- if (!res.ok) return [];
8145
+ if (!res.ok) {
8146
+ let detail = "";
8147
+ try {
8148
+ const errBody = await res.clone().json();
8149
+ detail = typeof errBody.error === "string" && errBody.error || typeof errBody.message === "string" && errBody.message || "";
8150
+ } catch {
8151
+ }
8152
+ throw new Error(`saga sessions read failed (HTTP ${res.status})${detail ? `: ${detail}` : ""}`);
8153
+ }
8104
8154
  const body = await res.json();
8105
8155
  return body.sessions ?? [];
8106
8156
  }
@@ -8169,11 +8219,11 @@ async function locateClaimedHandoff(key, claimedBySessionId, retry = FOREGROUND_
8169
8219
  }
8170
8220
  async function postKeyedNote(key, summary, options) {
8171
8221
  const cfg = await loadConfig();
8172
- if (!cfg.sagaApiUrl) fail("handoff: Hub API URL not configured");
8222
+ if (!cfg.sagaApiUrl) return failGraceful("handoff: Hub API URL not configured");
8173
8223
  const sha = await gitOut(["rev-parse", "--short", "HEAD"]);
8174
8224
  const capture = buildNoteCapture(summary, options, (0, import_node_crypto4.randomUUID)(), { sha: sha || void 0, branch: key.branch });
8175
8225
  const result = await postCaptureOnce(cfg.sagaApiUrl, { ...capture, ...key });
8176
- if (!result.ok) fail(`handoff: write failed${result.status ? ` (HTTP ${result.status})` : ""}${result.message ? `: ${result.message}` : ""}`);
8226
+ if (!result.ok) await failGraceful(`handoff: write failed${result.status ? ` (HTTP ${result.status})` : ""}${result.message ? `: ${result.message}` : ""}`);
8177
8227
  }
8178
8228
  function deriveOpenFields(summary, opts, head, key) {
8179
8229
  const northStarSlug = opts.northStarSlug ?? head.anchor?.slug;
@@ -8192,6 +8242,7 @@ function deriveOpenFields(summary, opts, head, key) {
8192
8242
  });
8193
8243
  }
8194
8244
  async function runHandoffOpen(summary, opts, io = consoleIo) {
8245
+ if (!await requireContinuityAccess("handoff", {}, io)) return;
8195
8246
  let resolvedSummary = summary;
8196
8247
  if (summary !== void 0 || opts.messageFile !== void 0) {
8197
8248
  try {
@@ -8201,23 +8252,29 @@ async function runHandoffOpen(summary, opts, io = consoleIo) {
8201
8252
  noun: "message"
8202
8253
  });
8203
8254
  } catch (e) {
8204
- return fail(`handoff open: ${e.message}`);
8255
+ return failGraceful(`handoff open: ${e.message}`);
8205
8256
  }
8206
8257
  }
8207
8258
  const current = await fetchCurrentState();
8208
- if (!current) return fail("handoff open: saga state unavailable");
8259
+ if (!current) return failGraceful("handoff open: saga state unavailable");
8209
8260
  let record;
8210
8261
  try {
8211
8262
  record = deriveOpenFields(resolvedSummary, opts, current.head, current.key);
8212
8263
  } catch (e) {
8213
- return fail(`handoff open: ${e.message}`);
8264
+ return failGraceful(`handoff open: ${e.message}`);
8214
8265
  }
8215
8266
  await postKeyedNote(current.key, `handoff opened ${record.key}`, { queueAdd: serializeHandoff(record), anchorSlug: record.northStarSlug });
8216
8267
  if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: record }));
8217
8268
  io.log(`handoff open: ${formatHandoffLine({ index: -1, done: false, record })}`);
8218
8269
  }
8219
8270
  async function runHandoffList(opts, io = consoleIo) {
8220
- const { handoffs } = await collectScopedHandoffs({ includeClosed: opts.all });
8271
+ if (!await requireContinuityAccess("handoff", {}, io)) return [];
8272
+ let handoffs;
8273
+ try {
8274
+ ({ handoffs } = await collectScopedHandoffs({ includeClosed: opts.all }));
8275
+ } catch (e) {
8276
+ return failGraceful(`handoff list: ${e.message}`);
8277
+ }
8221
8278
  if (opts.json) {
8222
8279
  io.log(JSON.stringify({ handoffs }, null, 2));
8223
8280
  } else if (!handoffs.length) {
@@ -8228,25 +8285,42 @@ async function runHandoffList(opts, io = consoleIo) {
8228
8285
  return handoffs;
8229
8286
  }
8230
8287
  async function runHandoffOffer(io = consoleIo, opts = {}) {
8288
+ if (!await requireContinuityAccess("handoff", { quiet: opts.fast }, io)) return;
8231
8289
  const retry = opts.fast ? SESSION_START_FETCH : FOREGROUND_FETCH;
8232
- const { handoffs } = await collectScopedHandoffs({ retry });
8290
+ let handoffs;
8291
+ try {
8292
+ ({ handoffs } = await collectScopedHandoffs({ retry }));
8293
+ } catch {
8294
+ return;
8295
+ }
8233
8296
  if (!handoffs.length) return;
8234
8297
  io.log("Open handoffs:");
8235
8298
  for (const h of handoffs) io.log(`- ${formatHandoffLine(h)}`);
8236
8299
  io.log("Use `mmi-cli handoff accept <key>` to claim, `mmi-cli handoff cancel <key>` to close, or decline in chat to leave it open.");
8237
8300
  }
8238
8301
  async function closeSourceHandoff(key, state, opts = {}, io = consoleIo) {
8302
+ if (!await requireContinuityAccess("handoff", {}, io)) return;
8239
8303
  const current = await sagaKey(await loadConfig(), resolveSessionId());
8240
- const located = await locateOpenHandoff(key);
8304
+ let located;
8305
+ try {
8306
+ located = await locateOpenHandoff(key);
8307
+ } catch (e) {
8308
+ return failGraceful(`handoff ${state}: ${e.message}`);
8309
+ }
8241
8310
  if (!located) {
8242
8311
  if (state === "claimed") {
8243
- const prior = await locateClaimedHandoff(key, current.sessionId);
8312
+ let prior;
8313
+ try {
8314
+ prior = await locateClaimedHandoff(key, current.sessionId);
8315
+ } catch (e) {
8316
+ return failGraceful(`handoff ${state}: ${e.message}`);
8317
+ }
8244
8318
  if (prior) {
8245
8319
  if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: prior.record, idempotent: true }));
8246
8320
  return io.log(`handoff claimed: ${formatHandoffLine(prior)} (already accepted)`);
8247
8321
  }
8248
8322
  }
8249
- return fail(`handoff ${state}: no open handoff matching ${key}`);
8323
+ return failGraceful(`handoff ${state}: no open handoff matching ${key}`);
8250
8324
  }
8251
8325
  const closed = closeHandoff(located.item.record, state, (/* @__PURE__ */ new Date()).toISOString(), state === "claimed" ? current.sessionId : void 0);
8252
8326
  const sourceBranch = located.item.record.sourceBranch ?? located.key.branch;
@@ -8274,13 +8348,76 @@ async function closeSourceHandoff(key, state, opts = {}, io = consoleIo) {
8274
8348
  var runHandoffAccept = (key, opts = {}, io = consoleIo) => closeSourceHandoff(key, "claimed", opts, io);
8275
8349
  var runHandoffCancel = (key, opts = {}, io = consoleIo) => closeSourceHandoff(key, "cancelled", opts, io);
8276
8350
  async function runHandoffDecline(key, opts = {}, io = consoleIo) {
8277
- const located = await locateOpenHandoff(key);
8278
- if (!located) return fail(`handoff decline: no open handoff matching ${key}`);
8351
+ if (!await requireContinuityAccess("handoff", {}, io)) return;
8352
+ let located;
8353
+ try {
8354
+ located = await locateOpenHandoff(key);
8355
+ } catch (e) {
8356
+ return failGraceful(`handoff decline: ${e.message}`);
8357
+ }
8358
+ if (!located) return failGraceful(`handoff decline: no open handoff matching ${key}`);
8279
8359
  if (opts.json) return io.log(JSON.stringify({ ok: true, unchanged: true, handoff: located.item.record }));
8280
8360
  io.log(`handoff decline: left open for re-offer \u2014 ${formatHandoffLine(located.item)}`);
8281
8361
  }
8362
+ async function resolveHandoffSummary(summary, messageFile, verb) {
8363
+ if (summary === void 0 && messageFile === void 0) return summary;
8364
+ try {
8365
+ return await resolveTextArg({ value: summary, file: messageFile }, { readFile: import_promises3.readFile, readStdin }, {
8366
+ value: "a summary argument",
8367
+ file: "--message-file",
8368
+ noun: "message"
8369
+ });
8370
+ } catch (e) {
8371
+ return failGraceful(`handoff ${verb}: ${e.message}`);
8372
+ }
8373
+ }
8374
+ async function runHandoffReplace(oldKey, summary, opts = {}, io = consoleIo) {
8375
+ if (!await requireContinuityAccess("handoff", {}, io)) return;
8376
+ const newKey = opts.key?.trim() || oldKey;
8377
+ const resolvedSummary = await resolveHandoffSummary(summary, opts.messageFile, "replace");
8378
+ let located;
8379
+ try {
8380
+ located = await locateOpenHandoff(oldKey);
8381
+ } catch (e) {
8382
+ return failGraceful(`handoff replace: ${e.message}`);
8383
+ }
8384
+ const current = await fetchCurrentState();
8385
+ if (!current) return failGraceful("handoff replace: saga state unavailable");
8386
+ let record;
8387
+ try {
8388
+ record = deriveOpenFields(resolvedSummary, { key: newKey, northStarSlug: opts.northStarSlug }, current.head, current.key);
8389
+ } catch (e) {
8390
+ return failGraceful(`handoff replace: ${e.message}`);
8391
+ }
8392
+ await postKeyedNote(current.key, `handoff opened ${record.key}`, { queueAdd: serializeHandoff(record), anchorSlug: record.northStarSlug });
8393
+ if (located) {
8394
+ const closed = closeHandoff(located.item.record, "cancelled", (/* @__PURE__ */ new Date()).toISOString());
8395
+ await postKeyedNote(located.key, `handoff cancelled ${located.item.record.key}`, {
8396
+ handoffClose: { index: located.item.index, closedText: serializeHandoff(closed) }
8397
+ });
8398
+ }
8399
+ if (opts.json) {
8400
+ return io.log(JSON.stringify({
8401
+ ok: true,
8402
+ replaced: located ? located.item.record.key : null,
8403
+ supersededIndex: located ? located.item.index : null,
8404
+ handoff: record
8405
+ }));
8406
+ }
8407
+ if (located) {
8408
+ io.log(`handoff replace: superseded ${located.item.record.key} (index ${located.item.index}) -> ${formatHandoffLine({ index: -1, done: false, record })}`);
8409
+ } else {
8410
+ io.log(`handoff replace: no open handoff matching ${oldKey} \u2014 opened ${formatHandoffLine({ index: -1, done: false, record })}`);
8411
+ }
8412
+ }
8413
+ async function runHandoffEdit(key, summary, opts = {}, io = consoleIo) {
8414
+ if (summary === void 0 && opts.messageFile === void 0) {
8415
+ return failGraceful("handoff edit: a new summary is required (pass it as an argument or via --message-file)");
8416
+ }
8417
+ return runHandoffReplace(key, summary, { ...opts, key }, io);
8418
+ }
8282
8419
  function registerHandoffCommands(program3) {
8283
- const handoff = program3.command("handoff").description("explicit saga + North Star handoff lifecycle");
8420
+ const handoff = program3.command("handoff").description("Jervaise-only saga + North Star handoff lifecycle");
8284
8421
  handoff.command("open [summary]").description("open a handoff bound to a North Star slug or a board issue (--key/--next)").option("--key <slug|#issue>", "handoff key: a North Star slug or a board issue (defaults to the current North Star slug)").option("--next <slug|#issue>", "alias for --key (mirrors `saga note --next`)").option("--north-star-slug <slug>", "North Star slug to bind (optional; omit for an issue-bound handoff)").option("--message-file <path|->", "read the handoff summary from a UTF-8 file, or from stdin with - (avoids cmd.exe quoting)").option("--json", "machine-readable output").action((summary, opts) => runHandoffOpen(summary, { ...opts, key: opts.key ?? opts.next }));
8285
8422
  handoff.command("list").description("list open handoffs for the current repo (any branch)").option("--all", "include claimed/cancelled records").option("--json", "machine-readable output").action(async (opts) => {
8286
8423
  await runHandoffList(opts);
@@ -8288,6 +8425,8 @@ function registerHandoffCommands(program3) {
8288
8425
  handoff.command("accept <key>").description("claim an open handoff and bind this session to its North Star").option("--json", "machine-readable output").action((key, opts) => runHandoffAccept(key, opts));
8289
8426
  handoff.command("cancel <key>").description("close an open handoff without claiming it").option("--json", "machine-readable output").action((key, opts) => runHandoffCancel(key, opts));
8290
8427
  handoff.command("decline <key>").description("leave an open handoff unchanged so it is re-offered later").option("--json", "machine-readable output").action((key, opts) => runHandoffDecline(key, opts));
8428
+ handoff.command("replace <old-key> [summary]").description("supersede an open handoff: open a corrected one (--key) and cancel the stale one atomically").option("--key <slug|#issue>", "the corrected handoff key to open (defaults to <old-key> for an in-place edit)").option("--north-star-slug <slug>", "North Star slug to bind on the corrected handoff (optional)").option("--message-file <path|->", "read the corrected summary from a UTF-8 file, or stdin with - (avoids cmd.exe quoting)").option("--json", "machine-readable output").action((oldKey, summary, opts) => runHandoffReplace(oldKey, summary, opts));
8429
+ handoff.command("edit <key> [summary]").description("update an open handoff's summary in place (supersedes the same key)").option("--north-star-slug <slug>", "North Star slug to bind (optional)").option("--message-file <path|->", "read the new summary from a UTF-8 file, or stdin with - (avoids cmd.exe quoting)").option("--json", "machine-readable output").action((key, summary, opts) => runHandoffEdit(key, summary, opts));
8291
8430
  }
8292
8431
 
8293
8432
  // src/coop-commands.ts
@@ -9694,16 +9833,21 @@ async function runSessionStart(parallel, sequential, io) {
9694
9833
  for (const lines of buffered) flush(lines, io);
9695
9834
  for (const step of sequential) flush(await runBufferedStep(step), io);
9696
9835
  }
9697
- function buildSessionStartPlan(verbs) {
9836
+ function buildSessionStartPlan(verbs, opts = {}) {
9837
+ const continuityEnabled = opts.continuityEnabled ?? true;
9838
+ const continuitySteps = continuityEnabled ? [
9839
+ { name: "saga show", run: verbs.sagaShow },
9840
+ { name: "handoff offer", run: verbs.handoffOffer },
9841
+ // northstar context (#1228): curated plan cards when relevance is high-confidence; silent otherwise.
9842
+ { name: "northstar context", run: verbs.northstarContext },
9843
+ { name: "saga health", run: verbs.sagaHealth }
9844
+ ] : [];
9698
9845
  return {
9699
9846
  parallel: [
9700
9847
  { name: "rules sync", run: verbs.rulesSync },
9701
- { name: "saga show", run: verbs.sagaShow },
9702
- { name: "handoff offer", run: verbs.handoffOffer },
9848
+ ...continuitySteps.slice(0, 2),
9703
9849
  { name: "coop pending", run: verbs.coopPending },
9704
- // northstar context (#1228): curated plan cards when relevance is high-confidence; silent otherwise.
9705
- { name: "northstar context", run: verbs.northstarContext },
9706
- { name: "saga health", run: verbs.sagaHealth },
9850
+ ...continuitySteps.slice(2),
9707
9851
  // whoami (#879): the resolved human lands in the banner so agents act --for them without asking.
9708
9852
  // Identity reads are memoized process-wide, so this adds no extra gh/Hub round-trip.
9709
9853
  { name: "whoami", run: verbs.whoami },
@@ -9781,6 +9925,10 @@ function northstarPointer(injected = false) {
9781
9925
  }
9782
9926
  return "North Stars: run `mmi-cli northstar relevant` to load plans relevant to your task (`northstar list` for all).";
9783
9927
  }
9928
+ function sessionStartContinuityLines(opts) {
9929
+ if (!opts.continuityEnabled) return [];
9930
+ return [northstarPointer(opts.northstarInjected), ...planStoreLines(opts.cwd)];
9931
+ }
9784
9932
  function kbPointer() {
9785
9933
  return "MM-KB (org knowledge): start at `mmi-cli kb get kb/INDEX.md`, then `kb get <path>` \u2014 consult before inventing conventions or storing long-term docs.";
9786
9934
  }
@@ -9788,6 +9936,7 @@ function kbPointer() {
9788
9936
  // src/coop-commands.ts
9789
9937
  var FOREGROUND_FETCH2 = { attempts: 2, timeoutMs: 15e3 };
9790
9938
  var SESSION_START_FETCH2 = { attempts: 1, timeoutMs: 3e3 };
9939
+ var COOP_WAIT_DELAYS_MS = [6e4, 12e4, 18e4, 3e5, 6e5, 18e5];
9791
9940
  async function hubBase() {
9792
9941
  const cfg = await loadConfig();
9793
9942
  if (!cfg.sagaApiUrl) fail("Hub API URL not configured (sagaApiUrl)");
@@ -9822,11 +9971,124 @@ async function sessionContext() {
9822
9971
  const session = resolveSessionId();
9823
9972
  const branch = key.branch !== "-" ? key.branch : await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "development") ?? "development";
9824
9973
  const sessionId = session.id;
9825
- const surface = process.env.MMI_SURFACE ?? "cursor";
9974
+ const surface = coopSurfaceFromEnv(process.env);
9826
9975
  const remote = await gitOut(["remote", "get-url", "origin"]).catch(() => "");
9827
9976
  const repo = remote.includes("github.com") ? remote.replace(/\.git$/, "").replace(/^.*github\.com[:/]/, "") : key.project !== "-" ? key.project : "mutmutco/MMI-Hub";
9828
9977
  return { branch, sessionId, surface, repo };
9829
9978
  }
9979
+ function coopSurfaceFromEnv(env) {
9980
+ const explicit = env.MMI_AGENT_SURFACE?.trim();
9981
+ if (explicit && ["claude", "codex", "cursor", "gemini", "opencode"].includes(explicit)) return explicit;
9982
+ const has = (k) => Boolean(env[k]?.trim());
9983
+ if (has("CODEX_THREAD_ID") || has("CODEX_HOME") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) return "codex";
9984
+ if (has("CURSOR_TRACE_ID") || has("CURSOR_USER") || has("CURSOR_SESSION_ID") || env.CURSOR_AGENT === "1" || has("CURSOR_EXTENSION_HOST_ROLE")) {
9985
+ return "cursor";
9986
+ }
9987
+ if (has("OPENCODE_CLIENT") || has("OPENCODE_SERVER_USERNAME")) return "opencode";
9988
+ if (has("CLAUDECODE") || has("CLAUDE_CODE_ENTRYPOINT") || has("CLAUDE_PLUGIN_ROOT")) return "claude";
9989
+ return "shell";
9990
+ }
9991
+ function coopOpenPath(repo) {
9992
+ return repo ? `/coop/status?repo=${encodeURIComponent(repo)}` : "/coop/status";
9993
+ }
9994
+ function parseTargetLogins(value) {
9995
+ return (value ?? "").split(",").map((x) => x.trim().replace(/^@/, "")).filter(Boolean);
9996
+ }
9997
+ function categorizeOpenCoops(open, viewer) {
9998
+ const login = viewer.login?.toLowerCase();
9999
+ const repo = viewer.repo?.toLowerCase();
10000
+ const categories = {
10001
+ invitedToMe: [],
10002
+ myAgents: [],
10003
+ sameRepo: [],
10004
+ internal: []
10005
+ };
10006
+ for (const coop of open) {
10007
+ const targetLogins = (coop.targetLogins ?? []).map((x) => x.toLowerCase());
10008
+ if (login && targetLogins.includes(login)) {
10009
+ categories.invitedToMe.push(coop);
10010
+ } else if (login && coop.coordinatorLogin.toLowerCase() === login) {
10011
+ categories.myAgents.push(coop);
10012
+ } else if (repo && coop.repo.toLowerCase() === repo) {
10013
+ categories.sameRepo.push(coop);
10014
+ } else {
10015
+ categories.internal.push(coop);
10016
+ }
10017
+ }
10018
+ return categories;
10019
+ }
10020
+ function formatOpenCoop(coop) {
10021
+ const code = coop.sessionCode ?? coop.coopId;
10022
+ const topic = coop.topic ?? "Untitled coop";
10023
+ const target = coop.targetKind ? ` \xB7 target=${coop.targetKind}` : "";
10024
+ return ` - ${code} \xB7 ${topic} \xB7 ${coop.repo}#${coop.issueNumber} \xB7 @${coop.coordinatorLogin}${target}`;
10025
+ }
10026
+ function formatOpenCoopCategories(categories) {
10027
+ const sections = [
10028
+ ["Invited to me", categories.invitedToMe],
10029
+ ["My agents", categories.myAgents],
10030
+ ["This repo", categories.sameRepo],
10031
+ ["Internal / other repos", categories.internal]
10032
+ ];
10033
+ const lines = [];
10034
+ let any = false;
10035
+ for (const [title, rows] of sections) {
10036
+ if (!rows.length) continue;
10037
+ any = true;
10038
+ lines.push(`${title}:`);
10039
+ for (const row of rows) lines.push(formatOpenCoop(row));
10040
+ }
10041
+ return any ? lines.join("\n") : '[coop] no open sessions. Create one with `mmi-cli coop start --topic "<topic>" --target <own-agents|user|internal>`.';
10042
+ }
10043
+ async function printOpenCoops(res, repo) {
10044
+ const open = res.open ?? [];
10045
+ const [ctx, login] = await Promise.all([
10046
+ sessionContext().catch(() => void 0),
10047
+ githubLogin().catch(() => void 0)
10048
+ ]);
10049
+ consoleIo.log(formatOpenCoopCategories(categorizeOpenCoops(open, { repo: repo ?? ctx?.repo, login })));
10050
+ }
10051
+ function newCoopMessages(messages, seenCount) {
10052
+ return messages.slice(seenCount);
10053
+ }
10054
+ function shouldStopCoopWait(status) {
10055
+ if (status.coop?.state === "ended") return true;
10056
+ return (status.messages ?? []).some((m) => m.phase === "SHOOK" || m.phase === "COOP_END");
10057
+ }
10058
+ async function runCoopWait(coopId, io, opts = {}) {
10059
+ const statusPath = `/coop/status?coopId=${encodeURIComponent(coopId)}`;
10060
+ let status = await getJson(statusPath);
10061
+ let seenCount = status.messages?.length ?? 0;
10062
+ if (shouldStopCoopWait(status)) {
10063
+ if (!opts.json) io.log(`[coop wait] ${coopId} is already complete`);
10064
+ else console.log(JSON.stringify({ coopId, status: "complete" }));
10065
+ return;
10066
+ }
10067
+ for (const delayMs of COOP_WAIT_DELAYS_MS) {
10068
+ if (!opts.json) io.log(`[coop wait] waiting ${Math.round(delayMs / 6e4)}m for ${coopId}`);
10069
+ await new Promise((r) => setTimeout(r, delayMs));
10070
+ status = await getJson(statusPath);
10071
+ const messages = status.messages ?? [];
10072
+ const fresh = newCoopMessages(messages, seenCount);
10073
+ seenCount = messages.length;
10074
+ if (opts.json) {
10075
+ console.log(JSON.stringify({ coopId, messages: fresh, complete: shouldStopCoopWait(status) }));
10076
+ } else {
10077
+ for (const message of fresh) {
10078
+ if (message.envelope) io.log(message.envelope);
10079
+ }
10080
+ }
10081
+ if (shouldStopCoopWait(status)) {
10082
+ if (!opts.json) io.log(`[coop wait] ${coopId} complete`);
10083
+ return;
10084
+ }
10085
+ }
10086
+ if (!opts.json) {
10087
+ io.log(`[coop wait] no answer after bounded wait; ${coopId} remains open and can be joined later`);
10088
+ } else {
10089
+ console.log(JSON.stringify({ coopId, status: "timeout-open" }));
10090
+ }
10091
+ }
9830
10092
  async function fetchCoopPending(retry = SESSION_START_FETCH2) {
9831
10093
  const body = await getJson("/coop/pending", retry);
9832
10094
  return body.pending ?? [];
@@ -9868,7 +10130,7 @@ async function runCoopDeliver(coopId, io, quiet = false) {
9868
10130
  }
9869
10131
  function registerCoopCommands(program3) {
9870
10132
  const coop = program3.command("coop").description("Cross-repo agent coordination (/coop skill)");
9871
- coop.command("start").description("Start a coop session (coordinator)").option("--repo <owner/name>", "coordinator repo (default: cwd origin)").option("--issue <n>", "existing issue number instead of creating one").option("--message-file <path>", "opening message file (or - for stdin)").option("--json", "machine output").action(async (opts) => {
10133
+ coop.command("start").description("Start a coop session (coordinator)").option("--repo <owner/name>", "coordinator repo (default: cwd origin)").option("--issue <n>", "existing issue number instead of creating one").option("--topic <topic>", "human topic shown in open-session lists").option("--target <kind>", "human target: own-agents, user, or internal").option("--target-users <logins>", "comma-separated GitHub logins for inter-user coop").option("--message-file <path>", "opening message file (or - for stdin)").option("--json", "machine output").action(async (opts) => {
9872
10134
  const ctx = await sessionContext();
9873
10135
  const body = opts.messageFile ? await resolveTextArg(
9874
10136
  { file: opts.messageFile },
@@ -9883,9 +10145,13 @@ function registerCoopCommands(program3) {
9883
10145
  body
9884
10146
  };
9885
10147
  if (opts.issue) payload.issueNumber = Number(opts.issue);
10148
+ if (opts.topic) payload.topic = opts.topic;
10149
+ if (opts.target) payload.targetKind = opts.target;
10150
+ const targetLogins = parseTargetLogins(opts.targetUsers);
10151
+ if (targetLogins.length) payload.targetLogins = targetLogins;
9886
10152
  const res = await postJson("/coop/start", payload);
9887
10153
  if (opts.json) console.log(JSON.stringify(res));
9888
- else consoleIo.log(`[coop] started ${res.coopId} \u2014 issue ${res.issue?.url ?? ""}`);
10154
+ else consoleIo.log(`[coop] started ${res.coopId} (code ${res.sessionCode ?? res.coopId}) \u2014 issue ${res.issue?.url ?? ""}`);
9889
10155
  });
9890
10156
  coop.command("join <coopId>").description("Join a coop session").option("--cloud", "enable Cursor cloud wake for this joiner").option("--repo <owner/name>", "joiner repo (default: cwd)").option("--json", "machine output").action(async (coopId, opts) => {
9891
10157
  const ctx = await sessionContext();
@@ -9919,11 +10185,42 @@ function registerCoopCommands(program3) {
9919
10185
  });
9920
10186
  consoleIo.log(`[coop] said ${opts.phase} on ${coopId}`);
9921
10187
  });
10188
+ coop.command("invite <coopId>").description("Post a human invite and optionally DM a target through the MMI Future Slack app").requiredOption("--target-user <login>", "target human GitHub login").option("--message-file <path>", "custom invite message file (or - for stdin)").option("--dm", "send a Slack DM in addition to the visible #mmi-agents invite").option("--slack-user <id>", "Slack user id for DM, used with conversations.open").option("--dm-channel <id>", "Slack DM channel id").option("--json", "machine output").action(async (coopId, opts) => {
10189
+ const ctx = await sessionContext();
10190
+ const body = opts.messageFile ? await resolveTextArg(
10191
+ { file: opts.messageFile },
10192
+ { readFile: import_promises4.readFile, readStdin },
10193
+ { value: "a message argument", file: "--message-file", noun: "message" }
10194
+ ) : void 0;
10195
+ const res = await postJson("/coop/invite", {
10196
+ coopId,
10197
+ targetLogin: opts.targetUser,
10198
+ body,
10199
+ branch: ctx.branch,
10200
+ sessionId: ctx.sessionId,
10201
+ surface: ctx.surface,
10202
+ dm: opts.dm === true,
10203
+ slackUserId: opts.slackUser,
10204
+ dmChannel: opts.dmChannel
10205
+ });
10206
+ if (opts.json) console.log(JSON.stringify(res));
10207
+ else {
10208
+ const out = res;
10209
+ const suffix = out.dm && out.dm !== "not-requested" ? `; dm ${out.dm}${out.dmReason ? ` (${out.dmReason})` : ""}` : "";
10210
+ consoleIo.log(`[coop] invited @${opts.targetUser} to ${coopId}${suffix}`);
10211
+ }
10212
+ });
9922
10213
  coop.command("status [coopId]").description("Show coop session status").option("--json", "machine output").action(async (coopId, opts) => {
9923
- const path2 = coopId ? `/coop/status?coopId=${encodeURIComponent(coopId)}` : "/coop/status";
10214
+ const path2 = coopId ? `/coop/status?coopId=${encodeURIComponent(coopId)}` : coopOpenPath();
9924
10215
  const res = await getJson(path2);
9925
10216
  if (opts.json) console.log(JSON.stringify(res));
9926
- else console.log(JSON.stringify(res, null, 2));
10217
+ else if (coopId) console.log(JSON.stringify(res, null, 2));
10218
+ else await printOpenCoops(res);
10219
+ });
10220
+ coop.command("open").description("List open coop sessions for join-or-start").option("--repo <owner/name>", "filter open sessions to one repo").option("--json", "machine output").action(async (opts) => {
10221
+ const res = await getJson(coopOpenPath(opts.repo));
10222
+ if (opts.json) console.log(JSON.stringify(res));
10223
+ else await printOpenCoops(res, opts.repo);
9927
10224
  });
9928
10225
  coop.command("pending").description("List pending coop messages for this login").option("--json", "machine output").action(async (opts) => {
9929
10226
  const pending = await fetchCoopPending();
@@ -9943,7 +10240,10 @@ function registerCoopCommands(program3) {
9943
10240
  await postJson("/coop/end", { coopId, body });
9944
10241
  consoleIo.log(`[coop] ended ${coopId}`);
9945
10242
  });
9946
- coop.command("watch <coopId>").description("Degraded poll \u2014 opt-in fallback when wake is unavailable").option("--interval <sec>", "poll interval seconds", "30").action(async (coopId, opts) => {
10243
+ coop.command("wait <coopId>").description("Bounded poll for coop replies: 1m, 2m, 3m, 5m, 10m, 30m").option("--json", "machine output").action(async (coopId, opts) => {
10244
+ await runCoopWait(coopId, consoleIo, { json: opts.json === true });
10245
+ });
10246
+ coop.command("watch <coopId>").description("Unbounded diagnostic poll for a coop session").option("--interval <sec>", "poll interval seconds", "30").action(async (coopId, opts) => {
9947
10247
  const ms = Math.max(5, Number(opts.interval) || 30) * 1e3;
9948
10248
  consoleIo.log(`[coop watch] polling ${coopId} every ${ms / 1e3}s (Ctrl+C to stop)`);
9949
10249
  for (; ; ) {
@@ -9956,9 +10256,8 @@ function registerCoopCommands(program3) {
9956
10256
  }
9957
10257
 
9958
10258
  // src/overlord.ts
9959
- var import_node_child_process8 = require("node:child_process");
9960
10259
  var import_node_fs15 = require("node:fs");
9961
- var import_node_os3 = require("node:os");
10260
+ var import_node_crypto5 = require("node:crypto");
9962
10261
  var import_node_path13 = require("node:path");
9963
10262
 
9964
10263
  // src/atomic-write.ts
@@ -9973,15 +10272,120 @@ function atomicWriteFileSync(path2, content) {
9973
10272
  var OVERLORD_DEFAULT_COUNT = 3;
9974
10273
  var OVERLORD_MIN_COUNT = 3;
9975
10274
  var OVERLORD_MAX_COUNT = 6;
10275
+ var OVERLORD_SERVANT_PARALLELISM = 6;
10276
+ function overlordServantParallelism() {
10277
+ const parsed = Number(process.env.MMI_OVERLORD_SERVANT_PARALLELISM);
10278
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.min(Math.trunc(parsed), 16);
10279
+ return OVERLORD_SERVANT_PARALLELISM;
10280
+ }
10281
+ var OVERLORD_DEFAULT_ENGINE = "fugu-api";
10282
+ var FUGU_API_DEFAULT_BASE_URL = "https://api.sakana.ai/v1";
10283
+ var FUGU_API_DEFAULT_MODEL = "fugu";
10284
+ var FUGU_API_DEFAULT_ULTRA_MODEL = "fugu-ultra";
10285
+ var FUGU_API_DEFAULT_TIMEOUT_MS = 48e4;
10286
+ var FUGU_API_DEFAULT_STARTUP_TIMEOUT_MS = 45e3;
10287
+ var FUGU_API_DEFAULT_IDLE_MS = 6e4;
10288
+ var FUGU_API_DEFAULT_MAX_TOKENS = 16e3;
10289
+ var FUGU_API_DEFAULT_MAX_ATTEMPTS = 3;
10290
+ var FUGU_API_RETRY_BASE_MS = 250;
10291
+ var FUGU_API_RETRY_CAP_MS = 8e3;
9976
10292
  var OVERLORD_HANDOFF_TIMEOUT_MS = 12e4;
10293
+ var SERVANT_REPORT_FIELDS = [
10294
+ "name",
10295
+ "role",
10296
+ "state",
10297
+ "assignment",
10298
+ "evidence",
10299
+ "changes",
10300
+ "verification",
10301
+ "blockers",
10302
+ "recommended next action"
10303
+ ];
10304
+ var SERVANT_REPORT_FIELD_SET = new Set(SERVANT_REPORT_FIELDS);
10305
+ function normalizeServantReportField(label) {
10306
+ const normalized = label.trim().toLowerCase().replace(/[`*_]+/g, "").replace(/\s+/g, " ").replace(/:$/, "");
10307
+ if (SERVANT_REPORT_FIELD_SET.has(normalized)) return normalized;
10308
+ if (normalized === "next action") return "recommended next action";
10309
+ return void 0;
10310
+ }
10311
+ function isVagueVerification(text) {
10312
+ const normalized = (text ?? "").trim().toLowerCase();
10313
+ if (!normalized) return true;
10314
+ return ["done", "complete", "completed", "fixed", "looks good", "verified", "tested", "not tested", "n/a", "na", "none"].includes(normalized);
10315
+ }
10316
+ function isCompletionState(text) {
10317
+ return /\b(done|complete|completed|fixed)\b/i.test(text ?? "");
10318
+ }
10319
+ function extractServantReportFields(text) {
10320
+ const fields = {};
10321
+ let currentField;
10322
+ const append = (field, value) => {
10323
+ const trimmed = value.trim();
10324
+ fields[field] = fields[field] ? [fields[field], trimmed].filter(Boolean).join("\n") : trimmed;
10325
+ };
10326
+ for (const rawLine of text.split(/\r?\n/)) {
10327
+ const line = rawLine.trim();
10328
+ const colon = line.match(/^(?:[-*]\s*)?([^:]{1,80}):\s*(.*)$/);
10329
+ const heading = line.match(/^#{1,6}\s+(.+?)\s*#*$/);
10330
+ const field = colon ? normalizeServantReportField(colon[1]) : heading ? normalizeServantReportField(heading[1]) : void 0;
10331
+ if (field) {
10332
+ currentField = field;
10333
+ append(field, colon ? colon[2] : "");
10334
+ continue;
10335
+ }
10336
+ if (currentField && line) append(currentField, line);
10337
+ }
10338
+ return fields;
10339
+ }
10340
+ function validateServantReport(text) {
10341
+ const fields = extractServantReportFields(text);
10342
+ const present = new Set(Object.keys(fields));
10343
+ const applies = present.size >= 2 || present.has("state") || present.has("assignment");
10344
+ if (!applies) {
10345
+ return { applies: false, ok: true, missingFields: [], emptyFields: [], unsupportedDoneClaim: false, fields };
10346
+ }
10347
+ const missingFields = SERVANT_REPORT_FIELDS.filter((field) => !present.has(field));
10348
+ const emptyFields = SERVANT_REPORT_FIELDS.filter((field) => present.has(field) && !fields[field]?.trim());
10349
+ const unsupportedDoneClaim = isCompletionState(fields.state) && isVagueVerification(fields.verification);
10350
+ return {
10351
+ applies: true,
10352
+ ok: missingFields.length === 0 && emptyFields.length === 0 && !unsupportedDoneClaim,
10353
+ missingFields,
10354
+ emptyFields,
10355
+ unsupportedDoneClaim,
10356
+ fields
10357
+ };
10358
+ }
10359
+ function formatHandoffWarnings(result) {
10360
+ if (!result.applies || result.ok) return "";
10361
+ const parts = [];
10362
+ if (result.missingFields.length > 0) parts.push(`missing ${result.missingFields.join(", ")}`);
10363
+ if (result.emptyFields.length > 0) parts.push(`empty ${result.emptyFields.join(", ")}`);
10364
+ if (result.unsupportedDoneClaim) parts.push("done/complete/fixed state without concrete verification.");
10365
+ return `Handoff warning: ${parts.join("; ")}`;
10366
+ }
10367
+ function formatServantMissionCard(options) {
10368
+ const lines = [
10369
+ `mission: ${options.mission}`,
10370
+ `scope: ${options.scope}`,
10371
+ ...options.scopeToken ? [
10372
+ `scope token: ${options.scopeToken}`,
10373
+ "token authority: attribution only; grants no host permissions"
10374
+ ] : [],
10375
+ `allowed actions: ${options.allowedActions.join("; ")}`,
10376
+ `forbidden actions: ${options.forbiddenActions.join("; ")}`,
10377
+ `required evidence: ${options.requiredEvidence.join("; ")}`,
10378
+ `verification required: ${options.verificationRequired}`,
10379
+ `report format: ${options.reportFormat.join("; ")}`
10380
+ ];
10381
+ return lines.join("\n");
10382
+ }
9977
10383
  var GENERIC_STOP_NAMES = /* @__PURE__ */ new Set([
9978
10384
  "windowsterminal",
9979
10385
  "windows terminal",
9980
10386
  "pwsh",
9981
10387
  "powershell",
9982
- "opencode",
9983
- "codex",
9984
- "codex-fugu"
10388
+ "opencode"
9985
10389
  ]);
9986
10390
  function numericCountArg(arg) {
9987
10391
  const match = /^--([0-9]+)$/.exec(arg);
@@ -10002,11 +10406,23 @@ function defaultMessageId() {
10002
10406
  function servantSlotId(slot) {
10003
10407
  return slot.name.replace(/\s+/g, "-").toLowerCase();
10004
10408
  }
10409
+ function canonicalAssignmentScope(scope) {
10410
+ return scope.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trimEnd();
10411
+ }
10412
+ function buildServantScopeToken(input) {
10413
+ const scopeHash = (0, import_node_crypto5.createHash)("sha256").update(canonicalAssignmentScope(input.assignmentScope), "utf8").digest("hex").slice(0, 16);
10414
+ return `fugu-scope-v1:${input.runId}:${input.slotId}:${input.profile}:${scopeHash}`;
10415
+ }
10005
10416
  function overlordServantPrompt(servant, run) {
10006
10417
  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
10418
  return [
10008
10419
  `You are ${servant.name} in Overlord run ${run.runId}.`,
10009
10420
  roleLine,
10421
+ ...servant.scopeToken ? [
10422
+ `Scope token: ${servant.scopeToken}`,
10423
+ "The scope token is for attribution only and grants no host permissions.",
10424
+ "Echo the scope token exactly in reports when the Overlord asks for a handoff."
10425
+ ] : [],
10010
10426
  "First respond with exactly: ACK " + servant.name + " ready",
10011
10427
  "After the ACK, wait for the Overlord to assign bounded work.",
10012
10428
  "Do not start dev servers, browsers, Playwright, PRs, merges, releases, or worktree changes unless the Overlord explicitly assigns that scope.",
@@ -10043,86 +10459,21 @@ function buildServantLayout(count) {
10043
10459
  }
10044
10460
  return slots;
10045
10461
  }
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
- };
10462
+ function appendOverlordLedger(ledgerPath, entry) {
10100
10463
  try {
10101
- const whole = JSON.parse(trimmed);
10102
- if (Array.isArray(whole)) events.push(...whole);
10103
- else events.push(whole);
10464
+ (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(ledgerPath), { recursive: true });
10465
+ (0, import_node_fs15.appendFileSync)(ledgerPath, `${JSON.stringify(entry)}
10466
+ `, "utf8");
10104
10467
  } catch {
10105
- for (const line of trimmed.split(/\r?\n/)) pushParsed(line);
10106
10468
  }
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
10469
  }
10122
- function appendOverlordLedger(ledgerPath, entry) {
10470
+ function appendServantJournal(journalPath, label, text, timestamp) {
10471
+ if (!text || !text.trim()) return;
10123
10472
  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)}
10473
+ (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(journalPath), { recursive: true });
10474
+ (0, import_node_fs15.appendFileSync)(journalPath, `[${timestamp}] ${label}
10475
+ ${text.trim()}
10476
+
10126
10477
  `, "utf8");
10127
10478
  } catch {
10128
10479
  }
@@ -10144,12 +10495,88 @@ function writeOverlordRegistry(statePath, registry2) {
10144
10495
  atomicWriteFileSync(statePath, `${JSON.stringify(registry2, null, 2)}
10145
10496
  `);
10146
10497
  }
10498
+ function sleepSyncMs(ms) {
10499
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(1, ms));
10500
+ }
10501
+ var OVERLORD_REGISTRY_LOCK_TIMEOUT_MS = 5e3;
10502
+ var OVERLORD_REGISTRY_LOCK_STALE_MS = 3e4;
10503
+ function withRegistryLock(statePath, fn) {
10504
+ const lockPath = `${statePath}.lock`;
10505
+ (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(statePath), { recursive: true });
10506
+ const deadline = Date.now() + OVERLORD_REGISTRY_LOCK_TIMEOUT_MS;
10507
+ let fd;
10508
+ for (; ; ) {
10509
+ try {
10510
+ fd = (0, import_node_fs15.openSync)(lockPath, "wx");
10511
+ break;
10512
+ } catch {
10513
+ let stale = false;
10514
+ try {
10515
+ stale = Date.now() - (0, import_node_fs15.statSync)(lockPath).mtimeMs > OVERLORD_REGISTRY_LOCK_STALE_MS;
10516
+ } catch {
10517
+ stale = false;
10518
+ }
10519
+ if (stale) {
10520
+ try {
10521
+ (0, import_node_fs15.rmSync)(lockPath, { force: true });
10522
+ } catch {
10523
+ }
10524
+ continue;
10525
+ }
10526
+ if (Date.now() > deadline) throw new Error(`Overlord registry lock is busy: ${lockPath}`);
10527
+ sleepSyncMs(25);
10528
+ }
10529
+ }
10530
+ try {
10531
+ return fn();
10532
+ } finally {
10533
+ if (fd !== void 0) (0, import_node_fs15.closeSync)(fd);
10534
+ try {
10535
+ (0, import_node_fs15.rmSync)(lockPath, { force: true });
10536
+ } catch {
10537
+ }
10538
+ }
10539
+ }
10540
+ function persistRunMutation(statePath, runId, mutate) {
10541
+ return withRegistryLock(statePath, () => {
10542
+ const fresh = readOverlordRegistry(statePath);
10543
+ const current = fresh.runs[runId];
10544
+ if (!current) throw new Error(`Overlord run ${runId} is no longer in the registry`);
10545
+ const next = mutate(current);
10546
+ writeOverlordRegistry(statePath, { ...fresh, runs: { ...fresh.runs, [runId]: next } });
10547
+ return next;
10548
+ });
10549
+ }
10550
+ function resolveOverlordSendMessage(messageArgs, messageFile) {
10551
+ const inline = messageArgs.join(" ").trim();
10552
+ if (inline) return inline;
10553
+ if (messageFile) {
10554
+ const raw = messageFile === "-" ? (0, import_node_fs15.readFileSync)(0, "utf8") : (0, import_node_fs15.readFileSync)(messageFile, "utf8");
10555
+ return raw.trim();
10556
+ }
10557
+ return "";
10558
+ }
10559
+ function computeAffectedSlots(servants, normalized, settled) {
10560
+ const targetSlots = normalized === "all" ? servants.map((s) => s.slotId) : servants.filter((s) => s.slotId === normalized || normalizeServantTarget(s.name) === normalized).map((s) => s.slotId);
10561
+ return /* @__PURE__ */ new Set([...(settled?.servantResults ?? []).map((r) => r.slotId), ...targetSlots]);
10562
+ }
10563
+ function mergeConcurrentDispatch(fresh, dispatched, settled, affected, messageId) {
10564
+ const dispatchedBySlot = new Map(dispatched.servants.map((s) => [s.slotId, s]));
10565
+ const updatedAt = [fresh.updatedAt, dispatched.updatedAt].filter(Boolean).sort().at(-1) ?? dispatched.updatedAt;
10566
+ return {
10567
+ ...fresh,
10568
+ updatedAt,
10569
+ servants: fresh.servants.map((s) => affected.has(s.slotId) ? dispatchedBySlot.get(s.slotId) ?? s : s),
10570
+ messages: [...(fresh.messages ?? []).filter((m) => m.id !== messageId), ...settled ? [settled] : []]
10571
+ };
10572
+ }
10147
10573
  function buildOverlordRun(options) {
10148
10574
  const runId = options.runId?.() ?? defaultRunId();
10149
10575
  const runToken = options.runToken?.() ?? defaultRunToken();
10150
10576
  const statePath = defaultOverlordStatePath(options.cwd);
10151
10577
  const journalDir = (0, import_node_path13.join)(options.cwd, "tmp", "overlord", runId);
10152
- const engine = options.engine ?? "codex-fugu";
10578
+ const engine = options.engine ?? OVERLORD_DEFAULT_ENGINE;
10579
+ const provider = "sakana";
10153
10580
  const timestamp = isoNow(options.now);
10154
10581
  return {
10155
10582
  runId,
@@ -10163,44 +10590,31 @@ function buildOverlordRun(options) {
10163
10590
  journalDir,
10164
10591
  ledgerPath: (0, import_node_path13.join)(journalDir, "ledger.jsonl"),
10165
10592
  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
- })),
10593
+ provider,
10594
+ servants: buildServantLayout(options.count).map((slot) => {
10595
+ const slotId = servantSlotId(slot);
10596
+ const profile = "consultation";
10597
+ return {
10598
+ ...slot,
10599
+ slotId,
10600
+ profile,
10601
+ state: "planned",
10602
+ journalPath: (0, import_node_path13.join)(journalDir, `${slotId}.log`),
10603
+ composerSubmitMode: "surface-api",
10604
+ engine,
10605
+ provider,
10606
+ eventJournalPath: (0, import_node_path13.join)(journalDir, `${slotId}.events.jsonl`),
10607
+ scopeToken: buildServantScopeToken({
10608
+ runId,
10609
+ slotId,
10610
+ profile,
10611
+ assignmentScope: options.task
10612
+ })
10613
+ };
10614
+ }),
10180
10615
  ownedResources: []
10181
10616
  };
10182
10617
  }
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
10618
  function buildOverlordStartupPlan(args, cwd) {
10205
10619
  const count = parseOverlordCount(args);
10206
10620
  const task = args.filter((arg) => numericCountArg(arg) === void 0).join(" ").trim();
@@ -10231,11 +10645,9 @@ function summarizeOverlordRun(run, probe) {
10231
10645
  state: run.state,
10232
10646
  controller,
10233
10647
  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
10648
  const progress = servantProgress(run, servant, now);
10238
10649
  if (progress !== servant.state) return { name: servant.name, state: progress };
10650
+ if (servant.pid != null && !probe.isPidAlive(servant.pid)) return { name: servant.name, state: "lost" };
10239
10651
  if (servant.state === "ready" && servant.lastAckAt && servant.composerSubmitMode !== "unknown") {
10240
10652
  return { name: servant.name, state: "ready" };
10241
10653
  }
@@ -10253,52 +10665,6 @@ function planOverlordRunStop(run) {
10253
10665
  }
10254
10666
  return { killPids, uncertain };
10255
10667
  }
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
10668
  function defaultIsPidAlive(pid) {
10303
10669
  if (!Number.isFinite(pid) || pid <= 0) return false;
10304
10670
  try {
@@ -10311,141 +10677,409 @@ function defaultIsPidAlive(pid) {
10311
10677
  function defaultKillPid(pid) {
10312
10678
  process.kill(pid);
10313
10679
  }
10314
- function shellQuote2(arg) {
10315
- return /^[A-Za-z0-9_./:-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`;
10680
+ function fuguApiTimeoutMs() {
10681
+ const parsed = Number(process.env.MMI_OVERLORD_LLM_TIMEOUT_MS);
10682
+ if (!Number.isFinite(parsed) || parsed <= 0) return FUGU_API_DEFAULT_TIMEOUT_MS;
10683
+ return Math.min(Math.max(Math.trunc(parsed), 5e3), 12e5);
10316
10684
  }
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
- }
10685
+ function fuguApiIdleMs() {
10686
+ const parsed = Number(process.env.MMI_OVERLORD_LLM_IDLE_MS);
10687
+ if (!Number.isFinite(parsed) || parsed <= 0) return FUGU_API_DEFAULT_IDLE_MS;
10688
+ return Math.min(Math.max(Math.trunc(parsed), 5e3), 6e5);
10689
+ }
10690
+ function fuguApiMaxTokens() {
10691
+ const parsed = Number(process.env.MMI_OVERLORD_LLM_MAX_TOKENS);
10692
+ if (!Number.isFinite(parsed) || parsed <= 0) return FUGU_API_DEFAULT_MAX_TOKENS;
10693
+ return Math.min(Math.max(Math.trunc(parsed), 256), 2e5);
10694
+ }
10695
+ function fuguApiStartupTimeoutMs() {
10696
+ const workTimeout = fuguApiTimeoutMs();
10697
+ const parsed = Number(process.env.MMI_OVERLORD_LLM_STARTUP_TIMEOUT_MS);
10698
+ const chosen = Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : FUGU_API_DEFAULT_STARTUP_TIMEOUT_MS;
10699
+ return Math.min(Math.max(chosen, 5e3), workTimeout);
10700
+ }
10701
+ function fuguApiConfig() {
10702
+ return {
10703
+ baseUrl: (process.env.MMI_OVERLORD_LLM_BASE_URL ?? FUGU_API_DEFAULT_BASE_URL).replace(/\/+$/, ""),
10704
+ apiKey: process.env.MMI_OVERLORD_LLM_API_KEY ?? process.env.SAKANA_API_KEY,
10705
+ model: process.env.MMI_OVERLORD_LLM_MODEL ?? FUGU_API_DEFAULT_MODEL,
10706
+ ultraModel: process.env.MMI_OVERLORD_LLM_ULTRA_MODEL ?? FUGU_API_DEFAULT_ULTRA_MODEL,
10707
+ timeoutMs: fuguApiTimeoutMs(),
10708
+ idleMs: fuguApiIdleMs(),
10709
+ maxTokens: fuguApiMaxTokens()
10710
+ };
10711
+ }
10712
+ function fuguApiModelForServant(config, servant) {
10713
+ return servant.role === "ultra" ? config.ultraModel : config.model;
10714
+ }
10715
+ function fuguApiSystemMessage(servant, run) {
10716
+ const roleLine = servant.role === "ultra" ? "You are the single Ultra Fugu servant. Take the hardest useful slice, surface risks, and give calibrated judgment." : "You are a normal Fugu servant. Take one bounded assignment at a time and report concise evidence.";
10717
+ const contract = formatServantMissionCard({
10718
+ mission: run.task,
10719
+ scope: servant.role === "ultra" ? "consult on the highest-uncertainty slice assigned by the Overlord" : "consult on one bounded slice assigned by the Overlord",
10720
+ scopeToken: servant.scopeToken,
10721
+ allowedActions: ["inspect assigned evidence", "reason from provided context", "report findings"],
10722
+ forbiddenActions: ["mutate files without explicit scope", "start stage/dev servers", "start browsers or Playwright", "open PRs or merges"],
10723
+ requiredEvidence: ["cite source paths, command results, or observations when available", "separate evidence from inference"],
10724
+ verificationRequired: "name the verification performed before any done/complete/fixed claim, or explain why verification is not applicable",
10725
+ reportFormat: [...SERVANT_REPORT_FIELDS, "scope token"]
10330
10726
  });
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 };
10727
+ return [
10728
+ `You are ${servant.name} in Overlord run ${run.runId}.`,
10729
+ roleLine,
10730
+ "The Overlord is the only coordinator.",
10731
+ "Do not claim ownership of the full mission.",
10732
+ "Do not mutate files, branches, worktrees, stage/dev servers, browsers, PRs, or releases unless the Overlord explicitly grants that scope.",
10733
+ "Gather evidence before acting.",
10734
+ "Verify before claiming done.",
10735
+ "Escalate blockers instead of looping.",
10736
+ contract
10737
+ ].join("\n");
10335
10738
  }
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");
10739
+ async function collectFuguApiPreflight() {
10740
+ const config = fuguApiConfig();
10741
+ const problems = [];
10742
+ if (!config.apiKey) problems.push("SAKANA_API_KEY or MMI_OVERLORD_LLM_API_KEY is not available");
10743
+ if (!config.baseUrl) problems.push("Fugu API base URL is not configured");
10744
+ if (!config.apiKey || !config.baseUrl) return { ok: false, problems };
10745
+ const controller = new AbortController();
10746
+ const timeout = setTimeout(() => controller.abort(), Math.min(config.timeoutMs, 15e3));
10339
10747
  try {
10340
- return (0, import_node_fs15.existsSync)(catalogPath) ? (0, import_node_fs15.readFileSync)(catalogPath, "utf8") : "";
10341
- } catch {
10342
- return "";
10748
+ const response = await fetch(`${config.baseUrl}/models`, {
10749
+ method: "GET",
10750
+ headers: { Authorization: `Bearer ${config.apiKey}` },
10751
+ signal: controller.signal
10752
+ });
10753
+ if (!response.ok) {
10754
+ problems.push(`Fugu API model probe returned HTTP ${response.status}`);
10755
+ } else {
10756
+ const body = await response.json();
10757
+ const models = new Set((body.data ?? []).map((model) => model.id).filter((id) => Boolean(id)));
10758
+ if (!models.has(config.model)) problems.push(`Fugu API model ${config.model} is not available`);
10759
+ if (!models.has(config.ultraModel)) problems.push(`Fugu API ultra model ${config.ultraModel} is not available`);
10760
+ }
10761
+ } catch (e) {
10762
+ problems.push(`Fugu API model probe failed: ${e instanceof Error ? e.message : String(e)}`);
10763
+ } finally {
10764
+ clearTimeout(timeout);
10343
10765
  }
10766
+ return { ok: problems.length === 0, problems };
10344
10767
  }
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"));
10768
+ function mapFuguApiUsage(usage) {
10769
+ if (!usage) return void 0;
10770
+ const mapped = {
10771
+ promptTokens: usage.prompt_tokens,
10772
+ completionTokens: usage.completion_tokens,
10773
+ totalTokens: usage.total_tokens
10774
+ };
10775
+ return Object.values(mapped).some((value) => typeof value === "number") ? mapped : void 0;
10348
10776
  }
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
- });
10777
+ function buildFuguRequestBody(model, messages, maxTokens) {
10778
+ return {
10779
+ model,
10780
+ messages,
10781
+ temperature: 0.2,
10782
+ max_tokens: maxTokens,
10783
+ stream: true,
10784
+ stream_options: { include_usage: true }
10785
+ };
10357
10786
  }
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
- });
10787
+ function foldFuguSse(payloads) {
10788
+ let text = "";
10789
+ let finishReason;
10790
+ let rawUsage;
10791
+ for (const payload of payloads) {
10792
+ const data = payload.trim();
10793
+ if (!data || data === "[DONE]") continue;
10794
+ let parsed;
10795
+ try {
10796
+ parsed = JSON.parse(data);
10797
+ } catch {
10798
+ continue;
10799
+ }
10800
+ const choice = parsed?.choices?.[0];
10801
+ const piece = choice?.delta?.content ?? choice?.message?.content;
10802
+ if (typeof piece === "string") text += piece;
10803
+ if (choice?.finish_reason) finishReason = choice.finish_reason;
10804
+ if (parsed?.usage) rawUsage = parsed.usage;
10805
+ }
10806
+ return { text, finishReason, usage: mapFuguApiUsage(rawUsage) };
10807
+ }
10808
+ async function defaultRunFuguApi(run, servant, message, options) {
10809
+ const config = fuguApiConfig();
10810
+ if (!config.apiKey) return { ok: false, error: "SAKANA_API_KEY or MMI_OVERLORD_LLM_API_KEY is not available" };
10811
+ const model = fuguApiModelForServant(config, servant);
10812
+ const messages = [
10813
+ ...servant.llmMessages ?? [{ role: "system", content: fuguApiSystemMessage(servant, run) }],
10814
+ { role: "user", content: message }
10815
+ ];
10816
+ const totalMs = options?.timeoutMs ?? config.timeoutMs;
10817
+ const idleMs = Math.min(config.idleMs, totalMs);
10818
+ const controller = new AbortController();
10819
+ let abortKind;
10820
+ const totalTimer = setTimeout(() => {
10821
+ abortKind = "total";
10822
+ controller.abort();
10823
+ }, totalMs);
10824
+ let idleTimer;
10825
+ let streamReader;
10826
+ const armIdle = () => {
10827
+ if (idleTimer) clearTimeout(idleTimer);
10828
+ idleTimer = setTimeout(() => {
10829
+ abortKind = "idle";
10830
+ controller.abort();
10831
+ }, idleMs);
10832
+ };
10833
+ armIdle();
10834
+ try {
10835
+ const response = await fetch(`${config.baseUrl}/chat/completions`, {
10836
+ method: "POST",
10837
+ headers: {
10838
+ Authorization: `Bearer ${config.apiKey}`,
10839
+ "Content-Type": "application/json"
10840
+ },
10841
+ body: JSON.stringify(buildFuguRequestBody(model, messages, config.maxTokens)),
10842
+ signal: controller.signal
10843
+ });
10844
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? void 0;
10845
+ if (!response.ok) {
10846
+ const errBody = await response.json().catch(() => void 0);
10847
+ return {
10848
+ ok: false,
10849
+ model,
10850
+ requestId,
10851
+ error: errBody?.error?.message ?? `Fugu API returned HTTP ${response.status}`,
10852
+ retryable: response.status === 429 || response.status >= 500
10853
+ };
10854
+ }
10855
+ if (!response.body) return { ok: false, model, requestId, error: "Fugu API returned no response body", retryable: true };
10856
+ const reader = response.body.getReader();
10857
+ streamReader = reader;
10858
+ const decoder = new TextDecoder();
10859
+ const payloads = [];
10860
+ let buffer = "";
10861
+ for (; ; ) {
10862
+ const { done, value } = await reader.read();
10863
+ if (done) break;
10864
+ armIdle();
10865
+ buffer += decoder.decode(value, { stream: true });
10866
+ const lines = buffer.split("\n");
10867
+ buffer = lines.pop() ?? "";
10868
+ for (const line of lines) {
10869
+ const trimmed = line.trim();
10870
+ if (trimmed.startsWith("data:")) payloads.push(trimmed.slice(5).trim());
10871
+ }
10872
+ }
10873
+ const tail = buffer.trim();
10874
+ if (tail.startsWith("data:")) payloads.push(tail.slice(5).trim());
10875
+ const { text: rawText, finishReason, usage } = foldFuguSse(payloads);
10876
+ const text = rawText.trim();
10877
+ if (!text) {
10878
+ const reason = finishReason === "length" ? "Fugu API hit max_tokens before emitting content (raise MMI_OVERLORD_LLM_MAX_TOKENS)" : "Fugu API returned no assistant text";
10879
+ return { ok: false, model, requestId, error: reason, finishReason, usage, retryable: false };
10880
+ }
10881
+ return {
10882
+ ok: true,
10883
+ text,
10884
+ model,
10885
+ requestId,
10886
+ finishReason,
10887
+ usage,
10888
+ messages: [...messages, { role: "assistant", content: text }]
10889
+ };
10890
+ } catch (e) {
10891
+ const aborted = e instanceof Error && e.name === "AbortError";
10892
+ if (aborted) {
10893
+ const bound = abortKind === "idle" ? `${Math.round(idleMs / 1e3)}s with no data` : `${Math.round(totalMs / 1e3)}s total`;
10894
+ return { ok: false, model, error: `Fugu API request timed out (${bound})`, retryable: false };
10895
+ }
10896
+ return { ok: false, model, error: e instanceof Error ? e.message : String(e), retryable: true };
10897
+ } finally {
10898
+ clearTimeout(totalTimer);
10899
+ if (idleTimer) clearTimeout(idleTimer);
10900
+ if (streamReader) {
10901
+ try {
10902
+ streamReader.releaseLock();
10903
+ } catch {
10904
+ }
10905
+ }
10906
+ }
10371
10907
  }
10372
- function renderCodexFuguPreflightFailure(report) {
10908
+ function renderOverlordPreflightFailure(report) {
10373
10909
  const lines = [
10374
10910
  "Overlord setup needed",
10375
- "The servant pool was not started because Codex/Fugu preflight failed.",
10911
+ "The servant pool was not started because Fugu preflight failed.",
10376
10912
  "",
10377
10913
  "Problems:",
10378
10914
  ...report.problems.map((problem) => `- ${problem}`),
10379
10915
  "",
10380
10916
  "Fixes:"
10381
10917
  ];
10382
- if (report.problems.some((problem) => problem.includes("codex is not installed"))) {
10383
- lines.push("- Install or update Codex, then confirm with `codex --version`.");
10918
+ if (report.problems.some((problem) => problem.includes("SAKANA_API_KEY") || problem.includes("Fugu API"))) {
10919
+ lines.push("- Confirm `SAKANA_API_KEY` is available and `https://api.sakana.ai/v1/models` is reachable.");
10384
10920
  }
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.");
10921
+ if (report.problems.some((problem) => problem.includes("model") || problem.includes("fugu"))) {
10922
+ lines.push("- Confirm the configured Fugu API models are exposed by `/models`.");
10399
10923
  }
10400
10924
  return lines.join("\n");
10401
10925
  }
10402
- function updateOpenCodeServant(run, servant, result, timestamp) {
10926
+ function markServantsStarting(run, timestamp) {
10927
+ return {
10928
+ ...run,
10929
+ updatedAt: timestamp,
10930
+ servants: run.servants.map((servant) => ({
10931
+ ...servant,
10932
+ state: "starting"
10933
+ }))
10934
+ };
10935
+ }
10936
+ async function mapWithConcurrency(items, limit, worker) {
10937
+ const results = new Array(items.length);
10938
+ let nextIndex = 0;
10939
+ const workerCount = Math.min(Math.max(1, limit), items.length);
10940
+ await Promise.all(Array.from({ length: workerCount }, async () => {
10941
+ while (true) {
10942
+ const index = nextIndex;
10943
+ nextIndex += 1;
10944
+ if (index >= items.length) return;
10945
+ results[index] = await worker(items[index]);
10946
+ }
10947
+ }));
10948
+ return results;
10949
+ }
10950
+ function hasCapturedFuguApiText(result) {
10951
+ return result.ok && Boolean(result.text?.trim());
10952
+ }
10953
+ function updateFuguApiServant(servant, result, timestamp) {
10954
+ const captured = hasCapturedFuguApiText(result);
10403
10955
  return {
10404
10956
  ...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,
10957
+ state: captured ? "ready" : "blocked",
10958
+ llmModel: result.model ?? servant.llmModel,
10959
+ llmMessages: result.messages ?? servant.llmMessages,
10960
+ llmRequestId: result.requestId ?? servant.llmRequestId,
10961
+ lastAckAt: captured ? timestamp : servant.lastAckAt,
10962
+ lastUsefulSignalAt: captured ? timestamp : servant.lastUsefulSignalAt,
10409
10963
  lastEventAt: timestamp,
10410
- lastMessageCompletedAt: result.ok ? timestamp : servant.lastMessageCompletedAt
10964
+ lastMessageCompletedAt: captured ? timestamp : servant.lastMessageCompletedAt
10965
+ };
10966
+ }
10967
+ function defaultSleep3(ms) {
10968
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
10969
+ }
10970
+ function fuguApiRetryConfig(sleep) {
10971
+ const parsed = Number(process.env.MMI_OVERLORD_LLM_MAX_ATTEMPTS);
10972
+ const maxAttempts = Number.isFinite(parsed) && parsed >= 1 ? Math.min(Math.trunc(parsed), 6) : FUGU_API_DEFAULT_MAX_ATTEMPTS;
10973
+ return { maxAttempts, baseDelayMs: FUGU_API_RETRY_BASE_MS, sleep };
10974
+ }
10975
+ function fuguApiBackoffMs(attempt, baseDelayMs) {
10976
+ const exponential = baseDelayMs * 2 ** (attempt - 1);
10977
+ const capped = Math.min(exponential, FUGU_API_RETRY_CAP_MS);
10978
+ const jitter = Math.random() * capped * 0.25;
10979
+ return Math.round(capped + jitter);
10980
+ }
10981
+ async function callFuguApiRunner(runFuguApi, run, servant, message, retry, callOptions) {
10982
+ const maxAttempts = Math.max(1, retry?.maxAttempts ?? 1);
10983
+ const baseDelayMs = retry?.baseDelayMs ?? FUGU_API_RETRY_BASE_MS;
10984
+ const sleep = retry?.sleep ?? defaultSleep3;
10985
+ let attempt = 0;
10986
+ for (; ; ) {
10987
+ attempt += 1;
10988
+ let result;
10989
+ try {
10990
+ result = await runFuguApi(run, servant, message, callOptions);
10991
+ } catch (e) {
10992
+ result = { ok: false, error: e instanceof Error ? e.message : String(e), retryable: true };
10993
+ }
10994
+ if (result.ok || !result.retryable || attempt >= maxAttempts) {
10995
+ return { ...result, attempts: attempt };
10996
+ }
10997
+ await sleep(fuguApiBackoffMs(attempt, baseDelayMs));
10998
+ }
10999
+ }
11000
+ function seedLazyServant(servant, run, timestamp) {
11001
+ const config = fuguApiConfig();
11002
+ const llmMessages = servant.llmMessages ?? [{ role: "system", content: fuguApiSystemMessage(servant, run) }];
11003
+ return {
11004
+ ...servant,
11005
+ state: "ready",
11006
+ llmModel: fuguApiModelForServant(config, servant),
11007
+ llmMessages,
11008
+ lastAckAt: timestamp,
11009
+ lastUsefulSignalAt: timestamp,
11010
+ lastEventAt: timestamp
10411
11011
  };
10412
11012
  }
10413
- function startOpenCodeServants(run, runOpenCode, now) {
11013
+ async function startFuguApiServants(run, runFuguApi, now, retry) {
11014
+ const startupCallOptions = { timeoutMs: fuguApiStartupTimeoutMs() };
11015
+ const verifiedTargets = run.servants.filter((servant) => servant.role !== "ultra");
11016
+ const results = await mapWithConcurrency(verifiedTargets, overlordServantParallelism(), async (servant) => ({
11017
+ servant,
11018
+ result: await callFuguApiRunner(runFuguApi, run, servant, overlordServantPrompt(servant, run), retry, startupCallOptions)
11019
+ }));
10414
11020
  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
- });
11021
+ const bySlot = /* @__PURE__ */ new Map();
11022
+ for (const { servant, result } of results) {
11023
+ bySlot.set(servant.slotId, result);
11024
+ if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: timestamp, kind: "servant-start", ownerSlotId: servant.slotId, ok: hasCapturedFuguApiText(result), model: result.model, requestId: result.requestId, responseText: result.text, error: result.error, usage: result.usage });
11025
+ appendServantJournal(servant.journalPath, "consultation", result.text, timestamp);
11026
+ }
11027
+ const servants = run.servants.map((servant) => servant.role === "ultra" ? seedLazyServant(servant, run, timestamp) : updateFuguApiServant(servant, bySlot.get(servant.slotId) ?? { ok: false, error: "missing Fugu API result" }, timestamp));
10420
11028
  return { ...run, state: "active", updatedAt: timestamp, servants };
10421
11029
  }
10422
- function dispatchOpenCodeMessage(run, message, runOpenCode, now) {
10423
- const timestamp = isoNow(now);
11030
+ async function dispatchFuguApiMessage(run, message, runFuguApi, now, retry) {
11031
+ const startedAt = isoNow(now);
10424
11032
  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;
11033
+ const results = await mapWithConcurrency(targets, overlordServantParallelism(), async (servant) => ({
11034
+ servant,
11035
+ result: await callFuguApiRunner(runFuguApi, run, servant, message.text, retry)
11036
+ }));
11037
+ const completedAt = isoNow(now);
11038
+ const successCount = results.filter(({ result }) => hasCapturedFuguApiText(result)).length;
11039
+ const failureCount = Math.max(0, targets.length - successCount);
11040
+ const state = targets.length > 0 && failureCount === 0 ? "completed" : successCount > 0 && message.target === "all" ? "partial" : "failed";
11041
+ const responses = results.filter(({ result }) => result.text).map(({ servant, result }) => `${servant.slotId}: ${result.text}`);
11042
+ const servantResults = results.map(({ servant, result }) => {
11043
+ if (hasCapturedFuguApiText(result)) {
11044
+ const warning = formatHandoffWarnings(validateServantReport(result.text ?? ""));
11045
+ return {
11046
+ slotId: servant.slotId,
11047
+ state: "completed",
11048
+ responseText: result.text?.trim(),
11049
+ handoffWarnings: warning ? [warning] : void 0,
11050
+ usage: result.usage
11051
+ };
11052
+ }
11053
+ return {
11054
+ slotId: servant.slotId,
11055
+ state: "failed",
11056
+ failureReason: result.error ?? "Fugu API servant returned no text"
11057
+ };
11058
+ });
11059
+ const bySlot = /* @__PURE__ */ new Map();
11060
+ for (const { servant, result } of results) {
11061
+ bySlot.set(servant.slotId, result);
11062
+ if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: completedAt, kind: "message-response", messageId: message.id, ownerSlotId: servant.slotId, ok: hasCapturedFuguApiText(result), model: result.model, requestId: result.requestId, responseText: result.text, error: result.error, usage: result.usage });
11063
+ appendServantJournal(servant.journalPath, `message ${message.id}`, result.text, completedAt);
11064
+ }
10427
11065
  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);
11066
+ const result = bySlot.get(servant.slotId);
11067
+ return result ? updateFuguApiServant(servant, result, completedAt) : servant;
10434
11068
  });
10435
11069
  const nextMessage = {
10436
11070
  ...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,
11071
+ state,
11072
+ startedAt,
11073
+ completedAt: state === "completed" || state === "partial" ? completedAt : void 0,
11074
+ failedAt: state === "failed" ? completedAt : void 0,
11075
+ ackText: state === "completed" ? "Fugu API response captured" : state === "partial" ? "Fugu API partial response captured" : void 0,
10442
11076
  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(",")
11077
+ failureReason: state === "completed" ? void 0 : state === "partial" ? `${failureCount} of ${targets.length} Fugu API servant calls failed or returned no text` : "one or more Fugu API servant calls failed or returned no text",
11078
+ servantResults
10445
11079
  };
10446
11080
  return {
10447
11081
  ...run,
10448
- updatedAt: timestamp,
11082
+ updatedAt: completedAt,
10449
11083
  servants: nextServants,
10450
11084
  messages: [...(run.messages ?? []).filter((m) => m.id !== message.id), nextMessage]
10451
11085
  };
@@ -10464,38 +11098,142 @@ function hasServantTarget(run, target) {
10464
11098
  (servant) => servant.slotId === normalized || normalizeServantTarget(servant.name) === normalized
10465
11099
  );
10466
11100
  }
10467
- function messageProgress(message, now = /* @__PURE__ */ new Date(), timeoutMs = OVERLORD_HANDOFF_TIMEOUT_MS) {
11101
+ function messageProgress(message, now = /* @__PURE__ */ new Date(), timeoutMs = OVERLORD_HANDOFF_TIMEOUT_MS, ownerAlive) {
11102
+ if (message.state === "partial") return "partial";
10468
11103
  if (message.completedAt || message.state === "completed") return "completed";
10469
11104
  if (message.failedAt || message.state === "failed") return "failed";
10470
11105
  const startedAt = message.startedAt ?? message.deliveredAt;
10471
11106
  if (!startedAt) return "queued";
10472
11107
  const elapsed = now.getTime() - new Date(startedAt).getTime();
10473
- return Number.isFinite(elapsed) && elapsed >= timeoutMs ? "stalled" : "started";
11108
+ if (!(Number.isFinite(elapsed) && elapsed >= timeoutMs)) return "started";
11109
+ return ownerAlive === false ? "orphaned" : "stalled";
11110
+ }
11111
+ function messageElapsedMs(message, now = /* @__PURE__ */ new Date()) {
11112
+ const startedAt = message.startedAt ?? message.deliveredAt;
11113
+ if (!startedAt) return void 0;
11114
+ const elapsed = now.getTime() - new Date(startedAt).getTime();
11115
+ return Number.isFinite(elapsed) && elapsed >= 0 ? elapsed : void 0;
11116
+ }
11117
+ function reconcileOverlordRun(run, ownerAlive, now = /* @__PURE__ */ new Date()) {
11118
+ let changed = 0;
11119
+ const messages = (run.messages ?? []).map((message) => {
11120
+ if (messageProgress(message, now, OVERLORD_HANDOFF_TIMEOUT_MS, ownerAlive) === "orphaned") {
11121
+ changed += 1;
11122
+ return {
11123
+ ...message,
11124
+ state: "failed",
11125
+ failedAt: now.toISOString(),
11126
+ failureReason: message.failureReason ?? "orphaned: Overlord controller is not alive"
11127
+ };
11128
+ }
11129
+ return message;
11130
+ });
11131
+ return changed > 0 ? { run: { ...run, messages }, changed } : { run, changed: 0 };
10474
11132
  }
10475
11133
  function servantProgress(run, servant, now = /* @__PURE__ */ new Date()) {
10476
11134
  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;
11135
+ return relevant.some((message) => {
11136
+ const progress = messageProgress(message, now);
11137
+ return progress === "stalled" || progress === "orphaned";
11138
+ }) ? "stalled-after-delivery" : servant.state;
11139
+ }
11140
+ function humanSafeText(text, maxLength = 160) {
11141
+ const redacted = (text ?? "").replace(/\bBearer\s+\S+/gi, "[redacted]").replace(/\b(api[_-]?key|token|secret)=\S+/gi, "$1=[redacted]").replace(/\b(sk|sakana|mmi)[-_][A-Za-z0-9_-]{10,}\b/g, "[redacted]").replace(/\s+/g, " ").trim();
11142
+ if (redacted.length <= maxLength) return redacted;
11143
+ return `${redacted.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
11144
+ }
11145
+ function renderOverlordSendSummary(message, statePath) {
11146
+ const results = message?.servantResults ?? [];
11147
+ const completed = results.filter((result) => result.state === "completed").length;
11148
+ const blocked = results.filter((result) => result.state === "failed").length;
11149
+ const state = message?.state ?? "failed";
11150
+ const lines = [
11151
+ `Message: ${message?.id ?? "unknown"}`,
11152
+ `Target: ${message?.target ?? "unknown"}`,
11153
+ `Result: ${state} - ${completed} completed, ${blocked} blocked.`
11154
+ ];
11155
+ for (const result of results.filter((item) => item.state === "failed").slice(0, 3)) {
11156
+ lines.push(`Blocked: ${result.slotId} - ${humanSafeText(result.failureReason ?? "no reason reported")}`);
11157
+ }
11158
+ const hidden = Math.max(0, blocked - 3);
11159
+ if (hidden > 0) lines.push(`Blocked: +${hidden} more`);
11160
+ lines.push(`State: ${statePath}`);
11161
+ return lines.join("\n");
10478
11162
  }
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");
11163
+ function countValues(values, value) {
11164
+ return values.filter((item) => item === value).length;
11165
+ }
11166
+ function latestServantFailureReason(run, slotId) {
11167
+ for (const message of [...run.messages ?? []].reverse()) {
11168
+ const result = message.servantResults?.find((item) => item.slotId === slotId && item.state === "failed");
11169
+ if (result?.failureReason?.trim()) return result.failureReason;
11170
+ }
11171
+ return void 0;
11172
+ }
11173
+ function affectedServantStatusLabel(state, reason) {
11174
+ if (state === "stalled-after-delivery") return "Stalled";
11175
+ if (/\btimed?\s*out\b/i.test(reason ?? "")) return "Timed out";
11176
+ return "Blocked";
11177
+ }
11178
+ function renderOverlordStatus(summary, run, now = /* @__PURE__ */ new Date()) {
11179
+ const servantStates = summary.servants.map((servant) => servant.state);
11180
+ const messageStates = (run.messages ?? []).map((message) => messageProgress(message, now));
11181
+ const activeMessages = messageStates.filter((state) => state === "queued" || state === "started" || state === "stalled").length;
11182
+ const blockedServants = summary.servants.filter((servant) => servant.state === "blocked" || servant.state === "stalled-after-delivery");
11183
+ const latest = (run.messages ?? []).at(-1);
11184
+ const latestState = latest ? messageProgress(latest, now) : void 0;
10482
11185
  return [
10483
11186
  `Overlord run ${summary.runId}`,
10484
11187
  `State: ${summary.state}`,
10485
- `Task: ${run.task || "not provided yet"}`,
10486
- `Controller: ${summary.controller}`,
10487
- "Servants:",
10488
- servants,
10489
- ...messages ? ["Messages:", messages] : []
11188
+ `Mission: ${humanSafeText(run.task || "not provided yet")}`,
11189
+ `Servants: ${countValues(servantStates, "ready")} ready, ${countValues(servantStates, "blocked") + countValues(servantStates, "stalled-after-delivery")} blocked, ${countValues(servantStates, "planned")} planned.`,
11190
+ `Messages: ${countValues(messageStates, "completed")} completed, ${countValues(messageStates, "partial")} partial, ${countValues(messageStates, "failed")} failed, ${activeMessages} active.`,
11191
+ ...blockedServants.slice(0, 3).map((servant) => {
11192
+ const slotId = run.servants.find((item) => item.name === servant.name)?.slotId;
11193
+ const reason = slotId ? latestServantFailureReason(run, slotId) : void 0;
11194
+ const label = affectedServantStatusLabel(servant.state, reason);
11195
+ const detail = reason ?? (servant.state === "stalled-after-delivery" ? "no recent update" : void 0);
11196
+ return `${label}: ${servant.name}${detail ? ` - ${humanSafeText(detail)}` : ""}`;
11197
+ }),
11198
+ ...blockedServants.length > 3 ? [`Blocked: +${blockedServants.length - 3} more`] : [],
11199
+ ...latest && latestState ? [`Latest: ${latest.id} ${latest.target} ${latestState}${latest.failureReason ? ` - ${humanSafeText(latest.failureReason)}` : ""}`] : [],
11200
+ `Coordinator: ${summary.controller}`
10490
11201
  ].join("\n");
10491
11202
  }
10492
11203
  function usefulJournalLines(text) {
10493
11204
  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
11205
  }
11206
+ var HANDOFF_SIGNAL = /\b(handoff|evidence|recommend(?:ed|ation|s)?|improvement|suggest(?:ion|s)?|next action|proposal|file)\b|`[^`]+`/i;
11207
+ var HANDOFF_MIN_RESPONSE_CHARS = 12;
11208
+ function parseJournalBlocks(raw) {
11209
+ const blocks = [];
11210
+ let current = null;
11211
+ for (const line of raw.split(/\r?\n/)) {
11212
+ const header = line.match(/^\[[^\]]+\]\s+(.+)$/);
11213
+ if (header) {
11214
+ if (current) blocks.push(current);
11215
+ current = { label: header[1].trim(), body: "" };
11216
+ } else if (current) {
11217
+ current.body += (current.body ? "\n" : "") + line;
11218
+ }
11219
+ }
11220
+ if (current) blocks.push(current);
11221
+ return blocks;
11222
+ }
11223
+ function journalHasHandoff(raw, lines) {
11224
+ for (const block of parseJournalBlocks(raw)) {
11225
+ if (!block.label.startsWith("message ")) continue;
11226
+ const body = block.body.trim();
11227
+ if (body.length >= HANDOFF_MIN_RESPONSE_CHARS) return true;
11228
+ if (validateServantReport(body).applies) return true;
11229
+ }
11230
+ return lines.some((line) => HANDOFF_SIGNAL.test(line));
11231
+ }
10495
11232
  function servantJournalSummary(servant) {
10496
11233
  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)) };
11234
+ const raw = (0, import_node_fs15.readFileSync)(servant.journalPath, "utf8");
11235
+ const lines = usefulJournalLines(raw);
11236
+ return { lines, hasHandoff: journalHasHandoff(raw, lines) };
10499
11237
  } catch {
10500
11238
  return { lines: [], hasHandoff: false };
10501
11239
  }
@@ -10519,7 +11257,11 @@ function runJson(run, extra = {}) {
10519
11257
  pid: servant.pid,
10520
11258
  journalPath: servant.journalPath,
10521
11259
  engine: servant.engine,
10522
- opencodeSessionId: servant.opencodeSessionId
11260
+ opencodeSessionId: servant.opencodeSessionId,
11261
+ scopeToken: servant.scopeToken,
11262
+ llmModel: servant.llmModel,
11263
+ llmRequestId: servant.llmRequestId,
11264
+ llmConversationLength: servant.llmMessages?.length
10523
11265
  })),
10524
11266
  ...extra
10525
11267
  };
@@ -10530,27 +11272,33 @@ function wantsJson(options, command) {
10530
11272
  function countArgsFromOptions(options) {
10531
11273
  return ["3", "4", "5", "6"].filter((key) => options[key]).map((key) => `--${key}`);
10532
11274
  }
11275
+ function percentile(values, p) {
11276
+ if (values.length === 0) return 0;
11277
+ const sorted = [...values].sort((a, b) => a - b);
11278
+ const rank = Math.ceil(p / 100 * sorted.length);
11279
+ return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
11280
+ }
10533
11281
  function registerOverlordCommands(program3, deps = {}) {
10534
11282
  const out = deps.out ?? ((text) => process.stdout.write(text));
10535
11283
  const err = deps.err ?? ((text) => process.stderr.write(text));
10536
11284
  const cwd = deps.cwd ?? (() => process.cwd());
10537
11285
  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;
11286
+ const fuguApiPreflight = deps.fuguApiPreflight ?? collectFuguApiPreflight;
11287
+ const runFuguApi = deps.runFuguApi ?? defaultRunFuguApi;
11288
+ const fuguApiRetry = fuguApiRetryConfig(deps.sleep ?? defaultSleep3);
10542
11289
  const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
10543
11290
  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) => {
11291
+ 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: fugu-api", OVERLORD_DEFAULT_ENGINE).option("--json", "print machine-readable output").action(async (task = [], options) => {
10545
11292
  try {
10546
11293
  const args = [...countArgsFromOptions(options), ...task];
10547
11294
  const root = cwd();
10548
11295
  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();
11296
+ const engine = `${options.engine ?? OVERLORD_DEFAULT_ENGINE}`;
11297
+ if (engine !== "fugu-api") throw new Error("--engine must be fugu-api");
11298
+ if (!options.json) out("Starting Overlord...\n");
11299
+ const preflightReport = await fuguApiPreflight();
10552
11300
  if (!preflightReport.ok) {
10553
- err(`${renderCodexFuguPreflightFailure(preflightReport)}
11301
+ err(`${renderOverlordPreflightFailure(preflightReport)}
10554
11302
  `);
10555
11303
  process.exitCode = 1;
10556
11304
  return;
@@ -10570,30 +11318,20 @@ function registerOverlordCommands(program3, deps = {}) {
10570
11318
  runToken: deps.runToken
10571
11319
  });
10572
11320
  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
- }
11321
+ if (!options.json) out(`Spawning ${run.servants.length} Fugu (1 ultra, ${run.servants.length - 1} normal)...
11322
+ `);
11323
+ run = markServantsStarting(run, isoNow(now));
11324
+ writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
11325
+ run = await startFuguApiServants(run, runFuguApi, now, fuguApiRetry);
11326
+ writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
10587
11327
  if (options.json) out(`${JSON.stringify(runJson(run, { nextPhase: "consult-servants" }), null, 2)}
10588
11328
  `);
10589
11329
  else {
11330
+ const ready = run.servants.filter((servant) => servant.state === "ready").length;
10590
11331
  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."
11332
+ `Ready: ${ready}/${run.servants.length}.`,
11333
+ `Mission: ${humanSafeText(run.task || "not provided yet")}`,
11334
+ `Run: ${run.runId}`
10597
11335
  ].join("\n")}
10598
11336
  `);
10599
11337
  }
@@ -10606,7 +11344,7 @@ function registerOverlordCommands(program3, deps = {}) {
10606
11344
  process.exitCode = 1;
10607
11345
  }
10608
11346
  });
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) => {
11347
+ overlord.command("send").description("queue an assignment or redirect for active Overlord servant conversations").argument("<target>", "servant slot id/name, or all").argument("[message...]", "message to deliver to the servant session").option("--message-file <path>", "read the message from a UTF-8 file, or - for stdin (prefer a path; #1511)").option("--json", "print machine-readable output").action(async (target, message = [], options, command) => {
10610
11348
  const json = wantsJson(options, command);
10611
11349
  try {
10612
11350
  const statePath = defaultOverlordStatePath(cwd());
@@ -10615,8 +11353,9 @@ function registerOverlordCommands(program3, deps = {}) {
10615
11353
  if (!run) throw new Error("no active Overlord run found");
10616
11354
  const normalized = normalizeServantTarget(target);
10617
11355
  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");
11356
+ const text = resolveOverlordSendMessage(message, typeof options.messageFile === "string" ? options.messageFile : void 0);
11357
+ if (!text) throw new Error("message is required (pass it inline or via --message-file)");
11358
+ if (run.engine !== "fugu-api") throw new Error("active Overlord run does not use the fugu-api engine");
10620
11359
  const timestamp = isoNow(now);
10621
11360
  const queued = {
10622
11361
  id: deps.runId?.() ?? defaultMessageId(),
@@ -10626,35 +11365,26 @@ function registerOverlordCommands(program3, deps = {}) {
10626
11365
  queuedAt: timestamp,
10627
11366
  state: "queued"
10628
11367
  };
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)}
11368
+ if (!json) out(`Sending assignment to ${normalized}...
11369
+ Awaiting Fugu responses...
10636
11370
  `);
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,
11371
+ const startedMessage = { ...queued, state: "started", startedAt: timestamp };
11372
+ const started = persistRunMutation(statePath, run.runId, (fresh) => ({
11373
+ ...fresh,
10645
11374
  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 };
11375
+ messages: [...(fresh.messages ?? []).filter((m) => m.id !== queued.id), startedMessage]
11376
+ }));
11377
+ const dispatched = await dispatchFuguApiMessage(started, queued, runFuguApi, now, fuguApiRetry);
11378
+ const settled = (dispatched.messages ?? []).find((m) => m.id === queued.id);
11379
+ const affected = computeAffectedSlots(dispatched.servants, normalized, settled);
11380
+ persistRunMutation(statePath, run.runId, (fresh) => mergeConcurrentDispatch(fresh, dispatched, settled, affected, queued.id));
11381
+ const ok = settled?.state === "completed" || settled?.state === "partial";
11382
+ const payload = { ok, runId: run.runId, target: normalized, messageId: queued.id, state: settled?.state, responseText: settled?.responseText, failureReason: settled?.failureReason, servantResults: settled?.servantResults, statePath };
10653
11383
  if (json) out(`${JSON.stringify(payload, null, 2)}
10654
11384
  `);
10655
- else out(`Queued message ${queued.id} for ${normalized}.
10656
- State: ${statePath}
11385
+ else out(`${renderOverlordSendSummary(settled, statePath)}
10657
11386
  `);
11387
+ if (!ok) process.exitCode = 1;
10658
11388
  } catch (e) {
10659
11389
  const messageText = e instanceof Error ? e.message : String(e);
10660
11390
  if (json) out(`${JSON.stringify({ ok: false, error: messageText }, null, 2)}
@@ -10680,10 +11410,13 @@ State: ${payload2.statePath}
10680
11410
  }
10681
11411
  const summary = summarizeOverlordRun(run, { isPidAlive, now });
10682
11412
  const current = now();
11413
+ const ownerAlive = summary.controller !== "lost";
11414
+ const { run: reconciledRun, changed } = reconcileOverlordRun(run, ownerAlive, current);
11415
+ if (changed > 0) persistRunMutation(statePath, run.runId, (fresh) => reconcileOverlordRun(fresh, ownerAlive, current).run);
10683
11416
  const payload = {
10684
11417
  ...summary,
10685
11418
  statePath,
10686
- task: run.task,
11419
+ task: reconciledRun.task,
10687
11420
  engine: run.engine,
10688
11421
  ledgerPath: run.ledgerPath,
10689
11422
  sessions: run.servants.map((servant) => ({
@@ -10691,14 +11424,20 @@ State: ${payload2.statePath}
10691
11424
  slotId: servant.slotId,
10692
11425
  engine: servant.engine,
10693
11426
  opencodeSessionId: servant.opencodeSessionId,
11427
+ scopeToken: servant.scopeToken,
11428
+ llmModel: servant.llmModel,
11429
+ llmRequestId: servant.llmRequestId,
11430
+ llmConversationLength: servant.llmMessages?.length,
10694
11431
  eventJournalPath: servant.eventJournalPath,
10695
11432
  lastEventAt: servant.lastEventAt,
10696
11433
  lastMessageCompletedAt: servant.lastMessageCompletedAt
10697
11434
  })),
10698
- messages: (run.messages ?? []).map((message) => ({
11435
+ messages: (reconciledRun.messages ?? []).map((message) => ({
10699
11436
  id: message.id,
10700
11437
  target: message.target,
10701
- state: messageProgress(message, current),
11438
+ state: messageProgress(message, current, OVERLORD_HANDOFF_TIMEOUT_MS, ownerAlive),
11439
+ elapsedMs: messageElapsedMs(message, current),
11440
+ boundMs: OVERLORD_HANDOFF_TIMEOUT_MS,
10702
11441
  queuedAt: message.queuedAt ?? message.createdAt,
10703
11442
  startedAt: message.startedAt ?? message.deliveredAt,
10704
11443
  completedAt: message.completedAt,
@@ -10711,7 +11450,7 @@ State: ${payload2.statePath}
10711
11450
  };
10712
11451
  if (json) out(`${JSON.stringify(payload, null, 2)}
10713
11452
  `);
10714
- else out(`${renderOverlordStatus(summary, run)}
11453
+ else out(`${renderOverlordStatus(summary, reconciledRun, current)}
10715
11454
  State: ${statePath}
10716
11455
  `);
10717
11456
  });
@@ -10794,17 +11533,128 @@ State: ${payload2.statePath}
10794
11533
  `);
10795
11534
  }
10796
11535
  });
11536
+ overlord.command("canary").description("native Fugu API smoke: summon a small pool, verify a bounded response, stop cleanly").option("--3", "one Ultra and two normal Fugus").option("--4", "one Ultra and three normal Fugus").option("--5", "one Ultra and four normal Fugus").option("--6", "one Ultra and five normal Fugus").option("--json", "print machine-readable output").option("--repeat <n>", "run the smoke N times and report p50/p90 elapsed latency", "1").action(async (options, command) => {
11537
+ const json = wantsJson(options, command);
11538
+ const repeat = Math.min(Math.max(1, Math.trunc(Number(options.repeat) || 1)), 20);
11539
+ const countArgs = countArgsFromOptions(options);
11540
+ try {
11541
+ if (!json) out("Overlord canary starting.\nChecking Fugu API...\n");
11542
+ const preflight2 = await fuguApiPreflight();
11543
+ if (!preflight2.ok) {
11544
+ if (json) out(`${JSON.stringify({ ok: false, phase: "preflight", problems: preflight2.problems }, null, 2)}
11545
+ `);
11546
+ else err(`${renderOverlordPreflightFailure(preflight2)}
11547
+ `);
11548
+ process.exitCode = 1;
11549
+ return;
11550
+ }
11551
+ const runOnce = async (verbose) => {
11552
+ const startedAtMs = now().getTime();
11553
+ const plan2 = buildOverlordStartupPlan(countArgs, cwd());
11554
+ let run = buildOverlordRun({ task: "overlord canary smoke", cwd: cwd(), count: plan2.count, engine: "fugu-api", now, runId: deps.runId, runToken: deps.runToken });
11555
+ if (verbose) out(`Summoning ${run.servants.length} Fugu servants...
11556
+ `);
11557
+ run = markServantsStarting(run, isoNow(now));
11558
+ run = await startFuguApiServants(run, runFuguApi, now, fuguApiRetry);
11559
+ const ready = run.servants.filter((servant) => servant.state === "ready").length;
11560
+ const problems = [];
11561
+ let responded = 0;
11562
+ let evidence;
11563
+ if (ready === run.servants.length) {
11564
+ if (verbose) out("Sending canary probe...\n");
11565
+ const timestamp = isoNow(now);
11566
+ const probe = {
11567
+ id: `${deps.runId?.() ?? defaultMessageId()}-canary`,
11568
+ target: "all",
11569
+ text: "Reply with the single word READY and nothing else.",
11570
+ createdAt: timestamp,
11571
+ queuedAt: timestamp,
11572
+ state: "queued"
11573
+ };
11574
+ run = await dispatchFuguApiMessage(run, probe, runFuguApi, now, fuguApiRetry);
11575
+ const settled = (run.messages ?? []).find((message) => message.id === probe.id);
11576
+ const results = settled?.servantResults ?? [];
11577
+ responded = results.filter((result) => result.state === "completed" && result.responseText?.trim()).length;
11578
+ evidence = results.find((result) => result.responseText?.trim())?.responseText?.trim();
11579
+ if (responded !== run.servants.length) problems.push(`only ${responded}/${run.servants.length} servants returned text`);
11580
+ } else {
11581
+ problems.push(`only ${ready}/${run.servants.length} servants became ready`);
11582
+ }
11583
+ const stopPlan = planOverlordRunStop(run);
11584
+ for (const pid of stopPlan.killPids) {
11585
+ try {
11586
+ killPid(pid);
11587
+ } catch {
11588
+ }
11589
+ }
11590
+ const cleanup = { stopped: stopPlan.killPids.length, uncertain: stopPlan.uncertain.length };
11591
+ if (cleanup.uncertain > 0) problems.push(`${cleanup.uncertain} uncertain resource(s) left untouched`);
11592
+ return { ok: problems.length === 0, runId: run.runId, requested: run.servants.length, ready, responded, elapsedMs: Math.max(0, now().getTime() - startedAtMs), evidence, cleanup, problems };
11593
+ };
11594
+ const runs = [];
11595
+ for (let i = 0; i < repeat; i += 1) {
11596
+ if (!json && repeat > 1) out(`Canary run ${i + 1}/${repeat}...
11597
+ `);
11598
+ runs.push(await runOnce(!json && repeat === 1));
11599
+ }
11600
+ if (repeat === 1) {
11601
+ const r = runs[0];
11602
+ if (json) out(`${JSON.stringify({ ok: r.ok, runId: r.runId, requested: r.requested, ready: r.ready, responded: r.responded, elapsedMs: r.elapsedMs, evidence: r.evidence, cleanup: r.cleanup, problems: r.problems }, null, 2)}
11603
+ `);
11604
+ else out(`${[
11605
+ r.ok ? "Canary OK." : "Canary FAILED.",
11606
+ `Ready: ${r.ready}/${r.requested}; Responded: ${r.responded}/${r.requested}`,
11607
+ `Cleanup: stopped ${r.cleanup.stopped}, uncertain ${r.cleanup.uncertain}`,
11608
+ ...r.problems.length ? [`Problems: ${r.problems.join("; ")}`] : []
11609
+ ].join("\n")}
11610
+ `);
11611
+ if (!r.ok) process.exitCode = 1;
11612
+ return;
11613
+ }
11614
+ const elapsed = runs.map((r) => r.elapsedMs);
11615
+ const passes = runs.filter((r) => r.ok).length;
11616
+ const requested = runs[0].requested;
11617
+ const ok = passes === runs.length;
11618
+ const summary = {
11619
+ ok,
11620
+ repeat,
11621
+ passes,
11622
+ requested,
11623
+ elapsedMs: { p50: percentile(elapsed, 50), p90: percentile(elapsed, 90), min: Math.min(...elapsed), max: Math.max(...elapsed) },
11624
+ readyRate: runs.reduce((sum, r) => sum + r.ready, 0) / (runs.length * requested),
11625
+ respondedRate: runs.reduce((sum, r) => sum + r.responded, 0) / (runs.length * requested),
11626
+ problems: runs.flatMap((r, i) => r.problems.map((problem) => `run ${i + 1}: ${problem}`))
11627
+ };
11628
+ if (json) out(`${JSON.stringify(summary, null, 2)}
11629
+ `);
11630
+ else out(`${[
11631
+ ok ? `Canary bench OK (${passes}/${repeat} passed).` : `Canary bench: ${passes}/${repeat} passed.`,
11632
+ `Elapsed p50 ${summary.elapsedMs.p50}ms, p90 ${summary.elapsedMs.p90}ms (min ${summary.elapsedMs.min}, max ${summary.elapsedMs.max})`,
11633
+ `Ready rate ${(summary.readyRate * 100).toFixed(0)}%, responded rate ${(summary.respondedRate * 100).toFixed(0)}%`,
11634
+ ...summary.problems.length ? [`Problems: ${summary.problems.join("; ")}`] : []
11635
+ ].join("\n")}
11636
+ `);
11637
+ if (!ok) process.exitCode = 1;
11638
+ } catch (e) {
11639
+ const message = e instanceof Error ? e.message : String(e);
11640
+ if (json) out(`${JSON.stringify({ ok: false, error: message }, null, 2)}
11641
+ `);
11642
+ else err(`overlord canary: ${message}
11643
+ `);
11644
+ process.exitCode = 1;
11645
+ }
11646
+ });
10797
11647
  return overlord;
10798
11648
  }
10799
11649
 
10800
11650
  // src/throttle-commands.ts
10801
- var import_node_child_process9 = require("node:child_process");
11651
+ var import_node_child_process8 = require("node:child_process");
10802
11652
  var import_node_fs16 = require("node:fs");
10803
11653
  var import_node_path14 = require("node:path");
10804
11654
  var THROTTLE_TRACE_REL = (0, import_node_path14.join)(".mmi", "throttle", "trace.jsonl");
10805
11655
  function resolveRepoGitRoot(cwd = process.cwd()) {
10806
11656
  try {
10807
- const root = (0, import_node_child_process9.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
11657
+ const root = (0, import_node_child_process8.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
10808
11658
  encoding: "utf8",
10809
11659
  timeout: 5e3
10810
11660
  }).trim();
@@ -10953,7 +11803,7 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
10953
11803
  }
10954
11804
 
10955
11805
  // src/board.ts
10956
- var import_node_child_process10 = require("node:child_process");
11806
+ var import_node_child_process9 = require("node:child_process");
10957
11807
  var import_node_util6 = require("node:util");
10958
11808
 
10959
11809
  // src/board-priority.ts
@@ -11061,7 +11911,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
11061
11911
  var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
11062
11912
 
11063
11913
  // src/board.ts
11064
- var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process10.execFile);
11914
+ var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process9.execFile);
11065
11915
  var BOARD_GIT_TIMEOUT_MS = 1e4;
11066
11916
  var WRITE_PROBE_CONCURRENCY = 8;
11067
11917
  var CLAIM_CONCURRENCY = 5;
@@ -12524,6 +13374,8 @@ var MANAGED_GITIGNORE_LINES = [
12524
13374
  // Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are S3-synced
12525
13375
  // via `mmi-cli northstar push` / auto-save on write; never git-tracked (AGENTS.md "Repo cleanliness", #1550, #1842).
12526
13376
  "**/plans/",
13377
+ // Superpowers plan/spec output is scratch too; authoritative docs live directly under docs/.
13378
+ "docs/superpowers/",
12527
13379
  ".playwright-mcp/",
12528
13380
  ".claude/worktrees/",
12529
13381
  // .mmi is agent/CI scratch — ignore the WHOLE tree at any depth (root + cli/.mmi, .github/workflows/.mmi,
@@ -13471,9 +14323,9 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
13471
14323
 
13472
14324
  // src/gh-create.ts
13473
14325
  var import_promises5 = require("node:fs/promises");
13474
- var import_node_os4 = require("node:os");
14326
+ var import_node_os3 = require("node:os");
13475
14327
  var import_node_path17 = require("node:path");
13476
- var import_node_crypto5 = require("node:crypto");
14328
+ var import_node_crypto6 = require("node:crypto");
13477
14329
  var ISSUE_TYPES = ["bug", "feature", "task"];
13478
14330
  var GH_MUTATION_TIMEOUT_MS = 12e4;
13479
14331
  function timeoutKillNote(err, timeoutMs) {
@@ -13513,7 +14365,7 @@ async function bodyArgsViaFile(args, deps = {}) {
13513
14365
  } };
13514
14366
  const write = deps.write ?? import_promises5.writeFile;
13515
14367
  const remove = deps.remove ?? import_promises5.unlink;
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`);
14368
+ const file = (0, import_node_path17.join)(deps.dir ?? (0, import_node_os3.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.md`);
13517
14369
  await write(file, args[i + 1], "utf8");
13518
14370
  return {
13519
14371
  args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
@@ -14726,7 +15578,7 @@ function designSystemSnapshot(root) {
14726
15578
  }
14727
15579
 
14728
15580
  // src/design-system-registry.ts
14729
- var import_node_crypto6 = require("node:crypto");
15581
+ var import_node_crypto7 = require("node:crypto");
14730
15582
  var import_node_fs20 = require("node:fs");
14731
15583
  var import_node_path19 = require("node:path");
14732
15584
  var DESIGN_SYSTEM_CACHE_DIR = ".mmi/design-system/components";
@@ -14789,7 +15641,7 @@ function scanCachedComponentNames(cacheDir) {
14789
15641
  return [...names].sort();
14790
15642
  }
14791
15643
  function contentHash(content) {
14792
- return (0, import_node_crypto6.createHash)("sha256").update(content, "utf8").digest("hex");
15644
+ return (0, import_node_crypto7.createHash)("sha256").update(content, "utf8").digest("hex");
14793
15645
  }
14794
15646
  function buildRegistryComponentsCheck(input) {
14795
15647
  const base = {
@@ -14949,6 +15801,17 @@ function renderVerifySecrets(body) {
14949
15801
  if (ssmStatus !== "Success") {
14950
15802
  return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
14951
15803
  }
15804
+ if (secrets.length === 0) {
15805
+ const raw = (body?.raw ?? "").trim();
15806
+ if (/no required runtime secrets declared/i.test(raw)) {
15807
+ return { lines: [raw], failure: null };
15808
+ }
15809
+ const detail = raw ? ` \u2014 on-box output: ${raw.replace(/\s+/g, " ").trim()}` : "";
15810
+ return {
15811
+ lines: raw ? raw.split("\n") : ["verify-secrets: no per-key verdicts returned"],
15812
+ failure: `verify-secrets returned no per-key verdicts, so nothing was verified${detail}`
15813
+ };
15814
+ }
14952
15815
  const bad = counts.mismatch + counts.missing;
14953
15816
  if (bad > 0) {
14954
15817
  return { lines, failure: `${bad} of ${secrets.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
@@ -14957,12 +15820,12 @@ function renderVerifySecrets(body) {
14957
15820
  }
14958
15821
 
14959
15822
  // src/hotfix-coverage.ts
14960
- var import_node_child_process11 = require("node:child_process");
15823
+ var import_node_child_process10 = require("node:child_process");
14961
15824
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
14962
15825
  function checkHotfixCoverage(options = {}) {
14963
15826
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
14964
15827
  const ack = (options.ack ?? []).filter(Boolean);
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"] }));
15828
+ const git = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
14966
15829
  const revList = (range) => {
14967
15830
  const out = git(["rev-list", "--no-merges", range]).trim();
14968
15831
  return out ? out.split("\n") : [];
@@ -17663,14 +18526,14 @@ function parseKbTree(stdout, prefix) {
17663
18526
 
17664
18527
  // src/northstar-commands.ts
17665
18528
  var import_node_fs22 = require("node:fs");
17666
- var import_node_child_process12 = require("node:child_process");
18529
+ var import_node_child_process11 = require("node:child_process");
17667
18530
  var import_promises6 = require("node:fs/promises");
17668
18531
  var planSyncDetached = false;
17669
18532
  function detachPlanSync() {
17670
18533
  if (planSyncDetached) return;
17671
18534
  planSyncDetached = true;
17672
18535
  try {
17673
- (0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
18536
+ (0, import_node_child_process11.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
17674
18537
  detached: true,
17675
18538
  stdio: "ignore",
17676
18539
  windowsHide: true,
@@ -17756,12 +18619,13 @@ function openInEditor(path2) {
17756
18619
  return;
17757
18620
  }
17758
18621
  try {
17759
- (0, import_node_child_process12.spawn)(editor, [path2], { stdio: "inherit" });
18622
+ (0, import_node_child_process11.spawn)(editor, [path2], { stdio: "inherit" });
17760
18623
  } catch {
17761
18624
  console.log(`open ${path2} manually`);
17762
18625
  }
17763
18626
  }
17764
18627
  async function withPlan(quiet, run, io = consoleIo) {
18628
+ if (!await requireContinuityAccess("northstar", { quiet }, io)) return;
17765
18629
  const cfg = await loadConfig();
17766
18630
  if (!cfg.sagaApiUrl) {
17767
18631
  if (!quiet) fail("plan: Hub API URL not configured");
@@ -17770,6 +18634,7 @@ async function withPlan(quiet, run, io = consoleIo) {
17770
18634
  await run(makePlanDeps(cfg, io));
17771
18635
  }
17772
18636
  async function runPlanAutosave(io = consoleIo, opts = {}) {
18637
+ if (!await requireContinuityAccess("northstar", { quiet: opts.quiet }, io)) return [];
17773
18638
  const cfg = await loadConfig();
17774
18639
  if (!cfg.sagaApiUrl) {
17775
18640
  if (!opts.quiet) fail("plan: Hub API URL not configured");
@@ -18776,10 +19641,10 @@ function registerEdgeCommands(program3) {
18776
19641
 
18777
19642
  // src/doctor-run.ts
18778
19643
  var import_node_fs28 = require("node:fs");
18779
- var import_node_child_process14 = require("node:child_process");
19644
+ var import_node_child_process13 = require("node:child_process");
18780
19645
  var import_promises7 = require("node:fs/promises");
18781
19646
  var import_node_path25 = require("node:path");
18782
- var import_node_os6 = require("node:os");
19647
+ var import_node_os5 = require("node:os");
18783
19648
 
18784
19649
  // src/plugin-guard.ts
18785
19650
  function buildPluginGuardDecision(i) {
@@ -18799,9 +19664,9 @@ function buildGuardSessionStartLine(state, opts = {}) {
18799
19664
  }
18800
19665
 
18801
19666
  // src/cursor-plugin-seed.ts
18802
- var import_node_child_process13 = require("node:child_process");
19667
+ var import_node_child_process12 = require("node:child_process");
18803
19668
  var import_node_fs24 = require("node:fs");
18804
- var import_node_os5 = require("node:os");
19669
+ var import_node_os4 = require("node:os");
18805
19670
  var import_node_path22 = require("node:path");
18806
19671
  var import_node_util7 = require("node:util");
18807
19672
  function isSemverVersion(v) {
@@ -18810,7 +19675,7 @@ function isSemverVersion(v) {
18810
19675
  var MMI_HUB_REPO = "mutmutco/MMI-Hub";
18811
19676
  var CURSOR_THIRD_PARTY_STATE_KEY = "cursor/thirdPartyExtensibilityEnabled";
18812
19677
  var PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
18813
- var execFileBuffer = (0, import_node_util7.promisify)(import_node_child_process13.execFile);
19678
+ var execFileBuffer = (0, import_node_util7.promisify)(import_node_child_process12.execFile);
18814
19679
  function gitFetchReleaseTagArgs(hubCheckout, tag) {
18815
19680
  return ["-C", hubCheckout, "fetch", "origin", "tag", tag, "--quiet"];
18816
19681
  }
@@ -18819,13 +19684,13 @@ function ghReleaseTarballApiArgs(tag) {
18819
19684
  }
18820
19685
  function cursorUserGlobalStatePath() {
18821
19686
  if (process.platform === "win32") {
18822
- const base = process.env.APPDATA || (0, import_node_path22.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
19687
+ const base = process.env.APPDATA || (0, import_node_path22.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
18823
19688
  return (0, import_node_path22.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
18824
19689
  }
18825
19690
  if (process.platform === "darwin") {
18826
- return (0, import_node_path22.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
19691
+ return (0, import_node_path22.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
18827
19692
  }
18828
- return (0, import_node_path22.join)((0, import_node_os5.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
19693
+ return (0, import_node_path22.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
18829
19694
  }
18830
19695
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
18831
19696
  const dbPath = cursorUserGlobalStatePath();
@@ -18981,6 +19846,17 @@ function buildContinuityFreshnessCheck(input) {
18981
19846
  fix: `local branch work is newer than ${stale.join(" + ")} continuity; run ${commands.join(" and ")}`
18982
19847
  };
18983
19848
  }
19849
+ function buildContinuityRestrictionNotice(input) {
19850
+ const login = input.login?.trim() || "unknown";
19851
+ return {
19852
+ ok: true,
19853
+ label: "saga / North Star continuity (Jervaise-only)",
19854
+ fix: `continuity tools (saga, North Star, /handoff) are restricted to ${CONTINUITY_OWNER_LOGIN}; not available to ${login}. Your durable record is the board issue, PR, git history, and MM-KB. Hooks no-op these verbs for you and SessionStart omits the continuity steps. If stray local continuity state exists (.mmi/.session, .mmi/saga-pending.jsonl), remove it with: mmi-cli gc --scratch --apply (safe: it never touches config.json or a live queue).`
19855
+ };
19856
+ }
19857
+ function continuityDoctorCheckForLogin(login) {
19858
+ return login?.trim().toLowerCase() === CONTINUITY_OWNER_LOGIN ? "freshness" : "restriction";
19859
+ }
18984
19860
  var MMI_PLUGIN_ID = "mmi@mutmutco";
18985
19861
  var LEGACY_MMI_PLUGIN_ID = "mmi@mmi";
18986
19862
  var LEGACY_MMI_MARKETPLACE = "mmi";
@@ -19940,7 +20816,7 @@ function buildBrowserArtifactsCheck(input) {
19940
20816
  };
19941
20817
  }
19942
20818
  var KB_DRIFT_ADVISORY_LABEL = "KB registry-fact drift (advisory)";
19943
- var KB_DRIFT_ADVISORY_FIX = "review kb-drift report on Hub (`node infra/saga-io.mjs get kb-drift/<date>.json`) and kb-keeper weekly digest; registry-owned facts belong in `mmi-cli project get`, not copied literals in MM-KB";
20819
+ var KB_DRIFT_ADVISORY_FIX = "review kb-drift report on Hub (`node infra/saga-io.mjs get kb-drift/<date>.json`); registry-owned facts belong in `mmi-cli project get`, not copied literals in MM-KB";
19944
20820
  function buildKbDriftAdvisoryCheck(input) {
19945
20821
  const base = {
19946
20822
  ok: true,
@@ -20423,7 +21299,7 @@ function reexecMmiCli(args) {
20423
21299
  }
20424
21300
  };
20425
21301
  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 });
21302
+ const child = isWin ? (0, import_node_child_process13.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", env }) : (0, import_node_child_process13.spawn)("mmi-cli", args, { stdio: "inherit", env });
20427
21303
  child.on("error", () => done(-1));
20428
21304
  child.on("exit", (code) => done(code ?? 0));
20429
21305
  });
@@ -20480,7 +21356,7 @@ async function applyPluginHeal(token, surface, log, opts) {
20480
21356
  }
20481
21357
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
20482
21358
  const homeDir = surface === "codex" ? ".codex" : ".claude";
20483
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
21359
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
20484
21360
  };
20485
21361
  function readInstalledPlugins(surface = detectSurface(process.env)) {
20486
21362
  try {
@@ -20495,13 +21371,13 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
20495
21371
  return {
20496
21372
  isOrgRepo,
20497
21373
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
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"))
21374
+ marketplaceClonePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
21375
+ pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
20500
21376
  };
20501
21377
  }
20502
21378
  function installedPluginSources() {
20503
21379
  return ["claude", "codex"].map((surface) => {
20504
- const recordPath = (0, import_node_path25.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
21380
+ const recordPath = (0, import_node_path25.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
20505
21381
  try {
20506
21382
  return { surface, installed: JSON.parse((0, import_node_fs28.readFileSync)(recordPath, "utf8")), recordPath };
20507
21383
  } catch {
@@ -20556,7 +21432,7 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
20556
21432
  }
20557
21433
  }
20558
21434
  function opencodeConfigDir() {
20559
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".config", "opencode");
21435
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".config", "opencode");
20560
21436
  }
20561
21437
  function opencodeConfigPath() {
20562
21438
  return (0, import_node_path25.join)(opencodeConfigDir(), "opencode.jsonc");
@@ -20642,7 +21518,7 @@ function writeOpencodeCommandFiles() {
20642
21518
  function readOpencodeAdapterDiskVersion() {
20643
21519
  const candidates = [
20644
21520
  (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")
21521
+ (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
20646
21522
  ];
20647
21523
  for (const path2 of candidates) {
20648
21524
  try {
@@ -20677,13 +21553,13 @@ function opencodePluginVersionsForReport() {
20677
21553
  }
20678
21554
  function opencodeDesktopLogsRoot() {
20679
21555
  if (process.platform === "win32") {
20680
- const base = process.env.APPDATA || (0, import_node_path25.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
21556
+ const base = process.env.APPDATA || (0, import_node_path25.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
20681
21557
  return (0, import_node_path25.join)(base, "ai.opencode.desktop", "logs");
20682
21558
  }
20683
21559
  if (process.platform === "darwin") {
20684
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
21560
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
20685
21561
  }
20686
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".config", "ai.opencode.desktop", "logs");
21562
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".config", "ai.opencode.desktop", "logs");
20687
21563
  }
20688
21564
  function opencodeDesktopBootstrapSnapshot() {
20689
21565
  const root = opencodeDesktopLogsRoot();
@@ -20699,7 +21575,7 @@ function opencodeDesktopBootstrapSnapshot() {
20699
21575
  }
20700
21576
  }
20701
21577
  function opencodeLegacyConfigSnapshot() {
20702
- const legacyPath = (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".opencode", "opencode.json");
21578
+ const legacyPath = (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".opencode", "opencode.json");
20703
21579
  if (!(0, import_node_fs28.existsSync)(legacyPath)) return {};
20704
21580
  const content = readTextFile(legacyPath);
20705
21581
  if (content == null) return {};
@@ -20720,7 +21596,7 @@ function quarantineOpencodeLegacyConfig(legacyPath) {
20720
21596
  }
20721
21597
  }
20722
21598
  function cursorPluginCacheRoot() {
20723
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
21599
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
20724
21600
  }
20725
21601
  function cursorPluginCachePinSnapshots() {
20726
21602
  const root = cursorPluginCacheRoot();
@@ -20766,8 +21642,8 @@ function hubCheckoutForCursorSeed() {
20766
21642
  }
20767
21643
  function mmiPluginCacheRootSnapshots() {
20768
21644
  const roots = [
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") }
21645
+ { surface: "claude", root: (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
21646
+ { surface: "codex", root: (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
20771
21647
  ];
20772
21648
  return roots.flatMap(({ surface, root }) => {
20773
21649
  try {
@@ -20831,7 +21707,7 @@ async function clearNestedPluginTreeDir(targetPath) {
20831
21707
  try {
20832
21708
  if (!(0, import_node_fs28.existsSync)(targetPath)) return true;
20833
21709
  if (isWin) {
20834
- const emptyDir = (0, import_node_path25.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
21710
+ const emptyDir = (0, import_node_path25.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
20835
21711
  (0, import_node_fs28.mkdirSync)(emptyDir, { recursive: true });
20836
21712
  try {
20837
21713
  await robocopyMirrorEmpty(emptyDir, targetPath);
@@ -20869,7 +21745,7 @@ function readTextFile(path2) {
20869
21745
  }
20870
21746
  function playwrightMcpConfigSnapshots() {
20871
21747
  const cwd = process.cwd();
20872
- const home = (0, import_node_os6.homedir)();
21748
+ const home = (0, import_node_os5.homedir)();
20873
21749
  const candidates = [
20874
21750
  (0, import_node_path25.join)(cwd, ".mcp.json"),
20875
21751
  (0, import_node_path25.join)(cwd, ".cursor", "mcp.json"),
@@ -21382,7 +22258,7 @@ async function runDoctor(opts, io = consoleIo) {
21382
22258
  releasedVersion,
21383
22259
  hubCheckout: hubCheckoutForCursorSeed(),
21384
22260
  execFileP: execFileP2,
21385
- mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0, import_node_os6.tmpdir)(), prefix)),
22261
+ mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0, import_node_os5.tmpdir)(), prefix)),
21386
22262
  log: (m) => io.err(m)
21387
22263
  });
21388
22264
  if (seeded) {
@@ -21474,15 +22350,19 @@ async function runDoctor(opts, io = consoleIo) {
21474
22350
  }
21475
22351
  }
21476
22352
  if (runExtended && !opts.banner) {
21477
- const continuity = readContinuityStamp();
21478
- checks.push(
21479
- buildContinuityFreshnessCheck({
21480
- isOrgRepo: Boolean(cfg.sagaApiUrl),
21481
- latestWorkAt: await latestBranchWorkAt(),
21482
- lastSagaNoteAt: continuity.lastSagaNoteAt,
21483
- lastNorthstarAt: latestNorthstarContinuityAt()
21484
- })
21485
- );
22353
+ if (continuityDoctorCheckForLogin(login) === "freshness") {
22354
+ const continuity = readContinuityStamp();
22355
+ checks.push(
22356
+ buildContinuityFreshnessCheck({
22357
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
22358
+ latestWorkAt: await latestBranchWorkAt(),
22359
+ lastSagaNoteAt: continuity.lastSagaNoteAt,
22360
+ lastNorthstarAt: latestNorthstarContinuityAt()
22361
+ })
22362
+ );
22363
+ } else {
22364
+ checks.push(buildContinuityRestrictionNotice({ login: login?.trim() }));
22365
+ }
21486
22366
  }
21487
22367
  if (runExtended) {
21488
22368
  const dashboardConsumer = await resolveDashboardConsumer(cfg);
@@ -21580,7 +22460,7 @@ function mergeGuardHook(settings) {
21580
22460
  next.hooks = hooks;
21581
22461
  return next;
21582
22462
  }
21583
- var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0, import_node_os6.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
22463
+ var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0, import_node_os5.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
21584
22464
  function ensureUserScopeGuardHook(opts = {}) {
21585
22465
  const path2 = opts.settingsPath ?? userScopeSettingsPath();
21586
22466
  try {
@@ -21794,7 +22674,7 @@ async function requireFreshTrainCli(commandName) {
21794
22674
  throw new Error(`running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; run doctor/update first so ${commandName} uses the current train path`);
21795
22675
  }
21796
22676
  var program2 = new Command();
21797
- program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
22677
+ program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
21798
22678
  async function runRulesSync(opts, io = consoleIo) {
21799
22679
  const cfg = await loadConfig();
21800
22680
  if (isRulesSource(cfg.orgRulesSource)) {
@@ -22037,7 +22917,7 @@ function runWorktreeInstall(command, cwd, quiet) {
22037
22917
  const file = isWin2 ? "cmd.exe" : bin;
22038
22918
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
22039
22919
  return new Promise((resolve6, reject) => {
22040
- const child = (0, import_node_child_process15.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
22920
+ const child = (0, import_node_child_process14.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
22041
22921
  const timer = setTimeout(() => {
22042
22922
  try {
22043
22923
  child.kill();
@@ -22255,7 +23135,7 @@ function scheduleRelatedDiscovery(o) {
22255
23135
  try {
22256
23136
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
22257
23137
  if (o.repo) args.push("--repo", o.repo);
22258
- (0, import_node_child_process15.spawn)(process.execPath, [process.argv[1], ...args], {
23138
+ (0, import_node_child_process14.spawn)(process.execPath, [process.argv[1], ...args], {
22259
23139
  detached: true,
22260
23140
  stdio: "ignore",
22261
23141
  windowsHide: true,
@@ -22264,7 +23144,7 @@ function scheduleRelatedDiscovery(o) {
22264
23144
  } catch {
22265
23145
  }
22266
23146
  }
22267
- var northstar = program2.command("northstar").description("North Star \u2014 your cross-device plans/SSOTs (S3-backed, git-clean)");
23147
+ var northstar = program2.command("northstar").description("North Star \u2014 Jervaise-only cross-device plans/SSOTs (S3-backed, git-clean)");
22268
23148
  registerNorthStarCommands(northstar);
22269
23149
  var plan = program2.command("plan").description("Alias for `northstar` (deprecated \u2014 use `northstar`)");
22270
23150
  plan.hook("preAction", () => {
@@ -22290,7 +23170,7 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
22290
23170
  try {
22291
23171
  const result = await runTenantControl(trainApplyDeps(), { repo, stage: stage2, action, watch: o.watch });
22292
23172
  if (!o.json && action === "verify-secrets" && result.secrets) {
22293
- const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed" };
23173
+ const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
22294
23174
  const { lines, failure } = renderVerifySecrets(body);
22295
23175
  for (const line of lines) printLine(line);
22296
23176
  if (failure) return failGraceful(`tenant control ${stage2} verify-secrets: ${failure}`);
@@ -23391,7 +24271,7 @@ async function remoteBranchExists2(branch, options = {}) {
23391
24271
  }
23392
24272
  var COMPOSE_TIMEOUT_MS = 12e4;
23393
24273
  function spawnDeferredGcSweep() {
23394
- spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24274
+ spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
23395
24275
  }
23396
24276
  async function createDeferredWorktreeStore() {
23397
24277
  try {
@@ -24631,85 +25511,90 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
24631
25511
  ));
24632
25512
  program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
24633
25513
  program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
24634
- program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, whoami, doctor, plan-store check) in one process; docs sync runs detached").action(async () => {
25514
+ program2.command("session-start").description("run the SessionStart verbs (rules sync, Jervaise-only continuity, whoami, doctor, plan-store check) in one process; docs sync runs detached").action(async () => {
24635
25515
  if (isInsideRepoSubdir(process.cwd())) {
24636
25516
  console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
24637
25517
  return;
24638
25518
  }
24639
25519
  if (!isOrgRepoRoot(process.cwd())) return;
24640
- try {
24641
- const hook = parseHookInput(await readStdin());
24642
- if (hook.session_id) persistSession(hook.session_id);
24643
- } catch (e) {
24644
- console.error(`[mmi-hook] saga session failed: ${e.message}`);
25520
+ const continuityEnabled = (await continuityAccess().catch(() => ({ allowed: false }))).allowed;
25521
+ if (continuityEnabled) {
25522
+ try {
25523
+ const hook = parseHookInput(await readStdin());
25524
+ if (hook.session_id) persistSession(hook.session_id);
25525
+ } catch (e) {
25526
+ console.error(`[mmi-hook] saga session failed: ${e.message}`);
25527
+ }
24645
25528
  }
24646
- spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
25529
+ spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24647
25530
  spawnDeferredGcSweep();
24648
25531
  let northstarInjected = false;
24649
- const { parallel, sequential } = buildSessionStartPlan({
24650
- rulesSync: (io) => runRulesSync({ quiet: true }, io),
24651
- sagaShow: (io) => runSagaShow({ quiet: true }, io),
24652
- handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
24653
- coopPending: (io) => runCoopPendingBanner(io),
24654
- northstarContext: async (io) => {
24655
- const cfg = await loadConfig();
24656
- if (!cfg.sagaApiUrl) return;
24657
- const planDeps = makePlanDeps(cfg, io);
24658
- northstarInjected = await runNorthstarContext(io, {
24659
- loadPlans: () => scopedPlanList(planDeps),
24660
- readLocal: (slug) => planDeps.readLocal(slug),
24661
- // #1812: thread the saga HEAD's North Star anchor (its NEXT slug) into the relevance gate so
24662
- // the plan the agent is actively on is force-injected even on a generic branch with no token
24663
- // overlap. fetchSagaHead errors are swallowed via a silent io a missing/failed HEAD just
24664
- // falls back to token-overlap scoring, never noises or blocks the banner.
24665
- gatherSignals: () => gatherRelevanceSignals({
24666
- anchorSlug: () => fetchSagaHead({ log: () => {
24667
- }, err: () => {
24668
- } }).then((h) => h?.anchor?.slug ?? void 0)
24669
- })
24670
- });
24671
- },
24672
- sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
24673
- // whoami (#879): surface the resolved human so agents act --for them without asking. Silent
24674
- // when unknown a missing gh login must not noise or fail the banner.
24675
- whoami: async (io) => {
24676
- const report = await resolveWhoami({
24677
- hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
24678
- ghLogin: githubLogin
24679
- });
24680
- const line = whoamiLine(report);
24681
- if (line) io.log(line);
24682
- },
24683
- boardSlice: (io) => runBoardSlice(io, {
24684
- loadConfig: () => loadConfigForRepo(),
24685
- readBoard,
24686
- // #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
24687
- // never costs banner time and next session's glance renders instantly within budget.
24688
- scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
24689
- }),
24690
- spineReconcile: async (io) => {
24691
- const cfg = await loadConfig();
24692
- const isSource = isRulesSource(cfg.orgRulesSource);
24693
- if (!isSource && !await isOrgRegisteredRepo(cfg)) return;
24694
- const restored = await restoreDirtyOrgSpineToHead(
24695
- { run: async (cmd, args) => (await execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })).stdout },
24696
- { isRulesSource: isSource }
24697
- );
24698
- if (restored.length) {
24699
- io.log(`[mmi] reset ${restored.length} org-delivered file(s) to your branch \u2014 pull latest base to refresh (${restored.join(", ")})`);
24700
- }
25532
+ const { parallel, sequential } = buildSessionStartPlan(
25533
+ {
25534
+ rulesSync: (io) => runRulesSync({ quiet: true }, io),
25535
+ sagaShow: (io) => runSagaShow({ quiet: true }, io),
25536
+ handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
25537
+ coopPending: (io) => runCoopPendingBanner(io),
25538
+ northstarContext: async (io) => {
25539
+ const cfg = await loadConfig();
25540
+ if (!cfg.sagaApiUrl) return;
25541
+ const planDeps = makePlanDeps(cfg, io);
25542
+ northstarInjected = await runNorthstarContext(io, {
25543
+ loadPlans: () => scopedPlanList(planDeps),
25544
+ readLocal: (slug) => planDeps.readLocal(slug),
25545
+ // #1812: thread the saga HEAD's North Star anchor (its NEXT slug) into the relevance gate so
25546
+ // the plan the agent is actively on is force-injected even on a generic branch with no token
25547
+ // overlap. fetchSagaHead errors are swallowed via a silent io a missing/failed HEAD just
25548
+ // falls back to token-overlap scoring, never noises or blocks the banner.
25549
+ gatherSignals: () => gatherRelevanceSignals({
25550
+ anchorSlug: () => fetchSagaHead({ log: () => {
25551
+ }, err: () => {
25552
+ } }).then((h) => h?.anchor?.slug ?? void 0)
25553
+ })
25554
+ });
25555
+ },
25556
+ sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
25557
+ // whoami (#879): surface the resolved human so agents act --for them without asking. Silent
25558
+ // when unknown a missing gh login must not noise or fail the banner.
25559
+ whoami: async (io) => {
25560
+ const report = await resolveWhoami({
25561
+ hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
25562
+ ghLogin: githubLogin
25563
+ });
25564
+ const line = whoamiLine(report);
25565
+ if (line) io.log(line);
25566
+ },
25567
+ boardSlice: (io) => runBoardSlice(io, {
25568
+ loadConfig: () => loadConfigForRepo(),
25569
+ readBoard,
25570
+ // #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
25571
+ // never costs banner time and next session's glance renders instantly within budget.
25572
+ scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
25573
+ }),
25574
+ spineReconcile: async (io) => {
25575
+ const cfg = await loadConfig();
25576
+ const isSource = isRulesSource(cfg.orgRulesSource);
25577
+ if (!isSource && !await isOrgRegisteredRepo(cfg)) return;
25578
+ const restored = await restoreDirtyOrgSpineToHead(
25579
+ { run: async (cmd, args) => (await execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })).stdout },
25580
+ { isRulesSource: isSource }
25581
+ );
25582
+ if (restored.length) {
25583
+ io.log(`[mmi] reset ${restored.length} org-delivered file(s) to your branch \u2014 pull latest base to refresh (${restored.join(", ")})`);
25584
+ }
25585
+ },
25586
+ doctor: (io) => runDoctor({ banner: true }, io)
24701
25587
  },
24702
- doctor: (io) => runDoctor({ banner: true }, io)
24703
- });
25588
+ { continuityEnabled }
25589
+ );
24704
25590
  await runSessionStart(parallel, sequential, consoleIo);
24705
- consoleIo.log(northstarPointer(northstarInjected));
25591
+ for (const line of sessionStartContinuityLines({ continuityEnabled, northstarInjected, cwd: process.cwd() })) consoleIo.log(line);
24706
25592
  consoleIo.log(kbPointer());
24707
- for (const line of planStoreLines(process.cwd())) consoleIo.log(line);
24708
- await runPlanAutosave(consoleIo, { quiet: true }).catch(() => void 0);
25593
+ if (continuityEnabled) await runPlanAutosave(consoleIo, { quiet: true }).catch(() => void 0);
24709
25594
  for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
24710
25595
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
24711
25596
  if (worktreeBanner) {
24712
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
25597
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24713
25598
  consoleIo.log(worktreeBanner);
24714
25599
  }
24715
25600
  });