@mutmutco/codex-plugin 4.6.9 → 4.6.11
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/cli/dist/main.cjs
CHANGED
|
@@ -13912,6 +13912,15 @@ function issueCommentAppendOnlyRecovery(flag, canonicalPath, argv) {
|
|
|
13912
13912
|
corrected
|
|
13913
13913
|
};
|
|
13914
13914
|
}
|
|
13915
|
+
function trainPlanRedundantFlagRecovery(flag, canonicalPath, argv) {
|
|
13916
|
+
if (canonicalPath !== "devops release" && canonicalPath !== "devops rcand") return void 0;
|
|
13917
|
+
if (flag !== "--plan" && !flag.startsWith("--plan=")) return void 0;
|
|
13918
|
+
const kept = argv.filter((tok) => tok !== "--plan" && !tok.startsWith("--plan="));
|
|
13919
|
+
return {
|
|
13920
|
+
message: `unknown option '--plan' \u2014 \`${canonicalPath}\` runs the dry-run plan by default when \`--apply\` is absent; planning needs no flag`,
|
|
13921
|
+
corrected: `mmi-cli ${kept.join(" ")}`
|
|
13922
|
+
};
|
|
13923
|
+
}
|
|
13915
13924
|
var BOARD_CLAIM_OWN_FLAGS = ["--repo", "--for", "--force", "--check", "--allow-partial", "--json"];
|
|
13916
13925
|
function splitInlineSelectors(argv, flags) {
|
|
13917
13926
|
const out = [];
|
|
@@ -15350,6 +15359,9 @@ var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
|
|
|
15350
15359
|
// #9587: one named run rerun (all or failed jobs) — same actions:write profile, nothing broader.
|
|
15351
15360
|
["run rerun", { metadata: "read", actions: "write" }],
|
|
15352
15361
|
["issue comment", { metadata: "read", issues: "write", pull_requests: "write" }],
|
|
15362
|
+
// #9614: closing an issue is an issue-state write — it rides the org App (mmi-github-app), so the close
|
|
15363
|
+
// is attributed to the App instead of the personal account. Nothing broader than issues.
|
|
15364
|
+
["issue close", { metadata: "read", issues: "write" }],
|
|
15353
15365
|
// #8401: the detached related-issue lookup (issue list/view reads + one marker comment) — no identity contract.
|
|
15354
15366
|
["issue discover-related", { metadata: "read", issues: "write" }]
|
|
15355
15367
|
]);
|
|
@@ -15738,6 +15750,13 @@ function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none")
|
|
|
15738
15750
|
].join(" ")
|
|
15739
15751
|
};
|
|
15740
15752
|
}
|
|
15753
|
+
function pendingEvidenceItems(evidence) {
|
|
15754
|
+
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"})`);
|
|
15755
|
+
for (const run of evidence.workflows) {
|
|
15756
|
+
if (run.status !== "completed") items.push(`${run.name}#${run.id} (${run.status})`);
|
|
15757
|
+
}
|
|
15758
|
+
return items;
|
|
15759
|
+
}
|
|
15741
15760
|
async function waitForPrChecks(deps) {
|
|
15742
15761
|
let { policy, reason } = await deps.resolvePolicy();
|
|
15743
15762
|
const baseBranch = deps.baseBranch ?? "development";
|
|
@@ -15768,10 +15787,10 @@ async function waitForPrChecks(deps) {
|
|
|
15768
15787
|
const timeoutMs = deps.timeoutMs ?? PR_CHECKS_TIMEOUT_MS;
|
|
15769
15788
|
const started = now();
|
|
15770
15789
|
const deadline = started + timeoutMs;
|
|
15771
|
-
const report = (state) => {
|
|
15790
|
+
const report = (state, detail) => {
|
|
15772
15791
|
try {
|
|
15773
15792
|
const elapsedMs = now() - started;
|
|
15774
|
-
deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()) });
|
|
15793
|
+
deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()), ...detail === void 0 ? {} : { detail } });
|
|
15775
15794
|
} catch {
|
|
15776
15795
|
}
|
|
15777
15796
|
};
|
|
@@ -15781,6 +15800,10 @@ async function waitForPrChecks(deps) {
|
|
|
15781
15800
|
let zeroRunsStreak = 0;
|
|
15782
15801
|
let evidenceHeadSha;
|
|
15783
15802
|
let lastEvidence;
|
|
15803
|
+
const pendingWaitDetail = (evidence) => {
|
|
15804
|
+
const items = pendingEvidenceItems(evidence);
|
|
15805
|
+
return items.length ? `pending (${items.join(", ")})` : void 0;
|
|
15806
|
+
};
|
|
15784
15807
|
report("starting");
|
|
15785
15808
|
while (now() < deadline) {
|
|
15786
15809
|
if (deps.pollRateLimit) {
|
|
@@ -15845,7 +15868,7 @@ async function waitForPrChecks(deps) {
|
|
|
15845
15868
|
await deps.sleep(waitMs);
|
|
15846
15869
|
continue;
|
|
15847
15870
|
}
|
|
15848
|
-
report(state);
|
|
15871
|
+
report(state, state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0);
|
|
15849
15872
|
if (state !== "success") successStreak = 0;
|
|
15850
15873
|
if (state !== "failure") failureStreak = 0;
|
|
15851
15874
|
if (state !== "no-checks-reported") zeroRunsStreak = 0;
|
|
@@ -15921,10 +15944,18 @@ async function waitForPrChecks(deps) {
|
|
|
15921
15944
|
await deps.sleep(pollIntervalMs(now() - started));
|
|
15922
15945
|
continue;
|
|
15923
15946
|
}
|
|
15924
|
-
|
|
15947
|
+
const pendingDetail = state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0;
|
|
15948
|
+
lastDetail = pendingDetail ?? state;
|
|
15925
15949
|
await deps.sleep(pollIntervalMs(now() - started));
|
|
15926
15950
|
}
|
|
15927
|
-
return {
|
|
15951
|
+
return {
|
|
15952
|
+
policy,
|
|
15953
|
+
status: "timeout",
|
|
15954
|
+
reason,
|
|
15955
|
+
detail: lastDetail,
|
|
15956
|
+
waitedMs: now() - started,
|
|
15957
|
+
...lastEvidence ? { headSha: lastEvidence.headSha, checks: lastEvidence.checks, workflowRuns: lastEvidence.workflows } : {}
|
|
15958
|
+
};
|
|
15928
15959
|
}
|
|
15929
15960
|
|
|
15930
15961
|
// src/release-auxiliary-workflows.ts
|
|
@@ -16394,16 +16425,23 @@ async function fetchHeadCheckRuns(headSha, repo, gh2) {
|
|
|
16394
16425
|
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
16426
|
return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
|
|
16396
16427
|
}
|
|
16397
|
-
async function
|
|
16428
|
+
async function fetchHeadCheckEntriesWithRuns(headSha, repo, gh2) {
|
|
16398
16429
|
const [runs2, statusesOut] = await Promise.all([
|
|
16399
16430
|
fetchHeadCheckRuns(headSha, repo, gh2),
|
|
16400
16431
|
gh2(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state, target_url, description}"])
|
|
16401
16432
|
]);
|
|
16402
16433
|
const statuses = parseNdjsonLines(statusesOut);
|
|
16403
|
-
return
|
|
16404
|
-
|
|
16405
|
-
|
|
16406
|
-
|
|
16434
|
+
return {
|
|
16435
|
+
runs: runs2,
|
|
16436
|
+
statuses,
|
|
16437
|
+
entries: [
|
|
16438
|
+
...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).map((run) => ({ name: run.name ?? `check-run ${run.id ?? "unknown"}`, bucket: classifyCheckRun(run) })),
|
|
16439
|
+
...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", bucket: classifyCommitStatus(s.state) }))
|
|
16440
|
+
]
|
|
16441
|
+
};
|
|
16442
|
+
}
|
|
16443
|
+
async function fetchHeadCheckEntries(headSha, repo, gh2) {
|
|
16444
|
+
return (await fetchHeadCheckEntriesWithRuns(headSha, repo, gh2)).entries;
|
|
16407
16445
|
}
|
|
16408
16446
|
async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredContexts) {
|
|
16409
16447
|
const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
|
|
@@ -16411,38 +16449,68 @@ async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredCont
|
|
|
16411
16449
|
const { relevant } = partitionByRequired(entries, requiredContexts);
|
|
16412
16450
|
return derivePollState(relevant);
|
|
16413
16451
|
}
|
|
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}"])
|
|
16452
|
+
async function pollRestHeadChecksWithWorkflowEvidence(headSha, repo, gh2) {
|
|
16453
|
+
const [{ runs: runs2, statuses }, rawRuns] = await Promise.all([
|
|
16454
|
+
fetchHeadCheckEntriesWithRuns(headSha, repo, gh2),
|
|
16455
|
+
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
16456
|
]);
|
|
16420
|
-
const checks = entries.filter((entry) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(entry.name)).map((entry) => ({ name: entry.name, state: entry.bucket }));
|
|
16421
16457
|
const workflows = parseNdjsonLines(rawRuns).flatMap((run) => {
|
|
16422
|
-
if (run.head_sha !==
|
|
16458
|
+
if (run.head_sha !== headSha || typeof run.id !== "number" || typeof run.name !== "string" || typeof run.status !== "string") return [];
|
|
16423
16459
|
if (AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name)) return [];
|
|
16424
16460
|
return [{
|
|
16425
16461
|
id: run.id,
|
|
16426
16462
|
name: run.name,
|
|
16427
|
-
headSha
|
|
16463
|
+
headSha,
|
|
16428
16464
|
status: run.status,
|
|
16429
16465
|
conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
|
|
16430
16466
|
...typeof run.html_url === "string" ? { url: run.html_url } : {}
|
|
16431
16467
|
}];
|
|
16432
16468
|
});
|
|
16433
|
-
const
|
|
16469
|
+
const workflowById = new Map(workflows.map((run) => [run.id, run]));
|
|
16470
|
+
const isStalePendingCheckRun = (run) => {
|
|
16471
|
+
if (classifyCheckRun(run) !== "pending") return false;
|
|
16472
|
+
const match = /\/actions\/runs\/(\d+)(?:\/|$)/.exec(run.details_url ?? "");
|
|
16473
|
+
const owning = match ? workflowById.get(Number(match[1])) : void 0;
|
|
16474
|
+
if (!owning) return false;
|
|
16475
|
+
if (owning.status === "completed") return true;
|
|
16476
|
+
return workflows.some((newer) => newer.id > owning.id && newer.name === owning.name && newer.status === "completed");
|
|
16477
|
+
};
|
|
16478
|
+
const checks = [
|
|
16479
|
+
...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).filter((run) => !isStalePendingCheckRun(run)).map((run) => ({
|
|
16480
|
+
name: run.name ?? `check-run ${run.id ?? "unknown"}`,
|
|
16481
|
+
state: classifyCheckRun(run),
|
|
16482
|
+
...run.status !== void 0 ? { status: run.status } : {},
|
|
16483
|
+
conclusion: run.conclusion ?? null,
|
|
16484
|
+
startedAt: run.started_at ?? null,
|
|
16485
|
+
appId: run.app_id ?? null
|
|
16486
|
+
})),
|
|
16487
|
+
...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", state: classifyCommitStatus(s.state) }))
|
|
16488
|
+
];
|
|
16489
|
+
const newestRunByName = /* @__PURE__ */ new Map();
|
|
16490
|
+
for (const run of workflows) {
|
|
16491
|
+
const current = newestRunByName.get(run.name);
|
|
16492
|
+
if (!current || run.id > current.id) newestRunByName.set(run.name, run);
|
|
16493
|
+
}
|
|
16494
|
+
const workflowBuckets = [...newestRunByName.values()].map((run) => {
|
|
16434
16495
|
if (run.status !== "completed") return "pending";
|
|
16435
16496
|
return run.conclusion === "success" || run.conclusion === "neutral" || run.conclusion === "skipped" ? "pass" : "fail";
|
|
16436
16497
|
});
|
|
16437
16498
|
return {
|
|
16438
16499
|
state: derivePollState([...checks.map((check) => check.state), ...workflowBuckets]),
|
|
16439
|
-
headSha
|
|
16500
|
+
headSha,
|
|
16440
16501
|
checks,
|
|
16441
16502
|
workflows
|
|
16442
16503
|
};
|
|
16443
16504
|
}
|
|
16505
|
+
async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaultGhApi) {
|
|
16506
|
+
const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
|
|
16507
|
+
return pollRestHeadChecksWithWorkflowEvidence(snapshot.headSha, repo, gh2);
|
|
16508
|
+
}
|
|
16509
|
+
async function pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2 = defaultGhApi) {
|
|
16510
|
+
return pollRestHeadChecksWithWorkflowEvidence(sha, repo, gh2);
|
|
16511
|
+
}
|
|
16444
16512
|
async function pollRestCommitChecks(sha, repo, gh2 = defaultGhApi) {
|
|
16445
|
-
return
|
|
16513
|
+
return (await pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2)).state;
|
|
16446
16514
|
}
|
|
16447
16515
|
var GH_ZERO_TIME = "0001-01-01T00:00:00Z";
|
|
16448
16516
|
var GH_PR_CHECKS_JSON_FIELDS = ["bucket", "completedAt", "description", "link", "name", "startedAt", "state"];
|
|
@@ -21018,8 +21086,9 @@ function resolveSpawnTarget(command, args, platform2 = process.platform) {
|
|
|
21018
21086
|
if (platform2 !== "win32") return { cmd: command, args };
|
|
21019
21087
|
for (const t of [command, ...args]) {
|
|
21020
21088
|
if (/[\r\n]/.test(t)) {
|
|
21089
|
+
const preview = JSON.stringify(t.length > 80 ? `${t.slice(0, 77)}...` : t);
|
|
21021
21090
|
throw new Error(
|
|
21022
|
-
`cannot spawn safely on Windows: cmd.exe truncates at a newline, even quoted: ${
|
|
21091
|
+
`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
21092
|
);
|
|
21024
21093
|
}
|
|
21025
21094
|
}
|
|
@@ -22319,8 +22388,22 @@ function renderBoardItem(item) {
|
|
|
22319
22388
|
}
|
|
22320
22389
|
return lines2.join("\n");
|
|
22321
22390
|
}
|
|
22391
|
+
function renderCachedBoardSource(report) {
|
|
22392
|
+
if (typeof report.stale?.ageMs === "number") {
|
|
22393
|
+
const ageSeconds = Math.floor(report.stale.ageMs / 1e3);
|
|
22394
|
+
return `source: cache \xB7 built ${report.builtAt ? `${report.builtAt} (${ageSeconds}s old)` : `${ageSeconds}s ago`} \xB7 stale`;
|
|
22395
|
+
}
|
|
22396
|
+
if (report.builtAt) {
|
|
22397
|
+
const builtMs = Date.parse(report.builtAt);
|
|
22398
|
+
if (Number.isFinite(builtMs)) {
|
|
22399
|
+
return `source: cache \xB7 built ${report.builtAt} (${Math.max(0, Math.floor((Date.now() - builtMs) / 1e3))}s old) \xB7 fresh`;
|
|
22400
|
+
}
|
|
22401
|
+
return `source: cache \xB7 built ${report.builtAt} \xB7 fresh`;
|
|
22402
|
+
}
|
|
22403
|
+
return "source: cache";
|
|
22404
|
+
}
|
|
22322
22405
|
function renderBoardReport(report) {
|
|
22323
|
-
const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ?
|
|
22406
|
+
const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ? renderCachedBoardSource(report) : renderBoardSource()];
|
|
22324
22407
|
if (report.github?.rateLimit) {
|
|
22325
22408
|
const { limit, remaining, cost, resetAt } = report.github.rateLimit;
|
|
22326
22409
|
lines2.push(`github: app installation \xB7 GraphQL ${remaining}/${limit} remaining \xB7 cost ${cost} \xB7 resets ${resetAt}`);
|
|
@@ -23576,7 +23659,7 @@ var contestedBoardReadCommand = (repo) => `mmi-cli oracle board read --json${rep
|
|
|
23576
23659
|
function contestedClaimRecovery(ref) {
|
|
23577
23660
|
const repo = /^([^/\s#]+\/[^/\s#]+)#\d+$/.exec(ref)?.[1];
|
|
23578
23661
|
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
|
|
23662
|
+
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
23663
|
}
|
|
23581
23664
|
function laneContestMessage(ref, contest, verb) {
|
|
23582
23665
|
const { holder, why } = laneContestParts(contest);
|
|
@@ -23594,6 +23677,7 @@ function ghError(e) {
|
|
|
23594
23677
|
init_github_client();
|
|
23595
23678
|
init_github_quota();
|
|
23596
23679
|
var CLAIM_CONCURRENCY = 5;
|
|
23680
|
+
var VERIFIED_FREE_CANDIDATE_LIMIT = 5;
|
|
23597
23681
|
function withGatedWriteChainHint(message2) {
|
|
23598
23682
|
return `${message2}
|
|
23599
23683
|
note: run refused writes separately or join dependent commands with '&&'; a ';' chain continues after failure`;
|
|
@@ -23854,9 +23938,16 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23854
23938
|
const { holder: holder2, why } = laneContestParts(contest);
|
|
23855
23939
|
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
23940
|
}
|
|
23857
|
-
const
|
|
23858
|
-
const
|
|
23859
|
-
const
|
|
23941
|
+
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);
|
|
23942
|
+
const verifiedFreeRefs = [];
|
|
23943
|
+
for (const candidate of candidates) {
|
|
23944
|
+
const candidateContest = await checkLaneContest(client, candidate, ctx.session, report.viewer, ctx.appClient, ctx.landing).catch(() => void 0);
|
|
23945
|
+
if (candidateContest && !candidateContest.contested && candidateContest.unverifiable.length === 0) {
|
|
23946
|
+
verifiedFreeRefs.push(candidate.ref);
|
|
23947
|
+
}
|
|
23948
|
+
}
|
|
23949
|
+
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`;
|
|
23950
|
+
const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}`;
|
|
23860
23951
|
throw new Error(withGatedWriteChainHint(refusal));
|
|
23861
23952
|
}
|
|
23862
23953
|
previousHolder = displaced();
|
|
@@ -25352,6 +25443,7 @@ var BLESSED_RUN_WITH_BUDGET_SHA = "5ce541add569e54e7ca96de67a768577d3d604e4";
|
|
|
25352
25443
|
var BLESSED_RUNNER_GATE_SHA = "a19e9392e5ad9b0675f02e4304d68dd933fad60f";
|
|
25353
25444
|
var BLESSED_KIT_AGENT_FILES_SHA = "e976df208d891b215844386c1cac0f161946b6fa";
|
|
25354
25445
|
var BLESSED_JEV_FAILURES_SHA = "d5278c6133318b33727483ddfdfddc8f841e3ef6";
|
|
25446
|
+
var BLESSED_RUNNER_CHOICE_SHA = "";
|
|
25355
25447
|
var BLESSED_RUNBOOK_GUARD_SHA = "e661e0aedc0e58b73c738bf6cdfeac65e5914584";
|
|
25356
25448
|
var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
|
|
25357
25449
|
var LOCAL_USE = "./.github/actions/run-with-budget";
|
|
@@ -27513,10 +27605,10 @@ var rollout_plan_default = {
|
|
|
27513
27605
|
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
27606
|
},
|
|
27515
27607
|
baseline: {
|
|
27516
|
-
version: "4.6.
|
|
27517
|
-
tag: "v4.6.
|
|
27518
|
-
commit: "
|
|
27519
|
-
npm: "@mutmutco/cli@4.6.
|
|
27608
|
+
version: "4.6.11",
|
|
27609
|
+
tag: "v4.6.11",
|
|
27610
|
+
commit: "cb31a94c8972",
|
|
27611
|
+
npm: "@mutmutco/cli@4.6.11"
|
|
27520
27612
|
},
|
|
27521
27613
|
exitCriterion: "fleet-n-of-n",
|
|
27522
27614
|
hubOnlyShortcut: "forbidden",
|
|
@@ -27533,14 +27625,14 @@ var rollout_plan_default = {
|
|
|
27533
27625
|
repo: "mutmutco/mmi-hub",
|
|
27534
27626
|
role: "canary",
|
|
27535
27627
|
schedule: "train",
|
|
27536
|
-
v3Target: "v4.6.
|
|
27628
|
+
v3Target: "v4.6.11"
|
|
27537
27629
|
}
|
|
27538
27630
|
],
|
|
27539
27631
|
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
27632
|
rollback: {
|
|
27541
27633
|
independent: true,
|
|
27542
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
27543
|
-
v3Target: "v4.6.
|
|
27634
|
+
mechanism: "npm dist-tag latest -> 4.6.11 and redeploy the Hub Lambda from tag v4.6.11 (cb31a94c8972); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
27635
|
+
v3Target: "v4.6.11 (@mutmutco/cli@4.6.11, tag commit cb31a94c8972 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
27544
27636
|
}
|
|
27545
27637
|
},
|
|
27546
27638
|
{
|
|
@@ -32113,17 +32205,30 @@ function classifyRunnerCost(selector) {
|
|
|
32113
32205
|
}
|
|
32114
32206
|
var STANDARD_HOSTED_ALLOWLIST = [
|
|
32115
32207
|
["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"],
|
|
32116
|
-
["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"]
|
|
32117
|
-
// #9571 (MM-Work#8894 chooser): owned mmi-heavy first, included ubuntu-latest only on a deep backlog.
|
|
32118
|
-
["mutmutco/MMI-Hub", ".github/workflows/gate.yml", "cli-part", "hosted-spill"]
|
|
32208
|
+
["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"]
|
|
32119
32209
|
];
|
|
32120
|
-
var HOSTED_SPILL_RE = /^\$\{\{\s*(?:matrix\.[\w-]+\s*==\s*'[\w-]+'\s*&&\s*'([^']+)'\s*\|\|\s*)?fromJSON\(needs\.[\w-]
|
|
32210
|
+
var HOSTED_SPILL_RE = /^\$\{\{\s*(?:matrix\.[\w-]+\s*==\s*'[\w-]+'\s*&&\s*'([^']+)'\s*\|\|\s*)?fromJSON\(needs\.([\w-]+)\.outputs\.(\w+_runs_on)\s*\|\|\s*('[^']+')\)\s*\}\}$/;
|
|
32211
|
+
var RUNNER_CHOICE_ACTION = "mutmutco/MMI-Hub/.github/actions/runner-choice";
|
|
32212
|
+
function runnerChoiceProduces(job, decider, output, source) {
|
|
32213
|
+
const needs = job.needs;
|
|
32214
|
+
if (!(needs === decider || Array.isArray(needs) && needs.includes(decider))) return false;
|
|
32215
|
+
const producer = source.jobs.get(decider);
|
|
32216
|
+
if (!record(producer) || !record(producer.outputs) || !Array.isArray(producer.steps)) return false;
|
|
32217
|
+
const step = /^\$\{\{\s*steps\.([\w-]+)\.outputs\.runs-on\s*\}\}$/.exec(String(producer.outputs[output] ?? ""))?.[1];
|
|
32218
|
+
const steps = producer.steps.filter((s) => record(s) && s.id === step);
|
|
32219
|
+
if (steps.length !== 1 || !record(steps[0])) return false;
|
|
32220
|
+
const { uses } = steps[0], inputs = steps[0].with ?? {};
|
|
32221
|
+
const pinned = /^[0-9a-f]{40}$/.test(source.blessedSha) && uses === `${RUNNER_CHOICE_ACTION}@${source.blessedSha}`;
|
|
32222
|
+
if (!pinned && !(source.repo === "mutmutco/MMI-Hub" && uses === "./.github/actions/runner-choice")) return false;
|
|
32223
|
+
const hosted = record(inputs) ? inputs.hosted ?? "ubuntu-latest" : void 0;
|
|
32224
|
+
return typeof hosted === "string" && STANDARD_HOSTED_LABEL_RE.test(hosted);
|
|
32225
|
+
}
|
|
32121
32226
|
function standardHostedLabelFor(repo, path2, jobId) {
|
|
32122
32227
|
if (repo === "mutmutco/Jerv-Hub") return ["ubuntu-latest", "windows-latest", "macos-14"];
|
|
32123
32228
|
return STANDARD_HOSTED_ALLOWLIST.find(([r, p, j]) => r === repo && p === path2 && j === jobId)?.[3];
|
|
32124
32229
|
}
|
|
32125
32230
|
var record = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
32126
|
-
function classifyJobRunnerCost(job, allowance2) {
|
|
32231
|
+
function classifyJobRunnerCost(job, allowance2, source) {
|
|
32127
32232
|
if (!record(job) || !Object.hasOwn(job, "runs-on")) return "unknown";
|
|
32128
32233
|
const selector = job["runs-on"];
|
|
32129
32234
|
const raw = typeof selector === "string" ? selector.trim() : Array.isArray(selector) && selector.length && selector.every((label) => typeof label === "string") ? JSON.stringify(selector) : "";
|
|
@@ -32132,8 +32237,8 @@ function classifyJobRunnerCost(job, allowance2) {
|
|
|
32132
32237
|
if (approved(raw) || allowance2 === "windows-11-arm" && raw === allowance2) return "standard-hosted";
|
|
32133
32238
|
const fallback = /^\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'([^']+)'\s*\|\|\s*(fromJSON\('[^']+'\))\s*\}\}$/.exec(raw);
|
|
32134
32239
|
if (fallback && approved(fallback[1]) && classifyRunnerCost("${{ " + fallback[2] + " }}") === "self-hosted") return "standard-hosted";
|
|
32135
|
-
const spill =
|
|
32136
|
-
if (spill
|
|
32240
|
+
const spill = HOSTED_SPILL_RE.exec(raw);
|
|
32241
|
+
if (spill && source && (!spill[1] || STANDARD_HOSTED_LABEL_RE.test(spill[1])) && classifyRunnerCost(`\${{ fromJSON(${spill[4]}) }}`) === "self-hosted" && runnerChoiceProduces(job, spill[2], spill[3], source)) return "standard-hosted";
|
|
32137
32242
|
const direct = classifyRunnerCost(raw);
|
|
32138
32243
|
const matrix = /^\$\{\{\s*matrix\.([\w-]+)(?:\.([\w-]+))?\s*\}\}$/.exec(raw);
|
|
32139
32244
|
if (!matrix || direct === "hosted") return direct;
|
|
@@ -32188,16 +32293,24 @@ function parseRunnerJobs(source) {
|
|
|
32188
32293
|
for (const { key, value: job } of jobs.items) {
|
|
32189
32294
|
if (!(0, import_yaml.isScalar)(key) || typeof key.value !== "string" || !literalMap(job)) return void 0;
|
|
32190
32295
|
const projected = /* @__PURE__ */ Object.create(null);
|
|
32191
|
-
for (const field of ["runs-on", "uses", "strategy"]) {
|
|
32296
|
+
for (const field of ["runs-on", "uses", "strategy", "needs", "outputs", "steps"]) {
|
|
32192
32297
|
if (!job.has(field)) continue;
|
|
32298
|
+
const optional = field === "needs" || field === "outputs" || field === "steps";
|
|
32193
32299
|
const value = job.get(field, true);
|
|
32194
|
-
if (value !== null && !(0, import_yaml.isNode)(value))
|
|
32300
|
+
if (value !== null && !(0, import_yaml.isNode)(value)) {
|
|
32301
|
+
if (optional) continue;
|
|
32302
|
+
return void 0;
|
|
32303
|
+
}
|
|
32195
32304
|
(0, import_yaml.visit)(value, (_key, node) => {
|
|
32196
32305
|
if (!literal(node) || (0, import_yaml.isPair)(node) && (0, import_yaml.isScalar)(node.key) && node.key.value === "<<") {
|
|
32197
32306
|
invalid = true;
|
|
32198
32307
|
return import_yaml.visit.BREAK;
|
|
32199
32308
|
}
|
|
32200
32309
|
});
|
|
32310
|
+
if (invalid && optional) {
|
|
32311
|
+
invalid = false;
|
|
32312
|
+
continue;
|
|
32313
|
+
}
|
|
32201
32314
|
if (invalid) return void 0;
|
|
32202
32315
|
projected[field] = value === null ? null : value.toJS(doc, { maxAliasCount: 0 });
|
|
32203
32316
|
}
|
|
@@ -32334,7 +32447,7 @@ function classifyRunnerWorkflows(files) {
|
|
|
32334
32447
|
const uses = hasCallee && typeof job.uses === "string" && !Object.hasOwn(job, "runs-on") ? job.uses : void 0;
|
|
32335
32448
|
const target = uses && resolveCallee(file, uses);
|
|
32336
32449
|
if (target && known.has(locationOf(target)) && resolved(file, /* @__PURE__ */ new Set())) return [];
|
|
32337
|
-
const cost = hasCallee ? "unknown" : classifyJobRunnerCost(job, standardHostedLabelFor(file.repo, file.path, id));
|
|
32450
|
+
const cost = hasCallee ? "unknown" : classifyJobRunnerCost(job, standardHostedLabelFor(file.repo, file.path, id), { repo: file.repo, jobs, blessedSha: BLESSED_RUNNER_CHOICE_SHA });
|
|
32338
32451
|
return [{ repo: file.repo, path: file.path, job: id, cost, callee: hasCallee }];
|
|
32339
32452
|
});
|
|
32340
32453
|
});
|
|
@@ -41262,7 +41375,18 @@ async function readGhPrStateWithRetry(fetchState, options) {
|
|
|
41262
41375
|
}
|
|
41263
41376
|
return { ok: false, error: lastError };
|
|
41264
41377
|
}
|
|
41378
|
+
function isNoFailedCheckQueueEjection(run) {
|
|
41379
|
+
return !run || run.conclusion === null || run.conclusion === "cancelled" || run.conclusion === "neutral" || run.conclusion === "skipped";
|
|
41380
|
+
}
|
|
41265
41381
|
async function runPrLand(prNumber, options, deps) {
|
|
41382
|
+
const first = await runPrLandOnce(prNumber, options, deps);
|
|
41383
|
+
if (options.enqueueOnly || options.expectHead || first.status !== "failed") return first;
|
|
41384
|
+
const queue = first.mergeQueue;
|
|
41385
|
+
const nothingFailed = queue?.state === "timeout" || queue?.state === "ejected" && isNoFailedCheckQueueEjection(queue.ejectedRun);
|
|
41386
|
+
if (!nothingFailed) return first;
|
|
41387
|
+
return { ...await runPrLandOnce(prNumber, options, deps), retriedAfterEjection: true };
|
|
41388
|
+
}
|
|
41389
|
+
async function runPrLandOnce(prNumber, options, deps) {
|
|
41266
41390
|
const resolved = await deps.resolveRepo(prNumber, options.repo);
|
|
41267
41391
|
const { repo } = resolved;
|
|
41268
41392
|
const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
|
|
@@ -41308,6 +41432,18 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
41308
41432
|
base.mergeStatus = "auto-merge-enqueued";
|
|
41309
41433
|
return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
|
|
41310
41434
|
}
|
|
41435
|
+
if (deps.preflightQueue) {
|
|
41436
|
+
const preflight2 = await deps.preflightQueue(prNumber, repo, pinned);
|
|
41437
|
+
if (preflight2 && preflight2.mergeStatus === "failed") {
|
|
41438
|
+
const rawError = preflight2.error ?? "queue preparation needed";
|
|
41439
|
+
return {
|
|
41440
|
+
...base,
|
|
41441
|
+
mergeStatus: "failed",
|
|
41442
|
+
...preflight2.preparation ? { preparation: preflight2.preparation } : {},
|
|
41443
|
+
error: !rawError.includes("read pr queue") ? `${rawError} \u2014 ${prQueueReadHint(prNumber, repo)}` : rawError
|
|
41444
|
+
};
|
|
41445
|
+
}
|
|
41446
|
+
}
|
|
41311
41447
|
}
|
|
41312
41448
|
const checksWaitError = (checksWait) => {
|
|
41313
41449
|
if (checksWait.status === "conflicting") {
|
|
@@ -41468,7 +41604,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
41468
41604
|
...base,
|
|
41469
41605
|
mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
|
|
41470
41606
|
...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)}`
|
|
41607
|
+
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
41608
|
};
|
|
41473
41609
|
deps.queueProgress?.(result2);
|
|
41474
41610
|
return result2;
|
|
@@ -41478,7 +41614,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
41478
41614
|
const result = {
|
|
41479
41615
|
...base,
|
|
41480
41616
|
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
|
|
41617
|
+
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
41618
|
};
|
|
41483
41619
|
deps.queueProgress?.(result);
|
|
41484
41620
|
return result;
|
|
@@ -41498,7 +41634,7 @@ async function landMany(prs, landOne, cleanup, hooks) {
|
|
|
41498
41634
|
const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
|
|
41499
41635
|
return Promise.all(prs.map(async (pr) => {
|
|
41500
41636
|
let result = await safe(pr);
|
|
41501
|
-
if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
|
|
41637
|
+
if (prLandOutcome(result) === "ejected" && !result.pinnedHead && !result.retriedAfterEjection) {
|
|
41502
41638
|
const baseRed = await hooks?.baseRedCheck?.(pr, result).catch(() => void 0);
|
|
41503
41639
|
if (baseRed) {
|
|
41504
41640
|
result = { ...result, baseRed: { base: result.base, baseRunId: baseRed.baseRunId }, error: `base-red: failures already on ${result.base} (${baseRed.baseRunId})` };
|
|
@@ -41670,10 +41806,16 @@ function trailerBlockBreak(body, line) {
|
|
|
41670
41806
|
const lines2 = String(body ?? "").split(/\r?\n/);
|
|
41671
41807
|
const at = lines2.findIndex((candidate) => candidate.trim() === line);
|
|
41672
41808
|
if (at === -1) return { after: "", before: "" };
|
|
41809
|
+
let sawBlank = false;
|
|
41673
41810
|
for (let i = at + 1; i < lines2.length; i += 1) {
|
|
41674
41811
|
const next = lines2[i];
|
|
41675
|
-
if (!next.trim())
|
|
41676
|
-
|
|
41812
|
+
if (!next.trim()) {
|
|
41813
|
+
sawBlank = true;
|
|
41814
|
+
continue;
|
|
41815
|
+
}
|
|
41816
|
+
if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) {
|
|
41817
|
+
return sawBlank ? { after: "", before: "", splitByBlankBefore: next.trim() } : { after: "", before: "" };
|
|
41818
|
+
}
|
|
41677
41819
|
return { after: next.trim(), before: "" };
|
|
41678
41820
|
}
|
|
41679
41821
|
let paraStart = at;
|
|
@@ -41693,7 +41835,10 @@ function clipParagraph(line) {
|
|
|
41693
41835
|
}
|
|
41694
41836
|
function invisibleTrailer(sha, line, body = "") {
|
|
41695
41837
|
const broken = trailerBlockBreak(body, line);
|
|
41696
|
-
const brokenDiagnostic = broken.
|
|
41838
|
+
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:
|
|
41839
|
+
${clipParagraph(broken.splitByBlankBefore)}
|
|
41840
|
+
Keep every Key: value trailer (e.g. ${TRAILER_KEY} and Runbook-Unaffected) in one contiguous final paragraph with no blank line between them.
|
|
41841
|
+
` : broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
|
|
41697
41842
|
${clipParagraph(broken.after)}
|
|
41698
41843
|
` : broken.before ? ` Broken by the non-trailer line before the trailer-shaped line, which prevents git from recognizing the final block:
|
|
41699
41844
|
${clipParagraph(broken.before)}
|
|
@@ -41713,9 +41858,11 @@ function invisibleTrailer(sha, line, body = "") {
|
|
|
41713
41858
|
The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.
|
|
41714
41859
|
Diagnosis: git parsed no ${TRAILER_KEY} trailer for ${sha.slice(0, 8)} at all \u2014 read its own view first:
|
|
41715
41860
|
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
|
|
41861
|
+
` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message. Run this
|
|
41862
|
+
yourself, right now, in this worktree; it is not a blocker to report and it needs no human:
|
|
41717
41863
|
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
|
|
41864
|
+
That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.
|
|
41865
|
+
Re-run the policy receipt and the focused test command immediately after it.`
|
|
41719
41866
|
};
|
|
41720
41867
|
}
|
|
41721
41868
|
function unknownScope(sha, unknown) {
|
|
@@ -42846,9 +42993,7 @@ ${list}
|
|
|
42846
42993
|
Co-authored-by: GitHub <noreply@github.com>
|
|
42847
42994
|
`;
|
|
42848
42995
|
}
|
|
42849
|
-
async function
|
|
42850
|
-
if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
|
|
42851
|
-
pr = pr.replace(/^0+(?=\d)/, "");
|
|
42996
|
+
async function inspectQueuePreparation(pr, repo, options) {
|
|
42852
42997
|
const read = options.read ?? readApi;
|
|
42853
42998
|
const queueRead = options.read ?? actorReadApi;
|
|
42854
42999
|
const write = options.write ?? writeApi;
|
|
@@ -42860,14 +43005,14 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42860
43005
|
const pull = metadata.pullRequest;
|
|
42861
43006
|
if (!pull || !pull.id || !/^[a-f0-9]{40}$/i.test(pull.headRefOid)) throw new Error(`merge queue PR #${pr} identity unreadable`);
|
|
42862
43007
|
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 = (
|
|
43008
|
+
if (head !== pull.headRefOid) return { kind: "settled", result: { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` } };
|
|
43009
|
+
const stamp = (result, selected = false, intentId) => {
|
|
42865
43010
|
const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
|
|
42866
|
-
return credential === "unknown" ?
|
|
43011
|
+
return credential === "unknown" ? result : { ...result, credential };
|
|
42867
43012
|
};
|
|
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` };
|
|
43013
|
+
if (pull.state === "MERGED") return { kind: "settled", result: stamp({ mergeStatus: "merged" }) };
|
|
43014
|
+
if (pull.isInMergeQueue) return { kind: "settled", result: stamp({ mergeStatus: "auto-merge-enqueued" }) };
|
|
43015
|
+
if (pull.state !== "OPEN") return { kind: "settled", result: { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` } };
|
|
42871
43016
|
const closing = [...pull.closingIssuesReferences.nodes.map((node) => node.number)];
|
|
42872
43017
|
let page = pull.closingIssuesReferences.pageInfo;
|
|
42873
43018
|
while (page.hasNextPage) {
|
|
@@ -42900,7 +43045,7 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42900
43045
|
status: "preparation-needed",
|
|
42901
43046
|
reason,
|
|
42902
43047
|
issueTargets: targets.map((number) => `${repo}#${number}`),
|
|
42903
|
-
nextAction: reason === "queue-message" ?
|
|
43048
|
+
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
43049
|
},
|
|
42905
43050
|
error: `queue preparation needed: ${detail}`
|
|
42906
43051
|
});
|
|
@@ -42918,9 +43063,9 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42918
43063
|
);
|
|
42919
43064
|
method = legacy.mergeQueue?.configuration?.mergeMethod;
|
|
42920
43065
|
}
|
|
42921
|
-
if (!["SQUASH", "MERGE", "REBASE"].includes(method ?? "")) return preparation("queue-message-policy", [], "the effective queue merge method is unknown; no submission occurred");
|
|
43066
|
+
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
43067
|
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");
|
|
43068
|
+
return { kind: "refused", result: preparation("queue-message-policy", [], "the repository squash-message policy is unreadable; no submission occurred") };
|
|
42924
43069
|
}
|
|
42925
43070
|
const messages = parsed.commits;
|
|
42926
43071
|
const headline = policy.title === "COMMIT_OR_PR_TITLE" && messages.length === 1 ? messages[0].headline : pull.title;
|
|
@@ -42937,7 +43082,7 @@ ${body}`;
|
|
|
42937
43082
|
${message2.body}`).join("\n\n");
|
|
42938
43083
|
if (method === "MERGE") {
|
|
42939
43084
|
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");
|
|
43085
|
+
return { kind: "refused", result: preparation("queue-message-policy", [], "the repository merge-message policy is unreadable; no submission occurred") };
|
|
42941
43086
|
}
|
|
42942
43087
|
if (repository.merge_commit_title === "PR_TITLE" || repository.merge_commit_message === "PR_TITLE") actualMessage += `
|
|
42943
43088
|
|
|
@@ -42959,19 +43104,22 @@ ${pull.body}`;
|
|
|
42959
43104
|
...findNegatedClosings(input.text, closing),
|
|
42960
43105
|
...findCrossRepoAmbiguousClosings(input.text, closing, repo)
|
|
42961
43106
|
])];
|
|
42962
|
-
return preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${verdict.message}
|
|
43107
|
+
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
43108
|
}
|
|
42964
43109
|
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
43110
|
${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title, pr, messages), process.cwd())) {
|
|
42966
43111
|
return {
|
|
42967
|
-
|
|
42968
|
-
|
|
42969
|
-
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
|
|
42973
|
-
|
|
42974
|
-
|
|
43112
|
+
kind: "refused",
|
|
43113
|
+
result: {
|
|
43114
|
+
mergeStatus: "failed",
|
|
43115
|
+
preparation: {
|
|
43116
|
+
status: "preparation-needed",
|
|
43117
|
+
reason: "queue-message-override",
|
|
43118
|
+
issueTargets: [],
|
|
43119
|
+
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."
|
|
43120
|
+
},
|
|
43121
|
+
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"
|
|
43122
|
+
}
|
|
42975
43123
|
};
|
|
42976
43124
|
}
|
|
42977
43125
|
const requiredBody = options.bodyText ?? composedBody;
|
|
@@ -42979,18 +43127,38 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
|
|
|
42979
43127
|
if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
|
|
42980
43128
|
if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
|
|
42981
43129
|
return {
|
|
42982
|
-
|
|
42983
|
-
|
|
42984
|
-
|
|
42985
|
-
|
|
42986
|
-
|
|
42987
|
-
|
|
42988
|
-
|
|
42989
|
-
|
|
43130
|
+
kind: "refused",
|
|
43131
|
+
result: {
|
|
43132
|
+
mergeStatus: "failed",
|
|
43133
|
+
preparation: {
|
|
43134
|
+
status: "preparation-needed",
|
|
43135
|
+
reason: "queue-message-override",
|
|
43136
|
+
issueTargets: [],
|
|
43137
|
+
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.`
|
|
43138
|
+
},
|
|
43139
|
+
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"
|
|
43140
|
+
}
|
|
42990
43141
|
};
|
|
42991
43142
|
}
|
|
42992
|
-
return preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission");
|
|
43143
|
+
return { kind: "refused", result: preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission") };
|
|
42993
43144
|
}
|
|
43145
|
+
return { kind: "ready", head, pull, read, queueRead, write, ...verdict.message ? { warning: verdict.message } : {} };
|
|
43146
|
+
}
|
|
43147
|
+
async function preflightPrMergeQueue(prNumber, repo, options = {}) {
|
|
43148
|
+
if (!/^\d+$/.test(prNumber)) return void 0;
|
|
43149
|
+
const inspected = await inspectQueuePreparation(prNumber.replace(/^0+(?=\d)/, ""), repo, options);
|
|
43150
|
+
return inspected.kind === "refused" ? inspected.result : void 0;
|
|
43151
|
+
}
|
|
43152
|
+
async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
43153
|
+
if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
|
|
43154
|
+
pr = pr.replace(/^0+(?=\d)/, "");
|
|
43155
|
+
const inspected = await inspectQueuePreparation(pr, repo, options);
|
|
43156
|
+
if (inspected.kind !== "ready") return inspected.result;
|
|
43157
|
+
const { head, pull, read, queueRead, write } = inspected;
|
|
43158
|
+
const stamp = (result2, selected = false, intentId) => {
|
|
43159
|
+
const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
|
|
43160
|
+
return credential === "unknown" ? result2 : { ...result2, credential };
|
|
43161
|
+
};
|
|
42994
43162
|
const current = await queueGraphql(
|
|
42995
43163
|
repo,
|
|
42996
43164
|
`pullRequest(number:${pr}) { headRefOid title body }`,
|
|
@@ -43001,7 +43169,12 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
|
|
|
43001
43169
|
}
|
|
43002
43170
|
const selection = { selected: false };
|
|
43003
43171
|
const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
|
|
43004
|
-
return { ...stamp(result, selection.selected, selection.intentId), ...
|
|
43172
|
+
return { ...stamp(result, selection.selected, selection.intentId), ...inspected.warning ? { warning: inspected.warning } : {} };
|
|
43173
|
+
}
|
|
43174
|
+
function closingGuardDiagnostic(message2) {
|
|
43175
|
+
if (!message2) return "";
|
|
43176
|
+
const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
|
|
43177
|
+
return (remedy === -1 ? message2 : message2.slice(0, remedy)).trim();
|
|
43005
43178
|
}
|
|
43006
43179
|
function decideQueueMergeRoute(hasQueue, method, headRef) {
|
|
43007
43180
|
if (!hasQueue) return "normal";
|
|
@@ -43178,8 +43351,10 @@ function prMergeQueueLine(result) {
|
|
|
43178
43351
|
return `pr land: PR #${result.pr} queued on ${queue.base}${queue.position === void 0 ? "" : ` (position ${queue.position})`}`;
|
|
43179
43352
|
case "merged":
|
|
43180
43353
|
return `pr land: PR #${result.pr} merged via merge queue`;
|
|
43181
|
-
case "ejected":
|
|
43182
|
-
|
|
43354
|
+
case "ejected": {
|
|
43355
|
+
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"}`;
|
|
43356
|
+
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;
|
|
43357
|
+
}
|
|
43183
43358
|
case "timeout":
|
|
43184
43359
|
return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
|
|
43185
43360
|
}
|
|
@@ -43697,9 +43872,27 @@ function refuseNotClaimable(e, json) {
|
|
|
43697
43872
|
process.exitCode = 1;
|
|
43698
43873
|
return true;
|
|
43699
43874
|
}
|
|
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";
|
|
43875
|
+
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
43876
|
function flattenBoardReport(report) {
|
|
43702
|
-
|
|
43877
|
+
const closingByRef = /* @__PURE__ */ new Map();
|
|
43878
|
+
for (const pull of report.pullRequests ?? []) {
|
|
43879
|
+
for (const closes of pull.closesIssues) {
|
|
43880
|
+
const rows = closingByRef.get(closes) ?? [];
|
|
43881
|
+
rows.push({ number: pull.number, url: pull.url });
|
|
43882
|
+
closingByRef.set(closes, rows);
|
|
43883
|
+
}
|
|
43884
|
+
}
|
|
43885
|
+
return [report.primary, report.secondary].flatMap((scope) => ["userOwned", "claimable", "taken", "unownedInFlight"].flatMap((bucket) => scope[bucket].map(({ ref, number, title, status, assignees, labels, claim }) => ({
|
|
43886
|
+
ref,
|
|
43887
|
+
number,
|
|
43888
|
+
title,
|
|
43889
|
+
status,
|
|
43890
|
+
assignees,
|
|
43891
|
+
labels,
|
|
43892
|
+
bucket,
|
|
43893
|
+
claim: claim ?? null,
|
|
43894
|
+
openPullRequests: closingByRef.get(ref) ?? []
|
|
43895
|
+
}))));
|
|
43703
43896
|
}
|
|
43704
43897
|
function registerBoardCommands(program3) {
|
|
43705
43898
|
function withDiscoverMissDetail(message2) {
|
|
@@ -54954,23 +55147,24 @@ async function closeIssue(client, options, deps = {}) {
|
|
|
54954
55147
|
if (options.evidence !== void 0) {
|
|
54955
55148
|
await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
|
|
54956
55149
|
}
|
|
55150
|
+
const appWriter = await selectClaimWriteClient(client, deps.appClient);
|
|
54957
55151
|
if (reason === "completed" && options.evidence !== void 0) {
|
|
54958
55152
|
const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
|
|
54959
55153
|
await ensureLabels([CLOSED_MERGED_LOOP_STATE_LABEL], repo);
|
|
54960
|
-
await
|
|
55154
|
+
await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, {
|
|
54961
55155
|
body: { labels: [CLOSED_MERGED_LOOP_STATE_LABEL] }
|
|
54962
55156
|
});
|
|
54963
55157
|
}
|
|
54964
|
-
await
|
|
55158
|
+
await appWriter.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
|
|
54965
55159
|
body: { state: "closed", state_reason: stateReason }
|
|
54966
55160
|
});
|
|
54967
55161
|
if (reason === "duplicate-of" && options.duplicateOf) {
|
|
54968
|
-
await
|
|
55162
|
+
await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
|
|
54969
55163
|
body: { body: `Duplicate of #${options.duplicateOf}` }
|
|
54970
55164
|
});
|
|
54971
55165
|
}
|
|
54972
55166
|
if (options.comment) {
|
|
54973
|
-
await
|
|
55167
|
+
await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
|
|
54974
55168
|
body: { body: options.comment }
|
|
54975
55169
|
});
|
|
54976
55170
|
}
|
|
@@ -59609,7 +59803,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59609
59803
|
timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
|
|
59610
59804
|
// Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
|
|
59611
59805
|
// 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`)
|
|
59806
|
+
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
59807
|
});
|
|
59614
59808
|
printChecksWaitResult("pr checks-wait", result, o.json);
|
|
59615
59809
|
});
|
|
@@ -59716,6 +59910,8 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59716
59910
|
readHeadSha: (prNumber, repo) => readPrLandPinnedHeadSha(prNumber, repo),
|
|
59717
59911
|
pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
|
|
59718
59912
|
hasMergeQueue: hasPrMergeQueue,
|
|
59913
|
+
// #9611: read-only preparation verdict BEFORE the check wait; the enqueue then re-reads and submits.
|
|
59914
|
+
preflightQueue: (prNumber, repo, expectedHeadOid) => preflightPrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly }),
|
|
59719
59915
|
enqueueQueue: async (prNumber, repo, expectedHeadOid) => {
|
|
59720
59916
|
const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly });
|
|
59721
59917
|
if (receipt.warning) console.warn(receipt.warning);
|
|
@@ -60381,6 +60577,9 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
60381
60577
|
const result = await waitForPrChecks({
|
|
60382
60578
|
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
60383
60579
|
pollChecks: () => pollRestCommitChecks(sha, repo),
|
|
60580
|
+
// #9589: the evidence-bearing read lets the wait name the rows holding it open and carry the
|
|
60581
|
+
// full check/workflow evidence on the timeout receipt.
|
|
60582
|
+
pollChecksWithEvidence: () => pollRestCommitChecksWithWorkflowEvidence(sha, repo),
|
|
60384
60583
|
pollRateLimit: () => waitLoopCorePool("ci checks-wait"),
|
|
60385
60584
|
checksUrl: `https://github.com/${repo}/commit/${sha}/checks`,
|
|
60386
60585
|
checksDiagnosticCommand: `gh api repos/${repo}/commits/${sha}/check-runs`,
|
|
@@ -60393,7 +60592,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
60393
60592
|
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
|
|
60394
60593
|
log: (message2) => console.warn(message2),
|
|
60395
60594
|
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`)
|
|
60595
|
+
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
60596
|
});
|
|
60398
60597
|
printChecksWaitResult("ci checks-wait", result, o.json);
|
|
60399
60598
|
});
|
|
@@ -66245,7 +66444,7 @@ function typedReadHint(path2) {
|
|
|
66245
66444
|
if (!match) return "";
|
|
66246
66445
|
const [, owner, repo, kind, item, sub] = match;
|
|
66247
66446
|
const slug = `${owner}/${repo}`;
|
|
66248
|
-
const say = (command, note = "") => ` Typed read for this route:
|
|
66447
|
+
const say = (command, note = "") => ` Typed read for this route: \`MMI_ACTOR=app ${command}\`${note}.`;
|
|
66249
66448
|
if (kind === "issues") {
|
|
66250
66449
|
if (!item) return say(`mmi-cli devops read issue list --repo ${slug} --json number,title,state,labels`);
|
|
66251
66450
|
if (item.startsWith("comments/")) {
|
|
@@ -69939,7 +70138,7 @@ function envelopeAwareWriteErr(str2) {
|
|
|
69939
70138
|
const invoked = resolveCommandFromArgv(program2, argv);
|
|
69940
70139
|
const invokedPath = invoked ? canonicalPathFor(commandPath3(invoked)) ?? commandPath3(invoked) : void 0;
|
|
69941
70140
|
const canonicalArgv = canonicalArgvFor(argv);
|
|
69942
|
-
const claimStatus = invoked ? boardClaimStatusFlagRecovery(flag, invokedPath, canonicalArgv) ?? issueCommentAppendOnlyRecovery(flag, invokedPath, canonicalArgv) : void 0;
|
|
70141
|
+
const claimStatus = invoked ? boardClaimStatusFlagRecovery(flag, invokedPath, canonicalArgv) ?? issueCommentAppendOnlyRecovery(flag, invokedPath, canonicalArgv) ?? trainPlanRedundantFlagRecovery(flag, invokedPath, canonicalArgv) : void 0;
|
|
69943
70142
|
if (claimStatus) {
|
|
69944
70143
|
if (argvWantsJson3()) {
|
|
69945
70144
|
process.stderr.write(
|
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
|
|
@@ -56,7 +56,12 @@ are recorded per worktree, so a receipt from another tree does not clear the gua
|
|
|
56
56
|
a lane root or sibling worktree (use `git -C` / `npm --prefix` instead, #3187). A historical main-based
|
|
57
57
|
hotfix worktree without test-policy.json chains
|
|
58
58
|
`mmi-cli tests policy --json --base origin/main --policy-ref <current origin/development commit> && <focused test>`
|
|
59
|
-
in one command (#3243).
|
|
59
|
+
in one command (#3243). A lane diff is only readable from revisions the task worktree actually has (#9627):
|
|
60
|
+
take the head from `HEAD` or `origin/<base>`, or prove the sha first with
|
|
61
|
+
`git rev-parse --verify <sha>^{commit}` (fetching that commit when it is absent), and never gate the
|
|
62
|
+
`mmi-cli tests policy --base <comparison-base> && <focused test>` chain on an unverified short sha — `git` exits 128 there, so the
|
|
63
|
+
`&&` short-circuits and the policy run is silently skipped while the receipt looks missing rather than failed.
|
|
64
|
+
Complete the documentation preflight below before pushing.
|
|
60
65
|
Open a PR through `mmi-cli devops pr create` with a closing issue link, and land through
|
|
61
66
|
`mmi-cli devops pr land`. Verify required checks, merge, issue closure, and cleanup before calling
|
|
62
67
|
the implementation complete. A merged implementation is not a production release.
|
|
@@ -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`.
|