@mutmutco/cli 3.101.0 → 3.103.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/dist/main.cjs +141 -30
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -7536,6 +7536,39 @@ function missingPlaceholders(rendered) {
|
|
|
7536
7536
|
for (const m of rendered.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
|
|
7537
7537
|
return [...out];
|
|
7538
7538
|
}
|
|
7539
|
+
async function resolveHubSeedSource(execGit, defaultBranch = "development") {
|
|
7540
|
+
let status;
|
|
7541
|
+
try {
|
|
7542
|
+
status = (await execGit(["status", "--porcelain"])).trim();
|
|
7543
|
+
} catch (e) {
|
|
7544
|
+
return { ok: false, reason: `could not read git status of this checkout (${e.message}) \u2014 refusing to treat an unknown working tree as the fleet's desired state` };
|
|
7545
|
+
}
|
|
7546
|
+
if (status) {
|
|
7547
|
+
return {
|
|
7548
|
+
ok: false,
|
|
7549
|
+
reason: `this checkout has uncommitted changes \u2014 refusing to read seeds from a working tree that has not been reviewed or merged:
|
|
7550
|
+
${status}`
|
|
7551
|
+
};
|
|
7552
|
+
}
|
|
7553
|
+
let head;
|
|
7554
|
+
let remoteSha;
|
|
7555
|
+
try {
|
|
7556
|
+
head = (await execGit(["rev-parse", "HEAD"])).trim();
|
|
7557
|
+
remoteSha = (await execGit(["ls-remote", "origin", `refs/heads/${defaultBranch}`])).trim().split(/\s+/)[0] ?? "";
|
|
7558
|
+
} catch (e) {
|
|
7559
|
+
return { ok: false, reason: `could not resolve this checkout's HEAD against origin/${defaultBranch} (${e.message}) \u2014 refusing to read seeds without a known upstream revision` };
|
|
7560
|
+
}
|
|
7561
|
+
if (!remoteSha) {
|
|
7562
|
+
return { ok: false, reason: `\`git ls-remote origin refs/heads/${defaultBranch}\` returned nothing \u2014 refusing to read seeds without a known upstream revision` };
|
|
7563
|
+
}
|
|
7564
|
+
if (head !== remoteSha) {
|
|
7565
|
+
return {
|
|
7566
|
+
ok: false,
|
|
7567
|
+
reason: `this checkout (${head}) is not origin/${defaultBranch} (${remoteSha}) \u2014 fast-forward to origin/${defaultBranch} first; a stale or ahead checkout must not seed the fleet`
|
|
7568
|
+
};
|
|
7569
|
+
}
|
|
7570
|
+
return { ok: true, sha: head };
|
|
7571
|
+
}
|
|
7539
7572
|
var GITIGNORE_MANAGED_BEGIN = "# >>> mmi-managed >>>";
|
|
7540
7573
|
var GITIGNORE_MANAGED_END = "# <<< mmi-managed <<<";
|
|
7541
7574
|
var MANAGED_GITIGNORE_LINES = [
|
|
@@ -17913,6 +17946,9 @@ function selectPrunablePluginVersions(names, currentVersion, installedVersion) {
|
|
|
17913
17946
|
function pluginCacheRootForConfig(configRoot) {
|
|
17914
17947
|
return `${configRoot}/plugins/cache/mutmutco/mmi`;
|
|
17915
17948
|
}
|
|
17949
|
+
function pluginCacheRoot(home) {
|
|
17950
|
+
return pluginCacheRootForConfig(`${home}/.claude`);
|
|
17951
|
+
}
|
|
17916
17952
|
function runningPluginVersion(env, cliVersion) {
|
|
17917
17953
|
const root = env.CLAUDE_PLUGIN_ROOT?.trim();
|
|
17918
17954
|
const leaf = root ? root.replace(/[\\/]+$/, "").split(/[\\/]/).pop() : void 0;
|
|
@@ -18134,12 +18170,28 @@ function renderPluginCachePlan(plan, applied) {
|
|
|
18134
18170
|
function isMmiPiPackage(entry) {
|
|
18135
18171
|
return /[\\/]mutmutco[\\/]mmi[\\/][^\\/]+[\\/]\.pi-plugin\/?$/.test(entry);
|
|
18136
18172
|
}
|
|
18173
|
+
function newestExistingPiPlugin(home) {
|
|
18174
|
+
const cacheRoot = pluginCacheRoot(home);
|
|
18175
|
+
let names;
|
|
18176
|
+
try {
|
|
18177
|
+
names = (0, import_node_fs22.readdirSync)(cacheRoot);
|
|
18178
|
+
} catch {
|
|
18179
|
+
return void 0;
|
|
18180
|
+
}
|
|
18181
|
+
const newest = names.filter(isVersionDirName).sort((a, b) => compareVersions(b, a))[0];
|
|
18182
|
+
if (!newest) return void 0;
|
|
18183
|
+
const candidate = (0, import_node_path20.join)(cacheRoot, newest, ".pi-plugin");
|
|
18184
|
+
return (0, import_node_fs22.existsSync)(candidate) ? candidate : void 0;
|
|
18185
|
+
}
|
|
18137
18186
|
function expectedPiPluginPath(home, env, installedVersion) {
|
|
18138
18187
|
const root = env.CLAUDE_PLUGIN_ROOT?.trim();
|
|
18139
18188
|
if (root && /[\\/]mutmutco[\\/]mmi[\\/]/.test(root)) return (0, import_node_path20.join)(root, ".pi-plugin");
|
|
18140
18189
|
const version = installedVersion ?? runningPluginVersion(env);
|
|
18141
|
-
if (
|
|
18142
|
-
|
|
18190
|
+
if (version) {
|
|
18191
|
+
const pinned = (0, import_node_path20.join)(home, ".claude", "plugins", "cache", "mutmutco", "mmi", version, ".pi-plugin");
|
|
18192
|
+
if ((0, import_node_fs22.existsSync)(pinned)) return pinned;
|
|
18193
|
+
}
|
|
18194
|
+
return newestExistingPiPlugin(home);
|
|
18143
18195
|
}
|
|
18144
18196
|
function settingsPath(home) {
|
|
18145
18197
|
return (0, import_node_path20.join)(home, ".pi", "agent", "settings.json");
|
|
@@ -18147,7 +18199,7 @@ function settingsPath(home) {
|
|
|
18147
18199
|
function readPiPluginState(home, env, installedVersion) {
|
|
18148
18200
|
if (!(0, import_node_fs22.existsSync)((0, import_node_path20.join)(home, ".pi", "agent"))) return void 0;
|
|
18149
18201
|
const expectedPath = expectedPiPluginPath(home, env, installedVersion);
|
|
18150
|
-
if (!expectedPath
|
|
18202
|
+
if (!expectedPath) return void 0;
|
|
18151
18203
|
const file = settingsPath(home);
|
|
18152
18204
|
if (!(0, import_node_fs22.existsSync)(file)) return { expectedPath, settingsReadable: true };
|
|
18153
18205
|
try {
|
|
@@ -23943,6 +23995,16 @@ var import_node_crypto6 = require("node:crypto");
|
|
|
23943
23995
|
function byteComparableSeeds(manifest, cls) {
|
|
23944
23996
|
return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
|
|
23945
23997
|
}
|
|
23998
|
+
function declaredRenderWaivers(manifest, repo) {
|
|
23999
|
+
const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
|
|
24000
|
+
const findings = [];
|
|
24001
|
+
for (const seed of manifest.seeds) {
|
|
24002
|
+
if (seed.ownership !== "org" || seed.source === "self") continue;
|
|
24003
|
+
const why = seed.waivers?.[slug];
|
|
24004
|
+
if (why) findings.push({ repo, target: seed.target, state: "waived", detail: `hand-authored, not rendered \u2014 waived: ${why}` });
|
|
24005
|
+
}
|
|
24006
|
+
return findings;
|
|
24007
|
+
}
|
|
23946
24008
|
function compareSeedBytes(hubContent, repoContent) {
|
|
23947
24009
|
if (repoContent === null) return "absent";
|
|
23948
24010
|
const normalize = (s) => s.replace(/\r\n/g, "\n");
|
|
@@ -24020,18 +24082,21 @@ function assignWaves(repos, canarySlug) {
|
|
|
24020
24082
|
rest.forEach((r, i) => waves.set(r.repo, i < wave1Count ? 1 : 2));
|
|
24021
24083
|
return waves;
|
|
24022
24084
|
}
|
|
24023
|
-
function statusFor(read) {
|
|
24085
|
+
function statusFor(read, currentSha) {
|
|
24024
24086
|
if (!read) return { status: "pending", record: {} };
|
|
24025
24087
|
if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
|
|
24026
24088
|
const pr2 = read.pr;
|
|
24027
24089
|
if (!pr2) return { status: "pending", record: {} };
|
|
24028
24090
|
if (pr2.state === "closed") return { status: "closed-unmerged", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
24091
|
+
if (pr2.state === "merged" && pr2.sourceSha && pr2.sourceSha !== currentSha) {
|
|
24092
|
+
return { status: "pending", record: {} };
|
|
24093
|
+
}
|
|
24029
24094
|
if (pr2.checks === "red") return { status: "red", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
24030
24095
|
return { status: "open-pending", record: { prNumber: pr2.number, prUrl: pr2.url, mergeSha: pr2.mergeSha } };
|
|
24031
24096
|
}
|
|
24032
24097
|
var HALTING_STATUSES = /* @__PURE__ */ new Set(["red", "closed-unmerged"]);
|
|
24033
24098
|
function planPropagationTick(input) {
|
|
24034
|
-
const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied } = input;
|
|
24099
|
+
const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied, currentSha } = input;
|
|
24035
24100
|
const readByRepo = new Map(reads.map((r) => [r.repo, r]));
|
|
24036
24101
|
const records = [];
|
|
24037
24102
|
const opened = [];
|
|
@@ -24065,7 +24130,7 @@ function planPropagationTick(input) {
|
|
|
24065
24130
|
let waveAllMatch = true;
|
|
24066
24131
|
let waveHasRed = false;
|
|
24067
24132
|
for (const r of waveRepos) {
|
|
24068
|
-
const { status, record } = statusFor(readByRepo.get(r.repo));
|
|
24133
|
+
const { status, record } = statusFor(readByRepo.get(r.repo), currentSha);
|
|
24069
24134
|
const shouldOpen = status === "pending";
|
|
24070
24135
|
if (shouldOpen) opened.push(r.repo);
|
|
24071
24136
|
records.push({
|
|
@@ -24807,6 +24872,7 @@ async function reconcileOrgNoAgentFilesRuleset(plan, client, org = ORG_LOGIN) {
|
|
|
24807
24872
|
}
|
|
24808
24873
|
|
|
24809
24874
|
// src/bootstrap-commands.ts
|
|
24875
|
+
var execGitForSeedSource = async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
24810
24876
|
function registerBootstrapCommands(program3) {
|
|
24811
24877
|
const bootstrap = program3.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).option("--json", "machine-readable output").action((o) => {
|
|
24812
24878
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
@@ -24886,6 +24952,8 @@ function registerBootstrapCommands(program3) {
|
|
|
24886
24952
|
const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
|
|
24887
24953
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
24888
24954
|
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
|
|
24955
|
+
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
24956
|
+
if (!seedSource.ok) return fail(`bootstrap drift: ${seedSource.reason}`);
|
|
24889
24957
|
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
24890
24958
|
const hubContents = /* @__PURE__ */ new Map();
|
|
24891
24959
|
for (const s of manifest.seeds) {
|
|
@@ -24928,12 +24996,14 @@ function registerBootstrapCommands(program3) {
|
|
|
24928
24996
|
reads.push({ target: seed.target, content });
|
|
24929
24997
|
}
|
|
24930
24998
|
findings.push(...auditRepoSeedDrift(repo, seeds, hubContents, reads));
|
|
24999
|
+
findings.push(...declaredRenderWaivers(manifest, repo));
|
|
24931
25000
|
}
|
|
24932
25001
|
if (o.json) {
|
|
24933
25002
|
console.log(JSON.stringify({
|
|
24934
25003
|
// #3842: `ok` reflects real findings; waivers ride the payload so a consumer can see every
|
|
24935
25004
|
// standing exception without them counting as drift.
|
|
24936
25005
|
ok: findings.every((f) => f.state === "waived"),
|
|
25006
|
+
sourceSha: seedSource.sha,
|
|
24937
25007
|
scope: o.repo ? "single-repo" : "fleet",
|
|
24938
25008
|
reposAudited: targets.length,
|
|
24939
25009
|
seedsPerRepo,
|
|
@@ -24968,6 +25038,8 @@ function registerBootstrapCommands(program3) {
|
|
|
24968
25038
|
}
|
|
24969
25039
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
24970
25040
|
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
|
|
25041
|
+
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
25042
|
+
if (!seedSource.ok) return fail(`bootstrap apply: ${seedSource.reason}`);
|
|
24971
25043
|
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
24972
25044
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
24973
25045
|
const slug = parsedRepo.slug;
|
|
@@ -25144,9 +25216,9 @@ function registerBootstrapCommands(program3) {
|
|
|
25144
25216
|
"--body",
|
|
25145
25217
|
onlyTarget ? `Auto-opened by \`mmi-cli bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
|
|
25146
25218
|
|
|
25147
|
-
\`${onlyTarget}\` is an org-owned seed declared in MMI-Hub's \`skills/bootstrap/seeds/manifest.json
|
|
25219
|
+
\`${onlyTarget}\` is an org-owned seed declared in MMI-Hub's \`skills/bootstrap/seeds/manifest.json\` at MMI-Hub@${seedSource.sha}; this PR brings this repo's copy to the Hub's. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
|
|
25148
25220
|
|
|
25149
|
-
\`${baseBranch}\` is protected (${seedPlan.reason}), so delivery goes via this branch + PR \u2014 a direct contents PUT 409s on a protected base.` : `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected").`
|
|
25221
|
+
\`${baseBranch}\` is protected (${seedPlan.reason}), so delivery goes via this branch + PR \u2014 a direct contents PUT 409s on a protected base.` : `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected"). Seeds are from MMI-Hub@${seedSource.sha}.`
|
|
25150
25222
|
]);
|
|
25151
25223
|
seedPrUrl = created.url;
|
|
25152
25224
|
}
|
|
@@ -25242,7 +25314,7 @@ function registerBootstrapCommands(program3) {
|
|
|
25242
25314
|
applied.push(`ddb register ${registerPayload.slug} (failed: ${why})`);
|
|
25243
25315
|
}
|
|
25244
25316
|
}
|
|
25245
|
-
if (o.json) console.log(JSON.stringify({ repo, class: o.class, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
|
|
25317
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
|
|
25246
25318
|
else {
|
|
25247
25319
|
console.log(renderSeedPlan(actions));
|
|
25248
25320
|
if (o.execute) console.log(`
|
|
@@ -25254,6 +25326,8 @@ LIVE apply to ${repo}:
|
|
|
25254
25326
|
const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
|
|
25255
25327
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
25256
25328
|
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
|
|
25329
|
+
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
25330
|
+
if (!seedSource.ok) return fail(`bootstrap propagate: ${seedSource.reason}`);
|
|
25257
25331
|
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
25258
25332
|
const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
|
|
25259
25333
|
if (!o.target) {
|
|
@@ -25337,13 +25411,14 @@ LIVE apply to ${repo}:
|
|
|
25337
25411
|
let pr2;
|
|
25338
25412
|
try {
|
|
25339
25413
|
const branch = `${branchPrefix}-${r.slug}`;
|
|
25340
|
-
const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit", "--limit", "1"]);
|
|
25414
|
+
const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit,body", "--limit", "1"]);
|
|
25341
25415
|
const arr = JSON.parse(listed.stdout || "[]");
|
|
25342
25416
|
const p = arr[0];
|
|
25343
25417
|
if (p) {
|
|
25344
25418
|
const rollup = p.statusCheckRollup ?? [];
|
|
25345
25419
|
const checks = rollup.length === 0 ? "none" : rollup.some((c) => c.conclusion === "FAILURE" || c.state === "FAILURE") ? "red" : rollup.every((c) => c.conclusion === "SUCCESS" || c.state === "SUCCESS") ? "success" : "pending";
|
|
25346
|
-
|
|
25420
|
+
const sourceSha = p.body?.match(/Propagates MMI-Hub@([0-9a-fA-F]{7,40})/)?.[1];
|
|
25421
|
+
pr2 = { number: p.number, url: p.url, state: p.state === "MERGED" ? "merged" : p.state === "CLOSED" ? "closed" : "open", checks, mergeSha: p.mergeCommit?.oid, sourceSha };
|
|
25347
25422
|
}
|
|
25348
25423
|
} catch {
|
|
25349
25424
|
pr2 = void 0;
|
|
@@ -25365,10 +25440,10 @@ LIVE apply to ${repo}:
|
|
|
25365
25440
|
}
|
|
25366
25441
|
}
|
|
25367
25442
|
}
|
|
25368
|
-
const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied });
|
|
25443
|
+
const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied, currentSha: seedSource.sha });
|
|
25369
25444
|
if (o.execute) {
|
|
25370
25445
|
if (plan.refusedNoCanary) return fail("bootstrap propagate --execute: no canary declared for this target \u2014 refusing to write (set seedCanary:true on exactly one registry repo)");
|
|
25371
|
-
const headSha =
|
|
25446
|
+
const headSha = seedSource.sha;
|
|
25372
25447
|
for (const rec of plan.records) {
|
|
25373
25448
|
if (rec.action !== "open-pr") continue;
|
|
25374
25449
|
const repoEntry = repos.find((r) => r.repo === rec.repo);
|
|
@@ -25433,8 +25508,9 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
|
|
|
25433
25508
|
rec.prUrl = prUrl;
|
|
25434
25509
|
}
|
|
25435
25510
|
}
|
|
25436
|
-
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25437
|
-
else console.log(renderPropagationReport(plan)
|
|
25511
|
+
if (o.json) console.log(JSON.stringify({ ...plan, sourceSha: seedSource.sha }, null, 2));
|
|
25512
|
+
else console.log(`${renderPropagationReport(plan)}
|
|
25513
|
+
source: MMI-Hub@${seedSource.sha}`);
|
|
25438
25514
|
if (plan.halted) process.exitCode = 1;
|
|
25439
25515
|
});
|
|
25440
25516
|
bootstrap.command("rollback <repo>").description("#4240: open a per-repo revert PR of the recorded seed-propagate merge \u2014 never a fleet-wide overwrite; dry-run unless --execute").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).option("--target <path>", "the manifest target to roll back (an ownership:org + source:self seed, e.g. .github/workflows/agent-pr.yml)").option("--record <path>", "read propagation candidates from a persisted `bootstrap propagate --json` report instead of the live seed-propagate PR history").option("--execute", "LIVE revert via gh (master-gated) \u2014 opens/reuses the seed-rollback PR; dry-run prints the plan only").option("--json", "machine-readable output").action(async (repo) => {
|
|
@@ -25454,6 +25530,8 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
|
|
|
25454
25530
|
}
|
|
25455
25531
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
25456
25532
|
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
|
|
25533
|
+
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
25534
|
+
if (!seedSource.ok) return fail(`bootstrap rollback: ${seedSource.reason}`);
|
|
25457
25535
|
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
25458
25536
|
const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
|
|
25459
25537
|
if (!o.target) {
|
|
@@ -25509,7 +25587,7 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
|
|
|
25509
25587
|
}
|
|
25510
25588
|
const plan = planRollback(repo, seed.target, slug, candidates);
|
|
25511
25589
|
if (!plan.resolution.found) {
|
|
25512
|
-
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25590
|
+
if (o.json) console.log(JSON.stringify({ ...plan, sourceSha: seedSource.sha }, null, 2));
|
|
25513
25591
|
else console.log(renderRollbackReport(plan));
|
|
25514
25592
|
return fail(`bootstrap rollback: ${plan.resolution.reason}`);
|
|
25515
25593
|
}
|
|
@@ -25564,8 +25642,9 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
|
|
|
25564
25642
|
}
|
|
25565
25643
|
plan.prUrl = prUrl;
|
|
25566
25644
|
}
|
|
25567
|
-
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25568
|
-
else console.log(renderRollbackReport(plan)
|
|
25645
|
+
if (o.json) console.log(JSON.stringify({ ...plan, sourceSha: seedSource.sha }, null, 2));
|
|
25646
|
+
else console.log(`${renderRollbackReport(plan)}
|
|
25647
|
+
manifest verified against: MMI-Hub@${seedSource.sha}`);
|
|
25569
25648
|
});
|
|
25570
25649
|
}
|
|
25571
25650
|
|
|
@@ -29582,6 +29661,25 @@ function findNegatedClosings(text, closingNumbers) {
|
|
|
29582
29661
|
return forIssue.length > 0 && forIssue.every((m) => m.negated);
|
|
29583
29662
|
});
|
|
29584
29663
|
}
|
|
29664
|
+
var CROSS_REPO_TOKEN_RE = /\b([\w.-]+\/[\w.-]+)#(\d+)\b/g;
|
|
29665
|
+
function findCrossRepoAmbiguousClosings(text, closingNumbers, repo) {
|
|
29666
|
+
if (!closingNumbers.length) return [];
|
|
29667
|
+
const closing = new Set(closingNumbers);
|
|
29668
|
+
const flagged = /* @__PURE__ */ new Set();
|
|
29669
|
+
const repoLower = repo.toLowerCase();
|
|
29670
|
+
for (const match of text.matchAll(CROSS_REPO_TOKEN_RE)) {
|
|
29671
|
+
const n = Number(match[2]);
|
|
29672
|
+
if (!closing.has(n)) continue;
|
|
29673
|
+
if ((match[1] ?? "").toLowerCase() === repoLower) continue;
|
|
29674
|
+
flagged.add(n);
|
|
29675
|
+
}
|
|
29676
|
+
return [...flagged];
|
|
29677
|
+
}
|
|
29678
|
+
function crossRepoAmbiguousRefusalMessage(ambiguous, repo, context = "pr merge") {
|
|
29679
|
+
const named = ambiguous.map((n) => `${repo}#${n}`).join(", ");
|
|
29680
|
+
const first = `#${ambiguous[0] ?? "N"}`;
|
|
29681
|
+
return `${context}: REFUSED \u2014 GitHub will close ${named} on merge (a bare \`#N\` after a closing keyword always means the repo the PR lives in), but this body also mentions ${first} qualified against a DIFFERENT repo elsewhere \u2014 check whether that closing keyword was meant to carry an explicit owner/repo#N qualifier instead. Reword the closing reference with the full owner/repo#N form, or re-run with --force to merge anyway and let ${named} close.`;
|
|
29682
|
+
}
|
|
29585
29683
|
function commitMessagesText(commits) {
|
|
29586
29684
|
if (!Array.isArray(commits)) return "";
|
|
29587
29685
|
return commits.map((commit) => {
|
|
@@ -29590,7 +29688,7 @@ function commitMessagesText(commits) {
|
|
|
29590
29688
|
${typeof messageBody === "string" ? messageBody : ""}`;
|
|
29591
29689
|
}).join("\n");
|
|
29592
29690
|
}
|
|
29593
|
-
function parseClosingGuardInput(raw) {
|
|
29691
|
+
function parseClosingGuardInput(raw, repo) {
|
|
29594
29692
|
if (!raw || typeof raw !== "object") return void 0;
|
|
29595
29693
|
const { title, body, state, closingIssuesReferences, commits } = raw;
|
|
29596
29694
|
if (typeof state !== "string" || !Array.isArray(closingIssuesReferences)) return void 0;
|
|
@@ -29603,7 +29701,7 @@ function parseClosingGuardInput(raw) {
|
|
|
29603
29701
|
const commitClosing = [...new Set(findClosingMentions(commitMessagesText(commits)).map((m) => m.issue))];
|
|
29604
29702
|
const text = `${typeof title === "string" ? title : ""}
|
|
29605
29703
|
${typeof body === "string" ? body : ""}`;
|
|
29606
|
-
return { state, text, closing, commitClosing };
|
|
29704
|
+
return { state, text, closing, commitClosing, repo };
|
|
29607
29705
|
}
|
|
29608
29706
|
function negatedClosingRefusalMessage(negated, context = "pr merge") {
|
|
29609
29707
|
const named = negated.map((n) => `#${n}`).join(", ");
|
|
@@ -29632,12 +29730,24 @@ function evaluateClosingGuard(input, opts) {
|
|
|
29632
29730
|
}
|
|
29633
29731
|
if (input.closing.length === 0) return { blocked: false };
|
|
29634
29732
|
const negated = findNegatedClosings(input.text, input.closing);
|
|
29635
|
-
if (
|
|
29636
|
-
|
|
29637
|
-
|
|
29638
|
-
|
|
29639
|
-
|
|
29640
|
-
|
|
29733
|
+
if (negated.length) {
|
|
29734
|
+
if (!opts.force) return { blocked: true, message: negatedClosingRefusalMessage(negated, opts.context) };
|
|
29735
|
+
return {
|
|
29736
|
+
blocked: false,
|
|
29737
|
+
message: `${opts.context}: --force past the negated-closing guard \u2014 GitHub will still close ${negated.map((n) => `#${n}`).join(", ")} on merge although the PR body says it does not.`
|
|
29738
|
+
};
|
|
29739
|
+
}
|
|
29740
|
+
if (input.repo) {
|
|
29741
|
+
const ambiguous = findCrossRepoAmbiguousClosings(input.text, input.closing, input.repo);
|
|
29742
|
+
if (ambiguous.length) {
|
|
29743
|
+
if (!opts.force) return { blocked: true, message: crossRepoAmbiguousRefusalMessage(ambiguous, input.repo, opts.context) };
|
|
29744
|
+
return {
|
|
29745
|
+
blocked: false,
|
|
29746
|
+
message: `${opts.context}: --force past the cross-repo-ambiguous closing guard \u2014 GitHub will still close ${ambiguous.map((n) => `${input.repo}#${n}`).join(", ")} on merge.`
|
|
29747
|
+
};
|
|
29748
|
+
}
|
|
29749
|
+
}
|
|
29750
|
+
return { blocked: false };
|
|
29641
29751
|
}
|
|
29642
29752
|
|
|
29643
29753
|
// src/session-report.ts
|
|
@@ -34556,12 +34666,13 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
|
|
|
34556
34666
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
34557
34667
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
34558
34668
|
});
|
|
34559
|
-
pr.command("land <number>").description("agent merge path (#1440): train probe ? checks-wait ? merge --auto ? poll enqueued ? development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--preserve-worktree", "after merge, keep the local PR worktree/stage/branch for an active batch (#1888)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword
|
|
34669
|
+
pr.command("land <number>").description("agent merge path (#1440): train probe ? checks-wait ? merge --auto ? poll enqueued ? development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--preserve-worktree", "after merge, keep the local PR worktree/stage/branch for an active batch (#1888)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").action(async (number, o) => {
|
|
34560
34670
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
34561
34671
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
34562
34672
|
assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
|
|
34563
34673
|
const landClosingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences,commits"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
|
|
34564
|
-
const
|
|
34674
|
+
const landRepoForGuard = await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
|
|
34675
|
+
const landClosingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(landClosingGuardRaw, landRepoForGuard), { force: o.force, context: "pr land" });
|
|
34565
34676
|
if (landClosingGuardVerdict.blocked) {
|
|
34566
34677
|
console.error(landClosingGuardVerdict.message);
|
|
34567
34678
|
process.exitCode = 1;
|
|
@@ -34672,14 +34783,14 @@ pr.command("land <number>").description("agent merge path (#1440): train probe ?
|
|
|
34672
34783
|
}
|
|
34673
34784
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
34674
34785
|
});
|
|
34675
|
-
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree ? no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge ? merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword
|
|
34786
|
+
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree ? no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge ? merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
|
|
34676
34787
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
34677
34788
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
34678
34789
|
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
34679
34790
|
const [headRef, baseRef, headRefOid] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid", "--jq", '.headRefName + " " + .baseRefName + " " + (.headRefOid // "")'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim().split(/\s+/);
|
|
34680
34791
|
const headIsProtected = isProtectedBranch(headRef);
|
|
34681
34792
|
const closingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences,commits"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
|
|
34682
|
-
const closingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(closingGuardRaw), { force: o.force, context: "pr merge" });
|
|
34793
|
+
const closingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(closingGuardRaw, repoForPostCleanup), { force: o.force, context: "pr merge" });
|
|
34683
34794
|
if (closingGuardVerdict.blocked) {
|
|
34684
34795
|
console.error(closingGuardVerdict.message);
|
|
34685
34796
|
process.exitCode = 1;
|
package/package.json
CHANGED