@mutmutco/cli 4.5.226 → 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/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) {
@@ -16032,6 +16131,9 @@ function partitionByRequired(entries, requiredContexts) {
16032
16131
  ignoredFailures.push(entry.name);
16033
16132
  }
16034
16133
  }
16134
+ if (relevant.length && [...requiredContexts].some((name) => !entries.some((entry) => entry.name === name))) {
16135
+ relevant.push("pending");
16136
+ }
16035
16137
  return { relevant, ignoredFailures };
16036
16138
  }
16037
16139
  function parseNdjsonLines(stdout) {
@@ -18735,6 +18837,7 @@ function shouldWithdrawSurfaceFlag(result) {
18735
18837
 
18736
18838
  // src/secrets-transport.ts
18737
18839
  init_client_version();
18840
+ init_hub_auth();
18738
18841
  var OWNER = "mutmutco";
18739
18842
  var SSM_ROOT = "/mmi-future";
18740
18843
  var ORG_INFRA_SLUG = "_org";
@@ -18896,6 +18999,17 @@ function errorDetail(body) {
18896
18999
  const error = typeof body.error === "string" ? body.error : "";
18897
19000
  return error ? `: ${error}` : "";
18898
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
+ }
18899
19013
  async function upgradeMessage(res, body) {
18900
19014
  if (res.status !== 426) return null;
18901
19015
  return upgradeRequiredError(res, body ?? await readJsonBody(res));
@@ -18944,7 +19058,7 @@ async function secretsList(deps, opts) {
18944
19058
  return;
18945
19059
  }
18946
19060
  if (!res.ok) {
18947
- 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)));
18948
19062
  return;
18949
19063
  }
18950
19064
  const { secrets } = await res.json();
@@ -19329,6 +19443,10 @@ function missingRequiredKeys(stage, required, present, declaredSecrets) {
19329
19443
  }
19330
19444
  async function secretsPreflight(deps, opts) {
19331
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
+ }
19332
19450
  const qs = new URLSearchParams({ repo }).toString();
19333
19451
  let res;
19334
19452
  try {
@@ -20265,7 +20383,7 @@ async function secretsRequest(deps, key, opts) {
20265
20383
  });
20266
20384
  const body = await readJsonBody(res);
20267
20385
  if (!res.ok) {
20268
- 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));
20269
20387
  return false;
20270
20388
  }
20271
20389
  if (opts.json) {
@@ -20962,7 +21080,7 @@ async function fetchSecretForUse(deps, { repo, key, slug }) {
20962
21080
  return null;
20963
21081
  }
20964
21082
  deps.err(
20965
- 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))
20966
21084
  );
20967
21085
  return null;
20968
21086
  }
@@ -24970,6 +25088,7 @@ function createDocsIndexDeps(repoRoot2) {
24970
25088
  // src/gate-budget.ts
24971
25089
  var BLESSED_RUN_WITH_BUDGET_SHA = "5ce541add569e54e7ca96de67a768577d3d604e4";
24972
25090
  var BLESSED_RUNNER_GATE_SHA = "a19e9392e5ad9b0675f02e4304d68dd933fad60f";
25091
+ var BLESSED_KIT_AGENT_FILES_SHA = "a8b273ed418e703c0aa501a53ea87228ba28b1f0";
24973
25092
  var BLESSED_RUNBOOK_GUARD_SHA = "e661e0aedc0e58b73c738bf6cdfeac65e5914584";
24974
25093
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
24975
25094
  var LOCAL_USE = "./.github/actions/run-with-budget";
@@ -25345,6 +25464,9 @@ function gateSeedVars(cls, releaseTrack, runtime = "node", requiredCheckBranches
25345
25464
  // The pin is the CLI's blessed SHA, not an operator knob — one central place to bump (#3178).
25346
25465
  GATE_BUDGET_SHA: BLESSED_RUN_WITH_BUDGET_SHA,
25347
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,
25348
25470
  // #8129: the guard and its bundled CLI must come from the same immutable Hub revision.
25349
25471
  GATE_RUNBOOK_SHA: BLESSED_RUNBOOK_GUARD_SHA,
25350
25472
  // #7407: repo-declared service containers and extra check steps; empty renders today's gate byte-for-byte.
@@ -27088,10 +27210,10 @@ var rollout_plan_default = {
27088
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)."
27089
27211
  },
27090
27212
  baseline: {
27091
- version: "4.5.226",
27092
- tag: "v4.5.226",
27093
- commit: "360239dd3172",
27094
- npm: "@mutmutco/cli@4.5.226"
27213
+ version: "4.5.228",
27214
+ tag: "v4.5.228",
27215
+ commit: "097f7c9ad0e0",
27216
+ npm: "@mutmutco/cli@4.5.228"
27095
27217
  },
27096
27218
  exitCriterion: "fleet-n-of-n",
27097
27219
  hubOnlyShortcut: "forbidden",
@@ -27108,14 +27230,14 @@ var rollout_plan_default = {
27108
27230
  repo: "mutmutco/mmi-hub",
27109
27231
  role: "canary",
27110
27232
  schedule: "train",
27111
- v3Target: "v4.5.226"
27233
+ v3Target: "v4.5.228"
27112
27234
  }
27113
27235
  ],
27114
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.",
27115
27237
  rollback: {
27116
27238
  independent: true,
27117
- mechanism: "npm dist-tag latest -> 4.5.226 and redeploy the Hub Lambda from tag v4.5.226 (360239dd3172); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27118
- v3Target: "v4.5.226 (@mutmutco/cli@4.5.226, tag commit 360239dd3172 \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)"
27119
27241
  }
27120
27242
  },
27121
27243
  {
@@ -29272,7 +29394,8 @@ async function buildTrainApplyContext(deps) {
29272
29394
  repo,
29273
29395
  owner,
29274
29396
  slug: name.toLowerCase(),
29275
- login
29397
+ login,
29398
+ ...verdict.authenticatedDelegation === true ? { delegatedActor: login } : {}
29276
29399
  };
29277
29400
  }
29278
29401
  function commandAuthorityLabel(owner) {
@@ -32049,57 +32172,24 @@ function installedClaudePluginVersion() {
32049
32172
  return void 0;
32050
32173
  }
32051
32174
  }
32052
- function readHermesPluginEvidence(env = process.env) {
32053
- const host = hermesConfigRoot(env);
32054
- const root = hermesPluginRoot(env);
32055
- const installRecordPresent = (0, import_node_fs25.existsSync)(root);
32056
- const manifestPath = (0, import_node_path22.join)(root, "plugin.yaml");
32057
- 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;
32058
32179
  let manifest = "missing";
32059
- try {
32060
- const text = (0, import_node_fs25.readFileSync)(manifestPath, "utf8");
32061
- let version;
32062
- try {
32063
- const parsed = JSON.parse(text).version;
32064
- if (typeof parsed === "string" && parsed.trim()) version = parsed.trim();
32065
- } catch {
32066
- }
32067
- if (!version) {
32068
- const match = /^version:\s*["']?([^\s"'#]+)["']?\s*(?:#.*)?$/m.exec(text);
32069
- version = match?.[1];
32070
- }
32071
- if (version) {
32072
- installedVersion = version;
32073
- manifest = "valid";
32074
- } else if ((0, import_node_fs25.existsSync)(manifestPath)) manifest = "invalid";
32075
- } catch {
32076
- if ((0, import_node_fs25.existsSync)(manifestPath)) manifest = "invalid";
32077
- }
32078
- let skills = false;
32079
- try {
32080
- 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();
32081
- } catch {
32082
- }
32083
- let provisioned = false;
32084
- const provisionedRoot = (0, import_node_path22.join)(host, "skills", "mmi");
32085
- try {
32086
- provisioned = (0, import_node_fs25.existsSync)(provisionedRoot) && (0, import_node_fs25.statSync)(provisionedRoot).isDirectory();
32087
- } catch {
32088
- }
32089
- const treeOk = (0, import_node_fs25.existsSync)((0, import_node_path22.join)(root, "__init__.py")) && skills && manifest === "valid";
32090
- const receipt = treeOk && !provisioned ? `hermes-skills-unprovisioned:${provisionedRoot}` : void 0;
32180
+ if (recordPath) manifest = record2?.version ? "valid" : "invalid";
32091
32181
  return {
32092
- hostPresent: (0, import_node_fs25.existsSync)(host),
32093
- installRecordPresent,
32182
+ hostPresent: workspaces.length > 0,
32183
+ installRecordPresent: record2 !== null,
32094
32184
  manifest,
32095
- payloadPresent: treeOk && provisioned,
32096
- ...installedVersion ? { installedVersion } : {},
32097
- ...receipt ? { receipt } : {}
32185
+ payloadPresent: record2 !== null && openbotWorkspaceSkillsHealthy(env),
32186
+ ...record2?.version ? { installedVersion: record2.version } : {},
32187
+ ...recordPath && !record2?.version ? { receipt: `openbot-ownership-unreadable:${recordPath}` } : {}
32098
32188
  };
32099
32189
  }
32100
32190
  function installedSurfacePluginVersion(surface) {
32101
32191
  const token = surfaceToken(surface);
32102
- if (token === "hermes") return readHermesPluginEvidence().installedVersion;
32192
+ if (token === "openbot") return readOpenBotPluginEvidence().installedVersion;
32103
32193
  if (token === "jervcode") {
32104
32194
  return installedJervCodePackageVersion();
32105
32195
  }
@@ -34298,6 +34388,22 @@ function supersedeablePublishLegShape(rec) {
34298
34388
  function deferredGatewayDeployLegShape(rec) {
34299
34389
  return rec.state === "pending" && rec.runId == null && rec.workflow === "jerv-gateway";
34300
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
+ }
34301
34407
  async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref, fence) {
34302
34408
  let liveSha = "";
34303
34409
  try {
@@ -34383,23 +34489,29 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
34383
34489
  };
34384
34490
  }
34385
34491
  if (deferredGateway) {
34386
- const dispatch = await appendJervGatewayReleaseDeploy(
34387
- deps,
34388
- prior.repo,
34389
- prior.tag,
34390
- prior.tagSha,
34391
- { note: `Jerv Gateway operator-host deploy leg for ${prior.tag} run during the publish supersession`, deployStatus: "success" }
34392
- );
34393
- phases.deploy = {
34394
- ...phaseEntry(deployPhaseInput("registry-publish", dispatch, { releaseSha: prior.tagSha })),
34395
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
34396
- };
34397
- if (dispatch.deployStatus !== "success") {
34398
- throw new Error(
34399
- `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" }
34400
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 };
34401
34514
  }
34402
- gatewayDeploy = { workflow: "jerv-gateway", state: "complete", note: dispatch.note };
34403
34515
  }
34404
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)`;
34405
34517
  await fence(() => atomicWriteJson(path2, {
@@ -34483,7 +34595,11 @@ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
34483
34595
  }
34484
34596
  let deployEntry;
34485
34597
  let gatewayDeploy;
34486
- 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) {
34487
34603
  const dispatch = await appendJervGatewayReleaseDeploy(
34488
34604
  deps,
34489
34605
  prior.repo,
@@ -37401,7 +37517,15 @@ async function runReleaseAbortLocked(deps, options) {
37401
37517
  );
37402
37518
  }
37403
37519
  await requireCleanTree(deps);
37404
- 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") {
37405
37529
  throw new Error("release --abort must run from the primary checkout left on main by the failed train; nothing was written");
37406
37530
  }
37407
37531
  await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
@@ -37411,7 +37535,7 @@ async function runReleaseAbortLocked(deps, options) {
37411
37535
  const tagSha = await probeRemoteTag(deps, tag);
37412
37536
  if (!tagSha) throw new Error(`release --abort: ${tag} is not on origin; nothing was written`);
37413
37537
  const localTagSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
37414
- const localMainSha = clean2(await deps.run("git", ["rev-parse", "main"]));
37538
+ const localMainSha = ciCheckout ? tagSha : clean2(await deps.run("git", ["rev-parse", "main"]));
37415
37539
  const originMainSha = clean2(await deps.run("git", ["rev-parse", "origin/main"]));
37416
37540
  if (localTagSha !== tagSha || localMainSha !== tagSha) {
37417
37541
  throw new Error(
@@ -37442,6 +37566,17 @@ async function runReleaseAbortLocked(deps, options) {
37442
37566
  throw new Error(`release --abort: origin still reports ${tag} after deletion; local state was preserved`);
37443
37567
  }
37444
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
+ }
37445
37580
  await deps.run("git", ["checkout", "development"]);
37446
37581
  await ffOnlyPull(deps, "development");
37447
37582
  await deps.run("git", ["branch", "-f", "main", "origin/main"]);
@@ -40783,7 +40918,7 @@ function registerBoardCommands(program3) {
40783
40918
  return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
40784
40919
  }
40785
40920
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
40786
- 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));
40787
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) => {
40788
40923
  try {
40789
40924
  if (o.since !== void 0 && !/^\d{1,15}$/.test(o.since)) throw new Error("--since must be a non-negative integer cursor");
@@ -43003,7 +43138,7 @@ function registerSecretsCommands(program3) {
43003
43138
  }
43004
43139
  const { keys, command, ...o } = parsed.value;
43005
43140
  const code = secretsUseExitCode(await secretsUse(d, keys, { ...o, command }));
43006
- if (code !== void 0) process.exitCode = code;
43141
+ if (code !== void 0) process.exitCode = lastHubAuthFailure()?.exitCode ?? code;
43007
43142
  });
43008
43143
  });
43009
43144
  withDeclaredOptions(
@@ -49623,15 +49758,40 @@ var PR_LAND_STATE_READ_RETRIES = 3;
49623
49758
  var PR_LAND_STATE_READ_DELAY_MS = 15e3;
49624
49759
  var PR_LAND_MERGE_RETRY_DELAY_MS = 3e4;
49625
49760
  async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
49626
- const first = await deps.mergeAuto(prNumber, repo);
49627
- if (first.mergeStatus !== "failed") return first;
49628
- const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
49629
- if (!ready.open || !ready.mergeable || !ready.checksPassing) return first;
49761
+ const now = deps.now ?? (() => Date.now());
49762
+ const deadline = now() + PR_LAND_ENQUEUE_TIMEOUT_MS;
49630
49763
  const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
49631
- await sleep2(PR_LAND_MERGE_RETRY_DELAY_MS);
49632
- const retried = await deps.mergeAuto(prNumber, repo);
49633
- if (retried.mergeStatus !== "failed") return retried;
49634
- return { mergeStatus: "failed", error: `merge retry after transient failure also failed: ${retried.error ?? first.error ?? "unknown error"}` };
49764
+ const first = await deps.mergeAuto(prNumber, repo);
49765
+ let result = first;
49766
+ let transientRetried = false;
49767
+ while (result.mergeStatus === "failed") {
49768
+ const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false, checksPending: false }));
49769
+ if (ready.open && ready.checksPending) {
49770
+ if (now() >= deadline) break;
49771
+ await sleep2(Math.min(PR_LAND_POLL_MS, deadline - now()));
49772
+ if (now() >= deadline) break;
49773
+ const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
49774
+ if (checks.status !== "success" && checks.status !== "skipped") {
49775
+ return { mergeStatus: "failed", error: `${result.error ?? "merge failed"}
49776
+ registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
49777
+ }
49778
+ if (now() >= deadline) break;
49779
+ } else {
49780
+ if (!ready.open || !ready.mergeable || !ready.checksPassing || transientRetried) return result;
49781
+ transientRetried = true;
49782
+ if (now() >= deadline) break;
49783
+ await sleep2(Math.min(PR_LAND_MERGE_RETRY_DELAY_MS, deadline - now()));
49784
+ if (now() >= deadline) break;
49785
+ }
49786
+ const retried = await deps.mergeAuto(prNumber, repo);
49787
+ result = retried.mergeStatus !== "failed" ? retried : {
49788
+ mergeStatus: "failed",
49789
+ error: `merge retry after transient failure also failed: first attempt: ${first.error ?? "unknown error"}
49790
+ retry: ${retried.error ?? "unknown error"}`
49791
+ };
49792
+ }
49793
+ return result.mergeStatus === "failed" ? { ...result, error: `${result.error ?? "merge failed"}
49794
+ required-check registration did not settle within the enqueue deadline` } : result;
49635
49795
  }
49636
49796
  var AUTO_MERGE_CONFIRM_RETRIES = 3;
49637
49797
  var AUTO_MERGE_CONFIRM_DELAY_MS = 15e3;
@@ -50025,15 +50185,15 @@ function invisibleTrailer(sha, line) {
50025
50185
  return {
50026
50186
  kind: "malformed-override-trailer",
50027
50187
  paths: [],
50028
- detail: `OVERRIDE TRAILER GIT CANNOT SEE \u2014 commit ${sha.slice(0, 8)} carries
50029
- ${line}
50030
- but \`git log --format='%(trailers:key=${TRAILER_KEY})'\` reports nothing for it, so the waiver would
50031
- exist only to the regex that granted it. The trailer was chosen over a flag because it can be
50032
- AUDITED, and a reason the auditor cannot see is not a reason this gate accepts (#3628).
50033
- Git reads trailers from the LAST paragraph only, and every line of that paragraph must be a
50034
- trailer or an indented continuation \u2014 one bare line such as \`Closes #123\` disqualifies the whole
50035
- block. Indent the continuation lines, and leave nothing but trailers in that paragraph.
50036
- This is fixable forward: push a LATER commit on this branch carrying a well-formed trailer.
50188
+ detail: `OVERRIDE TRAILER GIT CANNOT SEE \u2014 commit ${sha.slice(0, 8)}
50189
+ Recovery (trailer): create a LATER commit on this branch with \`${TRAILER_KEY}: <reason>\`
50190
+ in the final trailer block, separated from the body by a blank line. Use \`Key: value\` lines
50191
+ only in that block; indent continuation lines and keep prose such as \`Closes #123\` above it.
50192
+ Verify: \`git log -1 --format='%(trailers:key=${TRAILER_KEY})' HEAD\` must show the waiver.
50193
+ Then run \`mmi-cli tests policy --json --base origin/development\` from the selected worktree root
50194
+ to refresh its receipt before retrying the focused test. Changing location alone cannot fix this trailer.
50195
+ Rejected line: ${line}
50196
+ Git's trailer parser reports no waiver; a regex-only reason is not git-auditable (#3628).
50037
50197
  The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.`
50038
50198
  };
50039
50199
  }
@@ -50911,9 +51071,8 @@ function ghFailureReason(e) {
50911
51071
  const err = e;
50912
51072
  const head = String(err?.message ?? "").split("\n")[0]?.trim() ?? "";
50913
51073
  const detail = `${err?.stderr ?? ""}
50914
- ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
50915
- if (detail && head) return `${head} \u2014 ${detail}`;
50916
- return detail || head || "unknown error";
51074
+ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
51075
+ return (detail && head ? `${head} \u2014 ${detail}` : detail || head || "unknown error").replace(/\x1b\[[0-9;]*m/g, "").replace(/\b(?:gh[pousr]_|github_pat_|AKIA|sk-)[\w-]+/g, "[redacted]").replace(/\b(authorization:\s*)(?:[^\n]*)/gi, "$1[redacted]");
50917
51076
  }
50918
51077
  var defaultGhMergeAutoIo = {
50919
51078
  gh: async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout
@@ -51066,13 +51225,7 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile, me
51066
51225
  error: stateRead.ok ? message2.split("\n")[0] : `could not confirm PR state after local branch cleanup warning: ${stateRead.error}`
51067
51226
  };
51068
51227
  }
51069
- if (!basePolicyBlocksImmediateMerge(message2)) {
51070
- return { mergeStatus: "failed", error: ghFailureReason(e) };
51071
- }
51072
- return {
51073
- mergeStatus: "failed",
51074
- error: `${ghFailureReason(e)} \u2014 auto-merge could not be armed yet (the PR's required checks have not registered); re-run the same --auto merge once they appear`
51075
- };
51228
+ return { mergeStatus: "failed", error: ghFailureReason(e) };
51076
51229
  }
51077
51230
  return confirmEnqueueOutcome();
51078
51231
  }
@@ -51225,6 +51378,61 @@ function prMergeQueueLine(result) {
51225
51378
  // src/command-register-train-operations.ts
51226
51379
  init_github_client();
51227
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
+
51228
51436
  // src/project-info-sync.ts
51229
51437
  var import_node_fs38 = require("node:fs");
51230
51438
  var import_node_path36 = require("node:path");
@@ -56773,9 +56981,10 @@ ${list}`);
56773
56981
  },
56774
56982
  fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
56775
56983
  resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
56776
- waitForChecks: async (prNumber, repo) => {
56984
+ waitForChecks: async (prNumber, repo, timeoutMs) => {
56777
56985
  const requiredContexts = await fetchRequiredCheckContexts(repo, "development").catch(() => null);
56778
56986
  return waitForPrChecks({
56987
+ timeoutMs,
56779
56988
  resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
56780
56989
  // #3024: REST-only wait loop — runPrLand's resolveRepo already guarantees `repo` is set.
56781
56990
  pollChecks: () => pollRestPrChecks(prNumber, repo, void 0, requiredContexts),
@@ -56817,7 +57026,8 @@ ${list}`);
56817
57026
  return {
56818
57027
  open: snapshot?.state === "open",
56819
57028
  mergeable: snapshot?.mergeable === "MERGEABLE",
56820
- checksPassing: checks === "success" || checks === "no-checks-reported"
57029
+ checksPassing: checks === "success",
57030
+ checksPending: checks === "pending" || checks === "failing" || checks === "no-checks-reported"
56821
57031
  };
56822
57032
  },
56823
57033
  readPrState: async (prNumber, repo) => {
@@ -56839,21 +57049,21 @@ ${list}`);
56839
57049
  // #8456: a pinned land has no update lane at all.
56840
57050
  ...o.expectHead ? {} : { updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)) },
56841
57051
  pollMerged: async (prNumber, repo, deadlineMs) => {
56842
- let lastFailure;
57052
+ let lastFailure2;
56843
57053
  while (Date.now() < deadlineMs) {
56844
57054
  const read = await pollRestPrMerged(prNumber, repo);
56845
57055
  if (read.state === "merged") return true;
56846
57056
  if (read.state === "failed") {
56847
- lastFailure = read.error;
57057
+ lastFailure2 = read.error;
56848
57058
  console.warn(`pr land: merged-state read FAILED (${read.error}) \u2014 retrying; this is not evidence the PR is unmerged`);
56849
57059
  } else {
56850
- lastFailure = void 0;
57060
+ lastFailure2 = void 0;
56851
57061
  }
56852
57062
  await new Promise((resolve7) => setTimeout(resolve7, PR_LAND_POLL_MS));
56853
57063
  }
56854
- if (lastFailure) {
57064
+ if (lastFailure2) {
56855
57065
  throw new Error(
56856
- `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.`
56857
57067
  );
56858
57068
  }
56859
57069
  return false;
@@ -57516,6 +57726,8 @@ function trainApplyDeps() {
57516
57726
  // parses, so the ONE self-spawn seam derives the prefix through the same rule the refusal names.
57517
57727
  runSelf: async (args) => (await execFileP(process.execPath, [process.argv[1], ...canonicalArgvFor(args)], { timeout: 3e4 })).stdout,
57518
57728
  trainAuthority: async (repo) => {
57729
+ const ci2 = await hubReleaseCiAuthority(repo, { env: process.env, getJson: pinnedGitHubGetJson(process.env.GH_TOKEN) });
57730
+ if (ci2) return ci2;
57519
57731
  const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
57520
57732
  return verdict.ok ? {
57521
57733
  ok: true,
@@ -57586,6 +57798,7 @@ function trainApplyDeps() {
57586
57798
  return (await execFileP(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS, ...env ? { env } : {} })).stdout;
57587
57799
  },
57588
57800
  postAnnouncement: async (payload) => {
57801
+ if (hubReleaseCiRun(process.env)) throw new Error(HUB_RELEASE_CI_ANNOUNCE_DEFERRED);
57589
57802
  const res = await releaseAnnouncement(payload, registryClientDeps(await loadConfig()));
57590
57803
  const body = res.body;
57591
57804
  if (!res.ok) throw new Error(body?.error ?? `Hub announcement failed (HTTP ${res.status}); verify delivery before retrying`);
@@ -57669,11 +57882,13 @@ function renderReleaseSupersedeDeploy(r) {
57669
57882
  }
57670
57883
  function renderReleaseSupersedePublish(r) {
57671
57884
  const base = `mmi-cli devops release --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
57672
- 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}`;
57673
57887
  }
57674
57888
  function renderReleaseSupersedeDeployAndPublish(r) {
57675
57889
  const base = `mmi-cli devops release --supersede-deploy --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
57676
- 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}`;
57677
57892
  }
57678
57893
  function renderReleasePublishRetry(r) {
57679
57894
  return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
@@ -58095,7 +58310,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
58095
58310
  if (!guard.ok) return fail(`release: ${guard.message}`);
58096
58311
  }
58097
58312
  try {
58098
- const result = await runReleaseAbort(trainApplyDeps(), { approved: true });
58313
+ const result = await runReleaseAbort(trainApplyDeps(), { approved: true, ciCheckout: hubReleaseCiRun(process.env) !== null });
58099
58314
  return emitTrainResult("release --abort", o.json ? JSON.stringify(result, null, 2) : renderReleaseAbort(result), o.out);
58100
58315
  } catch (e) {
58101
58316
  return failGraceful(`release --abort: ${e.message}`);
@@ -60559,29 +60774,23 @@ var surfaces_default = {
60559
60774
  ]
60560
60775
  },
60561
60776
  {
60562
- token: "hermes",
60563
- displayName: "Hermes Agent",
60777
+ token: "openbot",
60778
+ displayName: "OpenBot",
60564
60779
  lifecycle: "active",
60565
60780
  artifactIds: [
60566
- "mmi-hermes-plugin",
60781
+ "mmi-openbot-plugin",
60567
60782
  "mmi-skills",
60568
60783
  "mmi-cli"
60569
60784
  ],
60570
60785
  plannedArtifactIds: [],
60571
60786
  assembly: {
60572
- mode: "package-directory",
60573
- rootPath: "packages/hermes-plugin",
60574
- sync: [
60575
- {
60576
- mode: "directories",
60577
- sourcePath: "skills",
60578
- targetPath: "packages/hermes-plugin/skills"
60579
- }
60580
- ]
60787
+ mode: "source-tree",
60788
+ rootPath: ".pi-plugin",
60789
+ sync: []
60581
60790
  },
60582
60791
  install: {
60583
60792
  mechanism: "npm",
60584
- 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)"
60585
60794
  },
60586
60795
  upgrade: {
60587
60796
  mechanism: "package-manager",
@@ -60590,10 +60799,10 @@ var surfaces_default = {
60590
60799
  skills: {
60591
60800
  delivery: "provisioned",
60592
60801
  sourceSurfaceId: "mmi-skills",
60593
- artifactId: "mmi-hermes-plugin",
60802
+ artifactId: "mmi-skills",
60594
60803
  invocation: {
60595
- entry: "/epic",
60596
- any: "skill_view(name='<skill>')"
60804
+ entry: "use the epic skill",
60805
+ any: "use the <skill> skill"
60597
60806
  }
60598
60807
  },
60599
60808
  cli: {
@@ -60606,20 +60815,19 @@ var surfaces_default = {
60606
60815
  repair: "mmi-hub"
60607
60816
  },
60608
60817
  certification: {
60609
- hostCommand: "hermes",
60610
- versionArgs: [
60611
- "--version"
60612
- ]
60818
+ hostProbe: "none",
60819
+ hostCommand: null,
60820
+ versionArgs: []
60613
60821
  },
60614
60822
  compatibility: {
60615
- hostVersion: "0.20.1",
60616
- 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"
60617
60825
  },
60618
60826
  enforcementCeilings: [
60619
- "MMI ships no Hermes hooks (#5908): the plugin contributes skills and the /mmi command only; enforcement is host-side.",
60620
- "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.",
60621
- "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.",
60622
- "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."
60623
60831
  ]
60624
60832
  }
60625
60833
  ],
@@ -60983,41 +61191,36 @@ var surfaces_default = {
60983
61191
  publishVisibility: "public"
60984
61192
  },
60985
61193
  {
60986
- id: "mmi-hermes-plugin",
61194
+ id: "mmi-openbot-plugin",
60987
61195
  classification: "packaging",
60988
61196
  kind: "plugin",
60989
- ownerPath: "packages/hermes-plugin/package.json",
60990
- deliveryPath: "packages/hermes-plugin/package.json",
61197
+ ownerPath: ".pi-plugin/package.json",
61198
+ deliveryPath: ".pi-plugin/package.json",
60991
61199
  delivery: "npm",
60992
- 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.",
60993
61201
  versionCoordinated: true,
60994
- surfaceToken: "hermes",
61202
+ surfaceToken: "openbot",
60995
61203
  versionPaths: [
60996
61204
  {
60997
- path: "packages/hermes-plugin/package.json",
60998
- pointer: "version"
60999
- },
61000
- {
61001
- path: "packages/hermes-plugin/plugin.yaml",
61205
+ path: ".pi-plugin/package.json",
61002
61206
  pointer: "version"
61003
61207
  }
61004
61208
  ],
61005
61209
  additionalPaths: [
61006
- "packages/hermes-plugin/plugin.yaml",
61007
- "packages/hermes-plugin/__init__.py",
61008
- "packages/hermes-plugin/skills"
61210
+ ".pi-plugin/skills"
61009
61211
  ],
61010
61212
  artifactIdentity: {
61011
61213
  kind: "npm-pack",
61012
- packagePath: "packages/hermes-plugin"
61214
+ packagePath: ".pi-plugin"
61013
61215
  },
61014
- publishVisibility: "public"
61216
+ publishVisibility: "n/a"
61015
61217
  }
61016
61218
  ]
61017
61219
  };
61018
61220
 
61019
61221
  // src/surface-doctor.ts
61020
- 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"]);
61021
61224
  function isActiveToken(value) {
61022
61225
  return TOKENS.has(value);
61023
61226
  }
@@ -61068,7 +61271,7 @@ function diagnoseSurface(evidence) {
61068
61271
  return { ...base, state: "clean" };
61069
61272
  }
61070
61273
  function reloadInstruction(descriptor) {
61071
- if (descriptor.token === "hermes") return "start a fresh Hermes Agent process";
61274
+ if (descriptor.token === "openbot") return "start a fresh OpenBot agent run";
61072
61275
  const verb = descriptor.reload === "workspace" ? "reload" : "restart";
61073
61276
  return `${verb} ${descriptor.displayName}`;
61074
61277
  }
@@ -61080,7 +61283,7 @@ function planSurfaceRepair(diagnosis) {
61080
61283
  supported: false,
61081
61284
  owner: "mmi-hub",
61082
61285
  changes: [],
61083
- // #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
61084
61287
  // mmi-hub-owned surface (Claude Code included) chasing a different host that was already current.
61085
61288
  instruction: `run \`mmi-hub update\` to converge the MMI-managed ${descriptor.displayName} plugin, then ${reloadInstruction(descriptor)}`
