@mutmutco/codex-plugin 4.6.30 → 4.6.32
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/.codex-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +408 -60
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/board/SKILL.md +14 -8
- package/skills/board/reference.md +4 -1
- package/skills/bootstrap/seeds/runbook-unaffected.json +2 -2
- package/skills/release/SKILL.md +1 -1
- package/skills/release/suite.md +2 -0
package/cli/dist/main.cjs
CHANGED
|
@@ -24526,19 +24526,7 @@ function formatBulkClaimSummary(results) {
|
|
|
24526
24526
|
const notClaimed = skipped.map((r) => `${r.ref}${/already claimed by /i.test(r.reason) ? " (held by another lane)" : ""}`);
|
|
24527
24527
|
return `claimed ${landed.length} of ${results.length}: ${landed.length ? `${landed.join(", ")} landed` : "none landed"}; not claimed: ${notClaimed.join(", ")}`;
|
|
24528
24528
|
}
|
|
24529
|
-
async function
|
|
24530
|
-
const cfg = resolveBoardConfig(options.config);
|
|
24531
|
-
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
24532
|
-
const selectors = [];
|
|
24533
|
-
const seen = /* @__PURE__ */ new Set();
|
|
24534
|
-
for (const raw of options.selectors) {
|
|
24535
|
-
const selector = parseIssueSelector(raw, collected.repo, options.repo);
|
|
24536
|
-
const key = `${selector.repo.toLowerCase()}#${selector.number}`;
|
|
24537
|
-
if (seen.has(key)) continue;
|
|
24538
|
-
seen.add(key);
|
|
24539
|
-
selectors.push(selector);
|
|
24540
|
-
}
|
|
24541
|
-
const ctx = await prepareClaimContext(options, selectors, deps, collected, snapshot);
|
|
24529
|
+
async function runBulkClaims(ctx, selectors, options) {
|
|
24542
24530
|
const results = new Array(selectors.length);
|
|
24543
24531
|
let next = 0;
|
|
24544
24532
|
const worker = async () => {
|
|
@@ -24555,6 +24543,71 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
24555
24543
|
}
|
|
24556
24544
|
};
|
|
24557
24545
|
await Promise.all(Array.from({ length: Math.min(CLAIM_CONCURRENCY, selectors.length) }, () => worker()));
|
|
24546
|
+
return results;
|
|
24547
|
+
}
|
|
24548
|
+
function claimArgsError(refs, all) {
|
|
24549
|
+
if (all && refs.length) return `--all claims the whole board; drop the explicit refs (${refs.join(" ")}) or drop --all`;
|
|
24550
|
+
if (!all && !refs.length) return "missing issue ref: pass one or more refs, or --all for every open claimable item on the board";
|
|
24551
|
+
return void 0;
|
|
24552
|
+
}
|
|
24553
|
+
async function claimAllBoardIssues(options, deps = {}) {
|
|
24554
|
+
const cfg = resolveBoardConfig(options.config);
|
|
24555
|
+
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
24556
|
+
const ctx = await prepareClaimContext(options, [], deps, collected, snapshot);
|
|
24557
|
+
const { primary } = ctx.report;
|
|
24558
|
+
const assignee = options.assignee ?? "@me";
|
|
24559
|
+
const login = assignee === "@me" ? ctx.report.viewer : assignee.replace(/^@/, "");
|
|
24560
|
+
const selectors = [];
|
|
24561
|
+
const skipped = [];
|
|
24562
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24563
|
+
for (const item of [...primary.userOwned, ...primary.claimable, ...primary.unownedInFlight, ...primary.taken]) {
|
|
24564
|
+
const key = item.ref.toLowerCase();
|
|
24565
|
+
if (seen.has(key)) continue;
|
|
24566
|
+
seen.add(key);
|
|
24567
|
+
if (item.contentType !== "Issue") {
|
|
24568
|
+
skipped.push({ ref: item.ref, reason: "not an issue" });
|
|
24569
|
+
continue;
|
|
24570
|
+
}
|
|
24571
|
+
if (item.state !== "OPEN") {
|
|
24572
|
+
skipped.push({ ref: item.ref, reason: `issue is ${item.state.toLowerCase()} but its card is ${item.status}` });
|
|
24573
|
+
continue;
|
|
24574
|
+
}
|
|
24575
|
+
if (item.status === "In Review") {
|
|
24576
|
+
skipped.push({ ref: item.ref, reason: "In Review \u2014 a whole-board claim does not pull review cards back to In Progress; claim it by ref to reclaim" });
|
|
24577
|
+
continue;
|
|
24578
|
+
}
|
|
24579
|
+
const verdict = evaluateClaim(item, login);
|
|
24580
|
+
if (!verdict.ok) {
|
|
24581
|
+
skipped.push({ ref: item.ref, reason: verdict.reason.replace(/ \(never chain separate claims.*$/, "") });
|
|
24582
|
+
continue;
|
|
24583
|
+
}
|
|
24584
|
+
selectors.push({ repo: item.repository, number: item.number });
|
|
24585
|
+
}
|
|
24586
|
+
ctx.selectedRefs = new Set(selectors.map((s) => `${s.repo}#${s.number}`.toLowerCase()));
|
|
24587
|
+
const results = await runBulkClaims(ctx, selectors, options);
|
|
24588
|
+
return {
|
|
24589
|
+
viewer: ctx.report.viewer,
|
|
24590
|
+
repo: ctx.report.repo,
|
|
24591
|
+
results,
|
|
24592
|
+
skipped,
|
|
24593
|
+
failed: results.filter((result) => !result.claimed).length,
|
|
24594
|
+
...ctx.report.warnings.length ? { warnings: [...ctx.report.warnings] } : {}
|
|
24595
|
+
};
|
|
24596
|
+
}
|
|
24597
|
+
async function claimBoardIssues(options, deps = {}) {
|
|
24598
|
+
const cfg = resolveBoardConfig(options.config);
|
|
24599
|
+
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
24600
|
+
const selectors = [];
|
|
24601
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24602
|
+
for (const raw of options.selectors) {
|
|
24603
|
+
const selector = parseIssueSelector(raw, collected.repo, options.repo);
|
|
24604
|
+
const key = `${selector.repo.toLowerCase()}#${selector.number}`;
|
|
24605
|
+
if (seen.has(key)) continue;
|
|
24606
|
+
seen.add(key);
|
|
24607
|
+
selectors.push(selector);
|
|
24608
|
+
}
|
|
24609
|
+
const ctx = await prepareClaimContext(options, selectors, deps, collected, snapshot);
|
|
24610
|
+
const results = await runBulkClaims(ctx, selectors, options);
|
|
24558
24611
|
return {
|
|
24559
24612
|
viewer: ctx.report.viewer,
|
|
24560
24613
|
repo: ctx.report.repo,
|
|
@@ -25980,7 +26033,7 @@ var BLESSED_RUN_WITH_BUDGET_SHA = "cb31a94c89729beac83500ad4d301132dcfe2e4c";
|
|
|
25980
26033
|
var BLESSED_RUNNER_GATE_SHA = "7f9a25df2ce7198c67407b42d0c548f75ce3f430";
|
|
25981
26034
|
var BLESSED_RUNNER_CHECK_POLICY_SHA = "67bb2012710a96d21be825c29a5d5c2b4b8001e2";
|
|
25982
26035
|
var BLESSED_KIT_AGENT_FILES_SHA = "e976df208d891b215844386c1cac0f161946b6fa";
|
|
25983
|
-
var BLESSED_JEV_FAILURES_SHA = "
|
|
26036
|
+
var BLESSED_JEV_FAILURES_SHA = "20ce7d5e0d85cdc7e14bbacbc8b6afdabee6bd41";
|
|
25984
26037
|
var BLESSED_VERDICT_RECEIPT_SHA = "";
|
|
25985
26038
|
var BLESSED_RUNBOOK_GUARD_SHA = "e661e0aedc0e58b73c738bf6cdfeac65e5914584";
|
|
25986
26039
|
var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
|
|
@@ -28248,10 +28301,10 @@ var rollout_plan_default = {
|
|
|
28248
28301
|
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)."
|
|
28249
28302
|
},
|
|
28250
28303
|
baseline: {
|
|
28251
|
-
version: "4.6.
|
|
28252
|
-
tag: "v4.6.
|
|
28253
|
-
commit: "
|
|
28254
|
-
npm: "@mutmutco/cli@4.6.
|
|
28304
|
+
version: "4.6.32",
|
|
28305
|
+
tag: "v4.6.32",
|
|
28306
|
+
commit: "0e477e5e2765",
|
|
28307
|
+
npm: "@mutmutco/cli@4.6.32"
|
|
28255
28308
|
},
|
|
28256
28309
|
exitCriterion: "fleet-n-of-n",
|
|
28257
28310
|
hubOnlyShortcut: "forbidden",
|
|
@@ -28268,14 +28321,14 @@ var rollout_plan_default = {
|
|
|
28268
28321
|
repo: "mutmutco/mmi-hub",
|
|
28269
28322
|
role: "canary",
|
|
28270
28323
|
schedule: "train",
|
|
28271
|
-
v3Target: "v4.6.
|
|
28324
|
+
v3Target: "v4.6.32"
|
|
28272
28325
|
}
|
|
28273
28326
|
],
|
|
28274
28327
|
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.",
|
|
28275
28328
|
rollback: {
|
|
28276
28329
|
independent: true,
|
|
28277
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
28278
|
-
v3Target: "v4.6.
|
|
28330
|
+
mechanism: "npm dist-tag latest -> 4.6.32 and redeploy the Hub Lambda from tag v4.6.32 (0e477e5e2765); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
28331
|
+
v3Target: "v4.6.32 (@mutmutco/cli@4.6.32, tag commit 0e477e5e2765 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
28279
28332
|
}
|
|
28280
28333
|
},
|
|
28281
28334
|
{
|
|
@@ -42455,7 +42508,7 @@ async function runPrLandOnce(prNumber, options, deps) {
|
|
|
42455
42508
|
}
|
|
42456
42509
|
}
|
|
42457
42510
|
const isConflicting = (state) => state.mergeStateStatus === "DIRTY" || state.mergeable === "CONFLICTING";
|
|
42458
|
-
const conflictError = () => `pr land #${prNumber}: branch conflicts with base \u2014 merge origin/development into the branch (never rebase a pushed branch),
|
|
42511
|
+
const conflictError = () => `pr land #${prNumber}: branch conflicts with base \u2014 \`git fetch origin\`, merge origin/development into the branch (never rebase a pushed branch), resolve the conflicts, regenerate any checked-in artifacts, commit and push, then re-run \`mmi-cli devops pr land ${prNumber} --repo ${repo}\` once${hasQueue ? `; ${prQueueReadHint(prNumber, repo)}` : ""}`;
|
|
42459
42512
|
const readPrState = async () => {
|
|
42460
42513
|
const state = await deps.readPrState(prNumber, repo);
|
|
42461
42514
|
if (!isConflicting(state)) return state;
|
|
@@ -42503,6 +42556,9 @@ async function runPrLandOnce(prNumber, options, deps) {
|
|
|
42503
42556
|
base.mergeStatus = merge.mergeStatus;
|
|
42504
42557
|
if (merge.credential) base.credential = merge.credential;
|
|
42505
42558
|
if (merge.credentialRole) base.credentialRole = merge.credentialRole;
|
|
42559
|
+
if (merge.ejectedThenReenqueued) base.ejectedThenReenqueued = true;
|
|
42560
|
+
if (merge.reconcileNeeded) base.reconcileNeeded = true;
|
|
42561
|
+
if (merge.queueRemovalEvidence) base.queueRemovalEvidence = merge.queueRemovalEvidence;
|
|
42506
42562
|
if (merge.mergeStatus === "queue-admission-pending") {
|
|
42507
42563
|
return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
|
|
42508
42564
|
}
|
|
@@ -43674,6 +43730,7 @@ var autoMergeApi = async (pr, repo, head) => {
|
|
|
43674
43730
|
};
|
|
43675
43731
|
var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
|
|
43676
43732
|
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
43733
|
+
var UNWITNESSED_EJECTION_GRACE_MS = 6e4;
|
|
43677
43734
|
function admissionJournal(repo, pr, root) {
|
|
43678
43735
|
const stateRoot = root ?? (process.platform === "win32" ? (0, import_node_path31.join)(process.env.LOCALAPPDATA || (0, import_node_path31.join)((0, import_node_os18.homedir)(), "AppData", "Local"), "MMI Future", "mmi-cli", "queue-admissions") : (0, import_node_path31.join)(process.env.XDG_STATE_HOME || (0, import_node_path31.join)((0, import_node_os18.homedir)(), ".local", "state"), "mmi-cli", "queue-admissions"));
|
|
43679
43736
|
const directory = (0, import_node_path31.join)(stateRoot, (0, import_node_crypto12.createHash)("sha256").update(`${repo.toLowerCase()}#${pr}`).digest("hex"));
|
|
@@ -43687,12 +43744,12 @@ function admissionJournal(repo, pr, root) {
|
|
|
43687
43744
|
const path2 = (number) => (0, import_node_path31.join)(directory, `${number}.jsonl`);
|
|
43688
43745
|
const validate = (value, previous) => {
|
|
43689
43746
|
const row = value;
|
|
43690
|
-
if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.queueWitnessed !== void 0 && typeof row.queueWitnessed !== "boolean" || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
|
|
43747
|
+
if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected", "abandoned"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.queueWitnessed !== void 0 && typeof row.queueWitnessed !== "boolean" || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
|
|
43691
43748
|
throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
|
|
43692
43749
|
}
|
|
43693
43750
|
if (!previous) {
|
|
43694
43751
|
if (row.state !== "dispatching" || row.uuid || row.queueEntry || row.observedAt !== row.createdAt) throw new Error("queue admission intent record is invalid");
|
|
43695
|
-
} else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || previous.credential !== void 0 && row.credential !== previous.credential || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && row.state
|
|
43752
|
+
} else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || previous.credential !== void 0 && row.credential !== previous.credential || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && ["ejected", "abandoned"].includes(row.state) && row.uuid === previous.uuid)) {
|
|
43696
43753
|
throw new Error("queue admission receipt has an illegal transition or changed operation identity");
|
|
43697
43754
|
}
|
|
43698
43755
|
return row;
|
|
@@ -43775,13 +43832,23 @@ function admissionRejected(reason, repo, pr) {
|
|
|
43775
43832
|
};
|
|
43776
43833
|
}
|
|
43777
43834
|
async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
|
|
43778
|
-
|
|
43835
|
+
let reenqueuedAfterEjection = false;
|
|
43836
|
+
let removalEvidence;
|
|
43837
|
+
const markReenqueued = (result) => reenqueuedAfterEjection ? {
|
|
43838
|
+
...result,
|
|
43839
|
+
ejectedThenReenqueued: true,
|
|
43840
|
+
...removalEvidence ? { queueRemovalEvidence: removalEvidence } : {},
|
|
43841
|
+
...result.error ? { error: `re-enqueued after ejection: ${result.error}` } : {}
|
|
43842
|
+
} : result;
|
|
43843
|
+
const unknown = (reason) => markReenqueued({
|
|
43779
43844
|
mergeStatus: "failed",
|
|
43780
43845
|
retryable: false,
|
|
43781
43846
|
outcomeUnknown: true,
|
|
43782
|
-
|
|
43847
|
+
reconcileNeeded: true,
|
|
43848
|
+
...removalEvidence ? { queueRemovalEvidence: removalEvidence } : {},
|
|
43849
|
+
error: `queue submission outcome unknown: ${reason}. Never hand-edit files under the mmi-cli queue-admissions receipt store (%LOCALAPPDATA%/MMI Future/mmi-cli/queue-admissions); read the queue entry with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\`; once the PR is verifiably OPEN at the submitted head and absent from the queue, abandon the stuck admission with \`mmi-cli devops pr land ${pr} --repo ${repo} --abandon-admission\` so a new intent needs no new head \u2014 no new request was sent after this uncertainty.`
|
|
43783
43850
|
});
|
|
43784
|
-
const rejected = (reason) => admissionRejected(reason, repo, pr);
|
|
43851
|
+
const rejected = (reason) => markReenqueued(admissionRejected(reason, repo, pr));
|
|
43785
43852
|
const reconcile = async (reason, recordEntry = false, resumingSaved = false) => {
|
|
43786
43853
|
const maxReads = options.enqueueOnly ? 3 : 1;
|
|
43787
43854
|
const sleep3 = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
@@ -43789,13 +43856,13 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43789
43856
|
if (readAttempt) await sleep3(readAttempt * 1e3);
|
|
43790
43857
|
try {
|
|
43791
43858
|
const observed = await readPrMergeQueue(pr, repo, read);
|
|
43792
|
-
if (observed.state === "MERGED" && observed.headRefOid === head) return { mergeStatus: "merged" };
|
|
43859
|
+
if (observed.state === "MERGED" && observed.headRefOid === head) return markReenqueued({ mergeStatus: "merged" });
|
|
43793
43860
|
if (observed.isInMergeQueue && observed.headRefOid === head) {
|
|
43794
43861
|
const entry = observed.mergeQueueEntry;
|
|
43795
43862
|
if (recordEntry && receipt?.head === head && entry?.headCommit && entry.enqueuedAt) {
|
|
43796
43863
|
journal.save("enqueued", receipt?.uuid, { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt });
|
|
43797
43864
|
}
|
|
43798
|
-
return { mergeStatus: "auto-merge-enqueued" };
|
|
43865
|
+
return markReenqueued({ mergeStatus: "auto-merge-enqueued" });
|
|
43799
43866
|
}
|
|
43800
43867
|
const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
|
|
43801
43868
|
const boundMs = saved?.queueEntry ? Date.parse(saved.queueEntry.enqueuedAt) : resumingSaved && saved?.state === "enqueued" && saved.queueWitnessed === void 0 ? saved.observedAt : void 0;
|
|
@@ -43827,11 +43894,37 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43827
43894
|
let receipt = journal.receipt;
|
|
43828
43895
|
let response;
|
|
43829
43896
|
let superseded = false;
|
|
43830
|
-
if (receipt && (receipt.state === "enqueued" || receipt.state === "merged")
|
|
43831
|
-
|
|
43832
|
-
|
|
43833
|
-
|
|
43834
|
-
|
|
43897
|
+
if (receipt && (receipt.state === "enqueued" || receipt.state === "merged")) {
|
|
43898
|
+
if (receipt.head.toLowerCase() !== head.toLowerCase()) {
|
|
43899
|
+
try {
|
|
43900
|
+
const observed = await readPrMergeQueue(pr, repo, read);
|
|
43901
|
+
superseded = observed.state === "OPEN" && !observed.isInMergeQueue;
|
|
43902
|
+
} catch {
|
|
43903
|
+
}
|
|
43904
|
+
} else {
|
|
43905
|
+
try {
|
|
43906
|
+
const observed = await readPrMergeQueue(pr, repo, read);
|
|
43907
|
+
if (observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid?.toLowerCase() === receipt.head.toLowerCase()) {
|
|
43908
|
+
const boundMs = receipt.queueEntry ? Date.parse(receipt.queueEntry.enqueuedAt) : receipt.observedAt + UNWITNESSED_EJECTION_GRACE_MS;
|
|
43909
|
+
const events = await readArrayPages(
|
|
43910
|
+
`repos/${repo}/issues/${pr}/timeline?per_page=100`,
|
|
43911
|
+
options.timelineRead ?? read,
|
|
43912
|
+
"queue ejection timeline unreadable"
|
|
43913
|
+
);
|
|
43914
|
+
const removals = events.filter((event) => event.event === "removed_from_merge_queue" && Number.isFinite(Date.parse(event.created_at))).map((event) => Date.parse(event.created_at)).filter((at) => at > boundMs).sort((a, b) => a - b);
|
|
43915
|
+
removalEvidence = {
|
|
43916
|
+
source: receipt.queueEntry ? "queue-entry" : "receipt-checkpoint",
|
|
43917
|
+
boundAt: new Date(boundMs).toISOString(),
|
|
43918
|
+
removalsAfterBound: removals.map((at) => new Date(at).toISOString()),
|
|
43919
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
43920
|
+
};
|
|
43921
|
+
if (removals.length > 0) {
|
|
43922
|
+
reenqueuedAfterEjection = true;
|
|
43923
|
+
superseded = true;
|
|
43924
|
+
}
|
|
43925
|
+
}
|
|
43926
|
+
} catch {
|
|
43927
|
+
}
|
|
43835
43928
|
}
|
|
43836
43929
|
}
|
|
43837
43930
|
if (superseded && receipt?.state === "enqueued") {
|
|
@@ -43841,7 +43934,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43841
43934
|
return unknown("the superseded admission could not be checkpointed");
|
|
43842
43935
|
}
|
|
43843
43936
|
}
|
|
43844
|
-
if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && !superseded) {
|
|
43937
|
+
if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && receipt.state !== "abandoned" && !superseded) {
|
|
43845
43938
|
if (options.selection) {
|
|
43846
43939
|
options.selection.selected = true;
|
|
43847
43940
|
options.selection.intentId = receipt.intentId;
|
|
@@ -43987,13 +44080,13 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43987
44080
|
if (receipt.head !== head) return reconcile("the previous head completed admission; inspect current head and membership before continuing");
|
|
43988
44081
|
return reconcile(`operation ${receipt.uuid} reported ${status}, but matching-head membership or merged state is unconfirmed`, status === "enqueued");
|
|
43989
44082
|
}
|
|
43990
|
-
return {
|
|
44083
|
+
return markReenqueued({
|
|
43991
44084
|
mergeStatus: "queue-admission-pending",
|
|
43992
44085
|
retryable: false,
|
|
43993
44086
|
outcomeUnknown: true,
|
|
43994
44087
|
queueAdmission: { state: "pending", uuid: receipt.uuid, head: receipt.head },
|
|
43995
44088
|
error: "GitHub accepted queue admission but has not confirmed queue membership. Rerun pr land to resume the saved operation read-only."
|
|
43996
|
-
};
|
|
44089
|
+
});
|
|
43997
44090
|
}
|
|
43998
44091
|
function githubQueueSquashMessage(title, pr, commits) {
|
|
43999
44092
|
const list = commits.map((commit) => `* ${commit.headline}
|
|
@@ -44068,13 +44161,13 @@ async function inspectQueuePreparation(pr, repo, options) {
|
|
|
44068
44161
|
const parsed = parseRestClosingGuardInput({ state: pull.state, title: pull.title, body: pull.body }, commits, repo);
|
|
44069
44162
|
parsed.closing = closing;
|
|
44070
44163
|
const policy = { title: repository.squash_merge_commit_title, body: repository.squash_merge_commit_message };
|
|
44071
|
-
const preparation = (reason, targets, detail) => ({
|
|
44164
|
+
const preparation = (reason, targets, detail, nextAction) => ({
|
|
44072
44165
|
mergeStatus: "failed",
|
|
44073
44166
|
preparation: {
|
|
44074
44167
|
status: "preparation-needed",
|
|
44075
44168
|
reason,
|
|
44076
44169
|
issueTargets: targets.map((number) => `${repo}#${number}`),
|
|
44077
|
-
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."
|
|
44170
|
+
nextAction: 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.")
|
|
44078
44171
|
},
|
|
44079
44172
|
error: `queue preparation needed: ${detail}`
|
|
44080
44173
|
});
|
|
@@ -44157,7 +44250,17 @@ ${pull.body}`;
|
|
|
44157
44250
|
}
|
|
44158
44251
|
};
|
|
44159
44252
|
}
|
|
44160
|
-
|
|
44253
|
+
const trailerKeys = (message2) => {
|
|
44254
|
+
const keys = [...new Set(parseTrailers(message2.trim(), process.cwd()).map((trailer) => trailer.split(":")[0].trim()))];
|
|
44255
|
+
return keys.length ? keys.join(", ") : "none";
|
|
44256
|
+
};
|
|
44257
|
+
const source = options.bodyText != null ? "the explicit body override" : "the body composed from the source commit messages";
|
|
44258
|
+
const detail = `the requested sanitized body or trailer arrangement cannot be sent to queue admission: queue merge method ${method}, squash body policy ${policy.body ?? "unknown"}; required body is ${source} (trailer keys: ${trailerKeys(`${headline}
|
|
44259
|
+
|
|
44260
|
+
${requiredBody}`)}), but the message the queue would send carries trailer keys: ${trailerKeys(actualMessage)}`;
|
|
44261
|
+
const next = options.bodyText != null ? `PR author: a merge queue accepts no body override; drop --body/--squash-body-file and place the required trailer(s) where the queue's ${method}${method === "SQUASH" ? ` (${policy.body})` : ""} message reads them, then rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\`. Keep repository settings unchanged.` : method !== "SQUASH" ? `PR author: the queue's ${method} method keeps the original commit messages, so the required trailer(s) must sit in a source commit message's final trailer block; amend the commit, push, re-run checks and \`mmi-cli devops pr land ${pr} --repo ${repo}\`. Keep repository settings unchanged.` : policy.body === "BLANK" ? `PR author: squash body policy BLANK sends an empty body, so no trailer from the commits or PR body reaches the queue commit; the waiver cannot survive queue admission with unchanged repository settings \u2014 remove the need for the override or ask a maintainer. Keep repository settings unchanged.` : `PR author: squash body policy ${policy.body} does not reproduce the composed body; consolidate the required trailer(s) into one source commit message's final trailer block, push, re-run checks and \`mmi-cli devops pr land ${pr} --repo ${repo}\`. Keep repository settings unchanged.`;
|
|
44262
|
+
const caveat = " 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 shared trailer reader.";
|
|
44263
|
+
return { kind: "refused", result: preparation("queue-message-override", [], detail, next + caveat) };
|
|
44161
44264
|
}
|
|
44162
44265
|
return { kind: "ready", head, pull, read, queueRead, write, ...verdict.message ? { warning: verdict.message } : {} };
|
|
44163
44266
|
}
|
|
@@ -44188,6 +44291,38 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
44188
44291
|
const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
|
|
44189
44292
|
return { ...stamp(result, selection.selected, selection.intentId), ...inspected.warning ? { warning: inspected.warning } : {} };
|
|
44190
44293
|
}
|
|
44294
|
+
async function abandonQueueAdmission(pr, repo, options = {}) {
|
|
44295
|
+
if (!/^\d+$/.test(pr)) return { status: "refused", repo, pr, reason: "a bare PR number is required" };
|
|
44296
|
+
const refuse = (reason) => ({ status: "refused", repo, pr, reason });
|
|
44297
|
+
let journal;
|
|
44298
|
+
try {
|
|
44299
|
+
journal = admissionJournal(repo, pr, options.receiptDirectory);
|
|
44300
|
+
} catch (error) {
|
|
44301
|
+
return refuse(`the durable admission receipt cannot be read (${error.message})`);
|
|
44302
|
+
}
|
|
44303
|
+
const receipt = journal.receipt;
|
|
44304
|
+
if (!receipt) return refuse("no queue-admission receipt exists for this PR; nothing to abandon");
|
|
44305
|
+
if (!["pending", "enqueued"].includes(receipt.state)) {
|
|
44306
|
+
return refuse(`the saved admission is '${receipt.state}', not a stuck non-terminal 'pending'/'enqueued' receipt; nothing to abandon`);
|
|
44307
|
+
}
|
|
44308
|
+
let snapshot;
|
|
44309
|
+
try {
|
|
44310
|
+
snapshot = await readPrMergeQueue(pr, repo, options.read ?? actorReadApi);
|
|
44311
|
+
} catch (error) {
|
|
44312
|
+
return refuse(`the PR and queue state could not be read (${error.message}); the admission is unchanged`);
|
|
44313
|
+
}
|
|
44314
|
+
if (snapshot.state !== "OPEN") return refuse(`the PR is '${snapshot.state}', not OPEN; the admission is unchanged`);
|
|
44315
|
+
if (!snapshot.headRefOid || snapshot.headRefOid.toLowerCase() !== receipt.head.toLowerCase()) {
|
|
44316
|
+
return refuse(`the PR head is ${snapshot.headRefOid ?? "unreadable"}, not the receipt's submitted head ${receipt.head}; the admission is unchanged`);
|
|
44317
|
+
}
|
|
44318
|
+
if (snapshot.isInMergeQueue) return refuse("the PR is currently in the merge queue, so the admission is not stuck; the admission is unchanged");
|
|
44319
|
+
try {
|
|
44320
|
+
journal.save("abandoned");
|
|
44321
|
+
} catch (error) {
|
|
44322
|
+
return refuse(`the abandoned state could not be checkpointed (${error.message}); the admission is unchanged`);
|
|
44323
|
+
}
|
|
44324
|
+
return { status: "abandoned", repo, pr, head: receipt.head, ...receipt.uuid ? { operation: receipt.uuid } : {} };
|
|
44325
|
+
}
|
|
44191
44326
|
function closingGuardDiagnostic(message2) {
|
|
44192
44327
|
if (!message2) return "";
|
|
44193
44328
|
const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
|
|
@@ -46100,7 +46235,7 @@ function refuseNotClaimable(e, json) {
|
|
|
46100
46235
|
process.exitCode = 1;
|
|
46101
46236
|
return true;
|
|
46102
46237
|
}
|
|
46103
|
-
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\nA zero-byte or empty .jerv/tmp/*.json means the read FAILED \u2014 `Unexpected end of JSON input` is\nthe symptom of shell-redirected empty stdout, not malformed JSON \u2014 re-run with --out (which writes\nthe file only on success) and check the exit code before parsing.\n\nChanging an existing item's Status is a move, never a hand-written `gh api graphql`\n`updateProjectV2ItemFieldValue` mutation with copied `PVT_`/`PVTI_` node ids and a manual Status\nfield-id lookup:\n mmi-cli oracle board move <status> <ref>\n";
|
|
46238
|
+
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\nNever sniff the envelope \u2014 `j.items ?? j.rows ?? j`, `Object.values(j)` or any generic \"find the list\"\nwrapper throws `items is not iterable` before any row is read (#9956). For a number|status|claim|title\nprojection across non-Done rows, read --json --flat --out .jerv/tmp/board.json and filter that array locally:\n mmi-cli oracle board read --json --flat --out .jerv/tmp/board.json\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\nA zero-byte or empty .jerv/tmp/*.json means the read FAILED \u2014 `Unexpected end of JSON input` is\nthe symptom of shell-redirected empty stdout, not malformed JSON \u2014 re-run with --out (which writes\nthe file only on success) and check the exit code before parsing.\n\nChanging an existing item's Status is a move, never a hand-written `gh api graphql`\n`updateProjectV2ItemFieldValue` mutation with copied `PVT_`/`PVTI_` node ids and a manual Status\nfield-id lookup:\n mmi-cli oracle board move <status> <ref>\n";
|
|
46104
46239
|
function flattenBoardReport(report) {
|
|
46105
46240
|
const closingByRef = /* @__PURE__ */ new Map();
|
|
46106
46241
|
for (const pull of report.pullRequests ?? []) {
|
|
@@ -46223,9 +46358,41 @@ function registerBoardCommands(program3) {
|
|
|
46223
46358
|
}
|
|
46224
46359
|
});
|
|
46225
46360
|
withExamples(mutating(
|
|
46226
|
-
board.command("claim
|
|
46361
|
+
board.command("claim [issues...]").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs, or --all for the whole board); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n\nreclaim from In Review (#6339): an In Review item with no holder, or one held by the claiming\nlogin, is claimable \u2014 it moves back to In Progress and the receipt names the status it came from.\nAn In Review item another login holds is refused (ask the holder, or wait for the review to land),\nand Done stays refused. The board status is the authority; no PR state is consulted.\n\nwhole board (#9930): --all reads the board once and claims every open item of this repo's primary\nboard that the per-ref claim may take (yours, unassigned Todo, unowned In Progress), carrying each\nitem's branch/worktree lane evidence forward like a per-ref claim. It prints one line per item, then\nthe refs it skipped and why (assigned to another login, In Review, closed, not an issue). Without\n--force a live foreign lane is skipped with its reason, never stolen; --force takes it as the\nper-ref --force would. --all with explicit refs is an error.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--all", "claim every open claimable item on the current repo's board in one pass (combine with --force/--repo; not with explicit refs) (#9930)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
46227
46362
|
(_opts, args) => ({ command: "oracle board claim", issues: args[0] ?? [] })
|
|
46228
|
-
).action(async (issueRefs, o) => {
|
|
46363
|
+
).action(async (issueRefs = [], o) => {
|
|
46364
|
+
const argsError = claimArgsError(issueRefs, o.all);
|
|
46365
|
+
if (argsError) return failGraceful(`oracle board claim failed: ${argsError}`);
|
|
46366
|
+
if (o.all) {
|
|
46367
|
+
try {
|
|
46368
|
+
const config = await loadConfigForBoardSelector2("", o.repo);
|
|
46369
|
+
const all = await claimAllBoardIssues({
|
|
46370
|
+
config,
|
|
46371
|
+
repo: o.repo,
|
|
46372
|
+
assignee: o.for,
|
|
46373
|
+
force: o.force,
|
|
46374
|
+
check: o.check,
|
|
46375
|
+
allowPartial: o.allowPartial
|
|
46376
|
+
}, { snapshot: registryClientDeps(config) });
|
|
46377
|
+
if (o.json) {
|
|
46378
|
+
console.log(JSON.stringify({ results: all.results, skipped: all.skipped }));
|
|
46379
|
+
printClaimWarnings(all.warnings, true);
|
|
46380
|
+
} else {
|
|
46381
|
+
for (const result of all.results) {
|
|
46382
|
+
console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
|
|
46383
|
+
}
|
|
46384
|
+
for (const skip of all.skipped) console.log(`Skipped ${skip.ref}: ${skip.reason}`);
|
|
46385
|
+
const landed = all.results.filter((r) => r.claimed).length;
|
|
46386
|
+
console.log(`claim --all: ${landed} of ${all.results.length} attempted item(s) claimed; ${all.skipped.length} not attempted`);
|
|
46387
|
+
printClaimWarnings(all.warnings);
|
|
46388
|
+
}
|
|
46389
|
+
if (all.failed > 0) process.exitCode = 1;
|
|
46390
|
+
} catch (e) {
|
|
46391
|
+
if (refuseRateLimited(e, o.json)) return;
|
|
46392
|
+
return failGraceful(`oracle board claim failed: ${e.message}`);
|
|
46393
|
+
}
|
|
46394
|
+
return;
|
|
46395
|
+
}
|
|
46229
46396
|
if (issueRefs.length === 1) {
|
|
46230
46397
|
const issueRef = issueRefs[0];
|
|
46231
46398
|
try {
|
|
@@ -46284,11 +46451,13 @@ function registerBoardCommands(program3) {
|
|
|
46284
46451
|
}), [
|
|
46285
46452
|
"mmi-cli oracle board claim 2680",
|
|
46286
46453
|
"mmi-cli oracle board claim 2680 2681 --for teammate-login",
|
|
46287
|
-
"mmi-cli oracle board claim 2680 --check"
|
|
46454
|
+
"mmi-cli oracle board claim 2680 --check",
|
|
46455
|
+
"mmi-cli oracle board claim --all --force"
|
|
46288
46456
|
], [
|
|
46289
46457
|
"Pass raw issue numbers/refs, not URLs.",
|
|
46290
46458
|
"Claim already assigns and moves Status to In Progress, so do not also board move it.",
|
|
46291
46459
|
"Multiple refs are handled as a batch and return per-item results.",
|
|
46460
|
+
"To claim the whole board, use --all (with --force to take live lanes) instead of a hand-built ref list.",
|
|
46292
46461
|
"--check is the live gate read (it calls GitHub); --dry-run only echoes the parsed argv plan.",
|
|
46293
46462
|
// #5552: agents guess `oracle issue claim`; that route does not exist — board claim is the only write.
|
|
46294
46463
|
"Never run `oracle issue claim` \u2014 claims are board mutations; only `oracle board claim <ref>` is valid.",
|
|
@@ -60798,23 +60967,53 @@ async function checkDocsIndexAtHead(opts, deps) {
|
|
|
60798
60967
|
}
|
|
60799
60968
|
|
|
60800
60969
|
// src/pr-create-preparation.ts
|
|
60970
|
+
function rejectionReason(error) {
|
|
60971
|
+
const text = (error instanceof Error ? error.message : String(error)).trim().split("\n")[0] || "no error detail";
|
|
60972
|
+
return text.length > 300 ? `${text.slice(0, 300)}\u2026` : text;
|
|
60973
|
+
}
|
|
60974
|
+
var PERMISSION_REFUSAL = /Resource not accessible|HTTP 40[13]|40[13] (?:Forbidden|Unauthorized)|Bad credentials|must have admin rights|insufficient (?:permissions?|scope)|not authorized/i;
|
|
60975
|
+
async function observePolicy(deps, candidate) {
|
|
60976
|
+
if (!candidate) return void 0;
|
|
60977
|
+
try {
|
|
60978
|
+
return await deps.policy(candidate);
|
|
60979
|
+
} catch (error) {
|
|
60980
|
+
const refusal = rejectionReason(error);
|
|
60981
|
+
if (!PERMISSION_REFUSAL.test(refusal)) throw error;
|
|
60982
|
+
await deps.fetchCandidate?.(candidate).catch(() => void 0);
|
|
60983
|
+
let retried;
|
|
60984
|
+
try {
|
|
60985
|
+
retried = await deps.policy(candidate);
|
|
60986
|
+
} catch (retryError) {
|
|
60987
|
+
const second = rejectionReason(retryError);
|
|
60988
|
+
if (!PERMISSION_REFUSAL.test(second)) throw retryError;
|
|
60989
|
+
return {
|
|
60990
|
+
check: "policy",
|
|
60991
|
+
state: "skipped",
|
|
60992
|
+
detail: `committed test policy was not observed: GitHub refused the policy read twice (${second}) \u2014 the identity running pr create lacks the permission that observation needs, repository Administration: read, which covers rulesets and the merge-queue configuration; grant it to the App installation (APP_POLL_PERMISSIONS or the installed App's permissions on ${candidate.repo}) or run with an owner GH_TOKEN. This is an unreadable guard, NOT a policy violation.`
|
|
60993
|
+
};
|
|
60994
|
+
}
|
|
60995
|
+
process.stderr.write(`pr create: WARNING \u2014 the committed test-policy read was refused (${refusal}); re-fetched the candidate objects and retried it once (#9946)
|
|
60996
|
+
`);
|
|
60997
|
+
return retried;
|
|
60998
|
+
}
|
|
60999
|
+
}
|
|
60801
61000
|
async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
60802
61001
|
const observations = [];
|
|
60803
|
-
const refs = await deps.candidate().catch(() => ({ findings: [
|
|
61002
|
+
const refs = await deps.candidate().catch((error) => ({ findings: [`candidate refs unavailable (${rejectionReason(error)}); verify the repository and pushed head/base before retrying`] }));
|
|
60804
61003
|
observations.push(...refs.findings.map((detail) => ({ check: "refs", state: "unavailable", detail })));
|
|
60805
61004
|
if (refs.candidate) observations.push({ check: "refs", state: "passed", detail: `candidate ${refs.candidate.headSha} against ${refs.candidate.baseSha}` });
|
|
60806
61005
|
const reads = await Promise.allSettled([
|
|
60807
61006
|
deps.claims(refs.candidate),
|
|
60808
61007
|
refs.candidate ? deps.docs(refs.candidate) : Promise.resolve(void 0),
|
|
60809
|
-
|
|
61008
|
+
observePolicy(deps, refs.candidate)
|
|
60810
61009
|
]);
|
|
60811
61010
|
const [claims, docs, policy] = reads;
|
|
60812
61011
|
if (claims.status === "fulfilled") {
|
|
60813
61012
|
observations.push(...claims.value.findings.length ? claims.value.findings.map((detail) => ({ check: "claims", state: "failed", detail })) : [{ check: "claims", state: "passed", detail: "issue and claim evidence read" }]);
|
|
60814
|
-
} else observations.push({ check: "claims", state: "unavailable", detail:
|
|
60815
|
-
if (docs.status === "rejected") observations.push({ check: "docs", state: "unavailable", detail:
|
|
61013
|
+
} else observations.push({ check: "claims", state: "unavailable", detail: `issue/claim evidence unavailable (${rejectionReason(claims.reason)}); retry the read` });
|
|
61014
|
+
if (docs.status === "rejected") observations.push({ check: "docs", state: "unavailable", detail: `committed docs observation unavailable (${rejectionReason(docs.reason)}); retry the read` });
|
|
60816
61015
|
else if (docs.value) observations.push({ check: "docs", state: docs.value.state, detail: [docs.value.detail, docs.value.fix].filter(Boolean).join(" \u2014 ") });
|
|
60817
|
-
if (policy.status === "rejected") observations.push({ check: "policy", state: "unavailable", detail:
|
|
61016
|
+
if (policy.status === "rejected") observations.push({ check: "policy", state: "unavailable", detail: `committed test-policy observation unavailable (${rejectionReason(policy.reason)}); check policy validity and fetch full candidate history before retrying` });
|
|
60818
61017
|
else if (policy.value) observations.push(policy.value);
|
|
60819
61018
|
const findings = observations.filter((item) => item.state === "failed" || item.state === "unavailable");
|
|
60820
61019
|
let attempted = false;
|
|
@@ -60843,7 +61042,7 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
|
60843
61042
|
await claims.value.restamp();
|
|
60844
61043
|
const beforeCreate = await deps.candidate();
|
|
60845
61044
|
if (beforeCreate.findings.length || JSON.stringify(beforeCreate.candidate) !== JSON.stringify(expected)) {
|
|
60846
|
-
throw new Error("pr create: candidate changed while restamping claims; prepare
|
|
61045
|
+
throw new Error("pr create: candidate identity changed during preparation while restamping claims; prepare the current head and base again");
|
|
60847
61046
|
}
|
|
60848
61047
|
}
|
|
60849
61048
|
};
|
|
@@ -60916,6 +61115,15 @@ function createPreparationDeps(input, io = {}) {
|
|
|
60916
61115
|
return {
|
|
60917
61116
|
candidate: readCandidate,
|
|
60918
61117
|
publish: (proof) => publishPreparedPrHead(proof, exec3),
|
|
61118
|
+
// #9946: the retry needs the exact candidate objects. Fetch each ref on its own so a fork-only head
|
|
61119
|
+
// that origin cannot resolve never costs the base fetch too, and never surface a fetch failure as the
|
|
61120
|
+
// reason the policy read stayed unreadable.
|
|
61121
|
+
fetchCandidate: async (candidate) => {
|
|
61122
|
+
if (!candidate.root) return;
|
|
61123
|
+
const branch = (name) => name.slice(name.lastIndexOf(":") + 1);
|
|
61124
|
+
const refspecs = [.../* @__PURE__ */ new Set([candidate.head, candidate.base])].map((name) => `+refs/heads/${branch(name)}:refs/remotes/origin/${branch(name)}`);
|
|
61125
|
+
await Promise.allSettled(refspecs.map((refspec) => exec3("git", ["-C", candidate.root, "fetch", "--no-tags", "origin", refspec], { timeout: GIT_TIMEOUT_MS })));
|
|
61126
|
+
},
|
|
60919
61127
|
claims: (candidate) => observePrCreateClaims(input.body, candidate?.repo ?? input.repo, { head: candidate?.head, headSha: candidate?.headSha }),
|
|
60920
61128
|
docs: (candidate) => checkDocsIndexAtHead({ repo: candidate.repo, head: candidate.headSha }, docs),
|
|
60921
61129
|
policy: async (candidate) => {
|
|
@@ -60946,7 +61154,12 @@ function createPreparationDeps(input, io = {}) {
|
|
|
60946
61154
|
return result("failed", `${refusal.error} \u2014 ${refusal.preparation.nextAction}`);
|
|
60947
61155
|
}
|
|
60948
61156
|
}
|
|
60949
|
-
|
|
61157
|
+
let policy;
|
|
61158
|
+
try {
|
|
61159
|
+
policy = runTestPolicy(candidate.root, { candidate: { head: candidate.headSha, base: candidate.baseSha } });
|
|
61160
|
+
} catch (error) {
|
|
61161
|
+
return result("unavailable", `committed test policy could not be evaluated: ${rejectionReason(error)} \u2014 fix test-policy.json if it is malformed, or run \`git fetch --unshallow\` if a history read failed`);
|
|
61162
|
+
}
|
|
60950
61163
|
return result(policy.ok ? "passed" : "failed", policy.ok ? `committed test policy passed at ${candidate.headSha}` : policy.findings.map((finding) => finding.detail).join("\n"));
|
|
60951
61164
|
}
|
|
60952
61165
|
};
|
|
@@ -62566,6 +62779,46 @@ var RESIDUE_LEDGER_FILE = "mmi-worktree-residue.json";
|
|
|
62566
62779
|
function canonicalWorktreeRoot(primary) {
|
|
62567
62780
|
return (0, import_node_path49.join)((0, import_node_path49.dirname)(primary), "mmi-worktrees", (0, import_node_path49.basename)(primary));
|
|
62568
62781
|
}
|
|
62782
|
+
function lastSegment(p) {
|
|
62783
|
+
return (0, import_node_path49.basename)(p.split(/[\\/]+/).filter(Boolean).pop() ?? p);
|
|
62784
|
+
}
|
|
62785
|
+
function readProvenance(folder, gitCommonDir, exists) {
|
|
62786
|
+
const found = {
|
|
62787
|
+
path: folder,
|
|
62788
|
+
gitdirPresent: false,
|
|
62789
|
+
registrationId: (0, import_node_path49.basename)(folder),
|
|
62790
|
+
registrationPresent: false
|
|
62791
|
+
};
|
|
62792
|
+
try {
|
|
62793
|
+
const pointer = /^\s*gitdir:\s*(.+?)\s*$/m.exec((0, import_node_fs53.readFileSync)((0, import_node_path49.join)(folder, ".git"), "utf8"));
|
|
62794
|
+
if (pointer) {
|
|
62795
|
+
const target = pointer[1];
|
|
62796
|
+
found.gitdir = (0, import_node_path49.isAbsolute)(target) ? target : (0, import_node_path49.join)(folder, target);
|
|
62797
|
+
found.gitdirPresent = exists(found.gitdir);
|
|
62798
|
+
found.registrationId = lastSegment(found.gitdir);
|
|
62799
|
+
}
|
|
62800
|
+
} catch {
|
|
62801
|
+
}
|
|
62802
|
+
try {
|
|
62803
|
+
found.registrationPresent = exists((0, import_node_path49.join)(gitCommonDir, "worktrees", found.registrationId));
|
|
62804
|
+
} catch {
|
|
62805
|
+
}
|
|
62806
|
+
if (found.gitdir && found.gitdirPresent) {
|
|
62807
|
+
try {
|
|
62808
|
+
const head = (0, import_node_fs53.readFileSync)((0, import_node_path49.join)(found.gitdir, "HEAD"), "utf8").trim();
|
|
62809
|
+
const branch = /^ref:\s*(.+?)\s*$/.exec(head);
|
|
62810
|
+
if (branch) found.head = branch[1].startsWith("refs/heads/") ? branch[1].slice("refs/heads/".length) : `ref ${branch[1]}`;
|
|
62811
|
+
else if (/^[0-9a-f]{7,64}$/i.test(head)) found.head = `detached ${head.slice(0, 12)}`;
|
|
62812
|
+
else if (head) found.head = `ref ${head}`;
|
|
62813
|
+
} catch {
|
|
62814
|
+
}
|
|
62815
|
+
}
|
|
62816
|
+
try {
|
|
62817
|
+
found.mtime = (0, import_node_fs53.statSync)(folder).mtime.toISOString();
|
|
62818
|
+
} catch {
|
|
62819
|
+
}
|
|
62820
|
+
return found;
|
|
62821
|
+
}
|
|
62569
62822
|
function norm2(p) {
|
|
62570
62823
|
const s = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
62571
62824
|
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
@@ -62660,7 +62913,7 @@ async function sweepWorktreeResidue(gitCommonDir, registered, deps = {}) {
|
|
|
62660
62913
|
} catch {
|
|
62661
62914
|
}
|
|
62662
62915
|
} else {
|
|
62663
|
-
report.unrecorded.push(child2);
|
|
62916
|
+
report.unrecorded.push(readProvenance(child2, gitCommonDir, exists));
|
|
62664
62917
|
}
|
|
62665
62918
|
}
|
|
62666
62919
|
}
|
|
@@ -62674,12 +62927,19 @@ function renderResidueSweepLines(report) {
|
|
|
62674
62927
|
if (cleaned) lines2.push(`pr merge: removed ${cleaned} leftover worktree folder(s) from earlier merges${where}`);
|
|
62675
62928
|
for (const k of report.kept) lines2.push(`pr merge: leftover folder still locked, retried on the next merge \u2014 ${k.path}`);
|
|
62676
62929
|
if (report.unrecorded.length) {
|
|
62677
|
-
const
|
|
62930
|
+
const listed = report.unrecorded.slice(0, 5);
|
|
62678
62931
|
const more = report.unrecorded.length > 5 ? `, \u2026 ${report.unrecorded.length - 5} more` : "";
|
|
62679
|
-
lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root${where} were left untouched (not recorded by a merge): ${
|
|
62932
|
+
lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root${where} were left untouched (not recorded by a merge): ${listed.map((u) => u.path).join(", ")}${more}`);
|
|
62933
|
+
for (const u of listed) lines2.push(`pr merge: left untouched ${u.path} \u2014 ${provenanceText(u)}`);
|
|
62680
62934
|
}
|
|
62681
62935
|
return lines2;
|
|
62682
62936
|
}
|
|
62937
|
+
function provenanceText(u) {
|
|
62938
|
+
const pointer = u.gitdir ? `.git \u2192 ${u.gitdir}${u.gitdirPresent ? "" : " (admin dir gone)"}` : ".git pointer missing";
|
|
62939
|
+
const registration = `worktrees/${u.registrationId} ${u.registrationPresent ? "registered" : "not registered"}`;
|
|
62940
|
+
const head = u.head ? `HEAD ${u.head}` : "HEAD unknown";
|
|
62941
|
+
return `${pointer}; ${registration}; ${head}; ${u.mtime ? `modified ${u.mtime}` : "mtime unknown"}`;
|
|
62942
|
+
}
|
|
62683
62943
|
|
|
62684
62944
|
// src/command-register-collaboration.ts
|
|
62685
62945
|
var DEFAULT_PRIORITY = "medium";
|
|
@@ -63829,9 +64089,19 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
63829
64089
|
}
|
|
63830
64090
|
class PrHeadBehindBaseError extends Error {
|
|
63831
64091
|
}
|
|
63832
|
-
pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
|
|
64092
|
+
pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--abandon-admission", "one stuck-receipt recovery, one PR per call: journal the saved non-terminal (pending/enqueued) queue-admission receipt terminal ONLY after positively observing the PR OPEN at the receipt head and absent from the queue; any unprovable observation refuses and changes nothing (#9951). The next pr land then submits a new intent with no new head").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
|
|
63833
64093
|
const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
|
|
63834
64094
|
if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
|
|
64095
|
+
if (opts.abandonAdmission) {
|
|
64096
|
+
if (numbers.length !== 1) return fail("pr land: --abandon-admission abandons one PR per call");
|
|
64097
|
+
const repo = await resolveRepo(opts.repo).catch(() => void 0) ?? opts.repo;
|
|
64098
|
+
if (!repo) return fail("pr land: --abandon-admission needs --repo <owner/repo>; the PR repo could not be resolved");
|
|
64099
|
+
const outcome = await abandonQueueAdmission(numbers[0], repo);
|
|
64100
|
+
if (opts.json) printLine(JSON.stringify(outcome));
|
|
64101
|
+
else printLine(outcome.status === "abandoned" ? `pr land: abandoned the stuck queue admission of PR #${outcome.pr} on ${repo}${outcome.operation ? ` (operation ${outcome.operation})` : ""} at head ${outcome.head} \u2014 the next pr land submits a new intent with no new head` : `pr land: refused to abandon the queue admission of PR #${outcome.pr} on ${repo} \u2014 ${outcome.reason}`);
|
|
64102
|
+
if (outcome.status !== "abandoned") process.exitCode = 1;
|
|
64103
|
+
return;
|
|
64104
|
+
}
|
|
63835
64105
|
if (opts.expectHead !== void 0) {
|
|
63836
64106
|
if (!/^[0-9a-f]{40}$/i.test(opts.expectHead)) return fail("pr land: --expect-head needs the full 40-hex head SHA");
|
|
63837
64107
|
if (numbers.length !== 1) return fail("pr land: --expect-head pins one PR head; land one PR per call");
|
|
@@ -64976,9 +65246,13 @@ function projectInfoOutcome(projectInfoSync) {
|
|
|
64976
65246
|
}
|
|
64977
65247
|
function buildReleaseVerdict(commandName, result, projectInfoSync) {
|
|
64978
65248
|
const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
|
|
65249
|
+
const deployOutcome = reduceFollowUpOutcomes([
|
|
65250
|
+
deployFollowUpOutcome(result.deployStatus),
|
|
65251
|
+
deployFollowUpOutcome(aggregateWorkflowRuns(result.workflowRuns ?? []))
|
|
65252
|
+
]);
|
|
64979
65253
|
const followUpStatus = deriveTrainFollowUpStatus({
|
|
64980
65254
|
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
64981
|
-
deploy:
|
|
65255
|
+
deploy: deployOutcome,
|
|
64982
65256
|
rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
|
|
64983
65257
|
alignment: alignmentPending ? "unresolved" : "ok",
|
|
64984
65258
|
foldPort: "ok"
|
|
@@ -64998,9 +65272,14 @@ function buildReleaseVerdict(commandName, result, projectInfoSync) {
|
|
|
64998
65272
|
}
|
|
64999
65273
|
function buildResumeReleaseVerdict(result, projectInfoSync) {
|
|
65000
65274
|
const resumeStatus = resumeFollowUpOf(result.state);
|
|
65275
|
+
const deployOutcome = reduceFollowUpOutcomes([
|
|
65276
|
+
// No dispatch (e.g. an alignment-only resume) keeps the old 'ok' deploy dimension.
|
|
65277
|
+
deployFollowUpOutcome(result.dispatch?.deployStatus ?? "success"),
|
|
65278
|
+
deployFollowUpOutcome(aggregateWorkflowRuns(result.dispatch?.workflowRuns ?? []))
|
|
65279
|
+
]);
|
|
65001
65280
|
const followUpStatus = deriveTrainFollowUpStatus({
|
|
65002
65281
|
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
65003
|
-
deploy:
|
|
65282
|
+
deploy: deployOutcome,
|
|
65004
65283
|
rcRetirement: "ok",
|
|
65005
65284
|
alignment: resumeStatus === "failed" ? "failure" : resumeStatus === "pending" ? "unresolved" : "ok",
|
|
65006
65285
|
foldPort: "ok"
|
|
@@ -71440,6 +71719,54 @@ function renderTenantLogs(r) {
|
|
|
71440
71719
|
return out.join("\n");
|
|
71441
71720
|
}
|
|
71442
71721
|
|
|
71722
|
+
// src/tenant-status-wait.ts
|
|
71723
|
+
var TENANT_STATUS_WAIT_INTERVAL_MS = 12e3;
|
|
71724
|
+
var TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S = 660;
|
|
71725
|
+
var TENANT_STATUS_WAIT_TIMEOUT_MAX_S = 1800;
|
|
71726
|
+
var TENANT_STATUS_WAIT_MIN_SHA_LENGTH = 7;
|
|
71727
|
+
function parseWaitForSha(raw) {
|
|
71728
|
+
const sha = raw.trim().toLowerCase();
|
|
71729
|
+
if (!new RegExp(`^[0-9a-f]{${TENANT_STATUS_WAIT_MIN_SHA_LENGTH},40}$`).test(sha)) {
|
|
71730
|
+
return { ok: false, reason: `--wait-for must be a commit SHA (${TENANT_STATUS_WAIT_MIN_SHA_LENGTH}-40 hex characters), got "${raw.trim()}"` };
|
|
71731
|
+
}
|
|
71732
|
+
return { ok: true, sha };
|
|
71733
|
+
}
|
|
71734
|
+
function parseWaitTimeoutSeconds(raw) {
|
|
71735
|
+
if (raw === void 0) return { ok: true, seconds: TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S, clamped: false };
|
|
71736
|
+
const seconds = Number(raw.trim());
|
|
71737
|
+
if (!Number.isInteger(seconds) || seconds <= 0) {
|
|
71738
|
+
return { ok: false, reason: `--timeout must be a positive whole number of seconds, got "${raw.trim()}"` };
|
|
71739
|
+
}
|
|
71740
|
+
if (seconds > TENANT_STATUS_WAIT_TIMEOUT_MAX_S) return { ok: true, seconds: TENANT_STATUS_WAIT_TIMEOUT_MAX_S, clamped: true };
|
|
71741
|
+
return { ok: true, seconds, clamped: false };
|
|
71742
|
+
}
|
|
71743
|
+
function deployedTagReaches(tag, sha) {
|
|
71744
|
+
const stamped = (tag ?? "").trim().toLowerCase();
|
|
71745
|
+
const needle = sha.trim().toLowerCase();
|
|
71746
|
+
if (!stamped || !needle) return false;
|
|
71747
|
+
if (stamped === needle) return true;
|
|
71748
|
+
const [shorter, longer] = stamped.length <= needle.length ? [stamped, needle] : [needle, stamped];
|
|
71749
|
+
return shorter.length >= TENANT_STATUS_WAIT_MIN_SHA_LENGTH && longer.startsWith(shorter);
|
|
71750
|
+
}
|
|
71751
|
+
async function waitForDeployedTag(deps) {
|
|
71752
|
+
const intervalMs = deps.intervalMs ?? TENANT_STATUS_WAIT_INTERVAL_MS;
|
|
71753
|
+
const now = deps.now ?? Date.now;
|
|
71754
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
71755
|
+
const startedAt = now();
|
|
71756
|
+
const deadline = startedAt + deps.timeoutMs;
|
|
71757
|
+
let polls = 0;
|
|
71758
|
+
for (; ; ) {
|
|
71759
|
+
const status = await deps.readStatus();
|
|
71760
|
+
polls += 1;
|
|
71761
|
+
if (deployedTagReaches(deps.deployedTagOf(status), deps.sha)) {
|
|
71762
|
+
return { matched: true, status, polls, elapsedMs: now() - startedAt };
|
|
71763
|
+
}
|
|
71764
|
+
const remaining = deadline - now();
|
|
71765
|
+
if (remaining <= 0) return { matched: false, status, polls, elapsedMs: now() - startedAt };
|
|
71766
|
+
await sleep2(Math.min(intervalMs, remaining));
|
|
71767
|
+
}
|
|
71768
|
+
}
|
|
71769
|
+
|
|
71443
71770
|
// src/tenant-sweep.ts
|
|
71444
71771
|
function isRcBearingTenant(p) {
|
|
71445
71772
|
if ((p.deployModel ?? "") !== "tenant-container") return false;
|
|
@@ -73498,12 +73825,33 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
|
|
|
73498
73825
|
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
73499
73826
|
}
|
|
73500
73827
|
});
|
|
73501
|
-
tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints; a root answering 401/403/404 is re-probed at /health and the `url` field names the endpoint that answered (#6137)").action(async (repo, stage) => {
|
|
73828
|
+
tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints; a root answering 401/403/404 is re-probed at /health and the `url` field names the endpoint that answered (#6137); `--wait-for <sha>` polls the same read to a deadline (#9942)").option("--wait-for <sha>", "poll until DEPLOY#<stage> deployedTag reaches this commit SHA (7-40 hex, short form ok), then print the final status and exit 0; exits 1 on timeout \u2014 use this instead of a shell sleep loop (#9942)").option("--timeout <secs>", `with --wait-for: give up after this many seconds (default ${TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S}, max ${TENANT_STATUS_WAIT_TIMEOUT_MAX_S}, polled every ${TENANT_STATUS_WAIT_INTERVAL_MS / 1e3}s)`).action(async (repo, stage, o) => {
|
|
73502
73829
|
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
73830
|
+
let wait;
|
|
73831
|
+
if (o.waitFor !== void 0) {
|
|
73832
|
+
const sha = parseWaitForSha(o.waitFor);
|
|
73833
|
+
if (!sha.ok) return fail(`runtime tenant status: ${sha.reason}`);
|
|
73834
|
+
const timeout = parseWaitTimeoutSeconds(o.timeout);
|
|
73835
|
+
if (!timeout.ok) return fail(`runtime tenant status: ${timeout.reason}`);
|
|
73836
|
+
wait = { sha: sha.sha, seconds: timeout.seconds, clamped: timeout.clamped };
|
|
73837
|
+
} else if (o.timeout !== void 0) {
|
|
73838
|
+
return fail("runtime tenant status: --timeout only applies with --wait-for <sha>");
|
|
73839
|
+
}
|
|
73503
73840
|
const cfg = await loadConfig();
|
|
73504
|
-
const
|
|
73841
|
+
const readStatus = () => buildTenantRuntimeStatusFor(repo, stage, cfg);
|
|
73842
|
+
let result;
|
|
73843
|
+
let waitMatched;
|
|
73844
|
+
if (!wait) result = await readStatus();
|
|
73845
|
+
else {
|
|
73846
|
+
console.error(`runtime tenant status: waiting up to ${wait.seconds}s for ${repo} ${stage} deployedTag to reach ${wait.sha}${wait.clamped ? ` (--timeout clamped to the ${TENANT_STATUS_WAIT_TIMEOUT_MAX_S}s max)` : ""}`);
|
|
73847
|
+
const waited = await waitForDeployedTag({ sha: wait.sha, timeoutMs: wait.seconds * 1e3, readStatus, deployedTagOf: (status) => status.deploy?.deployedTag });
|
|
73848
|
+
result = waited.status;
|
|
73849
|
+
waitMatched = waited.matched;
|
|
73850
|
+
if (!waited.matched) console.error(`runtime tenant status: timed out after ${Math.round(waited.elapsedMs / 1e3)}s (limit ${wait.seconds}s, ${waited.polls} read(s)); deployedTag is ${waited.status.deploy?.deployedTag ?? "unset"}, not ${wait.sha}`);
|
|
73851
|
+
}
|
|
73505
73852
|
console.log(JSON.stringify(result, null, 2));
|
|
73506
|
-
if (
|
|
73853
|
+
if (waitMatched !== void 0) process.exitCode = waitMatched ? 0 : 1;
|
|
73854
|
+
else if (result.publicProbe?.ok === false || result.edgeCertificate?.missing) process.exitCode = 1;
|
|
73507
73855
|
});
|
|
73508
73856
|
tenant.command("logs <owner/repo> <stage>").description("READ-ONLY: read a tenant stage's application journal (journald, so a deploy-recreated container still answers) \u2014 the unit namespace comes from the tenant's DEPLOY coords and the blue/green slot is chosen on the box, never from a flag; bounded and redacted (#9230)").option("--tail <n>", `lines to return (1-${TENANT_LOG_TAIL_MAX}, default ${TENANT_LOG_TAIL_DEFAULT}); the applied bound is printed`).option("--since <window>", "window start: a relative window like 15m, 6h or 2d, or a UTC time like 2026-10-07T13:10Z (default 1h)").option("--until <time>", "window end: same forms as --since (default: now) \u2014 the box returns the newest N lines OF the window, so end it earlier to reach older lines on a busy tenant (#9878)").option("--grep <needle>", "keep only returned lines containing this literal text (applied to the bounded tail, not the whole journal)").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
73509
73857
|
try {
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
package/skills/board/SKILL.md
CHANGED
|
@@ -13,17 +13,23 @@ permission to take anything.
|
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
mkdir -p .jerv/tmp
|
|
16
|
-
mmi-cli oracle board read --json --out .jerv/tmp/board.json
|
|
16
|
+
mmi-cli oracle board read --json --flat --out .jerv/tmp/board.json
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
One call;
|
|
20
|
-
|
|
19
|
+
One call; `--flat` gives one array of rows to iterate. A routine read already excludes Done and closed
|
|
20
|
+
items, so no status post-filter is needed. The detailed form (`--json` without `--flat`) carries `viewer`,
|
|
21
|
+
`repo` and the project title; use its `primary` for this repo and `secondary` for other repos on the
|
|
22
|
+
Project. Without `--flat`, `board read --json` returns an object
|
|
21
23
|
`{project, viewer, repo, primary, secondary, warnings, partial}` whose `primary` and `secondary` are bucket
|
|
22
24
|
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`.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
a bucket (e.g. `report.primary.claimable`) or pass `--json --flat`, never `(j.items||j).filter`. Never sniff
|
|
26
|
+
for the list (`j.items ?? j.rows ?? j`, `Object.values`, any generic "find the list" wrapper): it throws
|
|
27
|
+
`items is not iterable` before any row is read. For a number|status|claim|title projection across non-Done
|
|
28
|
+
rows, run `mmi-cli oracle board read --json --flat --out .jerv/tmp/board.json` and filter that array locally.
|
|
29
|
+
`--flat` rows are `{ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}` (`claim` is the
|
|
30
|
+
parsed mmi-claim evidence object, null when unclaimed; `openPullRequests` lists the OPEN PRs whose
|
|
31
|
+
closing-issue body links name the item — on the full JSON object that list lives at `report.pullRequests`,
|
|
32
|
+
never `openPullRequests`). Partial reads
|
|
27
33
|
exit nonzero; `--allow-partial` only when the dev accepts an incomplete board. The CLI partitions rows into
|
|
28
34
|
**Yours**, **Free to claim** and **Taken**; stale stamps (4 hours without work) hold nothing. Recovery
|
|
29
35
|
moves: a zero-byte or empty `.jerv/tmp/*.json` (`Unexpected end of JSON input`) means a shell-redirected
|
|
@@ -33,7 +39,7 @@ item is `mmi-cli oracle board move <status> <ref>`, never a hand-written `gh api
|
|
|
33
39
|
lookup. Partition rules,
|
|
34
40
|
stamp details and `--out` rationale: [reference.md](reference.md).
|
|
35
41
|
|
|
36
|
-
**Under the App pool, never type raw `gh` for a read** (`gh issue list/view --jq`, `gh run list --event`/`--jq`, `gh run view --jq`, `gh api -q` all bounce). Use `MMI_ACTOR=app mmi-cli devops read issue view <n> --repo <owner/repo> --json <fields>` (or `issue list`, `pr view`, `run view`, `run list --event <event>`, or `MMI_ACTOR=app mmi-cli devops api get "repos/<owner>/<repo>/actions/runs?event=<event>&per_page=<n>"`); `--jq '<expr>'` on that typed read filters locally. A single issue or PR item (`repos/<owner>/<repo>/issues/<n>` and `pulls/<n>`) has no `devops api get` route — `MMI_ACTOR=app mmi-cli devops read issue view <n> --repo <owner>/<repo> --json <fields>` (and `pr view`) is the only read. Those typed reads take **one** number per call, exactly like native gh: a second number is refused as `too many arguments`, so read several issues by repeating the typed read once per number (one `--jq` each, joined `&&`). Filter in a pipe or save output (
|
|
42
|
+
**Under the App pool, never type raw `gh` for a read** (`gh issue list/view --jq`, `gh run list --event`/`--jq`, `gh run view --jq`, `gh api -q` all bounce). Use `MMI_ACTOR=app mmi-cli devops read issue view <n> --repo <owner/repo> --json <fields>` (or `issue list`, `pr view`, `run view`, `run list --event <event>`, or `MMI_ACTOR=app mmi-cli devops api get "repos/<owner>/<repo>/actions/runs?event=<event>&per_page=<n>"`); `--jq '<expr>'` on that typed read filters locally. A single issue or PR item (`repos/<owner>/<repo>/issues/<n>` and `pulls/<n>`) has no `devops api get` route — `MMI_ACTOR=app mmi-cli devops read issue view <n> --repo <owner>/<repo> --json <fields>` (and `pr view`) is the only read. Those typed reads take **one** number per call, exactly like native gh: a second number is refused as `too many arguments`, so read several issues by repeating the typed read once per number (one `--jq` each, joined `&&`). Filter in a pipe or save output (`--out`, or `>`; never `2>&1` into a JSON parser) under this worktree's `.jerv/tmp/` (`mkdir -p .jerv/tmp` first), never `../` or a sibling path: the workspace output guard refuses those. Body edits are read-then-edit: `MMI_ACTOR=app mmi-cli devops read issue view <n> --repo <owner/repo> --json body > .jerv/tmp/issue-body.json`, append or filter the JSON locally (`node -e`; the typed read's own `--jq` also filters locally), then write back with `mmi-cli oracle issue edit <ref> --body-file .jerv/tmp/issue-body.md`. When `node -e` reads saved JSON, pass an absolute path or use `JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))`: `require()` of a relative path (e.g. `tmp\board.json` from `cygpath -w`) throws MODULE_NOT_FOUND.
|
|
37
43
|
|
|
38
44
|
## Step 2 — render it
|
|
39
45
|
|
|
@@ -15,7 +15,10 @@ the gate again.
|
|
|
15
15
|
Prefer `--out` for a shell-independent UTF-8 receipt. Windows PowerShell 5.1's `>` writes
|
|
16
16
|
UTF-16LE with a BOM (#5802); Bash redirects preserve the CLI's UTF-8 output.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
The canonical read is `mmi-cli oracle board read --json --flat --out .jerv/tmp/board.json`: one array of
|
|
19
|
+
rows to iterate. A routine read already excludes Done and closed items, so no status post-filter is needed.
|
|
20
|
+
|
|
21
|
+
One call. The detailed form (`--json` without `--flat`) carries `viewer`, `repo` and the project title — do not spend extra calls on
|
|
19
22
|
`gh api user` or `gh repo view`. Use `primary` for this repo and `secondary` for other repos on the same
|
|
20
23
|
Project. Without `--flat`, `board read --json` returns an object
|
|
21
24
|
`{project, viewer, repo, primary, secondary, warnings, partial}` whose `primary` and `secondary` are bucket
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
".github/workflows/agent-pr.yml": {
|
|
3
|
-
"blob": "
|
|
4
|
-
"reason": "
|
|
3
|
+
"blob": "e85a80c65b26531a330917fb4b5d8f563a0f0d0c",
|
|
4
|
+
"reason": "sole-write-path guard reads retry transient GitHub API 5xx/429/transport failures up to 3 attempts, then fail closed with the same messages (#9911); permissions, refs, guarded paths and operator responsibilities unchanged"
|
|
5
5
|
},
|
|
6
6
|
"scripts/next-version.mjs": {
|
|
7
7
|
"blob": "2c1985b59edc779695479559074e7654c1574a11",
|
package/skills/release/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ Bare `minor|major|patch` (default `patch`), never a flag. A production release n
|
|
|
11
11
|
|
|
12
12
|
## Lane
|
|
13
13
|
|
|
14
|
-
Read `mmi-cli oracle org project get {owner}/{repo} --json` and name the lane ([verification.md](verification.md) Lane rules); unreadable META → stop. [suite.md](suite.md) for
|
|
14
|
+
Read `mmi-cli oracle org project get {owner}/{repo} --json` and name the lane ([verification.md](verification.md) Lane rules); unreadable META → stop. On `releaseTrack: single` (MM-Work and its modules) or `trunk` there is no release lane. On single track a merge to development auto-deploys the `main` stage, and release, rcand and hotfix refuse. A merged single-track change is live once that deploy is green. A trunk repo follows its own declared publishing workflow. Never tell the owner a landed change waits on a release. [suite.md](suite.md) for suite cuts; [vercel.md](vercel.md) for Vercel.
|
|
15
15
|
|
|
16
16
|
## Preflight
|
|
17
17
|
|
package/skills/release/suite.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
MM-Work and most of its modules are `releaseTrack: single` ([work-and-releases.md](../org-guide/references/work-and-releases.md)). A merge to development auto-deploys their `main` stage, so a landed change is live once that deploy is green. Never tell the owner such a change waits on a release. Read each repo's `releaseTrack` with `mmi-cli oracle org project get <owner/repo> --json` before naming any cut. A suite cut also cuts the MM-Work lead's rcand, which refuses while MM-Work is single track.
|
|
2
|
+
|
|
1
3
|
In the suite lead (MM-Work) a plain rcand, release or hotfix refuses: the suite path is `mmi-cli devops suite rcand --owner-go` or `mmi-cli devops suite release --owner-go` (a lead-only cut needs `--suite-solo --owner-go`). `--owner-go` records Jervaise's explicit go for that one cut; it is never the agent's own flag. Never pass it on your own authority: a held train or a stale MM-Work cut (`suite abandon --owner-go`, then `suite rcand --owner-go`) is handed to the suite orchestrator with that exact command sequence, and runs only after the owner says go. On your own, run only the read-only previews `mmi-cli devops suite rcand --dry-run` and `mmi-cli devops suite hotfix <member> --dry-run`; `suite release` and `suite abandon` have no dry-run, so never run them yourself. A real suite rcand, release, hotfix or abandon runs only on the owner's go, executed by the owner or the suite orchestrator. A suite member cannot run standalone rcand, release or hotfix: cut from the suite lead; only a backward-compatible solo backend release/hotfix with explicit owner approval may pass `--suite-solo --owner-go`, and its MM-Work development pin PR must land before the suite is in sync. A suite member never asks for, proposes or prompts an rc, release or hotfix — the suite orchestrator is the only voice that brings a cut to Jervaise, so a member session states what landed and stops instead of offering a cut.
|
|
2
4
|
|
|
3
5
|
Support sessions (MMI-Hub, Jerv-Hub, Jerv-JervCode) are not suite members and hold standing owner authorization (2026-10-25) to cut and ship releases at their discretion during suite runs without repeated per-release approval, and under the owner's 2026-11-01 ruling each support session strives during a suite run to finish its own board and cut a patch release for landed-but-unreleased merges so suite members pick the update up in that run instead of waiting for a later cut, keeping all standard release preflight, gate, and verification checks.
|