@mutmutco/cli 2.56.0 → 2.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.cjs +4 -2
- package/dist/main.cjs +1403 -518
- 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,120 @@ function atomicWriteFileSync(path2, content) {
|
|
|
9973
10272
|
var OVERLORD_DEFAULT_COUNT = 3;
|
|
9974
10273
|
var OVERLORD_MIN_COUNT = 3;
|
|
9975
10274
|
var OVERLORD_MAX_COUNT = 6;
|
|
10275
|
+
var OVERLORD_SERVANT_PARALLELISM = 6;
|
|
10276
|
+
function overlordServantParallelism() {
|
|
10277
|
+
const parsed = Number(process.env.MMI_OVERLORD_SERVANT_PARALLELISM);
|
|
10278
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(Math.trunc(parsed), 16);
|
|
10279
|
+
return OVERLORD_SERVANT_PARALLELISM;
|
|
10280
|
+
}
|
|
10281
|
+
var OVERLORD_DEFAULT_ENGINE = "fugu-api";
|
|
10282
|
+
var FUGU_API_DEFAULT_BASE_URL = "https://api.sakana.ai/v1";
|
|
10283
|
+
var FUGU_API_DEFAULT_MODEL = "fugu";
|
|
10284
|
+
var FUGU_API_DEFAULT_ULTRA_MODEL = "fugu-ultra";
|
|
10285
|
+
var FUGU_API_DEFAULT_TIMEOUT_MS = 48e4;
|
|
10286
|
+
var FUGU_API_DEFAULT_STARTUP_TIMEOUT_MS = 45e3;
|
|
10287
|
+
var FUGU_API_DEFAULT_IDLE_MS = 6e4;
|
|
10288
|
+
var FUGU_API_DEFAULT_MAX_TOKENS = 16e3;
|
|
10289
|
+
var FUGU_API_DEFAULT_MAX_ATTEMPTS = 3;
|
|
10290
|
+
var FUGU_API_RETRY_BASE_MS = 250;
|
|
10291
|
+
var FUGU_API_RETRY_CAP_MS = 8e3;
|
|
9976
10292
|
var OVERLORD_HANDOFF_TIMEOUT_MS = 12e4;
|
|
10293
|
+
var SERVANT_REPORT_FIELDS = [
|
|
10294
|
+
"name",
|
|
10295
|
+
"role",
|
|
10296
|
+
"state",
|
|
10297
|
+
"assignment",
|
|
10298
|
+
"evidence",
|
|
10299
|
+
"changes",
|
|
10300
|
+
"verification",
|
|
10301
|
+
"blockers",
|
|
10302
|
+
"recommended next action"
|
|
10303
|
+
];
|
|
10304
|
+
var SERVANT_REPORT_FIELD_SET = new Set(SERVANT_REPORT_FIELDS);
|
|
10305
|
+
function normalizeServantReportField(label) {
|
|
10306
|
+
const normalized = label.trim().toLowerCase().replace(/[`*_]+/g, "").replace(/\s+/g, " ").replace(/:$/, "");
|
|
10307
|
+
if (SERVANT_REPORT_FIELD_SET.has(normalized)) return normalized;
|
|
10308
|
+
if (normalized === "next action") return "recommended next action";
|
|
10309
|
+
return void 0;
|
|
10310
|
+
}
|
|
10311
|
+
function isVagueVerification(text) {
|
|
10312
|
+
const normalized = (text ?? "").trim().toLowerCase();
|
|
10313
|
+
if (!normalized) return true;
|
|
10314
|
+
return ["done", "complete", "completed", "fixed", "looks good", "verified", "tested", "not tested", "n/a", "na", "none"].includes(normalized);
|
|
10315
|
+
}
|
|
10316
|
+
function isCompletionState(text) {
|
|
10317
|
+
return /\b(done|complete|completed|fixed)\b/i.test(text ?? "");
|
|
10318
|
+
}
|
|
10319
|
+
function extractServantReportFields(text) {
|
|
10320
|
+
const fields = {};
|
|
10321
|
+
let currentField;
|
|
10322
|
+
const append = (field, value) => {
|
|
10323
|
+
const trimmed = value.trim();
|
|
10324
|
+
fields[field] = fields[field] ? [fields[field], trimmed].filter(Boolean).join("\n") : trimmed;
|
|
10325
|
+
};
|
|
10326
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
10327
|
+
const line = rawLine.trim();
|
|
10328
|
+
const colon = line.match(/^(?:[-*]\s*)?([^:]{1,80}):\s*(.*)$/);
|
|
10329
|
+
const heading = line.match(/^#{1,6}\s+(.+?)\s*#*$/);
|
|
10330
|
+
const field = colon ? normalizeServantReportField(colon[1]) : heading ? normalizeServantReportField(heading[1]) : void 0;
|
|
10331
|
+
if (field) {
|
|
10332
|
+
currentField = field;
|
|
10333
|
+
append(field, colon ? colon[2] : "");
|
|
10334
|
+
continue;
|
|
10335
|
+
}
|
|
10336
|
+
if (currentField && line) append(currentField, line);
|
|
10337
|
+
}
|
|
10338
|
+
return fields;
|
|
10339
|
+
}
|
|
10340
|
+
function validateServantReport(text) {
|
|
10341
|
+
const fields = extractServantReportFields(text);
|
|
10342
|
+
const present = new Set(Object.keys(fields));
|
|
10343
|
+
const applies = present.size >= 2 || present.has("state") || present.has("assignment");
|
|
10344
|
+
if (!applies) {
|
|
10345
|
+
return { applies: false, ok: true, missingFields: [], emptyFields: [], unsupportedDoneClaim: false, fields };
|
|
10346
|
+
}
|
|
10347
|
+
const missingFields = SERVANT_REPORT_FIELDS.filter((field) => !present.has(field));
|
|
10348
|
+
const emptyFields = SERVANT_REPORT_FIELDS.filter((field) => present.has(field) && !fields[field]?.trim());
|
|
10349
|
+
const unsupportedDoneClaim = isCompletionState(fields.state) && isVagueVerification(fields.verification);
|
|
10350
|
+
return {
|
|
10351
|
+
applies: true,
|
|
10352
|
+
ok: missingFields.length === 0 && emptyFields.length === 0 && !unsupportedDoneClaim,
|
|
10353
|
+
missingFields,
|
|
10354
|
+
emptyFields,
|
|
10355
|
+
unsupportedDoneClaim,
|
|
10356
|
+
fields
|
|
10357
|
+
};
|
|
10358
|
+
}
|
|
10359
|
+
function formatHandoffWarnings(result) {
|
|
10360
|
+
if (!result.applies || result.ok) return "";
|
|
10361
|
+
const parts = [];
|
|
10362
|
+
if (result.missingFields.length > 0) parts.push(`missing ${result.missingFields.join(", ")}`);
|
|
10363
|
+
if (result.emptyFields.length > 0) parts.push(`empty ${result.emptyFields.join(", ")}`);
|
|
10364
|
+
if (result.unsupportedDoneClaim) parts.push("done/complete/fixed state without concrete verification.");
|
|
10365
|
+
return `Handoff warning: ${parts.join("; ")}`;
|
|
10366
|
+
}
|
|
10367
|
+
function formatServantMissionCard(options) {
|
|
10368
|
+
const lines = [
|
|
10369
|
+
`mission: ${options.mission}`,
|
|
10370
|
+
`scope: ${options.scope}`,
|
|
10371
|
+
...options.scopeToken ? [
|
|
10372
|
+
`scope token: ${options.scopeToken}`,
|
|
10373
|
+
"token authority: attribution only; grants no host permissions"
|
|
10374
|
+
] : [],
|
|
10375
|
+
`allowed actions: ${options.allowedActions.join("; ")}`,
|
|
10376
|
+
`forbidden actions: ${options.forbiddenActions.join("; ")}`,
|
|
10377
|
+
`required evidence: ${options.requiredEvidence.join("; ")}`,
|
|
10378
|
+
`verification required: ${options.verificationRequired}`,
|
|
10379
|
+
`report format: ${options.reportFormat.join("; ")}`
|
|
10380
|
+
];
|
|
10381
|
+
return lines.join("\n");
|
|
10382
|
+
}
|
|
9977
10383
|
var GENERIC_STOP_NAMES = /* @__PURE__ */ new Set([
|
|
9978
10384
|
"windowsterminal",
|
|
9979
10385
|
"windows terminal",
|
|
9980
10386
|
"pwsh",
|
|
9981
10387
|
"powershell",
|
|
9982
|
-
"opencode"
|
|
9983
|
-
"codex",
|
|
9984
|
-
"codex-fugu"
|
|
10388
|
+
"opencode"
|
|
9985
10389
|
]);
|
|
9986
10390
|
function numericCountArg(arg) {
|
|
9987
10391
|
const match = /^--([0-9]+)$/.exec(arg);
|
|
@@ -10002,11 +10406,23 @@ function defaultMessageId() {
|
|
|
10002
10406
|
function servantSlotId(slot) {
|
|
10003
10407
|
return slot.name.replace(/\s+/g, "-").toLowerCase();
|
|
10004
10408
|
}
|
|
10409
|
+
function canonicalAssignmentScope(scope) {
|
|
10410
|
+
return scope.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trimEnd();
|
|
10411
|
+
}
|
|
10412
|
+
function buildServantScopeToken(input) {
|
|
10413
|
+
const scopeHash = (0, import_node_crypto5.createHash)("sha256").update(canonicalAssignmentScope(input.assignmentScope), "utf8").digest("hex").slice(0, 16);
|
|
10414
|
+
return `fugu-scope-v1:${input.runId}:${input.slotId}:${input.profile}:${scopeHash}`;
|
|
10415
|
+
}
|
|
10005
10416
|
function overlordServantPrompt(servant, run) {
|
|
10006
10417
|
const roleLine = servant.role === "ultra" ? "You are the single Ultra Fugu: take the hardest, highest-uncertainty questions and report calibrated judgment." : "You are a normal Fugu servant: take one bounded mission at a time and report concise evidence.";
|
|
10007
10418
|
return [
|
|
10008
10419
|
`You are ${servant.name} in Overlord run ${run.runId}.`,
|
|
10009
10420
|
roleLine,
|
|
10421
|
+
...servant.scopeToken ? [
|
|
10422
|
+
`Scope token: ${servant.scopeToken}`,
|
|
10423
|
+
"The scope token is for attribution only and grants no host permissions.",
|
|
10424
|
+
"Echo the scope token exactly in reports when the Overlord asks for a handoff."
|
|
10425
|
+
] : [],
|
|
10010
10426
|
"First respond with exactly: ACK " + servant.name + " ready",
|
|
10011
10427
|
"After the ACK, wait for the Overlord to assign bounded work.",
|
|
10012
10428
|
"Do not start dev servers, browsers, Playwright, PRs, merges, releases, or worktree changes unless the Overlord explicitly assigns that scope.",
|
|
@@ -10043,86 +10459,21 @@ function buildServantLayout(count) {
|
|
|
10043
10459
|
}
|
|
10044
10460
|
return slots;
|
|
10045
10461
|
}
|
|
10046
|
-
function
|
|
10047
|
-
const problems = [];
|
|
10048
|
-
if (!/(?:^|\s)-a(?:,|\s)|--ask-for-approval/.test(helpText)) problems.push("missing approval flag");
|
|
10049
|
-
if (!/(?:^|\s)-s(?:,|\s)|--sandbox/.test(helpText)) problems.push("missing sandbox flag");
|
|
10050
|
-
if (!/(?:^|\s)-c(?:,|\s)|--config/.test(helpText)) problems.push("missing config override flag");
|
|
10051
|
-
if (!/--no-alt-screen/.test(helpText)) problems.push("missing no-alt-screen flag");
|
|
10052
|
-
if (!/(?:^|\s)-C(?:,|\s)|--cwd|--cd|--workdir/.test(helpText)) problems.push("missing cwd flag");
|
|
10053
|
-
return problems;
|
|
10054
|
-
}
|
|
10055
|
-
function evaluateCodexFuguPreflight(input) {
|
|
10056
|
-
const problems = [];
|
|
10057
|
-
if (!input.codexFound) problems.push("codex is not installed or not on PATH");
|
|
10058
|
-
if (!input.fuguFound) problems.push("codex-fugu is not installed or not on PATH");
|
|
10059
|
-
if (!input.authConfigured && !input.envNames.includes("OPENAI_API_KEY") && !input.envNames.includes("CODEX_API_KEY")) {
|
|
10060
|
-
problems.push("missing API key environment: OPENAI_API_KEY or CODEX_API_KEY");
|
|
10061
|
-
}
|
|
10062
|
-
problems.push(...validateCodexFuguHelp(input.helpText ?? ""));
|
|
10063
|
-
const status = input.statusText ?? "";
|
|
10064
|
-
const modelCatalog = input.modelCatalogText ?? "";
|
|
10065
|
-
if (!/\bfugu-ultra\b/i.test(`${status}
|
|
10066
|
-
${modelCatalog}`)) problems.push("fugu-ultra is not available");
|
|
10067
|
-
if (/native windows codex[^\n]*\/c\/users|\/c\/users[^\n]*native windows codex/i.test(status)) {
|
|
10068
|
-
problems.push("native Windows Codex is configured with a Git Bash /c/Users path");
|
|
10069
|
-
}
|
|
10070
|
-
return { ok: problems.length === 0, problems };
|
|
10071
|
-
}
|
|
10072
|
-
function evaluateOpenCodePreflight(input) {
|
|
10073
|
-
const problems = [];
|
|
10074
|
-
if (!input.found) problems.push("opencode is not installed or not on PATH");
|
|
10075
|
-
if (!/sakana\/fugu\b/i.test(input.modelsText ?? "")) problems.push("sakana/fugu is not available");
|
|
10076
|
-
if (!/sakana\/fugu-ultra\b/i.test(input.modelsText ?? "")) problems.push("sakana/fugu-ultra is not available");
|
|
10077
|
-
const probe = input.jsonProbeText ?? "";
|
|
10078
|
-
if (probe && !/(sessionID|sessionId|step_finish|finish)/i.test(probe)) problems.push("opencode run --format json did not emit session or finish events");
|
|
10079
|
-
return { ok: problems.length === 0, problems };
|
|
10080
|
-
}
|
|
10081
|
-
function buildOpenCodeLaunch(slot, message, sessionId) {
|
|
10082
|
-
const model = slot.role === "ultra" ? "sakana/fugu-ultra" : "sakana/fugu";
|
|
10083
|
-
const args = ["run", "-m", model, "--format", "json"];
|
|
10084
|
-
if (sessionId) args.push("--session", sessionId);
|
|
10085
|
-
args.push(message);
|
|
10086
|
-
return { command: "opencode", args };
|
|
10087
|
-
}
|
|
10088
|
-
function parseOpenCodeEvents(raw) {
|
|
10089
|
-
const events = [];
|
|
10090
|
-
const trimmed = (raw ?? "").trim();
|
|
10091
|
-
if (!trimmed) return { text: "", finished: false, events };
|
|
10092
|
-
const pushParsed = (chunk) => {
|
|
10093
|
-
const t = chunk.trim();
|
|
10094
|
-
if (!t) return;
|
|
10095
|
-
try {
|
|
10096
|
-
events.push(JSON.parse(t));
|
|
10097
|
-
} catch {
|
|
10098
|
-
}
|
|
10099
|
-
};
|
|
10462
|
+
function appendOverlordLedger(ledgerPath, entry) {
|
|
10100
10463
|
try {
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10464
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(ledgerPath), { recursive: true });
|
|
10465
|
+
(0, import_node_fs15.appendFileSync)(ledgerPath, `${JSON.stringify(entry)}
|
|
10466
|
+
`, "utf8");
|
|
10104
10467
|
} catch {
|
|
10105
|
-
for (const line of trimmed.split(/\r?\n/)) pushParsed(line);
|
|
10106
10468
|
}
|
|
10107
|
-
let sessionId;
|
|
10108
|
-
let text = "";
|
|
10109
|
-
let finished = false;
|
|
10110
|
-
for (const ev of events) {
|
|
10111
|
-
if (!ev || typeof ev !== "object") continue;
|
|
10112
|
-
const e = ev;
|
|
10113
|
-
const sid = e.sessionID ?? e.sessionId ?? e.session?.id;
|
|
10114
|
-
if (typeof sid === "string" && sid) sessionId = sid;
|
|
10115
|
-
const type = typeof e.type === "string" ? e.type : void 0;
|
|
10116
|
-
if (typeof e.text === "string") text += e.text;
|
|
10117
|
-
else if (type === "text" && typeof e.content === "string") text += e.content;
|
|
10118
|
-
if (type === "step_finish" || type === "finish" || e.finishReason || e.finish_reason) finished = true;
|
|
10119
|
-
}
|
|
10120
|
-
return { sessionId, text: text.trim(), finished, events };
|
|
10121
10469
|
}
|
|
10122
|
-
function
|
|
10470
|
+
function appendServantJournal(journalPath, label, text, timestamp) {
|
|
10471
|
+
if (!text || !text.trim()) return;
|
|
10123
10472
|
try {
|
|
10124
|
-
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(
|
|
10125
|
-
(0, import_node_fs15.appendFileSync)(
|
|
10473
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(journalPath), { recursive: true });
|
|
10474
|
+
(0, import_node_fs15.appendFileSync)(journalPath, `[${timestamp}] ${label}
|
|
10475
|
+
${text.trim()}
|
|
10476
|
+
|
|
10126
10477
|
`, "utf8");
|
|
10127
10478
|
} catch {
|
|
10128
10479
|
}
|
|
@@ -10144,12 +10495,88 @@ function writeOverlordRegistry(statePath, registry2) {
|
|
|
10144
10495
|
atomicWriteFileSync(statePath, `${JSON.stringify(registry2, null, 2)}
|
|
10145
10496
|
`);
|
|
10146
10497
|
}
|
|
10498
|
+
function sleepSyncMs(ms) {
|
|
10499
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(1, ms));
|
|
10500
|
+
}
|
|
10501
|
+
var OVERLORD_REGISTRY_LOCK_TIMEOUT_MS = 5e3;
|
|
10502
|
+
var OVERLORD_REGISTRY_LOCK_STALE_MS = 3e4;
|
|
10503
|
+
function withRegistryLock(statePath, fn) {
|
|
10504
|
+
const lockPath = `${statePath}.lock`;
|
|
10505
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(statePath), { recursive: true });
|
|
10506
|
+
const deadline = Date.now() + OVERLORD_REGISTRY_LOCK_TIMEOUT_MS;
|
|
10507
|
+
let fd;
|
|
10508
|
+
for (; ; ) {
|
|
10509
|
+
try {
|
|
10510
|
+
fd = (0, import_node_fs15.openSync)(lockPath, "wx");
|
|
10511
|
+
break;
|
|
10512
|
+
} catch {
|
|
10513
|
+
let stale = false;
|
|
10514
|
+
try {
|
|
10515
|
+
stale = Date.now() - (0, import_node_fs15.statSync)(lockPath).mtimeMs > OVERLORD_REGISTRY_LOCK_STALE_MS;
|
|
10516
|
+
} catch {
|
|
10517
|
+
stale = false;
|
|
10518
|
+
}
|
|
10519
|
+
if (stale) {
|
|
10520
|
+
try {
|
|
10521
|
+
(0, import_node_fs15.rmSync)(lockPath, { force: true });
|
|
10522
|
+
} catch {
|
|
10523
|
+
}
|
|
10524
|
+
continue;
|
|
10525
|
+
}
|
|
10526
|
+
if (Date.now() > deadline) throw new Error(`Overlord registry lock is busy: ${lockPath}`);
|
|
10527
|
+
sleepSyncMs(25);
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
try {
|
|
10531
|
+
return fn();
|
|
10532
|
+
} finally {
|
|
10533
|
+
if (fd !== void 0) (0, import_node_fs15.closeSync)(fd);
|
|
10534
|
+
try {
|
|
10535
|
+
(0, import_node_fs15.rmSync)(lockPath, { force: true });
|
|
10536
|
+
} catch {
|
|
10537
|
+
}
|
|
10538
|
+
}
|
|
10539
|
+
}
|
|
10540
|
+
function persistRunMutation(statePath, runId, mutate) {
|
|
10541
|
+
return withRegistryLock(statePath, () => {
|
|
10542
|
+
const fresh = readOverlordRegistry(statePath);
|
|
10543
|
+
const current = fresh.runs[runId];
|
|
10544
|
+
if (!current) throw new Error(`Overlord run ${runId} is no longer in the registry`);
|
|
10545
|
+
const next = mutate(current);
|
|
10546
|
+
writeOverlordRegistry(statePath, { ...fresh, runs: { ...fresh.runs, [runId]: next } });
|
|
10547
|
+
return next;
|
|
10548
|
+
});
|
|
10549
|
+
}
|
|
10550
|
+
function resolveOverlordSendMessage(messageArgs, messageFile) {
|
|
10551
|
+
const inline = messageArgs.join(" ").trim();
|
|
10552
|
+
if (inline) return inline;
|
|
10553
|
+
if (messageFile) {
|
|
10554
|
+
const raw = messageFile === "-" ? (0, import_node_fs15.readFileSync)(0, "utf8") : (0, import_node_fs15.readFileSync)(messageFile, "utf8");
|
|
10555
|
+
return raw.trim();
|
|
10556
|
+
}
|
|
10557
|
+
return "";
|
|
10558
|
+
}
|
|
10559
|
+
function computeAffectedSlots(servants, normalized, settled) {
|
|
10560
|
+
const targetSlots = normalized === "all" ? servants.map((s) => s.slotId) : servants.filter((s) => s.slotId === normalized || normalizeServantTarget(s.name) === normalized).map((s) => s.slotId);
|
|
10561
|
+
return /* @__PURE__ */ new Set([...(settled?.servantResults ?? []).map((r) => r.slotId), ...targetSlots]);
|
|
10562
|
+
}
|
|
10563
|
+
function mergeConcurrentDispatch(fresh, dispatched, settled, affected, messageId) {
|
|
10564
|
+
const dispatchedBySlot = new Map(dispatched.servants.map((s) => [s.slotId, s]));
|
|
10565
|
+
const updatedAt = [fresh.updatedAt, dispatched.updatedAt].filter(Boolean).sort().at(-1) ?? dispatched.updatedAt;
|
|
10566
|
+
return {
|
|
10567
|
+
...fresh,
|
|
10568
|
+
updatedAt,
|
|
10569
|
+
servants: fresh.servants.map((s) => affected.has(s.slotId) ? dispatchedBySlot.get(s.slotId) ?? s : s),
|
|
10570
|
+
messages: [...(fresh.messages ?? []).filter((m) => m.id !== messageId), ...settled ? [settled] : []]
|
|
10571
|
+
};
|
|
10572
|
+
}
|
|
10147
10573
|
function buildOverlordRun(options) {
|
|
10148
10574
|
const runId = options.runId?.() ?? defaultRunId();
|
|
10149
10575
|
const runToken = options.runToken?.() ?? defaultRunToken();
|
|
10150
10576
|
const statePath = defaultOverlordStatePath(options.cwd);
|
|
10151
10577
|
const journalDir = (0, import_node_path13.join)(options.cwd, "tmp", "overlord", runId);
|
|
10152
|
-
const engine = options.engine ??
|
|
10578
|
+
const engine = options.engine ?? OVERLORD_DEFAULT_ENGINE;
|
|
10579
|
+
const provider = "sakana";
|
|
10153
10580
|
const timestamp = isoNow(options.now);
|
|
10154
10581
|
return {
|
|
10155
10582
|
runId,
|
|
@@ -10163,44 +10590,31 @@ function buildOverlordRun(options) {
|
|
|
10163
10590
|
journalDir,
|
|
10164
10591
|
ledgerPath: (0, import_node_path13.join)(journalDir, "ledger.jsonl"),
|
|
10165
10592
|
engine,
|
|
10166
|
-
provider
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10593
|
+
provider,
|
|
10594
|
+
servants: buildServantLayout(options.count).map((slot) => {
|
|
10595
|
+
const slotId = servantSlotId(slot);
|
|
10596
|
+
const profile = "consultation";
|
|
10597
|
+
return {
|
|
10598
|
+
...slot,
|
|
10599
|
+
slotId,
|
|
10600
|
+
profile,
|
|
10601
|
+
state: "planned",
|
|
10602
|
+
journalPath: (0, import_node_path13.join)(journalDir, `${slotId}.log`),
|
|
10603
|
+
composerSubmitMode: "surface-api",
|
|
10604
|
+
engine,
|
|
10605
|
+
provider,
|
|
10606
|
+
eventJournalPath: (0, import_node_path13.join)(journalDir, `${slotId}.events.jsonl`),
|
|
10607
|
+
scopeToken: buildServantScopeToken({
|
|
10608
|
+
runId,
|
|
10609
|
+
slotId,
|
|
10610
|
+
profile,
|
|
10611
|
+
assignmentScope: options.task
|
|
10612
|
+
})
|
|
10613
|
+
};
|
|
10614
|
+
}),
|
|
10180
10615
|
ownedResources: []
|
|
10181
10616
|
};
|
|
10182
10617
|
}
|
|
10183
|
-
function recordOverlordHeartbeat(run, options) {
|
|
10184
|
-
const timestamp = isoNow(options.now);
|
|
10185
|
-
const controllerResource = {
|
|
10186
|
-
kind: "process",
|
|
10187
|
-
pid: options.controllerPid,
|
|
10188
|
-
commandName: "mmi-cli overlord controller",
|
|
10189
|
-
runId: run.runId,
|
|
10190
|
-
runToken: run.runToken,
|
|
10191
|
-
fingerprint: options.fingerprint
|
|
10192
|
-
};
|
|
10193
|
-
const others = run.ownedResources.filter((resource) => resource.commandName !== "mmi-cli overlord controller");
|
|
10194
|
-
return {
|
|
10195
|
-
...run,
|
|
10196
|
-
state: run.state === "starting" ? "active" : run.state,
|
|
10197
|
-
updatedAt: timestamp,
|
|
10198
|
-
controllerPid: options.controllerPid,
|
|
10199
|
-
controllerFingerprint: options.fingerprint,
|
|
10200
|
-
lastControllerHeartbeatAt: timestamp,
|
|
10201
|
-
ownedResources: [controllerResource, ...others]
|
|
10202
|
-
};
|
|
10203
|
-
}
|
|
10204
10618
|
function buildOverlordStartupPlan(args, cwd) {
|
|
10205
10619
|
const count = parseOverlordCount(args);
|
|
10206
10620
|
const task = args.filter((arg) => numericCountArg(arg) === void 0).join(" ").trim();
|
|
@@ -10231,11 +10645,9 @@ function summarizeOverlordRun(run, probe) {
|
|
|
10231
10645
|
state: run.state,
|
|
10232
10646
|
controller,
|
|
10233
10647
|
servants: run.servants.map((servant) => {
|
|
10234
|
-
if (run.engine === "opencode" || servant.engine === "opencode") return { name: servant.name, state: servantProgress(run, servant, now) };
|
|
10235
|
-
if (servant.pid == null) return { name: servant.name, state: "not-started" };
|
|
10236
|
-
if (!probe.isPidAlive(servant.pid)) return { name: servant.name, state: "lost" };
|
|
10237
10648
|
const progress = servantProgress(run, servant, now);
|
|
10238
10649
|
if (progress !== servant.state) return { name: servant.name, state: progress };
|
|
10650
|
+
if (servant.pid != null && !probe.isPidAlive(servant.pid)) return { name: servant.name, state: "lost" };
|
|
10239
10651
|
if (servant.state === "ready" && servant.lastAckAt && servant.composerSubmitMode !== "unknown") {
|
|
10240
10652
|
return { name: servant.name, state: "ready" };
|
|
10241
10653
|
}
|
|
@@ -10253,52 +10665,6 @@ function planOverlordRunStop(run) {
|
|
|
10253
10665
|
}
|
|
10254
10666
|
return { killPids, uncertain };
|
|
10255
10667
|
}
|
|
10256
|
-
function controllerFingerprint(run) {
|
|
10257
|
-
return `mmi-overlord-controller:${run.runId}:${run.worktree}`;
|
|
10258
|
-
}
|
|
10259
|
-
function defaultStartController(run) {
|
|
10260
|
-
const scriptPath = (0, import_node_path13.join)(__dirname, "overlord-controller.cjs");
|
|
10261
|
-
const fingerprint = controllerFingerprint(run);
|
|
10262
|
-
const child = (0, import_node_child_process8.spawn)(process.execPath, [scriptPath, run.runId, run.statePath, fingerprint], {
|
|
10263
|
-
detached: true,
|
|
10264
|
-
stdio: "ignore",
|
|
10265
|
-
windowsHide: true,
|
|
10266
|
-
cwd: run.worktree
|
|
10267
|
-
});
|
|
10268
|
-
child.unref();
|
|
10269
|
-
return { pid: child.pid, fingerprint };
|
|
10270
|
-
}
|
|
10271
|
-
function defaultRunOpenCode(run, servant, message, sessionId) {
|
|
10272
|
-
const launch = buildOpenCodeLaunch(servant, message, sessionId ?? servant.opencodeSessionId);
|
|
10273
|
-
const shell2 = process.platform === "win32";
|
|
10274
|
-
const file = shell2 ? [launch.command, ...launch.args].map(shellQuote2).join(" ") : launch.command;
|
|
10275
|
-
const result = (0, import_node_child_process8.spawnSync)(file, shell2 ? [] : launch.args, {
|
|
10276
|
-
encoding: "utf8",
|
|
10277
|
-
shell: shell2,
|
|
10278
|
-
cwd: run.worktree,
|
|
10279
|
-
timeout: 6e5,
|
|
10280
|
-
windowsHide: true,
|
|
10281
|
-
env: { ...process.env }
|
|
10282
|
-
});
|
|
10283
|
-
if (result.error && result.error.code === "ENOENT") {
|
|
10284
|
-
return { ok: false, error: "opencode is not installed or not on PATH" };
|
|
10285
|
-
}
|
|
10286
|
-
const raw = `${result.stdout ?? ""}
|
|
10287
|
-
${result.stderr ?? ""}`;
|
|
10288
|
-
if (servant.eventJournalPath) {
|
|
10289
|
-
try {
|
|
10290
|
-
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(servant.eventJournalPath), { recursive: true });
|
|
10291
|
-
(0, import_node_fs15.appendFileSync)(servant.eventJournalPath, `${raw.trim()}
|
|
10292
|
-
`, "utf8");
|
|
10293
|
-
} catch {
|
|
10294
|
-
}
|
|
10295
|
-
}
|
|
10296
|
-
const parsed = parseOpenCodeEvents(raw);
|
|
10297
|
-
if (result.status !== 0 && !parsed.finished) {
|
|
10298
|
-
return { ok: false, sessionId: parsed.sessionId, text: parsed.text, error: `opencode run exited ${result.status ?? "with error"}`, events: parsed.events };
|
|
10299
|
-
}
|
|
10300
|
-
return { ok: true, sessionId: parsed.sessionId, text: parsed.text, events: parsed.events };
|
|
10301
|
-
}
|
|
10302
10668
|
function defaultIsPidAlive(pid) {
|
|
10303
10669
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
10304
10670
|
try {
|
|
@@ -10311,141 +10677,409 @@ function defaultIsPidAlive(pid) {
|
|
|
10311
10677
|
function defaultKillPid(pid) {
|
|
10312
10678
|
process.kill(pid);
|
|
10313
10679
|
}
|
|
10314
|
-
function
|
|
10315
|
-
|
|
10680
|
+
function fuguApiTimeoutMs() {
|
|
10681
|
+
const parsed = Number(process.env.MMI_OVERLORD_LLM_TIMEOUT_MS);
|
|
10682
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return FUGU_API_DEFAULT_TIMEOUT_MS;
|
|
10683
|
+
return Math.min(Math.max(Math.trunc(parsed), 5e3), 12e5);
|
|
10316
10684
|
}
|
|
10317
|
-
function
|
|
10318
|
-
const
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10685
|
+
function fuguApiIdleMs() {
|
|
10686
|
+
const parsed = Number(process.env.MMI_OVERLORD_LLM_IDLE_MS);
|
|
10687
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return FUGU_API_DEFAULT_IDLE_MS;
|
|
10688
|
+
return Math.min(Math.max(Math.trunc(parsed), 5e3), 6e5);
|
|
10689
|
+
}
|
|
10690
|
+
function fuguApiMaxTokens() {
|
|
10691
|
+
const parsed = Number(process.env.MMI_OVERLORD_LLM_MAX_TOKENS);
|
|
10692
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return FUGU_API_DEFAULT_MAX_TOKENS;
|
|
10693
|
+
return Math.min(Math.max(Math.trunc(parsed), 256), 2e5);
|
|
10694
|
+
}
|
|
10695
|
+
function fuguApiStartupTimeoutMs() {
|
|
10696
|
+
const workTimeout = fuguApiTimeoutMs();
|
|
10697
|
+
const parsed = Number(process.env.MMI_OVERLORD_LLM_STARTUP_TIMEOUT_MS);
|
|
10698
|
+
const chosen = Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : FUGU_API_DEFAULT_STARTUP_TIMEOUT_MS;
|
|
10699
|
+
return Math.min(Math.max(chosen, 5e3), workTimeout);
|
|
10700
|
+
}
|
|
10701
|
+
function fuguApiConfig() {
|
|
10702
|
+
return {
|
|
10703
|
+
baseUrl: (process.env.MMI_OVERLORD_LLM_BASE_URL ?? FUGU_API_DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
10704
|
+
apiKey: process.env.MMI_OVERLORD_LLM_API_KEY ?? process.env.SAKANA_API_KEY,
|
|
10705
|
+
model: process.env.MMI_OVERLORD_LLM_MODEL ?? FUGU_API_DEFAULT_MODEL,
|
|
10706
|
+
ultraModel: process.env.MMI_OVERLORD_LLM_ULTRA_MODEL ?? FUGU_API_DEFAULT_ULTRA_MODEL,
|
|
10707
|
+
timeoutMs: fuguApiTimeoutMs(),
|
|
10708
|
+
idleMs: fuguApiIdleMs(),
|
|
10709
|
+
maxTokens: fuguApiMaxTokens()
|
|
10710
|
+
};
|
|
10711
|
+
}
|
|
10712
|
+
function fuguApiModelForServant(config, servant) {
|
|
10713
|
+
return servant.role === "ultra" ? config.ultraModel : config.model;
|
|
10714
|
+
}
|
|
10715
|
+
function fuguApiSystemMessage(servant, run) {
|
|
10716
|
+
const roleLine = servant.role === "ultra" ? "You are the single Ultra Fugu servant. Take the hardest useful slice, surface risks, and give calibrated judgment." : "You are a normal Fugu servant. Take one bounded assignment at a time and report concise evidence.";
|
|
10717
|
+
const contract = formatServantMissionCard({
|
|
10718
|
+
mission: run.task,
|
|
10719
|
+
scope: servant.role === "ultra" ? "consult on the highest-uncertainty slice assigned by the Overlord" : "consult on one bounded slice assigned by the Overlord",
|
|
10720
|
+
scopeToken: servant.scopeToken,
|
|
10721
|
+
allowedActions: ["inspect assigned evidence", "reason from provided context", "report findings"],
|
|
10722
|
+
forbiddenActions: ["mutate files without explicit scope", "start stage/dev servers", "start browsers or Playwright", "open PRs or merges"],
|
|
10723
|
+
requiredEvidence: ["cite source paths, command results, or observations when available", "separate evidence from inference"],
|
|
10724
|
+
verificationRequired: "name the verification performed before any done/complete/fixed claim, or explain why verification is not applicable",
|
|
10725
|
+
reportFormat: [...SERVANT_REPORT_FIELDS, "scope token"]
|
|
10330
10726
|
});
|
|
10331
|
-
|
|
10332
|
-
${
|
|
10333
|
-
|
|
10334
|
-
|
|
10727
|
+
return [
|
|
10728
|
+
`You are ${servant.name} in Overlord run ${run.runId}.`,
|
|
10729
|
+
roleLine,
|
|
10730
|
+
"The Overlord is the only coordinator.",
|
|
10731
|
+
"Do not claim ownership of the full mission.",
|
|
10732
|
+
"Do not mutate files, branches, worktrees, stage/dev servers, browsers, PRs, or releases unless the Overlord explicitly grants that scope.",
|
|
10733
|
+
"Gather evidence before acting.",
|
|
10734
|
+
"Verify before claiming done.",
|
|
10735
|
+
"Escalate blockers instead of looping.",
|
|
10736
|
+
contract
|
|
10737
|
+
].join("\n");
|
|
10335
10738
|
}
|
|
10336
|
-
function
|
|
10337
|
-
const
|
|
10338
|
-
const
|
|
10739
|
+
async function collectFuguApiPreflight() {
|
|
10740
|
+
const config = fuguApiConfig();
|
|
10741
|
+
const problems = [];
|
|
10742
|
+
if (!config.apiKey) problems.push("SAKANA_API_KEY or MMI_OVERLORD_LLM_API_KEY is not available");
|
|
10743
|
+
if (!config.baseUrl) problems.push("Fugu API base URL is not configured");
|
|
10744
|
+
if (!config.apiKey || !config.baseUrl) return { ok: false, problems };
|
|
10745
|
+
const controller = new AbortController();
|
|
10746
|
+
const timeout = setTimeout(() => controller.abort(), Math.min(config.timeoutMs, 15e3));
|
|
10339
10747
|
try {
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10748
|
+
const response = await fetch(`${config.baseUrl}/models`, {
|
|
10749
|
+
method: "GET",
|
|
10750
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
10751
|
+
signal: controller.signal
|
|
10752
|
+
});
|
|
10753
|
+
if (!response.ok) {
|
|
10754
|
+
problems.push(`Fugu API model probe returned HTTP ${response.status}`);
|
|
10755
|
+
} else {
|
|
10756
|
+
const body = await response.json();
|
|
10757
|
+
const models = new Set((body.data ?? []).map((model) => model.id).filter((id) => Boolean(id)));
|
|
10758
|
+
if (!models.has(config.model)) problems.push(`Fugu API model ${config.model} is not available`);
|
|
10759
|
+
if (!models.has(config.ultraModel)) problems.push(`Fugu API ultra model ${config.ultraModel} is not available`);
|
|
10760
|
+
}
|
|
10761
|
+
} catch (e) {
|
|
10762
|
+
problems.push(`Fugu API model probe failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10763
|
+
} finally {
|
|
10764
|
+
clearTimeout(timeout);
|
|
10343
10765
|
}
|
|
10766
|
+
return { ok: problems.length === 0, problems };
|
|
10344
10767
|
}
|
|
10345
|
-
function
|
|
10346
|
-
|
|
10347
|
-
|
|
10768
|
+
function mapFuguApiUsage(usage) {
|
|
10769
|
+
if (!usage) return void 0;
|
|
10770
|
+
const mapped = {
|
|
10771
|
+
promptTokens: usage.prompt_tokens,
|
|
10772
|
+
completionTokens: usage.completion_tokens,
|
|
10773
|
+
totalTokens: usage.total_tokens
|
|
10774
|
+
};
|
|
10775
|
+
return Object.values(mapped).some((value) => typeof value === "number") ? mapped : void 0;
|
|
10348
10776
|
}
|
|
10349
|
-
function
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10777
|
+
function buildFuguRequestBody(model, messages, maxTokens) {
|
|
10778
|
+
return {
|
|
10779
|
+
model,
|
|
10780
|
+
messages,
|
|
10781
|
+
temperature: 0.2,
|
|
10782
|
+
max_tokens: maxTokens,
|
|
10783
|
+
stream: true,
|
|
10784
|
+
stream_options: { include_usage: true }
|
|
10785
|
+
};
|
|
10357
10786
|
}
|
|
10358
|
-
function
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10787
|
+
function foldFuguSse(payloads) {
|
|
10788
|
+
let text = "";
|
|
10789
|
+
let finishReason;
|
|
10790
|
+
let rawUsage;
|
|
10791
|
+
for (const payload of payloads) {
|
|
10792
|
+
const data = payload.trim();
|
|
10793
|
+
if (!data || data === "[DONE]") continue;
|
|
10794
|
+
let parsed;
|
|
10795
|
+
try {
|
|
10796
|
+
parsed = JSON.parse(data);
|
|
10797
|
+
} catch {
|
|
10798
|
+
continue;
|
|
10799
|
+
}
|
|
10800
|
+
const choice = parsed?.choices?.[0];
|
|
10801
|
+
const piece = choice?.delta?.content ?? choice?.message?.content;
|
|
10802
|
+
if (typeof piece === "string") text += piece;
|
|
10803
|
+
if (choice?.finish_reason) finishReason = choice.finish_reason;
|
|
10804
|
+
if (parsed?.usage) rawUsage = parsed.usage;
|
|
10805
|
+
}
|
|
10806
|
+
return { text, finishReason, usage: mapFuguApiUsage(rawUsage) };
|
|
10807
|
+
}
|
|
10808
|
+
async function defaultRunFuguApi(run, servant, message, options) {
|
|
10809
|
+
const config = fuguApiConfig();
|
|
10810
|
+
if (!config.apiKey) return { ok: false, error: "SAKANA_API_KEY or MMI_OVERLORD_LLM_API_KEY is not available" };
|
|
10811
|
+
const model = fuguApiModelForServant(config, servant);
|
|
10812
|
+
const messages = [
|
|
10813
|
+
...servant.llmMessages ?? [{ role: "system", content: fuguApiSystemMessage(servant, run) }],
|
|
10814
|
+
{ role: "user", content: message }
|
|
10815
|
+
];
|
|
10816
|
+
const totalMs = options?.timeoutMs ?? config.timeoutMs;
|
|
10817
|
+
const idleMs = Math.min(config.idleMs, totalMs);
|
|
10818
|
+
const controller = new AbortController();
|
|
10819
|
+
let abortKind;
|
|
10820
|
+
const totalTimer = setTimeout(() => {
|
|
10821
|
+
abortKind = "total";
|
|
10822
|
+
controller.abort();
|
|
10823
|
+
}, totalMs);
|
|
10824
|
+
let idleTimer;
|
|
10825
|
+
let streamReader;
|
|
10826
|
+
const armIdle = () => {
|
|
10827
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
10828
|
+
idleTimer = setTimeout(() => {
|
|
10829
|
+
abortKind = "idle";
|
|
10830
|
+
controller.abort();
|
|
10831
|
+
}, idleMs);
|
|
10832
|
+
};
|
|
10833
|
+
armIdle();
|
|
10834
|
+
try {
|
|
10835
|
+
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
10836
|
+
method: "POST",
|
|
10837
|
+
headers: {
|
|
10838
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
10839
|
+
"Content-Type": "application/json"
|
|
10840
|
+
},
|
|
10841
|
+
body: JSON.stringify(buildFuguRequestBody(model, messages, config.maxTokens)),
|
|
10842
|
+
signal: controller.signal
|
|
10843
|
+
});
|
|
10844
|
+
const requestId = response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? void 0;
|
|
10845
|
+
if (!response.ok) {
|
|
10846
|
+
const errBody = await response.json().catch(() => void 0);
|
|
10847
|
+
return {
|
|
10848
|
+
ok: false,
|
|
10849
|
+
model,
|
|
10850
|
+
requestId,
|
|
10851
|
+
error: errBody?.error?.message ?? `Fugu API returned HTTP ${response.status}`,
|
|
10852
|
+
retryable: response.status === 429 || response.status >= 500
|
|
10853
|
+
};
|
|
10854
|
+
}
|
|
10855
|
+
if (!response.body) return { ok: false, model, requestId, error: "Fugu API returned no response body", retryable: true };
|
|
10856
|
+
const reader = response.body.getReader();
|
|
10857
|
+
streamReader = reader;
|
|
10858
|
+
const decoder = new TextDecoder();
|
|
10859
|
+
const payloads = [];
|
|
10860
|
+
let buffer = "";
|
|
10861
|
+
for (; ; ) {
|
|
10862
|
+
const { done, value } = await reader.read();
|
|
10863
|
+
if (done) break;
|
|
10864
|
+
armIdle();
|
|
10865
|
+
buffer += decoder.decode(value, { stream: true });
|
|
10866
|
+
const lines = buffer.split("\n");
|
|
10867
|
+
buffer = lines.pop() ?? "";
|
|
10868
|
+
for (const line of lines) {
|
|
10869
|
+
const trimmed = line.trim();
|
|
10870
|
+
if (trimmed.startsWith("data:")) payloads.push(trimmed.slice(5).trim());
|
|
10871
|
+
}
|
|
10872
|
+
}
|
|
10873
|
+
const tail = buffer.trim();
|
|
10874
|
+
if (tail.startsWith("data:")) payloads.push(tail.slice(5).trim());
|
|
10875
|
+
const { text: rawText, finishReason, usage } = foldFuguSse(payloads);
|
|
10876
|
+
const text = rawText.trim();
|
|
10877
|
+
if (!text) {
|
|
10878
|
+
const reason = finishReason === "length" ? "Fugu API hit max_tokens before emitting content (raise MMI_OVERLORD_LLM_MAX_TOKENS)" : "Fugu API returned no assistant text";
|
|
10879
|
+
return { ok: false, model, requestId, error: reason, finishReason, usage, retryable: false };
|
|
10880
|
+
}
|
|
10881
|
+
return {
|
|
10882
|
+
ok: true,
|
|
10883
|
+
text,
|
|
10884
|
+
model,
|
|
10885
|
+
requestId,
|
|
10886
|
+
finishReason,
|
|
10887
|
+
usage,
|
|
10888
|
+
messages: [...messages, { role: "assistant", content: text }]
|
|
10889
|
+
};
|
|
10890
|
+
} catch (e) {
|
|
10891
|
+
const aborted = e instanceof Error && e.name === "AbortError";
|
|
10892
|
+
if (aborted) {
|
|
10893
|
+
const bound = abortKind === "idle" ? `${Math.round(idleMs / 1e3)}s with no data` : `${Math.round(totalMs / 1e3)}s total`;
|
|
10894
|
+
return { ok: false, model, error: `Fugu API request timed out (${bound})`, retryable: false };
|
|
10895
|
+
}
|
|
10896
|
+
return { ok: false, model, error: e instanceof Error ? e.message : String(e), retryable: true };
|
|
10897
|
+
} finally {
|
|
10898
|
+
clearTimeout(totalTimer);
|
|
10899
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
10900
|
+
if (streamReader) {
|
|
10901
|
+
try {
|
|
10902
|
+
streamReader.releaseLock();
|
|
10903
|
+
} catch {
|
|
10904
|
+
}
|
|
10905
|
+
}
|
|
10906
|
+
}
|
|
10371
10907
|
}
|
|
10372
|
-
function
|
|
10908
|
+
function renderOverlordPreflightFailure(report) {
|
|
10373
10909
|
const lines = [
|
|
10374
10910
|
"Overlord setup needed",
|
|
10375
|
-
"The servant pool was not started because
|
|
10911
|
+
"The servant pool was not started because Fugu preflight failed.",
|
|
10376
10912
|
"",
|
|
10377
10913
|
"Problems:",
|
|
10378
10914
|
...report.problems.map((problem) => `- ${problem}`),
|
|
10379
10915
|
"",
|
|
10380
10916
|
"Fixes:"
|
|
10381
10917
|
];
|
|
10382
|
-
if (report.problems.some((problem) => problem.includes("
|
|
10383
|
-
lines.push("-
|
|
10918
|
+
if (report.problems.some((problem) => problem.includes("SAKANA_API_KEY") || problem.includes("Fugu API"))) {
|
|
10919
|
+
lines.push("- Confirm `SAKANA_API_KEY` is available and `https://api.sakana.ai/v1/models` is reachable.");
|
|
10384
10920
|
}
|
|
10385
|
-
if (report.problems.some((problem) => problem.includes("
|
|
10386
|
-
lines.push("-
|
|
10387
|
-
}
|
|
10388
|
-
if (report.problems.some((problem) => problem.includes("missing API key"))) {
|
|
10389
|
-
lines.push("- Set OPENAI_API_KEY or CODEX_API_KEY in the launching shell; do not paste key values into chat.");
|
|
10390
|
-
}
|
|
10391
|
-
if (report.problems.some((problem) => problem.includes("fugu-ultra"))) {
|
|
10392
|
-
lines.push("- Recheck or update Fugu configs so `fugu-ultra` appears in the Fugu model catalog.");
|
|
10393
|
-
}
|
|
10394
|
-
if (report.problems.some((problem) => problem.includes("missing ") && problem.includes("flag"))) {
|
|
10395
|
-
lines.push("- Update Codex/Fugu until `codex-fugu --help` exposes approval, sandbox, config, cwd, and no-alt-screen flags.");
|
|
10396
|
-
}
|
|
10397
|
-
if (report.problems.some((problem) => problem.includes("/c/Users"))) {
|
|
10398
|
-
lines.push("- Launch from a native shell adapter or repair Fugu path configuration so Windows paths stay native.");
|
|
10921
|
+
if (report.problems.some((problem) => problem.includes("model") || problem.includes("fugu"))) {
|
|
10922
|
+
lines.push("- Confirm the configured Fugu API models are exposed by `/models`.");
|
|
10399
10923
|
}
|
|
10400
10924
|
return lines.join("\n");
|
|
10401
10925
|
}
|
|
10402
|
-
function
|
|
10926
|
+
function markServantsStarting(run, timestamp) {
|
|
10927
|
+
return {
|
|
10928
|
+
...run,
|
|
10929
|
+
updatedAt: timestamp,
|
|
10930
|
+
servants: run.servants.map((servant) => ({
|
|
10931
|
+
...servant,
|
|
10932
|
+
state: "starting"
|
|
10933
|
+
}))
|
|
10934
|
+
};
|
|
10935
|
+
}
|
|
10936
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
10937
|
+
const results = new Array(items.length);
|
|
10938
|
+
let nextIndex = 0;
|
|
10939
|
+
const workerCount = Math.min(Math.max(1, limit), items.length);
|
|
10940
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
10941
|
+
while (true) {
|
|
10942
|
+
const index = nextIndex;
|
|
10943
|
+
nextIndex += 1;
|
|
10944
|
+
if (index >= items.length) return;
|
|
10945
|
+
results[index] = await worker(items[index]);
|
|
10946
|
+
}
|
|
10947
|
+
}));
|
|
10948
|
+
return results;
|
|
10949
|
+
}
|
|
10950
|
+
function hasCapturedFuguApiText(result) {
|
|
10951
|
+
return result.ok && Boolean(result.text?.trim());
|
|
10952
|
+
}
|
|
10953
|
+
function updateFuguApiServant(servant, result, timestamp) {
|
|
10954
|
+
const captured = hasCapturedFuguApiText(result);
|
|
10403
10955
|
return {
|
|
10404
10956
|
...servant,
|
|
10405
|
-
state:
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10957
|
+
state: captured ? "ready" : "blocked",
|
|
10958
|
+
llmModel: result.model ?? servant.llmModel,
|
|
10959
|
+
llmMessages: result.messages ?? servant.llmMessages,
|
|
10960
|
+
llmRequestId: result.requestId ?? servant.llmRequestId,
|
|
10961
|
+
lastAckAt: captured ? timestamp : servant.lastAckAt,
|
|
10962
|
+
lastUsefulSignalAt: captured ? timestamp : servant.lastUsefulSignalAt,
|
|
10409
10963
|
lastEventAt: timestamp,
|
|
10410
|
-
lastMessageCompletedAt:
|
|
10964
|
+
lastMessageCompletedAt: captured ? timestamp : servant.lastMessageCompletedAt
|
|
10965
|
+
};
|
|
10966
|
+
}
|
|
10967
|
+
function defaultSleep3(ms) {
|
|
10968
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
10969
|
+
}
|
|
10970
|
+
function fuguApiRetryConfig(sleep) {
|
|
10971
|
+
const parsed = Number(process.env.MMI_OVERLORD_LLM_MAX_ATTEMPTS);
|
|
10972
|
+
const maxAttempts = Number.isFinite(parsed) && parsed >= 1 ? Math.min(Math.trunc(parsed), 6) : FUGU_API_DEFAULT_MAX_ATTEMPTS;
|
|
10973
|
+
return { maxAttempts, baseDelayMs: FUGU_API_RETRY_BASE_MS, sleep };
|
|
10974
|
+
}
|
|
10975
|
+
function fuguApiBackoffMs(attempt, baseDelayMs) {
|
|
10976
|
+
const exponential = baseDelayMs * 2 ** (attempt - 1);
|
|
10977
|
+
const capped = Math.min(exponential, FUGU_API_RETRY_CAP_MS);
|
|
10978
|
+
const jitter = Math.random() * capped * 0.25;
|
|
10979
|
+
return Math.round(capped + jitter);
|
|
10980
|
+
}
|
|
10981
|
+
async function callFuguApiRunner(runFuguApi, run, servant, message, retry, callOptions) {
|
|
10982
|
+
const maxAttempts = Math.max(1, retry?.maxAttempts ?? 1);
|
|
10983
|
+
const baseDelayMs = retry?.baseDelayMs ?? FUGU_API_RETRY_BASE_MS;
|
|
10984
|
+
const sleep = retry?.sleep ?? defaultSleep3;
|
|
10985
|
+
let attempt = 0;
|
|
10986
|
+
for (; ; ) {
|
|
10987
|
+
attempt += 1;
|
|
10988
|
+
let result;
|
|
10989
|
+
try {
|
|
10990
|
+
result = await runFuguApi(run, servant, message, callOptions);
|
|
10991
|
+
} catch (e) {
|
|
10992
|
+
result = { ok: false, error: e instanceof Error ? e.message : String(e), retryable: true };
|
|
10993
|
+
}
|
|
10994
|
+
if (result.ok || !result.retryable || attempt >= maxAttempts) {
|
|
10995
|
+
return { ...result, attempts: attempt };
|
|
10996
|
+
}
|
|
10997
|
+
await sleep(fuguApiBackoffMs(attempt, baseDelayMs));
|
|
10998
|
+
}
|
|
10999
|
+
}
|
|
11000
|
+
function seedLazyServant(servant, run, timestamp) {
|
|
11001
|
+
const config = fuguApiConfig();
|
|
11002
|
+
const llmMessages = servant.llmMessages ?? [{ role: "system", content: fuguApiSystemMessage(servant, run) }];
|
|
11003
|
+
return {
|
|
11004
|
+
...servant,
|
|
11005
|
+
state: "ready",
|
|
11006
|
+
llmModel: fuguApiModelForServant(config, servant),
|
|
11007
|
+
llmMessages,
|
|
11008
|
+
lastAckAt: timestamp,
|
|
11009
|
+
lastUsefulSignalAt: timestamp,
|
|
11010
|
+
lastEventAt: timestamp
|
|
10411
11011
|
};
|
|
10412
11012
|
}
|
|
10413
|
-
function
|
|
11013
|
+
async function startFuguApiServants(run, runFuguApi, now, retry) {
|
|
11014
|
+
const startupCallOptions = { timeoutMs: fuguApiStartupTimeoutMs() };
|
|
11015
|
+
const verifiedTargets = run.servants.filter((servant) => servant.role !== "ultra");
|
|
11016
|
+
const results = await mapWithConcurrency(verifiedTargets, overlordServantParallelism(), async (servant) => ({
|
|
11017
|
+
servant,
|
|
11018
|
+
result: await callFuguApiRunner(runFuguApi, run, servant, overlordServantPrompt(servant, run), retry, startupCallOptions)
|
|
11019
|
+
}));
|
|
10414
11020
|
const timestamp = isoNow(now);
|
|
10415
|
-
const
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
11021
|
+
const bySlot = /* @__PURE__ */ new Map();
|
|
11022
|
+
for (const { servant, result } of results) {
|
|
11023
|
+
bySlot.set(servant.slotId, result);
|
|
11024
|
+
if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: timestamp, kind: "servant-start", ownerSlotId: servant.slotId, ok: hasCapturedFuguApiText(result), model: result.model, requestId: result.requestId, responseText: result.text, error: result.error, usage: result.usage });
|
|
11025
|
+
appendServantJournal(servant.journalPath, "consultation", result.text, timestamp);
|
|
11026
|
+
}
|
|
11027
|
+
const servants = run.servants.map((servant) => servant.role === "ultra" ? seedLazyServant(servant, run, timestamp) : updateFuguApiServant(servant, bySlot.get(servant.slotId) ?? { ok: false, error: "missing Fugu API result" }, timestamp));
|
|
10420
11028
|
return { ...run, state: "active", updatedAt: timestamp, servants };
|
|
10421
11029
|
}
|
|
10422
|
-
function
|
|
10423
|
-
const
|
|
11030
|
+
async function dispatchFuguApiMessage(run, message, runFuguApi, now, retry) {
|
|
11031
|
+
const startedAt = isoNow(now);
|
|
10424
11032
|
const targets = run.servants.filter((servant) => message.target === "all" || servant.slotId === message.target || normalizeServantTarget(servant.name) === message.target);
|
|
10425
|
-
const
|
|
10426
|
-
|
|
11033
|
+
const results = await mapWithConcurrency(targets, overlordServantParallelism(), async (servant) => ({
|
|
11034
|
+
servant,
|
|
11035
|
+
result: await callFuguApiRunner(runFuguApi, run, servant, message.text, retry)
|
|
11036
|
+
}));
|
|
11037
|
+
const completedAt = isoNow(now);
|
|
11038
|
+
const successCount = results.filter(({ result }) => hasCapturedFuguApiText(result)).length;
|
|
11039
|
+
const failureCount = Math.max(0, targets.length - successCount);
|
|
11040
|
+
const state = targets.length > 0 && failureCount === 0 ? "completed" : successCount > 0 && message.target === "all" ? "partial" : "failed";
|
|
11041
|
+
const responses = results.filter(({ result }) => result.text).map(({ servant, result }) => `${servant.slotId}: ${result.text}`);
|
|
11042
|
+
const servantResults = results.map(({ servant, result }) => {
|
|
11043
|
+
if (hasCapturedFuguApiText(result)) {
|
|
11044
|
+
const warning = formatHandoffWarnings(validateServantReport(result.text ?? ""));
|
|
11045
|
+
return {
|
|
11046
|
+
slotId: servant.slotId,
|
|
11047
|
+
state: "completed",
|
|
11048
|
+
responseText: result.text?.trim(),
|
|
11049
|
+
handoffWarnings: warning ? [warning] : void 0,
|
|
11050
|
+
usage: result.usage
|
|
11051
|
+
};
|
|
11052
|
+
}
|
|
11053
|
+
return {
|
|
11054
|
+
slotId: servant.slotId,
|
|
11055
|
+
state: "failed",
|
|
11056
|
+
failureReason: result.error ?? "Fugu API servant returned no text"
|
|
11057
|
+
};
|
|
11058
|
+
});
|
|
11059
|
+
const bySlot = /* @__PURE__ */ new Map();
|
|
11060
|
+
for (const { servant, result } of results) {
|
|
11061
|
+
bySlot.set(servant.slotId, result);
|
|
11062
|
+
if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: completedAt, kind: "message-response", messageId: message.id, ownerSlotId: servant.slotId, ok: hasCapturedFuguApiText(result), model: result.model, requestId: result.requestId, responseText: result.text, error: result.error, usage: result.usage });
|
|
11063
|
+
appendServantJournal(servant.journalPath, `message ${message.id}`, result.text, completedAt);
|
|
11064
|
+
}
|
|
10427
11065
|
const nextServants = run.servants.map((servant) => {
|
|
10428
|
-
|
|
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);
|
|
11066
|
+
const result = bySlot.get(servant.slotId);
|
|
11067
|
+
return result ? updateFuguApiServant(servant, result, completedAt) : servant;
|
|
10434
11068
|
});
|
|
10435
11069
|
const nextMessage = {
|
|
10436
11070
|
...message,
|
|
10437
|
-
state
|
|
10438
|
-
startedAt
|
|
10439
|
-
completedAt:
|
|
10440
|
-
failedAt:
|
|
10441
|
-
ackText:
|
|
11071
|
+
state,
|
|
11072
|
+
startedAt,
|
|
11073
|
+
completedAt: state === "completed" || state === "partial" ? completedAt : void 0,
|
|
11074
|
+
failedAt: state === "failed" ? completedAt : void 0,
|
|
11075
|
+
ackText: state === "completed" ? "Fugu API response captured" : state === "partial" ? "Fugu API partial response captured" : void 0,
|
|
10442
11076
|
responseText: responses.join("\n"),
|
|
10443
|
-
failureReason:
|
|
10444
|
-
|
|
11077
|
+
failureReason: state === "completed" ? void 0 : state === "partial" ? `${failureCount} of ${targets.length} Fugu API servant calls failed or returned no text` : "one or more Fugu API servant calls failed or returned no text",
|
|
11078
|
+
servantResults
|
|
10445
11079
|
};
|
|
10446
11080
|
return {
|
|
10447
11081
|
...run,
|
|
10448
|
-
updatedAt:
|
|
11082
|
+
updatedAt: completedAt,
|
|
10449
11083
|
servants: nextServants,
|
|
10450
11084
|
messages: [...(run.messages ?? []).filter((m) => m.id !== message.id), nextMessage]
|
|
10451
11085
|
};
|
|
@@ -10464,38 +11098,142 @@ function hasServantTarget(run, target) {
|
|
|
10464
11098
|
(servant) => servant.slotId === normalized || normalizeServantTarget(servant.name) === normalized
|
|
10465
11099
|
);
|
|
10466
11100
|
}
|
|
10467
|
-
function messageProgress(message, now = /* @__PURE__ */ new Date(), timeoutMs = OVERLORD_HANDOFF_TIMEOUT_MS) {
|
|
11101
|
+
function messageProgress(message, now = /* @__PURE__ */ new Date(), timeoutMs = OVERLORD_HANDOFF_TIMEOUT_MS, ownerAlive) {
|
|
11102
|
+
if (message.state === "partial") return "partial";
|
|
10468
11103
|
if (message.completedAt || message.state === "completed") return "completed";
|
|
10469
11104
|
if (message.failedAt || message.state === "failed") return "failed";
|
|
10470
11105
|
const startedAt = message.startedAt ?? message.deliveredAt;
|
|
10471
11106
|
if (!startedAt) return "queued";
|
|
10472
11107
|
const elapsed = now.getTime() - new Date(startedAt).getTime();
|
|
10473
|
-
|
|
11108
|
+
if (!(Number.isFinite(elapsed) && elapsed >= timeoutMs)) return "started";
|
|
11109
|
+
return ownerAlive === false ? "orphaned" : "stalled";
|
|
11110
|
+
}
|
|
11111
|
+
function messageElapsedMs(message, now = /* @__PURE__ */ new Date()) {
|
|
11112
|
+
const startedAt = message.startedAt ?? message.deliveredAt;
|
|
11113
|
+
if (!startedAt) return void 0;
|
|
11114
|
+
const elapsed = now.getTime() - new Date(startedAt).getTime();
|
|
11115
|
+
return Number.isFinite(elapsed) && elapsed >= 0 ? elapsed : void 0;
|
|
11116
|
+
}
|
|
11117
|
+
function reconcileOverlordRun(run, ownerAlive, now = /* @__PURE__ */ new Date()) {
|
|
11118
|
+
let changed = 0;
|
|
11119
|
+
const messages = (run.messages ?? []).map((message) => {
|
|
11120
|
+
if (messageProgress(message, now, OVERLORD_HANDOFF_TIMEOUT_MS, ownerAlive) === "orphaned") {
|
|
11121
|
+
changed += 1;
|
|
11122
|
+
return {
|
|
11123
|
+
...message,
|
|
11124
|
+
state: "failed",
|
|
11125
|
+
failedAt: now.toISOString(),
|
|
11126
|
+
failureReason: message.failureReason ?? "orphaned: Overlord controller is not alive"
|
|
11127
|
+
};
|
|
11128
|
+
}
|
|
11129
|
+
return message;
|
|
11130
|
+
});
|
|
11131
|
+
return changed > 0 ? { run: { ...run, messages }, changed } : { run, changed: 0 };
|
|
10474
11132
|
}
|
|
10475
11133
|
function servantProgress(run, servant, now = /* @__PURE__ */ new Date()) {
|
|
10476
11134
|
const relevant = (run.messages ?? []).filter((message) => message.target === "all" || servant.slotId === message.target || normalizeServantTarget(servant.name) === message.target);
|
|
10477
|
-
return relevant.some((message) =>
|
|
11135
|
+
return relevant.some((message) => {
|
|
11136
|
+
const progress = messageProgress(message, now);
|
|
11137
|
+
return progress === "stalled" || progress === "orphaned";
|
|
11138
|
+
}) ? "stalled-after-delivery" : servant.state;
|
|
11139
|
+
}
|
|
11140
|
+
function humanSafeText(text, maxLength = 160) {
|
|
11141
|
+
const redacted = (text ?? "").replace(/\bBearer\s+\S+/gi, "[redacted]").replace(/\b(api[_-]?key|token|secret)=\S+/gi, "$1=[redacted]").replace(/\b(sk|sakana|mmi)[-_][A-Za-z0-9_-]{10,}\b/g, "[redacted]").replace(/\s+/g, " ").trim();
|
|
11142
|
+
if (redacted.length <= maxLength) return redacted;
|
|
11143
|
+
return `${redacted.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
11144
|
+
}
|
|
11145
|
+
function renderOverlordSendSummary(message, statePath) {
|
|
11146
|
+
const results = message?.servantResults ?? [];
|
|
11147
|
+
const completed = results.filter((result) => result.state === "completed").length;
|
|
11148
|
+
const blocked = results.filter((result) => result.state === "failed").length;
|
|
11149
|
+
const state = message?.state ?? "failed";
|
|
11150
|
+
const lines = [
|
|
11151
|
+
`Message: ${message?.id ?? "unknown"}`,
|
|
11152
|
+
`Target: ${message?.target ?? "unknown"}`,
|
|
11153
|
+
`Result: ${state} - ${completed} completed, ${blocked} blocked.`
|
|
11154
|
+
];
|
|
11155
|
+
for (const result of results.filter((item) => item.state === "failed").slice(0, 3)) {
|
|
11156
|
+
lines.push(`Blocked: ${result.slotId} - ${humanSafeText(result.failureReason ?? "no reason reported")}`);
|
|
11157
|
+
}
|
|
11158
|
+
const hidden = Math.max(0, blocked - 3);
|
|
11159
|
+
if (hidden > 0) lines.push(`Blocked: +${hidden} more`);
|
|
11160
|
+
lines.push(`State: ${statePath}`);
|
|
11161
|
+
return lines.join("\n");
|
|
10478
11162
|
}
|
|
10479
|
-
function
|
|
10480
|
-
|
|
10481
|
-
|
|
11163
|
+
function countValues(values, value) {
|
|
11164
|
+
return values.filter((item) => item === value).length;
|
|
11165
|
+
}
|
|
11166
|
+
function latestServantFailureReason(run, slotId) {
|
|
11167
|
+
for (const message of [...run.messages ?? []].reverse()) {
|
|
11168
|
+
const result = message.servantResults?.find((item) => item.slotId === slotId && item.state === "failed");
|
|
11169
|
+
if (result?.failureReason?.trim()) return result.failureReason;
|
|
11170
|
+
}
|
|
11171
|
+
return void 0;
|
|
11172
|
+
}
|
|
11173
|
+
function affectedServantStatusLabel(state, reason) {
|
|
11174
|
+
if (state === "stalled-after-delivery") return "Stalled";
|
|
11175
|
+
if (/\btimed?\s*out\b/i.test(reason ?? "")) return "Timed out";
|
|
11176
|
+
return "Blocked";
|
|
11177
|
+
}
|
|
11178
|
+
function renderOverlordStatus(summary, run, now = /* @__PURE__ */ new Date()) {
|
|
11179
|
+
const servantStates = summary.servants.map((servant) => servant.state);
|
|
11180
|
+
const messageStates = (run.messages ?? []).map((message) => messageProgress(message, now));
|
|
11181
|
+
const activeMessages = messageStates.filter((state) => state === "queued" || state === "started" || state === "stalled").length;
|
|
11182
|
+
const blockedServants = summary.servants.filter((servant) => servant.state === "blocked" || servant.state === "stalled-after-delivery");
|
|
11183
|
+
const latest = (run.messages ?? []).at(-1);
|
|
11184
|
+
const latestState = latest ? messageProgress(latest, now) : void 0;
|
|
10482
11185
|
return [
|
|
10483
11186
|
`Overlord run ${summary.runId}`,
|
|
10484
11187
|
`State: ${summary.state}`,
|
|
10485
|
-
`
|
|
10486
|
-
`
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
11188
|
+
`Mission: ${humanSafeText(run.task || "not provided yet")}`,
|
|
11189
|
+
`Servants: ${countValues(servantStates, "ready")} ready, ${countValues(servantStates, "blocked") + countValues(servantStates, "stalled-after-delivery")} blocked, ${countValues(servantStates, "planned")} planned.`,
|
|
11190
|
+
`Messages: ${countValues(messageStates, "completed")} completed, ${countValues(messageStates, "partial")} partial, ${countValues(messageStates, "failed")} failed, ${activeMessages} active.`,
|
|
11191
|
+
...blockedServants.slice(0, 3).map((servant) => {
|
|
11192
|
+
const slotId = run.servants.find((item) => item.name === servant.name)?.slotId;
|
|
11193
|
+
const reason = slotId ? latestServantFailureReason(run, slotId) : void 0;
|
|
11194
|
+
const label = affectedServantStatusLabel(servant.state, reason);
|
|
11195
|
+
const detail = reason ?? (servant.state === "stalled-after-delivery" ? "no recent update" : void 0);
|
|
11196
|
+
return `${label}: ${servant.name}${detail ? ` - ${humanSafeText(detail)}` : ""}`;
|
|
11197
|
+
}),
|
|
11198
|
+
...blockedServants.length > 3 ? [`Blocked: +${blockedServants.length - 3} more`] : [],
|
|
11199
|
+
...latest && latestState ? [`Latest: ${latest.id} ${latest.target} ${latestState}${latest.failureReason ? ` - ${humanSafeText(latest.failureReason)}` : ""}`] : [],
|
|
11200
|
+
`Coordinator: ${summary.controller}`
|
|
10490
11201
|
].join("\n");
|
|
10491
11202
|
}
|
|
10492
11203
|
function usefulJournalLines(text) {
|
|
10493
11204
|
return text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !/^fugu\s/i.test(line) && !/^›/.test(line) && !/^\[overlord\] launched/.test(line)).slice(-20);
|
|
10494
11205
|
}
|
|
11206
|
+
var HANDOFF_SIGNAL = /\b(handoff|evidence|recommend(?:ed|ation|s)?|improvement|suggest(?:ion|s)?|next action|proposal|file)\b|`[^`]+`/i;
|
|
11207
|
+
var HANDOFF_MIN_RESPONSE_CHARS = 12;
|
|
11208
|
+
function parseJournalBlocks(raw) {
|
|
11209
|
+
const blocks = [];
|
|
11210
|
+
let current = null;
|
|
11211
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
11212
|
+
const header = line.match(/^\[[^\]]+\]\s+(.+)$/);
|
|
11213
|
+
if (header) {
|
|
11214
|
+
if (current) blocks.push(current);
|
|
11215
|
+
current = { label: header[1].trim(), body: "" };
|
|
11216
|
+
} else if (current) {
|
|
11217
|
+
current.body += (current.body ? "\n" : "") + line;
|
|
11218
|
+
}
|
|
11219
|
+
}
|
|
11220
|
+
if (current) blocks.push(current);
|
|
11221
|
+
return blocks;
|
|
11222
|
+
}
|
|
11223
|
+
function journalHasHandoff(raw, lines) {
|
|
11224
|
+
for (const block of parseJournalBlocks(raw)) {
|
|
11225
|
+
if (!block.label.startsWith("message ")) continue;
|
|
11226
|
+
const body = block.body.trim();
|
|
11227
|
+
if (body.length >= HANDOFF_MIN_RESPONSE_CHARS) return true;
|
|
11228
|
+
if (validateServantReport(body).applies) return true;
|
|
11229
|
+
}
|
|
11230
|
+
return lines.some((line) => HANDOFF_SIGNAL.test(line));
|
|
11231
|
+
}
|
|
10495
11232
|
function servantJournalSummary(servant) {
|
|
10496
11233
|
try {
|
|
10497
|
-
const
|
|
10498
|
-
|
|
11234
|
+
const raw = (0, import_node_fs15.readFileSync)(servant.journalPath, "utf8");
|
|
11235
|
+
const lines = usefulJournalLines(raw);
|
|
11236
|
+
return { lines, hasHandoff: journalHasHandoff(raw, lines) };
|
|
10499
11237
|
} catch {
|
|
10500
11238
|
return { lines: [], hasHandoff: false };
|
|
10501
11239
|
}
|
|
@@ -10519,7 +11257,11 @@ function runJson(run, extra = {}) {
|
|
|
10519
11257
|
pid: servant.pid,
|
|
10520
11258
|
journalPath: servant.journalPath,
|
|
10521
11259
|
engine: servant.engine,
|
|
10522
|
-
opencodeSessionId: servant.opencodeSessionId
|
|
11260
|
+
opencodeSessionId: servant.opencodeSessionId,
|
|
11261
|
+
scopeToken: servant.scopeToken,
|
|
11262
|
+
llmModel: servant.llmModel,
|
|
11263
|
+
llmRequestId: servant.llmRequestId,
|
|
11264
|
+
llmConversationLength: servant.llmMessages?.length
|
|
10523
11265
|
})),
|
|
10524
11266
|
...extra
|
|
10525
11267
|
};
|
|
@@ -10530,27 +11272,33 @@ function wantsJson(options, command) {
|
|
|
10530
11272
|
function countArgsFromOptions(options) {
|
|
10531
11273
|
return ["3", "4", "5", "6"].filter((key) => options[key]).map((key) => `--${key}`);
|
|
10532
11274
|
}
|
|
11275
|
+
function percentile(values, p) {
|
|
11276
|
+
if (values.length === 0) return 0;
|
|
11277
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
11278
|
+
const rank = Math.ceil(p / 100 * sorted.length);
|
|
11279
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
|
|
11280
|
+
}
|
|
10533
11281
|
function registerOverlordCommands(program3, deps = {}) {
|
|
10534
11282
|
const out = deps.out ?? ((text) => process.stdout.write(text));
|
|
10535
11283
|
const err = deps.err ?? ((text) => process.stderr.write(text));
|
|
10536
11284
|
const cwd = deps.cwd ?? (() => process.cwd());
|
|
10537
11285
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
10538
|
-
const
|
|
10539
|
-
const
|
|
10540
|
-
const
|
|
10541
|
-
const runOpenCode = deps.runOpenCode ?? defaultRunOpenCode;
|
|
11286
|
+
const fuguApiPreflight = deps.fuguApiPreflight ?? collectFuguApiPreflight;
|
|
11287
|
+
const runFuguApi = deps.runFuguApi ?? defaultRunFuguApi;
|
|
11288
|
+
const fuguApiRetry = fuguApiRetryConfig(deps.sleep ?? defaultSleep3);
|
|
10542
11289
|
const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
|
|
10543
11290
|
const killPid = deps.killPid ?? defaultKillPid;
|
|
10544
|
-
const overlord = program3.command("overlord").description("coordinate one Ultra and normal Fugu servants for hard org work").allowUnknownOption(true).argument("[task...]", "task for the Overlord system").option("--3", "run one Ultra and two normal Fugus").option("--4", "run one Ultra and three normal Fugus").option("--5", "run one Ultra and four normal Fugus").option("--6", "run one Ultra and five normal Fugus").option("--engine <engine>", "servant engine:
|
|
11291
|
+
const overlord = program3.command("overlord").description("coordinate one Ultra and normal Fugu servants for hard org work").allowUnknownOption(true).argument("[task...]", "task for the Overlord system").option("--3", "run one Ultra and two normal Fugus").option("--4", "run one Ultra and three normal Fugus").option("--5", "run one Ultra and four normal Fugus").option("--6", "run one Ultra and five normal Fugus").option("--engine <engine>", "servant engine: fugu-api", OVERLORD_DEFAULT_ENGINE).option("--json", "print machine-readable output").action(async (task = [], options) => {
|
|
10545
11292
|
try {
|
|
10546
11293
|
const args = [...countArgsFromOptions(options), ...task];
|
|
10547
11294
|
const root = cwd();
|
|
10548
11295
|
const plan2 = buildOverlordStartupPlan(args, root);
|
|
10549
|
-
const engine = `${options.engine ??
|
|
10550
|
-
if (engine !== "
|
|
10551
|
-
|
|
11296
|
+
const engine = `${options.engine ?? OVERLORD_DEFAULT_ENGINE}`;
|
|
11297
|
+
if (engine !== "fugu-api") throw new Error("--engine must be fugu-api");
|
|
11298
|
+
if (!options.json) out("Starting Overlord...\n");
|
|
11299
|
+
const preflightReport = await fuguApiPreflight();
|
|
10552
11300
|
if (!preflightReport.ok) {
|
|
10553
|
-
err(`${
|
|
11301
|
+
err(`${renderOverlordPreflightFailure(preflightReport)}
|
|
10554
11302
|
`);
|
|
10555
11303
|
process.exitCode = 1;
|
|
10556
11304
|
return;
|
|
@@ -10570,30 +11318,20 @@ function registerOverlordCommands(program3, deps = {}) {
|
|
|
10570
11318
|
runToken: deps.runToken
|
|
10571
11319
|
});
|
|
10572
11320
|
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
10573
|
-
if (
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
run = recordOverlordHeartbeat(run, {
|
|
10580
|
-
controllerPid: controller.pid,
|
|
10581
|
-
fingerprint: controller.fingerprint,
|
|
10582
|
-
now
|
|
10583
|
-
});
|
|
10584
|
-
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
10585
|
-
}
|
|
10586
|
-
}
|
|
11321
|
+
if (!options.json) out(`Spawning ${run.servants.length} Fugu (1 ultra, ${run.servants.length - 1} normal)...
|
|
11322
|
+
`);
|
|
11323
|
+
run = markServantsStarting(run, isoNow(now));
|
|
11324
|
+
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
11325
|
+
run = await startFuguApiServants(run, runFuguApi, now, fuguApiRetry);
|
|
11326
|
+
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
10587
11327
|
if (options.json) out(`${JSON.stringify(runJson(run, { nextPhase: "consult-servants" }), null, 2)}
|
|
10588
11328
|
`);
|
|
10589
11329
|
else {
|
|
11330
|
+
const ready = run.servants.filter((servant) => servant.state === "ready").length;
|
|
10590
11331
|
out(`${[
|
|
10591
|
-
|
|
10592
|
-
`
|
|
10593
|
-
`Run: ${run.runId}
|
|
10594
|
-
`Servants: ${run.servants.length} total`,
|
|
10595
|
-
`Controller: ${run.controllerPid ? `started (pid ${run.controllerPid})` : "launch requested"}`,
|
|
10596
|
-
"Next: consult servants, interview the human, then print an approved todo list."
|
|
11332
|
+
`Ready: ${ready}/${run.servants.length}.`,
|
|
11333
|
+
`Mission: ${humanSafeText(run.task || "not provided yet")}`,
|
|
11334
|
+
`Run: ${run.runId}`
|
|
10597
11335
|
].join("\n")}
|
|
10598
11336
|
`);
|
|
10599
11337
|
}
|
|
@@ -10606,7 +11344,7 @@ function registerOverlordCommands(program3, deps = {}) {
|
|
|
10606
11344
|
process.exitCode = 1;
|
|
10607
11345
|
}
|
|
10608
11346
|
});
|
|
10609
|
-
overlord.command("send").description("queue an assignment or redirect for
|
|
11347
|
+
overlord.command("send").description("queue an assignment or redirect for active Overlord servant conversations").argument("<target>", "servant slot id/name, or all").argument("[message...]", "message to deliver to the servant session").option("--message-file <path>", "read the message from a UTF-8 file, or - for stdin (prefer a path; #1511)").option("--json", "print machine-readable output").action(async (target, message = [], options, command) => {
|
|
10610
11348
|
const json = wantsJson(options, command);
|
|
10611
11349
|
try {
|
|
10612
11350
|
const statePath = defaultOverlordStatePath(cwd());
|
|
@@ -10615,8 +11353,9 @@ function registerOverlordCommands(program3, deps = {}) {
|
|
|
10615
11353
|
if (!run) throw new Error("no active Overlord run found");
|
|
10616
11354
|
const normalized = normalizeServantTarget(target);
|
|
10617
11355
|
if (!hasServantTarget(run, normalized)) throw new Error(`unknown Overlord servant target: ${target}`);
|
|
10618
|
-
const text = message.
|
|
10619
|
-
if (!text) throw new Error("message is required");
|
|
11356
|
+
const text = resolveOverlordSendMessage(message, typeof options.messageFile === "string" ? options.messageFile : void 0);
|
|
11357
|
+
if (!text) throw new Error("message is required (pass it inline or via --message-file)");
|
|
11358
|
+
if (run.engine !== "fugu-api") throw new Error("active Overlord run does not use the fugu-api engine");
|
|
10620
11359
|
const timestamp = isoNow(now);
|
|
10621
11360
|
const queued = {
|
|
10622
11361
|
id: deps.runId?.() ?? defaultMessageId(),
|
|
@@ -10626,35 +11365,26 @@ function registerOverlordCommands(program3, deps = {}) {
|
|
|
10626
11365
|
queuedAt: timestamp,
|
|
10627
11366
|
state: "queued"
|
|
10628
11367
|
};
|
|
10629
|
-
if (
|
|
10630
|
-
|
|
10631
|
-
writeOverlordRegistry(statePath, { ...registry2, runs: { ...registry2.runs, [run.runId]: dispatched } });
|
|
10632
|
-
const settled = (dispatched.messages ?? []).find((m) => m.id === queued.id);
|
|
10633
|
-
const ok = settled?.state === "completed";
|
|
10634
|
-
const payload2 = { ok, runId: run.runId, target: normalized, messageId: queued.id, state: settled?.state, responseText: settled?.responseText, statePath };
|
|
10635
|
-
if (json) out(`${JSON.stringify(payload2, null, 2)}
|
|
11368
|
+
if (!json) out(`Sending assignment to ${normalized}...
|
|
11369
|
+
Awaiting Fugu responses...
|
|
10636
11370
|
`);
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
if (!ok) process.exitCode = 1;
|
|
10641
|
-
return;
|
|
10642
|
-
}
|
|
10643
|
-
const next = {
|
|
10644
|
-
...run,
|
|
11371
|
+
const startedMessage = { ...queued, state: "started", startedAt: timestamp };
|
|
11372
|
+
const started = persistRunMutation(statePath, run.runId, (fresh) => ({
|
|
11373
|
+
...fresh,
|
|
10645
11374
|
updatedAt: timestamp,
|
|
10646
|
-
messages: [...
|
|
10647
|
-
};
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
const
|
|
11375
|
+
messages: [...(fresh.messages ?? []).filter((m) => m.id !== queued.id), startedMessage]
|
|
11376
|
+
}));
|
|
11377
|
+
const dispatched = await dispatchFuguApiMessage(started, queued, runFuguApi, now, fuguApiRetry);
|
|
11378
|
+
const settled = (dispatched.messages ?? []).find((m) => m.id === queued.id);
|
|
11379
|
+
const affected = computeAffectedSlots(dispatched.servants, normalized, settled);
|
|
11380
|
+
persistRunMutation(statePath, run.runId, (fresh) => mergeConcurrentDispatch(fresh, dispatched, settled, affected, queued.id));
|
|
11381
|
+
const ok = settled?.state === "completed" || settled?.state === "partial";
|
|
11382
|
+
const payload = { ok, runId: run.runId, target: normalized, messageId: queued.id, state: settled?.state, responseText: settled?.responseText, failureReason: settled?.failureReason, servantResults: settled?.servantResults, statePath };
|
|
10653
11383
|
if (json) out(`${JSON.stringify(payload, null, 2)}
|
|
10654
11384
|
`);
|
|
10655
|
-
else out(
|
|
10656
|
-
State: ${statePath}
|
|
11385
|
+
else out(`${renderOverlordSendSummary(settled, statePath)}
|
|
10657
11386
|
`);
|
|
11387
|
+
if (!ok) process.exitCode = 1;
|
|
10658
11388
|
} catch (e) {
|
|
10659
11389
|
const messageText = e instanceof Error ? e.message : String(e);
|
|
10660
11390
|
if (json) out(`${JSON.stringify({ ok: false, error: messageText }, null, 2)}
|
|
@@ -10680,10 +11410,13 @@ State: ${payload2.statePath}
|
|
|
10680
11410
|
}
|
|
10681
11411
|
const summary = summarizeOverlordRun(run, { isPidAlive, now });
|
|
10682
11412
|
const current = now();
|
|
11413
|
+
const ownerAlive = summary.controller !== "lost";
|
|
11414
|
+
const { run: reconciledRun, changed } = reconcileOverlordRun(run, ownerAlive, current);
|
|
11415
|
+
if (changed > 0) persistRunMutation(statePath, run.runId, (fresh) => reconcileOverlordRun(fresh, ownerAlive, current).run);
|
|
10683
11416
|
const payload = {
|
|
10684
11417
|
...summary,
|
|
10685
11418
|
statePath,
|
|
10686
|
-
task:
|
|
11419
|
+
task: reconciledRun.task,
|
|
10687
11420
|
engine: run.engine,
|
|
10688
11421
|
ledgerPath: run.ledgerPath,
|
|
10689
11422
|
sessions: run.servants.map((servant) => ({
|
|
@@ -10691,14 +11424,20 @@ State: ${payload2.statePath}
|
|
|
10691
11424
|
slotId: servant.slotId,
|
|
10692
11425
|
engine: servant.engine,
|
|
10693
11426
|
opencodeSessionId: servant.opencodeSessionId,
|
|
11427
|
+
scopeToken: servant.scopeToken,
|
|
11428
|
+
llmModel: servant.llmModel,
|
|
11429
|
+
llmRequestId: servant.llmRequestId,
|
|
11430
|
+
llmConversationLength: servant.llmMessages?.length,
|
|
10694
11431
|
eventJournalPath: servant.eventJournalPath,
|
|
10695
11432
|
lastEventAt: servant.lastEventAt,
|
|
10696
11433
|
lastMessageCompletedAt: servant.lastMessageCompletedAt
|
|
10697
11434
|
})),
|
|
10698
|
-
messages: (
|
|
11435
|
+
messages: (reconciledRun.messages ?? []).map((message) => ({
|
|
10699
11436
|
id: message.id,
|
|
10700
11437
|
target: message.target,
|
|
10701
|
-
state: messageProgress(message, current),
|
|
11438
|
+
state: messageProgress(message, current, OVERLORD_HANDOFF_TIMEOUT_MS, ownerAlive),
|
|
11439
|
+
elapsedMs: messageElapsedMs(message, current),
|
|
11440
|
+
boundMs: OVERLORD_HANDOFF_TIMEOUT_MS,
|
|
10702
11441
|
queuedAt: message.queuedAt ?? message.createdAt,
|
|
10703
11442
|
startedAt: message.startedAt ?? message.deliveredAt,
|
|
10704
11443
|
completedAt: message.completedAt,
|
|
@@ -10711,7 +11450,7 @@ State: ${payload2.statePath}
|
|
|
10711
11450
|
};
|
|
10712
11451
|
if (json) out(`${JSON.stringify(payload, null, 2)}
|
|
10713
11452
|
`);
|
|
10714
|
-
else out(`${renderOverlordStatus(summary,
|
|
11453
|
+
else out(`${renderOverlordStatus(summary, reconciledRun, current)}
|
|
10715
11454
|
State: ${statePath}
|
|
10716
11455
|
`);
|
|
10717
11456
|
});
|
|
@@ -10794,17 +11533,128 @@ State: ${payload2.statePath}
|
|
|
10794
11533
|
`);
|
|
10795
11534
|
}
|
|
10796
11535
|
});
|
|
11536
|
+
overlord.command("canary").description("native Fugu API smoke: summon a small pool, verify a bounded response, stop cleanly").option("--3", "one Ultra and two normal Fugus").option("--4", "one Ultra and three normal Fugus").option("--5", "one Ultra and four normal Fugus").option("--6", "one Ultra and five normal Fugus").option("--json", "print machine-readable output").option("--repeat <n>", "run the smoke N times and report p50/p90 elapsed latency", "1").action(async (options, command) => {
|
|
11537
|
+
const json = wantsJson(options, command);
|
|
11538
|
+
const repeat = Math.min(Math.max(1, Math.trunc(Number(options.repeat) || 1)), 20);
|
|
11539
|
+
const countArgs = countArgsFromOptions(options);
|
|
11540
|
+
try {
|
|
11541
|
+
if (!json) out("Overlord canary starting.\nChecking Fugu API...\n");
|
|
11542
|
+
const preflight2 = await fuguApiPreflight();
|
|
11543
|
+
if (!preflight2.ok) {
|
|
11544
|
+
if (json) out(`${JSON.stringify({ ok: false, phase: "preflight", problems: preflight2.problems }, null, 2)}
|
|
11545
|
+
`);
|
|
11546
|
+
else err(`${renderOverlordPreflightFailure(preflight2)}
|
|
11547
|
+
`);
|
|
11548
|
+
process.exitCode = 1;
|
|
11549
|
+
return;
|
|
11550
|
+
}
|
|
11551
|
+
const runOnce = async (verbose) => {
|
|
11552
|
+
const startedAtMs = now().getTime();
|
|
11553
|
+
const plan2 = buildOverlordStartupPlan(countArgs, cwd());
|
|
11554
|
+
let run = buildOverlordRun({ task: "overlord canary smoke", cwd: cwd(), count: plan2.count, engine: "fugu-api", now, runId: deps.runId, runToken: deps.runToken });
|
|
11555
|
+
if (verbose) out(`Summoning ${run.servants.length} Fugu servants...
|
|
11556
|
+
`);
|
|
11557
|
+
run = markServantsStarting(run, isoNow(now));
|
|
11558
|
+
run = await startFuguApiServants(run, runFuguApi, now, fuguApiRetry);
|
|
11559
|
+
const ready = run.servants.filter((servant) => servant.state === "ready").length;
|
|
11560
|
+
const problems = [];
|
|
11561
|
+
let responded = 0;
|
|
11562
|
+
let evidence;
|
|
11563
|
+
if (ready === run.servants.length) {
|
|
11564
|
+
if (verbose) out("Sending canary probe...\n");
|
|
11565
|
+
const timestamp = isoNow(now);
|
|
11566
|
+
const probe = {
|
|
11567
|
+
id: `${deps.runId?.() ?? defaultMessageId()}-canary`,
|
|
11568
|
+
target: "all",
|
|
11569
|
+
text: "Reply with the single word READY and nothing else.",
|
|
11570
|
+
createdAt: timestamp,
|
|
11571
|
+
queuedAt: timestamp,
|
|
11572
|
+
state: "queued"
|
|
11573
|
+
};
|
|
11574
|
+
run = await dispatchFuguApiMessage(run, probe, runFuguApi, now, fuguApiRetry);
|
|
11575
|
+
const settled = (run.messages ?? []).find((message) => message.id === probe.id);
|
|
11576
|
+
const results = settled?.servantResults ?? [];
|
|
11577
|
+
responded = results.filter((result) => result.state === "completed" && result.responseText?.trim()).length;
|
|
11578
|
+
evidence = results.find((result) => result.responseText?.trim())?.responseText?.trim();
|
|
11579
|
+
if (responded !== run.servants.length) problems.push(`only ${responded}/${run.servants.length} servants returned text`);
|
|
11580
|
+
} else {
|
|
11581
|
+
problems.push(`only ${ready}/${run.servants.length} servants became ready`);
|
|
11582
|
+
}
|
|
11583
|
+
const stopPlan = planOverlordRunStop(run);
|
|
11584
|
+
for (const pid of stopPlan.killPids) {
|
|
11585
|
+
try {
|
|
11586
|
+
killPid(pid);
|
|
11587
|
+
} catch {
|
|
11588
|
+
}
|
|
11589
|
+
}
|
|
11590
|
+
const cleanup = { stopped: stopPlan.killPids.length, uncertain: stopPlan.uncertain.length };
|
|
11591
|
+
if (cleanup.uncertain > 0) problems.push(`${cleanup.uncertain} uncertain resource(s) left untouched`);
|
|
11592
|
+
return { ok: problems.length === 0, runId: run.runId, requested: run.servants.length, ready, responded, elapsedMs: Math.max(0, now().getTime() - startedAtMs), evidence, cleanup, problems };
|
|
11593
|
+
};
|
|
11594
|
+
const runs = [];
|
|
11595
|
+
for (let i = 0; i < repeat; i += 1) {
|
|
11596
|
+
if (!json && repeat > 1) out(`Canary run ${i + 1}/${repeat}...
|
|
11597
|
+
`);
|
|
11598
|
+
runs.push(await runOnce(!json && repeat === 1));
|
|
11599
|
+
}
|
|
11600
|
+
if (repeat === 1) {
|
|
11601
|
+
const r = runs[0];
|
|
11602
|
+
if (json) out(`${JSON.stringify({ ok: r.ok, runId: r.runId, requested: r.requested, ready: r.ready, responded: r.responded, elapsedMs: r.elapsedMs, evidence: r.evidence, cleanup: r.cleanup, problems: r.problems }, null, 2)}
|
|
11603
|
+
`);
|
|
11604
|
+
else out(`${[
|
|
11605
|
+
r.ok ? "Canary OK." : "Canary FAILED.",
|
|
11606
|
+
`Ready: ${r.ready}/${r.requested}; Responded: ${r.responded}/${r.requested}`,
|
|
11607
|
+
`Cleanup: stopped ${r.cleanup.stopped}, uncertain ${r.cleanup.uncertain}`,
|
|
11608
|
+
...r.problems.length ? [`Problems: ${r.problems.join("; ")}`] : []
|
|
11609
|
+
].join("\n")}
|
|
11610
|
+
`);
|
|
11611
|
+
if (!r.ok) process.exitCode = 1;
|
|
11612
|
+
return;
|
|
11613
|
+
}
|
|
11614
|
+
const elapsed = runs.map((r) => r.elapsedMs);
|
|
11615
|
+
const passes = runs.filter((r) => r.ok).length;
|
|
11616
|
+
const requested = runs[0].requested;
|
|
11617
|
+
const ok = passes === runs.length;
|
|
11618
|
+
const summary = {
|
|
11619
|
+
ok,
|
|
11620
|
+
repeat,
|
|
11621
|
+
passes,
|
|
11622
|
+
requested,
|
|
11623
|
+
elapsedMs: { p50: percentile(elapsed, 50), p90: percentile(elapsed, 90), min: Math.min(...elapsed), max: Math.max(...elapsed) },
|
|
11624
|
+
readyRate: runs.reduce((sum, r) => sum + r.ready, 0) / (runs.length * requested),
|
|
11625
|
+
respondedRate: runs.reduce((sum, r) => sum + r.responded, 0) / (runs.length * requested),
|
|
11626
|
+
problems: runs.flatMap((r, i) => r.problems.map((problem) => `run ${i + 1}: ${problem}`))
|
|
11627
|
+
};
|
|
11628
|
+
if (json) out(`${JSON.stringify(summary, null, 2)}
|
|
11629
|
+
`);
|
|
11630
|
+
else out(`${[
|
|
11631
|
+
ok ? `Canary bench OK (${passes}/${repeat} passed).` : `Canary bench: ${passes}/${repeat} passed.`,
|
|
11632
|
+
`Elapsed p50 ${summary.elapsedMs.p50}ms, p90 ${summary.elapsedMs.p90}ms (min ${summary.elapsedMs.min}, max ${summary.elapsedMs.max})`,
|
|
11633
|
+
`Ready rate ${(summary.readyRate * 100).toFixed(0)}%, responded rate ${(summary.respondedRate * 100).toFixed(0)}%`,
|
|
11634
|
+
...summary.problems.length ? [`Problems: ${summary.problems.join("; ")}`] : []
|
|
11635
|
+
].join("\n")}
|
|
11636
|
+
`);
|
|
11637
|
+
if (!ok) process.exitCode = 1;
|
|
11638
|
+
} catch (e) {
|
|
11639
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
11640
|
+
if (json) out(`${JSON.stringify({ ok: false, error: message }, null, 2)}
|
|
11641
|
+
`);
|
|
11642
|
+
else err(`overlord canary: ${message}
|
|
11643
|
+
`);
|
|
11644
|
+
process.exitCode = 1;
|
|
11645
|
+
}
|
|
11646
|
+
});
|
|
10797
11647
|
return overlord;
|
|
10798
11648
|
}
|
|
10799
11649
|
|
|
10800
11650
|
// src/throttle-commands.ts
|
|
10801
|
-
var
|
|
11651
|
+
var import_node_child_process8 = require("node:child_process");
|
|
10802
11652
|
var import_node_fs16 = require("node:fs");
|
|
10803
11653
|
var import_node_path14 = require("node:path");
|
|
10804
11654
|
var THROTTLE_TRACE_REL = (0, import_node_path14.join)(".mmi", "throttle", "trace.jsonl");
|
|
10805
11655
|
function resolveRepoGitRoot(cwd = process.cwd()) {
|
|
10806
11656
|
try {
|
|
10807
|
-
const root = (0,
|
|
11657
|
+
const root = (0, import_node_child_process8.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
10808
11658
|
encoding: "utf8",
|
|
10809
11659
|
timeout: 5e3
|
|
10810
11660
|
}).trim();
|
|
@@ -10953,7 +11803,7 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
|
10953
11803
|
}
|
|
10954
11804
|
|
|
10955
11805
|
// src/board.ts
|
|
10956
|
-
var
|
|
11806
|
+
var import_node_child_process9 = require("node:child_process");
|
|
10957
11807
|
var import_node_util6 = require("node:util");
|
|
10958
11808
|
|
|
10959
11809
|
// src/board-priority.ts
|
|
@@ -11061,7 +11911,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
|
11061
11911
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
11062
11912
|
|
|
11063
11913
|
// src/board.ts
|
|
11064
|
-
var rawExecFileP3 = (0, import_node_util6.promisify)(
|
|
11914
|
+
var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process9.execFile);
|
|
11065
11915
|
var BOARD_GIT_TIMEOUT_MS = 1e4;
|
|
11066
11916
|
var WRITE_PROBE_CONCURRENCY = 8;
|
|
11067
11917
|
var CLAIM_CONCURRENCY = 5;
|
|
@@ -12524,6 +13374,8 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
12524
13374
|
// Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are S3-synced
|
|
12525
13375
|
// via `mmi-cli northstar push` / auto-save on write; never git-tracked (AGENTS.md "Repo cleanliness", #1550, #1842).
|
|
12526
13376
|
"**/plans/",
|
|
13377
|
+
// Superpowers plan/spec output is scratch too; authoritative docs live directly under docs/.
|
|
13378
|
+
"docs/superpowers/",
|
|
12527
13379
|
".playwright-mcp/",
|
|
12528
13380
|
".claude/worktrees/",
|
|
12529
13381
|
// .mmi is agent/CI scratch — ignore the WHOLE tree at any depth (root + cli/.mmi, .github/workflows/.mmi,
|
|
@@ -13471,9 +14323,9 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
|
|
|
13471
14323
|
|
|
13472
14324
|
// src/gh-create.ts
|
|
13473
14325
|
var import_promises5 = require("node:fs/promises");
|
|
13474
|
-
var
|
|
14326
|
+
var import_node_os3 = require("node:os");
|
|
13475
14327
|
var import_node_path17 = require("node:path");
|
|
13476
|
-
var
|
|
14328
|
+
var import_node_crypto6 = require("node:crypto");
|
|
13477
14329
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
13478
14330
|
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
13479
14331
|
function timeoutKillNote(err, timeoutMs) {
|
|
@@ -13513,7 +14365,7 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
13513
14365
|
} };
|
|
13514
14366
|
const write = deps.write ?? import_promises5.writeFile;
|
|
13515
14367
|
const remove = deps.remove ?? import_promises5.unlink;
|
|
13516
|
-
const file = (0, import_node_path17.join)(deps.dir ?? (0,
|
|
14368
|
+
const file = (0, import_node_path17.join)(deps.dir ?? (0, import_node_os3.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.md`);
|
|
13517
14369
|
await write(file, args[i + 1], "utf8");
|
|
13518
14370
|
return {
|
|
13519
14371
|
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
@@ -14726,7 +15578,7 @@ function designSystemSnapshot(root) {
|
|
|
14726
15578
|
}
|
|
14727
15579
|
|
|
14728
15580
|
// src/design-system-registry.ts
|
|
14729
|
-
var
|
|
15581
|
+
var import_node_crypto7 = require("node:crypto");
|
|
14730
15582
|
var import_node_fs20 = require("node:fs");
|
|
14731
15583
|
var import_node_path19 = require("node:path");
|
|
14732
15584
|
var DESIGN_SYSTEM_CACHE_DIR = ".mmi/design-system/components";
|
|
@@ -14789,7 +15641,7 @@ function scanCachedComponentNames(cacheDir) {
|
|
|
14789
15641
|
return [...names].sort();
|
|
14790
15642
|
}
|
|
14791
15643
|
function contentHash(content) {
|
|
14792
|
-
return (0,
|
|
15644
|
+
return (0, import_node_crypto7.createHash)("sha256").update(content, "utf8").digest("hex");
|
|
14793
15645
|
}
|
|
14794
15646
|
function buildRegistryComponentsCheck(input) {
|
|
14795
15647
|
const base = {
|
|
@@ -14949,6 +15801,17 @@ function renderVerifySecrets(body) {
|
|
|
14949
15801
|
if (ssmStatus !== "Success") {
|
|
14950
15802
|
return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
|
|
14951
15803
|
}
|
|
15804
|
+
if (secrets.length === 0) {
|
|
15805
|
+
const raw = (body?.raw ?? "").trim();
|
|
15806
|
+
if (/no required runtime secrets declared/i.test(raw)) {
|
|
15807
|
+
return { lines: [raw], failure: null };
|
|
15808
|
+
}
|
|
15809
|
+
const detail = raw ? ` \u2014 on-box output: ${raw.replace(/\s+/g, " ").trim()}` : "";
|
|
15810
|
+
return {
|
|
15811
|
+
lines: raw ? raw.split("\n") : ["verify-secrets: no per-key verdicts returned"],
|
|
15812
|
+
failure: `verify-secrets returned no per-key verdicts, so nothing was verified${detail}`
|
|
15813
|
+
};
|
|
15814
|
+
}
|
|
14952
15815
|
const bad = counts.mismatch + counts.missing;
|
|
14953
15816
|
if (bad > 0) {
|
|
14954
15817
|
return { lines, failure: `${bad} of ${secrets.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
|
|
@@ -14957,12 +15820,12 @@ function renderVerifySecrets(body) {
|
|
|
14957
15820
|
}
|
|
14958
15821
|
|
|
14959
15822
|
// src/hotfix-coverage.ts
|
|
14960
|
-
var
|
|
15823
|
+
var import_node_child_process10 = require("node:child_process");
|
|
14961
15824
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
14962
15825
|
function checkHotfixCoverage(options = {}) {
|
|
14963
15826
|
const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
|
|
14964
15827
|
const ack = (options.ack ?? []).filter(Boolean);
|
|
14965
|
-
const git = options.git ?? ((args, opts) => (0,
|
|
15828
|
+
const git = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
14966
15829
|
const revList = (range) => {
|
|
14967
15830
|
const out = git(["rev-list", "--no-merges", range]).trim();
|
|
14968
15831
|
return out ? out.split("\n") : [];
|
|
@@ -17663,14 +18526,14 @@ function parseKbTree(stdout, prefix) {
|
|
|
17663
18526
|
|
|
17664
18527
|
// src/northstar-commands.ts
|
|
17665
18528
|
var import_node_fs22 = require("node:fs");
|
|
17666
|
-
var
|
|
18529
|
+
var import_node_child_process11 = require("node:child_process");
|
|
17667
18530
|
var import_promises6 = require("node:fs/promises");
|
|
17668
18531
|
var planSyncDetached = false;
|
|
17669
18532
|
function detachPlanSync() {
|
|
17670
18533
|
if (planSyncDetached) return;
|
|
17671
18534
|
planSyncDetached = true;
|
|
17672
18535
|
try {
|
|
17673
|
-
(0,
|
|
18536
|
+
(0, import_node_child_process11.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
|
|
17674
18537
|
detached: true,
|
|
17675
18538
|
stdio: "ignore",
|
|
17676
18539
|
windowsHide: true,
|
|
@@ -17756,12 +18619,13 @@ function openInEditor(path2) {
|
|
|
17756
18619
|
return;
|
|
17757
18620
|
}
|
|
17758
18621
|
try {
|
|
17759
|
-
(0,
|
|
18622
|
+
(0, import_node_child_process11.spawn)(editor, [path2], { stdio: "inherit" });
|
|
17760
18623
|
} catch {
|
|
17761
18624
|
console.log(`open ${path2} manually`);
|
|
17762
18625
|
}
|
|
17763
18626
|
}
|
|
17764
18627
|
async function withPlan(quiet, run, io = consoleIo) {
|
|
18628
|
+
if (!await requireContinuityAccess("northstar", { quiet }, io)) return;
|
|
17765
18629
|
const cfg = await loadConfig();
|
|
17766
18630
|
if (!cfg.sagaApiUrl) {
|
|
17767
18631
|
if (!quiet) fail("plan: Hub API URL not configured");
|
|
@@ -17770,6 +18634,7 @@ async function withPlan(quiet, run, io = consoleIo) {
|
|
|
17770
18634
|
await run(makePlanDeps(cfg, io));
|
|
17771
18635
|
}
|
|
17772
18636
|
async function runPlanAutosave(io = consoleIo, opts = {}) {
|
|
18637
|
+
if (!await requireContinuityAccess("northstar", { quiet: opts.quiet }, io)) return [];
|
|
17773
18638
|
const cfg = await loadConfig();
|
|
17774
18639
|
if (!cfg.sagaApiUrl) {
|
|
17775
18640
|
if (!opts.quiet) fail("plan: Hub API URL not configured");
|
|
@@ -18776,10 +19641,10 @@ function registerEdgeCommands(program3) {
|
|
|
18776
19641
|
|
|
18777
19642
|
// src/doctor-run.ts
|
|
18778
19643
|
var import_node_fs28 = require("node:fs");
|
|
18779
|
-
var
|
|
19644
|
+
var import_node_child_process13 = require("node:child_process");
|
|
18780
19645
|
var import_promises7 = require("node:fs/promises");
|
|
18781
19646
|
var import_node_path25 = require("node:path");
|
|
18782
|
-
var
|
|
19647
|
+
var import_node_os5 = require("node:os");
|
|
18783
19648
|
|
|
18784
19649
|
// src/plugin-guard.ts
|
|
18785
19650
|
function buildPluginGuardDecision(i) {
|
|
@@ -18799,9 +19664,9 @@ function buildGuardSessionStartLine(state, opts = {}) {
|
|
|
18799
19664
|
}
|
|
18800
19665
|
|
|
18801
19666
|
// src/cursor-plugin-seed.ts
|
|
18802
|
-
var
|
|
19667
|
+
var import_node_child_process12 = require("node:child_process");
|
|
18803
19668
|
var import_node_fs24 = require("node:fs");
|
|
18804
|
-
var
|
|
19669
|
+
var import_node_os4 = require("node:os");
|
|
18805
19670
|
var import_node_path22 = require("node:path");
|
|
18806
19671
|
var import_node_util7 = require("node:util");
|
|
18807
19672
|
function isSemverVersion(v) {
|
|
@@ -18810,7 +19675,7 @@ function isSemverVersion(v) {
|
|
|
18810
19675
|
var MMI_HUB_REPO = "mutmutco/MMI-Hub";
|
|
18811
19676
|
var CURSOR_THIRD_PARTY_STATE_KEY = "cursor/thirdPartyExtensibilityEnabled";
|
|
18812
19677
|
var PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
|
|
18813
|
-
var execFileBuffer = (0, import_node_util7.promisify)(
|
|
19678
|
+
var execFileBuffer = (0, import_node_util7.promisify)(import_node_child_process12.execFile);
|
|
18814
19679
|
function gitFetchReleaseTagArgs(hubCheckout, tag) {
|
|
18815
19680
|
return ["-C", hubCheckout, "fetch", "origin", "tag", tag, "--quiet"];
|
|
18816
19681
|
}
|
|
@@ -18819,13 +19684,13 @@ function ghReleaseTarballApiArgs(tag) {
|
|
|
18819
19684
|
}
|
|
18820
19685
|
function cursorUserGlobalStatePath() {
|
|
18821
19686
|
if (process.platform === "win32") {
|
|
18822
|
-
const base = process.env.APPDATA || (0, import_node_path22.join)((0,
|
|
19687
|
+
const base = process.env.APPDATA || (0, import_node_path22.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
|
|
18823
19688
|
return (0, import_node_path22.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
18824
19689
|
}
|
|
18825
19690
|
if (process.platform === "darwin") {
|
|
18826
|
-
return (0, import_node_path22.join)((0,
|
|
19691
|
+
return (0, import_node_path22.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
18827
19692
|
}
|
|
18828
|
-
return (0, import_node_path22.join)((0,
|
|
19693
|
+
return (0, import_node_path22.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
18829
19694
|
}
|
|
18830
19695
|
async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
18831
19696
|
const dbPath = cursorUserGlobalStatePath();
|
|
@@ -18981,6 +19846,17 @@ function buildContinuityFreshnessCheck(input) {
|
|
|
18981
19846
|
fix: `local branch work is newer than ${stale.join(" + ")} continuity; run ${commands.join(" and ")}`
|
|
18982
19847
|
};
|
|
18983
19848
|
}
|
|
19849
|
+
function buildContinuityRestrictionNotice(input) {
|
|
19850
|
+
const login = input.login?.trim() || "unknown";
|
|
19851
|
+
return {
|
|
19852
|
+
ok: true,
|
|
19853
|
+
label: "saga / North Star continuity (Jervaise-only)",
|
|
19854
|
+
fix: `continuity tools (saga, North Star, /handoff) are restricted to ${CONTINUITY_OWNER_LOGIN}; not available to ${login}. Your durable record is the board issue, PR, git history, and MM-KB. Hooks no-op these verbs for you and SessionStart omits the continuity steps. If stray local continuity state exists (.mmi/.session, .mmi/saga-pending.jsonl), remove it with: mmi-cli gc --scratch --apply (safe: it never touches config.json or a live queue).`
|
|
19855
|
+
};
|
|
19856
|
+
}
|
|
19857
|
+
function continuityDoctorCheckForLogin(login) {
|
|
19858
|
+
return login?.trim().toLowerCase() === CONTINUITY_OWNER_LOGIN ? "freshness" : "restriction";
|
|
19859
|
+
}
|
|
18984
19860
|
var MMI_PLUGIN_ID = "mmi@mutmutco";
|
|
18985
19861
|
var LEGACY_MMI_PLUGIN_ID = "mmi@mmi";
|
|
18986
19862
|
var LEGACY_MMI_MARKETPLACE = "mmi";
|
|
@@ -19940,7 +20816,7 @@ function buildBrowserArtifactsCheck(input) {
|
|
|
19940
20816
|
};
|
|
19941
20817
|
}
|
|
19942
20818
|
var KB_DRIFT_ADVISORY_LABEL = "KB registry-fact drift (advisory)";
|
|
19943
|
-
var KB_DRIFT_ADVISORY_FIX = "review kb-drift report on Hub (`node infra/saga-io.mjs get kb-drift/<date>.json`)
|
|
20819
|
+
var KB_DRIFT_ADVISORY_FIX = "review kb-drift report on Hub (`node infra/saga-io.mjs get kb-drift/<date>.json`); registry-owned facts belong in `mmi-cli project get`, not copied literals in MM-KB";
|
|
19944
20820
|
function buildKbDriftAdvisoryCheck(input) {
|
|
19945
20821
|
const base = {
|
|
19946
20822
|
ok: true,
|
|
@@ -20423,7 +21299,7 @@ function reexecMmiCli(args) {
|
|
|
20423
21299
|
}
|
|
20424
21300
|
};
|
|
20425
21301
|
const env = { ...process.env, [DOCTOR_POST_SELF_UPDATE_ENV]: "1" };
|
|
20426
|
-
const child = isWin ? (0,
|
|
21302
|
+
const child = isWin ? (0, import_node_child_process13.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", env }) : (0, import_node_child_process13.spawn)("mmi-cli", args, { stdio: "inherit", env });
|
|
20427
21303
|
child.on("error", () => done(-1));
|
|
20428
21304
|
child.on("exit", (code) => done(code ?? 0));
|
|
20429
21305
|
});
|
|
@@ -20480,7 +21356,7 @@ async function applyPluginHeal(token, surface, log, opts) {
|
|
|
20480
21356
|
}
|
|
20481
21357
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
20482
21358
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
20483
|
-
return (0, import_node_path25.join)((0,
|
|
21359
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
20484
21360
|
};
|
|
20485
21361
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
20486
21362
|
try {
|
|
@@ -20495,13 +21371,13 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
20495
21371
|
return {
|
|
20496
21372
|
isOrgRepo,
|
|
20497
21373
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
20498
|
-
marketplaceClonePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0,
|
|
20499
|
-
pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0,
|
|
21374
|
+
marketplaceClonePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
|
|
21375
|
+
pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
20500
21376
|
};
|
|
20501
21377
|
}
|
|
20502
21378
|
function installedPluginSources() {
|
|
20503
21379
|
return ["claude", "codex"].map((surface) => {
|
|
20504
|
-
const recordPath = (0, import_node_path25.join)((0,
|
|
21380
|
+
const recordPath = (0, import_node_path25.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
20505
21381
|
try {
|
|
20506
21382
|
return { surface, installed: JSON.parse((0, import_node_fs28.readFileSync)(recordPath, "utf8")), recordPath };
|
|
20507
21383
|
} catch {
|
|
@@ -20556,7 +21432,7 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
20556
21432
|
}
|
|
20557
21433
|
}
|
|
20558
21434
|
function opencodeConfigDir() {
|
|
20559
|
-
return (0, import_node_path25.join)((0,
|
|
21435
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".config", "opencode");
|
|
20560
21436
|
}
|
|
20561
21437
|
function opencodeConfigPath() {
|
|
20562
21438
|
return (0, import_node_path25.join)(opencodeConfigDir(), "opencode.jsonc");
|
|
@@ -20642,7 +21518,7 @@ function writeOpencodeCommandFiles() {
|
|
|
20642
21518
|
function readOpencodeAdapterDiskVersion() {
|
|
20643
21519
|
const candidates = [
|
|
20644
21520
|
(0, import_node_path25.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
|
|
20645
|
-
(0, import_node_path25.join)((0,
|
|
21521
|
+
(0, import_node_path25.join)((0, import_node_os5.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
|
|
20646
21522
|
];
|
|
20647
21523
|
for (const path2 of candidates) {
|
|
20648
21524
|
try {
|
|
@@ -20677,13 +21553,13 @@ function opencodePluginVersionsForReport() {
|
|
|
20677
21553
|
}
|
|
20678
21554
|
function opencodeDesktopLogsRoot() {
|
|
20679
21555
|
if (process.platform === "win32") {
|
|
20680
|
-
const base = process.env.APPDATA || (0, import_node_path25.join)((0,
|
|
21556
|
+
const base = process.env.APPDATA || (0, import_node_path25.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
|
|
20681
21557
|
return (0, import_node_path25.join)(base, "ai.opencode.desktop", "logs");
|
|
20682
21558
|
}
|
|
20683
21559
|
if (process.platform === "darwin") {
|
|
20684
|
-
return (0, import_node_path25.join)((0,
|
|
21560
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
|
|
20685
21561
|
}
|
|
20686
|
-
return (0, import_node_path25.join)((0,
|
|
21562
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".config", "ai.opencode.desktop", "logs");
|
|
20687
21563
|
}
|
|
20688
21564
|
function opencodeDesktopBootstrapSnapshot() {
|
|
20689
21565
|
const root = opencodeDesktopLogsRoot();
|
|
@@ -20699,7 +21575,7 @@ function opencodeDesktopBootstrapSnapshot() {
|
|
|
20699
21575
|
}
|
|
20700
21576
|
}
|
|
20701
21577
|
function opencodeLegacyConfigSnapshot() {
|
|
20702
|
-
const legacyPath = (0, import_node_path25.join)((0,
|
|
21578
|
+
const legacyPath = (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".opencode", "opencode.json");
|
|
20703
21579
|
if (!(0, import_node_fs28.existsSync)(legacyPath)) return {};
|
|
20704
21580
|
const content = readTextFile(legacyPath);
|
|
20705
21581
|
if (content == null) return {};
|
|
@@ -20720,7 +21596,7 @@ function quarantineOpencodeLegacyConfig(legacyPath) {
|
|
|
20720
21596
|
}
|
|
20721
21597
|
}
|
|
20722
21598
|
function cursorPluginCacheRoot() {
|
|
20723
|
-
return (0, import_node_path25.join)((0,
|
|
21599
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
|
|
20724
21600
|
}
|
|
20725
21601
|
function cursorPluginCachePinSnapshots() {
|
|
20726
21602
|
const root = cursorPluginCacheRoot();
|
|
@@ -20766,8 +21642,8 @@ function hubCheckoutForCursorSeed() {
|
|
|
20766
21642
|
}
|
|
20767
21643
|
function mmiPluginCacheRootSnapshots() {
|
|
20768
21644
|
const roots = [
|
|
20769
|
-
{ surface: "claude", root: (0, import_node_path25.join)((0,
|
|
20770
|
-
{ surface: "codex", root: (0, import_node_path25.join)((0,
|
|
21645
|
+
{ surface: "claude", root: (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
|
|
21646
|
+
{ surface: "codex", root: (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
|
|
20771
21647
|
];
|
|
20772
21648
|
return roots.flatMap(({ surface, root }) => {
|
|
20773
21649
|
try {
|
|
@@ -20831,7 +21707,7 @@ async function clearNestedPluginTreeDir(targetPath) {
|
|
|
20831
21707
|
try {
|
|
20832
21708
|
if (!(0, import_node_fs28.existsSync)(targetPath)) return true;
|
|
20833
21709
|
if (isWin) {
|
|
20834
|
-
const emptyDir = (0, import_node_path25.join)((0,
|
|
21710
|
+
const emptyDir = (0, import_node_path25.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
20835
21711
|
(0, import_node_fs28.mkdirSync)(emptyDir, { recursive: true });
|
|
20836
21712
|
try {
|
|
20837
21713
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
@@ -20869,7 +21745,7 @@ function readTextFile(path2) {
|
|
|
20869
21745
|
}
|
|
20870
21746
|
function playwrightMcpConfigSnapshots() {
|
|
20871
21747
|
const cwd = process.cwd();
|
|
20872
|
-
const home = (0,
|
|
21748
|
+
const home = (0, import_node_os5.homedir)();
|
|
20873
21749
|
const candidates = [
|
|
20874
21750
|
(0, import_node_path25.join)(cwd, ".mcp.json"),
|
|
20875
21751
|
(0, import_node_path25.join)(cwd, ".cursor", "mcp.json"),
|
|
@@ -21382,7 +22258,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
21382
22258
|
releasedVersion,
|
|
21383
22259
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
21384
22260
|
execFileP: execFileP2,
|
|
21385
|
-
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0,
|
|
22261
|
+
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0, import_node_os5.tmpdir)(), prefix)),
|
|
21386
22262
|
log: (m) => io.err(m)
|
|
21387
22263
|
});
|
|
21388
22264
|
if (seeded) {
|
|
@@ -21474,15 +22350,19 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
21474
22350
|
}
|
|
21475
22351
|
}
|
|
21476
22352
|
if (runExtended && !opts.banner) {
|
|
21477
|
-
|
|
21478
|
-
|
|
21479
|
-
|
|
21480
|
-
|
|
21481
|
-
|
|
21482
|
-
|
|
21483
|
-
|
|
21484
|
-
|
|
21485
|
-
|
|
22353
|
+
if (continuityDoctorCheckForLogin(login) === "freshness") {
|
|
22354
|
+
const continuity = readContinuityStamp();
|
|
22355
|
+
checks.push(
|
|
22356
|
+
buildContinuityFreshnessCheck({
|
|
22357
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
22358
|
+
latestWorkAt: await latestBranchWorkAt(),
|
|
22359
|
+
lastSagaNoteAt: continuity.lastSagaNoteAt,
|
|
22360
|
+
lastNorthstarAt: latestNorthstarContinuityAt()
|
|
22361
|
+
})
|
|
22362
|
+
);
|
|
22363
|
+
} else {
|
|
22364
|
+
checks.push(buildContinuityRestrictionNotice({ login: login?.trim() }));
|
|
22365
|
+
}
|
|
21486
22366
|
}
|
|
21487
22367
|
if (runExtended) {
|
|
21488
22368
|
const dashboardConsumer = await resolveDashboardConsumer(cfg);
|
|
@@ -21580,7 +22460,7 @@ function mergeGuardHook(settings) {
|
|
|
21580
22460
|
next.hooks = hooks;
|
|
21581
22461
|
return next;
|
|
21582
22462
|
}
|
|
21583
|
-
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0,
|
|
22463
|
+
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0, import_node_os5.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
|
|
21584
22464
|
function ensureUserScopeGuardHook(opts = {}) {
|
|
21585
22465
|
const path2 = opts.settingsPath ?? userScopeSettingsPath();
|
|
21586
22466
|
try {
|
|
@@ -21794,7 +22674,7 @@ async function requireFreshTrainCli(commandName) {
|
|
|
21794
22674
|
throw new Error(`running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; run doctor/update first so ${commandName} uses the current train path`);
|
|
21795
22675
|
}
|
|
21796
22676
|
var program2 = new Command();
|
|
21797
|
-
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery,
|
|
22677
|
+
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
|
|
21798
22678
|
async function runRulesSync(opts, io = consoleIo) {
|
|
21799
22679
|
const cfg = await loadConfig();
|
|
21800
22680
|
if (isRulesSource(cfg.orgRulesSource)) {
|
|
@@ -22037,7 +22917,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
22037
22917
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
22038
22918
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
22039
22919
|
return new Promise((resolve6, reject) => {
|
|
22040
|
-
const child = (0,
|
|
22920
|
+
const child = (0, import_node_child_process14.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
22041
22921
|
const timer = setTimeout(() => {
|
|
22042
22922
|
try {
|
|
22043
22923
|
child.kill();
|
|
@@ -22255,7 +23135,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
22255
23135
|
try {
|
|
22256
23136
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
22257
23137
|
if (o.repo) args.push("--repo", o.repo);
|
|
22258
|
-
(0,
|
|
23138
|
+
(0, import_node_child_process14.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
22259
23139
|
detached: true,
|
|
22260
23140
|
stdio: "ignore",
|
|
22261
23141
|
windowsHide: true,
|
|
@@ -22264,7 +23144,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
22264
23144
|
} catch {
|
|
22265
23145
|
}
|
|
22266
23146
|
}
|
|
22267
|
-
var northstar = program2.command("northstar").description("North Star \u2014
|
|
23147
|
+
var northstar = program2.command("northstar").description("North Star \u2014 Jervaise-only cross-device plans/SSOTs (S3-backed, git-clean)");
|
|
22268
23148
|
registerNorthStarCommands(northstar);
|
|
22269
23149
|
var plan = program2.command("plan").description("Alias for `northstar` (deprecated \u2014 use `northstar`)");
|
|
22270
23150
|
plan.hook("preAction", () => {
|
|
@@ -22290,7 +23170,7 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
22290
23170
|
try {
|
|
22291
23171
|
const result = await runTenantControl(trainApplyDeps(), { repo, stage: stage2, action, watch: o.watch });
|
|
22292
23172
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
22293
|
-
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed" };
|
|
23173
|
+
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
|
|
22294
23174
|
const { lines, failure } = renderVerifySecrets(body);
|
|
22295
23175
|
for (const line of lines) printLine(line);
|
|
22296
23176
|
if (failure) return failGraceful(`tenant control ${stage2} verify-secrets: ${failure}`);
|
|
@@ -23391,7 +24271,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
23391
24271
|
}
|
|
23392
24272
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
23393
24273
|
function spawnDeferredGcSweep() {
|
|
23394
|
-
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn:
|
|
24274
|
+
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
23395
24275
|
}
|
|
23396
24276
|
async function createDeferredWorktreeStore() {
|
|
23397
24277
|
try {
|
|
@@ -24631,85 +25511,90 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
24631
25511
|
));
|
|
24632
25512
|
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
|
|
24633
25513
|
program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
|
|
24634
|
-
program2.command("session-start").description("run the SessionStart verbs (rules sync,
|
|
25514
|
+
program2.command("session-start").description("run the SessionStart verbs (rules sync, Jervaise-only continuity, whoami, doctor, plan-store check) in one process; docs sync runs detached").action(async () => {
|
|
24635
25515
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
24636
25516
|
console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
24637
25517
|
return;
|
|
24638
25518
|
}
|
|
24639
25519
|
if (!isOrgRepoRoot(process.cwd())) return;
|
|
24640
|
-
|
|
24641
|
-
|
|
24642
|
-
|
|
24643
|
-
|
|
24644
|
-
|
|
25520
|
+
const continuityEnabled = (await continuityAccess().catch(() => ({ allowed: false }))).allowed;
|
|
25521
|
+
if (continuityEnabled) {
|
|
25522
|
+
try {
|
|
25523
|
+
const hook = parseHookInput(await readStdin());
|
|
25524
|
+
if (hook.session_id) persistSession(hook.session_id);
|
|
25525
|
+
} catch (e) {
|
|
25526
|
+
console.error(`[mmi-hook] saga session failed: ${e.message}`);
|
|
25527
|
+
}
|
|
24645
25528
|
}
|
|
24646
|
-
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn:
|
|
25529
|
+
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
24647
25530
|
spawnDeferredGcSweep();
|
|
24648
25531
|
let northstarInjected = false;
|
|
24649
|
-
const { parallel, sequential } = buildSessionStartPlan(
|
|
24650
|
-
|
|
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
|
-
|
|
25532
|
+
const { parallel, sequential } = buildSessionStartPlan(
|
|
25533
|
+
{
|
|
25534
|
+
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
25535
|
+
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|
|
25536
|
+
handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
|
|
25537
|
+
coopPending: (io) => runCoopPendingBanner(io),
|
|
25538
|
+
northstarContext: async (io) => {
|
|
25539
|
+
const cfg = await loadConfig();
|
|
25540
|
+
if (!cfg.sagaApiUrl) return;
|
|
25541
|
+
const planDeps = makePlanDeps(cfg, io);
|
|
25542
|
+
northstarInjected = await runNorthstarContext(io, {
|
|
25543
|
+
loadPlans: () => scopedPlanList(planDeps),
|
|
25544
|
+
readLocal: (slug) => planDeps.readLocal(slug),
|
|
25545
|
+
// #1812: thread the saga HEAD's North Star anchor (its NEXT slug) into the relevance gate so
|
|
25546
|
+
// the plan the agent is actively on is force-injected even on a generic branch with no token
|
|
25547
|
+
// overlap. fetchSagaHead errors are swallowed via a silent io — a missing/failed HEAD just
|
|
25548
|
+
// falls back to token-overlap scoring, never noises or blocks the banner.
|
|
25549
|
+
gatherSignals: () => gatherRelevanceSignals({
|
|
25550
|
+
anchorSlug: () => fetchSagaHead({ log: () => {
|
|
25551
|
+
}, err: () => {
|
|
25552
|
+
} }).then((h) => h?.anchor?.slug ?? void 0)
|
|
25553
|
+
})
|
|
25554
|
+
});
|
|
25555
|
+
},
|
|
25556
|
+
sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
|
|
25557
|
+
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
25558
|
+
// when unknown — a missing gh login must not noise or fail the banner.
|
|
25559
|
+
whoami: async (io) => {
|
|
25560
|
+
const report = await resolveWhoami({
|
|
25561
|
+
hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
25562
|
+
ghLogin: githubLogin
|
|
25563
|
+
});
|
|
25564
|
+
const line = whoamiLine(report);
|
|
25565
|
+
if (line) io.log(line);
|
|
25566
|
+
},
|
|
25567
|
+
boardSlice: (io) => runBoardSlice(io, {
|
|
25568
|
+
loadConfig: () => loadConfigForRepo(),
|
|
25569
|
+
readBoard,
|
|
25570
|
+
// #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
|
|
25571
|
+
// never costs banner time and next session's glance renders instantly within budget.
|
|
25572
|
+
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
|
|
25573
|
+
}),
|
|
25574
|
+
spineReconcile: async (io) => {
|
|
25575
|
+
const cfg = await loadConfig();
|
|
25576
|
+
const isSource = isRulesSource(cfg.orgRulesSource);
|
|
25577
|
+
if (!isSource && !await isOrgRegisteredRepo(cfg)) return;
|
|
25578
|
+
const restored = await restoreDirtyOrgSpineToHead(
|
|
25579
|
+
{ run: async (cmd, args) => (await execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })).stdout },
|
|
25580
|
+
{ isRulesSource: isSource }
|
|
25581
|
+
);
|
|
25582
|
+
if (restored.length) {
|
|
25583
|
+
io.log(`[mmi] reset ${restored.length} org-delivered file(s) to your branch \u2014 pull latest base to refresh (${restored.join(", ")})`);
|
|
25584
|
+
}
|
|
25585
|
+
},
|
|
25586
|
+
doctor: (io) => runDoctor({ banner: true }, io)
|
|
24701
25587
|
},
|
|
24702
|
-
|
|
24703
|
-
|
|
25588
|
+
{ continuityEnabled }
|
|
25589
|
+
);
|
|
24704
25590
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
24705
|
-
consoleIo.log(
|
|
25591
|
+
for (const line of sessionStartContinuityLines({ continuityEnabled, northstarInjected, cwd: process.cwd() })) consoleIo.log(line);
|
|
24706
25592
|
consoleIo.log(kbPointer());
|
|
24707
|
-
|
|
24708
|
-
await runPlanAutosave(consoleIo, { quiet: true }).catch(() => void 0);
|
|
25593
|
+
if (continuityEnabled) await runPlanAutosave(consoleIo, { quiet: true }).catch(() => void 0);
|
|
24709
25594
|
for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
|
|
24710
25595
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
24711
25596
|
if (worktreeBanner) {
|
|
24712
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
25597
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
24713
25598
|
consoleIo.log(worktreeBanner);
|
|
24714
25599
|
}
|
|
24715
25600
|
});
|