@sentropic/h2a-runtime 0.85.25 → 0.87.0

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 (68) hide show
  1. package/dist/cli-help-groups.d.ts +124 -0
  2. package/dist/cli-help-groups.d.ts.map +1 -0
  3. package/dist/cli-help-groups.js +297 -0
  4. package/dist/cli-help-groups.js.map +1 -0
  5. package/dist/enroll.d.ts.map +1 -1
  6. package/dist/enroll.js +40 -2
  7. package/dist/enroll.js.map +1 -1
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +314 -27
  11. package/dist/index.js.map +1 -1
  12. package/dist/llm-gateway-runtime/accounts.d.ts.map +1 -1
  13. package/dist/llm-gateway-runtime/accounts.js +2 -6
  14. package/dist/llm-gateway-runtime/accounts.js.map +1 -1
  15. package/dist/llm-gateway-runtime/index.d.ts.map +1 -1
  16. package/dist/llm-gateway-runtime/index.js +9 -1
  17. package/dist/llm-gateway-runtime/index.js.map +1 -1
  18. package/dist/llm-gateway-runtime/model-catalog.d.ts +10 -3
  19. package/dist/llm-gateway-runtime/model-catalog.d.ts.map +1 -1
  20. package/dist/llm-gateway-runtime/model-catalog.js +49 -119
  21. package/dist/llm-gateway-runtime/model-catalog.js.map +1 -1
  22. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts.map +1 -1
  23. package/dist/llm-gateway-runtime/proxy-anthropic.js +117 -24
  24. package/dist/llm-gateway-runtime/proxy-anthropic.js.map +1 -1
  25. package/dist/llm-gateway-runtime/proxy-gemini.d.ts.map +1 -1
  26. package/dist/llm-gateway-runtime/proxy-gemini.js +0 -2
  27. package/dist/llm-gateway-runtime/proxy-gemini.js.map +1 -1
  28. package/dist/llm-gateway-runtime/proxy-openai.d.ts +4 -7
  29. package/dist/llm-gateway-runtime/proxy-openai.d.ts.map +1 -1
  30. package/dist/llm-gateway-runtime/proxy-openai.js +18 -13
  31. package/dist/llm-gateway-runtime/proxy-openai.js.map +1 -1
  32. package/dist/llm-gateway-runtime/session-ledger.d.ts +34 -0
  33. package/dist/llm-gateway-runtime/session-ledger.d.ts.map +1 -1
  34. package/dist/llm-gateway-runtime/session-ledger.js +216 -6
  35. package/dist/llm-gateway-runtime/session-ledger.js.map +1 -1
  36. package/dist/llm-mesh.d.ts +3 -0
  37. package/dist/llm-mesh.d.ts.map +1 -1
  38. package/dist/llm-mesh.js +23 -0
  39. package/dist/llm-mesh.js.map +1 -1
  40. package/dist/protocol-local.d.ts +3 -0
  41. package/dist/protocol-local.d.ts.map +1 -1
  42. package/dist/protocol-local.js +7 -0
  43. package/dist/protocol-local.js.map +1 -1
  44. package/dist/registry.d.ts +53 -4
  45. package/dist/registry.d.ts.map +1 -1
  46. package/dist/registry.js +109 -6
  47. package/dist/registry.js.map +1 -1
  48. package/dist/restore.d.ts +75 -4
  49. package/dist/restore.d.ts.map +1 -1
  50. package/dist/restore.js +268 -24
  51. package/dist/restore.js.map +1 -1
  52. package/dist/session-class.d.ts +10 -0
  53. package/dist/session-class.d.ts.map +1 -0
  54. package/dist/session-class.js +11 -0
  55. package/dist/session-class.js.map +1 -0
  56. package/dist/status-projection.d.ts +97 -0
  57. package/dist/status-projection.d.ts.map +1 -0
  58. package/dist/status-projection.js +464 -0
  59. package/dist/status-projection.js.map +1 -0
  60. package/dist/tmux-name-migration.d.ts +52 -0
  61. package/dist/tmux-name-migration.d.ts.map +1 -0
  62. package/dist/tmux-name-migration.js +408 -0
  63. package/dist/tmux-name-migration.js.map +1 -0
  64. package/dist/tmux.d.ts +54 -1
  65. package/dist/tmux.d.ts.map +1 -1
  66. package/dist/tmux.js +232 -16
  67. package/dist/tmux.js.map +1 -1
  68. package/package.json +8 -4
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { homedir } from "node:os";
6
6
  import { basename, isAbsolute, join, resolve } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { Command } from "commander";
9
+ import { applyRuntimeHelpGroups, groupRuntimeHelpItems, } from "./cli-help-groups.js";
9
10
  import { attach, createRemoteSession, getRemoteSession, listRemoteSessions, refreshRemoteSession, renameRemoteSession, sessionTerminalHealth, stopRemoteSession, } from "./attach.js";
10
11
  import { authHeaders, clearDefaultRemote, getDefaultRemote, getDefaultTarget, getDefaultTools, getH2aConfig, getJobMaxAgeHours, getMaxConcurrent, getLlmMeshRuntimeConfig, getTmuxProfileConfig, getTunnel, resolveConfigPath, setDefaultRemote, setDefaultTarget, migrateConfigHomeIfNeeded, setDefaultTools, setLlmMeshRuntimeConfig, setTmuxProfileConfig, setToken, setTunnel, } from "./config.js";
