@mutmutco/claude-plugin 4.6.34 → 4.6.36

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,7 +1,7 @@
1
1
  {
2
2
  "name": "mmi",
3
3
  "displayName": "MMI",
4
- "version": "4.6.34",
4
+ "version": "4.6.36",
5
5
  "mmiCompat": "4.x",
6
6
  "description": "MMI workflow skills and org gates delivery.",
7
7
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -15366,13 +15366,6 @@ async function fetchHubBoardSnapshot(request, deps) {
15366
15366
  // src/board-read.ts
15367
15367
  init_github_quota();
15368
15368
 
15369
- // src/claim-marker-client.ts
15370
- init_github_client();
15371
- init_github_quota();
15372
-
15373
- // src/pr-checks-rest.ts
15374
- init_cli_shared();
15375
-
15376
15369
  // ../shared/github-quota-scope.ts
15377
15370
  var import_node_crypto4 = require("node:crypto");
15378
15371
  function appQuotaScope(appId, installationId, origin = "https://api.github.com", resource = "graphql") {
@@ -15497,6 +15490,7 @@ var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
15497
15490
  ["run cancel", { metadata: "read", actions: "write" }],
15498
15491
  // #9587: one named run rerun (all or failed jobs) — same actions:write profile, nothing broader.
15499
15492
  ["run rerun", { metadata: "read", actions: "write" }],
15493
+ ["run dispatch", { metadata: "read", actions: "write" }],
15500
15494
  // #9780: repository Actions variables — get/list mint only Variables:read; create/update/delete
15501
15495
  // Variables:write. Repository scope only; no secret store, no rename, no workflow dispatch.
15502
15496
  // #9790: the token key is `actions_variables` (as ci-reconcile.yml mints); `variables` was refused 422.
@@ -15736,6 +15730,13 @@ async function activateAppActor(commandPath4, env, mint, note = (line) => proces
15736
15730
  return "app";
15737
15731
  }
15738
15732
 
15733
+ // src/claim-marker-client.ts
15734
+ init_github_client();
15735
+ init_github_quota();
15736
+
15737
+ // src/pr-checks-rest.ts
15738
+ init_cli_shared();
15739
+
15739
15740
  // src/merge-ci-policy.ts
15740
15741
  function resolveMergeCiPolicy(input) {
15741
15742
  if (input.registryCi === "none") {
@@ -17173,9 +17174,11 @@ async function fetchRestCorePool(gh2 = defaultGhApi, nowMs = Date.now()) {
17173
17174
  // src/claim-marker-client.ts
17174
17175
  var claimWriteTokenProvider;
17175
17176
  var claimWriteTokenOnce;
17177
+ var notedPersonalClaimWrite = false;
17176
17178
  function setClaimWriteTokenProvider(provider) {
17177
17179
  claimWriteTokenProvider = provider;
17178
17180
  claimWriteTokenOnce = void 0;
17181
+ notedPersonalClaimWrite = false;
17179
17182
  }
17180
17183
  async function selectClaimWriteClient(client, appClient) {
17181
17184
  if (appClient) return appClient;
@@ -17186,15 +17189,26 @@ async function selectClaimWriteClient(client, appClient) {
17186
17189
  token = await claimWriteTokenOnce;
17187
17190
  } catch (e) {
17188
17191
  claimWriteTokenOnce = void 0;
17192
+ if (e instanceof AppActorError && e.code === "vault-denied" && process.env.MMI_ACTOR?.trim() !== "app") return personalClaimWrite(client);
17189
17193
  const detail = e instanceof Error ? e.message : String(e);
17190
17194
  throw new Error(`${detail} \u2014 refusing personal-token fallback for claim write (#9511)`);
17191
17195
  }
17192
17196
  if (!token) {
17193
17197
  claimWriteTokenOnce = void 0;
17194
- throw new Error("app actor: claim write token unavailable \u2014 refusing personal-token fallback for claim write (#9511)");
17198
+ if (process.env.MMI_ACTOR?.trim() === "app") {
17199
+ throw new Error("app actor: claim write token unavailable \u2014 refusing personal-token fallback for claim write (#9511)");
17200
+ }
17201
+ return personalClaimWrite(client);
17195
17202
  }
17196
17203
  return createGitHubClient({ token: async () => token });
17197
17204
  }
17205
+ function personalClaimWrite(client) {
17206
+ if (!notedPersonalClaimWrite) {
17207
+ notedPersonalClaimWrite = true;
17208
+ process.stderr.write("note: no App route on this host (no MMI-Hub vault access) \u2014 claim writes go as your own GitHub login\n");
17209
+ }
17210
+ return client;
17211
+ }
17198
17212
  async function selectLivenessClient(client, appClient) {
17199
17213
  if (appClient) return appClient;
17200
17214
  let token;
@@ -22744,6 +22758,7 @@ mutation($projectId: ID!, $itemId: ID!) {
22744
22758
  item { id }
22745
22759
  }
22746
22760
  }`;
22761
+ var BOARD_WRITE_REFUSAL_TYPES = /* @__PURE__ */ new Set(["INSUFFICIENT_SCOPES", "FORBIDDEN", "NOT_FOUND"]);
22747
22762
  async function updateItemSingleSelect(client, projectId, itemId, fieldId, optionId) {
22748
22763
  if (process.env.MMI_ACTOR?.trim() === "app") {
22749
22764
  const { writeHubBoardField: writeHubBoardField2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
@@ -22770,10 +22785,14 @@ async function updateItemSingleSelect(client, projectId, itemId, fieldId, option
22770
22785
  return { credential: "user" };
22771
22786
  } catch (error) {
22772
22787
  const errors = error.graphqlErrors;
22773
- if (errors?.length && errors.every((entry) => entry.type === "INSUFFICIENT_SCOPES")) {
22788
+ if (errors?.length && errors.every((entry) => entry.type !== void 0 && BOARD_WRITE_REFUSAL_TYPES.has(entry.type))) {
22774
22789
  const { writeHubBoardField: writeHubBoardField2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
22775
- await writeHubBoardField2({ projectId, itemId, fieldId, optionId });
22776
- return { credential: "app_installation" };
22790
+ try {
22791
+ await writeHubBoardField2({ projectId, itemId, fieldId, optionId });
22792
+ return { credential: "app_installation" };
22793
+ } catch {
22794
+ throw error;
22795
+ }
22777
22796
  }
22778
22797
  if (isGitHubRateLimitError(error)) {
22779
22798
  const { writeHubBoardField: writeHubBoardField2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
@@ -28366,10 +28385,10 @@ var rollout_plan_default = {
28366
28385
  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
28386
  },
28368
28387
  baseline: {
28369
- version: "4.6.34",
28370
- tag: "v4.6.34",
28371
- commit: "a094b40adf8d",
28372
- npm: "@mutmutco/cli@4.6.34"
28388
+ version: "4.6.36",
28389
+ tag: "v4.6.36",
28390
+ commit: "d1c15e58d6e5",
28391
+ npm: "@mutmutco/cli@4.6.36"
28373
28392
  },
28374
28393
  exitCriterion: "fleet-n-of-n",
28375
28394
  hubOnlyShortcut: "forbidden",
@@ -28386,14 +28405,14 @@ var rollout_plan_default = {
28386
28405
  repo: "mutmutco/mmi-hub",
28387
28406
  role: "canary",
28388
28407
  schedule: "train",
28389
- v3Target: "v4.6.34"
28408
+ v3Target: "v4.6.36"
28390
28409
  }
28391
28410
  ],
28392
28411
  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
28412
  rollback: {
28394
28413
  independent: true,
28395
- mechanism: "npm dist-tag latest -> 4.6.34 and redeploy the Hub Lambda from tag v4.6.34 (a094b40adf8d); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28396
- v3Target: "v4.6.34 (@mutmutco/cli@4.6.34, tag commit a094b40adf8d \u2014 last known-good release carrying the repo-index v4-only contract)"
28414
+ mechanism: "npm dist-tag latest -> 4.6.36 and redeploy the Hub Lambda from tag v4.6.36 (d1c15e58d6e5); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28415
+ v3Target: "v4.6.36 (@mutmutco/cli@4.6.36, tag commit d1c15e58d6e5 \u2014 last known-good release carrying the repo-index v4-only contract)"
28397
28416
  }
28398
28417
  },
28399
28418
  {
@@ -30698,6 +30717,8 @@ var CORRELATE_ATTEMPTS = 5;
30698
30717
  var CORRELATE_DELAY_MS = 1500;
30699
30718
  var CORRELATE_SKEW_SLACK_MS = 1e4;
30700
30719
  var CORRELATE_PAGE_LIMIT = 50;
30720
+ var GET_PUBLISH_CORRELATE_ATTEMPTS = 12;
30721
+ var GET_PUBLISH_CORRELATE_DELAY_MS = 2500;
30701
30722
  var RUN_CONFIRM_ATTEMPTS = 3;
30702
30723
  var RUN_CONFIRM_DELAY_MS = 1e3;
30703
30724
  var defaultSleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
@@ -30744,11 +30765,13 @@ async function correlateRun(deps, args) {
30744
30765
  const sleep2 = resolveSleep(deps);
30745
30766
  const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
30746
30767
  const repo = args.mode === "workflow" ? args.repo ?? HUB_REPO2 : HUB_REPO2;
30768
+ const attempts = args.mode === "dispatch" ? args.attempts ?? CORRELATE_ATTEMPTS : CORRELATE_ATTEMPTS;
30769
+ const delayMs = args.mode === "dispatch" ? args.delayMs ?? CORRELATE_DELAY_MS : CORRELATE_DELAY_MS;
30747
30770
  let lastError;
30748
30771
  let parsedAnyResponse = false;
30749
30772
  let truncatedWindow = false;
30750
- for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
30751
- if (attempt > 0) await sleep2(CORRELATE_DELAY_MS);
30773
+ for (let attempt = 0; attempt < attempts; attempt++) {
30774
+ if (attempt > 0) await sleep2(delayMs);
30752
30775
  const listArgs = [
30753
30776
  "run",
30754
30777
  "list",
@@ -30791,7 +30814,7 @@ async function correlateRun(deps, args) {
30791
30814
  if (!parsedAnyResponse) {
30792
30815
  return {
30793
30816
  read: "failed",
30794
- error: `\`gh run list --workflow ${args.workflow} --repo ${repo}\` failed on all ${CORRELATE_ATTEMPTS} attempts${lastError ? ` (${describeReadError(lastError)})` : ""}`
30817
+ error: `\`gh run list --workflow ${args.workflow} --repo ${repo}\` failed on all ${attempts} attempts${lastError ? ` (${describeReadError(lastError)})` : ""}`
30795
30818
  };
30796
30819
  }
30797
30820
  if (truncatedWindow) {
@@ -30806,7 +30829,7 @@ function correlateTenantRun(deps, since, titleIncludes) {
30806
30829
  return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch", titleIncludes });
30807
30830
  }
30808
30831
  function correlateGetPublishRun(deps, since, product, mode) {
30809
- return correlateRun(deps, { workflow: "get-publish.yml", since, mode: "dispatch", titleIncludes: [product, ...mode ? [mode] : []], ...mode ? { requireTitleMatch: true } : {} });
30832
+ 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
30833
  }
30811
30834
  function correlateControlRun(deps, since, titleIncludes) {
30812
30835
  return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes, titleFieldsFrom: 2, ...titleIncludes?.length ? { requireTitleMatch: true } : {} });
@@ -31937,7 +31960,7 @@ async function fetchServedChannelVersion(url, fetchImpl = fetch) {
31937
31960
  return { ok: false, error: e instanceof Error ? e.message : String(e) };
31938
31961
  }
31939
31962
  }
31940
- async function findPriorGreenDispatch(deps, repo, workflow, sha, titleTokens) {
31963
+ async function findPriorGreenDispatch(deps, repo, workflow, sha, titleTokens, opts = {}) {
31941
31964
  if (!/^[0-9a-f]{40}$/i.test(sha)) return void 0;
31942
31965
  try {
31943
31966
  const rows = JSON.parse(await deps.run("gh", [
@@ -31956,12 +31979,18 @@ async function findPriorGreenDispatch(deps, repo, workflow, sha, titleTokens) {
31956
31979
  "--json",
31957
31980
  "databaseId,url,status,conclusion,displayTitle,headSha"
31958
31981
  ]));
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;
31982
+ const titleMatch = (r) => titleTokens.every((t) => (r.displayTitle ?? "").split(/[^\w.-]+/).includes(t));
31983
+ const atSha = (r) => r.headSha?.toLowerCase() === sha.toLowerCase();
31984
+ const hit = rows.find((r) => typeof r.databaseId === "number" && atSha(r) && r.status === "completed" && r.conclusion === "success" && titleMatch(r));
31985
+ if (hit) return { runId: hit.databaseId, ...hit.url ? { runUrl: hit.url } : {} };
31986
+ if (!opts.includeActive) return void 0;
31987
+ const active = rows.find((r) => typeof r.databaseId === "number" && atSha(r) && PRIOR_DISPATCH_ACTIVE_STATUSES.has(r.status ?? "") && titleMatch(r));
31988
+ return active ? { runId: active.databaseId, ...active.url ? { runUrl: active.url } : {}, active: true } : void 0;
31961
31989
  } catch {
31962
31990
  return void 0;
31963
31991
  }
31964
31992
  }
31993
+ var PRIOR_DISPATCH_ACTIVE_STATUSES = /* @__PURE__ */ new Set(["in_progress", "queued", "waiting", "requested", "pending"]);
31965
31994
  function planeFailureRow(workflow, evidence) {
31966
31995
  return { workflow, runUrlNote: evidence, conclusion: "failure" };
31967
31996
  }
@@ -32040,10 +32069,13 @@ async function appendMmiGetRedeploy(deps, input, dispatch) {
32040
32069
  async function appendServedChannelStage(deps, input, dispatch) {
32041
32070
  if (!isHubControlRepo(input.repo)) return dispatch;
32042
32071
  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"]);
32072
+ const prior = await findPriorGreenDispatch(deps, input.repo, "get-publish.yml", input.releaseSha, [HUB_SERVED_PRODUCT, "stage"], { includeActive: true });
32044
32073
  let row;
32045
- if (prior) {
32074
+ if (prior && !prior.active) {
32046
32075
  row = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {}, conclusion: "success" };
32076
+ } else if (prior?.active) {
32077
+ row = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {}, conclusion: "pending" };
32078
+ if (input.watch) row.conclusion = await watchTenantRun(deps, prior.runId, input.repo);
32047
32079
  } else {
32048
32080
  const since = (deps.now ?? Date.now)();
32049
32081
  try {
@@ -32058,7 +32090,7 @@ async function appendServedChannelStage(deps, input, dispatch) {
32058
32090
  };
32059
32091
  }
32060
32092
  const correlated = await correlateGetPublishRun(deps, since, HUB_SERVED_PRODUCT, "stage");
32061
- if (correlated.read !== "found") {
32093
+ if (correlated.read === "failed") {
32062
32094
  return {
32063
32095
  ...dispatch,
32064
32096
  note: `${dispatch.note}; get-publish stage dispatched but its run could not be READ \u2014 check Actions; recover with \`${remedy}\``,
@@ -32066,6 +32098,14 @@ async function appendServedChannelStage(deps, input, dispatch) {
32066
32098
  workflowRuns: [...dispatch.workflowRuns ?? [], planeFailureRow("get-publish.yml", "stage dispatched, but its run could not be read")]
32067
32099
  };
32068
32100
  }
32101
+ if (correlated.read === "absent") {
32102
+ const dispatchedAt = new Date(since).toISOString();
32103
+ return {
32104
+ ...dispatch,
32105
+ 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`,
32106
+ 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"] }]
32107
+ };
32108
+ }
32069
32109
  row = { workflow: "get-publish.yml", runId: correlated.runId, ...correlated.runUrl ? { runUrl: correlated.runUrl } : {}, conclusion: "pending" };
32070
32110
  if (input.watch) row.conclusion = await watchTenantRun(deps, correlated.runId, input.repo);
32071
32111
  }
@@ -32090,12 +32130,15 @@ async function appendServedChannelPublish(deps, input, dispatch, redeployed) {
32090
32130
  if (input.publishStatus !== "success") {
32091
32131
  return withNote(`served channel NOT moved (publish did not succeed) \u2014 after the publish is green, run \`${remedy}\``);
32092
32132
  }
32093
- const prior = input.releaseSha ? await findPriorGreenDispatch(deps, input.repo, "get-publish.yml", input.releaseSha, [HUB_SERVED_PRODUCT, "pointer"]) : void 0;
32133
+ const prior = input.releaseSha ? await findPriorGreenDispatch(deps, input.repo, "get-publish.yml", input.releaseSha, [HUB_SERVED_PRODUCT, "pointer"], { includeActive: true }) : void 0;
32094
32134
  let runRow;
32095
32135
  let runStatus;
32096
- if (prior) {
32136
+ if (prior && !prior.active) {
32097
32137
  runRow = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {} };
32098
32138
  runStatus = "success";
32139
+ } else if (prior?.active) {
32140
+ runRow = { workflow: "get-publish.yml", runId: prior.runId, ...prior.runUrl ? { runUrl: prior.runUrl } : {} };
32141
+ runStatus = input.watch ? await watchTenantRun(deps, prior.runId, input.repo) : "pending";
32099
32142
  } else {
32100
32143
  const version = input.tag?.replace(/^v/, "") ?? (deps.readLauncherVersion ?? readLocalLauncherVersion)();
32101
32144
  if (!version) return withNote("the local launcher version could not be read \u2014 nothing was compared, so the move is unverified");
@@ -32107,9 +32150,17 @@ async function appendServedChannelPublish(deps, input, dispatch, redeployed) {
32107
32150
  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
32151
  }
32109
32152
  const correlated = await correlateGetPublishRun(deps, since, HUB_SERVED_PRODUCT, "pointer");
32110
- if (correlated.read !== "found") {
32153
+ if (correlated.read === "failed") {
32111
32154
  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
32155
  }
32156
+ if (correlated.read === "absent") {
32157
+ const dispatchedAt = new Date(since).toISOString();
32158
+ return {
32159
+ ...dispatch,
32160
+ 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`,
32161
+ 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"] }]
32162
+ };
32163
+ }
32113
32164
  const { runId, runUrl } = correlated;
32114
32165
  runRow = { workflow: "get-publish.yml", ...runId ? { runId } : {}, ...runUrl ? { runUrl } : {} };
32115
32166
  if (!input.watch) {
@@ -32121,7 +32172,7 @@ async function appendServedChannelPublish(deps, input, dispatch, redeployed) {
32121
32172
  }
32122
32173
  runStatus = await watchTenantRun(deps, runId, input.repo);
32123
32174
  }
32124
- if (runStatus !== "success") {
32175
+ if (runStatus === "failure") {
32125
32176
  return {
32126
32177
  ...dispatch,
32127
32178
  note: `${dispatch.note}; get-publish.yml ${runStatus} \u2014 the SERVED CHANNEL IS UNCHANGED; recover with \`${pointerRemedy}\``,
@@ -32129,6 +32180,13 @@ async function appendServedChannelPublish(deps, input, dispatch, redeployed) {
32129
32180
  deployStatus: "failure"
32130
32181
  };
32131
32182
  }
32183
+ if (runStatus === "pending") {
32184
+ return {
32185
+ ...dispatch,
32186
+ 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`,
32187
+ workflowRuns: [...dispatch.workflowRuns ?? [], { ...runRow, conclusion: "pending" }]
32188
+ };
32189
+ }
32132
32190
  const expected = input.tag ? input.tag.replace(/^v/, "") : (deps.readLauncherVersion ?? readLocalLauncherVersion)();
32133
32191
  const runs2 = [...dispatch.workflowRuns ?? [], { ...runRow, conclusion: "success" }];
32134
32192
  if (!channelUrl) {
@@ -32269,6 +32327,14 @@ async function appendInstallHomePublish(deps, input, dispatch) {
32269
32327
  deployStatus: "failure"
32270
32328
  };
32271
32329
  }
32330
+ if (correlated.read === "absent") {
32331
+ const dispatchedAt = new Date(since).toISOString();
32332
+ return {
32333
+ ...dispatch,
32334
+ 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`),
32335
+ 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] }]
32336
+ };
32337
+ }
32272
32338
  const row = { workflow: "get-publish.yml", ...correlated.runId ? { runId: correlated.runId } : {}, ...correlated.runUrl ? { runUrl: correlated.runUrl } : {} };
32273
32339
  if (!input.watch) {
32274
32340
  return {
@@ -32278,7 +32344,7 @@ async function appendInstallHomePublish(deps, input, dispatch) {
32278
32344
  };
32279
32345
  }
32280
32346
  const status = await watchTenantRun(deps, correlated.runId, HUB_REPO2);
32281
- if (status !== "success") {
32347
+ if (status === "failure") {
32282
32348
  return {
32283
32349
  ...dispatch,
32284
32350
  note: note(`get-publish.yml (product=${product}) ${status} \u2014 the INSTALLER HOME IS UNCHANGED; recover with \`${remedy}\``),
@@ -32286,6 +32352,13 @@ async function appendInstallHomePublish(deps, input, dispatch) {
32286
32352
  deployStatus: "failure"
32287
32353
  };
32288
32354
  }
32355
+ if (status === "pending") {
32356
+ return {
32357
+ ...dispatch,
32358
+ 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`),
32359
+ workflowRuns: [...dispatch.workflowRuns ?? [], { ...row, conclusion: "pending" }]
32360
+ };
32361
+ }
32289
32362
  return {
32290
32363
  ...dispatch,
32291
32364
  note: note(`installer home published for ${product} (get-publish.yml green)`),
@@ -32324,8 +32397,10 @@ async function appendServedChannelLegs(deps, input, dispatch) {
32324
32397
  ]);
32325
32398
  return mergeServedLegs(dispatch, home2, proof2);
32326
32399
  }
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/, "")}\`` };
32400
+ const stageRows = (prepared.workflowRuns ?? []).filter((r) => r.workflow === "get-publish.yml");
32401
+ const stageGreen = stageRows.every((r) => r.conclusion === "success");
32402
+ const stagePending = !stageGreen && stageRows.every((r) => r.conclusion !== "failure");
32403
+ 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
32404
  const out = mergeServedLegs(dispatch, {
32330
32405
  ...dispatch,
32331
32406
  note: dispatch.note + prepared.note,
@@ -44822,6 +44897,56 @@ function registerRunRerun(run, writeToken, projects) {
44822
44897
  });
44823
44898
  }
44824
44899
 
44900
+ // src/run-dispatch.ts
44901
+ init_cli_shared();
44902
+ async function dispatchWorkflow(repo, workflow, ref, inputs, deps) {
44903
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error("run dispatch: invalid repository");
44904
+ if (!/^[A-Za-z0-9_.-]+\.ya?ml$/.test(workflow)) throw new Error("run dispatch: workflow must be a YAML filename");
44905
+ 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");
44906
+ if (inputs.length > 10) throw new Error("run dispatch: at most 10 inputs");
44907
+ const fields = ["-f", `ref=${ref}`];
44908
+ const names = /* @__PURE__ */ new Set();
44909
+ for (const input of inputs) {
44910
+ const match = /^([A-Za-z_][A-Za-z0-9_]{0,63})=([^\r\n]{0,2048})$/.exec(input);
44911
+ if (!match || names.has(match[1])) throw new Error("run dispatch: invalid or duplicate input");
44912
+ names.add(match[1]);
44913
+ fields.push("-f", `inputs[${match[1]}]=${match[2]}`);
44914
+ }
44915
+ const projects = await deps.projects();
44916
+ if (!projects) throw new Error("run dispatch: registry unavailable; no dispatch attempted");
44917
+ if (!projects.some((p) => (p.repos ?? []).some((r) => typeof r === "string" && registryRepoKey(qualifyRegistryRepo(r)) === registryRepoKey(repo)))) {
44918
+ throw new Error("run dispatch: repository is not registered; no dispatch attempted");
44919
+ }
44920
+ let status;
44921
+ try {
44922
+ status = await deps.post(`repos/${repo}/actions/workflows/${workflow}/dispatches`, fields);
44923
+ } catch {
44924
+ throw new Error("run dispatch: outcome unknown; inspect GitHub before any retry");
44925
+ }
44926
+ if (status !== 204) throw new Error("run dispatch: outcome unknown; inspect GitHub before any retry");
44927
+ return { ok: true, repo, workflow, ref, credential: "app_installation", status: "dispatch-requested", httpStatus: status };
44928
+ }
44929
+ function appDispatchPost(writeToken, exec3 = execFileP) {
44930
+ return async (path2, fields) => {
44931
+ const token = await writeToken();
44932
+ const { stdout } = await exec3("gh", ["api", "--include", "--method", "POST", path2, ...fields], {
44933
+ timeout: 2e4,
44934
+ env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
44935
+ });
44936
+ return Number(/^HTTP\/[\d.]+ (\d{3})/.exec(stdout)?.[1] ?? 0);
44937
+ };
44938
+ }
44939
+ function registerRunDispatch(run, writeToken, projects) {
44940
+ 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) => {
44941
+ try {
44942
+ const receipt = await dispatchWorkflow(options.repo, workflow, options.ref, options.input, { projects, post: appDispatchPost(writeToken) });
44943
+ console.log(options.json ? JSON.stringify(receipt) : `run dispatch: requested ${receipt.repo}/${receipt.workflow} at ${receipt.ref}`);
44944
+ } catch (error) {
44945
+ return fail(error.message);
44946
+ }
44947
+ });
44948
+ }
44949
+
44825
44950
  // src/devops-vars.ts
44826
44951
  var import_node_crypto13 = require("node:crypto");
44827
44952
  init_github_client();
@@ -46042,6 +46167,15 @@ async function attachOnIntegrationRefusal(detail, cfg, contentNodeId, attach = a
46042
46167
  return { warning: `board attach refused: the gh login is an App installation token without Projects access (${detail}) and the Hub App leg failed (${e.message}). If the Hub reports the same refusal, ${PROJECTS_PERMISSION_HINT}` };
46043
46168
  }
46044
46169
  }
46170
+ var ATTACH_AUTHORITY_REFUSAL = /FORBIDDEN|NOT_FOUND|Resource not accessible|missing required scopes|insufficient (?:permissions?|scope)|Could not resolve to a node|does not have (?:access|permission)/i;
46171
+ async function attachOnAuthorityRefusal(detail, cfg, contentNodeId, attach = attachViaHubApp) {
46172
+ if (!ATTACH_AUTHORITY_REFUSAL.test(detail)) return void 0;
46173
+ try {
46174
+ return { projectItemId: await attach(cfg, contentNodeId) };
46175
+ } catch (e) {
46176
+ return { warning: `${detail}; the Hub attach also refused (${e.message}) \u2014 attach needs org-owner, repo-admin or repository write authority on the board's repository` };
46177
+ }
46178
+ }
46045
46179
  async function issueNodeIdForAttach(repo, number, ghView, client) {
46046
46180
  try {
46047
46181
  return (await ghView()).trim();
@@ -46300,7 +46434,7 @@ function refuseNotClaimable(e, json) {
46300
46434
  process.exitCode = 1;
46301
46435
  return true;
46302
46436
  }
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";
46437
+ 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
46438
  function flattenBoardReport(report) {
46305
46439
  const closingByRef = /* @__PURE__ */ new Map();
46306
46440
  for (const pull of report.pullRequests ?? []) {
@@ -46322,6 +46456,9 @@ function flattenBoardReport(report) {
46322
46456
  openPullRequests: closingByRef.get(ref) ?? []
46323
46457
  }))));
46324
46458
  }
46459
+ function withBoardItems(report) {
46460
+ return { ...report, items: flattenBoardReport(report) };
46461
+ }
46325
46462
  function registerBoardCommands(program3) {
46326
46463
  function withDiscoverMissDetail(message2) {
46327
46464
  const miss = lastBoardDiscoverMiss();
@@ -46346,7 +46483,7 @@ function registerBoardCommands(program3) {
46346
46483
  snapshotOnly: o.snapshotOnly,
46347
46484
  ...o.minCursor !== void 0 ? { minCursor: Number(o.minCursor) } : {}
46348
46485
  }, o.direct ? {} : { snapshot: registryClientDeps(config) });
46349
- const output = o.json ? JSON.stringify(o.flat ? flattenBoardReport(report) : report) : renderBoardReport(report);
46486
+ const output = o.json ? JSON.stringify(o.flat ? flattenBoardReport(report) : withBoardItems(report)) : renderBoardReport(report);
46350
46487
  if (!o.out) console.log(output);
46351
46488
  else console.log(`Wrote board read to ${o.out} (UTF-8, ${writeUtf8Receipt(o.out, output)} bytes)`);
46352
46489
  } catch (e) {
@@ -63066,9 +63203,11 @@ function renderResidueSweepLines(report) {
63066
63203
  const more = report.unrecorded.length > 5 ? `, \u2026 ${report.unrecorded.length - 5} more` : "";
63067
63204
  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}`);
63068
63205
  for (const u of listed) lines2.push(`pr merge: left untouched ${u.path} \u2014 ${provenanceText(u)}`);
63206
+ lines2.push(`pr merge: recovery: ${RESIDUE_RECOVERY_MOVE}`);
63069
63207
  }
63070
63208
  return lines2;
63071
63209
  }
63210
+ 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`;
63072
63211
  function provenanceText(u) {
63073
63212
  const pointer = u.gitdir ? `.git \u2192 ${u.gitdir}${u.gitdirPresent ? "" : " (admin dir gone)"}` : ".git pointer missing";
63074
63213
  const registration = `worktrees/${u.registrationId} ${u.registrationPresent ? "registered" : "not registered"}`;
@@ -63226,6 +63365,24 @@ async function attachToProject(issueNumber, repo, priority) {
63226
63365
  }
63227
63366
  return boardAttachRateLimitedReceipt(resetEpochSeconds);
63228
63367
  }
63368
+ const authority = contentId ? await attachOnAuthorityRefusal(detail, cfg, contentId) : void 0;
63369
+ if (authority && "projectItemId" in authority) {
63370
+ if (priority) {
63371
+ try {
63372
+ await setBoardItemPriority(defaultGitHubClient(), cfg, authority.projectItemId, priority);
63373
+ } catch (pe) {
63374
+ const perr = pe;
63375
+ process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(perr.stderr || perr.message || String(pe)).trim()}
63376
+ `);
63377
+ }
63378
+ }
63379
+ return { projectItemId: authority.projectItemId, onBoard: true, credential: "app_installation" };
63380
+ }
63381
+ if (authority) {
63382
+ process.stderr.write(`warning: issue #${issueNumber} created but NOT added to the project board: ${authority.warning}
63383
+ `);
63384
+ return { onBoard: false };
63385
+ }
63229
63386
  process.stderr.write(`warning: issue #${issueNumber} created but NOT added to the project board: ${detail}
63230
63387
  `);
63231
63388
  return { onBoard: false };
@@ -73836,7 +73993,16 @@ setGhWriteTokenProvider(() => {
73836
73993
  throw e;
73837
73994
  });
73838
73995
  });
73839
- setClaimWriteTokenProvider(() => mintFilingWriteToken(appActorDeps()).then((t) => t.token));
73996
+ var claimWriteToken;
73997
+ setClaimWriteTokenProvider(() => {
73998
+ if (process.env.MMI_ACTOR?.trim() === "app") {
73999
+ return mintFilingWriteToken(appActorDeps()).then((t) => t.token);
74000
+ }
74001
+ return claimWriteToken ??= mintFilingWriteToken(appActorDeps()).then((t) => t.token, (e) => {
74002
+ if (e instanceof AppActorError && e.code === "vault-denied") return void 0;
74003
+ throw e;
74004
+ });
74005
+ });
73840
74006
  setStatusWriteTokenProvider(() => mintStatusWriteToken(appActorDeps()));
73841
74007
  var registerPreAction = Reflect.get(program2, ["h", "ook"].join(""));
73842
74008
  registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand) => {
@@ -73848,6 +74014,7 @@ registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand
73848
74014
  "ci cancel-orphans",
73849
74015
  "run cancel",
73850
74016
  "run rerun",
74017
+ "run dispatch",
73851
74018
  // #9779/#9780: the vars verbs and archive-status validate --repo/name/value before the first
73852
74019
  // dispatch, so their scoped App mint waits for the action handler like the run writes above.
73853
74020
  "vars get",
@@ -74727,13 +74894,18 @@ ci.command("hosted-usage").description("read-only hosted tick and lane-demand al
74727
74894
  });
74728
74895
  registerCiChecksWait(ci);
74729
74896
  registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
74730
- 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");
74897
+ var runRoot = program2.command("run").description("bounded workflow-run writes on the org App actions:write profile");
74731
74898
  registerRunCancel(runRoot, async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run cancel")).token);
74732
74899
  registerRunRerun(
74733
74900
  runRoot,
74734
74901
  async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run rerun")).token,
74735
74902
  async () => fetchProjectsList(registryClientDeps(await loadConfig()))
74736
74903
  );
74904
+ registerRunDispatch(
74905
+ runRoot,
74906
+ async () => (await mintCommandWriteToken(appActorDeps(), "run dispatch")).token,
74907
+ async () => fetchProjectsList(registryClientDeps(await loadConfig()))
74908
+ );
74737
74909
  var varsAppClient = (commandPath4) => async () => (
74738
74910
  // The concrete client always provides restWithStatus (optional on the interface only so hand-built
74739
74911
  // 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.34",
3
+ "version": "4.6.36",
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/claude-plugin",
3
- "version": "4.6.34",
3
+ "version": "4.6.36",
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:
@@ -2,4 +2,7 @@
2
2
  ## MMI developer environment
3
3
 
4
4
  Use the [canonical MMI developer-environment guide](https://github.com/mutmutco/MMI-Hub/blob/main/docs/Architecture/agentic-dev-environment.md) for shared setup and daily agent work.
5
+
6
+ Daily loop: `mmi-cli oracle issue create` → `mmi-cli oracle board claim <n>` (stamps the mmi-claim lease) → `mmi-cli devops pr create` → `mmi-cli devops pr land <pr>`.
7
+ Any org member with repository write (developer) or repository admin (project admin) can run it, with no vault access; release trains stay org-owner only.
5
8
  <!-- mmi-developer-environment:end -->
@@ -6,5 +6,9 @@
6
6
  "scripts/next-version.mjs": {
7
7
  "blob": "2c1985b59edc779695479559074e7654c1574a11",
8
8
  "reason": "comment-only note that rc/release modes are unused in dev-only MM suite repos (#8282), no runtime or operator behaviour change"
9
+ },
10
+ "README.md": {
11
+ "blob": "768c5e35cdb48849c77bce6b334658adefe6a5d8",
12
+ "reason": "the developer-environment seed block now states the concrete mmi-cli loop (issue create, board claim, PR create, PR land) and who may run it — the README propagation is intended (#9987); no runbook step changes"
9
13
  }
10
14
  }
@@ -13,6 +13,23 @@ our self-hosted runners, dispatched by an org owner, never on a workstation (#91
13
13
  A 503 `github-rate-limited` answer means the org App's hourly GitHub allowance is used up. It is
14
14
  not a denial: wait for the reset time it names, then retry.
15
15
 
16
+ ## Developer and project-admin access
17
+
18
+ A developer is an org member with GitHub write on a product repository. A project admin is that
19
+ repository's admin. Both run the full loop with mmi-cli and need no MMI-Hub vault access:
20
+
21
+ ```sh
22
+ mmi-cli oracle issue create --title "<title>" --body-file <path> # files and attaches the board card
23
+ mmi-cli oracle board claim <n> # stamps the mmi-claim lease
24
+ mmi-cli devops pr create --title "<title>" --base development
25
+ mmi-cli devops pr land <pr-number>
26
+ ```
27
+
28
+ Board writes (attach, claim stamp, status move) gate on org owner, repo admin or repository write, so
29
+ these roles pass. Where the GitHub App route is absent the writes go as the person's own GitHub login
30
+ (one note says so); an agent host with the App route writes as the App. MMI-Hub's own merges and all
31
+ release trains stay org-owner only.
32
+
16
33
  Start with `mmi-cli onboard` and the [board skill](../../board/SKILL.md).
17
34
  Reading or discussing the board is read-only until the developer authorizes work.
18
35
  Claim approved work with `mmi-cli oracle board claim <issue>`, then prepare an isolated task