@sentropic/h2a-runtime 0.98.6 → 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 (236) 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 +5 -0
  13. package/dist/attach.d.ts.map +1 -1
  14. package/dist/attach.js +75 -6
  15. package/dist/attach.js.map +1 -1
  16. package/dist/auth-login.d.ts +24 -1
  17. package/dist/auth-login.d.ts.map +1 -1
  18. package/dist/auth-login.js +132 -4
  19. package/dist/auth-login.js.map +1 -1
  20. package/dist/central-mcp.d.ts +34 -0
  21. package/dist/central-mcp.d.ts.map +1 -1
  22. package/dist/central-mcp.js +275 -71
  23. package/dist/central-mcp.js.map +1 -1
  24. package/dist/claude-code-cli-isolation.d.ts +53 -0
  25. package/dist/claude-code-cli-isolation.d.ts.map +1 -0
  26. package/dist/claude-code-cli-isolation.js +277 -0
  27. package/dist/claude-code-cli-isolation.js.map +1 -0
  28. package/dist/claude-code-cli-lifecycle.d.ts +80 -0
  29. package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
  30. package/dist/claude-code-cli-lifecycle.js +402 -0
  31. package/dist/claude-code-cli-lifecycle.js.map +1 -0
  32. package/dist/claude-code-cli-runner.d.ts +89 -0
  33. package/dist/claude-code-cli-runner.d.ts.map +1 -0
  34. package/dist/claude-code-cli-runner.js +659 -0
  35. package/dist/claude-code-cli-runner.js.map +1 -0
  36. package/dist/claude-seat-input.d.ts +47 -0
  37. package/dist/claude-seat-input.d.ts.map +1 -0
  38. package/dist/claude-seat-input.js +272 -0
  39. package/dist/claude-seat-input.js.map +1 -0
  40. package/dist/claude-seat-mesh-contract.d.ts +88 -0
  41. package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
  42. package/dist/claude-seat-mesh-contract.js +13 -0
  43. package/dist/claude-seat-mesh-contract.js.map +1 -0
  44. package/dist/cli-help-groups.js +1 -1
  45. package/dist/cli-help-groups.js.map +1 -1
  46. package/dist/cli-seat-codecs.d.ts +86 -0
  47. package/dist/cli-seat-codecs.d.ts.map +1 -0
  48. package/dist/cli-seat-codecs.js +255 -0
  49. package/dist/cli-seat-codecs.js.map +1 -0
  50. package/dist/cli-seat-fixtures.d.ts +86 -0
  51. package/dist/cli-seat-fixtures.d.ts.map +1 -0
  52. package/dist/cli-seat-fixtures.js +114 -0
  53. package/dist/cli-seat-fixtures.js.map +1 -0
  54. package/dist/cli-seat-materialization.d.ts +245 -0
  55. package/dist/cli-seat-materialization.d.ts.map +1 -0
  56. package/dist/cli-seat-materialization.js +646 -0
  57. package/dist/cli-seat-materialization.js.map +1 -0
  58. package/dist/conv-guard.d.ts.map +1 -1
  59. package/dist/conv-guard.js +6 -2
  60. package/dist/conv-guard.js.map +1 -1
  61. package/dist/crash-log.d.ts +112 -0
  62. package/dist/crash-log.d.ts.map +1 -0
  63. package/dist/crash-log.js +560 -0
  64. package/dist/crash-log.js.map +1 -0
  65. package/dist/delegate.d.ts +10 -0
  66. package/dist/delegate.d.ts.map +1 -1
  67. package/dist/delegate.js +29 -0
  68. package/dist/delegate.js.map +1 -1
  69. package/dist/file-lock.d.ts +4 -2
  70. package/dist/file-lock.d.ts.map +1 -1
  71. package/dist/file-lock.js +24 -10
  72. package/dist/file-lock.js.map +1 -1
  73. package/dist/gateway-host/claude-seat-client.d.ts +32 -0
  74. package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
  75. package/dist/gateway-host/claude-seat-client.js +38 -0
  76. package/dist/gateway-host/claude-seat-client.js.map +1 -0
  77. package/dist/gateway-host/config-file.d.ts +2 -0
  78. package/dist/gateway-host/config-file.d.ts.map +1 -1
  79. package/dist/gateway-host/config-file.js +5 -1
  80. package/dist/gateway-host/config-file.js.map +1 -1
  81. package/dist/gateway-host/daemon.d.ts.map +1 -1
  82. package/dist/gateway-host/daemon.js +1 -2
  83. package/dist/gateway-host/daemon.js.map +1 -1
  84. package/dist/gateway-host/host.d.ts +6 -0
  85. package/dist/gateway-host/host.d.ts.map +1 -1
  86. package/dist/gateway-host/host.js +24 -3
  87. package/dist/gateway-host/host.js.map +1 -1
  88. package/dist/gateway-host/live-auth.d.ts +62 -0
  89. package/dist/gateway-host/live-auth.d.ts.map +1 -0
  90. package/dist/gateway-host/live-auth.js +87 -0
  91. package/dist/gateway-host/live-auth.js.map +1 -0
  92. package/dist/gateway-host/live-declarations.d.ts +58 -0
  93. package/dist/gateway-host/live-declarations.d.ts.map +1 -0
  94. package/dist/gateway-host/live-declarations.js +429 -0
  95. package/dist/gateway-host/live-declarations.js.map +1 -0
  96. package/dist/gateway-host/live-disclosure.d.ts +124 -0
  97. package/dist/gateway-host/live-disclosure.d.ts.map +1 -0
  98. package/dist/gateway-host/live-disclosure.js +347 -0
  99. package/dist/gateway-host/live-disclosure.js.map +1 -0
  100. package/dist/gateway-host/live-github.d.ts +11 -0
  101. package/dist/gateway-host/live-github.d.ts.map +1 -0
  102. package/dist/gateway-host/live-github.js +374 -0
  103. package/dist/gateway-host/live-github.js.map +1 -0
  104. package/dist/gateway-host/live-host.d.ts +20 -0
  105. package/dist/gateway-host/live-host.d.ts.map +1 -0
  106. package/dist/gateway-host/live-host.js +14 -0
  107. package/dist/gateway-host/live-host.js.map +1 -0
  108. package/dist/gateway-host/live-object-store.d.ts +35 -0
  109. package/dist/gateway-host/live-object-store.d.ts.map +1 -0
  110. package/dist/gateway-host/live-object-store.js +447 -0
  111. package/dist/gateway-host/live-object-store.js.map +1 -0
  112. package/dist/gateway-host/live-registration.d.ts +162 -0
  113. package/dist/gateway-host/live-registration.d.ts.map +1 -0
  114. package/dist/gateway-host/live-registration.js +455 -0
  115. package/dist/gateway-host/live-registration.js.map +1 -0
  116. package/dist/gateway-model-catalog/model-catalog.d.ts +38 -0
  117. package/dist/gateway-model-catalog/model-catalog.d.ts.map +1 -0
  118. package/dist/gateway-model-catalog/model-catalog.js +167 -0
  119. package/dist/gateway-model-catalog/model-catalog.js.map +1 -0
  120. package/dist/identity-cull/cull.js +1 -1
  121. package/dist/identity-cull/cull.js.map +1 -1
  122. package/dist/identity-cull/identity-fence.d.ts +48 -0
  123. package/dist/identity-cull/identity-fence.d.ts.map +1 -0
  124. package/dist/identity-cull/identity-fence.js +221 -0
  125. package/dist/identity-cull/identity-fence.js.map +1 -0
  126. package/dist/identity-operator-break.d.ts +43 -0
  127. package/dist/identity-operator-break.d.ts.map +1 -0
  128. package/dist/identity-operator-break.js +85 -0
  129. package/dist/identity-operator-break.js.map +1 -0
  130. package/dist/index.d.ts +8 -1
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +525 -132
  133. package/dist/index.js.map +1 -1
  134. package/dist/launch-layout.d.ts.map +1 -1
  135. package/dist/launch-layout.js +10 -2
  136. package/dist/launch-layout.js.map +1 -1
  137. package/dist/llm-mesh-accounts.d.ts +128 -6
  138. package/dist/llm-mesh-accounts.d.ts.map +1 -1
  139. package/dist/llm-mesh-accounts.js +370 -14
  140. package/dist/llm-mesh-accounts.js.map +1 -1
  141. package/dist/native-host.d.ts +25 -1
  142. package/dist/native-host.d.ts.map +1 -1
  143. package/dist/native-host.js +211 -28
  144. package/dist/native-host.js.map +1 -1
  145. package/dist/native-restart.d.ts +2 -0
  146. package/dist/native-restart.d.ts.map +1 -1
  147. package/dist/native-restart.js +7 -0
  148. package/dist/native-restart.js.map +1 -1
  149. package/dist/native-terminal/fleet.d.ts +11 -0
  150. package/dist/native-terminal/fleet.d.ts.map +1 -1
  151. package/dist/native-terminal/fleet.js +58 -1
  152. package/dist/native-terminal/fleet.js.map +1 -1
  153. package/dist/native-terminal/host-scope.d.ts +18 -0
  154. package/dist/native-terminal/host-scope.d.ts.map +1 -0
  155. package/dist/native-terminal/host-scope.js +65 -0
  156. package/dist/native-terminal/host-scope.js.map +1 -0
  157. package/dist/native-terminal/host.d.ts +7 -1
  158. package/dist/native-terminal/host.d.ts.map +1 -1
  159. package/dist/native-terminal/host.js +10 -26
  160. package/dist/native-terminal/host.js.map +1 -1
  161. package/dist/native-terminal/journal.d.ts +5 -0
  162. package/dist/native-terminal/journal.d.ts.map +1 -1
  163. package/dist/native-terminal/journal.js +28 -16
  164. package/dist/native-terminal/journal.js.map +1 -1
  165. package/dist/native-terminal/op.d.ts +49 -1
  166. package/dist/native-terminal/op.d.ts.map +1 -1
  167. package/dist/native-terminal/op.js +254 -38
  168. package/dist/native-terminal/op.js.map +1 -1
  169. package/dist/native-terminal/process.d.ts.map +1 -1
  170. package/dist/native-terminal/process.js +10 -1
  171. package/dist/native-terminal/process.js.map +1 -1
  172. package/dist/native-terminal/screen.d.ts.map +1 -1
  173. package/dist/native-terminal/screen.js +1 -1
  174. package/dist/native-terminal/screen.js.map +1 -1
  175. package/dist/native-terminal/server.d.ts +12 -2
  176. package/dist/native-terminal/server.d.ts.map +1 -1
  177. package/dist/native-terminal/server.js +143 -42
  178. package/dist/native-terminal/server.js.map +1 -1
  179. package/dist/native-terminal/session-scope.d.ts +2 -0
  180. package/dist/native-terminal/session-scope.d.ts.map +1 -0
  181. package/dist/native-terminal/session-scope.js +32 -0
  182. package/dist/native-terminal/session-scope.js.map +1 -0
  183. package/dist/proc-identity.d.ts +31 -0
  184. package/dist/proc-identity.d.ts.map +1 -1
  185. package/dist/proc-identity.js +54 -0
  186. package/dist/proc-identity.js.map +1 -1
  187. package/dist/prompt-delivery.d.ts +3 -0
  188. package/dist/prompt-delivery.d.ts.map +1 -1
  189. package/dist/prompt-delivery.js +8 -3
  190. package/dist/prompt-delivery.js.map +1 -1
  191. package/dist/protocol-local.d.ts +16 -0
  192. package/dist/protocol-local.d.ts.map +1 -1
  193. package/dist/protocol-local.js +14 -0
  194. package/dist/protocol-local.js.map +1 -1
  195. package/dist/registry.d.ts +16 -12
  196. package/dist/registry.d.ts.map +1 -1
  197. package/dist/registry.js +39 -5
  198. package/dist/registry.js.map +1 -1
  199. package/dist/relaunch.d.ts +16 -0
  200. package/dist/relaunch.d.ts.map +1 -1
  201. package/dist/relaunch.js +42 -1
  202. package/dist/relaunch.js.map +1 -1
  203. package/dist/restore.d.ts +4 -0
  204. package/dist/restore.d.ts.map +1 -1
  205. package/dist/restore.js +9 -37
  206. package/dist/restore.js.map +1 -1
  207. package/dist/resume-launch-options.d.ts +3 -3
  208. package/dist/resume-launch-options.d.ts.map +1 -1
  209. package/dist/resume-launch-options.js +7 -4
  210. package/dist/resume-launch-options.js.map +1 -1
  211. package/dist/run.d.ts +2 -0
  212. package/dist/run.d.ts.map +1 -1
  213. package/dist/run.js +88 -20
  214. package/dist/run.js.map +1 -1
  215. package/dist/soft-refresh.d.ts.map +1 -1
  216. package/dist/soft-refresh.js +6 -0
  217. package/dist/soft-refresh.js.map +1 -1
  218. package/dist/terminal-reset.d.ts +104 -0
  219. package/dist/terminal-reset.d.ts.map +1 -0
  220. package/dist/terminal-reset.js +229 -0
  221. package/dist/terminal-reset.js.map +1 -0
  222. package/dist/tmux.d.ts +54 -8
  223. package/dist/tmux.d.ts.map +1 -1
  224. package/dist/tmux.js +143 -36
  225. package/dist/tmux.js.map +1 -1
  226. package/dist/vendor/engram-live/contracts.d.ts +825 -0
  227. package/dist/vendor/engram-live/contracts.js +1734 -0
  228. package/dist/vendor/engram-live/integration.d.ts +113 -0
  229. package/dist/vendor/engram-live/integration.js +254 -0
  230. package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
  231. package/dist/vendor/engram-live/pr-sync.js +411 -0
  232. package/dist/vendor/engram-live/registration.d.ts +138 -0
  233. package/dist/vendor/engram-live/registration.js +358 -0
  234. package/dist/vendor/engram-live/retention.d.ts +56 -0
  235. package/dist/vendor/engram-live/retention.js +179 -0
  236. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -1,15 +1,19 @@
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";
12
- import { prepareCentralMcpForLaunch, prepareCentralMcpForRestore, } from "./central-mcp.js";
14
+ import { prepareCentralMcpForLaunch, prepareCentralMcpForRestore, resolveCentralMcpLogPath, } from "./central-mcp.js";
15
+ import { CRASH_LOG_COMPONENTS, formatCrashLogHumanRow, installCliWitnessOnce, installCrashHandlers, readFileTailLines, redactCrashText, readRecentCrashLogs, resolveCrashLogDir, } from "./crash-log.js";
16
+ import { readHostJournal, resolveHostJournalPath } from "./native-terminal/journal.js";
13
17
  import { ensureConnected, stopTunnel } from "./tunnel.js";
