@mutmutco/codex-plugin 4.6.33 → 4.6.35

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.6.33",
3
+ "version": "4.6.35",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -15497,6 +15497,7 @@ var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
15497
15497
  ["run cancel", { metadata: "read", actions: "write" }],
15498
15498
  // #9587: one named run rerun (all or failed jobs) — same actions:write profile, nothing broader.
15499
15499
  ["run rerun", { metadata: "read", actions: "write" }],
15500
+ ["run dispatch", { metadata: "read", actions: "write" }],
15500
15501
  // #9780: repository Actions variables — get/list mint only Variables:read; create/update/delete
15501
15502
  // Variables:write. Repository scope only; no secret store, no rename, no workflow dispatch.
15502
15503
  // #9790: the token key is `actions_variables` (as ci-reconcile.yml mints); `variables` was refused 422.
@@ -28366,10 +28367,10 @@ var rollout_plan_default = {
28366
28367
  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)."
28367
28368
  },
28368
28369
  baseline: {
28369
- version: "4.6.33",
28370
- tag: "v4.6.33",
28371
- commit: "80768b0b2052",
28372
- npm: "@mutmutco/cli@4.6.33"
28370
+ version: "4.6.35",
28371
+ tag: "v4.6.35",
28372
+ commit: "2a54cf011975",
28373
+ npm: "@mutmutco/cli@4.6.35"
28373
28374
  },
28374
28375
  exitCriterion: "fleet-n-of-n",
28375
28376
  hubOnlyShortcut: "forbidden",
@@ -28386,14 +28387,14 @@ var rollout_plan_default = {
28386
28387
  repo: "mutmutco/mmi-hub",
28387
28388
  role: "canary",
28388
28389
  schedule: "train",
28389
- v3Target: "v4.6.33"
28390
+ v3Target: "v4.6.35"
28390
28391
  }
28391
28392
  ],
28392
28393
  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.",
28393
28394
  rollback: {
28394
28395
  independent: true,
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)"
28396
+ mechanism: "npm dist-tag latest -> 4.6.35 and redeploy the Hub Lambda from tag v4.6.35 (2a54cf011975); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28397
+ v3Target: "v4.6.35 (@mutmutco/cli@4.6.35, tag commit 2a54cf011975 \u2014 last known-good release carrying the repo-index v4-only contract)"
28397
28398
  }
28398
28399
  },
