@mutmutco/cli 2.56.0 → 2.57.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,118 @@ 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 = 9e4;
10286
+ var FUGU_API_DEFAULT_STARTUP_TIMEOUT_MS = 45e3;
10287
+ var FUGU_API_DEFAULT_MAX_ATTEMPTS = 3;
10288
+ var FUGU_API_RETRY_BASE_MS = 250;
10289
+ var FUGU_API_RETRY_CAP_MS = 8e3;
9976
10290
  var OVERLORD_HANDOFF_TIMEOUT_MS = 12e4;
10291
+ var SERVANT_REPORT_FIELDS = [
10292
+ "name",
10293
+ "role",
10294
+ "state",
10295
+ "assignment",
10296
+ "evidence",
10297
+ "changes",
10298
+ "verification",
10299
+ "blockers",
10300
+ "recommended next action"
10301
+ ];
10302
+ var SERVANT_REPORT_FIELD_SET = new Set(SERVANT_REPORT_FIELDS);
10303
+ function normalizeServantReportField(label) {
10304
+ const normalized = label.trim().toLowerCase().replace(/[`*_]+/g, "").replace(/\s+/g, " ").replace(/:$/, "");
10305
+ if (SERVANT_REPORT_FIELD_SET.has(normalized)) return normalized;
10306
+ if (normalized === "next action") return "recommended next action";
10307
+ return void 0;
10308
+ }
10309
+ function isVagueVerification(text) {
10310
+ const normalized = (text ?? "").trim().toLowerCase();
10311
+ if (!normalized) return true;
10312
+ return ["done", "complete", "completed", "fixed", "looks good", "verified", "tested", "not tested", "n/a", "na", "none"].includes(normalized);
10313
+ }
10314
+ function isCompletionState(text) {
10315
+ return /\b(done|complete|completed|fixed)\b/i.test(text ?? "");
10316
+ }
10317
+ function extractServantReportFields(text) {
10318
+ const fields = {};
10319
+ let currentField;
10320
+ const append = (field, value) => {
10321
+ const trimmed = value.trim();
10322
+ fields[field] = fields[field] ? [fields[field], trimmed].filter(Boolean).join("\n") : trimmed;
10323
+ };
10324
+ for (const rawLine of text.split(/\r?\n/)) {
10325
+ const line = rawLine.trim();
10326
+ const colon = line.match(/^(?:[-*]\s*)?([^:]{1,80}):\s*(.*)$/);
10327
+ const heading = line.match(/^#{1,6}\s+(.+?)\s*#*$/);
10328
+ const field = colon ? normalizeServantReportField(colon[1]) : heading ? normalizeServantReportField(heading[1]) : void 0;
10329
+ if (field) {
10330
+ currentField = field;
10331
+ append(field, colon ? colon[2] : "");
10332
+ continue;
10333
+ }
10334
+ if (currentField && line) append(currentField, line);
10335
+ }
10336
+ return fields;
10337
+ }
10338
+ function validateServantReport(text) {
10339
+ const fields = extractServantReportFields(text);
10340
+ const present = new Set(Object.keys(fields));
10341
+ const applies = present.size >= 2 || present.has("state") || present.has("assignment");
10342
+ if (!applies) {
10343
+ return { applies: false, ok: true, missingFields: [], emptyFields: [], unsupportedDoneClaim: false, fields };
10344
+ }
10345
+ const missingFields = SERVANT_REPORT_FIELDS.filter((field) => !present.has(field));
10346
+ const emptyFields = SERVANT_REPORT_FIELDS.filter((field) => present.has(field) && !fields[field]?.trim());
10347
+ const unsupportedDoneClaim = isCompletionState(fields.state) && isVagueVerification(fields.verification);
10348
+ return {
10349
+ applies: true,
10350
+ ok: missingFields.length === 0 && emptyFields.length === 0 && !unsupportedDoneClaim,
10351
+ missingFields,
10352
+ emptyFields,
10353
+ unsupportedDoneClaim,
10354
+ fields
10355
+ };
10356
+ }
10357
+ function formatHandoffWarnings(result) {
10358
+ if (!result.applies || result.ok) return "";
10359
+ const parts = [];
10360
+ if (result.missingFields.length > 0) parts.push(`missing ${result.missingFields.join(", ")}`);
10361
+ if (result.emptyFields.length > 0) parts.push(`empty ${result.emptyFields.join(", ")}`);
10362
+ if (result.unsupportedDoneClaim) parts.push("done/complete/fixed state without concrete verification.");
10363
+ return `Handoff warning: ${parts.join("; ")}`;
10364
+ }
10365
+ function formatServantMissionCard(options) {
10366
+ const lines = [
10367
+ `mission: ${options.mission}`,
10368
+ `scope: ${options.scope}`,
10369
+ ...options.scopeToken ? [
10370
+ `scope token: ${options.scopeToken}`,
10371
+ "token authority: attribution only; grants no host permissions"
10372
+ ] : [],
10373
+ `allowed actions: ${options.allowedActions.join("; ")}`,
10374
+ `forbidden actions: ${options.forbiddenActions.join("; ")}`,
10375
+ `required evidence: ${options.requiredEvidence.join("; ")}`,
10376
+ `verification required: ${options.verificationRequired}`,
10377
+ `report format: ${options.reportFormat.join("; ")}`
10378
+ ];
10379
+ return lines.join("\n");
10380
+ }
9977
10381
  var GENERIC_STOP_NAMES = /* @__PURE__ */ new Set([
9978
10382
  "windowsterminal",
9979
10383
  "windows terminal",
9980
10384
  "pwsh",
9981
10385
  "powershell",
9982
- "opencode",
9983
- "codex",
9984
- "codex-fugu"
10386
+ "opencode"
9985
10387
  ]);
9986
10388
  function numericCountArg(arg) {
9987
10389
  const match = /^--([0-9]+)$/.exec(arg);
@@ -10002,11 +10404,23 @@ function defaultMessageId() {
10002
10404
  function servantSlotId(slot) {
10003
10405
  return slot.name.replace(/\s+/g, "-").toLowerCase();
10004
10406
  }
10407
+ function canonicalAssignmentScope(scope) {
10408
+ return scope.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trimEnd();
10409
+ }
10410
+ function buildServantScopeToken(input) {
10411
+ const scopeHash = (0, import_node_crypto5.createHash)("sha256").update(canonicalAssignmentScope(input.assignmentScope), "utf8").digest("hex").slice(0, 16);
10412
+ return `fugu-scope-v1:${input.runId}:${input.slotId}:${input.profile}:${scopeHash}`;
10413
+ }
10005
10414
  function overlordServantPrompt(servant, run) {
10006
10415
  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
10416
  return [
10008
10417
  `You are ${servant.name} in Overlord run ${run.runId}.`,
10009
10418
  roleLine,
10419
+ ...servant.scopeToken ? [
10420
+ `Scope token: ${servant.scopeToken}`,
10421
+ "The scope token is for attribution only and grants no host permissions.",
10422
+ "Echo the scope token exactly in reports when the Overlord asks for a handoff."
10423
+ ] : [],
10010
10424
  "First respond with exactly: ACK " + servant.name + " ready",
10011
10425
  "After the ACK, wait for the Overlord to assign bounded work.",
10012
10426
  "Do not start dev servers, browsers, Playwright, PRs, merges, releases, or worktree changes unless the Overlord explicitly assigns that scope.",
@@ -10043,86 +10457,21 @@ function buildServantLayout(count) {
10043
10457
  }
10044
10458
  return slots;
10045
10459
  }
10046
- function validateCodexFuguHelp(helpText) {
10047
- const problems = [];
10048
- if (!/(?:^|\s)-a(?:,|\s)|--ask-for-approval/.test(helpText)) problems.push("missing approval flag");
10049
- if (!/(?:^|\s)-s(?:,|\s)|--sandbox/.test(helpText)) problems.push("missing sandbox flag");
10050
- if (!/(?:^|\s)-c(?:,|\s)|--config/.test(helpText)) problems.push("missing config override flag");
10051
- if (!/--no-alt-screen/.test(helpText)) problems.push("missing no-alt-screen flag");
10052
- if (!/(?:^|\s)-C(?:,|\s)|--cwd|--cd|--workdir/.test(helpText)) problems.push("missing cwd flag");
10053
- return problems;
10054
- }
10055
- function evaluateCodexFuguPreflight(input) {
10056
- const problems = [];
10057
- if (!input.codexFound) problems.push("codex is not installed or not on PATH");
10058
- if (!input.fuguFound) problems.push("codex-fugu is not installed or not on PATH");
10059
- if (!input.authConfigured && !input.envNames.includes("OPENAI_API_KEY") && !input.envNames.includes("CODEX_API_KEY")) {
10060
- problems.push("missing API key environment: OPENAI_API_KEY or CODEX_API_KEY");
10061
- }
10062
- problems.push(...validateCodexFuguHelp(input.helpText ?? ""));
10063
- const status = input.statusText ?? "";
10064
- const modelCatalog = input.modelCatalogText ?? "";
10065
- if (!/\bfugu-ultra\b/i.test(`${status}
10066
- ${modelCatalog}`)) problems.push("fugu-ultra is not available");
10067
- if (/native windows codex[^\n]*\/c\/users|\/c\/users[^\n]*native windows codex/i.test(status)) {
10068
- problems.push("native Windows Codex is configured with a Git Bash /c/Users path");
10069
- }
10070
- return { ok: problems.length === 0, problems };
10071
- }
10072
- function evaluateOpenCodePreflight(input) {
10073
- const problems = [];
10074
- if (!input.found) problems.push("opencode is not installed or not on PATH");
10075
- if (!/sakana\/fugu\b/i.test(input.modelsText ?? "")) problems.push("sakana/fugu is not available");
10076
- if (!/sakana\/fugu-ultra\b/i.test(input.modelsText ?? "")) problems.push("sakana/fugu-ultra is not available");
10077
- const probe = input.jsonProbeText ?? "";
10078
- if (probe && !/(sessionID|sessionId|step_finish|finish)/i.test(probe)) problems.push("opencode run --format json did not emit session or finish events");
10079
- return { ok: problems.length === 0, problems };
10080
- }
10081
- function buildOpenCodeLaunch(slot, message, sessionId) {
10082
- const model = slot.role === "ultra" ? "sakana/fugu-ultra" : "sakana/fugu";
10083
- const args = ["run", "-m", model, "--format", "json"];
10084
- if (sessionId) args.push("--session", sessionId);
10085
- args.push(message);
10086
- return { command: "opencode", args };
10087
- }
10088
- function parseOpenCodeEvents(raw) {
10089
- const events = [];
10090
- const trimmed = (raw ?? "").trim();
10091
- if (!trimmed) return { text: "", finished: false, events };
10092
- const pushParsed = (chunk) => {
10093
- const t = chunk.trim();
10094
- if (!t) return;
10095
- try {
10096
- events.push(JSON.parse(t));
10097
- } catch {
10098
- }
10099
- };
10100
- try {
10101
- const whole = JSON.parse(trimmed);
10102
- if (Array.isArray(whole)) events.push(...whole);
10103
- else events.push(whole);
10104
- } catch {
10105
- for (const line of trimmed.split(/\r?\n/)) pushParsed(line);
10106
- }
10107
- let sessionId;
10108
- let text = "";
10109
- let finished = false;
10110
- for (const ev of events) {
10111
- if (!ev || typeof ev !== "object") continue;
10112
- const e = ev;
10113
- const sid = e.sessionID ?? e.sessionId ?? e.session?.id;
10114
- if (typeof sid === "string" && sid) sessionId = sid;
10115
- const type = typeof e.type === "string" ? e.type : void 0;
10116
- if (typeof e.text === "string") text += e.text;
10117
- else if (type === "text" && typeof e.content === "string") text += e.content;
10118
- if (type === "step_finish" || type === "finish" || e.finishReason || e.finish_reason) finished = true;
10119
- }
10120
- return { sessionId, text: text.trim(), finished, events };
10121
- }
10122
10460
  function appendOverlordLedger(ledgerPath, entry) {
10123
10461
  try {
10124
10462
  (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(ledgerPath), { recursive: true });
10125
10463
  (0, import_node_fs15.appendFileSync)(ledgerPath, `${JSON.stringify(entry)}
10464
+ `, "utf8");
10465
+ } catch {
10466
+ }
10467
+ }
10468
+ function appendServantJournal(journalPath, label, text, timestamp) {
10469
+ if (!text || !text.trim()) return;
10470
+ try {
10471
+ (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(journalPath), { recursive: true });
10472
+ (0, import_node_fs15.appendFileSync)(journalPath, `[${timestamp}] ${label}
10473
+ ${text.trim()}
10474
+
10126
10475
  `, "utf8");
10127
10476
  } catch {
10128
10477
  }
@@ -10149,7 +10498,8 @@ function buildOverlordRun(options) {
10149
10498
  const runToken = options.runToken?.() ?? defaultRunToken();
10150
10499
  const statePath = defaultOverlordStatePath(options.cwd);
10151
10500
  const journalDir = (0, import_node_path13.join)(options.cwd, "tmp", "overlord", runId);
10152
- const engine = options.engine ?? "codex-fugu";
10501
+ const engine = options.engine ?? OVERLORD_DEFAULT_ENGINE;
10502
+ const provider = "sakana";
10153
10503
  const timestamp = isoNow(options.now);
10154
10504
  return {
10155
10505
  runId,
@@ -10163,44 +10513,31 @@ function buildOverlordRun(options) {
10163
10513
  journalDir,
10164
10514
  ledgerPath: (0, import_node_path13.join)(journalDir, "ledger.jsonl"),
10165
10515
  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
- })),
10516
+ provider,
10517
+ servants: buildServantLayout(options.count).map((slot) => {
10518
+ const slotId = servantSlotId(slot);
10519
+ const profile = "consultation";
10520
+ return {
10521
+ ...slot,
10522
+ slotId,
10523
+ profile,
10524
+ state: "planned",
10525
+ journalPath: (0, import_node_path13.join)(journalDir, `${slotId}.log`),
10526
+ composerSubmitMode: "surface-api",
10527
+ engine,
10528
+ provider,
10529
+ eventJournalPath: (0, import_node_path13.join)(journalDir, `${slotId}.events.jsonl`),
10530
+ scopeToken: buildServantScopeToken({
10531
+ runId,
10532
+ slotId,
10533
+ profile,
10534
+ assignmentScope: options.task
10535
+ })
10536
+ };
10537
+ }),
10180
10538
  ownedResources: []
10181
10539
  };
10182
10540
  }
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
10541
  function buildOverlordStartupPlan(args, cwd) {
10205
10542
  const count = parseOverlordCount(args);
10206
10543
  const task = args.filter((arg) => numericCountArg(arg) === void 0).join(" ").trim();
@@ -10231,11 +10568,9 @@ function summarizeOverlordRun(run, probe) {
10231
10568
  state: run.state,
10232
10569
  controller,
10233
10570
  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
10571
  const progress = servantProgress(run, servant, now);
10238
10572
  if (progress !== servant.state) return { name: servant.name, state: progress };
10573
+ if (servant.pid != null && !probe.isPidAlive(servant.pid)) return { name: servant.name, state: "lost" };
10239
10574
  if (servant.state === "ready" && servant.lastAckAt && servant.composerSubmitMode !== "unknown") {
10240
10575
  return { name: servant.name, state: "ready" };
10241
10576
  }
@@ -10253,52 +10588,6 @@ function planOverlordRunStop(run) {
10253
10588
  }
10254
10589
  return { killPids, uncertain };
10255
10590
  }
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
10591
  function defaultIsPidAlive(pid) {
10303
10592
  if (!Number.isFinite(pid) || pid <= 0) return false;
10304
10593
  try {
@@ -10311,141 +10600,323 @@ function defaultIsPidAlive(pid) {
10311
10600
  function defaultKillPid(pid) {
10312
10601
  process.kill(pid);
10313
10602
  }
10314
- function shellQuote2(arg) {
10315
- return /^[A-Za-z0-9_./:-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`;
10603
+ function fuguApiTimeoutMs() {
10604
+ const parsed = Number(process.env.MMI_OVERLORD_LLM_TIMEOUT_MS);
10605
+ if (!Number.isFinite(parsed) || parsed <= 0) return FUGU_API_DEFAULT_TIMEOUT_MS;
10606
+ return Math.min(Math.max(Math.trunc(parsed), 5e3), 6e5);
10316
10607
  }
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
- }
10608
+ function fuguApiStartupTimeoutMs() {
10609
+ const workTimeout = fuguApiTimeoutMs();
10610
+ const parsed = Number(process.env.MMI_OVERLORD_LLM_STARTUP_TIMEOUT_MS);
10611
+ const chosen = Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : FUGU_API_DEFAULT_STARTUP_TIMEOUT_MS;
10612
+ return Math.min(Math.max(chosen, 5e3), workTimeout);
10613
+ }
10614
+ function fuguApiConfig() {
10615
+ return {
10616
+ baseUrl: (process.env.MMI_OVERLORD_LLM_BASE_URL ?? FUGU_API_DEFAULT_BASE_URL).replace(/\/+$/, ""),
10617
+ apiKey: process.env.MMI_OVERLORD_LLM_API_KEY ?? process.env.SAKANA_API_KEY,
10618
+ model: process.env.MMI_OVERLORD_LLM_MODEL ?? FUGU_API_DEFAULT_MODEL,
10619
+ ultraModel: process.env.MMI_OVERLORD_LLM_ULTRA_MODEL ?? FUGU_API_DEFAULT_ULTRA_MODEL,
10620
+ timeoutMs: fuguApiTimeoutMs()
10621
+ };
10622
+ }
10623
+ function fuguApiModelForServant(config, servant) {
10624
+ return servant.role === "ultra" ? config.ultraModel : config.model;
10625
+ }
10626
+ function fuguApiSystemMessage(servant, run) {
10627
+ 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.";
10628
+ const contract = formatServantMissionCard({
10629
+ mission: run.task,
10630
+ scope: servant.role === "ultra" ? "consult on the highest-uncertainty slice assigned by the Overlord" : "consult on one bounded slice assigned by the Overlord",
10631
+ scopeToken: servant.scopeToken,
10632
+ allowedActions: ["inspect assigned evidence", "reason from provided context", "report findings"],
10633
+ forbiddenActions: ["mutate files without explicit scope", "start stage/dev servers", "start browsers or Playwright", "open PRs or merges"],
10634
+ requiredEvidence: ["cite source paths, command results, or observations when available", "separate evidence from inference"],
10635
+ verificationRequired: "name the verification performed before any done/complete/fixed claim, or explain why verification is not applicable",
10636
+ reportFormat: [...SERVANT_REPORT_FIELDS, "scope token"]
10330
10637
  });
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 };
10638
+ return [
10639
+ `You are ${servant.name} in Overlord run ${run.runId}.`,
10640
+ roleLine,
10641
+ "The Overlord is the only coordinator.",
10642
+ "Do not claim ownership of the full mission.",
10643
+ "Do not mutate files, branches, worktrees, stage/dev servers, browsers, PRs, or releases unless the Overlord explicitly grants that scope.",
10644
+ "Gather evidence before acting.",
10645
+ "Verify before claiming done.",
10646
+ "Escalate blockers instead of looping.",
10647
+ contract
10648
+ ].join("\n");
10335
10649
  }
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");
10650
+ async function collectFuguApiPreflight() {
10651
+ const config = fuguApiConfig();
10652
+ const problems = [];
10653
+ if (!config.apiKey) problems.push("SAKANA_API_KEY or MMI_OVERLORD_LLM_API_KEY is not available");
10654
+ if (!config.baseUrl) problems.push("Fugu API base URL is not configured");
10655
+ if (!config.apiKey || !config.baseUrl) return { ok: false, problems };
10656
+ const controller = new AbortController();
10657
+ const timeout = setTimeout(() => controller.abort(), Math.min(config.timeoutMs, 15e3));
10339
10658
  try {
10340
- return (0, import_node_fs15.existsSync)(catalogPath) ? (0, import_node_fs15.readFileSync)(catalogPath, "utf8") : "";
10341
- } catch {
10342
- return "";
10659
+ const response = await fetch(`${config.baseUrl}/models`, {
10660
+ method: "GET",
10661
+ headers: { Authorization: `Bearer ${config.apiKey}` },
10662
+ signal: controller.signal
10663
+ });
10664
+ if (!response.ok) {
10665
+ problems.push(`Fugu API model probe returned HTTP ${response.status}`);
10666
+ } else {
10667
+ const body = await response.json();
10668
+ const models = new Set((body.data ?? []).map((model) => model.id).filter((id) => Boolean(id)));
10669
+ if (!models.has(config.model)) problems.push(`Fugu API model ${config.model} is not available`);
10670
+ if (!models.has(config.ultraModel)) problems.push(`Fugu API ultra model ${config.ultraModel} is not available`);
10671
+ }
10672
+ } catch (e) {
10673
+ problems.push(`Fugu API model probe failed: ${e instanceof Error ? e.message : String(e)}`);
10674
+ } finally {
10675
+ clearTimeout(timeout);
10343
10676
  }
10677
+ return { ok: problems.length === 0, problems };
10344
10678
  }
10345
- function hasCodexAuthEvidence() {
10346
- const codexHome = process.env.CODEX_HOME ?? (0, import_node_path13.join)((0, import_node_os3.homedir)(), ".codex");
10347
- return (0, import_node_fs15.existsSync)((0, import_node_path13.join)(codexHome, "auth.json"));
10348
- }
10349
- function collectOpenCodePreflight() {
10350
- const version = boundedCommandText("opencode", ["--version"]);
10351
- const models = boundedCommandText("opencode", ["models"]);
10352
- return evaluateOpenCodePreflight({
10353
- found: version.found,
10354
- versionText: version.text,
10355
- modelsText: models.text
10356
- });
10357
- }
10358
- function collectCodexFuguPreflight() {
10359
- const codexHelp = boundedCommandText("codex", ["--help"]);
10360
- const fuguHelp = boundedCommandText("codex-fugu", ["--help"]);
10361
- const fuguStatus = boundedCommandText("codex-fugu", ["--status"]);
10362
- return evaluateCodexFuguPreflight({
10363
- codexFound: codexHelp.found,
10364
- fuguFound: fuguHelp.found,
10365
- helpText: fuguHelp.text || codexHelp.text,
10366
- statusText: fuguStatus.text,
10367
- modelCatalogText: readFuguModelCatalogText(),
10368
- authConfigured: hasCodexAuthEvidence(),
10369
- envNames: Object.keys(process.env)
10370
- });
10679
+ function mapFuguApiUsage(usage) {
10680
+ if (!usage) return void 0;
10681
+ const mapped = {
10682
+ promptTokens: usage.prompt_tokens,
10683
+ completionTokens: usage.completion_tokens,
10684
+ totalTokens: usage.total_tokens
10685
+ };
10686
+ return Object.values(mapped).some((value) => typeof value === "number") ? mapped : void 0;
10687
+ }
10688
+ async function defaultRunFuguApi(run, servant, message, options) {
10689
+ const config = fuguApiConfig();
10690
+ if (!config.apiKey) return { ok: false, error: "SAKANA_API_KEY or MMI_OVERLORD_LLM_API_KEY is not available" };
10691
+ const model = fuguApiModelForServant(config, servant);
10692
+ const messages = [
10693
+ ...servant.llmMessages ?? [{ role: "system", content: fuguApiSystemMessage(servant, run) }],
10694
+ { role: "user", content: message }
10695
+ ];
10696
+ const controller = new AbortController();
10697
+ const timeout = setTimeout(() => controller.abort(), options?.timeoutMs ?? config.timeoutMs);
10698
+ try {
10699
+ const response = await fetch(`${config.baseUrl}/chat/completions`, {
10700
+ method: "POST",
10701
+ headers: {
10702
+ Authorization: `Bearer ${config.apiKey}`,
10703
+ "Content-Type": "application/json"
10704
+ },
10705
+ body: JSON.stringify({
10706
+ model,
10707
+ messages,
10708
+ temperature: 0.2
10709
+ }),
10710
+ signal: controller.signal
10711
+ });
10712
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? void 0;
10713
+ const json = await response.json().catch(() => void 0);
10714
+ if (!response.ok) {
10715
+ return {
10716
+ ok: false,
10717
+ model,
10718
+ requestId,
10719
+ error: json?.error?.message ?? `Fugu API returned HTTP ${response.status}`,
10720
+ retryable: response.status === 429 || response.status >= 500
10721
+ };
10722
+ }
10723
+ const text = json?.choices?.[0]?.message?.content?.trim() ?? "";
10724
+ if (!text) return { ok: false, model, requestId, error: "Fugu API returned no assistant text" };
10725
+ return {
10726
+ ok: true,
10727
+ text,
10728
+ model,
10729
+ requestId,
10730
+ usage: mapFuguApiUsage(json?.usage),
10731
+ messages: [...messages, { role: "assistant", content: text }]
10732
+ };
10733
+ } catch (e) {
10734
+ const aborted = e instanceof Error && e.name === "AbortError";
10735
+ return {
10736
+ ok: false,
10737
+ model,
10738
+ error: aborted ? "Fugu API request timed out" : e instanceof Error ? e.message : String(e),
10739
+ retryable: true
10740
+ };
10741
+ } finally {
10742
+ clearTimeout(timeout);
10743
+ }
10371
10744
  }
10372
- function renderCodexFuguPreflightFailure(report) {
10745
+ function renderOverlordPreflightFailure(report) {
10373
10746
  const lines = [
10374
10747
  "Overlord setup needed",
10375
- "The servant pool was not started because Codex/Fugu preflight failed.",
10748
+ "The servant pool was not started because Fugu preflight failed.",
10376
10749
  "",
10377
10750
  "Problems:",
10378
10751
  ...report.problems.map((problem) => `- ${problem}`),
10379
10752
  "",
10380
10753
  "Fixes:"
10381
10754
  ];
10382
- if (report.problems.some((problem) => problem.includes("codex is not installed"))) {
10383
- lines.push("- Install or update Codex, then confirm with `codex --version`.");
10384
- }
10385
- if (report.problems.some((problem) => problem.includes("codex-fugu is not installed"))) {
10386
- lines.push("- Install or repair codex-fugu, then confirm with `codex-fugu --status`.");
10387
- }
10388
- if (report.problems.some((problem) => problem.includes("missing API key"))) {
10389
- lines.push("- Set OPENAI_API_KEY or CODEX_API_KEY in the launching shell; do not paste key values into chat.");
10755
+ if (report.problems.some((problem) => problem.includes("SAKANA_API_KEY") || problem.includes("Fugu API"))) {
10756
+ lines.push("- Confirm `SAKANA_API_KEY` is available and `https://api.sakana.ai/v1/models` is reachable.");
10390
10757
  }
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.");
10758
+ if (report.problems.some((problem) => problem.includes("model") || problem.includes("fugu"))) {
10759
+ lines.push("- Confirm the configured Fugu API models are exposed by `/models`.");
10399
10760
  }
10400
10761
  return lines.join("\n");
10401
10762
  }
10402
- function updateOpenCodeServant(run, servant, result, timestamp) {
10763
+ function markServantsStarting(run, timestamp) {
10764
+ return {
10765
+ ...run,
10766
+ updatedAt: timestamp,
10767
+ servants: run.servants.map((servant) => ({
10768
+ ...servant,
10769
+ state: "starting"
10770
+ }))
10771
+ };
10772
+ }
10773
+ async function mapWithConcurrency(items, limit, worker) {
10774
+ const results = new Array(items.length);
10775
+ let nextIndex = 0;
10776
+ const workerCount = Math.min(Math.max(1, limit), items.length);
10777
+ await Promise.all(Array.from({ length: workerCount }, async () => {
10778
+ while (true) {
10779
+ const index = nextIndex;
10780
+ nextIndex += 1;
10781
+ if (index >= items.length) return;
10782
+ results[index] = await worker(items[index]);
10783
+ }
10784
+ }));
10785
+ return results;
10786
+ }
10787
+ function hasCapturedFuguApiText(result) {
10788
+ return result.ok && Boolean(result.text?.trim());
10789
+ }
10790
+ function updateFuguApiServant(servant, result, timestamp) {
10791
+ const captured = hasCapturedFuguApiText(result);
10403
10792
  return {
10404
10793
  ...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,
10794
+ state: captured ? "ready" : "blocked",
10795
+ llmModel: result.model ?? servant.llmModel,
10796
+ llmMessages: result.messages ?? servant.llmMessages,
10797
+ llmRequestId: result.requestId ?? servant.llmRequestId,
10798
+ lastAckAt: captured ? timestamp : servant.lastAckAt,
10799
+ lastUsefulSignalAt: captured ? timestamp : servant.lastUsefulSignalAt,
10409
10800
  lastEventAt: timestamp,
10410
- lastMessageCompletedAt: result.ok ? timestamp : servant.lastMessageCompletedAt
10801
+ lastMessageCompletedAt: captured ? timestamp : servant.lastMessageCompletedAt
10411
10802
  };
10412
10803
  }
10413
- function startOpenCodeServants(run, runOpenCode, now) {
10804
+ function defaultSleep3(ms) {
10805
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
10806
+ }
10807
+ function fuguApiRetryConfig(sleep) {
10808
+ const parsed = Number(process.env.MMI_OVERLORD_LLM_MAX_ATTEMPTS);
10809
+ const maxAttempts = Number.isFinite(parsed) && parsed >= 1 ? Math.min(Math.trunc(parsed), 6) : FUGU_API_DEFAULT_MAX_ATTEMPTS;
10810
+ return { maxAttempts, baseDelayMs: FUGU_API_RETRY_BASE_MS, sleep };
10811
+ }
10812
+ function fuguApiBackoffMs(attempt, baseDelayMs) {
10813
+ const exponential = baseDelayMs * 2 ** (attempt - 1);
10814
+ const capped = Math.min(exponential, FUGU_API_RETRY_CAP_MS);
10815
+ const jitter = Math.random() * capped * 0.25;
10816
+ return Math.round(capped + jitter);
10817
+ }
10818
+ async function callFuguApiRunner(runFuguApi, run, servant, message, retry, callOptions) {
10819
+ const maxAttempts = Math.max(1, retry?.maxAttempts ?? 1);
10820
+ const baseDelayMs = retry?.baseDelayMs ?? FUGU_API_RETRY_BASE_MS;
10821
+ const sleep = retry?.sleep ?? defaultSleep3;
10822
+ let attempt = 0;
10823
+ for (; ; ) {
10824
+ attempt += 1;
10825
+ let result;
10826
+ try {
10827
+ result = await runFuguApi(run, servant, message, callOptions);
10828
+ } catch (e) {
10829
+ result = { ok: false, error: e instanceof Error ? e.message : String(e), retryable: true };
10830
+ }
10831
+ if (result.ok || !result.retryable || attempt >= maxAttempts) {
10832
+ return { ...result, attempts: attempt };
10833
+ }
10834
+ await sleep(fuguApiBackoffMs(attempt, baseDelayMs));
10835
+ }
10836
+ }
10837
+ function seedLazyServant(servant, run, timestamp) {
10838
+ const config = fuguApiConfig();
10839
+ const llmMessages = servant.llmMessages ?? [{ role: "system", content: fuguApiSystemMessage(servant, run) }];
10840
+ return {
10841
+ ...servant,
10842
+ state: "ready",
10843
+ llmModel: fuguApiModelForServant(config, servant),
10844
+ llmMessages,
10845
+ lastAckAt: timestamp,
10846
+ lastUsefulSignalAt: timestamp,
10847
+ lastEventAt: timestamp
10848
+ };
10849
+ }
10850
+ async function startFuguApiServants(run, runFuguApi, now, retry) {
10851
+ const startupCallOptions = { timeoutMs: fuguApiStartupTimeoutMs() };
10852
+ const verifiedTargets = run.servants.filter((servant) => servant.role !== "ultra");
10853
+ const results = await mapWithConcurrency(verifiedTargets, overlordServantParallelism(), async (servant) => ({
10854
+ servant,
10855
+ result: await callFuguApiRunner(runFuguApi, run, servant, overlordServantPrompt(servant, run), retry, startupCallOptions)
10856
+ }));
10414
10857
  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
- });
10858
+ const bySlot = /* @__PURE__ */ new Map();
10859
+ for (const { servant, result } of results) {
10860
+ bySlot.set(servant.slotId, result);
10861
+ 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 });
10862
+ appendServantJournal(servant.journalPath, "consultation", result.text, timestamp);
10863
+ }
10864
+ 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
10865
  return { ...run, state: "active", updatedAt: timestamp, servants };
10421
10866
  }
10422
- function dispatchOpenCodeMessage(run, message, runOpenCode, now) {
10423
- const timestamp = isoNow(now);
10867
+ async function dispatchFuguApiMessage(run, message, runFuguApi, now, retry) {
10868
+ const startedAt = isoNow(now);
10424
10869
  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;
10870
+ const results = await mapWithConcurrency(targets, overlordServantParallelism(), async (servant) => ({
10871
+ servant,
10872
+ result: await callFuguApiRunner(runFuguApi, run, servant, message.text, retry)
10873
+ }));
10874
+ const completedAt = isoNow(now);
10875
+ const successCount = results.filter(({ result }) => hasCapturedFuguApiText(result)).length;
10876
+ const failureCount = Math.max(0, targets.length - successCount);
10877
+ const state = targets.length > 0 && failureCount === 0 ? "completed" : successCount > 0 && message.target === "all" ? "partial" : "failed";
10878
+ const responses = results.filter(({ result }) => result.text).map(({ servant, result }) => `${servant.slotId}: ${result.text}`);
10879
+ const servantResults = results.map(({ servant, result }) => {
10880
+ if (hasCapturedFuguApiText(result)) {
10881
+ const warning = formatHandoffWarnings(validateServantReport(result.text ?? ""));
10882
+ return {
10883
+ slotId: servant.slotId,
10884
+ state: "completed",
10885
+ responseText: result.text?.trim(),
10886
+ handoffWarnings: warning ? [warning] : void 0,
10887
+ usage: result.usage
10888
+ };
10889
+ }
10890
+ return {
10891
+ slotId: servant.slotId,
10892
+ state: "failed",
10893
+ failureReason: result.error ?? "Fugu API servant returned no text"
10894
+ };
10895
+ });
10896
+ const bySlot = /* @__PURE__ */ new Map();
10897
+ for (const { servant, result } of results) {
10898
+ bySlot.set(servant.slotId, result);
10899
+ 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 });
10900
+ appendServantJournal(servant.journalPath, `message ${message.id}`, result.text, completedAt);
10901
+ }
10427
10902
  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);
10903
+ const result = bySlot.get(servant.slotId);
10904
+ return result ? updateFuguApiServant(servant, result, completedAt) : servant;
10434
10905
  });