14
18
  import { detectToolAuth, KNOWN_TOOLS, partitionTools } from "./auth-tools.js";
15
19
  import { transmittedSecrets, secretsSummary } from "./secrets.js";
@@ -20,13 +24,14 @@ import { attachLocalSession, attachPodTmux, capturePane, capturePaneVisible, cle
20
24
  import { deliverInitialPrompt, sleepSync, } from "./prompt-delivery.js";
21
25
  import { startLaunchGuard } from "./launch-guard.js";
22
26
  import { buildLaunchContext } from "./launch-context.js";
23
- 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";
24
28
  import { executeNativeRestart, NativeRestartError, } from "./native-restart.js";
25
29
  import { migrateTmuxNames } from "./tmux-name-migration.js";
26
30
  import { projectStatusForH2a } from "./status-projection.js";
27
31
  import { buildAgentLaunchArgs, buildAgentLaunchStdin, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
32
+ import { resolveGatewayModelAlias } from "./gateway-model-catalog/model-catalog.js";
28
33
  import { deriveSessionClass } from "./session-class.js";
29
- import { isRelaunchKillable, planRelaunch, relaunchContinuationPrompt, tryAcquireResumeLaunchClaim, wakeRelaunchedSession, } from "./relaunch.js";
34
+ import { isRelaunchKillable, planRelaunch, relaunchContinuationPrompt, scrubbedRetypeCommand, prepareGatewayRetypeCommand, tryAcquireResumeLaunchClaim, wakeRelaunchedSession, } from "./relaunch.js";
30
35
  import { isHumanFacingSession, legacySessionEvidence, readConversationCustomTitle, readLastLayout, reconcileRunConvIds, restore as restoreLayout, } from "./restore.js";
31
36
  import { getLayoutConfig } from "./config.js";
32
37
  import { resolvePinnedLaunchEntries } from "./resume-launch-options.js";
@@ -40,7 +45,11 @@ import { DEFAULT_FANOUT_MAX, mapWithConcurrency, planRemoteFanout, } from "./fan
40
45
  import { authenticateJobEnvelopes, buildDecisionReply, dropEnvelope, emitJobDone, envelopeFileName, isAwaitingDecision, jobInstance, parentInstance, pendingDecisions, readInboxEnvelopes, renderPendingDecisions, repliedDecisionJobIds, } from "./h2a-jobs.js";
41
46
  import { buildConductorTask, computeDurableWorkspaceId, normalizeRootCommits, detectAvailableHosts, freshestLaunchEnvelope, h2aReportsLiveConductor, markLaunchEnvelopeProcessed, readLastLaunchAt, readLaunchEnvelopes, recordLaunchAt, selectHost, shouldLaunch, } from "./conductor-launch.js";
42
47
  import { defaultLocalH2aRoot } from "./h2a-bridge.js";
43
- 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.
44
53
  import { guardConvWriters } from "./conv-guard.js";
45
54
  import { handleClaudeHook, installClaudeHooks, manualEnroll, readStdin, } from "./enroll.js";
46
55
  import { probePodCredHealth, softRefreshSession } from "./soft-refresh.js";
@@ -72,8 +81,7 @@ import { createInterface } from "node:readline";
72
81
  const H2A_RUN_API_VERSION = "h2a.run/v1";
73
82
  const STRUCTURED_LAUNCH_PHASE_PREFIX = "[h2a] h2a.run.phase/v1 ";
74
83
  const H2A_RUNTIME_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
75
- import { CLI_PROFILES, gatewayModeForProfile, profileUsesLlmMeshGateway, } from "./protocol-local.js";
76
- import { enrollViaFacade, formatLlmMeshAccountError, formatLlmMeshAccountList, listAccountsViaFacade, removeAccountViaFacade, } from "./llm-mesh-accounts.js";
84
+ import { CLI_PROFILES, gatewayModeForProfile, profileUsesLlmMeshGateway, resolveResumedGatewayMode, } from "./protocol-local.js";
77
85
  import { readLlmMeshConfig, updateLlmMeshRoutingConfig, } from "./gateway-host/config-file.js";
78
86
  import { startGateway, stopGateway, readGatewayPid, llmMeshLogPath, replaceAnthropicGatewayEnvironment, acquireLlmMeshSessionEnv, } from "./gateway-host/daemon.js";
79
87
  import { describeLlmMeshRoutingConfig, parseLlmMeshRoutingConfig, preferredRoutingConfig, strategyRoutingConfig, } from "./routing-preferences.js";
@@ -133,6 +141,7 @@ async function startRemoteFanout(remote, profileName, members, spec) {
133
141
  ? { startupArgs: spec.startupArgs }
134
142
  : {}),
135
143
  ...(spec.credentials ? { credentials: spec.credentials } : {}),
144
+ ...(spec.llmGateway === true ? { llmGateway: true } : {}),
136
145
  });