28399
28400
  {
@@ -30698,6 +30699,8 @@ var CORRELATE_ATTEMPTS = 5;
30698
30699
  var CORRELATE_DELAY_MS = 1500;
30699
30700
  var CORRELATE_SKEW_SLACK_MS = 1e4;
30700
30701
  var CORRELATE_PAGE_LIMIT = 50;
30702
+ var GET_PUBLISH_CORRELATE_ATTEMPTS = 12;
30703
+ var GET_PUBLISH_CORRELATE_DELAY_MS = 2500;
30701
30704
  var RUN_CONFIRM_ATTEMPTS = 3;
30702
30705
  var RUN_CONFIRM_DELAY_MS = 1e3;
30703
30706
  var defaultSleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
@@ -30744,11 +30747,13 @@ async function correlateRun(deps, args) {
30744
30747
  const sleep2 = resolveSleep(deps);
30745
30748
  const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
30746
30749
  const repo = args.mode === "workflow" ? args.repo ?? HUB_REPO2 : HUB_REPO2;
30750
+ const attempts = args.mode === "dispatch" ? args.attempts ?? CORRELATE_ATTEMPTS : CORRELATE_ATTEMPTS;
30751
+ const delayMs = args.mode === "dispatch" ? args.delayMs ?? CORRELATE_DELAY_MS : CORRELATE_DELAY_MS;
30747
30752
  let lastError;
30748
30753
  let parsedAnyResponse = false;
30749
30754
  let truncatedWindow = false;
30750
- for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
30751
- if (attempt > 0) await sleep2(CORRELATE_DELAY_MS);
30755
+ for (let attempt = 0; attempt < attempts; attempt++) {
30756
+ if (attempt > 0) await sleep2(delayMs);
30752
30757
  const listArgs = [
30753
30758
  "run",
30754
30759
  "list",
@@ -30791,7 +30796,7 @@ async function correlateRun(deps, args) {
30791
30796
  if (!parsedAnyResponse) {
30792
30797
  return {
30793
30798
  read: "failed",
30794
- error: `\`gh run list --workflow ${args.workflow} --repo ${repo}\` failed on all ${CORRELATE_ATTEMPTS} attempts${lastError ? ` (${describeReadError(lastError)})` : ""}`
30799
+ error: `\`gh run list --workflow ${args.workflow} --repo ${repo}\` failed on all ${attempts} attempts${lastError ? ` (${describeReadError(lastError)})` : ""}`
30795
30800
  };
30796
30801
  }
30797
30802
  if (truncatedWindow) {
@@ -30806,7 +30811,7 @@ function correlateTenantRun(deps, since, titleIncludes) {
30806
30811
  return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch", titleIncludes });
30807
30812
  }
30808
30813
  function correlateGetPublishRun(deps, since, product, mode) {
30809
- return correlateRun(deps, { workflow: "get-publish.yml", since, mode: "dispatch", titleIncludes: [product, ...mode ? [mode] : []], ...mode ? { requireTitleMatch: true } : {} });
30814
+ return correlateRun(deps, { workflow: "get-publish.yml", since, mode: "dispatch", titleIncludes: [product, ...mode ? [mode] : []], ...mode ? { requireTitleMatch: true } : {}, attempts: GET_PUBLISH_CORRELATE_ATTEMPTS, delayMs: GET_PUBLISH_CORRELATE_DELAY_MS });
30810
30815
  }
30811
30816
  function correlateControlRun(deps, since, titleIncludes) {
30812
30817
  return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes, titleFieldsFrom: 2, ...titleIncludes?.length ? { requireTitleMatch: true } : {} });
@@ -31937,7 +31942,7 @@ async function fetchServedChannelVersion(url, fetchImpl = fetch) {
31937
31942
  return { ok: false, error: e instanceof Error ? e.message : String(e) };
31938
31943
  }
31939
31944
  }
31940
- async function findPriorGreenDispatch(deps, repo, workflow, sha, titleTokens) {
31945
+ async function findPriorGreenDispatch(deps, repo, workflow, sha, titleTokens, opts = {}) {
31941
31946
  if (!/^[0-9a-f]{40}$/i.test(sha)) return void 0;
31942
31947
  try {
31943
31948
  const rows = JSON.parse(await deps.run("gh", [
@@ -31956,12 +31961,18 @@ async function findPriorGreenDispatch(deps, repo, workflow, sha, titleTokens) {
31956
31961
  "--json",
31957
31962
  "databaseId,url,status,conclusion,displayTitle,headSha"
31958
31963
  ]));
31959
- const hit = rows.find((r) => typeof r.databaseId === "number" && r.headSha?.toLowerCase() === sha.toLowerCase() && r.status === "completed" && r.conclusion === "success" && titleTokens.every((t) => (r.displayTitle ?? "").split(/[^\w.-]+/).includes(t)));
31960
- return hit ? { runId: hit.databaseId, ...hit.url ? { runUrl: hit.url } : {} } : void 0;
31964
+ const titleMatch = (r) => titleTokens.every((t) => (r.displayTitle ?? "").split(/[^\w.-]+/).includes(t));
31965
+ const atSha = (r) => r.headSha?.toLowerCase() === sha.toLowerCase();
31966
+ const hit = rows.find((r) => typeof r.databaseId === "number" && atSha(r) && r.status === "completed" && r.conclusion === "success" && titleMatch(r));
31967
+ if (hit) return { runId: hit.databaseId, ...hit.url ? { runUrl: hit.url } : {} };
31968
+ if (!opts.includeActive) return void 0;
31969
+ const active = rows.find((r) => typeof r.databaseId === "number" && atSha(r) && PRIOR_DISPATCH_ACTIVE_STATUSES.has(r.status ?? "") && titleMatch(r));
31970
+ return active ? { runId: active.databaseId, ...active.url ? { runUrl: active.url } : {}, active: true } : void 0;
31961
31971
  } catch {
31962
31972
  return void 0;
31963
31973
  }
31964
31974
  }
31975
+ var PRIOR_DISPATCH_ACTIVE_STATUSES = /* @__PURE__ */ new Set(["in_progress", "queued", "waiting", "requested", "pending"]);
31965
31976
  function planeFailureRow(workflow, evidence) {
31966
31977
  return { workflow, runUrlNote: evidence, conclusion: "failure" };
31967
31978
  }
@@ -32040,10 +32051,13 @@ async function appendMmiGetRedeploy(deps, input, dispatch) {
32040
32051
  async function appendServedChannelStage(deps, input, dispatch) {
32041
32052
  if (!isHubControlRepo(input.repo)) return dispatch;
32042
32053
  const remedy = `${getPublishDispatchCommand(input.repo)} -f mode=stage`;
32043
- const prior = await findPriorGreenDispatch(deps, input.repo, "get-publish.yml", input.releaseSha, [HUB_SERVED_PRODUCT, "stage"]);
32054
+ const prior = await findPriorGreenDispatch(deps, input.repo, "get-publish.yml", input.releaseSha, [HUB_SERVED_PRODUCT, "stage"], { includeActive: true });
32044
32055
  let row;
32045
- if (prior) {
32056
+ if (prior && !prior.active) {
32046
32057
  row = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {}, conclusion: "success" };
32058
+ } else if (prior?.active) {
32059
+ row = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {}, conclusion: "pending" };
32060
+ if (input.watch) row.conclusion = await watchTenantRun(deps, prior.runId, input.repo);
32047
32061
  } else {
32048
32062
  const since = (deps.now ?? Date.now)();
32049
32063
  try {
@@ -32058,7 +32072,7 @@ async function appendServedChannelStage(deps, input, dispatch) {
32058
32072
  };
32059
32073
  }
32060
32074
  const correlated = await correlateGetPublishRun(deps, since, HUB_SERVED_PRODUCT, "stage");
32061
- if (correlated.read !== "found") {
32075
+ if (correlated.read === "failed") {
32062
32076
  return {
32063
32077
  ...dispatch,
32064
32078
  note: `${dispatch.note}; get-publish stage dispatched but its run could not be READ \u2014 check Actions; recover with \`${remedy}\``,
@@ -32066,6 +32080,14 @@ async function appendServedChannelStage(deps, input, dispatch) {
32066
32080
  workflowRuns: [...dispatch.workflowRuns ?? [], planeFailureRow("get-publish.yml", "stage dispatched, but its run could not be read")]
32067
32081
  };
32068
32082
  }
32083
+ if (correlated.read === "absent") {
32084
+ const dispatchedAt = new Date(since).toISOString();
32085
+ return {
32086
+ ...dispatch,
32087
+ note: `${dispatch.note}; get-publish stage dispatched at ${dispatchedAt} but its run is not listed yet \u2014 recorded PENDING; \`devops release --resume\` re-correlates it by title and time`,
32088
+ workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "get-publish.yml", runUrlNote: `stage dispatched ${dispatchedAt}; run not listed within the correlation window`, conclusion: "pending", dispatchedAt, titleTokens: [HUB_SERVED_PRODUCT, "stage"] }]
32089
+ };
32090
+ }
32069
32091
  row = { workflow: "get-publish.yml", runId: correlated.runId, ...correlated.runUrl ? { runUrl: correlated.runUrl } : {}, conclusion: "pending" };
32070
32092
  if (input.watch) row.conclusion = await watchTenantRun(deps, correlated.runId, input.repo);
32071
32093
  }
@@ -32090,12 +32112,15 @@ async function appendServedChannelPublish(deps, input, dispatch, redeployed) {
32090
32112
  if (input.publishStatus !== "success") {
32091
32113
  return withNote(`served channel NOT moved (publish did not succeed) \u2014 after the publish is green, run \`${remedy}\``);
32092
32114
  }
32093
- const prior = input.releaseSha ? await findPriorGreenDispatch(deps, input.repo, "get-publish.yml", input.releaseSha, [HUB_SERVED_PRODUCT, "pointer"]) : void 0;
32115
+ const prior = input.releaseSha ? await findPriorGreenDispatch(deps, input.repo, "get-publish.yml", input.releaseSha, [HUB_SERVED_PRODUCT, "pointer"], { includeActive: true }) : void 0;
32094
32116
  let runRow;
32095
32117
  let runStatus;
32096
- if (prior) {
32118
+ if (prior && !prior.active) {
32097
32119
  runRow = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {} };
32098
32120
  runStatus = "success";
32121
+ } else if (prior?.active) {
32122
+ runRow = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {} };
32123
+ runStatus = input.watch ? await watchTenantRun(deps, prior.runId, input.repo) : "pending";
32099
32124
  } else {
32100
32125
  const version = input.tag?.replace(/^v/, "") ?? (deps.readLauncherVersion ?? readLocalLauncherVersion)();
32101
32126
  if (!version) return withNote("the local launcher version could not be read \u2014 nothing was compared, so the move is unverified");
@@ -32107,9 +32132,17 @@ async function appendServedChannelPublish(deps, input, dispatch, redeployed) {
32107
32132
  return withNote(`get-publish dispatch FAILED: ${msg}. The release itself landed, but the SERVED CHANNEL IS UNCHANGED \u2014 recover with \`${pointerRemedy}\``, "failure", planeFailureRow("get-publish.yml", `dispatch refused: ${msg}`));
32108
32133
  }
32109
32134
  const correlated = await correlateGetPublishRun(deps, since, HUB_SERVED_PRODUCT, "pointer");
32110
- if (correlated.read !== "found") {
32135
+ if (correlated.read === "failed") {
32111
32136
  return withNote(`get-publish dispatched but its run could not be READ: ${correlated.error ?? "no matching run appeared"}. Check the Actions tab; if it never ran, recover with \`${pointerRemedy}\``, "failure", planeFailureRow("get-publish.yml", `dispatched, but its run could not be read`));
32112
32137
  }
32138
+ if (correlated.read === "absent") {
32139
+ const dispatchedAt = new Date(since).toISOString();
32140
+ return {
32141
+ ...dispatch,
32142
+ note: `${dispatch.note}; get-publish pointer dispatched at ${dispatchedAt} but its run is not listed yet \u2014 recorded PENDING; \`devops release --resume\` re-correlates it by title and time`,
32143
+ workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "get-publish.yml", runUrlNote: `pointer dispatched ${dispatchedAt}; run not listed within the correlation window`, conclusion: "pending", dispatchedAt, titleTokens: [HUB_SERVED_PRODUCT, "pointer"] }]
32144
+ };
32145
+ }
32113
32146
  const { runId, runUrl } = correlated;
32114
32147
  runRow = { workflow: "get-publish.yml", ...runId ? { runId } : {}, ...runUrl ? { runUrl } : {} };
32115
32148
  if (!input.watch) {
@@ -32121,7 +32154,7 @@ async function appendServedChannelPublish(deps, input, dispatch, redeployed) {
32121
32154
  }
32122
32155
  runStatus = await watchTenantRun(deps, runId, input.repo);
32123
32156
  }
32124
- if (runStatus !== "success") {
32157
+ if (runStatus === "failure") {
32125
32158
  return {
32126
32159
  ...dispatch,
32127
32160
  note: `${dispatch.note}; get-publish.yml ${runStatus} \u2014 the SERVED CHANNEL IS UNCHANGED; recover with \`${pointerRemedy}\``,
@@ -32129,6 +32162,13 @@ async function appendServedChannelPublish(deps, input, dispatch, redeployed) {
32129
32162
  deployStatus: "failure"
32130
32163
  };
32131
32164
  }
32165
+ if (runStatus === "pending") {
32166
+ return {
32167
+ ...dispatch,
32168
+ note: `${dispatch.note}; get-publish.yml pointer run is still unverified (pending) \u2014 the served channel move is not yet proven; \`devops release --resume\` re-reads it`,
32169
+ workflowRuns: [...dispatch.workflowRuns ?? [], { ...runRow, conclusion: "pending" }]
32170
+ };
32171
+ }
32132
32172
  const expected = input.tag ? input.tag.replace(/^v/, "") : (deps.readLauncherVersion ?? readLocalLauncherVersion)();
32133
32173
  const runs2 = [...dispatch.workflowRuns ?? [], { ...runRow, conclusion: "success" }];
32134
32174
  if (!channelUrl) {
@@ -32269,6 +32309,14 @@ async function appendInstallHomePublish(deps, input, dispatch) {
32269
32309
  deployStatus: "failure"
32270
32310
  };
32271
32311
  }
32312
+ if (correlated.read === "absent") {
32313
+ const dispatchedAt = new Date(since).toISOString();
32314
+ return {
32315
+ ...dispatch,
32316
+ note: note(`installer home publish for ${product} dispatched at ${dispatchedAt} but its run is not listed yet \u2014 recorded PENDING; \`devops release --resume\` re-correlates it by title and time`),
32317
+ workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "get-publish.yml", runUrlNote: `installer home (${product}) dispatched ${dispatchedAt}; run not listed within the correlation window`, conclusion: "pending", dispatchedAt, titleTokens: [product] }]
32318
+ };
32319
+ }
32272
32320
  const row = { workflow: "get-publish.yml", ...correlated.runId ? { runId: correlated.runId } : {}, ...correlated.runUrl ? { runUrl: correlated.runUrl } : {} };
32273
32321
  if (!input.watch) {
32274
32322
  return {
@@ -32278,7 +32326,7 @@ async function appendInstallHomePublish(deps, input, dispatch) {
32278
32326
  };
32279
32327
  }
32280
32328
  const status = await watchTenantRun(deps, correlated.runId, HUB_REPO2);
32281
- if (status !== "success") {
32329
+ if (status === "failure") {
32282
32330
  return {
32283
32331
  ...dispatch,
32284
32332
  note: note(`get-publish.yml (product=${product}) ${status} \u2014 the INSTALLER HOME IS UNCHANGED; recover with \`${remedy}\``),
@@ -32286,6 +32334,13 @@ async function appendInstallHomePublish(deps, input, dispatch) {
32286
32334
  deployStatus: "failure"
32287
32335
  };
32288
32336
  }
32337
+ if (status === "pending") {
32338
+ return {
32339
+ ...dispatch,
32340
+ note: note(`get-publish.yml (product=${product}) run is still unverified (pending) \u2014 the installer home is not yet proven published; \`devops release --resume\` re-reads it`),
32341
+ workflowRuns: [...dispatch.workflowRuns ?? [], { ...row, conclusion: "pending" }]
32342
+ };
32343
+ }
32289
32344
  return {
32290
32345
  ...dispatch,
32291
32346
  note: note(`installer home published for ${product} (get-publish.yml green)`),
@@ -32324,8 +32379,10 @@ async function appendServedChannelLegs(deps, input, dispatch) {
32324
32379
  ]);
32325
32380
  return mergeServedLegs(dispatch, home2, proof2);
32326
32381
  }
32327
- const stageGreen = (prepared.workflowRuns ?? []).filter((r) => r.workflow === "get-publish.yml").every((r) => r.conclusion === "success");
32328
- const channel = stageGreen || !isHubControlRepo(input.repo) || publishStatus !== "success" ? await appendServedChannelPublish(deps, { repo: input.repo, publishStatus, watch: input.watch, releaseSha: input.releaseSha, tag: input.tag }, dispatch, Promise.resolve(prepared)) : { ...dispatch, note: `${dispatch.note}; served channel NOT moved (stage not green) \u2014 recover with \`${getPublishDispatchCommand(input.repo)} -f mode=stage\`, then \`${getPublishDispatchCommand(input.repo)} -f mode=pointer -f version=${input.tag.replace(/^v/, "")}\`` };
32382
+ const stageRows = (prepared.workflowRuns ?? []).filter((r) => r.workflow === "get-publish.yml");
32383
+ const stageGreen = stageRows.every((r) => r.conclusion === "success");
32384
+ const stagePending = !stageGreen && stageRows.every((r) => r.conclusion !== "failure");
32385
+ const channel = stageGreen || !isHubControlRepo(input.repo) || publishStatus !== "success" ? await appendServedChannelPublish(deps, { repo: input.repo, publishStatus, watch: input.watch, releaseSha: input.releaseSha, tag: input.tag }, dispatch, Promise.resolve(prepared)) : { ...dispatch, note: `${dispatch.note}; served channel NOT moved (stage ${stagePending ? "still pending \u2014 `devops release --resume` re-correlates the stage run and moves the pointer" : "not green"}) \u2014 recover with \`${getPublishDispatchCommand(input.repo)} -f mode=stage\`, then \`${getPublishDispatchCommand(input.repo)} -f mode=pointer -f version=${input.tag.replace(/^v/, "")}\`` };
32329
32386
  const out = mergeServedLegs(dispatch, {
32330
32387
  ...dispatch,
32331
32388
  note: dispatch.note + prepared.note,
@@ -44822,6 +44879,56 @@ function registerRunRerun(run, writeToken, projects) {
44822
44879
  });
44823
44880
  }
44824
44881
 
44882
+ // src/run-dispatch.ts
44883
+ init_cli_shared();
44884
+ async function dispatchWorkflow(repo, workflow, ref, inputs, deps) {
44885
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error("run dispatch: invalid repository");
44886
+ if (!/^[A-Za-z0-9_.-]+\.ya?ml$/.test(workflow)) throw new Error("run dispatch: workflow must be a YAML filename");
44887
+ if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/.test(ref) || ref.includes("..") || ref.includes("//") || ref.endsWith("/")) throw new Error("run dispatch: invalid ref");
44888
+ if (inputs.length > 10) throw new Error("run dispatch: at most 10 inputs");
44889
+ const fields = ["-f", `ref=${ref}`];
44890
+ const names = /* @__PURE__ */ new Set();
44891
+ for (const input of inputs) {
44892
+ const match = /^([A-Za-z_][A-Za-z0-9_]{0,63})=([^\r\n]{0,2048})$/.exec(input);
44893
+ if (!match || names.has(match[1])) throw new Error("run dispatch: invalid or duplicate input");
44894
+ names.add(match[1]);
44895
+ fields.push("-f", `inputs[${match[1]}]=${match[2]}`);
44896
+ }
44897
+ const projects = await deps.projects();
44898
+ if (!projects) throw new Error("run dispatch: registry unavailable; no dispatch attempted");
44899
+ if (!projects.some((p) => (p.repos ?? []).some((r) => typeof r === "string" && registryRepoKey(qualifyRegistryRepo(r)) === registryRepoKey(repo)))) {
44900
+ throw new Error("run dispatch: repository is not registered; no dispatch attempted");
44901
+ }
44902
+ let status;
44903
+ try {
44904
+ status = await deps.post(`repos/${repo}/actions/workflows/${workflow}/dispatches`, fields);
44905
+ } catch {
44906
+ throw new Error("run dispatch: outcome unknown; inspect GitHub before any retry");
44907
+ }
44908
+ if (status !== 204) throw new Error("run dispatch: outcome unknown; inspect GitHub before any retry");
44909
+ return { ok: true, repo, workflow, ref, credential: "app_installation", status: "dispatch-requested", httpStatus: status };
44910
+ }
44911
+ function appDispatchPost(writeToken, exec3 = execFileP) {
44912
+ return async (path2, fields) => {
44913
+ const token = await writeToken();
44914
+ const { stdout } = await exec3("gh", ["api", "--include", "--method", "POST", path2, ...fields], {
44915
+ timeout: 2e4,
44916
+ env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
44917
+ });
44918
+ return Number(/^HTTP\/[\d.]+ (\d{3})/.exec(stdout)?.[1] ?? 0);
44919
+ };
44920
+ }
44921
+ function registerRunDispatch(run, writeToken, projects) {
44922
+ run.command("dispatch <workflow>").description("request one workflow dispatch using the org App; an ambiguous result is never retried").requiredOption("--repo <owner/repo>", "registered repository").requiredOption("--ref <ref>", "workflow ref").option("--input <name=value>", "workflow input (repeatable)", (v, values) => [...values, v], []).option("--json", "machine-readable receipt").action(async (workflow, options) => {
44923
+ try {
44924
+ const receipt = await dispatchWorkflow(options.repo, workflow, options.ref, options.input, { projects, post: appDispatchPost(writeToken) });
44925
+ console.log(options.json ? JSON.stringify(receipt) : `run dispatch: requested ${receipt.repo}/${receipt.workflow} at ${receipt.ref}`);
44926
+ } catch (error) {
44927
+ return fail(error.message);
44928
+ }
44929
+ });
44930
+ }
44931
+
44825
44932
  // src/devops-vars.ts
44826
44933
  var import_node_crypto13 = require("node:crypto");
44827
44934
  init_github_client();
@@ -46300,7 +46407,7 @@ function refuseNotClaimable(e, json) {
46300
46407
  process.exitCode = 1;
46301
46408
  return true;
46302
46409
  }
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";
46410
+ 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:\n {project, viewer, repo, primary, secondary, items, 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 for ownership context:\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\n`items` (#9974) is that same flat row array, already on the full report: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n node -e \"const b=require('./.jerv/tmp/board.json');for(const i of b.items)console.log(i.number,i.status,i.title)\"\nIt is built from the same flattener as --flat, so `const rows = j.items || j` is now safe on either form.\n\nAdd --flat with --json for that array as the WHOLE output (no project/viewer/warnings envelope):\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 read `report.items` (or `primary`/`secondary`) by name. `j.items ?? j.rows ?? j`,\n`Object.values(j)` and any generic \"find the list\" wrapper are still guesses: before `items` existed they\nthrew `items is not iterable` before any row was read (#9956), and `j.rows` has never been a key. For a\nnumber|status|claim|title projection across non-Done rows, read --json --flat --out .jerv/tmp/board.json\nand filter that array locally (identical rows to `report.items`):\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";
46304
46411
  function flattenBoardReport(report) {
46305
46412
  const closingByRef = /* @__PURE__ */ new Map();
46306
46413
  for (const pull of report.pullRequests ?? []) {
@@ -46322,6 +46429,9 @@ function flattenBoardReport(report) {
46322
46429
  openPullRequests: closingByRef.get(ref) ?? []
46323
46430
  }))));
46324
46431
  }
46432
+ function withBoardItems(report) {
46433
+ return { ...report, items: flattenBoardReport(report) };
46434
+ }
46325
46435
  function registerBoardCommands(program3) {
46326
46436
  function withDiscoverMissDetail(message2) {
46327
46437
  const miss = lastBoardDiscoverMiss();
@@ -46346,7 +46456,7 @@ function registerBoardCommands(program3) {
46346
46456
  snapshotOnly: o.snapshotOnly,
46347
46457
  ...o.minCursor !== void 0 ? { minCursor: Number(o.minCursor) } : {}
46348
46458
  }, o.direct ? {} : { snapshot: registryClientDeps(config) });
46349
- const output = o.json ? JSON.stringify(o.flat ? flattenBoardReport(report) : report) : renderBoardReport(report);
46459
+ const output = o.json ? JSON.stringify(o.flat ? flattenBoardReport(report) : withBoardItems(report)) : renderBoardReport(report);
46350
46460
  if (!o.out) console.log(output);
46351
46461
  else console.log(`Wrote board read to ${o.out} (UTF-8, ${writeUtf8Receipt(o.out, output)} bytes)`);
46352
46462
  } catch (e) {
@@ -61128,9 +61238,10 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
61128
61238
  const reads = await Promise.allSettled([
61129
61239
  deps.claims(refs.candidate),
61130
61240
  refs.candidate ? deps.docs(refs.candidate) : Promise.resolve(void 0),
61131
- observePolicy(deps, refs.candidate)
61241
+ observePolicy(deps, refs.candidate),
61242
+ refs.candidate ? deps.queueWaiver?.(refs.candidate) : Promise.resolve(void 0)
61132
61243
  ]);
61133
- const [claims, docs, policy] = reads;
61244
+ const [claims, docs, policy, waiver] = reads;
61134
61245
  if (claims.status === "fulfilled") {
61135
61246
  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" }]);
61136
61247
  } else observations.push({ check: "claims", state: "unavailable", detail: `issue/claim evidence unavailable (${rejectionReason(claims.reason)}); retry the read` });
@@ -61138,6 +61249,8 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
61138
61249
  else if (docs.value) observations.push({ check: "docs", state: docs.value.state, detail: [docs.value.detail, docs.value.fix].filter(Boolean).join(" \u2014 ") });
61139
61250
  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` });
61140
61251
  else if (policy.value) observations.push(policy.value);
61252
+ if (waiver.status === "rejected") observations.push({ check: "queue", state: "skipped", detail: `the queue squash waiver placement was not checked (${rejectionReason(waiver.reason)}); this is non-blocking` });
61253
+ else if (waiver.value) observations.push(waiver.value);
61141
61254
  const findings = observations.filter((item) => item.state === "failed" || item.state === "unavailable");
61142
61255
  let attempted = false;
61143
61256
  return {
@@ -61238,6 +61351,34 @@ function createPreparationDeps(input, io = {}) {
61238
61351
  return {
61239
61352
  candidate: readCandidate,
61240
61353
  publish: (proof) => publishPreparedPrHead(proof, exec3),
61354
+ // #9831/#9968: publication happens only in commit(), after this read-only preparation, so the queue's
61355
+ // buried-waiver refusal is decided here — with the queue's own reader and setting reads — before an
61356
+ // absent remote head is pushed and a PR opened. A range or setting it cannot read is a skip (#9968).
61357
+ queueWaiver: async (candidate) => {
61358
+ const result = (state, detail) => ({ check: "queue", state, detail });
61359
+ if (!candidate.root) return void 0;
61360
+ const range = await exec3("git", ["-C", candidate.root, "log", "--reverse", "--format=%B%x00", `${candidate.baseSha}..${candidate.headSha}`], { timeout: GIT_TIMEOUT_MS }).then((read) => ({ log: read.stdout }), (error) => ({ reason: rejectionReason(error) }));
61361
+ if (!("log" in range)) return result("skipped", `candidate commits ${candidate.baseSha}..${candidate.headSha} could not be read (${range.reason}), so the queue squash waiver placement was not checked; this is non-blocking`);
61362
+ const messages = range.log.split("\0").filter((message2) => message2.trim()).map((message2) => {
61363
+ const [headline, ...body] = message2.trim().split("\n");
61364
+ return { headline, body: body.join("\n") };
61365
+ });
61366
+ const refusal = checkQueueSquashOverride("Candidate PR", "0", messages, candidate.root);
61367
+ if (!refusal) return void 0;
61368
+ const [owner, name] = candidate.repo.split("/");
61369
+ const setting = await (async () => {
61370
+ const metadata = await client.graphql(
61371
+ "query($owner:String!,$name:String!,$base:String!) { repository(owner:$owner,name:$name) { squashMergeCommitMessage mergeQueue(branch:$base) { configuration { mergeMethod } } } }",
61372
+ { owner, name, base: candidate.base }
61373
+ );
61374
+ const rules = await client.restPaginate(`repos/${candidate.repo}/rules/branches/${encodeURIComponent(candidate.base)}?per_page=100`);
61375
+ const methods = [...new Set(rules.filter((rule) => rule.type === "merge_queue").map((rule) => rule.parameters?.merge_method))];
61376
+ const method = methods.length === 1 ? methods[0] : !methods.length ? metadata.repository.mergeQueue?.configuration?.mergeMethod : void 0;
61377
+ return method === "SQUASH" && metadata.repository.squashMergeCommitMessage === "COMMIT_MESSAGES";
61378
+ })().then((squash) => squash, (error) => rejectionReason(error));
61379
+ if (typeof setting === "string") return result("skipped", `the repository's queue squash-message setting could not be read (${setting}), so the queue squash waiver placement was not checked; this is non-blocking`);
61380
+ return setting ? result("failed", `${refusal.error} \u2014 ${refusal.preparation.nextAction}`) : void 0;
61381
+ },
61241
61382
  // #9946: the retry needs the exact candidate objects. Fetch each ref on its own so a fork-only head
61242
61383
  // that origin cannot resolve never costs the base fetch too, and never surface a fetch failure as the
61243
61384
  // reason the policy read stayed unreadable.
@@ -61258,25 +61399,6 @@ function createPreparationDeps(input, io = {}) {
61258
61399
  if (!await docs.refResolves(candidate.root, candidate.headSha) || !await docs.refResolves(candidate.root, candidate.baseSha)) {
61259
61400
  return result("skipped", "exact remote candidate objects are not available locally; committed test policy was not checked");
61260
61401
  }
61261
- const log = (await exec3("git", ["-C", candidate.root, "log", "--reverse", "--format=%B%x00", `${candidate.baseSha}..${candidate.headSha}`], { timeout: GIT_TIMEOUT_MS })).stdout;
61262
- const messages = log.split("\0").filter((message2) => message2.trim()).map((message2) => {
61263
- const [headline, ...body] = message2.trim().split("\n");
61264
- return { headline, body: body.join("\n") };
61265
- });
61266
- const refusal = checkQueueSquashOverride("Candidate PR", "0", messages, candidate.root);
61267
- if (refusal) {
61268
- const [owner, name] = candidate.repo.split("/");
61269
- const metadata = await client.graphql(
61270
- "query($owner:String!,$name:String!,$base:String!) { repository(owner:$owner,name:$name) { squashMergeCommitMessage mergeQueue(branch:$base) { configuration { mergeMethod } } } }",
61271
- { owner, name, base: candidate.base }
61272
- );
61273
- const rules = await client.restPaginate(`repos/${candidate.repo}/rules/branches/${encodeURIComponent(candidate.base)}?per_page=100`);
61274
- const methods = [...new Set(rules.filter((rule) => rule.type === "merge_queue").map((rule) => rule.parameters?.merge_method))];
61275
- const method = methods.length === 1 ? methods[0] : !methods.length ? metadata.repository.mergeQueue?.configuration?.mergeMethod : void 0;
61276
- if (method === "SQUASH" && metadata.repository.squashMergeCommitMessage === "COMMIT_MESSAGES") {
61277
- return result("failed", `${refusal.error} \u2014 ${refusal.preparation.nextAction}`);
61278
- }
61279
- }
61280
61402
  let policy;
61281
61403
  try {
61282
61404
  policy = runTestPolicy(candidate.root, { candidate: { head: candidate.headSha, base: candidate.baseSha } });
@@ -63054,9 +63176,11 @@ function renderResidueSweepLines(report) {
63054
63176
  const more = report.unrecorded.length > 5 ? `, \u2026 ${report.unrecorded.length - 5} more` : "";
63055
63177
  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
63178
  for (const u of listed) lines2.push(`pr merge: left untouched ${u.path} \u2014 ${provenanceText(u)}`);
63179
+ lines2.push(`pr merge: recovery: ${RESIDUE_RECOVERY_MOVE}`);
63057
63180
  }
63058
63181
  return lines2;
63059
63182
  }
63183
+ var RESIDUE_RECOVERY_MOVE = `run \`git worktree list --porcelain\` in the primary checkout and confirm the path is absent and its provenance above shows no live lane (not registered; admin dir gone or .git pointer missing); nothing can record it for this sweep (only \`pr merge\` records its own merged worktree in <git-common-dir>/${RESIDUE_LEDGER_FILE}), so removing a non-empty unrecorded folder stays a human decision \u2014 hand the confirmed path to a human; this sweep never deletes it`;
63060
63184
  function provenanceText(u) {
63061
63185
  const pointer = u.gitdir ? `.git \u2192 ${u.gitdir}${u.gitdirPresent ? "" : " (admin dir gone)"}` : ".git pointer missing";
63062
63186
  const registration = `worktrees/${u.registrationId} ${u.registrationPresent ? "registered" : "not registered"}`;
@@ -73836,6 +73960,7 @@ registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand
73836
73960
  "ci cancel-orphans",
73837
73961
  "run cancel",
73838
73962
  "run rerun",
73963
+ "run dispatch",
73839
73964
  // #9779/#9780: the vars verbs and archive-status validate --repo/name/value before the first
73840
73965
  // dispatch, so their scoped App mint waits for the action handler like the run writes above.
73841
73966
  "vars get",
@@ -74715,13 +74840,18 @@ ci.command("hosted-usage").description("read-only hosted tick and lane-demand al
74715
74840
  });
74716
74841
  registerCiChecksWait(ci);
74717
74842
  registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
74718
- var runRoot = program2.command("run").description("bounded workflow-run writes \u2014 `run cancel <id>` and `run rerun <id> [--failed]`, both on the org App actions:write profile");
74843
+ var runRoot = program2.command("run").description("bounded workflow-run writes on the org App actions:write profile");
74719
74844
  registerRunCancel(runRoot, async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run cancel")).token);
74720
74845
  registerRunRerun(
74721
74846
  runRoot,
74722
74847
  async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run rerun")).token,
74723
74848
  async () => fetchProjectsList(registryClientDeps(await loadConfig()))
74724
74849
  );
74850
+ registerRunDispatch(
74851
+ runRoot,
74852
+ async () => (await mintCommandWriteToken(appActorDeps(), "run dispatch")).token,
74853
+ async () => fetchProjectsList(registryClientDeps(await loadConfig()))
74854
+ );
74725
74855
  var varsAppClient = (commandPath4) => async () => (
74726
74856
  // The concrete client always provides restWithStatus (optional on the interface only so hand-built
74727
74857
  // test stubs of GitHubClient keep compiling).
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.6.33",
3
+ "version": "4.6.35",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/codex-plugin",
3
- "version": "4.6.33",
3
+ "version": "4.6.35",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -20,12 +20,16 @@ One call; `--flat` gives one array of rows to iterate. A routine read already ex
20
20
  items, so no status post-filter is needed. The detailed form (`--json` without `--flat`) carries `viewer`,
21
21
  `repo` and the project title; use its `primary` for this repo and `secondary` for other repos on the
22
22
  Project. Without `--flat`, `board read --json` returns an object
23
- `{project, viewer, repo, primary, secondary, warnings, partial}` whose `primary` and `secondary` are bucket
24
- objects `{userOwned, claimable, taken, unownedInFlight}` of item arrays (no top-level `items` array), so filter
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.
23
+ `{project, viewer, repo, primary, secondary, items, warnings, partial}` whose `primary` and `secondary` are bucket
24
+ objects `{userOwned, claimable, taken, unownedInFlight}` of item arrays, and whose top-level `items` (#9974) is
25
+ the flat row array — the same rows `--flat` prints — so a number|status|title projection can iterate
26
+ `report.items` without a second read. Filter a bucket (`report.primary.claimable`) when ownership context
27
+ matters, or `report.items` / `--json --flat` for a projection. Never sniff
28
+ for the list (`j.items ?? j.rows ?? j`, `Object.values`, any generic "find the list" wrapper): `j.rows` is not a
29
+ key, and on an older report it threw
30
+ `items is not iterable` before any row was read. For a number|status|claim|title projection across non-Done
31
+ rows, read `report.items` or run `mmi-cli oracle board read --json --flat --out .jerv/tmp/board.json` and filter
32
+ that array locally.
29
33
  `--flat` rows are `{ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}` (`claim` is the
30
34
  parsed mmi-claim evidence object, null when unclaimed; `openPullRequests` lists the OPEN PRs whose
31
35
  closing-issue body links name the item — on the full JSON object that list lives at `report.pullRequests`,
@@ -21,9 +21,12 @@ rows to iterate. A routine read already excludes Done and closed items, so no st
21
21
  One call. The detailed form (`--json` without `--flat`) carries `viewer`, `repo` and the project title — do not spend extra calls on
22
22
  `gh api user` or `gh repo view`. Use `primary` for this repo and `secondary` for other repos on the same
23
23
  Project. Without `--flat`, `board read --json` returns an object
24
- `{project, viewer, repo, primary, secondary, warnings, partial}` whose `primary` and `secondary` are bucket
25
- objects `{userOwned, claimable, taken, unownedInFlight}` of item arrays (no top-level `items` array), so filter
26
- a bucket (e.g. `report.primary.claimable`) or pass `--json --flat`, never `(j.items||j).filter`. Partial reads
24
+ `{project, viewer, repo, primary, secondary, items, warnings, partial}` whose `primary` and `secondary` are bucket
25
+ objects `{userOwned, claimable, taken, unownedInFlight}` of item arrays, and whose top-level `items` (#9974) holds
26
+ the flat rows — exactly what `--flat` prints — so iterate `report.items` instead of spending a second read.
27
+ Filter a bucket (`report.primary.claimable`) when ownership context matters. Never sniff for the list
28
+ (`j.items ?? j.rows ?? j` is only coincidentally safe now, `Object.values` is not): `j.rows` is not a key.
29
+ Partial reads
27
30
  exit nonzero; pass `--allow-partial` only when the dev accepts an incomplete board.
28
31
 
29
32
  Partition, as the CLI computes it: