@sentropic/h2a-runtime 0.98.6 → 0.98.7-rc.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.
Files changed (130) hide show
  1. package/dist/agent-launch-args.d.ts.map +1 -1
  2. package/dist/agent-launch-args.js +5 -5
  3. package/dist/agent-launch-args.js.map +1 -1
  4. package/dist/agy-claude.d.ts +2 -0
  5. package/dist/agy-claude.d.ts.map +1 -0
  6. package/dist/agy-claude.js +3 -0
  7. package/dist/agy-claude.js.map +1 -0
  8. package/dist/agy-headless.d.ts +11 -0
  9. package/dist/agy-headless.d.ts.map +1 -0
  10. package/dist/agy-headless.js +104 -0
  11. package/dist/agy-headless.js.map +1 -0
  12. package/dist/attach.d.ts +2 -0
  13. package/dist/attach.d.ts.map +1 -1
  14. package/dist/attach.js +73 -6
  15. package/dist/attach.js.map +1 -1
  16. package/dist/auth-login.d.ts +24 -1
  17. package/dist/auth-login.d.ts.map +1 -1
  18. package/dist/auth-login.js +132 -4
  19. package/dist/auth-login.js.map +1 -1
  20. package/dist/central-mcp.d.ts +2 -0
  21. package/dist/central-mcp.d.ts.map +1 -1
  22. package/dist/central-mcp.js +90 -3
  23. package/dist/central-mcp.js.map +1 -1
  24. package/dist/claude-code-cli-isolation.d.ts +53 -0
  25. package/dist/claude-code-cli-isolation.d.ts.map +1 -0
  26. package/dist/claude-code-cli-isolation.js +277 -0
  27. package/dist/claude-code-cli-isolation.js.map +1 -0
  28. package/dist/claude-code-cli-lifecycle.d.ts +80 -0
  29. package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
  30. package/dist/claude-code-cli-lifecycle.js +402 -0
  31. package/dist/claude-code-cli-lifecycle.js.map +1 -0
  32. package/dist/claude-code-cli-runner.d.ts +89 -0
  33. package/dist/claude-code-cli-runner.d.ts.map +1 -0
  34. package/dist/claude-code-cli-runner.js +659 -0
  35. package/dist/claude-code-cli-runner.js.map +1 -0
  36. package/dist/claude-seat-input.d.ts +47 -0
  37. package/dist/claude-seat-input.d.ts.map +1 -0
  38. package/dist/claude-seat-input.js +272 -0
  39. package/dist/claude-seat-input.js.map +1 -0
  40. package/dist/claude-seat-mesh-contract.d.ts +88 -0
  41. package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
  42. package/dist/claude-seat-mesh-contract.js +13 -0
  43. package/dist/claude-seat-mesh-contract.js.map +1 -0
  44. package/dist/cli-help-groups.js +1 -1
  45. package/dist/cli-help-groups.js.map +1 -1
  46. package/dist/crash-log.d.ts +112 -0
  47. package/dist/crash-log.d.ts.map +1 -0
  48. package/dist/crash-log.js +560 -0
  49. package/dist/crash-log.js.map +1 -0
  50. package/dist/delegate.d.ts +10 -0
  51. package/dist/delegate.d.ts.map +1 -1
  52. package/dist/delegate.js +29 -0
  53. package/dist/delegate.js.map +1 -1
  54. package/dist/gateway-host/claude-seat-client.d.ts +32 -0
  55. package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
  56. package/dist/gateway-host/claude-seat-client.js +38 -0
  57. package/dist/gateway-host/claude-seat-client.js.map +1 -0
  58. package/dist/gateway-host/host.d.ts +6 -0
  59. package/dist/gateway-host/host.d.ts.map +1 -1
  60. package/dist/gateway-host/host.js +24 -3
  61. package/dist/gateway-host/host.js.map +1 -1
  62. package/dist/gateway-host/live-declarations.d.ts +58 -0
  63. package/dist/gateway-host/live-declarations.d.ts.map +1 -0
  64. package/dist/gateway-host/live-declarations.js +429 -0
  65. package/dist/gateway-host/live-declarations.js.map +1 -0
  66. package/dist/gateway-host/live-github.d.ts +11 -0
  67. package/dist/gateway-host/live-github.d.ts.map +1 -0
  68. package/dist/gateway-host/live-github.js +374 -0
  69. package/dist/gateway-host/live-github.js.map +1 -0
  70. package/dist/gateway-host/live-object-store.d.ts +35 -0
  71. package/dist/gateway-host/live-object-store.d.ts.map +1 -0
  72. package/dist/gateway-host/live-object-store.js +447 -0
  73. package/dist/gateway-host/live-object-store.js.map +1 -0
  74. package/dist/gateway-host/live-registration.d.ts +162 -0
  75. package/dist/gateway-host/live-registration.d.ts.map +1 -0
  76. package/dist/gateway-host/live-registration.js +455 -0
  77. package/dist/gateway-host/live-registration.js.map +1 -0
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +290 -23
  80. package/dist/index.js.map +1 -1
  81. package/dist/llm-mesh-accounts.d.ts +70 -4
  82. package/dist/llm-mesh-accounts.d.ts.map +1 -1
  83. package/dist/llm-mesh-accounts.js +296 -9
  84. package/dist/llm-mesh-accounts.js.map +1 -1
  85. package/dist/native-host.d.ts +1 -1
  86. package/dist/native-host.d.ts.map +1 -1
  87. package/dist/native-host.js +30 -17
  88. package/dist/native-host.js.map +1 -1
  89. package/dist/native-terminal/host.d.ts +6 -1
  90. package/dist/native-terminal/host.d.ts.map +1 -1
  91. package/dist/native-terminal/host.js +6 -25
  92. package/dist/native-terminal/host.js.map +1 -1
  93. package/dist/native-terminal/journal.d.ts +5 -0
  94. package/dist/native-terminal/journal.d.ts.map +1 -1
  95. package/dist/native-terminal/journal.js +28 -16
  96. package/dist/native-terminal/journal.js.map +1 -1
  97. package/dist/native-terminal/op.d.ts +13 -0
  98. package/dist/native-terminal/op.d.ts.map +1 -1
  99. package/dist/native-terminal/op.js +64 -5
  100. package/dist/native-terminal/op.js.map +1 -1
  101. package/dist/proc-identity.d.ts +31 -0
  102. package/dist/proc-identity.d.ts.map +1 -1
  103. package/dist/proc-identity.js +54 -0
  104. package/dist/proc-identity.js.map +1 -1
  105. package/dist/prompt-delivery.d.ts +3 -0
  106. package/dist/prompt-delivery.d.ts.map +1 -1
  107. package/dist/prompt-delivery.js +8 -3
  108. package/dist/prompt-delivery.js.map +1 -1
  109. package/dist/run.d.ts.map +1 -1
  110. package/dist/run.js +79 -16
  111. package/dist/run.js.map +1 -1
  112. package/dist/terminal-reset.d.ts +104 -0
  113. package/dist/terminal-reset.d.ts.map +1 -0
  114. package/dist/terminal-reset.js +229 -0
  115. package/dist/terminal-reset.js.map +1 -0
  116. package/dist/tmux.d.ts +26 -4
  117. package/dist/tmux.d.ts.map +1 -1
  118. package/dist/tmux.js +60 -25
  119. package/dist/tmux.js.map +1 -1
  120. package/dist/vendor/engram-live/contracts.d.ts +825 -0
  121. package/dist/vendor/engram-live/contracts.js +1734 -0
  122. package/dist/vendor/engram-live/integration.d.ts +113 -0
  123. package/dist/vendor/engram-live/integration.js +254 -0
  124. package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
  125. package/dist/vendor/engram-live/pr-sync.js +411 -0
  126. package/dist/vendor/engram-live/registration.d.ts +138 -0
  127. package/dist/vendor/engram-live/registration.js +358 -0
  128. package/dist/vendor/engram-live/retention.d.ts +56 -0
  129. package/dist/vendor/engram-live/retention.js +179 -0
  130. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { spawnSync } from "node:child_process";