61086
61289
  };
@@ -61108,9 +61311,9 @@ function buildSurfaceDoctorCheck(diagnosis) {
61108
61311
  const detailByState = {
61109
61312
  skipped: "skipped \u2014 host not active",
61110
61313
  clean: diagnosis.repairDetail ? `clean \u2014 repaired and verified (${diagnosis.repairDetail})` : `clean${versions ? ` \u2014 ${versions}` : ""}`,
61111
- "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})` : ""}`,
61112
61315
  // #5833: the update already staged the released tree; the wording mirrors `mmi-hub status`.
61113
- 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`,
61114
61317
  "freshness-unknown": `${diagnosis.installedVersion ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
61115
61318
  stale: `stale${versions ? ` \u2014 ${versions}` : ""}`,
61116
61319
  missing: "missing \u2014 no install record",
@@ -62656,8 +62859,17 @@ function buildRunWatchSpec(id, o) {
62656
62859
  const timeoutMin = o.timeoutMin !== void 0 ? parseBrokerInt(o.timeoutMin, "--timeout", 1, 120) : 30;
62657
62860
  return { program: "gh", args, timeoutMs: timeoutMin * 6e4 };
62658
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
+ }
62659
62868
  function buildPrViewSpec(ref, o) {
62660
- 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);
62661
62873
  if (o.comments) args.push("--comments");
62662
62874
  return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
62663
62875
  }
@@ -62730,21 +62942,50 @@ function ghExitCodeOf(error) {
62730
62942
  const code = error.code;
62731
62943
  return typeof code === "number" ? code : null;
62732
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
+ }
62733
62959
  async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk)) {
62734
62960
  const env = brokerChildEnv();
62961
+ let output;
62735
62962
  try {
62736
- write(redactPrivateCredential(await run(spec.args, { timeoutMs: spec.timeoutMs, env }), env));
62737
- return 0;
62963
+ output = redactPrivateCredential(await run(spec.args, { timeoutMs: spec.timeoutMs, env }), env);
62738
62964
  } catch (e) {
62739
62965
  const err = e;
62740
62966
  if (typeof err.stdout === "string" && err.stdout) process.stderr.write(redactPrivateCredential(err.stdout, env));
62741
62967
  const code = ghExitCodeOf(e);
62742
62968
  if (code !== null) {
62743
62969
  if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
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}
62973
+ `);
62744
62974
  return code;
62745
62975
  }
62746
62976
  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`);
62747
62977
  }
