@mutmutco/claude-plugin 4.5.114 → 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.114",
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;
@@ -17713,10 +17939,10 @@ var rollout_plan_default = {
17713
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)."
17714
17940
  },
17715
17941
  baseline: {
17716
- version: "4.5.114",
17717
- tag: "v4.5.114",
17718
- commit: "d262796d0d3f",
17719
- npm: "@mutmutco/cli@4.5.114"
17942
+ version: "4.5.115",
17943
+ tag: "v4.5.115",
17944
+ commit: "8d0add650762",
17945
+ npm: "@mutmutco/cli@4.5.115"
17720
17946
  },
17721
17947
  exitCriterion: "fleet-n-of-n",
17722
17948
  hubOnlyShortcut: "forbidden",
@@ -17733,14 +17959,14 @@ var rollout_plan_default = {
17733
17959
  repo: "mutmutco/mmi-hub",
17734
17960
  role: "canary",
17735
17961
  schedule: "train",
17736
- v3Target: "v4.5.114"
17962
+ v3Target: "v4.5.115"
17737
17963
  }
17738
17964
  ],
17739
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.",
17740
17966
  rollback: {
17741
17967
  independent: true,
17742
- mechanism: "npm dist-tag latest -> 4.5.114 and redeploy the Hub Lambda from tag v4.5.114 (d262796d0d3f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17743
- v3Target: "v4.5.114 (@mutmutco/cli@4.5.114, tag commit d262796d0d3f \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)"
17744
17970
  }
17745
17971
  },
17746
17972
  {
@@ -29524,7 +29750,7 @@ var PRIMARY_GROUPS = [
29524
29750
  ["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
29525
29751
  ];
29526
29752
  var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin", "packages"]);
29527
- var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup"]);
29753
+ var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup", "read"]);
29528
29754
  var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "dist", "wave", "report", "skill-lesson", "closure-rate"]);
29529
29755
  var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
29530
29756
  var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