137
146
  return { name: member.name, sessionId: session.id, workspaceId: ws.id };
138
147
  });
@@ -198,6 +207,20 @@ async function runProfile(profileName, opts, commandArgs = []) {
198
207
  if (opts.resume !== undefined && resumeArgs.length === 0) {
199
208
  throw new Error(`profile "${profileName}" has no verified resume argv; start it without -r/--resume`);
200
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 } : {};
201
224
  const startupArgs = [...resumeArgs, ...commandArgs];
202
225
  // WP6 — REMOTE fan-out: --count N spawns N concurrent remote sessions, each
203
226
  // on its OWN workspace subPath of the shared RWX volume. count<=1 falls
@@ -227,6 +250,7 @@ async function runProfile(profileName, opts, commandArgs = []) {
227
250
  target: opts.target,
228
251
  startupArgs,
229
252
  ...(credentials ? { credentials } : {}),
253
+ ...remoteGatewayChoice,
230
254
  });
231
255
  return;
232
256
  }
@@ -245,6 +269,7 @@ async function runProfile(profileName, opts, commandArgs = []) {
245
269
  ...(opts.workspaceId ? { workspaceId: opts.workspaceId } : {}),
246
270
  ...(opts.browser ? { metadata: { browser: true } } : {}),
247
271
  ...(opts.agentImage ? { agentImage: opts.agentImage } : {}),
272
+ ...remoteGatewayChoice,
248
273
  });
249
274
  if (archive) {
250
275
  await uploadWorkspaceArchive(opts.remote, session.id, archive);
@@ -268,7 +293,21 @@ async function runProfile(profileName, opts, commandArgs = []) {
268
293
  ...(opts.port !== undefined ? { port: opts.port } : {}),
269
294
  ...(commandArgs.length > 0 ? { startupArgs: commandArgs } : {}),
270
295
  };
271
- 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
+ }
272
311
  process.stderr.write(`[h2a] session ${result.sessionId} attach at http://127.0.0.1:${result.port}\n`);
273
312
  const { exitCode } = await result.exit;
274
313
  process.exitCode = exitCode;
@@ -850,32 +889,12 @@ export async function startJob(job) {
850
889
  const prevJobId = process.env[JOB_ID_ENV];
851
890
  process.env[DEPTH_ENV] = childDepthEnvValue(job.depthBudget ?? clampDepth(undefined));
852
891
  process.env[JOB_ID_ENV] = job.id;
853
- // Inject llm-mesh gateway env if running. Do not trust a parent env that may
854
- // contain an old gateway token from a previous restart.
855
- // tmux inherits process.env, so claude + its subagents all get the gateway automatically.
856
- const jobSessionSlug = slugify(job.id);
857
- const jobSessionResolution = resolveLocalSession(jobSessionSlug);
858
- const gatewayClientSessionId = jobSessionResolution.kind === "found"
859
- ? jobSessionResolution.session.name
860
- : jobSessionResolution.kind === "missing"
861
- ? localSessionName(jobSessionSlug)
862
- : undefined;
863
- let meshEnv;
864
- try {
865
- meshEnv =
866
- profileUsesLlmMeshGateway(job.tool) &&
867
- getLlmMeshRuntimeConfig().enabled &&
868
- gatewayClientSessionId
869
- ? await acquireLlmMeshSessionEnv(undefined, gatewayClientSessionId)
870
- : null;
871
- }
872
- catch (error) {
873
- return {
874
- started: false,
875
- error: error instanceof Error ? error.message : String(error),
876
- };
877
- }
878
- 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);
879
898
  let tmuxSession;