4
- import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { Command, Help } from "commander";
9
+ import { agyClaudeQuotaError, assertAgyClaudeBuildAllowed, detectAgyClaudeQuotaExhaustion, isAgyClaudeModel, normalizeAgyEffort, readAgyClaudeQuotaStatus, resolveAgyModel, writeAgyClaudeQuotaStatus } from "./agy-claude.js";
9
10
  import { applyRuntimeHelpGroups, groupRuntimeHelpItems, } from "./cli-help-groups.js";
10
11
  import { attach, createRemoteSession, getRemoteSession, listRemoteSessions, refreshRemoteSession, renameRemoteSession, sessionTerminalHealth, stopRemoteSession, } from "./attach.js";
11
12
  import { authHeaders, clearDefaultRemote, getDefaultRemote, getDefaultTarget, getDefaultTools, getH2aConfig, getJobMaxAgeHours, getMaxConcurrent, getLlmMeshRuntimeConfig, getTmuxProfileConfig, getTunnel, resolveConfigPath, setDefaultRemote, setDefaultTarget, migrateConfigHomeIfNeeded, setDefaultTools, setLlmMeshRuntimeConfig, setTmuxProfileConfig, setToken, setTunnel, } from "./config.js";
12
- import { prepareCentralMcpForLaunch, prepareCentralMcpForRestore, } from "./central-mcp.js";
13
+ import { prepareCentralMcpForLaunch, prepareCentralMcpForRestore, resolveCentralMcpLogPath, } from "./central-mcp.js";
14
+ import { CRASH_LOG_COMPONENTS, formatCrashLogHumanRow, installCliWitnessOnce, installCrashHandlers, readFileTailLines, redactCrashText, readRecentCrashLogs, resolveCrashLogDir, } from "./crash-log.js";
15
+ import { readHostJournal, resolveHostJournalPath } from "./native-terminal/journal.js";
13
16
  import { ensureConnected, stopTunnel } from "./tunnel.js";
