@sentropic/h2a-runtime 0.98.7-rc.1 → 0.98.7-rc.10

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.
Files changed (213) hide show
  1. package/dist/agent-launch-args.d.ts.map +1 -1
  2. package/dist/agent-launch-args.js +5 -5
  3. package/dist/agent-launch-args.js.map +1 -1
  4. package/dist/agy-claude.d.ts +42 -0
  5. package/dist/agy-claude.d.ts.map +1 -0
  6. package/dist/agy-claude.js +148 -0
  7. package/dist/agy-claude.js.map +1 -0
  8. package/dist/agy-headless.d.ts +11 -0
  9. package/dist/agy-headless.d.ts.map +1 -0
  10. package/dist/agy-headless.js +104 -0
  11. package/dist/agy-headless.js.map +1 -0
  12. package/dist/attach.d.ts +3 -0
  13. package/dist/attach.d.ts.map +1 -1
  14. package/dist/attach.js +2 -0
  15. package/dist/attach.js.map +1 -1
  16. package/dist/central-mcp.d.ts +32 -0
  17. package/dist/central-mcp.d.ts.map +1 -1
  18. package/dist/central-mcp.js +186 -69
  19. package/dist/central-mcp.js.map +1 -1
  20. package/dist/claude-code-cli-isolation.d.ts +53 -0
  21. package/dist/claude-code-cli-isolation.d.ts.map +1 -0
  22. package/dist/claude-code-cli-isolation.js +277 -0
  23. package/dist/claude-code-cli-isolation.js.map +1 -0
  24. package/dist/claude-code-cli-lifecycle.d.ts +80 -0
  25. package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
  26. package/dist/claude-code-cli-lifecycle.js +402 -0
  27. package/dist/claude-code-cli-lifecycle.js.map +1 -0
  28. package/dist/claude-code-cli-runner.d.ts +89 -0
  29. package/dist/claude-code-cli-runner.d.ts.map +1 -0
  30. package/dist/claude-code-cli-runner.js +659 -0
  31. package/dist/claude-code-cli-runner.js.map +1 -0
  32. package/dist/claude-seat-input.d.ts +47 -0
  33. package/dist/claude-seat-input.d.ts.map +1 -0
  34. package/dist/claude-seat-input.js +272 -0
  35. package/dist/claude-seat-input.js.map +1 -0
  36. package/dist/claude-seat-mesh-contract.d.ts +88 -0
  37. package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
  38. package/dist/claude-seat-mesh-contract.js +13 -0
  39. package/dist/claude-seat-mesh-contract.js.map +1 -0
  40. package/dist/cli-seat-codecs.d.ts +86 -0
  41. package/dist/cli-seat-codecs.d.ts.map +1 -0
  42. package/dist/cli-seat-codecs.js +255 -0
  43. package/dist/cli-seat-codecs.js.map +1 -0
  44. package/dist/cli-seat-fixtures.d.ts +86 -0
  45. package/dist/cli-seat-fixtures.d.ts.map +1 -0
  46. package/dist/cli-seat-fixtures.js +114 -0
  47. package/dist/cli-seat-fixtures.js.map +1 -0
  48. package/dist/cli-seat-materialization.d.ts +245 -0
  49. package/dist/cli-seat-materialization.d.ts.map +1 -0
  50. package/dist/cli-seat-materialization.js +646 -0
  51. package/dist/cli-seat-materialization.js.map +1 -0
  52. package/dist/conv-guard.d.ts.map +1 -1
  53. package/dist/conv-guard.js +6 -2
  54. package/dist/conv-guard.js.map +1 -1
  55. package/dist/delegate.d.ts +10 -0
  56. package/dist/delegate.d.ts.map +1 -1
  57. package/dist/delegate.js +29 -0
  58. package/dist/delegate.js.map +1 -1
  59. package/dist/file-lock.d.ts +4 -2
  60. package/dist/file-lock.d.ts.map +1 -1
  61. package/dist/file-lock.js +24 -10
  62. package/dist/file-lock.js.map +1 -1
  63. package/dist/gateway-host/claude-seat-client.d.ts +32 -0
  64. package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
  65. package/dist/gateway-host/claude-seat-client.js +38 -0
  66. package/dist/gateway-host/claude-seat-client.js.map +1 -0
  67. package/dist/gateway-host/config-file.d.ts +2 -0
  68. package/dist/gateway-host/config-file.d.ts.map +1 -1
  69. package/dist/gateway-host/config-file.js +5 -1
  70. package/dist/gateway-host/config-file.js.map +1 -1
  71. package/dist/gateway-host/daemon.d.ts.map +1 -1
  72. package/dist/gateway-host/daemon.js +1 -2
  73. package/dist/gateway-host/daemon.js.map +1 -1
  74. package/dist/gateway-host/host.d.ts +6 -0
  75. package/dist/gateway-host/host.d.ts.map +1 -1
  76. package/dist/gateway-host/host.js +24 -3
  77. package/dist/gateway-host/host.js.map +1 -1
  78. package/dist/gateway-host/live-auth.d.ts +62 -0
  79. package/dist/gateway-host/live-auth.d.ts.map +1 -0
  80. package/dist/gateway-host/live-auth.js +87 -0
  81. package/dist/gateway-host/live-auth.js.map +1 -0
  82. package/dist/gateway-host/live-declarations.d.ts.map +1 -1
  83. package/dist/gateway-host/live-declarations.js +13 -7
  84. package/dist/gateway-host/live-declarations.js.map +1 -1
  85. package/dist/gateway-host/live-disclosure.d.ts +124 -0
  86. package/dist/gateway-host/live-disclosure.d.ts.map +1 -0
  87. package/dist/gateway-host/live-disclosure.js +347 -0
  88. package/dist/gateway-host/live-disclosure.js.map +1 -0
  89. package/dist/gateway-host/live-github.d.ts +11 -0
  90. package/dist/gateway-host/live-github.d.ts.map +1 -0
  91. package/dist/gateway-host/live-github.js +374 -0
  92. package/dist/gateway-host/live-github.js.map +1 -0
  93. package/dist/gateway-host/live-host.d.ts +20 -0
  94. package/dist/gateway-host/live-host.d.ts.map +1 -0
  95. package/dist/gateway-host/live-host.js +14 -0
  96. package/dist/gateway-host/live-host.js.map +1 -0
  97. package/dist/gateway-host/live-object-store.d.ts +35 -0
  98. package/dist/gateway-host/live-object-store.d.ts.map +1 -0
  99. package/dist/gateway-host/live-object-store.js +447 -0
  100. package/dist/gateway-host/live-object-store.js.map +1 -0
  101. package/dist/gateway-model-catalog/model-catalog.d.ts +38 -0
  102. package/dist/gateway-model-catalog/model-catalog.d.ts.map +1 -0
  103. package/dist/gateway-model-catalog/model-catalog.js +167 -0
  104. package/dist/gateway-model-catalog/model-catalog.js.map +1 -0
  105. package/dist/identity-cull/cull.js +1 -1
  106. package/dist/identity-cull/cull.js.map +1 -1
  107. package/dist/identity-cull/identity-fence.d.ts +48 -0
  108. package/dist/identity-cull/identity-fence.d.ts.map +1 -0
  109. package/dist/identity-cull/identity-fence.js +221 -0
  110. package/dist/identity-cull/identity-fence.js.map +1 -0
  111. package/dist/identity-operator-break.d.ts +43 -0
  112. package/dist/identity-operator-break.d.ts.map +1 -0
  113. package/dist/identity-operator-break.js +85 -0
  114. package/dist/identity-operator-break.js.map +1 -0
  115. package/dist/index.d.ts +8 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +446 -131
  118. package/dist/index.js.map +1 -1
  119. package/dist/launch-layout.d.ts.map +1 -1
  120. package/dist/launch-layout.js +10 -2
  121. package/dist/launch-layout.js.map +1 -1
  122. package/dist/llm-mesh-accounts.d.ts +128 -6
  123. package/dist/llm-mesh-accounts.d.ts.map +1 -1
  124. package/dist/llm-mesh-accounts.js +370 -14
  125. package/dist/llm-mesh-accounts.js.map +1 -1
  126. package/dist/native-host.d.ts +25 -1
  127. package/dist/native-host.d.ts.map +1 -1
  128. package/dist/native-host.js +184 -23
  129. package/dist/native-host.js.map +1 -1
  130. package/dist/native-restart.d.ts +2 -0
  131. package/dist/native-restart.d.ts.map +1 -1
  132. package/dist/native-restart.js +7 -0
  133. package/dist/native-restart.js.map +1 -1
  134. package/dist/native-terminal/fleet.d.ts +11 -0
  135. package/dist/native-terminal/fleet.d.ts.map +1 -1
  136. package/dist/native-terminal/fleet.js +58 -1
  137. package/dist/native-terminal/fleet.js.map +1 -1
  138. package/dist/native-terminal/host-scope.d.ts +18 -0
  139. package/dist/native-terminal/host-scope.d.ts.map +1 -0
  140. package/dist/native-terminal/host-scope.js +65 -0
  141. package/dist/native-terminal/host-scope.js.map +1 -0
  142. package/dist/native-terminal/host.d.ts +7 -1
  143. package/dist/native-terminal/host.d.ts.map +1 -1
  144. package/dist/native-terminal/host.js +10 -26
  145. package/dist/native-terminal/host.js.map +1 -1
  146. package/dist/native-terminal/op.d.ts +36 -1
  147. package/dist/native-terminal/op.d.ts.map +1 -1
  148. package/dist/native-terminal/op.js +190 -33
  149. package/dist/native-terminal/op.js.map +1 -1
  150. package/dist/native-terminal/process.d.ts.map +1 -1
  151. package/dist/native-terminal/process.js +10 -1
  152. package/dist/native-terminal/process.js.map +1 -1
  153. package/dist/native-terminal/screen.d.ts.map +1 -1
  154. package/dist/native-terminal/screen.js +1 -1
  155. package/dist/native-terminal/screen.js.map +1 -1
  156. package/dist/native-terminal/server.d.ts +12 -2
  157. package/dist/native-terminal/server.d.ts.map +1 -1
  158. package/dist/native-terminal/server.js +143 -42
  159. package/dist/native-terminal/server.js.map +1 -1
  160. package/dist/native-terminal/session-scope.d.ts +2 -0
  161. package/dist/native-terminal/session-scope.d.ts.map +1 -0
  162. package/dist/native-terminal/session-scope.js +32 -0
  163. package/dist/native-terminal/session-scope.js.map +1 -0
  164. package/dist/proc-identity.d.ts +31 -0
  165. package/dist/proc-identity.d.ts.map +1 -1
  166. package/dist/proc-identity.js +54 -0
  167. package/dist/proc-identity.js.map +1 -1
  168. package/dist/prompt-delivery.d.ts +3 -0
  169. package/dist/prompt-delivery.d.ts.map +1 -1
  170. package/dist/prompt-delivery.js +8 -3
  171. package/dist/prompt-delivery.js.map +1 -1
  172. package/dist/protocol-local.d.ts +16 -0
  173. package/dist/protocol-local.d.ts.map +1 -1
  174. package/dist/protocol-local.js +14 -0
  175. package/dist/protocol-local.js.map +1 -1
  176. package/dist/registry.d.ts +16 -12
  177. package/dist/registry.d.ts.map +1 -1
  178. package/dist/registry.js +39 -5
  179. package/dist/registry.js.map +1 -1
  180. package/dist/relaunch.d.ts +16 -0
  181. package/dist/relaunch.d.ts.map +1 -1
  182. package/dist/relaunch.js +42 -1
  183. package/dist/relaunch.js.map +1 -1
  184. package/dist/restore.d.ts +4 -0
  185. package/dist/restore.d.ts.map +1 -1
  186. package/dist/restore.js +9 -37
  187. package/dist/restore.js.map +1 -1
  188. package/dist/resume-launch-options.d.ts +3 -3
  189. package/dist/resume-launch-options.d.ts.map +1 -1
  190. package/dist/resume-launch-options.js +7 -4
  191. package/dist/resume-launch-options.js.map +1 -1
  192. package/dist/run.d.ts +2 -0
  193. package/dist/run.d.ts.map +1 -1
  194. package/dist/run.js +9 -4
  195. package/dist/run.js.map +1 -1
  196. package/dist/soft-refresh.d.ts.map +1 -1
  197. package/dist/soft-refresh.js +6 -0
  198. package/dist/soft-refresh.js.map +1 -1
  199. package/dist/tmux.d.ts +31 -5
  200. package/dist/tmux.d.ts.map +1 -1
  201. package/dist/tmux.js +96 -20
  202. package/dist/tmux.js.map +1 -1
  203. package/dist/vendor/engram-live/contracts.d.ts +619 -2
  204. package/dist/vendor/engram-live/contracts.js +1201 -25
  205. package/dist/vendor/engram-live/integration.d.ts +44 -4
  206. package/dist/vendor/engram-live/integration.js +65 -5
  207. package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
  208. package/dist/vendor/engram-live/pr-sync.js +411 -0
  209. package/dist/vendor/engram-live/registration.d.ts +2 -2
  210. package/dist/vendor/engram-live/registration.js +2 -4
  211. package/dist/vendor/engram-live/retention.d.ts +56 -0
  212. package/dist/vendor/engram-live/retention.js +179 -0
  213. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { spawnSync } from "node:child_process";
4
- import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { Command, Help } from "commander";
9
+ import { resolveSeatEnrollmentProvider } from "./cli-seat-codecs.js";
10
+ import { agyClaudeQuotaError, assertAgyClaudeBuildAllowed, detectAgyClaudeQuotaExhaustion, isAgyClaudeModel, normalizeAgyEffort, readAgyClaudeQuotaStatus, resolveAgyModel, writeAgyClaudeQuotaStatus } from "./agy-claude.js";
9
11
  import { applyRuntimeHelpGroups, groupRuntimeHelpItems, } from "./cli-help-groups.js";
10
12
  import { attach, createRemoteSession, getRemoteSession, listRemoteSessions, refreshRemoteSession, renameRemoteSession, sessionTerminalHealth, stopRemoteSession, } from "./attach.js";
11
13
  import { authHeaders, clearDefaultRemote, getDefaultRemote, getDefaultTarget, getDefaultTools, getH2aConfig, getJobMaxAgeHours, getMaxConcurrent, getLlmMeshRuntimeConfig, getTmuxProfileConfig, getTunnel, resolveConfigPath, setDefaultRemote, setDefaultTarget, migrateConfigHomeIfNeeded, setDefaultTools, setLlmMeshRuntimeConfig, setTmuxProfileConfig, setToken, setTunnel, } from "./config.js";
@@ -22,13 +24,14 @@ import { attachLocalSession, attachPodTmux, capturePane, capturePaneVisible, cle
22
24
  import { deliverInitialPrompt, sleepSync, } from "./prompt-delivery.js";
23
25
  import { startLaunchGuard } from "./launch-guard.js";
24
26
  import { buildLaunchContext } from "./launch-context.js";
25
- import { attachNativeSession, driveNativeInstruction, killNativeSessionIfIncarnation, killNativeSessionTree, nativeHostAvailable, nativePromptDeliveryDeps, nativeSessionLiveness, nativeSessionPid, nativeSessionState, nativeTreeCpuMs, nativeWorkerPid, preflightNativeLaunch, resolveSessionHostKind, startNativeH2aSidecar, startNativeHeadlessSession, startNativeSession, NativeHostCapabilityMismatchError, NativeLaunchAdmissionError, } from "./native-host.js";
27
+ import { attachNativeSession, driveNativeInstruction, killNativeSessionIfIncarnation, killNativeSessionTree, nativeHostAvailable, nativePromptDeliveryDeps, nativeSessionLiveness, nativeSessionPid, nativeSessionState, nativeSessionStateAsync, nativeTreeCpuMs, nativeWorkerPid, preflightNativeLaunch, resolveSessionHostKind, startNativeH2aSidecar, startNativeHeadlessSession, startNativeSession, startNativeSessionSingleShot, NativeHostCapabilityMismatchError, NativeLaunchAdmissionError, } from "./native-host.js";
26
28
  import { executeNativeRestart, NativeRestartError, } from "./native-restart.js";
27
29
  import { migrateTmuxNames } from "./tmux-name-migration.js";
28
30
  import { projectStatusForH2a } from "./status-projection.js";
29
31
  import { buildAgentLaunchArgs, buildAgentLaunchStdin, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
32
+ import { resolveGatewayModelAlias } from "./gateway-model-catalog/model-catalog.js";
30
33
  import { deriveSessionClass } from "./session-class.js";
31
- import { isRelaunchKillable, planRelaunch, relaunchContinuationPrompt, tryAcquireResumeLaunchClaim, wakeRelaunchedSession, } from "./relaunch.js";
34
+ import { isRelaunchKillable, planRelaunch, relaunchContinuationPrompt, scrubbedRetypeCommand, prepareGatewayRetypeCommand, tryAcquireResumeLaunchClaim, wakeRelaunchedSession, } from "./relaunch.js";
32
35
  import { isHumanFacingSession, legacySessionEvidence, readConversationCustomTitle, readLastLayout, reconcileRunConvIds, restore as restoreLayout, } from "./restore.js";
33
36
  import { getLayoutConfig } from "./config.js";
34
37
  import { resolvePinnedLaunchEntries } from "./resume-launch-options.js";
@@ -42,7 +45,11 @@ import { DEFAULT_FANOUT_MAX, mapWithConcurrency, planRemoteFanout, } from "./fan
42
45
  import { authenticateJobEnvelopes, buildDecisionReply, dropEnvelope, emitJobDone, envelopeFileName, isAwaitingDecision, jobInstance, parentInstance, pendingDecisions, readInboxEnvelopes, renderPendingDecisions, repliedDecisionJobIds, } from "./h2a-jobs.js";
43
46
  import { buildConductorTask, computeDurableWorkspaceId, normalizeRootCommits, detectAvailableHosts, freshestLaunchEnvelope, h2aReportsLiveConductor, markLaunchEnvelopeProcessed, readLastLaunchAt, readLaunchEnvelopes, recordLaunchAt, selectHost, shouldLaunch, } from "./conductor-launch.js";
44
47
  import { defaultLocalH2aRoot } from "./h2a-bridge.js";
45
- import { refuseCullExecution, runIdentityCullDryRun } from "./identity-cull/cull.js";
48
+ import { operatorBreak as operatorBreakToCore } from "./identity-operator-break.js";
49
+ // identity-cull is imported lazily at its verb below: cull.js needs the
50
+ // core identity fence (`@sentropic/h2a`), which must never become a
51
+ // load-time edge of this barrel — isolated installs of this package have
52
+ // no core beside them.
46
53
  import { guardConvWriters } from "./conv-guard.js";
47
54
  import { handleClaudeHook, installClaudeHooks, manualEnroll, readStdin, } from "./enroll.js";
48
55
  import { probePodCredHealth, softRefreshSession } from "./soft-refresh.js";
@@ -74,8 +81,7 @@ import { createInterface } from "node:readline";
74
81
  const H2A_RUN_API_VERSION = "h2a.run/v1";
75
82
  const STRUCTURED_LAUNCH_PHASE_PREFIX = "[h2a] h2a.run.phase/v1 ";
76
83
  const H2A_RUNTIME_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
77
- import { CLI_PROFILES, gatewayModeForProfile, profileUsesLlmMeshGateway, } from "./protocol-local.js";
78
- import { enrollViaFacade, formatLlmMeshAccountError, formatLlmMeshAccountList, listAccountsViaFacade, removeAccountViaFacade, } from "./llm-mesh-accounts.js";
84
+ import { CLI_PROFILES, gatewayModeForProfile, profileUsesLlmMeshGateway, resolveResumedGatewayMode, } from "./protocol-local.js";
79
85
  import { readLlmMeshConfig, updateLlmMeshRoutingConfig, } from "./gateway-host/config-file.js";
80
86
  import { startGateway, stopGateway, readGatewayPid, llmMeshLogPath, replaceAnthropicGatewayEnvironment, acquireLlmMeshSessionEnv, } from "./gateway-host/daemon.js";
81
87
  import { describeLlmMeshRoutingConfig, parseLlmMeshRoutingConfig, preferredRoutingConfig, strategyRoutingConfig, } from "./routing-preferences.js";
@@ -135,6 +141,7 @@ async function startRemoteFanout(remote, profileName, members, spec) {
135
141
  ? { startupArgs: spec.startupArgs }
136
142
  : {}),
137
143
  ...(spec.credentials ? { credentials: spec.credentials } : {}),
144
+ ...(spec.llmGateway === true ? { llmGateway: true } : {}),
138
145
  });