11
12
  import { ensureConnected, stopTunnel } from "./tunnel.js";
@@ -14,7 +15,10 @@ import { transmittedSecrets, secretsSummary } from "./secrets.js";
14
15
  import { localConvStat, remoteConvStat, alignment } from "./convsync.js";
15
16
  import { gitAlignment, localAncestry, localGitStat, remoteGitStat, } from "./gitdiff.js";
16
17
  import { syncConversation } from "./sync.js";
17
- import { attachLocalSession, attachPodTmux, capturePane, conductorRunning, currentTmuxSessionIs, ensureManagedTmuxProfile, existingLocalSessionSlugs, fanoutLabels, findLocalSession, killLocalSession, localSessionGatewayEnvStatus, listLocalSessions, localSessionIdle, localSessionPanePid, localSessionName, managedSessionCandidates, parseManagedSessionName, readLaunchContext, relaunchInSession, resolveAgentPaneForInstance, resolveLocalSession, runLocalCliForeground, sendKeysLiteral, sessionAttached, sessionAttachedCount, setLocalSessionDisplayName, slugify, cleanupHeadlessPromptFile, startH2aWindow, startH2aWindowVerified, startHeadlessSession, startLocalSession, tmuxAvailable, } from "./tmux.js";
18
+ import { attachLocalSession, attachPodTmux, capturePane, conductorRunning, currentTmuxSessionName, currentTmuxSessionIs, ensureManagedTmuxProfile, existingLocalSessionSlugs, fanoutLabels, findLocalSession, killLocalSession, localSessionGatewayEnvStatus, listLocalSessions, localSessionIdle, localSessionPanePid, localSessionName, installH2aStatusSurface, managedSessionCandidates, parseManagedSessionName, openH2aStatusWindow, persistLaunchContext, readLaunchContext, relaunchInSession, resolveAgentPaneForInstance, resolveLocalSession, runLocalCliForeground, sendKeysLiteral, sessionAttached, sessionAttachedCount, setLocalSessionDisplayName, slugify, cleanupHeadlessPromptFile, startH2aWindow, startH2aWindowVerified, startHeadlessSession, startLocalSession, tmuxAvailable, uninstallH2aStatusSurface, } from "./tmux.js";
19
+ import { buildLaunchContext } from "./launch-context.js";
20
+ import { migrateTmuxNames } from "./tmux-name-migration.js";
21
+ import { projectStatusForH2a } from "./status-projection.js";
18
22
  import { buildAgentLaunchArgs, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
19
23
  import { planRelaunch } from "./relaunch.js";
20
24
  import { readLastLayout, restore as restoreLayout, } from "./restore.js";
@@ -59,8 +63,8 @@ import { createInterface } from "node:readline";
59
63
  import { appendSessionLogEntry, clearBinding, clearExhaustion, enrollAccount, listAccounts, listAccountsWithTokens, listAccountsWithStatus, listBindings, lookupBinding, markExhausted, readClaudeCredential, readCodexCredential, removeAccount, selectAccount, selectAccountWithFallback, stickyBind, loadCandidates, sessionLogPath, exportSessionLogToS3, QUOTA_WINDOW_5H_MS, QUOTA_WINDOW_WEEK_MS, } from "./account-pool.js";
60
64
  const H2A_RUN_API_VERSION = "h2a.run/v1";
61
65
  const H2A_RUNTIME_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
62
- import { CLI_PROFILES } from "./protocol-local.js";
63
- import { enrollCodexAccount, enrollClaudeAccount, enrollGeminiAccount, readLlmMeshConfig, startGateway, stopGateway, writeLlmMeshConfig, readGatewayPid, llmMeshLogPath, jwtExpiry, refreshAccountToken, readLlmMeshSessionEnv, acquireLlmMeshSessionEnv, } from "./llm-mesh.js";
66
+ import { CLI_PROFILES, gatewayModeForProfile, profileUsesLlmMeshGateway, } from "./protocol-local.js";
67
+ import { enrollCodexAccount, enrollClaudeAccount, enrollGeminiAccount, readLlmMeshConfig, startGateway, stopGateway, writeLlmMeshConfig, readGatewayPid, llmMeshLogPath, jwtExpiry, refreshAccountToken, replaceAnthropicGatewayEnvironment, readLlmMeshSessionEnv, acquireLlmMeshSessionEnv, } from "./llm-mesh.js";
64
68
  const KNOWN_PROFILE_HELP = `${CLI_PROFILES.join(", ")} (aliases: claude-code, antigravity, gemini-cli, mistralcli)`;
65
69
  export const packageName = "@sentropic/h2a-runtime";
66
70
  export const H2A_RUNTIME_CLI_API_VERSION = 1;
@@ -754,10 +758,20 @@ export async function startJob(job) {
754
758
  kind: "remote",
755
759
  cwd: ws.id,
756
760
  source: "remote",
761
+ sessionClass: "background",
757
762
  label: job.id,
758
763
  remoteId: session.id,
759
764
  role: "job",
760
765
  jobState: "running",
766
+ ...(job.delegationOrigin !== undefined
767
+ ? { delegationOrigin: job.delegationOrigin }
768
+ : {}),
769
+ ...(job.delegatorInstance !== undefined
770
+ ? { delegatorInstance: job.delegatorInstance }
771
+ : {}),
772
+ ...(job.delegatorTmuxSession !== undefined
773
+ ? { delegatorTmuxSession: job.delegatorTmuxSession }
774
+ : {}),
761
775
  // Persist originCwd so reconcile reads result.json under the right dir
762
776
  // (H2) regardless of the conductor's cwd.
763
777
  ...(job.originCwd !== undefined ? { originCwd: job.originCwd } : {}),
@@ -860,23 +874,52 @@ export async function startJob(job) {
860
874
  // Inject llm-mesh gateway env if running. Do not trust a parent env that may
861
875
  // contain an old gateway token from a previous restart.
862
876
  // tmux inherits process.env, so claude + its subagents all get the gateway automatically.
863
- const meshEnv = getLlmMeshRuntimeConfig().enabled
864
- ? readLlmMeshSessionEnv()
865
- : null;
866
- if (meshEnv) {
867
- for (const [k, v] of Object.entries(meshEnv)) {
868
- prevAccountEnvs[k] = process.env[k];
869
- process.env[k] = v;
877
+ const jobSessionSlug = slugify(job.id);
878
+ const jobSessionResolution = resolveLocalSession(jobSessionSlug);
879
+ const gatewayClientSessionId = jobSessionResolution.kind === "found"
880
+ ? jobSessionResolution.session.name
881
+ : jobSessionResolution.kind === "missing"
882
+ ? localSessionName(jobSessionSlug)
883
+ : undefined;
884
+ let meshEnv;
885
+ try {
886
+ meshEnv =
887
+ profileUsesLlmMeshGateway(job.tool) &&
888
+ getLlmMeshRuntimeConfig().enabled &&
889
+ gatewayClientSessionId
890
+ ? await acquireLlmMeshSessionEnv(undefined, gatewayClientSessionId)
891
+ : null;
892
+ }
893
+ catch (error) {
894
+ for (const [key, previous] of Object.entries(prevAccountEnvs)) {
895
+ if (previous === undefined)
896
+ delete process.env[key];
897
+ else
898
+ process.env[key] = previous;
870
899
  }
900
+ return {
901
+ started: false,
902
+ error: error instanceof Error ? error.message : String(error),
903
+ };
871
904
  }
905
+ const restoreGatewayEnvironment = replaceAnthropicGatewayEnvironment(process.env, meshEnv ?? undefined);
872
906
  let tmuxSession;
907
+ let workerPid;
873
908
  try {
874
909
  if (headless) {
875
910
  const dir = jobDir(originCwd, job.id);
876
- ({ name: tmuxSession } = startHeadlessSession(job.tool, argv.command, runCwd, argv.args, join(dir, "result.json"), join(dir, "output.log"), job.id));
911
+ const launched = startHeadlessSession(job.tool, argv.command, runCwd, argv.args, join(dir, "result.json"), join(dir, "output.log"), job.id, undefined, undefined, false, "background");
912
+ tmuxSession = launched.name;
913
+ workerPid = launched.agentPane
914
+ ? localSessionPanePid(launched.agentPane)
915
+ : undefined;
877
916
  }
878
917
  else {
879
- ({ name: tmuxSession } = startLocalSession(job.tool, argv.command, runCwd, argv.args, job.id));
918
+ const launched = startLocalSession(job.tool, argv.command, runCwd, argv.args, job.id, undefined, { sessionClass: "background" });
919
+ tmuxSession = launched.name;
920
+ workerPid = launched.agentPane
921
+ ? localSessionPanePid(launched.agentPane)
922
+ : undefined;
880
923
  const h2a = getH2aConfig();
881
924
  startH2aWindow(tmuxSession, runCwd, h2a.command);
882
925
  }
@@ -885,6 +928,7 @@ export async function startJob(job) {
885
928
  return { started: false, error: err.message };
886
929
  }
887
930
  finally {
931
+ restoreGatewayEnvironment();
888
932
  if (prevDepth === undefined)
889
933
  delete process.env[DEPTH_ENV];
890
934
  else
@@ -906,8 +950,10 @@ export async function startJob(job) {
906
950
  kind: "local-tmux",
907
951
  cwd: runCwd,
908
952
  source: "run",
953
+ sessionClass: "background",
909
954
  label: job.id,
910
955
  tmuxSession,
956
+ ...(workerPid !== undefined ? { pid: workerPid } : {}),
911
957
  role: "job",
912
958
  jobState: "running",
913
959
  // Persist originCwd (H2): result.json/output.log live under originCwd, read
@@ -915,6 +961,15 @@ export async function startJob(job) {
915
961
  originCwd,
916
962
  ...(job.task !== undefined ? { task: job.task } : {}),
917
963
  ...(job.callbackTo !== undefined ? { callbackTo: job.callbackTo } : {}),
964
+ ...(job.delegationOrigin !== undefined
965
+ ? { delegationOrigin: job.delegationOrigin }
966
+ : {}),
967
+ ...(job.delegatorInstance !== undefined
968
+ ? { delegatorInstance: job.delegatorInstance }
969
+ : {}),
970
+ ...(job.delegatorTmuxSession !== undefined
971
+ ? { delegatorTmuxSession: job.delegatorTmuxSession }
972
+ : {}),
918
973
  });
919
974
  mirrorNew();
920
975
  return {
@@ -1000,9 +1055,14 @@ export function resumeThrottledJob(job) {
1000
1055
  process.env[DEPTH_ENV] = childDepthEnvValue(job.depthBudget ?? clampDepth(undefined));
1001
1056
  process.env[JOB_ID_ENV] = job.id;
1002
1057
  let tmuxSession;
1058
+ let workerPid;
1003
1059
  try {
1004
1060
  const dir = jobDir(originCwd, job.id);
1005
- ({ name: tmuxSession } = startHeadlessSession(job.tool, argv.command, runCwd, argv.args, join(dir, "result.json"), join(dir, "output.log"), job.id));
1061
+ const launched = startHeadlessSession(job.tool, argv.command, runCwd, argv.args, join(dir, "result.json"), join(dir, "output.log"), job.id, undefined, undefined, false, "background");
1062
+ tmuxSession = launched.name;
1063
+ workerPid = launched.agentPane
1064
+ ? localSessionPanePid(launched.agentPane)
1065
+ : undefined;
1006
1066
  }
1007
1067
  catch (err) {
1008
1068
  return { started: false, error: err.message };
@@ -1038,13 +1098,24 @@ export function resumeThrottledJob(job) {
1038
1098
  kind: "local-tmux",
1039
1099
  cwd: runCwd,
1040
1100
  source: "run",
1101
+ sessionClass: "background",
1041
1102
  label: job.id,
1042
1103
  tmuxSession,
1104
+ ...(workerPid !== undefined ? { pid: workerPid } : {}),
1043
1105
  role: "job",
1044
1106
  jobState: "running",
1045
1107
  originCwd,
1046
1108
  ...(job.task !== undefined ? { task: job.task } : {}),
1047
1109
  ...(job.callbackTo !== undefined ? { callbackTo: job.callbackTo } : {}),
1110
+ ...(job.delegationOrigin !== undefined
1111
+ ? { delegationOrigin: job.delegationOrigin }
1112
+ : {}),
1113
+ ...(job.delegatorInstance !== undefined
1114
+ ? { delegatorInstance: job.delegatorInstance }
1115
+ : {}),
1116
+ ...(job.delegatorTmuxSession !== undefined
1117
+ ? { delegatorTmuxSession: job.delegatorTmuxSession }
1118
+ : {}),
1048
1119
  ...(job.throttle !== undefined ? { throttle: job.throttle } : {}),
1049
1120
  });
1050
1121
  return { started: true, target: "local", detail: `${runCwd} [resume]` };
@@ -1161,7 +1232,7 @@ function gatewayModeFromOptions(opts) {
1161
1232
  return "auto";
1162
1233
  }
1163
1234
  function shouldUseClaudeBare(profile) {
1164
- return ((profile === "claude" || profile === "claude-code") &&
1235
+ return (profileUsesLlmMeshGateway(profile) &&
1165
1236
  Boolean(process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_AUTH_TOKEN));
1166
1237
  }
1167
1238
  async function injectLlmMeshGatewayEnv(mode = "auto", allowDirectFallback = true, clientSessionId) {
@@ -1256,7 +1327,12 @@ function registryEntryForResumeTarget(target, local) {
1256
1327
  ? [target]
1257
1328
  : managedSessionCandidates(canonicalSlug);
1258
1329
  const matches = loadRegistry().filter((e) => {
1259
- if (e.role !== undefined || e.kind !== "local-tmux")
1330
+ // Resume is a human-facing operation, not a promotion path. Keep the
1331
+ // durable class intact by accepting only explicitly human rows; legacy and
1332
+ // background entries fail closed instead of becoming restorable here.
1333
+ if (e.role !== undefined ||
1334
+ e.kind !== "local-tmux" ||
1335
+ e.sessionClass !== "human")
1260
1336
  return false;
1261
1337
  // Full managed names are exact targets; never reinterpret one as an id
1262
1338
  // or label that happens to share a prefix-shaped string.
@@ -1368,8 +1444,112 @@ export async function main(argv) {
1368
1444
  const program = new Command();
1369
1445
  program
1370
1446
  .name("h2a")
1371
- .description("Wrap a local agent CLI (codex/claude/agy/gemini/mistral) and expose its session for h2a attach.")
1447
+ // The previous wording ("Wrap a local agent CLI … and expose its session for
1448
+ // h2a attach.") described h2a before the consolidation: it named only the
1449
+ // session-wrapper role and taught nothing about track, harness, focus or the
1450
+ // h2a protocol. See docs/TRANSITION.md § 1. Deliberately NOT claimed here:
1451
+ // that h2a is a native agent.
1452
+ //
1453
+ // An earlier version of this comment justified that omission by calling the
1454
+ // change "owner-approved but unshipped", citing an unpublished internal
1455
+ // study. Review was right to strike it: there is no reachable warrant for
1456
+ // the approval — no committed decision record, and the study is on no git
1457
+ // ref — so the claim could not be checked by anyone reading this file. An
1458
+ // uncheckable appeal to the owner's approval is the same defect as an
1459
+ // uncheckable citation to a study. The omission needs no warrant anyway:
1460
+ // h2a is not a native agent today, which is a fact about this binary.
1461
+ .description("The unified sentropic CLI and core: start and return to agent work sessions " +
1462
+ "(local tmux or cluster), coordinate agents over the h2a protocol, and read " +
1463
+ "or record the work — one entry point for the surfaces that used to live in " +
1464
+ "the separate remote and track CLIs. The heavy session runtime loads on " +
1465
+ "demand. h2a runs and coordinates agents; it is not itself an agent.")
1372
1466
  .version("0.0.0");
1467
+ const tmuxCommand = program
1468
+ .command("tmux")
1469
+ .description("Manage h2a's local tmux naming and status surface");
1470
+ tmuxCommand
1471
+ .command("migrate-names")
1472
+ .description("Plan, apply, or roll back legacy remote-* to h2a-* session names")
1473
+ .option("--dry-run", "show the exact rename plan (default)")
1474
+ .option("--apply", "rename collision-free sessions and update the registry")
1475
+ .option("--rollback", "reverse the journalled migration")
1476
+ .action((opts) => {
1477
+ const selected = [opts.dryRun, opts.apply, opts.rollback].filter(Boolean);
1478
+ if (selected.length > 1) {
1479
+ process.stderr.write("[h2a] choose exactly one of --dry-run, --apply, or --rollback\n");
1480
+ process.exitCode = 2;
1481
+ return;
1482
+ }
1483
+ const mode = opts.apply
1484
+ ? "apply"
1485
+ : opts.rollback
1486
+ ? "rollback"
1487
+ : "dry-run";
1488
+ const result = migrateTmuxNames(mode);
1489
+ for (const entry of result.entries) {
1490
+ process.stdout.write(`${entry.tmuxSessionId.padEnd(5)} ${entry.state.padEnd(11)} ${entry.oldName} -> ${entry.newName}` +
1491
+ `${entry.registryEntriesUpdated ? ` registry:${entry.registryEntriesUpdated}` : ""}\n`);
1492
+ }
1493
+ for (const collision of result.collisions) {
1494
+ process.stderr.write(`[h2a] collision: ${collision.oldName} cannot become ${collision.newName}\n`);
1495
+ }
1496
+ for (const warning of result.warnings) {
1497
+ process.stderr.write(`[h2a] ${warning}\n`);
1498
+ }
1499
+ process.stderr.write(`[h2a] tmux name migration ${mode}: ${result.changed} session(s) changed\n`);
1500
+ if (result.collisions.length > 0 || result.warnings.length > 0) {
1501
+ process.exitCode = 2;
1502
+ }
1503
+ });
1504
+ const tmuxStatusCommand = tmuxCommand
1505
+ .command("status")
1506
+ .description("Install or restore the h2a tmux status surface");
1507
+ tmuxStatusCommand
1508
+ .command("install [session]")
1509
+ .option("--all", "install on every currently managed h2a/legacy session")
1510
+ .action((session, opts) => {
1511
+ const targets = opts.all
1512
+ ? listLocalSessions().map((item) => item.name)
1513
+ : session
1514
+ ? [session]
1515
+ : [];
1516
+ if (targets.length === 0) {
1517
+ process.stderr.write("[h2a] tmux status install requires an exact session or --all\n");
1518
+ process.exitCode = 2;
1519
+ return;
1520
+ }
1521
+ let installed = 0;
1522
+ for (const target of targets) {
1523
+ if (installH2aStatusSurface(target))
1524
+ installed += 1;
1525
+ }
1526
+ process.stderr.write(`[h2a] installed status surface on ${installed} session(s)\n`);
1527
+ if (installed !== targets.length)
1528
+ process.exitCode = 2;
1529
+ });
1530
+ tmuxStatusCommand
1531
+ .command("uninstall [session]")
1532
+ .option("--all", "restore captured options on every managed session")
1533
+ .action((session, opts) => {
1534
+ const targets = opts.all
1535
+ ? listLocalSessions().map((item) => item.name)
1536
+ : session
1537
+ ? [session]
1538
+ : [];
1539
+ if (targets.length === 0) {
1540
+ process.stderr.write("[h2a] tmux status uninstall requires an exact session or --all\n");
1541
+ process.exitCode = 2;
1542
+ return;
1543
+ }
1544
+ let restored = 0;
1545
+ for (const target of targets) {
1546
+ if (uninstallH2aStatusSurface(target))
1547
+ restored += 1;
1548
+ }
1549
+ process.stderr.write(`[h2a] restored prior status options on ${restored} session(s)\n`);
1550
+ if (restored !== targets.length)
1551
+ process.exitCode = 2;
1552
+ });
1373
1553
  for (const [profileName, alias] of [
1374
1554
  ["codex", undefined],
1375
1555
  ["claude", "claude-code"],
@@ -3106,7 +3286,13 @@ export async function main(argv) {
3106
3286
  process.exitCode = 1;
3107
3287
  return;
3108
3288
  }
3109
- const gateway = await injectLlmMeshGatewayEnv(gatewayMode, true, entry.convId ?? localSessionName(resumeSlug));
3289
+ if (gatewayMode === "gateway" && !profileUsesLlmMeshGateway(profile)) {
3290
+ process.stderr.write(`[h2a] --llm-gateway/--gw is unsupported for ${profile}; this gateway is Anthropic-compatible and only Claude profiles consume it.\n`);
3291
+ process.exitCode = 2;
3292
+ return;
3293
+ }
3294
+ const effectiveGatewayMode = gatewayModeForProfile(profile, gatewayMode);
3295
+ let gateway;
3110
3296
  const useBare = shouldUseClaudeBare(profile);
3111
3297
  const command = localCliCommand(profile);
3112
3298
  const args = localResumeArgs(profile, entry.convId, {
@@ -3135,6 +3321,13 @@ export async function main(argv) {
3135
3321
  process.stderr.write(`[h2a] local session ${displaySlug} already exists and does not look idle; no new ${profile} was started.\n`);
3136
3322
  if (opts.attach || explicitProfile) {
3137
3323
  if (explicitProfile && currentTmuxSessionIs(local.name)) {
3324
+ gateway = await injectLlmMeshGatewayEnv(effectiveGatewayMode, true, local.name);
3325
+ persistLaunchContext(local.name, buildLaunchContext({
3326
+ profile,
3327
+ cwd: entry.cwd,
3328
+ label: displaySlug,
3329
+ ...(entry.convId ? { resumeId: entry.convId } : {}),
3330
+ }));
3138
3331
  process.stderr.write(`[h2a] already inside ${displaySlug}; running ${profile} resume in this pane\n`);
3139
3332
  process.exitCode = runLocalCliForeground(command, args);
3140
3333
  return;
@@ -3214,12 +3407,14 @@ export async function main(argv) {
3214
3407
  }
3215
3408
  }
3216
3409
  }
3217
- const { name } = startLocalSession(profile, command, entry.cwd, args, resumeSlug);
3410
+ gateway = await injectLlmMeshGatewayEnv(effectiveGatewayMode, true, localSessionName(resumeSlug));
3411
+ const { name } = startLocalSession(profile, command, entry.cwd, args, resumeSlug, undefined, { sessionClass: "human" });
3218
3412
  enrollFromRun({
3219
3413
  profile,
3220
3414
  slug: resumeSlug,
3221
3415
  tmuxSession: name,
3222
3416
  cwd: entry.cwd,
3417
+ sessionClass: "human",
3223
3418
  ...(entry.convId ? { convId: entry.convId } : {}),
3224
3419
  ...(gatewayMode !== "auto" ? { gatewayMode } : {}),
3225
3420
  });
@@ -3398,15 +3593,17 @@ export async function main(argv) {
3398
3593
  process.exitCode = 1;
3399
3594
  return;
3400
3595
  }
3401
- if (structuredLaunch &&
3402
- profile === "codex" &&
3403
- gatewayMode === "gateway") {
3404
- process.stderr.write("[h2a] --gw is unsupported for structured codex launches (llm-mesh is Anthropic-compatible)\n");
3596
+ if (gatewayMode === "gateway" && !profileUsesLlmMeshGateway(profile)) {
3597
+ process.stderr.write(`[h2a] --llm-gateway/--gw is unsupported for ${profile}; this gateway is Anthropic-compatible and only Claude profiles consume it.\n`);
3405
3598
  process.exitCode = 2;
3406
3599
  return;
3407
3600
  }
3408
- const launchGatewayMode = structuredLaunch && profile === "codex" ? "direct" : gatewayMode;
3601
+ const launchGatewayMode = gatewayModeForProfile(profile, gatewayMode);
3409
3602
  const command = localCliCommand(profile);
3603
+ // A detached/background or run-once launch is a worker, even when its
3604
+ // agent later emits a Claude SessionStart hook. Stamp this through tmux
3605
+ // so that hook cannot reclassify it as a human session.
3606
+ const sessionClass = opts.background || opts.headless ? "background" : "human";
3410
3607
  let activeGateway;
3411
3608
  const h2a = getH2aConfig();
3412
3609
  const h2aSidecar = opts.h2a ?? h2a.enabled;
@@ -3422,13 +3619,13 @@ export async function main(argv) {
3422
3619
  }
3423
3620
  const started = [];
3424
3621
  for (const label of labels) {
3425
- const clientSessionId = opts.resume ?? localSessionName(slugify(label ?? cwd));
3622
+ const clientSessionId = localSessionName(slugify(label ?? cwd));
3426
3623
  try {
3427
3624
  if (structuredLaunch) {
3428
3625
  activeGateway = await prepareStructuredGateway(launchGatewayMode, (selectedMode) => injectLlmMeshGatewayEnv(selectedMode, selectedMode !== "gateway", clientSessionId));
3429
3626
  }
3430
3627
  else {
3431
- activeGateway = await injectLlmMeshGatewayEnv(gatewayMode, true, clientSessionId);
3628
+ activeGateway = await injectLlmMeshGatewayEnv(launchGatewayMode, true, clientSessionId);
3432
3629
  }
3433
3630
  }
3434
3631
  catch (error) {
@@ -3481,7 +3678,7 @@ export async function main(argv) {
3481
3678
  mkdirSync(runDir, { recursive: true });
3482
3679
  outputLog = join(runDir, "output.log");
3483
3680
  resultJson = join(runDir, "result.json");
3484
- ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, initialPrompt, structuredLaunch));
3681
+ ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, initialPrompt, structuredLaunch, sessionClass));
3485
3682
  }
3486
3683
  else {
3487
3684
  ({ name, slug, agentPane } = startLocalSession(profile, command, cwd, args, label, getTmuxProfileConfig().profile, {
@@ -3493,6 +3690,7 @@ export async function main(argv) {
3493
3690
  ? { terminateOnAgentExit: true }
3494
3691
  : {}),
3495
3692
  ...(structuredLaunch ? { refuseExisting: true } : {}),
3693
+ sessionClass,
3496
3694
  }));
3497
3695
  }
3498
3696
  if (structuredLaunch && !agentPane) {
@@ -3553,10 +3751,11 @@ export async function main(argv) {
3553
3751
  profile,
3554
3752
  slug,
3555
3753
  tmuxSession: name,
3754
+ ...(pid !== undefined ? { pid } : {}),
3556
3755
  cwd,
3756
+ sessionClass,
3557
3757
  ...(opts.resume !== undefined ? { convId: opts.resume } : {}),
3558
3758
  ...(gatewayMode !== "auto" ? { gatewayMode } : {}),
3559
- ...(opts.background ? { sessionClass: "background" } : {}),
3560
3759
  });
3561
3760
  started.push({
3562
3761
  name,
@@ -3653,6 +3852,12 @@ export async function main(argv) {
3653
3852
  const jobId = opts.name ?? `${jobType}-${Math.random().toString(36).slice(2, 8)}`;
3654
3853
  // Parent h2a instance to notify on done + answer decisions (best-effort).
3655
3854
  const callbackTo = opts.onDone ?? opts.parent;
3855
+ // A CLI delegation is attributable only when the caller explicitly
3856
+ // names its callback owner AND tmux can identify the exact delegating
3857
+ // session. Do not infer either value from a job name or cwd.
3858
+ const delegatorTmuxSession = callbackTo
3859
+ ? currentTmuxSessionName()
3860
+ : undefined;
3656
3861
  try {
3657
3862
  assertSafeName(jobId);
3658
3863
  }
@@ -3716,6 +3921,7 @@ export async function main(argv) {
3716
3921
  kind: isRemote ? "remote" : "local-tmux",
3717
3922
  cwd: isRemote ? `job-${jobId}` : process.cwd(),
3718
3923
  source: isRemote ? "remote" : "run",
3924
+ sessionClass: "background",
3719
3925
  label: jobId,
3720
3926
  role: "job",
3721
3927
  jobState: "pending",
@@ -3726,6 +3932,13 @@ export async function main(argv) {
3726
3932
  ...(remoteTarget !== undefined ? { remoteTarget } : {}),
3727
3933
  ...(explicitCwd !== undefined ? { explicitCwd } : {}),
3728
3934
  ...(callbackTo !== undefined ? { callbackTo } : {}),
3935
+ ...(callbackTo !== undefined && delegatorTmuxSession !== undefined
3936
+ ? {
3937
+ delegationOrigin: "cli:h2a-delegate",
3938
+ delegatorInstance: callbackTo,
3939
+ delegatorTmuxSession,
3940
+ }
3941
+ : {}),
3729
3942
  ...(opts.track !== undefined ? { trackWp: opts.track } : {}),
3730
3943
  ...(opts.model !== undefined ? { model: opts.model } : {}),
3731
3944
  ...(opts.effort !== undefined ? { effort: opts.effort } : {}),
@@ -3745,6 +3958,7 @@ export async function main(argv) {
3745
3958
  kind: (isRemote ? "remote" : "local-tmux"),
3746
3959
  cwd: isRemote ? `job-${jobId}` : process.cwd(),
3747
3960
  source: (isRemote ? "remote" : "run"),
3961
+ sessionClass: "background",
3748
3962
  label: jobId,
3749
3963
  role: "job",
3750
3964
  task,
@@ -3754,6 +3968,13 @@ export async function main(argv) {
3754
3968
  ...(remoteTarget !== undefined ? { remoteTarget } : {}),
3755
3969
  ...(explicitCwd !== undefined ? { explicitCwd } : {}),
3756
3970
  ...(callbackTo !== undefined ? { callbackTo } : {}),
3971
+ ...(callbackTo !== undefined && delegatorTmuxSession !== undefined
3972
+ ? {
3973
+ delegationOrigin: "cli:h2a-delegate",
3974
+ delegatorInstance: callbackTo,
3975
+ delegatorTmuxSession,
3976
+ }
3977
+ : {}),
3757
3978
  ...(opts.track !== undefined ? { trackWp: opts.track } : {}),
3758
3979
  ...(opts.model !== undefined ? { model: opts.model } : {}),
3759
3980
  ...(opts.effort !== undefined ? { effort: opts.effort } : {}),
@@ -3797,6 +4018,13 @@ export async function main(argv) {
3797
4018
  ...(remoteTarget !== undefined ? { remoteTarget } : {}),
3798
4019
  ...(explicitCwd !== undefined ? { explicitCwd } : {}),
3799
4020
  ...(callbackTo !== undefined ? { callbackTo } : {}),
4021
+ ...(callbackTo !== undefined && delegatorTmuxSession !== undefined
4022
+ ? {
4023
+ delegationOrigin: "cli:h2a-delegate",
4024
+ delegatorInstance: callbackTo,
4025
+ delegatorTmuxSession,
4026
+ }
4027
+ : {}),
3800
4028
  ...(opts.track !== undefined ? { trackWp: opts.track } : {}),
3801
4029
  ...(opts.model !== undefined ? { model: opts.model } : {}),
3802
4030
  ...(opts.effort !== undefined ? { effort: opts.effort } : {}),
@@ -4591,6 +4819,7 @@ export async function main(argv) {
4591
4819
  enrolledAt: new Date().toISOString(),
4592
4820
  lastSeenAt: new Date().toISOString(),
4593
4821
  source: "run",
4822
+ sessionClass: "background",
4594
4823
  label: slug,
4595
4824
  role: "job",
4596
4825
  jobState: "running",
@@ -4605,6 +4834,7 @@ export async function main(argv) {
4605
4834
  kind: "local-tmux",
4606
4835
  cwd: process.cwd(),
4607
4836
  source: "run",
4837
+ sessionClass: "background",
4608
4838
  label: slug,
4609
4839
  role: "job",
4610
4840
  jobState: "pending",
@@ -6271,6 +6501,57 @@ export async function main(argv) {
6271
6501
  ? `[h2a] lineage ${id} resumed\n`
6272
6502
  : `[h2a] lineage ${id} was not suspended (no-op)\n`);
6273
6503
  });
6504
+ // Help layout only — assign every top-level command an intention group and
6505
+ // render the groups in the declared order instead of registration order.
6506
+ // Presentational for every PRE-EXISTING RECOGNIZED COMMAND: `helpGroup()` and
6507
+ // the `groupItems` override touch nothing but the heading a command is listed
6508
+ // under, so dispatch, argv parsing, exit codes and `--json` payloads are
6509
+ // unchanged for all 46 of them. The top-level help layout and the root
6510
+ // description DO change here — that is the point of the change, not an
6511
+ // exception to it. Stating the narrow property rather than "zero behaviour
6512
+ // change", which was wider than the evidence.
6513
+ //
6514
+ // ┌── TWO ORDER-DEPENDENCIES, both recorded because neither is guarded.
6515
+ // │ Keep both calls after the last `program.command(...)` registration.
6516
+ // │
6517
+ // │ A COMMENT IS NOT A SAFETY MECHANISM. Neither (a) nor (b) below is
6518
+ // │ enforced by any test. If you move these calls, the suite will not tell
6519
+ // │ you — it will stay green, because both dependencies are currently inert
6520
+ // │ for the reasons given. They are written down so the next person can see
6521
+ // │ the latch, not because the latch is held shut.
6522
+ // │
6523
+ // │ (a) `configureHelp` writes `program._helpConfiguration`, which Commander
6524
+ // │ copies to each subcommand in `copyInheritedSettings` — invoked from
6525
+ // │ `.command()` AT REGISTRATION TIME (commander/lib/command.js:104). So
6526
+ // │ placing this after all registration means no subcommand inherits
6527
+ // │ `groupItems`.
6528
+ // │
6529
+ // │ MEASURED, not assumed: hoisting this call to before registration was
6530
+ // │ tried, and `--help`, `run --help`, `account --help`, `jobs --help` and
6531
+ // │ `relay --help` all came back BYTE-IDENTICAL. The inheritance is real
6532
+ // │ but INERT, because `groupRuntimeHelpItems` re-emits any heading it does
6533
+ // │ not own (`Commands:`, `Options:`) unchanged — and a subcommand's own
6534
+ // │ children never carry one of our headings. So placement is currently a
6535
+ // │ preference, NOT a correctness requirement, and no test claims
6536
+ // │ otherwise. What IS pinned is the pass-through property that makes it
6537
+ // │ inert: "the group override passes through headings it does not own"
6538
+ // │ in packages/h2a/test/cli-command-map.test.js. It would stop being inert
6539
+ // │ if a subcommand's child were ever given one of our group headings.
6540
+ // │
6541
+ // │ (b) `visibleCommands()` forces Commander to create its implicit `help`
6542
+ // │ command NOW (`_addImplicitHelpCommand`), so that it gets a group too —
6543
+ // │ otherwise it is the lone survivor of the default `Commands:` heading.
6544
+ // │ Side effect: a command registered AFTER this point would inherit the
6545
+ // │ parent's already-created help-command instance
6546
+ // │ (commander/lib/command.js:103) instead of getting its own. An
6547
+ // │ adversarial A/B confirmed that inheritance does happen. It is inert
6548
+ // │ today only because nothing is registered after this line, it is
6549
+ // │ harmless in the output even then, and NOTHING GUARDS IT — a property
6550
+ // │ of statement order, not an invariant. Recorded, not relied upon.
6551
+ // └──
6552
+ applyRuntimeHelpGroups(program.createHelp().visibleCommands(program));
6553
+ program.configureHelp({ groupItems: groupRuntimeHelpItems });
6554
+ program.addHelpText("after", "\nGrouped map of every h2a command (core + runtime): h2a explain\n");
6274
6555
  await program.parseAsync([...argv]);
6275
6556
  const code = process.exitCode;
6276
6557
  return typeof code === "number" ? code : 0;
@@ -6311,6 +6592,12 @@ export { main as dispatch };
6311
6592
  export function projectAgentsForH2a() {
6312
6593
  return projectRemoteAgents({ jobs: listJobs(), localRows: listLocalForLs() });
6313
6594
  }
6595
+ export { projectStatusForH2a };
6596
+ /** Open the detailed watcher only for an exact, currently managed session. */
6597
+ export function openStatusWindowForH2a(session) {
6598
+ const exact = listLocalSessions().find((item) => item.name === session);
6599
+ return exact ? openH2aStatusWindow(exact.name, exact.path) : false;
6600
+ }
6314
6601
  /**
6315
6602
  * h2a-facing surface: read-only tmux pane snapshot for the canevas ③ session
6316
6603
  * view, consumed by `@sentropic/h2a`'s canevas adapter via lazy `import()`.