@mutmutco/claude-plugin 4.6.9 → 4.6.10
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/.claude-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +253 -85
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/board/SKILL.md +5 -2
- package/skills/release/train-common.md +1 -1
package/cli/dist/main.cjs
CHANGED
|
@@ -15350,6 +15350,9 @@ var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
|
|
|
15350
15350
|
// #9587: one named run rerun (all or failed jobs) — same actions:write profile, nothing broader.
|
|
15351
15351
|
["run rerun", { metadata: "read", actions: "write" }],
|
|
15352
15352
|
["issue comment", { metadata: "read", issues: "write", pull_requests: "write" }],
|
|
15353
|
+
// #9614: closing an issue is an issue-state write — it rides the org App (mmi-github-app), so the close
|
|
15354
|
+
// is attributed to the App instead of the personal account. Nothing broader than issues.
|
|
15355
|
+
["issue close", { metadata: "read", issues: "write" }],
|
|
15353
15356
|
// #8401: the detached related-issue lookup (issue list/view reads + one marker comment) — no identity contract.
|
|
15354
15357
|
["issue discover-related", { metadata: "read", issues: "write" }]
|
|
15355
15358
|
]);
|
|
@@ -15738,6 +15741,13 @@ function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none")
|
|
|
15738
15741
|
].join(" ")
|
|
15739
15742
|
};
|
|
15740
15743
|
}
|
|
15744
|
+
function pendingEvidenceItems(evidence) {
|
|
15745
|
+
const items = evidence.checks.filter((check) => check.state === "pending").map((check) => `${check.name} (${check.status ?? "unknown"}${check.conclusion ? `/${check.conclusion}` : ""}, startedAt=${check.startedAt ?? "null"}, appId=${check.appId ?? "null"})`);
|
|
15746
|
+
for (const run of evidence.workflows) {
|
|
15747
|
+
if (run.status !== "completed") items.push(`${run.name}#${run.id} (${run.status})`);
|
|
15748
|
+
}
|
|
15749
|
+
return items;
|
|
15750
|
+
}
|
|
15741
15751
|
async function waitForPrChecks(deps) {
|
|
15742
15752
|
let { policy, reason } = await deps.resolvePolicy();
|
|
15743
15753
|
const baseBranch = deps.baseBranch ?? "development";
|
|
@@ -15768,10 +15778,10 @@ async function waitForPrChecks(deps) {
|
|
|
15768
15778
|
const timeoutMs = deps.timeoutMs ?? PR_CHECKS_TIMEOUT_MS;
|
|
15769
15779
|
const started = now();
|
|
15770
15780
|
const deadline = started + timeoutMs;
|
|
15771
|
-
const report = (state) => {
|
|
15781
|
+
const report = (state, detail) => {
|
|
15772
15782
|
try {
|
|
15773
15783
|
const elapsedMs = now() - started;
|
|
15774
|
-
deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()) });
|
|
15784
|
+
deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()), ...detail === void 0 ? {} : { detail } });
|
|
15775
15785
|
} catch {
|
|
15776
15786
|
}
|
|
15777
15787
|
};
|
|
@@ -15781,6 +15791,10 @@ async function waitForPrChecks(deps) {
|
|
|
15781
15791
|
let zeroRunsStreak = 0;
|
|
15782
15792
|
let evidenceHeadSha;
|
|
15783
15793
|
let lastEvidence;
|
|
15794
|
+
const pendingWaitDetail = (evidence) => {
|
|
15795
|
+
const items = pendingEvidenceItems(evidence);
|
|
15796
|
+
return items.length ? `pending (${items.join(", ")})` : void 0;
|
|
15797
|
+
};
|
|
15784
15798
|
report("starting");
|
|
15785
15799
|
while (now() < deadline) {
|
|
15786
15800
|
if (deps.pollRateLimit) {
|
|
@@ -15845,7 +15859,7 @@ async function waitForPrChecks(deps) {
|
|
|
15845
15859
|
await deps.sleep(waitMs);
|
|
15846
15860
|
continue;
|
|
15847
15861
|
}
|
|
15848
|
-
report(state);
|
|
15862
|
+
report(state, state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0);
|
|
15849
15863
|
if (state !== "success") successStreak = 0;
|
|
15850
15864
|
if (state !== "failure") failureStreak = 0;
|
|
15851
15865
|
if (state !== "no-checks-reported") zeroRunsStreak = 0;
|
|
@@ -15921,10 +15935,18 @@ async function waitForPrChecks(deps) {
|
|
|
15921
15935
|
await deps.sleep(pollIntervalMs(now() - started));
|
|
15922
15936
|
continue;
|
|
15923
15937
|
}
|
|
15924
|
-
|
|
15938
|
+
const pendingDetail = state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0;
|
|
15939
|
+
lastDetail = pendingDetail ?? state;
|
|
15925
15940
|
await deps.sleep(pollIntervalMs(now() - started));
|
|
15926
15941
|
}
|
|
15927
|
-
return {
|
|
15942
|
+
return {
|
|
15943
|
+
policy,
|
|
15944
|
+
status: "timeout",
|
|
15945
|
+
reason,
|
|
15946
|
+
detail: lastDetail,
|
|
15947
|
+
waitedMs: now() - started,
|
|
15948
|
+
...lastEvidence ? { headSha: lastEvidence.headSha, checks: lastEvidence.checks, workflowRuns: lastEvidence.workflows } : {}
|
|
15949
|
+
};
|
|
15928
15950
|
}
|
|
15929
15951
|
|
|
15930
15952
|
// src/release-auxiliary-workflows.ts
|
|
@@ -16394,16 +16416,23 @@ async function fetchHeadCheckRuns(headSha, repo, gh2) {
|
|
|
16394
16416
|
const runsOut = await gh2(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id, details_url, started_at, completed_at}"]);
|
|
16395
16417
|
return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
|
|
16396
16418
|
}
|
|
16397
|
-
async function
|
|
16419
|
+
async function fetchHeadCheckEntriesWithRuns(headSha, repo, gh2) {
|
|
16398
16420
|
const [runs2, statusesOut] = await Promise.all([
|
|
16399
16421
|
fetchHeadCheckRuns(headSha, repo, gh2),
|
|
16400
16422
|
gh2(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state, target_url, description}"])
|
|
16401
16423
|
]);
|
|
16402
16424
|
const statuses = parseNdjsonLines(statusesOut);
|
|
16403
|
-
return
|
|
16404
|
-
|
|
16405
|
-
|
|
16406
|
-
|
|
16425
|
+
return {
|
|
16426
|
+
runs: runs2,
|
|
16427
|
+
statuses,
|
|
16428
|
+
entries: [
|
|
16429
|
+
...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).map((run) => ({ name: run.name ?? `check-run ${run.id ?? "unknown"}`, bucket: classifyCheckRun(run) })),
|
|
16430
|
+
...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", bucket: classifyCommitStatus(s.state) }))
|
|
16431
|
+
]
|
|
16432
|
+
};
|
|
16433
|
+
}
|
|
16434
|
+
async function fetchHeadCheckEntries(headSha, repo, gh2) {
|
|
16435
|
+
return (await fetchHeadCheckEntriesWithRuns(headSha, repo, gh2)).entries;
|
|
16407
16436
|
}
|
|
16408
16437
|
async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredContexts) {
|
|
16409
16438
|
const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
|
|
@@ -16411,38 +16440,68 @@ async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredCont
|
|
|
16411
16440
|
const { relevant } = partitionByRequired(entries, requiredContexts);
|
|
16412
16441
|
return derivePollState(relevant);
|
|
16413
16442
|
}
|
|
16414
|
-
async function
|
|
16415
|
-
const
|
|
16416
|
-
|
|
16417
|
-
|
|
16418
|
-
gh2(["--paginate", `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(snapshot.headSha)}&per_page=100`, "--jq", ".workflow_runs[] | {id, name, head_sha, status, conclusion, html_url}"])
|
|
16443
|
+
async function pollRestHeadChecksWithWorkflowEvidence(headSha, repo, gh2) {
|
|
16444
|
+
const [{ runs: runs2, statuses }, rawRuns] = await Promise.all([
|
|
16445
|
+
fetchHeadCheckEntriesWithRuns(headSha, repo, gh2),
|
|
16446
|
+
gh2(["--paginate", `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&per_page=100`, "--jq", ".workflow_runs[] | {id, name, head_sha, status, conclusion, html_url}"])
|
|
16419
16447
|
]);
|
|
16420
|
-
const checks = entries.filter((entry) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(entry.name)).map((entry) => ({ name: entry.name, state: entry.bucket }));
|
|
16421
16448
|
const workflows = parseNdjsonLines(rawRuns).flatMap((run) => {
|
|
16422
|
-
if (run.head_sha !==
|
|
16449
|
+
if (run.head_sha !== headSha || typeof run.id !== "number" || typeof run.name !== "string" || typeof run.status !== "string") return [];
|
|
16423
16450
|
if (AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name)) return [];
|
|
16424
16451
|
return [{
|
|
16425
16452
|
id: run.id,
|
|
16426
16453
|
name: run.name,
|
|
16427
|
-
headSha
|
|
16454
|
+
headSha,
|
|
16428
16455
|
status: run.status,
|
|
16429
16456
|
conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
|
|
16430
16457
|
...typeof run.html_url === "string" ? { url: run.html_url } : {}
|
|
16431
16458
|
}];
|
|
16432
16459
|
});
|
|
16433
|
-
const
|
|
16460
|
+
const workflowById = new Map(workflows.map((run) => [run.id, run]));
|
|
16461
|
+
const isStalePendingCheckRun = (run) => {
|
|
16462
|
+
if (classifyCheckRun(run) !== "pending") return false;
|
|
16463
|
+
const match = /\/actions\/runs\/(\d+)(?:\/|$)/.exec(run.details_url ?? "");
|
|
16464
|
+
const owning = match ? workflowById.get(Number(match[1])) : void 0;
|
|
16465
|
+
if (!owning) return false;
|
|
16466
|
+
if (owning.status === "completed") return true;
|
|
16467
|
+
return workflows.some((newer) => newer.id > owning.id && newer.name === owning.name && newer.status === "completed");
|
|
16468
|
+
};
|
|
16469
|
+
const checks = [
|
|
16470
|
+
...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).filter((run) => !isStalePendingCheckRun(run)).map((run) => ({
|
|
16471
|
+
name: run.name ?? `check-run ${run.id ?? "unknown"}`,
|
|
16472
|
+
state: classifyCheckRun(run),
|
|
16473
|
+
...run.status !== void 0 ? { status: run.status } : {},
|
|
16474
|
+
conclusion: run.conclusion ?? null,
|
|
16475
|
+
startedAt: run.started_at ?? null,
|
|
16476
|
+
appId: run.app_id ?? null
|
|
16477
|
+
})),
|
|
16478
|
+
...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", state: classifyCommitStatus(s.state) }))
|
|
16479
|
+
];
|
|
16480
|
+
const newestRunByName = /* @__PURE__ */ new Map();
|
|
16481
|
+
for (const run of workflows) {
|
|
16482
|
+
const current = newestRunByName.get(run.name);
|
|
16483
|
+
if (!current || run.id > current.id) newestRunByName.set(run.name, run);
|
|
16484
|
+
}
|
|
16485
|
+
const workflowBuckets = [...newestRunByName.values()].map((run) => {
|
|
16434
16486
|
if (run.status !== "completed") return "pending";
|
|
16435
16487
|
return run.conclusion === "success" || run.conclusion === "neutral" || run.conclusion === "skipped" ? "pass" : "fail";
|
|
16436
16488
|
});
|
|
16437
16489
|
return {
|
|
16438
16490
|
state: derivePollState([...checks.map((check) => check.state), ...workflowBuckets]),
|
|
16439
|
-
headSha
|
|
16491
|
+
headSha,
|
|
16440
16492
|
checks,
|
|
16441
16493
|
workflows
|
|
16442
16494
|
};
|
|
16443
16495
|
}
|
|
16496
|
+
async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaultGhApi) {
|
|
16497
|
+
const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
|
|
16498
|
+
return pollRestHeadChecksWithWorkflowEvidence(snapshot.headSha, repo, gh2);
|
|
16499
|
+
}
|
|
16500
|
+
async function pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2 = defaultGhApi) {
|
|
16501
|
+
return pollRestHeadChecksWithWorkflowEvidence(sha, repo, gh2);
|
|
16502
|
+
}
|
|
16444
16503
|
async function pollRestCommitChecks(sha, repo, gh2 = defaultGhApi) {
|
|
16445
|
-
return
|
|
16504
|
+
return (await pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2)).state;
|
|
16446
16505
|
}
|
|
16447
16506
|
var GH_ZERO_TIME = "0001-01-01T00:00:00Z";
|
|
16448
16507
|
var GH_PR_CHECKS_JSON_FIELDS = ["bucket", "completedAt", "description", "link", "name", "startedAt", "state"];
|
|
@@ -21018,8 +21077,9 @@ function resolveSpawnTarget(command, args, platform2 = process.platform) {
|
|
|
21018
21077
|
if (platform2 !== "win32") return { cmd: command, args };
|
|
21019
21078
|
for (const t of [command, ...args]) {
|
|
21020
21079
|
if (/[\r\n]/.test(t)) {
|
|
21080
|
+
const preview = JSON.stringify(t.length > 80 ? `${t.slice(0, 77)}...` : t);
|
|
21021
21081
|
throw new Error(
|
|
21022
|
-
`cannot spawn safely on Windows: cmd.exe truncates at a newline, even quoted: ${
|
|
21082
|
+
`cannot spawn safely on Windows: cmd.exe truncates at a newline, even quoted: ${preview} \u2014 write the multiline script to a git-ignored file and run \`mmi-cli vault secrets use <KEY> -- node .jerv/tmp/<script>.mjs\` (#6048)`
|
|
21023
21083
|
);
|
|
21024
21084
|
}
|
|
21025
21085
|
}
|
|
@@ -22319,8 +22379,22 @@ function renderBoardItem(item) {
|
|
|
22319
22379
|
}
|
|
22320
22380
|
return lines2.join("\n");
|
|
22321
22381
|
}
|
|
22382
|
+
function renderCachedBoardSource(report) {
|
|
22383
|
+
if (typeof report.stale?.ageMs === "number") {
|
|
22384
|
+
const ageSeconds = Math.floor(report.stale.ageMs / 1e3);
|
|
22385
|
+
return `source: cache \xB7 built ${report.builtAt ? `${report.builtAt} (${ageSeconds}s old)` : `${ageSeconds}s ago`} \xB7 stale`;
|
|
22386
|
+
}
|
|
22387
|
+
if (report.builtAt) {
|
|
22388
|
+
const builtMs = Date.parse(report.builtAt);
|
|
22389
|
+
if (Number.isFinite(builtMs)) {
|
|
22390
|
+
return `source: cache \xB7 built ${report.builtAt} (${Math.max(0, Math.floor((Date.now() - builtMs) / 1e3))}s old) \xB7 fresh`;
|
|
22391
|
+
}
|
|
22392
|
+
return `source: cache \xB7 built ${report.builtAt} \xB7 fresh`;
|
|
22393
|
+
}
|
|
22394
|
+
return "source: cache";
|
|
22395
|
+
}
|
|
22322
22396
|
function renderBoardReport(report) {
|
|
22323
|
-
const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ?
|
|
22397
|
+
const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ? renderCachedBoardSource(report) : renderBoardSource()];
|
|
22324
22398
|
if (report.github?.rateLimit) {
|
|
22325
22399
|
const { limit, remaining, cost, resetAt } = report.github.rateLimit;
|
|
22326
22400
|
lines2.push(`github: app installation \xB7 GraphQL ${remaining}/${limit} remaining \xB7 cost ${cost} \xB7 resets ${resetAt}`);
|
|
@@ -23576,7 +23650,7 @@ var contestedBoardReadCommand = (repo) => `mmi-cli oracle board read --json${rep
|
|
|
23576
23650
|
function contestedClaimRecovery(ref) {
|
|
23577
23651
|
const repo = /^([^/\s#]+\/[^/\s#]+)#\d+$/.exec(ref)?.[1];
|
|
23578
23652
|
const boardRead = contestedBoardReadCommand(repo);
|
|
23579
|
-
return `${CONTESTED_CLAIM_RECOVERY}; recovery order: refresh the board read (${boardRead}), then choose an item from its claimable list, or coordinate a handoff with the holder before using --force
|
|
23653
|
+
return `${CONTESTED_CLAIM_RECOVERY}; recovery order: refresh the board read (${boardRead}), then choose an item from its claimable list, or coordinate a handoff with the holder before using --force`;
|
|
23580
23654
|
}
|
|
23581
23655
|
function laneContestMessage(ref, contest, verb) {
|
|
23582
23656
|
const { holder, why } = laneContestParts(contest);
|
|
@@ -23594,6 +23668,7 @@ function ghError(e) {
|
|
|
23594
23668
|
init_github_client();
|
|
23595
23669
|
init_github_quota();
|
|
23596
23670
|
var CLAIM_CONCURRENCY = 5;
|
|
23671
|
+
var VERIFIED_FREE_CANDIDATE_LIMIT = 5;
|
|
23597
23672
|
function withGatedWriteChainHint(message2) {
|
|
23598
23673
|
return `${message2}
|
|
23599
23674
|
note: run refused writes separately or join dependent commands with '&&'; a ';' chain continues after failure`;
|
|
@@ -23854,9 +23929,16 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23854
23929
|
const { holder: holder2, why } = laneContestParts(contest);
|
|
23855
23930
|
throw new Error(isUnknownHold(contest) ? `${item.ref} cannot be verified as free: ${holder2} (${why}); the liveness reads failed so the claim is refused fail-closed \u2014 ${CONTESTED_CLAIM_RECOVERY}` : `${item.ref} is already claimed by ${holder2} with live evidence of active work (${why}); ${CONTESTED_CLAIM_RECOVERY}`);
|
|
23856
23931
|
}
|
|
23857
|
-
const
|
|
23858
|
-
const
|
|
23859
|
-
const
|
|
23932
|
+
const candidates = [...report.primary.claimable, ...report.secondary.claimable].filter((candidate) => candidate.contentType === "Issue" && candidate.state === "OPEN" && !candidate.claim?.currentlyClaimed && !ctx.selectedRefs.has(candidate.ref.toLowerCase())).slice(0, VERIFIED_FREE_CANDIDATE_LIMIT);
|
|
23933
|
+
const verifiedFreeRefs = [];
|
|
23934
|
+
for (const candidate of candidates) {
|
|
23935
|
+
const candidateContest = await checkLaneContest(client, candidate, ctx.session, report.viewer, ctx.appClient, ctx.landing).catch(() => void 0);
|
|
23936
|
+
if (candidateContest && !candidateContest.contested && candidateContest.unverifiable.length === 0) {
|
|
23937
|
+
verifiedFreeRefs.push(candidate.ref);
|
|
23938
|
+
}
|
|
23939
|
+
}
|
|
23940
|
+
const alternatives = verifiedFreeRefs.length ? `Next verified free item: \`mmi-cli oracle board claim ${verifiedFreeRefs[0]}\`${verifiedFreeRefs.length > 1 ? ` (also free: ${verifiedFreeRefs.slice(1).join(", ")})` : ""}` : `Verified free items in this read: none \u2014 refresh \`mmi-cli oracle board read --json --repo ${report.repo}\` or coordinate a handoff`;
|
|
23941
|
+
const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}`;
|
|
23860
23942
|
throw new Error(withGatedWriteChainHint(refusal));
|
|
23861
23943
|
}
|
|
23862
23944
|
previousHolder = displaced();
|
|
@@ -27513,10 +27595,10 @@ var rollout_plan_default = {
|
|
|
27513
27595
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
27514
27596
|
},
|
|
27515
27597
|
baseline: {
|
|
27516
|
-
version: "4.6.
|
|
27517
|
-
tag: "v4.6.
|
|
27518
|
-
commit: "
|
|
27519
|
-
npm: "@mutmutco/cli@4.6.
|
|
27598
|
+
version: "4.6.10",
|
|
27599
|
+
tag: "v4.6.10",
|
|
27600
|
+
commit: "a4bbe43232e9",
|
|
27601
|
+
npm: "@mutmutco/cli@4.6.10"
|
|
27520
27602
|
},
|
|
27521
27603
|
exitCriterion: "fleet-n-of-n",
|
|
27522
27604
|
hubOnlyShortcut: "forbidden",
|
|
@@ -27533,14 +27615,14 @@ var rollout_plan_default = {
|
|
|
27533
27615
|
repo: "mutmutco/mmi-hub",
|
|
27534
27616
|
role: "canary",
|
|
27535
27617
|
schedule: "train",
|
|
27536
|
-
v3Target: "v4.6.
|
|
27618
|
+
v3Target: "v4.6.10"
|
|
27537
27619
|
}
|
|
27538
27620
|
],
|
|
27539
27621
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
27540
27622
|
rollback: {
|
|
27541
27623
|
independent: true,
|
|
27542
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
27543
|
-
v3Target: "v4.6.
|
|
27624
|
+
mechanism: "npm dist-tag latest -> 4.6.10 and redeploy the Hub Lambda from tag v4.6.10 (a4bbe43232e9); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
27625
|
+
v3Target: "v4.6.10 (@mutmutco/cli@4.6.10, tag commit a4bbe43232e9 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
27544
27626
|
}
|
|
27545
27627
|
},
|
|
27546
27628
|
{
|
|
@@ -41262,7 +41344,18 @@ async function readGhPrStateWithRetry(fetchState, options) {
|
|
|
41262
41344
|
}
|
|
41263
41345
|
return { ok: false, error: lastError };
|
|
41264
41346
|
}
|
|
41347
|
+
function isNoFailedCheckQueueEjection(run) {
|
|
41348
|
+
return !run || run.conclusion === null || run.conclusion === "cancelled" || run.conclusion === "neutral" || run.conclusion === "skipped";
|
|
41349
|
+
}
|
|
41265
41350
|
async function runPrLand(prNumber, options, deps) {
|
|
41351
|
+
const first = await runPrLandOnce(prNumber, options, deps);
|
|
41352
|
+
if (options.enqueueOnly || options.expectHead || first.status !== "failed") return first;
|
|
41353
|
+
const queue = first.mergeQueue;
|
|
41354
|
+
const nothingFailed = queue?.state === "timeout" || queue?.state === "ejected" && isNoFailedCheckQueueEjection(queue.ejectedRun);
|
|
41355
|
+
if (!nothingFailed) return first;
|
|
41356
|
+
return { ...await runPrLandOnce(prNumber, options, deps), retriedAfterEjection: true };
|
|
41357
|
+
}
|
|
41358
|
+
async function runPrLandOnce(prNumber, options, deps) {
|
|
41266
41359
|
const resolved = await deps.resolveRepo(prNumber, options.repo);
|
|
41267
41360
|
const { repo } = resolved;
|
|
41268
41361
|
const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
|
|
@@ -41308,6 +41401,18 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
41308
41401
|
base.mergeStatus = "auto-merge-enqueued";
|
|
41309
41402
|
return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
|
|
41310
41403
|
}
|
|
41404
|
+
if (deps.preflightQueue) {
|
|
41405
|
+
const preflight2 = await deps.preflightQueue(prNumber, repo, pinned);
|
|
41406
|
+
if (preflight2 && preflight2.mergeStatus === "failed") {
|
|
41407
|
+
const rawError = preflight2.error ?? "queue preparation needed";
|
|
41408
|
+
return {
|
|
41409
|
+
...base,
|
|
41410
|
+
mergeStatus: "failed",
|
|
41411
|
+
...preflight2.preparation ? { preparation: preflight2.preparation } : {},
|
|
41412
|
+
error: !rawError.includes("read pr queue") ? `${rawError} \u2014 ${prQueueReadHint(prNumber, repo)}` : rawError
|
|
41413
|
+
};
|
|
41414
|
+
}
|
|
41415
|
+
}
|
|
41311
41416
|
}
|
|
41312
41417
|
const checksWaitError = (checksWait) => {
|
|
41313
41418
|
if (checksWait.status === "conflicting") {
|
|
@@ -41468,7 +41573,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
41468
41573
|
...base,
|
|
41469
41574
|
mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
|
|
41470
41575
|
...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
|
|
41471
|
-
error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}; ${prQueueReadHint(pr, base.repo)}`
|
|
41576
|
+
error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}; ${prQueueReadHint(pr, base.repo)}${isNoFailedCheckQueueEjection(run) ? `; no check failed \u2014 re-run \`mmi-cli devops pr land ${pr} --repo ${base.repo}\` once to re-enqueue, and if a land is refused again report the exact refusal verbatim (never fall back to a direct/admin merge or an \`MMI_ACTOR=personal\` landing workaround)` : ""}`
|
|
41472
41577
|
};
|
|
41473
41578
|
deps.queueProgress?.(result2);
|
|
41474
41579
|
return result2;
|
|
@@ -41478,7 +41583,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
41478
41583
|
const result = {
|
|
41479
41584
|
...base,
|
|
41480
41585
|
mergeQueue: { ...base.mergeQueue, state: "timeout" },
|
|
41481
|
-
error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 ${prQueueReadHint(pr, base.repo)} and rerun pr land to resume
|
|
41586
|
+
error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 ${prQueueReadHint(pr, base.repo)} and rerun \`mmi-cli devops pr land ${pr} --repo ${base.repo}\` once to resume/re-enqueue; if it still does not land, report the exact refusal verbatim (never bypass the ruleset or fall back to a direct/admin or \`MMI_ACTOR=personal\` merge)`
|
|
41482
41587
|
};
|
|
41483
41588
|
deps.queueProgress?.(result);
|
|
41484
41589
|
return result;
|
|
@@ -41498,7 +41603,7 @@ async function landMany(prs, landOne, cleanup, hooks) {
|
|
|
41498
41603
|
const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
|
|
41499
41604
|
return Promise.all(prs.map(async (pr) => {
|
|
41500
41605
|
let result = await safe(pr);
|
|
41501
|
-
if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
|
|
41606
|
+
if (prLandOutcome(result) === "ejected" && !result.pinnedHead && !result.retriedAfterEjection) {
|
|
41502
41607
|
const baseRed = await hooks?.baseRedCheck?.(pr, result).catch(() => void 0);
|
|
41503
41608
|
if (baseRed) {
|
|
41504
41609
|
result = { ...result, baseRed: { base: result.base, baseRunId: baseRed.baseRunId }, error: `base-red: failures already on ${result.base} (${baseRed.baseRunId})` };
|
|
@@ -41670,10 +41775,16 @@ function trailerBlockBreak(body, line) {
|
|
|
41670
41775
|
const lines2 = String(body ?? "").split(/\r?\n/);
|
|
41671
41776
|
const at = lines2.findIndex((candidate) => candidate.trim() === line);
|
|
41672
41777
|
if (at === -1) return { after: "", before: "" };
|
|
41778
|
+
let sawBlank = false;
|
|
41673
41779
|
for (let i = at + 1; i < lines2.length; i += 1) {
|
|
41674
41780
|
const next = lines2[i];
|
|
41675
|
-
if (!next.trim())
|
|
41676
|
-
|
|
41781
|
+
if (!next.trim()) {
|
|
41782
|
+
sawBlank = true;
|
|
41783
|
+
continue;
|
|
41784
|
+
}
|
|
41785
|
+
if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) {
|
|
41786
|
+
return sawBlank ? { after: "", before: "", splitByBlankBefore: next.trim() } : { after: "", before: "" };
|
|
41787
|
+
}
|
|
41677
41788
|
return { after: next.trim(), before: "" };
|
|
41678
41789
|
}
|
|
41679
41790
|
let paraStart = at;
|
|
@@ -41693,7 +41804,10 @@ function clipParagraph(line) {
|
|
|
41693
41804
|
}
|
|
41694
41805
|
function invisibleTrailer(sha, line, body = "") {
|
|
41695
41806
|
const broken = trailerBlockBreak(body, line);
|
|
41696
|
-
const brokenDiagnostic = broken.
|
|
41807
|
+
const brokenDiagnostic = broken.splitByBlankBefore ? ` Broken by a blank line before the final trailer paragraph, so git only read the last paragraph as the trailer block:
|
|
41808
|
+
${clipParagraph(broken.splitByBlankBefore)}
|
|
41809
|
+
Keep every Key: value trailer (e.g. ${TRAILER_KEY} and Runbook-Unaffected) in one contiguous final paragraph with no blank line between them.
|
|
41810
|
+
` : broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
|
|
41697
41811
|
${clipParagraph(broken.after)}
|
|
41698
41812
|
` : broken.before ? ` Broken by the non-trailer line before the trailer-shaped line, which prevents git from recognizing the final block:
|
|
41699
41813
|
${clipParagraph(broken.before)}
|
|
@@ -41713,9 +41827,11 @@ function invisibleTrailer(sha, line, body = "") {
|
|
|
41713
41827
|
The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.
|
|
41714
41828
|
Diagnosis: git parsed no ${TRAILER_KEY} trailer for ${sha.slice(0, 8)} at all \u2014 read its own view first:
|
|
41715
41829
|
git log -1 --format='%(trailers)' ${sha.slice(0, 8)}
|
|
41716
|
-
` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message
|
|
41830
|
+
` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message. Run this
|
|
41831
|
+
yourself, right now, in this worktree; it is not a blocker to report and it needs no human:
|
|
41717
41832
|
git commit --allow-empty --trailer "${TRAILER_KEY}: <reason>"
|
|
41718
|
-
That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver
|
|
41833
|
+
That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.
|
|
41834
|
+
Re-run the policy receipt and the focused test command immediately after it.`
|
|
41719
41835
|
};
|
|
41720
41836
|
}
|
|
41721
41837
|
function unknownScope(sha, unknown) {
|
|
@@ -42846,9 +42962,7 @@ ${list}
|
|
|
42846
42962
|
Co-authored-by: GitHub <noreply@github.com>
|
|
42847
42963
|
`;
|
|
42848
42964
|
}
|
|
42849
|
-
async function
|
|
42850
|
-
if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
|
|
42851
|
-
pr = pr.replace(/^0+(?=\d)/, "");
|
|
42965
|
+
async function inspectQueuePreparation(pr, repo, options) {
|
|
42852
42966
|
const read = options.read ?? readApi;
|
|
42853
42967
|
const queueRead = options.read ?? actorReadApi;
|
|
42854
42968
|
const write = options.write ?? writeApi;
|
|
@@ -42860,14 +42974,14 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42860
42974
|
const pull = metadata.pullRequest;
|
|
42861
42975
|
if (!pull || !pull.id || !/^[a-f0-9]{40}$/i.test(pull.headRefOid)) throw new Error(`merge queue PR #${pr} identity unreadable`);
|
|
42862
42976
|
const head = options.expectedHeadOid ?? pull.headRefOid;
|
|
42863
|
-
if (head !== pull.headRefOid) return { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` };
|
|
42864
|
-
const stamp = (
|
|
42977
|
+
if (head !== pull.headRefOid) return { kind: "settled", result: { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` } };
|
|
42978
|
+
const stamp = (result, selected = false, intentId) => {
|
|
42865
42979
|
const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
|
|
42866
|
-
return credential === "unknown" ?
|
|
42980
|
+
return credential === "unknown" ? result : { ...result, credential };
|
|
42867
42981
|
};
|
|
42868
|
-
if (pull.state === "MERGED") return stamp({ mergeStatus: "merged" });
|
|
42869
|
-
if (pull.isInMergeQueue) return stamp({ mergeStatus: "auto-merge-enqueued" });
|
|
42870
|
-
if (pull.state !== "OPEN") return { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` };
|
|
42982
|
+
if (pull.state === "MERGED") return { kind: "settled", result: stamp({ mergeStatus: "merged" }) };
|
|
42983
|
+
if (pull.isInMergeQueue) return { kind: "settled", result: stamp({ mergeStatus: "auto-merge-enqueued" }) };
|
|
42984
|
+
if (pull.state !== "OPEN") return { kind: "settled", result: { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` } };
|
|
42871
42985
|
const closing = [...pull.closingIssuesReferences.nodes.map((node) => node.number)];
|
|
42872
42986
|
let page = pull.closingIssuesReferences.pageInfo;
|
|
42873
42987
|
while (page.hasNextPage) {
|
|
@@ -42900,7 +43014,7 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42900
43014
|
status: "preparation-needed",
|
|
42901
43015
|
reason,
|
|
42902
43016
|
issueTargets: targets.map((number) => `${repo}#${number}`),
|
|
42903
|
-
nextAction: reason === "queue-message" ?
|
|
43017
|
+
nextAction: reason === "queue-message" ? `PR author: if PR #${pr} is meant to close ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"}, add ${targets.length ? targets.map((number) => `\`Closes #${number}\``).join(", ") : "`Closes #N`"} to the PR body (\`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) and rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\` without pushing a new head; if ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"} must stay open, remove the closing phrases from the source commit or PR text using ${targets.length ? targets.map((number) => `\`Part of #${number}\``).join(", ") : "`Part of #N`"} / ${targets.length ? targets.map((number) => `\`Refs #${number}\``).join(", ") : "`Refs #N`"}, then re-run checks and \`pr land\` with the prepared head. A queue cannot accept --squash-body-file.` : "PR author: prepare the source commit messages and required trailers without a body override; re-run checks and pr land. GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy. The combined queue candidate must pass with the updated shared trailer reader. Keep repository settings unchanged."
|
|
42904
43018
|
},
|
|
42905
43019
|
error: `queue preparation needed: ${detail}`
|
|
42906
43020
|
});
|
|
@@ -42918,9 +43032,9 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42918
43032
|
);
|
|
42919
43033
|
method = legacy.mergeQueue?.configuration?.mergeMethod;
|
|
42920
43034
|
}
|
|
42921
|
-
if (!["SQUASH", "MERGE", "REBASE"].includes(method ?? "")) return preparation("queue-message-policy", [], "the effective queue merge method is unknown; no submission occurred");
|
|
43035
|
+
if (!["SQUASH", "MERGE", "REBASE"].includes(method ?? "")) return { kind: "refused", result: preparation("queue-message-policy", [], "the effective queue merge method is unknown; no submission occurred") };
|
|
42922
43036
|
if (method === "SQUASH" && (!["PR_TITLE", "COMMIT_OR_PR_TITLE"].includes(policy.title ?? "") || !["PR_BODY", "COMMIT_MESSAGES", "BLANK"].includes(policy.body ?? ""))) {
|
|
42923
|
-
return preparation("queue-message-policy", [], "the repository squash-message policy is unreadable; no submission occurred");
|
|
43037
|
+
return { kind: "refused", result: preparation("queue-message-policy", [], "the repository squash-message policy is unreadable; no submission occurred") };
|
|
42924
43038
|
}
|
|
42925
43039
|
const messages = parsed.commits;
|
|
42926
43040
|
const headline = policy.title === "COMMIT_OR_PR_TITLE" && messages.length === 1 ? messages[0].headline : pull.title;
|
|
@@ -42937,7 +43051,7 @@ ${body}`;
|
|
|
42937
43051
|
${message2.body}`).join("\n\n");
|
|
42938
43052
|
if (method === "MERGE") {
|
|
42939
43053
|
if (!["PR_TITLE", "MERGE_MESSAGE"].includes(repository.merge_commit_title ?? "") || !["PR_TITLE", "PR_BODY", "BLANK"].includes(repository.merge_commit_message ?? "")) {
|
|
42940
|
-
return preparation("queue-message-policy", [], "the repository merge-message policy is unreadable; no submission occurred");
|
|
43054
|
+
return { kind: "refused", result: preparation("queue-message-policy", [], "the repository merge-message policy is unreadable; no submission occurred") };
|
|
42941
43055
|
}
|
|
42942
43056
|
if (repository.merge_commit_title === "PR_TITLE" || repository.merge_commit_message === "PR_TITLE") actualMessage += `
|
|
42943
43057
|
|
|
@@ -42959,19 +43073,22 @@ ${pull.body}`;
|
|
|
42959
43073
|
...findNegatedClosings(input.text, closing),
|
|
42960
43074
|
...findCrossRepoAmbiguousClosings(input.text, closing, repo)
|
|
42961
43075
|
])];
|
|
42962
|
-
return preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${verdict.message}
|
|
43076
|
+
return { kind: "refused", result: preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${closingGuardDiagnostic(verdict.message)} See preparation.nextAction for the exact \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\` remedy.`) };
|
|
42963
43077
|
}
|
|
42964
43078
|
if (method === "SQUASH" && policy.body === "COMMIT_MESSAGES" && messages.length > 1 && options.bodyText == null && messages.some((message2) => /^\s*Test-Policy-Override\s*:/im.test(`${message2.headline}
|
|
42965
43079
|
${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title, pr, messages), process.cwd())) {
|
|
42966
43080
|
return {
|
|
42967
|
-
|
|
42968
|
-
|
|
42969
|
-
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
|
|
42973
|
-
|
|
42974
|
-
|
|
43081
|
+
kind: "refused",
|
|
43082
|
+
result: {
|
|
43083
|
+
mergeStatus: "failed",
|
|
43084
|
+
preparation: {
|
|
43085
|
+
status: "preparation-needed",
|
|
43086
|
+
reason: "queue-message-override",
|
|
43087
|
+
issueTargets: [],
|
|
43088
|
+
nextAction: "PR author: this repository squashes with COMMIT_MESSAGES, and GitHub composes a multi-commit squash as a list of every commit message followed by its own co-author block, so this Test-Policy-Override trailer would not be in the final trailer block the queue check reads. Put the change in ONE commit whose message ends with a trailer block carrying the waiver (no force-push: push a new branch whose single commit has the same tree, parented on the PR base; open it as a new PR and close this one), or make the waiver the final trailer block of the LAST commit, then rerun pr land. Keep repository settings unchanged."
|
|
43089
|
+
},
|
|
43090
|
+
error: "queue preparation needed: the multi-commit COMMIT_MESSAGES squash would bury the Test-Policy-Override waiver mid-message, so the queue check would fail malformed-override-trailer"
|
|
43091
|
+
}
|
|
42975
43092
|
};
|
|
42976
43093
|
}
|
|
42977
43094
|
const requiredBody = options.bodyText ?? composedBody;
|
|
@@ -42979,18 +43096,38 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
|
|
|
42979
43096
|
if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
|
|
42980
43097
|
if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
|
|
42981
43098
|
return {
|
|
42982
|
-
|
|
42983
|
-
|
|
42984
|
-
|
|
42985
|
-
|
|
42986
|
-
|
|
42987
|
-
|
|
42988
|
-
|
|
42989
|
-
|
|
43099
|
+
kind: "refused",
|
|
43100
|
+
result: {
|
|
43101
|
+
mergeStatus: "failed",
|
|
43102
|
+
preparation: {
|
|
43103
|
+
status: "preparation-needed",
|
|
43104
|
+
reason: "queue-message-override",
|
|
43105
|
+
issueTargets: [],
|
|
43106
|
+
nextAction: `PR author: this repository squashes with PR_BODY, so place the required Test-Policy-Override trailer(s) in the PR body final trailer block (e.g. \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) without a body override, then rerun \`pr land\`. Keep repository settings unchanged.`
|
|
43107
|
+
},
|
|
43108
|
+
error: "queue preparation needed: repository squash policy is PR_BODY, and the PR body does not carry the required Test-Policy-Override trailer(s) in its final Git-recognized trailer block"
|
|
43109
|
+
}
|
|
42990
43110
|
};
|
|
42991
43111
|
}
|
|
42992
|
-
return preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission");
|
|
43112
|
+
return { kind: "refused", result: preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission") };
|
|
42993
43113
|
}
|
|
43114
|
+
return { kind: "ready", head, pull, read, queueRead, write, ...verdict.message ? { warning: verdict.message } : {} };
|
|
43115
|
+
}
|
|
43116
|
+
async function preflightPrMergeQueue(prNumber, repo, options = {}) {
|
|
43117
|
+
if (!/^\d+$/.test(prNumber)) return void 0;
|
|
43118
|
+
const inspected = await inspectQueuePreparation(prNumber.replace(/^0+(?=\d)/, ""), repo, options);
|
|
43119
|
+
return inspected.kind === "refused" ? inspected.result : void 0;
|
|
43120
|
+
}
|
|
43121
|
+
async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
43122
|
+
if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
|
|
43123
|
+
pr = pr.replace(/^0+(?=\d)/, "");
|
|
43124
|
+
const inspected = await inspectQueuePreparation(pr, repo, options);
|
|
43125
|
+
if (inspected.kind !== "ready") return inspected.result;
|
|
43126
|
+
const { head, pull, read, queueRead, write } = inspected;
|
|
43127
|
+
const stamp = (result2, selected = false, intentId) => {
|
|
43128
|
+
const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
|
|
43129
|
+
return credential === "unknown" ? result2 : { ...result2, credential };
|
|
43130
|
+
};
|
|
42994
43131
|
const current = await queueGraphql(
|
|
42995
43132
|
repo,
|
|
42996
43133
|
`pullRequest(number:${pr}) { headRefOid title body }`,
|
|
@@ -43001,7 +43138,12 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
|
|
|
43001
43138
|
}
|
|
43002
43139
|
const selection = { selected: false };
|
|
43003
43140
|
const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
|
|
43004
|
-
return { ...stamp(result, selection.selected, selection.intentId), ...
|
|
43141
|
+
return { ...stamp(result, selection.selected, selection.intentId), ...inspected.warning ? { warning: inspected.warning } : {} };
|
|
43142
|
+
}
|
|
43143
|
+
function closingGuardDiagnostic(message2) {
|
|
43144
|
+
if (!message2) return "";
|
|
43145
|
+
const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
|
|
43146
|
+
return (remedy === -1 ? message2 : message2.slice(0, remedy)).trim();
|
|
43005
43147
|
}
|
|
43006
43148
|
function decideQueueMergeRoute(hasQueue, method, headRef) {
|
|
43007
43149
|
if (!hasQueue) return "normal";
|
|
@@ -43178,8 +43320,10 @@ function prMergeQueueLine(result) {
|
|
|
43178
43320
|
return `pr land: PR #${result.pr} queued on ${queue.base}${queue.position === void 0 ? "" : ` (position ${queue.position})`}`;
|
|
43179
43321
|
case "merged":
|
|
43180
43322
|
return `pr land: PR #${result.pr} merged via merge queue`;
|
|
43181
|
-
case "ejected":
|
|
43182
|
-
|
|
43323
|
+
case "ejected": {
|
|
43324
|
+
const line = `pr land: PR #${result.pr} ejected from merge queue \u2014 ${queue.ejectedRun ? `merge_group run ${queue.ejectedRun.id} ${queue.ejectedRun.conclusion} ${queue.ejectedRun.url}` : "merge_group run not found"}`;
|
|
43325
|
+
return isNoFailedCheckQueueEjection(queue.ejectedRun) ? `${line} \u2014 no check failed; re-run \`mmi-cli devops pr land ${result.pr}${result.repo ? ` --repo ${result.repo}` : ""}\` once to re-enqueue, and report any further refusal verbatim (never fall back to a direct/admin merge or \`MMI_ACTOR=personal\` workaround)` : line;
|
|
43326
|
+
}
|
|
43183
43327
|
case "timeout":
|
|
43184
43328
|
return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
|
|
43185
43329
|
}
|
|
@@ -43697,9 +43841,27 @@ function refuseNotClaimable(e, json) {
|
|
|
43697
43841
|
process.exitCode = 1;
|
|
43698
43842
|
return true;
|
|
43699
43843
|
}
|
|
43700
|
-
var BOARD_READ_HELP = "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,title,status,assignees,labels,bucket}.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n";
|
|
43844
|
+
var BOARD_READ_HELP = "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n(#9595: claim is the parsed mmi-claim evidence object, null when unclaimed; openPullRequests lists the OPEN\n PRs whose closing-issue body links name the item. On the FULL JSON object that list lives at\n `report.pullRequests` \u2014 there is no top-level `openPullRequests`, and #9596 hand-written projections\n that assume one crash on `.filter`.)\n\nA cached snapshot read renders `source: cache \xB7 built <timestamp> \xB7 fresh|stale` (#9584) and carries\n`builtAt` under --json, so a same-turn re-read of the same builtAt buys nothing new.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n";
|
|
43701
43845
|
function flattenBoardReport(report) {
|
|
43702
|
-
|
|
43846
|
+
const closingByRef = /* @__PURE__ */ new Map();
|
|
43847
|
+
for (const pull of report.pullRequests ?? []) {
|
|
43848
|
+
for (const closes of pull.closesIssues) {
|
|
43849
|
+
const rows = closingByRef.get(closes) ?? [];
|
|
43850
|
+
rows.push({ number: pull.number, url: pull.url });
|
|
43851
|
+
closingByRef.set(closes, rows);
|
|
43852
|
+
}
|
|
43853
|
+
}
|
|
43854
|
+
return [report.primary, report.secondary].flatMap((scope) => ["userOwned", "claimable", "taken", "unownedInFlight"].flatMap((bucket) => scope[bucket].map(({ ref, number, title, status, assignees, labels, claim }) => ({
|
|
43855
|
+
ref,
|
|
43856
|
+
number,
|
|
43857
|
+
title,
|
|
43858
|
+
status,
|
|
43859
|
+
assignees,
|
|
43860
|
+
labels,
|
|
43861
|
+
bucket,
|
|
43862
|
+
claim: claim ?? null,
|
|
43863
|
+
openPullRequests: closingByRef.get(ref) ?? []
|
|
43864
|
+
}))));
|
|
43703
43865
|
}
|
|
43704
43866
|
function registerBoardCommands(program3) {
|
|
43705
43867
|
function withDiscoverMissDetail(message2) {
|
|
@@ -54954,23 +55116,24 @@ async function closeIssue(client, options, deps = {}) {
|
|
|
54954
55116
|
if (options.evidence !== void 0) {
|
|
54955
55117
|
await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
|
|
54956
55118
|
}
|
|
55119
|
+
const appWriter = await selectClaimWriteClient(client, deps.appClient);
|
|
54957
55120
|
if (reason === "completed" && options.evidence !== void 0) {
|
|
54958
55121
|
const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
|
|
54959
55122
|
await ensureLabels([CLOSED_MERGED_LOOP_STATE_LABEL], repo);
|
|
54960
|
-
await
|
|
55123
|
+
await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, {
|
|
54961
55124
|
body: { labels: [CLOSED_MERGED_LOOP_STATE_LABEL] }
|
|
54962
55125
|
});
|
|
54963
55126
|
}
|
|
54964
|
-
await
|
|
55127
|
+
await appWriter.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
|
|
54965
55128
|
body: { state: "closed", state_reason: stateReason }
|
|
54966
55129
|
});
|
|
54967
55130
|
if (reason === "duplicate-of" && options.duplicateOf) {
|
|
54968
|
-
await
|
|
55131
|
+
await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
|
|
54969
55132
|
body: { body: `Duplicate of #${options.duplicateOf}` }
|
|
54970
55133
|
});
|
|
54971
55134
|
}
|
|
54972
55135
|
if (options.comment) {
|
|
54973
|
-
await
|
|
55136
|
+
await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
|
|
54974
55137
|
body: { body: options.comment }
|
|
54975
55138
|
});
|
|
54976
55139
|
}
|
|
@@ -59609,7 +59772,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59609
59772
|
timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
|
|
59610
59773
|
// Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
|
|
59611
59774
|
// an agent harness kills it on its own (shorter) deadline before the verdict ever prints (#2940).
|
|
59612
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
59775
|
+
progress: ({ state, elapsedMs, remainingMs, detail }) => console.warn(`pr checks-wait: ${detail && detail.startsWith("pending (") ? detail : state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
59613
59776
|
});
|
|
59614
59777
|
printChecksWaitResult("pr checks-wait", result, o.json);
|
|
59615
59778
|
});
|
|
@@ -59716,6 +59879,8 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59716
59879
|
readHeadSha: (prNumber, repo) => readPrLandPinnedHeadSha(prNumber, repo),
|
|
59717
59880
|
pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
|
|
59718
59881
|
hasMergeQueue: hasPrMergeQueue,
|
|
59882
|
+
// #9611: read-only preparation verdict BEFORE the check wait; the enqueue then re-reads and submits.
|
|
59883
|
+
preflightQueue: (prNumber, repo, expectedHeadOid) => preflightPrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly }),
|
|
59719
59884
|
enqueueQueue: async (prNumber, repo, expectedHeadOid) => {
|
|
59720
59885
|
const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly });
|
|
59721
59886
|
if (receipt.warning) console.warn(receipt.warning);
|
|
@@ -60381,6 +60546,9 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
60381
60546
|
const result = await waitForPrChecks({
|
|
60382
60547
|
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
60383
60548
|
pollChecks: () => pollRestCommitChecks(sha, repo),
|
|
60549
|
+
// #9589: the evidence-bearing read lets the wait name the rows holding it open and carry the
|
|
60550
|
+
// full check/workflow evidence on the timeout receipt.
|
|
60551
|
+
pollChecksWithEvidence: () => pollRestCommitChecksWithWorkflowEvidence(sha, repo),
|
|
60384
60552
|
pollRateLimit: () => waitLoopCorePool("ci checks-wait"),
|
|
60385
60553
|
checksUrl: `https://github.com/${repo}/commit/${sha}/checks`,
|
|
60386
60554
|
checksDiagnosticCommand: `gh api repos/${repo}/commits/${sha}/check-runs`,
|
|
@@ -60393,7 +60561,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
60393
60561
|
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
|
|
60394
60562
|
log: (message2) => console.warn(message2),
|
|
60395
60563
|
timeoutMs: timeoutMs ?? PR_CHECKS_TIMEOUT_MS,
|
|
60396
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`ci checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
60564
|
+
progress: ({ state, elapsedMs, remainingMs, detail }) => console.warn(`ci checks-wait: ${detail && detail.startsWith("pending (") ? detail : state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
60397
60565
|
});
|
|
60398
60566
|
printChecksWaitResult("ci checks-wait", result, o.json);
|
|
60399
60567
|
});
|
|
@@ -66245,7 +66413,7 @@ function typedReadHint(path2) {
|
|
|
66245
66413
|
if (!match) return "";
|
|
66246
66414
|
const [, owner, repo, kind, item, sub] = match;
|
|
66247
66415
|
const slug = `${owner}/${repo}`;
|
|
66248
|
-
const say = (command, note = "") => ` Typed read for this route:
|
|
66416
|
+
const say = (command, note = "") => ` Typed read for this route: \`MMI_ACTOR=app ${command}\`${note}.`;
|
|
66249
66417
|
if (kind === "issues") {
|
|
66250
66418
|
if (!item) return say(`mmi-cli devops read issue list --repo ${slug} --json number,title,state,labels`);
|
|
66251
66419
|
if (item.startsWith("comments/")) {
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
package/skills/board/SKILL.md
CHANGED
|
@@ -20,7 +20,10 @@ One call; the JSON carries `viewer`, `repo` and the project title. Use `primary`
|
|
|
20
20
|
`secondary` for other repos on the Project. Without `--flat`, `board read --json` returns an object
|
|
21
21
|
`{project, viewer, repo, primary, secondary, warnings, partial}` whose `primary` and `secondary` are bucket
|
|
22
22
|
objects `{userOwned, claimable, taken, unownedInFlight}` of item arrays (no top-level `items` array), so filter
|
|
23
|
-
a bucket (e.g. `report.primary.claimable`) or pass `--json --flat`, never `(j.items||j).filter`.
|
|
23
|
+
a bucket (e.g. `report.primary.claimable`) or pass `--json --flat`, never `(j.items||j).filter`. `--flat` rows
|
|
24
|
+
are `{ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}` (`claim` is the parsed mmi-claim
|
|
25
|
+
evidence object, null when unclaimed; `openPullRequests` lists the OPEN PRs whose closing-issue body links name
|
|
26
|
+
the item — on the full JSON object that list lives at `report.pullRequests`, never `openPullRequests`). Partial reads
|
|
24
27
|
exit nonzero; `--allow-partial` only when the dev accepts an incomplete board. The CLI partitions rows into
|
|
25
28
|
**Yours**, **Free to claim** and **Taken**; stale stamps (4 hours without work) hold nothing. Partition rules,
|
|
26
29
|
stamp details and `--out` rationale: [reference.md](reference.md).
|
|
@@ -62,7 +65,7 @@ Present the plan — batch count, issue count, one line per batch naming its sha
|
|
|
62
65
|
mmi-cli oracle board claim <ref> <ref> <ref> --json
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
Announce issue/PR counts; refresh `mmi-cli oracle board read` immediately before each claim (a list read earlier in the session may already be stale) and select ordinary claims from **Free to claim**. Read every result: failures exit nonzero. On conflict, do not blindly retry: stop that batch, never retry the same ref, refresh and reselect from the fresh **Free to claim** list, or coordinate a handoff; `--force` only for an owner-authorized intentional takeover. Run writes separately, never `;`; dependent follow-ups use `&&`.
|
|
68
|
+
Announce issue/PR counts; refresh `mmi-cli oracle board read` immediately before each claim (a list read earlier in the session may already be stale) and select ordinary claims from **Free to claim**. A cached read states its provenance — the render prints `source: cache · built <timestamp> · fresh|stale` and `--json` carries `builtAt` — so if a read in the same turn/window already returned a fresh snapshot and no board write has intervened since, re-reading the same `builtAt` snapshot buys no new information: reuse it instead of re-reading. Read every result: failures exit nonzero. On conflict, do not blindly retry: stop that batch, never retry the same ref, refresh and reselect from the fresh **Free to claim** list, or coordinate a handoff; `--force` only for an owner-authorized intentional takeover. Run writes separately, never `;`; dependent follow-ups use `&&`.
|
|
66
69
|
|
|
67
70
|
Run each `mmi-cli oracle issue create` as its own simple command. Any command whose text must carry quote
|
|
68
71
|
characters — an issue body, an inline `node -e` / `bash -c` one-liner — is written to a file, never spliced: a
|
|
@@ -12,7 +12,7 @@ Every refusal and every doctor blocker names its `code` and `anchor`: open the f
|
|
|
12
12
|
|
|
13
13
|
## Merge floor
|
|
14
14
|
|
|
15
|
-
- **Ordinary development PRs land with `mmi-cli devops pr land <number>`.** The rules below are for the train's own merges.
|
|
15
|
+
- **Ordinary development PRs land with `mmi-cli devops pr land <number>`.** If a merge-queue group leaves the queue with no failed check, `pr land` re-queues once (or rerun `mmi-cli devops pr land <number>` once); a refused land is a stop to report verbatim — never fall back to a hand/admin merge, actor override (`MMI_ACTOR=personal`), or interim workaround. The rules below are for the train's own merges.
|
|
16
16
|
- **Never squash-merge a tagged commit (#3167).** A squash re-mints the SHA and orphans the tag, so tag-anchored checks mis-read. Any merge that carries an already-tagged commit — the `main → development` roll-forward, an alignment PR, a hotfix fold PR — lands as a true merge: `mmi-cli devops pr merge <number> --auto --merge` (or `--wait --merge`); `--wait` alone still squashes. The hotfix main-base PR itself may squash: its tag lands after, on the merged `main` HEAD, and the `-x` trailer must survive the squash message.
|
|
17
17
|
- **A refused train is a stop, never a license to finish by hand.** Every fail-closed halt — doctor blocker, authority probe, coverage guard, untolerated conflict, required checks, stray tag, ledger — ends the run. Fix the cause on `development` through a normal PR and rerun the train; never hand-resolve on `main`, hand-cherry-pick, bare-push a train branch, ack to save time, or bypass a check.
|
|
18
18
|
- **Probe the latest Release tag, not branches or stale local tags (#2734).** Queue: `mmi-cli devops read pr queue <number> --repo <owner/repo> --json`; gh rejects `mergeQueueEntry`.
|