139
146
  return { name: member.name, sessionId: session.id, workspaceId: ws.id };
140
147
  });
@@ -200,6 +207,20 @@ async function runProfile(profileName, opts, commandArgs = []) {
200
207
  if (opts.resume !== undefined && resumeArgs.length === 0) {
201
208
  throw new Error(`profile "${profileName}" has no verified resume argv; start it without -r/--resume`);
202
209
  }
210
+ // Remote sessions are direct unless the gateway is explicitly asked for.
211
+ // Only an explicit --gw/--llm-gateway transmits the opt-in; the server
212
+ // ignores a globally configured gateway URL without it.
213
+ let remoteGatewayMode;
214
+ try {
215
+ remoteGatewayMode = gatewayModeFromOptions(opts);
216
+ }
217
+ catch (error) {
218
+ throw new Error(`[h2a] ${error.message}.`);
219
+ }
220
+ if (remoteGatewayMode === "gateway" && !profileUsesLlmMeshGateway(profileName)) {
221
+ throw new Error(`[h2a] --llm-gateway/--gw is unsupported for ${profileName}; this gateway is Anthropic-compatible and only Claude profiles consume it.`);
222
+ }
223
+ const remoteGatewayChoice = remoteGatewayMode === "gateway" ? { llmGateway: true } : {};
203
224
  const startupArgs = [...resumeArgs, ...commandArgs];
204
225
  // WP6 — REMOTE fan-out: --count N spawns N concurrent remote sessions, each
205
226
  // on its OWN workspace subPath of the shared RWX volume. count<=1 falls
@@ -229,6 +250,7 @@ async function runProfile(profileName, opts, commandArgs = []) {
229
250
  target: opts.target,
230
251
  startupArgs,
231
252
  ...(credentials ? { credentials } : {}),
253
+ ...remoteGatewayChoice,
232
254
  });
233
255
  return;
234
256
  }
@@ -247,6 +269,7 @@ async function runProfile(profileName, opts, commandArgs = []) {
247
269
  ...(opts.workspaceId ? { workspaceId: opts.workspaceId } : {}),
248
270
  ...(opts.browser ? { metadata: { browser: true } } : {}),
249
271
  ...(opts.agentImage ? { agentImage: opts.agentImage } : {}),
272
+ ...remoteGatewayChoice,
250
273
  });