10435
10906
  const nextMessage = {
10436
10907
  ...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,
10908
+ state,
10909
+ startedAt,
10910
+ completedAt: state === "completed" || state === "partial" ? completedAt : void 0,
10911
+ failedAt: state === "failed" ? completedAt : void 0,
10912
+ ackText: state === "completed" ? "Fugu API response captured" : state === "partial" ? "Fugu API partial response captured" : void 0,
10442
10913
  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(",")
10914
+ 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",
10915
+ servantResults
10445
10916
  };
10446
10917
  return {
10447
10918
  ...run,
10448
- updatedAt: timestamp,
10919
+ updatedAt: completedAt,
10449
10920
  servants: nextServants,
10450
10921
  messages: [...(run.messages ?? []).filter((m) => m.id !== message.id), nextMessage]
10451
10922
  };
@@ -10465,6 +10936,7 @@ function hasServantTarget(run, target) {
10465
10936
  );
10466
10937
  }
10467
10938
  function messageProgress(message, now = /* @__PURE__ */ new Date(), timeoutMs = OVERLORD_HANDOFF_TIMEOUT_MS) {
10939
+ if (message.state === "partial") return "partial";
10468
10940
  if (message.completedAt || message.state === "completed") return "completed";
10469
10941
  if (message.failedAt || message.state === "failed") return "failed";
10470
10942
  const startedAt = message.startedAt ?? message.deliveredAt;
@@ -10476,26 +10948,103 @@ function servantProgress(run, servant, now = /* @__PURE__ */ new Date()) {
10476
10948
  const relevant = (run.messages ?? []).filter((message) => message.target === "all" || servant.slotId === message.target || normalizeServantTarget(servant.name) === message.target);
10477
10949
  return relevant.some((message) => messageProgress(message, now) === "stalled") ? "stalled-after-delivery" : servant.state;
10478
10950
  }
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");
10951
+ function humanSafeText(text, maxLength = 160) {
10952
+ 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();
10953
+ if (redacted.length <= maxLength) return redacted;
10954
+ return `${redacted.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
10955
+ }
10956
+ function renderOverlordSendSummary(message, statePath) {
10957
+ const results = message?.servantResults ?? [];
10958
+ const completed = results.filter((result) => result.state === "completed").length;
10959
+ const blocked = results.filter((result) => result.state === "failed").length;
10960
+ const state = message?.state ?? "failed";
10961
+ const lines = [
10962
+ `Message: ${message?.id ?? "unknown"}`,
10963
+ `Target: ${message?.target ?? "unknown"}`,
10964
+ `Result: ${state} - ${completed} completed, ${blocked} blocked.`
10965
+ ];
10966
+ for (const result of results.filter((item) => item.state === "failed").slice(0, 3)) {
10967
+ lines.push(`Blocked: ${result.slotId} - ${humanSafeText(result.failureReason ?? "no reason reported")}`);
10968
+ }
10969
+ const hidden = Math.max(0, blocked - 3);
10970
+ if (hidden > 0) lines.push(`Blocked: +${hidden} more`);
10971
+ lines.push(`State: ${statePath}`);
10972
+ return lines.join("\n");
10973
+ }
10974
+ function countValues(values, value) {
10975
+ return values.filter((item) => item === value).length;
10976
+ }
10977
+ function latestServantFailureReason(run, slotId) {
10978
+ for (const message of [...run.messages ?? []].reverse()) {
10979
+ const result = message.servantResults?.find((item) => item.slotId === slotId && item.state === "failed");
10980
+ if (result?.failureReason?.trim()) return result.failureReason;
10981
+ }
10982
+ return void 0;
10983
+ }
10984
+ function affectedServantStatusLabel(state, reason) {
10985
+ if (state === "stalled-after-delivery") return "Stalled";
10986
+ if (/\btimed?\s*out\b/i.test(reason ?? "")) return "Timed out";
10987
+ return "Blocked";
10988
+ }
10989
+ function renderOverlordStatus(summary, run, now = /* @__PURE__ */ new Date()) {
10990
+ const servantStates = summary.servants.map((servant) => servant.state);
10991
+ const messageStates = (run.messages ?? []).map((message) => messageProgress(message, now));
10992
+ const activeMessages = messageStates.filter((state) => state === "queued" || state === "started" || state === "stalled").length;
10993
+ const blockedServants = summary.servants.filter((servant) => servant.state === "blocked" || servant.state === "stalled-after-delivery");
10994
+ const latest = (run.messages ?? []).at(-1);
10995
+ const latestState = latest ? messageProgress(latest, now) : void 0;
10482
10996
  return [
10483
10997
  `Overlord run ${summary.runId}`,
10484
10998
  `State: ${summary.state}`,
10485
- `Task: ${run.task || "not provided yet"}`,
10486
- `Controller: ${summary.controller}`,
10487
- "Servants:",
10488
- servants,
10489
- ...messages ? ["Messages:", messages] : []
10999
+ `Mission: ${humanSafeText(run.task || "not provided yet")}`,
11000
+ `Servants: ${countValues(servantStates, "ready")} ready, ${countValues(servantStates, "blocked") + countValues(servantStates, "stalled-after-delivery")} blocked, ${countValues(servantStates, "planned")} planned.`,
11001
+ `Messages: ${countValues(messageStates, "completed")} completed, ${countValues(messageStates, "partial")} partial, ${countValues(messageStates, "failed")} failed, ${activeMessages} active.`,
11002
+ ...blockedServants.slice(0, 3).map((servant) => {
11003
+ const slotId = run.servants.find((item) => item.name === servant.name)?.slotId;
11004
+ const reason = slotId ? latestServantFailureReason(run, slotId) : void 0;
11005
+ const label = affectedServantStatusLabel(servant.state, reason);
11006
+ const detail = reason ?? (servant.state === "stalled-after-delivery" ? "no recent update" : void 0);
11007
+ return `${label}: ${servant.name}${detail ? ` - ${humanSafeText(detail)}` : ""}`;
11008
+ }),
11009
+ ...blockedServants.length > 3 ? [`Blocked: +${blockedServants.length - 3} more`] : [],
11010
+ ...latest && latestState ? [`Latest: ${latest.id} ${latest.target} ${latestState}${latest.failureReason ? ` - ${humanSafeText(latest.failureReason)}` : ""}`] : [],
11011
+ `Coordinator: ${summary.controller}`
10490
11012
  ].join("\n");
10491
11013
  }
10492
11014
  function usefulJournalLines(text) {
10493
11015
  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
11016
  }
11017
+ var HANDOFF_SIGNAL = /\b(handoff|evidence|recommend(?:ed|ation|s)?|improvement|suggest(?:ion|s)?|next action|proposal|file)\b|`[^`]+`/i;
11018
+ var HANDOFF_MIN_RESPONSE_CHARS = 12;
11019
+ function parseJournalBlocks(raw) {
11020
+ const blocks = [];
11021
+ let current = null;
11022
+ for (const line of raw.split(/\r?\n/)) {
11023
+ const header = line.match(/^\[[^\]]+\]\s+(.+)$/);
11024
+ if (header) {
11025
+ if (current) blocks.push(current);
11026
+ current = { label: header[1].trim(), body: "" };
11027
+ } else if (current) {
11028
+ current.body += (current.body ? "\n" : "") + line;
11029
+ }
11030
+ }
11031
+ if (current) blocks.push(current);
11032
+ return blocks;
11033
+ }
11034
+ function journalHasHandoff(raw, lines) {
11035
+ for (const block of parseJournalBlocks(raw)) {
11036
+ if (!block.label.startsWith("message ")) continue;
11037
+ const body = block.body.trim();
11038
+ if (body.length >= HANDOFF_MIN_RESPONSE_CHARS) return true;
11039
+ if (validateServantReport(body).applies) return true;
11040
+ }
11041
+ return lines.some((line) => HANDOFF_SIGNAL.test(line));
11042
+ }
10495
11043
  function servantJournalSummary(servant) {
10496
11044
  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)) };
