@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/saga.cjs CHANGED
@@ -3612,7 +3612,6 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
3612
3612
  }
3613
3613
 
3614
3614
  // src/saga-health.ts
3615
- var MEMORY_STALE_DAYS = 14;
3616
3615
  var SESSION_START_LIVENESS = { attempts: 1, timeoutMs: 3e3 };
3617
3616
  function buildHealth(i) {
3618
3617
  const problems = [];
@@ -3625,9 +3624,6 @@ function buildHealth(i) {
3625
3624
  if (i.reachable && i.authorized === false) problems.push("saga backend rejected authenticated state access");
3626
3625
  if (!i.key.sessionId || i.key.sessionId === "-") problems.push("unsafe session id");
3627
3626
  const warnings = [];
3628
- if (i.memoryAgeDays !== void 0 && i.memoryAgeDays > MEMORY_STALE_DAYS) {
3629
- warnings.push(`PROJECT MEMORY is ${Math.round(i.memoryAgeDays)}d stale \u2014 the saga-keeper may have stalled`);
3630
- }
3631
3627
  const safeToWrite = problems.length === 0;
3632
3628
  return {
3633
3629
  ok: safeToWrite,
@@ -3642,8 +3638,7 @@ function buildHealth(i) {
3642
3638
  key: i.key,
3643
3639
  source: i.source,
3644
3640
  problems,
3645
- warnings,
3646
- memoryAgeDays: i.memoryAgeDays
3641
+ warnings
3647
3642
  };
3648
3643
  }
3649
3644
  function healthBanner(report) {
@@ -4407,6 +4402,23 @@ function hardExit(code) {
4407
4402
  process.exit(code);
4408
4403
  }
4409
4404
 
4405
+ // src/continuity-access.ts
4406
+ var CONTINUITY_OWNER_LOGIN = "jervaise";
4407
+ function continuityAllowedForLogin(login) {
4408
+ return login?.trim().toLowerCase() === CONTINUITY_OWNER_LOGIN;
4409
+ }
4410
+ async function firstLogin(source, read) {
4411
+ const login = (await read?.().catch(() => void 0))?.trim();
4412
+ if (!login) return null;
4413
+ return { allowed: continuityAllowedForLogin(login), login, source };
4414
+ }
4415
+ async function resolveContinuityAccess(deps) {
4416
+ return await firstLogin("hub-session", deps.hubLogin) ?? await firstLogin("github", deps.ghLogin) ?? { allowed: false, source: "unknown" };
4417
+ }
4418
+ function formatContinuityAccessDenied(kind) {
4419
+ return `${kind}: continuity is restricted to ${CONTINUITY_OWNER_LOGIN}`;
4420
+ }
4421
+
4410
4422
  // src/stdin-inject.ts
4411
4423
  var import_node_fs7 = require("node:fs");
4412
4424
  var injectedStdin;
@@ -4472,12 +4484,33 @@ var execFileP3 = (file, args, options = {}) => (
4472
4484
  // promisify(execFile)'s overloads widen to string|Buffer when options is spread in.
4473
4485
  rawExecFileP2(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
4474
4486
  );
4487
+ var cachedGithubLogin;
4488
+ async function githubLogin() {
4489
+ cachedGithubLogin ??= execFileP3("gh", ["api", "user", "--jq", ".login"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
4490
+ return cachedGithubLogin;
4491
+ }
4475
4492
  async function hubHeaders(extra = {}) {
4476
4493
  const cfg = await loadConfig();
4477
4494
  const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken });
4478
4495
  const base = { ...clientVersionHeaders(), ...extra };
4479
4496
  return t ? { ...base, Authorization: `Bearer ${t}` } : base;
4480
4497
  }
4498
+ async function continuityAccess() {
4499
+ const cfg = await loadConfig();
4500
+ return resolveContinuityAccess({
4501
+ hubLogin: async () => (await hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }))?.login,
4502
+ ghLogin: githubLogin
4503
+ });
4504
+ }
4505
+ async function requireContinuityAccess(kind, opts = {}, io) {
4506
+ const access = await continuityAccess();
4507
+ if (access.allowed) return true;
4508
+ if (!opts.quiet) {
4509
+ (io ?? consoleIo).err(formatContinuityAccessDenied(kind));
4510
+ process.exitCode = 1;
4511
+ }
4512
+ return false;
4513
+ }
4481
4514
  var CONFIG_FILE = ".mmi/config.json";
4482
4515
  async function loadConfig() {
4483
4516
  let file = {};
@@ -4549,6 +4582,7 @@ async function postCaptureOnce(sagaApiUrl, body) {
4549
4582
  }
4550
4583
  }
4551
4584
  async function postCapture(capture, quiet = false) {
4585
+ if (!await requireContinuityAccess("saga", { quiet })) return;
4552
4586
  const cfg = await loadConfig();
4553
4587
  if (!cfg.sagaApiUrl) {
4554
4588
  if (!quiet) console.error("mmi-cli saga: Hub API URL not configured");
@@ -4571,6 +4605,7 @@ function spawnSagaFlush() {
4571
4605
  }
4572
4606
  async function maybeSpawnHeadUpdate() {
4573
4607
  try {
4608
+ if (!await requireContinuityAccess("saga", { quiet: true })) return;
4574
4609
  const gateKey = await sagaKey(await loadConfig());
4575
4610
  const tsPath = headTsPath(gateKey);
4576
4611
  if (!headGateDue(tsPath)) return;
@@ -4595,6 +4630,7 @@ function installProcessBackstop() {
4595
4630
 
4596
4631
  // src/saga-commands.ts
4597
4632
  async function runNote(summary, o) {
4633
+ if (!await requireContinuityAccess("saga")) return;
4598
4634
  const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
4599
4635
  const capture = buildNoteCapture(summary, o, (0, import_node_crypto3.randomUUID)(), { sha: sha || void 0, branch: key.branch });
4600
4636
  await postCapture(capture);
@@ -4608,6 +4644,10 @@ function resolveSummary(summary, o) {
4608
4644
  });
4609
4645
  }
4610
4646
  async function runSagaFlush(o, io = consoleIo) {
4647
+ if (!await requireContinuityAccess("saga", { quiet: o.run }, io)) {
4648
+ if (o.json) io.log(JSON.stringify({ flushed: 0, dropped: 0, remaining: 0, restricted: true }));
4649
+ return;
4650
+ }
4611
4651
  if (o.run) {
4612
4652
  try {
4613
4653
  const cfg2 = await loadConfig();
@@ -4631,6 +4671,7 @@ async function runSagaFlush(o, io = consoleIo) {
4631
4671
  io.log(`saga flush: rolled forward ${result.flushed}, dropped ${result.dropped.length}, ${result.remaining} still pending`);
4632
4672
  }
4633
4673
  async function runSagaShow(opts, io = consoleIo) {
4674
+ if (!await requireContinuityAccess("saga", { quiet: opts.quiet }, io)) return;
4634
4675
  const cfg = await loadConfig();
4635
4676
  if (!cfg.sagaApiUrl) {
4636
4677
  if (opts.quiet) return;
@@ -4679,20 +4720,11 @@ async function probeSagaAccess(url, key) {
4679
4720
  return false;
4680
4721
  }
4681
4722
  }
4682
- async function fetchMemoryAge(url, project) {
4683
- try {
4684
- const qs = new URLSearchParams({ project }).toString();
4685
- const res = await fetch(`${url}/saga/memory-age?${qs}`, { headers: await hubHeaders(), signal: AbortSignal.timeout(8e3) });
4686
- if (!res.ok) return void 0;
4687
- const body = await res.json();
4688
- if (!body.updatedAt) return void 0;
4689
- const ms = Date.now() - Date.parse(body.updatedAt);
4690
- return Number.isFinite(ms) && ms >= 0 ? ms / 864e5 : void 0;
4691
- } catch {
4692
- return void 0;
4693
- }
4694
- }
4695
4723
  async function runSagaHealth(o, io = consoleIo) {
4724
+ if (!await requireContinuityAccess("saga", { quiet: o.quiet || o.banner }, io)) {
4725
+ if (o.json) io.log(JSON.stringify({ ok: false, restricted: true, problems: ["continuity is restricted to jervaise"], warnings: [] }));
4726
+ return;
4727
+ }
4696
4728
  const cfg = await loadConfig();
4697
4729
  const session = resolveSessionId();
4698
4730
  const key = await sagaKey(cfg, session);
@@ -4703,7 +4735,6 @@ async function runSagaHealth(o, io = consoleIo) {
4703
4735
  cfg.sagaApiUrl ? probeBackend(cfg.sagaApiUrl, livenessOpts) : Promise.resolve({ reachable: false })
4704
4736
  ]);
4705
4737
  const authorized = o.banner ? void 0 : cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
4706
- const memoryAgeDays = o.banner ? void 0 : cfg.sagaApiUrl && liveness.reachable ? await fetchMemoryAge(cfg.sagaApiUrl, key.project) : void 0;
4707
4738
  const report = buildHealth({
4708
4739
  key,
4709
4740
  source,
@@ -4713,8 +4744,7 @@ async function runSagaHealth(o, io = consoleIo) {
4713
4744
  livenessMessage: liveness.message,
4714
4745
  authorized,
4715
4746
  sagaApiUrl: cfg.sagaApiUrl,
4716
- pendingNotes: readPending().length,
4717
- memoryAgeDays
4747
+ pendingNotes: readPending().length
4718
4748
  });
4719
4749
  if (o.json) return io.log(JSON.stringify(report));
4720
4750
  if (o.banner) {
@@ -4731,6 +4761,7 @@ async function runSagaHealth(o, io = consoleIo) {
4731
4761
  if (report.pendingNotes > 0) io.log(` - ${report.pendingNotes} note(s) queued locally \u2014 \`mmi-cli saga flush\` to roll forward`);
4732
4762
  }
4733
4763
  async function fetchSagaHead(io = consoleIo) {
4764
+ if (!await requireContinuityAccess("saga", {}, io)) return null;
4734
4765
  const cfg = await loadConfig();
4735
4766
  if (!cfg.sagaApiUrl) {
4736
4767
  io.err("saga snapshot: Hub API URL not configured");
@@ -4752,6 +4783,7 @@ async function fetchSagaHead(io = consoleIo) {
4752
4783
  }
4753
4784
  }
4754
4785
  async function postSnapshotNotes(plan, anchorForce) {
4786
+ if (!await requireContinuityAccess("saga")) return;
4755
4787
  const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
4756
4788
  const evidence = { sha: sha || void 0, branch: key.branch };
4757
4789
  for (const idx of [...plan.clearIndices].sort((a, b) => b - a)) {
@@ -4786,7 +4818,7 @@ async function runSagaSnapshotSet(snapshot, opts = {}, io = consoleIo) {
4786
4818
  io.log(`saga snapshot: wrote ${snapshot.kind} snapshot (retired ${plan.clearIndices.length}, ${plan.queueOps.length + 1} capture(s))`);
4787
4819
  }
4788
4820
  function registerSagaCommands(program2) {
4789
- const saga = program2.command("saga").description("per-session continuity");
4821
+ const saga = program2.command("saga").description("Jervaise-only per-session continuity");
4790
4822
  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) => {
4791
4823
  let text;
4792
4824
  try {
@@ -4806,9 +4838,10 @@ function registerSagaCommands(program2) {
4806
4838
  await runNote(text, { ...o, diagnostic: true });
4807
4839
  });
4808
4840
  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));
4809
- 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));
4841
+ 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));
4810
4842
  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) => {
4811
4843
  if (!isOrgRepoRoot()) return;
4844
+ if (!await requireContinuityAccess("saga", { quiet: opts.quiet })) return;
4812
4845
  const hook = parseHookInput(await readStdin());
4813
4846
  if (hook.session_id) persistSession(hook.session_id);
4814
4847
  await postCapture({ event: "stop", id: (0, import_node_crypto3.randomUUID)(), source: "hook", sha: await gitOut(["rev-parse", "--short", "HEAD"]), surface: agentSurface() }, opts.quiet ?? false);
@@ -4816,10 +4849,12 @@ function registerSagaCommands(program2) {
4816
4849
  });
4817
4850
  saga.command("session").option("--quiet", "silent (for the SessionStart hook)").description("persist the harness session id for this repo (SessionStart hook)").action(async () => {
4818
4851
  if (!isOrgRepoRoot()) return;
4852
+ if (!await requireContinuityAccess("saga", { quiet: true })) return;
4819
4853
  const hook = parseHookInput(await readStdin());
4820
4854
  if (hook.session_id) persistSession(hook.session_id);
4821
4855
  });
4822
4856
  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) => {
4857
+ if (!await requireContinuityAccess("saga", { quiet: o.quiet || o.run })) return;
4823
4858
  if (!o.run) {
4824
4859
  return maybeSpawnHeadUpdate();
4825
4860
  }
@@ -4844,6 +4879,7 @@ function registerSagaCommands(program2) {
4844
4879
  }
4845
4880
  });
4846
4881
  saga.command("key").option("--json", "machine-readable output").description("print the resolved saga key + session-id source (no write)").action(async (o) => {
4882
+ if (!await requireContinuityAccess("saga")) return;
4847
4883
  const cfg = await loadConfig();
4848
4884
  const session = resolveSessionId();
4849
4885
  const key = await sagaKey(cfg, session);
@@ -4983,6 +5019,7 @@ function socketPath(env = process.env, platform2 = process.platform, user) {
4983
5019
  return platform2 === "win32" ? `\\\\.\\pipe\\mmi-cli-${hash}` : (0, import_node_path8.join)(daemonDir(env), `mmi-cli-${hash}.sock`);
4984
5020
  }
4985
5021
  var HOT_VERBS = /* @__PURE__ */ new Set(["note", "probe", "capture", "session", "head-update"]);
5022
+ var CONTINUITY_HOT_VERBS = /* @__PURE__ */ new Set(["note", "probe", "capture", "session", "head-update"]);
4986
5023
  function argvReadsStdin(args) {
4987
5024
  for (let i = 0; i < args.length; i++) {
4988
5025
  const a = args[i];
@@ -4992,7 +5029,8 @@ function argvReadsStdin(args) {
4992
5029
  return false;
4993
5030
  }
4994
5031
  function daemonEligible(args) {
4995
- return args[0] === "saga" && HOT_VERBS.has(args[1] ?? "") && !args.includes("--run") && !args.includes("--help") && !args.includes("-h") && !argvReadsStdin(args);
5032
+ const verb = args[1] ?? "";
5033
+ return args[0] === "saga" && HOT_VERBS.has(verb) && !CONTINUITY_HOT_VERBS.has(verb) && !args.includes("--run") && !args.includes("--help") && !args.includes("-h") && !argvReadsStdin(args);
4996
5034
  }
4997
5035
  function buildStamp(version, bundleMtimeMs) {
4998
5036
  return `${version}#${Math.trunc(bundleMtimeMs)}`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.56.0",
4
- "description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
3
+ "version": "2.57.0",
4
+ "description": "MMI Future CLI — delivers the org rules (whole-file), Jervaise-only continuity, and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "author": {
@@ -1,322 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") {
10
- for (let key of __getOwnPropNames(from))
11
- if (!__hasOwnProp.call(to, key) && key !== except)
12
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
- }
14
- return to;
15
- };
16
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
- // If the importer is in node compatibility mode or this is not an ESM
18
- // file that has been converted to a CommonJS file using a Babel-
19
- // compatible transform (i.e. "__esModule" has not been set), then set
20
- // "default" to the CommonJS "module.exports" for node compatibility.
21
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
- mod
23
- ));
24
-
25
- // src/overlord-controller.ts
26
- var import_node_fs3 = require("node:fs");
27
- var import_node_path2 = require("node:path");
28
-
29
- // src/overlord.ts
30
- var import_node_fs2 = require("node:fs");
31
- var import_node_path = require("node:path");
32
-
33
- // src/atomic-write.ts
34
- var import_node_fs = require("node:fs");
35
- function atomicWriteFileSync(path, content) {
36
- const tmp = `${path}.${process.pid}.tmp`;
37
- (0, import_node_fs.writeFileSync)(tmp, content, "utf8");
38
- (0, import_node_fs.renameSync)(tmp, path);
39
- }
40
-
41
- // src/overlord.ts
42
- function isoNow(now = () => /* @__PURE__ */ new Date()) {
43
- return now().toISOString();
44
- }
45
- function buildCodexFuguLaunch(slot, options) {
46
- const args = [];
47
- if (slot.role === "ultra") args.push("--model", "fugu-ultra");
48
- args.push("--no-alt-screen");
49
- if (options.profile === "full-trust-repair") {
50
- args.push("-a", "never", "-s", "danger-full-access");
51
- if (options.cwd) args.push("-C", options.cwd);
52
- return { command: "codex-fugu", args };
53
- }
54
- const sandbox = options.profile === "implementation" ? "workspace-write" : "read-only";
55
- if (options.profile === "implementation" && !options.cwd) {
56
- throw new Error("implementation servant launch requires an owned worktree cwd");
57
- }
58
- args.push(
59
- "-a",
60
- "never",
61
- "-s",
62
- sandbox,
63
- "-c",
64
- 'sandbox_permissions=["disk-full-read-access"]'
65
- );
66
- if (options.cwd) args.push("-C", options.cwd);
67
- return { command: "codex-fugu", args };
68
- }
69
- function readOverlordRegistry(statePath2) {
70
- if (!(0, import_node_fs2.existsSync)(statePath2)) return { runs: {} };
71
- try {
72
- const parsed = JSON.parse((0, import_node_fs2.readFileSync)(statePath2, "utf8"));
73
- return { activeRunId: parsed.activeRunId, runs: parsed.runs ?? {} };
74
- } catch {
75
- return { runs: {} };
76
- }
77
- }
78
- function writeOverlordRegistry(statePath2, registry) {
79
- (0, import_node_fs2.mkdirSync)((0, import_node_path.dirname)(statePath2), { recursive: true });
80
- atomicWriteFileSync(statePath2, `${JSON.stringify(registry, null, 2)}
81
- `);
82
- }
83
- function recordOverlordHeartbeat(run, options) {
84
- const timestamp = isoNow(options.now);
85
- const controllerResource = {
86
- kind: "process",
87
- pid: options.controllerPid,
88
- commandName: "mmi-cli overlord controller",
89
- runId: run.runId,
90
- runToken: run.runToken,
91
- fingerprint: options.fingerprint
92
- };
93
- const others = run.ownedResources.filter((resource) => resource.commandName !== "mmi-cli overlord controller");
94
- return {
95
- ...run,
96
- state: run.state === "starting" ? "active" : run.state,
97
- updatedAt: timestamp,
98
- controllerPid: options.controllerPid,
99
- controllerFingerprint: options.fingerprint,
100
- lastControllerHeartbeatAt: timestamp,
101
- ownedResources: [controllerResource, ...others]
102
- };
103
- }
104
- function recordOverlordControllerHeartbeat(runId2, statePath2, fingerprint2, controllerPid, now = () => /* @__PURE__ */ new Date()) {
105
- const registry = readOverlordRegistry(statePath2);
106
- const run = registry.runs[runId2];
107
- if (!run || run.state === "stopped" || run.state === "failed") return false;
108
- const next = recordOverlordHeartbeat(run, { controllerPid, fingerprint: fingerprint2, now });
109
- writeOverlordRegistry(statePath2, {
110
- ...registry,
111
- activeRunId: registry.activeRunId ?? runId2,
112
- runs: { ...registry.runs, [runId2]: next }
113
- });
114
- return true;
115
- }
116
-
117
- // src/overlord-controller.ts
118
- var [runId, statePath, fingerprint] = process.argv.slice(2);
119
- if (!runId || !statePath || !fingerprint) {
120
- console.error("mmi-cli overlord controller: missing run id, state path, or fingerprint");
121
- process.exit(2);
122
- }
123
- var intervalMs = Number(process.env.MMI_OVERLORD_HEARTBEAT_MS ?? 1e4);
124
- var servants = /* @__PURE__ */ new Map();
125
- function heartbeat() {
126
- const alive = recordOverlordControllerHeartbeat(runId, statePath, fingerprint, process.pid);
127
- if (!alive) process.exit(0);
128
- }
129
- function servantPrompt(servant, run) {
130
- 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.";
131
- return [
132
- `You are ${servant.name} in Overlord run ${run.runId}.`,
133
- roleLine,
134
- "First respond with exactly: ACK " + servant.name + " ready",
135
- "After the ACK, wait for the Overlord to assign bounded work.",
136
- "Do not start dev servers, browsers, Playwright, PRs, merges, releases, or worktree changes unless the Overlord explicitly assigns that scope.",
137
- "When assigned work, gather evidence before editing, verify before claiming done, and escalate blockers instead of looping."
138
- ].join("\n");
139
- }
140
- function quoteCmdArg(arg) {
141
- const normalized = arg.replace(/\r?\n/g, " ");
142
- return `"${normalized.replace(/"/g, '\\"')}"`;
143
- }
144
- function ptyLaunchCommand(command, args) {
145
- if (process.platform !== "win32") return { file: command, args };
146
- return {
147
- file: "cmd.exe",
148
- args: `/d /s /c ${command} ${args.map(quoteCmdArg).join(" ")}`
149
- };
150
- }
151
- function servantFingerprint(run, servant) {
152
- return `mmi-overlord-servant:${run.runId}:${servant.slotId}`;
153
- }
154
- function writeJournal(servant, text) {
155
- try {
156
- (0, import_node_fs3.mkdirSync)((0, import_node_path2.dirname)(servant.journalPath), { recursive: true });
157
- (0, import_node_fs3.appendFileSync)(servant.journalPath, text, "utf8");
158
- } catch {
159
- }
160
- }
161
- function upsertOwnedResource(resources, resource) {
162
- return [
163
- resource,
164
- ...resources.filter(
165
- (existing) => !(existing.kind === resource.kind && existing.pid === resource.pid && existing.fingerprint === resource.fingerprint)
166
- )
167
- ];
168
- }
169
- function updateServant(slotId, mutate) {
170
- const registry = readOverlordRegistry(statePath);
171
- const run = registry.runs[runId];
172
- if (!run) return;
173
- const nextServants = run.servants.map((servant) => servant.slotId === slotId ? mutate(run, servant) : servant);
174
- writeOverlordRegistry(statePath, {
175
- ...registry,
176
- runs: {
177
- ...registry.runs,
178
- [runId]: {
179
- ...run,
180
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
181
- servants: nextServants
182
- }
183
- }
184
- });
185
- }
186
- function recordServantStart(run, servant, pid, servantFp) {
187
- const registry = readOverlordRegistry(statePath);
188
- const current = registry.runs[run.runId] ?? run;
189
- const resource = {
190
- kind: "process",
191
- pid,
192
- commandName: "codex-fugu",
193
- runId: run.runId,
194
- runToken: run.runToken,
195
- fingerprint: servantFp
196
- };
197
- writeOverlordRegistry(statePath, {
198
- ...registry,
199
- activeRunId: registry.activeRunId ?? run.runId,
200
- runs: {
201
- ...registry.runs,
202
- [run.runId]: {
203
- ...current,
204
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
205
- servants: current.servants.map((item) => item.slotId === servant.slotId ? {
206
- ...item,
207
- state: "starting",
208
- pid,
209
- fingerprint: servantFp,
210
- composerSubmitMode: "crlf",
211
- lastLivenessCheckAt: (/* @__PURE__ */ new Date()).toISOString()
212
- } : item),
213
- ownedResources: upsertOwnedResource(current.ownedResources, resource)
214
- }
215
- }
216
- });
217
- }
218
- function markServantExit(slotId) {
219
- updateServant(slotId, (_run, servant) => ({
220
- ...servant,
221
- state: servant.state === "stopped" ? "stopped" : "lost",
222
- lastLivenessCheckAt: (/* @__PURE__ */ new Date()).toISOString()
223
- }));
224
- }
225
- function markServantAck(slotId) {
226
- updateServant(slotId, (_run, servant) => ({
227
- ...servant,
228
- state: "ready",
229
- lastAckAt: (/* @__PURE__ */ new Date()).toISOString(),
230
- lastUsefulSignalAt: (/* @__PURE__ */ new Date()).toISOString()
231
- }));
232
- }
233
- function messageTargets(run, target) {
234
- if (target === "all") return run.servants;
235
- return run.servants.filter((servant) => servant.slotId === target || servant.name.toLowerCase().replace(/\s+/g, "-") === target);
236
- }
237
- function deliverPendingMessages() {
238
- const registry = readOverlordRegistry(statePath);
239
- const run = registry.runs[runId];
240
- if (!run?.messages?.length) return;
241
- let changed = false;
242
- const nextMessages = run.messages.map((message) => {
243
- if (message.startedAt || message.completedAt || message.failedAt || message.deliveredAt) return message;
244
- const targets = messageTargets(run, message.target);
245
- if (!targets.length) return message;
246
- const liveTargets = targets.map((servant) => servants.get(servant.slotId));
247
- if (liveTargets.some((servant) => !servant)) return message;
248
- for (const child of liveTargets) child?.write(`${message.text}\r
249
- `);
250
- changed = true;
251
- return { ...message, state: "started", startedAt: (/* @__PURE__ */ new Date()).toISOString() };
252
- });
253
- if (!changed) return;
254
- writeOverlordRegistry(statePath, {
255
- ...registry,
256
- runs: {
257
- ...registry.runs,
258
- [runId]: {
259
- ...run,
260
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
261
- messages: nextMessages
262
- }
263
- }
264
- });
265
- }
266
- async function launchServants() {
267
- const registry = readOverlordRegistry(statePath);
268
- const run = registry.runs[runId];
269
- if (!run || run.state === "stopped" || run.state === "failed") return;
270
- const pty = await import("@homebridge/node-pty-prebuilt-multiarch");
271
- (0, import_node_fs3.mkdirSync)(run.journalDir, { recursive: true });
272
- for (const servant of run.servants) {
273
- if (servants.has(servant.slotId) || servant.state === "ready" || servant.state === "starting") continue;
274
- const launch = buildCodexFuguLaunch(servant, { profile: servant.profile, cwd: run.worktree });
275
- const command = ptyLaunchCommand(launch.command, [...launch.args, servantPrompt(servant, run)]);
276
- const child = pty.spawn(command.file, command.args, {
277
- name: "xterm-256color",
278
- cols: 120,
279
- rows: 40,
280
- cwd: run.worktree,
281
- env: {
282
- ...process.env,
283
- TERM: "xterm-256color",
284
- CODEX_FUGU_NO_NOTICE: "1",
285
- CODEX_FUGU_NO_UPDATE: "1"
286
- }
287
- });
288
- servants.set(servant.slotId, child);
289
- const servantFp = servantFingerprint(run, servant);
290
- writeJournal(servant, `
291
- [overlord] launched ${launch.command} ${launch.args.join(" ")}
292
- `);
293
- recordServantStart(run, servant, child.pid, servantFp);
294
- child.onData((data) => {
295
- writeJournal(servant, data);
296
- if (/ACK\s+.+\s+ready/i.test(data)) markServantAck(servant.slotId);
297
- });
298
- child.onExit(() => {
299
- servants.delete(servant.slotId);
300
- markServantExit(servant.slotId);
301
- });
302
- }
303
- }
304
- function shutdown() {
305
- for (const child of servants.values()) {
306
- try {
307
- child.kill();
308
- } catch {
309
- }
310
- }
311
- process.exit(0);
312
- }
313
- process.on("SIGTERM", shutdown);
314
- process.on("SIGINT", shutdown);
315
- heartbeat();
316
- if (process.env.MMI_OVERLORD_SKIP_SERVANT_LAUNCH !== "1") {
317
- void launchServants().then(deliverPendingMessages).catch(() => void 0);
318
- }
319
- setInterval(() => {
320
- heartbeat();
321
- deliverPendingMessages();
322
- }, Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 1e4);