62978
+ if (spec.args.includes("--json")) {
62979
+ try {
62980
+ JSON.parse(output);
62981
+ } catch {
62982
+ process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} returned ${output.trim() ? "invalid" : "empty"} JSON on the App pool \u2014 retry the same typed read; only parse stdout after success
62983
+ `);
62984
+ return 1;
62985
+ }
62986
+ }
62987
+ write(output);
62988
+ return 0;
62748
62989
  }
62749
62990
  async function runBrokerRestChecks(deps, spec, resolveRepo2) {
62750
62991
  const repo = await resolveRepo2(spec.repo);
@@ -63032,11 +63273,33 @@ var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$
63032
63273
  var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
63033
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>)";
63034
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
+ }
63035
63298
  function parseApiGetPath(rawPath) {
63036
63299
  const path2 = rawPath.trim();
63037
63300
  const refuse = () => {
63038
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." : "";
63039
- 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}`);
63040
63303
  };
63041
63304
  if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
63042
63305
  const q = path2.indexOf("?");
@@ -65737,22 +66000,23 @@ function mmiDoctorDeps(opts = {}) {
65737
66000
  if (!token || token === "opencode") return void 0;
65738
66001
  const descriptor = doctorSurface(token);
65739
66002
  const snapshot = snapshotPluginGuardInput(runtimeSurface, isOrgRepo);
65740
- const hermes = token === "hermes" ? readHermesPluginEvidence(process.env) : void 0;
65741
- const installedVersion = hermes?.installedVersion ?? installedSurfacePluginVersion(runtimeSurface);
66003
+ const native = token === "openbot" ? readOpenBotPluginEvidence(process.env) : void 0;
66004
+ const installedVersion = native?.installedVersion ?? installedSurfacePluginVersion(runtimeSurface);
65742
66005
  const stagedVersion = token === "claude" ? stagedClaudePluginVersion() : void 0;
65743
66006
  surfaceEvidence = {
65744
66007
  descriptor,
65745
- // Hermes' root is the host evidence: a configured but uninstalled MMI tree is missing, while an
65746
- // absent root is skipped. Other mature surfaces retain their established org/install applicability.
65747
- applicable: hermes ? hermes.hostPresent : isOrgRepo || snapshot.installRecordPresent || snapshot.pluginCachePresent,
65748
- installRecordPresent: hermes?.installRecordPresent ?? snapshot.installRecordPresent,
65749
- deliveryPresent: hermes ? hermes.installRecordPresent : snapshot.marketplaceClonePresent,
65750
- payloadPresent: hermes?.payloadPresent ?? snapshot.pluginCachePresent,
65751
- 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"),
65752
66016
  guardState: buildPluginGuardDecision(snapshot).state,
65753
66017
  ...installedVersion ? { installedVersion } : {},
65754
66018
  ...stagedVersion ? { stagedVersion } : {},
65755
- ...hermes?.receipt ? { receipt: hermes.receipt } : {}
66019
+ ...native?.receipt ? { receipt: native.receipt } : {}
65756
66020
  };
65757
66021
  return surfaceEvidence;
65758
66022
  };
@@ -8640,6 +8640,7 @@ init_error_codes();
8640
8640
 
8641
8641
  // src/secrets-transport.ts
8642
8642
  init_client_version();
8643
+ init_hub_auth();
8643
8644
  var KEY_MAX_PATH_SEGMENTS = 4;
8644
8645
  var KEY_RE = new RegExp(`^(?:[a-z0-9][a-z0-9-]*/){0,${KEY_MAX_PATH_SEGMENTS}}[A-Za-z][A-Za-z0-9_]*$`);
8645
8646
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.226",
3
+ "version": "4.5.228",
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",