11045
+ const raw = (0, import_node_fs15.readFileSync)(servant.journalPath, "utf8");
11046
+ const lines = usefulJournalLines(raw);
11047
+ return { lines, hasHandoff: journalHasHandoff(raw, lines) };
10499
11048
  } catch {
10500
11049
  return { lines: [], hasHandoff: false };
10501
11050
  }
@@ -10519,7 +11068,11 @@ function runJson(run, extra = {}) {
10519
11068
  pid: servant.pid,
10520
11069
  journalPath: servant.journalPath,
10521
11070
  engine: servant.engine,
10522
- opencodeSessionId: servant.opencodeSessionId
11071
+ opencodeSessionId: servant.opencodeSessionId,
11072
+ scopeToken: servant.scopeToken,
11073
+ llmModel: servant.llmModel,
11074
+ llmRequestId: servant.llmRequestId,
11075
+ llmConversationLength: servant.llmMessages?.length
10523
11076
  })),
10524
11077
  ...extra
10525
11078
  };
@@ -10530,27 +11083,33 @@ function wantsJson(options, command) {
10530
11083
  function countArgsFromOptions(options) {
10531
11084
  return ["3", "4", "5", "6"].filter((key) => options[key]).map((key) => `--${key}`);
10532
11085
  }
11086
+ function percentile(values, p) {
11087
+ if (values.length === 0) return 0;
11088
+ const sorted = [...values].sort((a, b) => a - b);
11089
+ const rank = Math.ceil(p / 100 * sorted.length);
11090
+ return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
11091
+ }
10533
11092
  function registerOverlordCommands(program3, deps = {}) {
10534
11093
  const out = deps.out ?? ((text) => process.stdout.write(text));
10535
11094
  const err = deps.err ?? ((text) => process.stderr.write(text));
10536
11095
  const cwd = deps.cwd ?? (() => process.cwd());
10537
11096
  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;
11097
+ const fuguApiPreflight = deps.fuguApiPreflight ?? collectFuguApiPreflight;
11098
+ const runFuguApi = deps.runFuguApi ?? defaultRunFuguApi;
11099
+ const fuguApiRetry = fuguApiRetryConfig(deps.sleep ?? defaultSleep3);
10542
11100
  const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
10543
11101
  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) => {
11102
+ 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
11103
  try {
10546
11104
  const args = [...countArgsFromOptions(options), ...task];
10547
11105
  const root = cwd();
10548
11106
  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();
11107
+ const engine = `${options.engine ?? OVERLORD_DEFAULT_ENGINE}`;
11108
+ if (engine !== "fugu-api") throw new Error("--engine must be fugu-api");
11109
+ if (!options.json) out("Starting Overlord.\nLoading run registry...\nChecking Fugu API...\n");
11110
+ const preflightReport = await fuguApiPreflight();
10552
11111
  if (!preflightReport.ok) {
10553
- err(`${renderCodexFuguPreflightFailure(preflightReport)}
11112
+ err(`${renderOverlordPreflightFailure(preflightReport)}
10554
11113
  `);
10555
11114
  process.exitCode = 1;
10556
11115
  return;
@@ -10570,30 +11129,25 @@ function registerOverlordCommands(program3, deps = {}) {
10570
11129
  runToken: deps.runToken
10571
11130
  });
10572
11131
  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
- }
11132
+ if (!options.json) out(`Summoning ${run.servants.length} Fugu servants...
11133
+ Preparing conversations...
11134
+ `);
11135
+ run = markServantsStarting(run, isoNow(now));
11136
+ writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
11137
+ run = await startFuguApiServants(run, runFuguApi, now, fuguApiRetry);
11138
+ writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
10587
11139
  if (options.json) out(`${JSON.stringify(runJson(run, { nextPhase: "consult-servants" }), null, 2)}
10588
11140
  `);
10589
11141
  else {
11142
+ const ready = run.servants.filter((servant) => servant.state === "ready").length;
10590
11143
  out(`${[
10591
- "Overlord startup",
10592
- `Task: ${run.task || "not provided yet"}`,
11144
+ `Ready: ${ready}/${run.servants.length} servants ready.`,
11145
+ `Mission: ${humanSafeText(run.task || "not provided yet")}`,
10593
11146
  `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."
11147
+ "Engine: Fugu API",
11148
+ "",
11149
+ "Overlord is idle and ready for your instruction.",
11150
+ "Next: I will consult the servants and interview you, then propose a todo list for your approval."
10597
11151
  ].join("\n")}
10598
11152
  `);
10599
11153
  }
@@ -10606,7 +11160,7 @@ function registerOverlordCommands(program3, deps = {}) {
10606
11160
  process.exitCode = 1;
10607
11161
  }
10608
11162
  });
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) => {
11163
+ 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("--json", "print machine-readable output").action(async (target, message = [], options, command) => {
10610
11164
  const json = wantsJson(options, command);
10611
11165
  try {
10612
11166
  const statePath = defaultOverlordStatePath(cwd());
@@ -10626,35 +11180,30 @@ function registerOverlordCommands(program3, deps = {}) {
10626
11180
  queuedAt: timestamp,
10627
11181
  state: "queued"
10628
11182
  };
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)}
11183
+ if (run.engine !== "fugu-api") throw new Error("active Overlord run does not use the fugu-api engine");
11184
+ if (!json) out(`Sending assignment to ${normalized}...
11185
+ Awaiting Fugu responses...
10636
11186
  `);
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 = {
11187
+ const started = {
10644
11188
  ...run,
10645
11189
  updatedAt: timestamp,
10646
- messages: [...run.messages ?? [], queued]
11190
+ messages: [...(run.messages ?? []).filter((m) => m.id !== queued.id), {
11191
+ ...queued,
11192
+ state: "started",
11193
+ startedAt: timestamp
11194
+ }]
10647
11195
  };
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 };
11196
+ writeOverlordRegistry(statePath, { ...registry2, runs: { ...registry2.runs, [run.runId]: started } });
11197
+ const dispatched = await dispatchFuguApiMessage(started, queued, runFuguApi, now, fuguApiRetry);
11198
+ writeOverlordRegistry(statePath, { ...registry2, runs: { ...registry2.runs, [run.runId]: dispatched } });
11199
+ const settled = (dispatched.messages ?? []).find((m) => m.id === queued.id);
11200
+ const ok = settled?.state === "completed" || settled?.state === "partial";
11201
+ 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
11202
  if (json) out(`${JSON.stringify(payload, null, 2)}
10654
11203
  `);
10655
- else out(`Queued message ${queued.id} for ${normalized}.
10656
- State: ${statePath}
11204
+ else out(`${renderOverlordSendSummary(settled, statePath)}
10657
11205
  `);
11206
+ if (!ok) process.exitCode = 1;
10658
11207
  } catch (e) {
10659
11208
  const messageText = e instanceof Error ? e.message : String(e);
10660
11209
  if (json) out(`${JSON.stringify({ ok: false, error: messageText }, null, 2)}
@@ -10691,6 +11240,10 @@ State: ${payload2.statePath}
10691
11240
  slotId: servant.slotId,
10692
11241
  engine: servant.engine,
10693
11242
  opencodeSessionId: servant.opencodeSessionId,
11243
+ scopeToken: servant.scopeToken,
11244
+ llmModel: servant.llmModel,
11245
+ llmRequestId: servant.llmRequestId,
11246
+ llmConversationLength: servant.llmMessages?.length,
10694
11247
  eventJournalPath: servant.eventJournalPath,
10695
11248
  lastEventAt: servant.lastEventAt,
10696
11249
  lastMessageCompletedAt: servant.lastMessageCompletedAt
@@ -10711,7 +11264,7 @@ State: ${payload2.statePath}
10711
11264
  };
10712
11265
  if (json) out(`${JSON.stringify(payload, null, 2)}
10713
11266
  `);
10714
- else out(`${renderOverlordStatus(summary, run)}
11267
+ else out(`${renderOverlordStatus(summary, run, current)}
10715
11268
  State: ${statePath}
10716
11269
  `);
10717
11270
  });
@@ -10794,17 +11347,128 @@ State: ${payload2.statePath}
10794
11347
  `);
10795
11348
  }
10796
11349
  });
11350
+ 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) => {
11351
+ const json = wantsJson(options, command);
11352
+ const repeat = Math.min(Math.max(1, Math.trunc(Number(options.repeat) || 1)), 20);
11353
+ const countArgs = countArgsFromOptions(options);
11354
+ try {
11355
+ if (!json) out("Overlord canary starting.\nChecking Fugu API...\n");
11356
+ const preflight2 = await fuguApiPreflight();
11357
+ if (!preflight2.ok) {
11358
+ if (json) out(`${JSON.stringify({ ok: false, phase: "preflight", problems: preflight2.problems }, null, 2)}
11359
+ `);
11360
+ else err(`${renderOverlordPreflightFailure(preflight2)}
11361
+ `);
11362
+ process.exitCode = 1;
11363
+ return;
11364
+ }
11365
+ const runOnce = async (verbose) => {
11366
+ const startedAtMs = now().getTime();
11367
+ const plan2 = buildOverlordStartupPlan(countArgs, cwd());
11368
+ let run = buildOverlordRun({ task: "overlord canary smoke", cwd: cwd(), count: plan2.count, engine: "fugu-api", now, runId: deps.runId, runToken: deps.runToken });
11369
+ if (verbose) out(`Summoning ${run.servants.length} Fugu servants...
11370
+ `);
11371
+ run = markServantsStarting(run, isoNow(now));
11372
+ run = await startFuguApiServants(run, runFuguApi, now, fuguApiRetry);
11373
+ const ready = run.servants.filter((servant) => servant.state === "ready").length;
11374
+ const problems = [];
11375
+ let responded = 0;
11376
+ let evidence;
11377
+ if (ready === run.servants.length) {
11378
+ if (verbose) out("Sending canary probe...\n");
11379
+ const timestamp = isoNow(now);
11380
+ const probe = {
11381
+ id: `${deps.runId?.() ?? defaultMessageId()}-canary`,
11382
+ target: "all",
11383
+ text: "Reply with the single word READY and nothing else.",
11384
+ createdAt: timestamp,
11385
+ queuedAt: timestamp,
11386
+ state: "queued"
11387
+ };
11388
+ run = await dispatchFuguApiMessage(run, probe, runFuguApi, now, fuguApiRetry);
11389
+ const settled = (run.messages ?? []).find((message) => message.id === probe.id);
11390
+ const results = settled?.servantResults ?? [];
11391
+ responded = results.filter((result) => result.state === "completed" && result.responseText?.trim()).length;
11392
+ evidence = results.find((result) => result.responseText?.trim())?.responseText?.trim();
11393
+ if (responded !== run.servants.length) problems.push(`only ${responded}/${run.servants.length} servants returned text`);
11394
+ } else {
11395
+ problems.push(`only ${ready}/${run.servants.length} servants became ready`);
11396
+ }
11397
+ const stopPlan = planOverlordRunStop(run);
11398
+ for (const pid of stopPlan.killPids) {
11399
+ try {
11400
+ killPid(pid);
11401
+ } catch {
11402
+ }
11403
+ }
11404
+ const cleanup = { stopped: stopPlan.killPids.length, uncertain: stopPlan.uncertain.length };
11405
+ if (cleanup.uncertain > 0) problems.push(`${cleanup.uncertain} uncertain resource(s) left untouched`);
11406
+ return { ok: problems.length === 0, runId: run.runId, requested: run.servants.length, ready, responded, elapsedMs: Math.max(0, now().getTime() - startedAtMs), evidence, cleanup, problems };
11407
+ };
11408
+ const runs = [];
11409
+ for (let i = 0; i < repeat; i += 1) {
11410
+ if (!json && repeat > 1) out(`Canary run ${i + 1}/${repeat}...
11411
+ `);
11412
+ runs.push(await runOnce(!json && repeat === 1));
11413
+ }
11414
+ if (repeat === 1) {
11415
+ const r = runs[0];
11416
+ 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)}
11417
+ `);
11418
+ else out(`${[
11419
+ r.ok ? "Canary OK." : "Canary FAILED.",
11420
+ `Ready: ${r.ready}/${r.requested}; Responded: ${r.responded}/${r.requested}`,
11421
+ `Cleanup: stopped ${r.cleanup.stopped}, uncertain ${r.cleanup.uncertain}`,
11422
+ ...r.problems.length ? [`Problems: ${r.problems.join("; ")}`] : []
11423
+ ].join("\n")}
11424
+ `);
11425
+ if (!r.ok) process.exitCode = 1;
11426
+ return;
11427
+ }
11428
+ const elapsed = runs.map((r) => r.elapsedMs);
11429
+ const passes = runs.filter((r) => r.ok).length;
11430
+ const requested = runs[0].requested;
11431
+ const ok = passes === runs.length;
11432
+ const summary = {
11433
+ ok,
11434
+ repeat,
11435
+ passes,
11436
+ requested,
11437
+ elapsedMs: { p50: percentile(elapsed, 50), p90: percentile(elapsed, 90), min: Math.min(...elapsed), max: Math.max(...elapsed) },
11438
+ readyRate: runs.reduce((sum, r) => sum + r.ready, 0) / (runs.length * requested),
11439
+ respondedRate: runs.reduce((sum, r) => sum + r.responded, 0) / (runs.length * requested),
11440
+ problems: runs.flatMap((r, i) => r.problems.map((problem) => `run ${i + 1}: ${problem}`))
11441
+ };
11442
+ if (json) out(`${JSON.stringify(summary, null, 2)}
11443
+ `);
11444
+ else out(`${[
11445
+ ok ? `Canary bench OK (${passes}/${repeat} passed).` : `Canary bench: ${passes}/${repeat} passed.`,
11446
+ `Elapsed p50 ${summary.elapsedMs.p50}ms, p90 ${summary.elapsedMs.p90}ms (min ${summary.elapsedMs.min}, max ${summary.elapsedMs.max})`,
11447
+ `Ready rate ${(summary.readyRate * 100).toFixed(0)}%, responded rate ${(summary.respondedRate * 100).toFixed(0)}%`,
11448
+ ...summary.problems.length ? [`Problems: ${summary.problems.join("; ")}`] : []
11449
+ ].join("\n")}
11450
+ `);
11451
+ if (!ok) process.exitCode = 1;
11452
+ } catch (e) {
11453
+ const message = e instanceof Error ? e.message : String(e);
11454
+ if (json) out(`${JSON.stringify({ ok: false, error: message }, null, 2)}
11455
+ `);
11456
+ else err(`overlord canary: ${message}
11457
+ `);
11458
+ process.exitCode = 1;
11459
+ }
11460
+ });
10797
11461
  return overlord;
10798
11462
  }
10799
11463
 
10800
11464
  // src/throttle-commands.ts
10801
- var import_node_child_process9 = require("node:child_process");
11465
+ var import_node_child_process8 = require("node:child_process");
10802
11466
  var import_node_fs16 = require("node:fs");
10803
11467
  var import_node_path14 = require("node:path");
10804
11468
  var THROTTLE_TRACE_REL = (0, import_node_path14.join)(".mmi", "throttle", "trace.jsonl");
10805
11469
  function resolveRepoGitRoot(cwd = process.cwd()) {
10806
11470
  try {
10807
- const root = (0, import_node_child_process9.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
11471
+ const root = (0, import_node_child_process8.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
10808
11472
  encoding: "utf8",
10809
11473
  timeout: 5e3
10810
11474
  }).trim();
@@ -10953,7 +11617,7 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
10953
11617
  }
10954
11618
 
10955
11619
  // src/board.ts
10956
- var import_node_child_process10 = require("node:child_process");
11620
+ var import_node_child_process9 = require("node:child_process");
10957
11621
  var import_node_util6 = require("node:util");
10958
11622
 
10959
11623
  // src/board-priority.ts
@@ -11061,7 +11725,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
11061
11725
  var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
11062
11726
 
11063
11727
  // src/board.ts
11064
- var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process10.execFile);
11728
+ var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process9.execFile);
11065
11729
  var BOARD_GIT_TIMEOUT_MS = 1e4;
11066
11730
  var WRITE_PROBE_CONCURRENCY = 8;
11067
11731
  var CLAIM_CONCURRENCY = 5;
@@ -12524,6 +13188,8 @@ var MANAGED_GITIGNORE_LINES = [
12524
13188
  // Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are S3-synced
12525
13189
  // via `mmi-cli northstar push` / auto-save on write; never git-tracked (AGENTS.md "Repo cleanliness", #1550, #1842).
12526
13190
  "**/plans/",
13191
+ // Superpowers plan/spec output is scratch too; authoritative docs live directly under docs/.
13192
+ "docs/superpowers/",
12527
13193
  ".playwright-mcp/",
12528
13194
  ".claude/worktrees/",
12529
13195
  // .mmi is agent/CI scratch — ignore the WHOLE tree at any depth (root + cli/.mmi, .github/workflows/.mmi,
@@ -13471,9 +14137,9 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
13471
14137
 
13472
14138
  // src/gh-create.ts
13473
14139
  var import_promises5 = require("node:fs/promises");
13474
- var import_node_os4 = require("node:os");
14140
+ var import_node_os3 = require("node:os");
13475
14141
  var import_node_path17 = require("node:path");
13476
- var import_node_crypto5 = require("node:crypto");
14142
+ var import_node_crypto6 = require("node:crypto");
13477
14143
  var ISSUE_TYPES = ["bug", "feature", "task"];
13478
14144
  var GH_MUTATION_TIMEOUT_MS = 12e4;
13479
14145
  function timeoutKillNote(err, timeoutMs) {
@@ -13513,7 +14179,7 @@ async function bodyArgsViaFile(args, deps = {}) {
13513
14179
  } };
13514
14180
  const write = deps.write ?? import_promises5.writeFile;
13515
14181
  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`);
14182
+ 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
14183
  await write(file, args[i + 1], "utf8");
13518
14184
  return {
13519
14185
  args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
@@ -14726,7 +15392,7 @@ function designSystemSnapshot(root) {
14726
15392
  }
14727
15393
 
14728
15394
  // src/design-system-registry.ts
14729
- var import_node_crypto6 = require("node:crypto");
15395
+ var import_node_crypto7 = require("node:crypto");
14730
15396
  var import_node_fs20 = require("node:fs");
14731
15397
  var import_node_path19 = require("node:path");
14732
15398
  var DESIGN_SYSTEM_CACHE_DIR = ".mmi/design-system/components";
@@ -14789,7 +15455,7 @@ function scanCachedComponentNames(cacheDir) {
14789
15455
  return [...names].sort();
14790
15456
  }
14791
15457
  function contentHash(content) {
14792
- return (0, import_node_crypto6.createHash)("sha256").update(content, "utf8").digest("hex");
15458
+ return (0, import_node_crypto7.createHash)("sha256").update(content, "utf8").digest("hex");
14793
15459
  }
14794
15460
  function buildRegistryComponentsCheck(input) {
14795
15461
  const base = {
@@ -14949,6 +15615,17 @@ function renderVerifySecrets(body) {
14949
15615
  if (ssmStatus !== "Success") {
14950
15616
  return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
14951
15617
  }
15618
+ if (secrets.length === 0) {
15619
+ const raw = (body?.raw ?? "").trim();
15620
+ if (/no required runtime secrets declared/i.test(raw)) {
15621
+ return { lines: [raw], failure: null };
15622
+ }
15623
+ const detail = raw ? ` \u2014 on-box output: ${raw.replace(/\s+/g, " ").trim()}` : "";
15624
+ return {
15625
+ lines: raw ? raw.split("\n") : ["verify-secrets: no per-key verdicts returned"],
15626
+ failure: `verify-secrets returned no per-key verdicts, so nothing was verified${detail}`
15627
+ };
15628
+ }
14952
15629
  const bad = counts.mismatch + counts.missing;
14953
15630
  if (bad > 0) {
14954
15631
  return { lines, failure: `${bad} of ${secrets.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
@@ -14957,12 +15634,12 @@ function renderVerifySecrets(body) {
14957
15634
  }
14958
15635
 
14959
15636
  // src/hotfix-coverage.ts
14960
- var import_node_child_process11 = require("node:child_process");
15637
+ var import_node_child_process10 = require("node:child_process");
14961
15638
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
14962
15639
  function checkHotfixCoverage(options = {}) {
14963
15640
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
14964
15641
  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"] }));
15642
+ 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
15643
  const revList = (range) => {
14967
15644
  const out = git(["rev-list", "--no-merges", range]).trim();
14968
15645
  return out ? out.split("\n") : [];
@@ -17663,14 +18340,14 @@ function parseKbTree(stdout, prefix) {
17663
18340
 
17664
18341
  // src/northstar-commands.ts
17665
18342
  var import_node_fs22 = require("node:fs");
17666
- var import_node_child_process12 = require("node:child_process");
18343
+ var import_node_child_process11 = require("node:child_process");
17667
18344
  var import_promises6 = require("node:fs/promises");
17668
18345
  var planSyncDetached = false;
17669
18346
  function detachPlanSync() {
17670
18347
  if (planSyncDetached) return;
17671
18348
  planSyncDetached = true;
17672
18349
  try {
17673
- (0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
18350
+ (0, import_node_child_process11.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
17674
18351
  detached: true,
17675
18352
  stdio: "ignore",
17676
18353
  windowsHide: true,
@@ -17756,12 +18433,13 @@ function openInEditor(path2) {
17756
18433
  return;
17757
18434
  }
17758
18435
  try {
17759
- (0, import_node_child_process12.spawn)(editor, [path2], { stdio: "inherit" });
18436
+ (0, import_node_child_process11.spawn)(editor, [path2], { stdio: "inherit" });
17760
18437
  } catch {
17761
18438
  console.log(`open ${path2} manually`);
17762
18439
  }
17763
18440
  }
17764
18441
  async function withPlan(quiet, run, io = consoleIo) {
18442
+ if (!await requireContinuityAccess("northstar", { quiet }, io)) return;
17765
18443
  const cfg = await loadConfig();
17766
18444
  if (!cfg.sagaApiUrl) {
17767
18445
  if (!quiet) fail("plan: Hub API URL not configured");
@@ -17770,6 +18448,7 @@ async function withPlan(quiet, run, io = consoleIo) {
17770
18448
  await run(makePlanDeps(cfg, io));
17771
18449
  }
17772
18450
  async function runPlanAutosave(io = consoleIo, opts = {}) {
18451
+ if (!await requireContinuityAccess("northstar", { quiet: opts.quiet }, io)) return [];
17773
18452
  const cfg = await loadConfig();
17774
18453
  if (!cfg.sagaApiUrl) {
17775
18454
  if (!opts.quiet) fail("plan: Hub API URL not configured");
@@ -18776,10 +19455,10 @@ function registerEdgeCommands(program3) {
18776
19455
 
18777
19456
  // src/doctor-run.ts
18778
19457
  var import_node_fs28 = require("node:fs");
18779
- var import_node_child_process14 = require("node:child_process");
19458
+ var import_node_child_process13 = require("node:child_process");
18780
19459
  var import_promises7 = require("node:fs/promises");
18781
19460
  var import_node_path25 = require("node:path");
18782
- var import_node_os6 = require("node:os");
19461
+ var import_node_os5 = require("node:os");
18783
19462
 
18784
19463
  // src/plugin-guard.ts
18785
19464
  function buildPluginGuardDecision(i) {
@@ -18799,9 +19478,9 @@ function buildGuardSessionStartLine(state, opts = {}) {
18799
19478
  }
18800
19479
 
18801
19480
  // src/cursor-plugin-seed.ts
18802
- var import_node_child_process13 = require("node:child_process");
19481
+ var import_node_child_process12 = require("node:child_process");
18803
19482
  var import_node_fs24 = require("node:fs");
18804
- var import_node_os5 = require("node:os");
19483
+ var import_node_os4 = require("node:os");
18805
19484
  var import_node_path22 = require("node:path");
18806
19485
  var import_node_util7 = require("node:util");
18807
19486
  function isSemverVersion(v) {
@@ -18810,7 +19489,7 @@ function isSemverVersion(v) {
18810
19489
  var MMI_HUB_REPO = "mutmutco/MMI-Hub";
18811
19490
  var CURSOR_THIRD_PARTY_STATE_KEY = "cursor/thirdPartyExtensibilityEnabled";
18812
19491
  var PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
18813
- var execFileBuffer = (0, import_node_util7.promisify)(import_node_child_process13.execFile);
19492
+ var execFileBuffer = (0, import_node_util7.promisify)(import_node_child_process12.execFile);
18814
19493
  function gitFetchReleaseTagArgs(hubCheckout, tag) {
18815
19494
  return ["-C", hubCheckout, "fetch", "origin", "tag", tag, "--quiet"];
18816
19495
  }
@@ -18819,13 +19498,13 @@ function ghReleaseTarballApiArgs(tag) {
18819
19498
  }
18820
19499
  function cursorUserGlobalStatePath() {
18821
19500
  if (process.platform === "win32") {
18822
- const base = process.env.APPDATA || (0, import_node_path22.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
19501
+ const base = process.env.APPDATA || (0, import_node_path22.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
18823
19502
  return (0, import_node_path22.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
18824
19503
  }
18825
19504
  if (process.platform === "darwin") {
18826
- return (0, import_node_path22.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
19505
+ return (0, import_node_path22.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
18827
19506
  }
18828
- return (0, import_node_path22.join)((0, import_node_os5.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
19507
+ return (0, import_node_path22.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
18829
19508
  }
18830
19509
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
18831
19510
  const dbPath = cursorUserGlobalStatePath();
@@ -18981,6 +19660,17 @@ function buildContinuityFreshnessCheck(input) {
18981
19660
  fix: `local branch work is newer than ${stale.join(" + ")} continuity; run ${commands.join(" and ")}`
18982
19661
  };
18983
19662
  }
19663
+ function buildContinuityRestrictionNotice(input) {
19664
+ const login = input.login?.trim() || "unknown";
19665
+ return {
19666
+ ok: true,
19667
+ label: "saga / North Star continuity (Jervaise-only)",
19668
+ 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).`
19669
+ };
19670
+ }
19671
+ function continuityDoctorCheckForLogin(login) {
19672
+ return login?.trim().toLowerCase() === CONTINUITY_OWNER_LOGIN ? "freshness" : "restriction";
19673
+ }
18984
19674
  var MMI_PLUGIN_ID = "mmi@mutmutco";
18985
19675
  var LEGACY_MMI_PLUGIN_ID = "mmi@mmi";
18986
19676
  var LEGACY_MMI_MARKETPLACE = "mmi";
@@ -19940,7 +20630,7 @@ function buildBrowserArtifactsCheck(input) {
19940
20630
  };
19941
20631
  }
19942
20632
  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";
20633
+ 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
20634
  function buildKbDriftAdvisoryCheck(input) {
19945
20635
  const base = {
19946
20636
  ok: true,
@@ -20423,7 +21113,7 @@ function reexecMmiCli(args) {
20423
21113
  }
20424
21114
  };
20425
21115
  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 });
21116
+ 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
21117
  child.on("error", () => done(-1));
20428
21118
  child.on("exit", (code) => done(code ?? 0));
20429
21119
  });
