@mutmutco/cli 3.78.0 → 3.80.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 +10 -1
- package/dist/main.cjs +1560 -265
- package/package.json +2 -2
package/dist/main.cjs
CHANGED
|
@@ -3580,6 +3580,8 @@ var ERROR_CODES = {
|
|
|
3580
3580
|
ERR_CONFLICTING_FLAGS: "ERR_CONFLICTING_FLAGS",
|
|
3581
3581
|
/** A flag was supplied but its resolved value is empty (e.g. an empty `--title-file` or empty stdin). */
|
|
3582
3582
|
ERR_EMPTY_INPUT: "ERR_EMPTY_INPUT",
|
|
3583
|
+
/** A supplied value has the right source but an invalid shape (e.g. a title with line breaks). */
|
|
3584
|
+
ERR_INVALID_INPUT: "ERR_INVALID_INPUT",
|
|
3583
3585
|
/** A flag's value is outside its allowed set (e.g. `--priority nope`). */
|
|
3584
3586
|
ERR_BAD_ENUM: "ERR_BAD_ENUM",
|
|
3585
3587
|
/** An unknown flag or subcommand — usually a typo; carries a `did_you_mean`. */
|
|
@@ -3612,6 +3614,11 @@ var ERROR_CODE_REFERENCE = [
|
|
|
3612
3614
|
meaning: "A flag was supplied but resolved to an empty value (e.g. an empty file or empty stdin).",
|
|
3613
3615
|
typical_fix: "Provide non-empty content for the flag in `offending_flag` (a file with text, or a real pipe/heredoc for stdin)."
|
|
3614
3616
|
},
|
|
3617
|
+
{
|
|
3618
|
+
code: ERROR_CODES.ERR_INVALID_INPUT,
|
|
3619
|
+
meaning: "A supplied value has an invalid shape for the flag.",
|
|
3620
|
+
typical_fix: "Correct the value named by `offending_flag` and retry."
|
|
3621
|
+
},
|
|
3615
3622
|
{
|
|
3616
3623
|
code: ERROR_CODES.ERR_BAD_ENUM,
|
|
3617
3624
|
meaning: "A flag value is outside the allowed enum.",
|
|
@@ -6425,7 +6432,7 @@ function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
|
6425
6432
|
return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
|
|
6426
6433
|
}
|
|
6427
6434
|
function npmInstallTargets(dirs) {
|
|
6428
|
-
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
|
|
6435
|
+
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install --no-package-lock" }));
|
|
6429
6436
|
}
|
|
6430
6437
|
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
6431
6438
|
return fs2.isFile((0, import_node_path10.join)(root, ".git"));
|
|
@@ -9430,6 +9437,132 @@ async function runReleaseResume(deps, options = {}) {
|
|
|
9430
9437
|
note: `resumed and completed ${tag} \u2014 the original version was preserved, not re-cut`
|
|
9431
9438
|
};
|
|
9432
9439
|
}
|
|
9440
|
+
async function runReleaseAbort(deps, options = {}) {
|
|
9441
|
+
if (!options.approved) {
|
|
9442
|
+
throw new Error("release --abort requires --apply after explicit approval; nothing was written");
|
|
9443
|
+
}
|
|
9444
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
9445
|
+
if (!isHubControlRepo(ctx.repo)) {
|
|
9446
|
+
throw new Error("release --abort is limited to mutmutco/MMI-Hub until other release tracks define equivalent publish-absence proof");
|
|
9447
|
+
}
|
|
9448
|
+
await requireCleanTree(deps);
|
|
9449
|
+
if (await currentBranch(deps) !== "main") {
|
|
9450
|
+
throw new Error("release --abort must run from the primary checkout left on main by the failed train; nothing was written");
|
|
9451
|
+
}
|
|
9452
|
+
await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
|
|
9453
|
+
const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((tag2) => tag2.trim()).filter((tag2) => /^v\d+\.\d+\.\d+$/.test(tag2));
|
|
9454
|
+
const tag = tags[0];
|
|
9455
|
+
if (!tag) throw new Error("release --abort found no final v* tag; nothing was written");
|
|
9456
|
+
const tagSha = await probeRemoteTag(deps, tag);
|
|
9457
|
+
if (!tagSha) throw new Error(`release --abort: ${tag} is not on origin; nothing was written`);
|
|
9458
|
+
const localTagSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
|
|
9459
|
+
const localMainSha = clean2(await deps.run("git", ["rev-parse", "main"]));
|
|
9460
|
+
const originMainSha = clean2(await deps.run("git", ["rev-parse", "origin/main"]));
|
|
9461
|
+
if (localTagSha !== tagSha || localMainSha !== tagSha) {
|
|
9462
|
+
throw new Error(
|
|
9463
|
+
`release --abort: local tag/main do not both identify origin ${tag} (${tagSha.slice(0, 12)}); nothing was written`
|
|
9464
|
+
);
|
|
9465
|
+
}
|
|
9466
|
+
if (!await probeAncestor(deps, "origin/main", tagSha, "release --abort: origin/main ancestry")) {
|
|
9467
|
+
throw new Error(`release --abort: ${tag} does not descend from origin/main; nothing was written`);
|
|
9468
|
+
}
|
|
9469
|
+
const candidateParents = clean2(await deps.run("git", ["rev-list", "--parents", "-n", "1", tagSha])).split(/\s+/);
|
|
9470
|
+
if (candidateParents.length !== 2 || candidateParents[0] !== tagSha) {
|
|
9471
|
+
throw new Error(`release --abort: ${tag} is not a single-parent release bump; nothing was written`);
|
|
9472
|
+
}
|
|
9473
|
+
const candidateBaseSha = candidateParents[1];
|
|
9474
|
+
if (!await probeAncestor(deps, candidateBaseSha, "origin/development", "release --abort: development ancestry")) {
|
|
9475
|
+
throw new Error(`release --abort: current development does not descend from ${tag}'s candidate base; nothing was written`);
|
|
9476
|
+
}
|
|
9477
|
+
if (!await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo)) {
|
|
9478
|
+
throw new Error(`release --abort: ${tag} is released or its unpublished state could not be proven; nothing was written`);
|
|
9479
|
+
}
|
|
9480
|
+
const version = tag.slice(1);
|
|
9481
|
+
try {
|
|
9482
|
+
const published = clean2(await deps.run("npm", ["view", `@mutmutco/cli@${version}`, "version", "--json"]));
|
|
9483
|
+
if (published) {
|
|
9484
|
+
throw new Error(`release --abort: @mutmutco/cli@${version} is already published; ${tag} cannot be reused`);
|
|
9485
|
+
}
|
|
9486
|
+
throw new Error("npm registry returned an empty successful response");
|
|
9487
|
+
} catch (error) {
|
|
9488
|
+
const detail = `${error instanceof Error ? error.message : String(error)} ${String(error.stderr ?? "")}`;
|
|
9489
|
+
if (!/E404|not found|No match found/i.test(detail)) {
|
|
9490
|
+
if (/already published/.test(detail)) throw error;
|
|
9491
|
+
throw new Error(`release --abort: npm publish absence could not be proven (${detail.trim()}); nothing was written`);
|
|
9492
|
+
}
|
|
9493
|
+
}
|
|
9494
|
+
await runGitPush(deps, ["push", "origin", "--delete", tag]);
|
|
9495
|
+
if (await probeRemoteTag(deps, tag)) {
|
|
9496
|
+
throw new Error(`release --abort: origin still reports ${tag} after deletion; local state was preserved`);
|
|
9497
|
+
}
|
|
9498
|
+
await deps.run("git", ["tag", "-d", tag]);
|
|
9499
|
+
await deps.run("git", ["checkout", "development"]);
|
|
9500
|
+
await ffOnlyPull(deps, "development");
|
|
9501
|
+
await deps.run("git", ["branch", "-f", "main", "origin/main"]);
|
|
9502
|
+
const restoredMainSha = clean2(await deps.run("git", ["rev-parse", "main"]));
|
|
9503
|
+
if (restoredMainSha !== originMainSha) {
|
|
9504
|
+
throw new Error(`release --abort removed ${tag}, but local main did not restore to origin/main; inspect before retrying`);
|
|
9505
|
+
}
|
|
9506
|
+
return {
|
|
9507
|
+
command: "release-abort",
|
|
9508
|
+
repo: ctx.repo,
|
|
9509
|
+
tag,
|
|
9510
|
+
tagSha,
|
|
9511
|
+
restoredMainSha,
|
|
9512
|
+
note: `removed the unpublished ${tag} candidate and restored local main; repair development before recutting`
|
|
9513
|
+
};
|
|
9514
|
+
}
|
|
9515
|
+
async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
9516
|
+
if (!options.approved) {
|
|
9517
|
+
throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
|
|
9518
|
+
}
|
|
9519
|
+
if (!Number.isSafeInteger(runId) || runId <= 0) throw new Error(`invalid publish run id ${runId}`);
|
|
9520
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
9521
|
+
if (!isHubControlRepo(ctx.repo)) {
|
|
9522
|
+
throw new Error("release --retry-publish is limited to mutmutco/MMI-Hub");
|
|
9523
|
+
}
|
|
9524
|
+
await requireCleanTree(deps);
|
|
9525
|
+
await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
|
|
9526
|
+
const readRun = async () => {
|
|
9527
|
+
const raw = await deps.run("gh", ["run", "view", String(runId), "--repo", ctx.repo, "--json", "databaseId,workflowName,event,headBranch,headSha,status,conclusion,url"]);
|
|
9528
|
+
try {
|
|
9529
|
+
return JSON.parse(raw);
|
|
9530
|
+
} catch {
|
|
9531
|
+
throw new Error(`publish run ${runId} metadata was not valid JSON`);
|
|
9532
|
+
}
|
|
9533
|
+
};
|
|
9534
|
+
const run = await readRun();
|
|
9535
|
+
if (run.databaseId !== runId || run.workflowName !== "publish" || run.event !== "release" || run.status !== "completed" || run.conclusion !== "failure") {
|
|
9536
|
+
throw new Error(`run ${runId} is not a completed failed Hub publish release run; nothing was written`);
|
|
9537
|
+
}
|
|
9538
|
+
const tag = run.headBranch ?? "";
|
|
9539
|
+
const tagSha = run.headSha ?? "";
|
|
9540
|
+
if (!/^v\d+\.\d+\.\d+$/.test(tag) || !/^[0-9a-f]{40}$/.test(tagSha)) {
|
|
9541
|
+
throw new Error(`run ${runId} has invalid release identity (${tag || "(no tag)"} / ${tagSha || "(no sha)"}); nothing was written`);
|
|
9542
|
+
}
|
|
9543
|
+
if (await probeRemoteTag(deps, tag) !== tagSha) {
|
|
9544
|
+
throw new Error(`run ${runId} does not match origin ${tag}; nothing was written`);
|
|
9545
|
+
}
|
|
9546
|
+
await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
|
|
9547
|
+
await deps.run("gh", ["run", "rerun", String(runId), "--repo", ctx.repo, "--failed"]);
|
|
9548
|
+
if (options.watch) {
|
|
9549
|
+
await deps.run("gh", ["run", "watch", String(runId), "--repo", ctx.repo, "--exit-status"]);
|
|
9550
|
+
const completed = await readRun();
|
|
9551
|
+
if (completed.status !== "completed" || completed.conclusion !== "success") {
|
|
9552
|
+
throw new Error(`publish run ${runId} did not finish successfully after retry`);
|
|
9553
|
+
}
|
|
9554
|
+
}
|
|
9555
|
+
return {
|
|
9556
|
+
command: "release-retry-publish",
|
|
9557
|
+
repo: ctx.repo,
|
|
9558
|
+
tag,
|
|
9559
|
+
tagSha,
|
|
9560
|
+
runId,
|
|
9561
|
+
runUrl: run.url ?? `https://github.com/${ctx.repo}/actions/runs/${runId}`,
|
|
9562
|
+
status: options.watch ? "success" : "pending",
|
|
9563
|
+
note: options.watch ? `publish run ${runId} retried and passed` : `publish run ${runId} failed jobs queued for retry`
|
|
9564
|
+
};
|
|
9565
|
+
}
|
|
9433
9566
|
async function runTrainApplyPipeline(mode, input) {
|
|
9434
9567
|
const { deps, ctx, command, meta, branchHints, watch, options } = input;
|
|
9435
9568
|
const directTrack = input.directTrack ?? false;
|
|
@@ -12318,6 +12451,18 @@ function formatVaultPointer(p) {
|
|
|
12318
12451
|
return lines.join("\n");
|
|
12319
12452
|
}
|
|
12320
12453
|
var TIMEOUT_MS = 8e3;
|
|
12454
|
+
var VAULT_READ_ATTEMPTS = 2;
|
|
12455
|
+
async function fetchVaultRead(deps, url, init) {
|
|
12456
|
+
let lastError;
|
|
12457
|
+
for (let attempt = 0; attempt < VAULT_READ_ATTEMPTS; attempt += 1) {
|
|
12458
|
+
try {
|
|
12459
|
+
return await deps.fetch(url, { ...init, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
12460
|
+
} catch (error) {
|
|
12461
|
+
lastError = error;
|
|
12462
|
+
}
|
|
12463
|
+
}
|
|
12464
|
+
throw lastError;
|
|
12465
|
+
}
|
|
12321
12466
|
var repoOf = (slug) => `${OWNER}/${slug}`;
|
|
12322
12467
|
var RECALL_REGIONS = ["us-east-1", "us-west-2", "eu-central-1", "ap-northeast-1"];
|
|
12323
12468
|
var PROVIDER_VERIFY_TIMEOUT_MS = 8e3;
|
|
@@ -12405,11 +12550,10 @@ async function fetchSecretValue(deps, key, opts) {
|
|
|
12405
12550
|
const repo = await targetRepo(deps, opts);
|
|
12406
12551
|
const slug = opts.slug?.toLowerCase();
|
|
12407
12552
|
try {
|
|
12408
|
-
const res = await deps
|
|
12553
|
+
const res = await fetchVaultRead(deps, `${deps.apiUrl}/secrets/get`, {
|
|
12409
12554
|
method: "POST",
|
|
12410
12555
|
headers: await deps.headers({ "content-type": "application/json" }),
|
|
12411
|
-
body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} })
|
|
12412
|
-
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
12556
|
+
body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} })
|
|
12413
12557
|
});
|
|
12414
12558
|
if (!res.ok) return null;
|
|
12415
12559
|
const { value } = await res.json();
|
|
@@ -12423,10 +12567,9 @@ async function secretsList(deps, opts) {
|
|
|
12423
12567
|
const qs = new URLSearchParams({ repo }).toString();
|
|
12424
12568
|
let res;
|
|
12425
12569
|
try {
|
|
12426
|
-
res = await deps
|
|
12570
|
+
res = await fetchVaultRead(deps, `${deps.apiUrl}/secrets/list?${qs}`, {
|
|
12427
12571
|
method: "GET",
|
|
12428
|
-
headers: await deps.headers()
|
|
12429
|
-
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
12572
|
+
headers: await deps.headers()
|
|
12430
12573
|
});
|
|
12431
12574
|
} catch (e) {
|
|
12432
12575
|
deps.err(`secrets list: ${e.message}`);
|
|
@@ -13478,12 +13621,17 @@ function secretsUseExitCode(result) {
|
|
|
13478
13621
|
return void 0;
|
|
13479
13622
|
}
|
|
13480
13623
|
async function fetchSecretForUse(deps, { repo, key, slug }) {
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13624
|
+
let res;
|
|
13625
|
+
try {
|
|
13626
|
+
res = await fetchVaultRead(deps, `${deps.apiUrl}/secrets/get`, {
|
|
13627
|
+
method: "POST",
|
|
13628
|
+
headers: await deps.headers({ "content-type": "application/json" }),
|
|
13629
|
+
body: JSON.stringify({ repo, key, use: true, ...slug ? { slug } : {} })
|
|
13630
|
+
});
|
|
13631
|
+
} catch (error) {
|
|
13632
|
+
deps.err(`secrets use: ${error.message}`);
|
|
13633
|
+
return null;
|
|
13634
|
+
}
|
|
13487
13635
|
if (!res.ok) {
|
|
13488
13636
|
const body = await readJsonBody(res);
|
|
13489
13637
|
if (res.status === 404 && body.code === "secret_not_found") {
|
|
@@ -14175,7 +14323,7 @@ function buildPluginGuardLine(state, opts = {}) {
|
|
|
14175
14323
|
if (state === "healthy" || state === "not-org") return { exitCode: 0 };
|
|
14176
14324
|
const recovery = opts.recovery ?? "mmi-cli plugin heal";
|
|
14177
14325
|
const restartHint = opts.restartHint ?? "restart your agent host / reload plugins";
|
|
14178
|
-
const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its
|
|
14326
|
+
const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its delivery/cache is unresolved";
|
|
14179
14327
|
return {
|
|
14180
14328
|
line: `[mmi-guard] ${reason}; run ${recovery} and ${restartHint}.`,
|
|
14181
14329
|
exitCode: 1
|
|
@@ -14195,6 +14343,9 @@ function detectSurface(env) {
|
|
|
14195
14343
|
if (env.MMI_AGENT_SURFACE === "kimi" || has("KIMI_PLUGIN_ROOT") || has("KIMI_CODE_HOME")) {
|
|
14196
14344
|
return "kimi";
|
|
14197
14345
|
}
|
|
14346
|
+
if (env.MMI_AGENT_SURFACE === "kilo" || has("KILO") || Object.keys(env).some((k) => /^(?:KILO|KILOCODE)_/.test(k))) {
|
|
14347
|
+
return "kilo";
|
|
14348
|
+
}
|
|
14198
14349
|
if (env.MMI_AGENT_SURFACE === "cursor" || has("CURSOR_TRACE_ID") || has("CURSOR_USER") || has("CURSOR_SESSION_ID") || env.CURSOR_AGENT === "1" || has("CURSOR_EXTENSION_HOST_ROLE")) {
|
|
14199
14350
|
return "cursor";
|
|
14200
14351
|
}
|
|
@@ -14214,6 +14365,8 @@ function surfaceToken(surface) {
|
|
|
14214
14365
|
return "codex";
|
|
14215
14366
|
case "kimi":
|
|
14216
14367
|
return "kimi";
|
|
14368
|
+
case "kilo":
|
|
14369
|
+
return "kilo";
|
|
14217
14370
|
case "cursor":
|
|
14218
14371
|
return "cursor";
|
|
14219
14372
|
case "opencode":
|
|
@@ -14231,10 +14384,12 @@ function reloadAction(surface) {
|
|
|
14231
14384
|
return "restart Codex";
|
|
14232
14385
|
case "kimi":
|
|
14233
14386
|
return "run /reload (or start a new session) in Kimi Code";
|
|
14387
|
+
case "kilo":
|
|
14388
|
+
return "run /reload (or start a new session) in Kilo Code";
|
|
14234
14389
|
case "opencode":
|
|
14235
14390
|
return "restart OpenCode";
|
|
14236
14391
|
case "cursor":
|
|
14237
|
-
return "
|
|
14392
|
+
return "reload the Cursor window";
|
|
14238
14393
|
case "claude-cli":
|
|
14239
14394
|
case "shell":
|
|
14240
14395
|
default:
|
|
@@ -14243,6 +14398,7 @@ function reloadAction(surface) {
|
|
|
14243
14398
|
}
|
|
14244
14399
|
var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub --ref main && claude plugin install mmi@mutmutco`;
|
|
14245
14400
|
var CODEX_RECOVERY = "codex plugin remove mmi@mutmutco && codex plugin marketplace remove mutmutco && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mutmutco";
|
|
14401
|
+
var CURSOR_RECOVERY = "mmi-cli plugin heal # installs ~/.cursor/plugins/local/mmi from mutmutco/MMI-Hub";
|
|
14246
14402
|
var PLUGIN_SURFACE_HEAL = {
|
|
14247
14403
|
claude: {
|
|
14248
14404
|
delivery: "plugin-cli",
|
|
@@ -14268,6 +14424,24 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
14268
14424
|
],
|
|
14269
14425
|
fix: (surface) => `${CODEX_RECOVERY} # then ${reloadAction(surface)} and review /hooks`,
|
|
14270
14426
|
updateRecipe: [CODEX_RECOVERY]
|
|
14427
|
+
},
|
|
14428
|
+
cursor: {
|
|
14429
|
+
delivery: "local-checkout",
|
|
14430
|
+
recovery: CURSOR_RECOVERY,
|
|
14431
|
+
healSteps: null,
|
|
14432
|
+
fix: (surface) => `${CURSOR_RECOVERY} # then ${reloadAction(surface)}`,
|
|
14433
|
+
updateRecipe: [CURSOR_RECOVERY]
|
|
14434
|
+
},
|
|
14435
|
+
kilo: {
|
|
14436
|
+
// kilo-p1: no marketplace step — `kilo plugin <npm>` installs + patches the config in one non-interactive
|
|
14437
|
+
// verb. --global writes ~/.config/kilo/opencode.json; the plugin's server() provisions the skills.
|
|
14438
|
+
delivery: "plugin-cli",
|
|
14439
|
+
recovery: "kilo plugin @mutmutco/kilo-plugin --global",
|
|
14440
|
+
healSteps: [
|
|
14441
|
+
{ args: ["plugin", "@mutmutco/kilo-plugin", "--global"], gated: true }
|
|
14442
|
+
],
|
|
14443
|
+
fix: (surface) => `kilo plugin @mutmutco/kilo-plugin --global # then ${reloadAction(surface)} to load the plugin and its provisioned skills`,
|
|
14444
|
+
updateRecipe: ["kilo plugin @mutmutco/kilo-plugin --global"]
|
|
14271
14445
|
}
|
|
14272
14446
|
};
|
|
14273
14447
|
function nonClaudeSurfaceHealMessage(surface) {
|
|
@@ -14277,7 +14451,13 @@ function nonClaudeSurfaceHealMessage(surface) {
|
|
|
14277
14451
|
if (surface === "kimi") {
|
|
14278
14452
|
return "Kimi Code CLI ships an MMI plugin (kimi-k3). Install or repair it from the Kimi TUI with:\n /plugins install https://github.com/mutmutco/MMI-Hub\n Then run /reload (plugin hooks start only after a reload), and TRUST the install when prompted.\n Update the CLI too: npm i -g @mutmutco/cli";
|
|
14279
14453
|
}
|
|
14280
|
-
|
|
14454
|
+
if (surface === "kilo") {
|
|
14455
|
+
return "Kilo Code ships an MMI plugin (kilo-p1). Install or repair it with:\n kilo plugin @mutmutco/kilo-plugin --global\n Then run /reload (or start a new session) \u2014 the plugin\u2019s first run provisions the skills\n into ~/.kilo, and the deny gates + redactor only load after a reload.\n Update the CLI too: npm i -g @mutmutco/cli";
|
|
14456
|
+
}
|
|
14457
|
+
if (surface === "cursor") {
|
|
14458
|
+
return "Cursor ships an MMI plugin (#3920). Install or repair its managed local checkout with:\n mmi-cli plugin heal\n Then reload the Cursor window. For one-off CLI use, pass --plugin-dir <MMI-Hub checkout>.\n Update the CLI too: npm i -g @mutmutco/cli";
|
|
14459
|
+
}
|
|
14460
|
+
return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, Kimi, Cursor, and Kilo only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
|
|
14281
14461
|
}
|
|
14282
14462
|
function healStepAborts(step, ok) {
|
|
14283
14463
|
return !ok && step.gated;
|
|
@@ -14319,6 +14499,8 @@ function runHostBin(bin, args, opts) {
|
|
|
14319
14499
|
function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os5.homedir)()) {
|
|
14320
14500
|
if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path16.join)(home, ".codex");
|
|
14321
14501
|
if (surface === "kimi") return env.KIMI_CODE_HOME?.trim() || (0, import_node_path16.join)(home, ".kimi-code");
|
|
14502
|
+
if (surface === "kilo") return env.KILO_CONFIG_DIR?.trim() || (0, import_node_path16.join)(home, ".config", "kilo");
|
|
14503
|
+
if (surface === "cursor") return (0, import_node_path16.join)(home, ".cursor");
|
|
14322
14504
|
return (0, import_node_path16.join)(home, ".claude");
|
|
14323
14505
|
}
|
|
14324
14506
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
@@ -14340,6 +14522,8 @@ function marketplaceCloneCandidates(surface, home, env = process.env) {
|
|
|
14340
14522
|
];
|
|
14341
14523
|
}
|
|
14342
14524
|
if (surface === "kimi") return [];
|
|
14525
|
+
if (surface === "kilo") return [];
|
|
14526
|
+
if (surface === "cursor") return [];
|
|
14343
14527
|
return [(0, import_node_path16.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
14344
14528
|
}
|
|
14345
14529
|
function marketplaceClonePresent(surface, home, exists = import_node_fs18.existsSync, env = process.env) {
|
|
@@ -14349,11 +14533,13 @@ function runHostBinSync(bin, args) {
|
|
|
14349
14533
|
return isWin ? (0, import_node_child_process7.execFileSync)("cmd.exe", ["/c", bin, ...args], {
|
|
14350
14534
|
encoding: "utf8",
|
|
14351
14535
|
stdio: ["ignore", "pipe", "ignore"],
|
|
14352
|
-
timeout: 15e3
|
|
14536
|
+
timeout: 15e3,
|
|
14537
|
+
windowsHide: true
|
|
14353
14538
|
}) : (0, import_node_child_process7.execFileSync)(bin, args, {
|
|
14354
14539
|
encoding: "utf8",
|
|
14355
14540
|
stdio: ["ignore", "pipe", "ignore"],
|
|
14356
|
-
timeout: 15e3
|
|
14541
|
+
timeout: 15e3,
|
|
14542
|
+
windowsHide: true
|
|
14357
14543
|
});
|
|
14358
14544
|
}
|
|
14359
14545
|
function codexPluginStatus() {
|
|
@@ -14417,32 +14603,75 @@ async function fetchNpmReleasedVersion() {
|
|
|
14417
14603
|
}
|
|
14418
14604
|
}
|
|
14419
14605
|
var NPM_INSTALL_TIMEOUT_MS = 12e4;
|
|
14420
|
-
async function npmSelfUpdateCli(target) {
|
|
14606
|
+
async function npmSelfUpdateCli(target, onStep) {
|
|
14421
14607
|
const command = cliUpdateCommand(target);
|
|
14422
14608
|
try {
|
|
14609
|
+
onStep?.(command);
|
|
14423
14610
|
await runHostBin("npm", ["install", "-g", `@mutmutco/cli@${target ?? "latest"}`], { timeout: NPM_INSTALL_TIMEOUT_MS });
|
|
14424
14611
|
return { ok: true, detail: `${command} exited 0` };
|
|
14425
14612
|
} catch (e) {
|
|
14426
14613
|
return { ok: false, detail: e.message.trim().slice(0, 200).replace(/\s+/g, " ") };
|
|
14427
14614
|
}
|
|
14428
14615
|
}
|
|
14616
|
+
function kiloConfigListsPlugin(configRoot, home = (0, import_node_os5.homedir)(), read = (p) => (0, import_node_fs18.readFileSync)(p, "utf8"), exists = import_node_fs18.existsSync) {
|
|
14617
|
+
const candidates = ["kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc", "config.json"];
|
|
14618
|
+
for (const dir of [configRoot, (0, import_node_path16.join)(home, ".kilo")]) {
|
|
14619
|
+
for (const file of candidates) {
|
|
14620
|
+
const path2 = (0, import_node_path16.join)(dir, file);
|
|
14621
|
+
if (!exists(path2)) continue;
|
|
14622
|
+
try {
|
|
14623
|
+
const stripped = read(path2).replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
14624
|
+
const parsed = JSON.parse(stripped);
|
|
14625
|
+
const plugins = Array.isArray(parsed?.plugin) ? parsed.plugin : [];
|
|
14626
|
+
if (plugins.some((p) => {
|
|
14627
|
+
const spec = String(Array.isArray(p) ? p[0] : p);
|
|
14628
|
+
return spec.includes("@mutmutco/kilo-plugin") || spec.includes(".kilo-plugin");
|
|
14629
|
+
})) return true;
|
|
14630
|
+
} catch {
|
|
14631
|
+
}
|
|
14632
|
+
}
|
|
14633
|
+
}
|
|
14634
|
+
return false;
|
|
14635
|
+
}
|
|
14636
|
+
function cursorLocalPluginRoot(env = process.env, home = (0, import_node_os5.homedir)()) {
|
|
14637
|
+
return (0, import_node_path16.join)(surfaceConfigRoot("cursor", env, home), "plugins", "local", "mmi");
|
|
14638
|
+
}
|
|
14639
|
+
function cursorPluginTreeHealthy(root, exists = import_node_fs18.existsSync) {
|
|
14640
|
+
return [
|
|
14641
|
+
".cursor-plugin/plugin.json",
|
|
14642
|
+
"skills/mmi/SKILL.md",
|
|
14643
|
+
"hooks/cursor-hooks.json",
|
|
14644
|
+
"scripts/hook-run.mjs",
|
|
14645
|
+
"scripts/hook-policy.mjs"
|
|
14646
|
+
].every((path2) => exists((0, import_node_path16.join)(root, ...path2.split("/"))));
|
|
14647
|
+
}
|
|
14648
|
+
function kimiPluginTreeHealthy(root, exists = import_node_fs18.existsSync) {
|
|
14649
|
+
return [
|
|
14650
|
+
".kimi-plugin/plugin.json",
|
|
14651
|
+
"skills/mmi/SKILL.md",
|
|
14652
|
+
"scripts/hook-run.mjs"
|
|
14653
|
+
].every((path2) => exists((0, import_node_path16.join)(root, ...path2.split("/"))));
|
|
14654
|
+
}
|
|
14429
14655
|
function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
|
|
14430
14656
|
const root = surfaceConfigRoot(surface);
|
|
14431
14657
|
const installed = readInstalledPlugins(surface);
|
|
14432
14658
|
const codexStatus = surface === "codex" ? codexPluginStatus() : void 0;
|
|
14433
14659
|
return {
|
|
14434
14660
|
isOrgRepo,
|
|
14435
|
-
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd())
|
|
14436
|
-
|
|
14661
|
+
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()) || // Kimi's managed plugin directory is its native install record; it has no Claude-style ledger.
|
|
14662
|
+
surface === "kimi" && (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "managed", "mmi")) || // kilo-p1: the install record is the config file itself.
|
|
14663
|
+
surface === "kilo" && kiloConfigListsPlugin(root) || surface === "cursor" && (0, import_node_fs18.existsSync)(cursorLocalPluginRoot()),
|
|
14664
|
+
// Kilo has no marketplace to clone — the config file IS the install record, so this dimension of
|
|
14665
|
+
// the shared guard table is vacuously satisfied.
|
|
14666
|
+
marketplaceClonePresent: surface === "kimi" || surface === "kilo" || surface === "cursor" ? true : marketplaceClonePresent(surface, (0, import_node_os5.homedir)()),
|
|
14437
14667
|
// Kimi keeps no plugin cache dir — installs are copied to plugins/managed/<id> and run from there.
|
|
14438
|
-
|
|
14668
|
+
// Kilo (kilo-p1) keeps no cache dir either: the plugin's server() provisions ~/.kilo behind the
|
|
14669
|
+
// version stamp, so the stamp's presence is the cache signal.
|
|
14670
|
+
pluginCachePresent: surface === "kilo" ? (0, import_node_fs18.existsSync)((0, import_node_path16.join)((0, import_node_os5.homedir)(), ".kilo", ".mmi-kilo-version")) : surface === "kimi" ? kimiPluginTreeHealthy((0, import_node_path16.join)(root, "plugins", "managed", "mmi")) : surface === "cursor" ? cursorPluginTreeHealthy(cursorLocalPluginRoot()) : surface === "codex" ? Boolean(
|
|
14439
14671
|
codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", codexStatus.version))
|
|
14440
14672
|
) : (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "cache", "mutmutco", "mmi"))
|
|
14441
14673
|
};
|
|
14442
14674
|
}
|
|
14443
|
-
function activePluginGuardState(isOrgRepo) {
|
|
14444
|
-
return buildPluginGuardDecision(snapshotPluginGuardInput(detectSurface(process.env), isOrgRepo)).state;
|
|
14445
|
-
}
|
|
14446
14675
|
async function runClaudePlugin(args) {
|
|
14447
14676
|
try {
|
|
14448
14677
|
await runHostBin("claude", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
@@ -14459,6 +14688,98 @@ async function runCodexPlugin(args) {
|
|
|
14459
14688
|
return false;
|
|
14460
14689
|
}
|
|
14461
14690
|
}
|
|
14691
|
+
async function runKiloPlugin(args) {
|
|
14692
|
+
try {
|
|
14693
|
+
await runHostBin("kilo", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
14694
|
+
return true;
|
|
14695
|
+
} catch {
|
|
14696
|
+
return false;
|
|
14697
|
+
}
|
|
14698
|
+
}
|
|
14699
|
+
function captureCodexHookLauncher() {
|
|
14700
|
+
const status = codexPluginStatus();
|
|
14701
|
+
if (!status.installed || !status.enabled || !status.version) return void 0;
|
|
14702
|
+
const root = (0, import_node_path16.join)(surfaceConfigRoot("codex"), "plugins", "cache", CODEX_MARKETPLACE, "mmi", status.version);
|
|
14703
|
+
const files = ["mmi-hook", "mmi-hook.exe"].flatMap((name) => {
|
|
14704
|
+
const path2 = (0, import_node_path16.join)(root, "bin", name);
|
|
14705
|
+
try {
|
|
14706
|
+
return [{ name, content: (0, import_node_fs18.readFileSync)(path2) }];
|
|
14707
|
+
} catch {
|
|
14708
|
+
return [];
|
|
14709
|
+
}
|
|
14710
|
+
});
|
|
14711
|
+
return files.length === 2 ? { root, files } : void 0;
|
|
14712
|
+
}
|
|
14713
|
+
function restoreCodexHookLauncher(snapshot) {
|
|
14714
|
+
if (!snapshot || (0, import_node_fs18.existsSync)((0, import_node_path16.join)(snapshot.root, "scripts", "hook-run.mjs"))) return false;
|
|
14715
|
+
const bin = (0, import_node_path16.join)(snapshot.root, "bin");
|
|
14716
|
+
(0, import_node_fs18.mkdirSync)(bin, { recursive: true });
|
|
14717
|
+
for (const file of snapshot.files) {
|
|
14718
|
+
const path2 = (0, import_node_path16.join)(bin, file.name);
|
|
14719
|
+
(0, import_node_fs18.writeFileSync)(path2, file.content);
|
|
14720
|
+
if (file.name === "mmi-hook") (0, import_node_fs18.chmodSync)(path2, 493);
|
|
14721
|
+
}
|
|
14722
|
+
return true;
|
|
14723
|
+
}
|
|
14724
|
+
function canonicalCursorRemote(remote) {
|
|
14725
|
+
return /^(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:|github\.com[:/])mutmutco\/MMI-Hub(?:\.git)?\/?$/i.test(remote.trim().replace(/\\/g, "/"));
|
|
14726
|
+
}
|
|
14727
|
+
async function installCursorPluginCheckout(env = process.env) {
|
|
14728
|
+
const configRoot = surfaceConfigRoot("cursor", env);
|
|
14729
|
+
const pluginsRoot = (0, import_node_path16.join)(configRoot, "plugins");
|
|
14730
|
+
const target = (0, import_node_path16.join)(pluginsRoot, "local", "mmi");
|
|
14731
|
+
const source = env.MMI_CURSOR_PLUGIN_SOURCE?.trim();
|
|
14732
|
+
if ((0, import_node_fs18.existsSync)(target) && !source) {
|
|
14733
|
+
try {
|
|
14734
|
+
const { stdout } = await runHostBin("git", ["-C", target, "remote", "get-url", "origin"], { timeout: 15e3 });
|
|
14735
|
+
if (!canonicalCursorRemote(stdout)) {
|
|
14736
|
+
return { ok: false, detail: `refused to replace unrelated Cursor plugin directory at ${target}` };
|
|
14737
|
+
}
|
|
14738
|
+
} catch {
|
|
14739
|
+
return { ok: false, detail: `refused to replace unmanaged Cursor plugin directory at ${target}` };
|
|
14740
|
+
}
|
|
14741
|
+
}
|
|
14742
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.join)(pluginsRoot, "local"), { recursive: true });
|
|
14743
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.join)(pluginsRoot, "staging"), { recursive: true });
|
|
14744
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.join)(pluginsRoot, "quarantine"), { recursive: true });
|
|
14745
|
+
const suffix = `${Date.now()}-${process.pid}`;
|
|
14746
|
+
const staged = (0, import_node_path16.join)(pluginsRoot, "staging", `mmi-${suffix}`);
|
|
14747
|
+
const quarantined = (0, import_node_path16.join)(pluginsRoot, "quarantine", `mmi-${suffix}`);
|
|
14748
|
+
try {
|
|
14749
|
+
if (source) {
|
|
14750
|
+
(0, import_node_fs18.cpSync)(source, staged, {
|
|
14751
|
+
recursive: true,
|
|
14752
|
+
filter: (path2) => !path2.split(/[\\/]/).some((part) => part === ".git" || part === "node_modules")
|
|
14753
|
+
});
|
|
14754
|
+
} else {
|
|
14755
|
+
await runHostBin("gh", ["repo", "clone", "mutmutco/MMI-Hub", staged, "--", "--branch", "main", "--depth", "1"], {
|
|
14756
|
+
timeout: CLAUDE_PLUGIN_TIMEOUT_MS
|
|
14757
|
+
});
|
|
14758
|
+
}
|
|
14759
|
+
if (!cursorPluginTreeHealthy(staged)) {
|
|
14760
|
+
(0, import_node_fs18.rmSync)(staged, { recursive: true, force: true });
|
|
14761
|
+
return { ok: false, detail: "downloaded Cursor plugin is incomplete; existing install was preserved" };
|
|
14762
|
+
}
|
|
14763
|
+
let movedOld = false;
|
|
14764
|
+
if ((0, import_node_fs18.existsSync)(target)) {
|
|
14765
|
+
(0, import_node_fs18.renameSync)(target, quarantined);
|
|
14766
|
+
movedOld = true;
|
|
14767
|
+
}
|
|
14768
|
+
try {
|
|
14769
|
+
(0, import_node_fs18.renameSync)(staged, target);
|
|
14770
|
+
} catch (error) {
|
|
14771
|
+
if (movedOld && !(0, import_node_fs18.existsSync)(target)) (0, import_node_fs18.renameSync)(quarantined, target);
|
|
14772
|
+
throw error;
|
|
14773
|
+
}
|
|
14774
|
+
return {
|
|
14775
|
+
ok: true,
|
|
14776
|
+
detail: movedOld ? `installed canonical Cursor plugin; previous checkout quarantined at ${quarantined}` : `installed canonical Cursor plugin at ${target}`
|
|
14777
|
+
};
|
|
14778
|
+
} catch (error) {
|
|
14779
|
+
if ((0, import_node_fs18.existsSync)(staged)) (0, import_node_fs18.rmSync)(staged, { recursive: true, force: true });
|
|
14780
|
+
return { ok: false, detail: error.message.trim().slice(0, 240).replace(/\s+/g, " ") };
|
|
14781
|
+
}
|
|
14782
|
+
}
|
|
14462
14783
|
async function marketplaceAddRefSupported(bin) {
|
|
14463
14784
|
try {
|
|
14464
14785
|
const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
|
|
@@ -14486,49 +14807,79 @@ function pluginReadGrantNote(login = "<your-github-login>") {
|
|
|
14486
14807
|
}
|
|
14487
14808
|
async function applyPluginHeal(surface, log, opts) {
|
|
14488
14809
|
const token = surfaceToken(surface);
|
|
14489
|
-
if (token !== "claude" && token !== "codex") return false;
|
|
14490
|
-
if (!opts?.force && !PLUGIN_SURFACE_HEAL[token]) return false;
|
|
14810
|
+
if (token !== "claude" && token !== "codex" && token !== "kilo") return false;
|
|
14491
14811
|
const descriptor = PLUGIN_SURFACE_HEAL[token];
|
|
14812
|
+
if (!descriptor || !descriptor.healSteps) return false;
|
|
14813
|
+
if (token === "kilo") {
|
|
14814
|
+
log(" \u21BB reinstalling the MMI plugin via `kilo plugin` (install \u2192 server() provisions the skills)\u2026");
|
|
14815
|
+
for (const step of descriptor.healSteps) {
|
|
14816
|
+
const ok = await runKiloPlugin([...step.args]);
|
|
14817
|
+
if (healStepAborts(step, ok)) return false;
|
|
14818
|
+
}
|
|
14819
|
+
return true;
|
|
14820
|
+
}
|
|
14492
14821
|
const tableSteps = descriptor.healSteps;
|
|
14493
14822
|
if (!tableSteps) return false;
|
|
14823
|
+
const loadedCodexLauncher = token === "codex" ? captureCodexHookLauncher() : void 0;
|
|
14494
14824
|
const bin = token;
|
|
14495
14825
|
const refSupported = await marketplaceAddRefSupported(bin);
|
|
14496
14826
|
const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
|
|
14497
14827
|
log(healBannerLine(bin, token, refSupported));
|
|
14498
14828
|
const pinsPath = (0, import_node_path16.join)((0, import_node_os5.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
|
|
14499
14829
|
const pins = token === "claude" ? captureMarketplacePins(readKnownMarketplacesFile(pinsPath), [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]) : /* @__PURE__ */ new Map();
|
|
14500
|
-
|
|
14501
|
-
const
|
|
14502
|
-
|
|
14830
|
+
try {
|
|
14831
|
+
for (const step of steps) {
|
|
14832
|
+
const ok = token === "claude" ? await runClaudePlugin([...step.args]) : await runCodexPlugin([...step.args]);
|
|
14833
|
+
if (healStepAborts(step, ok)) return false;
|
|
14834
|
+
}
|
|
14835
|
+
} finally {
|
|
14836
|
+
if (restoreCodexHookLauncher(loadedCodexLauncher)) {
|
|
14837
|
+
log(" retained the windowless Codex hook bridge for the loaded session; restart removes its need");
|
|
14838
|
+
}
|
|
14503
14839
|
}
|
|
14504
14840
|
const restored = token === "claude" ? restoreMarketplacePinsOnDisk(pinsPath, pins) : void 0;
|
|
14505
14841
|
if (restored) log(` ${restored}`);
|
|
14506
14842
|
return true;
|
|
14507
14843
|
}
|
|
14508
|
-
async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
|
|
14844
|
+
async function healClaudePluginForDoctor(surface = detectSurface(process.env), onStep) {
|
|
14509
14845
|
if (surfaceToken(surface) !== "claude") {
|
|
14510
14846
|
return { ok: false, detail: `not a Claude surface (${surface}) \u2014 no Hub-shipped plugin to reinstall` };
|
|
14511
14847
|
}
|
|
14512
14848
|
const steps = [];
|
|
14513
|
-
const ok = await applyPluginHeal(surface, (msg) =>
|
|
14849
|
+
const ok = await applyPluginHeal(surface, (msg) => {
|
|
14850
|
+
const line = msg.trim();
|
|
14851
|
+
steps.push(line);
|
|
14852
|
+
if (line) onStep?.(line);
|
|
14853
|
+
});
|
|
14514
14854
|
const pinNote = steps.find((s) => s.startsWith("re-pinned ") || s.includes("re-pin by hand"));
|
|
14515
14855
|
return {
|
|
14516
14856
|
ok,
|
|
14517
14857
|
detail: ok ? `marketplace remove \u2192 add \u2192 install succeeded${pinNote ? `; ${pinNote}` : ""}` : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
|
|
14518
14858
|
};
|
|
14519
14859
|
}
|
|
14520
|
-
async function healActivePluginForDoctor(surface = detectSurface(process.env)) {
|
|
14860
|
+
async function healActivePluginForDoctor(surface = detectSurface(process.env), onStep) {
|
|
14521
14861
|
const token = surfaceToken(surface);
|
|
14522
|
-
if (token !== "claude" && token !== "codex") {
|
|
14862
|
+
if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
|
|
14523
14863
|
return { ok: false, detail: `not a supported plugin surface (${surface})` };
|
|
14524
14864
|
}
|
|
14525
|
-
if (token === "claude") return healClaudePluginForDoctor(surface);
|
|
14865
|
+
if (token === "claude") return healClaudePluginForDoctor(surface, onStep);
|
|
14866
|
+
if (token === "cursor") return installCursorPluginCheckout();
|
|
14526
14867
|
const steps = [];
|
|
14527
|
-
const applied = await applyPluginHeal(surface, (msg) =>
|
|
14868
|
+
const applied = await applyPluginHeal(surface, (msg) => {
|
|
14869
|
+
const line = msg.trim();
|
|
14870
|
+
steps.push(line);
|
|
14871
|
+
if (line) onStep?.(line);
|
|
14872
|
+
});
|
|
14528
14873
|
const snapshot = applied ? snapshotPluginGuardInput(surface, true) : void 0;
|
|
14529
14874
|
const guardState = snapshot ? buildPluginGuardDecision(snapshot).state : "unresolved";
|
|
14530
14875
|
const ok = applied && guardState === "healthy";
|
|
14531
14876
|
const verification = snapshot ? `record=${snapshot.installRecordPresent ? "yes" : "no"}, marketplace=${snapshot.marketplaceClonePresent ? "yes" : "no"}, enabled-cache=${snapshot.pluginCachePresent ? "yes" : "no"}` : "reinstall steps did not complete";
|
|
14877
|
+
if (token === "kilo") {
|
|
14878
|
+
return {
|
|
14879
|
+
ok,
|
|
14880
|
+
detail: ok ? `kilo plugin install succeeded; full guard verified (${verification})` : `\`kilo plugin\` reinstall failed full-guard verification (${verification})${steps.length ? `; ${steps[steps.length - 1]}` : ""}`
|
|
14881
|
+
};
|
|
14882
|
+
}
|
|
14532
14883
|
return {
|
|
14533
14884
|
ok,
|
|
14534
14885
|
detail: ok ? `marketplace remove \u2192 add --ref main \u2192 plugin add succeeded; full guard verified (${verification})` : `\`codex plugin\` reinstall failed full-guard verification (${verification})${steps.length ? `; ${steps[steps.length - 1]}` : ""}`
|
|
@@ -14562,14 +14913,14 @@ async function runGuard(readOrigin) {
|
|
|
14562
14913
|
const surface = detectSurface(process.env);
|
|
14563
14914
|
try {
|
|
14564
14915
|
const token = surfaceToken(surface);
|
|
14565
|
-
if (token !== "claude" && token !== "codex") {
|
|
14916
|
+
if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
|
|
14566
14917
|
process.exitCode = 0;
|
|
14567
14918
|
return;
|
|
14568
14919
|
}
|
|
14569
14920
|
const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
|
|
14570
14921
|
const input = snapshotPluginGuardInput(surface, isOrgRepo);
|
|
14571
14922
|
const { state } = buildPluginGuardDecision(input);
|
|
14572
|
-
const { line, exitCode } = buildPluginGuardLine(state);
|
|
14923
|
+
const { line, exitCode } = buildPluginGuardLine(state, { restartHint: reloadAction(surface) });
|
|
14573
14924
|
if (line) console.error(line);
|
|
14574
14925
|
if (token === "codex" && exitCode === 0) {
|
|
14575
14926
|
const trust = codexHookTrustState();
|
|
@@ -14582,6 +14933,10 @@ async function runGuard(readOrigin) {
|
|
|
14582
14933
|
if (surfaceToken(surface) === "codex") {
|
|
14583
14934
|
console.error("[mmi-guard] Could not inspect the active Codex plugin; run `mmi-cli plugin heal`.");
|
|
14584
14935
|
process.exitCode = 1;
|
|
14936
|
+
} else if (surfaceToken(surface) === "kilo" || surfaceToken(surface) === "cursor") {
|
|
14937
|
+
const host = surfaceToken(surface) === "cursor" ? "Cursor" : "Kilo";
|
|
14938
|
+
console.error(`[mmi-guard] Could not inspect the active ${host} plugin; run \`mmi-cli plugin heal\`.`);
|
|
14939
|
+
process.exitCode = 1;
|
|
14585
14940
|
} else {
|
|
14586
14941
|
process.exitCode = 0;
|
|
14587
14942
|
}
|
|
@@ -14589,21 +14944,24 @@ async function runGuard(readOrigin) {
|
|
|
14589
14944
|
}
|
|
14590
14945
|
async function runPluginHeal(surface = detectSurface(process.env)) {
|
|
14591
14946
|
const token = surfaceToken(surface);
|
|
14592
|
-
if (token !== "claude" && token !== "codex") {
|
|
14947
|
+
if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
|
|
14593
14948
|
console.log(nonClaudeSurfaceHealMessage(token ?? void 0));
|
|
14594
14949
|
return;
|
|
14595
14950
|
}
|
|
14596
14951
|
const descriptor = PLUGIN_SURFACE_HEAL[token];
|
|
14597
|
-
const
|
|
14598
|
-
|
|
14952
|
+
const cursorResult = token === "cursor" ? await installCursorPluginCheckout() : void 0;
|
|
14953
|
+
if (cursorResult) console.log(` \u21BB ${cursorResult.detail}`);
|
|
14954
|
+
const applied = cursorResult ? cursorResult.ok : await applyPluginHeal(surface, console.log, { force: true });
|
|
14955
|
+
const healed = token !== "claude" ? applied && buildPluginGuardDecision(snapshotPluginGuardInput(surface, true)).state === "healthy" : applied;
|
|
14599
14956
|
if (healed) {
|
|
14600
14957
|
const trust = token === "codex" ? " Then run /hooks and review + trust the MMI hooks." : "";
|
|
14601
|
-
|
|
14958
|
+
const reload = token === "kilo" ? " The plugin provisions the skills on first load." : "";
|
|
14959
|
+
console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI skills and commands.${trust}${reload}`);
|
|
14602
14960
|
} else {
|
|
14603
14961
|
process.exitCode = 1;
|
|
14604
|
-
const refSupported = await marketplaceAddRefSupported(token);
|
|
14605
|
-
const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
|
|
14606
|
-
const note = refAbsenceNote(token, refSupported);
|
|
14962
|
+
const refSupported = token === "kilo" || token === "cursor" ? false : await marketplaceAddRefSupported(token);
|
|
14963
|
+
const recovery = token === "kilo" || token === "cursor" ? descriptor.recovery : refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
|
|
14964
|
+
const note = token === "kilo" || token === "cursor" ? "" : refAbsenceNote(token, refSupported);
|
|
14607
14965
|
console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
|
|
14608
14966
|
${recovery}${note}${pluginReadGrantNote()}`);
|
|
14609
14967
|
}
|
|
@@ -16126,12 +16484,20 @@ function resolveIssueBody(input, deps) {
|
|
|
16126
16484
|
noun: "body"
|
|
16127
16485
|
});
|
|
16128
16486
|
}
|
|
16129
|
-
function resolveIssueTitle(input, deps) {
|
|
16130
|
-
|
|
16487
|
+
async function resolveIssueTitle(input, deps) {
|
|
16488
|
+
const title = await resolveTextArg({ value: input.title, file: input.titleFile }, deps, {
|
|
16131
16489
|
value: "--title",
|
|
16132
16490
|
file: "--title-file",
|
|
16133
16491
|
noun: "title"
|
|
16134
16492
|
});
|
|
16493
|
+
const normalized = title.replace(/(?:\r\n|\n|\r)$/, "");
|
|
16494
|
+
if (!normalized.trim()) {
|
|
16495
|
+
throw new TextArgError("--title produced an empty title", ERROR_CODES.ERR_EMPTY_INPUT, "--title");
|
|
16496
|
+
}
|
|
16497
|
+
if (/[\r\n]/.test(normalized)) {
|
|
16498
|
+
throw new TextArgError("--title must be one line; internal line breaks are not allowed", ERROR_CODES.ERR_INVALID_INPUT, "--title");
|
|
16499
|
+
}
|
|
16500
|
+
return normalized;
|
|
16135
16501
|
}
|
|
16136
16502
|
|
|
16137
16503
|
// src/issue-view-json.ts
|
|
@@ -16219,6 +16585,40 @@ var PATH_OVERRIDES = {
|
|
|
16219
16585
|
"secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
16220
16586
|
"secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
|
|
16221
16587
|
};
|
|
16588
|
+
var COMMAND_OWNERSHIP = {
|
|
16589
|
+
onboard: { module_owner: "cli/src/discovery-commands.ts", consumer: "agent-session" },
|
|
16590
|
+
status: { module_owner: "cli/src/discovery-commands.ts", consumer: "agent-session" },
|
|
16591
|
+
next: { module_owner: "cli/src/discovery-commands.ts", consumer: "agent-session" },
|
|
16592
|
+
doctor: { module_owner: "cli/src/doctor-clean.ts", consumer: "agent-session" },
|
|
16593
|
+
whoami: { module_owner: "cli/src/whoami.ts", consumer: "agent-session" },
|
|
16594
|
+
commands: { module_owner: "cli/src/command-manifest.ts", consumer: "agent-session" },
|
|
16595
|
+
explain: { module_owner: "cli/src/explain-command.ts", consumer: "agent-session" },
|
|
16596
|
+
board: { module_owner: "cli/src/board-commands.ts", consumer: "agent-workflow" },
|
|
16597
|
+
issue: { module_owner: "cli/src/issue-commands.ts", consumer: "agent-workflow" },
|
|
16598
|
+
worktree: { module_owner: "cli/src/worktree-lifecycle-commands.ts", consumer: "agent-workflow" },
|
|
16599
|
+
stage: { module_owner: "cli/src/stage-commands.ts", consumer: "agent-workflow" },
|
|
16600
|
+
pr: { module_owner: "cli/src/pr-commands.ts", consumer: "agent-workflow" },
|
|
16601
|
+
ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
|
|
16602
|
+
rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
16603
|
+
release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
16604
|
+
hotfix: { module_owner: "cli/src/hotfix-apply.ts", consumer: "release-operator" },
|
|
16605
|
+
train: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
16606
|
+
bootstrap: { module_owner: "cli/src/bootstrap-commands.ts", consumer: "repo-bootstrap" },
|
|
16607
|
+
secrets: { module_owner: "cli/src/secrets-commands.ts", consumer: "authenticated-operator" },
|
|
16608
|
+
docs: { module_owner: "cli/src/docs-index-command.ts", consumer: "repo-gates" },
|
|
16609
|
+
tests: { module_owner: "cli/src/test-policy-core.ts", consumer: "repo-gates" },
|
|
16610
|
+
wave: { module_owner: "cli/src/wave-land.ts", consumer: "campaign-orchestrator" },
|
|
16611
|
+
report: { module_owner: "cli/src/report.ts", consumer: "campaign-orchestrator" },
|
|
16612
|
+
"skill-lesson": { module_owner: "cli/src/skill-lesson.ts", consumer: "campaign-orchestrator" },
|
|
16613
|
+
org: { module_owner: "cli/src/command-consolidation.ts", consumer: "org-operator" },
|
|
16614
|
+
runtime: { module_owner: "cli/src/command-consolidation.ts", consumer: "runtime-operator" },
|
|
16615
|
+
plugin: { module_owner: "cli/src/plugin-guard-io.ts", consumer: "host-runtime" }
|
|
16616
|
+
};
|
|
16617
|
+
function commandOwnership(name) {
|
|
16618
|
+
const ownership = COMMAND_OWNERSHIP[name];
|
|
16619
|
+
if (!ownership) throw new Error(`command taxonomy: root "${name}" has no module owner or consumer`);
|
|
16620
|
+
return ownership;
|
|
16621
|
+
}
|
|
16222
16622
|
var DEFAULT_ADMIN_MARKER = "(master-only)";
|
|
16223
16623
|
var ADMIN_MARKERS = {
|
|
16224
16624
|
"secrets org-catalog": "(writes master-only)"
|
|
@@ -16233,7 +16633,8 @@ function primaryMetadata(name) {
|
|
|
16233
16633
|
return {
|
|
16234
16634
|
category: SUPPORT_PRIMARY.has(name) ? "support" : "core",
|
|
16235
16635
|
discovery: "primary",
|
|
16236
|
-
help_group: helpGroup
|
|
16636
|
+
help_group: helpGroup,
|
|
16637
|
+
...commandOwnership(name)
|
|
16237
16638
|
};
|
|
16238
16639
|
}
|
|
16239
16640
|
}
|
|
@@ -16248,7 +16649,8 @@ function topLevelMetadata(name) {
|
|
|
16248
16649
|
return {
|
|
16249
16650
|
category: name === "plugin" ? "internal" : "admin",
|
|
16250
16651
|
discovery: "all-only",
|
|
16251
|
-
help_group: "Operations"
|
|
16652
|
+
help_group: "Operations",
|
|
16653
|
+
...commandOwnership(name)
|
|
16252
16654
|
};
|
|
16253
16655
|
}
|
|
16254
16656
|
function setCommandMetadata(command, metadata) {
|
|
@@ -16265,7 +16667,7 @@ var REQUIRED_OPTION_HELP = {
|
|
|
16265
16667
|
};
|
|
16266
16668
|
function classifyTree(command, path2, inherited, hideFromParent) {
|
|
16267
16669
|
const override = PATH_OVERRIDES[path2];
|
|
16268
|
-
const metadata = override
|
|
16670
|
+
const metadata = override ? { ...inherited, ...override } : inherited;
|
|
16269
16671
|
setCommandMetadata(command, metadata);
|
|
16270
16672
|
const hideByOverride = override !== void 0 && metadata.discovery === "hidden";
|
|
16271
16673
|
if ((hideByOverride || hideFromParent) && metadata.discovery !== "primary") {
|
|
@@ -16401,7 +16803,9 @@ function buildCommand(cmd, path2) {
|
|
|
16401
16803
|
const metadata = commandMetadata(cmd) ?? {
|
|
16402
16804
|
category: "core",
|
|
16403
16805
|
discovery: "primary",
|
|
16404
|
-
help_group: "Plan and work"
|
|
16806
|
+
help_group: "Plan and work",
|
|
16807
|
+
module_owner: "unclassified",
|
|
16808
|
+
consumer: "unclassified"
|
|
16405
16809
|
};
|
|
16406
16810
|
const out = {
|
|
16407
16811
|
name: cmd.name(),
|
|
@@ -20960,7 +21364,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
20960
21364
|
name: e.name,
|
|
20961
21365
|
executor: e.executor,
|
|
20962
21366
|
detail: "armed with no SCHEDULE# row",
|
|
20963
|
-
remedy: 'add the eight-field header (docs/schedules.md "The entry template") and re-run
|
|
21367
|
+
remedy: 'add the eight-field header (docs/schedules.md "The entry template") and re-run schedules register so it creates a SCHEDULE# row'
|
|
20964
21368
|
});
|
|
20965
21369
|
}
|
|
20966
21370
|
}
|
|
@@ -20973,7 +21377,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
20973
21377
|
name: r.id,
|
|
20974
21378
|
executor: r.executor || live.executor,
|
|
20975
21379
|
detail: `live cadence \`${live.cadence}\` disagrees with registered \`${r.cadence}\``,
|
|
20976
|
-
remedy: "re-run
|
|
21380
|
+
remedy: "re-run schedules register to refresh the SCHEDULE# row from the current header/cron"
|
|
20977
21381
|
});
|
|
20978
21382
|
}
|
|
20979
21383
|
continue;
|
|
@@ -20985,7 +21389,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
20985
21389
|
name: r.id,
|
|
20986
21390
|
executor: r.executor || "github-actions",
|
|
20987
21391
|
detail: "registered with no workflow file behind it",
|
|
20988
|
-
remedy: `restore the workflow in ${r.repo} (or, if it was retired on purpose, re-run
|
|
21392
|
+
remedy: `restore the workflow in ${r.repo} (or, if it was retired on purpose, re-run schedules register so the replace prunes the stale SCHEDULE# row)`
|
|
20989
21393
|
});
|
|
20990
21394
|
}
|
|
20991
21395
|
}
|
|
@@ -21433,27 +21837,27 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
21433
21837
|
const expectedId = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
|
|
21434
21838
|
const missing = SCHEDULE_HEADER_FIELDS.filter((f) => !header[f]);
|
|
21435
21839
|
if (missing.length) {
|
|
21436
|
-
throw new Error(`schedules
|
|
21840
|
+
throw new Error(`schedules register: ${expectedId} (${workflowPath}) is a scheduled workflow but is missing the eight-field entry-template header field(s): ${missing.join(", ")} \u2014 see docs/schedules.md "The entry template".`);
|
|
21437
21841
|
}
|
|
21438
21842
|
const h = header;
|
|
21439
21843
|
if (h.schedule !== expectedId) {
|
|
21440
|
-
throw new Error(`schedules
|
|
21844
|
+
throw new Error(`schedules register: ${workflowPath} header \`schedule: ${h.schedule}\` must equal the join key \`${expectedId}\` (<repo>/<workflow-basename>).`);
|
|
21441
21845
|
}
|
|
21442
21846
|
for (const cron of crons) {
|
|
21443
21847
|
if (!cadenceContainsCron(h.cadence, cron)) {
|
|
21444
|
-
throw new Error(`schedules
|
|
21848
|
+
throw new Error(`schedules register: ${expectedId} cadence \`${h.cadence}\` does not contain the workflow cron \`${cron}\` (whole-token comparison) \u2014 the declared cadence disagrees with the actual schedule.`);
|
|
21445
21849
|
}
|
|
21446
21850
|
}
|
|
21447
21851
|
if (llmFromHeader(yamlText) === "unknown") {
|
|
21448
|
-
throw new Error(`schedules
|
|
21852
|
+
throw new Error(`schedules register: ${expectedId} header \`llm: ${h.llm}\` does not classify (yes / no / embeddings) \u2014 an unreadable declaration would register a row whose LLM column lies.`);
|
|
21449
21853
|
}
|
|
21450
21854
|
const target = header.target;
|
|
21451
21855
|
if (target !== void 0 && !/^mmi-fleet-[A-Za-z0-9_-]{1,130}$/.test(target)) {
|
|
21452
|
-
throw new Error(`schedules
|
|
21856
|
+
throw new Error(`schedules register: ${expectedId} header \`target: ${target}\` must be a bare lambda function name in the reserved fleet namespace \`mmi-fleet-*\` (allowed after the prefix: A-Z a-z 0-9 _ -; total max 140). The dispatcher can only invoke mmi-fleet-* functions.`);
|
|
21453
21857
|
}
|
|
21454
21858
|
const model = header.model;
|
|
21455
21859
|
if (model !== void 0 && !/^[a-z][a-z0-9-]{0,30}$/.test(model)) {
|
|
21456
|
-
throw new Error(`schedules
|
|
21860
|
+
throw new Error(`schedules register: ${expectedId} header \`model: ${model}\` must be a model ROLE KEY (lowercase kebab, max 31 chars \u2014 e.g. janitor, arbiter), never a model id \u2014 ids resolve from the vault at dispatch.`);
|
|
21457
21861
|
}
|
|
21458
21862
|
return {
|
|
21459
21863
|
id: expectedId,
|
|
@@ -21478,7 +21882,7 @@ function scheduleRecordsFromWorkflows(repo, files) {
|
|
|
21478
21882
|
const rec = scheduleRecordFromWorkflow(repo, path2, text);
|
|
21479
21883
|
if (!rec) continue;
|
|
21480
21884
|
if (seen.has(rec.id)) {
|
|
21481
|
-
throw new Error(`schedules
|
|
21885
|
+
throw new Error(`schedules register: duplicate schedule id \`${rec.id}\` \u2014 two workflow files in ${repo} share one join key (${rec.sourcePath}).`);
|
|
21482
21886
|
}
|
|
21483
21887
|
seen.add(rec.id);
|
|
21484
21888
|
records.push(rec);
|
|
@@ -21530,16 +21934,16 @@ async function runSchedulesLift(opts, deps = {}) {
|
|
|
21530
21934
|
const url = await (deps.originUrl ?? (() => gitOut(["remote", "get-url", "origin"])))();
|
|
21531
21935
|
repo = repoNameFromRemoteUrl(url) ?? void 0;
|
|
21532
21936
|
if (!repo) {
|
|
21533
|
-
throw new Error("schedules
|
|
21937
|
+
throw new Error("schedules register: could not resolve the repo name from the origin remote \u2014 pass --repo <name>.");
|
|
21534
21938
|
}
|
|
21535
21939
|
}
|
|
21536
21940
|
if (!SCHEDULE_REPO_RE.test(repo)) {
|
|
21537
|
-
throw new SchedulesLiftUsageError(`schedules
|
|
21941
|
+
throw new SchedulesLiftUsageError(`schedules register: repo \`${repo}\` is not a bare repo segment (allowed: A-Z a-z 0-9 _ . -; no \`/\` or \`#\`) \u2014 the schedule id is <repo>/<name>, so a separator would form an invalid multi-segment id.`);
|
|
21538
21942
|
}
|
|
21539
21943
|
const dir = opts.dir ?? DEFAULT_WORKFLOWS_DIR;
|
|
21540
21944
|
const files = await (deps.readFiles ?? readWorkflowFiles)(dir);
|
|
21541
21945
|
if (!files.length) {
|
|
21542
|
-
throw new Error(`schedules
|
|
21946
|
+
throw new Error(`schedules register: no workflow files under ${dir} \u2014 refusing to post an empty registration (the route prunes; run from the repo checkout or pass --dir).`);
|
|
21543
21947
|
}
|
|
21544
21948
|
const records = scheduleRecordsFromWorkflows(repo, files);
|
|
21545
21949
|
const payload = { repo, schedules: records };
|
|
@@ -21548,11 +21952,11 @@ async function runSchedulesLift(opts, deps = {}) {
|
|
|
21548
21952
|
if (!response.ok) {
|
|
21549
21953
|
const unreachable = response.unreachable ?? (response.status === 404 ? "route-absent" : void 0);
|
|
21550
21954
|
if (unreachable) {
|
|
21551
|
-
throw new RegistryUnreachableError(`schedules
|
|
21955
|
+
throw new RegistryUnreachableError(`schedules register: registry unreachable (${unreachable}) \u2014 ${response.error ?? `HTTP ${response.status}`}`);
|
|
21552
21956
|
}
|
|
21553
|
-
if (response.error) throw new Error(`schedules
|
|
21957
|
+
if (response.error) throw new Error(`schedules register: ${response.error}`);
|
|
21554
21958
|
const detail = response.body?.error ?? "";
|
|
21555
|
-
throw new Error(`schedules
|
|
21959
|
+
throw new Error(`schedules register: HTTP ${response.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
21556
21960
|
}
|
|
21557
21961
|
return { repo, records, payload, response };
|
|
21558
21962
|
}
|
|
@@ -21573,8 +21977,8 @@ function withArmingReport(body) {
|
|
|
21573
21977
|
}
|
|
21574
21978
|
function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
21575
21979
|
const schedules = program3.commands.find((c) => c.name() === "schedules");
|
|
21576
|
-
if (!schedules) throw new Error("schedules
|
|
21577
|
-
jsonParity(schedules.command("register").
|
|
21980
|
+
if (!schedules) throw new Error("schedules register: registerSchedulesCommands must run first \u2014 register attaches to the `schedules` command");
|
|
21981
|
+
jsonParity(schedules.command("register").description(`register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from \`lift\`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187). Registering does NOT arm the clock: the fleet-clock reconciler arms the rows within ${FLEET_CLOCK_RECONCILE_WINDOW}, and the success JSON carries an \`arming\` block saying so (#3281)`).option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to register (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write")).action(async (o) => {
|
|
21578
21982
|
try {
|
|
21579
21983
|
const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
|
|
21580
21984
|
if (o.dryRun) {
|
|
@@ -23624,7 +24028,7 @@ function registerBoardCommands(program3) {
|
|
|
23624
24028
|
"Claim already assigns and moves Status to In Progress, so do not also board move it.",
|
|
23625
24029
|
"Multiple refs are handled as a batch and return per-item results."
|
|
23626
24030
|
]);
|
|
23627
|
-
board.command("show <issue>").
|
|
24031
|
+
board.command("show <issue>").description("print one board item (status, assignees, type, url) with its body and comments").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return the item even if its body/comments fetch fails").action(async (issueRef, o) => {
|
|
23628
24032
|
try {
|
|
23629
24033
|
const item = await showBoardItem({ config: await loadConfigForBoardSelector2(issueRef, o.repo), selector: issueRef, repo: o.repo, allowPartial: o.allowPartial });
|
|
23630
24034
|
console.log(o.json ? JSON.stringify(item) : renderBoardItem(item));
|
|
@@ -26954,21 +27358,790 @@ function renderTally(checks) {
|
|
|
26954
27358
|
const healthy = total - checks.filter((c) => !c.ok).length;
|
|
26955
27359
|
return healthy === total ? `\u2713 all ${total} checks healthy` : `\u2713 ${healthy} of ${total} checks healthy`;
|
|
26956
27360
|
}
|
|
26957
|
-
function
|
|
26958
|
-
|
|
26959
|
-
|
|
26960
|
-
|
|
26961
|
-
|
|
26962
|
-
|
|
26963
|
-
|
|
27361
|
+
function doctorReportExitCode(checks) {
|
|
27362
|
+
return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
|
|
27363
|
+
}
|
|
27364
|
+
|
|
27365
|
+
// ../surfaces.json
|
|
27366
|
+
var surfaces_default = {
|
|
27367
|
+
_comment: "Hub-owned agent/developer contract. sharedAgentCore owns policy and capability sources once; agentSurfaces contains only host delivery differences; surfaces inventories and assembles the outward artifacts those adapters reference. See docs/surfaces.schema.json (v7) and docs/surface-delivery-template.md.",
|
|
27368
|
+
schemaVersion: 7,
|
|
27369
|
+
productScope: "agentic-coding",
|
|
27370
|
+
sharedAgentCore: {
|
|
27371
|
+
skills: {
|
|
27372
|
+
ownerPath: "skills",
|
|
27373
|
+
sourceSurfaceId: "mmi-skills"
|
|
27374
|
+
},
|
|
27375
|
+
cli: {
|
|
27376
|
+
ownerPath: "cli",
|
|
27377
|
+
surfaceId: "mmi-cli"
|
|
27378
|
+
},
|
|
27379
|
+
hookPolicy: {
|
|
27380
|
+
launcherPath: "bin/mmi-hook",
|
|
27381
|
+
ownerPath: "scripts/hook-policy.mjs",
|
|
27382
|
+
runnerPath: "scripts/hook-run.mjs",
|
|
27383
|
+
sourceSurfaceId: "mmi-hook-policy",
|
|
27384
|
+
version: 1,
|
|
27385
|
+
windowsLauncherPath: "bin/mmi-hook.exe"
|
|
27386
|
+
},
|
|
27387
|
+
releaseMetadata: {
|
|
27388
|
+
ownerPath: "surfaces.json",
|
|
27389
|
+
bomPath: "distribution-bom.json"
|
|
26964
27390
|
}
|
|
27391
|
+
},
|
|
27392
|
+
agentSurfaces: [
|
|
27393
|
+
{
|
|
27394
|
+
token: "claude",
|
|
27395
|
+
displayName: "Claude Code",
|
|
27396
|
+
lifecycle: "active",
|
|
27397
|
+
artifactIds: ["mmi-claude-plugin", "mmi-claude-marketplace", "mmi-skills", "mmi-hook-policy", "mmi-hooks", "mmi-cli"],
|
|
27398
|
+
plannedArtifactIds: [],
|
|
27399
|
+
assembly: {
|
|
27400
|
+
mode: "source-tree",
|
|
27401
|
+
rootPath: ".",
|
|
27402
|
+
sync: []
|
|
27403
|
+
},
|
|
27404
|
+
install: {
|
|
27405
|
+
mechanism: "marketplace",
|
|
27406
|
+
locator: "mmi@mutmutco"
|
|
27407
|
+
},
|
|
27408
|
+
upgrade: {
|
|
27409
|
+
mechanism: "reinstall",
|
|
27410
|
+
reload: "session"
|
|
27411
|
+
},
|
|
27412
|
+
skills: {
|
|
27413
|
+
delivery: "plugin",
|
|
27414
|
+
sourceSurfaceId: "mmi-skills",
|
|
27415
|
+
artifactId: "mmi-skills",
|
|
27416
|
+
invocation: {
|
|
27417
|
+
entry: "/mmi:mmi",
|
|
27418
|
+
any: "/mmi:<skill>"
|
|
27419
|
+
}
|
|
27420
|
+
},
|
|
27421
|
+
hooks: {
|
|
27422
|
+
adapterPath: "hooks/hooks.json",
|
|
27423
|
+
sourceSurfaceId: "mmi-hook-policy",
|
|
27424
|
+
gates: [
|
|
27425
|
+
{ id: "command-ladder", failure: "closed" },
|
|
27426
|
+
{ id: "vault-edit", failure: "closed" },
|
|
27427
|
+
{ id: "secret-output", failure: "open" }
|
|
27428
|
+
],
|
|
27429
|
+
probe: "installed-contract",
|
|
27430
|
+
execution: "command",
|
|
27431
|
+
preToolUse: "enforced",
|
|
27432
|
+
postToolOutput: "rewrite",
|
|
27433
|
+
finalOutput: "unsupported"
|
|
27434
|
+
},
|
|
27435
|
+
cli: {
|
|
27436
|
+
delivery: "bundled",
|
|
27437
|
+
artifactId: "mmi-cli"
|
|
27438
|
+
},
|
|
27439
|
+
ownership: {
|
|
27440
|
+
trust: "host",
|
|
27441
|
+
cache: "host",
|
|
27442
|
+
repair: "mmi-cli"
|
|
27443
|
+
},
|
|
27444
|
+
certification: {
|
|
27445
|
+
hostCommand: "claude",
|
|
27446
|
+
versionArgs: ["--version"]
|
|
27447
|
+
},
|
|
27448
|
+
enforcementCeilings: [
|
|
27449
|
+
"Final assistant text cannot be rewritten by the hook surface.",
|
|
27450
|
+
"Disabled or untrusted hooks enforce nothing."
|
|
27451
|
+
]
|
|
27452
|
+
},
|
|
27453
|
+
{
|
|
27454
|
+
token: "codex",
|
|
27455
|
+
displayName: "Codex",
|
|
27456
|
+
lifecycle: "active",
|
|
27457
|
+
artifactIds: ["mmi-codex-plugin", "mmi-skills", "mmi-hook-policy", "mmi-cli"],
|
|
27458
|
+
plannedArtifactIds: [],
|
|
27459
|
+
assembly: {
|
|
27460
|
+
mode: "source-tree",
|
|
27461
|
+
rootPath: ".",
|
|
27462
|
+
sync: []
|
|
27463
|
+
},
|
|
27464
|
+
install: {
|
|
27465
|
+
mechanism: "marketplace",
|
|
27466
|
+
locator: "mmi@mutmutco"
|
|
27467
|
+
},
|
|
27468
|
+
upgrade: {
|
|
27469
|
+
mechanism: "marketplace-upgrade",
|
|
27470
|
+
reload: "session"
|
|
27471
|
+
},
|
|
27472
|
+
skills: {
|
|
27473
|
+
delivery: "plugin",
|
|
27474
|
+
sourceSurfaceId: "mmi-skills",
|
|
27475
|
+
artifactId: "mmi-skills",
|
|
27476
|
+
invocation: {
|
|
27477
|
+
entry: "$mmi:mmi",
|
|
27478
|
+
any: "$mmi:<skill>"
|
|
27479
|
+
}
|
|
27480
|
+
},
|
|
27481
|
+
hooks: {
|
|
27482
|
+
adapterPath: "hooks/codex-hooks.json",
|
|
27483
|
+
sourceSurfaceId: "mmi-hook-policy",
|
|
27484
|
+
gates: [
|
|
27485
|
+
{ id: "command-ladder", failure: "closed" },
|
|
27486
|
+
{ id: "vault-edit", failure: "closed" },
|
|
27487
|
+
{ id: "secret-output", failure: "open" }
|
|
27488
|
+
],
|
|
27489
|
+
probe: "installed-contract",
|
|
27490
|
+
execution: "node-bootstrap",
|
|
27491
|
+
preToolUse: "enforced",
|
|
27492
|
+
postToolOutput: "detect-only",
|
|
27493
|
+
finalOutput: "unsupported"
|
|
27494
|
+
},
|
|
27495
|
+
cli: {
|
|
27496
|
+
delivery: "standalone",
|
|
27497
|
+
artifactId: "mmi-cli"
|
|
27498
|
+
},
|
|
27499
|
+
ownership: {
|
|
27500
|
+
trust: "operator",
|
|
27501
|
+
cache: "host",
|
|
27502
|
+
repair: "mmi-cli"
|
|
27503
|
+
},
|
|
27504
|
+
certification: {
|
|
27505
|
+
hostCommand: "codex",
|
|
27506
|
+
versionArgs: ["--version"]
|
|
27507
|
+
},
|
|
27508
|
+
enforcementCeilings: [
|
|
27509
|
+
"Post-tool hooks can detect secret output but cannot rewrite it.",
|
|
27510
|
+
"Hosted tools and disabled or untrusted hooks bypass local enforcement."
|
|
27511
|
+
]
|
|
27512
|
+
},
|
|
27513
|
+
{
|
|
27514
|
+
token: "kimi",
|
|
27515
|
+
displayName: "Kimi Code CLI",
|
|
27516
|
+
lifecycle: "active",
|
|
27517
|
+
artifactIds: ["mmi-kimi-plugin", "mmi-skills", "mmi-hook-policy", "mmi-cli"],
|
|
27518
|
+
plannedArtifactIds: [],
|
|
27519
|
+
assembly: {
|
|
27520
|
+
mode: "source-tree",
|
|
27521
|
+
rootPath: ".",
|
|
27522
|
+
sync: []
|
|
27523
|
+
},
|
|
27524
|
+
install: {
|
|
27525
|
+
mechanism: "github-source",
|
|
27526
|
+
locator: "https://github.com/mutmutco/MMI-Hub"
|
|
27527
|
+
},
|
|
27528
|
+
upgrade: {
|
|
27529
|
+
mechanism: "reinstall",
|
|
27530
|
+
reload: "session"
|
|
27531
|
+
},
|
|
27532
|
+
skills: {
|
|
27533
|
+
delivery: "plugin",
|
|
27534
|
+
sourceSurfaceId: "mmi-skills",
|
|
27535
|
+
artifactId: "mmi-skills",
|
|
27536
|
+
invocation: {
|
|
27537
|
+
entry: "/skill:mmi",
|
|
27538
|
+
any: "/skill:<skill>"
|
|
27539
|
+
}
|
|
27540
|
+
},
|
|
27541
|
+
hooks: {
|
|
27542
|
+
adapterPath: ".kimi-plugin/plugin.json",
|
|
27543
|
+
sourceSurfaceId: "mmi-hook-policy",
|
|
27544
|
+
gates: [
|
|
27545
|
+
{ id: "command-ladder", failure: "closed" },
|
|
27546
|
+
{ id: "vault-edit", failure: "closed" },
|
|
27547
|
+
{ id: "secret-output", failure: "open" }
|
|
27548
|
+
],
|
|
27549
|
+
probe: "installed-contract",
|
|
27550
|
+
execution: "inline-manifest",
|
|
27551
|
+
preToolUse: "enforced",
|
|
27552
|
+
postToolOutput: "detect-only",
|
|
27553
|
+
finalOutput: "unsupported"
|
|
27554
|
+
},
|
|
27555
|
+
cli: {
|
|
27556
|
+
delivery: "standalone",
|
|
27557
|
+
artifactId: "mmi-cli"
|
|
27558
|
+
},
|
|
27559
|
+
ownership: {
|
|
27560
|
+
trust: "host",
|
|
27561
|
+
cache: "host",
|
|
27562
|
+
repair: "operator"
|
|
27563
|
+
},
|
|
27564
|
+
certification: {
|
|
27565
|
+
hostCommand: "kimi",
|
|
27566
|
+
versionArgs: ["--version"]
|
|
27567
|
+
},
|
|
27568
|
+
enforcementCeilings: [
|
|
27569
|
+
"Post-tool hooks are observation-only and cannot rewrite secret output.",
|
|
27570
|
+
"Hook errors, timeouts, disabled plugins, and untrusted plugins enforce nothing."
|
|
27571
|
+
]
|
|
27572
|
+
},
|
|
27573
|
+
{
|
|
27574
|
+
token: "cursor",
|
|
27575
|
+
displayName: "Cursor",
|
|
27576
|
+
lifecycle: "active",
|
|
27577
|
+
artifactIds: ["mmi-cursor-plugin", "mmi-skills", "mmi-hook-policy", "mmi-cli"],
|
|
27578
|
+
plannedArtifactIds: [],
|
|
27579
|
+
assembly: {
|
|
27580
|
+
mode: "source-tree",
|
|
27581
|
+
rootPath: ".",
|
|
27582
|
+
sync: []
|
|
27583
|
+
},
|
|
27584
|
+
install: {
|
|
27585
|
+
mechanism: "github-source",
|
|
27586
|
+
locator: "~/.cursor/plugins/local/mmi"
|
|
27587
|
+
},
|
|
27588
|
+
upgrade: {
|
|
27589
|
+
mechanism: "reinstall",
|
|
27590
|
+
reload: "workspace"
|
|
27591
|
+
},
|
|
27592
|
+
skills: {
|
|
27593
|
+
delivery: "plugin",
|
|
27594
|
+
sourceSurfaceId: "mmi-skills",
|
|
27595
|
+
artifactId: "mmi-skills",
|
|
27596
|
+
invocation: {
|
|
27597
|
+
entry: "/mmi",
|
|
27598
|
+
any: "/<skill>"
|
|
27599
|
+
}
|
|
27600
|
+
},
|
|
27601
|
+
hooks: {
|
|
27602
|
+
adapterPath: "hooks/cursor-hooks.json",
|
|
27603
|
+
sourceSurfaceId: "mmi-hook-policy",
|
|
27604
|
+
gates: [
|
|
27605
|
+
{ id: "command-ladder", failure: "closed" },
|
|
27606
|
+
{ id: "vault-edit", failure: "closed" },
|
|
27607
|
+
{ id: "secret-output", failure: "open" }
|
|
27608
|
+
],
|
|
27609
|
+
probe: "installed-contract",
|
|
27610
|
+
execution: "command",
|
|
27611
|
+
preToolUse: "enforced",
|
|
27612
|
+
postToolOutput: "detect-only",
|
|
27613
|
+
finalOutput: "unsupported"
|
|
27614
|
+
},
|
|
27615
|
+
cli: {
|
|
27616
|
+
delivery: "standalone",
|
|
27617
|
+
artifactId: "mmi-cli"
|
|
27618
|
+
},
|
|
27619
|
+
ownership: {
|
|
27620
|
+
trust: "host",
|
|
27621
|
+
cache: "mmi-cli",
|
|
27622
|
+
repair: "mmi-cli"
|
|
27623
|
+
},
|
|
27624
|
+
certification: {
|
|
27625
|
+
hostCommand: "cursor-agent",
|
|
27626
|
+
versionArgs: ["--version"]
|
|
27627
|
+
},
|
|
27628
|
+
enforcementCeilings: [
|
|
27629
|
+
"Cursor can replace MCP results only; it cannot rewrite ordinary tool output after execution, though MMI still detects secret-shaped output.",
|
|
27630
|
+
"Final assistant output is outside Cursor hook control.",
|
|
27631
|
+
"Cloud agents do not run sessionStart or sessionEnd hooks, and disabled plugins or untrusted workspaces enforce nothing."
|
|
27632
|
+
]
|
|
27633
|
+
},
|
|
27634
|
+
{
|
|
27635
|
+
token: "kilo",
|
|
27636
|
+
displayName: "Kilo Code",
|
|
27637
|
+
lifecycle: "active",
|
|
27638
|
+
artifactIds: ["mmi-kilo-plugin", "mmi-kilo-skills", "mmi-hook-policy", "mmi-cli"],
|
|
27639
|
+
plannedArtifactIds: [],
|
|
27640
|
+
assembly: {
|
|
27641
|
+
mode: "package-directory",
|
|
27642
|
+
rootPath: ".kilo-plugin",
|
|
27643
|
+
sync: [
|
|
27644
|
+
{
|
|
27645
|
+
mode: "directories",
|
|
27646
|
+
sourcePath: "skills",
|
|
27647
|
+
targetPath: ".kilo-plugin/skills",
|
|
27648
|
+
excludeNamePrefixes: ["_"]
|
|
27649
|
+
},
|
|
27650
|
+
{
|
|
27651
|
+
mode: "files",
|
|
27652
|
+
sourcePath: "scripts",
|
|
27653
|
+
targetPath: ".kilo-plugin/scripts",
|
|
27654
|
+
include: [
|
|
27655
|
+
"pretooluse-shell-gates.mjs",
|
|
27656
|
+
"vault-edit-gate.mjs",
|
|
27657
|
+
"secret-redact.mjs",
|
|
27658
|
+
"deny-gate-crash.mjs",
|
|
27659
|
+
"secret-echo-lint.mjs",
|
|
27660
|
+
"env-write-lint.mjs",
|
|
27661
|
+
"command-ladder-gate.mjs",
|
|
27662
|
+
"command-ladder-core.mjs",
|
|
27663
|
+
"validate-hook.mjs",
|
|
27664
|
+
"hook-io.mjs",
|
|
27665
|
+
"hook-trace.mjs",
|
|
27666
|
+
"edit-tool-paths.mjs",
|
|
27667
|
+
"throttle-core.mjs",
|
|
27668
|
+
"hook-policy.mjs",
|
|
27669
|
+
"hook-run.mjs"
|
|
27670
|
+
]
|
|
27671
|
+
}
|
|
27672
|
+
]
|
|
27673
|
+
},
|
|
27674
|
+
install: {
|
|
27675
|
+
mechanism: "npm",
|
|
27676
|
+
locator: "@mutmutco/kilo-plugin"
|
|
27677
|
+
},
|
|
27678
|
+
upgrade: {
|
|
27679
|
+
mechanism: "package-manager",
|
|
27680
|
+
reload: "session"
|
|
27681
|
+
},
|
|
27682
|
+
skills: {
|
|
27683
|
+
delivery: "provisioned",
|
|
27684
|
+
sourceSurfaceId: "mmi-skills",
|
|
27685
|
+
artifactId: "mmi-kilo-skills",
|
|
27686
|
+
invocation: {
|
|
27687
|
+
entry: "mmi skill via the skill tool",
|
|
27688
|
+
any: "skill tool"
|
|
27689
|
+
}
|
|
27690
|
+
},
|
|
27691
|
+
hooks: {
|
|
27692
|
+
adapterPath: ".kilo-plugin/server.mjs",
|
|
27693
|
+
sourceSurfaceId: "mmi-hook-policy",
|
|
27694
|
+
gates: [
|
|
27695
|
+
{ id: "command-ladder", failure: "closed" },
|
|
27696
|
+
{ id: "vault-edit", failure: "closed" },
|
|
27697
|
+
{ id: "secret-output", failure: "open" }
|
|
27698
|
+
],
|
|
27699
|
+
probe: "installed-contract",
|
|
27700
|
+
execution: "in-process",
|
|
27701
|
+
preToolUse: "enforced",
|
|
27702
|
+
postToolOutput: "rewrite",
|
|
27703
|
+
finalOutput: "rewrite"
|
|
27704
|
+
},
|
|
27705
|
+
cli: {
|
|
27706
|
+
delivery: "standalone",
|
|
27707
|
+
artifactId: "mmi-cli"
|
|
27708
|
+
},
|
|
27709
|
+
ownership: {
|
|
27710
|
+
trust: "host",
|
|
27711
|
+
cache: "host",
|
|
27712
|
+
repair: "mmi-cli"
|
|
27713
|
+
},
|
|
27714
|
+
certification: {
|
|
27715
|
+
hostCommand: "kilo",
|
|
27716
|
+
versionArgs: ["--version"]
|
|
27717
|
+
},
|
|
27718
|
+
enforcementCeilings: [
|
|
27719
|
+
"Some read, write, and web-fetch tool outputs are not rewriteable by the host.",
|
|
27720
|
+
"Provisioned skills and agents become discoverable only after a fresh session."
|
|
27721
|
+
]
|
|
27722
|
+
}
|
|
27723
|
+
],
|
|
27724
|
+
surfaces: [
|
|
27725
|
+
{
|
|
27726
|
+
id: "mmi-skills",
|
|
27727
|
+
classification: "capability",
|
|
27728
|
+
kind: "skills",
|
|
27729
|
+
ownerPath: "skills",
|
|
27730
|
+
deliveryPath: "skills",
|
|
27731
|
+
delivery: "plugin-install",
|
|
27732
|
+
applicability: "plugin-enabled agent surfaces",
|
|
27733
|
+
versionCoordinated: false,
|
|
27734
|
+
artifactIdentity: {
|
|
27735
|
+
kind: "sha256-tree",
|
|
27736
|
+
paths: ["skills"]
|
|
27737
|
+
},
|
|
27738
|
+
verify: [
|
|
27739
|
+
{
|
|
27740
|
+
command: "node",
|
|
27741
|
+
args: ["scripts/check-skill-payload.mjs", "--contract"],
|
|
27742
|
+
expected: "skill payload check: ok"
|
|
27743
|
+
}
|
|
27744
|
+
],
|
|
27745
|
+
publishVisibility: "public"
|
|
27746
|
+
},
|
|
27747
|
+
{
|
|
27748
|
+
id: "mmi-claude-plugin",
|
|
27749
|
+
classification: "packaging",
|
|
27750
|
+
kind: "plugin",
|
|
27751
|
+
ownerPath: ".claude-plugin/plugin.json",
|
|
27752
|
+
deliveryPath: ".claude-plugin/plugin.json",
|
|
27753
|
+
delivery: "release",
|
|
27754
|
+
applicability: "Claude Code plugin marketplace",
|
|
27755
|
+
versionCoordinated: true,
|
|
27756
|
+
surfaceToken: "claude",
|
|
27757
|
+
versionPaths: [
|
|
27758
|
+
{ path: ".claude-plugin/plugin.json", pointer: "version" }
|
|
27759
|
+
],
|
|
27760
|
+
artifactIdentity: {
|
|
27761
|
+
kind: "sha256-tree",
|
|
27762
|
+
paths: [".claude-plugin/plugin.json", "skills", "hooks", "scripts", "bin"]
|
|
27763
|
+
},
|
|
27764
|
+
publishVisibility: "public"
|
|
27765
|
+
},
|
|
27766
|
+
{
|
|
27767
|
+
id: "mmi-codex-plugin",
|
|
27768
|
+
classification: "packaging",
|
|
27769
|
+
kind: "plugin",
|
|
27770
|
+
ownerPath: ".codex-plugin/plugin.json",
|
|
27771
|
+
deliveryPath: ".codex-plugin/plugin.json",
|
|
27772
|
+
delivery: "release",
|
|
27773
|
+
applicability: "Codex plugin marketplace; carries the MMI lifecycle hooks via hooks/codex-hooks.json (#3563)",
|
|
27774
|
+
versionCoordinated: true,
|
|
27775
|
+
surfaceToken: "codex",
|
|
27776
|
+
versionPaths: [
|
|
27777
|
+
{ path: ".codex-plugin/plugin.json", pointer: "version" }
|
|
27778
|
+
],
|
|
27779
|
+
artifactIdentity: {
|
|
27780
|
+
kind: "sha256-tree",
|
|
27781
|
+
paths: [".codex-plugin/plugin.json", "skills", "hooks/codex-hooks.json", "scripts", "bin"]
|
|
27782
|
+
},
|
|
27783
|
+
publishVisibility: "public"
|
|
27784
|
+
},
|
|
27785
|
+
{
|
|
27786
|
+
id: "mmi-kimi-plugin",
|
|
27787
|
+
classification: "packaging",
|
|
27788
|
+
kind: "plugin",
|
|
27789
|
+
ownerPath: ".kimi-plugin/plugin.json",
|
|
27790
|
+
deliveryPath: ".kimi-plugin/plugin.json",
|
|
27791
|
+
delivery: "release",
|
|
27792
|
+
applicability: "Kimi Code CLI plugin install (GitHub source); inline event mapping invokes the shared hook runner",
|
|
27793
|
+
versionCoordinated: true,
|
|
27794
|
+
surfaceToken: "kimi",
|
|
27795
|
+
versionPaths: [
|
|
27796
|
+
{ path: ".kimi-plugin/plugin.json", pointer: "version" }
|
|
27797
|
+
],
|
|
27798
|
+
artifactIdentity: {
|
|
27799
|
+
kind: "sha256-tree",
|
|
27800
|
+
paths: [".kimi-plugin/plugin.json", "skills", "scripts", "bin"]
|
|
27801
|
+
},
|
|
27802
|
+
publishVisibility: "public"
|
|
27803
|
+
},
|
|
27804
|
+
{
|
|
27805
|
+
id: "mmi-cursor-plugin",
|
|
27806
|
+
classification: "packaging",
|
|
27807
|
+
kind: "plugin",
|
|
27808
|
+
ownerPath: ".cursor-plugin/plugin.json",
|
|
27809
|
+
deliveryPath: "~/.cursor/plugins/local/mmi",
|
|
27810
|
+
delivery: "local-copy",
|
|
27811
|
+
applicability: "Cursor IDE and Cursor Agent CLI; local plugin checkout managed by mmi-cli",
|
|
27812
|
+
versionCoordinated: true,
|
|
27813
|
+
surfaceToken: "cursor",
|
|
27814
|
+
versionPaths: [
|
|
27815
|
+
{ path: ".cursor-plugin/plugin.json", pointer: "version" }
|
|
27816
|
+
],
|
|
27817
|
+
artifactIdentity: {
|
|
27818
|
+
kind: "sha256-tree",
|
|
27819
|
+
paths: [".cursor-plugin/plugin.json", "skills", "hooks/cursor-hooks.json", "scripts", "bin"]
|
|
27820
|
+
},
|
|
27821
|
+
publishVisibility: "public"
|
|
27822
|
+
},
|
|
27823
|
+
{
|
|
27824
|
+
id: "mmi-kilo-plugin",
|
|
27825
|
+
classification: "packaging",
|
|
27826
|
+
kind: "plugin",
|
|
27827
|
+
ownerPath: ".kilo-plugin/package.json",
|
|
27828
|
+
deliveryPath: ".kilo-plugin/package.json",
|
|
27829
|
+
delivery: "npm",
|
|
27830
|
+
applicability: "Kilo Code extension + Kilo CLI; npm package @mutmutco/kilo-plugin (kilo-p1), installed via `kilo plugin @mutmutco/kilo-plugin`",
|
|
27831
|
+
versionCoordinated: true,
|
|
27832
|
+
surfaceToken: "kilo",
|
|
27833
|
+
versionPaths: [
|
|
27834
|
+
{ path: ".kilo-plugin/package.json", pointer: "version" }
|
|
27835
|
+
],
|
|
27836
|
+
additionalPaths: [
|
|
27837
|
+
".kilo-plugin/server.mjs",
|
|
27838
|
+
".kilo-plugin/skills",
|
|
27839
|
+
".kilo-plugin/scripts"
|
|
27840
|
+
],
|
|
27841
|
+
artifactIdentity: {
|
|
27842
|
+
kind: "npm-pack",
|
|
27843
|
+
packagePath: ".kilo-plugin"
|
|
27844
|
+
},
|
|
27845
|
+
publishVisibility: "public"
|
|
27846
|
+
},
|
|
27847
|
+
{
|
|
27848
|
+
id: "mmi-kilo-skills",
|
|
27849
|
+
classification: "capability",
|
|
27850
|
+
kind: "skills",
|
|
27851
|
+
ownerPath: "skills",
|
|
27852
|
+
deliveryPath: "skills",
|
|
27853
|
+
delivery: "plugin-install",
|
|
27854
|
+
applicability: "Kilo Code (via @mutmutco/kilo-plugin provisioning to ~/.kilo/skills)",
|
|
27855
|
+
versionCoordinated: false,
|
|
27856
|
+
artifactIdentity: {
|
|
27857
|
+
kind: "sha256-tree",
|
|
27858
|
+
paths: [".kilo-plugin/skills"]
|
|
27859
|
+
},
|
|
27860
|
+
surfaceToken: "kilo",
|
|
27861
|
+
publishVisibility: "public"
|
|
27862
|
+
},
|
|
27863
|
+
{
|
|
27864
|
+
id: "mmi-claude-marketplace",
|
|
27865
|
+
classification: "delivery",
|
|
27866
|
+
kind: "marketplace",
|
|
27867
|
+
ownerPath: ".claude-plugin/marketplace.json",
|
|
27868
|
+
deliveryPath: ".claude-plugin/marketplace.json",
|
|
27869
|
+
delivery: "release",
|
|
27870
|
+
applicability: "Claude Code plugin marketplace",
|
|
27871
|
+
versionCoordinated: true,
|
|
27872
|
+
surfaceToken: "claude",
|
|
27873
|
+
versionPaths: [
|
|
27874
|
+
{ path: ".claude-plugin/marketplace.json", pointer: "plugins.0.version" },
|
|
27875
|
+
{ path: ".claude-plugin/marketplace.json", pointer: "plugins.0.displayName", template: "MMI {version}" }
|
|
27876
|
+
],
|
|
27877
|
+
artifactIdentity: {
|
|
27878
|
+
kind: "sha256-tree",
|
|
27879
|
+
paths: [".claude-plugin/marketplace.json"]
|
|
27880
|
+
},
|
|
27881
|
+
publishVisibility: "public"
|
|
27882
|
+
},
|
|
27883
|
+
{
|
|
27884
|
+
id: "mmi-cli",
|
|
27885
|
+
classification: "capability",
|
|
27886
|
+
kind: "cli",
|
|
27887
|
+
ownerPath: "cli/package.json",
|
|
27888
|
+
deliveryPath: "cli/package.json",
|
|
27889
|
+
delivery: "npm",
|
|
27890
|
+
applicability: "editor-agnostic agent surfaces",
|
|
27891
|
+
versionCoordinated: true,
|
|
27892
|
+
versionPaths: [
|
|
27893
|
+
{ path: "cli/package.json", pointer: "version" }
|
|
27894
|
+
],
|
|
27895
|
+
additionalPaths: ["cli/dist"],
|
|
27896
|
+
artifactIdentity: {
|
|
27897
|
+
kind: "npm-pack",
|
|
27898
|
+
packagePath: "cli"
|
|
27899
|
+
},
|
|
27900
|
+
publishVisibility: "public"
|
|
27901
|
+
},
|
|
27902
|
+
{
|
|
27903
|
+
id: "mmi-cli-lock",
|
|
27904
|
+
classification: "packaging",
|
|
27905
|
+
kind: "package-lock",
|
|
27906
|
+
ownerPath: "cli/package-lock.json",
|
|
27907
|
+
deliveryPath: "cli/package-lock.json",
|
|
27908
|
+
delivery: "npm",
|
|
27909
|
+
applicability: "editor-agnostic agent surfaces",
|
|
27910
|
+
versionCoordinated: true,
|
|
27911
|
+
versionPaths: [
|
|
27912
|
+
{ path: "cli/package-lock.json", pointer: "version" },
|
|
27913
|
+
{ path: "cli/package-lock.json", pointer: "packages.$root.version" }
|
|
27914
|
+
],
|
|
27915
|
+
artifactIdentity: {
|
|
27916
|
+
kind: "sha256-tree",
|
|
27917
|
+
paths: ["cli/package-lock.json"]
|
|
27918
|
+
},
|
|
27919
|
+
publishVisibility: "public"
|
|
27920
|
+
},
|
|
27921
|
+
{
|
|
27922
|
+
id: "mmi-cli-dist",
|
|
27923
|
+
classification: "packaging",
|
|
27924
|
+
kind: "bundle",
|
|
27925
|
+
ownerPath: "cli/dist/index.cjs",
|
|
27926
|
+
deliveryPath: "cli/dist/index.cjs",
|
|
27927
|
+
delivery: "npm",
|
|
27928
|
+
applicability: "editor-agnostic agent surfaces",
|
|
27929
|
+
versionCoordinated: true,
|
|
27930
|
+
versionPaths: [
|
|
27931
|
+
{ path: "cli/package.json", pointer: "version" }
|
|
27932
|
+
],
|
|
27933
|
+
additionalPaths: ["cli/dist/main.cjs"],
|
|
27934
|
+
prepare: [
|
|
27935
|
+
{
|
|
27936
|
+
command: "npm",
|
|
27937
|
+
args: ["--prefix", "cli", "run", "build"],
|
|
27938
|
+
inputs: ["cli/src", "cli/build.mjs", "cli/package.json", "cli/README.md", "cli/tsconfig.json"],
|
|
27939
|
+
outputs: ["cli/dist/index.cjs", "cli/dist/main.cjs"]
|
|
27940
|
+
}
|
|
27941
|
+
],
|
|
27942
|
+
verify: [
|
|
27943
|
+
{
|
|
27944
|
+
command: "node",
|
|
27945
|
+
args: ["cli/dist/index.cjs", "--version"],
|
|
27946
|
+
expected: "{version}"
|
|
27947
|
+
}
|
|
27948
|
+
],
|
|
27949
|
+
artifactIdentity: {
|
|
27950
|
+
kind: "sha256-tree",
|
|
27951
|
+
paths: ["cli/dist"]
|
|
27952
|
+
},
|
|
27953
|
+
publishVisibility: "public"
|
|
27954
|
+
},
|
|
27955
|
+
{
|
|
27956
|
+
id: "mmi-hook-policy",
|
|
27957
|
+
classification: "guard",
|
|
27958
|
+
kind: "hooks",
|
|
27959
|
+
ownerPath: "scripts/hook-policy.mjs",
|
|
27960
|
+
deliveryPath: "scripts/hook-policy.mjs",
|
|
27961
|
+
delivery: "plugin-install",
|
|
27962
|
+
applicability: "all five active hook-capable agent surfaces",
|
|
27963
|
+
versionCoordinated: false,
|
|
27964
|
+
artifactIdentity: {
|
|
27965
|
+
kind: "sha256-tree",
|
|
27966
|
+
paths: [
|
|
27967
|
+
"scripts/hook-policy.mjs",
|
|
27968
|
+
"scripts/hook-run.mjs",
|
|
27969
|
+
"scripts/pretooluse-shell-gates.mjs",
|
|
27970
|
+
"scripts/vault-edit-gate.mjs",
|
|
27971
|
+
"scripts/secret-redact.mjs",
|
|
27972
|
+
"scripts/deny-gate-crash.mjs",
|
|
27973
|
+
"scripts/hook-io.mjs",
|
|
27974
|
+
"scripts/hook-trace.mjs",
|
|
27975
|
+
"bin/mmi-hook",
|
|
27976
|
+
"bin/mmi-hook.exe",
|
|
27977
|
+
"native/windows-hook-launcher.c",
|
|
27978
|
+
"scripts/build-windows-hook-launcher.ps1",
|
|
27979
|
+
"scripts/probe-windows-hook-window.ps1"
|
|
27980
|
+
]
|
|
27981
|
+
},
|
|
27982
|
+
verify: [
|
|
27983
|
+
{
|
|
27984
|
+
command: "node",
|
|
27985
|
+
args: ["scripts/check-hook-contract.mjs"],
|
|
27986
|
+
expected: "hook contract: 5 active adapters proven, 12 repositories covered"
|
|
27987
|
+
}
|
|
27988
|
+
],
|
|
27989
|
+
publishVisibility: "public"
|
|
27990
|
+
},
|
|
27991
|
+
{
|
|
27992
|
+
id: "mmi-hooks",
|
|
27993
|
+
classification: "guard",
|
|
27994
|
+
kind: "hooks",
|
|
27995
|
+
ownerPath: "hooks/hooks.json",
|
|
27996
|
+
deliveryPath: "hooks/hooks.json",
|
|
27997
|
+
delivery: "plugin",
|
|
27998
|
+
applicability: "Claude hook-enabled agents",
|
|
27999
|
+
versionCoordinated: false,
|
|
28000
|
+
artifactIdentity: {
|
|
28001
|
+
kind: "sha256-tree",
|
|
28002
|
+
paths: ["hooks", "scripts"]
|
|
28003
|
+
},
|
|
28004
|
+
surfaceToken: "claude",
|
|
28005
|
+
publishVisibility: "public"
|
|
28006
|
+
},
|
|
28007
|
+
{
|
|
28008
|
+
id: "mmi-github-app",
|
|
28009
|
+
classification: "integration",
|
|
28010
|
+
kind: "github-app",
|
|
28011
|
+
ownerPath: "infra/src/github-app.ts",
|
|
28012
|
+
deliveryPath: "infra/dist/handler.js",
|
|
28013
|
+
delivery: "lambda",
|
|
28014
|
+
applicability: "org automation and authorization",
|
|
28015
|
+
versionCoordinated: true,
|
|
28016
|
+
versionPaths: [
|
|
28017
|
+
{ path: "infra/package.json", pointer: "version" }
|
|
28018
|
+
],
|
|
28019
|
+
publishVisibility: "n/a",
|
|
28020
|
+
deployedVersionSource: "http-header:x-hub-version"
|
|
28021
|
+
},
|
|
28022
|
+
{
|
|
28023
|
+
id: "mmi-slack-notify",
|
|
28024
|
+
classification: "integration",
|
|
28025
|
+
kind: "notification",
|
|
28026
|
+
ownerPath: "scripts/slack-post.mjs",
|
|
28027
|
+
deliveryPath: "scripts/slack-post.mjs",
|
|
28028
|
+
delivery: "github-actions",
|
|
28029
|
+
applicability: "master DM notifications only; chatops out by default",
|
|
28030
|
+
versionCoordinated: false,
|
|
28031
|
+
publishVisibility: "n/a"
|
|
28032
|
+
}
|
|
28033
|
+
]
|
|
28034
|
+
};
|
|
28035
|
+
|
|
28036
|
+
// src/surface-doctor.ts
|
|
28037
|
+
var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "kimi", "cursor", "kilo"]);
|
|
28038
|
+
function isActiveToken(value) {
|
|
28039
|
+
return TOKENS.has(value);
|
|
28040
|
+
}
|
|
28041
|
+
var DOCTOR_SURFACES = Object.freeze(
|
|
28042
|
+
(surfaces_default.agentSurfaces ?? []).filter((surface) => surface.lifecycle === "active" && isActiveToken(surface.token)).map((surface) => Object.freeze({
|
|
28043
|
+
token: surface.token,
|
|
28044
|
+
displayName: surface.displayName,
|
|
28045
|
+
artifactIds: Object.freeze([...surface.artifactIds]),
|
|
28046
|
+
installMechanism: surface.install.mechanism,
|
|
28047
|
+
installLocator: surface.install.locator,
|
|
28048
|
+
upgradeMechanism: surface.upgrade.mechanism,
|
|
28049
|
+
reload: surface.upgrade.reload,
|
|
28050
|
+
repairOwner: surface.ownership.repair === "mmi-cli" ? "mmi-cli" : "operator",
|
|
28051
|
+
trustOwner: surface.ownership.trust === "operator" ? "operator" : "host"
|
|
28052
|
+
}))
|
|
28053
|
+
);
|
|
28054
|
+
function doctorSurface(token) {
|
|
28055
|
+
const descriptor = DOCTOR_SURFACES.find((surface) => surface.token === token);
|
|
28056
|
+
if (!descriptor) throw new Error(`surface registry has no active doctor descriptor for ${token}`);
|
|
28057
|
+
return descriptor;
|
|
28058
|
+
}
|
|
28059
|
+
function diagnoseSurface(evidence) {
|
|
28060
|
+
const base = {
|
|
28061
|
+
descriptor: evidence.descriptor,
|
|
28062
|
+
...evidence.installedVersion ? { installedVersion: evidence.installedVersion } : {},
|
|
28063
|
+
...evidence.releasedVersion ? { releasedVersion: evidence.releasedVersion } : {}
|
|
28064
|
+
};
|
|
28065
|
+
if (!evidence.applicable) return { ...base, state: "skipped" };
|
|
28066
|
+
if (evidence.repair?.attempted && !evidence.repair.ok) {
|
|
28067
|
+
return { ...base, state: "repair-failed", repairDetail: evidence.repair.detail };
|
|
26965
28068
|
}
|
|
26966
|
-
|
|
26967
|
-
|
|
26968
|
-
|
|
28069
|
+
if (evidence.repair?.attempted && evidence.repair.ok) {
|
|
28070
|
+
return { ...base, state: "clean", repairDetail: evidence.repair.detail };
|
|
28071
|
+
}
|
|
28072
|
+
if (!evidence.installRecordPresent) return { ...base, state: "missing" };
|
|
28073
|
+
if (!evidence.deliveryPresent || !evidence.payloadPresent || evidence.manifest === "missing") {
|
|
28074
|
+
return { ...base, state: "partial" };
|
|
28075
|
+
}
|
|
28076
|
+
if (evidence.manifest === "invalid") return { ...base, state: "corrupt" };
|
|
28077
|
+
if (!evidence.releasedVersion) return { ...base, state: "freshness-unknown" };
|
|
28078
|
+
if (evidence.installedVersion && evidence.releasedVersion && compareVersions(evidence.installedVersion, evidence.releasedVersion) < 0) {
|
|
28079
|
+
return { ...base, state: "stale" };
|
|
28080
|
+
}
|
|
28081
|
+
return { ...base, state: "clean" };
|
|
26969
28082
|
}
|
|
26970
|
-
function
|
|
26971
|
-
|
|
28083
|
+
function reloadInstruction(descriptor) {
|
|
28084
|
+
const verb = descriptor.reload === "workspace" ? "reload" : "restart";
|
|
28085
|
+
return `${verb} ${descriptor.displayName}`;
|
|
28086
|
+
}
|
|
28087
|
+
function planSurfaceRepair(diagnosis) {
|
|
28088
|
+
if (diagnosis.state === "clean" || diagnosis.state === "skipped" || diagnosis.state === "freshness-unknown") return null;
|
|
28089
|
+
const descriptor = diagnosis.descriptor;
|
|
28090
|
+
if (descriptor.repairOwner !== "mmi-cli") {
|
|
28091
|
+
return {
|
|
28092
|
+
supported: false,
|
|
28093
|
+
owner: "operator",
|
|
28094
|
+
changes: [],
|
|
28095
|
+
instruction: `repair is unsupported by mmi-cli; reinstall via ${descriptor.installMechanism} (${descriptor.installLocator}), then ${reloadInstruction(descriptor)}`
|
|
28096
|
+
};
|
|
28097
|
+
}
|
|
28098
|
+
return {
|
|
28099
|
+
supported: true,
|
|
28100
|
+
owner: "mmi-cli",
|
|
28101
|
+
command: "mmi-cli plugin heal",
|
|
28102
|
+
changes: ["replace the active MMI plugin installation", `reload via ${descriptor.reload}`],
|
|
28103
|
+
instruction: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`), then ${reloadInstruction(descriptor)}`
|
|
28104
|
+
};
|
|
28105
|
+
}
|
|
28106
|
+
function buildSurfaceDoctorCheck(diagnosis) {
|
|
28107
|
+
const { descriptor, state } = diagnosis;
|
|
28108
|
+
const plan = planSurfaceRepair(diagnosis);
|
|
28109
|
+
const versions = diagnosis.installedVersion && diagnosis.releasedVersion ? compareVersions(diagnosis.installedVersion, diagnosis.releasedVersion) === 0 ? diagnosis.installedVersion : `${diagnosis.installedVersion} \u2192 ${diagnosis.releasedVersion}` : diagnosis.installedVersion;
|
|
28110
|
+
const detailByState = {
|
|
28111
|
+
skipped: "skipped \u2014 host not active",
|
|
28112
|
+
clean: diagnosis.repairDetail ? `clean \u2014 repaired and verified (${diagnosis.repairDetail})` : `clean${versions ? ` \u2014 ${versions}` : ""}`,
|
|
28113
|
+
"freshness-unknown": `${diagnosis.installedVersion ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
|
|
28114
|
+
stale: `stale${versions ? ` \u2014 ${versions}` : ""}`,
|
|
28115
|
+
missing: "missing \u2014 no install record",
|
|
28116
|
+
corrupt: "corrupt \u2014 installed manifest is unreadable",
|
|
28117
|
+
partial: "partial \u2014 one or more delivery artifacts are absent",
|
|
28118
|
+
"repair-failed": `repair failed${diagnosis.repairDetail ? ` \u2014 ${diagnosis.repairDetail}` : ""}`
|
|
28119
|
+
};
|
|
28120
|
+
return {
|
|
28121
|
+
id: `${descriptor.token}-plugin`,
|
|
28122
|
+
surface: descriptor.token,
|
|
28123
|
+
state,
|
|
28124
|
+
ok: state === "clean" || state === "skipped",
|
|
28125
|
+
...state === "freshness-unknown" ? { reportOnly: true } : {},
|
|
28126
|
+
label: `${descriptor.displayName} plugin`,
|
|
28127
|
+
detail: detailByState[state],
|
|
28128
|
+
...plan ? { fix: plan.instruction } : state === "freshness-unknown" ? { fix: "check `mmi-cli --version` against `npm view @mutmutco/cli version`, then rerun doctor" } : {},
|
|
28129
|
+
verbose: [
|
|
28130
|
+
// The three numbers the legacy builder used to print. They belong here now that this is the only
|
|
28131
|
+
// plugin row, and a report that names a state without naming the versions behind it is not evidence.
|
|
28132
|
+
`installed: ${diagnosis.installedVersion ?? "(none)"}`,
|
|
28133
|
+
`released: ${diagnosis.releasedVersion ?? "(not checked \u2014 offline or --fast)"}`,
|
|
28134
|
+
`state: ${state}`,
|
|
28135
|
+
`registry surface: ${descriptor.token}`,
|
|
28136
|
+
`install: ${descriptor.installMechanism} (${descriptor.installLocator})`,
|
|
28137
|
+
`repair owner: ${descriptor.repairOwner}`,
|
|
28138
|
+
`artifacts: ${descriptor.artifactIds.join(", ")}`,
|
|
28139
|
+
...diagnosis.repairDetail ? [`heal: ${diagnosis.repairDetail}`] : []
|
|
28140
|
+
]
|
|
28141
|
+
};
|
|
28142
|
+
}
|
|
28143
|
+
function surfaceRestartAction(descriptor) {
|
|
28144
|
+
return reloadInstruction(descriptor);
|
|
26972
28145
|
}
|
|
26973
28146
|
|
|
26974
28147
|
// src/doctor-clean.ts
|
|
@@ -27061,75 +28234,23 @@ function checkWorktreeRoots(probe) {
|
|
|
27061
28234
|
verbose: evidence
|
|
27062
28235
|
};
|
|
27063
28236
|
}
|
|
27064
|
-
function
|
|
27065
|
-
if (probe.guardState === "no-install" || probe.guardState === "unresolved") return "unresolved";
|
|
27066
|
-
const behind = Boolean(probe.installed && probe.released) && compareVersions(probe.installed, probe.released) < 0;
|
|
27067
|
-
return behind ? "behind" : null;
|
|
27068
|
-
}
|
|
27069
|
-
function checkClaudePlugin(probe) {
|
|
27070
|
-
const { installed, released, guardState } = probe;
|
|
27071
|
-
const codex = probe.surface === "codex";
|
|
27072
|
-
const id = codex ? "codex-plugin" : "claude-plugin";
|
|
27073
|
-
const label = codex ? "Codex plugin" : "Claude plugin";
|
|
27074
|
-
const restart = codex ? "restart Codex" : "restart Claude";
|
|
27075
|
-
const evidence = [
|
|
27076
|
-
`installed: ${installed ?? "(none)"}`,
|
|
27077
|
-
// #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
|
|
27078
|
-
`released: ${released ?? "(not checked \u2014 offline or --fast)"}${released && probe.releasedNote ? ` ${probe.releasedNote}` : ""}`,
|
|
27079
|
-
`resolvable: ${guardState}`
|
|
27080
|
-
];
|
|
27081
|
-
const trigger = pluginHealTrigger(probe);
|
|
27082
|
-
if (trigger === "unresolved") {
|
|
27083
|
-
return {
|
|
27084
|
-
id,
|
|
27085
|
-
ok: false,
|
|
27086
|
-
label,
|
|
27087
|
-
detail: guardState === "no-install" ? "not installed" : "unresolved (marketplace/cache missing)",
|
|
27088
|
-
fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall the MMI marketplace + plugin, then ${restart}`,
|
|
27089
|
-
verbose: evidence
|
|
27090
|
-
};
|
|
27091
|
-
}
|
|
27092
|
-
if (trigger === "behind") {
|
|
27093
|
-
return {
|
|
27094
|
-
id,
|
|
27095
|
-
ok: false,
|
|
27096
|
-
label,
|
|
27097
|
-
detail: `${installed} \u2192 ${released}`,
|
|
27098
|
-
// Name the CLI verb that actually fixes it, like every other red row — `/plugin` is the manual
|
|
27099
|
-
// fallback, not the first resort (#3282).
|
|
27100
|
-
fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall it, then ${restart}`,
|
|
27101
|
-
verbose: evidence
|
|
27102
|
-
};
|
|
27103
|
-
}
|
|
27104
|
-
if (!released) {
|
|
27105
|
-
return {
|
|
27106
|
-
id,
|
|
27107
|
-
ok: false,
|
|
27108
|
-
reportOnly: true,
|
|
27109
|
-
label,
|
|
27110
|
-
detail: `${installed ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
|
|
27111
|
-
fix: "check it directly: `npm view @mutmutco/cli version`, then `mmi-cli plugin heal` if behind",
|
|
27112
|
-
verbose: evidence
|
|
27113
|
-
};
|
|
27114
|
-
}
|
|
27115
|
-
return { id, ok: true, label, ...installed ? { detail: installed } : {}, verbose: evidence };
|
|
27116
|
-
}
|
|
27117
|
-
function checkCodexHookTrust(probe) {
|
|
28237
|
+
function checkCodexHookTrust(probe, displayName = "Codex") {
|
|
27118
28238
|
if (!probe?.applicable) return null;
|
|
27119
28239
|
const evidence = [
|
|
27120
28240
|
`stored approval rows: ${probe.trustedCount}/${probe.requiredCount}`,
|
|
27121
28241
|
"current command hashes: verify interactively in /hooks"
|
|
27122
28242
|
];
|
|
28243
|
+
const label = `${displayName} hook trust`;
|
|
27123
28244
|
if (probe.trusted) {
|
|
27124
|
-
return { id: "codex-hook-trust", ok: true, label
|
|
28245
|
+
return { id: "codex-hook-trust", ok: true, label, detail: "trusted", verbose: evidence };
|
|
27125
28246
|
}
|
|
27126
28247
|
return {
|
|
27127
28248
|
id: "codex-hook-trust",
|
|
27128
28249
|
ok: true,
|
|
27129
28250
|
warn: true,
|
|
27130
|
-
label
|
|
28251
|
+
label,
|
|
27131
28252
|
detail: probe.requiredCount > 0 ? `${probe.trustedCount}/${probe.requiredCount} approval rows present \u2014 current hashes unverified` : "hook bundle could not be verified",
|
|
27132
|
-
fix: probe.requiredCount > 0 ?
|
|
28253
|
+
fix: probe.requiredCount > 0 ? `${displayName} does not allow silent hook approval; run \`/hooks\`, review the MMI commands, and trust them` : `run \`mmi-cli plugin heal\`, restart ${displayName}, then review and trust MMI under \`/hooks\``,
|
|
27133
28254
|
verbose: evidence
|
|
27134
28255
|
};
|
|
27135
28256
|
}
|
|
@@ -27354,6 +28475,12 @@ function gcReapable(plan) {
|
|
|
27354
28475
|
async function runDoctorClean(opts, io, deps) {
|
|
27355
28476
|
const applyEnv = Boolean(opts.apply) || Boolean(opts.preflight);
|
|
27356
28477
|
const applyRepo = Boolean(opts.apply) && opts.repoWrites !== false;
|
|
28478
|
+
const lane = {
|
|
28479
|
+
banner: Boolean(opts.banner),
|
|
28480
|
+
fast: Boolean(opts.fast),
|
|
28481
|
+
preflight: Boolean(opts.preflight),
|
|
28482
|
+
full: !opts.fast && !opts.banner && !opts.preflight
|
|
28483
|
+
};
|
|
27357
28484
|
const probeReleased = !opts.fast || Boolean(opts.self);
|
|
27358
28485
|
const probeAws = !opts.fast && !opts.banner;
|
|
27359
28486
|
const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
|
|
@@ -27368,91 +28495,69 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
27368
28495
|
opts.banner || opts.preflight || opts.fast && !opts.self ? Promise.resolve(void 0) : deps.githubRepoReach?.() ?? Promise.resolve(void 0)
|
|
27369
28496
|
]);
|
|
27370
28497
|
const ghInstalled2 = login ? true : await deps.ghInstalled();
|
|
27371
|
-
const
|
|
27372
|
-
const
|
|
27373
|
-
const codexSurface = pluginSurface === "codex";
|
|
28498
|
+
const registryEvidence = deps.surfaceEvidence(isOrgRepo);
|
|
28499
|
+
const restartAction = registryEvidence ? surfaceRestartAction(registryEvidence.descriptor) : "restart the agent host";
|
|
27374
28500
|
const checks = [];
|
|
27375
28501
|
let restartPending = false;
|
|
27376
|
-
|
|
27377
|
-
|
|
27378
|
-
|
|
27379
|
-
|
|
27380
|
-
|
|
27381
|
-
|
|
27382
|
-
|
|
27383
|
-
|
|
27384
|
-
|
|
27385
|
-
|
|
27386
|
-
|
|
27387
|
-
`.gitignore: ${current === null ? "absent" : `${current.split("\n").length} lines`}`,
|
|
27388
|
-
`managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
|
|
27389
|
-
];
|
|
27390
|
-
if (gi.ok) {
|
|
27391
|
-
checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
|
|
27392
|
-
} else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
|
|
27393
|
-
checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
|
|
27394
|
-
restartPending = true;
|
|
27395
|
-
} else {
|
|
27396
|
-
checks.push({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
27397
|
-
}
|
|
27398
|
-
}
|
|
27399
|
-
const releasedNote = deps.releasedVersionNote?.();
|
|
27400
|
-
const pluginProbe = {
|
|
27401
|
-
installed,
|
|
27402
|
-
released,
|
|
27403
|
-
guardState: deps.pluginGuardState(isOrgRepo),
|
|
27404
|
-
releasedNote,
|
|
27405
|
-
surface: pluginSurface
|
|
28502
|
+
const streamed = /* @__PURE__ */ new Set();
|
|
28503
|
+
const worthPrinting = (c) => Boolean(opts.verbose) || !c.ok || Boolean(c.warn);
|
|
28504
|
+
const emitNow = (check) => {
|
|
28505
|
+
checks.push(check);
|
|
28506
|
+
if (opts.json || !worthPrinting(check)) return;
|
|
28507
|
+
streamed.add(check);
|
|
28508
|
+
io.log(renderCheckLine(check));
|
|
28509
|
+
if (opts.verbose) for (const evidence of check.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
|
|
28510
|
+
};
|
|
28511
|
+
const healIntent = (line) => {
|
|
28512
|
+
if (!opts.json) io.log(`\u21BB ${line}`);
|
|
27406
28513
|
};
|
|
27407
|
-
const
|
|
28514
|
+
const healStep = (message) => {
|
|
28515
|
+
if (!opts.json) io.log(`${VERBOSE_INDENT}${message.trim()}`);
|
|
28516
|
+
};
|
|
28517
|
+
const releasedNote = deps.releasedVersionNote?.();
|
|
27408
28518
|
let pluginHealed = false;
|
|
27409
|
-
|
|
27410
|
-
|
|
27411
|
-
|
|
27412
|
-
const
|
|
27413
|
-
|
|
27414
|
-
|
|
27415
|
-
|
|
27416
|
-
|
|
27417
|
-
|
|
27418
|
-
|
|
27419
|
-
|
|
27420
|
-
|
|
27421
|
-
|
|
27422
|
-
|
|
27423
|
-
|
|
27424
|
-
|
|
27425
|
-
|
|
27426
|
-
|
|
27427
|
-
|
|
27428
|
-
|
|
27429
|
-
|
|
27430
|
-
|
|
27431
|
-
|
|
27432
|
-
|
|
27433
|
-
|
|
27434
|
-
|
|
27435
|
-
|
|
27436
|
-
|
|
27437
|
-
}
|
|
27438
|
-
|
|
27439
|
-
|
|
27440
|
-
const
|
|
27441
|
-
|
|
27442
|
-
|
|
27443
|
-
|
|
28519
|
+
async function runPluginRow() {
|
|
28520
|
+
if (!registryEvidence) return;
|
|
28521
|
+
const diagnosis = diagnoseSurface({ ...registryEvidence, releasedVersion: released });
|
|
28522
|
+
const repair = planSurfaceRepair(diagnosis);
|
|
28523
|
+
if (applyEnv && deps.healPlugin && repair?.supported) {
|
|
28524
|
+
const { descriptor } = registryEvidence;
|
|
28525
|
+
healIntent(`${descriptor.displayName} plugin \u2014 healing via ${descriptor.installMechanism} (${descriptor.installLocator})`);
|
|
28526
|
+
const heal = await deps.healPlugin(healStep);
|
|
28527
|
+
pluginHealed = heal.ok;
|
|
28528
|
+
const row = buildSurfaceDoctorCheck(diagnoseSurface({
|
|
28529
|
+
...registryEvidence,
|
|
28530
|
+
releasedVersion: released,
|
|
28531
|
+
repair: { attempted: true, ok: heal.ok, detail: heal.detail }
|
|
28532
|
+
}));
|
|
28533
|
+
if (heal.skipped) {
|
|
28534
|
+
row.reportOnly = true;
|
|
28535
|
+
row.fix = `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes`;
|
|
28536
|
+
}
|
|
28537
|
+
emitNow(row);
|
|
28538
|
+
if (!heal.skipped) restartPending = true;
|
|
28539
|
+
} else if (diagnosis.state !== "skipped") {
|
|
28540
|
+
const row = buildSurfaceDoctorCheck(diagnosis);
|
|
28541
|
+
emitNow(row);
|
|
28542
|
+
if (!row.ok && repair?.supported) restartPending = true;
|
|
28543
|
+
}
|
|
28544
|
+
if (registryEvidence.descriptor.trustOwner === "operator" && (registryEvidence.guardState === "healthy" || pluginHealed)) {
|
|
28545
|
+
const trust = checkCodexHookTrust(deps.pluginTrustState?.(), registryEvidence.descriptor.displayName);
|
|
28546
|
+
if (trust) emitNow(trust);
|
|
28547
|
+
}
|
|
28548
|
+
}
|
|
28549
|
+
async function runCliRow() {
|
|
28550
|
+
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
28551
|
+
const cliReport = buildVersionLagReport(cliInput);
|
|
28552
|
+
if (!(applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm")) {
|
|
28553
|
+
const cli = checkCliVersion(cliInput, releasedNote);
|
|
28554
|
+
if (cli) emitNow(cli);
|
|
28555
|
+
return;
|
|
27444
28556
|
}
|
|
27445
|
-
|
|
27446
|
-
|
|
27447
|
-
const trust = checkCodexHookTrust(deps.pluginTrustState?.());
|
|
27448
|
-
if (trust) checks.push(trust);
|
|
27449
|
-
}
|
|
27450
|
-
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
27451
|
-
const cliReport = buildVersionLagReport(cliInput);
|
|
27452
|
-
if (applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
|
|
27453
|
-
const heal = await deps.updateCli(cliReport.releasedVersion);
|
|
28557
|
+
healIntent(`mmi-cli \u2014 self-updating ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion} via npm install -g`);
|
|
28558
|
+
const heal = await deps.updateCli(cliReport.releasedVersion, healStep);
|
|
27454
28559
|
const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
|
|
27455
|
-
|
|
28560
|
+
emitNow(heal.ok ? {
|
|
27456
28561
|
id: "cli-version",
|
|
27457
28562
|
ok: true,
|
|
27458
28563
|
label: "mmi-cli",
|
|
@@ -27468,17 +28573,47 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
27468
28573
|
fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
|
|
27469
28574
|
verbose: healEvidence
|
|
27470
28575
|
});
|
|
27471
|
-
} else {
|
|
27472
|
-
const cli = checkCliVersion(cliInput, releasedNote);
|
|
27473
|
-
if (cli) checks.push(cli);
|
|
27474
28576
|
}
|
|
27475
|
-
|
|
27476
|
-
|
|
28577
|
+
async function runGithubAuthRow() {
|
|
28578
|
+
emitNow(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
|
|
28579
|
+
}
|
|
28580
|
+
async function runGithubPoolsRows() {
|
|
28581
|
+
for (const pool of checkGithubPools(await deps.githubPools())) emitNow(pool);
|
|
28582
|
+
}
|
|
28583
|
+
async function runAwsRow() {
|
|
28584
|
+
const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
|
|
28585
|
+
if (aws) emitNow(aws);
|
|
28586
|
+
}
|
|
28587
|
+
async function runRepoWorktreesRow() {
|
|
28588
|
+
emitNow(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
|
|
28589
|
+
}
|
|
28590
|
+
async function runGitignoreRow() {
|
|
28591
|
+
const current = deps.readGitignore();
|
|
28592
|
+
const gi = planGitignore(current);
|
|
28593
|
+
const giEvidence = [
|
|
28594
|
+
`.gitignore: ${current === null ? "absent" : `${current.split("\n").length} lines`}`,
|
|
28595
|
+
`managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
|
|
28596
|
+
];
|
|
28597
|
+
if (gi.ok) {
|
|
28598
|
+
emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
|
|
28599
|
+
} else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
|
|
28600
|
+
emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
|
|
28601
|
+
restartPending = true;
|
|
28602
|
+
} else {
|
|
28603
|
+
emitNow({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
28604
|
+
}
|
|
28605
|
+
}
|
|
28606
|
+
async function runPluginCacheRow() {
|
|
28607
|
+
emitNow(checkPluginCache(deps.pluginCache()));
|
|
28608
|
+
}
|
|
28609
|
+
async function runSessionPayloadRow() {
|
|
27477
28610
|
const payload = checkSessionPayload(deps.sessionPayload());
|
|
27478
|
-
if (payload)
|
|
28611
|
+
if (payload) emitNow(payload);
|
|
28612
|
+
}
|
|
28613
|
+
async function runMarketplaceRows() {
|
|
28614
|
+
for (const row of deps.marketplaceRows()) emitNow(row);
|
|
27479
28615
|
}
|
|
27480
|
-
|
|
27481
|
-
if (!opts.fast && !opts.banner && !opts.preflight) {
|
|
28616
|
+
async function runSchedulesRow() {
|
|
27482
28617
|
const probe = await deps.schedulesNotebook().catch((e) => ({
|
|
27483
28618
|
armed: 0,
|
|
27484
28619
|
live: 0,
|
|
@@ -27487,32 +28622,32 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
27487
28622
|
drift: []
|
|
27488
28623
|
}));
|
|
27489
28624
|
const sched = checkSchedules(probe);
|
|
27490
|
-
if (sched)
|
|
28625
|
+
if (sched) emitNow(sched);
|
|
27491
28626
|
}
|
|
27492
|
-
|
|
28627
|
+
async function runRedactorRow() {
|
|
27493
28628
|
const redactor = checkRedactorLiveness(deps.redactorLiveness());
|
|
27494
|
-
if (redactor)
|
|
28629
|
+
if (redactor) emitNow(redactor);
|
|
27495
28630
|
}
|
|
27496
|
-
|
|
28631
|
+
async function runDocsAuditRow() {
|
|
27497
28632
|
const probe = await deps.docsAudit().catch((e) => ({
|
|
27498
28633
|
armed: true,
|
|
27499
28634
|
ok: false,
|
|
27500
28635
|
detail: `docs-audit probe failed \u2014 ${e.message}`
|
|
27501
28636
|
}));
|
|
27502
28637
|
const docsAudit2 = checkDocsAudit(probe);
|
|
27503
|
-
if (docsAudit2)
|
|
28638
|
+
if (docsAudit2) emitNow(docsAudit2);
|
|
27504
28639
|
}
|
|
27505
|
-
|
|
28640
|
+
async function runWorktreeRootsRow() {
|
|
27506
28641
|
const probe = await deps.worktreeRoots().catch(() => void 0);
|
|
27507
28642
|
const roots = checkWorktreeRoots(probe);
|
|
27508
|
-
if (roots)
|
|
28643
|
+
if (roots) emitNow(roots);
|
|
27509
28644
|
}
|
|
27510
|
-
|
|
28645
|
+
async function runTrainSyncRow() {
|
|
27511
28646
|
try {
|
|
27512
|
-
|
|
28647
|
+
emitNow(checkTrainSync(await deps.syncTrain()));
|
|
27513
28648
|
} catch (e) {
|
|
27514
28649
|
const message = e instanceof Error ? e.message : String(e);
|
|
27515
|
-
|
|
28650
|
+
emitNow({
|
|
27516
28651
|
ok: false,
|
|
27517
28652
|
id: "train-branches",
|
|
27518
28653
|
label: "train branches",
|
|
@@ -27520,6 +28655,8 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
27520
28655
|
verbose: [`sync threw: ${message}`, "no train branch was fast-forwarded this run"]
|
|
27521
28656
|
});
|
|
27522
28657
|
}
|
|
28658
|
+
}
|
|
28659
|
+
async function runHousekeeperRows() {
|
|
27523
28660
|
const repoRoot2 = await deps.repoRoot();
|
|
27524
28661
|
try {
|
|
27525
28662
|
const plan = await deps.gcPlan("origin", 200);
|
|
@@ -27530,7 +28667,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
27530
28667
|
if (r.removedBranches.length || r.removedRemoteBranches.length) detailParts.push(`${r.removedBranches.length + r.removedRemoteBranches.length} merged branches`);
|
|
27531
28668
|
if (r.removedWorktreeDirs.length) detailParts.push(`${r.removedWorktreeDirs.length} dead worktrees`);
|
|
27532
28669
|
if (r.removedTrackingRefs.length) detailParts.push(`${r.removedTrackingRefs.length} stale refs`);
|
|
27533
|
-
|
|
28670
|
+
emitNow({
|
|
27534
28671
|
id: "branches-worktrees",
|
|
27535
28672
|
ok: true,
|
|
27536
28673
|
label: "branches / worktrees",
|
|
@@ -27553,7 +28690,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
27553
28690
|
...plan.trackingRefs.map((r) => `stale tracking ref: ${r.ref}`),
|
|
27554
28691
|
...plan.worktreeDirs.map((w) => `dead worktree: ${w.path} (${w.reason})`)
|
|
27555
28692
|
];
|
|
27556
|
-
|
|
28693
|
+
emitNow(n === 0 ? { id: "branches-worktrees", ok: true, label: "branches / worktrees", verbose: ["nothing reapable"] } : {
|
|
27557
28694
|
id: "branches-worktrees",
|
|
27558
28695
|
ok: false,
|
|
27559
28696
|
label: "branches / worktrees",
|
|
@@ -27571,35 +28708,65 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
27571
28708
|
if (applyRepo && scratch.plan.safeAuto.length > 0) {
|
|
27572
28709
|
const applied = deps.executeScratchGc(repoRoot2, { apply: true });
|
|
27573
28710
|
const pruned = applied.applied?.pruned.length ?? 0;
|
|
27574
|
-
|
|
28711
|
+
emitNow({ id: "scratch", ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
|
|
27575
28712
|
if (pruned) restartPending = true;
|
|
27576
28713
|
} else {
|
|
27577
|
-
|
|
27578
|
-
}
|
|
27579
|
-
}
|
|
28714
|
+
emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor --apply`", verbose: scratchEvidence });
|
|
28715
|
+
}
|
|
28716
|
+
}
|
|
28717
|
+
const table = [
|
|
28718
|
+
// Heals first: the slowest work starts at second zero, and a stale mmi-cli is the binary every row
|
|
28719
|
+
// below is being measured with (#3954).
|
|
28720
|
+
{ id: "plugin", when: true, run: runPluginRow },
|
|
28721
|
+
{ id: "cli-version", when: true, run: runCliRow },
|
|
28722
|
+
{ id: "github-auth", when: true, run: runGithubAuthRow },
|
|
28723
|
+
{ id: "github-pools", when: lane.full, run: runGithubPoolsRows },
|
|
28724
|
+
{ id: "aws-identity", when: true, run: runAwsRow },
|
|
28725
|
+
{ id: "repo-worktrees", when: true, run: runRepoWorktreesRow },
|
|
28726
|
+
{ id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow },
|
|
28727
|
+
{ id: "plugin-cache", when: true, run: runPluginCacheRow },
|
|
28728
|
+
{ id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
|
|
28729
|
+
{ id: "marketplace", when: true, run: runMarketplaceRows },
|
|
28730
|
+
{ id: "schedules", when: lane.full, run: runSchedulesRow },
|
|
28731
|
+
{ id: "redactor-liveness", when: lane.full, run: runRedactorRow },
|
|
28732
|
+
{ id: "docs-audit", when: lane.full, run: runDocsAuditRow },
|
|
28733
|
+
{ id: "worktree-roots", when: lane.full, run: runWorktreeRootsRow },
|
|
28734
|
+
// The two most expensive things in this file — a real `git fetch` plus train-branch fast-forward,
|
|
28735
|
+
// and a `gh`-backed gc sweep with a 20s timeout — so org repos on the full lane only (#3485).
|
|
28736
|
+
{ id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
|
|
28737
|
+
{ id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
|
|
28738
|
+
];
|
|
28739
|
+
for (const entry of table) if (entry.when) await entry.run();
|
|
27580
28740
|
const exitCode = doctorReportExitCode(checks);
|
|
27581
28741
|
if (opts.json) {
|
|
27582
28742
|
const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
|
|
27583
28743
|
io.log(JSON.stringify({
|
|
27584
28744
|
checks: payload,
|
|
27585
28745
|
restartPending,
|
|
27586
|
-
...restartPending ? { restartAction
|
|
28746
|
+
...restartPending ? { restartAction } : {},
|
|
27587
28747
|
exitCode
|
|
27588
28748
|
}, null, 2));
|
|
27589
28749
|
return exitCode;
|
|
27590
28750
|
}
|
|
27591
28751
|
if (opts.banner) {
|
|
27592
|
-
const actionable = checks.filter((c) => !c.ok || c.warn);
|
|
28752
|
+
const actionable = checks.filter((c) => (!c.ok || c.warn) && !streamed.has(c));
|
|
27593
28753
|
for (const c of actionable) {
|
|
27594
28754
|
io.log(renderReport([c], { restartPending: false }));
|
|
27595
28755
|
if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
|
|
27596
28756
|
}
|
|
27597
|
-
if (restartPending) io.log(
|
|
28757
|
+
if (restartPending) io.log(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
|
|
27598
28758
|
return 0;
|
|
27599
28759
|
}
|
|
27600
|
-
const
|
|
27601
|
-
|
|
27602
|
-
|
|
28760
|
+
const rest = checks.filter((c) => !streamed.has(c));
|
|
28761
|
+
const shown = opts.verbose ? rest : rest.filter((c) => !c.ok || c.warn);
|
|
28762
|
+
const lines = [];
|
|
28763
|
+
for (const check of shown) {
|
|
28764
|
+
lines.push(renderCheckLine(check));
|
|
28765
|
+
if (opts.verbose) for (const evidence of check.verbose ?? []) lines.push(`${VERBOSE_INDENT}${evidence}`);
|
|
28766
|
+
}
|
|
28767
|
+
lines.push(renderTally(checks));
|
|
28768
|
+
if (restartPending) lines.push(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
|
|
28769
|
+
io.log(lines.join("\n"));
|
|
27603
28770
|
return exitCode;
|
|
27604
28771
|
}
|
|
27605
28772
|
|
|
@@ -27701,17 +28868,43 @@ function installedClaudePluginVersion() {
|
|
|
27701
28868
|
return void 0;
|
|
27702
28869
|
}
|
|
27703
28870
|
}
|
|
27704
|
-
function
|
|
27705
|
-
|
|
28871
|
+
function manifestVersion(path2) {
|
|
28872
|
+
try {
|
|
28873
|
+
const manifest = JSON.parse((0, import_node_fs33.readFileSync)(path2, "utf8"));
|
|
28874
|
+
return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
|
|
28875
|
+
} catch {
|
|
28876
|
+
return void 0;
|
|
28877
|
+
}
|
|
28878
|
+
}
|
|
28879
|
+
function installedSurfacePluginVersion(surface) {
|
|
28880
|
+
const token = surfaceToken(surface);
|
|
28881
|
+
if (token === "kilo") {
|
|
28882
|
+
try {
|
|
28883
|
+
const stamp = (0, import_node_fs33.readFileSync)((0, import_node_path32.join)((0, import_node_os12.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
|
|
28884
|
+
return stamp || void 0;
|
|
28885
|
+
} catch {
|
|
28886
|
+
return void 0;
|
|
28887
|
+
}
|
|
28888
|
+
}
|
|
28889
|
+
if (token === "cursor") {
|
|
28890
|
+
return manifestVersion((0, import_node_path32.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
|
|
28891
|
+
}
|
|
28892
|
+
if (token === "kimi") {
|
|
28893
|
+
return manifestVersion((0, import_node_path32.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
|
|
28894
|
+
}
|
|
28895
|
+
if (token === "claude") return installedClaudePluginVersion();
|
|
28896
|
+
if (token !== "codex") return void 0;
|
|
27706
28897
|
try {
|
|
27707
28898
|
const raw = process.platform === "win32" ? (0, import_node_child_process14.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
|
|
27708
28899
|
encoding: "utf8",
|
|
27709
28900
|
stdio: ["ignore", "pipe", "ignore"],
|
|
27710
|
-
timeout: 15e3
|
|
28901
|
+
timeout: 15e3,
|
|
28902
|
+
windowsHide: true
|
|
27711
28903
|
}) : (0, import_node_child_process14.execFileSync)("codex", ["plugin", "list", "--json"], {
|
|
27712
28904
|
encoding: "utf8",
|
|
27713
28905
|
stdio: ["ignore", "pipe", "ignore"],
|
|
27714
|
-
timeout: 15e3
|
|
28906
|
+
timeout: 15e3,
|
|
28907
|
+
windowsHide: true
|
|
27715
28908
|
});
|
|
27716
28909
|
const parsed = JSON.parse(raw);
|
|
27717
28910
|
const plugin = parsed.installed?.find((entry) => entry.pluginId === MMI_PLUGIN_ID2 && entry.installed === true && entry.enabled === true);
|
|
@@ -27720,6 +28913,9 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
|
|
|
27720
28913
|
return void 0;
|
|
27721
28914
|
}
|
|
27722
28915
|
}
|
|
28916
|
+
function installedActivePluginVersion(surface = detectSurface(process.env)) {
|
|
28917
|
+
return installedSurfacePluginVersion(surface);
|
|
28918
|
+
}
|
|
27723
28919
|
function worktreeRootSync() {
|
|
27724
28920
|
try {
|
|
27725
28921
|
const out = (0, import_node_child_process14.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -27857,6 +29053,29 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
27857
29053
|
const throttled = opts.throttleReleasedRead ? throttledReleasedVersion() : void 0;
|
|
27858
29054
|
let notebook;
|
|
27859
29055
|
const notebookOnce = () => notebook ??= fetchNotebook();
|
|
29056
|
+
let surfaceEvidence;
|
|
29057
|
+
let surfaceEvidenceRead = false;
|
|
29058
|
+
const surfaceEvidenceOnce = (isOrgRepo) => {
|
|
29059
|
+
if (surfaceEvidenceRead) return surfaceEvidence;
|
|
29060
|
+
surfaceEvidenceRead = true;
|
|
29061
|
+
const runtimeSurface = detectSurface(process.env);
|
|
29062
|
+
const token = surfaceToken(runtimeSurface);
|
|
29063
|
+
if (!token || token === "opencode") return void 0;
|
|
29064
|
+
const descriptor = doctorSurface(token);
|
|
29065
|
+
const snapshot = snapshotPluginGuardInput(runtimeSurface, isOrgRepo);
|
|
29066
|
+
const installedVersion = installedSurfacePluginVersion(runtimeSurface);
|
|
29067
|
+
surfaceEvidence = {
|
|
29068
|
+
descriptor,
|
|
29069
|
+
applicable: isOrgRepo || snapshot.installRecordPresent || snapshot.pluginCachePresent,
|
|
29070
|
+
installRecordPresent: snapshot.installRecordPresent,
|
|
29071
|
+
deliveryPresent: snapshot.marketplaceClonePresent,
|
|
29072
|
+
payloadPresent: snapshot.pluginCachePresent,
|
|
29073
|
+
manifest: installedVersion ? "valid" : snapshot.installRecordPresent ? "invalid" : "missing",
|
|
29074
|
+
guardState: buildPluginGuardDecision(snapshot).state,
|
|
29075
|
+
...installedVersion ? { installedVersion } : {}
|
|
29076
|
+
};
|
|
29077
|
+
return surfaceEvidence;
|
|
29078
|
+
};
|
|
27860
29079
|
const docsJanitorArmedAt = async (repo) => {
|
|
27861
29080
|
const wanted = docsJanitorScheduleId(repo);
|
|
27862
29081
|
const { entries } = await notebookOnce();
|
|
@@ -27868,9 +29087,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
27868
29087
|
githubRepoReach: githubRepoReachProbe,
|
|
27869
29088
|
awsCallerArn,
|
|
27870
29089
|
isOrgRepo: () => isOrgRepoRoot(),
|
|
27871
|
-
|
|
27872
|
-
pluginGuardState: activePluginGuardState,
|
|
27873
|
-
pluginSurface: () => detectSurface(process.env),
|
|
29090
|
+
surfaceEvidence: surfaceEvidenceOnce,
|
|
27874
29091
|
pluginTrustState: () => codexHookTrustState(),
|
|
27875
29092
|
releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
|
|
27876
29093
|
releasedVersionNote: throttled ? throttled.note : void 0,
|
|
@@ -27879,12 +29096,13 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
27879
29096
|
// marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
|
|
27880
29097
|
// rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
|
|
27881
29098
|
// carries a CLI shim, so a concurrent npm global install races the same PATH surface.
|
|
27882
|
-
updateCli: (target) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target)),
|
|
29099
|
+
updateCli: (target, onStep) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target, onStep)),
|
|
27883
29100
|
// #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
|
|
27884
29101
|
// `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
|
|
27885
|
-
healPlugin: () => {
|
|
29102
|
+
healPlugin: (onStep) => {
|
|
27886
29103
|
const surface = detectSurface(process.env);
|
|
27887
|
-
|
|
29104
|
+
const host = surfaceEvidenceOnce(true)?.descriptor.displayName ?? surface;
|
|
29105
|
+
return withEnvHealLock(`${host} plugin reinstall`, () => healActivePluginForDoctor(surface, onStep));
|
|
27888
29106
|
},
|
|
27889
29107
|
currentCliVersion: resolveClientVersion,
|
|
27890
29108
|
readGitignore,
|
|
@@ -29344,13 +30562,25 @@ function surfaceWaived() {
|
|
|
29344
30562
|
}
|
|
29345
30563
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
29346
30564
|
withExamples(mutating(
|
|
29347
|
-
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks
|
|
29348
|
-
// --dry-run/--validate-only plan:
|
|
29349
|
-
//
|
|
29350
|
-
//
|
|
30565
|
+
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Required when the target repo runs the one-surface-label board rule; any value satisfies it, so this is not a closed enum").option("--no-surface", "file without a surface label on a repo that requires one \u2014 for a genuinely exempt filing (e.g. a coop proof issue that spans every surface)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
|
|
30566
|
+
// --dry-run/--validate-only plan: resolve the same title source and validate the same type, priority,
|
|
30567
|
+
// and surface contract as the real action. A plan that echoes the title-file PATH instead of its value
|
|
30568
|
+
// is not a plan of the mutation that will run (#3914).
|
|
29351
30569
|
async (opts) => {
|
|
29352
30570
|
const type = resolveCreateType(opts.type, "issue create", opts.label);
|
|
29353
30571
|
const priority = resolveCreatePriority(opts.priority, "issue create");
|
|
30572
|
+
let title;
|
|
30573
|
+
try {
|
|
30574
|
+
title = await resolveIssueTitle(
|
|
30575
|
+
{ title: opts.title, titleFile: opts.titleFile },
|
|
30576
|
+
{ readFile: import_promises10.readFile, readStdin }
|
|
30577
|
+
);
|
|
30578
|
+
} catch (e) {
|
|
30579
|
+
return fail(
|
|
30580
|
+
`issue create: ${e.message}`,
|
|
30581
|
+
e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0
|
|
30582
|
+
);
|
|
30583
|
+
}
|
|
29354
30584
|
const planLabels = opts.label ?? [];
|
|
29355
30585
|
const clash = conflictingSurfaceInputs(opts.surface, planLabels);
|
|
29356
30586
|
if (clash) fail(clash.message, clash.payload);
|
|
@@ -29368,7 +30598,7 @@ withExamples(mutating(
|
|
|
29368
30598
|
return {
|
|
29369
30599
|
command: "issue create",
|
|
29370
30600
|
type,
|
|
29371
|
-
title
|
|
30601
|
+
title,
|
|
29372
30602
|
priority,
|
|
29373
30603
|
repo: opts.repo,
|
|
29374
30604
|
...surface ? { surface } : {}
|
|
@@ -30271,6 +31501,12 @@ function renderReleaseResume(r) {
|
|
|
30271
31501
|
if (r.rcAlignment) lines.push(` rc: ${r.rcAlignment.note}`);
|
|
30272
31502
|
return lines.join("\n");
|
|
30273
31503
|
}
|
|
31504
|
+
function renderReleaseAbort(r) {
|
|
31505
|
+
return `mmi-cli release --abort --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
|
|
31506
|
+
}
|
|
31507
|
+
function renderReleasePublishRetry(r) {
|
|
31508
|
+
return `mmi-cli release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
|
|
31509
|
+
}
|
|
30274
31510
|
function renderRcandResume(r) {
|
|
30275
31511
|
return `mmi-cli rcand --resume: promoted ${r.repo} \u2192 rc at ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]; ${renderDeployLine(r)}; ${r.note}`;
|
|
30276
31512
|
}
|
|
@@ -30315,9 +31551,11 @@ async function resolveRcandPlanTargets() {
|
|
|
30315
31551
|
for (const commandName of ["rcand", "release"]) {
|
|
30316
31552
|
const trainCmd = 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("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)");
|
|
30317
31553
|
const RELEASE_ONLY_FLAGS = [
|
|
30318
|
-
{ flags: "--announce-summary-file <path>", description: "agent-curated summary
|
|
31554
|
+
{ flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line Hub Slack summary; required for a new MMI-Hub --apply (#883/#3901)" },
|
|
30319
31555
|
{ flags: "--ack <shas>", description: "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)" },
|
|
30320
|
-
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" }
|
|
31556
|
+
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
|
|
31557
|
+
{ flags: "--abort", description: "with --apply, delete only a proven unpublished failed Hub tag and restore local main for a clean recut (#3944)" },
|
|
31558
|
+
{ flags: "--retry-publish <run-id>", description: "with --apply, retry failed jobs of one proven Hub publish release run (#3949)" }
|
|
30321
31559
|
];
|
|
30322
31560
|
for (const f of RELEASE_ONLY_FLAGS) {
|
|
30323
31561
|
if (commandName === "release") {
|
|
@@ -30341,6 +31579,45 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
30341
31579
|
if (o.announceSummaryFile && commandName !== "release") {
|
|
30342
31580
|
return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no Hub Slack announcement. Run: mmi-cli release --announce-summary-file <path>`);
|
|
30343
31581
|
}
|
|
31582
|
+
if (o.abort && commandName !== "release") {
|
|
31583
|
+
return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished Hub release tag. Run: mmi-cli release --abort --apply`);
|
|
31584
|
+
}
|
|
31585
|
+
if (o.retryPublish && commandName !== "release") {
|
|
31586
|
+
return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli release --retry-publish <run-id> --apply --watch`);
|
|
31587
|
+
}
|
|
31588
|
+
if (o.retryPublish) {
|
|
31589
|
+
if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
|
|
31590
|
+
if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
|
|
31591
|
+
if (o.announceSummaryFile || o.ack || o.dev) return fail("release: --retry-publish accepts only --apply, --watch, --repo and --json");
|
|
31592
|
+
if (o.repo) {
|
|
31593
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli release --retry-publish ${o.retryPublish} --apply${o.watch ? " --watch" : ""}`);
|
|
31594
|
+
if (!guard.ok) return fail(`release: ${guard.message}`);
|
|
31595
|
+
}
|
|
31596
|
+
const runId = Number.parseInt(o.retryPublish, 10);
|
|
31597
|
+
try {
|
|
31598
|
+
const result = await runReleasePublishRetry(trainApplyDeps(), runId, { approved: true, watch: o.watch });
|
|
31599
|
+
return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleasePublishRetry(result));
|
|
31600
|
+
} catch (e) {
|
|
31601
|
+
return failGraceful(`release --retry-publish: ${e.message}`);
|
|
31602
|
+
}
|
|
31603
|
+
}
|
|
31604
|
+
if (o.abort) {
|
|
31605
|
+
if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
|
|
31606
|
+
if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
|
|
31607
|
+
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
31608
|
+
return fail("release: --abort accepts only --apply, --repo and --json; promotion flags cannot be combined with rollback");
|
|
31609
|
+
}
|
|
31610
|
+
if (o.repo) {
|
|
31611
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), "mmi-cli release --abort --apply");
|
|
31612
|
+
if (!guard.ok) return fail(`release: ${guard.message}`);
|
|
31613
|
+
}
|
|
31614
|
+
try {
|
|
31615
|
+
const result = await runReleaseAbort(trainApplyDeps(), { approved: true });
|
|
31616
|
+
return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleaseAbort(result));
|
|
31617
|
+
} catch (e) {
|
|
31618
|
+
return failGraceful(`release --abort: ${e.message}`);
|
|
31619
|
+
}
|
|
31620
|
+
}
|
|
30344
31621
|
if (o.resume) {
|
|
30345
31622
|
if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive \u2014 --apply cuts the NEXT version, --resume finishes the immutable tag already on origin`);
|
|
30346
31623
|
try {
|
|
@@ -30362,6 +31639,24 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
30362
31639
|
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
|
|
30363
31640
|
if (!guard.ok) return fail(`${commandName}: ${guard.message}`);
|
|
30364
31641
|
}
|
|
31642
|
+
if (o.apply && commandName === "release" && (await resolveRepo())?.toLowerCase() === ANNOUNCE_REPO.toLowerCase()) {
|
|
31643
|
+
if (!o.announceSummaryFile) {
|
|
31644
|
+
return fail(
|
|
31645
|
+
"release: a new MMI-Hub release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same release command"
|
|
31646
|
+
);
|
|
31647
|
+
}
|
|
31648
|
+
let summaryLines;
|
|
31649
|
+
try {
|
|
31650
|
+
summaryLines = summaryFileLines(await (0, import_promises10.readFile)(o.announceSummaryFile, "utf8"));
|
|
31651
|
+
} catch (e) {
|
|
31652
|
+
return fail(`release: could not read --announce-summary-file ${o.announceSummaryFile}: ${e.message}`);
|
|
31653
|
+
}
|
|
31654
|
+
if (summaryLines.length < 3 || summaryLines.length > 6) {
|
|
31655
|
+
return fail(
|
|
31656
|
+
`release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${summaryLines.length})`
|
|
31657
|
+
);
|
|
31658
|
+
}
|
|
31659
|
+
}
|
|
30365
31660
|
if (o.apply) {
|
|
30366
31661
|
try {
|
|
30367
31662
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|