14
17
  import { detectToolAuth, KNOWN_TOOLS, partitionTools } from "./auth-tools.js";
15
18
  import { transmittedSecrets, secretsSummary } from "./secrets.js";
@@ -1434,7 +1437,7 @@ async function injectLlmMeshGatewayEnv(mode = "auto", allowDirectFallback = true
1434
1437
  delete process.env.ANTHROPIC_BASE_URL;
1435
1438
  delete process.env.ANTHROPIC_AUTH_TOKEN;
1436
1439
  if (mode === "gateway") {
1437
- throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse` first");
1440
+ throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse|claude-code` first");
1438
1441
  }
1439
1442
  if (allowDirectFallback) {
1440
1443
  process.stderr.write("[h2a] llm-mesh: gateway unavailable; continuing with native authentication.\n");
@@ -1516,7 +1519,7 @@ function renderNativeRestartResult(result, json) {
1516
1519
  export async function prepareStructuredGateway(mode, inject = (selectedMode) => injectLlmMeshGatewayEnv(selectedMode, selectedMode !== "gateway")) {
1517
1520
  const gateway = await inject(mode);
1518
1521
  if (mode === "gateway" && !gateway) {
1519
- throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse` first");
1522
+ throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse|claude-code` first");
1520
1523
  }
1521
1524
  return gateway;
1522
1525
  }
@@ -1718,7 +1721,45 @@ function setAndReportDefaultRemote(url) {
1718
1721
  ensureManagedTmuxProfile(getTmuxProfileConfig().profile);
1719
1722
  process.stderr.write(`[h2a] default remote set to ${configured}\n`);
1720
1723
  }
1724
+ // Root commander error boundary (DEC-119): commander never prints its own
1725
+ // error text. `outputError` is a no-op so the commander-built message (which
1726
+ // echoes argv, possibly multi-line) is dropped; `exitOverride` writes a
1727
+ // static message chosen ONLY from `err.code` — never `err.message`, never
1728
+ // user-typed text. Registered on the ROOT program before any `.command()`
1729
+ // so the whole tree inherits it via `copyInheritedSettings`.
1730
+ function staticCommanderErrorForCode(code) {
1731
+ switch (code) {
1732
+ case "commander.unknownOption":
1733
+ return "error: unknown option\n";
1734
+ case "commander.unknownCommand":
1735
+ return "error: unknown command\n";
1736
+ case "commander.excessArguments":
1737
+ return "error: too many arguments\n";
1738
+ case "commander.missingArgument":
1739
+ return "error: missing required argument\n";
1740
+ case "commander.optionMissingArgument":
1741
+ return "error: option argument missing\n";
1742
+ case "commander.missingMandatoryOptionValue":
1743
+ return "error: required option not specified\n";
1744
+ case "commander.conflictingOption":
1745
+ return "error: conflicting option\n";
1746
+ case "commander.invalidArgument":
1747
+ return "error: invalid argument\n";
1748
+ default:
1749
+ return "error: command error\n";
1750
+ }
1751
+ }
1752
+ function isCommanderNonErrorCode(code) {
1753
+ return (code === "commander.help" ||
1754
+ code === "commander.helpDisplayed" ||
1755
+ code === "commander.version" ||
1756
+ code === "commander.executeSubCommandAsync");
1757
+ }
1721
1758
  export async function main(argv) {
1759
+ // Passive fatal-path witness on the real entry path (see
1760
+ // installCliWitnessOnce): observes via uncaughtExceptionMonitor, so the
1761
+ // default crash/exit behavior is unchanged.
1762
+ installCliWitnessOnce();
1722
1763
  // ②b — auto-migrate the config home remote-cli/ -> h2a/ on first run (backup +
1723
1764
  // compat symlink). Best-effort: never let it break a command. Runs once
1724
1765
  // (idempotent) before any config/registry read.
@@ -1767,6 +1808,25 @@ export async function main(argv) {
1767
1808
  "the separate remote and track CLIs. The heavy session runtime loads on " +
1768
1809
  "demand. h2a runs and coordinates agents; it is not itself an agent.")
1769
1810
  .version("0.0.0");
1811
+ // Root error boundary (DEC-119): commander never prints its own error
1812
+ // text. `outputError` is a no-op (commander message dropped);
1813
+ // `exitOverride` writes the static per-code message. Registered here —
1814
+ // before any `.command()` — so the whole tree inherits it through
1815
+ // `copyInheritedSettings`.
1816
+ program.configureOutput({
1817
+ writeErr: (str) => {
1818
+ process.stderr.write(str);
1819
+ },
1820
+ outputError: () => { },
1821
+ });
1822
+ program.showHelpAfterError(false);
1823
+ program.showSuggestionAfterError(false);
1824
+ program.exitOverride((err) => {
1825
+ if (isCommanderNonErrorCode(err.code))
1826
+ throw err;
1827
+ process.stderr.write(staticCommanderErrorForCode(err.code));
1828
+ throw err;
1829
+ });
1770
1830
  const identityCommand = program
1771
1831
  .command("identity")
1772
1832
  .description("Inspect identity bindings or unlock a token-fenced identity lock");
@@ -3991,8 +4051,11 @@ export async function main(argv) {
3991
4051
  .option("--prompt-stdin", "read the initial Claude/Codex/AGY prompt from stdin (keeps it out of process argv)")
3992
4052
  .option("--agent <agent>", "AGY agent override (for example: stp)")
3993
4053
  .option("--model <model>", "Claude/Codex/AGY model override")
3994
- .option("--effort <level>", "reasoning effort: low|medium|high|xhigh (AGY supports low|medium|high)")
3995
- .option("--headless", "run once and exit (Claude -p / Codex exec / AGY --print), recording output under .h2a/runs/<name>")
4054
+ .option("--role <role>", "launch purpose: review|challenge|build; AGY Claude builds require explicit opt-in")
4055
+ .option("--allow-claude-build", "explicitly spend the reserved AGY Claude pool on a build")
4056
+ .option("--output-file <path>", "expected AGY Claude headless artifact; report missing or unchanged output at exit")
4057
+ .option("--effort <level>", "reasoning effort: low|medium|high|xhigh (AGY: low|medium|high, max maps to high)")
4058
+ .option("--headless", "run once and exit (Claude -p / Codex exec / AGY stream-json), recording output under .h2a/runs/<name>")
3996
4059
  .option("--background", "classify this detached session as a background worker excluded from human restore")
3997
4060
  .option("--json", "emit one machine-readable h2a.run.result object")
3998
4061
  .option("--llm-gateway", "launch the CLI through the local llm-mesh gateway")
@@ -4014,13 +4077,30 @@ export async function main(argv) {
4014
4077
  process.exitCode = 2;
4015
4078
  return;
4016
4079
  }
4017
- if (opts.effort !== undefined && !isAgentLaunchEffort(opts.effort)) {
4018
- process.stderr.write(`[h2a] --effort must be low|medium|high|xhigh\n`);
4080
+ try {
4081
+ if (profile === "agy") {
4082
+ const effort = normalizeAgyEffort(opts.effort);
4083
+ if (effort !== undefined)
4084
+ opts.effort = effort;
4085
+ const model = resolveAgyModel(opts.model, effort);
4086
+ if (model !== undefined)
4087
+ opts.model = model;
4088
+ }
4089
+ assertAgyClaudeBuildAllowed({ profile, model: opts.model, role: opts.role, allowClaudeBuild: opts.allowClaudeBuild });
4090
+ if (opts.outputFile && !(profile === "agy" && isAgyClaudeModel(opts.model) && opts.headless)) {
4091
+ throw new Error("--output-file requires an AGY Claude headless run");
4092
+ }
4093
+ }
4094
+ catch (error) {
4095
+ process.stderr.write(`[h2a] ${error.message}\n`);
4096
+ if (opts.json)
4097
+ process.stdout.write(`${JSON.stringify({ kind: "h2a.run.failure", version: 1, state: "not-started",
4098
+ code: error.code ?? "invalid-agy-launch", error: error.message, retrySafe: false })}\n`);
4019
4099
  process.exitCode = 2;
4020
4100
  return;
4021
4101
  }
4022
- if (profile === "agy" && opts.effort === "xhigh") {
4023
- process.stderr.write("[h2a] AGY --effort must be low|medium|high\n");
4102
+ if (opts.effort !== undefined && !isAgentLaunchEffort(opts.effort)) {
4103
+ process.stderr.write(`[h2a] --effort must be low|medium|high|xhigh\n`);
4024
4104
  process.exitCode = 2;
4025
4105
  return;
4026
4106
  }
@@ -4383,6 +4463,10 @@ export async function main(argv) {
4383
4463
  mkdirSync(runDir, { recursive: true });
4384
4464
  outputLog = join(runDir, "output.log");
4385
4465
  resultJson = join(runDir, "result.json");
4466
+ // Name admission above has completed. Retire the previous result
4467
+ // before returning a path or starting the new run-once supervisor.
4468
+ if (profile === "agy" && isAgyClaudeModel(opts.model))
4469
+ rmSync(resultJson, { force: true });
4386
4470
  const headlessPromptInput = structuredLaunch && isAgentLaunchProfile(profile)
4387
4471
  ? buildAgentLaunchStdin({
4388
4472
  profile,
@@ -4396,10 +4480,10 @@ export async function main(argv) {
4396
4480
  ({ name, slug, promptFile } = startNativeHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt, structuredLaunch ? (owned) => {
4397
4481
  launchOwnership = { host: "native", sessions: [owned] };
4398
4482
  launchGuard = startLaunchGuard(runDir, launchOwnership);
4399
- } : undefined));
4483
+ } : undefined, opts.outputFile ? resolve(cwd, opts.outputFile) : undefined));
4400
4484
  }
4401
4485
  else {
4402
- ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt));
4486
+ ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt, opts.outputFile ? resolve(cwd, opts.outputFile) : undefined));
4403
4487
  }
4404
4488
  }
4405
4489
  else if (sessionHost === "native") {
@@ -4532,6 +4616,18 @@ export async function main(argv) {
4532
4616
  now: () => Date.now(),
4533
4617
  }, { profile });
4534
4618
  if (promptDelivery.state !== "working") {
4619
+ const quotaDiagnostic = promptDelivery.state === "submitted-idle" ? "" : `${promptDelivery.reason}\n${promptDelivery.capture ?? ""}`;
4620
+ const quotaVerdict = profile === "agy" && isAgyClaudeModel(opts.model)
4621
+ ? detectAgyClaudeQuotaExhaustion(quotaDiagnostic) : { exhausted: false };
4622
+ const quotaFailure = profile === "agy" && isAgyClaudeModel(opts.model) &&
4623
+ promptDelivery.state === "provider-blocked" && promptDelivery.providerError
4624
+ ? promptDelivery.providerError : quotaVerdict.exhausted
4625
+ ? agyClaudeQuotaError(quotaVerdict.signature, quotaDiagnostic) : undefined;
4626
+ if (quotaFailure)
4627
+ try {
4628
+ writeAgyClaudeQuotaStatus(quotaFailure.quota);
4629
+ }
4630
+ catch { /* Response still carries the observation. */ }
4535
4631
  cleanupHeadlessPromptFile(promptFile);
4536
4632
  // Only claim the session is gone when the kill actually reported
4537
4633
  // success. "nothing is left running" over an unchecked call is the
@@ -4561,15 +4657,47 @@ export async function main(argv) {
4561
4657
  ? `[h2a] last screen:\n${promptDelivery.capture}\n`
4562
4658
  : ""));
4563
4659
  process.exitCode = 1;
4564
- if (opts.json && promptDelivery.state === "provider-blocked") {
4660
+ if (opts.json && (promptDelivery.state === "provider-blocked" || quotaFailure)) {
4565
4661
  process.stdout.write(`${JSON.stringify({ kind: "h2a.run.failure", version: 1,
4566
- state: "provider-blocked", launchId: slug, error: promptDelivery.reason,
4567
- stopped, retrySafe: false, prompt: { delivered: true, waitedMs: promptDelivery.waitedMs },
4662
+ state: "provider-blocked", launchId: slug, error: quotaFailure?.message ?? (promptDelivery.state === "submitted-idle" ? "agent idle after submission" : promptDelivery.reason),
4663
+ stopped, retrySafe: false, prompt: { delivered: promptDelivery.state === "provider-blocked", ...("waitedMs" in promptDelivery ? { waitedMs: promptDelivery.waitedMs } : {}) },
4664
+ ...(quotaFailure ? { code: quotaFailure.code, error: quotaFailure.message, quota: quotaFailure.quota,
4665
+ resetsAt: quotaFailure.resetsAt, retryable: false, model: opts.model } : {}),
4568
4666
  })}\n`);
4569
4667
  }
4570
4668
  return;
4571
4669
  }
4572
4670
  }
