@mutmutco/codex-plugin 4.5.227 → 4.5.228

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/dist/main.cjs CHANGED
@@ -982,12 +982,14 @@ function roleFromToken(token) {
982
982
  function readCache(path2, apiUrl, now, githubTokenFingerprint) {
983
983
  try {
984
984
  const session = JSON.parse((0, import_node_fs3.readFileSync)(path2, "utf8"));
985
- if (!session.token || !session.expiresAt || session.apiUrl !== apiUrl) return null;
986
- if (session.githubTokenFingerprint !== githubTokenFingerprint) return null;
987
- if (new Date(session.expiresAt).getTime() <= now.getTime() + REFRESH_WINDOW_MS) return null;
988
- return { ...session, role: roleFromToken(session.token) };
985
+ if (!session.token || !session.expiresAt || session.apiUrl !== apiUrl) return { session: null, stale: false };
986
+ if (session.githubTokenFingerprint !== githubTokenFingerprint) {
987
+ return { session: null, stale: true, staleLogin: typeof session.login === "string" ? session.login : void 0 };
988
+ }
989
+ if (new Date(session.expiresAt).getTime() <= now.getTime() + REFRESH_WINDOW_MS) return { session: null, stale: false };
990
+ return { session: { ...session, role: roleFromToken(session.token) }, stale: false };
989
991
  } catch {
990
- return null;
992
+ return { session: null, stale: false };
991
993
  }
992
994
  }
993
995
  function writeCache(path2, session) {
@@ -1004,53 +1006,115 @@ function writeCache(path2, session) {
1004
1006
  } catch {
1005
1007
  }
1006
1008
  }
1007
- async function hubAuthSession(deps) {
1008
- if (!deps.baseUrl) return void 0;
1009
+ function lastHubAuthFailure() {
1010
+ return lastFailure;
1011
+ }
1012
+ function hubAuthFailureMessage(input) {
1013
+ const r = input.refusal;
1014
+ const said = r ? [
1015
+ r.error ? `Hub error: ${r.error}` : `Hub error: HTTP ${r.status ?? "unknown"}`,
1016
+ r.reason ? `reason: ${r.reason}` : "",
1017
+ r.hint ? `hint: ${r.hint}` : ""
1018
+ ].filter(Boolean).join(" ") : "";
1019
+ if (input.kind === "stale-session") {
1020
+ return `cached Hub session is stale: it was minted from a different GitHub token${input.staleLogin ? ` (cached login ${input.staleLogin})` : ""}, and the automatic re-mint was refused${said ? ` \u2014 ${said}` : ""}. Re-authenticate so ONE token source feeds both the cache and the mint (\`gh auth login\` / \`gh auth switch\`, or unset \`MMI_GH_PAT\` so the shell falls back to the gh keyring), then retry; \`mmi-cli whoami\` confirms the identity.`;
1021
+ }
1022
+ if (input.kind === "refused") {
1023
+ return `Hub refused this GitHub token for a Hub session${said ? ` \u2014 ${said}` : ""} (${MEMBERSHIP_FIX}).`;
1024
+ }
1025
+ return `Hub session exchange failed: ${(input.transient ?? said) || "no answer from the Hub"} \u2014 the Hub is unreachable or cold-started; retry, and \`mmi-cli doctor\` reports Hub reachability.`;
1026
+ }
1027
+ async function hubAuthSessionOutcome(deps) {
1028
+ if (!deps.baseUrl) return {};
1009
1029
  const apiUrl = normalizeBaseUrl(deps.baseUrl);
1010
1030
  const envToken = process.env.MMI_HUB_TOKEN;
1011
1031
  if (envToken) {
1012
- return { ...{ token: envToken }, expiresAt: new Date(Date.now() + 30 * 60 * 1e3).toISOString(), apiUrl, githubTokenFingerprint: "env-override" };
1032
+ lastFailure = void 0;
1033
+ return { session: { ...{ token: envToken }, expiresAt: new Date(Date.now() + 30 * 60 * 1e3).toISOString(), apiUrl, githubTokenFingerprint: "env-override" } };
1013
1034
  }
1014
1035
  const now = deps.now?.() ?? /* @__PURE__ */ new Date();
1015
1036
  const cachePath = deps.cachePath ?? defaultHubSessionCachePath();
1016
1037
  const ghToken = await deps.githubToken();
1017
- if (!ghToken) return void 0;
1018
- const githubTokenFingerprint = tokenFingerprint(ghToken);
1019
- const cached = readCache(cachePath, apiUrl, now, githubTokenFingerprint);
1020
- if (cached) return cached;
1021
- try {
1022
- const res = await fetchWithRetry(
1023
- deps.fetch ?? fetch,
1024
- `${apiUrl}/auth/session`,
1025
- { method: "POST", headers: { ...clientVersionHeaders(), Authorization: `Bearer ${ghToken}` } },
1026
- { attempts: EXCHANGE_ATTEMPTS, timeoutMs: EXCHANGE_TIMEOUT_MS }
1027
- );
1028
- if (!res.ok) return void 0;
1029
- const body = await res.json();
1030
- if (!body.token || !body.expiresAt) return void 0;
1031
- const session = {
1032
- token: body.token,
1033
- expiresAt: body.expiresAt,
1034
- login: typeof body.login === "string" ? body.login : void 0,
1035
- // #3513: from the SIGNED token, not the response body — one rule at every hop, so the cache
1036
- // read and the network read cannot disagree about what counts as an assertion.
1037
- role: roleFromToken(body.token),
1038
- apiUrl,
1039
- githubTokenFingerprint
1038
+ if (!ghToken) {
1039
+ lastFailure = {
1040
+ kind: "unreachable",
1041
+ message: "no GitHub token available for a Hub session \u2014 `gh auth login` (or set MMI_GH_PAT/GH_TOKEN), then retry."
1040
1042
  };
1043
+ return { failure: lastFailure };
1044
+ }
1045
+ const githubTokenFingerprint = tokenFingerprint(ghToken);
1046
+ const cache = readCache(cachePath, apiUrl, now, githubTokenFingerprint);
1047
+ if (cache.session) {
1048
+ lastFailure = void 0;
1049
+ return { session: cache.session };
1050
+ }
1051
+ return mintSession(deps, cachePath, apiUrl, ghToken, githubTokenFingerprint, cache);
1052
+ }
1053
+ async function mintSession(deps, cachePath, apiUrl, ghToken, githubTokenFingerprint, cache) {
1054
+ const attempts = cache.stale ? 2 : 1;
1055
+ let refusal;
1056
+ let transient;
1057
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
1041
1058
  try {
1042
- writeCache(cachePath, session);
1043
- } catch {
1059
+ const res = await fetchWithRetry(
1060
+ deps.fetch ?? fetch,
1061
+ `${apiUrl}/auth/session`,
1062
+ { method: "POST", headers: { ...clientVersionHeaders(), Authorization: `Bearer ${ghToken}` } },
1063
+ { attempts: EXCHANGE_ATTEMPTS, timeoutMs: EXCHANGE_TIMEOUT_MS }
1064
+ );
1065
+ if (!res.ok) {
1066
+ const body2 = await res.json().catch(() => ({}));
1067
+ refusal = {
1068
+ status: res.status,
1069
+ error: typeof body2.error === "string" ? body2.error : void 0,
1070
+ reason: typeof body2.reason === "string" ? body2.reason : void 0,
1071
+ hint: typeof body2.hint === "string" ? body2.hint : void 0
1072
+ };
1073
+ continue;
1074
+ }
1075
+ const body = await res.json();
1076
+ if (!body.token || !body.expiresAt) {
1077
+ refusal = { status: res.status, error: "the Hub returned no session token" };
1078
+ continue;
1079
+ }
1080
+ const session = {
1081
+ token: body.token,
1082
+ expiresAt: body.expiresAt,
1083
+ login: typeof body.login === "string" ? body.login : void 0,
1084
+ // #3513: from the SIGNED token, not the response body — one rule at every hop, so the cache
1085
+ // read and the network read cannot disagree about what counts as an assertion.
1086
+ role: roleFromToken(body.token),
1087
+ apiUrl,
1088
+ githubTokenFingerprint
1089
+ };
1090
+ try {
1091
+ writeCache(cachePath, session);
1092
+ } catch {
1093
+ }
1094
+ lastFailure = void 0;
1095
+ return { session };
1096
+ } catch (e) {
1097
+ transient = e?.message || "network error";
1044
1098
  }
1045
- return session;
1046
- } catch {
1047
- return void 0;
1048
1099
  }
1100
+ const kind = cache.stale ? "stale-session" : refusal ? "refused" : "unreachable";
1101
+ const failure = {
1102
+ kind,
1103
+ message: hubAuthFailureMessage({ kind, staleLogin: cache.staleLogin, refusal, transient }),
1104
+ ...refusal?.reason ? { reason: refusal.reason } : {},
1105
+ ...refusal?.hint ? { hint: refusal.hint } : {},
1106
+ ...kind === "stale-session" ? { exitCode: EXIT_HUB_SESSION_STALE } : kind === "refused" ? { exitCode: EXIT_HUB_SESSION_REFUSED } : {}
1107
+ };
1108
+ lastFailure = failure;
1109
+ return { failure };
1110
+ }
1111
+ async function hubAuthSession(deps) {
1112
+ return (await hubAuthSessionOutcome(deps)).session;
1049
1113
  }
1050
1114
  async function hubAuthToken(deps) {
1051
1115
  return (await hubAuthSession(deps))?.token;
1052
1116
  }
1053
- var import_node_crypto, import_node_fs3, import_node_path3, import_node_os2, REFRESH_WINDOW_MS, EXCHANGE_TIMEOUT_MS, EXCHANGE_ATTEMPTS;
1117
+ var import_node_crypto, import_node_fs3, import_node_path3, import_node_os2, REFRESH_WINDOW_MS, EXCHANGE_TIMEOUT_MS, EXCHANGE_ATTEMPTS, EXIT_HUB_SESSION_STALE, EXIT_HUB_SESSION_REFUSED, lastFailure, MEMBERSHIP_FIX;
1054
1118
  var init_hub_auth = __esm({
1055
1119
  "src/hub-auth.ts"() {
1056
1120
  "use strict";
@@ -1063,6 +1127,9 @@ var init_hub_auth = __esm({
1063
1127
  REFRESH_WINDOW_MS = 10 * 60 * 1e3;
1064
1128
  EXCHANGE_TIMEOUT_MS = 8e3;
1065
1129
  EXCHANGE_ATTEMPTS = 2;
1130
+ EXIT_HUB_SESSION_STALE = 76;
1131
+ EXIT_HUB_SESSION_REFUSED = 77;
1132
+ MEMBERSHIP_FIX = "the token needs Organization \u2192 Members: read (`read:org`) \u2014 use a classic PAT or an App carrying that permission, or `gh auth switch` to a login that has it; then `mmi-cli whoami` confirms the identity";
1066
1133
  }
1067
1134
  });
1068
1135
 
@@ -9359,12 +9426,22 @@ var isWin = process.platform === "win32";
9359
9426
  var MMI_PLUGIN_ID = "mmi@mutmutco";
9360
9427
  var LEGACY_MMI_MARKETPLACE = "mmi";
9361
9428
  var CODEX_MARKETPLACE = "mutmutco";
9362
- function detectSurface(env) {
9429
+ function insideOpenBotAgentWorkspace(cwd, env = process.env, home = (0, import_node_os4.homedir)()) {
9430
+ let dir = cwd;
9431
+ try {
9432
+ dir ??= process.cwd();
9433
+ } catch {
9434
+ return false;
9435
+ }
9436
+ const rel = (0, import_node_path4.relative)(openbotAgentsRoot(env, home), dir);
9437
+ return !(0, import_node_path4.isAbsolute)(rel) && (rel.split(/[\\/]/)[0] ?? "").toLowerCase().startsWith("agent-");
9438
+ }
9439
+ function detectSurface(env, cwd) {
9363
9440
  const has = (k) => Boolean(env[k]?.trim());
9441
+ if (env.MMI_AGENT_SURFACE === "openbot" || insideOpenBotAgentWorkspace(cwd, env)) return "openbot";
9364
9442
  if (env.MMI_AGENT_SURFACE === "codex" || has("CODEX_THREAD_ID") || has("CODEX_MANAGED_BY_NPM") || has("CODEX_MANAGED_PACKAGE_ROOT") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) {
9365
9443
  return "codex";
9366
9444
  }
9367
- if (env.MMI_AGENT_SURFACE === "hermes" || has("HERMES_SESSION_ID") || has("HERMES_PLUGIN_ROOT")) return "hermes";
9368
9445
  if (env.MMI_AGENT_SURFACE === "jervcode" || has("PI_SESSION_ID")) {
9369
9446
  return "jervcode";
9370
9447
  }
@@ -9384,8 +9461,8 @@ function surfaceToken(surface) {
9384
9461
  return "codex";
9385
9462
  case "jervcode":
9386
9463
  return "jervcode";
9387
- case "hermes":
9388
- return "hermes";
9464
+ case "openbot":
9465
+ return "openbot";
9389
9466
  case "opencode":
9390
9467
  return "opencode";
9391
9468
  case "shell":
@@ -9403,8 +9480,8 @@ function reloadAction(surface) {
9403
9480
  return "restart OpenCode";
9404
9481
  case "jervcode":
9405
9482
  return "restart JervCode (the package set is read at launch)";
9406
- case "hermes":
9407
- return "start a fresh Hermes Agent process";
9483
+ case "openbot":
9484
+ return "start a fresh OpenBot agent run";
9408
9485
  case "claude-cli":
9409
9486
  case "shell":
9410
9487
  default:
@@ -9444,7 +9521,7 @@ function nonClaudeSurfaceHealMessage(surface) {
9444
9521
  if (surface === "codex") {
9445
9522
  return "Codex ships an MMI plugin (#3563). Install or repair it with:\n codex plugin marketplace add mutmutco/MMI-Hub && codex plugin add mmi@mutmutco\n Update the CLI too: npm i -g @mutmutco/cli";
9446
9523
  }
9447
- return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, JervCode, and Hermes only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
9524
+ return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, JervCode, and OpenBot only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
9448
9525
  }
9449
9526
  function healStepAborts(step, ok) {
9450
9527
  return !ok && step.gated;
@@ -9487,14 +9564,20 @@ function runHostBin(bin, args, opts) {
9487
9564
  const argv = isWin ? ["/c", bin, ...args] : args;
9488
9565
  return step ? execFileHard(file, argv, { ...shared, step }) : execFileP(file, argv, shared);
9489
9566
  }
9490
- function hermesConfigRoot(env = process.env, home = (0, import_node_os4.homedir)(), platform2 = process.platform) {
9491
- if (env.HERMES_HOME?.trim()) return env.HERMES_HOME.trim();
9492
- if (platform2 === "win32") return (0, import_node_path4.join)(env.LOCALAPPDATA?.trim() || (0, import_node_path4.join)(home, "AppData", "Local"), "hermes");
9493
- return (0, import_node_path4.join)(home, ".hermes");
9567
+ function openbotAgentsRoot(env = process.env, home = (0, import_node_os4.homedir)()) {
9568
+ return env.MMI_UPDATER_OPENBOT_AGENTS_DIR?.trim() || (0, import_node_path4.join)(home, "OpenBot", "Agents");
9569
+ }
9570
+ function openbotAgentWorkspaces(env = process.env, home = (0, import_node_os4.homedir)()) {
9571
+ const root = openbotAgentsRoot(env, home);
9572
+ try {
9573
+ return (0, import_node_fs7.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("agent-")).map((entry) => (0, import_node_path4.join)(root, entry.name)).sort();
9574
+ } catch {
9575
+ return [];
9576
+ }
9494
9577
  }
9495
9578
  function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os4.homedir)()) {
9496
9579
  if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path4.join)(home, ".codex");
9497
- if (surface === "hermes") return hermesConfigRoot(env, home);
9580
+ if (surface === "openbot") return openbotAgentsRoot(env, home);
9498
9581
  if (surface === "jervcode") {
9499
9582
  return env.JERVCODE_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path4.join)(home, ".jerv", "agent");
9500
9583
  }
@@ -9518,7 +9601,7 @@ function marketplaceCloneCandidates(surface, home, env = process.env) {
9518
9601
  (0, import_node_path4.join)(root, "plugins", "marketplaces", CODEX_MARKETPLACE)
9519
9602
  ];
9520
9603
  }
9521
- if (surface === "hermes") return [];
9604
+ if (surface === "openbot") return [];
9522
9605
  if (surface === "jervcode") return [];
9523
9606
  return [(0, import_node_path4.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
9524
9607
  }
@@ -9628,13 +9711,26 @@ function mmiPiPackageHealthy(env = process.env, home = (0, import_node_os4.homed
9628
9711
  function piDoctorEnv(agentDir) {
9629
9712
  return { ...process.env, PI_CODING_AGENT_DIR: agentDir };
9630
9713
  }
9631
- function hermesPluginRoot(env = process.env, home = (0, import_node_os4.homedir)()) {
9632
- return (0, import_node_path4.join)(hermesConfigRoot(env, home), "plugins", "mmi");
9714
+ function openbotManagedRecord(env = process.env, home = (0, import_node_os4.homedir)()) {
9715
+ for (const workspace of openbotAgentWorkspaces(env, home)) {
9716
+ const path2 = (0, import_node_path4.join)(workspace, ".agents", "skills", ".mmi-managed.json");
9717
+ if (!(0, import_node_fs7.existsSync)(path2)) continue;
9718
+ try {
9719
+ const record2 = JSON.parse((0, import_node_fs7.readFileSync)(path2, "utf8"));
9720
+ return { workspace, ...typeof record2.version === "string" ? { version: record2.version } : {} };
9721
+ } catch {
9722
+ return { workspace };
9723
+ }
9724
+ }
9725
+ return null;
9633
9726
  }
9634
- function hermesPluginTreeHealthy(root, exists = import_node_fs7.existsSync) {
9635
- if (!["plugin.yaml", "__init__.py"].every((file) => exists((0, import_node_path4.join)(root, file)))) return false;
9727
+ function openbotWorkspaceSkillsHealthy(env = process.env, home = (0, import_node_os4.homedir)()) {
9728
+ const record2 = openbotManagedRecord(env, home);
9729
+ if (!record2) return false;
9636
9730
  try {
9637
- return (0, import_node_fs7.statSync)((0, import_node_path4.join)(root, "skills")).isDirectory();
9731
+ const recordFile = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path4.join)(record2.workspace, ".agents", "skills", ".mmi-managed.json"), "utf8"));
9732
+ const owned = Array.isArray(recordFile.skills) ? recordFile.skills : [];
9733
+ return owned.length > 0 && owned.every((name) => typeof name === "string" && (0, import_node_fs7.existsSync)((0, import_node_path4.join)(record2.workspace, ".agents", "skills", name, "SKILL.md")));
9638
9734
  } catch {
9639
9735
  return false;
9640
9736
  }
@@ -9649,10 +9745,11 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
9649
9745
  // Codex's native projection is authoritative; its legacy installed_plugins.json may be empty
9650
9746
  // while `plugin list --json` proves the managed plugin installed (#6111).
9651
9747
  installRecordPresent: surface === "codex" ? codexStatus?.installed === true : hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()) || // #4188: jervcode's install record is the settings-file packages[] entry itself.
9652
- surface === "jervcode" && piEntry !== null || surface === "hermes" && (0, import_node_fs7.existsSync)(hermesPluginRoot()),
9748
+ surface === "jervcode" && piEntry !== null || // #9130: OpenBot's install record is the workspace ownership file MMI itself writes.
9749
+ surface === "openbot" && openbotManagedRecord() !== null,
9653
9750
  // JervCode's settings entry and native local surfaces have no marketplace clone.
9654
- marketplaceClonePresent: surface === "jervcode" || surface === "hermes" ? true : marketplaceClonePresent(surface, (0, import_node_os4.homedir)()),
9655
- pluginCachePresent: surface === "jervcode" ? mmiPiPackageHealthy() : surface === "hermes" ? hermesPluginTreeHealthy(hermesPluginRoot()) : surface === "codex" ? Boolean(
9751
+ marketplaceClonePresent: surface === "jervcode" || surface === "openbot" ? true : marketplaceClonePresent(surface, (0, import_node_os4.homedir)()),
9752
+ pluginCachePresent: surface === "jervcode" ? mmiPiPackageHealthy() : surface === "openbot" ? openbotWorkspaceSkillsHealthy() : surface === "codex" ? Boolean(
9656
9753
  codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0, import_node_fs7.existsSync)((0, import_node_path4.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", codexStatus.version))
9657
9754
  ) : (0, import_node_fs7.existsSync)((0, import_node_path4.join)(root, "plugins", "cache", "mutmutco", "mmi"))
9658
9755
  };
@@ -9951,7 +10048,6 @@ var SESSION_ID_ENV_VARS = [
9951
10048
  "CLAUDE_CODE_SESSION_ID",
9952
10049
  "CODEX_SESSION_ID",
9953
10050
  "CODEX_THREAD_ID",
9954
- "HERMES_SESSION_ID",
9955
10051
  "PI_SESSION_ID",
9956
10052
  // #6193: lane runners spawn the CLI through the host's extension shell operations, which strip
9957
10053
  // the PI_* session variables; the JervCode launcher's durable lane id (JERV_SESSION_ID) is what
@@ -9960,6 +10056,9 @@ var SESSION_ID_ENV_VARS = [
9960
10056
  // synth/shell identity and the claim guard contests the caller's OWN lane (the #3414/#3438
9961
10057
  // refusals of jerv_worktree_land's `devops pr create` from the claimed task worktree).
9962
10058
  "JERV_SESSION_ID"
10059
+ // #9130: OpenBot exports no session variable of its own (its surface comes from the agent-workspace
10060
+ // cwd). Merv's codex runtime carries CODEX_THREAD_ID, honoured above; otherwise an OpenBot claim's
10061
+ // session is the per-process `synth-openbot-…` fallback — it proves the surface, never one lane.
9963
10062
  ];
9964
10063
  var SYNTH_SESSION_PREFIX = "synth-";
9965
10064
  function isHostSessionId(session) {
@@ -18738,6 +18837,7 @@ function shouldWithdrawSurfaceFlag(result) {
18738
18837
 
18739
18838
  // src/secrets-transport.ts
18740
18839
  init_client_version();
18840
+ init_hub_auth();
18741
18841
  var OWNER = "mutmutco";
18742
18842
  var SSM_ROOT = "/mmi-future";
18743
18843
  var ORG_INFRA_SLUG = "_org";
@@ -18899,6 +18999,17 @@ function errorDetail(body) {
18899
18999
  const error = typeof body.error === "string" ? body.error : "";
18900
19000
  return error ? `: ${error}` : "";
18901
19001
  }
19002
+ function hubAuthErrorDetail(body) {
19003
+ const parts = [errorDetail(body)];
19004
+ if (safeServerReason(body.reason)) parts.push(` (reason: ${String(body.reason)})`);
19005
+ if (safeServerReason(body.hint)) parts.push(` \u2014 ${String(body.hint)}`);
19006
+ return parts.join("");
19007
+ }
19008
+ function refusedLine(prefix, res, body) {
19009
+ const local = lastHubAuthFailure();
19010
+ if (res.status === 401 && local) return local.message;
19011
+ return `${prefix}: HTTP ${res.status}${hubAuthErrorDetail(body)}`;
19012
+ }
18902
19013
  async function upgradeMessage(res, body) {
18903
19014
  if (res.status !== 426) return null;
18904
19015
  return upgradeRequiredError(res, body ?? await readJsonBody(res));
@@ -18947,7 +19058,7 @@ async function secretsList(deps, opts) {
18947
19058
  return;
18948
19059
  }
18949
19060
  if (!res.ok) {
18950
- deps.err(await upgradeMessage(res) ?? `secrets list failed: HTTP ${res.status}${await readErr(res)}`);
19061
+ deps.err(await upgradeMessage(res) ?? refusedLine("secrets list failed", res, await readJsonBody(res)));
18951
19062
  return;
18952
19063
  }
18953
19064
  const { secrets } = await res.json();
@@ -19332,6 +19443,10 @@ function missingRequiredKeys(stage, required, present, declaredSecrets) {
19332
19443
  }
19333
19444
  async function secretsPreflight(deps, opts) {
19334
19445
  const repo = await targetRepo(deps, opts);
19446
+ if (opts.required.length === 0 && !opts.json) {
19447
+ deps.log(`0 required ${opts.stage} secret names`);
19448
+ return true;
19449
+ }
19335
19450
  const qs = new URLSearchParams({ repo }).toString();
19336
19451
  let res;
19337
19452
  try {
@@ -20268,7 +20383,7 @@ async function secretsRequest(deps, key, opts) {
20268
20383
  });
20269
20384
  const body = await readJsonBody(res);
20270
20385
  if (!res.ok) {
20271
- deps.err(await upgradeMessage(res, body) ?? `secrets request failed: HTTP ${res.status}${errorDetail(body)}`);
20386
+ deps.err(await upgradeMessage(res, body) ?? refusedLine("secrets request failed", res, body));
20272
20387
  return false;
20273
20388
  }
20274
20389
  if (opts.json) {
@@ -20965,7 +21080,7 @@ async function fetchSecretForUse(deps, { repo, key, slug }) {
20965
21080
  return null;
20966
21081
  }
20967
21082
  deps.err(
20968
- await upgradeMessage(res, body) ?? (res.status === 403 ? `secrets use: not authorized for ${key} (HTTP 403) \u2014 ask the master for a \`secrets grant\`${errorDetail(body)}` : `secrets use failed: HTTP ${res.status}${errorDetail(body)}`)
21083
+ await upgradeMessage(res, body) ?? (res.status === 403 ? `secrets use: not authorized for ${key} (HTTP 403) \u2014 ask the master for a \`secrets grant\`${errorDetail(body)}` : refusedLine("secrets use failed", res, body))
20969
21084
  );
20970
21085
  return null;
20971
21086
  }
@@ -24973,6 +25088,7 @@ function createDocsIndexDeps(repoRoot2) {
24973
25088
  // src/gate-budget.ts
24974
25089
  var BLESSED_RUN_WITH_BUDGET_SHA = "5ce541add569e54e7ca96de67a768577d3d604e4";
24975
25090
  var BLESSED_RUNNER_GATE_SHA = "a19e9392e5ad9b0675f02e4304d68dd933fad60f";
25091
+ var BLESSED_KIT_AGENT_FILES_SHA = "a8b273ed418e703c0aa501a53ea87228ba28b1f0";
24976
25092
  var BLESSED_RUNBOOK_GUARD_SHA = "e661e0aedc0e58b73c738bf6cdfeac65e5914584";
24977
25093
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
24978
25094
  var LOCAL_USE = "./.github/actions/run-with-budget";
@@ -25348,6 +25464,9 @@ function gateSeedVars(cls, releaseTrack, runtime = "node", requiredCheckBranches
25348
25464
  // The pin is the CLI's blessed SHA, not an operator knob — one central place to bump (#3178).
25349
25465
  GATE_BUDGET_SHA: BLESSED_RUN_WITH_BUDGET_SHA,
25350
25466
  GATE_RUNNER_SHA: BLESSED_RUNNER_GATE_SHA,
25467
+ // #9161: the suite-only kit-file drift action arrived after the shared runner pin, so it carries its own
25468
+ // blessed revision — bumping GATE_RUNNER_SHA for it would move every generic repo's toolchain too.
25469
+ KIT_AGENT_FILES_SHA: BLESSED_KIT_AGENT_FILES_SHA,
25351
25470
  // #8129: the guard and its bundled CLI must come from the same immutable Hub revision.
25352
25471
  GATE_RUNBOOK_SHA: BLESSED_RUNBOOK_GUARD_SHA,
25353
25472
  // #7407: repo-declared service containers and extra check steps; empty renders today's gate byte-for-byte.
@@ -27091,10 +27210,10 @@ var rollout_plan_default = {
27091
27210
  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)."
27092
27211
  },
27093
27212
  baseline: {
27094
- version: "4.5.227",
27095
- tag: "v4.5.227",
27096
- commit: "9498ff4da322",
27097
- npm: "@mutmutco/cli@4.5.227"
27213
+ version: "4.5.228",
27214
+ tag: "v4.5.228",
27215
+ commit: "097f7c9ad0e0",
27216
+ npm: "@mutmutco/cli@4.5.228"
27098
27217
  },
27099
27218
  exitCriterion: "fleet-n-of-n",
27100
27219
  hubOnlyShortcut: "forbidden",
@@ -27111,14 +27230,14 @@ var rollout_plan_default = {
27111
27230
  repo: "mutmutco/mmi-hub",
27112
27231
  role: "canary",
27113
27232
  schedule: "train",
27114
- v3Target: "v4.5.227"
27233
+ v3Target: "v4.5.228"
27115
27234
  }
27116
27235
  ],
27117
27236
  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.",
27118
27237
  rollback: {
27119
27238
  independent: true,
27120
- mechanism: "npm dist-tag latest -> 4.5.227 and redeploy the Hub Lambda from tag v4.5.227 (9498ff4da322); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27121
- v3Target: "v4.5.227 (@mutmutco/cli@4.5.227, tag commit 9498ff4da322 \u2014 last known-good release carrying the repo-index v4-only contract)"
27239
+ mechanism: "npm dist-tag latest -> 4.5.228 and redeploy the Hub Lambda from tag v4.5.228 (097f7c9ad0e0); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27240
+ v3Target: "v4.5.228 (@mutmutco/cli@4.5.228, tag commit 097f7c9ad0e0 \u2014 last known-good release carrying the repo-index v4-only contract)"
27122
27241
  }
27123
27242
  },
27124
27243
  {
@@ -29275,7 +29394,8 @@ async function buildTrainApplyContext(deps) {
29275
29394
  repo,
29276
29395
  owner,
29277
29396
  slug: name.toLowerCase(),
29278
- login
29397
+ login,
29398
+ ...verdict.authenticatedDelegation === true ? { delegatedActor: login } : {}
29279
29399
  };
29280
29400
  }
29281
29401
  function commandAuthorityLabel(owner) {
@@ -32052,57 +32172,24 @@ function installedClaudePluginVersion() {
32052
32172
  return void 0;
32053
32173
  }
32054
32174
  }
32055
- function readHermesPluginEvidence(env = process.env) {
32056
- const host = hermesConfigRoot(env);
32057
- const root = hermesPluginRoot(env);
32058
- const installRecordPresent = (0, import_node_fs25.existsSync)(root);
32059
- const manifestPath = (0, import_node_path22.join)(root, "plugin.yaml");
32060
- let installedVersion;
32175
+ function readOpenBotPluginEvidence(env = process.env) {
32176
+ const workspaces = openbotAgentWorkspaces(env);
32177
+ const record2 = openbotManagedRecord(env);
32178
+ const recordPath = record2 ? (0, import_node_path22.join)(record2.workspace, ".agents", "skills", ".mmi-managed.json") : void 0;
32061
32179
  let manifest = "missing";
32062
- try {
32063
- const text = (0, import_node_fs25.readFileSync)(manifestPath, "utf8");
32064
- let version;
32065
- try {
32066
- const parsed = JSON.parse(text).version;
32067
- if (typeof parsed === "string" && parsed.trim()) version = parsed.trim();
32068
- } catch {
32069
- }
32070
- if (!version) {
32071
- const match = /^version:\s*["']?([^\s"'#]+)["']?\s*(?:#.*)?$/m.exec(text);
32072
- version = match?.[1];
32073
- }
32074
- if (version) {
32075
- installedVersion = version;
32076
- manifest = "valid";
32077
- } else if ((0, import_node_fs25.existsSync)(manifestPath)) manifest = "invalid";
32078
- } catch {
32079
- if ((0, import_node_fs25.existsSync)(manifestPath)) manifest = "invalid";
32080
- }
32081
- let skills = false;
32082
- try {
32083
- skills = (0, import_node_fs25.existsSync)((0, import_node_path22.join)(root, "skills")) && (0, import_node_fs25.statSync)((0, import_node_path22.join)(root, "skills")).isDirectory();
32084
- } catch {
32085
- }
32086
- let provisioned = false;
32087
- const provisionedRoot = (0, import_node_path22.join)(host, "skills", "mmi");
32088
- try {
32089
- provisioned = (0, import_node_fs25.existsSync)(provisionedRoot) && (0, import_node_fs25.statSync)(provisionedRoot).isDirectory();
32090
- } catch {
32091
- }
32092
- const treeOk = (0, import_node_fs25.existsSync)((0, import_node_path22.join)(root, "__init__.py")) && skills && manifest === "valid";
32093
- const receipt = treeOk && !provisioned ? `hermes-skills-unprovisioned:${provisionedRoot}` : void 0;
32180
+ if (recordPath) manifest = record2?.version ? "valid" : "invalid";
32094
32181
  return {
32095
- hostPresent: (0, import_node_fs25.existsSync)(host),
32096
- installRecordPresent,
32182
+ hostPresent: workspaces.length > 0,
32183
+ installRecordPresent: record2 !== null,
32097
32184
  manifest,
32098
- payloadPresent: treeOk && provisioned,
32099
- ...installedVersion ? { installedVersion } : {},
32100
- ...receipt ? { receipt } : {}
32185
+ payloadPresent: record2 !== null && openbotWorkspaceSkillsHealthy(env),
32186
+ ...record2?.version ? { installedVersion: record2.version } : {},
32187
+ ...recordPath && !record2?.version ? { receipt: `openbot-ownership-unreadable:${recordPath}` } : {}
32101
32188
  };
32102
32189
  }
32103
32190
  function installedSurfacePluginVersion(surface) {
32104
32191
  const token = surfaceToken(surface);
32105
- if (token === "hermes") return readHermesPluginEvidence().installedVersion;
32192
+ if (token === "openbot") return readOpenBotPluginEvidence().installedVersion;
32106
32193
  if (token === "jervcode") {
32107
32194
  return installedJervCodePackageVersion();
32108
32195
  }
@@ -34301,6 +34388,22 @@ function supersedeablePublishLegShape(rec) {
34301
34388
  function deferredGatewayDeployLegShape(rec) {
34302
34389
  return rec.state === "pending" && rec.runId == null && rec.workflow === "jerv-gateway";
34303
34390
  }
34391
+ function releaseAssetsCarryInstallerPayload(assets, tag) {
34392
+ const version = tag.replace(/^v/, "");
34393
+ return assets.some((asset) => typeof asset.name === "string" && asset.name.startsWith("installer-payload-") && asset.name.endsWith(`-${version}.tar.gz`));
34394
+ }
34395
+ async function proveInstallerPayloadPresence(deps, prior, label, ref) {
34396
+ let assets;
34397
+ try {
34398
+ const parsed = JSON.parse(await deps.run("gh", ["release", "view", prior.tag, "--repo", prior.repo, "--json", "assets"]));
34399
+ assets = Array.isArray(parsed.assets) ? parsed.assets : [];
34400
+ } catch (e) {
34401
+ throw new Error(
34402
+ `${label}: could not read the ${prior.tag} GitHub Release asset list (${e instanceof Error ? e.message.split("\n")[0] : String(e)}) \u2014 whether the installer payload exists is unproven, so the deploy leg cannot be closed; retry when GitHub answers; nothing was written (${ref})`
34403
+ );
34404
+ }
34405
+ return releaseAssetsCarryInstallerPayload(assets, prior.tag) ? "present" : "absent";
34406
+ }
34304
34407
  async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref, fence) {
34305
34408
  let liveSha = "";
34306
34409
  try {
@@ -34386,23 +34489,29 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
34386
34489
  };
34387
34490
  }
34388
34491
  if (deferredGateway) {
34389
- const dispatch = await appendJervGatewayReleaseDeploy(
34390
- deps,
34391
- prior.repo,
34392
- prior.tag,
34393
- prior.tagSha,
34394
- { note: `Jerv Gateway operator-host deploy leg for ${prior.tag} run during the publish supersession`, deployStatus: "success" }
34395
- );
34396
- phases.deploy = {
34397
- ...phaseEntry(deployPhaseInput("registry-publish", dispatch, { releaseSha: prior.tagSha })),
34398
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
34399
- };
34400
- if (dispatch.deployStatus !== "success") {
34401
- throw new Error(
34402
- `release --supersede-publish: the deferred ${prior.tag} Jerv Gateway operator-host deploy leg did not verify (${dispatch.note}) \u2014 run \`scripts/jerv-gateway-release-deploy.sh --tag ${prior.tag}\` to a healthy receipt naming the released tag and commit, then retry; nothing was written (#6742)`
34492
+ if (prior.phases.publish.state === "failed" && await proveInstallerPayloadPresence(deps, prior, "release --supersede-publish", "#9182") === "absent") {
34493
+ const payloadNote = `superseded: the ${prior.tag} publish never produced the installer payload (the live release carries no installer-payload asset for this version) \u2014 the deferred Jerv Gateway operator-host deploy leg was skipped, nothing was deployable from this tag; the cause fix rides the next patch train (#9182)`;
34494
+ phases.deploy = { state: "skipped", sha: prior.tagSha, superseded: true, note: payloadNote, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
34495
+ gatewayDeploy = { workflow: "jerv-gateway", state: "skipped", note: payloadNote };
34496
+ } else {
34497
+ const dispatch = await appendJervGatewayReleaseDeploy(
34498
+ deps,
34499
+ prior.repo,
34500
+ prior.tag,
34501
+ prior.tagSha,
34502
+ { note: `Jerv Gateway operator-host deploy leg for ${prior.tag} run during the publish supersession`, deployStatus: "success" }
34403
34503
  );
34504
+ phases.deploy = {
34505
+ ...phaseEntry(deployPhaseInput("registry-publish", dispatch, { releaseSha: prior.tagSha })),
34506
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
34507
+ };
34508
+ if (dispatch.deployStatus !== "success") {
34509
+ throw new Error(
34510
+ `release --supersede-publish: the deferred ${prior.tag} Jerv Gateway operator-host deploy leg did not verify (${dispatch.note}) \u2014 run \`scripts/jerv-gateway-release-deploy.sh --tag ${prior.tag}\` to a healthy receipt naming the released tag and commit, then retry; nothing was written (#6742)`
34511
+ );
34512
+ }
34513
+ gatewayDeploy = { workflow: "jerv-gateway", state: "complete", note: dispatch.note };
34404
34514
  }
34405
- gatewayDeploy = { workflow: "jerv-gateway", state: "complete", note: dispatch.note };
34406
34515
  }
34407
34516
  const note = `superseded: content-caused publish failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, #6638)`;
34408
34517
  await fence(() => atomicWriteJson(path2, {
@@ -34486,7 +34595,11 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
34486
34595
  }
34487
34596
  let deployEntry;
34488
34597
  let gatewayDeploy;
34489
- if (deferredGateway) {
34598
+ if (deferredGateway && publish.state === "failed" && await proveInstallerPayloadPresence(deps, prior, label, "#9182") === "absent") {
34599
+ const payloadNote = `superseded: the ${prior.tag} publish never produced the installer payload (the live release carries no installer-payload asset for this version) \u2014 the deferred Jerv Gateway operator-host deploy leg was skipped, nothing was deployable from this tag; the cause fix rides the next patch train (#9182)`;
34600
+ deployEntry = { state: "skipped", sha: prior.tagSha, superseded: true, note: payloadNote, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
34601
+ gatewayDeploy = { workflow: "jerv-gateway", state: "skipped", note: payloadNote };
34602
+ } else if (deferredGateway) {
34490
34603
  const dispatch = await appendJervGatewayReleaseDeploy(
34491
34604
  deps,
34492
34605
  prior.repo,
@@ -37404,7 +37517,15 @@ async function runReleaseAbortLocked(deps, options) {
37404
37517
  );
37405
37518
  }
37406
37519
  await requireCleanTree(deps);
37407
- if (await currentBranch(deps) !== "main") {
37520
+ const ciCheckout = options.ciCheckout === true;
37521
+ if (ciCheckout) {
37522
+ if (!isHubControlRepo(ctx.repo)) {
37523
+ throw new Error(`release --abort: the CI checkout path aborts ${ctx.repo} never; it is for the Hub release workflow only; nothing was written`);
37524
+ }
37525
+ if (!ctx.delegatedActor) {
37526
+ throw new Error("release --abort: the CI checkout path needs the run's proven org-owner trigger (hub release workflow authority); nothing was written");
37527
+ }
37528
+ } else if (await currentBranch(deps) !== "main") {
37408
37529
  throw new Error("release --abort must run from the primary checkout left on main by the failed train; nothing was written");
37409
37530
  }
37410
37531
  await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
@@ -37414,7 +37535,7 @@ async function runReleaseAbortLocked(deps, options) {
37414
37535
  const tagSha = await probeRemoteTag(deps, tag);
37415
37536
  if (!tagSha) throw new Error(`release --abort: ${tag} is not on origin; nothing was written`);
37416
37537
  const localTagSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
37417
- const localMainSha = clean2(await deps.run("git", ["rev-parse", "main"]));
37538
+ const localMainSha = ciCheckout ? tagSha : clean2(await deps.run("git", ["rev-parse", "main"]));
37418
37539
  const originMainSha = clean2(await deps.run("git", ["rev-parse", "origin/main"]));
37419
37540
  if (localTagSha !== tagSha || localMainSha !== tagSha) {
37420
37541
  throw new Error(
@@ -37445,6 +37566,17 @@ async function runReleaseAbortLocked(deps, options) {
37445
37566
  throw new Error(`release --abort: origin still reports ${tag} after deletion; local state was preserved`);
37446
37567
  }
37447
37568
  await deps.run("git", ["tag", "-d", tag]);
37569
+ if (ciCheckout) {
37570
+ const ciLedgerNote = await clearReleaseLedger(process.cwd(), { repo: ctx.repo, tag, tagSha });
37571
+ return {
37572
+ command: "release-abort",
37573
+ repo: ctx.repo,
37574
+ tag,
37575
+ tagSha,
37576
+ restoredMainSha: originMainSha,
37577
+ note: `removed the unpublished ${tag} candidate from the Hub release workflow; destructive approval: @${ctx.delegatedActor} triggered this run and is a proven active org owner; origin/main stays ${originMainSha.slice(0, 12)}; repair development before recutting` + (ciLedgerNote ? `; ${ciLedgerNote}` : "")
37578
+ };
37579
+ }
37448
37580
  await deps.run("git", ["checkout", "development"]);
37449
37581
  await ffOnlyPull(deps, "development");
37450
37582
  await deps.run("git", ["branch", "-f", "main", "origin/main"]);
@@ -40786,7 +40918,7 @@ function registerBoardCommands(program3) {
40786
40918
  return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
40787
40919
  }
40788
40920
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
40789
- board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--snapshot-only", "never fall back to your personal GraphQL pool; failure exits: 20 hub-unreachable, 21 app-quota-exhausted, 22 refused. With --json, stdout is {ok:false,outcome,reason[,resetAt,pool]} (pool is rest or graphql when known); success is unchanged").option("--min-cursor <n>", "board-event cursor (from `oracle board events`) the Hub snapshot must postdate; a cached snapshot older than that event is rebuilt (#7240)").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
40921
+ board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--snapshot-only", "never fall back to your personal GraphQL pool; failure exits: 20 hub-unreachable, 21 app-quota-exhausted, 22 refused. With --json, stdout is {ok:false,outcome,reason[,resetAt,pool]} (pool is rest or graphql when known) and the human-readable reason goes to stderr; success is unchanged").option("--min-cursor <n>", "board-event cursor (from `oracle board events`) the Hub snapshot must postdate; a cached snapshot older than that event is rebuilt (#7240)").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n").action((o) => runBoardRead(o));
40790
40922
  board.command("events").description("content-free board/CI change records after a cursor, from the Hub event feed (no GitHub call) \u2014 for real-time sidebars (#7235)").option("--since <cursor>", "cursor from the previous answer; omit on first read (answers the head cursor with resync:true)").option("--wait <seconds>", "hold up to this long (max 120), polling every 2s, and return on the first event or resync", "0").option("--repo <owner/repo>", "repo whose Hub config to use (defaults to git origin)").option("--json", "machine-readable output (already the default)").addHelpText("after", "\nPrints {cursor, events, resync, more[, hidden]}. Pass the returned cursor as the next --since.\nresync:true means records were missed (retention ~1h, or a gap): re-read the board with\n`mmi-cli oracle board read --snapshot-only --json`.\n").action(async (o) => {
40791
40923
  try {
40792
40924
  if (o.since !== void 0 && !/^\d{1,15}$/.test(o.since)) throw new Error("--since must be a non-negative integer cursor");
@@ -43006,7 +43138,7 @@ function registerSecretsCommands(program3) {
43006
43138
  }
43007
43139
  const { keys, command, ...o } = parsed.value;
43008
43140
  const code = secretsUseExitCode(await secretsUse(d, keys, { ...o, command }));
43009
- if (code !== void 0) process.exitCode = code;
43141
+ if (code !== void 0) process.exitCode = lastHubAuthFailure()?.exitCode ?? code;
43010
43142
  });
43011
43143
  });
43012
43144
  withDeclaredOptions(
@@ -51246,6 +51378,61 @@ function prMergeQueueLine(result) {
51246
51378
  // src/command-register-train-operations.ts
51247
51379
  init_github_client();
51248
51380
 
51381
+ // src/hub-release-ci-authority.ts
51382
+ var HUB_RELEASE_REPO = "mutmutco/MMI-Hub";
51383
+ var HUB_RELEASE_WORKFLOW = ".github/workflows/hub-release.yml";
51384
+ var HUB_RELEASE_REF = "refs/heads/development";
51385
+ var WORKFLOW_REF = `${HUB_RELEASE_REPO}/${HUB_RELEASE_WORKFLOW}@${HUB_RELEASE_REF}`;
51386
+ var LOGIN = /^[A-Za-z0-9-]{1,39}$/;
51387
+ var ID = /^[1-9][0-9]*$/;
51388
+ function hubReleaseCiRun(env) {
51389
+ if (env.MMI_HUB_RELEASE_CI !== "1" || env.GITHUB_ACTIONS !== "true") return null;
51390
+ if (env.GITHUB_REPOSITORY !== HUB_RELEASE_REPO || env.GITHUB_EVENT_NAME !== "workflow_dispatch") return null;
51391
+ if (env.GITHUB_WORKFLOW_REF !== WORKFLOW_REF || env.GITHUB_REF !== HUB_RELEASE_REF) return null;
51392
+ const runId = env.GITHUB_RUN_ID ?? "";
51393
+ const runAttempt = env.GITHUB_RUN_ATTEMPT ?? "";
51394
+ if (!ID.test(runId) || !ID.test(runAttempt)) return null;
51395
+ return { runId: Number(runId), runAttempt: Number(runAttempt) };
51396
+ }
51397
+ async function hubReleaseCiAuthority(repo, deps) {
51398
+ const run = hubReleaseCiRun(deps.env);
51399
+ if (!run) return null;
51400
+ const refuse = (check) => ({ ok: false, error: `hub release workflow authority refused: ${check}` });
51401
+ if (repo.toLowerCase() !== HUB_RELEASE_REPO.toLowerCase()) return refuse(`this workflow releases ${HUB_RELEASE_REPO} only`);
51402
+ let live;
51403
+ try {
51404
+ live = await deps.getJson(`repos/${HUB_RELEASE_REPO}/actions/runs/${run.runId}`);
51405
+ } catch (e) {
51406
+ return refuse(`live run read failed (${e instanceof Error ? e.message : String(e)})`);
51407
+ }
51408
+ const login = live?.triggering_actor?.login ?? "";
51409
+ const bound = live?.id === run.runId && live.run_attempt === run.runAttempt && live.status === "in_progress" && live.event === "workflow_dispatch" && live.path === HUB_RELEASE_WORKFLOW && live.head_branch === "development" && live.repository?.full_name === HUB_RELEASE_REPO;
51410
+ if (!bound) return refuse("live run binding");
51411
+ if (live.triggering_actor?.type !== "User" || !LOGIN.test(login)) return refuse("the run was not triggered by a person");
51412
+ let membership;
51413
+ try {
51414
+ membership = await deps.getJson(`orgs/mutmutco/memberships/${login}`);
51415
+ } catch (e) {
51416
+ return refuse(`org membership read for ${login} failed (${e instanceof Error ? e.message : String(e)})`);
51417
+ }
51418
+ if (membership?.role !== "admin" || membership?.state !== "active") return refuse(`${login} is not an active org owner`);
51419
+ return { ok: true, role: "master", train: true, authenticatedDelegation: true, delegatedActor: login };
51420
+ }
51421
+ var GITHUB_API = "https://api.github.com";
51422
+ function pinnedGitHubGetJson(token, fetchImpl = fetch) {
51423
+ return async (path2) => {
51424
+ if (!token) throw new Error("GH_TOKEN is not set in this run");
51425
+ if (!/^[A-Za-z0-9_.\/-]+$/.test(path2) || path2.includes("..")) throw new Error("refusing an unexpected API path");
51426
+ const res = await fetchImpl(`${GITHUB_API}/${path2}`, {
51427
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" },
51428
+ signal: AbortSignal.timeout(15e3)
51429
+ });
51430
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
51431
+ return res.json();
51432
+ };
51433
+ }
51434
+ var HUB_RELEASE_CI_ANNOUNCE_DEFERRED = "announcement deferred: the Hub cannot authorize a release-workflow session to post yet (#9138); the curated summary is in this run's step summary";
51435
+
51249
51436
  // src/project-info-sync.ts
51250
51437
  var import_node_fs38 = require("node:fs");
51251
51438
  var import_node_path36 = require("node:path");
@@ -56862,21 +57049,21 @@ ${list}`);
56862
57049
  // #8456: a pinned land has no update lane at all.
56863
57050
  ...o.expectHead ? {} : { updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)) },
56864
57051
  pollMerged: async (prNumber, repo, deadlineMs) => {
56865
- let lastFailure;
57052
+ let lastFailure2;
56866
57053
  while (Date.now() < deadlineMs) {
56867
57054
  const read = await pollRestPrMerged(prNumber, repo);
56868
57055
  if (read.state === "merged") return true;
56869
57056
  if (read.state === "failed") {
56870
- lastFailure = read.error;
57057
+ lastFailure2 = read.error;
56871
57058
  console.warn(`pr land: merged-state read FAILED (${read.error}) \u2014 retrying; this is not evidence the PR is unmerged`);
56872
57059
  } else {
56873
- lastFailure = void 0;
57060
+ lastFailure2 = void 0;
56874
57061
  }
56875
57062
  await new Promise((resolve7) => setTimeout(resolve7, PR_LAND_POLL_MS));
56876
57063
  }
56877
- if (lastFailure) {
57064
+ if (lastFailure2) {
56878
57065
  throw new Error(
56879
- `pr land: could not verify whether PR #${prNumber} merged \u2014 the last merged-state read FAILED (${lastFailure}). The auto-merge enqueue stands; read the PR directly (gh pr view ${prNumber} --repo ${repo} --json merged,state) and rerun cleanup if it merged.`
57066
+ `pr land: could not verify whether PR #${prNumber} merged \u2014 the last merged-state read FAILED (${lastFailure2}). The auto-merge enqueue stands; read the PR directly (gh pr view ${prNumber} --repo ${repo} --json merged,state) and rerun cleanup if it merged.`
56880
57067
  );
56881
57068
  }
56882
57069
  return false;
@@ -57539,6 +57726,8 @@ function trainApplyDeps() {
57539
57726
  // parses, so the ONE self-spawn seam derives the prefix through the same rule the refusal names.
57540
57727
  runSelf: async (args) => (await execFileP(process.execPath, [process.argv[1], ...canonicalArgvFor(args)], { timeout: 3e4 })).stdout,
57541
57728
  trainAuthority: async (repo) => {
57729
+ const ci2 = await hubReleaseCiAuthority(repo, { env: process.env, getJson: pinnedGitHubGetJson(process.env.GH_TOKEN) });
57730
+ if (ci2) return ci2;
57542
57731
  const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
57543
57732
  return verdict.ok ? {
57544
57733
  ok: true,
@@ -57609,6 +57798,7 @@ function trainApplyDeps() {
57609
57798
  return (await execFileP(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS, ...env ? { env } : {} })).stdout;
57610
57799
  },
57611
57800
  postAnnouncement: async (payload) => {
57801
+ if (hubReleaseCiRun(process.env)) throw new Error(HUB_RELEASE_CI_ANNOUNCE_DEFERRED);
57612
57802
  const res = await releaseAnnouncement(payload, registryClientDeps(await loadConfig()));
57613
57803
  const body = res.body;
57614
57804
  if (!res.ok) throw new Error(body?.error ?? `Hub announcement failed (HTTP ${res.status}); verify delivery before retrying`);
@@ -57692,11 +57882,13 @@ function renderReleaseSupersedeDeploy(r) {
57692
57882
  }
57693
57883
  function renderReleaseSupersedePublish(r) {
57694
57884
  const base = `mmi-cli devops release --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
57695
- return r.gatewayDeploy ? `${base}; the deferred Jerv Gateway operator-host deploy leg was run and verified during the supersession (jerv-gateway=complete): ${r.gatewayDeploy.note}` : base;
57885
+ if (!r.gatewayDeploy) return base;
57886
+ return r.gatewayDeploy.state === "skipped" ? `${base}; the deferred Jerv Gateway operator-host deploy leg was SKIPPED because the failed publish never produced an installer payload (jerv-gateway=skipped, superseded): ${r.gatewayDeploy.note}` : `${base}; the deferred Jerv Gateway operator-host deploy leg was run and verified during the supersession (jerv-gateway=complete): ${r.gatewayDeploy.note}`;
57696
57887
  }
57697
57888
  function renderReleaseSupersedeDeployAndPublish(r) {
57698
57889
  const base = `mmi-cli devops release --supersede-deploy --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
57699
- return r.gatewayDeploy ? `${base}; the deferred Jerv Gateway operator-host deploy leg was run and verified during the closure (jerv-gateway=complete): ${r.gatewayDeploy.note}` : base;
57890
+ if (!r.gatewayDeploy) return base;
57891
+ return r.gatewayDeploy.state === "skipped" ? `${base}; the deferred Jerv Gateway operator-host deploy leg was SKIPPED because the failed publish never produced an installer payload (jerv-gateway=skipped, superseded): ${r.gatewayDeploy.note}` : `${base}; the deferred Jerv Gateway operator-host deploy leg was run and verified during the closure (jerv-gateway=complete): ${r.gatewayDeploy.note}`;
57700
57892
  }
57701
57893
  function renderReleasePublishRetry(r) {
57702
57894
  return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
@@ -58118,7 +58310,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
58118
58310
  if (!guard.ok) return fail(`release: ${guard.message}`);
58119
58311
  }
58120
58312
  try {
58121
- const result = await runReleaseAbort(trainApplyDeps(), { approved: true });
58313
+ const result = await runReleaseAbort(trainApplyDeps(), { approved: true, ciCheckout: hubReleaseCiRun(process.env) !== null });
58122
58314
  return emitTrainResult("release --abort", o.json ? JSON.stringify(result, null, 2) : renderReleaseAbort(result), o.out);
58123
58315
  } catch (e) {
58124
58316
  return failGraceful(`release --abort: ${e.message}`);
@@ -60582,29 +60774,23 @@ var surfaces_default = {
60582
60774
  ]
60583
60775
  },
60584
60776
  {
60585
- token: "hermes",
60586
- displayName: "Hermes Agent",
60777
+ token: "openbot",
60778
+ displayName: "OpenBot",
60587
60779
  lifecycle: "active",
60588
60780
  artifactIds: [
60589
- "mmi-hermes-plugin",
60781
+ "mmi-openbot-plugin",
60590
60782
  "mmi-skills",
60591
60783
  "mmi-cli"
60592
60784
  ],
60593
60785
  plannedArtifactIds: [],
60594
60786
  assembly: {
60595
- mode: "package-directory",
60596
- rootPath: "packages/hermes-plugin",
60597
- sync: [
60598
- {
60599
- mode: "directories",
60600
- sourcePath: "skills",
60601
- targetPath: "packages/hermes-plugin/skills"
60602
- }
60603
- ]
60787
+ mode: "source-tree",
60788
+ rootPath: ".pi-plugin",
60789
+ sync: []
60604
60790
  },
60605
60791
  install: {
60606
60792
  mechanism: "npm",
60607
- locator: "@mutmutco/hermes-plugin (npm transport; MMI-Hub extracts the package into $HERMES_HOME/plugins/mmi)"
60793
+ locator: "@mutmutco/pi-plugin (npm transport; MMI-Hub extracts its skills into each OpenBot agent workspace at <home>/OpenBot/Agents/agent-<uuid>/.agents/skills)"
60608
60794
  },
60609
60795
  upgrade: {
60610
60796
  mechanism: "package-manager",
@@ -60613,10 +60799,10 @@ var surfaces_default = {
60613
60799
  skills: {
60614
60800
  delivery: "provisioned",
60615
60801
  sourceSurfaceId: "mmi-skills",
60616
- artifactId: "mmi-hermes-plugin",
60802
+ artifactId: "mmi-skills",
60617
60803
  invocation: {
60618
- entry: "/epic",
60619
- any: "skill_view(name='<skill>')"
60804
+ entry: "use the epic skill",
60805
+ any: "use the <skill> skill"
60620
60806
  }
60621
60807
  },
60622
60808
  cli: {
@@ -60629,20 +60815,19 @@ var surfaces_default = {
60629
60815
  repair: "mmi-hub"
60630
60816
  },
60631
60817
  certification: {
60632
- hostCommand: "hermes",
60633
- versionArgs: [
60634
- "--version"
60635
- ]
60818
+ hostProbe: "none",
60819
+ hostCommand: null,
60820
+ versionArgs: []
60636
60821
  },
60637
60822
  compatibility: {
60638
- hostVersion: "0.20.1",
60639
- certification: "Hermes Agent v0.20.1 release v2026.8.13, upstream f80f453ae0679347e38abc917c7f94f717bf96c5 (#5053)"
60823
+ hostVersion: "0.53.2",
60824
+ certification: "OpenBot desktop 0.53.2 ships no host CLI: surface presence is the agent workspace root (<home>/OpenBot/Agents/agent-*), never a --version probe"
60640
60825
  },
60641
60826
  enforcementCeilings: [
60642
- "MMI ships no Hermes hooks (#5908): the plugin contributes skills and the /mmi command only; enforcement is host-side.",
60643
- "Hermes user plugins are opt-in through plugins.enabled (#6656): after a verified converge MMI-Hub enables its OWN mmi key through Hermes' own `hermes plugins enable mmi --no-allow-tool-override` command, and never reads, reorders, or writes another operator entry. Built-in tool override is explicitly declined, since MMI ships no Hermes hooks. Host process isolation remains Hermes-owned.",
60644
- "npm is MMI transport only: MMI-Hub extracts @mutmutco/hermes-plugin into $HERMES_HOME/plugins/mmi and provisions the canonical skills into $HERMES_HOME/skills/mmi/; a fresh Hermes Agent process is required for discovery.",
60645
- "Every Hermes home is covered per home (#6672): a profile under $HERMES_HOME/profiles/<name> is a full sibling home and Hermes scans only the active home's plugins/, so MMI-Hub repeats the same single-home install, skills provisioning, and host-command enable in the resolved home and in each profile home with HERMES_HOME bound to it. No profile is enumerated for any other purpose, and nothing outside those per-home MMI paths is read or written."
60827
+ "OpenBot ships no MMI hooks: this surface contributes the provisioned skills and a standalone mmi-cli only; enforcement is host-side.",
60828
+ "OpenBot's agent registry (%APPDATA%/OpenBot/openbot.db) is event-sourced and host-owned: MMI-Hub never reads or writes it, and enumerates agent workspaces by directory instead.",
60829
+ "OpenBot agent shells inherit the user PATH, so the standalone mmi-cli resolves; MMI provisions the skill payload, never the shell environment. OpenBot exports no identifying variable: mmi-cli recognizes an OpenBot session by a working directory inside <home>/OpenBot/Agents/agent-<uuid> (or an explicit MMI_AGENT_SURFACE=openbot), and a session working elsewhere is classified by its runtime.",
60830
+ "Every Agent workspace holds its own skills tree: MMI-Hub writes only <workspace>/.agents/skills/<skill> folders it recorded in .agents/skills/.mmi-managed.json, and never .claude/skills, an unrecorded folder, or a skill it does not own."
60646
60831
  ]
60647
60832
  }
60648
60833
  ],
@@ -61006,41 +61191,36 @@ var surfaces_default = {
61006
61191
  publishVisibility: "public"
61007
61192
  },
61008
61193
  {
61009
- id: "mmi-hermes-plugin",
61194
+ id: "mmi-openbot-plugin",
61010
61195
  classification: "packaging",
61011
61196
  kind: "plugin",
61012
- ownerPath: "packages/hermes-plugin/package.json",
61013
- deliveryPath: "packages/hermes-plugin/package.json",
61197
+ ownerPath: ".pi-plugin/package.json",
61198
+ deliveryPath: ".pi-plugin/package.json",
61014
61199
  delivery: "npm",
61015
- applicability: "Hermes Agent native directory plugin transported as npm package @mutmutco/hermes-plugin; MMI-Hub extracts its package bytes into $HERMES_HOME/plugins/mmi, where Hermes v0.20.1 discovers it as an enabled user plugin.",
61200
+ applicability: "OpenBot desktop agent workspaces; the SAME npm package as JervCode (@mutmutco/pi-plugin, surfaceToken jervcode) re-declared as OpenBot's transport so the registry can name a token-owned plugin artifact for a host that has no CLI of its own. No OpenBot-specific package exists or is planned.",
61016
61201
  versionCoordinated: true,
61017
- surfaceToken: "hermes",
61202
+ surfaceToken: "openbot",
61018
61203
  versionPaths: [
61019
61204
  {
61020
- path: "packages/hermes-plugin/package.json",
61021
- pointer: "version"
61022
- },
61023
- {
61024
- path: "packages/hermes-plugin/plugin.yaml",
61205
+ path: ".pi-plugin/package.json",
61025
61206
  pointer: "version"
61026
61207
  }
61027
61208
  ],
61028
61209
  additionalPaths: [
61029
- "packages/hermes-plugin/plugin.yaml",
61030
- "packages/hermes-plugin/__init__.py",
61031
- "packages/hermes-plugin/skills"
61210
+ ".pi-plugin/skills"
61032
61211
  ],
61033
61212
  artifactIdentity: {
61034
61213
  kind: "npm-pack",
61035
- packagePath: "packages/hermes-plugin"
61214
+ packagePath: ".pi-plugin"
61036
61215
  },
61037
- publishVisibility: "public"
61216
+ publishVisibility: "n/a"
61038
61217
  }
61039
61218
  ]
61040
61219
  };
61041
61220
 
61042
61221
  // src/surface-doctor.ts
61043
- var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "jervcode", "hermes"]);
61222
+ var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "jervcode", "openbot"]);
61223
+ var FRESH_PROCESS_SURFACES = /* @__PURE__ */ new Set(["openbot"]);
61044
61224
  function isActiveToken(value) {
61045
61225
  return TOKENS.has(value);
61046
61226
  }
@@ -61091,7 +61271,7 @@ function diagnoseSurface(evidence) {
61091
61271
  return { ...base, state: "clean" };
61092
61272
  }
61093
61273
  function reloadInstruction(descriptor) {
61094
- if (descriptor.token === "hermes") return "start a fresh Hermes Agent process";
61274
+ if (descriptor.token === "openbot") return "start a fresh OpenBot agent run";
61095
61275
  const verb = descriptor.reload === "workspace" ? "reload" : "restart";
61096
61276
  return `${verb} ${descriptor.displayName}`;
61097
61277
  }
@@ -61103,7 +61283,7 @@ function planSurfaceRepair(diagnosis) {
61103
61283
  supported: false,
61104
61284
  owner: "mmi-hub",
61105
61285
  changes: [],
61106
- // #5833: name the SURFACE this row is about — the literal "Hermes plugin" sent every other
61286
+ // #5833: name the SURFACE this row is about — a hard-coded host name sent every other
61107
61287
  // mmi-hub-owned surface (Claude Code included) chasing a different host that was already current.
61108
61288
  instruction: `run \`mmi-hub update\` to converge the MMI-managed ${descriptor.displayName} plugin, then ${reloadInstruction(descriptor)}`
61109
61289
  };
@@ -61131,9 +61311,9 @@ function buildSurfaceDoctorCheck(diagnosis) {
61131
61311
  const detailByState = {
61132
61312
  skipped: "skipped \u2014 host not active",
61133
61313
  clean: diagnosis.repairDetail ? `clean \u2014 repaired and verified (${diagnosis.repairDetail})` : `clean${versions ? ` \u2014 ${versions}` : ""}`,
61134
- "pending-reload": `pending ${descriptor.token === "hermes" ? "fresh process" : descriptor.reload === "workspace" ? "reload" : "restart"} \u2014 repaired to ${diagnosis.releasedVersion}, but ${diagnosis.installedVersion} is still the version this host has loaded${diagnosis.repairDetail ? ` (${diagnosis.repairDetail})` : ""}`,
61314
+ "pending-reload": `pending ${FRESH_PROCESS_SURFACES.has(descriptor.token) ? "fresh process" : descriptor.reload === "workspace" ? "reload" : "restart"} \u2014 repaired to ${diagnosis.releasedVersion}, but ${diagnosis.installedVersion} is still the version this host has loaded${diagnosis.repairDetail ? ` (${diagnosis.repairDetail})` : ""}`,
61135
61315
  // #5833: the update already staged the released tree; the wording mirrors `mmi-hub status`.
61136
- staged: `loaded ${diagnosis.installedVersion}; staged ${diagnosis.releasedVersion} for next launch \u2014 ${descriptor.token === "hermes" ? "start a fresh process" : descriptor.reload === "workspace" ? "reload" : "restart"} ${descriptor.displayName} to converge`,
61316
+ staged: `loaded ${diagnosis.installedVersion}; staged ${diagnosis.releasedVersion} for next launch \u2014 ${FRESH_PROCESS_SURFACES.has(descriptor.token) ? "start a fresh process" : descriptor.reload === "workspace" ? "reload" : "restart"} ${descriptor.displayName} to converge`,
61137
61317
  "freshness-unknown": `${diagnosis.installedVersion ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
61138
61318
  stale: `stale${versions ? ` \u2014 ${versions}` : ""}`,
61139
61319
  missing: "missing \u2014 no install record",
@@ -62679,8 +62859,17 @@ function buildRunWatchSpec(id, o) {
62679
62859
  const timeoutMin = o.timeoutMin !== void 0 ? parseBrokerInt(o.timeoutMin, "--timeout", 1, 120) : 30;
62680
62860
  return { program: "gh", args, timeoutMs: timeoutMin * 6e4 };
62681
62861
  }
62862
+ function rejectPrViewQueueFields(fields, number) {
62863
+ if (!fields || !fields.split(",").includes("mergeQueueEntry")) return;
62864
+ throw new Error(
62865
+ `read pr view: 'mergeQueueEntry' is not a gh pr view --json field \u2014 read the merge queue with \`mmi-cli devops read pr queue ${number} --repo <owner/repo> --json\` and drop it from --json`
62866
+ );
62867
+ }
62682
62868
  function buildPrViewSpec(ref, o) {
62683
- const args = withJson(withRepo(["pr", "view", parseBrokerId(ref, "PR number")], parseBrokerRepo(o.repo, "--repo")), parseBrokerJsonFields(o.json), o.json);
62869
+ const number = parseBrokerId(ref, "PR number");
62870
+ const jsonFields = parseBrokerJsonFields(o.json);
62871
+ rejectPrViewQueueFields(jsonFields, number);
62872
+ const args = withJson(withRepo(["pr", "view", number], parseBrokerRepo(o.repo, "--repo")), jsonFields, o.json);
62684
62873
  if (o.comments) args.push("--comments");
62685
62874
  return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
62686
62875
  }
@@ -62753,6 +62942,20 @@ function ghExitCodeOf(error) {
62753
62942
  const code = error.code;
62754
62943
  return typeof code === "number" ? code : null;
62755
62944
  }
62945
+ function brokerGhFailureHint(failureText, args) {
62946
+ const target = args[0] === "pr" && args[1] === "view" ? args[2] : void 0;
62947
+ const unknownField = /Unknown JSON field: "([^"]+)"/.exec(failureText)?.[1] ?? /Unknown JSON field: ([A-Za-z][A-Za-z0-9]*)/.exec(failureText)?.[1];
62948
+ if (unknownField === "mergeQueueEntry") {
62949
+ return `'mergeQueueEntry' is not a gh pr view --json field \u2014 read the merge queue with \`mmi-cli devops read pr queue ${target ?? "<number>"} --repo <owner/repo> --json\` and drop it from --json; repeating this read cannot succeed`;
62950
+ }
62951
+ if (unknownField) {
62952
+ return `gh rejected --json field '${unknownField}' \u2014 drop it or use the typed read whose fields cover it (the "Available fields" list gh printed is its own vocabulary)`;
62953
+ }
62954
+ if (/Could not resolve to a PullRequest/i.test(failureText)) {
62955
+ return `GitHub could not resolve the PR${target ? ` #${target}` : ""} \u2014 verify the number and that --repo names the repo that owns it before retrying; an identical re-run fails the same way`;
62956
+ }
62957
+ return void 0;
62958
+ }
62756
62959
  async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk)) {
62757
62960
  const env = brokerChildEnv();
62758
62961
  let output;
@@ -62764,7 +62967,9 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
62764
62967
  const code = ghExitCodeOf(e);
62765
62968
  if (code !== null) {
62766
62969
  if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
62767
- process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} failed on the App pool (exit ${code}) \u2014 inspect stderr; retry the same typed read and only parse stdout after success
62970
+ const failure = `${typeof err.stdout === "string" ? err.stdout : ""}${typeof err.stderr === "string" ? err.stderr : ""}`;
62971
+ const recovery = brokerGhFailureHint(failure, spec.args) ?? "inspect stderr; retry the same typed read and only parse stdout after success";
62972
+ process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} failed on the App pool (exit ${code}) \u2014 ${recovery}
62768
62973
  `);
62769
62974
  return code;
62770
62975
  }
@@ -63068,11 +63273,33 @@ var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$
63068
63273
  var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
63069
63274
  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[?per_page=1-100&page=<n>&status=&branch=&event=&head_sha=<40-hex>&created=<date-range>&actor=, each key at most once], repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs, repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs, repos/{owner}/{repo}/actions/runs/{id}/artifacts, repos/{owner}/{repo}/actions/artifacts/{id}/zip (needs --out <file>), repos/{owner}/{repo}/actions/jobs/{id}/logs (needs --out <file>), repos/{owner}/{repo}/releases[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/releases/latest, repos/{owner}/{repo}/releases/tags/{tag}, repos/{owner}/{repo}/releases/assets/{id} (needs --out <file>)";
63070
63275
  var API_GET_RECOVERY = " No other GET read exists \u2014 do not retry path variants; use a dedicated mmi-cli subcommand or ask a human token holder. This refusal exits 1 on stderr with nothing on stdout: check the exit code and never merge stderr into stdout (2>&1) before JSON-parsing.";
63276
+ var TYPED_READ_ROUTE_RE = new RegExp(`^repos/(${NAME})/(${NAME})/(issues|pulls)(?:/(comments/\\d+|\\d+)(?:/([a-z_]+))?)?$`);
63277
+ function typedReadHint(path2) {
63278
+ const match = TYPED_READ_ROUTE_RE.exec(path2.split("?")[0]);
63279
+ if (!match) return "";
63280
+ const [, owner, repo, kind, item, sub] = match;
63281
+ const slug = `${owner}/${repo}`;
63282
+ const say = (command, note = "") => ` Typed read for this route: \`${command}\`${note}.`;
63283
+ if (kind === "issues") {
63284
+ if (!item) return say(`mmi-cli devops read issue list --repo ${slug} --json number,title,state,labels`);
63285
+ if (item.startsWith("comments/")) {
63286
+ const id = item.slice("comments/".length);
63287
+ return say(`mmi-cli devops read issue view <issue> --repo ${slug} --json comments`, `; a comment is read through its issue, so match the comment whose url ends in issuecomment-${id}`);
63288
+ }
63289
+ if (sub === "events" || sub === "timeline") {
63290
+ return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json state,stateReason,closedAt,labels,assignees,comments`, "; the raw event/timeline history itself has no read");
63291
+ }
63292
+ return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json title,body,state,labels,comments`);
63293
+ }
63294
+ if (!item) return say(`mmi-cli devops read pr list --repo ${slug} --json number,title,state,headRefName`);
63295
+ if (item.startsWith("comments/")) return say(`mmi-cli devops read pr view <pr> --repo ${slug} --json comments,reviews`);
63296
+ return say(`mmi-cli devops read pr view ${item} --repo ${slug} --json title,state,comments,reviews,commits,statusCheckRollup`);
63297
+ }
63071
63298
  function parseApiGetPath(rawPath) {
63072
63299
  const path2 = rawPath.trim();
63073
63300
  const refuse = () => {
63074
63301
  const hint = /^[A-Za-z]:[/\\]/.test(path2) ? " Git Bash/MSYS may have rewritten a leading-slash argument; retry with MSYS_NO_PATHCONV=1 and the slash-free repos/... path. The endpoint must still be allowlisted." : "";
63075
- return new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.${API_GET_RECOVERY}${hint}`);
63302
+ return new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.${API_GET_RECOVERY}${typedReadHint(path2)}${hint}`);
63076
63303
  };
63077
63304
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
63078
63305
  const q = path2.indexOf("?");
@@ -65773,22 +66000,23 @@ function mmiDoctorDeps(opts = {}) {
65773
66000
  if (!token || token === "opencode") return void 0;
65774
66001
  const descriptor = doctorSurface(token);
65775
66002
  const snapshot = snapshotPluginGuardInput(runtimeSurface, isOrgRepo);
65776
- const hermes = token === "hermes" ? readHermesPluginEvidence(process.env) : void 0;
65777
- const installedVersion = hermes?.installedVersion ?? installedSurfacePluginVersion(runtimeSurface);
66003
+ const native = token === "openbot" ? readOpenBotPluginEvidence(process.env) : void 0;
66004
+ const installedVersion = native?.installedVersion ?? installedSurfacePluginVersion(runtimeSurface);
65778
66005
  const stagedVersion = token === "claude" ? stagedClaudePluginVersion() : void 0;
65779
66006
  surfaceEvidence = {
65780
66007
  descriptor,
65781
- // Hermes' root is the host evidence: a configured but uninstalled MMI tree is missing, while an
65782
- // absent root is skipped. Other mature surfaces retain their established org/install applicability.
65783
- applicable: hermes ? hermes.hostPresent : isOrgRepo || snapshot.installRecordPresent || snapshot.pluginCachePresent,
65784
- installRecordPresent: hermes?.installRecordPresent ?? snapshot.installRecordPresent,
65785
- deliveryPresent: hermes ? hermes.installRecordPresent : snapshot.marketplaceClonePresent,
65786
- payloadPresent: hermes?.payloadPresent ?? snapshot.pluginCachePresent,
65787
- manifest: hermes?.manifest ?? (installedVersion ? "valid" : snapshot.installRecordPresent ? "invalid" : "missing"),
66008
+ // A native evidence reader IS the host evidence: a configured but uninstalled MMI tree is missing,
66009
+ // while an absent root is skipped. Other mature surfaces retain their established org/install
66010
+ // applicability.
66011
+ applicable: native ? native.hostPresent : isOrgRepo || snapshot.installRecordPresent || snapshot.pluginCachePresent,
66012
+ installRecordPresent: native?.installRecordPresent ?? snapshot.installRecordPresent,
66013
+ deliveryPresent: native ? native.installRecordPresent : snapshot.marketplaceClonePresent,
66014
+ payloadPresent: native?.payloadPresent ?? snapshot.pluginCachePresent,
66015
+ manifest: native?.manifest ?? (installedVersion ? "valid" : snapshot.installRecordPresent ? "invalid" : "missing"),
65788
66016
  guardState: buildPluginGuardDecision(snapshot).state,
65789
66017
  ...installedVersion ? { installedVersion } : {},
65790
66018
  ...stagedVersion ? { stagedVersion } : {},
65791
- ...hermes?.receipt ? { receipt: hermes.receipt } : {}
66019
+ ...native?.receipt ? { receipt: native.receipt } : {}
65792
66020
  };
65793
66021
  return surfaceEvidence;
65794
66022
  };