@mutmutco/cli 2.55.0 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.cjs +4 -2
- package/dist/main.cjs +1720 -516
- package/dist/saga.cjs +63 -25
- package/package.json +2 -2
- package/dist/overlord-controller.cjs +0 -322
package/dist/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 ?? [];
|
|
@@ -9862,19 +10124,19 @@ async function runCoopDeliver(coopId, io, quiet = false) {
|
|
|
9862
10124
|
if (!quiet) {
|
|
9863
10125
|
io.log(`[coop deliver] ${coopId} \u2014 issue ${match.meta.issueUrl}`);
|
|
9864
10126
|
for (const m of match.messages) io.log(m.envelope);
|
|
9865
|
-
io.log("[coop deliver]
|
|
10127
|
+
io.log("[coop deliver] Coordinate in `#mmi-agents` via `mmi-cli coop say`; the issue keeps proof/context.");
|
|
9866
10128
|
}
|
|
9867
10129
|
await postJson("/coop/delivered", { coopId });
|
|
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 },
|
|
9875
10137
|
{ readFile: import_promises4.readFile, readStdin },
|
|
9876
10138
|
{ value: "a message argument", file: "--message-file", noun: "message" }
|
|
9877
|
-
) : "Coop session started
|
|
10139
|
+
) : "Coop session started - coordinate in #mmi-agents; the issue keeps proof/context.";
|
|
9878
10140
|
const payload = {
|
|
9879
10141
|
repo: opts.repo ?? ctx.repo,
|
|
9880
10142
|
branch: ctx.branch,
|
|
@@ -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,14 +10272,118 @@ function atomicWriteFileSync(path2, content) {
|
|
|
9973
10272
|
var OVERLORD_DEFAULT_COUNT = 3;
|
|
9974
10273
|
var OVERLORD_MIN_COUNT = 3;
|
|
9975
10274
|
var OVERLORD_MAX_COUNT = 6;
|
|
10275
|
+
var OVERLORD_SERVANT_PARALLELISM = 6;
|
|
10276
|
+
function overlordServantParallelism() {
|
|
10277
|
+
const parsed = Number(process.env.MMI_OVERLORD_SERVANT_PARALLELISM);
|
|
10278
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(Math.trunc(parsed), 16);
|
|
10279
|
+
return OVERLORD_SERVANT_PARALLELISM;
|
|
10280
|
+
}
|
|
10281
|
+
var OVERLORD_DEFAULT_ENGINE = "fugu-api";
|
|
10282
|
+
var FUGU_API_DEFAULT_BASE_URL = "https://api.sakana.ai/v1";
|
|
10283
|
+
var FUGU_API_DEFAULT_MODEL = "fugu";
|
|
10284
|
+
var FUGU_API_DEFAULT_ULTRA_MODEL = "fugu-ultra";
|
|
10285
|
+
var FUGU_API_DEFAULT_TIMEOUT_MS = 9e4;
|
|
10286
|
+
var FUGU_API_DEFAULT_STARTUP_TIMEOUT_MS = 45e3;
|
|
10287
|
+
var FUGU_API_DEFAULT_MAX_ATTEMPTS = 3;
|
|
10288
|
+
var FUGU_API_RETRY_BASE_MS = 250;
|
|
10289
|
+
var FUGU_API_RETRY_CAP_MS = 8e3;
|
|
10290
|
+
var OVERLORD_HANDOFF_TIMEOUT_MS = 12e4;
|
|
10291
|
+
var SERVANT_REPORT_FIELDS = [
|
|
10292
|
+
"name",
|
|
10293
|
+
"role",
|
|
10294
|
+
"state",
|
|
10295
|
+
"assignment",
|
|
10296
|
+
"evidence",
|
|
10297
|
+
"changes",
|
|
10298
|
+
"verification",
|
|
10299
|
+
"blockers",
|
|
10300
|
+
"recommended next action"
|
|
10301
|
+
];
|
|
10302
|
+
var SERVANT_REPORT_FIELD_SET = new Set(SERVANT_REPORT_FIELDS);
|
|
10303
|
+
function normalizeServantReportField(label) {
|
|
10304
|
+
const normalized = label.trim().toLowerCase().replace(/[`*_]+/g, "").replace(/\s+/g, " ").replace(/:$/, "");
|
|
10305
|
+
if (SERVANT_REPORT_FIELD_SET.has(normalized)) return normalized;
|
|
10306
|
+
if (normalized === "next action") return "recommended next action";
|
|
10307
|
+
return void 0;
|
|
10308
|
+
}
|
|
10309
|
+
function isVagueVerification(text) {
|
|
10310
|
+
const normalized = (text ?? "").trim().toLowerCase();
|
|
10311
|
+
if (!normalized) return true;
|
|
10312
|
+
return ["done", "complete", "completed", "fixed", "looks good", "verified", "tested", "not tested", "n/a", "na", "none"].includes(normalized);
|
|
10313
|
+
}
|
|
10314
|
+
function isCompletionState(text) {
|
|
10315
|
+
return /\b(done|complete|completed|fixed)\b/i.test(text ?? "");
|
|
10316
|
+
}
|
|
10317
|
+
function extractServantReportFields(text) {
|
|
10318
|
+
const fields = {};
|
|
10319
|
+
let currentField;
|
|
10320
|
+
const append = (field, value) => {
|
|
10321
|
+
const trimmed = value.trim();
|
|
10322
|
+
fields[field] = fields[field] ? [fields[field], trimmed].filter(Boolean).join("\n") : trimmed;
|
|
10323
|
+
};
|
|
10324
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
10325
|
+
const line = rawLine.trim();
|
|
10326
|
+
const colon = line.match(/^(?:[-*]\s*)?([^:]{1,80}):\s*(.*)$/);
|
|
10327
|
+
const heading = line.match(/^#{1,6}\s+(.+?)\s*#*$/);
|
|
10328
|
+
const field = colon ? normalizeServantReportField(colon[1]) : heading ? normalizeServantReportField(heading[1]) : void 0;
|
|
10329
|
+
if (field) {
|
|
10330
|
+
currentField = field;
|
|
10331
|
+
append(field, colon ? colon[2] : "");
|
|
10332
|
+
continue;
|
|
10333
|
+
}
|
|
10334
|
+
if (currentField && line) append(currentField, line);
|
|
10335
|
+
}
|
|
10336
|
+
return fields;
|
|
10337
|
+
}
|
|
10338
|
+
function validateServantReport(text) {
|
|
10339
|
+
const fields = extractServantReportFields(text);
|
|
10340
|
+
const present = new Set(Object.keys(fields));
|
|
10341
|
+
const applies = present.size >= 2 || present.has("state") || present.has("assignment");
|
|
10342
|
+
if (!applies) {
|
|
10343
|
+
return { applies: false, ok: true, missingFields: [], emptyFields: [], unsupportedDoneClaim: false, fields };
|
|
10344
|
+
}
|
|
10345
|
+
const missingFields = SERVANT_REPORT_FIELDS.filter((field) => !present.has(field));
|
|
10346
|
+
const emptyFields = SERVANT_REPORT_FIELDS.filter((field) => present.has(field) && !fields[field]?.trim());
|
|
10347
|
+
const unsupportedDoneClaim = isCompletionState(fields.state) && isVagueVerification(fields.verification);
|
|
10348
|
+
return {
|
|
10349
|
+
applies: true,
|
|
10350
|
+
ok: missingFields.length === 0 && emptyFields.length === 0 && !unsupportedDoneClaim,
|
|
10351
|
+
missingFields,
|
|
10352
|
+
emptyFields,
|
|
10353
|
+
unsupportedDoneClaim,
|
|
10354
|
+
fields
|
|
10355
|
+
};
|
|
10356
|
+
}
|
|
10357
|
+
function formatHandoffWarnings(result) {
|
|
10358
|
+
if (!result.applies || result.ok) return "";
|
|
10359
|
+
const parts = [];
|
|
10360
|
+
if (result.missingFields.length > 0) parts.push(`missing ${result.missingFields.join(", ")}`);
|
|
10361
|
+
if (result.emptyFields.length > 0) parts.push(`empty ${result.emptyFields.join(", ")}`);
|
|
10362
|
+
if (result.unsupportedDoneClaim) parts.push("done/complete/fixed state without concrete verification.");
|
|
10363
|
+
return `Handoff warning: ${parts.join("; ")}`;
|
|
10364
|
+
}
|
|
10365
|
+
function formatServantMissionCard(options) {
|
|
10366
|
+
const lines = [
|
|
10367
|
+
`mission: ${options.mission}`,
|
|
10368
|
+
`scope: ${options.scope}`,
|
|
10369
|
+
...options.scopeToken ? [
|
|
10370
|
+
`scope token: ${options.scopeToken}`,
|
|
10371
|
+
"token authority: attribution only; grants no host permissions"
|
|
10372
|
+
] : [],
|
|
10373
|
+
`allowed actions: ${options.allowedActions.join("; ")}`,
|
|
10374
|
+
`forbidden actions: ${options.forbiddenActions.join("; ")}`,
|
|
10375
|
+
`required evidence: ${options.requiredEvidence.join("; ")}`,
|
|
10376
|
+
`verification required: ${options.verificationRequired}`,
|
|
10377
|
+
`report format: ${options.reportFormat.join("; ")}`
|
|
10378
|
+
];
|
|
10379
|
+
return lines.join("\n");
|
|
10380
|
+
}
|
|
9976
10381
|
var GENERIC_STOP_NAMES = /* @__PURE__ */ new Set([
|
|
9977
10382
|
"windowsterminal",
|
|
9978
10383
|
"windows terminal",
|
|
9979
10384
|
"pwsh",
|
|
9980
10385
|
"powershell",
|
|
9981
|
-
"opencode"
|
|
9982
|
-
"codex",
|
|
9983
|
-
"codex-fugu"
|
|
10386
|
+
"opencode"
|
|
9984
10387
|
]);
|
|
9985
10388
|
function numericCountArg(arg) {
|
|
9986
10389
|
const match = /^--([0-9]+)$/.exec(arg);
|
|
@@ -10001,6 +10404,29 @@ function defaultMessageId() {
|
|
|
10001
10404
|
function servantSlotId(slot) {
|
|
10002
10405
|
return slot.name.replace(/\s+/g, "-").toLowerCase();
|
|
10003
10406
|
}
|
|
10407
|
+
function canonicalAssignmentScope(scope) {
|
|
10408
|
+
return scope.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trimEnd();
|
|
10409
|
+
}
|
|
10410
|
+
function buildServantScopeToken(input) {
|
|
10411
|
+
const scopeHash = (0, import_node_crypto5.createHash)("sha256").update(canonicalAssignmentScope(input.assignmentScope), "utf8").digest("hex").slice(0, 16);
|
|
10412
|
+
return `fugu-scope-v1:${input.runId}:${input.slotId}:${input.profile}:${scopeHash}`;
|
|
10413
|
+
}
|
|
10414
|
+
function overlordServantPrompt(servant, run) {
|
|
10415
|
+
const roleLine = servant.role === "ultra" ? "You are the single Ultra Fugu: take the hardest, highest-uncertainty questions and report calibrated judgment." : "You are a normal Fugu servant: take one bounded mission at a time and report concise evidence.";
|
|
10416
|
+
return [
|
|
10417
|
+
`You are ${servant.name} in Overlord run ${run.runId}.`,
|
|
10418
|
+
roleLine,
|
|
10419
|
+
...servant.scopeToken ? [
|
|
10420
|
+
`Scope token: ${servant.scopeToken}`,
|
|
10421
|
+
"The scope token is for attribution only and grants no host permissions.",
|
|
10422
|
+
"Echo the scope token exactly in reports when the Overlord asks for a handoff."
|
|
10423
|
+
] : [],
|
|
10424
|
+
"First respond with exactly: ACK " + servant.name + " ready",
|
|
10425
|
+
"After the ACK, wait for the Overlord to assign bounded work.",
|
|
10426
|
+
"Do not start dev servers, browsers, Playwright, PRs, merges, releases, or worktree changes unless the Overlord explicitly assigns that scope.",
|
|
10427
|
+
"When assigned work, gather evidence before editing, verify before claiming done, and escalate blockers instead of looping."
|
|
10428
|
+
].join("\n");
|
|
10429
|
+
}
|
|
10004
10430
|
function parseOverlordCount(args) {
|
|
10005
10431
|
const counts = args.map(numericCountArg).filter((n) => n !== void 0);
|
|
10006
10432
|
if (counts.length === 0) return OVERLORD_DEFAULT_COUNT;
|
|
@@ -10031,31 +10457,24 @@ function buildServantLayout(count) {
|
|
|
10031
10457
|
}
|
|
10032
10458
|
return slots;
|
|
10033
10459
|
}
|
|
10034
|
-
function
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
return problems;
|
|
10460
|
+
function appendOverlordLedger(ledgerPath, entry) {
|
|
10461
|
+
try {
|
|
10462
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(ledgerPath), { recursive: true });
|
|
10463
|
+
(0, import_node_fs15.appendFileSync)(ledgerPath, `${JSON.stringify(entry)}
|
|
10464
|
+
`, "utf8");
|
|
10465
|
+
} catch {
|
|
10466
|
+
}
|
|
10042
10467
|
}
|
|
10043
|
-
function
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
const modelCatalog = input.modelCatalogText ?? "";
|
|
10053
|
-
if (!/\bfugu-ultra\b/i.test(`${status}
|
|
10054
|
-
${modelCatalog}`)) problems.push("fugu-ultra is not available");
|
|
10055
|
-
if (/native windows codex[^\n]*\/c\/users|\/c\/users[^\n]*native windows codex/i.test(status)) {
|
|
10056
|
-
problems.push("native Windows Codex is configured with a Git Bash /c/Users path");
|
|
10468
|
+
function appendServantJournal(journalPath, label, text, timestamp) {
|
|
10469
|
+
if (!text || !text.trim()) return;
|
|
10470
|
+
try {
|
|
10471
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(journalPath), { recursive: true });
|
|
10472
|
+
(0, import_node_fs15.appendFileSync)(journalPath, `[${timestamp}] ${label}
|
|
10473
|
+
${text.trim()}
|
|
10474
|
+
|
|
10475
|
+
`, "utf8");
|
|
10476
|
+
} catch {
|
|
10057
10477
|
}
|
|
10058
|
-
return { ok: problems.length === 0, problems };
|
|
10059
10478
|
}
|
|
10060
10479
|
function defaultOverlordStatePath(cwd) {
|
|
10061
10480
|
return (0, import_node_path13.join)(cwd, "tmp", "overlord", "runs.json");
|
|
@@ -10079,6 +10498,8 @@ function buildOverlordRun(options) {
|
|
|
10079
10498
|
const runToken = options.runToken?.() ?? defaultRunToken();
|
|
10080
10499
|
const statePath = defaultOverlordStatePath(options.cwd);
|
|
10081
10500
|
const journalDir = (0, import_node_path13.join)(options.cwd, "tmp", "overlord", runId);
|
|
10501
|
+
const engine = options.engine ?? OVERLORD_DEFAULT_ENGINE;
|
|
10502
|
+
const provider = "sakana";
|
|
10082
10503
|
const timestamp = isoNow(options.now);
|
|
10083
10504
|
return {
|
|
10084
10505
|
runId,
|
|
@@ -10090,38 +10511,33 @@ function buildOverlordRun(options) {
|
|
|
10090
10511
|
worktree: options.cwd,
|
|
10091
10512
|
statePath,
|
|
10092
10513
|
journalDir,
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10514
|
+
ledgerPath: (0, import_node_path13.join)(journalDir, "ledger.jsonl"),
|
|
10515
|
+
engine,
|
|
10516
|
+
provider,
|
|
10517
|
+
servants: buildServantLayout(options.count).map((slot) => {
|
|
10518
|
+
const slotId = servantSlotId(slot);
|
|
10519
|
+
const profile = "consultation";
|
|
10520
|
+
return {
|
|
10521
|
+
...slot,
|
|
10522
|
+
slotId,
|
|
10523
|
+
profile,
|
|
10524
|
+
state: "planned",
|
|
10525
|
+
journalPath: (0, import_node_path13.join)(journalDir, `${slotId}.log`),
|
|
10526
|
+
composerSubmitMode: "surface-api",
|
|
10527
|
+
engine,
|
|
10528
|
+
provider,
|
|
10529
|
+
eventJournalPath: (0, import_node_path13.join)(journalDir, `${slotId}.events.jsonl`),
|
|
10530
|
+
scopeToken: buildServantScopeToken({
|
|
10531
|
+
runId,
|
|
10532
|
+
slotId,
|
|
10533
|
+
profile,
|
|
10534
|
+
assignmentScope: options.task
|
|
10535
|
+
})
|
|
10536
|
+
};
|
|
10537
|
+
}),
|
|
10101
10538
|
ownedResources: []
|
|
10102
10539
|
};
|
|
10103
10540
|
}
|
|
10104
|
-
function recordOverlordHeartbeat(run, options) {
|
|
10105
|
-
const timestamp = isoNow(options.now);
|
|
10106
|
-
const controllerResource = {
|
|
10107
|
-
kind: "process",
|
|
10108
|
-
pid: options.controllerPid,
|
|
10109
|
-
commandName: "mmi-cli overlord controller",
|
|
10110
|
-
runId: run.runId,
|
|
10111
|
-
runToken: run.runToken,
|
|
10112
|
-
fingerprint: options.fingerprint
|
|
10113
|
-
};
|
|
10114
|
-
const others = run.ownedResources.filter((resource) => resource.commandName !== "mmi-cli overlord controller");
|
|
10115
|
-
return {
|
|
10116
|
-
...run,
|
|
10117
|
-
state: run.state === "starting" ? "active" : run.state,
|
|
10118
|
-
updatedAt: timestamp,
|
|
10119
|
-
controllerPid: options.controllerPid,
|
|
10120
|
-
controllerFingerprint: options.fingerprint,
|
|
10121
|
-
lastControllerHeartbeatAt: timestamp,
|
|
10122
|
-
ownedResources: [controllerResource, ...others]
|
|
10123
|
-
};
|
|
10124
|
-
}
|
|
10125
10541
|
function buildOverlordStartupPlan(args, cwd) {
|
|
10126
10542
|
const count = parseOverlordCount(args);
|
|
10127
10543
|
const task = args.filter((arg) => numericCountArg(arg) === void 0).join(" ").trim();
|
|
@@ -10145,14 +10561,16 @@ function planStopResource(resource, context) {
|
|
|
10145
10561
|
}
|
|
10146
10562
|
function summarizeOverlordRun(run, probe) {
|
|
10147
10563
|
const controller = run.controllerPid == null ? "not-started" : probe.isPidAlive(run.controllerPid) ? "alive" : "lost";
|
|
10564
|
+
const now = probe.now?.() ?? /* @__PURE__ */ new Date();
|
|
10148
10565
|
return {
|
|
10149
10566
|
active: run.state !== "stopped" && run.state !== "failed",
|
|
10150
10567
|
runId: run.runId,
|
|
10151
10568
|
state: run.state,
|
|
10152
10569
|
controller,
|
|
10153
10570
|
servants: run.servants.map((servant) => {
|
|
10154
|
-
|
|
10155
|
-
if (
|
|
10571
|
+
const progress = servantProgress(run, servant, now);
|
|
10572
|
+
if (progress !== servant.state) return { name: servant.name, state: progress };
|
|
10573
|
+
if (servant.pid != null && !probe.isPidAlive(servant.pid)) return { name: servant.name, state: "lost" };
|
|
10156
10574
|
if (servant.state === "ready" && servant.lastAckAt && servant.composerSubmitMode !== "unknown") {
|
|
10157
10575
|
return { name: servant.name, state: "ready" };
|
|
10158
10576
|
}
|
|
@@ -10170,21 +10588,6 @@ function planOverlordRunStop(run) {
|
|
|
10170
10588
|
}
|
|
10171
10589
|
return { killPids, uncertain };
|
|
10172
10590
|
}
|
|
10173
|
-
function controllerFingerprint(run) {
|
|
10174
|
-
return `mmi-overlord-controller:${run.runId}:${run.worktree}`;
|
|
10175
|
-
}
|
|
10176
|
-
function defaultStartController(run) {
|
|
10177
|
-
const scriptPath = (0, import_node_path13.join)(__dirname, "overlord-controller.cjs");
|
|
10178
|
-
const fingerprint = controllerFingerprint(run);
|
|
10179
|
-
const child = (0, import_node_child_process8.spawn)(process.execPath, [scriptPath, run.runId, run.statePath, fingerprint], {
|
|
10180
|
-
detached: true,
|
|
10181
|
-
stdio: "ignore",
|
|
10182
|
-
windowsHide: true,
|
|
10183
|
-
cwd: run.worktree
|
|
10184
|
-
});
|
|
10185
|
-
child.unref();
|
|
10186
|
-
return { pid: child.pid, fingerprint };
|
|
10187
|
-
}
|
|
10188
10591
|
function defaultIsPidAlive(pid) {
|
|
10189
10592
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
10190
10593
|
try {
|
|
@@ -10197,85 +10600,327 @@ function defaultIsPidAlive(pid) {
|
|
|
10197
10600
|
function defaultKillPid(pid) {
|
|
10198
10601
|
process.kill(pid);
|
|
10199
10602
|
}
|
|
10200
|
-
function
|
|
10201
|
-
|
|
10603
|
+
function fuguApiTimeoutMs() {
|
|
10604
|
+
const parsed = Number(process.env.MMI_OVERLORD_LLM_TIMEOUT_MS);
|
|
10605
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return FUGU_API_DEFAULT_TIMEOUT_MS;
|
|
10606
|
+
return Math.min(Math.max(Math.trunc(parsed), 5e3), 6e5);
|
|
10202
10607
|
}
|
|
10203
|
-
function
|
|
10204
|
-
const
|
|
10205
|
-
const
|
|
10206
|
-
const
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10608
|
+
function fuguApiStartupTimeoutMs() {
|
|
10609
|
+
const workTimeout = fuguApiTimeoutMs();
|
|
10610
|
+
const parsed = Number(process.env.MMI_OVERLORD_LLM_STARTUP_TIMEOUT_MS);
|
|
10611
|
+
const chosen = Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : FUGU_API_DEFAULT_STARTUP_TIMEOUT_MS;
|
|
10612
|
+
return Math.min(Math.max(chosen, 5e3), workTimeout);
|
|
10613
|
+
}
|
|
10614
|
+
function fuguApiConfig() {
|
|
10615
|
+
return {
|
|
10616
|
+
baseUrl: (process.env.MMI_OVERLORD_LLM_BASE_URL ?? FUGU_API_DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
10617
|
+
apiKey: process.env.MMI_OVERLORD_LLM_API_KEY ?? process.env.SAKANA_API_KEY,
|
|
10618
|
+
model: process.env.MMI_OVERLORD_LLM_MODEL ?? FUGU_API_DEFAULT_MODEL,
|
|
10619
|
+
ultraModel: process.env.MMI_OVERLORD_LLM_ULTRA_MODEL ?? FUGU_API_DEFAULT_ULTRA_MODEL,
|
|
10620
|
+
timeoutMs: fuguApiTimeoutMs()
|
|
10621
|
+
};
|
|
10622
|
+
}
|
|
10623
|
+
function fuguApiModelForServant(config, servant) {
|
|
10624
|
+
return servant.role === "ultra" ? config.ultraModel : config.model;
|
|
10625
|
+
}
|
|
10626
|
+
function fuguApiSystemMessage(servant, run) {
|
|
10627
|
+
const roleLine = servant.role === "ultra" ? "You are the single Ultra Fugu servant. Take the hardest useful slice, surface risks, and give calibrated judgment." : "You are a normal Fugu servant. Take one bounded assignment at a time and report concise evidence.";
|
|
10628
|
+
const contract = formatServantMissionCard({
|
|
10629
|
+
mission: run.task,
|
|
10630
|
+
scope: servant.role === "ultra" ? "consult on the highest-uncertainty slice assigned by the Overlord" : "consult on one bounded slice assigned by the Overlord",
|
|
10631
|
+
scopeToken: servant.scopeToken,
|
|
10632
|
+
allowedActions: ["inspect assigned evidence", "reason from provided context", "report findings"],
|
|
10633
|
+
forbiddenActions: ["mutate files without explicit scope", "start stage/dev servers", "start browsers or Playwright", "open PRs or merges"],
|
|
10634
|
+
requiredEvidence: ["cite source paths, command results, or observations when available", "separate evidence from inference"],
|
|
10635
|
+
verificationRequired: "name the verification performed before any done/complete/fixed claim, or explain why verification is not applicable",
|
|
10636
|
+
reportFormat: [...SERVANT_REPORT_FIELDS, "scope token"]
|
|
10216
10637
|
});
|
|
10217
|
-
|
|
10218
|
-
${
|
|
10219
|
-
|
|
10220
|
-
|
|
10638
|
+
return [
|
|
10639
|
+
`You are ${servant.name} in Overlord run ${run.runId}.`,
|
|
10640
|
+
roleLine,
|
|
10641
|
+
"The Overlord is the only coordinator.",
|
|
10642
|
+
"Do not claim ownership of the full mission.",
|
|
10643
|
+
"Do not mutate files, branches, worktrees, stage/dev servers, browsers, PRs, or releases unless the Overlord explicitly grants that scope.",
|
|
10644
|
+
"Gather evidence before acting.",
|
|
10645
|
+
"Verify before claiming done.",
|
|
10646
|
+
"Escalate blockers instead of looping.",
|
|
10647
|
+
contract
|
|
10648
|
+
].join("\n");
|
|
10221
10649
|
}
|
|
10222
|
-
function
|
|
10223
|
-
const
|
|
10224
|
-
const
|
|
10650
|
+
async function collectFuguApiPreflight() {
|
|
10651
|
+
const config = fuguApiConfig();
|
|
10652
|
+
const problems = [];
|
|
10653
|
+
if (!config.apiKey) problems.push("SAKANA_API_KEY or MMI_OVERLORD_LLM_API_KEY is not available");
|
|
10654
|
+
if (!config.baseUrl) problems.push("Fugu API base URL is not configured");
|
|
10655
|
+
if (!config.apiKey || !config.baseUrl) return { ok: false, problems };
|
|
10656
|
+
const controller = new AbortController();
|
|
10657
|
+
const timeout = setTimeout(() => controller.abort(), Math.min(config.timeoutMs, 15e3));
|
|
10225
10658
|
try {
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10659
|
+
const response = await fetch(`${config.baseUrl}/models`, {
|
|
10660
|
+
method: "GET",
|
|
10661
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
10662
|
+
signal: controller.signal
|
|
10663
|
+
});
|
|
10664
|
+
if (!response.ok) {
|
|
10665
|
+
problems.push(`Fugu API model probe returned HTTP ${response.status}`);
|
|
10666
|
+
} else {
|
|
10667
|
+
const body = await response.json();
|
|
10668
|
+
const models = new Set((body.data ?? []).map((model) => model.id).filter((id) => Boolean(id)));
|
|
10669
|
+
if (!models.has(config.model)) problems.push(`Fugu API model ${config.model} is not available`);
|
|
10670
|
+
if (!models.has(config.ultraModel)) problems.push(`Fugu API ultra model ${config.ultraModel} is not available`);
|
|
10671
|
+
}
|
|
10672
|
+
} catch (e) {
|
|
10673
|
+
problems.push(`Fugu API model probe failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10674
|
+
} finally {
|
|
10675
|
+
clearTimeout(timeout);
|
|
10229
10676
|
}
|
|
10677
|
+
return { ok: problems.length === 0, problems };
|
|
10230
10678
|
}
|
|
10231
|
-
function
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10679
|
+
function mapFuguApiUsage(usage) {
|
|
10680
|
+
if (!usage) return void 0;
|
|
10681
|
+
const mapped = {
|
|
10682
|
+
promptTokens: usage.prompt_tokens,
|
|
10683
|
+
completionTokens: usage.completion_tokens,
|
|
10684
|
+
totalTokens: usage.total_tokens
|
|
10685
|
+
};
|
|
10686
|
+
return Object.values(mapped).some((value) => typeof value === "number") ? mapped : void 0;
|
|
10687
|
+
}
|
|
10688
|
+
async function defaultRunFuguApi(run, servant, message, options) {
|
|
10689
|
+
const config = fuguApiConfig();
|
|
10690
|
+
if (!config.apiKey) return { ok: false, error: "SAKANA_API_KEY or MMI_OVERLORD_LLM_API_KEY is not available" };
|
|
10691
|
+
const model = fuguApiModelForServant(config, servant);
|
|
10692
|
+
const messages = [
|
|
10693
|
+
...servant.llmMessages ?? [{ role: "system", content: fuguApiSystemMessage(servant, run) }],
|
|
10694
|
+
{ role: "user", content: message }
|
|
10695
|
+
];
|
|
10696
|
+
const controller = new AbortController();
|
|
10697
|
+
const timeout = setTimeout(() => controller.abort(), options?.timeoutMs ?? config.timeoutMs);
|
|
10698
|
+
try {
|
|
10699
|
+
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
10700
|
+
method: "POST",
|
|
10701
|
+
headers: {
|
|
10702
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
10703
|
+
"Content-Type": "application/json"
|
|
10704
|
+
},
|
|
10705
|
+
body: JSON.stringify({
|
|
10706
|
+
model,
|
|
10707
|
+
messages,
|
|
10708
|
+
temperature: 0.2
|
|
10709
|
+
}),
|
|
10710
|
+
signal: controller.signal
|
|
10711
|
+
});
|
|
10712
|
+
const requestId = response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? void 0;
|
|
10713
|
+
const json = await response.json().catch(() => void 0);
|
|
10714
|
+
if (!response.ok) {
|
|
10715
|
+
return {
|
|
10716
|
+
ok: false,
|
|
10717
|
+
model,
|
|
10718
|
+
requestId,
|
|
10719
|
+
error: json?.error?.message ?? `Fugu API returned HTTP ${response.status}`,
|
|
10720
|
+
retryable: response.status === 429 || response.status >= 500
|
|
10721
|
+
};
|
|
10722
|
+
}
|
|
10723
|
+
const text = json?.choices?.[0]?.message?.content?.trim() ?? "";
|
|
10724
|
+
if (!text) return { ok: false, model, requestId, error: "Fugu API returned no assistant text" };
|
|
10725
|
+
return {
|
|
10726
|
+
ok: true,
|
|
10727
|
+
text,
|
|
10728
|
+
model,
|
|
10729
|
+
requestId,
|
|
10730
|
+
usage: mapFuguApiUsage(json?.usage),
|
|
10731
|
+
messages: [...messages, { role: "assistant", content: text }]
|
|
10732
|
+
};
|
|
10733
|
+
} catch (e) {
|
|
10734
|
+
const aborted = e instanceof Error && e.name === "AbortError";
|
|
10735
|
+
return {
|
|
10736
|
+
ok: false,
|
|
10737
|
+
model,
|
|
10738
|
+
error: aborted ? "Fugu API request timed out" : e instanceof Error ? e.message : String(e),
|
|
10739
|
+
retryable: true
|
|
10740
|
+
};
|
|
10741
|
+
} finally {
|
|
10742
|
+
clearTimeout(timeout);
|
|
10743
|
+
}
|
|
10248
10744
|
}
|
|
10249
|
-
function
|
|
10745
|
+
function renderOverlordPreflightFailure(report) {
|
|
10250
10746
|
const lines = [
|
|
10251
10747
|
"Overlord setup needed",
|
|
10252
|
-
"The servant pool was not started because
|
|
10748
|
+
"The servant pool was not started because Fugu preflight failed.",
|
|
10253
10749
|
"",
|
|
10254
10750
|
"Problems:",
|
|
10255
10751
|
...report.problems.map((problem) => `- ${problem}`),
|
|
10256
10752
|
"",
|
|
10257
10753
|
"Fixes:"
|
|
10258
10754
|
];
|
|
10259
|
-
if (report.problems.some((problem) => problem.includes("
|
|
10260
|
-
lines.push("-
|
|
10261
|
-
}
|
|
10262
|
-
if (report.problems.some((problem) => problem.includes("codex-fugu is not installed"))) {
|
|
10263
|
-
lines.push("- Install or repair codex-fugu, then confirm with `codex-fugu --status`.");
|
|
10264
|
-
}
|
|
10265
|
-
if (report.problems.some((problem) => problem.includes("missing API key"))) {
|
|
10266
|
-
lines.push("- Set OPENAI_API_KEY or CODEX_API_KEY in the launching shell; do not paste key values into chat.");
|
|
10267
|
-
}
|
|
10268
|
-
if (report.problems.some((problem) => problem.includes("fugu-ultra"))) {
|
|
10269
|
-
lines.push("- Recheck or update Fugu configs so `fugu-ultra` appears in the Fugu model catalog.");
|
|
10755
|
+
if (report.problems.some((problem) => problem.includes("SAKANA_API_KEY") || problem.includes("Fugu API"))) {
|
|
10756
|
+
lines.push("- Confirm `SAKANA_API_KEY` is available and `https://api.sakana.ai/v1/models` is reachable.");
|
|
10270
10757
|
}
|
|
10271
|
-
if (report.problems.some((problem) => problem.includes("
|
|
10272
|
-
lines.push("-
|
|
10273
|
-
}
|
|
10274
|
-
if (report.problems.some((problem) => problem.includes("/c/Users"))) {
|
|
10275
|
-
lines.push("- Launch from a native shell adapter or repair Fugu path configuration so Windows paths stay native.");
|
|
10758
|
+
if (report.problems.some((problem) => problem.includes("model") || problem.includes("fugu"))) {
|
|
10759
|
+
lines.push("- Confirm the configured Fugu API models are exposed by `/models`.");
|
|
10276
10760
|
}
|
|
10277
10761
|
return lines.join("\n");
|
|
10278
10762
|
}
|
|
10763
|
+
function markServantsStarting(run, timestamp) {
|
|
10764
|
+
return {
|
|
10765
|
+
...run,
|
|
10766
|
+
updatedAt: timestamp,
|
|
10767
|
+
servants: run.servants.map((servant) => ({
|
|
10768
|
+
...servant,
|
|
10769
|
+
state: "starting"
|
|
10770
|
+
}))
|
|
10771
|
+
};
|
|
10772
|
+
}
|
|
10773
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
10774
|
+
const results = new Array(items.length);
|
|
10775
|
+
let nextIndex = 0;
|
|
10776
|
+
const workerCount = Math.min(Math.max(1, limit), items.length);
|
|
10777
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
10778
|
+
while (true) {
|
|
10779
|
+
const index = nextIndex;
|
|
10780
|
+
nextIndex += 1;
|
|
10781
|
+
if (index >= items.length) return;
|
|
10782
|
+
results[index] = await worker(items[index]);
|
|
10783
|
+
}
|
|
10784
|
+
}));
|
|
10785
|
+
return results;
|
|
10786
|
+
}
|
|
10787
|
+
function hasCapturedFuguApiText(result) {
|
|
10788
|
+
return result.ok && Boolean(result.text?.trim());
|
|
10789
|
+
}
|
|
10790
|
+
function updateFuguApiServant(servant, result, timestamp) {
|
|
10791
|
+
const captured = hasCapturedFuguApiText(result);
|
|
10792
|
+
return {
|
|
10793
|
+
...servant,
|
|
10794
|
+
state: captured ? "ready" : "blocked",
|
|
10795
|
+
llmModel: result.model ?? servant.llmModel,
|
|
10796
|
+
llmMessages: result.messages ?? servant.llmMessages,
|
|
10797
|
+
llmRequestId: result.requestId ?? servant.llmRequestId,
|
|
10798
|
+
lastAckAt: captured ? timestamp : servant.lastAckAt,
|
|
10799
|
+
lastUsefulSignalAt: captured ? timestamp : servant.lastUsefulSignalAt,
|
|
10800
|
+
lastEventAt: timestamp,
|
|
10801
|
+
lastMessageCompletedAt: captured ? timestamp : servant.lastMessageCompletedAt
|
|
10802
|
+
};
|
|
10803
|
+
}
|
|
10804
|
+
function defaultSleep3(ms) {
|
|
10805
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
10806
|
+
}
|
|
10807
|
+
function fuguApiRetryConfig(sleep) {
|
|
10808
|
+
const parsed = Number(process.env.MMI_OVERLORD_LLM_MAX_ATTEMPTS);
|
|
10809
|
+
const maxAttempts = Number.isFinite(parsed) && parsed >= 1 ? Math.min(Math.trunc(parsed), 6) : FUGU_API_DEFAULT_MAX_ATTEMPTS;
|
|
10810
|
+
return { maxAttempts, baseDelayMs: FUGU_API_RETRY_BASE_MS, sleep };
|
|
10811
|
+
}
|
|
10812
|
+
function fuguApiBackoffMs(attempt, baseDelayMs) {
|
|
10813
|
+
const exponential = baseDelayMs * 2 ** (attempt - 1);
|
|
10814
|
+
const capped = Math.min(exponential, FUGU_API_RETRY_CAP_MS);
|
|
10815
|
+
const jitter = Math.random() * capped * 0.25;
|
|
10816
|
+
return Math.round(capped + jitter);
|
|
10817
|
+
}
|
|
10818
|
+
async function callFuguApiRunner(runFuguApi, run, servant, message, retry, callOptions) {
|
|
10819
|
+
const maxAttempts = Math.max(1, retry?.maxAttempts ?? 1);
|
|
10820
|
+
const baseDelayMs = retry?.baseDelayMs ?? FUGU_API_RETRY_BASE_MS;
|
|
10821
|
+
const sleep = retry?.sleep ?? defaultSleep3;
|
|
10822
|
+
let attempt = 0;
|
|
10823
|
+
for (; ; ) {
|
|
10824
|
+
attempt += 1;
|
|
10825
|
+
let result;
|
|
10826
|
+
try {
|
|
10827
|
+
result = await runFuguApi(run, servant, message, callOptions);
|
|
10828
|
+
} catch (e) {
|
|
10829
|
+
result = { ok: false, error: e instanceof Error ? e.message : String(e), retryable: true };
|
|
10830
|
+
}
|
|
10831
|
+
if (result.ok || !result.retryable || attempt >= maxAttempts) {
|
|
10832
|
+
return { ...result, attempts: attempt };
|
|
10833
|
+
}
|
|
10834
|
+
await sleep(fuguApiBackoffMs(attempt, baseDelayMs));
|
|
10835
|
+
}
|
|
10836
|
+
}
|
|
10837
|
+
function seedLazyServant(servant, run, timestamp) {
|
|
10838
|
+
const config = fuguApiConfig();
|
|
10839
|
+
const llmMessages = servant.llmMessages ?? [{ role: "system", content: fuguApiSystemMessage(servant, run) }];
|
|
10840
|
+
return {
|
|
10841
|
+
...servant,
|
|
10842
|
+
state: "ready",
|
|
10843
|
+
llmModel: fuguApiModelForServant(config, servant),
|
|
10844
|
+
llmMessages,
|
|
10845
|
+
lastAckAt: timestamp,
|
|
10846
|
+
lastUsefulSignalAt: timestamp,
|
|
10847
|
+
lastEventAt: timestamp
|
|
10848
|
+
};
|
|
10849
|
+
}
|
|
10850
|
+
async function startFuguApiServants(run, runFuguApi, now, retry) {
|
|
10851
|
+
const startupCallOptions = { timeoutMs: fuguApiStartupTimeoutMs() };
|
|
10852
|
+
const verifiedTargets = run.servants.filter((servant) => servant.role !== "ultra");
|
|
10853
|
+
const results = await mapWithConcurrency(verifiedTargets, overlordServantParallelism(), async (servant) => ({
|
|
10854
|
+
servant,
|
|
10855
|
+
result: await callFuguApiRunner(runFuguApi, run, servant, overlordServantPrompt(servant, run), retry, startupCallOptions)
|
|
10856
|
+
}));
|
|
10857
|
+
const timestamp = isoNow(now);
|
|
10858
|
+
const bySlot = /* @__PURE__ */ new Map();
|
|
10859
|
+
for (const { servant, result } of results) {
|
|
10860
|
+
bySlot.set(servant.slotId, result);
|
|
10861
|
+
if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: timestamp, kind: "servant-start", ownerSlotId: servant.slotId, ok: hasCapturedFuguApiText(result), model: result.model, requestId: result.requestId, responseText: result.text, error: result.error, usage: result.usage });
|
|
10862
|
+
appendServantJournal(servant.journalPath, "consultation", result.text, timestamp);
|
|
10863
|
+
}
|
|
10864
|
+
const servants = run.servants.map((servant) => servant.role === "ultra" ? seedLazyServant(servant, run, timestamp) : updateFuguApiServant(servant, bySlot.get(servant.slotId) ?? { ok: false, error: "missing Fugu API result" }, timestamp));
|
|
10865
|
+
return { ...run, state: "active", updatedAt: timestamp, servants };
|
|
10866
|
+
}
|
|
10867
|
+
async function dispatchFuguApiMessage(run, message, runFuguApi, now, retry) {
|
|
10868
|
+
const startedAt = isoNow(now);
|
|
10869
|
+
const targets = run.servants.filter((servant) => message.target === "all" || servant.slotId === message.target || normalizeServantTarget(servant.name) === message.target);
|
|
10870
|
+
const results = await mapWithConcurrency(targets, overlordServantParallelism(), async (servant) => ({
|
|
10871
|
+
servant,
|
|
10872
|
+
result: await callFuguApiRunner(runFuguApi, run, servant, message.text, retry)
|
|
10873
|
+
}));
|
|
10874
|
+
const completedAt = isoNow(now);
|
|
10875
|
+
const successCount = results.filter(({ result }) => hasCapturedFuguApiText(result)).length;
|
|
10876
|
+
const failureCount = Math.max(0, targets.length - successCount);
|
|
10877
|
+
const state = targets.length > 0 && failureCount === 0 ? "completed" : successCount > 0 && message.target === "all" ? "partial" : "failed";
|
|
10878
|
+
const responses = results.filter(({ result }) => result.text).map(({ servant, result }) => `${servant.slotId}: ${result.text}`);
|
|
10879
|
+
const servantResults = results.map(({ servant, result }) => {
|
|
10880
|
+
if (hasCapturedFuguApiText(result)) {
|
|
10881
|
+
const warning = formatHandoffWarnings(validateServantReport(result.text ?? ""));
|
|
10882
|
+
return {
|
|
10883
|
+
slotId: servant.slotId,
|
|
10884
|
+
state: "completed",
|
|
10885
|
+
responseText: result.text?.trim(),
|
|
10886
|
+
handoffWarnings: warning ? [warning] : void 0,
|
|
10887
|
+
usage: result.usage
|
|
10888
|
+
};
|
|
10889
|
+
}
|
|
10890
|
+
return {
|
|
10891
|
+
slotId: servant.slotId,
|
|
10892
|
+
state: "failed",
|
|
10893
|
+
failureReason: result.error ?? "Fugu API servant returned no text"
|
|
10894
|
+
};
|
|
10895
|
+
});
|
|
10896
|
+
const bySlot = /* @__PURE__ */ new Map();
|
|
10897
|
+
for (const { servant, result } of results) {
|
|
10898
|
+
bySlot.set(servant.slotId, result);
|
|
10899
|
+
if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: completedAt, kind: "message-response", messageId: message.id, ownerSlotId: servant.slotId, ok: hasCapturedFuguApiText(result), model: result.model, requestId: result.requestId, responseText: result.text, error: result.error, usage: result.usage });
|
|
10900
|
+
appendServantJournal(servant.journalPath, `message ${message.id}`, result.text, completedAt);
|
|
10901
|
+
}
|
|
10902
|
+
const nextServants = run.servants.map((servant) => {
|
|
10903
|
+
const result = bySlot.get(servant.slotId);
|
|
10904
|
+
return result ? updateFuguApiServant(servant, result, completedAt) : servant;
|
|
10905
|
+
});
|
|
10906
|
+
const nextMessage = {
|
|
10907
|
+
...message,
|
|
10908
|
+
state,
|
|
10909
|
+
startedAt,
|
|
10910
|
+
completedAt: state === "completed" || state === "partial" ? completedAt : void 0,
|
|
10911
|
+
failedAt: state === "failed" ? completedAt : void 0,
|
|
10912
|
+
ackText: state === "completed" ? "Fugu API response captured" : state === "partial" ? "Fugu API partial response captured" : void 0,
|
|
10913
|
+
responseText: responses.join("\n"),
|
|
10914
|
+
failureReason: state === "completed" ? void 0 : state === "partial" ? `${failureCount} of ${targets.length} Fugu API servant calls failed or returned no text` : "one or more Fugu API servant calls failed or returned no text",
|
|
10915
|
+
servantResults
|
|
10916
|
+
};
|
|
10917
|
+
return {
|
|
10918
|
+
...run,
|
|
10919
|
+
updatedAt: completedAt,
|
|
10920
|
+
servants: nextServants,
|
|
10921
|
+
messages: [...(run.messages ?? []).filter((m) => m.id !== message.id), nextMessage]
|
|
10922
|
+
};
|
|
10923
|
+
}
|
|
10279
10924
|
function findActiveRun(registry2) {
|
|
10280
10925
|
const runId = registry2.activeRunId;
|
|
10281
10926
|
if (!runId) return void 0;
|
|
@@ -10290,17 +10935,120 @@ function hasServantTarget(run, target) {
|
|
|
10290
10935
|
(servant) => servant.slotId === normalized || normalizeServantTarget(servant.name) === normalized
|
|
10291
10936
|
);
|
|
10292
10937
|
}
|
|
10293
|
-
function
|
|
10294
|
-
|
|
10938
|
+
function messageProgress(message, now = /* @__PURE__ */ new Date(), timeoutMs = OVERLORD_HANDOFF_TIMEOUT_MS) {
|
|
10939
|
+
if (message.state === "partial") return "partial";
|
|
10940
|
+
if (message.completedAt || message.state === "completed") return "completed";
|
|
10941
|
+
if (message.failedAt || message.state === "failed") return "failed";
|
|
10942
|
+
const startedAt = message.startedAt ?? message.deliveredAt;
|
|
10943
|
+
if (!startedAt) return "queued";
|
|
10944
|
+
const elapsed = now.getTime() - new Date(startedAt).getTime();
|
|
10945
|
+
return Number.isFinite(elapsed) && elapsed >= timeoutMs ? "stalled" : "started";
|
|
10946
|
+
}
|
|
10947
|
+
function servantProgress(run, servant, now = /* @__PURE__ */ new Date()) {
|
|
10948
|
+
const relevant = (run.messages ?? []).filter((message) => message.target === "all" || servant.slotId === message.target || normalizeServantTarget(servant.name) === message.target);
|
|
10949
|
+
return relevant.some((message) => messageProgress(message, now) === "stalled") ? "stalled-after-delivery" : servant.state;
|
|
10950
|
+
}
|
|
10951
|
+
function humanSafeText(text, maxLength = 160) {
|
|
10952
|
+
const redacted = (text ?? "").replace(/\bBearer\s+\S+/gi, "[redacted]").replace(/\b(api[_-]?key|token|secret)=\S+/gi, "$1=[redacted]").replace(/\b(sk|sakana|mmi)[-_][A-Za-z0-9_-]{10,}\b/g, "[redacted]").replace(/\s+/g, " ").trim();
|
|
10953
|
+
if (redacted.length <= maxLength) return redacted;
|
|
10954
|
+
return `${redacted.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
10955
|
+
}
|
|
10956
|
+
function renderOverlordSendSummary(message, statePath) {
|
|
10957
|
+
const results = message?.servantResults ?? [];
|
|
10958
|
+
const completed = results.filter((result) => result.state === "completed").length;
|
|
10959
|
+
const blocked = results.filter((result) => result.state === "failed").length;
|
|
10960
|
+
const state = message?.state ?? "failed";
|
|
10961
|
+
const lines = [
|
|
10962
|
+
`Message: ${message?.id ?? "unknown"}`,
|
|
10963
|
+
`Target: ${message?.target ?? "unknown"}`,
|
|
10964
|
+
`Result: ${state} - ${completed} completed, ${blocked} blocked.`
|
|
10965
|
+
];
|
|
10966
|
+
for (const result of results.filter((item) => item.state === "failed").slice(0, 3)) {
|
|
10967
|
+
lines.push(`Blocked: ${result.slotId} - ${humanSafeText(result.failureReason ?? "no reason reported")}`);
|
|
10968
|
+
}
|
|
10969
|
+
const hidden = Math.max(0, blocked - 3);
|
|
10970
|
+
if (hidden > 0) lines.push(`Blocked: +${hidden} more`);
|
|
10971
|
+
lines.push(`State: ${statePath}`);
|
|
10972
|
+
return lines.join("\n");
|
|
10973
|
+
}
|
|
10974
|
+
function countValues(values, value) {
|
|
10975
|
+
return values.filter((item) => item === value).length;
|
|
10976
|
+
}
|
|
10977
|
+
function latestServantFailureReason(run, slotId) {
|
|
10978
|
+
for (const message of [...run.messages ?? []].reverse()) {
|
|
10979
|
+
const result = message.servantResults?.find((item) => item.slotId === slotId && item.state === "failed");
|
|
10980
|
+
if (result?.failureReason?.trim()) return result.failureReason;
|
|
10981
|
+
}
|
|
10982
|
+
return void 0;
|
|
10983
|
+
}
|
|
10984
|
+
function affectedServantStatusLabel(state, reason) {
|
|
10985
|
+
if (state === "stalled-after-delivery") return "Stalled";
|
|
10986
|
+
if (/\btimed?\s*out\b/i.test(reason ?? "")) return "Timed out";
|
|
10987
|
+
return "Blocked";
|
|
10988
|
+
}
|
|
10989
|
+
function renderOverlordStatus(summary, run, now = /* @__PURE__ */ new Date()) {
|
|
10990
|
+
const servantStates = summary.servants.map((servant) => servant.state);
|
|
10991
|
+
const messageStates = (run.messages ?? []).map((message) => messageProgress(message, now));
|
|
10992
|
+
const activeMessages = messageStates.filter((state) => state === "queued" || state === "started" || state === "stalled").length;
|
|
10993
|
+
const blockedServants = summary.servants.filter((servant) => servant.state === "blocked" || servant.state === "stalled-after-delivery");
|
|
10994
|
+
const latest = (run.messages ?? []).at(-1);
|
|
10995
|
+
const latestState = latest ? messageProgress(latest, now) : void 0;
|
|
10295
10996
|
return [
|
|
10296
10997
|
`Overlord run ${summary.runId}`,
|
|
10297
10998
|
`State: ${summary.state}`,
|
|
10298
|
-
`
|
|
10299
|
-
`
|
|
10300
|
-
|
|
10301
|
-
|
|
10999
|
+
`Mission: ${humanSafeText(run.task || "not provided yet")}`,
|
|
11000
|
+
`Servants: ${countValues(servantStates, "ready")} ready, ${countValues(servantStates, "blocked") + countValues(servantStates, "stalled-after-delivery")} blocked, ${countValues(servantStates, "planned")} planned.`,
|
|
11001
|
+
`Messages: ${countValues(messageStates, "completed")} completed, ${countValues(messageStates, "partial")} partial, ${countValues(messageStates, "failed")} failed, ${activeMessages} active.`,
|
|
11002
|
+
...blockedServants.slice(0, 3).map((servant) => {
|
|
11003
|
+
const slotId = run.servants.find((item) => item.name === servant.name)?.slotId;
|
|
11004
|
+
const reason = slotId ? latestServantFailureReason(run, slotId) : void 0;
|
|
11005
|
+
const label = affectedServantStatusLabel(servant.state, reason);
|
|
11006
|
+
const detail = reason ?? (servant.state === "stalled-after-delivery" ? "no recent update" : void 0);
|
|
11007
|
+
return `${label}: ${servant.name}${detail ? ` - ${humanSafeText(detail)}` : ""}`;
|
|
11008
|
+
}),
|
|
11009
|
+
...blockedServants.length > 3 ? [`Blocked: +${blockedServants.length - 3} more`] : [],
|
|
11010
|
+
...latest && latestState ? [`Latest: ${latest.id} ${latest.target} ${latestState}${latest.failureReason ? ` - ${humanSafeText(latest.failureReason)}` : ""}`] : [],
|
|
11011
|
+
`Coordinator: ${summary.controller}`
|
|
10302
11012
|
].join("\n");
|
|
10303
11013
|
}
|
|
11014
|
+
function usefulJournalLines(text) {
|
|
11015
|
+
return text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !/^fugu\s/i.test(line) && !/^›/.test(line) && !/^\[overlord\] launched/.test(line)).slice(-20);
|
|
11016
|
+
}
|
|
11017
|
+
var HANDOFF_SIGNAL = /\b(handoff|evidence|recommend(?:ed|ation|s)?|improvement|suggest(?:ion|s)?|next action|proposal|file)\b|`[^`]+`/i;
|
|
11018
|
+
var HANDOFF_MIN_RESPONSE_CHARS = 12;
|
|
11019
|
+
function parseJournalBlocks(raw) {
|
|
11020
|
+
const blocks = [];
|
|
11021
|
+
let current = null;
|
|
11022
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
11023
|
+
const header = line.match(/^\[[^\]]+\]\s+(.+)$/);
|
|
11024
|
+
if (header) {
|
|
11025
|
+
if (current) blocks.push(current);
|
|
11026
|
+
current = { label: header[1].trim(), body: "" };
|
|
11027
|
+
} else if (current) {
|
|
11028
|
+
current.body += (current.body ? "\n" : "") + line;
|
|
11029
|
+
}
|
|
11030
|
+
}
|
|
11031
|
+
if (current) blocks.push(current);
|
|
11032
|
+
return blocks;
|
|
11033
|
+
}
|
|
11034
|
+
function journalHasHandoff(raw, lines) {
|
|
11035
|
+
for (const block of parseJournalBlocks(raw)) {
|
|
11036
|
+
if (!block.label.startsWith("message ")) continue;
|
|
11037
|
+
const body = block.body.trim();
|
|
11038
|
+
if (body.length >= HANDOFF_MIN_RESPONSE_CHARS) return true;
|
|
11039
|
+
if (validateServantReport(body).applies) return true;
|
|
11040
|
+
}
|
|
11041
|
+
return lines.some((line) => HANDOFF_SIGNAL.test(line));
|
|
11042
|
+
}
|
|
11043
|
+
function servantJournalSummary(servant) {
|
|
11044
|
+
try {
|
|
11045
|
+
const raw = (0, import_node_fs15.readFileSync)(servant.journalPath, "utf8");
|
|
11046
|
+
const lines = usefulJournalLines(raw);
|
|
11047
|
+
return { lines, hasHandoff: journalHasHandoff(raw, lines) };
|
|
11048
|
+
} catch {
|
|
11049
|
+
return { lines: [], hasHandoff: false };
|
|
11050
|
+
}
|
|
11051
|
+
}
|
|
10304
11052
|
function runJson(run, extra = {}) {
|
|
10305
11053
|
return {
|
|
10306
11054
|
ok: true,
|
|
@@ -10308,15 +11056,23 @@ function runJson(run, extra = {}) {
|
|
|
10308
11056
|
state: run.state,
|
|
10309
11057
|
task: run.task,
|
|
10310
11058
|
count: run.servants.length,
|
|
11059
|
+
engine: run.engine,
|
|
10311
11060
|
controllerPid: run.controllerPid,
|
|
10312
11061
|
statePath: run.statePath,
|
|
11062
|
+
ledgerPath: run.ledgerPath,
|
|
10313
11063
|
servants: run.servants.map((servant) => ({
|
|
10314
11064
|
name: servant.name,
|
|
10315
11065
|
role: servant.role,
|
|
10316
11066
|
model: servant.model,
|
|
10317
11067
|
state: servant.state,
|
|
10318
11068
|
pid: servant.pid,
|
|
10319
|
-
journalPath: servant.journalPath
|
|
11069
|
+
journalPath: servant.journalPath,
|
|
11070
|
+
engine: servant.engine,
|
|
11071
|
+
opencodeSessionId: servant.opencodeSessionId,
|
|
11072
|
+
scopeToken: servant.scopeToken,
|
|
11073
|
+
llmModel: servant.llmModel,
|
|
11074
|
+
llmRequestId: servant.llmRequestId,
|
|
11075
|
+
llmConversationLength: servant.llmMessages?.length
|
|
10320
11076
|
})),
|
|
10321
11077
|
...extra
|
|
10322
11078
|
};
|
|
@@ -10327,23 +11083,33 @@ function wantsJson(options, command) {
|
|
|
10327
11083
|
function countArgsFromOptions(options) {
|
|
10328
11084
|
return ["3", "4", "5", "6"].filter((key) => options[key]).map((key) => `--${key}`);
|
|
10329
11085
|
}
|
|
11086
|
+
function percentile(values, p) {
|
|
11087
|
+
if (values.length === 0) return 0;
|
|
11088
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
11089
|
+
const rank = Math.ceil(p / 100 * sorted.length);
|
|
11090
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
|
|
11091
|
+
}
|
|
10330
11092
|
function registerOverlordCommands(program3, deps = {}) {
|
|
10331
11093
|
const out = deps.out ?? ((text) => process.stdout.write(text));
|
|
10332
11094
|
const err = deps.err ?? ((text) => process.stderr.write(text));
|
|
10333
11095
|
const cwd = deps.cwd ?? (() => process.cwd());
|
|
10334
11096
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
10335
|
-
const
|
|
10336
|
-
const
|
|
11097
|
+
const fuguApiPreflight = deps.fuguApiPreflight ?? collectFuguApiPreflight;
|
|
11098
|
+
const runFuguApi = deps.runFuguApi ?? defaultRunFuguApi;
|
|
11099
|
+
const fuguApiRetry = fuguApiRetryConfig(deps.sleep ?? defaultSleep3);
|
|
10337
11100
|
const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
|
|
10338
11101
|
const killPid = deps.killPid ?? defaultKillPid;
|
|
10339
|
-
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("--json", "print machine-readable output").action((task = [], options) => {
|
|
11102
|
+
const overlord = program3.command("overlord").description("coordinate one Ultra and normal Fugu servants for hard org work").allowUnknownOption(true).argument("[task...]", "task for the Overlord system").option("--3", "run one Ultra and two normal Fugus").option("--4", "run one Ultra and three normal Fugus").option("--5", "run one Ultra and four normal Fugus").option("--6", "run one Ultra and five normal Fugus").option("--engine <engine>", "servant engine: fugu-api", OVERLORD_DEFAULT_ENGINE).option("--json", "print machine-readable output").action(async (task = [], options) => {
|
|
10340
11103
|
try {
|
|
10341
11104
|
const args = [...countArgsFromOptions(options), ...task];
|
|
10342
11105
|
const root = cwd();
|
|
10343
11106
|
const plan2 = buildOverlordStartupPlan(args, root);
|
|
10344
|
-
const
|
|
11107
|
+
const engine = `${options.engine ?? OVERLORD_DEFAULT_ENGINE}`;
|
|
11108
|
+
if (engine !== "fugu-api") throw new Error("--engine must be fugu-api");
|
|
11109
|
+
if (!options.json) out("Starting Overlord.\nLoading run registry...\nChecking Fugu API...\n");
|
|
11110
|
+
const preflightReport = await fuguApiPreflight();
|
|
10345
11111
|
if (!preflightReport.ok) {
|
|
10346
|
-
err(`${
|
|
11112
|
+
err(`${renderOverlordPreflightFailure(preflightReport)}
|
|
10347
11113
|
`);
|
|
10348
11114
|
process.exitCode = 1;
|
|
10349
11115
|
return;
|
|
@@ -10357,30 +11123,31 @@ function registerOverlordCommands(program3, deps = {}) {
|
|
|
10357
11123
|
task: plan2.task,
|
|
10358
11124
|
cwd: root,
|
|
10359
11125
|
count: plan2.count,
|
|
11126
|
+
engine,
|
|
10360
11127
|
now,
|
|
10361
11128
|
runId: deps.runId,
|
|
10362
11129
|
runToken: deps.runToken
|
|
10363
11130
|
});
|
|
10364
11131
|
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
10373
|
-
}
|
|
11132
|
+
if (!options.json) out(`Summoning ${run.servants.length} Fugu servants...
|
|
11133
|
+
Preparing conversations...
|
|
11134
|
+
`);
|
|
11135
|
+
run = markServantsStarting(run, isoNow(now));
|
|
11136
|
+
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
11137
|
+
run = await startFuguApiServants(run, runFuguApi, now, fuguApiRetry);
|
|
11138
|
+
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
10374
11139
|
if (options.json) out(`${JSON.stringify(runJson(run, { nextPhase: "consult-servants" }), null, 2)}
|
|
10375
11140
|
`);
|
|
10376
11141
|
else {
|
|
11142
|
+
const ready = run.servants.filter((servant) => servant.state === "ready").length;
|
|
10377
11143
|
out(`${[
|
|
10378
|
-
|
|
10379
|
-
`
|
|
11144
|
+
`Ready: ${ready}/${run.servants.length} servants ready.`,
|
|
11145
|
+
`Mission: ${humanSafeText(run.task || "not provided yet")}`,
|
|
10380
11146
|
`Run: ${run.runId}`,
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
"
|
|
11147
|
+
"Engine: Fugu API",
|
|
11148
|
+
"",
|
|
11149
|
+
"Overlord is idle and ready for your instruction.",
|
|
11150
|
+
"Next: I will consult the servants and interview you, then propose a todo list for your approval."
|
|
10384
11151
|
].join("\n")}
|
|
10385
11152
|
`);
|
|
10386
11153
|
}
|
|
@@ -10393,7 +11160,7 @@ function registerOverlordCommands(program3, deps = {}) {
|
|
|
10393
11160
|
process.exitCode = 1;
|
|
10394
11161
|
}
|
|
10395
11162
|
});
|
|
10396
|
-
overlord.command("send").description("queue an assignment or redirect for
|
|
11163
|
+
overlord.command("send").description("queue an assignment or redirect for active Overlord servant conversations").argument("<target>", "servant slot id/name, or all").argument("[message...]", "message to deliver to the servant session").option("--json", "print machine-readable output").action(async (target, message = [], options, command) => {
|
|
10397
11164
|
const json = wantsJson(options, command);
|
|
10398
11165
|
try {
|
|
10399
11166
|
const statePath = defaultOverlordStatePath(cwd());
|
|
@@ -10404,27 +11171,39 @@ function registerOverlordCommands(program3, deps = {}) {
|
|
|
10404
11171
|
if (!hasServantTarget(run, normalized)) throw new Error(`unknown Overlord servant target: ${target}`);
|
|
10405
11172
|
const text = message.join(" ").trim();
|
|
10406
11173
|
if (!text) throw new Error("message is required");
|
|
11174
|
+
const timestamp = isoNow(now);
|
|
10407
11175
|
const queued = {
|
|
10408
11176
|
id: deps.runId?.() ?? defaultMessageId(),
|
|
10409
11177
|
target: normalized,
|
|
10410
11178
|
text,
|
|
10411
|
-
createdAt:
|
|
11179
|
+
createdAt: timestamp,
|
|
11180
|
+
queuedAt: timestamp,
|
|
11181
|
+
state: "queued"
|
|
10412
11182
|
};
|
|
10413
|
-
|
|
11183
|
+
if (run.engine !== "fugu-api") throw new Error("active Overlord run does not use the fugu-api engine");
|
|
11184
|
+
if (!json) out(`Sending assignment to ${normalized}...
|
|
11185
|
+
Awaiting Fugu responses...
|
|
11186
|
+
`);
|
|
11187
|
+
const started = {
|
|
10414
11188
|
...run,
|
|
10415
|
-
updatedAt:
|
|
10416
|
-
messages: [...run.messages ?? [],
|
|
11189
|
+
updatedAt: timestamp,
|
|
11190
|
+
messages: [...(run.messages ?? []).filter((m) => m.id !== queued.id), {
|
|
11191
|
+
...queued,
|
|
11192
|
+
state: "started",
|
|
11193
|
+
startedAt: timestamp
|
|
11194
|
+
}]
|
|
10417
11195
|
};
|
|
10418
|
-
writeOverlordRegistry(statePath, {
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
const
|
|
11196
|
+
writeOverlordRegistry(statePath, { ...registry2, runs: { ...registry2.runs, [run.runId]: started } });
|
|
11197
|
+
const dispatched = await dispatchFuguApiMessage(started, queued, runFuguApi, now, fuguApiRetry);
|
|
11198
|
+
writeOverlordRegistry(statePath, { ...registry2, runs: { ...registry2.runs, [run.runId]: dispatched } });
|
|
11199
|
+
const settled = (dispatched.messages ?? []).find((m) => m.id === queued.id);
|
|
11200
|
+
const ok = settled?.state === "completed" || settled?.state === "partial";
|
|
11201
|
+
const payload = { ok, runId: run.runId, target: normalized, messageId: queued.id, state: settled?.state, responseText: settled?.responseText, failureReason: settled?.failureReason, servantResults: settled?.servantResults, statePath };
|
|
10423
11202
|
if (json) out(`${JSON.stringify(payload, null, 2)}
|
|
10424
11203
|
`);
|
|
10425
|
-
else out(
|
|
10426
|
-
State: ${statePath}
|
|
11204
|
+
else out(`${renderOverlordSendSummary(settled, statePath)}
|
|
10427
11205
|
`);
|
|
11206
|
+
if (!ok) process.exitCode = 1;
|
|
10428
11207
|
} catch (e) {
|
|
10429
11208
|
const messageText = e instanceof Error ? e.message : String(e);
|
|
10430
11209
|
if (json) out(`${JSON.stringify({ ok: false, error: messageText }, null, 2)}
|
|
@@ -10448,11 +11227,77 @@ State: ${payload2.statePath}
|
|
|
10448
11227
|
`);
|
|
10449
11228
|
return;
|
|
10450
11229
|
}
|
|
10451
|
-
const summary = summarizeOverlordRun(run, { isPidAlive });
|
|
10452
|
-
const
|
|
11230
|
+
const summary = summarizeOverlordRun(run, { isPidAlive, now });
|
|
11231
|
+
const current = now();
|
|
11232
|
+
const payload = {
|
|
11233
|
+
...summary,
|
|
11234
|
+
statePath,
|
|
11235
|
+
task: run.task,
|
|
11236
|
+
engine: run.engine,
|
|
11237
|
+
ledgerPath: run.ledgerPath,
|
|
11238
|
+
sessions: run.servants.map((servant) => ({
|
|
11239
|
+
name: servant.name,
|
|
11240
|
+
slotId: servant.slotId,
|
|
11241
|
+
engine: servant.engine,
|
|
11242
|
+
opencodeSessionId: servant.opencodeSessionId,
|
|
11243
|
+
scopeToken: servant.scopeToken,
|
|
11244
|
+
llmModel: servant.llmModel,
|
|
11245
|
+
llmRequestId: servant.llmRequestId,
|
|
11246
|
+
llmConversationLength: servant.llmMessages?.length,
|
|
11247
|
+
eventJournalPath: servant.eventJournalPath,
|
|
11248
|
+
lastEventAt: servant.lastEventAt,
|
|
11249
|
+
lastMessageCompletedAt: servant.lastMessageCompletedAt
|
|
11250
|
+
})),
|
|
11251
|
+
messages: (run.messages ?? []).map((message) => ({
|
|
11252
|
+
id: message.id,
|
|
11253
|
+
target: message.target,
|
|
11254
|
+
state: messageProgress(message, current),
|
|
11255
|
+
queuedAt: message.queuedAt ?? message.createdAt,
|
|
11256
|
+
startedAt: message.startedAt ?? message.deliveredAt,
|
|
11257
|
+
completedAt: message.completedAt,
|
|
11258
|
+
failedAt: message.failedAt,
|
|
11259
|
+
ackText: message.ackText,
|
|
11260
|
+
responseText: message.responseText,
|
|
11261
|
+
eventJournalPath: message.eventJournalPath,
|
|
11262
|
+
failureReason: message.failureReason
|
|
11263
|
+
}))
|
|
11264
|
+
};
|
|
11265
|
+
if (json) out(`${JSON.stringify(payload, null, 2)}
|
|
11266
|
+
`);
|
|
11267
|
+
else out(`${renderOverlordStatus(summary, run, current)}
|
|
11268
|
+
State: ${statePath}
|
|
11269
|
+
`);
|
|
11270
|
+
});
|
|
11271
|
+
overlord.command("collect").description("summarize servant handoff/liveness evidence from the active Overlord journals").option("--json", "print machine-readable output").action((options, command) => {
|
|
11272
|
+
const json = wantsJson(options, command);
|
|
11273
|
+
const statePath = defaultOverlordStatePath(cwd());
|
|
11274
|
+
const registry2 = readOverlordRegistry(statePath);
|
|
11275
|
+
const run = findActiveRun(registry2);
|
|
11276
|
+
if (!run) {
|
|
11277
|
+
const payload2 = { active: false, statePath, message: "no active Overlord run found" };
|
|
11278
|
+
if (json) out(`${JSON.stringify(payload2, null, 2)}
|
|
11279
|
+
`);
|
|
11280
|
+
else out(`No active Overlord run found.
|
|
11281
|
+
State: ${payload2.statePath}
|
|
11282
|
+
`);
|
|
11283
|
+
return;
|
|
11284
|
+
}
|
|
11285
|
+
const current = now();
|
|
11286
|
+
const servants = run.servants.map((servant) => {
|
|
11287
|
+
const journal = servantJournalSummary(servant);
|
|
11288
|
+
return {
|
|
11289
|
+
slotId: servant.slotId,
|
|
11290
|
+
name: servant.name,
|
|
11291
|
+
state: servantProgress(run, servant, current),
|
|
11292
|
+
hasHandoff: journal.hasHandoff,
|
|
11293
|
+
journalPath: servant.journalPath,
|
|
11294
|
+
lines: journal.lines
|
|
11295
|
+
};
|
|
11296
|
+
});
|
|
11297
|
+
const payload = { active: true, runId: run.runId, statePath, servants };
|
|
10453
11298
|
if (json) out(`${JSON.stringify(payload, null, 2)}
|
|
10454
11299
|
`);
|
|
10455
|
-
else out(`${
|
|
11300
|
+
else out(`${servants.map((servant) => `${servant.name}: ${servant.state}; handoff=${servant.hasHandoff ? "yes" : "no"}; ${servant.journalPath}`).join("\n")}
|
|
10456
11301
|
State: ${statePath}
|
|
10457
11302
|
`);
|
|
10458
11303
|
});
|
|
@@ -10502,17 +11347,128 @@ State: ${payload2.statePath}
|
|
|
10502
11347
|
`);
|
|
10503
11348
|
}
|
|
10504
11349
|
});
|
|
11350
|
+
overlord.command("canary").description("native Fugu API smoke: summon a small pool, verify a bounded response, stop cleanly").option("--3", "one Ultra and two normal Fugus").option("--4", "one Ultra and three normal Fugus").option("--5", "one Ultra and four normal Fugus").option("--6", "one Ultra and five normal Fugus").option("--json", "print machine-readable output").option("--repeat <n>", "run the smoke N times and report p50/p90 elapsed latency", "1").action(async (options, command) => {
|
|
11351
|
+
const json = wantsJson(options, command);
|
|
11352
|
+
const repeat = Math.min(Math.max(1, Math.trunc(Number(options.repeat) || 1)), 20);
|
|
11353
|
+
const countArgs = countArgsFromOptions(options);
|
|
11354
|
+
try {
|
|
11355
|
+
if (!json) out("Overlord canary starting.\nChecking Fugu API...\n");
|
|
11356
|
+
const preflight2 = await fuguApiPreflight();
|
|
11357
|
+
if (!preflight2.ok) {
|
|
11358
|
+
if (json) out(`${JSON.stringify({ ok: false, phase: "preflight", problems: preflight2.problems }, null, 2)}
|
|
11359
|
+
`);
|
|
11360
|
+
else err(`${renderOverlordPreflightFailure(preflight2)}
|
|
11361
|
+
`);
|
|
11362
|
+
process.exitCode = 1;
|
|
11363
|
+
return;
|
|
11364
|
+
}
|
|
11365
|
+
const runOnce = async (verbose) => {
|
|
11366
|
+
const startedAtMs = now().getTime();
|
|
11367
|
+
const plan2 = buildOverlordStartupPlan(countArgs, cwd());
|
|
11368
|
+
let run = buildOverlordRun({ task: "overlord canary smoke", cwd: cwd(), count: plan2.count, engine: "fugu-api", now, runId: deps.runId, runToken: deps.runToken });
|
|
11369
|
+
if (verbose) out(`Summoning ${run.servants.length} Fugu servants...
|
|
11370
|
+
`);
|
|
11371
|
+
run = markServantsStarting(run, isoNow(now));
|
|
11372
|
+
run = await startFuguApiServants(run, runFuguApi, now, fuguApiRetry);
|
|
11373
|
+
const ready = run.servants.filter((servant) => servant.state === "ready").length;
|
|
11374
|
+
const problems = [];
|
|
11375
|
+
let responded = 0;
|
|
11376
|
+
let evidence;
|
|
11377
|
+
if (ready === run.servants.length) {
|
|
11378
|
+
if (verbose) out("Sending canary probe...\n");
|
|
11379
|
+
const timestamp = isoNow(now);
|
|
11380
|
+
const probe = {
|
|
11381
|
+
id: `${deps.runId?.() ?? defaultMessageId()}-canary`,
|
|
11382
|
+
target: "all",
|
|
11383
|
+
text: "Reply with the single word READY and nothing else.",
|
|
11384
|
+
createdAt: timestamp,
|
|
11385
|
+
queuedAt: timestamp,
|
|
11386
|
+
state: "queued"
|
|
11387
|
+
};
|
|
11388
|
+
run = await dispatchFuguApiMessage(run, probe, runFuguApi, now, fuguApiRetry);
|
|
11389
|
+
const settled = (run.messages ?? []).find((message) => message.id === probe.id);
|
|
11390
|
+
const results = settled?.servantResults ?? [];
|
|
11391
|
+
responded = results.filter((result) => result.state === "completed" && result.responseText?.trim()).length;
|
|
11392
|
+
evidence = results.find((result) => result.responseText?.trim())?.responseText?.trim();
|
|
11393
|
+
if (responded !== run.servants.length) problems.push(`only ${responded}/${run.servants.length} servants returned text`);
|
|
11394
|
+
} else {
|
|
11395
|
+
problems.push(`only ${ready}/${run.servants.length} servants became ready`);
|
|
11396
|
+
}
|
|
11397
|
+
const stopPlan = planOverlordRunStop(run);
|
|
11398
|
+
for (const pid of stopPlan.killPids) {
|
|
11399
|
+
try {
|
|
11400
|
+
killPid(pid);
|
|
11401
|
+
} catch {
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
11404
|
+
const cleanup = { stopped: stopPlan.killPids.length, uncertain: stopPlan.uncertain.length };
|
|
11405
|
+
if (cleanup.uncertain > 0) problems.push(`${cleanup.uncertain} uncertain resource(s) left untouched`);
|
|
11406
|
+
return { ok: problems.length === 0, runId: run.runId, requested: run.servants.length, ready, responded, elapsedMs: Math.max(0, now().getTime() - startedAtMs), evidence, cleanup, problems };
|
|
11407
|
+
};
|
|
11408
|
+
const runs = [];
|
|
11409
|
+
for (let i = 0; i < repeat; i += 1) {
|
|
11410
|
+
if (!json && repeat > 1) out(`Canary run ${i + 1}/${repeat}...
|
|
11411
|
+
`);
|
|
11412
|
+
runs.push(await runOnce(!json && repeat === 1));
|
|
11413
|
+
}
|
|
11414
|
+
if (repeat === 1) {
|
|
11415
|
+
const r = runs[0];
|
|
11416
|
+
if (json) out(`${JSON.stringify({ ok: r.ok, runId: r.runId, requested: r.requested, ready: r.ready, responded: r.responded, elapsedMs: r.elapsedMs, evidence: r.evidence, cleanup: r.cleanup, problems: r.problems }, null, 2)}
|
|
11417
|
+
`);
|
|
11418
|
+
else out(`${[
|
|
11419
|
+
r.ok ? "Canary OK." : "Canary FAILED.",
|
|
11420
|
+
`Ready: ${r.ready}/${r.requested}; Responded: ${r.responded}/${r.requested}`,
|
|
11421
|
+
`Cleanup: stopped ${r.cleanup.stopped}, uncertain ${r.cleanup.uncertain}`,
|
|
11422
|
+
...r.problems.length ? [`Problems: ${r.problems.join("; ")}`] : []
|
|
11423
|
+
].join("\n")}
|
|
11424
|
+
`);
|
|
11425
|
+
if (!r.ok) process.exitCode = 1;
|
|
11426
|
+
return;
|
|
11427
|
+
}
|
|
11428
|
+
const elapsed = runs.map((r) => r.elapsedMs);
|
|
11429
|
+
const passes = runs.filter((r) => r.ok).length;
|
|
11430
|
+
const requested = runs[0].requested;
|
|
11431
|
+
const ok = passes === runs.length;
|
|
11432
|
+
const summary = {
|
|
11433
|
+
ok,
|
|
11434
|
+
repeat,
|
|
11435
|
+
passes,
|
|
11436
|
+
requested,
|
|
11437
|
+
elapsedMs: { p50: percentile(elapsed, 50), p90: percentile(elapsed, 90), min: Math.min(...elapsed), max: Math.max(...elapsed) },
|
|
11438
|
+
readyRate: runs.reduce((sum, r) => sum + r.ready, 0) / (runs.length * requested),
|
|
11439
|
+
respondedRate: runs.reduce((sum, r) => sum + r.responded, 0) / (runs.length * requested),
|
|
11440
|
+
problems: runs.flatMap((r, i) => r.problems.map((problem) => `run ${i + 1}: ${problem}`))
|
|
11441
|
+
};
|
|
11442
|
+
if (json) out(`${JSON.stringify(summary, null, 2)}
|
|
11443
|
+
`);
|
|
11444
|
+
else out(`${[
|
|
11445
|
+
ok ? `Canary bench OK (${passes}/${repeat} passed).` : `Canary bench: ${passes}/${repeat} passed.`,
|
|
11446
|
+
`Elapsed p50 ${summary.elapsedMs.p50}ms, p90 ${summary.elapsedMs.p90}ms (min ${summary.elapsedMs.min}, max ${summary.elapsedMs.max})`,
|
|
11447
|
+
`Ready rate ${(summary.readyRate * 100).toFixed(0)}%, responded rate ${(summary.respondedRate * 100).toFixed(0)}%`,
|
|
11448
|
+
...summary.problems.length ? [`Problems: ${summary.problems.join("; ")}`] : []
|
|
11449
|
+
].join("\n")}
|
|
11450
|
+
`);
|
|
11451
|
+
if (!ok) process.exitCode = 1;
|
|
11452
|
+
} catch (e) {
|
|
11453
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
11454
|
+
if (json) out(`${JSON.stringify({ ok: false, error: message }, null, 2)}
|
|
11455
|
+
`);
|
|
11456
|
+
else err(`overlord canary: ${message}
|
|
11457
|
+
`);
|
|
11458
|
+
process.exitCode = 1;
|
|
11459
|
+
}
|
|
11460
|
+
});
|
|
10505
11461
|
return overlord;
|
|
10506
11462
|
}
|
|
10507
11463
|
|
|
10508
11464
|
// src/throttle-commands.ts
|
|
10509
|
-
var
|
|
11465
|
+
var import_node_child_process8 = require("node:child_process");
|
|
10510
11466
|
var import_node_fs16 = require("node:fs");
|
|
10511
11467
|
var import_node_path14 = require("node:path");
|
|
10512
11468
|
var THROTTLE_TRACE_REL = (0, import_node_path14.join)(".mmi", "throttle", "trace.jsonl");
|
|
10513
11469
|
function resolveRepoGitRoot(cwd = process.cwd()) {
|
|
10514
11470
|
try {
|
|
10515
|
-
const root = (0,
|
|
11471
|
+
const root = (0, import_node_child_process8.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
10516
11472
|
encoding: "utf8",
|
|
10517
11473
|
timeout: 5e3
|
|
10518
11474
|
}).trim();
|
|
@@ -10661,7 +11617,7 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
|
10661
11617
|
}
|
|
10662
11618
|
|
|
10663
11619
|
// src/board.ts
|
|
10664
|
-
var
|
|
11620
|
+
var import_node_child_process9 = require("node:child_process");
|
|
10665
11621
|
var import_node_util6 = require("node:util");
|
|
10666
11622
|
|
|
10667
11623
|
// src/board-priority.ts
|
|
@@ -10769,7 +11725,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
|
10769
11725
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
10770
11726
|
|
|
10771
11727
|
// src/board.ts
|
|
10772
|
-
var rawExecFileP3 = (0, import_node_util6.promisify)(
|
|
11728
|
+
var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process9.execFile);
|
|
10773
11729
|
var BOARD_GIT_TIMEOUT_MS = 1e4;
|
|
10774
11730
|
var WRITE_PROBE_CONCURRENCY = 8;
|
|
10775
11731
|
var CLAIM_CONCURRENCY = 5;
|
|
@@ -12232,6 +13188,8 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
12232
13188
|
// Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are S3-synced
|
|
12233
13189
|
// via `mmi-cli northstar push` / auto-save on write; never git-tracked (AGENTS.md "Repo cleanliness", #1550, #1842).
|
|
12234
13190
|
"**/plans/",
|
|
13191
|
+
// Superpowers plan/spec output is scratch too; authoritative docs live directly under docs/.
|
|
13192
|
+
"docs/superpowers/",
|
|
12235
13193
|
".playwright-mcp/",
|
|
12236
13194
|
".claude/worktrees/",
|
|
12237
13195
|
// .mmi is agent/CI scratch — ignore the WHOLE tree at any depth (root + cli/.mmi, .github/workflows/.mmi,
|
|
@@ -12776,14 +13734,14 @@ async function auditRepoCi(repo, deps) {
|
|
|
12776
13734
|
if (deployableGated) {
|
|
12777
13735
|
checks.push({
|
|
12778
13736
|
ok: hasGateWorkflow,
|
|
12779
|
-
label:
|
|
12780
|
-
detail: hasGateWorkflow ?
|
|
13737
|
+
label: `gate workflow committed on ${baseBranch}`,
|
|
13738
|
+
detail: hasGateWorkflow ? `read ${PRODUCT_GATE_PATH} at refs/heads/${baseBranch}` : `missing ${PRODUCT_GATE_PATH} at refs/heads/${baseBranch}`,
|
|
12781
13739
|
remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute (seeds gate.yml)`
|
|
12782
13740
|
});
|
|
12783
13741
|
checks.push({
|
|
12784
13742
|
ok: await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF),
|
|
12785
|
-
label:
|
|
12786
|
-
detail: `
|
|
13743
|
+
label: `product ruleset reference committed on ${baseBranch}`,
|
|
13744
|
+
detail: `read ${PRODUCT_RULESET_REF} at refs/heads/${baseBranch}`,
|
|
12787
13745
|
remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute`
|
|
12788
13746
|
});
|
|
12789
13747
|
}
|
|
@@ -13179,9 +14137,9 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
|
|
|
13179
14137
|
|
|
13180
14138
|
// src/gh-create.ts
|
|
13181
14139
|
var import_promises5 = require("node:fs/promises");
|
|
13182
|
-
var
|
|
14140
|
+
var import_node_os3 = require("node:os");
|
|
13183
14141
|
var import_node_path17 = require("node:path");
|
|
13184
|
-
var
|
|
14142
|
+
var import_node_crypto6 = require("node:crypto");
|
|
13185
14143
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
13186
14144
|
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
13187
14145
|
function timeoutKillNote(err, timeoutMs) {
|
|
@@ -13221,7 +14179,7 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
13221
14179
|
} };
|
|
13222
14180
|
const write = deps.write ?? import_promises5.writeFile;
|
|
13223
14181
|
const remove = deps.remove ?? import_promises5.unlink;
|
|
13224
|
-
const file = (0, import_node_path17.join)(deps.dir ?? (0,
|
|
14182
|
+
const file = (0, import_node_path17.join)(deps.dir ?? (0, import_node_os3.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.md`);
|
|
13225
14183
|
await write(file, args[i + 1], "utf8");
|
|
13226
14184
|
return {
|
|
13227
14185
|
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
@@ -14434,7 +15392,7 @@ function designSystemSnapshot(root) {
|
|
|
14434
15392
|
}
|
|
14435
15393
|
|
|
14436
15394
|
// src/design-system-registry.ts
|
|
14437
|
-
var
|
|
15395
|
+
var import_node_crypto7 = require("node:crypto");
|
|
14438
15396
|
var import_node_fs20 = require("node:fs");
|
|
14439
15397
|
var import_node_path19 = require("node:path");
|
|
14440
15398
|
var DESIGN_SYSTEM_CACHE_DIR = ".mmi/design-system/components";
|
|
@@ -14497,7 +15455,7 @@ function scanCachedComponentNames(cacheDir) {
|
|
|
14497
15455
|
return [...names].sort();
|
|
14498
15456
|
}
|
|
14499
15457
|
function contentHash(content) {
|
|
14500
|
-
return (0,
|
|
15458
|
+
return (0, import_node_crypto7.createHash)("sha256").update(content, "utf8").digest("hex");
|
|
14501
15459
|
}
|
|
14502
15460
|
function buildRegistryComponentsCheck(input) {
|
|
14503
15461
|
const base = {
|
|
@@ -14657,6 +15615,17 @@ function renderVerifySecrets(body) {
|
|
|
14657
15615
|
if (ssmStatus !== "Success") {
|
|
14658
15616
|
return { lines, failure: `verify-secrets did not complete (ssm status ${ssmStatus})${body?.commandId ? ` \u2014 command ${body.commandId}` : ""}` };
|
|
14659
15617
|
}
|
|
15618
|
+
if (secrets.length === 0) {
|
|
15619
|
+
const raw = (body?.raw ?? "").trim();
|
|
15620
|
+
if (/no required runtime secrets declared/i.test(raw)) {
|
|
15621
|
+
return { lines: [raw], failure: null };
|
|
15622
|
+
}
|
|
15623
|
+
const detail = raw ? ` \u2014 on-box output: ${raw.replace(/\s+/g, " ").trim()}` : "";
|
|
15624
|
+
return {
|
|
15625
|
+
lines: raw ? raw.split("\n") : ["verify-secrets: no per-key verdicts returned"],
|
|
15626
|
+
failure: `verify-secrets returned no per-key verdicts, so nothing was verified${detail}`
|
|
15627
|
+
};
|
|
15628
|
+
}
|
|
14660
15629
|
const bad = counts.mismatch + counts.missing;
|
|
14661
15630
|
if (bad > 0) {
|
|
14662
15631
|
return { lines, failure: `${bad} of ${secrets.length} required secret(s) not matching the vault (${counts.mismatch} mismatch, ${counts.missing} missing)` };
|
|
@@ -14665,12 +15634,12 @@ function renderVerifySecrets(body) {
|
|
|
14665
15634
|
}
|
|
14666
15635
|
|
|
14667
15636
|
// src/hotfix-coverage.ts
|
|
14668
|
-
var
|
|
15637
|
+
var import_node_child_process10 = require("node:child_process");
|
|
14669
15638
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
14670
15639
|
function checkHotfixCoverage(options = {}) {
|
|
14671
15640
|
const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
|
|
14672
15641
|
const ack = (options.ack ?? []).filter(Boolean);
|
|
14673
|
-
const git = options.git ?? ((args, opts) => (0,
|
|
15642
|
+
const git = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
14674
15643
|
const revList = (range) => {
|
|
14675
15644
|
const out = git(["rev-list", "--no-merges", range]).trim();
|
|
14676
15645
|
return out ? out.split("\n") : [];
|
|
@@ -15483,7 +16452,8 @@ var OVERGRANT_ROLES = /* @__PURE__ */ new Set(["admin", "maintain"]);
|
|
|
15483
16452
|
var REQUIRED_DATA_ACCESS = {
|
|
15484
16453
|
"mutmutco/MM-Chat": [{ name: "kb-projection-reader", dbRole: "kb_reader", vaultParamNeedle: "KB_READ_DB_URL" }]
|
|
15485
16454
|
};
|
|
15486
|
-
function lockedBranches(repoClass) {
|
|
16455
|
+
function lockedBranches(repoClass, releaseTrack) {
|
|
16456
|
+
if (releaseTrack) return branchesForTrack(releaseTrack);
|
|
15487
16457
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
15488
16458
|
}
|
|
15489
16459
|
function safeJson(text, fallback) {
|
|
@@ -15610,14 +16580,15 @@ function auditDataAccessContracts(repo, contracts = { consumers: {} }) {
|
|
|
15610
16580
|
}
|
|
15611
16581
|
return findings;
|
|
15612
16582
|
}
|
|
15613
|
-
async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /* @__PURE__ */ new Set(), dataAccess) {
|
|
16583
|
+
async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /* @__PURE__ */ new Set(), dataAccess, releaseTrack) {
|
|
15614
16584
|
const findings = [];
|
|
15615
16585
|
findings.push(...await auditRepoCollaborators(repo, owners, deps));
|
|
15616
16586
|
if (dataAccess) findings.push(...auditDataAccessContracts(repo, dataAccess));
|
|
15617
|
-
|
|
16587
|
+
const track = releaseTrack ?? (repoClass === "content" ? "trunk" : void 0);
|
|
16588
|
+
for (const branch of lockedBranches(repoClass, track)) {
|
|
15618
16589
|
findings.push(...await auditTrainBranch(repo, branch, owners, deps, projectAdmins));
|
|
15619
16590
|
}
|
|
15620
|
-
return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
|
|
16591
|
+
return { repo, class: repoClass, releaseTrack: track, ok: !findings.some((f) => f.severity === "high"), findings };
|
|
15621
16592
|
}
|
|
15622
16593
|
async function auditOrgBasePermission(deps) {
|
|
15623
16594
|
const org = await restJson2(deps, `orgs/${OWNER}`, {});
|
|
@@ -15638,7 +16609,7 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
|
|
|
15638
16609
|
const orgFindings = await auditOrgBasePermission(deps);
|
|
15639
16610
|
const repos = [];
|
|
15640
16611
|
for (const target of targets) {
|
|
15641
|
-
repos.push(await auditRepoAccess(target.repo, target.class, owners, deps, new Set(entriesValueByCanonicalRepo(matrix, target.repo) ?? []), dataAccess));
|
|
16612
|
+
repos.push(await auditRepoAccess(target.repo, target.class, owners, deps, new Set(entriesValueByCanonicalRepo(matrix, target.repo) ?? []), dataAccess, target.releaseTrack));
|
|
15642
16613
|
}
|
|
15643
16614
|
const ok = orgFindings.every((f) => f.severity !== "high") && repos.every((r) => r.ok);
|
|
15644
16615
|
return { ok, owners: [...owners], orgFindings, repos };
|
|
@@ -15658,7 +16629,8 @@ function loadAccessTargets(projectsJson, fanoutJson) {
|
|
|
15658
16629
|
seen.add(repo);
|
|
15659
16630
|
const repoName = repo.split("/").pop()?.toLowerCase() ?? repo.toLowerCase();
|
|
15660
16631
|
const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) || legacyContentNames.has(repo.toLowerCase()) || legacyContentNames.has(repoName) ? "content" : "deployable";
|
|
15661
|
-
|
|
16632
|
+
const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
|
|
16633
|
+
targets.push({ repo, class: cls, releaseTrack });
|
|
15662
16634
|
}
|
|
15663
16635
|
}
|
|
15664
16636
|
return targets;
|
|
@@ -15708,7 +16680,7 @@ function renderAccessReport(report) {
|
|
|
15708
16680
|
if (finding.remediation) lines.push(` ${finding.remediation}`);
|
|
15709
16681
|
}
|
|
15710
16682
|
for (const repo of report.repos) {
|
|
15711
|
-
lines.push(`${repo.ok ? "OK" : "FLAG"} ${repo.repo} (${repo.class})`);
|
|
16683
|
+
lines.push(`${repo.ok ? "OK" : "FLAG"} ${repo.repo} (${repo.class}${repo.releaseTrack ? `, ${repo.releaseTrack}` : ""})`);
|
|
15712
16684
|
for (const finding of repo.findings) {
|
|
15713
16685
|
lines.push(` [${finding.severity}] ${finding.kind}${finding.branch ? ` @${finding.branch}` : ""}: ${finding.detail}`);
|
|
15714
16686
|
if (finding.remediation) lines.push(` ${finding.remediation}`);
|
|
@@ -16254,6 +17226,23 @@ async function fetchDeployStatusBySlug(slug, deps) {
|
|
|
16254
17226
|
return null;
|
|
16255
17227
|
}
|
|
16256
17228
|
}
|
|
17229
|
+
async function fetchDeployFactsBySlug(slug, deps) {
|
|
17230
|
+
if (!deps.baseUrl || !slug) return null;
|
|
17231
|
+
const token = await deps.token();
|
|
17232
|
+
if (!token) return null;
|
|
17233
|
+
try {
|
|
17234
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-facts`, {
|
|
17235
|
+
method: "GET",
|
|
17236
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
17237
|
+
});
|
|
17238
|
+
if (!res.ok) return null;
|
|
17239
|
+
const body = await res.json();
|
|
17240
|
+
if (!body?.stages) return null;
|
|
17241
|
+
return { slug: body.slug ?? slug, stages: body.stages };
|
|
17242
|
+
} catch {
|
|
17243
|
+
return null;
|
|
17244
|
+
}
|
|
17245
|
+
}
|
|
16257
17246
|
async function fetchOrgConfig(deps) {
|
|
16258
17247
|
if (!deps.baseUrl) return null;
|
|
16259
17248
|
const token = await deps.token();
|
|
@@ -16427,7 +17416,7 @@ function attestedLine(att) {
|
|
|
16427
17416
|
return `App-owned readiness attested by @${att.by} on ${att.at.slice(0, 10)} \u2014 the static checklist is cleared (the doctor reads no product repo files); re-run \`mmi-cli project attest\` after app-owned structural changes.`;
|
|
16428
17417
|
}
|
|
16429
17418
|
var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
|
|
16430
|
-
function appGapsFor(meta, model, slug, projectType) {
|
|
17419
|
+
function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
|
|
16431
17420
|
const attested = appAttestationOf(meta);
|
|
16432
17421
|
const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
|
|
16433
17422
|
const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
|
|
@@ -16479,8 +17468,12 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
16479
17468
|
if (slug === "mmi-katip") {
|
|
16480
17469
|
gaps.push("Katip-specific app plan: declare Google Workspace service-account requirements, use the service account numeric OAuth2 client ID for DWD, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
|
|
16481
17470
|
}
|
|
16482
|
-
if (
|
|
16483
|
-
|
|
17471
|
+
if (model === "solo-container" || model === "tenant-container") {
|
|
17472
|
+
if (typeof mainDeployFact?.port === "number") {
|
|
17473
|
+
gaps.push(`Seed DEPLOY#main healthUrl to http://127.0.0.1:${mainDeployFact.port}/health during bootstrap (tenant reconcile/control health gate \u2014 #1202; port read from DEPLOY#main).`);
|
|
17474
|
+
} else if (!appAttestationOf(meta)) {
|
|
17475
|
+
gaps.push("Seed DEPLOY#main healthUrl from the DEPLOY#main edgeVhost.port during bootstrap (tenant reconcile/control health gate \u2014 #1202); doctor has no committed DEPLOY port to cite yet.");
|
|
17476
|
+
}
|
|
16484
17477
|
}
|
|
16485
17478
|
if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
|
|
16486
17479
|
return gaps;
|
|
@@ -16525,7 +17518,7 @@ function sameNames(a, b) {
|
|
|
16525
17518
|
function sameStageContract(a, b) {
|
|
16526
17519
|
return STAGES.every((stage2) => sameNames(a[stage2], b[stage2]));
|
|
16527
17520
|
}
|
|
16528
|
-
function buildV2HealPatch(repoOrSlug, meta) {
|
|
17521
|
+
function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
|
|
16529
17522
|
const slug = slugOfRepo(repoOrSlug);
|
|
16530
17523
|
const repo = repoFrom(repoOrSlug, slug);
|
|
16531
17524
|
const sub = defaultSubdomain(slug);
|
|
@@ -16563,7 +17556,7 @@ function buildV2HealPatch(repoOrSlug, meta) {
|
|
|
16563
17556
|
patch.requiredRuntimeSecrets = next;
|
|
16564
17557
|
}
|
|
16565
17558
|
}
|
|
16566
|
-
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
17559
|
+
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
16567
17560
|
if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
|
|
16568
17561
|
return { slug, patch, appOwnedGaps };
|
|
16569
17562
|
}
|
|
@@ -16608,7 +17601,8 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
16608
17601
|
const meta = read.project;
|
|
16609
17602
|
const projectType = resolveProjectType(meta, repo);
|
|
16610
17603
|
const model = resolveDeployModel(meta, repo);
|
|
16611
|
-
const
|
|
17604
|
+
const mainDeployFact = meta && deps.getDeployFact ? await deps.getDeployFact(slug, "main") : null;
|
|
17605
|
+
const autoHeal = buildV2HealPatch(repo, meta, mainDeployFact);
|
|
16612
17606
|
if (!meta) {
|
|
16613
17607
|
const emptySecrets = {
|
|
16614
17608
|
dev: { required: [], present: [], missing: [] },
|
|
@@ -16678,48 +17672,194 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
16678
17672
|
appAttested: appAttestationOf(meta) ?? void 0
|
|
16679
17673
|
};
|
|
16680
17674
|
}
|
|
16681
|
-
function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
16682
|
-
const start = "<!-- mmi-v2-readiness:start -->";
|
|
16683
|
-
const end = "<!-- mmi-v2-readiness:end -->";
|
|
16684
|
-
const stageLines = STAGES.map((stage2) => {
|
|
16685
|
-
const coordState = report.hubOwned.deployCoords[stage2];
|
|
16686
|
-
const coords = !coordState.required ? "coords n/a" : coordState.ok ? "coords ok" : "coords missing/unverified";
|
|
16687
|
-
const state = report.hubOwned.deployState[stage2];
|
|
16688
|
-
const statePart = !state.required ? [] : [state.ok ? "deploy state stamped" : "deploy state not stamped"];
|
|
16689
|
-
const missing = report.hubOwned.secrets[stage2].missing;
|
|
16690
|
-
return `- ${stage2}: ${[coords, ...statePart].join("; ")}; ${missing.length ? `missing ${missing.join(", ")}` : "required secret names present"}`;
|
|
17675
|
+
function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
17676
|
+
const start = "<!-- mmi-v2-readiness:start -->";
|
|
17677
|
+
const end = "<!-- mmi-v2-readiness:end -->";
|
|
17678
|
+
const stageLines = STAGES.map((stage2) => {
|
|
17679
|
+
const coordState = report.hubOwned.deployCoords[stage2];
|
|
17680
|
+
const coords = !coordState.required ? "coords n/a" : coordState.ok ? "coords ok" : "coords missing/unverified";
|
|
17681
|
+
const state = report.hubOwned.deployState[stage2];
|
|
17682
|
+
const statePart = !state.required ? [] : [state.ok ? "deploy state stamped" : "deploy state not stamped"];
|
|
17683
|
+
const missing = report.hubOwned.secrets[stage2].missing;
|
|
17684
|
+
return `- ${stage2}: ${[coords, ...statePart].join("; ")}; ${missing.length ? `missing ${missing.join(", ")}` : "required secret names present"}`;
|
|
17685
|
+
});
|
|
17686
|
+
const section = [
|
|
17687
|
+
start,
|
|
17688
|
+
"## Hub v2 readiness",
|
|
17689
|
+
"",
|
|
17690
|
+
`Repo: ${report.repo}`,
|
|
17691
|
+
`Project type: ${report.projectType ?? "(unresolved)"}`,
|
|
17692
|
+
`Deploy model: ${report.deployModel ?? "(unresolved)"}`,
|
|
17693
|
+
`Overall: ${report.ok ? "ready" : "not ready"}`,
|
|
17694
|
+
"",
|
|
17695
|
+
"### Hub-owned diagnosis",
|
|
17696
|
+
`- META: ${report.hubOwned.meta.ok ? "ok" : `missing ${report.hubOwned.meta.missing.join(", ")}`}`,
|
|
17697
|
+
...stageLines,
|
|
17698
|
+
...report.secretsError ? [`- secrets UNVERIFIED (treated as not ready): ${report.secretsError}`] : [],
|
|
17699
|
+
...(report.edgeDomainWarnings ?? []).map(
|
|
17700
|
+
(w) => `- \u26A0 edge domain does not resolve in DNS (advisory): ${w.stage} \u2192 ${w.host}; verify the registry edgeDomains value against the live public host`
|
|
17701
|
+
),
|
|
17702
|
+
...(report.runtimeSecretStreamWarnings ?? []).map(
|
|
17703
|
+
(w) => `- \u26A0 required secrets provisioned but not in requiredRuntimeSecrets (advisory): ${w.stage} \u2192 ${w.names.join(", ")}; add them to the registry stream list or they will not be materialized into tenant.env`
|
|
17704
|
+
),
|
|
17705
|
+
"",
|
|
17706
|
+
"### Auto-heal applied / available",
|
|
17707
|
+
...opts.healed?.length ? opts.healed.map((x) => `- ${x}`) : report.autoHealAvailable.map((x) => `- ${x}`),
|
|
17708
|
+
"",
|
|
17709
|
+
"### App-owned implementation plan",
|
|
17710
|
+
...report.appOwnedGaps.map((x) => `- ${x}`),
|
|
17711
|
+
end
|
|
17712
|
+
].join("\n");
|
|
17713
|
+
const re = new RegExp(`${start}[\\s\\S]*?${end}`);
|
|
17714
|
+
return re.test(existingBody) ? existingBody.replace(re, section) : `${existingBody.trim()}
|
|
17715
|
+
|
|
17716
|
+
${section}`.trim();
|
|
17717
|
+
}
|
|
17718
|
+
|
|
17719
|
+
// src/readiness-audit.ts
|
|
17720
|
+
function publicUrlFromDeployFact(fact) {
|
|
17721
|
+
return fact?.domain ? `https://${fact.domain}` : void 0;
|
|
17722
|
+
}
|
|
17723
|
+
function tenantRuntimeHints(stage2, fact, probe) {
|
|
17724
|
+
const hints = [];
|
|
17725
|
+
if (!fact) {
|
|
17726
|
+
hints.push(`DEPLOY#${stage2} row missing or state-only; seed deploy coords before runtime audit can prove the endpoint.`);
|
|
17727
|
+
return hints;
|
|
17728
|
+
}
|
|
17729
|
+
if (!fact.sshHostPresent && fact.substrate === "hetzner-ssh") hints.push(`DEPLOY#${stage2} has hetzner-ssh substrate but no sshHost presence; tenant-deploy cannot reach the box.`);
|
|
17730
|
+
if (!fact.domain) hints.push(`DEPLOY#${stage2} has no edgeVhost.domain; Cloudflare/Caddy public URL cannot be derived.`);
|
|
17731
|
+
if (typeof fact.port !== "number") hints.push(`DEPLOY#${stage2} has no edgeVhost.port; Caddy upstream/healthUrl hints cannot be checked.`);
|
|
17732
|
+
if (probe?.ok === false || probe?.status != null && probe.status >= 500) hints.push("Public URL probe failed; for Cloudflare 525 check Caddy TLS/origin certificate and Cloudflare SSL mode before changing app code.");
|
|
17733
|
+
if (stage2 === "rc") hints.push("rc runtime is expected to be ephemeral: present between /rcand and /release, then retired after release.");
|
|
17734
|
+
return hints;
|
|
17735
|
+
}
|
|
17736
|
+
function buildTenantRuntimeStatus(input) {
|
|
17737
|
+
const publicUrl = publicUrlFromDeployFact(input.deploy);
|
|
17738
|
+
return {
|
|
17739
|
+
repo: input.repo,
|
|
17740
|
+
slug: input.slug,
|
|
17741
|
+
stage: input.stage,
|
|
17742
|
+
deployRowPresent: Boolean(input.deploy?.present),
|
|
17743
|
+
deploy: input.deploy ?? null,
|
|
17744
|
+
publicUrl,
|
|
17745
|
+
publicProbe: input.publicProbe,
|
|
17746
|
+
lastTenantDeployRun: input.lastTenantDeployRun,
|
|
17747
|
+
expectedEphemeralRc: input.stage === "rc",
|
|
17748
|
+
hints: tenantRuntimeHints(input.stage, input.deploy, input.publicProbe)
|
|
17749
|
+
};
|
|
17750
|
+
}
|
|
17751
|
+
function buildFullTrackReadinessReport(input) {
|
|
17752
|
+
const releaseTrack = resolveReleaseTrack(input.meta, void 0, input.repo);
|
|
17753
|
+
const branches = branchesForTrack(releaseTrack);
|
|
17754
|
+
const reasons = [];
|
|
17755
|
+
if (releaseTrack !== "full") reasons.push(`releaseTrack resolves ${releaseTrack}; set full for development -> rc -> main`);
|
|
17756
|
+
if (!input.trainAuthority?.train) reasons.push("caller does not have train authority");
|
|
17757
|
+
if (!input.deployFacts?.stages.rc?.present) reasons.push("DEPLOY#rc coords missing");
|
|
17758
|
+
const rcRuntime = input.runtime.rc;
|
|
17759
|
+
if (rcRuntime.publicProbe?.ok === false) reasons.push("rc public endpoint probe failed");
|
|
17760
|
+
return {
|
|
17761
|
+
repo: input.repo,
|
|
17762
|
+
slug: input.slug,
|
|
17763
|
+
releaseTrack,
|
|
17764
|
+
branches,
|
|
17765
|
+
trainAuthority: input.trainAuthority,
|
|
17766
|
+
deployFacts: input.deployFacts,
|
|
17767
|
+
rcand: { canApply: reasons.length === 0, reasons },
|
|
17768
|
+
runtime: input.runtime
|
|
17769
|
+
};
|
|
17770
|
+
}
|
|
17771
|
+
|
|
17772
|
+
// src/oauth.ts
|
|
17773
|
+
var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
|
|
17774
|
+
var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
|
|
17775
|
+
var ENV_PREFIXES = ["", "dev", "rc"];
|
|
17776
|
+
var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
17777
|
+
var SSM_ENVS = ["dev", "rc", "main"];
|
|
17778
|
+
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
17779
|
+
var uniq = (xs) => [...new Set(xs)];
|
|
17780
|
+
function defaultSubdomain2(slug) {
|
|
17781
|
+
const i = slug.indexOf("-");
|
|
17782
|
+
return i === -1 ? slug : slug.slice(i + 1);
|
|
17783
|
+
}
|
|
17784
|
+
function expectedHosts(cfg) {
|
|
17785
|
+
const out = [];
|
|
17786
|
+
for (const sub of cfg.subdomains) {
|
|
17787
|
+
for (const domain of cfg.domains) {
|
|
17788
|
+
const base = sub ? `${sub}.${domain}` : domain;
|
|
17789
|
+
for (const env of ENV_PREFIXES) out.push(env ? `${env}.${base}` : base);
|
|
17790
|
+
}
|
|
17791
|
+
}
|
|
17792
|
+
if (cfg.fofuSubdomain !== void 0) {
|
|
17793
|
+
out.push(cfg.fofuSubdomain ? `${cfg.fofuSubdomain}.fofu.ai` : "fofu.ai");
|
|
17794
|
+
}
|
|
17795
|
+
return uniq(out);
|
|
17796
|
+
}
|
|
17797
|
+
function expectedJsOrigins(cfg) {
|
|
17798
|
+
return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
|
|
17799
|
+
}
|
|
17800
|
+
function expectedRedirectUris(cfg) {
|
|
17801
|
+
const { callbackPath } = cfg;
|
|
17802
|
+
return uniq([
|
|
17803
|
+
...expectedHosts(cfg).map((h) => `https://${h}${callbackPath}`),
|
|
17804
|
+
...LOOPBACK.map((l) => `${l}${callbackPath}`)
|
|
17805
|
+
]);
|
|
17806
|
+
}
|
|
17807
|
+
function oauthSsmKeys() {
|
|
17808
|
+
return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
|
|
17809
|
+
}
|
|
17810
|
+
function parseOauthClientJson(input) {
|
|
17811
|
+
let parsed;
|
|
17812
|
+
try {
|
|
17813
|
+
parsed = JSON.parse(input);
|
|
17814
|
+
} catch {
|
|
17815
|
+
throw new Error('not valid JSON \u2014 pipe the Google client JSON (the Console "Download JSON" file)');
|
|
17816
|
+
}
|
|
17817
|
+
const root = parsed ?? {};
|
|
17818
|
+
const obj = root.web ?? root.installed ?? parsed;
|
|
17819
|
+
const clientId = typeof obj?.client_id === "string" ? obj.client_id.trim() : "";
|
|
17820
|
+
const clientSecret = typeof obj?.client_secret === "string" ? obj.client_secret.trim() : "";
|
|
17821
|
+
if (!clientId || !clientSecret) {
|
|
17822
|
+
throw new Error("missing client_id or client_secret in the JSON");
|
|
17823
|
+
}
|
|
17824
|
+
return { clientId, clientSecret };
|
|
17825
|
+
}
|
|
17826
|
+
function parseOauthConfig(mmiConfig, slug) {
|
|
17827
|
+
const rawUnknown = mmiConfig?.oauth;
|
|
17828
|
+
if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
|
|
17829
|
+
if (!rawUnknown || typeof rawUnknown !== "object" || Array.isArray(rawUnknown)) {
|
|
17830
|
+
throw new Error("oauth must be an object when configured");
|
|
17831
|
+
}
|
|
17832
|
+
const raw = rawUnknown;
|
|
17833
|
+
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
|
|
17834
|
+
const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
|
|
17835
|
+
const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
|
|
17836
|
+
if (!callbackPath.startsWith("/")) {
|
|
17837
|
+
throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
|
|
17838
|
+
}
|
|
17839
|
+
if (callbackPath !== DEFAULT_CALLBACK_PATH) {
|
|
17840
|
+
throw new Error(`oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
|
|
17841
|
+
}
|
|
17842
|
+
const meta = mmiConfig ?? {};
|
|
17843
|
+
const rawFofuSub = raw.fofuSubdomain;
|
|
17844
|
+
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
|
|
17845
|
+
return { subdomains, domains, callbackPath, fofuSubdomain };
|
|
17846
|
+
}
|
|
17847
|
+
function probeRedirectUri(callbackPath, port = 9123) {
|
|
17848
|
+
return `http://localhost:${port}${callbackPath}`;
|
|
17849
|
+
}
|
|
17850
|
+
function buildAuthorizeProbeUrl(clientId, redirectUri) {
|
|
17851
|
+
const qs = new URLSearchParams({
|
|
17852
|
+
client_id: clientId,
|
|
17853
|
+
redirect_uri: redirectUri,
|
|
17854
|
+
response_type: "code",
|
|
17855
|
+
scope: "openid email",
|
|
17856
|
+
access_type: "offline",
|
|
17857
|
+
prompt: "consent"
|
|
16691
17858
|
});
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
`Repo: ${report.repo}`,
|
|
16697
|
-
`Project type: ${report.projectType ?? "(unresolved)"}`,
|
|
16698
|
-
`Deploy model: ${report.deployModel ?? "(unresolved)"}`,
|
|
16699
|
-
`Overall: ${report.ok ? "ready" : "not ready"}`,
|
|
16700
|
-
"",
|
|
16701
|
-
"### Hub-owned diagnosis",
|
|
16702
|
-
`- META: ${report.hubOwned.meta.ok ? "ok" : `missing ${report.hubOwned.meta.missing.join(", ")}`}`,
|
|
16703
|
-
...stageLines,
|
|
16704
|
-
...report.secretsError ? [`- secrets UNVERIFIED (treated as not ready): ${report.secretsError}`] : [],
|
|
16705
|
-
...(report.edgeDomainWarnings ?? []).map(
|
|
16706
|
-
(w) => `- \u26A0 edge domain does not resolve in DNS (advisory): ${w.stage} \u2192 ${w.host}; verify the registry edgeDomains value against the live public host`
|
|
16707
|
-
),
|
|
16708
|
-
...(report.runtimeSecretStreamWarnings ?? []).map(
|
|
16709
|
-
(w) => `- \u26A0 required secrets provisioned but not in requiredRuntimeSecrets (advisory): ${w.stage} \u2192 ${w.names.join(", ")}; add them to the registry stream list or they will not be materialized into tenant.env`
|
|
16710
|
-
),
|
|
16711
|
-
"",
|
|
16712
|
-
"### Auto-heal applied / available",
|
|
16713
|
-
...opts.healed?.length ? opts.healed.map((x) => `- ${x}`) : report.autoHealAvailable.map((x) => `- ${x}`),
|
|
16714
|
-
"",
|
|
16715
|
-
"### App-owned implementation plan",
|
|
16716
|
-
...report.appOwnedGaps.map((x) => `- ${x}`),
|
|
16717
|
-
end
|
|
16718
|
-
].join("\n");
|
|
16719
|
-
const re = new RegExp(`${start}[\\s\\S]*?${end}`);
|
|
16720
|
-
return re.test(existingBody) ? existingBody.replace(re, section) : `${existingBody.trim()}
|
|
16721
|
-
|
|
16722
|
-
${section}`.trim();
|
|
17859
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${qs.toString()}`;
|
|
17860
|
+
}
|
|
17861
|
+
function authorizeBodyHasMismatch(body) {
|
|
17862
|
+
return /redirect_uri_mismatch/i.test(body);
|
|
16723
17863
|
}
|
|
16724
17864
|
|
|
16725
17865
|
// src/project-set.ts
|
|
@@ -16843,7 +17983,7 @@ function parseOauthVar(raw) {
|
|
|
16843
17983
|
try {
|
|
16844
17984
|
parsed = JSON.parse(raw);
|
|
16845
17985
|
} catch {
|
|
16846
|
-
throw new Error(
|
|
17986
|
+
throw new Error(`project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}`);
|
|
16847
17987
|
}
|
|
16848
17988
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
16849
17989
|
throw new Error("project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
|
|
@@ -16858,7 +17998,11 @@ function parseOauthVar(raw) {
|
|
|
16858
17998
|
out[key] = value.map((v) => v.trim());
|
|
16859
17999
|
} else if (key === "callbackPath") {
|
|
16860
18000
|
if (typeof value !== "string" || !value.trim()) throw new Error("project set: oauth.callbackPath must be a non-empty string");
|
|
16861
|
-
|
|
18001
|
+
const callbackPath = value.trim();
|
|
18002
|
+
if (callbackPath !== DEFAULT_CALLBACK_PATH) {
|
|
18003
|
+
throw new Error(`project set: oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
|
|
18004
|
+
}
|
|
18005
|
+
out.callbackPath = callbackPath;
|
|
16862
18006
|
} else if (key === "fofuSubdomain") {
|
|
16863
18007
|
if (typeof value !== "string") throw new Error('project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
|
|
16864
18008
|
out.fofuSubdomain = value.trim();
|
|
@@ -17196,14 +18340,14 @@ function parseKbTree(stdout, prefix) {
|
|
|
17196
18340
|
|
|
17197
18341
|
// src/northstar-commands.ts
|
|
17198
18342
|
var import_node_fs22 = require("node:fs");
|
|
17199
|
-
var
|
|
18343
|
+
var import_node_child_process11 = require("node:child_process");
|
|
17200
18344
|
var import_promises6 = require("node:fs/promises");
|
|
17201
18345
|
var planSyncDetached = false;
|
|
17202
18346
|
function detachPlanSync() {
|
|
17203
18347
|
if (planSyncDetached) return;
|
|
17204
18348
|
planSyncDetached = true;
|
|
17205
18349
|
try {
|
|
17206
|
-
(0,
|
|
18350
|
+
(0, import_node_child_process11.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
|
|
17207
18351
|
detached: true,
|
|
17208
18352
|
stdio: "ignore",
|
|
17209
18353
|
windowsHide: true,
|
|
@@ -17289,12 +18433,13 @@ function openInEditor(path2) {
|
|
|
17289
18433
|
return;
|
|
17290
18434
|
}
|
|
17291
18435
|
try {
|
|
17292
|
-
(0,
|
|
18436
|
+
(0, import_node_child_process11.spawn)(editor, [path2], { stdio: "inherit" });
|
|
17293
18437
|
} catch {
|
|
17294
18438
|
console.log(`open ${path2} manually`);
|
|
17295
18439
|
}
|
|
17296
18440
|
}
|
|
17297
18441
|
async function withPlan(quiet, run, io = consoleIo) {
|
|
18442
|
+
if (!await requireContinuityAccess("northstar", { quiet }, io)) return;
|
|
17298
18443
|
const cfg = await loadConfig();
|
|
17299
18444
|
if (!cfg.sagaApiUrl) {
|
|
17300
18445
|
if (!quiet) fail("plan: Hub API URL not configured");
|
|
@@ -17303,6 +18448,7 @@ async function withPlan(quiet, run, io = consoleIo) {
|
|
|
17303
18448
|
await run(makePlanDeps(cfg, io));
|
|
17304
18449
|
}
|
|
17305
18450
|
async function runPlanAutosave(io = consoleIo, opts = {}) {
|
|
18451
|
+
if (!await requireContinuityAccess("northstar", { quiet: opts.quiet }, io)) return [];
|
|
17306
18452
|
const cfg = await loadConfig();
|
|
17307
18453
|
if (!cfg.sagaApiUrl) {
|
|
17308
18454
|
if (!opts.quiet) fail("plan: Hub API URL not configured");
|
|
@@ -17640,6 +18786,11 @@ async function secretsList(deps, opts) {
|
|
|
17640
18786
|
const { secrets } = await res.json();
|
|
17641
18787
|
deps.log(formatSecretList(secrets ?? []));
|
|
17642
18788
|
}
|
|
18789
|
+
var CAPABILITIES_TIMEOUT_MS = 2e4;
|
|
18790
|
+
function isTimeoutError(e) {
|
|
18791
|
+
const name = e?.name;
|
|
18792
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
18793
|
+
}
|
|
17643
18794
|
function formatCapabilities(r) {
|
|
17644
18795
|
const head = `@${r.login} on ${r.repo} \u2014 ${r.role}`;
|
|
17645
18796
|
const items = [...r.capabilities ?? []].sort((a, b) => a.scope.localeCompare(b.scope));
|
|
@@ -17670,10 +18821,15 @@ async function secretsCapabilities(deps, opts) {
|
|
|
17670
18821
|
res = await deps.fetch(`${deps.apiUrl}/secrets/capabilities?${qs}`, {
|
|
17671
18822
|
method: "GET",
|
|
17672
18823
|
headers: await deps.headers(),
|
|
17673
|
-
signal: AbortSignal.timeout(
|
|
18824
|
+
signal: AbortSignal.timeout(CAPABILITIES_TIMEOUT_MS)
|
|
17674
18825
|
});
|
|
17675
18826
|
} catch (e) {
|
|
17676
|
-
|
|
18827
|
+
const message = e.message;
|
|
18828
|
+
if (isTimeoutError(e)) {
|
|
18829
|
+
deps.err(`access capabilities: timed out after ${CAPABILITIES_TIMEOUT_MS}ms while aggregating vault scopes for ${repo}. No access conclusion was made; retry with a warm Hub or run scoped reads such as \`mmi-cli secrets list --repo ${repo}\` while investigating the slow source.`);
|
|
18830
|
+
return;
|
|
18831
|
+
}
|
|
18832
|
+
deps.err(`access capabilities: ${message}`);
|
|
17677
18833
|
return;
|
|
17678
18834
|
}
|
|
17679
18835
|
if (!res.ok) {
|
|
@@ -18297,102 +19453,12 @@ function registerEdgeCommands(program3) {
|
|
|
18297
19453
|
});
|
|
18298
19454
|
}
|
|
18299
19455
|
|
|
18300
|
-
// src/oauth.ts
|
|
18301
|
-
var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
|
|
18302
|
-
var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
|
|
18303
|
-
var ENV_PREFIXES = ["", "dev", "rc"];
|
|
18304
|
-
var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
18305
|
-
var SSM_ENVS = ["dev", "rc", "main"];
|
|
18306
|
-
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
18307
|
-
var uniq = (xs) => [...new Set(xs)];
|
|
18308
|
-
function defaultSubdomain2(slug) {
|
|
18309
|
-
const i = slug.indexOf("-");
|
|
18310
|
-
return i === -1 ? slug : slug.slice(i + 1);
|
|
18311
|
-
}
|
|
18312
|
-
function expectedHosts(cfg) {
|
|
18313
|
-
const out = [];
|
|
18314
|
-
for (const sub of cfg.subdomains) {
|
|
18315
|
-
for (const domain of cfg.domains) {
|
|
18316
|
-
const base = sub ? `${sub}.${domain}` : domain;
|
|
18317
|
-
for (const env of ENV_PREFIXES) out.push(env ? `${env}.${base}` : base);
|
|
18318
|
-
}
|
|
18319
|
-
}
|
|
18320
|
-
if (cfg.fofuSubdomain !== void 0) {
|
|
18321
|
-
out.push(cfg.fofuSubdomain ? `${cfg.fofuSubdomain}.fofu.ai` : "fofu.ai");
|
|
18322
|
-
}
|
|
18323
|
-
return uniq(out);
|
|
18324
|
-
}
|
|
18325
|
-
function expectedJsOrigins(cfg) {
|
|
18326
|
-
return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
|
|
18327
|
-
}
|
|
18328
|
-
function expectedRedirectUris(cfg) {
|
|
18329
|
-
const { callbackPath } = cfg;
|
|
18330
|
-
return uniq([
|
|
18331
|
-
...expectedHosts(cfg).map((h) => `https://${h}${callbackPath}`),
|
|
18332
|
-
...LOOPBACK.map((l) => `${l}${callbackPath}`)
|
|
18333
|
-
]);
|
|
18334
|
-
}
|
|
18335
|
-
function oauthSsmKeys() {
|
|
18336
|
-
return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
|
|
18337
|
-
}
|
|
18338
|
-
function parseOauthClientJson(input) {
|
|
18339
|
-
let parsed;
|
|
18340
|
-
try {
|
|
18341
|
-
parsed = JSON.parse(input);
|
|
18342
|
-
} catch {
|
|
18343
|
-
throw new Error('not valid JSON \u2014 pipe the Google client JSON (the Console "Download JSON" file)');
|
|
18344
|
-
}
|
|
18345
|
-
const root = parsed ?? {};
|
|
18346
|
-
const obj = root.web ?? root.installed ?? parsed;
|
|
18347
|
-
const clientId = typeof obj?.client_id === "string" ? obj.client_id.trim() : "";
|
|
18348
|
-
const clientSecret = typeof obj?.client_secret === "string" ? obj.client_secret.trim() : "";
|
|
18349
|
-
if (!clientId || !clientSecret) {
|
|
18350
|
-
throw new Error("missing client_id or client_secret in the JSON");
|
|
18351
|
-
}
|
|
18352
|
-
return { clientId, clientSecret };
|
|
18353
|
-
}
|
|
18354
|
-
function parseOauthConfig(mmiConfig, slug) {
|
|
18355
|
-
const rawUnknown = mmiConfig?.oauth;
|
|
18356
|
-
if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
|
|
18357
|
-
if (!rawUnknown || typeof rawUnknown !== "object" || Array.isArray(rawUnknown)) {
|
|
18358
|
-
throw new Error("oauth must be an object when configured");
|
|
18359
|
-
}
|
|
18360
|
-
const raw = rawUnknown;
|
|
18361
|
-
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
|
|
18362
|
-
const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
|
|
18363
|
-
const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
|
|
18364
|
-
if (!callbackPath.startsWith("/")) {
|
|
18365
|
-
throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
|
|
18366
|
-
}
|
|
18367
|
-
const meta = mmiConfig ?? {};
|
|
18368
|
-
const rawFofuSub = raw.fofuSubdomain;
|
|
18369
|
-
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
|
|
18370
|
-
return { subdomains, domains, callbackPath, fofuSubdomain };
|
|
18371
|
-
}
|
|
18372
|
-
function probeRedirectUri(callbackPath, port = 9123) {
|
|
18373
|
-
return `http://localhost:${port}${callbackPath}`;
|
|
18374
|
-
}
|
|
18375
|
-
function buildAuthorizeProbeUrl(clientId, redirectUri) {
|
|
18376
|
-
const qs = new URLSearchParams({
|
|
18377
|
-
client_id: clientId,
|
|
18378
|
-
redirect_uri: redirectUri,
|
|
18379
|
-
response_type: "code",
|
|
18380
|
-
scope: "openid email",
|
|
18381
|
-
access_type: "offline",
|
|
18382
|
-
prompt: "consent"
|
|
18383
|
-
});
|
|
18384
|
-
return `https://accounts.google.com/o/oauth2/v2/auth?${qs.toString()}`;
|
|
18385
|
-
}
|
|
18386
|
-
function authorizeBodyHasMismatch(body) {
|
|
18387
|
-
return /redirect_uri_mismatch/i.test(body);
|
|
18388
|
-
}
|
|
18389
|
-
|
|
18390
19456
|
// src/doctor-run.ts
|
|
18391
19457
|
var import_node_fs28 = require("node:fs");
|
|
18392
|
-
var
|
|
19458
|
+
var import_node_child_process13 = require("node:child_process");
|
|
18393
19459
|
var import_promises7 = require("node:fs/promises");
|
|
18394
19460
|
var import_node_path25 = require("node:path");
|
|
18395
|
-
var
|
|
19461
|
+
var import_node_os5 = require("node:os");
|
|
18396
19462
|
|
|
18397
19463
|
// src/plugin-guard.ts
|
|
18398
19464
|
function buildPluginGuardDecision(i) {
|
|
@@ -18412,9 +19478,9 @@ function buildGuardSessionStartLine(state, opts = {}) {
|
|
|
18412
19478
|
}
|
|
18413
19479
|
|
|
18414
19480
|
// src/cursor-plugin-seed.ts
|
|
18415
|
-
var
|
|
19481
|
+
var import_node_child_process12 = require("node:child_process");
|
|
18416
19482
|
var import_node_fs24 = require("node:fs");
|
|
18417
|
-
var
|
|
19483
|
+
var import_node_os4 = require("node:os");
|
|
18418
19484
|
var import_node_path22 = require("node:path");
|
|
18419
19485
|
var import_node_util7 = require("node:util");
|
|
18420
19486
|
function isSemverVersion(v) {
|
|
@@ -18423,7 +19489,7 @@ function isSemverVersion(v) {
|
|
|
18423
19489
|
var MMI_HUB_REPO = "mutmutco/MMI-Hub";
|
|
18424
19490
|
var CURSOR_THIRD_PARTY_STATE_KEY = "cursor/thirdPartyExtensibilityEnabled";
|
|
18425
19491
|
var PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
|
|
18426
|
-
var execFileBuffer = (0, import_node_util7.promisify)(
|
|
19492
|
+
var execFileBuffer = (0, import_node_util7.promisify)(import_node_child_process12.execFile);
|
|
18427
19493
|
function gitFetchReleaseTagArgs(hubCheckout, tag) {
|
|
18428
19494
|
return ["-C", hubCheckout, "fetch", "origin", "tag", tag, "--quiet"];
|
|
18429
19495
|
}
|
|
@@ -18432,13 +19498,13 @@ function ghReleaseTarballApiArgs(tag) {
|
|
|
18432
19498
|
}
|
|
18433
19499
|
function cursorUserGlobalStatePath() {
|
|
18434
19500
|
if (process.platform === "win32") {
|
|
18435
|
-
const base = process.env.APPDATA || (0, import_node_path22.join)((0,
|
|
19501
|
+
const base = process.env.APPDATA || (0, import_node_path22.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
|
|
18436
19502
|
return (0, import_node_path22.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
18437
19503
|
}
|
|
18438
19504
|
if (process.platform === "darwin") {
|
|
18439
|
-
return (0, import_node_path22.join)((0,
|
|
19505
|
+
return (0, import_node_path22.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
18440
19506
|
}
|
|
18441
|
-
return (0, import_node_path22.join)((0,
|
|
19507
|
+
return (0, import_node_path22.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
18442
19508
|
}
|
|
18443
19509
|
async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
18444
19510
|
const dbPath = cursorUserGlobalStatePath();
|
|
@@ -18594,6 +19660,17 @@ function buildContinuityFreshnessCheck(input) {
|
|
|
18594
19660
|
fix: `local branch work is newer than ${stale.join(" + ")} continuity; run ${commands.join(" and ")}`
|
|
18595
19661
|
};
|
|
18596
19662
|
}
|
|
19663
|
+
function buildContinuityRestrictionNotice(input) {
|
|
19664
|
+
const login = input.login?.trim() || "unknown";
|
|
19665
|
+
return {
|
|
19666
|
+
ok: true,
|
|
19667
|
+
label: "saga / North Star continuity (Jervaise-only)",
|
|
19668
|
+
fix: `continuity tools (saga, North Star, /handoff) are restricted to ${CONTINUITY_OWNER_LOGIN}; not available to ${login}. Your durable record is the board issue, PR, git history, and MM-KB. Hooks no-op these verbs for you and SessionStart omits the continuity steps. If stray local continuity state exists (.mmi/.session, .mmi/saga-pending.jsonl), remove it with: mmi-cli gc --scratch --apply (safe: it never touches config.json or a live queue).`
|
|
19669
|
+
};
|
|
19670
|
+
}
|
|
19671
|
+
function continuityDoctorCheckForLogin(login) {
|
|
19672
|
+
return login?.trim().toLowerCase() === CONTINUITY_OWNER_LOGIN ? "freshness" : "restriction";
|
|
19673
|
+
}
|
|
18597
19674
|
var MMI_PLUGIN_ID = "mmi@mutmutco";
|
|
18598
19675
|
var LEGACY_MMI_PLUGIN_ID = "mmi@mmi";
|
|
18599
19676
|
var LEGACY_MMI_MARKETPLACE = "mmi";
|
|
@@ -19553,7 +20630,7 @@ function buildBrowserArtifactsCheck(input) {
|
|
|
19553
20630
|
};
|
|
19554
20631
|
}
|
|
19555
20632
|
var KB_DRIFT_ADVISORY_LABEL = "KB registry-fact drift (advisory)";
|
|
19556
|
-
var KB_DRIFT_ADVISORY_FIX = "review kb-drift report on Hub (`node infra/saga-io.mjs get kb-drift/<date>.json`)
|
|
20633
|
+
var KB_DRIFT_ADVISORY_FIX = "review kb-drift report on Hub (`node infra/saga-io.mjs get kb-drift/<date>.json`); registry-owned facts belong in `mmi-cli project get`, not copied literals in MM-KB";
|
|
19557
20634
|
function buildKbDriftAdvisoryCheck(input) {
|
|
19558
20635
|
const base = {
|
|
19559
20636
|
ok: true,
|
|
@@ -20036,7 +21113,7 @@ function reexecMmiCli(args) {
|
|
|
20036
21113
|
}
|
|
20037
21114
|
};
|
|
20038
21115
|
const env = { ...process.env, [DOCTOR_POST_SELF_UPDATE_ENV]: "1" };
|
|
20039
|
-
const child = isWin ? (0,
|
|
21116
|
+
const child = isWin ? (0, import_node_child_process13.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", env }) : (0, import_node_child_process13.spawn)("mmi-cli", args, { stdio: "inherit", env });
|
|
20040
21117
|
child.on("error", () => done(-1));
|
|
20041
21118
|
child.on("exit", (code) => done(code ?? 0));
|
|
20042
21119
|
});
|
|
@@ -20093,7 +21170,7 @@ async function applyPluginHeal(token, surface, log, opts) {
|
|
|
20093
21170
|
}
|
|
20094
21171
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
20095
21172
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
20096
|
-
return (0, import_node_path25.join)((0,
|
|
21173
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
20097
21174
|
};
|
|
20098
21175
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
20099
21176
|
try {
|
|
@@ -20108,13 +21185,13 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
20108
21185
|
return {
|
|
20109
21186
|
isOrgRepo,
|
|
20110
21187
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
20111
|
-
marketplaceClonePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0,
|
|
20112
|
-
pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0,
|
|
21188
|
+
marketplaceClonePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
|
|
21189
|
+
pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
20113
21190
|
};
|
|
20114
21191
|
}
|
|
20115
21192
|
function installedPluginSources() {
|
|
20116
21193
|
return ["claude", "codex"].map((surface) => {
|
|
20117
|
-
const recordPath = (0, import_node_path25.join)((0,
|
|
21194
|
+
const recordPath = (0, import_node_path25.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
20118
21195
|
try {
|
|
20119
21196
|
return { surface, installed: JSON.parse((0, import_node_fs28.readFileSync)(recordPath, "utf8")), recordPath };
|
|
20120
21197
|
} catch {
|
|
@@ -20169,7 +21246,7 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
20169
21246
|
}
|
|
20170
21247
|
}
|
|
20171
21248
|
function opencodeConfigDir() {
|
|
20172
|
-
return (0, import_node_path25.join)((0,
|
|
21249
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".config", "opencode");
|
|
20173
21250
|
}
|
|
20174
21251
|
function opencodeConfigPath() {
|
|
20175
21252
|
return (0, import_node_path25.join)(opencodeConfigDir(), "opencode.jsonc");
|
|
@@ -20255,7 +21332,7 @@ function writeOpencodeCommandFiles() {
|
|
|
20255
21332
|
function readOpencodeAdapterDiskVersion() {
|
|
20256
21333
|
const candidates = [
|
|
20257
21334
|
(0, import_node_path25.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
|
|
20258
|
-
(0, import_node_path25.join)((0,
|
|
21335
|
+
(0, import_node_path25.join)((0, import_node_os5.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
|
|
20259
21336
|
];
|
|
20260
21337
|
for (const path2 of candidates) {
|
|
20261
21338
|
try {
|
|
@@ -20290,13 +21367,13 @@ function opencodePluginVersionsForReport() {
|
|
|
20290
21367
|
}
|
|
20291
21368
|
function opencodeDesktopLogsRoot() {
|
|
20292
21369
|
if (process.platform === "win32") {
|
|
20293
|
-
const base = process.env.APPDATA || (0, import_node_path25.join)((0,
|
|
21370
|
+
const base = process.env.APPDATA || (0, import_node_path25.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
|
|
20294
21371
|
return (0, import_node_path25.join)(base, "ai.opencode.desktop", "logs");
|
|
20295
21372
|
}
|
|
20296
21373
|
if (process.platform === "darwin") {
|
|
20297
|
-
return (0, import_node_path25.join)((0,
|
|
21374
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
|
|
20298
21375
|
}
|
|
20299
|
-
return (0, import_node_path25.join)((0,
|
|
21376
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".config", "ai.opencode.desktop", "logs");
|
|
20300
21377
|
}
|
|
20301
21378
|
function opencodeDesktopBootstrapSnapshot() {
|
|
20302
21379
|
const root = opencodeDesktopLogsRoot();
|
|
@@ -20312,7 +21389,7 @@ function opencodeDesktopBootstrapSnapshot() {
|
|
|
20312
21389
|
}
|
|
20313
21390
|
}
|
|
20314
21391
|
function opencodeLegacyConfigSnapshot() {
|
|
20315
|
-
const legacyPath = (0, import_node_path25.join)((0,
|
|
21392
|
+
const legacyPath = (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".opencode", "opencode.json");
|
|
20316
21393
|
if (!(0, import_node_fs28.existsSync)(legacyPath)) return {};
|
|
20317
21394
|
const content = readTextFile(legacyPath);
|
|
20318
21395
|
if (content == null) return {};
|
|
@@ -20333,7 +21410,7 @@ function quarantineOpencodeLegacyConfig(legacyPath) {
|
|
|
20333
21410
|
}
|
|
20334
21411
|
}
|
|
20335
21412
|
function cursorPluginCacheRoot() {
|
|
20336
|
-
return (0, import_node_path25.join)((0,
|
|
21413
|
+
return (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
|
|
20337
21414
|
}
|
|
20338
21415
|
function cursorPluginCachePinSnapshots() {
|
|
20339
21416
|
const root = cursorPluginCacheRoot();
|
|
@@ -20379,8 +21456,8 @@ function hubCheckoutForCursorSeed() {
|
|
|
20379
21456
|
}
|
|
20380
21457
|
function mmiPluginCacheRootSnapshots() {
|
|
20381
21458
|
const roots = [
|
|
20382
|
-
{ surface: "claude", root: (0, import_node_path25.join)((0,
|
|
20383
|
-
{ surface: "codex", root: (0, import_node_path25.join)((0,
|
|
21459
|
+
{ surface: "claude", root: (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
|
|
21460
|
+
{ surface: "codex", root: (0, import_node_path25.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
|
|
20384
21461
|
];
|
|
20385
21462
|
return roots.flatMap(({ surface, root }) => {
|
|
20386
21463
|
try {
|
|
@@ -20444,7 +21521,7 @@ async function clearNestedPluginTreeDir(targetPath) {
|
|
|
20444
21521
|
try {
|
|
20445
21522
|
if (!(0, import_node_fs28.existsSync)(targetPath)) return true;
|
|
20446
21523
|
if (isWin) {
|
|
20447
|
-
const emptyDir = (0, import_node_path25.join)((0,
|
|
21524
|
+
const emptyDir = (0, import_node_path25.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
20448
21525
|
(0, import_node_fs28.mkdirSync)(emptyDir, { recursive: true });
|
|
20449
21526
|
try {
|
|
20450
21527
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
@@ -20482,7 +21559,7 @@ function readTextFile(path2) {
|
|
|
20482
21559
|
}
|
|
20483
21560
|
function playwrightMcpConfigSnapshots() {
|
|
20484
21561
|
const cwd = process.cwd();
|
|
20485
|
-
const home = (0,
|
|
21562
|
+
const home = (0, import_node_os5.homedir)();
|
|
20486
21563
|
const candidates = [
|
|
20487
21564
|
(0, import_node_path25.join)(cwd, ".mcp.json"),
|
|
20488
21565
|
(0, import_node_path25.join)(cwd, ".cursor", "mcp.json"),
|
|
@@ -20995,7 +22072,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20995
22072
|
releasedVersion,
|
|
20996
22073
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
20997
22074
|
execFileP: execFileP2,
|
|
20998
|
-
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0,
|
|
22075
|
+
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0, import_node_os5.tmpdir)(), prefix)),
|
|
20999
22076
|
log: (m) => io.err(m)
|
|
21000
22077
|
});
|
|
21001
22078
|
if (seeded) {
|
|
@@ -21087,15 +22164,19 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
21087
22164
|
}
|
|
21088
22165
|
}
|
|
21089
22166
|
if (runExtended && !opts.banner) {
|
|
21090
|
-
|
|
21091
|
-
|
|
21092
|
-
|
|
21093
|
-
|
|
21094
|
-
|
|
21095
|
-
|
|
21096
|
-
|
|
21097
|
-
|
|
21098
|
-
|
|
22167
|
+
if (continuityDoctorCheckForLogin(login) === "freshness") {
|
|
22168
|
+
const continuity = readContinuityStamp();
|
|
22169
|
+
checks.push(
|
|
22170
|
+
buildContinuityFreshnessCheck({
|
|
22171
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
22172
|
+
latestWorkAt: await latestBranchWorkAt(),
|
|
22173
|
+
lastSagaNoteAt: continuity.lastSagaNoteAt,
|
|
22174
|
+
lastNorthstarAt: latestNorthstarContinuityAt()
|
|
22175
|
+
})
|
|
22176
|
+
);
|
|
22177
|
+
} else {
|
|
22178
|
+
checks.push(buildContinuityRestrictionNotice({ login: login?.trim() }));
|
|
22179
|
+
}
|
|
21099
22180
|
}
|
|
21100
22181
|
if (runExtended) {
|
|
21101
22182
|
const dashboardConsumer = await resolveDashboardConsumer(cfg);
|
|
@@ -21193,7 +22274,7 @@ function mergeGuardHook(settings) {
|
|
|
21193
22274
|
next.hooks = hooks;
|
|
21194
22275
|
return next;
|
|
21195
22276
|
}
|
|
21196
|
-
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0,
|
|
22277
|
+
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0, import_node_os5.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
|
|
21197
22278
|
function ensureUserScopeGuardHook(opts = {}) {
|
|
21198
22279
|
const path2 = opts.settingsPath ?? userScopeSettingsPath();
|
|
21199
22280
|
try {
|
|
@@ -21407,7 +22488,7 @@ async function requireFreshTrainCli(commandName) {
|
|
|
21407
22488
|
throw new Error(`running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; run doctor/update first so ${commandName} uses the current train path`);
|
|
21408
22489
|
}
|
|
21409
22490
|
var program2 = new Command();
|
|
21410
|
-
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery,
|
|
22491
|
+
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
|
|
21411
22492
|
async function runRulesSync(opts, io = consoleIo) {
|
|
21412
22493
|
const cfg = await loadConfig();
|
|
21413
22494
|
if (isRulesSource(cfg.orgRulesSource)) {
|
|
@@ -21650,7 +22731,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
21650
22731
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
21651
22732
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
21652
22733
|
return new Promise((resolve6, reject) => {
|
|
21653
|
-
const child = (0,
|
|
22734
|
+
const child = (0, import_node_child_process14.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
21654
22735
|
const timer = setTimeout(() => {
|
|
21655
22736
|
try {
|
|
21656
22737
|
child.kill();
|
|
@@ -21868,7 +22949,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
21868
22949
|
try {
|
|
21869
22950
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
21870
22951
|
if (o.repo) args.push("--repo", o.repo);
|
|
21871
|
-
(0,
|
|
22952
|
+
(0, import_node_child_process14.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
21872
22953
|
detached: true,
|
|
21873
22954
|
stdio: "ignore",
|
|
21874
22955
|
windowsHide: true,
|
|
@@ -21877,7 +22958,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
21877
22958
|
} catch {
|
|
21878
22959
|
}
|
|
21879
22960
|
}
|
|
21880
|
-
var northstar = program2.command("northstar").description("North Star \u2014
|
|
22961
|
+
var northstar = program2.command("northstar").description("North Star \u2014 Jervaise-only cross-device plans/SSOTs (S3-backed, git-clean)");
|
|
21881
22962
|
registerNorthStarCommands(northstar);
|
|
21882
22963
|
var plan = program2.command("plan").description("Alias for `northstar` (deprecated \u2014 use `northstar`)");
|
|
21883
22964
|
plan.hook("preAction", () => {
|
|
@@ -21903,7 +22984,7 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
21903
22984
|
try {
|
|
21904
22985
|
const result = await runTenantControl(trainApplyDeps(), { repo, stage: stage2, action, watch: o.watch });
|
|
21905
22986
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
21906
|
-
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed" };
|
|
22987
|
+
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
|
|
21907
22988
|
const { lines, failure } = renderVerifySecrets(body);
|
|
21908
22989
|
for (const line of lines) printLine(line);
|
|
21909
22990
|
if (failure) return failGraceful(`tenant control ${stage2} verify-secrets: ${failure}`);
|
|
@@ -21917,6 +22998,20 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
21917
22998
|
return failGraceful(`tenant control: ${e.message}`);
|
|
21918
22999
|
}
|
|
21919
23000
|
});
|
|
23001
|
+
tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints").option("--json", "machine-readable output").action(async (repo, stage2, _o) => {
|
|
23002
|
+
if (!["dev", "rc", "main"].includes(stage2)) return fail("tenant status: <stage> must be dev, rc, or main");
|
|
23003
|
+
const cfg = await loadConfig();
|
|
23004
|
+
const result = await buildTenantRuntimeStatusFor(repo, stage2, cfg);
|
|
23005
|
+
console.log(JSON.stringify(result, null, 2));
|
|
23006
|
+
if (result.publicProbe?.ok === false) process.exitCode = 1;
|
|
23007
|
+
});
|
|
23008
|
+
tenant.command("readiness <owner/repo> <stage>").description("alias for tenant status: read-only tenant runtime readiness").option("--json", "machine-readable output").action(async (repo, stage2, _o) => {
|
|
23009
|
+
if (!["dev", "rc", "main"].includes(stage2)) return fail("tenant readiness: <stage> must be dev, rc, or main");
|
|
23010
|
+
const cfg = await loadConfig();
|
|
23011
|
+
const result = await buildTenantRuntimeStatusFor(repo, stage2, cfg);
|
|
23012
|
+
console.log(JSON.stringify(result, null, 2));
|
|
23013
|
+
if (result.publicProbe?.ok === false) process.exitCode = 1;
|
|
23014
|
+
});
|
|
21920
23015
|
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
|
|
21921
23016
|
if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
|
|
21922
23017
|
try {
|
|
@@ -21959,6 +23054,44 @@ async function resolveDnsBounded(host, timeoutMs = 3e3) {
|
|
|
21959
23054
|
});
|
|
21960
23055
|
return Promise.race([probe, timeout]);
|
|
21961
23056
|
}
|
|
23057
|
+
async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
23058
|
+
const controller = new AbortController();
|
|
23059
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
23060
|
+
timeout.unref?.();
|
|
23061
|
+
try {
|
|
23062
|
+
const res = await fetch(url, { method: "GET", signal: controller.signal });
|
|
23063
|
+
return { ok: res.ok, status: res.status };
|
|
23064
|
+
} catch (e) {
|
|
23065
|
+
return { ok: false, error: e.message };
|
|
23066
|
+
} finally {
|
|
23067
|
+
clearTimeout(timeout);
|
|
23068
|
+
}
|
|
23069
|
+
}
|
|
23070
|
+
async function lastWorkflowRun(workflow) {
|
|
23071
|
+
try {
|
|
23072
|
+
const out = await execFileP2("gh", ["run", "list", "--repo", HUB_REPO3, "--workflow", workflow, "--limit", "1", "--json", "databaseId,url,status,conclusion,headBranch,headSha,createdAt"], { timeout: 2e4 });
|
|
23073
|
+
const rows = JSON.parse(out.stdout || "[]");
|
|
23074
|
+
return rows[0];
|
|
23075
|
+
} catch {
|
|
23076
|
+
return void 0;
|
|
23077
|
+
}
|
|
23078
|
+
}
|
|
23079
|
+
async function buildTenantRuntimeStatusFor(target, stage2, cfg) {
|
|
23080
|
+
const slug = slugOf(target);
|
|
23081
|
+
const reg = registryClientDeps(cfg);
|
|
23082
|
+
const facts = await fetchDeployFactsBySlug(slug, reg);
|
|
23083
|
+
const deploy = facts?.stages[stage2] ?? null;
|
|
23084
|
+
const publicUrl = publicUrlFromDeployFact(deploy);
|
|
23085
|
+
const publicProbe = publicUrl ? await probeHttpBounded(publicUrl) : void 0;
|
|
23086
|
+
return buildTenantRuntimeStatus({
|
|
23087
|
+
repo: target,
|
|
23088
|
+
slug,
|
|
23089
|
+
stage: stage2,
|
|
23090
|
+
deploy,
|
|
23091
|
+
publicProbe,
|
|
23092
|
+
lastTenantDeployRun: await lastWorkflowRun("tenant-deploy.yml")
|
|
23093
|
+
});
|
|
23094
|
+
}
|
|
21962
23095
|
async function v2ReadinessDeps(cfg) {
|
|
21963
23096
|
const reg = registryClientDeps(cfg);
|
|
21964
23097
|
return {
|
|
@@ -21973,6 +23106,11 @@ async function v2ReadinessDeps(cfg) {
|
|
|
21973
23106
|
const status = await fetchDeployStatusBySlug(slug, reg);
|
|
21974
23107
|
return Boolean(status?.deployState[stage2]);
|
|
21975
23108
|
},
|
|
23109
|
+
getDeployFact: async (slug, stage2) => {
|
|
23110
|
+
const facts = await fetchDeployFactsBySlug(slug, reg);
|
|
23111
|
+
const fact = facts?.stages[stage2];
|
|
23112
|
+
return fact ? { port: fact.port, domain: fact.domain } : null;
|
|
23113
|
+
},
|
|
21976
23114
|
listSecrets: async (targetRepo2) => {
|
|
21977
23115
|
const apiUrl = cfg.sagaApiUrl;
|
|
21978
23116
|
if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
|
|
@@ -22060,7 +23198,7 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
|
|
|
22060
23198
|
}
|
|
22061
23199
|
});
|
|
22062
23200
|
project.command("resolve <owner/repo>").description("deploy coords for a stage \u2014 for diagnosis. NOTE: /deploy-coords is OIDC-gated (a deploy job\u2019s id-token), so a gh-token CLI cannot read it from a dev machine").option("--stage <main|rc>", "deploy stage", "main").option("--json", "machine-readable output").action((_repoOrRepo, o) => {
|
|
22063
|
-
const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect
|
|
23201
|
+
const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect nonsecret DEPLOY facts with `mmi-cli project deploy get`. Full coords stay OIDC-gated.";
|
|
22064
23202
|
if (o.json) {
|
|
22065
23203
|
console.log(JSON.stringify({ ok: false, stage: o.stage, error: msg }));
|
|
22066
23204
|
process.exitCode = 1;
|
|
@@ -22068,6 +23206,37 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
|
|
|
22068
23206
|
}
|
|
22069
23207
|
fail(msg);
|
|
22070
23208
|
});
|
|
23209
|
+
var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
|
|
23210
|
+
projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
23211
|
+
const cfg = await loadConfig();
|
|
23212
|
+
let target;
|
|
23213
|
+
try {
|
|
23214
|
+
target = await projectTarget("project deploy get", repoOrSlug);
|
|
23215
|
+
} catch (e) {
|
|
23216
|
+
return fail(e.message);
|
|
23217
|
+
}
|
|
23218
|
+
const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
|
|
23219
|
+
if (!out) return failGraceful(`project deploy get: Hub deploy facts read failed for ${target}`);
|
|
23220
|
+
const stage2 = o.stage?.trim();
|
|
23221
|
+
if (stage2 && !["dev", "rc", "main"].includes(stage2)) return fail("project deploy get: --stage must be dev, rc, or main");
|
|
23222
|
+
const payload = stage2 ? { slug: out.slug, stage: stage2, deploy: out.stages[stage2] ?? null } : out;
|
|
23223
|
+
console.log(JSON.stringify(payload));
|
|
23224
|
+
});
|
|
23225
|
+
projectDeploy.command("list [owner/repo]").description("alias for project deploy get; lists all stages by default").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
23226
|
+
const cfg = await loadConfig();
|
|
23227
|
+
let target;
|
|
23228
|
+
try {
|
|
23229
|
+
target = await projectTarget("project deploy list", repoOrSlug);
|
|
23230
|
+
} catch (e) {
|
|
23231
|
+
return fail(e.message);
|
|
23232
|
+
}
|
|
23233
|
+
const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
|
|
23234
|
+
if (!out) return failGraceful(`project deploy list: Hub deploy facts read failed for ${target}`);
|
|
23235
|
+
const stage2 = o.stage?.trim();
|
|
23236
|
+
if (stage2 && !["dev", "rc", "main"].includes(stage2)) return fail("project deploy list: --stage must be dev, rc, or main");
|
|
23237
|
+
const payload = stage2 ? { slug: out.slug, stage: stage2, deploy: out.stages[stage2] ?? null } : out;
|
|
23238
|
+
console.log(JSON.stringify(payload));
|
|
23239
|
+
});
|
|
22071
23240
|
project.command("doctor [owner/repo]").description("diagnose Hub v2 readiness for a repo without reading product repo files \u2014 appOwnedGaps are advisory templates; clear them with `project attest`; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--json", "machine-readable output").action(async (repo, _o) => {
|
|
22072
23241
|
const cfg = await loadConfig();
|
|
22073
23242
|
let target;
|
|
@@ -22160,6 +23329,31 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
22160
23329
|
const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
|
|
22161
23330
|
return reportWrite("project set", res);
|
|
22162
23331
|
});
|
|
23332
|
+
var fullTrack = program2.command("full-track").description("direct-to-full-track readiness audits");
|
|
23333
|
+
fullTrack.command("readiness <owner/repo>").description("aggregate branch topology, train authority, deploy facts, endpoint health, and rcand readiness for direct -> full switches").option("--json", "machine-readable output").action(async (repo, _o) => {
|
|
23334
|
+
const cfg = await loadConfig();
|
|
23335
|
+
const reg = registryClientDeps(cfg);
|
|
23336
|
+
const slug = slugOf(repo);
|
|
23337
|
+
const [metaRead, deployFacts, authority, dev, rc, main] = await Promise.all([
|
|
23338
|
+
fetchProjectBySlugChecked(slug, reg),
|
|
23339
|
+
fetchDeployFactsBySlug(slug, reg),
|
|
23340
|
+
fetchTrainAuthority(repo, reg),
|
|
23341
|
+
buildTenantRuntimeStatusFor(repo, "dev", cfg),
|
|
23342
|
+
buildTenantRuntimeStatusFor(repo, "rc", cfg),
|
|
23343
|
+
buildTenantRuntimeStatusFor(repo, "main", cfg)
|
|
23344
|
+
]);
|
|
23345
|
+
if (!metaRead.ok) return failGraceful(`full-track readiness: Hub registry read failed (${metaRead.error})`);
|
|
23346
|
+
const report = buildFullTrackReadinessReport({
|
|
23347
|
+
repo,
|
|
23348
|
+
slug,
|
|
23349
|
+
meta: metaRead.project,
|
|
23350
|
+
deployFacts,
|
|
23351
|
+
trainAuthority: authority.ok ? authority.authority : void 0,
|
|
23352
|
+
runtime: { dev, rc, main }
|
|
23353
|
+
});
|
|
23354
|
+
console.log(JSON.stringify(report, null, 2));
|
|
23355
|
+
if (!report.rcand.canApply) process.exitCode = 1;
|
|
23356
|
+
});
|
|
22163
23357
|
project.command("set-deploy [owner/repo]").description("write the DEPLOY#<stage> Hetzner deploy coords for a tenant (master-only) \u2014 the explicit-coords path that seeds a freshly-bootstrapped tenant; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into (required for hetzner-ssh)").option("--ssh-user <user>", "ssh user (default root)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535)").option("--substrate <substrate>", "hetzner-ssh (default)").option("--deploy-path <path>", "on-box per-stage release root (default /opt/mmi/<slug>/<stage>)").option("--service <name>", "systemd/compose service name (default the slug)").option("--domain <domain>", "canonical serving host (default the project edgeDomains[stage])").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
22164
23358
|
const cfg = await loadConfig();
|
|
22165
23359
|
let target;
|
|
@@ -22891,7 +24085,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
22891
24085
|
}
|
|
22892
24086
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
22893
24087
|
function spawnDeferredGcSweep() {
|
|
22894
|
-
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn:
|
|
24088
|
+
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
22895
24089
|
}
|
|
22896
24090
|
async function createDeferredWorktreeStore() {
|
|
22897
24091
|
try {
|
|
@@ -23590,7 +24784,7 @@ async function resolveRcandPlanTargets() {
|
|
|
23590
24784
|
}
|
|
23591
24785
|
}
|
|
23592
24786
|
for (const commandName of ["rcand", "release"]) {
|
|
23593
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").option("--dev", "release only: full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)").action(async (o) => {
|
|
24787
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").option("--dev", "release only: full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)").option("--repo <owner/repo>", "dry-run plan for a target repo without relying on the current checkout; --apply still uses the current checkout").action(async (o) => {
|
|
23594
24788
|
try {
|
|
23595
24789
|
await requireFreshTrainCli(commandName);
|
|
23596
24790
|
} catch (e) {
|
|
@@ -23602,6 +24796,9 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
23602
24796
|
if (o.dev && commandName !== "release") {
|
|
23603
24797
|
return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
|
|
23604
24798
|
}
|
|
24799
|
+
if (o.apply && o.repo) {
|
|
24800
|
+
return fail(`${commandName}: --repo is read-only for dry-run planning; --apply must run from the target repo checkout`);
|
|
24801
|
+
}
|
|
23605
24802
|
if (o.apply) {
|
|
23606
24803
|
try {
|
|
23607
24804
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -23611,8 +24808,8 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
23611
24808
|
return failGraceful(`${commandName}: ${e.message}`);
|
|
23612
24809
|
}
|
|
23613
24810
|
}
|
|
23614
|
-
const repo = await resolveRepo();
|
|
23615
|
-
const targets = commandName === "rcand" ? await resolveRcandPlanTargets() : void 0;
|
|
24811
|
+
const repo = o.repo ?? await resolveRepo();
|
|
24812
|
+
const targets = commandName === "rcand" && !o.repo ? await resolveRcandPlanTargets() : void 0;
|
|
23616
24813
|
let releaseTrack;
|
|
23617
24814
|
if (repo) {
|
|
23618
24815
|
try {
|
|
@@ -24026,7 +25223,9 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
24026
25223
|
const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
|
|
24027
25224
|
if (o.repo) {
|
|
24028
25225
|
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
24029
|
-
|
|
25226
|
+
const meta = registryProjects?.find((project2) => (project2.repos ?? []).some((repo) => repo.toLowerCase() === o.repo.toLowerCase())) ?? null;
|
|
25227
|
+
const repoClass = o.class;
|
|
25228
|
+
targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
|
|
24030
25229
|
} else {
|
|
24031
25230
|
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs29.existsSync)("projects.json") ? (0, import_node_fs29.readFileSync)("projects.json", "utf8") : null;
|
|
24032
25231
|
if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
|
|
@@ -24126,85 +25325,90 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
24126
25325
|
));
|
|
24127
25326
|
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
|
|
24128
25327
|
program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
|
|
24129
|
-
program2.command("session-start").description("run the SessionStart verbs (rules sync,
|
|
25328
|
+
program2.command("session-start").description("run the SessionStart verbs (rules sync, Jervaise-only continuity, whoami, doctor, plan-store check) in one process; docs sync runs detached").action(async () => {
|
|
24130
25329
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
24131
25330
|
console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
24132
25331
|
return;
|
|
24133
25332
|
}
|
|
24134
25333
|
if (!isOrgRepoRoot(process.cwd())) return;
|
|
24135
|
-
|
|
24136
|
-
|
|
24137
|
-
|
|
24138
|
-
|
|
24139
|
-
|
|
25334
|
+
const continuityEnabled = (await continuityAccess().catch(() => ({ allowed: false }))).allowed;
|
|
25335
|
+
if (continuityEnabled) {
|
|
25336
|
+
try {
|
|
25337
|
+
const hook = parseHookInput(await readStdin());
|
|
25338
|
+
if (hook.session_id) persistSession(hook.session_id);
|
|
25339
|
+
} catch (e) {
|
|
25340
|
+
console.error(`[mmi-hook] saga session failed: ${e.message}`);
|
|
25341
|
+
}
|
|
24140
25342
|
}
|
|
24141
|
-
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn:
|
|
25343
|
+
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
24142
25344
|
spawnDeferredGcSweep();
|
|
24143
25345
|
let northstarInjected = false;
|
|
24144
|
-
const { parallel, sequential } = buildSessionStartPlan(
|
|
24145
|
-
|
|
24146
|
-
|
|
24147
|
-
|
|
24148
|
-
|
|
24149
|
-
|
|
24150
|
-
|
|
24151
|
-
|
|
24152
|
-
|
|
24153
|
-
|
|
24154
|
-
|
|
24155
|
-
|
|
24156
|
-
|
|
24157
|
-
|
|
24158
|
-
|
|
24159
|
-
|
|
24160
|
-
|
|
24161
|
-
|
|
24162
|
-
|
|
24163
|
-
|
|
24164
|
-
|
|
24165
|
-
|
|
24166
|
-
|
|
24167
|
-
|
|
24168
|
-
|
|
24169
|
-
|
|
24170
|
-
|
|
24171
|
-
|
|
24172
|
-
|
|
24173
|
-
|
|
24174
|
-
|
|
24175
|
-
|
|
24176
|
-
|
|
24177
|
-
|
|
24178
|
-
|
|
24179
|
-
|
|
24180
|
-
|
|
24181
|
-
|
|
24182
|
-
|
|
24183
|
-
|
|
24184
|
-
|
|
24185
|
-
|
|
24186
|
-
|
|
24187
|
-
|
|
24188
|
-
|
|
24189
|
-
|
|
24190
|
-
|
|
24191
|
-
|
|
24192
|
-
|
|
24193
|
-
|
|
24194
|
-
|
|
24195
|
-
|
|
25346
|
+
const { parallel, sequential } = buildSessionStartPlan(
|
|
25347
|
+
{
|
|
25348
|
+
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
25349
|
+
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|
|
25350
|
+
handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
|
|
25351
|
+
coopPending: (io) => runCoopPendingBanner(io),
|
|
25352
|
+
northstarContext: async (io) => {
|
|
25353
|
+
const cfg = await loadConfig();
|
|
25354
|
+
if (!cfg.sagaApiUrl) return;
|
|
25355
|
+
const planDeps = makePlanDeps(cfg, io);
|
|
25356
|
+
northstarInjected = await runNorthstarContext(io, {
|
|
25357
|
+
loadPlans: () => scopedPlanList(planDeps),
|
|
25358
|
+
readLocal: (slug) => planDeps.readLocal(slug),
|
|
25359
|
+
// #1812: thread the saga HEAD's North Star anchor (its NEXT slug) into the relevance gate so
|
|
25360
|
+
// the plan the agent is actively on is force-injected even on a generic branch with no token
|
|
25361
|
+
// overlap. fetchSagaHead errors are swallowed via a silent io — a missing/failed HEAD just
|
|
25362
|
+
// falls back to token-overlap scoring, never noises or blocks the banner.
|
|
25363
|
+
gatherSignals: () => gatherRelevanceSignals({
|
|
25364
|
+
anchorSlug: () => fetchSagaHead({ log: () => {
|
|
25365
|
+
}, err: () => {
|
|
25366
|
+
} }).then((h) => h?.anchor?.slug ?? void 0)
|
|
25367
|
+
})
|
|
25368
|
+
});
|
|
25369
|
+
},
|
|
25370
|
+
sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
|
|
25371
|
+
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
25372
|
+
// when unknown — a missing gh login must not noise or fail the banner.
|
|
25373
|
+
whoami: async (io) => {
|
|
25374
|
+
const report = await resolveWhoami({
|
|
25375
|
+
hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
25376
|
+
ghLogin: githubLogin
|
|
25377
|
+
});
|
|
25378
|
+
const line = whoamiLine(report);
|
|
25379
|
+
if (line) io.log(line);
|
|
25380
|
+
},
|
|
25381
|
+
boardSlice: (io) => runBoardSlice(io, {
|
|
25382
|
+
loadConfig: () => loadConfigForRepo(),
|
|
25383
|
+
readBoard,
|
|
25384
|
+
// #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
|
|
25385
|
+
// never costs banner time and next session's glance renders instantly within budget.
|
|
25386
|
+
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
|
|
25387
|
+
}),
|
|
25388
|
+
spineReconcile: async (io) => {
|
|
25389
|
+
const cfg = await loadConfig();
|
|
25390
|
+
const isSource = isRulesSource(cfg.orgRulesSource);
|
|
25391
|
+
if (!isSource && !await isOrgRegisteredRepo(cfg)) return;
|
|
25392
|
+
const restored = await restoreDirtyOrgSpineToHead(
|
|
25393
|
+
{ run: async (cmd, args) => (await execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })).stdout },
|
|
25394
|
+
{ isRulesSource: isSource }
|
|
25395
|
+
);
|
|
25396
|
+
if (restored.length) {
|
|
25397
|
+
io.log(`[mmi] reset ${restored.length} org-delivered file(s) to your branch \u2014 pull latest base to refresh (${restored.join(", ")})`);
|
|
25398
|
+
}
|
|
25399
|
+
},
|
|
25400
|
+
doctor: (io) => runDoctor({ banner: true }, io)
|
|
24196
25401
|
},
|
|
24197
|
-
|
|
24198
|
-
|
|
25402
|
+
{ continuityEnabled }
|
|
25403
|
+
);
|
|
24199
25404
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
24200
|
-
consoleIo.log(
|
|
25405
|
+
for (const line of sessionStartContinuityLines({ continuityEnabled, northstarInjected, cwd: process.cwd() })) consoleIo.log(line);
|
|
24201
25406
|
consoleIo.log(kbPointer());
|
|
24202
|
-
|
|
24203
|
-
await runPlanAutosave(consoleIo, { quiet: true }).catch(() => void 0);
|
|
25407
|
+
if (continuityEnabled) await runPlanAutosave(consoleIo, { quiet: true }).catch(() => void 0);
|
|
24204
25408
|
for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
|
|
24205
25409
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
24206
25410
|
if (worktreeBanner) {
|
|
24207
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
25411
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process14.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
24208
25412
|
consoleIo.log(worktreeBanner);
|
|
24209
25413
|
}
|
|
24210
25414
|
});
|