@sentropic/h2a-runtime 0.96.0 → 0.96.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,9 +23,9 @@ import { attachNativeSession, driveNativeInstruction, killNativeSessionIfIncarna
23
23
  import { executeNativeRestart, NativeRestartError, } from "./native-restart.js";
24
24
  import { migrateTmuxNames } from "./tmux-name-migration.js";
25
25
  import { projectStatusForH2a } from "./status-projection.js";
26
- import { buildAgentLaunchArgs, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
26
+ import { buildAgentLaunchArgs, buildAgentLaunchStdin, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
27
27
  import { deriveSessionClass } from "./session-class.js";
28
- import { isRelaunchKillable, planRelaunch, relaunchContinuationPrompt, wakeRelaunchedSession, } from "./relaunch.js";
28
+ import { isRelaunchKillable, planRelaunch, relaunchContinuationPrompt, tryAcquireResumeLaunchClaim, wakeRelaunchedSession, } from "./relaunch.js";
29
29
  import { isHumanFacingSession, legacySessionEvidence, readConversationCustomTitle, readLastLayout, reconcileRunConvIds, restore as restoreLayout, } from "./restore.js";
30
30
  import { getLayoutConfig } from "./config.js";
31
31
  import { isManagedLocalKind, advanceJob, coerceRegistryTool, enroll, enrollFromRun, isLive, listJobs, listLocalForLs, loadRegistry, loadRegistryWithDiagnostics, probeNativeSession, probeTmuxSession, registryEntriesForNativeTarget, resolveLocalTmuxSessionForName, resolveManagedHost, tryClaimSlot, unreadableRegistryRowsForTarget, withRegistryLock, resolveRegistryPath, } from "./registry.js";
@@ -68,6 +68,7 @@ import { acquireLease, createLineage, handoffLease, isIncarnationSuspended, list
68
68
  import { checkReadiness } from "./readiness.js";
69
69
  import { createInterface } from "node:readline";
70
70
  const H2A_RUN_API_VERSION = "h2a.run/v1";
71
+ const STRUCTURED_LAUNCH_PHASE_PREFIX = "[h2a] h2a.run.phase/v1 ";
71
72
  const H2A_RUNTIME_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
72
73
  import { CLI_PROFILES, gatewayModeForProfile, profileUsesLlmMeshGateway, } from "./protocol-local.js";
73
74
  import { enrollViaFacade, formatLlmMeshAccountError, formatLlmMeshAccountList, listAccountsViaFacade, readLlmMeshConfig, startGateway, stopGateway, readGatewayPid, llmMeshLogPath, replaceAnthropicGatewayEnvironment, removeAccountViaFacade, acquireLlmMeshSessionEnv, updateLlmMeshRoutingConfig, } from "./llm-mesh.js";
@@ -77,7 +78,7 @@ export const packageName = "@sentropic/h2a-runtime";
77
78
  export const H2A_RUNTIME_CLI_API_VERSION = 1;
78
79
  export { run } from "./run.js";
79
80
  export { executeNativeRestart, nativeMcpRelaunchInstruction, NativeRestartError, } from "./native-restart.js";
80
- export { AGENT_LAUNCH_EFFORTS, AGENT_LAUNCH_PROFILES, AGENT_LAUNCH_PROMPT_MAX_BYTES, assertAgentLaunchModel, assertAgentLaunchPrompt, buildAgentLaunchArgs, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
81
+ export { AGENT_LAUNCH_EFFORTS, AGENT_LAUNCH_PROFILES, AGENT_LAUNCH_PROMPT_MAX_BYTES, assertAgentLaunchModel, assertAgentLaunchPrompt, buildAgentLaunchArgs, buildAgentLaunchStdin, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
81
82
  export { attach, createRemoteSession, getRemoteSession, listRemoteSessions, refreshRemoteSession, renameRemoteSession, stopRemoteSession, } from "./attach.js";
82
83
  export { inspectProfileAuth } from "./auth-diagnostics.js";
83
84
  export { AuthRefreshError, ensureProfileAuthFresh } from "./auth-refresh.js";
@@ -3883,7 +3884,7 @@ export async function main(argv) {
3883
3884
  });
3884
3885
  program
3885
3886
  .command("run <profile> [path]")
3886
- .description("Start a LOCAL session in tmux (claude/codex/…) in <path> (default: cwd), then attach this terminal by default. h2a applies its embedded scroll-safe tmux profile at launch; no ~/.tmux.conf is required. Manage it with `h2a ls`, `h2a attach <slug>`, and `h2a stop <slug>`. Detach with Ctrl-b d; the session keeps running.")
3887
+ .description("Start a LOCAL session in tmux (claude/codex/agy/…) in <path> (default: cwd), then attach this terminal by default. h2a applies its embedded scroll-safe tmux profile at launch; no ~/.tmux.conf is required. Manage it with `h2a ls`, `h2a attach <slug>`, and `h2a stop <slug>`. Detach with Ctrl-b d; the session keeps running.")
3887
3888
  .option("--no-attach", "start detached and print the attach command instead (for scripts / fan-out orchestration)")
3888
3889
  .option("--no-attached-terminal", "disable the persistent headless tmux client (legacy compatibility; cannot be used with --prompt-stdin)")
3889
3890
  .option("--tmux", "host the session in tmux instead of the native PTY host (native is the default; H2A_SESSION_HOST=tmux flips the default fleet-wide)")
@@ -3893,10 +3894,11 @@ export async function main(argv) {
3893
3894
  .option("--count <n>", "fan out N parallel agents (named <base>#1…#N) — run more than the per-project layout cap of claude/codex sessions")
3894
3895
  .option("--h2a", 'also start the h2a MCP server in a side tmux window "h2a" (launcher contract: agent reachable/wakeable via ~/h2a-workspace/.h2a); config key `h2a: {enabled, command}` makes it the default')
3895
3896
  .option("--no-h2a", "do not start the h2a MCP side window, overriding config")
3896
- .option("--prompt-stdin", "read the initial Claude/Codex prompt from stdin (keeps it out of process argv)")
3897
- .option("--model <model>", "Claude/Codex model override")
3898
- .option("--effort <level>", "reasoning effort: low|medium|high|xhigh (Claude and Codex)")
3899
- .option("--headless", "run once and exit (Claude -p / Codex exec), recording output under .h2a/runs/<name>")
3897
+ .option("--prompt-stdin", "read the initial Claude/Codex/AGY prompt from stdin (keeps it out of process argv)")
3898
+ .option("--agent <agent>", "AGY agent override (for example: stp)")
3899
+ .option("--model <model>", "Claude/Codex/AGY model override")
3900
+ .option("--effort <level>", "reasoning effort: low|medium|high|xhigh (AGY supports low|medium|high)")
3901
+ .option("--headless", "run once and exit (Claude -p / Codex exec / AGY --print), recording output under .h2a/runs/<name>")
3900
3902
  .option("--background", "classify this detached session as a background worker excluded from human restore")
3901
3903
  .option("--json", "emit one machine-readable h2a.run.result object")
3902
3904
  .option("--llm-gateway", "launch the CLI through the local llm-mesh gateway")
@@ -3905,13 +3907,14 @@ export async function main(argv) {
3905
3907
  .option("--no-gw", "alias for --no-llm-gateway")
3906
3908
  .action(async (profile, path, opts) => {
3907
3909
  const structuredLaunch = opts.promptStdin === true ||
3910
+ opts.agent !== undefined ||
3908
3911
  opts.model !== undefined ||
3909
3912
  opts.effort !== undefined ||
3910
3913
  opts.headless === true ||
3911
3914
  opts.background === true ||
3912
3915
  opts.json === true;
3913
3916
  if (structuredLaunch && !isAgentLaunchProfile(profile)) {
3914
- process.stderr.write(`[h2a] structured run supports only claude|codex (got "${profile}")\n`);
3917
+ process.stderr.write(`[h2a] structured run supports only claude|codex|agy (got "${profile}")\n`);
3915
3918
  process.exitCode = 2;
3916
3919
  return;
3917
3920
  }
@@ -3920,6 +3923,16 @@ export async function main(argv) {
3920
3923
  process.exitCode = 2;
3921
3924
  return;
3922
3925
  }
3926
+ if (profile === "agy" && opts.effort === "xhigh") {
3927
+ process.stderr.write("[h2a] AGY --effort must be low|medium|high\n");
3928
+ process.exitCode = 2;
3929
+ return;
3930
+ }
3931
+ if (opts.agent !== undefined && profile !== "agy") {
3932
+ process.stderr.write("[h2a] --agent is supported only for the AGY profile\n");
3933
+ process.exitCode = 2;
3934
+ return;
3935
+ }
3923
3936
  if (opts.json && !opts.background) {
3924
3937
  process.stderr.write("[h2a] --json launch requires --background (MCP launches are detached)\n");
3925
3938
  process.exitCode = 2;
@@ -3945,6 +3958,26 @@ export async function main(argv) {
3945
3958
  process.exitCode = 2;
3946
3959
  return;
3947
3960
  }
3961
+ let gatewayMode;
3962
+ try {
3963
+ gatewayMode = gatewayModeFromOptions(opts);
3964
+ }
3965
+ catch (error) {
3966
+ process.stderr.write(`[h2a] ${error.message}.\n`);
3967
+ process.exitCode = 1;
3968
+ return;
3969
+ }
3970
+ if (gatewayMode === "gateway" && !profileUsesLlmMeshGateway(profile)) {
3971
+ process.stderr.write(`[h2a] --llm-gateway/--gw is unsupported for ${profile}; this gateway is Anthropic-compatible and only Claude profiles consume it.\n`);
3972
+ process.exitCode = 2;
3973
+ return;
3974
+ }
3975
+ if (opts.json && opts.name) {
3976
+ process.stderr.write(`${STRUCTURED_LAUNCH_PHASE_PREFIX}${JSON.stringify({
3977
+ launchId: opts.name,
3978
+ phase: "pre-creation",
3979
+ })}\n`);
3980
+ }
3948
3981
  const sessionHost = resolveSessionHostKind(opts);
3949
3982
  if (sessionHost === "local-tmux") {
3950
3983
  if (!tmuxAvailable()) {
@@ -4018,440 +4051,481 @@ export async function main(argv) {
4018
4051
  const labels = count > 1
4019
4052
  ? fanoutLabels(opts.name ?? basename(cwd), count)
4020
4053
  : [opts.name];
4021
- const existingTmuxSlugs = tmuxAvailable()
4022
- ? existingLocalSessionSlugs(labels, cwd)
4023
- : [];
4024
- // Native existence is THREE-state (the resolver's probe vocabulary):
4025
- // a probe FAILURE is "unknown", never absence. Treating a thrown
4026
- // probe as "no session" would let `run --tmux --resume` start a tmux
4027
- // twin over a live-but-unprovable native writer (sol-F3): the same
4028
- // fail-closed rule as resolveManagedHost's rule 4 applies — refuse
4029
- // to launch on an unprovable host state.
4030
- const nativeExistence = labels
4031
- .map((label) => slugify(label ?? cwd))
4032
- .map((slug) => ({
4033
- slug,
4034
- probe: probeNativeSession(localSessionName(slug)),
4035
- }));
4036
- const unknownNativeSlugs = nativeExistence
4037
- .filter((probed) => probed.probe === "unknown")
4038
- .map((probed) => probed.slug);
4039
- if (unknownNativeSlugs.length > 0) {
4040
- for (const slug of unknownNativeSlugs) {
4041
- process.stderr.write(`[h2a] cannot start ${slug}: native host state is unknown (the probe failed) — a probe failure is never proof of absence; refusing to risk a second writer (fail closed).\n`);
4042
- }
4043
- process.exitCode = 1;
4044
- return;
4045
- }
4046
- const existingNativeSlugs = nativeExistence
4047
- .filter((probed) => probed.probe === "live")
4048
- .map((probed) => probed.slug);
4049
- const existingLocalSessions = [
4050
- ...new Set([...existingTmuxSlugs, ...existingNativeSlugs]),
4051
- ];
4052
- if (existingLocalSessions.length > 0) {
4053
- for (const slug of existingLocalSessions) {
4054
- process.stderr.write(`[h2a] local session ${slug} already exists; no new ${profile} was started.\n`);
4055
- process.stderr.write(`[h2a] attach: h2a attach ${slug}\n`);
4056
- process.stderr.write(`[h2a] stop first: h2a stop ${slug} --reason restart\n`);
4057
- }
4058
- process.exitCode = 1;
4059
- return;
4060
- }
4061
- if (!existsSync(cwd) || !statSync(cwd).isDirectory()) {
4062
- process.stderr.write(`[h2a] workspace is not an existing directory: ${cwd}\n`);
4063
- process.exitCode = 2;
4054
+ const resumeClaim = opts.resume
4055
+ ? tryAcquireResumeLaunchClaim(opts.resume)
4056
+ : undefined;
4057
+ if (opts.resume && !resumeClaim) {
4058
+ process.stderr.write(`[h2a] conversation ${opts.resume} is already being resumed; duplicate launch request skipped.\n`);
4064
4059
  return;
4065
4060
  }
4066
- let initialPrompt;
4067
- if (opts.promptStdin) {
4068
- initialPrompt = readFileSync(0, "utf8");
4069
- }
4070
- // Single-writer guard: refuse to resume a conversation another live
4071
- // session (local registry / remote pod) is already writing.
4072
- if (opts.resume) {
4073
- const ok = await guardConvWriters({
4074
- convId: opts.resume,
4075
- cwd,
4076
- ...(opts.force ? { force: true } : {}),
4077
- fetchRemoteSessions: async () => {
4078
- const url = getConfiguredRemoteOptional();
4079
- return url ? await listRemoteSessions(url) : [];
4080
- },
4061
+ try {
4062
+ const reservedTmuxSlugs = tmuxAvailable()
4063
+ ? existingLocalSessionSlugs(labels, cwd)
4064
+ : [];
4065
+ // The inventory and this decision are separate reads: a structured
4066
+ // launch can leave a name visible just before its last tmux process
4067
+ // exits. Re-probe every canonical/legacy spelling and reclaim the slug
4068
+ // only when ALL probes positively report death. A live or unknown
4069
+ // spelling remains reserved, so probe failure can never authorize a
4070
+ // second writer.
4071
+ const tmuxExistence = reservedTmuxSlugs.map((slug) => {
4072
+ const probes = managedSessionCandidates(slug).map((name) => probeTmuxSession(name));
4073
+ const probe = probes.includes("live")
4074
+ ? "live"
4075
+ : probes.includes("unknown")
4076
+ ? "unknown"
4077
+ : "dead";
4078
+ return { slug, probe };
4081
4079
  });
4082
- if (!ok) {
4080
+ const unknownTmuxSlugs = tmuxExistence
4081
+ .filter((probed) => probed.probe === "unknown")
4082
+ .map((probed) => probed.slug);
4083
+ if (unknownTmuxSlugs.length > 0) {
4084
+ for (const slug of unknownTmuxSlugs) {
4085
+ process.stderr.write(`[h2a] cannot start ${slug}: tmux host state is unknown (the probe failed) — refusing to reclaim an unproven session name.\n`);
4086
+ }
4083
4087
  process.exitCode = 1;
4084
4088
  return;
4085
4089
  }
4086
- }
4087
- // By default launches are direct. --llm-gateway/--gw opts into the
4088
- // local gateway for any profile that consumes Anthropic-compatible env.
4089
- let gatewayMode;
4090
- try {
4091
- gatewayMode = gatewayModeFromOptions(opts);
4092
- }
4093
- catch (error) {
4094
- process.stderr.write(`[h2a] ${error.message}.\n`);
4095
- process.exitCode = 1;
4096
- return;
4097
- }
4098
- if (gatewayMode === "gateway" && !profileUsesLlmMeshGateway(profile)) {
4099
- process.stderr.write(`[h2a] --llm-gateway/--gw is unsupported for ${profile}; this gateway is Anthropic-compatible and only Claude profiles consume it.\n`);
4100
- process.exitCode = 2;
4101
- return;
4102
- }
4103
- const launchGatewayMode = gatewayModeForProfile(profile, gatewayMode);
4104
- const command = localCliCommand(profile);
4105
- // A detached/background or run-once launch is a worker, even when its
4106
- // agent later emits a Claude SessionStart hook. Stamp this through tmux
4107
- // so that hook cannot reclassify it as a human session.
4108
- const sessionClass = deriveSessionClass({
4109
- background: opts.background === true,
4110
- headless: opts.headless === true,
4111
- humanTerminal: opts.attachedTerminal !== false,
4112
- });
4113
- let activeGateway;
4114
- const h2a = getH2aConfig();
4115
- const h2aSidecar = opts.h2a ?? h2a.enabled;
4116
- if (opts.headless && h2aSidecar) {
4117
- process.stderr.write("[h2a] --headless requires --no-h2a (a sidecar would keep the run-once session alive)\n");
4118
- process.exitCode = 2;
4119
- return;
4120
- }
4121
- if (opts.headless && !opts.name) {
4122
- process.stderr.write("[h2a] --headless requires --name\n");
4123
- process.exitCode = 2;
4124
- return;
4125
- }
4126
- try {
4127
- await prepareCentralMcpForLaunch({ root: cwd, profile, cwd });
4128
- }
4129
- catch (error) {
4130
- process.stderr.write(`[h2a] ${error.message}\n`);
4131
- process.exitCode = 1;
4132
- return;
4133
- }
4134
- const started = [];
4135
- for (const label of labels) {
4136
- const clientSessionId = localSessionName(slugify(label ?? cwd));
4137
- try {
4138
- if (structuredLaunch) {
4139
- activeGateway = await prepareStructuredGateway(launchGatewayMode, (selectedMode) => injectLlmMeshGatewayEnv(selectedMode, selectedMode !== "gateway", clientSessionId));
4140
- }
4141
- else {
4142
- activeGateway = await injectLlmMeshGatewayEnv(launchGatewayMode, true, clientSessionId);
4090
+ const existingTmuxSlugs = tmuxExistence
4091
+ .filter((probed) => probed.probe === "live")
4092
+ .map((probed) => probed.slug);
4093
+ // Native existence is THREE-state (the resolver's probe vocabulary):
4094
+ // a probe FAILURE is "unknown", never absence. Treating a thrown
4095
+ // probe as "no session" would let `run --tmux --resume` start a tmux
4096
+ // twin over a live-but-unprovable native writer (sol-F3): the same
4097
+ // fail-closed rule as resolveManagedHost's rule 4 applies — refuse
4098
+ // to launch on an unprovable host state.
4099
+ const nativeExistence = labels
4100
+ .map((label) => slugify(label ?? cwd))
4101
+ .map((slug) => ({
4102
+ slug,
4103
+ probe: probeNativeSession(localSessionName(slug)),
4104
+ }));
4105
+ const unknownNativeSlugs = nativeExistence
4106
+ .filter((probed) => probed.probe === "unknown")
4107
+ .map((probed) => probed.slug);
4108
+ if (unknownNativeSlugs.length > 0) {
4109
+ for (const slug of unknownNativeSlugs) {
4110
+ process.stderr.write(`[h2a] cannot start ${slug}: native host state is unknown (the probe failed) — a probe failure is never proof of absence; refusing to risk a second writer (fail closed).\n`);
4143
4111
  }
4144
- }
4145
- catch (error) {
4146
- process.stderr.write(`[h2a] ${error.message}\n`);
4147
4112
  process.exitCode = 1;
4148
4113
  return;
4149
4114
  }
4150
- const useBare = shouldUseClaudeBare(profile);
4151
- let args;
4152
- try {
4153
- args =
4154
- structuredLaunch && isAgentLaunchProfile(profile)
4155
- ? buildAgentLaunchArgs({
4156
- profile,
4157
- ...(initialPrompt !== undefined
4158
- ? { prompt: initialPrompt }
4159
- : {}),
4160
- ...(opts.model !== undefined ? { model: opts.model } : {}),
4161
- ...(opts.effort !== undefined
4162
- ? { effort: opts.effort }
4163
- : {}),
4164
- ...(opts.resume !== undefined
4165
- ? { resumeId: opts.resume }
4166
- : {}),
4167
- ...(opts.headless ? { headless: true } : {}),
4168
- ...(useBare ? { bare: true } : {}),
4169
- })
4170
- : opts.resume
4171
- ? localResumeArgs(profile, opts.resume, { bare: useBare })
4172
- : localStartArgs(profile, { bare: useBare });
4115
+ const existingNativeSlugs = nativeExistence
4116
+ .filter((probed) => probed.probe === "live")
4117
+ .map((probed) => probed.slug);
4118
+ const existingLocalSessions = [
4119
+ ...new Set([...existingTmuxSlugs, ...existingNativeSlugs]),
4120
+ ];
4121
+ if (existingLocalSessions.length > 0) {
4122
+ for (const slug of existingLocalSessions) {
4123
+ process.stderr.write(`[h2a] local session ${slug} already exists; no new ${profile} was started.\n`);
4124
+ process.stderr.write(`[h2a] attach: h2a attach ${slug}\n`);
4125
+ process.stderr.write(`[h2a] stop first: h2a stop ${slug} --reason restart\n`);
4126
+ }
4127
+ process.exitCode = 1;
4128
+ return;
4173
4129
  }
4174
- catch (error) {
4175
- process.stderr.write(`[h2a] ${error.message}\n`);
4130
+ if (!existsSync(cwd) || !statSync(cwd).isDirectory()) {
4131
+ process.stderr.write(`[h2a] workspace is not an existing directory: ${cwd}\n`);
4176
4132
  process.exitCode = 2;
4177
4133
  return;
4178
4134
  }
4179
- if (opts.resume && args.length === 0) {
4180
- process.stderr.write(`[h2a] profile "${profile}" has no verified local resume argv; start it without -r/--resume\n`);
4181
- process.exitCode = 1;
4182
- return;
4135
+ let initialPrompt;
4136
+ if (opts.promptStdin) {
4137
+ initialPrompt = readFileSync(0, "utf8");
4183
4138
  }
4184
- let name;
4185
- let slug;
4186
- let outputLog;
4187
- let resultJson;
4188
- let agentPane;
4189
- let promptFile;
4190
- let promptDelivery;
4191
- if (opts.headless) {
4192
- const runDir = join(cwd, ".h2a", "runs", label);
4193
- mkdirSync(runDir, { recursive: true });
4194
- outputLog = join(runDir, "output.log");
4195
- resultJson = join(runDir, "result.json");
4196
- if (sessionHost === "native") {
4197
- ({ name, slug, promptFile } = startNativeHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, initialPrompt, structuredLaunch, sessionClass));
4198
- }
4199
- else {
4200
- ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, initialPrompt, structuredLaunch, sessionClass));
4139
+ // Single-writer guard: refuse to resume a conversation another live
4140
+ // session (local registry / remote pod) is already writing.
4141
+ if (opts.resume) {
4142
+ const ok = await guardConvWriters({
4143
+ convId: opts.resume,
4144
+ cwd,
4145
+ ...(opts.force ? { force: true } : {}),
4146
+ fetchRemoteSessions: async () => {
4147
+ const url = getConfiguredRemoteOptional();
4148
+ return url ? await listRemoteSessions(url) : [];
4149
+ },
4150
+ });
4151
+ if (!ok) {
4152
+ process.exitCode = 1;
4153
+ return;
4201
4154
  }
4202
4155
  }
4203
- else if (sessionHost === "native") {
4204
- ({ name, slug } = startNativeSession(profile, command, cwd, args, label, {
4205
- ...(opts.resume !== undefined ? { resumeId: opts.resume } : {}),
4206
- ...(initialPrompt !== undefined
4207
- ? { terminateOnAgentExit: true }
4208
- : {}),
4209
- ...(structuredLaunch ? { refuseExisting: true } : {}),
4210
- sessionClass,
4211
- }));
4212
- }
4213
- else {
4214
- ({ name, slug, agentPane } = startLocalSession(profile, command, cwd, args, label, getTmuxProfileConfig().profile, {
4215
- ...(opts.resume !== undefined
4216
- ? { resumeId: opts.resume }
4217
- : {}),
4218
- ...(h2aSidecar ? { h2aCommand: h2a.command } : {}),
4219
- ...(initialPrompt !== undefined
4220
- ? { terminateOnAgentExit: true }
4221
- : {}),
4222
- ...(structuredLaunch ? { refuseExisting: true } : {}),
4223
- attachedTerminal: opts.attachedTerminal !== false,
4224
- sessionClass,
4225
- }));
4156
+ // By default launches are direct. --llm-gateway/--gw opts into the
4157
+ // local gateway for any profile that consumes Anthropic-compatible env.
4158
+ const launchGatewayMode = gatewayModeForProfile(profile, gatewayMode);
4159
+ const command = localCliCommand(profile);
4160
+ // A detached/background or run-once launch is a worker, even when its
4161
+ // agent later emits a Claude SessionStart hook. Stamp this through tmux
4162
+ // so that hook cannot reclassify it as a human session.
4163
+ const sessionClass = deriveSessionClass({
4164
+ background: opts.background === true,
4165
+ headless: opts.headless === true,
4166
+ humanTerminal: opts.attachedTerminal !== false,
4167
+ });
4168
+ let activeGateway;
4169
+ const h2a = getH2aConfig();
4170
+ const h2aSidecar = opts.h2a ?? h2a.enabled;
4171
+ if (opts.headless && h2aSidecar) {
4172
+ process.stderr.write("[h2a] --headless requires --no-h2a (a sidecar would keep the run-once session alive)\n");
4173
+ process.exitCode = 2;
4174
+ return;
4226
4175
  }
4227
- if (structuredLaunch && sessionHost !== "native" && !agentPane) {
4228
- cleanupHeadlessPromptFile(promptFile);
4229
- // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4230
- // `name` is the session THIS command just created above, on the
4231
- // host it chose. Re-resolving could return missing/unknown and
4232
- // refuse — leaking the partial session it exists to remove.
4233
- killLocalSession(name);
4234
- process.stderr.write(`[h2a] could not capture the exact agent pane for ${slug}; the partial session was stopped\n`);
4235
- process.exitCode = 1;
4176
+ if (opts.headless && !opts.name) {
4177
+ process.stderr.write("[h2a] --headless requires --name\n");
4178
+ process.exitCode = 2;
4236
4179
  return;
4237
4180
  }
4238
- let h2aSidecarStarted = false;
4239
- if (h2aSidecar && sessionHost === "native") {
4240
- h2aSidecarStarted = startNativeH2aSidecar(name, cwd, h2a.command, {
4241
- ...(structuredLaunch ? { verified: true } : {}),
4242
- });
4243
- if (!h2aSidecarStarted && structuredLaunch) {
4244
- cleanupHeadlessPromptFile(promptFile);
4245
- // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4246
- // this kills the session THIS command just created on the host
4247
- // it chose; the resolver could refuse and leak the partial.
4248
- killNativeSessionTree(name);
4249
- process.stderr.write(`[h2a] required h2a sidecar failed for ${slug}; the partial session was stopped\n`);
4181
+ const centralMcp = await prepareCentralMcpForLaunch({ root: cwd, profile, cwd });
4182
+ if (centralMcp?.status === "degraded") {
4183
+ process.stderr.write(`[h2a] central MCP unavailable, falling back to per-session sidecar: ${centralMcp.reason}\n`);
4184
+ }
4185
+ const started = [];
4186
+ for (const label of labels) {
4187
+ const clientSessionId = localSessionName(slugify(label ?? cwd));
4188
+ try {
4189
+ if (structuredLaunch) {
4190
+ activeGateway = await prepareStructuredGateway(launchGatewayMode, (selectedMode) => injectLlmMeshGatewayEnv(selectedMode, selectedMode !== "gateway", clientSessionId));
4191
+ }
4192
+ else {
4193
+ activeGateway = await injectLlmMeshGatewayEnv(launchGatewayMode, true, clientSessionId);
4194
+ }
4195
+ }
4196
+ catch (error) {
4197
+ process.stderr.write(`[h2a] ${error.message}\n`);
4250
4198
  process.exitCode = 1;
4251
4199
  return;
4252
4200
  }
4253
- if (h2aSidecarStarted) {
4254
- process.stderr.write(`[h2a] h2a sidecar session started for ${slug} (${h2a.command})\n`);
4201
+ const useBare = shouldUseClaudeBare(profile);
4202
+ let args;
4203
+ try {
4204
+ args =
4205
+ structuredLaunch && isAgentLaunchProfile(profile)
4206
+ ? buildAgentLaunchArgs({
4207
+ profile,
4208
+ ...(initialPrompt !== undefined
4209
+ ? { prompt: initialPrompt }
4210
+ : {}),
4211
+ ...(opts.agent !== undefined ? { agent: opts.agent } : {}),
4212
+ ...(opts.model !== undefined ? { model: opts.model } : {}),
4213
+ ...(opts.effort !== undefined
4214
+ ? { effort: opts.effort }
4215
+ : {}),
4216
+ ...(opts.resume !== undefined
4217
+ ? { resumeId: opts.resume }
4218
+ : {}),
4219
+ ...(opts.headless ? { headless: true } : {}),
4220
+ ...(useBare ? { bare: true } : {}),
4221
+ })
4222
+ : opts.resume
4223
+ ? localResumeArgs(profile, opts.resume, { bare: useBare })
4224
+ : localStartArgs(profile, { bare: useBare });
4255
4225
  }
4256
- }
4257
- else if (h2aSidecar) {
4258
- if (structuredLaunch) {
4259
- try {
4260
- h2aSidecarStarted = Boolean(await startH2aWindowVerified(name, cwd, h2a.command, agentPane));
4226
+ catch (error) {
4227
+ process.stderr.write(`[h2a] ${error.message}\n`);
4228
+ process.exitCode = 2;
4229
+ return;
4230
+ }
4231
+ if (opts.resume && args.length === 0) {
4232
+ process.stderr.write(`[h2a] profile "${profile}" has no verified local resume argv; start it without -r/--resume\n`);
4233
+ process.exitCode = 1;
4234
+ return;
4235
+ }
4236
+ let name;
4237
+ let slug;
4238
+ let outputLog;
4239
+ let resultJson;
4240
+ let agentPane;
4241
+ let promptFile;
4242
+ let promptDelivery;
4243
+ if (opts.json && opts.name) {
4244
+ process.stderr.write(`${STRUCTURED_LAUNCH_PHASE_PREFIX}${JSON.stringify({
4245
+ launchId: opts.name,
4246
+ phase: "creation-attempted",
4247
+ })}\n`);
4248
+ }
4249
+ if (opts.headless) {
4250
+ const runDir = join(cwd, ".h2a", "runs", label);
4251
+ mkdirSync(runDir, { recursive: true });
4252
+ outputLog = join(runDir, "output.log");
4253
+ resultJson = join(runDir, "result.json");
4254
+ const headlessPromptInput = structuredLaunch && isAgentLaunchProfile(profile)
4255
+ ? buildAgentLaunchStdin({
4256
+ profile,
4257
+ ...(initialPrompt !== undefined
4258
+ ? { prompt: initialPrompt }
4259
+ : {}),
4260
+ headless: true,
4261
+ })
4262
+ : initialPrompt;
4263
+ if (sessionHost === "native") {
4264
+ ({ name, slug, promptFile } = startNativeHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, headlessPromptInput, structuredLaunch, sessionClass));
4261
4265
  }
4262
- catch (error) {
4263
- process.stderr.write(`[h2a] required h2a sidecar verification failed: ${error.message}\n`);
4264
- h2aSidecarStarted = false;
4266
+ else {
4267
+ ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, headlessPromptInput, structuredLaunch, sessionClass));
4265
4268
  }
4266
4269
  }
4270
+ else if (sessionHost === "native") {
4271
+ ({ name, slug } = startNativeSession(profile, command, cwd, args, label, {
4272
+ ...(opts.resume !== undefined ? { resumeId: opts.resume } : {}),
4273
+ ...(initialPrompt !== undefined
4274
+ ? { terminateOnAgentExit: true }
4275
+ : {}),
4276
+ ...(structuredLaunch ? { refuseExisting: true } : {}),
4277
+ sessionClass,
4278
+ }));
4279
+ }
4267
4280
  else {
4268
- h2aSidecarStarted = startH2aWindow(name, cwd, h2a.command);
4281
+ ({ name, slug, agentPane } = startLocalSession(profile, command, cwd, args, label, getTmuxProfileConfig().profile, {
4282
+ ...(opts.resume !== undefined
4283
+ ? { resumeId: opts.resume }
4284
+ : {}),
4285
+ ...(h2aSidecar ? { h2aCommand: h2a.command } : {}),
4286
+ ...(initialPrompt !== undefined
4287
+ ? { terminateOnAgentExit: true }
4288
+ : {}),
4289
+ ...(structuredLaunch ? { refuseExisting: true } : {}),
4290
+ attachedTerminal: opts.attachedTerminal !== false,
4291
+ sessionClass,
4292
+ }));
4269
4293
  }
4270
- if (!h2aSidecarStarted && structuredLaunch) {
4294
+ if (structuredLaunch && sessionHost !== "native" && !agentPane) {
4271
4295
  cleanupHeadlessPromptFile(promptFile);
4272
4296
  // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4273
- // this kills the session THIS command just created on the host
4274
- // it chose; the resolver could refuse and leak the partial.
4297
+ // `name` is the session THIS command just created above, on the
4298
+ // host it chose. Re-resolving could return missing/unknown and
4299
+ // refuse — leaking the partial session it exists to remove.
4275
4300
  killLocalSession(name);
4276
- process.stderr.write(`[h2a] required h2a sidecar failed for ${slug}; the partial session was stopped\n`);
4301
+ process.stderr.write(`[h2a] could not capture the exact agent pane for ${slug}; the partial session was stopped\n`);
4277
4302
  process.exitCode = 1;
4278
4303
  return;
4279
4304
  }
4280
- if (h2aSidecarStarted) {
4281
- process.stderr.write(`[h2a] h2a window started in ${slug} (${h2a.command})\n`);
4282
- }
4283
- }
4284
- if (!opts.headless && initialPrompt !== undefined) {
4285
- // NEVER paste-and-hope: `tmux new-session -d` returns before the
4286
- // agent TUI draws anything, and a prompt sent into that window is
4287
- // echoed by the raw terminal and then discarded. Delivery is
4288
- // OBSERVED — brief seen in a real composer, then real work — because
4289
- // reporting "started" over a lost brief is what left a lane inert
4290
- // for 33 minutes on 2026-07-28.
4291
- promptDelivery = sessionHost === "native"
4292
- ? deliverInitialPrompt(name, initialPrompt, nativePromptDeliveryDeps(sleepSync))
4293
- : deliverInitialPrompt(agentPane, initialPrompt, {
4294
- capturePane: capturePaneVisible,
4295
- clearComposer: clearPaneComposer,
4296
- pasteBlock: pasteLiteralBlock,
4297
- submit: submitPane,
4298
- cpuMs: paneTreeCpuMs,
4299
- sleep: sleepSync,
4300
- now: () => Date.now(),
4305
+ let h2aSidecarStarted = false;
4306
+ if (h2aSidecar && sessionHost === "native") {
4307
+ h2aSidecarStarted = startNativeH2aSidecar(name, cwd, h2a.command, {
4308
+ ...(structuredLaunch ? { verified: true } : {}),
4301
4309
  });
4302
- if (promptDelivery.state !== "working") {
4310
+ if (!h2aSidecarStarted && structuredLaunch) {
4311
+ cleanupHeadlessPromptFile(promptFile);
4312
+ // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4313
+ // this kills the session THIS command just created on the host
4314
+ // it chose; the resolver could refuse and leak the partial.
4315
+ killNativeSessionTree(name);
4316
+ process.stderr.write(`[h2a] required h2a sidecar failed for ${slug}; the partial session was stopped\n`);
4317
+ process.exitCode = 1;
4318
+ return;
4319
+ }
4320
+ if (h2aSidecarStarted) {
4321
+ process.stderr.write(`[h2a] h2a sidecar session started for ${slug} (${h2a.command})\n`);
4322
+ }
4323
+ }
4324
+ else if (h2aSidecar) {
4325
+ if (structuredLaunch) {
4326
+ try {
4327
+ h2aSidecarStarted = Boolean(await startH2aWindowVerified(name, cwd, h2a.command, agentPane));
4328
+ }
4329
+ catch (error) {
4330
+ process.stderr.write(`[h2a] required h2a sidecar verification failed: ${error.message}\n`);
4331
+ h2aSidecarStarted = false;
4332
+ }
4333
+ }
4334
+ else {
4335
+ h2aSidecarStarted = startH2aWindow(name, cwd, h2a.command);
4336
+ }
4337
+ if (!h2aSidecarStarted && structuredLaunch) {
4338
+ cleanupHeadlessPromptFile(promptFile);
4339
+ // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4340
+ // this kills the session THIS command just created on the host
4341
+ // it chose; the resolver could refuse and leak the partial.
4342
+ killLocalSession(name);
4343
+ process.stderr.write(`[h2a] required h2a sidecar failed for ${slug}; the partial session was stopped\n`);
4344
+ process.exitCode = 1;
4345
+ return;
4346
+ }
4347
+ if (h2aSidecarStarted) {
4348
+ process.stderr.write(`[h2a] h2a window started in ${slug} (${h2a.command})\n`);
4349
+ }
4350
+ }
4351
+ if (!opts.headless && initialPrompt !== undefined) {
4352
+ // NEVER paste-and-hope: `tmux new-session -d` returns before the
4353
+ // agent TUI draws anything, and a prompt sent into that window is
4354
+ // echoed by the raw terminal and then discarded. Delivery is
4355
+ // OBSERVED — brief seen in a real composer, then real work — because
4356
+ // reporting "started" over a lost brief is what left a lane inert
4357
+ // for 33 minutes on 2026-07-28.
4358
+ promptDelivery = sessionHost === "native"
4359
+ ? deliverInitialPrompt(name, initialPrompt, nativePromptDeliveryDeps(sleepSync))
4360
+ : deliverInitialPrompt(agentPane, initialPrompt, {
4361
+ capturePane: capturePaneVisible,
4362
+ clearComposer: clearPaneComposer,
4363
+ pasteBlock: pasteLiteralBlock,
4364
+ submit: submitPane,
4365
+ cpuMs: paneTreeCpuMs,
4366
+ sleep: sleepSync,
4367
+ now: () => Date.now(),
4368
+ });
4369
+ if (promptDelivery.state !== "working") {
4370
+ cleanupHeadlessPromptFile(promptFile);
4371
+ // Only claim the session is gone when the kill actually reported
4372
+ // success. "nothing is left running" over an unchecked call is the
4373
+ // same wider-than-its-evidence claim this change exists to remove,
4374
+ // and a surviving partial session is precisely the idle lane the
4375
+ // operator was just told could not happen.
4376
+ // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4377
+ // `name`/`sessionHost` are the session and host THIS command
4378
+ // just created; re-resolving could refuse and leak the partial.
4379
+ const stopped = sessionHost === "native"
4380
+ ? killNativeSessionTree(name)
4381
+ : killLocalSession(name);
4382
+ const detail = promptDelivery.state === "submitted-idle"
4383
+ ? `the brief was submitted but the agent never started working ` +
4384
+ `(${Math.round(promptDelivery.cpuDeltaMs)}ms of CPU): it is not "started"`
4385
+ : promptDelivery.reason;
4386
+ process.stderr.write(`[h2a] the initial prompt did not reach ${slug}: ${detail}\n` +
4387
+ (promptDelivery.state === "host-modal"
4388
+ ? `[h2a] fix: ${promptDelivery.hint}\n`
4389
+ : "") +
4390
+ (stopped
4391
+ ? `[h2a] the partial session was stopped, so nothing is left running idle\n`
4392
+ : `[h2a] WARNING: stopping the partial session FAILED — ${name} may still be alive; ` +
4393
+ `list it with h2a ls and stop it with h2a stop ${slug} --reason failed-launch\n`) +
4394
+ (promptDelivery.state !== "submitted-idle" &&
4395
+ promptDelivery.capture
4396
+ ? `[h2a] last screen:\n${promptDelivery.capture}\n`
4397
+ : ""));
4398
+ process.exitCode = 1;
4399
+ return;
4400
+ }
4401
+ }
4402
+ const pid = sessionHost === "native"
4403
+ ? nativeSessionPid(name)
4404
+ : agentPane
4405
+ ? localSessionPanePid(agentPane)
4406
+ : undefined;
4407
+ const workerPid = sessionHost === "native"
4408
+ ? nativeWorkerPid(name)
4409
+ : agentPane !== undefined
4410
+ ? paneWorkerPid(agentPane)
4411
+ : undefined;
4412
+ if (structuredLaunch && pid === undefined) {
4303
4413
  cleanupHeadlessPromptFile(promptFile);
4304
- // Only claim the session is gone when the kill actually reported
4305
- // success. "nothing is left running" over an unchecked call is the
4306
- // same wider-than-its-evidence claim this change exists to remove,
4307
- // and a surviving partial session is precisely the idle lane the
4308
- // operator was just told could not happen.
4309
4414
  // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4310
4415
  // `name`/`sessionHost` are the session and host THIS command
4311
4416
  // just created; re-resolving could refuse and leak the partial.
4312
- const stopped = sessionHost === "native"
4313
- ? killNativeSessionTree(name)
4314
- : killLocalSession(name);
4315
- const detail = promptDelivery.state === "submitted-idle"
4316
- ? `the brief was submitted but the agent never started working ` +
4317
- `(${Math.round(promptDelivery.cpuDeltaMs)}ms of CPU): it is not "started"`
4318
- : promptDelivery.reason;
4319
- process.stderr.write(`[h2a] the initial prompt did not reach ${slug}: ${detail}\n` +
4320
- (promptDelivery.state === "host-modal"
4321
- ? `[h2a] fix: ${promptDelivery.hint}\n`
4322
- : "") +
4323
- (stopped
4324
- ? `[h2a] the partial session was stopped, so nothing is left running idle\n`
4325
- : `[h2a] WARNING: stopping the partial session FAILED — ${name} may still be alive; ` +
4326
- `list it with h2a ls and stop it with h2a stop ${slug} --reason failed-launch\n`) +
4327
- (promptDelivery.state !== "submitted-idle" &&
4328
- promptDelivery.capture
4329
- ? `[h2a] last screen:\n${promptDelivery.capture}\n`
4330
- : ""));
4417
+ if (sessionHost === "native")
4418
+ killNativeSessionTree(name);
4419
+ else
4420
+ killLocalSession(name);
4421
+ process.stderr.write(`[h2a] could not verify the agent pane pid for ${slug}; the partial session was stopped\n`);
4331
4422
  process.exitCode = 1;
4332
4423
  return;
4333
4424
  }
4425
+ // Auto-enroll in the live-session registry (feeds `h2a ls`/`restore`).
4426
+ // Pin the gateway posture ONLY when the user was explicit (--gw/--no-gw);
4427
+ // an "auto" launch stays unpinned so restore follows the live default.
4428
+ enrollFromRun({
4429
+ profile,
4430
+ slug,
4431
+ tmuxSession: name,
4432
+ // The ACTUAL host that just started the terminal — never a default.
4433
+ hostKind: sessionHost === "native" ? "local-native" : "local-tmux",
4434
+ ...(pid !== undefined ? { pid } : {}),
4435
+ ...(workerPid !== undefined ? { workerPid } : {}),
4436
+ cwd,
4437
+ sessionClass,
4438
+ ...(opts.resume !== undefined ? { convId: opts.resume } : {}),
4439
+ ...(gatewayMode !== "auto" ? { gatewayMode } : {}),
4440
+ });
4441
+ started.push({
4442
+ name,
4443
+ slug,
4444
+ ...(agentPane !== undefined ? { pane: agentPane } : {}),
4445
+ h2aSidecar: h2aSidecarStarted,
4446
+ ...(pid !== undefined ? { pid } : {}),
4447
+ ...(outputLog !== undefined ? { outputLog } : {}),
4448
+ ...(resultJson !== undefined ? { resultJson } : {}),
4449
+ ...(promptDelivery !== undefined ? { promptDelivery } : {}),
4450
+ });
4451
+ // The writer is now durable in the registry. End the short critical
4452
+ // section before a foreground attach can keep this command alive.
4453
+ resumeClaim?.release();
4334
4454
  }
4335
- const pid = sessionHost === "native"
4336
- ? nativeSessionPid(name)
4337
- : agentPane
4338
- ? localSessionPanePid(agentPane)
4339
- : undefined;
4340
- const workerPid = sessionHost === "native"
4341
- ? nativeWorkerPid(name)
4342
- : agentPane !== undefined
4343
- ? paneWorkerPid(agentPane)
4344
- : undefined;
4345
- if (structuredLaunch && pid === undefined) {
4346
- cleanupHeadlessPromptFile(promptFile);
4347
- // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4348
- // `name`/`sessionHost` are the session and host THIS command
4349
- // just created; re-resolving could refuse and leak the partial.
4350
- if (sessionHost === "native")
4351
- killNativeSessionTree(name);
4352
- else
4353
- killLocalSession(name);
4354
- process.stderr.write(`[h2a] could not verify the agent pane pid for ${slug}; the partial session was stopped\n`);
4355
- process.exitCode = 1;
4356
- return;
4455
+ if (count > 1) {
4456
+ process.stderr.write(`[h2a] ${started.length} ${profile} agents started in ${cwd}: ${started.map((s) => s.slug).join(", ")}\n` +
4457
+ `[h2a] attach one with: h2a attach <slug>\n`);
4458
+ return; // never auto-attach a fleet
4357
4459
  }
4358
- // Auto-enroll in the live-session registry (feeds `h2a ls`/`restore`).
4359
- // Pin the gateway posture ONLY when the user was explicit (--gw/--no-gw);
4360
- // an "auto" launch stays unpinned so restore follows the live default.
4361
- enrollFromRun({
4362
- profile,
4363
- slug,
4364
- tmuxSession: name,
4365
- // The ACTUAL host that just started the terminal — never a default.
4366
- hostKind: sessionHost === "native" ? "local-native" : "local-tmux",
4367
- ...(pid !== undefined ? { pid } : {}),
4368
- ...(workerPid !== undefined ? { workerPid } : {}),
4369
- cwd,
4370
- sessionClass,
4371
- ...(opts.resume !== undefined ? { convId: opts.resume } : {}),
4372
- ...(gatewayMode !== "auto" ? { gatewayMode } : {}),
4373
- });
4374
- started.push({
4375
- name,
4376
- slug,
4377
- ...(agentPane !== undefined ? { pane: agentPane } : {}),
4378
- h2aSidecar: h2aSidecarStarted,
4379
- ...(pid !== undefined ? { pid } : {}),
4380
- ...(outputLog !== undefined ? { outputLog } : {}),
4381
- ...(resultJson !== undefined ? { resultJson } : {}),
4382
- ...(promptDelivery !== undefined ? { promptDelivery } : {}),
4383
- });
4384
- }
4385
- if (count > 1) {
4386
- process.stderr.write(`[h2a] ${started.length} ${profile} agents started in ${cwd}: ${started.map((s) => s.slug).join(", ")}\n` +
4387
- `[h2a] attach one with: h2a attach <slug>\n`);
4388
- return; // never auto-attach a fleet
4389
- }
4390
- const only = started[0];
4391
- if (opts.json) {
4392
- process.stdout.write(`${JSON.stringify({
4393
- kind: "h2a.run.result",
4394
- version: 1,
4395
- apiVersion: H2A_RUN_API_VERSION,
4396
- runtimeVersion: H2A_RUNTIME_VERSION,
4397
- ok: true,
4398
- state: "started",
4399
- session: {
4400
- id: only.slug,
4401
- tmuxSession: only.name,
4402
- host: sessionHost === "native" ? "native" : "tmux",
4403
- profile,
4404
- workspace: cwd,
4405
- mode: opts.headless ? "headless" : "interactive",
4406
- background: opts.background === true,
4407
- gateway: activeGateway ? "gateway" : "direct",
4408
- h2aSidecar: only.h2aSidecar,
4409
- ...(only.pane !== undefined ? { pane: only.pane } : {}),
4410
- ...(only.pid !== undefined ? { pid: only.pid } : {}),
4411
- // `pid` is the launch WRAPPER. Measured 2026-07-29: it showed 0s
4412
- // of CPU after 37s while its codex child had burned 13s, so a
4413
- // liveness check on `pid` alone reports a working agent as dead.
4414
- // Name the process that actually works, and say it is distinct.
4415
- ...(only.workerPid !== undefined
4416
- ? { workerPid: only.workerPid }
4460
+ const only = started[0];
4461
+ if (opts.json) {
4462
+ process.stdout.write(`${JSON.stringify({
4463
+ kind: "h2a.run.result",
4464
+ version: 1,
4465
+ apiVersion: H2A_RUN_API_VERSION,
4466
+ runtimeVersion: H2A_RUNTIME_VERSION,
4467
+ ok: true,
4468
+ state: "started",
4469
+ session: {
4470
+ id: only.slug,
4471
+ tmuxSession: only.name,
4472
+ host: sessionHost === "native" ? "native" : "tmux",
4473
+ profile,
4474
+ workspace: cwd,
4475
+ mode: opts.headless ? "headless" : "interactive",
4476
+ background: opts.background === true,
4477
+ gateway: activeGateway ? "gateway" : "direct",
4478
+ h2aSidecar: only.h2aSidecar,
4479
+ ...(only.pane !== undefined ? { pane: only.pane } : {}),
4480
+ ...(only.pid !== undefined ? { pid: only.pid } : {}),
4481
+ // `pid` is the launch WRAPPER. Measured 2026-07-29: it showed 0s
4482
+ // of CPU after 37s while its codex child had burned 13s, so a
4483
+ // liveness check on `pid` alone reports a working agent as dead.
4484
+ // Name the process that actually works, and say it is distinct.
4485
+ ...(only.workerPid !== undefined
4486
+ ? { workerPid: only.workerPid }
4487
+ : {}),
4488
+ },
4489
+ attach: opts.headless
4490
+ ? null
4491
+ : { command: "h2a", args: ["attach", only.slug] },
4492
+ // What was OBSERVED, not what was attempted: the brief was seen in
4493
+ // a real composer, submitted, and the agent's CPU then moved.
4494
+ ...(only.promptDelivery?.state === "working"
4495
+ ? {
4496
+ prompt: {
4497
+ delivered: true,
4498
+ waitedMs: only.promptDelivery.waitedMs,
4499
+ cpuDeltaMs: Math.round(only.promptDelivery.cpuDeltaMs),
4500
+ },
4501
+ }
4417
4502
  : {}),
4418
- },
4419
- attach: opts.headless
4420
- ? null
4421
- : { command: "h2a", args: ["attach", only.slug] },
4422
- // What was OBSERVED, not what was attempted: the brief was seen in
4423
- // a real composer, submitted, and the agent's CPU then moved.
4424
- ...(only.promptDelivery?.state === "working"
4425
- ? {
4426
- prompt: {
4427
- delivered: true,
4428
- waitedMs: only.promptDelivery.waitedMs,
4429
- cpuDeltaMs: Math.round(only.promptDelivery.cpuDeltaMs),
4430
- },
4431
- }
4432
- : {}),
4433
- ...(only.outputLog !== undefined
4434
- ? {
4435
- logs: { path: only.outputLog },
4436
- result: { path: only.resultJson },
4437
- }
4438
- : {}),
4439
- })}\n`);
4503
+ ...(only.outputLog !== undefined
4504
+ ? {
4505
+ logs: { path: only.outputLog },
4506
+ result: { path: only.resultJson },
4507
+ }
4508
+ : {}),
4509
+ })}\n`);
4510
+ return;
4511
+ }
4512
+ process.stderr.write(`[h2a] local session ${only.slug} started (${profile}${opts.resume ? ` --resume ${opts.resume}` : ""} in ${cwd})\n`);
4513
+ // Single local runs hand off the terminal by default: there is no value
4514
+ // in making users type a second `h2a attach <slug>` command. Commander
4515
+ // sets opts.attach=false only for --no-attach.
4516
+ if (opts.attach === false) {
4517
+ process.stderr.write(`[h2a] attach with: h2a attach ${only.slug}\n`);
4518
+ return;
4519
+ }
4520
+ process.exitCode =
4521
+ sessionHost === "native"
4522
+ ? attachNativeSession(only.name)
4523
+ : attachLocalSession(only.name);
4440
4524
  return;
4441
4525
  }
4442
- process.stderr.write(`[h2a] local session ${only.slug} started (${profile}${opts.resume ? ` --resume ${opts.resume}` : ""} in ${cwd})\n`);
4443
- // Single local runs hand off the terminal by default: there is no value
4444
- // in making users type a second `h2a attach <slug>` command. Commander
4445
- // sets opts.attach=false only for --no-attach.
4446
- if (opts.attach === false) {
4447
- process.stderr.write(`[h2a] attach with: h2a attach ${only.slug}\n`);
4448
- return;
4526
+ finally {
4527
+ resumeClaim?.release();
4449
4528
  }
4450
- process.exitCode =
4451
- sessionHost === "native"
4452
- ? attachNativeSession(only.name)
4453
- : attachLocalSession(only.name);
4454
- return;
4455
4529
  });
4456
4530
  // ---------------------------------------------------------------------------
4457
4531
  // delegate — spawn a cross-type agent (claude/codex/agy) as a LOCAL job (P1)
@@ -6695,13 +6769,9 @@ export async function main(argv) {
6695
6769
  }
6696
6770
  if (forceGateway)
6697
6771
  restoreOpts.forceGateway = forceGateway;
6698
- try {
6699
- await prepareCentralMcpForRestore({ root: process.cwd() });
6700
- }
6701
- catch (error) {
6702
- process.stderr.write(`[h2a] ${error.message}\n`);
6703
- process.exitCode = 1;
6704
- return;
6772
+ const centralMcp = await prepareCentralMcpForRestore({ root: process.cwd() });
6773
+ if (centralMcp?.status === "degraded") {
6774
+ process.stderr.write(`[h2a] central MCP unavailable, falling back to per-session sidecar: ${centralMcp.reason}\n`);
6705
6775
  }
6706
6776
  if (needRemote) {
6707
6777
  const url = getConfiguredRemote();