@mutmutco/claude-plugin 4.6.31 → 4.6.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +392 -36
- 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/SKILL.md +2 -0
package/cli/dist/main.cjs
CHANGED
|
@@ -27062,6 +27062,71 @@ function contentPutInputArgs(repo, path2, inputFile) {
|
|
|
27062
27062
|
inputFile
|
|
27063
27063
|
];
|
|
27064
27064
|
}
|
|
27065
|
+
var BANK_REQUEST_REPO = "mutmutco/Jerv-Hub";
|
|
27066
|
+
var BANK_REQUEST_LABEL = "bank-request";
|
|
27067
|
+
var BANK_REQUEST_SOURCE = "MMI-Hub#9958";
|
|
27068
|
+
function bankRequestTitle(repoName2) {
|
|
27069
|
+
return `bank-request: coding-agent::${repoName2}`;
|
|
27070
|
+
}
|
|
27071
|
+
function bankRequestBody(fullName) {
|
|
27072
|
+
return `Create the coding-agent memory bank for \`${fullName}\` (asked by \`mmi-cli devops bootstrap apply\`; see ${BANK_REQUEST_SOURCE}).`;
|
|
27073
|
+
}
|
|
27074
|
+
function planBankRequest(fullName, existing) {
|
|
27075
|
+
const parsed = parseOwnerRepo(fullName);
|
|
27076
|
+
const base = { title: bankRequestTitle(parsed.name), label: BANK_REQUEST_LABEL, body: bankRequestBody(parsed.fullName) };
|
|
27077
|
+
if (existing == null) return { ...base, action: "skip", reason: `could not read ${BANK_REQUEST_REPO} issues` };
|
|
27078
|
+
const found = existing.find((issue) => issue?.title === base.title);
|
|
27079
|
+
if (found) {
|
|
27080
|
+
return {
|
|
27081
|
+
...base,
|
|
27082
|
+
action: "exists",
|
|
27083
|
+
number: found.number,
|
|
27084
|
+
state: found.state ?? "open",
|
|
27085
|
+
url: found.url ?? `https://github.com/${BANK_REQUEST_REPO}/issues/${found.number}`
|
|
27086
|
+
};
|
|
27087
|
+
}
|
|
27088
|
+
return { ...base, action: "create" };
|
|
27089
|
+
}
|
|
27090
|
+
var oneLine = (e) => String(e?.message ?? e).split("\n")[0].trim();
|
|
27091
|
+
function bankRequestManualStep(request, reason) {
|
|
27092
|
+
return `file it by hand at https://github.com/${BANK_REQUEST_REPO}/issues/new \u2014 title "${request.title}", label ${request.label}, body: ${request.body} (${reason})`;
|
|
27093
|
+
}
|
|
27094
|
+
function renderBankRequestPlan(result) {
|
|
27095
|
+
return [
|
|
27096
|
+
"bootstrap apply \u2014 coding-agent memory bank request (dry-run; no issue filed):",
|
|
27097
|
+
` PLAN file issue "${result.title}" (label ${result.label}) in ${BANK_REQUEST_REPO}`,
|
|
27098
|
+
` body: ${result.body}`
|
|
27099
|
+
].join("\n");
|
|
27100
|
+
}
|
|
27101
|
+
function renderBankRequestLine(result) {
|
|
27102
|
+
if (result.status === "opened") {
|
|
27103
|
+
return `memory bank request: ${BANK_REQUEST_REPO}#${result.number} opened \u2014 "${result.title}" (label ${result.label})${result.url ? ` ${result.url}` : ""}`;
|
|
27104
|
+
}
|
|
27105
|
+
if (result.status === "exists") {
|
|
27106
|
+
return `memory bank request: already requested \u2014 ${BANK_REQUEST_REPO}#${result.number} (${result.state})${result.url ? ` ${result.url}` : ""}`;
|
|
27107
|
+
}
|
|
27108
|
+
return `memory bank request: SKIPPED (${result.reason ?? "not attempted"}) \u2014 ${bankRequestManualStep(result, result.reason ?? "not attempted")}`;
|
|
27109
|
+
}
|
|
27110
|
+
async function runBankRequest(fullName, options, deps) {
|
|
27111
|
+
const parsed = parseOwnerRepo(fullName);
|
|
27112
|
+
const request = { title: bankRequestTitle(parsed.name), label: BANK_REQUEST_LABEL, body: bankRequestBody(parsed.fullName) };
|
|
27113
|
+
if (options.dryRun) return { ...request, status: "planned" };
|
|
27114
|
+
let existing;
|
|
27115
|
+
try {
|
|
27116
|
+
existing = await deps.listIssues();
|
|
27117
|
+
} catch (e) {
|
|
27118
|
+
return { ...request, status: "skipped", reason: `could not read ${BANK_REQUEST_REPO} issues: ${oneLine(e)}` };
|
|
27119
|
+
}
|
|
27120
|
+
const plan = planBankRequest(fullName, existing);
|
|
27121
|
+
if (plan.action === "exists") return { ...request, status: "exists", number: plan.number, state: plan.state, url: plan.url };
|
|
27122
|
+
if (plan.action === "skip") return { ...request, status: "skipped", reason: plan.reason };
|
|
27123
|
+
try {
|
|
27124
|
+
const created = await deps.createIssue(request);
|
|
27125
|
+
return { ...request, status: "opened", number: created.number, url: created.url };
|
|
27126
|
+
} catch (e) {
|
|
27127
|
+
return { ...request, status: "skipped", reason: `App issue write failed: ${oneLine(e)}` };
|
|
27128
|
+
}
|
|
27129
|
+
}
|
|
27065
27130
|
|
|
27066
27131
|
// src/train-apply-contracts.ts
|
|
27067
27132
|
var import_node_fs21 = require("node:fs");
|
|
@@ -28301,10 +28366,10 @@ var rollout_plan_default = {
|
|
|
28301
28366
|
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)."
|
|
28302
28367
|
},
|
|
28303
28368
|
baseline: {
|
|
28304
|
-
version: "4.6.
|
|
28305
|
-
tag: "v4.6.
|
|
28306
|
-
commit: "
|
|
28307
|
-
npm: "@mutmutco/cli@4.6.
|
|
28369
|
+
version: "4.6.33",
|
|
28370
|
+
tag: "v4.6.33",
|
|
28371
|
+
commit: "80768b0b2052",
|
|
28372
|
+
npm: "@mutmutco/cli@4.6.33"
|
|
28308
28373
|
},
|
|
28309
28374
|
exitCriterion: "fleet-n-of-n",
|
|
28310
28375
|
hubOnlyShortcut: "forbidden",
|
|
@@ -28321,14 +28386,14 @@ var rollout_plan_default = {
|
|
|
28321
28386
|
repo: "mutmutco/mmi-hub",
|
|
28322
28387
|
role: "canary",
|
|
28323
28388
|
schedule: "train",
|
|
28324
|
-
v3Target: "v4.6.
|
|
28389
|
+
v3Target: "v4.6.33"
|
|
28325
28390
|
}
|
|
28326
28391
|
],
|
|
28327
28392
|
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.",
|
|
28328
28393
|
rollback: {
|
|
28329
28394
|
independent: true,
|
|
28330
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
28331
|
-
v3Target: "v4.6.
|
|
28395
|
+
mechanism: "npm dist-tag latest -> 4.6.33 and redeploy the Hub Lambda from tag v4.6.33 (80768b0b2052); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
28396
|
+
v3Target: "v4.6.33 (@mutmutco/cli@4.6.33, tag commit 80768b0b2052 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
28332
28397
|
}
|
|
28333
28398
|
},
|
|
28334
28399
|
{
|
|
@@ -42556,6 +42621,9 @@ async function runPrLandOnce(prNumber, options, deps) {
|
|
|
42556
42621
|
base.mergeStatus = merge.mergeStatus;
|
|
42557
42622
|
if (merge.credential) base.credential = merge.credential;
|
|
42558
42623
|
if (merge.credentialRole) base.credentialRole = merge.credentialRole;
|
|
42624
|
+
if (merge.ejectedThenReenqueued) base.ejectedThenReenqueued = true;
|
|
42625
|
+
if (merge.reconcileNeeded) base.reconcileNeeded = true;
|
|
42626
|
+
if (merge.queueRemovalEvidence) base.queueRemovalEvidence = merge.queueRemovalEvidence;
|
|
42559
42627
|
if (merge.mergeStatus === "queue-admission-pending") {
|
|
42560
42628
|
return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
|
|
42561
42629
|
}
|
|
@@ -43727,6 +43795,7 @@ var autoMergeApi = async (pr, repo, head) => {
|
|
|
43727
43795
|
};
|
|
43728
43796
|
var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
|
|
43729
43797
|
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
43798
|
+
var UNWITNESSED_EJECTION_GRACE_MS = 6e4;
|
|
43730
43799
|
function admissionJournal(repo, pr, root) {
|
|
43731
43800
|
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"));
|
|
43732
43801
|
const directory = (0, import_node_path31.join)(stateRoot, (0, import_node_crypto12.createHash)("sha256").update(`${repo.toLowerCase()}#${pr}`).digest("hex"));
|
|
@@ -43740,12 +43809,12 @@ function admissionJournal(repo, pr, root) {
|
|
|
43740
43809
|
const path2 = (number) => (0, import_node_path31.join)(directory, `${number}.jsonl`);
|
|
43741
43810
|
const validate = (value, previous) => {
|
|
43742
43811
|
const row = value;
|
|
43743
|
-
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)))) {
|
|
43812
|
+
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)))) {
|
|
43744
43813
|
throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
|
|
43745
43814
|
}
|
|
43746
43815
|
if (!previous) {
|
|
43747
43816
|
if (row.state !== "dispatching" || row.uuid || row.queueEntry || row.observedAt !== row.createdAt) throw new Error("queue admission intent record is invalid");
|
|
43748
|
-
} 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
|
|
43817
|
+
} 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)) {
|
|
43749
43818
|
throw new Error("queue admission receipt has an illegal transition or changed operation identity");
|
|
43750
43819
|
}
|
|
43751
43820
|
return row;
|
|
@@ -43828,13 +43897,23 @@ function admissionRejected(reason, repo, pr) {
|
|
|
43828
43897
|
};
|
|
43829
43898
|
}
|
|
43830
43899
|
async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
|
|
43831
|
-
|
|
43900
|
+
let reenqueuedAfterEjection = false;
|
|
43901
|
+
let removalEvidence;
|
|
43902
|
+
const markReenqueued = (result) => reenqueuedAfterEjection ? {
|
|
43903
|
+
...result,
|
|
43904
|
+
ejectedThenReenqueued: true,
|
|
43905
|
+
...removalEvidence ? { queueRemovalEvidence: removalEvidence } : {},
|
|
43906
|
+
...result.error ? { error: `re-enqueued after ejection: ${result.error}` } : {}
|
|
43907
|
+
} : result;
|
|
43908
|
+
const unknown = (reason) => markReenqueued({
|
|
43832
43909
|
mergeStatus: "failed",
|
|
43833
43910
|
retryable: false,
|
|
43834
43911
|
outcomeUnknown: true,
|
|
43835
|
-
|
|
43912
|
+
reconcileNeeded: true,
|
|
43913
|
+
...removalEvidence ? { queueRemovalEvidence: removalEvidence } : {},
|
|
43914
|
+
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.`
|
|
43836
43915
|
});
|
|
43837
|
-
const rejected = (reason) => admissionRejected(reason, repo, pr);
|
|
43916
|
+
const rejected = (reason) => markReenqueued(admissionRejected(reason, repo, pr));
|
|
43838
43917
|
const reconcile = async (reason, recordEntry = false, resumingSaved = false) => {
|
|
43839
43918
|
const maxReads = options.enqueueOnly ? 3 : 1;
|
|
43840
43919
|
const sleep3 = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
@@ -43842,13 +43921,13 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43842
43921
|
if (readAttempt) await sleep3(readAttempt * 1e3);
|
|
43843
43922
|
try {
|
|
43844
43923
|
const observed = await readPrMergeQueue(pr, repo, read);
|
|
43845
|
-
if (observed.state === "MERGED" && observed.headRefOid === head) return { mergeStatus: "merged" };
|
|
43924
|
+
if (observed.state === "MERGED" && observed.headRefOid === head) return markReenqueued({ mergeStatus: "merged" });
|
|
43846
43925
|
if (observed.isInMergeQueue && observed.headRefOid === head) {
|
|
43847
43926
|
const entry = observed.mergeQueueEntry;
|
|
43848
43927
|
if (recordEntry && receipt?.head === head && entry?.headCommit && entry.enqueuedAt) {
|
|
43849
43928
|
journal.save("enqueued", receipt?.uuid, { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt });
|
|
43850
43929
|
}
|
|
43851
|
-
return { mergeStatus: "auto-merge-enqueued" };
|
|
43930
|
+
return markReenqueued({ mergeStatus: "auto-merge-enqueued" });
|
|
43852
43931
|
}
|
|
43853
43932
|
const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
|
|
43854
43933
|
const boundMs = saved?.queueEntry ? Date.parse(saved.queueEntry.enqueuedAt) : resumingSaved && saved?.state === "enqueued" && saved.queueWitnessed === void 0 ? saved.observedAt : void 0;
|
|
@@ -43880,11 +43959,37 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43880
43959
|
let receipt = journal.receipt;
|
|
43881
43960
|
let response;
|
|
43882
43961
|
let superseded = false;
|
|
43883
|
-
if (receipt && (receipt.state === "enqueued" || receipt.state === "merged")
|
|
43884
|
-
|
|
43885
|
-
|
|
43886
|
-
|
|
43887
|
-
|
|
43962
|
+
if (receipt && (receipt.state === "enqueued" || receipt.state === "merged")) {
|
|
43963
|
+
if (receipt.head.toLowerCase() !== head.toLowerCase()) {
|
|
43964
|
+
try {
|
|
43965
|
+
const observed = await readPrMergeQueue(pr, repo, read);
|
|
43966
|
+
superseded = observed.state === "OPEN" && !observed.isInMergeQueue;
|
|
43967
|
+
} catch {
|
|
43968
|
+
}
|
|
43969
|
+
} else {
|
|
43970
|
+
try {
|
|
43971
|
+
const observed = await readPrMergeQueue(pr, repo, read);
|
|
43972
|
+
if (observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid?.toLowerCase() === receipt.head.toLowerCase()) {
|
|
43973
|
+
const boundMs = receipt.queueEntry ? Date.parse(receipt.queueEntry.enqueuedAt) : receipt.observedAt + UNWITNESSED_EJECTION_GRACE_MS;
|
|
43974
|
+
const events = await readArrayPages(
|
|
43975
|
+
`repos/${repo}/issues/${pr}/timeline?per_page=100`,
|
|
43976
|
+
options.timelineRead ?? read,
|
|
43977
|
+
"queue ejection timeline unreadable"
|
|
43978
|
+
);
|
|
43979
|
+
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);
|
|
43980
|
+
removalEvidence = {
|
|
43981
|
+
source: receipt.queueEntry ? "queue-entry" : "receipt-checkpoint",
|
|
43982
|
+
boundAt: new Date(boundMs).toISOString(),
|
|
43983
|
+
removalsAfterBound: removals.map((at) => new Date(at).toISOString()),
|
|
43984
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
43985
|
+
};
|
|
43986
|
+
if (removals.length > 0) {
|
|
43987
|
+
reenqueuedAfterEjection = true;
|
|
43988
|
+
superseded = true;
|
|
43989
|
+
}
|
|
43990
|
+
}
|
|
43991
|
+
} catch {
|
|
43992
|
+
}
|
|
43888
43993
|
}
|
|
43889
43994
|
}
|
|
43890
43995
|
if (superseded && receipt?.state === "enqueued") {
|
|
@@ -43894,7 +43999,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43894
43999
|
return unknown("the superseded admission could not be checkpointed");
|
|
43895
44000
|
}
|
|
43896
44001
|
}
|
|
43897
|
-
if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && !superseded) {
|
|
44002
|
+
if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && receipt.state !== "abandoned" && !superseded) {
|
|
43898
44003
|
if (options.selection) {
|
|
43899
44004
|
options.selection.selected = true;
|
|
43900
44005
|
options.selection.intentId = receipt.intentId;
|
|
@@ -44040,13 +44145,13 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
44040
44145
|
if (receipt.head !== head) return reconcile("the previous head completed admission; inspect current head and membership before continuing");
|
|
44041
44146
|
return reconcile(`operation ${receipt.uuid} reported ${status}, but matching-head membership or merged state is unconfirmed`, status === "enqueued");
|
|
44042
44147
|
}
|
|
44043
|
-
return {
|
|
44148
|
+
return markReenqueued({
|
|
44044
44149
|
mergeStatus: "queue-admission-pending",
|
|
44045
44150
|
retryable: false,
|
|
44046
44151
|
outcomeUnknown: true,
|
|
44047
44152
|
queueAdmission: { state: "pending", uuid: receipt.uuid, head: receipt.head },
|
|
44048
44153
|
error: "GitHub accepted queue admission but has not confirmed queue membership. Rerun pr land to resume the saved operation read-only."
|
|
44049
|
-
};
|
|
44154
|
+
});
|
|
44050
44155
|
}
|
|
44051
44156
|
function githubQueueSquashMessage(title, pr, commits) {
|
|
44052
44157
|
const list = commits.map((commit) => `* ${commit.headline}
|
|
@@ -44251,6 +44356,38 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
44251
44356
|
const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
|
|
44252
44357
|
return { ...stamp(result, selection.selected, selection.intentId), ...inspected.warning ? { warning: inspected.warning } : {} };
|
|
44253
44358
|
}
|
|
44359
|
+
async function abandonQueueAdmission(pr, repo, options = {}) {
|
|
44360
|
+
if (!/^\d+$/.test(pr)) return { status: "refused", repo, pr, reason: "a bare PR number is required" };
|
|
44361
|
+
const refuse = (reason) => ({ status: "refused", repo, pr, reason });
|
|
44362
|
+
let journal;
|
|
44363
|
+
try {
|
|
44364
|
+
journal = admissionJournal(repo, pr, options.receiptDirectory);
|
|
44365
|
+
} catch (error) {
|
|
44366
|
+
return refuse(`the durable admission receipt cannot be read (${error.message})`);
|
|
44367
|
+
}
|
|
44368
|
+
const receipt = journal.receipt;
|
|
44369
|
+
if (!receipt) return refuse("no queue-admission receipt exists for this PR; nothing to abandon");
|
|
44370
|
+
if (!["pending", "enqueued"].includes(receipt.state)) {
|
|
44371
|
+
return refuse(`the saved admission is '${receipt.state}', not a stuck non-terminal 'pending'/'enqueued' receipt; nothing to abandon`);
|
|
44372
|
+
}
|
|
44373
|
+
let snapshot;
|
|
44374
|
+
try {
|
|
44375
|
+
snapshot = await readPrMergeQueue(pr, repo, options.read ?? actorReadApi);
|
|
44376
|
+
} catch (error) {
|
|
44377
|
+
return refuse(`the PR and queue state could not be read (${error.message}); the admission is unchanged`);
|
|
44378
|
+
}
|
|
44379
|
+
if (snapshot.state !== "OPEN") return refuse(`the PR is '${snapshot.state}', not OPEN; the admission is unchanged`);
|
|
44380
|
+
if (!snapshot.headRefOid || snapshot.headRefOid.toLowerCase() !== receipt.head.toLowerCase()) {
|
|
44381
|
+
return refuse(`the PR head is ${snapshot.headRefOid ?? "unreadable"}, not the receipt's submitted head ${receipt.head}; the admission is unchanged`);
|
|
44382
|
+
}
|
|
44383
|
+
if (snapshot.isInMergeQueue) return refuse("the PR is currently in the merge queue, so the admission is not stuck; the admission is unchanged");
|
|
44384
|
+
try {
|
|
44385
|
+
journal.save("abandoned");
|
|
44386
|
+
} catch (error) {
|
|
44387
|
+
return refuse(`the abandoned state could not be checkpointed (${error.message}); the admission is unchanged`);
|
|
44388
|
+
}
|
|
44389
|
+
return { status: "abandoned", repo, pr, head: receipt.head, ...receipt.uuid ? { operation: receipt.uuid } : {} };
|
|
44390
|
+
}
|
|
44254
44391
|
function closingGuardDiagnostic(message2) {
|
|
44255
44392
|
if (!message2) return "";
|
|
44256
44393
|
const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
|
|
@@ -46163,7 +46300,7 @@ function refuseNotClaimable(e, json) {
|
|
|
46163
46300
|
process.exitCode = 1;
|
|
46164
46301
|
return true;
|
|
46165
46302
|
}
|
|
46166
|
-
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";
|
|
46303
|
+
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";
|
|
46167
46304
|
function flattenBoardReport(report) {
|
|
46168
46305
|
const closingByRef = /* @__PURE__ */ new Map();
|
|
46169
46306
|
for (const pull of report.pullRequests ?? []) {
|
|
@@ -51436,6 +51573,7 @@ function registerBootstrapCommands(program3) {
|
|
|
51436
51573
|
}
|
|
51437
51574
|
const actions = [];
|
|
51438
51575
|
const applied = [];
|
|
51576
|
+
let bankRequest = null;
|
|
51439
51577
|
let applyDeployModel;
|
|
51440
51578
|
let applyProjectType;
|
|
51441
51579
|
try {
|
|
@@ -51914,6 +52052,48 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
51914
52052
|
applied.push(`ddb register ${registerPayload.slug} (failed: ${why})`);
|
|
51915
52053
|
}
|
|
51916
52054
|
}
|
|
52055
|
+
if (!onlyTarget) {
|
|
52056
|
+
bankRequest = await runBankRequest(repo, { dryRun: !o.execute }, {
|
|
52057
|
+
listIssues: async () => {
|
|
52058
|
+
const listed = await gh2([
|
|
52059
|
+
"issue",
|
|
52060
|
+
"list",
|
|
52061
|
+
"--repo",
|
|
52062
|
+
BANK_REQUEST_REPO,
|
|
52063
|
+
"--state",
|
|
52064
|
+
"all",
|
|
52065
|
+
"--search",
|
|
52066
|
+
`in:title "${bankRequestTitle(parsedRepo.name)}"`,
|
|
52067
|
+
"--json",
|
|
52068
|
+
"number,title,state,url",
|
|
52069
|
+
"--limit",
|
|
52070
|
+
"50"
|
|
52071
|
+
]);
|
|
52072
|
+
const rows = JSON.parse(listed.stdout || "[]");
|
|
52073
|
+
if (!Array.isArray(rows)) throw new Error("issue list is not an array");
|
|
52074
|
+
return rows;
|
|
52075
|
+
},
|
|
52076
|
+
createIssue: async (request) => {
|
|
52077
|
+
const out = await gh2([
|
|
52078
|
+
"issue",
|
|
52079
|
+
"create",
|
|
52080
|
+
"--repo",
|
|
52081
|
+
BANK_REQUEST_REPO,
|
|
52082
|
+
"--title",
|
|
52083
|
+
request.title,
|
|
52084
|
+
"--body",
|
|
52085
|
+
request.body,
|
|
52086
|
+
"--label",
|
|
52087
|
+
request.label
|
|
52088
|
+
]);
|
|
52089
|
+
const url = out.stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
52090
|
+
const number = Number(/(\d+)$/.exec(url)?.[1]);
|
|
52091
|
+
if (!url || !Number.isFinite(number)) throw new Error(`issue create returned no issue URL: ${out.stdout.trim().slice(0, 200)}`);
|
|
52092
|
+
return { number, url };
|
|
52093
|
+
}
|
|
52094
|
+
});
|
|
52095
|
+
if (o.execute) applied.push(renderBankRequestLine(bankRequest));
|
|
52096
|
+
}
|
|
51917
52097
|
if (o.execute && !onlyTarget) {
|
|
51918
52098
|
const github = await readBootstrapGitHubFacts(repo, effectiveTrack, controlClient);
|
|
51919
52099
|
controlDeployFacts = await fetchDeployFactsBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -52009,13 +52189,15 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
52009
52189
|
return failGraceful(`bootstrap apply: cannot plan labels/environments; no writes made: ${e.message}`);
|
|
52010
52190
|
}
|
|
52011
52191
|
}
|
|
52012
|
-
if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedBranch: seedPlan.branch, seedBaseSha: scopedPlan?.baseSha, seedPrUrl, actions, controlPlane: controlRows, labels: ancillaryPlan, environments: environmentPlan, applied, ddbWrites }, null, 2));
|
|
52192
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedBranch: seedPlan.branch, seedBaseSha: scopedPlan?.baseSha, seedPrUrl, actions, controlPlane: controlRows, labels: ancillaryPlan, environments: environmentPlan, bankRequest, applied, ddbWrites }, null, 2));
|
|
52013
52193
|
else {
|
|
52014
52194
|
console.log(renderSeedPlan(actions));
|
|
52015
52195
|
if (controlRows.length) console.log(`
|
|
52016
52196
|
${renderBootstrapControlPlan(controlRows)}`);
|
|
52017
52197
|
if (ancillaryPlan && environmentPlan) console.log(`
|
|
52018
52198
|
${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan, repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug).filter((spec) => spec.exception).map((spec) => ({ name: spec.name, detail: spec.exception })))}`);
|
|
52199
|
+
if (bankRequest?.status === "planned") console.log(`
|
|
52200
|
+
${renderBankRequestPlan(bankRequest)}`);
|
|
52019
52201
|
if (o.execute) console.log(`
|
|
52020
52202
|
LIVE apply to ${repo}:
|
|
52021
52203
|
${applied.join("\n ")}`);
|
|
@@ -56383,6 +56565,7 @@ async function verifyCustodyBootstrap(value, api, controller, requiredContexts,
|
|
|
56383
56565
|
// src/release-custody.ts
|
|
56384
56566
|
var RELEASE_CUSTODY_BRANCH = "mmi-release-state";
|
|
56385
56567
|
var RELEASE_CUSTODY_PATH = "release-custody.json";
|
|
56568
|
+
var HUB_RELEASE_CUSTODY_JOB = "release";
|
|
56386
56569
|
var ReleaseCustodyError = class extends Error {
|
|
56387
56570
|
constructor(message2) {
|
|
56388
56571
|
super("Release custody: " + message2);
|
|
@@ -56456,6 +56639,14 @@ function githubCustodyStore(repo, baseSha, api = defaultGitHubClient()) {
|
|
|
56456
56639
|
`repos/${repo}/actions/runs/${controller.runId}/attempts/${controller.runAttempt}`
|
|
56457
56640
|
);
|
|
56458
56641
|
return run.id === controller.runId && run.run_attempt === controller.runAttempt && run.path === HUB_RELEASE_WORKFLOW && run.status === "completed";
|
|
56642
|
+
},
|
|
56643
|
+
async jobCompleted(controller) {
|
|
56644
|
+
const jobs = await api.rest(
|
|
56645
|
+
"GET",
|
|
56646
|
+
`repos/${repo}/actions/runs/${controller.runId}/attempts/${controller.runAttempt}/jobs?per_page=100`
|
|
56647
|
+
);
|
|
56648
|
+
const job = (jobs?.jobs ?? []).find((row) => row.name === (controller.job ?? HUB_RELEASE_CUSTODY_JOB));
|
|
56649
|
+
return job?.status === "completed" && job.conclusion === "success";
|
|
56459
56650
|
}
|
|
56460
56651
|
};
|
|
56461
56652
|
}
|
|
@@ -56474,8 +56665,12 @@ var ReleaseCustodySession = class _ReleaseCustodySession {
|
|
|
56474
56665
|
poison;
|
|
56475
56666
|
static async acquire(store, controller, repo, initialize, prepare) {
|
|
56476
56667
|
const found = await store.read();
|
|
56477
|
-
|
|
56478
|
-
|
|
56668
|
+
const prior = found.state?.controller;
|
|
56669
|
+
if (prior) {
|
|
56670
|
+
const handoff = prior.runId === controller.runId && prior.runAttempt === controller.runAttempt;
|
|
56671
|
+
if (!(handoff ? await store.jobCompleted(prior) : await store.terminal(prior))) {
|
|
56672
|
+
throw new ReleaseCustodyError(handoff ? `previous controller ${prior.job ?? HUB_RELEASE_CUSTODY_JOB} job of run ${prior.runId} attempt ${prior.runAttempt} is not proven completed` : `previous controller run ${prior.runId} attempt ${prior.runAttempt} is not proven terminal`);
|
|
56673
|
+
}
|
|
56479
56674
|
}
|
|
56480
56675
|
let state = {
|
|
56481
56676
|
...found.state ?? { schema: "mmi-release-custody/v1", repo, precutId: (0, import_node_crypto21.randomUUID)(), effects: {}, ...await initialize() },
|
|
@@ -56773,7 +56968,7 @@ function withHubReleaseCustody(deps) {
|
|
|
56773
56968
|
if (!authority.ok || !authority.train || authority.authenticatedDelegation !== true) {
|
|
56774
56969
|
throw new ReleaseCustodyError("repository custody requires the server-verified owner-triggered train session");
|
|
56775
56970
|
}
|
|
56776
|
-
const controller = { ...runIdentity, token: (0, import_node_crypto21.randomUUID)() };
|
|
56971
|
+
const controller = { ...runIdentity, token: (0, import_node_crypto21.randomUUID)(), ...process.env.GITHUB_JOB ? { job: process.env.GITHUB_JOB } : {} };
|
|
56777
56972
|
const raw = localRaw(path2);
|
|
56778
56973
|
const requiredContexts = () => discoverRequiredCheckContexts(
|
|
56779
56974
|
deps,
|
|
@@ -60899,6 +61094,32 @@ function rejectionReason(error) {
|
|
|
60899
61094
|
const text = (error instanceof Error ? error.message : String(error)).trim().split("\n")[0] || "no error detail";
|
|
60900
61095
|
return text.length > 300 ? `${text.slice(0, 300)}\u2026` : text;
|
|
60901
61096
|
}
|
|
61097
|
+
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;
|
|
61098
|
+
async function observePolicy(deps, candidate) {
|
|
61099
|
+
if (!candidate) return void 0;
|
|
61100
|
+
try {
|
|
61101
|
+
return await deps.policy(candidate);
|
|
61102
|
+
} catch (error) {
|
|
61103
|
+
const refusal = rejectionReason(error);
|
|
61104
|
+
if (!PERMISSION_REFUSAL.test(refusal)) throw error;
|
|
61105
|
+
await deps.fetchCandidate?.(candidate).catch(() => void 0);
|
|
61106
|
+
let retried;
|
|
61107
|
+
try {
|
|
61108
|
+
retried = await deps.policy(candidate);
|
|
61109
|
+
} catch (retryError) {
|
|
61110
|
+
const second = rejectionReason(retryError);
|
|
61111
|
+
if (!PERMISSION_REFUSAL.test(second)) throw retryError;
|
|
61112
|
+
return {
|
|
61113
|
+
check: "policy",
|
|
61114
|
+
state: "skipped",
|
|
61115
|
+
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.`
|
|
61116
|
+
};
|
|
61117
|
+
}
|
|
61118
|
+
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)
|
|
61119
|
+
`);
|
|
61120
|
+
return retried;
|
|
61121
|
+
}
|
|
61122
|
+
}
|
|
60902
61123
|
async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
60903
61124
|
const observations = [];
|
|
60904
61125
|
const refs = await deps.candidate().catch((error) => ({ findings: [`candidate refs unavailable (${rejectionReason(error)}); verify the repository and pushed head/base before retrying`] }));
|
|
@@ -60907,7 +61128,7 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
|
60907
61128
|
const reads = await Promise.allSettled([
|
|
60908
61129
|
deps.claims(refs.candidate),
|
|
60909
61130
|
refs.candidate ? deps.docs(refs.candidate) : Promise.resolve(void 0),
|
|
60910
|
-
|
|
61131
|
+
observePolicy(deps, refs.candidate)
|
|
60911
61132
|
]);
|
|
60912
61133
|
const [claims, docs, policy] = reads;
|
|
60913
61134
|
if (claims.status === "fulfilled") {
|
|
@@ -60944,7 +61165,7 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
|
60944
61165
|
await claims.value.restamp();
|
|
60945
61166
|
const beforeCreate = await deps.candidate();
|
|
60946
61167
|
if (beforeCreate.findings.length || JSON.stringify(beforeCreate.candidate) !== JSON.stringify(expected)) {
|
|
60947
|
-
throw new Error("pr create: candidate changed while restamping claims; prepare
|
|
61168
|
+
throw new Error("pr create: candidate identity changed during preparation while restamping claims; prepare the current head and base again");
|
|
60948
61169
|
}
|
|
60949
61170
|
}
|
|
60950
61171
|
};
|
|
@@ -61017,6 +61238,15 @@ function createPreparationDeps(input, io = {}) {
|
|
|
61017
61238
|
return {
|
|
61018
61239
|
candidate: readCandidate,
|
|
61019
61240
|
publish: (proof) => publishPreparedPrHead(proof, exec3),
|
|
61241
|
+
// #9946: the retry needs the exact candidate objects. Fetch each ref on its own so a fork-only head
|
|
61242
|
+
// that origin cannot resolve never costs the base fetch too, and never surface a fetch failure as the
|
|
61243
|
+
// reason the policy read stayed unreadable.
|
|
61244
|
+
fetchCandidate: async (candidate) => {
|
|
61245
|
+
if (!candidate.root) return;
|
|
61246
|
+
const branch = (name) => name.slice(name.lastIndexOf(":") + 1);
|
|
61247
|
+
const refspecs = [.../* @__PURE__ */ new Set([candidate.head, candidate.base])].map((name) => `+refs/heads/${branch(name)}:refs/remotes/origin/${branch(name)}`);
|
|
61248
|
+
await Promise.allSettled(refspecs.map((refspec) => exec3("git", ["-C", candidate.root, "fetch", "--no-tags", "origin", refspec], { timeout: GIT_TIMEOUT_MS })));
|
|
61249
|
+
},
|
|
61020
61250
|
claims: (candidate) => observePrCreateClaims(input.body, candidate?.repo ?? input.repo, { head: candidate?.head, headSha: candidate?.headSha }),
|
|
61021
61251
|
docs: (candidate) => checkDocsIndexAtHead({ repo: candidate.repo, head: candidate.headSha }, docs),
|
|
61022
61252
|
policy: async (candidate) => {
|
|
@@ -62672,6 +62902,46 @@ var RESIDUE_LEDGER_FILE = "mmi-worktree-residue.json";
|
|
|
62672
62902
|
function canonicalWorktreeRoot(primary) {
|
|
62673
62903
|
return (0, import_node_path49.join)((0, import_node_path49.dirname)(primary), "mmi-worktrees", (0, import_node_path49.basename)(primary));
|
|
62674
62904
|
}
|
|
62905
|
+
function lastSegment(p) {
|
|
62906
|
+
return (0, import_node_path49.basename)(p.split(/[\\/]+/).filter(Boolean).pop() ?? p);
|
|
62907
|
+
}
|
|
62908
|
+
function readProvenance(folder, gitCommonDir, exists) {
|
|
62909
|
+
const found = {
|
|
62910
|
+
path: folder,
|
|
62911
|
+
gitdirPresent: false,
|
|
62912
|
+
registrationId: (0, import_node_path49.basename)(folder),
|
|
62913
|
+
registrationPresent: false
|
|
62914
|
+
};
|
|
62915
|
+
try {
|
|
62916
|
+
const pointer = /^\s*gitdir:\s*(.+?)\s*$/m.exec((0, import_node_fs53.readFileSync)((0, import_node_path49.join)(folder, ".git"), "utf8"));
|
|
62917
|
+
if (pointer) {
|
|
62918
|
+
const target = pointer[1];
|
|
62919
|
+
found.gitdir = (0, import_node_path49.isAbsolute)(target) ? target : (0, import_node_path49.join)(folder, target);
|
|
62920
|
+
found.gitdirPresent = exists(found.gitdir);
|
|
62921
|
+
found.registrationId = lastSegment(found.gitdir);
|
|
62922
|
+
}
|
|
62923
|
+
} catch {
|
|
62924
|
+
}
|
|
62925
|
+
try {
|
|
62926
|
+
found.registrationPresent = exists((0, import_node_path49.join)(gitCommonDir, "worktrees", found.registrationId));
|
|
62927
|
+
} catch {
|
|
62928
|
+
}
|
|
62929
|
+
if (found.gitdir && found.gitdirPresent) {
|
|
62930
|
+
try {
|
|
62931
|
+
const head = (0, import_node_fs53.readFileSync)((0, import_node_path49.join)(found.gitdir, "HEAD"), "utf8").trim();
|
|
62932
|
+
const branch = /^ref:\s*(.+?)\s*$/.exec(head);
|
|
62933
|
+
if (branch) found.head = branch[1].startsWith("refs/heads/") ? branch[1].slice("refs/heads/".length) : `ref ${branch[1]}`;
|
|
62934
|
+
else if (/^[0-9a-f]{7,64}$/i.test(head)) found.head = `detached ${head.slice(0, 12)}`;
|
|
62935
|
+
else if (head) found.head = `ref ${head}`;
|
|
62936
|
+
} catch {
|
|
62937
|
+
}
|
|
62938
|
+
}
|
|
62939
|
+
try {
|
|
62940
|
+
found.mtime = (0, import_node_fs53.statSync)(folder).mtime.toISOString();
|
|
62941
|
+
} catch {
|
|
62942
|
+
}
|
|
62943
|
+
return found;
|
|
62944
|
+
}
|
|
62675
62945
|
function norm2(p) {
|
|
62676
62946
|
const s = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
62677
62947
|
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
@@ -62766,7 +63036,7 @@ async function sweepWorktreeResidue(gitCommonDir, registered, deps = {}) {
|
|
|
62766
63036
|
} catch {
|
|
62767
63037
|
}
|
|
62768
63038
|
} else {
|
|
62769
|
-
report.unrecorded.push(child2);
|
|
63039
|
+
report.unrecorded.push(readProvenance(child2, gitCommonDir, exists));
|
|
62770
63040
|
}
|
|
62771
63041
|
}
|
|
62772
63042
|
}
|
|
@@ -62780,12 +63050,19 @@ function renderResidueSweepLines(report) {
|
|
|
62780
63050
|
if (cleaned) lines2.push(`pr merge: removed ${cleaned} leftover worktree folder(s) from earlier merges${where}`);
|
|
62781
63051
|
for (const k of report.kept) lines2.push(`pr merge: leftover folder still locked, retried on the next merge \u2014 ${k.path}`);
|
|
62782
63052
|
if (report.unrecorded.length) {
|
|
62783
|
-
const
|
|
63053
|
+
const listed = report.unrecorded.slice(0, 5);
|
|
62784
63054
|
const more = report.unrecorded.length > 5 ? `, \u2026 ${report.unrecorded.length - 5} more` : "";
|
|
62785
|
-
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): ${
|
|
63055
|
+
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}`);
|
|
63056
|
+
for (const u of listed) lines2.push(`pr merge: left untouched ${u.path} \u2014 ${provenanceText(u)}`);
|
|
62786
63057
|
}
|
|
62787
63058
|
return lines2;
|
|
62788
63059
|
}
|
|
63060
|
+
function provenanceText(u) {
|
|
63061
|
+
const pointer = u.gitdir ? `.git \u2192 ${u.gitdir}${u.gitdirPresent ? "" : " (admin dir gone)"}` : ".git pointer missing";
|
|
63062
|
+
const registration = `worktrees/${u.registrationId} ${u.registrationPresent ? "registered" : "not registered"}`;
|
|
63063
|
+
const head = u.head ? `HEAD ${u.head}` : "HEAD unknown";
|
|
63064
|
+
return `${pointer}; ${registration}; ${head}; ${u.mtime ? `modified ${u.mtime}` : "mtime unknown"}`;
|
|
63065
|
+
}
|
|
62789
63066
|
|
|
62790
63067
|
// src/command-register-collaboration.ts
|
|
62791
63068
|
var DEFAULT_PRIORITY = "medium";
|
|
@@ -63935,9 +64212,19 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
63935
64212
|
}
|
|
63936
64213
|
class PrHeadBehindBaseError extends Error {
|
|
63937
64214
|
}
|
|
63938
|
-
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) => {
|
|
64215
|
+
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) => {
|
|
63939
64216
|
const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
|
|
63940
64217
|
if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
|
|
64218
|
+
if (opts.abandonAdmission) {
|
|
64219
|
+
if (numbers.length !== 1) return fail("pr land: --abandon-admission abandons one PR per call");
|
|
64220
|
+
const repo = await resolveRepo(opts.repo).catch(() => void 0) ?? opts.repo;
|
|
64221
|
+
if (!repo) return fail("pr land: --abandon-admission needs --repo <owner/repo>; the PR repo could not be resolved");
|
|
64222
|
+
const outcome = await abandonQueueAdmission(numbers[0], repo);
|
|
64223
|
+
if (opts.json) printLine(JSON.stringify(outcome));
|
|
64224
|
+
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}`);
|
|
64225
|
+
if (outcome.status !== "abandoned") process.exitCode = 1;
|
|
64226
|
+
return;
|
|
64227
|
+
}
|
|
63941
64228
|
if (opts.expectHead !== void 0) {
|
|
63942
64229
|
if (!/^[0-9a-f]{40}$/i.test(opts.expectHead)) return fail("pr land: --expect-head needs the full 40-hex head SHA");
|
|
63943
64230
|
if (numbers.length !== 1) return fail("pr land: --expect-head pins one PR head; land one PR per call");
|
|
@@ -71555,6 +71842,54 @@ function renderTenantLogs(r) {
|
|
|
71555
71842
|
return out.join("\n");
|
|
71556
71843
|
}
|
|
71557
71844
|
|
|
71845
|
+
// src/tenant-status-wait.ts
|
|
71846
|
+
var TENANT_STATUS_WAIT_INTERVAL_MS = 12e3;
|
|
71847
|
+
var TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S = 660;
|
|
71848
|
+
var TENANT_STATUS_WAIT_TIMEOUT_MAX_S = 1800;
|
|
71849
|
+
var TENANT_STATUS_WAIT_MIN_SHA_LENGTH = 7;
|
|
71850
|
+
function parseWaitForSha(raw) {
|
|
71851
|
+
const sha = raw.trim().toLowerCase();
|
|
71852
|
+
if (!new RegExp(`^[0-9a-f]{${TENANT_STATUS_WAIT_MIN_SHA_LENGTH},40}$`).test(sha)) {
|
|
71853
|
+
return { ok: false, reason: `--wait-for must be a commit SHA (${TENANT_STATUS_WAIT_MIN_SHA_LENGTH}-40 hex characters), got "${raw.trim()}"` };
|
|
71854
|
+
}
|
|
71855
|
+
return { ok: true, sha };
|
|
71856
|
+
}
|
|
71857
|
+
function parseWaitTimeoutSeconds(raw) {
|
|
71858
|
+
if (raw === void 0) return { ok: true, seconds: TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S, clamped: false };
|
|
71859
|
+
const seconds = Number(raw.trim());
|
|
71860
|
+
if (!Number.isInteger(seconds) || seconds <= 0) {
|
|
71861
|
+
return { ok: false, reason: `--timeout must be a positive whole number of seconds, got "${raw.trim()}"` };
|
|
71862
|
+
}
|
|
71863
|
+
if (seconds > TENANT_STATUS_WAIT_TIMEOUT_MAX_S) return { ok: true, seconds: TENANT_STATUS_WAIT_TIMEOUT_MAX_S, clamped: true };
|
|
71864
|
+
return { ok: true, seconds, clamped: false };
|
|
71865
|
+
}
|
|
71866
|
+
function deployedTagReaches(tag, sha) {
|
|
71867
|
+
const stamped = (tag ?? "").trim().toLowerCase();
|
|
71868
|
+
const needle = sha.trim().toLowerCase();
|
|
71869
|
+
if (!stamped || !needle) return false;
|
|
71870
|
+
if (stamped === needle) return true;
|
|
71871
|
+
const [shorter, longer] = stamped.length <= needle.length ? [stamped, needle] : [needle, stamped];
|
|
71872
|
+
return shorter.length >= TENANT_STATUS_WAIT_MIN_SHA_LENGTH && longer.startsWith(shorter);
|
|
71873
|
+
}
|
|
71874
|
+
async function waitForDeployedTag(deps) {
|
|
71875
|
+
const intervalMs = deps.intervalMs ?? TENANT_STATUS_WAIT_INTERVAL_MS;
|
|
71876
|
+
const now = deps.now ?? Date.now;
|
|
71877
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
71878
|
+
const startedAt = now();
|
|
71879
|
+
const deadline = startedAt + deps.timeoutMs;
|
|
71880
|
+
let polls = 0;
|
|
71881
|
+
for (; ; ) {
|
|
71882
|
+
const status = await deps.readStatus();
|
|
71883
|
+
polls += 1;
|
|
71884
|
+
if (deployedTagReaches(deps.deployedTagOf(status), deps.sha)) {
|
|
71885
|
+
return { matched: true, status, polls, elapsedMs: now() - startedAt };
|
|
71886
|
+
}
|
|
71887
|
+
const remaining = deadline - now();
|
|
71888
|
+
if (remaining <= 0) return { matched: false, status, polls, elapsedMs: now() - startedAt };
|
|
71889
|
+
await sleep2(Math.min(intervalMs, remaining));
|
|
71890
|
+
}
|
|
71891
|
+
}
|
|
71892
|
+
|
|
71558
71893
|
// src/tenant-sweep.ts
|
|
71559
71894
|
function isRcBearingTenant(p) {
|
|
71560
71895
|
if ((p.deployModel ?? "") !== "tenant-container") return false;
|
|
@@ -73613,12 +73948,33 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
|
|
|
73613
73948
|
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
73614
73949
|
}
|
|
73615
73950
|
});
|
|
73616
|
-
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) => {
|
|
73951
|
+
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) => {
|
|
73617
73952
|
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
73953
|
+
let wait;
|
|
73954
|
+
if (o.waitFor !== void 0) {
|
|
73955
|
+
const sha = parseWaitForSha(o.waitFor);
|
|
73956
|
+
if (!sha.ok) return fail(`runtime tenant status: ${sha.reason}`);
|
|
73957
|
+
const timeout = parseWaitTimeoutSeconds(o.timeout);
|
|
73958
|
+
if (!timeout.ok) return fail(`runtime tenant status: ${timeout.reason}`);
|
|
73959
|
+
wait = { sha: sha.sha, seconds: timeout.seconds, clamped: timeout.clamped };
|
|
73960
|
+
} else if (o.timeout !== void 0) {
|
|
73961
|
+
return fail("runtime tenant status: --timeout only applies with --wait-for <sha>");
|
|
73962
|
+
}
|
|
73618
73963
|
const cfg = await loadConfig();
|
|
73619
|
-
const
|
|
73964
|
+
const readStatus = () => buildTenantRuntimeStatusFor(repo, stage, cfg);
|
|
73965
|
+
let result;
|
|
73966
|
+
let waitMatched;
|
|
73967
|
+
if (!wait) result = await readStatus();
|
|
73968
|
+
else {
|
|
73969
|
+
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)` : ""}`);
|
|
73970
|
+
const waited = await waitForDeployedTag({ sha: wait.sha, timeoutMs: wait.seconds * 1e3, readStatus, deployedTagOf: (status) => status.deploy?.deployedTag });
|
|
73971
|
+
result = waited.status;
|
|
73972
|
+
waitMatched = waited.matched;
|
|
73973
|
+
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}`);
|
|
73974
|
+
}
|
|
73620
73975
|
console.log(JSON.stringify(result, null, 2));
|
|
73621
|
-
if (
|
|
73976
|
+
if (waitMatched !== void 0) process.exitCode = waitMatched ? 0 : 1;
|
|
73977
|
+
else if (result.publicProbe?.ok === false || result.edgeCertificate?.missing) process.exitCode = 1;
|
|
73622
73978
|
});
|
|
73623
73979
|
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) => {
|
|
73624
73980
|
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
|
|
@@ -55,6 +55,8 @@ mmi-cli devops bootstrap apply "$OWNER/$REPO" --class deployable \
|
|
|
55
55
|
|
|
56
56
|
`tenant-container` repos need a `docker-compose.yml` and Dockerfile building from the shipped source archive (no prebuilt `dist/`); bootstrap seeds both from `skills/bootstrap/seeds/`. The seeded compose is fileless: label `mmi.runtime-env: "true"`, no `env_file`, loopback-only port; deploy injects the stage's vault values (`DEPLOY#<stage>.noEnvFile: true`), so the app reads plain env vars, never self-loads SSM, never ships a `.env`. For an MM-Work module or non-suite web-app with OAuth META, read [reference.md](reference.md) before the first CI/deploy or OAuth provisioning.
|
|
57
57
|
|
|
58
|
+
`bootstrap apply --execute` also asks Jerv-Hub for this repo's coding-agent memory bank, by filing one `bank-request: coding-agent::<RepoName>` issue (label `bank-request`) there; if it reports that step SKIPPED with a manual next step, the MMI App could not file it — file that issue by hand at https://github.com/mutmutco/Jerv-Hub/issues/new and carry on.
|
|
59
|
+
|
|
58
60
|
**Stage port block:** `tenant-container` and `solo-container` need Hub registry `portRange` (`bootstrap verify` fails without it). `bootstrap apply --execute` assigns it; for older META run `mmi-cli stage port-range <Repo>` (idempotent).
|
|
59
61
|
|
|
60
62
|
## Step 7 — org-managed .gitignore block
|