@@ -20480,7 +21170,7 @@ async function applyPluginHeal(token, surface, log, opts) {
20480
21170
  }
20481
21171
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
20482
21172
  const homeDir = surface === "codex" ? ".codex" : ".claude";
20483
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
21173
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
20484
21174
  };
20485
21175
  function readInstalledPlugins(surface = detectSurface(process.env)) {
20486
21176
  try {
@@ -20495,13 +21185,13 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
20495
21185
  return {
20496
21186
  isOrgRepo,
20497
21187
  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"))
21188
+ marketplaceClonePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
21189
+ pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
20500
21190
  };
20501
21191
  }
20502
21192
  function installedPluginSources() {
20503
21193
  return ["claude", "codex"].map((surface) => {
20504
- const recordPath = (0, import_node_path25.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
21194
+ const recordPath = (0, import_node_path25.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
20505
21195
  try {
20506
21196
  return { surface, installed: JSON.parse((0, import_node_fs28.readFileSync)(recordPath, "utf8")), recordPath };
20507
21197
  } catch {
@@ -20556,7 +21246,7 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
20556
21246
  }
20557
21247
  }
20558
21248
  function opencodeConfigDir() {
20559
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".config", "opencode");
21249
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".config", "opencode");
20560
21250
  }
20561
21251
  function opencodeConfigPath() {
20562
21252
  return (0, import_node_path25.join)(opencodeConfigDir(), "opencode.jsonc");
@@ -20642,7 +21332,7 @@ function writeOpencodeCommandFiles() {
20642
21332
  function readOpencodeAdapterDiskVersion() {
20643
21333
  const candidates = [
20644
21334
  (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")
21335
+ (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
20646
21336
  ];
20647
21337
  for (const path2 of candidates) {
20648
21338
  try {
@@ -20677,13 +21367,13 @@ function opencodePluginVersionsForReport() {
20677
21367
  }
20678
21368
  function opencodeDesktopLogsRoot() {
20679
21369
  if (process.platform === "win32") {
20680
- const base = process.env.APPDATA || (0, import_node_path25.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
21370
+ const base = process.env.APPDATA || (0, import_node_path25.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
20681
21371
  return (0, import_node_path25.join)(base, "ai.opencode.desktop", "logs");
20682
21372
  }
20683
21373
  if (process.platform === "darwin") {
20684
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
21374
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
20685
21375
  }
20686
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".config", "ai.opencode.desktop", "logs");
21376
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".config", "ai.opencode.desktop", "logs");
20687
21377
  }
20688
21378
  function opencodeDesktopBootstrapSnapshot() {
20689
21379
  const root = opencodeDesktopLogsRoot();
@@ -20699,7 +21389,7 @@ function opencodeDesktopBootstrapSnapshot() {
20699
21389
  }
20700
21390
  }
20701
21391
  function opencodeLegacyConfigSnapshot() {
20702
- const legacyPath = (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".opencode", "opencode.json");
21392
+ const legacyPath = (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".opencode", "opencode.json");
20703
21393
  if (!(0, import_node_fs28.existsSync)(legacyPath)) return {};
20704
21394
  const content = readTextFile(legacyPath);
20705
21395
  if (content == null) return {};
@@ -20720,7 +21410,7 @@ function quarantineOpencodeLegacyConfig(legacyPath) {
20720
21410
  }
20721
21411
  }
20722
21412
  function cursorPluginCacheRoot() {
20723
- return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
21413
+ return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
20724
21414
  }
20725
21415
  function cursorPluginCachePinSnapshots() {
20726
21416
  const root = cursorPluginCacheRoot();
@@ -20766,8 +21456,8 @@ function hubCheckoutForCursorSeed() {
20766
21456
  }
20767
21457
  function mmiPluginCacheRootSnapshots() {
20768
21458
  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") }
21459
+ { surface: "claude", root: (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
21460
+ { surface: "codex", root: (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
20771
21461
  ];
20772
21462
  return roots.flatMap(({ surface, root }) => {
20773
21463
  try {
@@ -20831,7 +21521,7 @@ async function clearNestedPluginTreeDir(targetPath) {
20831
21521
  try {
20832
21522
  if (!(0, import_node_fs28.existsSync)(targetPath)) return true;
20833
21523
  if (isWin) {
20834
- const emptyDir = (0, import_node_path25.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
21524
+ const emptyDir = (0, import_node_path25.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
20835
21525
  (0, import_node_fs28.mkdirSync)(emptyDir, { recursive: true });
20836
21526
  try {
20837
21527
  await robocopyMirrorEmpty(emptyDir, targetPath);
@@ -20869,7 +21559,7 @@ function readTextFile(path2) {
20869
21559
  }
20870
21560
  function playwrightMcpConfigSnapshots() {
20871
21561
  const cwd = process.cwd();
20872
- const home = (0, import_node_os6.homedir)();
21562
+ const home = (0, import_node_os5.homedir)();
20873
21563
  const candidates = [
20874
21564
  (0, import_node_path25.join)(cwd, ".mcp.json"),
20875
21565
  (0, import_node_path25.join)(cwd, ".cursor", "mcp.json"),
@@ -21382,7 +22072,7 @@ async function runDoctor(opts, io = consoleIo) {
21382
22072
  releasedVersion,
21383
22073
  hubCheckout: hubCheckoutForCursorSeed(),
21384
22074
  execFileP: execFileP2,
21385
- mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0, import_node_os6.tmpdir)(), prefix)),
22075
+ mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0, import_node_os5.tmpdir)(), prefix)),
21386
22076
  log: (m) => io.err(m)
21387
22077
  });
21388
22078
  if (seeded) {
@@ -21474,15 +22164,19 @@ async function runDoctor(opts, io = consoleIo) {
21474
22164
  }
21475
22165
  }
21476
22166
  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
- );
22167
+ if (continuityDoctorCheckForLogin(login) === "freshness") {
22168
+ const continuity = readContinuityStamp();
22169
+ checks.push(
22170
+ buildContinuityFreshnessCheck({
22171
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
22172
+ latestWorkAt: await latestBranchWorkAt(),
22173
+ lastSagaNoteAt: continuity.lastSagaNoteAt,
22174
+ lastNorthstarAt: latestNorthstarContinuityAt()
22175
+ })
22176
+ );
22177
+ } else {
22178
+ checks.push(buildContinuityRestrictionNotice({ login: login?.trim() }));
22179
+ }
21486
22180
  }
21487
22181
  if (runExtended) {
21488
22182
  const dashboardConsumer = await resolveDashboardConsumer(cfg);
@@ -21580,7 +22274,7 @@ function mergeGuardHook(settings) {
21580
22274
  next.hooks = hooks;
21581
22275
  return next;
21582
22276
  }
21583
- var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0, import_node_os6.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
22277
+ var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0, import_node_os5.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
21584
22278
  function ensureUserScopeGuardHook(opts = {}) {
21585
22279
  const path2 = opts.settingsPath ?? userScopeSettingsPath();
21586
22280
  try {
@@ -21794,7 +22488,7 @@ async function requireFreshTrainCli(commandName) {
21794
22488
  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
22489
  }
21796
22490
  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)");
22491
+ 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
22492
  async function runRulesSync(opts, io = consoleIo) {
21799
22493
  const cfg = await loadConfig();
21800
22494
  if (isRulesSource(cfg.orgRulesSource)) {
@@ -22037,7 +22731,7 @@ function runWorktreeInstall(command, cwd, quiet) {
22037
22731
  const file = isWin2 ? "cmd.exe" : bin;
22038
22732
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
22039
22733
  return new Promise((resolve6, reject) => {
22040
- const child = (0, import_node_child_process15.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
22734
+ const child = (0, import_node_child_process14.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
22041
22735
  const timer = setTimeout(() => {
22042
22736
  try {
22043
22737
  child.kill();
@@ -22255,7 +22949,7 @@ function scheduleRelatedDiscovery(o) {
22255
22949
  try {
22256
22950
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
22257
22951
  if (o.repo) args.push("--repo", o.repo);
22258
- (0, import_node_child_process15.spawn)(process.execPath, [process.argv[1], ...args], {
22952
+ (0, import_node_child_process14.spawn)(process.execPath, [process.argv[1], ...args], {
22259
22953
  detached: true,
22260
22954
  stdio: "ignore",
22261
22955
  windowsHide: true,
@@ -22264,7 +22958,7 @@ function scheduleRelatedDiscovery(o) {
22264
22958
  } catch {
22265
22959
  }
22266
22960
  }
22267
- var northstar = program2.command("northstar").description("North Star \u2014 your cross-device plans/SSOTs (S3-backed, git-clean)");
22961
+ var northstar = program2.command("northstar").description("North Star \u2014 Jervaise-only cross-device plans/SSOTs (S3-backed, git-clean)");
22268
22962
  registerNorthStarCommands(northstar);
22269
22963
  var plan = program2.command("plan").description("Alias for `northstar` (deprecated \u2014 use `northstar`)");
22270
22964
  plan.hook("preAction", () => {
@@ -22290,7 +22984,7 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
22290
22984
  try {
22291
22985
  const result = await runTenantControl(trainApplyDeps(), { repo, stage: stage2, action, watch: o.watch });
22292
22986
  if (!o.json && action === "verify-secrets" && result.secrets) {
22293
- const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed" };
22987
+ const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
22294
22988
  const { lines, failure } = renderVerifySecrets(body);
22295
22989
  for (const line of lines) printLine(line);
22296
22990
  if (failure) return failGraceful(`tenant control ${stage2} verify-secrets: ${failure}`);
@@ -23391,7 +24085,7 @@ async function remoteBranchExists2(branch, options = {}) {
23391
24085
  }
23392
24086
  var COMPOSE_TIMEOUT_MS = 12e4;
23393
24087
  function spawnDeferredGcSweep() {
23394
- spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24088
+ spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
23395
24089
  }
23396
24090
  async function createDeferredWorktreeStore() {
23397
24091
  try {
@@ -24631,85 +25325,90 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
24631
25325
  ));
24632
25326
  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
25327
  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 () => {
25328
+ 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
25329
  if (isInsideRepoSubdir(process.cwd())) {
24636
25330
  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
25331
  return;
24638
25332
  }
24639
25333
  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}`);
25334
+ const continuityEnabled = (await continuityAccess().catch(() => ({ allowed: false }))).allowed;
25335
+ if (continuityEnabled) {
25336
+ try {
25337
+ const hook = parseHookInput(await readStdin());
25338
+ if (hook.session_id) persistSession(hook.session_id);
25339
+ } catch (e) {
25340
+ console.error(`[mmi-hook] saga session failed: ${e.message}`);
25341
+ }
24645
25342
  }
24646
- spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
25343
+ spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24647
25344
  spawnDeferredGcSweep();
24648
25345
  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
- }
25346
+ const { parallel, sequential } = buildSessionStartPlan(
25347
+ {
25348
+ rulesSync: (io) => runRulesSync({ quiet: true }, io),
25349
+ sagaShow: (io) => runSagaShow({ quiet: true }, io),
25350
+ handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
25351
+ coopPending: (io) => runCoopPendingBanner(io),
25352
+ northstarContext: async (io) => {
25353
+ const cfg = await loadConfig();
25354
+ if (!cfg.sagaApiUrl) return;
25355
+ const planDeps = makePlanDeps(cfg, io);
25356
+ northstarInjected = await runNorthstarContext(io, {
25357
+ loadPlans: () => scopedPlanList(planDeps),
25358
+ readLocal: (slug) => planDeps.readLocal(slug),
25359
+ // #1812: thread the saga HEAD's North Star anchor (its NEXT slug) into the relevance gate so
25360
+ // the plan the agent is actively on is force-injected even on a generic branch with no token
25361
+ // overlap. fetchSagaHead errors are swallowed via a silent io a missing/failed HEAD just
25362
+ // falls back to token-overlap scoring, never noises or blocks the banner.
25363
+ gatherSignals: () => gatherRelevanceSignals({
25364
+ anchorSlug: () => fetchSagaHead({ log: () => {
25365
+ }, err: () => {
25366
+ } }).then((h) => h?.anchor?.slug ?? void 0)
25367
+ })
25368
+ });
25369
+ },
25370
+ sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
25371
+ // whoami (#879): surface the resolved human so agents act --for them without asking. Silent
25372
+ // when unknown a missing gh login must not noise or fail the banner.
25373
+ whoami: async (io) => {
25374
+ const report = await resolveWhoami({
25375
+ hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
25376
+ ghLogin: githubLogin
25377
+ });
25378
+ const line = whoamiLine(report);
25379
+ if (line) io.log(line);
25380
+ },
25381
+ boardSlice: (io) => runBoardSlice(io, {
25382
+ loadConfig: () => loadConfigForRepo(),
25383
+ readBoard,
25384
+ // #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
25385
+ // never costs banner time and next session's glance renders instantly within budget.
25386
+ scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
25387
+ }),
25388
+ spineReconcile: async (io) => {
25389
+ const cfg = await loadConfig();
25390
+ const isSource = isRulesSource(cfg.orgRulesSource);
25391
+ if (!isSource && !await isOrgRegisteredRepo(cfg)) return;
25392
+ const restored = await restoreDirtyOrgSpineToHead(
25393
+ { run: async (cmd, args) => (await execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })).stdout },
25394
+ { isRulesSource: isSource }
25395
+ );
25396
+ if (restored.length) {
25397
+ io.log(`[mmi] reset ${restored.length} org-delivered file(s) to your branch \u2014 pull latest base to refresh (${restored.join(", ")})`);
25398
+ }
25399
+ },
25400
+ doctor: (io) => runDoctor({ banner: true }, io)
24701
25401
  },
24702
- doctor: (io) => runDoctor({ banner: true }, io)
24703
- });
25402
+ { continuityEnabled }
25403
+ );
24704
25404
  await runSessionStart(parallel, sequential, consoleIo);
24705
- consoleIo.log(northstarPointer(northstarInjected));
25405
+ for (const line of sessionStartContinuityLines({ continuityEnabled, northstarInjected, cwd: process.cwd() })) consoleIo.log(line);
24706
25406
  consoleIo.log(kbPointer());
24707
- for (const line of planStoreLines(process.cwd())) consoleIo.log(line);
24708
- await runPlanAutosave(consoleIo, { quiet: true }).catch(() => void 0);
25407
+ if (continuityEnabled) await runPlanAutosave(consoleIo, { quiet: true }).catch(() => void 0);
24709
25408
  for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
24710
25409
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
24711
25410
  if (worktreeBanner) {
24712
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
25411
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24713
25412
  consoleIo.log(worktreeBanner);
24714
25413
  }
24715
25414
  });