@sentropic/h2a-runtime 0.85.25 → 0.86.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.
- package/dist/cli-help-groups.d.ts +124 -0
- package/dist/cli-help-groups.d.ts.map +1 -0
- package/dist/cli-help-groups.js +297 -0
- package/dist/cli-help-groups.js.map +1 -0
- package/dist/enroll.d.ts.map +1 -1
- package/dist/enroll.js +40 -2
- package/dist/enroll.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +314 -27
- package/dist/index.js.map +1 -1
- package/dist/llm-gateway-runtime/accounts.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/accounts.js +2 -6
- package/dist/llm-gateway-runtime/accounts.js.map +1 -1
- package/dist/llm-gateway-runtime/index.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/index.js +9 -1
- package/dist/llm-gateway-runtime/index.js.map +1 -1
- package/dist/llm-gateway-runtime/model-catalog.d.ts +10 -3
- package/dist/llm-gateway-runtime/model-catalog.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/model-catalog.js +49 -119
- package/dist/llm-gateway-runtime/model-catalog.js.map +1 -1
- package/dist/llm-gateway-runtime/proxy-anthropic.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/proxy-anthropic.js +117 -24
- package/dist/llm-gateway-runtime/proxy-anthropic.js.map +1 -1
- package/dist/llm-gateway-runtime/proxy-gemini.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/proxy-gemini.js +0 -2
- package/dist/llm-gateway-runtime/proxy-gemini.js.map +1 -1
- package/dist/llm-gateway-runtime/proxy-openai.d.ts +4 -7
- package/dist/llm-gateway-runtime/proxy-openai.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/proxy-openai.js +18 -13
- package/dist/llm-gateway-runtime/proxy-openai.js.map +1 -1
- package/dist/llm-gateway-runtime/session-ledger.d.ts +34 -0
- package/dist/llm-gateway-runtime/session-ledger.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/session-ledger.js +216 -6
- package/dist/llm-gateway-runtime/session-ledger.js.map +1 -1
- package/dist/llm-mesh.d.ts +3 -0
- package/dist/llm-mesh.d.ts.map +1 -1
- package/dist/llm-mesh.js +23 -0
- package/dist/llm-mesh.js.map +1 -1
- package/dist/protocol-local.d.ts +3 -0
- package/dist/protocol-local.d.ts.map +1 -1
- package/dist/protocol-local.js +7 -0
- package/dist/protocol-local.js.map +1 -1
- package/dist/registry.d.ts +53 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +109 -6
- package/dist/registry.js.map +1 -1
- package/dist/restore.d.ts +75 -4
- package/dist/restore.d.ts.map +1 -1
- package/dist/restore.js +268 -24
- package/dist/restore.js.map +1 -1
- package/dist/session-class.d.ts +10 -0
- package/dist/session-class.d.ts.map +1 -0
- package/dist/session-class.js +11 -0
- package/dist/session-class.js.map +1 -0
- package/dist/status-projection.d.ts +97 -0
- package/dist/status-projection.d.ts.map +1 -0
- package/dist/status-projection.js +464 -0
- package/dist/status-projection.js.map +1 -0
- package/dist/tmux-name-migration.d.ts +52 -0
- package/dist/tmux-name-migration.d.ts.map +1 -0
- package/dist/tmux-name-migration.js +408 -0
- package/dist/tmux-name-migration.js.map +1 -0
- package/dist/tmux.d.ts +54 -1
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +232 -16
- package/dist/tmux.js.map +1 -1
- package/package.json +6 -2
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
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
3402
|
-
profile
|
|
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 =
|
|
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 =
|
|
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(
|
|
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()`.
|