@mutmutco/cli 2.55.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/README.md +2 -2
- package/dist/index.cjs +4 -2
- package/dist/main.cjs +1720 -516
- package/dist/saga.cjs +63 -25
- package/package.json +2 -2
- package/dist/overlord-controller.cjs +0 -322
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 +
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "MMI Future CLI — delivers the org rules (whole-file),
|
|
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.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, deliveredAt: (/* @__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);
|