4671
+ const reportAgyCompletion = () => {
4672
+ if (!(profile === "agy" && isAgyClaudeModel(opts.model) && opts.headless && resultJson))
4673
+ return false;
4674
+ try {
4675
+ const completed = JSON.parse(readFileSync(resultJson, "utf8"));
4676
+ // The previous result was removed before this supervisor started.
4677
+ // Filesystem and wall-clock timestamps cannot prove generation.
4678
+ if (!["h2a.run.failure", "h2a.run.completion"].includes(completed.kind))
4679
+ return false;
4680
+ launchGuard?.complete();
4681
+ if (opts.json)
4682
+ process.stdout.write(`${JSON.stringify({ ...completed, launchId: slug, result: { path: resultJson }, logs: { path: outputLog } })}\n`);
4683
+ else
4684
+ process.stderr.write(`[h2a] ${completed.error ?? "AGY run completed"}\n`);
4685
+ process.exitCode = completed.state === "done" ? 0 : 1;
4686
+ return true;
4687
+ }
4688
+ catch {
4689
+ return false;
4690
+ } // Still running; the durable result may appear later.
4691
+ };
4692
+ if (profile === "agy" && isAgyClaudeModel(opts.model) && opts.headless && resultJson) {
4693
+ // A fast provider refusal may finish before the host PID probe.
4694
+ // Return that typed completion rather than a misleading PID error.
4695
+ for (let attempt = 0; attempt < 5; attempt++) {
4696
+ if (reportAgyCompletion())
4697
+ return;
4698
+ sleepSync(50);
4699
+ }
4700
+ }
4573
4701
  const pid = sessionHost === "native"