251
274
  if (archive) {
252
275
  await uploadWorkspaceArchive(opts.remote, session.id, archive);
@@ -270,7 +293,21 @@ async function runProfile(profileName, opts, commandArgs = []) {
270
293
  ...(opts.port !== undefined ? { port: opts.port } : {}),
271
294
  ...(commandArgs.length > 0 ? { startupArgs: commandArgs } : {}),
272
295
  };
273
- const result = await run(runOptions);
296
+ const gatewayMode = gatewayModeFromOptions(opts);
297
+ if (gatewayMode === "gateway" && !profileUsesLlmMeshGateway(profileName)) {
298
+ throw new Error(`--llm-gateway/--gw is unsupported for ${profileName}`);
299
+ }
300
+ // Resolve local PTY routing before spawning, just as for managed sessions.
301
+ // Restore the caller's native auth even if acquisition or spawning fails.
302
+ const restoreEnv = replaceAnthropicGatewayEnvironment(process.env, captureAnthropicGatewayEnvironment());
303
+ let result;
304
+ try {
305
+ await injectLlmMeshGatewayEnv(gatewayMode, false);
306
+ result = await run({ ...runOptions, gatewayMode: gatewayMode === "gateway" ? "gateway" : "direct" });
307
+ }
308
+ finally {
309
+ restoreEnv();
310
+ }
274
311
  process.stderr.write(`[h2a] session ${result.sessionId} attach at http://127.0.0.1:${result.port}\n`);
275
312
  const { exitCode } = await result.exit;
276
313
  process.exitCode = exitCode;
@@ -852,32 +889,12 @@ export async function startJob(job) {
852
889
  const prevJobId = process.env[JOB_ID_ENV];
853
890
  process.env[DEPTH_ENV] = childDepthEnvValue(job.depthBudget ?? clampDepth(undefined));
854
891
  process.env[JOB_ID_ENV] = job.id;
855
- // Inject llm-mesh gateway env if running. Do not trust a parent env that may
856
- // contain an old gateway token from a previous restart.
857
- // tmux inherits process.env, so claude + its subagents all get the gateway automatically.
858
- const jobSessionSlug = slugify(job.id);
859
- const jobSessionResolution = resolveLocalSession(jobSessionSlug);
860
- const gatewayClientSessionId = jobSessionResolution.kind === "found"
861
- ? jobSessionResolution.session.name
862
- : jobSessionResolution.kind === "missing"
863
- ? localSessionName(jobSessionSlug)
864
- : undefined;
865
- let meshEnv;
866
- try {
867
- meshEnv =
868
- profileUsesLlmMeshGateway(job.tool) &&
869
- getLlmMeshRuntimeConfig().enabled &&
870
- gatewayClientSessionId
871
- ? await acquireLlmMeshSessionEnv(undefined, gatewayClientSessionId)
872
- : null;
873
- }
874
- catch (error) {
875
- return {
876
- started: false,
877
- error: error instanceof Error ? error.message : String(error),
878
- };
879
- }
880
- const restoreGatewayEnvironment = replaceAnthropicGatewayEnvironment(process.env, meshEnv ?? undefined);
892
+ // Gateway off unless explicitly asked: jobs never acquire the gateway.
893
+ // A legacy llmMesh.enabled=true, a running service, enrolled accounts or a
894
+ // parent env never select gateway routing for immediate or deferred jobs.
895
+ // Strip any stale h2a routing vars so the child starts direct; a user-owned
896
+ // ANTHROPIC_API_KEY survives for native authentication.
897
+ const restoreGatewayEnvironment = replaceAnthropicGatewayEnvironment(process.env, undefined);
881
898
  let tmuxSession;
882
899
  let workerPid;
883
900
  let agentPane;
@@ -1399,13 +1416,12 @@ export function bareChoiceFromOptions(opts) {
1399
1416
  return false;
1400
1417
  return undefined;
1401
1418
  }
1402
- async function injectLlmMeshGatewayEnv(mode = "auto", allowDirectFallback = true, clientSessionId) {
1403
- if (mode === "direct") {
1404
- delete process.env.ANTHROPIC_BASE_URL;
1405
- delete process.env.ANTHROPIC_AUTH_TOKEN;
1406
- return undefined;
1407
- }
1408
- if (mode !== "gateway" && !getLlmMeshRuntimeConfig().enabled) {
1419
+ export async function injectLlmMeshGatewayEnv(mode = "auto", allowDirectFallback = true, clientSessionId) {
1420
+ if (mode !== "gateway") {
1421
+ // Gateway off unless explicitly asked: auto resolves to direct BEFORE any
1422
+ // acquire/start. A legacy llmMesh.enabled=true, a running service, enrolled
1423
+ // accounts or a parent env never select gateway routing. A user-owned
1424
+ // ANTHROPIC_API_KEY is preserved for native authentication.
1409
1425
  delete process.env.ANTHROPIC_BASE_URL;
1410
1426
  delete process.env.ANTHROPIC_AUTH_TOKEN;
1411
1427
  return undefined;
@@ -1436,7 +1452,7 @@ async function injectLlmMeshGatewayEnv(mode = "auto", allowDirectFallback = true
1436
1452
  delete process.env.ANTHROPIC_BASE_URL;
1437
1453
  delete process.env.ANTHROPIC_AUTH_TOKEN;
1438
1454
  if (mode === "gateway") {
1439
- throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse` first");
1455
+ throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse|claude-code` first");
1440
1456
  }
1441
1457
  if (allowDirectFallback) {
1442
1458
  process.stderr.write("[h2a] llm-mesh: gateway unavailable; continuing with native authentication.\n");
@@ -1518,7 +1534,7 @@ function renderNativeRestartResult(result, json) {
1518
1534
  export async function prepareStructuredGateway(mode, inject = (selectedMode) => injectLlmMeshGatewayEnv(selectedMode, selectedMode !== "gateway")) {
1519
1535
  const gateway = await inject(mode);
1520
1536
  if (mode === "gateway" && !gateway) {
1521
- throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse` first");
1537
+ throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse|claude-code` first");
1522
1538
  }
1523
1539
  return gateway;
1524
1540
  }
@@ -1529,7 +1545,9 @@ export async function prepareLlmMeshForRestore(opts = {}, context = {
1529
1545
  injectGateway: (mode) => injectLlmMeshGatewayEnv(mode, mode !== "gateway"),
1530
1546
  }) {
1531
1547
  const mode = opts.mode ?? "auto";
1532
- if (mode !== "gateway" && !context.runtimeEnabled)
1548
+ // Global enablement is not restore consent. Pinned sessions request --gw
1549
+ // individually through their launch command; only --gw prewarms globally.
1550
+ if (mode !== "gateway")
1533
1551
  return;
1534
1552
  const config = context.config ?? {};
1535
1553
  const port = config.port ?? 3002;
@@ -1720,6 +1738,40 @@ function setAndReportDefaultRemote(url) {
1720
1738
  ensureManagedTmuxProfile(getTmuxProfileConfig().profile);
1721
1739
  process.stderr.write(`[h2a] default remote set to ${configured}\n`);
1722
1740
  }
1741
+ // Root commander error boundary (DEC-119): commander never prints its own
1742
+ // error text. `outputError` is a no-op so the commander-built message (which
1743
+ // echoes argv, possibly multi-line) is dropped; `exitOverride` writes a
1744
+ // static message chosen ONLY from `err.code` — never `err.message`, never
1745
+ // user-typed text. Registered on the ROOT program before any `.command()`
1746
+ // so the whole tree inherits it via `copyInheritedSettings`.
1747
+ function staticCommanderErrorForCode(code) {
1748
+ switch (code) {
1749
+ case "commander.unknownOption":
1750
+ return "error: unknown option\n";
1751
+ case "commander.unknownCommand":
1752
+ return "error: unknown command\n";
1753
+ case "commander.excessArguments":
1754
+ return "error: too many arguments\n";
1755
+ case "commander.missingArgument":
1756
+ return "error: missing required argument\n";
1757
+ case "commander.optionMissingArgument":
1758
+ return "error: option argument missing\n";
1759
+ case "commander.missingMandatoryOptionValue":
1760
+ return "error: required option not specified\n";
1761
+ case "commander.conflictingOption":
1762
+ return "error: conflicting option\n";
1763
+ case "commander.invalidArgument":
1764
+ return "error: invalid argument\n";
1765
+ default:
1766
+ return "error: command error\n";
1767
+ }
1768
+ }
1769
+ function isCommanderNonErrorCode(code) {
1770
+ return (code === "commander.help" ||
1771
+ code === "commander.helpDisplayed" ||
1772
+ code === "commander.version" ||
1773
+ code === "commander.executeSubCommandAsync");
1774
+ }
1723
1775
  export async function main(argv) {
1724
1776
  // Passive fatal-path witness on the real entry path (see
1725
1777
  // installCliWitnessOnce): observes via uncaughtExceptionMonitor, so the
@@ -1773,22 +1825,34 @@ export async function main(argv) {
1773
1825
  "the separate remote and track CLIs. The heavy session runtime loads on " +
1774
1826
  "demand. h2a runs and coordinates agents; it is not itself an agent.")
1775
1827
  .version("0.0.0");
1828
+ // Root error boundary (DEC-119): commander never prints its own error
1829
+ // text. `outputError` is a no-op (commander message dropped);
1830
+ // `exitOverride` writes the static per-code message. Registered here —
1831
+ // before any `.command()` — so the whole tree inherits it through
1832
+ // `copyInheritedSettings`.
1833
+ program.configureOutput({
1834
+ writeErr: (str) => {
1835
+ process.stderr.write(str);
1836
+ },
1837
+ outputError: () => { },
1838
+ });
1839
+ program.showHelpAfterError(false);
1840
+ program.showSuggestionAfterError(false);
1841
+ program.exitOverride((err) => {
1842
+ if (isCommanderNonErrorCode(err.code))
1843
+ throw err;
1844
+ process.stderr.write(staticCommanderErrorForCode(err.code));
1845
+ throw err;
1846
+ });
1776
1847
  const identityCommand = program
1777
1848
  .command("identity")
1778
1849
  .description("Inspect identity bindings or unlock a token-fenced identity lock");
1779
- const operatorBreak = async (lockPath, token, assertDead) => {
1780
- const { breakLockAsOperator, breakIdentityLockAsOperator } = await import("@sentropic/h2a");
1781
- const identityLock = basename(lockPath) === '.lock' && basename(dirname(lockPath)) === 'identity';
1782
- const breaker = identityLock ? breakIdentityLockAsOperator : breakLockAsOperator;
1783
- const result = breaker(lockPath, { expectToken: token, assertDead });
1784
- process.stdout.write(`${JSON.stringify({ path: identityLock ? 'identity/.lock' : lockPath, ...result })}\n`);
1785
- if (!result.broken) {
1786
- process.stderr.write(`[h2a] lock break refused: ${result.reason}${result.pid !== undefined ? ` (pid ${result.pid})` : ""}\n`);
1787
- if (result.reason === "assert-dead-required") {
1788
- process.stderr.write("[h2a] verify the holder has stopped, then explicitly pass --assert-dead; no PID is killed\n");
1789
- }
1790
- process.exitCode = 1;
1791
- }
1850
+ // Operator break delegates to the core CLI's hidden `__operator-break`
1851
+ // plumbing (same `h2a`-binary spawn pattern as central-mcp): this package
1852
+ // must not import `@sentropic/h2a`, and the relay keeps stdio/exit codes
1853
+ // byte-for-byte identical where core is installed.
1854
+ const operatorBreak = (lockPath, token, assertDead) => {
1855
+ operatorBreakToCore(lockPath, token, assertDead, (code) => { process.exitCode = code; });
1792
1856
  };
1793
1857
  identityCommand
1794
1858
  .command("unlock")
@@ -1818,9 +1882,18 @@ export async function main(argv) {
1818
1882
  .option("--execute", "refuse destructive execution; no bypass exists")
1819
1883
  .option("--root <path>", "DEF h2a root (default: H2A_ROOT or ~/h2a-workspace/.h2a)")
1820
1884
  .option("--output <path>", "new external directory for the proof packet")
1821
- .action((opts) => {
1885
+ .action(async (opts) => {
1886
+ let cull;
1887
+ try {
1888
+ cull = await import("./identity-cull/cull.js");
1889
+ }
1890
+ catch (error) {
1891
+ process.stderr.write(`[h2a] identity cull unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
1892
+ process.exitCode = 1;
1893
+ return;
1894
+ }
1822
1895
  if (opts.execute) {
1823
- const refusal = refuseCullExecution();
1896
+ const refusal = cull.refuseCullExecution();
1824
1897
  process.stderr.write(`[h2a] identity cull refused: ${refusal.refusalCodes.join(", ")}\n`);
1825
1898
  process.exitCode = 2;
1826
1899
  return;
@@ -1828,7 +1901,7 @@ export async function main(argv) {
1828
1901
  const defRoot = opts.root ?? process.env.H2A_ROOT ?? defaultLocalH2aRoot();
1829
1902
  const outputDir = opts.output ?? join(tmpdir(), `h2a-identity-cull-${randomUUID()}`);
1830
1903
  try {
1831
- const result = runIdentityCullDryRun({
1904
+ const result = cull.runIdentityCullDryRun({
1832
1905
  defRoot,
1833
1906
  pinRoot: join(homedir(), "src", "a2a-cli"),
1834
1907
  outputDir,
@@ -1955,6 +2028,10 @@ export async function main(argv) {
1955
2028
  .option("--name <label>", "base label for the --count fan-out fleet names (default: cwd basename)")
1956
2029
  .option("--browser", "add the headful-browser noVNC sidecar (WP7) — enables 2FA / authenticated-site flows from inside the pod")
1957
2030
  .option("--image <ref>", "override the session-agent container image (fully-qualified registry ref, e.g. rg.fr-par.scw.cloud/sentropic-geo/geo-acquisition:0.1.0)")
2031
+ .option("--llm-gateway", "route this remote session through the llm-mesh gateway (explicit opt-in; default is direct)")
2032
+ .option("--gw", "alias for --llm-gateway")
2033
+ .option("--no-llm-gateway", "run this remote session without llm-mesh gateway env")
2034
+ .option("--no-gw", "alias for --no-llm-gateway")
1958
2035
  .action(async (commandArgs, opts) => {
1959
2036
  const { remote: remoteOverride, local, workspace, ...rest } = opts;
1960
2037
  if (local) {
@@ -3640,6 +3717,8 @@ export async function main(argv) {
3640
3717
  process.exitCode = 1;
3641
3718
  return;
3642
3719
  }
3720
+ const sameResumeIdentity = resolvedConvId !== undefined && registryEntry?.convId === resolvedConvId &&
3721
+ registryEntry?.cwd === process.cwd() && registryEntry?.tool === explicitProfile;
3643
3722
  const entry = explicitProfile
3644
3723
  ? {
3645
3724
  id: displaySlug,
@@ -3654,8 +3733,9 @@ export async function main(argv) {
3654
3733
  enrolledAt: new Date().toISOString(),
3655
3734
  lastSeenAt: new Date().toISOString(),
3656
3735
  source: "run",
3657
- bare: registryEntry?.convId === resolvedConvId ? registryEntry?.bare : undefined,
3658
- gatewayMode: registryEntry?.convId === resolvedConvId ? registryEntry?.gatewayMode : undefined,
3736
+ bare: sameResumeIdentity ? registryEntry?.bare : undefined,
3737
+ gatewayMode: sameResumeIdentity ? registryEntry?.gatewayMode : undefined,
3738
+ gatewayProvenance: sameResumeIdentity ? registryEntry?.gatewayProvenance : undefined,
3659
3739
  }
3660
3740
  : registryEntry;
3661
3741
  // WHICH host serves this resume — resolved BEFORE ANY act (F1): every
@@ -3753,7 +3833,7 @@ export async function main(argv) {
3753
3833
  process.exitCode = 2;
3754
3834
  return;
3755
3835
  }
3756
- const effectiveGatewayMode = gatewayModeForProfile(profile, gatewayMode === "auto" ? entry.gatewayMode ?? "auto" : gatewayMode);
3836
+ const effectiveGatewayMode = resolveResumedGatewayMode(profile, gatewayMode, entry.gatewayMode, entry.gatewayProvenance);
3757
3837
  let gateway;
3758
3838
  // Resolve against the selected mode, not the parent's environment:
3759
3839
  // required gateway injection below must succeed before either launch.
@@ -3966,7 +4046,11 @@ export async function main(argv) {
3966
4046
  cwd: entry.cwd,
3967
4047
  sessionClass,
3968
4048
  ...(entry.convId ? { convId: entry.convId } : {}),
3969
- gatewayMode: effectiveGatewayMode,
4049
+ // Only an explicit CLI choice becomes a new proven pin; otherwise
4050
+ // the previous pin (if any) is preserved untouched by the merge.
4051
+ ...(gatewayMode !== "auto"
4052
+ ? { gatewayMode, gatewayProvenance: "explicit" }
4053
+ : {}),
3970
4054
  bare: useBare,
3971
4055
  });
3972
4056
  process.stderr.write(`[h2a] resumed local session ${resumeSlug} (${profile} resume${entry.convId ? ` ${entry.convId}` : ""} in ${entry.cwd})\n`);
@@ -3997,8 +4081,11 @@ export async function main(argv) {
3997
4081
  .option("--prompt-stdin", "read the initial Claude/Codex/AGY prompt from stdin (keeps it out of process argv)")
3998
4082
  .option("--agent <agent>", "AGY agent override (for example: stp)")
3999
4083
  .option("--model <model>", "Claude/Codex/AGY model override")
4000
- .option("--effort <level>", "reasoning effort: low|medium|high|xhigh (AGY supports low|medium|high)")
4001
- .option("--headless", "run once and exit (Claude -p / Codex exec / AGY --print), recording output under .h2a/runs/<name>")
4084
+ .option("--role <role>", "launch purpose: review|challenge|build; AGY Claude builds require explicit opt-in")
4085
+ .option("--allow-claude-build", "explicitly spend the reserved AGY Claude pool on a build")
4086
+ .option("--output-file <path>", "expected AGY Claude headless artifact; report missing or unchanged output at exit")
4087
+ .option("--effort <level>", "reasoning effort: low|medium|high|xhigh (AGY: low|medium|high, max maps to high)")
4088
+ .option("--headless", "run once and exit (Claude -p / Codex exec / AGY stream-json), recording output under .h2a/runs/<name>")
4002
4089
  .option("--background", "classify this detached session as a background worker excluded from human restore")
4003
4090
  .option("--json", "emit one machine-readable h2a.run.result object")
4004
4091
  .option("--llm-gateway", "launch the CLI through the local llm-mesh gateway")
@@ -4020,13 +4107,30 @@ export async function main(argv) {
4020
4107
  process.exitCode = 2;
4021
4108
  return;
4022
4109
  }
4023
- if (opts.effort !== undefined && !isAgentLaunchEffort(opts.effort)) {
4024
- process.stderr.write(`[h2a] --effort must be low|medium|high|xhigh\n`);
4110
+ try {
4111
+ if (profile === "agy") {
4112
+ const effort = normalizeAgyEffort(opts.effort);
4113
+ if (effort !== undefined)
4114
+ opts.effort = effort;
4115
+ const model = resolveAgyModel(opts.model, effort);
4116
+ if (model !== undefined)
4117
+ opts.model = model;
4118
+ }
4119
+ assertAgyClaudeBuildAllowed({ profile, model: opts.model, role: opts.role, allowClaudeBuild: opts.allowClaudeBuild });
4120
+ if (opts.outputFile && !(profile === "agy" && isAgyClaudeModel(opts.model) && opts.headless)) {
4121
+ throw new Error("--output-file requires an AGY Claude headless run");
4122
+ }
4123
+ }
4124
+ catch (error) {
4125
+ process.stderr.write(`[h2a] ${error.message}\n`);
4126
+ if (opts.json)
4127
+ process.stdout.write(`${JSON.stringify({ kind: "h2a.run.failure", version: 1, state: "not-started",
4128
+ code: error.code ?? "invalid-agy-launch", error: error.message, retrySafe: false })}\n`);
4025
4129
  process.exitCode = 2;
4026
4130
  return;
4027
4131
  }
4028
- if (profile === "agy" && opts.effort === "xhigh") {
4029
- process.stderr.write("[h2a] AGY --effort must be low|medium|high\n");
4132
+ if (opts.effort !== undefined && !isAgentLaunchEffort(opts.effort)) {
4133
+ process.stderr.write(`[h2a] --effort must be low|medium|high|xhigh\n`);
4030
4134
  process.exitCode = 2;
4031
4135
  return;
4032
4136
  }
@@ -4172,13 +4276,11 @@ export async function main(argv) {
4172
4276
  }
4173
4277
  };
4174
4278
  try {
4175
- if (structuredLaunch && sessionHost === "native") {
4176
- for (const label of labels)
4177
- preflightNativeLaunch(localSessionName(slugify(label ?? cwd)));
4178
- }
4179
- const reservedTmuxSlugs = tmuxAvailable()
4180
- ? existingLocalSessionSlugs(labels, cwd)
4181
- : [];
4279
+ // No separate preflight fork: the in-process name probe below prepares
4280
+ // the host for a native run. The single-shot launch still performs its
4281
+ // fenced admission with a fresh inventory before creating any session.
4282
+ void preflightNativeLaunch;
4283
+ const reservedTmuxSlugs = existingLocalSessionSlugs(labels, cwd);
4182
4284
  // The inventory and this decision are separate reads: a structured
4183
4285
  // launch can leave a name visible just before its last tmux process
4184
4286
  // exits. Re-probe every canonical/legacy spelling and reclaim the slug
@@ -4213,14 +4315,16 @@ export async function main(argv) {
4213
4315
  // twin over a live-but-unprovable native writer (sol-F3): the same
4214
4316
  // fail-closed rule as resolveManagedHost's rule 4 applies — refuse
4215
4317
  // to launch on an unprovable host state.
4216
- const nativeExistence = labels
4318
+ const nativeExistence = await Promise.all(labels
4217
4319
  .map((label) => slugify(label ?? cwd))
4218
- .map((slug) => {
4219
- const detail = nativeSessionState(localSessionName(slug));
4320
+ .map(async (slug) => {
4321
+ const detail = sessionHost === "native"
4322
+ ? await nativeSessionStateAsync(localSessionName(slug), true)
4323
+ : nativeSessionState(localSessionName(slug));
4220
4324
  const probe = detail.state === "unknown" ? "unknown"
4221
4325
  : detail.state === "found" && detail.session.status === "running" ? "live" : "dead";
4222
4326
  return { slug, probe, reason: detail.state === "unknown" ? detail.reason : undefined };
4223
- });
4327
+ }));
4224
4328
  const unknownNativeSlugs = nativeExistence
4225
4329
  .filter((probed) => probed.probe === "unknown");
4226
4330
  if (unknownNativeSlugs.length > 0) {
@@ -4265,9 +4369,15 @@ export async function main(argv) {
4265
4369
  // Single-writer guard: refuse to resume a conversation another live
4266
4370
  // session (local registry / remote pod) is already writing.
4267
4371
  if (opts.resume) {
4372
+ // Reuse this operation's certified name verdict for that SAME
4373
+ // session. Other conversation owners still require their own probe.
4374
+ // Creation rechecks the complete inventory and name admission at
4375
+ // the host, so this snapshot never authorizes a colliding create.
4376
+ const nativeLivenessByName = new Map(nativeExistence.map(({ slug, probe }) => [localSessionName(slug), probe === "dead" ? false : probe === "live" ? true : "unknown"]));
4268
4377
  const ok = await guardConvWriters({
4269
4378
  convId: opts.resume,
4270
4379
  cwd,
4380
+ nativeSessionLiveness: name => nativeLivenessByName.get(name) ?? nativeSessionLiveness(name),
4271
4381
  ...(opts.force ? { force: true } : {}),
4272
4382
  fetchRemoteSessions: async () => {
4273
4383
  const url = getConfiguredRemoteOptional();
@@ -4296,8 +4406,11 @@ export async function main(argv) {
4296
4406
  for (const prunedId of resumeResolution.prunedIds) {
4297
4407
  process.stderr.write(`[h2a] pruned dead registry row ${prunedId} that pinned conflicting launch options for this resume\n`);
4298
4408
  }
4299
- const pinnedLaunch = resumeResolution.entries[0];
4300
- const launchGatewayMode = gatewayModeForProfile(profile, gatewayMode === "auto" ? pinnedLaunch?.gatewayMode ?? "auto" : gatewayMode);
4409
+ // Conflicting pins have already failed closed above. For agreeing
4410
+ // pins, prefer the proven choice over an equivalent legacy row.
4411
+ const pinnedLaunch = resumeResolution.entries.find(entry => entry.gatewayProvenance === "explicit")
4412
+ ?? resumeResolution.entries[0];
4413
+ const launchGatewayMode = resolveResumedGatewayMode(profile, gatewayMode, pinnedLaunch?.gatewayMode, pinnedLaunch?.gatewayProvenance);
4301
4414
  const command = localCliCommand(profile);
4302
4415
  // A detached/background or run-once launch is a worker, even when its
4303
4416
  // agent later emits a Claude SessionStart hook. Stamp this through tmux
@@ -4341,6 +4454,17 @@ export async function main(argv) {
4341
4454
  return;
4342
4455
  }
4343
4456
  const useBare = resolveClaudeBare(profile, bareChoiceFromOptions(opts) ?? pinnedLaunch?.bare);
4457
+ // Preserve the conversation's bare pin, including absence on a
4458
+ // legacy row: materializing it only on the new row creates a conflict.
4459
+ const enrollmentBare = pinnedLaunch && bareChoiceFromOptions(opts) === undefined
4460
+ ? pinnedLaunch.bare
4461
+ : useBare;
4462
+ // H0 (#662): normalize readable gateway aliases (argon-high, wonder,
4463
+ // wonder-full, spark) to their exact canonical model id. Unknown
4464
+ // names pass through unchanged for exact-model validation downstream.
4465
+ const runModel = opts.model !== undefined
4466
+ ? (resolveGatewayModelAlias(opts.model) ?? opts.model)
4467
+ : undefined;
4344
4468
  let args;
4345
4469
  try {
4346
4470
  args =
@@ -4351,7 +4475,7 @@ export async function main(argv) {
4351
4475
  ? { prompt: initialPrompt }
4352
4476
  : {}),
4353
4477
  ...(opts.agent !== undefined ? { agent: opts.agent } : {}),
4354
- ...(opts.model !== undefined ? { model: opts.model } : {}),
4478
+ ...(runModel !== undefined ? { model: runModel } : {}),
4355
4479
  ...(opts.effort !== undefined
4356
4480
  ? { effort: opts.effort }
4357
4481
  : {}),
@@ -4377,6 +4501,10 @@ export async function main(argv) {
4377
4501
  }
4378
4502
  let name;
4379
4503
  let slug;
4504
+ // R3 P1: pid/identity returned by the single-shot launch; the
4505
+ // re-probes below prefer these and skip their forks.
4506
+ let launchedPid;
4507
+ let launchedWorkerPid;
4380
4508
  let outputLog;
4381
4509
  let resultJson;
4382
4510
  let agentPane;
@@ -4389,6 +4517,10 @@ export async function main(argv) {
4389
4517
  mkdirSync(runDir, { recursive: true });
4390
4518
  outputLog = join(runDir, "output.log");
4391
4519
  resultJson = join(runDir, "result.json");
4520
+ // Name admission above has completed. Retire the previous result
4521
+ // before returning a path or starting the new run-once supervisor.
4522
+ if (profile === "agy" && isAgyClaudeModel(opts.model))
4523
+ rmSync(resultJson, { force: true });
4392
4524
  const headlessPromptInput = structuredLaunch && isAgentLaunchProfile(profile)
4393
4525
  ? buildAgentLaunchStdin({
4394
4526
  profile,
@@ -4402,14 +4534,16 @@ export async function main(argv) {
4402
4534
  ({ name, slug, promptFile } = startNativeHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt, structuredLaunch ? (owned) => {
4403
4535
  launchOwnership = { host: "native", sessions: [owned] };
4404
4536
  launchGuard = startLaunchGuard(runDir, launchOwnership);
4405
- } : undefined));
4537
+ } : undefined, opts.outputFile ? resolve(cwd, opts.outputFile) : undefined));
4406
4538
  }
4407
4539
  else {
4408
- ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt));
4540
+ ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt, opts.outputFile ? resolve(cwd, opts.outputFile) : undefined));
4409
4541
  }
4410
4542
  }
4411
4543
  else if (sessionHost === "native") {
4412
- ({ name, slug } = startNativeSession(profile, command, cwd, args, label, {
4544
+ // One in-process async operation, with a fresh admission inventory;
4545
+ // returns pid/identity so the re-probes below are skipped.
4546
+ ({ name, slug, pid: launchedPid, workerPid: launchedWorkerPid } = await startNativeSessionSingleShot(profile, command, cwd, args, label, {
4413
4547
  onCreateAttempt,
4414
4548
  ...(opts.resume !== undefined ? { resumeId: opts.resume } : {}),
4415
4549
  ...(initialPrompt !== undefined
@@ -4538,6 +4672,18 @@ export async function main(argv) {
4538
4672
  now: () => Date.now(),
4539
4673
  }, { profile });
4540
4674
  if (promptDelivery.state !== "working") {
4675
+ const quotaDiagnostic = promptDelivery.state === "submitted-idle" ? "" : `${promptDelivery.reason}\n${promptDelivery.capture ?? ""}`;
4676
+ const quotaVerdict = profile === "agy" && isAgyClaudeModel(opts.model)
4677
+ ? detectAgyClaudeQuotaExhaustion(quotaDiagnostic) : { exhausted: false };
4678
+ const quotaFailure = profile === "agy" && isAgyClaudeModel(opts.model) &&
4679
+ promptDelivery.state === "provider-blocked" && promptDelivery.providerError
4680
+ ? promptDelivery.providerError : quotaVerdict.exhausted
4681
+ ? agyClaudeQuotaError(quotaVerdict.signature, quotaDiagnostic) : undefined;
4682
+ if (quotaFailure)
4683
+ try {
4684
+ writeAgyClaudeQuotaStatus(quotaFailure.quota);
4685
+ }
4686
+ catch { /* Response still carries the observation. */ }
4541
4687
  cleanupHeadlessPromptFile(promptFile);
4542
4688
  // Only claim the session is gone when the kill actually reported
4543
4689
  // success. "nothing is left running" over an unchecked call is the
@@ -4567,26 +4713,64 @@ export async function main(argv) {
4567
4713
  ? `[h2a] last screen:\n${promptDelivery.capture}\n`
4568
4714
  : ""));
4569
4715
  process.exitCode = 1;
4570
- if (opts.json && promptDelivery.state === "provider-blocked") {
4716
+ if (opts.json && (promptDelivery.state === "provider-blocked" || quotaFailure)) {
4571
4717
  process.stdout.write(`${JSON.stringify({ kind: "h2a.run.failure", version: 1,
4572
- state: "provider-blocked", launchId: slug, error: promptDelivery.reason,
4573
- stopped, retrySafe: false, prompt: { delivered: true, waitedMs: promptDelivery.waitedMs },
4718
+ state: "provider-blocked", launchId: slug, error: quotaFailure?.message ?? (promptDelivery.state === "submitted-idle" ? "agent idle after submission" : promptDelivery.reason),
4719
+ stopped, retrySafe: false, prompt: { delivered: promptDelivery.state === "provider-blocked", ...("waitedMs" in promptDelivery ? { waitedMs: promptDelivery.waitedMs } : {}) },
4720
+ ...(quotaFailure ? { code: quotaFailure.code, error: quotaFailure.message, quota: quotaFailure.quota,
4721
+ resetsAt: quotaFailure.resetsAt, retryable: false, model: opts.model } : {}),
4574
4722
  })}\n`);
4575
4723
  }
4576
4724
  return;
4577
4725
  }
4578
4726
  }
4727
+ const reportAgyCompletion = () => {
4728
+ if (!(profile === "agy" && isAgyClaudeModel(opts.model) && opts.headless && resultJson))
4729
+ return false;
4730
+ try {
4731
+ const completed = JSON.parse(readFileSync(resultJson, "utf8"));
4732
+ // The previous result was removed before this supervisor started.
4733
+ // Filesystem and wall-clock timestamps cannot prove generation.
4734
+ if (!["h2a.run.failure", "h2a.run.completion"].includes(completed.kind))
4735
+ return false;
4736
+ launchGuard?.complete();
4737
+ if (opts.json)
4738
+ process.stdout.write(`${JSON.stringify({ ...completed, launchId: slug, result: { path: resultJson }, logs: { path: outputLog } })}\n`);
4739
+ else
4740
+ process.stderr.write(`[h2a] ${completed.error ?? "AGY run completed"}\n`);
4741
+ process.exitCode = completed.state === "done" ? 0 : 1;
4742
+ return true;
4743
+ }
4744
+ catch {
4745
+ return false;
4746
+ } // Still running; the durable result may appear later.
4747
+ };
4748
+ if (profile === "agy" && isAgyClaudeModel(opts.model) && opts.headless && resultJson) {
4749
+ // A fast provider refusal may finish before the host PID probe.
4750
+ // Return that typed completion rather than a misleading PID error.
4751
+ for (let attempt = 0; attempt < 5; attempt++) {
4752
+ if (reportAgyCompletion())
4753
+ return;
4754
+ sleepSync(50);
4755
+ }
4756
+ }
4757
+ // R3 P1: prefer the pid/identity returned by the single-shot
4758
+ // launch (no fork); fall back to probes only when absent (e.g.
4759
+ // tmux path or adopted pre-existing session without a fresh pid).
4579
4760
  const pid = sessionHost === "native"
4580
- ? nativeSessionPid(name)
4761
+ ? (launchedPid ?? nativeSessionPid(name))
4581
4762
  : agentPane
4582
4763
  ? localSessionPanePid(agentPane)
4583
4764
  : undefined;
4584
4765
  const workerPid = sessionHost === "native"
4585
- ? nativeWorkerPid(name)
4766
+ ? (launchedWorkerPid ?? nativeWorkerPid(name))
4586
4767
  : agentPane !== undefined
4587
4768
  ? paneWorkerPid(agentPane)
4588
4769
  : undefined;
4589
4770
  if (structuredLaunch && pid === undefined) {
4771
+ // The supervisor can publish and exit DURING the PID probe.
4772
+ if (reportAgyCompletion())
4773
+ return;
4590
4774
  cleanupHeadlessPromptFile(promptFile);
4591
4775
  // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4592
4776
  // `name`/`sessionHost` are the session and host THIS command
@@ -4602,8 +4786,8 @@ export async function main(argv) {
4602
4786
  return;
4603
4787
  }
4604
4788
  // Auto-enroll in the live-session registry (feeds `h2a ls`/`restore`).
4605
- // Pin the gateway posture ONLY when the user was explicit (--gw/--no-gw);
4606
- // an "auto" launch stays unpinned so restore follows the live default.
4789
+ // A CLI choice or a proven pin for this exact convId/cwd/tool
4790
+ // survives enrollment under a new name. Legacy/auto stays unpinned.
4607
4791
  enrollFromRun({
4608
4792
  profile,
4609
4793
  slug,
@@ -4615,8 +4799,10 @@ export async function main(argv) {
4615
4799
  cwd,
4616
4800
  sessionClass,
4617
4801
  ...(opts.resume !== undefined ? { convId: opts.resume } : {}),
4618
- gatewayMode: launchGatewayMode,
4619
- bare: useBare,
4802
+ ...(gatewayMode !== "auto" || pinnedLaunch?.gatewayProvenance === "explicit"
4803
+ ? { gatewayMode: launchGatewayMode, gatewayProvenance: "explicit" }
4804
+ : {}),
4805
+ ...(enrollmentBare !== undefined ? { bare: enrollmentBare } : {}),
4620
4806
  });
4621
4807
  const nativeOwnerSocket = launchOwnership?.host === "native"
4622
4808
  ? launchOwnership.sessions.find(owned => owned.name === name)?.socketPath : undefined;
@@ -4656,6 +4842,8 @@ export async function main(argv) {
4656
4842
  tmuxSession: only.name,
4657
4843
  host: sessionHost === "native" ? "native" : "tmux",
4658
4844
  profile,
4845
+ ...(opts.model ? { model: opts.model, modelSource: "launch-argument", effectiveModel: "unknown" } : {}),
4846
+ ...(profile === "agy" && isAgyClaudeModel(opts.model) ? { quota: readAgyClaudeQuotaStatus() } : {}),
4659
4847
  workspace: cwd,
4660
4848
  mode: opts.headless ? "headless" : "interactive",
4661
4849
  background: opts.background === true,
@@ -6140,6 +6328,9 @@ export async function main(argv) {
6140
6328
  ...(entry.gatewayMode !== undefined
6141
6329
  ? { gatewayMode: entry.gatewayMode }
6142
6330
  : {}),
6331
+ ...(entry.gatewayProvenance !== undefined
6332
+ ? { gatewayProvenance: entry.gatewayProvenance }
6333
+ : {}),
6143
6334
  ...(entry.bare !== undefined ? { bare: entry.bare } : {}),
6144
6335
  }));
6145
6336
  const result = await executeNativeRestart({
@@ -6194,7 +6385,9 @@ export async function main(argv) {
6194
6385
  ...(candidate.convId !== undefined
6195
6386
  ? { convId: candidate.convId }
6196
6387
  : {}),
6197
- ...(mode !== "auto" ? { gatewayMode: mode } : {}),
6388
+ ...(mode !== "auto"
6389
+ ? { gatewayMode: mode, gatewayProvenance: "explicit" }
6390
+ : {}),
6198
6391
  ...(candidate.bare !== undefined ? { bare: candidate.bare } : {}),
6199
6392
  });
6200
6393
  }
@@ -6307,7 +6500,7 @@ export async function main(argv) {
6307
6500
  process.exitCode = 1;
6308
6501
  return;
6309
6502
  }
6310
- const relaunchMode = gatewayModeForProfile(nativeEntry.tool, nativeEntry.gatewayMode ?? "auto");
6503
+ const relaunchMode = resolveResumedGatewayMode(nativeEntry.tool, "auto", nativeEntry.gatewayMode, nativeEntry.gatewayProvenance);
6311
6504
  await injectLlmMeshGatewayEnv(relaunchMode, relaunchMode !== "gateway", actName);
6312
6505
  if (!killNativeSessionTree(actName)) {
6313
6506
  process.stderr.write(`[h2a] native session ${slug} could not be killed; nothing was relaunched.\n`);
@@ -6334,7 +6527,15 @@ export async function main(argv) {
6334
6527
  ...(relaunched.pid !== undefined ? { pid: relaunched.pid } : {}),
6335
6528
  cwd: nativeEntry.cwd,
6336
6529
  sessionClass: nativeEntry.sessionClass ?? "human",
6337
- gatewayMode: relaunchMode,
6530
+ // No new CLI choice here: preserve the recorded pin verbatim so a
6531
+ // proven choice survives the relaunch and a legacy row keeps its
6532
+ // (unproven) data without gaining authority.
6533
+ ...(nativeEntry.gatewayMode !== undefined
6534
+ ? { gatewayMode: nativeEntry.gatewayMode }
6535
+ : {}),
6536
+ ...(nativeEntry.gatewayProvenance !== undefined
6537
+ ? { gatewayProvenance: nativeEntry.gatewayProvenance }
6538
+ : {}),
6338
6539
  bare: useBare,
6339
6540
  ...(nativeEntry.convId ? { convId: nativeEntry.convId } : {}),
6340
6541
  });
@@ -6464,14 +6665,55 @@ export async function main(argv) {
6464
6665
  if (!forced) {
6465
6666
  let ok = 0;
6466
6667
  for (const action of plan.actions) {
6467
- const sessionId = entryByTmuxSession.get(action.name)?.id;
6468
- const relaunched = sessionId !== undefined
6469
- ? relaunchSessionWithLease({
6470
- sessionId,
6471
- tmuxSession: action.name,
6472
- command: action.cmd,
6473
- })
6474
- : relaunchInSession(action.name, action.cmd);
6668
+ // The pane is NOT recreated, so apply the routing policy to the
6669
+ // retyped command itself: resolve the session's consent, scrub the
6670
+ // routing vars for direct, or pass the freshly acquired gateway
6671
+ // route: the idle shell may carry an expired bearer or no route.
6672
+ const retypeEntry = entryByTmuxSession.get(action.name);
6673
+ const retypeMode = resolveResumedGatewayMode(action.profile, "auto", retypeEntry?.gatewayMode, retypeEntry?.gatewayProvenance);
6674
+ let retypeCmd = action.cmd;
6675
+ let cleanupRetypeEnv = () => { };
6676
+ if (retypeMode === "gateway") {
6677
+ // Validate the explicit choice before retyping: fail closed when
6678
+ // the gateway is unavailable instead of typing a doomed command.
6679
+ // Hand off privately before restoring this process's environment.
6680
+ const restoreRetypeEnv = replaceAnthropicGatewayEnvironment(process.env, captureAnthropicGatewayEnvironment());
6681
+ try {
6682
+ await injectLlmMeshGatewayEnv("gateway", false, localSessionName(action.slug));
6683
+ const prepared = prepareGatewayRetypeCommand(action.cmd, {
6684
+ ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
6685
+ ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
6686
+ });
6687
+ retypeCmd = prepared.command;
6688
+ cleanupRetypeEnv = prepared.cleanup;
6689
+ }
6690
+ catch (error) {
6691
+ process.stderr.write(`[h2a] FAILED to relaunch ${action.slug}: ${error.message}\n`);
6692
+ continue;
6693
+ }
6694
+ finally {
6695
+ restoreRetypeEnv();
6696
+ }
6697
+ }
6698
+ else {
6699
+ retypeCmd = scrubbedRetypeCommand(action.cmd);
6700
+ }
6701
+ const sessionId = retypeEntry?.id;
6702
+ let relaunched = false;
6703
+ try {
6704
+ relaunched = sessionId !== undefined
6705
+ ? relaunchSessionWithLease({
6706
+ sessionId,
6707
+ tmuxSession: action.name,
6708
+ command: retypeCmd,
6709
+ })
6710
+ : relaunchInSession(action.name, retypeCmd);
6711
+ }
6712
+ finally {
6713
+ // A successful retype transfers cleanup to the private reader.
6714
+ if (!relaunched)
6715
+ cleanupRetypeEnv();
6716
+ }
6475
6717
  if (relaunched) {
6476
6718
  ok += 1;
6477
6719
  process.stderr.write(`[h2a] relaunched ${action.slug}: ${action.cmd}\n`);
@@ -6547,6 +6789,11 @@ export async function main(argv) {
6547
6789
  }
6548
6790
  finalReady.push(item);
6549
6791
  }
6792
+ // Isolate the routing env per session: a gateway relaunch deletes the
6793
+ // native ANTHROPIC_API_KEY from the shared process env, so without a
6794
+ // per-iteration restore the next (direct) session would lose native
6795
+ // auth. Every iteration starts from the loop-entry environment.
6796
+ const loopEntryGatewayEnv = captureAnthropicGatewayEnvironment();
6550
6797
  let ok = 0;
6551
6798
  for (const { action, entry, sessionClass } of finalReady) {
6552
6799
  const safety = sessionRelaunchSafety(action.name);
@@ -6573,8 +6820,9 @@ export async function main(argv) {
6573
6820
  process.stderr.write(`[h2a] skipped ${action.slug}: tmux session ${action.name} changed or was no longer proven dead immediately before kill.\n`);
6574
6821
  continue;
6575
6822
  }
6823
+ const restoreLoopEnv = replaceAnthropicGatewayEnvironment(process.env, loopEntryGatewayEnv);
6576
6824
  try {
6577
- const gatewayMode = gatewayModeForProfile(action.profile, entry.gatewayMode ?? "auto");
6825
+ const gatewayMode = resolveResumedGatewayMode(action.profile, "auto", entry.gatewayMode, entry.gatewayProvenance);
6578
6826
  await injectLlmMeshGatewayEnv(gatewayMode, true, localSessionName(action.slug));
6579
6827
  const { name, agentPane } = startLocalSession(action.profile, action.command, entry.cwd, localResumeArgs(action.profile, action.convId, {
6580
6828
  bare: resolveClaudeBare(action.profile, entry.bare),
@@ -6596,6 +6844,9 @@ export async function main(argv) {
6596
6844
  ...(entry.gatewayMode !== undefined
6597
6845
  ? { gatewayMode: entry.gatewayMode }
6598
6846
  : {}),
6847
+ ...(entry.gatewayProvenance !== undefined
6848
+ ? { gatewayProvenance: entry.gatewayProvenance }
6849
+ : {}),
6599
6850
  ...(entry.bare !== undefined ? { bare: entry.bare } : {}),
6600
6851
  });
6601
6852
  const wake = wakeRelaunchedSession(relaunchContinuationPrompt(entry.task), {
@@ -6628,6 +6879,9 @@ export async function main(argv) {
6628
6879
  catch (error) {
6629
6880
  process.stderr.write(`[h2a] FAILED to force-restart ${action.slug} after killing ${action.name}: ${error.message}\n`);
6630
6881
  }
6882
+ finally {
6883
+ restoreLoopEnv();
6884
+ }
6631
6885
  }
6632
6886
  process.stderr.write(`[h2a] force-restarted ${ok}/${plan.actions.length}${plan.skipped.length ? `, ${plan.skipped.length} skipped` : ""}\n`);
6633
6887
  if (ok !== plan.actions.length)
@@ -6991,13 +7245,13 @@ export async function main(argv) {
6991
7245
  }
6992
7246
  if (opts.reattach)
6993
7247
  restoreOpts.reattach = true;
6994
- // Forced-direct restore must NOT spin up the gateway; otherwise prepare
6995
- // it as before (no-op when the gateway is disabled).
6996
- if (forceGateway !== "direct") {
7248
+ // Only explicit --gw prewarms globally. In auto mode, any pinned
7249
+ // gateway session acquires its gateway through its own launch.
7250
+ if (forceGateway === "gateway") {
6997
7251
  try {
6998
7252
  await prepareLlmMeshForRestore({
6999
7253
  dryRun: Boolean(opts.dryRun),
7000
- mode: forceGateway ?? "auto",
7254
+ mode: forceGateway,
7001
7255
  });
7002
7256
  }
7003
7257
  catch (error) {
@@ -7531,13 +7785,29 @@ export async function main(argv) {
7531
7785
  llmMeshAccountCommand
7532
7786
  .command("enroll <provider>")
7533
7787
  .description("Enroll through the sentropic-owned OAuth state machine " +
7534
- "(cloud-code, codex, muse OAuth device flow, or mistral-vibe browser sign-in — alias: vibe). " +
7535
- "--cli imports credentials from the provider's local CLI store instead (muse only)")
7788
+ "(cloud-code, codex, muse OAuth device flow, mistral-vibe browser sign-in — alias: vibe, or claude-code). " +
7789
+ "--from oauth|cli|token selects the source (default oauth): cli reads a private L1 parser profile selected by --seat-profile; " +
7790
+ "token reads a consumer SeatBundle via bounded stdin or masked input for all five CLIs. " +
7791
+ "--cli / --token are the same modes as booleans. Seat aliases: agy/cloud-code, claude/claude-code, vibe/mistral-vibe")
7536
7792
  .option("--config-ref <ref>", "sentropic configuration reference for OAuth")
7537
- .option("--cli", "import credentials from the provider's local CLI store (muse only) instead of the OAuth flow")
7538
- .action(async (provider, opts) => {
7539
- // "vibe" is the CLI-friendly alias of the mistral-vibe transport.
7540
- const normalized = provider === "vibe" ? "mistral-vibe" : provider;
7793
+ .option("--from <mode>", "enrollment source: oauth (default), cli (private L1 seat profile) or token (consumer SeatBundle on stdin)")
7794
+ .option("--cli", "import a consumer seat from the explicitly selected --seat-profile. Takes no value")
7795
+ .option("--token", "import a consumer SeatBundle for any of the five CLIs via bounded stdin or masked input; never argv")
7796
+ .option("--seat-profile <path>", "private L1 CliSeatProfileInput file for --from cli; no ambient store fallback")
7797
+ .allowExcessArguments(true)
7798
+ .action(async (provider, opts, command) => {
7799
+ // Static excess-argument refusal (m1): commander would otherwise echo
7800
+ // every positional value (including a secret) on stderr before the
7801
+ // action runs. Excess positionals are refused here without echoing
7802
+ // any value. Token material travels on stdin only, never argv.
7803
+ const positionalCount = Array.isArray(command?.args) ? command.args.length : 1;
7804
+ if (positionalCount > 1) {
7805
+ process.stderr.write("[h2a] llm-mesh account: too many arguments (expected provider only; never pass secrets in argv)\n");
7806
+ process.exitCode = 1;
7807
+ return;
7808
+ }
7809
+ // Seat names and enrollment providers share the five-CLI codec map.
7810
+ const normalized = ["agy", "claude", "vibe"].includes(provider) ? resolveSeatEnrollmentProvider(provider) : provider;
7541
7811
  if (normalized === "muse-code") {
7542
7812
  // Owner decision 2026-10-10: muse-code is no longer a user entry —
7543
7813
  // muse enrolls via the OAuth device flow by default, --cli selects
@@ -7547,20 +7817,37 @@ export async function main(argv) {
7547
7817
  process.exitCode = 1;
7548
7818
  return;
7549
7819
  }
7820
+ if (opts.from !== undefined && opts.from !== "oauth" && opts.from !== "cli" && opts.from !== "token") {
7821
+ process.stderr.write("[h2a] llm-mesh account: --from must be one of oauth, cli, token\n");
7822
+ process.exitCode = 1;
7823
+ return;
7824
+ }
7825
+ if (opts.from !== undefined && (opts.cli || opts.token)) {
7826
+ process.stderr.write("[h2a] llm-mesh account: --from cannot be combined with --cli or --token\n");
7827
+ process.exitCode = 1;
7828
+ return;
7829
+ }
7830
+ if (opts.cli && opts.token) {
7831
+ process.stderr.write("[h2a] llm-mesh account: --cli and --token cannot be combined\n");
7832
+ process.exitCode = 1;
7833
+ return;
7834
+ }
7550
7835
  if (normalized !== "cloud-code" &&
7551
7836
  normalized !== "codex" &&
7552
7837
  normalized !== "muse" &&
7553
- normalized !== "mistral-vibe") {
7554
- process.stderr.write(`[h2a] llm-mesh account: unsupported provider "${provider}". ` +
7555
- "Supported: cloud-code, codex, muse (OAuth; --cli imports the CLI store), mistral-vibe (alias: vibe)\n");
7838
+ normalized !== "mistral-vibe" &&
7839
+ normalized !== "claude-code") {
7840
+ process.stderr.write("[h2a] llm-mesh account: unsupported provider. " +
7841
+ "Supported: cloud-code (agy), codex, muse, mistral-vibe (vibe), claude-code (claude); oauth is the default, cli/token import consumer seats\n");
7556
7842
  process.exitCode = 1;
7557
7843
  return;
7558
7844
  }
7559
7845
  try {
7560
- const account = await enrollViaFacade(normalized, {
7561
- ...(opts.configRef ? { configRef: opts.configRef } : {}),
7562
- ...(opts.cli ? { cliImport: true } : {}),
7563
- });
7846
+ const source = opts.from ?? (opts.cli ? "cli" : opts.token ? "token" : "oauth");
7847
+ const { enrollViaFacade } = await import("./llm-mesh-accounts.js");
7848
+ const account = source === "oauth"
7849
+ ? await enrollViaFacade(normalized, { ...(opts.configRef ? { configRef: opts.configRef } : {}), from: "oauth" })
7850
+ : await enrollSeatConsumerViaFacade(normalized, { from: source, ...(opts.seatProfile ? { seatProfile: opts.seatProfile } : {}) });
7564
7851
  process.stdout.write(`[h2a] llm-mesh account: enrolled ${account.provider}; ` +
7565
7852
  "account inventory remains in Sentropic\n");
7566
7853
  // The gateway loads the account inventory at boot only: an account
@@ -7582,6 +7869,7 @@ export async function main(argv) {
7582
7869
  .description("List public accounts owned by this local llm-mesh scope")
7583
7870
  .option("--json", "emit public account metadata as JSON")
7584
7871
  .action(async (opts) => {
7872
+ const { listAccountsViaFacade, formatLlmMeshAccountList, formatLlmMeshAccountError } = await import("./llm-mesh-accounts.js");
7585
7873
  try {
7586
7874
  const accounts = await listAccountsViaFacade();
7587
7875
  process.stdout.write(`${formatLlmMeshAccountList(accounts, opts.json === true)}\n`);
@@ -7596,6 +7884,7 @@ export async function main(argv) {
7596
7884
  .aliases(["rm", "unenroll"])
7597
7885
  .description("Remove one account owned by this local llm-mesh scope")
7598
7886
  .action(async (accountId) => {
7887
+ const { removeAccountViaFacade, formatLlmMeshAccountError } = await import("./llm-mesh-accounts.js");
7599
7888
  try {
7600
7889
  const result = await removeAccountViaFacade(accountId);
7601
7890
  process.stdout.write(`[h2a] llm-mesh account: removed ${result.accountId}\n`);
@@ -7661,17 +7950,15 @@ export async function main(argv) {
7661
7950
  const config = readLlmMeshConfig() ?? {};
7662
7951
  const existing = readGatewayPid();
7663
7952
  if (existing) {
7664
- setLlmMeshRuntimeConfig({ enabled: true });
7953
+ // Starting the service is not routing consent: launches stay direct
7954
+ // unless --gw is passed, so the durable flag is left untouched.
7665
7955
  process.stdout.write(`[h2a] llm-mesh: already running (pid ${existing})\n`);
7666
- process.stdout.write(`[h2a] llm-mesh: restore config enabled\n`);
7667
7956
  return;
7668
7957
  }
7669
7958
  process.stdout.write(`[h2a] llm-mesh: starting gateway…\n`);
7670
7959
  const result = await startGateway(config, { verbose: opts.verbose });
7671
- setLlmMeshRuntimeConfig({ enabled: true });
7672
7960
  const port = result.port;
7673
7961
  process.stdout.write(`[h2a] llm-mesh: gateway started (pid ${result.pid}, port ${port})\n`);
7674
- process.stdout.write(`[h2a] llm-mesh: restore config enabled\n`);
7675
7962
  process.stdout.write("[h2a] llm-mesh: use `h2a run claude --gw`; the opaque bearer is not printed\n");
7676
7963
  });
7677
7964
  llmMeshCommand
@@ -7699,6 +7986,9 @@ export async function main(argv) {
7699
7986
  else {
7700
7987
  process.stdout.write(`[h2a] llm-mesh: not running\n`);
7701
7988
  }
7989
+ // A previous start must not keep authorizing reactivation: stopping the
7990
+ // service clears the durable flag so a later restore stays direct.
7991
+ setLlmMeshRuntimeConfig({ enabled: false });
7702
7992
  });
7703
7993
  llmMeshCommand
7704
7994
  .command("restart")
@@ -7715,9 +8005,9 @@ export async function main(argv) {
7715
8005
  }
7716
8006
  process.stdout.write(`[h2a] llm-mesh: starting gateway…\n`);
7717
8007
  const result = await startGateway(config, { verbose: opts.verbose });
7718
- setLlmMeshRuntimeConfig({ enabled: true });
8008
+ // Restarting the service is not routing consent: the durable flag is
8009
+ // left untouched so a later restore stays direct unless --gw is passed.
7719
8010
  process.stdout.write(`[h2a] llm-mesh: gateway restarted (pid ${result.pid}, port ${result.port})\n`);
7720
- process.stdout.write(`[h2a] llm-mesh: restore config enabled\n`);
7721
8011
  process.stdout.write(`[h2a] llm-mesh: no tmux/Claude session was restarted; active sessions may keep their old gateway token until explicitly relaunched.\n`);
7722
8012
  process.stdout.write("[h2a] llm-mesh: use `h2a run claude --gw`; the opaque bearer is not printed\n");
7723
8013
  });
@@ -7897,7 +8187,25 @@ export async function main(argv) {
7897
8187
  applyRuntimeHelpGroups(program.createHelp().visibleCommands(program));
7898
8188
  program.configureHelp({ groupItems: groupRuntimeHelpItems });
7899
8189
  program.addHelpText("after", "\nGrouped map of every h2a command (core + runtime): h2a explain\n");
7900
- await program.parseAsync([...argv]);
8190
+ try {
8191
+ await program.parseAsync([...argv]);
8192
+ }
8193
+ catch (err) {
8194
+ // Commander errors already wrote their static per-code message in
8195
+ // `exitOverride`; only set the exit code here, never `err.message`.
8196
+ if (err !== null &&
8197
+ typeof err === "object" &&
8198
+ "code" in err &&
8199
+ typeof err.code === "string") {
8200
+ const code = err.code;
8201
+ if (code.startsWith("commander.")) {
8202
+ const exitCode = err.exitCode;
8203
+ process.exitCode = typeof exitCode === "number" ? exitCode : 1;
8204
+ return process.exitCode;
8205
+ }
8206
+ }
8207
+ throw err;
8208
+ }
7901
8209
  const code = process.exitCode;
7902
8210
  return typeof code === "number" ? code : 0;
7903
8211
  }
@@ -7931,6 +8239,13 @@ export { main as dispatchH2a };
7931
8239
  /** @deprecated Compatibility for older h2a cores; new cores require dispatchH2a. */
7932
8240
  export { main as dispatch };
7933
8241
  export { ensureCentralMcp } from "./central-mcp.js";
8242
+ /** Offline five-CLI seat contract and run-scoped materialization. */
8243
+ export * from "./cli-seat-fixtures.js";
8244
+ export * from "./cli-seat-codecs.js";
8245
+ export * from "./cli-seat-materialization.js";
8246
+ // Keep the public async consumer contract without loading account providers in
8247
+ // every local crash-recovery launcher before it can elect its native host.
8248
+ export const enrollSeatConsumerViaFacade = async (...args) => (await import("./llm-mesh-accounts.js")).enrollSeatConsumerViaFacade(...args);
7934
8249
  /**
7935
8250
  * h2a-facing surface: the read-only remote-agents projection consumed by the
7936
8251
  * objective-loop tick's *agents adapter* in `@sentropic/h2a` (via lazy