@@ -29564,6 +29790,7 @@ var COMMAND_OWNERSHIP = {
29564
29790
  stage: { module_owner: "cli/src/stage-commands.ts", consumer: "agent-workflow" },
29565
29791
  pr: { module_owner: "cli/src/pr-commands.ts", consumer: "agent-workflow" },
29566
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" },
29567
29794
  ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
29568
29795
  rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
29569
29796
  release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
@@ -43185,6 +43412,7 @@ var SERVICE_NAME_RE = /^[A-Za-z0-9_.-]+$/;
43185
43412
  var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
43186
43413
  var EXCLUDE_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
43187
43414
  var EXCLUDE_LABEL = "mmi.runtime-env.exclude";
43415
+ var KEYS_LABEL = "mmi.runtime-env.keys";
43188
43416
  var RUNTIME_ENV_OVERRIDE_RELPATH = "tmp/stage/runtime-env.override.yml";
43189
43417
  function labelsMarkRuntimeEnv(labels) {
43190
43418
  if (Array.isArray(labels)) {
@@ -43211,12 +43439,16 @@ function labelValue(labels, name) {
43211
43439
  if (labels && typeof labels === "object") return String(labels[name] ?? "");
43212
43440
  return "";
43213
43441
  }
43214
- 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) {
43215
43447
  if (!value.trim()) return [];
43216
43448
  return value.split(",").map((raw) => {
43217
43449
  const key = raw.trim();
43218
43450
  if (!EXCLUDE_KEY_RE.test(key)) {
43219
- 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_]*$)`);
43220
43452
  }
43221
43453
  return key;
43222
43454
  });
@@ -43239,6 +43471,23 @@ function runtimeEnvExcludes(composeConfigJson) {
43239
43471
  }
43240
43472
  return out;
43241
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
+ }
43242
43491
  function runtimeEnvMarkedServices(composeConfigJson) {
43243
43492
  let parsed;
43244
43493
  try {
@@ -43250,12 +43499,16 @@ function runtimeEnvMarkedServices(composeConfigJson) {
43250
43499
  if (!services || typeof services !== "object" || Array.isArray(services)) return [];
43251
43500
  return Object.entries(services).filter(([name, svc]) => SERVICE_NAME_RE.test(name) && labelsMarkRuntimeEnv(svc?.labels)).map(([name]) => name).sort();
43252
43501
  }
43253
- function renderRuntimeEnvOverride(services, keys, excludes = {}) {
43502
+ function renderRuntimeEnvOverride(services, keys, excludes = {}, selects = {}) {
43254
43503
  const safeServices = services.filter((s) => SERVICE_NAME_RE.test(s)).sort();
43255
43504
  const safeKeys = [...new Set(keys.filter((k) => ENV_KEY_RE.test(k)))].sort();
43256
43505
  const keysFor = (service) => {
43257
43506
  const skip = new Set(excludes[service] ?? []);
43258
- 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)));
43259
43512
  };
43260
43513
  const withKeys = safeServices.filter((s) => keysFor(s).length);
43261
43514
  if (!withKeys.length) return null;
@@ -43678,7 +43931,7 @@ async function prepareRuntimeEnvPassthrough(cwd, composeEnv, vaultEnvMerge) {
43678
43931
  } catch {
43679
43932
  return {};
43680
43933
  }
43681
- const body = renderRuntimeEnvOverride(runtimeEnvMarkedServices(configJson), keys, runtimeEnvExcludes(configJson));
43934
+ const body = renderRuntimeEnvOverride(runtimeEnvMarkedServices(configJson), keys, runtimeEnvExcludes(configJson), runtimeEnvSelects(configJson));
43682
43935
  if (!body) return {};
43683
43936
  const overridePath = (0, import_node_path38.join)(cwd, RUNTIME_ENV_OVERRIDE_RELPATH);
43684
43937
  (0, import_node_fs39.mkdirSync)((0, import_node_path38.join)(cwd, "tmp", "stage"), { recursive: true });
@@ -51577,6 +51830,352 @@ function registerOrgHealthQuery(program3, deps = defaultOrgHealthQueryDeps()) {
51577
51830
  });
51578
51831
  }
51579
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
+
51580
52179
  // src/api-read.ts
51581
52180
  init_cli_shared();
51582
52181
  var API_ACCEPT_HEADERS = {
@@ -51597,13 +52196,19 @@ var PULL_FILES_RE = new RegExp(`^repos/${NAME}/${NAME}/pulls/\\d+/files$`);
51597
52196
  var COMPARE_RE = new RegExp(`^repos/${NAME}/${NAME}/compare/${FREE}\\.\\.\\.${FREE}$`);
51598
52197
  var CONTENTS_BASE_RE = new RegExp(`^repos/${NAME}/${NAME}/contents/${FREE}$`);
51599
52198
  var CONTENTS_QUERY_RE = /^ref=[^&\s]+$/;
51600
- 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";
51601
52203
  function parseApiGetPath(rawPath) {
51602
52204
  const path2 = rawPath.trim();
51603
52205
  const refuse = () => new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
51604
52206
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
51605
52207
  if (PULL_FILES_RE.test(path2)) return { kind: "pull-files", path: path2, paginate: true };
51606
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 };
51607
52212
  const q = path2.indexOf("?");
51608
52213
  const base = q === -1 ? path2 : path2.slice(0, q);
51609
52214
  const query = q === -1 ? "" : path2.slice(q + 1);
@@ -51623,14 +52228,21 @@ function buildApiGetArgs(path2, accept) {
51623
52228
  async function runApiGet(run, path2, accept) {
51624
52229
  return run(buildApiGetArgs(path2, accept), GIT_TIMEOUT_MS);
51625
52230
  }
51626
- function registerApiReadCommands(program3) {
51627
- const api = program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents)");
51628
- 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;
51629
52241
  let body;
51630
52242
  try {
51631
52243
  body = await runApiGet(
51632
- async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout,
51633
- path2,
52244
+ async (args, timeoutMs) => spawn6(args, timeoutMs, env),
52245
+ req.path,
51634
52246
  o.accept
51635
52247
  );
51636
52248
  } catch (e) {
@@ -55350,6 +55962,10 @@ registerDiscoveryCommands(program2);
55350
55962
  registerExplainCommand(program2);
55351
55963
  registerPrLifecycleCommands(program2);
55352
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));
55353
55969
  registerSessionReport(program2);
55354
55970
  registerLearningClosureRateCommand(program2);
55355
55971
  registerLearningPickupCommand(program2);
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.114",
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.114",
3
+ "version": "4.5.115",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",