4574
4702
  ? nativeSessionPid(name)
4575
4703
  : agentPane
@@ -4581,6 +4709,9 @@ export async function main(argv) {
4581
4709
  ? paneWorkerPid(agentPane)
4582
4710
  : undefined;
4583
4711
  if (structuredLaunch && pid === undefined) {
4712
+ // The supervisor can publish and exit DURING the PID probe.
4713
+ if (reportAgyCompletion())
4714
+ return;
4584
4715
  cleanupHeadlessPromptFile(promptFile);
4585
4716
  // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4586
4717
  // `name`/`sessionHost` are the session and host THIS command
@@ -4650,6 +4781,8 @@ export async function main(argv) {
4650
4781
  tmuxSession: only.name,
4651
4782
  host: sessionHost === "native" ? "native" : "tmux",
4652
4783
  profile,
4784
+ ...(opts.model ? { model: opts.model, modelSource: "launch-argument", effectiveModel: "unknown" } : {}),
4785
+ ...(profile === "agy" && isAgyClaudeModel(opts.model) ? { quota: readAgyClaudeQuotaStatus() } : {}),
4653
4786
  workspace: cwd,
4654
4787
  mode: opts.headless ? "headless" : "interactive",
4655
4788
  background: opts.background === true,
@@ -7525,11 +7658,26 @@ export async function main(argv) {
7525
7658
  llmMeshAccountCommand
7526
7659
  .command("enroll <provider>")
7527
7660
  .description("Enroll through the sentropic-owned OAuth state machine " +
7528
- "(cloud-code, codex, muse OAuth device flow, or mistral-vibe browser sign-in — alias: vibe). " +
7529
- "--cli imports credentials from the provider's local CLI store instead (muse only)")
7661
+ "(cloud-code, codex, muse OAuth device flow, mistral-vibe browser sign-in — alias: vibe, or claude-code). " +
7662
+ "--from oauth|cli|token selects the source (default oauth): cli imports from the provider's official CLI store (muse, claude-code), " +
7663
+ "token imports a serialized credential from a secret manager via stdin pipe or masked prompt (claude-code only). " +
7664
+ "--cli / --token are the same modes as booleans")
7530
7665
  .option("--config-ref <ref>", "sentropic configuration reference for OAuth")
7531
- .option("--cli", "import credentials from the provider's local CLI store (muse only) instead of the OAuth flow")
7532
- .action(async (provider, opts) => {
7666
+ .option("--from <mode>", "enrollment source: oauth (default), cli (provider official CLI store) or token (secret-manager credential)")
7667
+ .option("--cli", "import credentials from the provider's local CLI store (muse, claude-code) instead of the OAuth flow. Takes no value")
7668
+ .option("--token", "import a serialized credential from a secret manager into llm-mesh (claude-code only): pipe it on stdin, or enter it at the masked prompt. Takes no value; never argv, never logged")
7669
+ .allowExcessArguments(true)
7670
+ .action(async (provider, opts, command) => {
7671
+ // Static excess-argument refusal (m1): commander would otherwise echo
7672
+ // every positional value (including a secret) on stderr before the
7673
+ // action runs. Excess positionals are refused here without echoing
7674
+ // any value. Token material travels on stdin only, never argv.
7675
+ const positionalCount = Array.isArray(command?.args) ? command.args.length : 1;
7676
+ if (positionalCount > 1) {
7677
+ process.stderr.write("[h2a] llm-mesh account: too many arguments (expected provider only; never pass secrets in argv)\n");
7678
+ process.exitCode = 1;
7679
+ return;
7680
+ }
7533
7681
  // "vibe" is the CLI-friendly alias of the mistral-vibe transport.
7534
7682
  const normalized = provider === "vibe" ? "mistral-vibe" : provider;
7535
7683
  if (normalized === "muse-code") {
@@ -7541,19 +7689,48 @@ export async function main(argv) {
7541
7689
  process.exitCode = 1;
7542
7690
  return;
7543
7691
  }
7692
+ if (opts.from !== undefined && opts.from !== "oauth" && opts.from !== "cli" && opts.from !== "token") {
7693
+ process.stderr.write("[h2a] llm-mesh account: --from must be one of oauth, cli, token\n");
7694
+ process.exitCode = 1;
7695
+ return;
7696
+ }
7697
+ if (opts.from !== undefined && (opts.cli || opts.token)) {
7698
+ process.stderr.write("[h2a] llm-mesh account: --from cannot be combined with --cli or --token\n");
7699
+ process.exitCode = 1;
7700
+ return;
7701
+ }
7702
+ if (opts.cli && opts.token) {
7703
+ process.stderr.write("[h2a] llm-mesh account: --cli and --token cannot be combined\n");
7704
+ process.exitCode = 1;
7705
+ return;
7706
+ }
7707
+ if ((opts.cli || opts.from === "cli") && normalized !== "muse" && normalized !== "claude-code") {
7708
+ process.stderr.write("[h2a] llm-mesh account: provider has no CLI-store import; " +
7709
+ "--cli/--from cli is only valid for providers with a local CLI import (muse, claude-code)\n");
7710
+ process.exitCode = 1;
7711
+ return;
7712
+ }
7713
+ if ((opts.token || opts.from === "token") && normalized !== "claude-code") {
7714
+ process.stderr.write(`[h2a] llm-mesh account: --token/--from token is only valid for claude-code\n`);
7715
+ process.exitCode = 1;
7716
+ return;
7717
+ }
7544
7718
  if (normalized !== "cloud-code" &&
7545
7719
  normalized !== "codex" &&
7546
7720
  normalized !== "muse" &&
7547
- normalized !== "mistral-vibe") {
7548
- process.stderr.write(`[h2a] llm-mesh account: unsupported provider "${provider}". ` +
7549
- "Supported: cloud-code, codex, muse (OAuth; --cli imports the CLI store), mistral-vibe (alias: vibe)\n");
7721
+ normalized !== "mistral-vibe" &&
7722
+ normalized !== "claude-code") {
7723
+ process.stderr.write("[h2a] llm-mesh account: unsupported provider. " +
7724
+ "Supported: cloud-code, codex, muse (OAuth; --cli imports the CLI store), mistral-vibe (alias: vibe), claude-code (OAuth; --cli imports the CLI store; --token imports a secret-manager credential)\n");
7550
7725
  process.exitCode = 1;
7551
7726
  return;
7552
7727
  }
7553
7728
  try {
7554
7729
  const account = await enrollViaFacade(normalized, {
7555
7730
  ...(opts.configRef ? { configRef: opts.configRef } : {}),
7731
+ ...(opts.from ? { from: opts.from } : {}),
7556
7732
  ...(opts.cli ? { cliImport: true } : {}),
7733
+ ...(opts.token ? { tokenImport: true } : {}),
7557
7734
  });
7558
7735
  process.stdout.write(`[h2a] llm-mesh account: enrolled ${account.provider}; ` +
7559
7736
  "account inventory remains in Sentropic\n");
@@ -7771,6 +7948,75 @@ export async function main(argv) {
7771
7948
  ? `[h2a] lineage ${id} resumed\n`
7772
7949
  : `[h2a] lineage ${id} was not suspended (no-op)\n`);
7773
7950
  });
7951
+ // ---------------------------------------------------------------------------
7952
+ // logs — last crash-log incidents (native host journal + per-component logs)
7953
+ // ---------------------------------------------------------------------------
7954
+ program
7955
+ .command("logs")
7956
+ .description("Show the last crash-log incidents: native host journal plus cli/attach/mcp-server logs.")
7957
+ .option("--component <name>", "only this component (native-host|cli|attach|mcp-server)")
7958
+ .option("-n, --lines <n>", "number of rows to show", "50")
7959
+ .option("--json", "emit JSON rows instead of human lines")
7960
+ .action((opts) => {
7961
+ const dir = resolveCrashLogDir();
7962
+ const lines = Math.max(1, parseInt(opts.lines ?? "50", 10) || 50);
7963
+ const only = (opts.component ?? "").trim();
7964
+ const wantHost = only === "" || only === "native-host";
7965
+ const wanted = CRASH_LOG_COMPONENTS.filter((c) => only === "" || only === c);
7966
+ if (!wantHost && wanted.length === 0) {
7967
+ process.stderr.write("h2a logs: --component must be native-host|cli|attach|mcp-server\n");
7968
+ process.exitCode = 1;
7969
+ return;
7970
+ }
7971
+ const rows = readRecentCrashLogs(wanted, lines).map((row) => ({
7972
+ timestamp: row.timestamp,
7973
+ component: row.component,
7974
+ version: row.version,
7975
+ pid: row.pid,
7976
+ event: row.event,
7977
+ ...(row.sessionId ? { sessionId: row.sessionId } : {}),
7978
+ ...(row.instanceId ? { instanceId: row.instanceId } : {}),
7979
+ ...(row.reason ? { reason: row.reason } : {}),
7980
+ ...(row.exitCode !== undefined ? { exitCode: row.exitCode } : {}),
7981
+ ...(row.signal ? { signal: row.signal } : {}),
7982
+ ...(row.detail ? { detail: row.detail } : {}),
7983
+ }));
7984
+ const hostRows = wantHost
7985
+ ? readHostJournal(resolveHostJournalPath())
7986
+ .slice(-lines)
7987
+ .map((entry) => ({
7988
+ timestamp: entry.timestamp ?? "unknown",
7989
+ component: "native-host",
7990
+ ...entry,
7991
+ }))
7992
+ : [];
7993
+ const merged = [...hostRows, ...rows]
7994
+ .sort((a, b) => (a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0))
7995
+ .slice(-lines);
7996
+ // The raw server output is verbatim child stderr (unredacted at rest):
7997
+ // read it before the empty check so a raw-only crash is shown, and
7998
+ // redact it on display like every structured row.
7999
+ const rawPath = resolveCentralMcpLogPath();
8000
+ const rawTail = (only === "" || only === "mcp-server") ? readFileTailLines(rawPath, 20) : [];
8001
+ if (merged.length === 0 && rawTail.length === 0) {
8002
+ process.stdout.write(`[h2a] no crash-log incidents in ${dir}\n`);
8003
+ return;
8004
+ }
8005
+ if (opts.json && merged.length > 0) {
8006
+ process.stdout.write(`${merged.map((row) => JSON.stringify(row)).join("\n")}\n`);
8007
+ return;
8008
+ }
8009
+ for (const row of merged) {
8010
+ process.stdout.write(`${formatCrashLogHumanRow(row)}\n`);
8011
+ }
8012
+ if (rawTail.length > 0) {
8013
+ // The server process itself leaves no structured rows (it dies with
8014
+ // its witness); its raw stderr tail is the only "why" for its crash.
8015
+ process.stdout.write(`[h2a] --- mcp-central.log raw tail (${rawPath}) ---\n`);
8016
+ for (const line of rawTail)
8017
+ process.stdout.write(`${redactCrashText(line)}\n`);
8018
+ }
8019
+ });
7774
8020
  // Help layout only — assign every top-level command an intention group and
7775
8021
  // render the groups in the declared order instead of registration order.
7776
8022
  // Presentational for every PRE-EXISTING RECOGNIZED COMMAND: `helpGroup()` and
@@ -7822,7 +8068,25 @@ export async function main(argv) {
7822
8068
  applyRuntimeHelpGroups(program.createHelp().visibleCommands(program));
7823
8069
  program.configureHelp({ groupItems: groupRuntimeHelpItems });
7824
8070
  program.addHelpText("after", "\nGrouped map of every h2a command (core + runtime): h2a explain\n");
7825
- await program.parseAsync([...argv]);
8071
+ try {
8072
+ await program.parseAsync([...argv]);
8073
+ }
8074
+ catch (err) {
8075
+ // Commander errors already wrote their static per-code message in
8076
+ // `exitOverride`; only set the exit code here, never `err.message`.
8077
+ if (err !== null &&
8078
+ typeof err === "object" &&
8079
+ "code" in err &&
8080
+ typeof err.code === "string") {
8081
+ const code = err.code;
8082
+ if (code.startsWith("commander.")) {
8083
+ const exitCode = err.exitCode;
8084
+ process.exitCode = typeof exitCode === "number" ? exitCode : 1;
8085
+ return process.exitCode;
8086
+ }
8087
+ }
8088
+ throw err;
8089
+ }
7826
8090
  const code = process.exitCode;
7827
8091
  return typeof code === "number" ? code : 0;
7828
8092
  }
@@ -7838,6 +8102,9 @@ function isEntryPoint() {
7838
8102
  }
7839
8103
  }
7840
8104
  if (isEntryPoint()) {
8105
+ // Passive witness for fatal paths outside runOp: installed once inside
8106
+ // main() so the lazy core-binary dispatch path is covered too.
8107
+ installCliWitnessOnce();
7841
8108
  main(process.argv).catch((error) => {
7842
8109
  if (error instanceof AuthRefreshError ||
7843
8110
  error instanceof AuthBundleMissingError) {