880
899
  let workerPid;
881
900
  let agentPane;
@@ -1397,13 +1416,12 @@ export function bareChoiceFromOptions(opts) {
1397
1416
  return false;
1398
1417
  return undefined;
1399
1418
  }
1400
- async function injectLlmMeshGatewayEnv(mode = "auto", allowDirectFallback = true, clientSessionId) {
1401
- if (mode === "direct") {
1402
- delete process.env.ANTHROPIC_BASE_URL;
1403
- delete process.env.ANTHROPIC_AUTH_TOKEN;
1404
- return undefined;
1405
- }
1406
- 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.
1407
1425
  delete process.env.ANTHROPIC_BASE_URL;
1408
1426
  delete process.env.ANTHROPIC_AUTH_TOKEN;
1409
1427
  return undefined;
@@ -1434,7 +1452,7 @@ async function injectLlmMeshGatewayEnv(mode = "auto", allowDirectFallback = true
1434
1452
  delete process.env.ANTHROPIC_BASE_URL;
1435
1453
  delete process.env.ANTHROPIC_AUTH_TOKEN;
1436
1454
  if (mode === "gateway") {
1437
- 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");
1438
1456
  }
1439
1457
  if (allowDirectFallback) {
1440
1458
  process.stderr.write("[h2a] llm-mesh: gateway unavailable; continuing with native authentication.\n");
@@ -1516,7 +1534,7 @@ function renderNativeRestartResult(result, json) {
1516
1534
  export async function prepareStructuredGateway(mode, inject = (selectedMode) => injectLlmMeshGatewayEnv(selectedMode, selectedMode !== "gateway")) {
1517
1535
  const gateway = await inject(mode);
1518
1536
  if (mode === "gateway" && !gateway) {
1519
- 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");
1520
1538
  }
1521
1539
  return gateway;
1522
1540
  }
@@ -1527,7 +1545,9 @@ export async function prepareLlmMeshForRestore(opts = {}, context = {
1527
1545
  injectGateway: (mode) => injectLlmMeshGatewayEnv(mode, mode !== "gateway"),
1528
1546
  }) {
1529
1547
  const mode = opts.mode ?? "auto";
1530
- 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")
1531
1551
  return;
1532
1552
  const config = context.config ?? {};
1533
1553
  const port = config.port ?? 3002;
@@ -1718,7 +1738,45 @@ function setAndReportDefaultRemote(url) {
1718
1738
  ensureManagedTmuxProfile(getTmuxProfileConfig().profile);
1719
1739
  process.stderr.write(`[h2a] default remote set to ${configured}\n`);
1720
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
+ }
1721
1775
  export async function main(argv) {
1776
+ // Passive fatal-path witness on the real entry path (see
1777
+ // installCliWitnessOnce): observes via uncaughtExceptionMonitor, so the
1778
+ // default crash/exit behavior is unchanged.
1779
+ installCliWitnessOnce();
1722
1780
  // ②b — auto-migrate the config home remote-cli/ -> h2a/ on first run (backup +
1723
1781
  // compat symlink). Best-effort: never let it break a command. Runs once
1724
1782
  // (idempotent) before any config/registry read.
@@ -1767,22 +1825,34 @@ export async function main(argv) {
1767
1825
  "the separate remote and track CLIs. The heavy session runtime loads on " +
1768
1826
  "demand. h2a runs and coordinates agents; it is not itself an agent.")
1769
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
+ });
1770
1847
  const identityCommand = program
1771
1848
  .command("identity")
1772
1849
  .description("Inspect identity bindings or unlock a token-fenced identity lock");
1773
- const operatorBreak = async (lockPath, token, assertDead) => {
1774
- const { breakLockAsOperator, breakIdentityLockAsOperator } = await import("@sentropic/h2a");
1775
- const identityLock = basename(lockPath) === '.lock' && basename(dirname(lockPath)) === 'identity';
1776
- const breaker = identityLock ? breakIdentityLockAsOperator : breakLockAsOperator;
1777
- const result = breaker(lockPath, { expectToken: token, assertDead });
1778
- process.stdout.write(`${JSON.stringify({ path: identityLock ? 'identity/.lock' : lockPath, ...result })}\n`);
1779
- if (!result.broken) {
1780
- process.stderr.write(`[h2a] lock break refused: ${result.reason}${result.pid !== undefined ? ` (pid ${result.pid})` : ""}\n`);
1781
- if (result.reason === "assert-dead-required") {
1782
- process.stderr.write("[h2a] verify the holder has stopped, then explicitly pass --assert-dead; no PID is killed\n");
1783
- }
1784
- process.exitCode = 1;
1785
- }
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; });
1786
1856
  };
1787
1857
  identityCommand
1788
1858
  .command("unlock")
@@ -1812,9 +1882,18 @@ export async function main(argv) {
1812
1882
  .option("--execute", "refuse destructive execution; no bypass exists")
1813
1883
  .option("--root <path>", "DEF h2a root (default: H2A_ROOT or ~/h2a-workspace/.h2a)")
1814
1884
  .option("--output <path>", "new external directory for the proof packet")
1815
- .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
+ }
1816
1895
  if (opts.execute) {
1817
- const refusal = refuseCullExecution();
1896
+ const refusal = cull.refuseCullExecution();
1818
1897
  process.stderr.write(`[h2a] identity cull refused: ${refusal.refusalCodes.join(", ")}\n`);
1819
1898
  process.exitCode = 2;
1820
1899
  return;
@@ -1822,7 +1901,7 @@ export async function main(argv) {
1822
1901
  const defRoot = opts.root ?? process.env.H2A_ROOT ?? defaultLocalH2aRoot();
1823
1902
  const outputDir = opts.output ?? join(tmpdir(), `h2a-identity-cull-${randomUUID()}`);
1824
1903
  try {
1825
- const result = runIdentityCullDryRun({
1904
+ const result = cull.runIdentityCullDryRun({
1826
1905
  defRoot,
1827
1906
  pinRoot: join(homedir(), "src", "a2a-cli"),
1828
1907
  outputDir,
@@ -1949,6 +2028,10 @@ export async function main(argv) {
1949
2028
  .option("--name <label>", "base label for the --count fan-out fleet names (default: cwd basename)")
1950
2029
  .option("--browser", "add the headful-browser noVNC sidecar (WP7) — enables 2FA / authenticated-site flows from inside the pod")
1951
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")
1952
2035
  .action(async (commandArgs, opts) => {
1953
2036
  const { remote: remoteOverride, local, workspace, ...rest } = opts;
1954
2037
  if (local) {
@@ -3634,6 +3717,8 @@ export async function main(argv) {
3634
3717
  process.exitCode = 1;
3635
3718
  return;
3636
3719
  }
3720
+ const sameResumeIdentity = resolvedConvId !== undefined && registryEntry?.convId === resolvedConvId &&
3721
+ registryEntry?.cwd === process.cwd() && registryEntry?.tool === explicitProfile;
3637
3722
  const entry = explicitProfile
3638
3723
  ? {
3639
3724
  id: displaySlug,
@@ -3648,8 +3733,9 @@ export async function main(argv) {
3648
3733
  enrolledAt: new Date().toISOString(),
3649
3734
  lastSeenAt: new Date().toISOString(),
3650
3735
  source: "run",
3651
- bare: registryEntry?.convId === resolvedConvId ? registryEntry?.bare : undefined,
3652
- 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,
3653
3739
  }
3654
3740
  : registryEntry;
3655
3741
  // WHICH host serves this resume — resolved BEFORE ANY act (F1): every
@@ -3747,7 +3833,7 @@ export async function main(argv) {
3747
3833
  process.exitCode = 2;
3748
3834
  return;
3749
3835
  }
3750
- const effectiveGatewayMode = gatewayModeForProfile(profile, gatewayMode === "auto" ? entry.gatewayMode ?? "auto" : gatewayMode);
3836
+ const effectiveGatewayMode = resolveResumedGatewayMode(profile, gatewayMode, entry.gatewayMode, entry.gatewayProvenance);
3751
3837
  let gateway;
3752
3838
  // Resolve against the selected mode, not the parent's environment:
3753
3839
  // required gateway injection below must succeed before either launch.
@@ -3960,7 +4046,11 @@ export async function main(argv) {
3960
4046
  cwd: entry.cwd,
3961
4047
  sessionClass,
3962
4048
  ...(entry.convId ? { convId: entry.convId } : {}),
3963
- 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
+ : {}),
3964
4054
  bare: useBare,
3965
4055
  });
3966
4056
  process.stderr.write(`[h2a] resumed local session ${resumeSlug} (${profile} resume${entry.convId ? ` ${entry.convId}` : ""} in ${entry.cwd})\n`);
@@ -3991,8 +4081,11 @@ export async function main(argv) {
3991
4081
  .option("--prompt-stdin", "read the initial Claude/Codex/AGY prompt from stdin (keeps it out of process argv)")
3992
4082
  .option("--agent <agent>", "AGY agent override (for example: stp)")
3993
4083
  .option("--model <model>", "Claude/Codex/AGY model override")
3994
- .option("--effort <level>", "reasoning effort: low|medium|high|xhigh (AGY supports low|medium|high)")
3995
- .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>")
3996
4089
  .option("--background", "classify this detached session as a background worker excluded from human restore")
3997
4090
  .option("--json", "emit one machine-readable h2a.run.result object")
3998
4091
  .option("--llm-gateway", "launch the CLI through the local llm-mesh gateway")
@@ -4014,13 +4107,30 @@ export async function main(argv) {
4014
4107
  process.exitCode = 2;
4015
4108
  return;
4016
4109
  }
4017
- if (opts.effort !== undefined && !isAgentLaunchEffort(opts.effort)) {
4018
- 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`);
4019
4129
  process.exitCode = 2;
4020
4130
  return;
4021
4131
  }
4022
- if (profile === "agy" && opts.effort === "xhigh") {
4023
- 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`);
4024
4134
  process.exitCode = 2;
4025
4135
  return;
4026
4136
  }
@@ -4166,13 +4276,11 @@ export async function main(argv) {
4166
4276
  }
4167
4277
  };
4168
4278
  try {
4169
- if (structuredLaunch && sessionHost === "native") {
4170
- for (const label of labels)
4171
- preflightNativeLaunch(localSessionName(slugify(label ?? cwd)));
4172
- }
4173
- const reservedTmuxSlugs = tmuxAvailable()
4174
- ? existingLocalSessionSlugs(labels, cwd)
4175
- : [];
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);
4176
4284
  // The inventory and this decision are separate reads: a structured
4177
4285
  // launch can leave a name visible just before its last tmux process
4178
4286
  // exits. Re-probe every canonical/legacy spelling and reclaim the slug
@@ -4207,14 +4315,16 @@ export async function main(argv) {
4207
4315
  // twin over a live-but-unprovable native writer (sol-F3): the same
4208
4316
  // fail-closed rule as resolveManagedHost's rule 4 applies — refuse
4209
4317
  // to launch on an unprovable host state.
4210
- const nativeExistence = labels
4318
+ const nativeExistence = await Promise.all(labels
4211
4319
  .map((label) => slugify(label ?? cwd))
4212
- .map((slug) => {
4213
- const detail = nativeSessionState(localSessionName(slug));
4320
+ .map(async (slug) => {
4321
+ const detail = sessionHost === "native"
4322
+ ? await nativeSessionStateAsync(localSessionName(slug), true)
4323
+ : nativeSessionState(localSessionName(slug));
4214
4324
  const probe = detail.state === "unknown" ? "unknown"
4215
4325
  : detail.state === "found" && detail.session.status === "running" ? "live" : "dead";
4216
4326
  return { slug, probe, reason: detail.state === "unknown" ? detail.reason : undefined };
4217
- });
4327
+ }));
4218
4328
  const unknownNativeSlugs = nativeExistence
4219
4329
  .filter((probed) => probed.probe === "unknown");
4220
4330
  if (unknownNativeSlugs.length > 0) {
@@ -4259,9 +4369,15 @@ export async function main(argv) {
4259
4369
  // Single-writer guard: refuse to resume a conversation another live
4260
4370
  // session (local registry / remote pod) is already writing.
4261
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"]));
4262
4377
  const ok = await guardConvWriters({
4263
4378
  convId: opts.resume,
4264
4379
  cwd,
4380
+ nativeSessionLiveness: name => nativeLivenessByName.get(name) ?? nativeSessionLiveness(name),
4265
4381
  ...(opts.force ? { force: true } : {}),
4266
4382
  fetchRemoteSessions: async () => {
4267
4383
  const url = getConfiguredRemoteOptional();
@@ -4290,8 +4406,11 @@ export async function main(argv) {
4290
4406
  for (const prunedId of resumeResolution.prunedIds) {
4291
4407
  process.stderr.write(`[h2a] pruned dead registry row ${prunedId} that pinned conflicting launch options for this resume\n`);
4292
4408
  }
4293
- const pinnedLaunch = resumeResolution.entries[0];
4294
- 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);
4295
4414
  const command = localCliCommand(profile);
4296
4415
  // A detached/background or run-once launch is a worker, even when its
4297
4416
  // agent later emits a Claude SessionStart hook. Stamp this through tmux
@@ -4335,6 +4454,17 @@ export async function main(argv) {
4335
4454
  return;
4336
4455
  }
4337
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;
4338
4468
  let args;
4339
4469
  try {
4340
4470
  args =
@@ -4345,7 +4475,7 @@ export async function main(argv) {
4345
4475
  ? { prompt: initialPrompt }
4346
4476
  : {}),
4347
4477
  ...(opts.agent !== undefined ? { agent: opts.agent } : {}),
4348
- ...(opts.model !== undefined ? { model: opts.model } : {}),
4478
+ ...(runModel !== undefined ? { model: runModel } : {}),
4349
4479
  ...(opts.effort !== undefined
4350
4480
  ? { effort: opts.effort }
4351
4481
  : {}),
@@ -4371,6 +4501,10 @@ export async function main(argv) {
4371
4501
  }
4372
4502
  let name;
4373
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;
4374
4508
  let outputLog;
4375
4509
  let resultJson;
4376
4510
  let agentPane;
@@ -4383,6 +4517,10 @@ export async function main(argv) {
4383
4517
  mkdirSync(runDir, { recursive: true });
4384
4518
  outputLog = join(runDir, "output.log");
4385
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 });
4386
4524
  const headlessPromptInput = structuredLaunch && isAgentLaunchProfile(profile)
4387
4525
  ? buildAgentLaunchStdin({
4388
4526
  profile,
@@ -4396,14 +4534,16 @@ export async function main(argv) {
4396
4534
  ({ name, slug, promptFile } = startNativeHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt, structuredLaunch ? (owned) => {
4397
4535
  launchOwnership = { host: "native", sessions: [owned] };
4398
4536
  launchGuard = startLaunchGuard(runDir, launchOwnership);
4399
- } : undefined));
4537
+ } : undefined, opts.outputFile ? resolve(cwd, opts.outputFile) : undefined));
4400
4538
  }
4401
4539
  else {
4402
- ({ 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));
4403
4541
  }
4404
4542
  }
4405
4543
  else if (sessionHost === "native") {
4406
- ({ 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, {
4407
4547
  onCreateAttempt,
4408
4548
  ...(opts.resume !== undefined ? { resumeId: opts.resume } : {}),
4409
4549
  ...(initialPrompt !== undefined
@@ -4532,6 +4672,18 @@ export async function main(argv) {
4532
4672
  now: () => Date.now(),
4533
4673
  }, { profile });
4534
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. */ }
4535
4687
  cleanupHeadlessPromptFile(promptFile);
4536
4688
  // Only claim the session is gone when the kill actually reported
4537
4689
  // success. "nothing is left running" over an unchecked call is the
@@ -4561,26 +4713,64 @@ export async function main(argv) {
4561
4713
  ? `[h2a] last screen:\n${promptDelivery.capture}\n`
4562
4714
  : ""));
4563
4715
  process.exitCode = 1;
4564
- if (opts.json && promptDelivery.state === "provider-blocked") {
4716
+ if (opts.json && (promptDelivery.state === "provider-blocked" || quotaFailure)) {
4565
4717
  process.stdout.write(`${JSON.stringify({ kind: "h2a.run.failure", version: 1,
4566
- state: "provider-blocked", launchId: slug, error: promptDelivery.reason,
4567
- 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 } : {}),
4568
4722
  })}\n`);
4569
4723
  }
4570
4724
  return;
4571
4725
  }
4572
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).
4573
4760
  const pid = sessionHost === "native"
4574
- ? nativeSessionPid(name)
4761
+ ? (launchedPid ?? nativeSessionPid(name))
4575
4762
  : agentPane
4576
4763
  ? localSessionPanePid(agentPane)
4577
4764
  : undefined;
4578
4765
  const workerPid = sessionHost === "native"
4579
- ? nativeWorkerPid(name)
4766
+ ? (launchedWorkerPid ?? nativeWorkerPid(name))
4580
4767
  : agentPane !== undefined
4581
4768
  ? paneWorkerPid(agentPane)
4582
4769
  : undefined;
4583
4770
  if (structuredLaunch && pid === undefined) {
4771
+ // The supervisor can publish and exit DURING the PID probe.
4772
+ if (reportAgyCompletion())
4773
+ return;
4584
4774
  cleanupHeadlessPromptFile(promptFile);
4585
4775
  // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4586
4776
  // `name`/`sessionHost` are the session and host THIS command
@@ -4596,8 +4786,8 @@ export async function main(argv) {
4596
4786
  return;
4597
4787
  }
4598
4788
  // Auto-enroll in the live-session registry (feeds `h2a ls`/`restore`).
4599
- // Pin the gateway posture ONLY when the user was explicit (--gw/--no-gw);
4600
- // 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.
4601
4791
  enrollFromRun({
4602
4792
  profile,
4603
4793
  slug,
@@ -4609,8 +4799,10 @@ export async function main(argv) {
4609
4799
  cwd,
4610
4800
  sessionClass,
4611
4801
  ...(opts.resume !== undefined ? { convId: opts.resume } : {}),
4612
- gatewayMode: launchGatewayMode,
4613
- bare: useBare,
4802
+ ...(gatewayMode !== "auto" || pinnedLaunch?.gatewayProvenance === "explicit"
4803
+ ? { gatewayMode: launchGatewayMode, gatewayProvenance: "explicit" }
4804
+ : {}),
4805
+ ...(enrollmentBare !== undefined ? { bare: enrollmentBare } : {}),
4614
4806
  });
4615
4807
  const nativeOwnerSocket = launchOwnership?.host === "native"
4616
4808
  ? launchOwnership.sessions.find(owned => owned.name === name)?.socketPath : undefined;
@@ -4650,6 +4842,8 @@ export async function main(argv) {
4650
4842
  tmuxSession: only.name,
4651
4843
  host: sessionHost === "native" ? "native" : "tmux",
4652
4844
  profile,
4845
+ ...(opts.model ? { model: opts.model, modelSource: "launch-argument", effectiveModel: "unknown" } : {}),
4846
+ ...(profile === "agy" && isAgyClaudeModel(opts.model) ? { quota: readAgyClaudeQuotaStatus() } : {}),
4653
4847
  workspace: cwd,
4654
4848
  mode: opts.headless ? "headless" : "interactive",
4655
4849
  background: opts.background === true,
@@ -6134,6 +6328,9 @@ export async function main(argv) {
6134
6328
  ...(entry.gatewayMode !== undefined
6135
6329
  ? { gatewayMode: entry.gatewayMode }
6136
6330
  : {}),
6331
+ ...(entry.gatewayProvenance !== undefined
6332
+ ? { gatewayProvenance: entry.gatewayProvenance }
6333
+ : {}),
6137
6334
  ...(entry.bare !== undefined ? { bare: entry.bare } : {}),
6138
6335
  }));
6139
6336
  const result = await executeNativeRestart({
@@ -6188,7 +6385,9 @@ export async function main(argv) {
6188
6385
  ...(candidate.convId !== undefined
6189
6386
  ? { convId: candidate.convId }
6190
6387
  : {}),
6191
- ...(mode !== "auto" ? { gatewayMode: mode } : {}),
6388
+ ...(mode !== "auto"
6389
+ ? { gatewayMode: mode, gatewayProvenance: "explicit" }
6390
+ : {}),
6192
6391
  ...(candidate.bare !== undefined ? { bare: candidate.bare } : {}),
6193
6392
  });
6194
6393
  }
@@ -6301,7 +6500,7 @@ export async function main(argv) {
6301
6500
  process.exitCode = 1;
6302
6501
  return;
6303
6502
  }
6304
- const relaunchMode = gatewayModeForProfile(nativeEntry.tool, nativeEntry.gatewayMode ?? "auto");
6503
+ const relaunchMode = resolveResumedGatewayMode(nativeEntry.tool, "auto", nativeEntry.gatewayMode, nativeEntry.gatewayProvenance);
6305
6504
  await injectLlmMeshGatewayEnv(relaunchMode, relaunchMode !== "gateway", actName);
6306
6505
  if (!killNativeSessionTree(actName)) {
6307
6506
  process.stderr.write(`[h2a] native session ${slug} could not be killed; nothing was relaunched.\n`);
@@ -6328,7 +6527,15 @@ export async function main(argv) {
6328
6527
  ...(relaunched.pid !== undefined ? { pid: relaunched.pid } : {}),
6329
6528
  cwd: nativeEntry.cwd,
6330
6529
  sessionClass: nativeEntry.sessionClass ?? "human",
6331
- 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
+ : {}),
6332
6539
  bare: useBare,
6333
6540
  ...(nativeEntry.convId ? { convId: nativeEntry.convId } : {}),
6334
6541
  });
@@ -6458,14 +6665,55 @@ export async function main(argv) {
6458
6665
  if (!forced) {
6459
6666
  let ok = 0;
6460
6667
  for (const action of plan.actions) {
6461
- const sessionId = entryByTmuxSession.get(action.name)?.id;
6462
- const relaunched = sessionId !== undefined
6463
- ? relaunchSessionWithLease({
6464
- sessionId,
6465
- tmuxSession: action.name,
6466
- command: action.cmd,
6467
- })
6468
- : 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
+ }
6469
6717
  if (relaunched) {
6470
6718
  ok += 1;
6471
6719
  process.stderr.write(`[h2a] relaunched ${action.slug}: ${action.cmd}\n`);
@@ -6541,6 +6789,11 @@ export async function main(argv) {
6541
6789
  }
6542
6790
  finalReady.push(item);
6543
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();
6544
6797
  let ok = 0;
6545
6798
  for (const { action, entry, sessionClass } of finalReady) {
6546
6799
  const safety = sessionRelaunchSafety(action.name);
@@ -6567,8 +6820,9 @@ export async function main(argv) {
6567
6820
  process.stderr.write(`[h2a] skipped ${action.slug}: tmux session ${action.name} changed or was no longer proven dead immediately before kill.\n`);
6568
6821
  continue;
6569
6822
  }
6823
+ const restoreLoopEnv = replaceAnthropicGatewayEnvironment(process.env, loopEntryGatewayEnv);
6570
6824
  try {
6571
- const gatewayMode = gatewayModeForProfile(action.profile, entry.gatewayMode ?? "auto");
6825
+ const gatewayMode = resolveResumedGatewayMode(action.profile, "auto", entry.gatewayMode, entry.gatewayProvenance);
6572
6826
  await injectLlmMeshGatewayEnv(gatewayMode, true, localSessionName(action.slug));
6573
6827
  const { name, agentPane } = startLocalSession(action.profile, action.command, entry.cwd, localResumeArgs(action.profile, action.convId, {
6574
6828
  bare: resolveClaudeBare(action.profile, entry.bare),
@@ -6590,6 +6844,9 @@ export async function main(argv) {
6590
6844
  ...(entry.gatewayMode !== undefined
6591
6845
  ? { gatewayMode: entry.gatewayMode }
6592
6846
  : {}),
6847
+ ...(entry.gatewayProvenance !== undefined
6848
+ ? { gatewayProvenance: entry.gatewayProvenance }
6849
+ : {}),
6593
6850
  ...(entry.bare !== undefined ? { bare: entry.bare } : {}),
6594
6851
  });
6595
6852
  const wake = wakeRelaunchedSession(relaunchContinuationPrompt(entry.task), {
@@ -6622,6 +6879,9 @@ export async function main(argv) {
6622
6879
  catch (error) {
6623
6880
  process.stderr.write(`[h2a] FAILED to force-restart ${action.slug} after killing ${action.name}: ${error.message}\n`);
6624
6881
  }
6882
+ finally {
6883
+ restoreLoopEnv();
6884
+ }
6625
6885
  }
6626
6886
  process.stderr.write(`[h2a] force-restarted ${ok}/${plan.actions.length}${plan.skipped.length ? `, ${plan.skipped.length} skipped` : ""}\n`);
6627
6887
  if (ok !== plan.actions.length)
@@ -6985,13 +7245,13 @@ export async function main(argv) {
6985
7245
  }
6986
7246
  if (opts.reattach)
6987
7247
  restoreOpts.reattach = true;
6988
- // Forced-direct restore must NOT spin up the gateway; otherwise prepare
6989
- // it as before (no-op when the gateway is disabled).
6990
- 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") {
6991
7251
  try {
6992
7252
  await prepareLlmMeshForRestore({
6993
7253
  dryRun: Boolean(opts.dryRun),
6994
- mode: forceGateway ?? "auto",
7254
+ mode: forceGateway,
6995
7255
  });
6996
7256
  }
6997
7257
  catch (error) {
@@ -7525,13 +7785,29 @@ export async function main(argv) {
7525
7785
  llmMeshAccountCommand
7526
7786
  .command("enroll <provider>")
7527
7787
  .description("Enroll through the sentropic-owned OAuth state machine " +
7528
- "(cloud-code, codex, muse OAuth device flow, or mistral-vibe browser sign-in — alias: vibe). " +
7529
- "--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")
7530
7792
  .option("--config-ref <ref>", "sentropic configuration reference for OAuth")
7531
- .option("--cli", "import credentials from the provider's local CLI store (muse only) instead of the OAuth flow")
7532
- .action(async (provider, opts) => {
7533
- // "vibe" is the CLI-friendly alias of the mistral-vibe transport.
7534
- 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;
7535
7811
  if (normalized === "muse-code") {
7536
7812
  // Owner decision 2026-10-10: muse-code is no longer a user entry —
7537
7813
  // muse enrolls via the OAuth device flow by default, --cli selects
@@ -7541,20 +7817,37 @@ export async function main(argv) {
7541
7817
  process.exitCode = 1;
7542
7818
  return;
7543
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
+ }
7544
7835
  if (normalized !== "cloud-code" &&
7545
7836
  normalized !== "codex" &&
7546
7837
  normalized !== "muse" &&
7547
- normalized !== "mistral-vibe") {
7548
- process.stderr.write(`[h2a] llm-mesh account: unsupported provider "${provider}". ` +
7549
- "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");
7550
7842
  process.exitCode = 1;
7551
7843
  return;
7552
7844
  }
7553
7845
  try {
7554
- const account = await enrollViaFacade(normalized, {
7555
- ...(opts.configRef ? { configRef: opts.configRef } : {}),
7556
- ...(opts.cli ? { cliImport: true } : {}),
7557
- });
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 } : {}) });
7558
7851
  process.stdout.write(`[h2a] llm-mesh account: enrolled ${account.provider}; ` +
7559
7852
  "account inventory remains in Sentropic\n");
7560
7853
  // The gateway loads the account inventory at boot only: an account
@@ -7576,6 +7869,7 @@ export async function main(argv) {
7576
7869
  .description("List public accounts owned by this local llm-mesh scope")
7577
7870
  .option("--json", "emit public account metadata as JSON")
7578
7871
  .action(async (opts) => {
7872
+ const { listAccountsViaFacade, formatLlmMeshAccountList, formatLlmMeshAccountError } = await import("./llm-mesh-accounts.js");
7579
7873
  try {
7580
7874
  const accounts = await listAccountsViaFacade();
7581
7875
  process.stdout.write(`${formatLlmMeshAccountList(accounts, opts.json === true)}\n`);
@@ -7590,6 +7884,7 @@ export async function main(argv) {
7590
7884
  .aliases(["rm", "unenroll"])
7591
7885
  .description("Remove one account owned by this local llm-mesh scope")
7592
7886
  .action(async (accountId) => {
7887
+ const { removeAccountViaFacade, formatLlmMeshAccountError } = await import("./llm-mesh-accounts.js");
7593
7888
  try {
7594
7889
  const result = await removeAccountViaFacade(accountId);
7595
7890
  process.stdout.write(`[h2a] llm-mesh account: removed ${result.accountId}\n`);
@@ -7655,17 +7950,15 @@ export async function main(argv) {
7655
7950
  const config = readLlmMeshConfig() ?? {};
7656
7951
  const existing = readGatewayPid();
7657
7952
  if (existing) {
7658
- 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.
7659
7955
  process.stdout.write(`[h2a] llm-mesh: already running (pid ${existing})\n`);
7660
- process.stdout.write(`[h2a] llm-mesh: restore config enabled\n`);
7661
7956
  return;
7662
7957
  }
7663
7958
  process.stdout.write(`[h2a] llm-mesh: starting gateway…\n`);
7664
7959
  const result = await startGateway(config, { verbose: opts.verbose });
7665
- setLlmMeshRuntimeConfig({ enabled: true });
7666
7960
  const port = result.port;
7667
7961
  process.stdout.write(`[h2a] llm-mesh: gateway started (pid ${result.pid}, port ${port})\n`);
7668
- process.stdout.write(`[h2a] llm-mesh: restore config enabled\n`);
7669
7962
  process.stdout.write("[h2a] llm-mesh: use `h2a run claude --gw`; the opaque bearer is not printed\n");
7670
7963
  });
7671
7964
  llmMeshCommand
@@ -7693,6 +7986,9 @@ export async function main(argv) {
7693
7986
  else {
7694
7987
  process.stdout.write(`[h2a] llm-mesh: not running\n`);
7695
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 });
7696
7992
  });
7697
7993
  llmMeshCommand
7698
7994
  .command("restart")
@@ -7709,9 +8005,9 @@ export async function main(argv) {
7709
8005
  }
7710
8006
  process.stdout.write(`[h2a] llm-mesh: starting gateway…\n`);
7711
8007
  const result = await startGateway(config, { verbose: opts.verbose });
7712
- 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.
7713
8010
  process.stdout.write(`[h2a] llm-mesh: gateway restarted (pid ${result.pid}, port ${result.port})\n`);
7714
- process.stdout.write(`[h2a] llm-mesh: restore config enabled\n`);
7715
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`);
7716
8012
  process.stdout.write("[h2a] llm-mesh: use `h2a run claude --gw`; the opaque bearer is not printed\n");
7717
8013
  });
@@ -7771,6 +8067,75 @@ export async function main(argv) {
7771
8067
  ? `[h2a] lineage ${id} resumed\n`
7772
8068
  : `[h2a] lineage ${id} was not suspended (no-op)\n`);
7773
8069
  });
8070
+ // ---------------------------------------------------------------------------
8071
+ // logs — last crash-log incidents (native host journal + per-component logs)
8072
+ // ---------------------------------------------------------------------------
8073
+ program
8074
+ .command("logs")
8075
+ .description("Show the last crash-log incidents: native host journal plus cli/attach/mcp-server logs.")
8076
+ .option("--component <name>", "only this component (native-host|cli|attach|mcp-server)")
8077
+ .option("-n, --lines <n>", "number of rows to show", "50")
8078
+ .option("--json", "emit JSON rows instead of human lines")
8079
+ .action((opts) => {
8080
+ const dir = resolveCrashLogDir();
8081
+ const lines = Math.max(1, parseInt(opts.lines ?? "50", 10) || 50);
8082
+ const only = (opts.component ?? "").trim();
8083
+ const wantHost = only === "" || only === "native-host";
8084
+ const wanted = CRASH_LOG_COMPONENTS.filter((c) => only === "" || only === c);
8085
+ if (!wantHost && wanted.length === 0) {
8086
+ process.stderr.write("h2a logs: --component must be native-host|cli|attach|mcp-server\n");
8087
+ process.exitCode = 1;
8088
+ return;
8089
+ }
8090
+ const rows = readRecentCrashLogs(wanted, lines).map((row) => ({
8091
+ timestamp: row.timestamp,
8092
+ component: row.component,
8093
+ version: row.version,
8094
+ pid: row.pid,
8095
+ event: row.event,
8096
+ ...(row.sessionId ? { sessionId: row.sessionId } : {}),
8097
+ ...(row.instanceId ? { instanceId: row.instanceId } : {}),
8098
+ ...(row.reason ? { reason: row.reason } : {}),
8099
+ ...(row.exitCode !== undefined ? { exitCode: row.exitCode } : {}),
8100
+ ...(row.signal ? { signal: row.signal } : {}),
8101
+ ...(row.detail ? { detail: row.detail } : {}),
8102
+ }));
8103
+ const hostRows = wantHost
8104
+ ? readHostJournal(resolveHostJournalPath())
8105
+ .slice(-lines)
8106
+ .map((entry) => ({
8107
+ timestamp: entry.timestamp ?? "unknown",
8108
+ component: "native-host",
8109
+ ...entry,
8110
+ }))
8111
+ : [];
8112
+ const merged = [...hostRows, ...rows]
8113
+ .sort((a, b) => (a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0))
8114
+ .slice(-lines);
8115
+ // The raw server output is verbatim child stderr (unredacted at rest):
8116
+ // read it before the empty check so a raw-only crash is shown, and
8117
+ // redact it on display like every structured row.
8118
+ const rawPath = resolveCentralMcpLogPath();
8119
+ const rawTail = (only === "" || only === "mcp-server") ? readFileTailLines(rawPath, 20) : [];
8120
+ if (merged.length === 0 && rawTail.length === 0) {
8121
+ process.stdout.write(`[h2a] no crash-log incidents in ${dir}\n`);
8122
+ return;
8123
+ }
8124
+ if (opts.json && merged.length > 0) {
8125
+ process.stdout.write(`${merged.map((row) => JSON.stringify(row)).join("\n")}\n`);
8126
+ return;
8127
+ }
8128
+ for (const row of merged) {
8129
+ process.stdout.write(`${formatCrashLogHumanRow(row)}\n`);
8130
+ }
8131
+ if (rawTail.length > 0) {
8132
+ // The server process itself leaves no structured rows (it dies with
8133
+ // its witness); its raw stderr tail is the only "why" for its crash.
8134
+ process.stdout.write(`[h2a] --- mcp-central.log raw tail (${rawPath}) ---\n`);
8135
+ for (const line of rawTail)
8136
+ process.stdout.write(`${redactCrashText(line)}\n`);
8137
+ }
8138
+ });
7774
8139
  // Help layout only — assign every top-level command an intention group and
7775
8140
  // render the groups in the declared order instead of registration order.
7776
8141
  // Presentational for every PRE-EXISTING RECOGNIZED COMMAND: `helpGroup()` and
@@ -7822,7 +8187,25 @@ export async function main(argv) {
7822
8187
  applyRuntimeHelpGroups(program.createHelp().visibleCommands(program));
7823
8188
  program.configureHelp({ groupItems: groupRuntimeHelpItems });
7824
8189
  program.addHelpText("after", "\nGrouped map of every h2a command (core + runtime): h2a explain\n");
7825
- 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
+ }
7826
8209
  const code = process.exitCode;
7827
8210
  return typeof code === "number" ? code : 0;
7828
8211
  }
@@ -7838,6 +8221,9 @@ function isEntryPoint() {
7838
8221
  }
7839
8222
  }
7840
8223
  if (isEntryPoint()) {
8224
+ // Passive witness for fatal paths outside runOp: installed once inside
8225
+ // main() so the lazy core-binary dispatch path is covered too.
8226
+ installCliWitnessOnce();
7841
8227
  main(process.argv).catch((error) => {
7842
8228
  if (error instanceof AuthRefreshError ||
7843
8229
  error instanceof AuthBundleMissingError) {
@@ -7853,6 +8239,13 @@ export { main as dispatchH2a };
7853
8239
  /** @deprecated Compatibility for older h2a cores; new cores require dispatchH2a. */
7854
8240
  export { main as dispatch };
7855
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);
7856
8249
  /**
7857
8250
  * h2a-facing surface: the read-only remote-agents projection consumed by the
7858
8251
  * objective-loop tick's *agents adapter* in `@sentropic/h2a` (via lazy