@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/README.md +2 -2
- package/dist/index.cjs +4 -2
- package/dist/main.cjs +1209 -510
- package/dist/saga.cjs +63 -25
- package/package.json +2 -2
- package/dist/overlord-controller.cjs +0 -322
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
|
|
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)
|
|
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 ? `
|
|
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
|
|
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 +
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
8255
|
+
return failGraceful(`handoff open: ${e.message}`);
|
|
8205
8256
|
}
|
|
8206
8257
|
}
|
|
8207
8258
|
const current = await fetchCurrentState();
|
|
8208
|
-
if (!current) return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
8278
|
-
|
|
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("
|
|
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
|
-
|
|
9702
|
-
{ name: "handoff offer", run: verbs.handoffOffer },
|
|
9848
|
+
...continuitySteps.slice(0, 2),
|
|
9703
9849
|
{ name: "coop pending", run: verbs.coopPending },
|
|
9704
|
-
|
|
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
|
|
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)}` :
|
|
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("
|
|
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
|
|
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 ??
|
|
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
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
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
|
|
10315
|
-
|
|
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
|
|
10318
|
-
const
|
|
10319
|
-
const
|
|
10320
|
-
const
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
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
|
-
|
|
10332
|
-
${
|
|
10333
|
-
|
|
10334
|
-
|
|
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
|
|
10337
|
-
const
|
|
10338
|
-
const
|
|
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
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
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
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
return
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
}
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
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
|
|
10745
|
+
function renderOverlordPreflightFailure(report) {
|
|
10373
10746
|
const lines = [
|
|
10374
10747
|
"Overlord setup needed",
|
|
10375
|
-
"The servant pool was not started because
|
|
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("
|
|
10383
|
-
lines.push("-
|
|
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
|
|
10392
|
-
lines.push("-
|
|
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
|
|
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:
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
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:
|
|
10801
|
+
lastMessageCompletedAt: captured ? timestamp : servant.lastMessageCompletedAt
|
|
10411
10802
|
};
|
|
10412
10803
|
}
|
|
10413
|
-
function
|
|
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
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
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
|
|
10423
|
-
const
|
|
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
|
|
10426
|
-
|
|
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
|
-
|
|
10429
|
-
|
|
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
|
|
10438
|
-
startedAt
|
|
10439
|
-
completedAt:
|
|
10440
|
-
failedAt:
|
|
10441
|
-
ackText:
|
|
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:
|
|
10444
|
-
|
|
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:
|
|
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
|
|
10480
|
-
const
|
|
10481
|
-
|
|
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
|
-
`
|
|
10486
|
-
`
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
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
|
|
10498
|
-
|
|
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
|
|
10539
|
-
const
|
|
10540
|
-
const
|
|
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:
|
|
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 ??
|
|
10550
|
-
if (engine !== "
|
|
10551
|
-
|
|
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(`${
|
|
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 (
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
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
|
-
|
|
10592
|
-
`
|
|
11144
|
+
`Ready: ${ready}/${run.servants.length} servants ready.`,
|
|
11145
|
+
`Mission: ${humanSafeText(run.task || "not provided yet")}`,
|
|
10593
11146
|
`Run: ${run.runId}`,
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
"
|
|
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
|
|
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
|
|
10630
|
-
|
|
10631
|
-
|
|
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
|
-
|
|
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 ?? [],
|
|
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
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
const
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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)(
|
|
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
|
|
14140
|
+
var import_node_os3 = require("node:os");
|
|
13475
14141
|
var import_node_path17 = require("node:path");
|
|
13476
|
-
var
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
19481
|
+
var import_node_child_process12 = require("node:child_process");
|
|
18803
19482
|
var import_node_fs24 = require("node:fs");
|
|
18804
|
-
var
|
|
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)(
|
|
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,
|
|
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,
|
|
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,
|
|
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`)
|
|
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,
|
|
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,
|
|
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,
|
|
20499
|
-
pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
20770
|
-
{ surface: "codex", root: (0, import_node_path25.join)((0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
21478
|
-
|
|
21479
|
-
|
|
21480
|
-
|
|
21481
|
-
|
|
21482
|
-
|
|
21483
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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:
|
|
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,
|
|
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
|
-
|
|
24641
|
-
|
|
24642
|
-
|
|
24643
|
-
|
|
24644
|
-
|
|
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:
|
|
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
|
-
|
|
24651
|
-
|
|
24652
|
-
|
|
24653
|
-
|
|
24654
|
-
|
|
24655
|
-
|
|
24656
|
-
|
|
24657
|
-
|
|
24658
|
-
|
|
24659
|
-
|
|
24660
|
-
|
|
24661
|
-
|
|
24662
|
-
|
|
24663
|
-
|
|
24664
|
-
|
|
24665
|
-
|
|
24666
|
-
|
|
24667
|
-
|
|
24668
|
-
|
|
24669
|
-
|
|
24670
|
-
|
|
24671
|
-
|
|
24672
|
-
|
|
24673
|
-
|
|
24674
|
-
|
|
24675
|
-
|
|
24676
|
-
|
|
24677
|
-
|
|
24678
|
-
|
|
24679
|
-
|
|
24680
|
-
|
|
24681
|
-
|
|
24682
|
-
|
|
24683
|
-
|
|
24684
|
-
|
|
24685
|
-
|
|
24686
|
-
|
|
24687
|
-
|
|
24688
|
-
|
|
24689
|
-
|
|
24690
|
-
|
|
24691
|
-
|
|
24692
|
-
|
|
24693
|
-
|
|
24694
|
-
|
|
24695
|
-
|
|
24696
|
-
|
|
24697
|
-
|
|
24698
|
-
|
|
24699
|
-
|
|
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
|
-
|
|
24703
|
-
|
|
25402
|
+
{ continuityEnabled }
|
|
25403
|
+
);
|
|
24704
25404
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
24705
|
-
consoleIo.log(
|
|
25405
|
+
for (const line of sessionStartContinuityLines({ continuityEnabled, northstarInjected, cwd: process.cwd() })) consoleIo.log(line);
|
|
24706
25406
|
consoleIo.log(kbPointer());
|
|
24707
|
-
|
|
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:
|
|
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
|
});
|