@mutmutco/claude-plugin 4.5.113 → 4.5.115

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.5.113",
4
+ "version": "4.5.115",
5
5
  "mmiCompat": "4.x",
6
6
  "description": "MMI workflow skills and org gates delivery.",
7
7
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -957,6 +957,8 @@ var init_house_map = __esm({
957
957
  // register/run/park schedule rows under the one lane contract
958
958
  // --- devops — shipping --------------------------------------------------------------------------
959
959
  pr: "devops",
960
+ read: "devops",
961
+ // closed typed GitHub reads on the App pool (run/pr/issue broker, #8042)
960
962
  api: "devops",
961
963
  // allowlisted GitHub REST GET reads for automation (pull files, compare, contents)
962
964
  ci: "devops",
@@ -5746,6 +5748,19 @@ var APP_VAULT_REPO = "mutmutco/MMI-Hub";
5746
5748
  var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
5747
5749
  var APP_ELIGIBLE_COMMANDS = /* @__PURE__ */ new Set([
5748
5750
  "pr checks-wait",
5751
+ // #8042: App-native required-only read (`pr checks-required`) — REST check/status rows scoped by
5752
+ // fetchRequiredCheckContexts, never native `gh pr checks` for the required verdict (historical EMPTY
5753
+ // App table on the #3035 fixture/profile — REST is the deterministic adapter, not a claim native
5754
+ // always fails: same-token parity on later fixtures agrees).
5755
+ "pr checks-required",
5756
+ // #8042: the closed typed read broker — run/pr/issue families, REST pr-checks, allowlisted api get.
5757
+ "read run view",
5758
+ "read run list",
5759
+ "read run watch",
5760
+ "read pr view",
5761
+ "read pr checks",
5762
+ "read issue view",
5763
+ "read issue list",
5749
5764
  "pr view",
5750
5765
  "issue list",
5751
5766
  "issue view",
@@ -6308,6 +6323,9 @@ async function ghReadEnv() {
6308
6323
  const appToken = await pollToken();
6309
6324
  return appToken ? { ...process.env, GH_TOKEN: appToken } : void 0;
6310
6325
  }
6326
+ function ghApiWithEnv(env) {
6327
+ return (args) => defaultGhApi(args, env);
6328
+ }
6311
6329
  var INCLUDED_HEADER_BLOCK = /^(?:\r?\n)?HTTP\/\d(?:\.\d)? (\d{3})[^\n]*\n(?:[^\n]+\n)*?\r?\n/gm;
6312
6330
  function parseIncludedOutput(out) {
6313
6331
  let status;
@@ -6361,7 +6379,7 @@ function observedCorePool(source, nowMs) {
6361
6379
  const seen = source ? observedPool.get(source) : void 0;
6362
6380
  return seen && nowMs - seen.atMs <= OBSERVED_POOL_MAX_AGE_MS && seen.reset * 1e3 > nowMs ? { remaining: seen.remaining, reset: seen.reset } : void 0;
6363
6381
  }
6364
- async function defaultGhApi(args) {
6382
+ async function defaultGhApi(args, env = process.env) {
6365
6383
  const appToken = await pollToken();
6366
6384
  const source = appToken ? "app" : "user";
6367
6385
  const key = `${source}\0${args.join("\0")}`;
@@ -6372,7 +6390,9 @@ async function defaultGhApi(args) {
6372
6390
  timeout: REST_GH_TIMEOUT_MS,
6373
6391
  // Only on the App source: passing `env` at all replaces the inherited environment wholesale,
6374
6392
  // and `gh` needs the rest of it (PATH, GH_HOST, config home). Ambient rides the inherited env.
6375
- ...source === "app" && appToken ? { env: { ...process.env, GH_TOKEN: appToken } } : {}
6393
+ // #8042: callers may pass a scrubbed env (brokerChildEnv) to pin the host and drop enterprise
6394
+ // credentials; the default is the process env (legacy waiter behavior, unchanged).
6395
+ ...source === "app" && appToken ? { env: { ...env, GH_TOKEN: appToken } } : env === process.env ? {} : { env }
6376
6396
  }));
6377
6397
  } catch (e) {
6378
6398
  const out = e.stdout;
@@ -6601,13 +6621,13 @@ async function fetchRestClosingGuardPayload(prNumber, repo, gh2 = defaultGhApi)
6601
6621
  return { pr, commits };
6602
6622
  }
6603
6623
  async function fetchHeadCheckRuns(headSha, repo, gh2) {
6604
- const runsOut = await gh2(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id, details_url}"]);
6624
+ const runsOut = await gh2(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id, details_url, started_at, completed_at}"]);
6605
6625
  return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
6606
6626
  }
6607
6627
  async function fetchHeadCheckEntries(headSha, repo, gh2) {
6608
6628
  const [runs, statusesOut] = await Promise.all([
6609
6629
  fetchHeadCheckRuns(headSha, repo, gh2),
6610
- gh2(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state}"])
6630
+ gh2(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state, target_url, description}"])
6611
6631
  ]);
6612
6632
  const statuses = parseNdjsonLines(statusesOut);
6613
6633
  return [
@@ -6654,6 +6674,212 @@ async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaul
6654
6674
  async function pollRestCommitChecks(sha, repo, gh2 = defaultGhApi) {
6655
6675
  return derivePollState(partitionByRequired(await fetchHeadCheckEntries(sha, repo, gh2), null).relevant);
6656
6676
  }
6677
+ var GH_ZERO_TIME = "0001-01-01T00:00:00Z";
6678
+ var GH_PR_CHECKS_JSON_FIELDS = ["bucket", "completedAt", "description", "link", "name", "startedAt", "state"];
6679
+ function nativeStateForRun(run) {
6680
+ if (run.status !== "completed") {
6681
+ const raw2 = (run.status ?? "pending").toUpperCase();
6682
+ if (raw2 === "QUEUED" || raw2 === "IN_PROGRESS" || raw2 === "WAITING" || raw2 === "REQUESTED" || raw2 === "PENDING") return raw2;
6683
+ return "PENDING";
6684
+ }
6685
+ const raw = (run.conclusion ?? "").toUpperCase();
6686
+ if (raw === "SUCCESS" || raw === "SKIPPED" || raw === "NEUTRAL" || raw === "ERROR" || raw === "FAILURE" || raw === "TIMED_OUT" || raw === "ACTION_REQUIRED" || raw === "CANCELLED" || raw === "STALE") return raw;
6687
+ return "PENDING";
6688
+ }
6689
+ function nativeStateForStatus(state) {
6690
+ const raw = (state ?? "").toUpperCase();
6691
+ if (raw === "SUCCESS" || raw === "FAILURE" || raw === "ERROR" || raw === "PENDING" || raw === "EXPECTED") return raw;
6692
+ return "PENDING";
6693
+ }
6694
+ function bucketForNativeState(state) {
6695
+ if (state === "SUCCESS") return "pass";
6696
+ if (state === "SKIPPED" || state === "NEUTRAL") return "skipping";
6697
+ if (state === "FAILURE" || state === "ERROR" || state === "TIMED_OUT" || state === "ACTION_REQUIRED") return "fail";
6698
+ if (state === "CANCELLED") return "cancel";
6699
+ return "pending";
6700
+ }
6701
+ async function fetchHeadCheckRows(headSha, repo, gh2 = defaultGhApi) {
6702
+ const [runs, statusesOut] = await Promise.all([
6703
+ fetchHeadCheckRuns(headSha, repo, gh2),
6704
+ gh2(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state, target_url, description}"])
6705
+ ]);
6706
+ const statuses = parseNdjsonLines(statusesOut);
6707
+ const rows = [];
6708
+ const nameAppIds = /* @__PURE__ */ new Map();
6709
+ for (const run of runs) {
6710
+ if (AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")) continue;
6711
+ const runName = run.name ?? `check-run ${run.id ?? "unknown"}`;
6712
+ if (typeof run.app_id === "number") {
6713
+ const known = nameAppIds.get(runName) ?? [];
6714
+ if (!known.includes(run.app_id)) known.push(run.app_id);
6715
+ nameAppIds.set(runName, known);
6716
+ }
6717
+ const state = nativeStateForRun(run);
6718
+ rows.push({
6719
+ bucket: bucketForNativeState(state),
6720
+ completedAt: typeof run.completed_at === "string" && run.completed_at ? run.completed_at : GH_ZERO_TIME,
6721
+ // Check-runs carry no description in this endpoint (no output join — never invented).
6722
+ description: "",
6723
+ link: typeof run.details_url === "string" && run.details_url ? run.details_url : "",
6724
+ name: runName,
6725
+ startedAt: typeof run.started_at === "string" && run.started_at ? run.started_at : GH_ZERO_TIME,
6726
+ state
6727
+ });
6728
+ }
6729
+ for (const s of statuses) {
6730
+ if (AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")) continue;
6731
+ const state = nativeStateForStatus(s.state);
6732
+ rows.push({
6733
+ bucket: bucketForNativeState(state),
6734
+ // Legacy statuses carry no timestamps: native-compatible zero, never omitted.
6735
+ completedAt: GH_ZERO_TIME,
6736
+ description: typeof s.description === "string" ? s.description : "",
6737
+ link: typeof s.target_url === "string" && s.target_url ? s.target_url : "",
6738
+ name: s.context ?? "unknown-status",
6739
+ startedAt: GH_ZERO_TIME,
6740
+ state
6741
+ });
6742
+ }
6743
+ return { rows, nameAppIds };
6744
+ }
6745
+ function identitiesFromProtectionPayload(parsed) {
6746
+ if (typeof parsed !== "object" || parsed === null) {
6747
+ throw new Error("malformed requirements metadata: protection body is not an object");
6748
+ }
6749
+ const { contexts, checks } = parsed;
6750
+ const entries = [];
6751
+ if (checks !== void 0) {
6752
+ if (!Array.isArray(checks)) throw new Error("malformed requirements metadata: protection checks is not an array");
6753
+ for (const c of checks) {
6754
+ entries.push(protectionCheckToEntry(c, "protection checks[]"));
6755
+ }
6756
+ }
6757
+ if (contexts !== void 0) {
6758
+ if (!Array.isArray(contexts)) throw new Error("malformed requirements metadata: protection contexts is not an array");
6759
+ for (const c of contexts) {
6760
+ if (typeof c !== "string" || !c) throw new Error("malformed requirements metadata: protection contexts holds a non-string entry");
6761
+ if (!entries.some((e) => e.context === c)) entries.push({ context: c, appId: null });
6762
+ }
6763
+ }
6764
+ if (!Array.isArray(checks) && !Array.isArray(contexts)) {
6765
+ throw new Error("malformed requirements metadata: protection body carries neither contexts nor checks");
6766
+ }
6767
+ return entries;
6768
+ }
6769
+ function protectionCheckToEntry(c, where) {
6770
+ if (typeof c?.context !== "string" || !c.context) {
6771
+ throw new Error(`malformed requirements metadata: ${where} holds an entry without a string context`);
6772
+ }
6773
+ return { context: c.context, appId: readAppId(c, where) };
6774
+ }
6775
+ function readAppId(c, where) {
6776
+ if (c.app_id === void 0 || c.app_id === null) {
6777
+ if (c.integration_id === void 0 || c.integration_id === null) return null;
6778
+ if (typeof c.integration_id !== "number") throw new Error(`malformed requirements metadata: ${where} holds a non-numeric integration id`);
6779
+ return c.integration_id;
6780
+ }
6781
+ if (typeof c.app_id !== "number") throw new Error(`malformed requirements metadata: ${where} holds a non-numeric app id`);
6782
+ return c.app_id;
6783
+ }
6784
+ function identitiesFromRulesetPayload(parsed) {
6785
+ if (!Array.isArray(parsed)) {
6786
+ throw new Error("malformed requirements metadata: ruleset body is not an array");
6787
+ }
6788
+ const entries = [];
6789
+ for (const rule of parsed) {
6790
+ if (!rule || typeof rule.type !== "string" || !rule.type) throw new Error("malformed requirements metadata: ruleset entry has no rule type");
6791
+ if (rule.type !== "required_status_checks") continue;
6792
+ const list = rule.parameters?.required_status_checks;
6793
+ if (!Array.isArray(list)) throw new Error("malformed requirements metadata: ruleset required_status_checks is not an array");
6794
+ for (const check of list) {
6795
+ entries.push(protectionCheckToEntry(check, "ruleset required_status_checks"));
6796
+ }
6797
+ }
6798
+ return entries;
6799
+ }
6800
+ async function fetchRequiredCheckIdentities(repo, branch, gh2 = defaultGhApi) {
6801
+ const byIdentity = /* @__PURE__ */ new Map();
6802
+ const keyOf = (context, appId) => `${context}\0${appId ?? ""}`;
6803
+ const add = (entry) => {
6804
+ const key = keyOf(entry.context, entry.appId);
6805
+ if (!byIdentity.has(key)) byIdentity.set(key, entry);
6806
+ };
6807
+ try {
6808
+ const raw = await gh2([`repos/${repo}/branches/${encodeURIComponent(branch)}/protection/required_status_checks`]);
6809
+ let parsed;
6810
+ try {
6811
+ parsed = JSON.parse(raw);
6812
+ } catch {
6813
+ throw new Error("malformed requirements metadata: protection body is not JSON");
6814
+ }
6815
+ for (const entry of identitiesFromProtectionPayload(parsed)) add(entry);
6816
+ } catch (e) {
6817
+ if (!isNotFoundError(e)) {
6818
+ if (e instanceof Error && e.message.startsWith("malformed requirements metadata")) throw e;
6819
+ return null;
6820
+ }
6821
+ }
6822
+ try {
6823
+ const raw = await gh2([`repos/${repo}/rules/branches/${encodeURIComponent(branch)}`]);
6824
+ let parsed;
6825
+ try {
6826
+ parsed = JSON.parse(raw);
6827
+ } catch {
6828
+ throw new Error("malformed requirements metadata: ruleset body is not JSON");
6829
+ }
6830
+ for (const entry of identitiesFromRulesetPayload(parsed)) add(entry);
6831
+ } catch (e) {
6832
+ if (!isNotFoundError(e)) {
6833
+ if (e instanceof Error && e.message.startsWith("malformed requirements metadata")) throw e;
6834
+ return null;
6835
+ }
6836
+ }
6837
+ return [...byIdentity.values()];
6838
+ }
6839
+ async function readRequiredCheckRows(prNumber, repo, gh2 = defaultGhApi) {
6840
+ const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
6841
+ let entries;
6842
+ try {
6843
+ entries = await fetchRequiredCheckIdentities(repo, snapshot.baseRef, gh2);
6844
+ } catch (e) {
6845
+ throw new Error(`required-context lookup failed for ${repo}@${snapshot.baseRef} (${readErrorText(e)}) \u2014 refusing a required verdict against unknown requirements`);
6846
+ }
6847
+ if (entries === null) {
6848
+ throw new Error(`required-context lookup failed for ${repo}@${snapshot.baseRef} (protection/ruleset read failed) \u2014 refusing a required verdict against unknown requirements`);
6849
+ }
6850
+ const appScope = [...entries].sort((a, b) => a.context.localeCompare(b.context));
6851
+ const required = [...new Set(appScope.map((e) => e.context))];
6852
+ const { rows: reported, nameAppIds } = await fetchHeadCheckRows(snapshot.headSha, repo, gh2);
6853
+ const scoped = required.length > 0;
6854
+ const requiredNames = new Set(required);
6855
+ const inScope = scoped ? reported.filter((row) => requiredNames.has(row.name)) : reported;
6856
+ const reportedNames = new Set(reported.map((row) => row.name));
6857
+ const missing = appScope.filter((entry) => !isRequirementMet(entry, reportedNames, nameAppIds)).map((entry) => entry.appId === null ? entry.context : `${entry.context} (app ${entry.appId})`);
6858
+ let state;
6859
+ if (missing.length > 0) {
6860
+ state = "pending";
6861
+ } else if (inScope.length === 0) {
6862
+ state = "no-checks-reported";
6863
+ } else {
6864
+ state = verdictFromBuckets(inScope.map((row) => row.bucket));
6865
+ }
6866
+ return { headSha: snapshot.headSha, baseRef: snapshot.baseRef, required, appScope, missing, rows: inScope, state, scoped };
6867
+ }
6868
+ function isRequirementMet(entry, reportedNames, nameAppIds) {
6869
+ if (entry.appId === null) return reportedNames.has(entry.context);
6870
+ return nameAppIds.get(entry.context)?.includes(entry.appId) ?? false;
6871
+ }
6872
+ function verdictFromBuckets(buckets) {
6873
+ const anyFail = buckets.includes("fail") || buckets.includes("cancel");
6874
+ const anyPending = buckets.includes("pending");
6875
+ if (buckets.length === 0) return "no-checks-reported";
6876
+ return anyFail ? anyPending ? "failing" : "failure" : anyPending ? "pending" : "success";
6877
+ }
6878
+ function checkRowsExitCode(state) {
6879
+ if (state === "success") return 0;
6880
+ if (state === "failure" || state === "failing") return 1;
6881
+ return 8;
6882
+ }
6657
6883
  async function pollRestPrMergeable(prNumber, repo, gh2 = defaultGhApi) {
6658
6884
  try {
6659
6885
  return (await fetchRestPrSnapshot(prNumber, repo, gh2)).mergeable;
@@ -6885,6 +7111,20 @@ async function fetchRestCorePool(gh2 = defaultGhApi, nowMs = Date.now()) {
6885
7111
  }
6886
7112
 
6887
7113
  // src/claim-marker-client.ts
7114
+ async function selectLivenessClient(client, appClient) {
7115
+ if (appClient) return appClient;
7116
+ let token;
7117
+ try {
7118
+ token = await pollToken();
7119
+ } catch (e) {
7120
+ const failing = async () => {
7121
+ throw e;
7122
+ };
7123
+ return { ...client, rest: failing, restPaginate: failing, graphql: failing };
7124
+ }
7125
+ if (!token) return client;
7126
+ return createGitHubClient({ token: async () => token });
7127
+ }
6888
7128
  function withAppClaimMarkerRead(client, appClient) {
6889
7129
  const commentsPath = /^repos\/[^/]+\/[^/]+\/issues\/\d+\/comments$/;
6890
7130
  return {
@@ -13850,10 +14090,10 @@ function laneResumeEvidence(evidence) {
13850
14090
  return `prior claim by lane ${describeClaimMarker(evidence.marker)} (@${evidence.markerBy}, ${formatClaimAge(evidence.markerAgeMs)} old) is verifiably not running on this host \u2014 its local session transcript is gone or stale${artifacts.length ? `; resuming over ${artifacts.join("; ")}` : ""}`;
13851
14091
  }
13852
14092
  async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin, appClient) {
13853
- const markerClient = withAppClaimMarkerRead(client, appClient);
13854
- let evidence = await gatherClaimLiveness(markerClient, item.repository, item.number, openPullsFetcher(client));
14093
+ const livenessClient = await selectLivenessClient(client, appClient);
14094
+ let evidence = await gatherClaimLiveness(livenessClient, item.repository, item.number, openPullsFetcher(livenessClient));
13855
14095
  if (evidence.failed.some((channel) => channel !== "session")) {
13856
- evidence = await gatherClaimLiveness(markerClient, item.repository, item.number, openPullsFetcher(client));
14096
+ evidence = await gatherClaimLiveness(livenessClient, item.repository, item.number, openPullsFetcher(livenessClient));
13857
14097
  }
13858
14098
  const ownership = laneOwnership(evidence.marker, actor);
13859
14099
  const sameOwner = Boolean(viewerLogin && evidence.markerBy && evidence.markerBy.toLowerCase() === viewerLogin.toLowerCase());
@@ -13874,13 +14114,19 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
13874
14114
  };
13875
14115
  }
13876
14116
  function laneContestParts(contest) {
13877
- const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : contest.unverifiable.includes("comments") ? "an unreadable lane (its claim-marker comments could not be read, so no holder can be named)" : "another lane";
14117
+ const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : contest.unverifiable.includes("comments") ? "an unreadable lane (its claim-marker comments could not be read, so no holder can be named)" : contest.live.length === 0 && contest.unverifiable.length > 0 ? "an unknown holder (no claim marker was read and liveness could not be fully verified, so no holder can be named)" : "another lane";
13878
14118
  const seen = contest.live.join("; ");
13879
14119
  const unreadable = contest.unverifiable.length ? `liveness could not be fully verified (failed to read: ${contest.unverifiable.join(", ")})` : "";
13880
14120
  return { holder, why: [seen, unreadable].filter(Boolean).join("; ") };
13881
14121
  }
14122
+ function isUnknownHold(contest) {
14123
+ return !contest.marker && contest.live.length === 0 && contest.unverifiable.length > 0;
14124
+ }
13882
14125
  function laneContestMessage(ref, contest, verb) {
13883
14126
  const { holder, why } = laneContestParts(contest);
14127
+ if (isUnknownHold(contest)) {
14128
+ return verb === "claim" ? `${ref} cannot be verified as free: ${holder} (${why}); the liveness reads failed so the claim is refused fail-closed \u2014 re-run with --force to take it anyway` : `refusing to unclaim ${ref}: ${holder} (${why}); the liveness reads failed so the unclaim is refused fail-closed \u2014 re-run with --force --holder-dead once you have evidence the holder lane is dead, or wait for it to go stale (#6625)`;
14129
+ }
13884
14130
  return verb === "claim" ? `${ref} is already claimed by ${holder} with live evidence of active work (${why}); re-run with --force to take it anyway` : `refusing to unclaim ${ref}: ${holder} shows live evidence of active work (${why}); re-run with --force --holder-dead once you have evidence the holder lane is dead, or wait for it to go stale (#6625)`;
13885
14131
  }
13886
14132
  function ghError(e) {
@@ -14547,8 +14793,9 @@ async function unclaimBoardIssue(options, deps = {}) {
14547
14793
  }
14548
14794
  if (!options.holderDead) {
14549
14795
  const { holder, why } = laneContestParts(contest);
14796
+ const still = isUnknownHold(contest) ? `${holder} (${why}); the liveness reads failed` : `${holder} still shows live evidence of active work (${why})`;
14550
14797
  throw new Error(withGatedWriteChainHint(
14551
- `refusing to unclaim ${item.ref} with --force: ${holder} still shows live evidence of active work (${why}); --force no longer overrides a live lane alone \u2014 re-run with --holder-dead once you have evidence the holder's session is dead, or wait for its lane to go stale (#6625)`
14798
+ `refusing to unclaim ${item.ref} with --force: ${still}; --force no longer overrides a live lane alone \u2014 re-run with --holder-dead once you have evidence the holder's session is dead, or wait for its lane to go stale (#6625)`
14552
14799
  ));
14553
14800
  }
14554
14801
  }
@@ -17692,10 +17939,10 @@ var rollout_plan_default = {
17692
17939
  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)."
17693
17940
  },
17694
17941
  baseline: {
17695
- version: "4.5.113",
17696
- tag: "v4.5.113",
17697
- commit: "c5ad425d42e2",
17698
- npm: "@mutmutco/cli@4.5.113"
17942
+ version: "4.5.115",
17943
+ tag: "v4.5.115",
17944
+ commit: "8d0add650762",
17945
+ npm: "@mutmutco/cli@4.5.115"
17699
17946
  },
17700
17947
  exitCriterion: "fleet-n-of-n",
17701
17948
  hubOnlyShortcut: "forbidden",
@@ -17712,14 +17959,14 @@ var rollout_plan_default = {
17712
17959
  repo: "mutmutco/mmi-hub",
17713
17960
  role: "canary",
17714
17961
  schedule: "train",
17715
- v3Target: "v4.5.113"
17962
+ v3Target: "v4.5.115"
17716
17963
  }
17717
17964
  ],
17718
17965
  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.",
17719
17966
  rollback: {
17720
17967
  independent: true,
17721
- mechanism: "npm dist-tag latest -> 4.5.113 and redeploy the Hub Lambda from tag v4.5.113 (c5ad425d42e2); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17722
- v3Target: "v4.5.113 (@mutmutco/cli@4.5.113, tag commit c5ad425d42e2 \u2014 last known-good release carrying the repo-index v4-only contract)"
17968
+ mechanism: "npm dist-tag latest -> 4.5.115 and redeploy the Hub Lambda from tag v4.5.115 (8d0add650762); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17969
+ v3Target: "v4.5.115 (@mutmutco/cli@4.5.115, tag commit 8d0add650762 \u2014 last known-good release carrying the repo-index v4-only contract)"
17723
17970
  }
17724
17971
  },
17725
17972
  {
@@ -29503,7 +29750,7 @@ var PRIMARY_GROUPS = [
29503
29750
  ["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
29504
29751
  ];
29505
29752
  var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin", "packages"]);
29506
- var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup"]);
29753
+ var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup", "read"]);
29507
29754
  var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "dist", "wave", "report", "skill-lesson", "closure-rate"]);
29508
29755
  var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
29509
29756
  var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
@@ -29543,6 +29790,7 @@ var COMMAND_OWNERSHIP = {
29543
29790
  stage: { module_owner: "cli/src/stage-commands.ts", consumer: "agent-workflow" },
29544
29791
  pr: { module_owner: "cli/src/pr-commands.ts", consumer: "agent-workflow" },
29545
29792
  api: { module_owner: "cli/src/api-read.ts", consumer: "agent-workflow" },
29793
+ read: { module_owner: "cli/src/app-read-broker.ts", consumer: "agent-workflow" },
29546
29794
  ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
29547
29795
  rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
29548
29796
  release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
@@ -43080,7 +43328,7 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
43080
43328
  }
43081
43329
  }
43082
43330
  if (state?.toLowerCase() === "closed") continue;
43083
- const contest = await checkContest(client, { repository: repo, number }, actor, viewerLogin);
43331
+ const contest = await checkContest(client, { repository: repo, number }, actor, viewerLogin, deps.appClient);
43084
43332
  if (!contest.contested) continue;
43085
43333
  const ref = `${repo}#${number}`;
43086
43334
  const commentsPath = `repos/${repo}/issues/${number}/comments`;
@@ -43164,6 +43412,7 @@ var SERVICE_NAME_RE = /^[A-Za-z0-9_.-]+$/;
43164
43412
  var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
43165
43413
  var EXCLUDE_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
43166
43414
  var EXCLUDE_LABEL = "mmi.runtime-env.exclude";
43415
+ var KEYS_LABEL = "mmi.runtime-env.keys";
43167
43416
  var RUNTIME_ENV_OVERRIDE_RELPATH = "tmp/stage/runtime-env.override.yml";
43168
43417
  function labelsMarkRuntimeEnv(labels) {
43169
43418
  if (Array.isArray(labels)) {
@@ -43190,12 +43439,16 @@ function labelValue(labels, name) {
43190
43439
  if (labels && typeof labels === "object") return String(labels[name] ?? "");
43191
43440
  return "";
43192
43441
  }
43193
- function parseRuntimeEnvExclude(value, service = "?") {
43442
+ function labelPresent(labels, name) {
43443
+ if (Array.isArray(labels)) return labels.some((l) => typeof l === "string" && (l.trim() === name || l.slice(0, l.indexOf("=")).trim() === name));
43444
+ return !!labels && typeof labels === "object" && name in labels;
43445
+ }
43446
+ function parseRuntimeEnvExclude(value, service = "?", label = EXCLUDE_LABEL) {
43194
43447
  if (!value.trim()) return [];
43195
43448
  return value.split(",").map((raw) => {
43196
43449
  const key = raw.trim();
43197
43450
  if (!EXCLUDE_KEY_RE.test(key)) {
43198
- throw new Error(`invalid ${EXCLUDE_LABEL} entry on service ${service}: '${key}' (comma-separated env-var names matching ^[A-Z_][A-Z0-9_]*$)`);
43451
+ throw new Error(`invalid ${label} entry on service ${service}: '${key}' (comma-separated env-var names matching ^[A-Z_][A-Z0-9_]*$)`);
43199
43452
  }
43200
43453
  return key;
43201
43454
  });
@@ -43218,6 +43471,23 @@ function runtimeEnvExcludes(composeConfigJson) {
43218
43471
  }
43219
43472
  return out;
43220
43473
  }
43474
+ function runtimeEnvSelects(composeConfigJson) {
43475
+ let parsed;
43476
+ try {
43477
+ parsed = JSON.parse(composeConfigJson);
43478
+ } catch {
43479
+ return {};
43480
+ }
43481
+ const services = parsed?.services;
43482
+ if (!services || typeof services !== "object" || Array.isArray(services)) return {};
43483
+ const out = {};
43484
+ for (const [name, svc] of Object.entries(services)) {
43485
+ const labels = svc?.labels;
43486
+ if (!SERVICE_NAME_RE.test(name) || !labelsMarkRuntimeEnv(labels) || !labelPresent(labels, KEYS_LABEL)) continue;
43487
+ out[name] = parseRuntimeEnvExclude(labelValue(labels, KEYS_LABEL), name, KEYS_LABEL);
43488
+ }
43489
+ return out;
43490
+ }
43221
43491
  function runtimeEnvMarkedServices(composeConfigJson) {
43222
43492
  let parsed;
43223
43493
  try {
@@ -43229,12 +43499,16 @@ function runtimeEnvMarkedServices(composeConfigJson) {
43229
43499
  if (!services || typeof services !== "object" || Array.isArray(services)) return [];
43230
43500
  return Object.entries(services).filter(([name, svc]) => SERVICE_NAME_RE.test(name) && labelsMarkRuntimeEnv(svc?.labels)).map(([name]) => name).sort();
43231
43501
  }
43232
- function renderRuntimeEnvOverride(services, keys, excludes = {}) {
43502
+ function renderRuntimeEnvOverride(services, keys, excludes = {}, selects = {}) {
43233
43503
  const safeServices = services.filter((s) => SERVICE_NAME_RE.test(s)).sort();
43234
43504
  const safeKeys = [...new Set(keys.filter((k) => ENV_KEY_RE.test(k)))].sort();
43235
43505
  const keysFor = (service) => {
43236
43506
  const skip = new Set(excludes[service] ?? []);
43237
- return safeKeys.filter((k) => !skip.has(k));
43507
+ const picked = selects[service];
43508
+ for (const k of picked ?? []) {
43509
+ if (!safeKeys.includes(k)) throw new Error(`${KEYS_LABEL} on service ${service} names '${k}', which is not a declared runtime key`);
43510
+ }
43511
+ return safeKeys.filter((k) => !skip.has(k) && (!picked || picked.includes(k)));
43238
43512
  };
43239
43513
  const withKeys = safeServices.filter((s) => keysFor(s).length);
43240
43514
  if (!withKeys.length) return null;
@@ -43657,7 +43931,7 @@ async function prepareRuntimeEnvPassthrough(cwd, composeEnv, vaultEnvMerge) {
43657
43931
  } catch {
43658
43932
  return {};
43659
43933
  }
43660
- const body = renderRuntimeEnvOverride(runtimeEnvMarkedServices(configJson), keys, runtimeEnvExcludes(configJson));
43934
+ const body = renderRuntimeEnvOverride(runtimeEnvMarkedServices(configJson), keys, runtimeEnvExcludes(configJson), runtimeEnvSelects(configJson));
43661
43935
  if (!body) return {};
43662
43936
  const overridePath = (0, import_node_path38.join)(cwd, RUNTIME_ENV_OVERRIDE_RELPATH);
43663
43937
  (0, import_node_fs39.mkdirSync)((0, import_node_path38.join)(cwd, "tmp", "stage"), { recursive: true });
@@ -51556,6 +51830,352 @@ function registerOrgHealthQuery(program3, deps = defaultOrgHealthQueryDeps()) {
51556
51830
  });
51557
51831
  }
51558
51832
 
51833
+ // src/app-read-broker.ts
51834
+ init_cli_shared();
51835
+ var OWNER_REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
51836
+ var JSON_FIELD_RE = /^[A-Za-z0-9_,]+$/;
51837
+ var ID_RE = /^\d+$/;
51838
+ function parseBrokerRepo(raw, flag) {
51839
+ if (raw === void 0) return void 0;
51840
+ const repo = raw.trim();
51841
+ if (!OWNER_REPO_RE.test(repo)) {
51842
+ throw new Error(`read: ${flag} must be owner/repo on github.com (received '${raw}') \u2014 hosts, URLs and empty segments are refused`);
51843
+ }
51844
+ return repo;
51845
+ }
51846
+ function parseBrokerId(raw, what) {
51847
+ if (!ID_RE.test(raw.trim())) {
51848
+ throw new Error(`read: ${what} must be a number (received '${raw}') \u2014 URLs and branch names are refused`);
51849
+ }
51850
+ return raw.trim();
51851
+ }
51852
+ function parseBrokerInt(raw, flag, min, max) {
51853
+ const n = Number(raw);
51854
+ if (!Number.isInteger(n) || n < min || n > max) {
51855
+ throw new Error(`read: ${flag} must be an integer ${min}-${max} (received '${raw}')`);
51856
+ }
51857
+ return n;
51858
+ }
51859
+ function parseBrokerJsonFields(raw, allowed) {
51860
+ if (raw === void 0 || raw === false) return void 0;
51861
+ if (raw === true) return void 0;
51862
+ const value = raw.trim();
51863
+ if (!JSON_FIELD_RE.test(value) || value.split(",").some((f) => !f)) {
51864
+ throw new Error(`read: --json fields must be comma-separated names (received '${raw}')`);
51865
+ }
51866
+ if (allowed) {
51867
+ const unknown = value.split(",").filter((f) => !allowed.includes(f));
51868
+ if (unknown.length > 0) {
51869
+ throw new Error(`read: unknown --json field(s): ${unknown.join(", ")} \u2014 one of: ${allowed.join(",")}`);
51870
+ }
51871
+ }
51872
+ return value;
51873
+ }
51874
+ var BROKER_JSON_FLAGS_NOTE = "formatter flags are refused (--jq/--template can read child credentials; --web opens a browser) \u2014 use --json with field selection";
51875
+ function parseBrokerIssueState(raw) {
51876
+ if (raw === void 0) return void 0;
51877
+ const v = raw.trim().toLowerCase();
51878
+ if (v !== "open" && v !== "closed" && v !== "all") {
51879
+ throw new Error(`read: --state must be one of: open, closed, all (received '${raw}')`);
51880
+ }
51881
+ return v;
51882
+ }
51883
+ var RUN_LIST_STATUSES = ["queued", "completed", "in_progress", "requested", "waiting", "pending", "action_required", "cancelled", "failure", "neutral", "skipped", "stale", "startup_failure", "success", "timed_out"];
51884
+ function parseBrokerRunStatus(raw) {
51885
+ if (raw === void 0) return void 0;
51886
+ const v = raw.trim().toLowerCase();
51887
+ if (!RUN_LIST_STATUSES.includes(v)) {
51888
+ throw new Error(`read: --status must be one of: ${RUN_LIST_STATUSES.join("|")} (received '${raw}')`);
51889
+ }
51890
+ return v;
51891
+ }
51892
+ function parseBrokerAssignee(raw) {
51893
+ if (raw === void 0) return void 0;
51894
+ const v = raw.trim();
51895
+ if (/^@?me$/i.test(v) || /^viewer$/i.test(v)) {
51896
+ throw new Error(`read: --assignee '${raw}' resolves the viewer \u2014 viewer-scoped reads stay personal, never the App pool`);
51897
+ }
51898
+ if (!/^[A-Za-z0-9_.-]+$/.test(v.replace(/^@/, ""))) {
51899
+ throw new Error(`read: --assignee must be a login (received '${raw}')`);
51900
+ }
51901
+ return v;
51902
+ }
51903
+ var SINGLE_SHOT_TIMEOUT_MS = 6e4;
51904
+ function withRepo(args, repo) {
51905
+ return repo ? [...args, "--repo", repo] : args;
51906
+ }
51907
+ function withJson(args, jsonFields, jsonFlag) {
51908
+ if (jsonFlag === void 0 || jsonFlag === false) return args;
51909
+ return typeof jsonFields === "string" ? [...args, "--json", jsonFields] : [...args, "--json"];
51910
+ }
51911
+ function buildRunViewSpec(id, o) {
51912
+ const args = withJson(withRepo(["run", "view", parseBrokerId(id, "run id"), ...o.attempt !== void 0 ? ["--attempt", String(parseBrokerInt(o.attempt, "--attempt", 1, 1e3))] : []], parseBrokerRepo(o.repo, "--repo")), parseBrokerJsonFields(o.json), o.json);
51913
+ if (o.exitStatus) args.push("--exit-status");
51914
+ return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
51915
+ }
51916
+ function buildRunListSpec(o) {
51917
+ const args = withJson(withRepo(["run", "list"], parseBrokerRepo(o.repo, "--repo")), parseBrokerJsonFields(o.json), o.json);
51918
+ const workflow = parseBrokerWorkflow(o.workflow);
51919
+ if (workflow) args.push("--workflow", workflow);
51920
+ const branch = parseBrokerBranch(o.branch);
51921
+ if (branch) args.push("--branch", branch);
51922
+ const status = parseBrokerRunStatus(o.status);
51923
+ if (status) args.push("--status", status);
51924
+ if (o.limit !== void 0) args.push("--limit", String(parseBrokerInt(o.limit, "--limit", 1, 100)));
51925
+ return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
51926
+ }
51927
+ function buildRunWatchSpec(id, o) {
51928
+ const args = withRepo(["run", "watch", parseBrokerId(id, "run id")], parseBrokerRepo(o.repo, "--repo"));
51929
+ if (o.exitStatus) args.push("--exit-status");
51930
+ if (o.compact) args.push("--compact");
51931
+ args.push("--interval", String(parseBrokerInt(o.interval ?? "60", "--interval", 60, 300)));
51932
+ const timeoutMin = o.timeoutMin !== void 0 ? parseBrokerInt(o.timeoutMin, "--timeout", 1, 120) : 30;
51933
+ return { program: "gh", args, timeoutMs: timeoutMin * 6e4 };
51934
+ }
51935
+ function buildPrViewSpec(ref, o) {
51936
+ const args = withJson(withRepo(["pr", "view", parseBrokerId(ref, "PR number")], parseBrokerRepo(o.repo, "--repo")), parseBrokerJsonFields(o.json), o.json);
51937
+ if (o.comments) args.push("--comments");
51938
+ return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
51939
+ }
51940
+ function buildPrChecksSpec(number, o) {
51941
+ return {
51942
+ program: "rest-checks",
51943
+ number: parseBrokerId(number, "PR number"),
51944
+ repo: parseBrokerRepo(o.repo, "--repo"),
51945
+ required: o.required === true,
51946
+ jsonFields: parseBrokerJsonFields(o.json, GH_PR_CHECKS_JSON_FIELDS)
51947
+ };
51948
+ }
51949
+ function buildIssueViewSpec(number, o) {
51950
+ const args = withJson(withRepo(["issue", "view", parseBrokerId(number, "issue number")], parseBrokerRepo(o.repo, "--repo")), parseBrokerJsonFields(o.json), o.json);
51951
+ if (o.comments) args.push("--comments");
51952
+ return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
51953
+ }
51954
+ function parseBrokerWorkflow(raw) {
51955
+ if (raw === void 0) return void 0;
51956
+ if (!/^[A-Za-z0-9_./-]{1,200}$/.test(raw)) throw new Error(`read: --workflow must be a workflow name or file (received '${raw}')`);
51957
+ return raw;
51958
+ }
51959
+ function parseBrokerBranch(raw) {
51960
+ if (raw === void 0) return void 0;
51961
+ if (!/^[A-Za-z0-9_./-]{1,200}$/.test(raw)) throw new Error(`read: --branch must be a branch name (received '${raw}')`);
51962
+ return raw;
51963
+ }
51964
+ function parseBrokerSearch(raw) {
51965
+ if (raw === void 0) return void 0;
51966
+ if (!raw.trim()) throw new Error("read: --search must not be empty");
51967
+ return raw;
51968
+ }
51969
+ function parseBrokerLabel(raw) {
51970
+ if (raw === void 0) return void 0;
51971
+ if (!/^[A-Za-z0-9_.,: /-]{1,200}$/.test(raw)) throw new Error(`read: --label must be label text (received '${raw}')`);
51972
+ return raw;
51973
+ }
51974
+ function buildIssueListSpec(o) {
51975
+ const args = withJson(withRepo(["issue", "list"], parseBrokerRepo(o.repo, "--repo")), parseBrokerJsonFields(o.json), o.json);
51976
+ const search = parseBrokerSearch(o.search);
51977
+ if (search !== void 0) args.push("--search", search);
51978
+ const assignee = parseBrokerAssignee(o.assignee);
51979
+ if (assignee) args.push("--assignee", assignee);
51980
+ const label = parseBrokerLabel(o.label);
51981
+ if (label !== void 0) args.push("--label", label);
51982
+ const state = parseBrokerIssueState(o.state);
51983
+ if (state) args.push("--state", state);
51984
+ if (o.limit !== void 0) args.push("--limit", String(parseBrokerInt(o.limit, "--limit", 1, 100)));
51985
+ return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
51986
+ }
51987
+ function brokerChildEnv(base = process.env) {
51988
+ const env = { ...base };
51989
+ delete env.GITHUB_TOKEN;
51990
+ delete env.GH_ENTERPRISE_TOKEN;
51991
+ delete env.GITHUB_ENTERPRISE_TOKEN;
51992
+ delete env.GH_ENTERPRISE_URL;
51993
+ delete env.GITHUB_ENTERPRISE_URL;
51994
+ delete env.GH_REPO;
51995
+ delete env.GITHUB_REPOSITORY;
51996
+ delete env.GH_DEBUG;
51997
+ env.GH_HOST = "github.com";
51998
+ return env;
51999
+ }
52000
+ function redactPrivateCredential(text, env) {
52001
+ const credential = env.GH_TOKEN;
52002
+ if (!credential) return text;
52003
+ return text.split(credential).join("[redacted-credential]");
52004
+ }
52005
+ function ghExitCodeOf(error) {
52006
+ const code = error.code;
52007
+ return typeof code === "number" ? code : null;
52008
+ }
52009
+ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk)) {
52010
+ const env = brokerChildEnv();
52011
+ try {
52012
+ write(redactPrivateCredential(await run(spec.args, { timeoutMs: spec.timeoutMs, env }), env));
52013
+ return 0;
52014
+ } catch (e) {
52015
+ const err = e;
52016
+ if (typeof err.stdout === "string" && err.stdout) write(redactPrivateCredential(err.stdout, env));
52017
+ const code = ghExitCodeOf(e);
52018
+ if (code !== null) {
52019
+ if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
52020
+ return code;
52021
+ }
52022
+ throw new Error(`read: gh ${spec.args.slice(0, 2).join(" ")} did not finish on the App pool (${err.message ?? e}) \u2014 re-run the same command to refresh (same custody); reads never fall back to personal`);
52023
+ }
52024
+ }
52025
+ async function runBrokerRestChecks(deps, spec, resolveRepo2) {
52026
+ const repo = await resolveRepo2(spec.repo);
52027
+ const ghApi = deps.restGhApi(brokerChildEnv());
52028
+ if (!spec.required) {
52029
+ const snapshot = await deps.fetchSnapshot(spec.number, repo, ghApi).catch((e) => {
52030
+ throw new Error(`read pr checks: cannot resolve PR #${spec.number} on ${repo}: ${e?.message ?? e}`);
52031
+ });
52032
+ const { rows } = await deps.fetchRows(snapshot.headSha, repo, ghApi).catch((e) => {
52033
+ throw new Error(`read pr checks: check read failed for PR #${spec.number} on ${repo}: ${e?.message ?? e}`);
52034
+ });
52035
+ const state = verdictFromBuckets(rows.map((row) => row.bucket));
52036
+ return { rows, state, exitCode: checkRowsExitCode(state) };
52037
+ }
52038
+ const read = await deps.readRequired(spec.number, repo, ghApi).catch((e) => {
52039
+ throw new Error(`read pr checks: ${e?.message ?? e}`);
52040
+ });
52041
+ return { rows: read.rows, state: read.state, exitCode: checkRowsExitCode(read.state) };
52042
+ }
52043
+ function requireBrokerApp(label) {
52044
+ if (process.env.MMI_ACTOR !== "app") {
52045
+ fail(`${label}: App-only read \u2014 re-run with spawn-scoped MMI_ACTOR=app (reads never fall back to personal; interactive use keeps gh and the human commands)`);
52046
+ }
52047
+ }
52048
+ function registerPrChecksRequiredCommand(prRoot, resolveRepo2, overrides) {
52049
+ const deps = {
52050
+ readRequired: readRequiredCheckRows,
52051
+ fetchSnapshot: fetchRestPrSnapshot,
52052
+ fetchRows: fetchHeadCheckRows,
52053
+ restGhApi: overrides?.restGhApi ?? ((env) => ghApiWithEnv(env))
52054
+ };
52055
+ prRoot.command("checks-required").description(`required-only PR check read on the PR head (#8042): the GitHub merge-button verdict \u2014 REST check-runs + commit statuses scoped to GitHub's own required-status-check contexts (branch protection + rulesets), printed as native \`gh pr checks --json\` rows. Single shot, no wait. Never the ALL-checks waiter (#5336). REST is the deterministic closed-custody adapter: native returned an EMPTY table on the historical #3035 fixture/profile, and same-token parity on later fixtures agrees \u2014 not a claim native always fails. Reads merged/closed PRs like native does \u2014 the merge gate (not this read) decides what a state means. Exit 0 = required checks all pass (or no required contexts: every reported check passes); exit 1 = a required check FAILED or the read itself failed; exit 8 = required checks still pending (native pending code)`).option("--json [fields]", "machine-readable native-shaped rows (default: all known fields; comma-separated subset of: " + GH_PR_CHECKS_JSON_FIELDS.join(",") + ")", (v) => parseBrokerJsonFields(v, GH_PR_CHECKS_JSON_FIELDS)).option("--repo <owner/repo>", "target repo (defaults to the current repo)", (v) => parseBrokerRepo(v, "--repo")).argument("<number>", "PR number", (v) => parseBrokerId(v, "PR number")).action(async (number, o) => {
52056
+ let spec;
52057
+ try {
52058
+ spec = buildPrChecksSpec(number, { repo: o.repo, json: o.json, required: true });
52059
+ } catch (e) {
52060
+ return fail(e.message);
52061
+ }
52062
+ await dispatchRestChecksRead(deps, resolveRepo2, "pr checks-required", spec, o.json);
52063
+ });
52064
+ }
52065
+ function selectCheckRowFields(rows, jsonFields) {
52066
+ if (jsonFields === void 0) return rows.map((row) => ({ ...row }));
52067
+ const fields = jsonFields.split(",");
52068
+ return rows.map((row) => Object.fromEntries(fields.map((f) => [f, row[f]])));
52069
+ }
52070
+ async function dispatchRestChecksRead(deps, resolveRepo2, label, spec, json) {
52071
+ requireBrokerApp(label);
52072
+ let result;
52073
+ try {
52074
+ result = await runBrokerRestChecks(deps, spec, resolveRepo2);
52075
+ } catch (e) {
52076
+ return fail(`${label}: ${e.message}`);
52077
+ }
52078
+ if (json !== void 0 && json !== false) {
52079
+ printLine(JSON.stringify(selectCheckRowFields(result.rows, spec.jsonFields)));
52080
+ } else {
52081
+ for (const row of result.rows) printLine(`${row.state} ${row.bucket} ${row.name}${row.link ? ` ${row.link}` : ""}`);
52082
+ if (result.rows.length === 0) printLine(`${label}: no checks reported`);
52083
+ }
52084
+ process.exitCode = result.exitCode;
52085
+ }
52086
+ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
52087
+ const ghRun = overrides?.ghRunner ?? (async (args, opts) => (await execFileP("gh", args, { timeout: opts.timeoutMs, env: opts.env })).stdout);
52088
+ const restGhApi = overrides?.restGhApi ?? ((env) => ghApiWithEnv(env));
52089
+ const deps = { readRequired: readRequiredCheckRows, fetchSnapshot: fetchRestPrSnapshot, fetchRows: fetchHeadCheckRows, restGhApi };
52090
+ const dispatchGh = (spec, label) => (async () => {
52091
+ requireBrokerApp(label);
52092
+ try {
52093
+ process.exitCode = await runBrokerGh(ghRun, spec);
52094
+ } catch (e) {
52095
+ return fail(`${label}: ${e.message}`);
52096
+ }
52097
+ })();
52098
+ const dispatchRestChecks = (spec, label, json) => dispatchRestChecksRead(deps, resolveRepo2, label, spec, json);
52099
+ const repoOpt = (v) => parseBrokerRepo(v, "--repo");
52100
+ const idOpt = (what) => (v) => parseBrokerId(v, what);
52101
+ const intOpt = (flag, min, max) => (v) => String(parseBrokerInt(v, flag, min, max));
52102
+ const jsonOpt = (v) => parseBrokerJsonFields(v);
52103
+ const checksJsonOpt = (v) => parseBrokerJsonFields(v, GH_PR_CHECKS_JSON_FIELDS);
52104
+ const workflowOpt = (v) => parseBrokerWorkflow(v);
52105
+ const branchOpt = (v) => parseBrokerBranch(v);
52106
+ const searchOpt = (v) => parseBrokerSearch(v);
52107
+ const labelOpt = (v) => parseBrokerLabel(v);
52108
+ const runStatusOpt = (v) => parseBrokerRunStatus(v);
52109
+ const assigneeOpt = (v) => parseBrokerAssignee(v);
52110
+ const read = program3.command("read").description(`closed typed GitHub reads on the App pool (${BROKER_JSON_FLAGS_NOTE})`);
52111
+ const run = read.command("run").description("workflow-run reads (view/list/watch) \u2014 stdout and exit codes pass gh through verbatim");
52112
+ run.command("view").description("read a workflow run (attempt/exit-status/json; formatters refused)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--exit-status", "exit non-zero when the run failed").option("--attempt <n>", "the attempt number of the run (1-1000)", intOpt("--attempt", 1, 1e3)).argument("<id>", "workflow run number", idOpt("run id")).action(async (id, o) => {
52113
+ let spec;
52114
+ try {
52115
+ spec = buildRunViewSpec(id, o);
52116
+ } catch (e) {
52117
+ return fail(e.message);
52118
+ }
52119
+ await dispatchGh(spec, "read run view");
52120
+ });
52121
+ run.command("list").description("list workflow runs (workflow/branch/status/limit/json; formatters refused)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--workflow <name>", "filter by workflow name or file", workflowOpt).option("--branch <name>", "filter by branch", branchOpt).option("--status <status>", "filter by run status", runStatusOpt).option("--limit <n>", "maximum runs to fetch (1-100)", intOpt("--limit", 1, 100)).action(async (o) => {
52122
+ let spec;
52123
+ try {
52124
+ spec = buildRunListSpec(o);
52125
+ } catch (e) {
52126
+ return fail(e.message);
52127
+ }
52128
+ await dispatchGh(spec, "read run list");
52129
+ });
52130
+ run.command("watch").description("watch a run until it finishes (exit-status passthrough; --timeout bounds the watch \u2014 expiry fails explicit App-pool, refresh re-runs the same command)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--exit-status", "exit non-zero when the run fails").option("--compact", "show only relevant/failed steps").option("--interval <n>", "refresh interval in seconds, 60-300 (pool pacing floor; default 60)", intOpt("--interval", 60, 300)).option("--timeout <minutes>", "watch bound in minutes, 1-120 (default 30)", intOpt("--timeout", 1, 120), "30").argument("<id>", "workflow run number", idOpt("run id")).action(async (id, o) => {
52131
+ let spec;
52132
+ try {
52133
+ spec = buildRunWatchSpec(id, { repo: o.repo, exitStatus: o.exitStatus, compact: o.compact, interval: o.interval, timeoutMin: o.timeout });
52134
+ } catch (e) {
52135
+ return fail(e.message);
52136
+ }
52137
+ await dispatchGh(spec, "read run watch");
52138
+ });
52139
+ const pr = read.command("pr").description("pull-request reads \u2014 view rides gh; checks is REST-backed (deterministic closed-custody adapter; native was EMPTY only on the historical #3035 fixture)");
52140
+ pr.command("view").description("read a PR as gh JSON (comments/json; --json supports statusCheckRollup selection; formatters refused)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--comments", "include PR comments").argument("<number>", "PR number", idOpt("PR number")).action(async (number, o) => {
52141
+ let spec;
52142
+ try {
52143
+ spec = buildPrViewSpec(number, o);
52144
+ } catch (e) {
52145
+ return fail(e.message);
52146
+ }
52147
+ await dispatchGh(spec, "read pr view");
52148
+ });
52149
+ pr.command("checks").description(`PR checks as native --json rows (REST-backed; --required scopes to GitHub's required contexts, failed lookup errors instead of falling back; exit 0 pass, 1 fail, 8 pending; --watch/--fail-fast/--interval/--web have no route \u2014 unsupported by name)`).option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", `machine-readable rows (subset of: ${GH_PR_CHECKS_JSON_FIELDS.join(",")})`, checksJsonOpt).option("--required", "only checks GitHub requires for merge").argument("<number>", "PR number (explicit \u2014 current-branch inference is unsupported)", idOpt("PR number")).action(async (number, o) => {
52150
+ let spec;
52151
+ try {
52152
+ spec = buildPrChecksSpec(number, o);
52153
+ } catch (e) {
52154
+ return fail(e.message);
52155
+ }
52156
+ await dispatchRestChecks(spec, "read pr checks", o.json);
52157
+ });
52158
+ const issue = read.command("issue").description("issue reads (view/list) \u2014 stdout and exit codes pass gh through verbatim");
52159
+ issue.command("view").description("read an issue as gh JSON (comments/json; formatters refused)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--comments", "include issue comments").argument("<number>", "issue number", idOpt("issue number")).action(async (number, o) => {
52160
+ let spec;
52161
+ try {
52162
+ spec = buildIssueViewSpec(number, o);
52163
+ } catch (e) {
52164
+ return fail(e.message);
52165
+ }
52166
+ await dispatchGh(spec, "read issue view");
52167
+ });
52168
+ issue.command("list").description("list/search issues (search/assignee/label/state/limit/json; viewer pronouns refused \u2014 viewer-scoped reads stay personal)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--search <query>", "search issues", searchOpt).option("--assignee <login>", "filter by assignee (viewer pronouns refused)", assigneeOpt).option("--label <label>", "filter by label", labelOpt).addOption(new Option("--state <state>", "open|closed|all").choices(["open", "closed", "all"])).option("--limit <n>", "maximum issues to fetch (1-100)", intOpt("--limit", 1, 100)).action(async (o) => {
52169
+ let spec;
52170
+ try {
52171
+ spec = buildIssueListSpec(o);
52172
+ } catch (e) {
52173
+ return fail(e.message);
52174
+ }
52175
+ await dispatchGh(spec, "read issue list");
52176
+ });
52177
+ }
52178
+
51559
52179
  // src/api-read.ts
51560
52180
  init_cli_shared();
51561
52181
  var API_ACCEPT_HEADERS = {
@@ -51576,13 +52196,19 @@ var PULL_FILES_RE = new RegExp(`^repos/${NAME}/${NAME}/pulls/\\d+/files$`);
51576
52196
  var COMPARE_RE = new RegExp(`^repos/${NAME}/${NAME}/compare/${FREE}\\.\\.\\.${FREE}$`);
51577
52197
  var CONTENTS_BASE_RE = new RegExp(`^repos/${NAME}/${NAME}/contents/${FREE}$`);
51578
52198
  var CONTENTS_QUERY_RE = /^ref=[^&\s]+$/;
51579
- var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>]";
52199
+ var ACTIONS_RUNS_RE = new RegExp(`^repos/${NAME}/${NAME}/actions/runs$`);
52200
+ var ACTIONS_RUN_RE = new RegExp(`^repos/${NAME}/${NAME}/actions/runs/\\d+$`);
52201
+ var ACTIONS_JOBS_RE = new RegExp(`^repos/${NAME}/${NAME}/actions/runs/\\d+/jobs$`);
52202
+ var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>], repos/{owner}/{repo}/actions/runs, repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs";
51580
52203
  function parseApiGetPath(rawPath) {
51581
52204
  const path2 = rawPath.trim();
51582
52205
  const refuse = () => new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
51583
52206
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
51584
52207
  if (PULL_FILES_RE.test(path2)) return { kind: "pull-files", path: path2, paginate: true };
51585
52208
  if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
52209
+ if (ACTIONS_RUNS_RE.test(path2)) return { kind: "actions-runs", path: path2, paginate: true };
52210
+ if (ACTIONS_RUN_RE.test(path2)) return { kind: "actions-run", path: path2, paginate: false };
52211
+ if (ACTIONS_JOBS_RE.test(path2)) return { kind: "actions-jobs", path: path2, paginate: true };
51586
52212
  const q = path2.indexOf("?");
51587
52213
  const base = q === -1 ? path2 : path2.slice(0, q);
51588
52214
  const query = q === -1 ? "" : path2.slice(q + 1);
@@ -51602,14 +52228,21 @@ function buildApiGetArgs(path2, accept) {
51602
52228
  async function runApiGet(run, path2, accept) {
51603
52229
  return run(buildApiGetArgs(path2, accept), GIT_TIMEOUT_MS);
51604
52230
  }
51605
- function registerApiReadCommands(program3) {
51606
- const api = program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents)");
51607
- api.command("get <path>").description("GET one allowlisted REST path and print the response body to stdout (JSON, or raw file bytes with --accept raw); refuses everything outside the allowlist").option("--accept <type>", "response media type: json (default) or raw (file bytes)", "json").action(async (path2, o) => {
52231
+ function registerApiReadCommands(program3, overrides) {
52232
+ const spawn6 = overrides?.runner ?? (async (args, timeoutMs, env) => (await execFileP("gh", args, { timeout: timeoutMs, ...env ? { env } : {} })).stdout);
52233
+ const api = program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents, actions runs/jobs)");
52234
+ api.command("get").description("GET one allowlisted REST path and print the response body to stdout (JSON, or raw file bytes with --accept raw); refuses everything outside the allowlist").option("--accept <type>", "response media type: json (default) or raw (file bytes)", (v) => {
52235
+ acceptHeaderFor(v);
52236
+ return v.trim().toLowerCase();
52237
+ }, "json").argument("<path>", "allowlisted GET path (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, o) => {
52238
+ const actionsRoute = req.kind.startsWith("actions-");
52239
+ if (actionsRoute) requireBrokerApp("api get");
52240
+ const env = actionsRoute || process.env.MMI_ACTOR === "app" ? brokerChildEnv() : void 0;
51608
52241
  let body;
51609
52242
  try {
51610
52243
  body = await runApiGet(
51611
- async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout,
51612
- path2,
52244
+ async (args, timeoutMs) => spawn6(args, timeoutMs, env),
52245
+ req.path,
51613
52246
  o.accept
51614
52247
  );
51615
52248
  } catch (e) {
@@ -55329,6 +55962,10 @@ registerDiscoveryCommands(program2);
55329
55962
  registerExplainCommand(program2);
55330
55963
  registerPrLifecycleCommands(program2);
55331
55964
  registerApiReadCommands(program2);
55965
+ registerAppReadBrokerCommands(program2, async (repo) => requireRepo(repo));
55966
+ var prBrokerRoot = program2.commands.find((c) => c.name() === "pr");
55967
+ if (!prBrokerRoot) throw new Error("read broker: pr root must be registered before pr checks-required");
55968
+ registerPrChecksRequiredCommand(prBrokerRoot, async (repo) => requireRepo(repo));
55332
55969
  registerSessionReport(program2);
55333
55970
  registerLearningClosureRateCommand(program2);
55334
55971
  registerLearningPickupCommand(program2);
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.113",
3
+ "version": "4.5.115",
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.5.113",
3
+ "version": "4.5.115",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",