@integrity-labs/agt-cli 0.28.947 → 0.28.949

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 (31) hide show
  1. package/dist/bin/agt.js +6 -5
  2. package/dist/bin/agt.js.map +1 -1
  3. package/dist/{chunk-XN5MOKSV.js → chunk-A3T3WOC2.js} +28 -304
  4. package/dist/chunk-A3T3WOC2.js.map +1 -0
  5. package/dist/chunk-DHWNVVX4.js +319 -0
  6. package/dist/chunk-DHWNVVX4.js.map +1 -0
  7. package/dist/{chunk-YZ5HGHPG.js → chunk-JANAAFED.js} +36 -1
  8. package/dist/{chunk-YZ5HGHPG.js.map → chunk-JANAAFED.js.map} +1 -1
  9. package/dist/{chunk-372DZ2RQ.js → chunk-MTRJTGJL.js} +9 -7
  10. package/dist/{chunk-372DZ2RQ.js.map → chunk-MTRJTGJL.js.map} +1 -1
  11. package/dist/{claude-code-updater-4E5T2X3Z.js → claude-code-updater-NAHJ6O6C.js} +37 -3
  12. package/dist/claude-code-updater-NAHJ6O6C.js.map +1 -0
  13. package/dist/{claude-pair-runtime-7WHDU2KR.js → claude-pair-runtime-7JTAECL4.js} +64 -9
  14. package/dist/claude-pair-runtime-7JTAECL4.js.map +1 -0
  15. package/dist/lib/manager-worker.js +33 -19
  16. package/dist/lib/manager-worker.js.map +1 -1
  17. package/dist/mcp/direct-chat-channel.js +35 -0
  18. package/dist/mcp/index.js +35 -0
  19. package/dist/mcp/origami.js +35 -0
  20. package/dist/mcp/slack-channel.js +35 -0
  21. package/dist/mcp/telegram-channel.js +35 -0
  22. package/dist/{persistent-session-523F5U4H.js → persistent-session-VX26HP7Z.js} +4 -3
  23. package/dist/{responsiveness-probe-CHGET573.js → responsiveness-probe-5LUHQCKN.js} +4 -3
  24. package/dist/{responsiveness-probe-CHGET573.js.map → responsiveness-probe-5LUHQCKN.js.map} +1 -1
  25. package/dist/{session-auth-dead-T5SUK2HT.js → session-auth-dead-2WOMHFPP.js} +2 -2
  26. package/package.json +1 -1
  27. package/dist/chunk-XN5MOKSV.js.map +0 -1
  28. package/dist/claude-code-updater-4E5T2X3Z.js.map +0 -1
  29. package/dist/claude-pair-runtime-7WHDU2KR.js.map +0 -1
  30. /package/dist/{persistent-session-523F5U4H.js.map → persistent-session-VX26HP7Z.js.map} +0 -0
  31. /package/dist/{session-auth-dead-T5SUK2HT.js.map → session-auth-dead-2WOMHFPP.js.map} +0 -0
@@ -5,7 +5,7 @@ import {
5
5
 
6
6
  // src/lib/claude-code-updater.ts
7
7
  import { execFile, execFileSync } from "child_process";
8
- import { existsSync, readFileSync } from "fs";
8
+ import { existsSync, readFileSync, realpathSync } from "fs";
9
9
  import { homedir } from "os";
10
10
  import { promisify } from "util";
11
11
  var execFileAsync = promisify(execFile);
@@ -67,6 +67,32 @@ function readsAsNativeInstall(raw) {
67
67
  return false;
68
68
  }
69
69
  }
70
+ function isNativeInstallPath(realPath) {
71
+ return /\/claude\/versions\/\d+\.\d+\.\d+[^/]*$/.test(realPath);
72
+ }
73
+ async function resolveNativeOwnership(run, realPath, claudeJson) {
74
+ const configSaysNative = readsAsNativeInstall(claudeJson);
75
+ let resolved = null;
76
+ const override = process.env["CLAUDE_PATH"];
77
+ if (override) {
78
+ resolved = override;
79
+ } else {
80
+ try {
81
+ const { stdout } = await run("which", ["claude"]);
82
+ resolved = stdout.trim().split("\n")[0]?.trim() || null;
83
+ } catch {
84
+ resolved = null;
85
+ }
86
+ }
87
+ if (resolved) {
88
+ try {
89
+ const real = realPath(resolved);
90
+ return { native: isNativeInstallPath(real), binary: real, configSaysNative };
91
+ } catch {
92
+ }
93
+ }
94
+ return { native: configSaysNative, binary: null, configSaysNative };
95
+ }
70
96
  async function resolveInstallTargetPath(run) {
71
97
  try {
72
98
  const { stdout } = await run("npm", ["prefix", "-g"]);
@@ -105,6 +131,7 @@ async function maybeUpdateClaudeCode(deps) {
105
131
  const ps = deps.psSnapshot ?? defaultPsSnapshot;
106
132
  const fileExists = deps.fileExists ?? existsSync;
107
133
  const readClaudeJson = deps.readClaudeJson ?? defaultReadClaudeJson;
134
+ const realPath = deps.realPath ?? realpathSync;
108
135
  const home = (deps.homeDir ?? homedir)();
109
136
  const { log } = deps;
110
137
  if (now - lastCheckAt < intervalMs()) {
@@ -121,7 +148,13 @@ async function maybeUpdateClaudeCode(deps) {
121
148
  }
122
149
  multiInstallWarned = true;
123
150
  }
124
- if (readsAsNativeInstall(readClaudeJson(`${home}/.claude.json`))) {
151
+ const ownership = await resolveNativeOwnership(run, realPath, readClaudeJson(`${home}/.claude.json`));
152
+ if (!ownership.native && ownership.configSaysNative) {
153
+ log(
154
+ `[claude-code-updater] ~/.claude.json says installMethod=native but the binary this host runs (${ownership.binary}) is not a native install \u2014 ignoring it and converging to the pin`
155
+ );
156
+ }
157
+ if (ownership.native) {
125
158
  log("[claude-code-updater] native install detected, deferring to Anthropic autoupdater");
126
159
  verifyMismatchState = null;
127
160
  return { action: "skipped_native_install", target: spec };
@@ -200,9 +233,10 @@ export {
200
233
  _resetClaudeCodeUpdaterState,
201
234
  hostHasLiveAgent,
202
235
  isConcreteVersion,
236
+ isNativeInstallPath,
203
237
  maybeUpdateClaudeCode,
204
238
  parseVersion,
205
239
  probeClaudeInstalls,
206
240
  readsAsNativeInstall
207
241
  };
208
- //# sourceMappingURL=claude-code-updater-4E5T2X3Z.js.map
242
+ //# sourceMappingURL=claude-code-updater-NAHJ6O6C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/claude-code-updater.ts"],"sourcesContent":["// ENG-5672: idle-gated, server-pinned Claude Code auto-updater.\n// ENG-5798: multi-install drift handling — detect coexisting claude binaries,\n// short-circuit on native autoupdater hosts, verify against absolute\n// install path, hard-error after N consecutive verify mismatches.\n//\n// Hosts install @anthropic-ai/claude-code once at bootstrap (host-bootstrap.ts)\n// and never refresh it, so a long-lived host runs a stale Claude Code forever.\n// This module brings a host to its *desired* version, which the server pins via\n// /host/refresh (per-host override, else fleet default, else 'latest').\n//\n// Safety properties (this runs npm install -g on live prod hosts):\n// • Idle-gated — we never install while a `claude --name agt-*` agent process\n// is running on the host (a running process holds the old binary anyway;\n// replacing it under a live session is pointless and risky). Busy → defer\n// to a later cycle.\n// • No-op when already on target — a concrete target equal to the installed\n// version short-circuits; a dist-tag ('latest') is resolved to a concrete\n// version via `npm view` and compared, so we don't reinstall every cycle.\n// • Host-global throttle — at most one check per interval (default 6h), so the\n// manager's ~10-30s poll doesn't shell out to npm/ps constantly.\n// • Log-and-continue — every failure is logged and swallowed; an error here\n// must never break the manager poll cycle.\n// • Multi-install awareness (ENG-5798) — on hosts where Anthropic's native\n// autoupdater owns /usr/bin/claude, this updater short-circuits cleanly\n// instead of fighting it. On hosts where multiple binaries coexist (e.g. a\n// brew cask alongside the npm global), we log every path+version once so\n// drift is visible in manager.log without spamming every cycle.\n//\n// The *installed* version is reported separately as hosts.framework_version\n// (the claude-code framework adapter's getVersion() == `claude --version`), so\n// this module does not report it itself.\n\nimport { execFile, execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync, realpathSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { promisify } from 'node:util';\nimport { parsePsRows, CLAUDE_AGENT_PROCESS_PATTERN } from './orphan-channel-mcp-reaper.js';\n\nconst execFileAsync = promisify(execFile);\n\nconst NPM_PKG = '@anthropic-ai/claude-code';\n\n/** Default host-global check interval. Overridable via env for tuning. */\nconst DEFAULT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h\n\n/**\n * ENG-5798: After this many consecutive verify mismatches against the same\n * target version, emit a distinct one-shot error code that the\n * synthetic-probe / CloudWatch alarm pipeline can latch onto, instead of\n * silently re-failing forever. Overridable via env for testing.\n */\nconst DEFAULT_VERIFY_MISMATCH_THRESHOLD = 5;\n\nfunction intervalMs(): number {\n const raw = Number.parseInt(process.env['AGT_CLAUDE_UPDATE_INTERVAL_MS'] ?? '', 10);\n return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_INTERVAL_MS;\n}\n\nfunction verifyMismatchThreshold(): number {\n const raw = Number.parseInt(process.env['AGT_CLAUDE_UPDATE_VERIFY_THRESHOLD'] ?? '', 10);\n return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_VERIFY_MISMATCH_THRESHOLD;\n}\n\n/** Extract a version string (leading semver) from `claude --version` / npm output. */\nexport function parseVersion(output: string): string | null {\n const m = output.match(/\\d+\\.\\d+\\.\\d+[A-Za-z0-9.+-]*/);\n return m ? m[0] : null;\n}\n\n/** A target spec is \"concrete\" when it already names an exact version. */\nexport function isConcreteVersion(spec: string): boolean {\n return /^\\d+\\.\\d+\\.\\d+[A-Za-z0-9.+-]*$/.test(spec.trim());\n}\n\n/**\n * Well-known absolute paths where a `claude` binary may live on managed hosts.\n * Probed alongside the PATH-resolved binary so we can see all install slots,\n * even ones not on the manager systemd unit's PATH.\n */\nexport const WELL_KNOWN_CLAUDE_PATHS: readonly string[] = [\n '/usr/bin/claude', // Anthropic native autoupdater (Linux deb/rpm)\n '/usr/local/bin/claude', // npm global on macOS Intel + Linux without nvm\n '/opt/homebrew/bin/claude', // Homebrew on macOS Apple Silicon\n '/home/linuxbrew/.linuxbrew/bin/claude', // Linuxbrew (used on agt-aws-* hosts)\n];\n\nexport interface ClaudeCodeUpdaterDeps {\n /** Server-pinned target: a concrete version or an npm dist-tag like 'latest'. */\n desiredVersion: string;\n /** Currently-installed version, if the caller already knows it (e.g. the\n * manager's cachedFrameworkVersion). When omitted we run `claude --version`. */\n currentVersion?: string | null;\n log: (msg: string) => void;\n /** Override \"now\" for tests. */\n now?: () => number;\n /** Run a command; injected in tests. Defaults to execFile. */\n runCommand?: (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>;\n /** Raw `ps -eo pid,ppid,args` snapshot; injected in tests. */\n psSnapshot?: () => string;\n /** Sync existence probe for binary detection; injected in tests. */\n fileExists?: (path: string) => boolean;\n /** Read ~/.claude.json contents (raw); returns null when absent. Injected in tests. */\n readClaudeJson?: (path: string) => string | null;\n /** Resolve the user's home directory; injected in tests. */\n homeDir?: () => string;\n /** Resolve symlinks to a real path (throws when absent); injected in tests. */\n realPath?: (path: string) => string;\n}\n\nexport type UpdaterOutcome =\n | { action: 'throttled' }\n | { action: 'up_to_date'; version: string }\n | { action: 'deferred_busy'; target: string }\n | { action: 'skipped_native_install'; target: string }\n | { action: 'updated'; from: string | null; to: string }\n | { action: 'failed'; reason: string };\n\n// ── Host-global state (module-level; one updater per host process) ──────────\nlet lastCheckAt = 0;\n/** Per-process one-shot guard for the multi-install warn line. */\nlet multiInstallWarned = false;\n/**\n * Sticky counter for consecutive `post-install verify mismatch` against the\n * same target version. Reset on success, on target change, or after the\n * threshold error has been emitted (so we emit it exactly once per stuck run).\n */\nlet verifyMismatchState: { target: string; count: number; alarmed: boolean } | null = null;\n\n/** Test helper — reset module state. Not for production use. */\nexport function _resetClaudeCodeUpdaterState(): void {\n lastCheckAt = 0;\n multiInstallWarned = false;\n verifyMismatchState = null;\n}\n\nfunction defaultPsSnapshot(): string {\n return execFileSync('ps', ['-eo', 'pid,ppid,args'], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });\n}\n\nfunction defaultReadClaudeJson(path: string): string | null {\n try {\n return readFileSync(path, 'utf-8');\n } catch {\n return null;\n }\n}\n\n/** True when at least one `claude --name agt-*` agent process is live. */\nexport function hostHasLiveAgent(psOutput: string): boolean {\n return parsePsRows(psOutput).some((r) => CLAUDE_AGENT_PROCESS_PATTERN.test(r.args));\n}\n\n/**\n * ENG-5798: returns true when ~/.claude.json reports `installMethod: \"native\"`,\n * meaning Anthropic's native autoupdater owns the on-disk binary. In that case\n * the brew/npm update path is actively harmful — it installs a second binary\n * the host's PATH never resolves to, and verify reads from yet a third.\n */\nexport function readsAsNativeInstall(raw: string | null): boolean {\n if (!raw) return false;\n try {\n const parsed = JSON.parse(raw) as { installMethod?: unknown };\n return parsed.installMethod === 'native';\n } catch {\n return false;\n }\n}\n\n/**\n * ENG-10620: true when `realPath` is a binary owned by Claude Code's native\n * installer, which keeps every version under `…/claude/versions/<semver>`\n * (e.g. `~/.local/share/claude/versions/2.1.274`, symlinked from\n * `~/.local/bin/claude`). An npm global resolves into\n * `…/node_modules/@anthropic-ai/claude-code/` instead.\n */\nexport function isNativeInstallPath(realPath: string): boolean {\n return /\\/claude\\/versions\\/\\d+\\.\\d+\\.\\d+[^/]*$/.test(realPath);\n}\n\n/**\n * ENG-10620: decide native ownership from the binary this host ACTUALLY runs,\n * not from `~/.claude.json`.\n *\n * `installMethod` is written by whichever `claude` last started, not by the\n * one the manager launches. On agt-aws-1 an operator shell's PATH resolved a\n * native `~/.local/bin/claude` while agents ran the npm global at\n * `/usr/bin/claude`; that one manual run flipped `installMethod` to `native`\n * and this updater stopped converging the host to its pin for good, while the\n * binary it would have updated was never native at all.\n *\n * Resolution mirrors how agent sessions pick their binary (resolveClaudeBinary:\n * CLAUDE_PATH, then PATH). The config key remains only as a fallback for a\n * host where no binary can be resolved.\n */\nasync function resolveNativeOwnership(\n run: (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>,\n realPath: (path: string) => string,\n claudeJson: string | null,\n): Promise<{ native: boolean; binary: string | null; configSaysNative: boolean }> {\n const configSaysNative = readsAsNativeInstall(claudeJson);\n let resolved: string | null = null;\n const override = process.env['CLAUDE_PATH'];\n if (override) {\n resolved = override;\n } else {\n try {\n const { stdout } = await run('which', ['claude']);\n resolved = stdout.trim().split('\\n')[0]?.trim() || null;\n } catch {\n resolved = null;\n }\n }\n if (resolved) {\n try {\n const real = realPath(resolved);\n return { native: isNativeInstallPath(real), binary: real, configSaysNative };\n } catch {\n /* unresolvable — fall back to the config key */\n }\n }\n return { native: configSaysNative, binary: null, configSaysNative };\n}\n\n/**\n * Resolve the absolute filesystem path that `npm install -g <pkg>` will write\n * the `claude` binary to. Returns null when `npm prefix -g` fails or is empty,\n * in which case the caller should fall back to PATH-based verify.\n *\n * We deliberately use `npm prefix -g` rather than `which claude` because the\n * latter resolves through PATH, which on agt-aws-* hosts can land on\n * /usr/bin/claude (the native autoupdater slot) — exactly the misalignment\n * ENG-5798 is fixing.\n */\nasync function resolveInstallTargetPath(\n run: (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>,\n): Promise<string | null> {\n try {\n const { stdout } = await run('npm', ['prefix', '-g']);\n const prefix = stdout.trim();\n if (!prefix) return null;\n return `${prefix}/bin/claude`;\n } catch {\n return null;\n }\n}\n\n/**\n * Enumerate every `claude` binary visible to the host: PATH-resolved + the\n * well-known absolute slots + `~/.claude/local/claude`. Returns one entry per\n * unique resolved path with the version each reports (or null if it failed to\n * execute). Used purely for the one-shot drift warn line.\n */\nexport async function probeClaudeInstalls(\n run: (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>,\n fileExists: (path: string) => boolean,\n home: string,\n): Promise<Array<{ path: string; version: string | null }>> {\n const candidates = new Set<string>(WELL_KNOWN_CLAUDE_PATHS);\n candidates.add(`${home}/.claude/local/claude`);\n // PATH-resolved. `which` is portable enough across the Linux + macOS hosts\n // we target; errors here just mean \"no claude on PATH\", which is fine.\n try {\n const { stdout } = await run('which', ['claude']);\n const p = stdout.trim().split('\\n')[0]?.trim();\n if (p) candidates.add(p);\n } catch {\n /* no-op */\n }\n\n const results: Array<{ path: string; version: string | null }> = [];\n for (const p of candidates) {\n if (!fileExists(p)) continue;\n let version: string | null = null;\n try {\n const { stdout } = await run(p, ['--version']);\n version = parseVersion(stdout);\n } catch {\n /* binary exists but failed to execute — record path with null version */\n }\n results.push({ path: p, version });\n }\n return results;\n}\n\n/**\n * Bring Claude Code to the desired version if the host is idle and not already\n * on target. Safe to call every manager poll — internally throttled. Returns an\n * outcome describing what happened (for logging/observability/tests).\n */\nexport async function maybeUpdateClaudeCode(deps: ClaudeCodeUpdaterDeps): Promise<UpdaterOutcome> {\n const now = deps.now ? deps.now() : Date.now();\n const run = deps.runCommand ?? ((cmd, args) => execFileAsync(cmd, args, { maxBuffer: 16 * 1024 * 1024 }));\n const ps = deps.psSnapshot ?? defaultPsSnapshot;\n const fileExists = deps.fileExists ?? existsSync;\n const readClaudeJson = deps.readClaudeJson ?? defaultReadClaudeJson;\n const realPath = deps.realPath ?? realpathSync;\n const home = (deps.homeDir ?? homedir)();\n const { log } = deps;\n\n // 1. Host-global throttle.\n if (now - lastCheckAt < intervalMs()) {\n return { action: 'throttled' };\n }\n lastCheckAt = now;\n\n try {\n const spec = (deps.desiredVersion || 'latest').trim();\n\n // 2. ENG-5798 multi-install drift detection. Emit one warn line per\n // process listing every claude binary + version we can see. This is\n // independent of the install decision below — even on hosts where we\n // proceed normally, surfacing drift is the difference between a 3-day\n // silent loop and a one-line lead.\n if (!multiInstallWarned) {\n const installs = await probeClaudeInstalls(run, fileExists, home);\n if (installs.length > 1) {\n const summary = installs\n .map((i) => `${i.path}=${i.version ?? 'unknown'}`)\n .join(', ');\n log(`[claude-code-updater] multiple claude installs detected: ${summary}`);\n }\n multiInstallWarned = true;\n }\n\n // 3. ENG-5798 native-install short-circuit. When Anthropic's native\n // autoupdater owns the host binary, the brew/npm update path is\n // actively harmful (it installs a second binary the manager's PATH\n // never resolves to, then verifies against yet a third). Defer.\n const ownership = await resolveNativeOwnership(run, realPath, readClaudeJson(`${home}/.claude.json`));\n if (!ownership.native && ownership.configSaysNative) {\n log(\n `[claude-code-updater] ~/.claude.json says installMethod=native but the binary this host runs (${ownership.binary}) is not a native install — ignoring it and converging to the pin`,\n );\n }\n if (ownership.native) {\n // One-shot info line per process — same throttle as the warn above, but\n // we re-use the throttle window to keep this from logging every poll.\n log('[claude-code-updater] native install detected, deferring to Anthropic autoupdater');\n // Reset any sticky verify-mismatch state — it can't apply once we stop\n // attempting installs.\n verifyMismatchState = null;\n return { action: 'skipped_native_install', target: spec };\n }\n\n // 4. Resolve the target to a concrete version (read-only). A concrete spec\n // is its own target; a dist-tag is resolved via `npm view`.\n let target: string;\n if (isConcreteVersion(spec)) {\n target = spec;\n } else {\n const { stdout } = await run('npm', ['view', `${NPM_PKG}@${spec}`, 'version']);\n const resolved = parseVersion(stdout);\n if (!resolved) {\n log(`[claude-code-updater] could not resolve ${NPM_PKG}@${spec} to a version; skipping`);\n return { action: 'failed', reason: 'unresolved_target' };\n }\n target = resolved;\n }\n\n // 5. Determine the installed version (prefer the caller's cached value).\n let current = deps.currentVersion ?? null;\n if (!current) {\n try {\n const { stdout } = await run('claude', ['--version']);\n current = parseVersion(stdout);\n } catch {\n current = null; // claude missing/broken — treat as \"needs install\".\n }\n }\n\n // 6. Already on target → nothing to do.\n if (current && current === target) {\n // Successful no-op — clear sticky verify mismatch state for this target.\n if (verifyMismatchState && verifyMismatchState.target === target) {\n verifyMismatchState = null;\n }\n return { action: 'up_to_date', version: target };\n }\n\n // 7. Idle gate — never replace the binary under a live agent session.\n if (hostHasLiveAgent(ps())) {\n log(`[claude-code-updater] host busy (live agent) — deferring update to ${target} to a later cycle`);\n return { action: 'deferred_busy', target };\n }\n\n // 8. Install the target, then verify against the *absolute* path of the\n // binary npm just wrote — not via PATH. PATH resolution is the original\n // ENG-5798 bug: on hosts with multiple claude installs, PATH lands on a\n // different binary than the one npm overwrites, so verify never agrees.\n const installTargetPath = await resolveInstallTargetPath(run);\n log(`[claude-code-updater] updating Claude Code ${current ?? '(none)'} → ${target}`);\n await run('npm', ['install', '-g', `${NPM_PKG}@${target}`]);\n\n // Prefer the absolute install target; fall back to PATH only when\n // `npm prefix -g` failed (in which case verify is best-effort).\n const verifyTarget = installTargetPath ?? 'claude';\n let installed: string | null = null;\n try {\n const { stdout } = await run(verifyTarget, ['--version']);\n installed = parseVersion(stdout);\n } catch {\n installed = null;\n }\n\n if (installed !== target) {\n log(\n `[claude-code-updater] post-install verify mismatch: expected ${target}, got ${installed ?? 'unknown'} from ${verifyTarget}`,\n );\n\n // ENG-5798: hard-error after N consecutive mismatches against the same\n // target. Reset the counter when the target changes — a new pin is a\n // new event, not a continuation of the old stuck run.\n if (!verifyMismatchState || verifyMismatchState.target !== target) {\n verifyMismatchState = { target, count: 1, alarmed: false };\n } else {\n verifyMismatchState.count += 1;\n }\n\n const threshold = verifyMismatchThreshold();\n if (verifyMismatchState.count >= threshold && !verifyMismatchState.alarmed) {\n verifyMismatchState.alarmed = true;\n log(\n `[claude-code-updater] ERROR verify_mismatch_stuck: ${verifyMismatchState.count} consecutive verify failures for target ${target} (verify path: ${verifyTarget}) — install is not taking effect, investigate host install slots`,\n );\n return { action: 'failed', reason: 'verify_mismatch_threshold' };\n }\n return { action: 'failed', reason: 'verify_mismatch' };\n }\n\n // Successful install — clear sticky state.\n verifyMismatchState = null;\n log(`[claude-code-updater] updated Claude Code ${current ?? '(none)'} → ${installed}`);\n return { action: 'updated', from: current, to: installed };\n } catch (err) {\n // Log-and-continue: an updater failure must never break the manager tick.\n log(`[claude-code-updater] update attempt failed: ${err instanceof Error ? err.message : String(err)}`);\n return { action: 'failed', reason: 'exception' };\n }\n}\n"],"mappings":";;;;;;AAgCA,SAAS,UAAU,oBAAoB;AACvC,SAAS,YAAY,cAAc,oBAAoB;AACvD,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAG1B,IAAM,gBAAgB,UAAU,QAAQ;AAExC,IAAM,UAAU;AAGhB,IAAM,sBAAsB,IAAI,KAAK,KAAK;AAQ1C,IAAM,oCAAoC;AAE1C,SAAS,aAAqB;AAC5B,QAAM,MAAM,OAAO,SAAS,QAAQ,IAAI,+BAA+B,KAAK,IAAI,EAAE;AAClF,SAAO,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM;AACjD;AAEA,SAAS,0BAAkC;AACzC,QAAM,MAAM,OAAO,SAAS,QAAQ,IAAI,oCAAoC,KAAK,IAAI,EAAE;AACvF,SAAO,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM;AACjD;AAGO,SAAS,aAAa,QAA+B;AAC1D,QAAM,IAAI,OAAO,MAAM,8BAA8B;AACrD,SAAO,IAAI,EAAE,CAAC,IAAI;AACpB;AAGO,SAAS,kBAAkB,MAAuB;AACvD,SAAO,iCAAiC,KAAK,KAAK,KAAK,CAAC;AAC1D;AAOO,IAAM,0BAA6C;AAAA,EACxD;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF;AAkCA,IAAI,cAAc;AAElB,IAAI,qBAAqB;AAMzB,IAAI,sBAAkF;AAG/E,SAAS,+BAAqC;AACnD,gBAAc;AACd,uBAAqB;AACrB,wBAAsB;AACxB;AAEA,SAAS,oBAA4B;AACnC,SAAO,aAAa,MAAM,CAAC,OAAO,eAAe,GAAG,EAAE,UAAU,SAAS,WAAW,KAAK,OAAO,KAAK,CAAC;AACxG;AAEA,SAAS,sBAAsB,MAA6B;AAC1D,MAAI;AACF,WAAO,aAAa,MAAM,OAAO;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,iBAAiB,UAA2B;AAC1D,SAAO,YAAY,QAAQ,EAAE,KAAK,CAAC,MAAM,6BAA6B,KAAK,EAAE,IAAI,CAAC;AACpF;AAQO,SAAS,qBAAqB,KAA6B;AAChE,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,OAAO,kBAAkB;AAAA,EAClC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASO,SAAS,oBAAoB,UAA2B;AAC7D,SAAO,0CAA0C,KAAK,QAAQ;AAChE;AAiBA,eAAe,uBACb,KACA,UACA,YACgF;AAChF,QAAM,mBAAmB,qBAAqB,UAAU;AACxD,MAAI,WAA0B;AAC9B,QAAM,WAAW,QAAQ,IAAI,aAAa;AAC1C,MAAI,UAAU;AACZ,eAAW;AAAA,EACb,OAAO;AACL,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,MAAM,IAAI,SAAS,CAAC,QAAQ,CAAC;AAChD,iBAAW,OAAO,KAAK,EAAE,MAAM,IAAI,EAAE,CAAC,GAAG,KAAK,KAAK;AAAA,IACrD,QAAQ;AACN,iBAAW;AAAA,IACb;AAAA,EACF;AACA,MAAI,UAAU;AACZ,QAAI;AACF,YAAM,OAAO,SAAS,QAAQ;AAC9B,aAAO,EAAE,QAAQ,oBAAoB,IAAI,GAAG,QAAQ,MAAM,iBAAiB;AAAA,IAC7E,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,kBAAkB,QAAQ,MAAM,iBAAiB;AACpE;AAYA,eAAe,yBACb,KACwB;AACxB,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,IAAI,OAAO,CAAC,UAAU,IAAI,CAAC;AACpD,UAAM,SAAS,OAAO,KAAK;AAC3B,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,GAAG,MAAM;AAAA,EAClB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,eAAsB,oBACpB,KACA,YACA,MAC0D;AAC1D,QAAM,aAAa,IAAI,IAAY,uBAAuB;AAC1D,aAAW,IAAI,GAAG,IAAI,uBAAuB;AAG7C,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,IAAI,SAAS,CAAC,QAAQ,CAAC;AAChD,UAAM,IAAI,OAAO,KAAK,EAAE,MAAM,IAAI,EAAE,CAAC,GAAG,KAAK;AAC7C,QAAI,EAAG,YAAW,IAAI,CAAC;AAAA,EACzB,QAAQ;AAAA,EAER;AAEA,QAAM,UAA2D,CAAC;AAClE,aAAW,KAAK,YAAY;AAC1B,QAAI,CAAC,WAAW,CAAC,EAAG;AACpB,QAAI,UAAyB;AAC7B,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,MAAM,IAAI,GAAG,CAAC,WAAW,CAAC;AAC7C,gBAAU,aAAa,MAAM;AAAA,IAC/B,QAAQ;AAAA,IAER;AACA,YAAQ,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAC;AAAA,EACnC;AACA,SAAO;AACT;AAOA,eAAsB,sBAAsB,MAAsD;AAChG,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,KAAK,IAAI;AAC7C,QAAM,MAAM,KAAK,eAAe,CAAC,KAAK,SAAS,cAAc,KAAK,MAAM,EAAE,WAAW,KAAK,OAAO,KAAK,CAAC;AACvG,QAAM,KAAK,KAAK,cAAc;AAC9B,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,QAAQ,KAAK,WAAW,SAAS;AACvC,QAAM,EAAE,IAAI,IAAI;AAGhB,MAAI,MAAM,cAAc,WAAW,GAAG;AACpC,WAAO,EAAE,QAAQ,YAAY;AAAA,EAC/B;AACA,gBAAc;AAEd,MAAI;AACF,UAAM,QAAQ,KAAK,kBAAkB,UAAU,KAAK;AAOpD,QAAI,CAAC,oBAAoB;AACvB,YAAM,WAAW,MAAM,oBAAoB,KAAK,YAAY,IAAI;AAChE,UAAI,SAAS,SAAS,GAAG;AACvB,cAAM,UAAU,SACb,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,IAAI,EAAE,WAAW,SAAS,EAAE,EAChD,KAAK,IAAI;AACZ,YAAI,4DAA4D,OAAO,EAAE;AAAA,MAC3E;AACA,2BAAqB;AAAA,IACvB;AAMA,UAAM,YAAY,MAAM,uBAAuB,KAAK,UAAU,eAAe,GAAG,IAAI,eAAe,CAAC;AACpG,QAAI,CAAC,UAAU,UAAU,UAAU,kBAAkB;AACnD;AAAA,QACE,iGAAiG,UAAU,MAAM;AAAA,MACnH;AAAA,IACF;AACA,QAAI,UAAU,QAAQ;AAGpB,UAAI,mFAAmF;AAGvF,4BAAsB;AACtB,aAAO,EAAE,QAAQ,0BAA0B,QAAQ,KAAK;AAAA,IAC1D;AAIA,QAAI;AACJ,QAAI,kBAAkB,IAAI,GAAG;AAC3B,eAAS;AAAA,IACX,OAAO;AACL,YAAM,EAAE,OAAO,IAAI,MAAM,IAAI,OAAO,CAAC,QAAQ,GAAG,OAAO,IAAI,IAAI,IAAI,SAAS,CAAC;AAC7E,YAAM,WAAW,aAAa,MAAM;AACpC,UAAI,CAAC,UAAU;AACb,YAAI,2CAA2C,OAAO,IAAI,IAAI,yBAAyB;AACvF,eAAO,EAAE,QAAQ,UAAU,QAAQ,oBAAoB;AAAA,MACzD;AACA,eAAS;AAAA,IACX;AAGA,QAAI,UAAU,KAAK,kBAAkB;AACrC,QAAI,CAAC,SAAS;AACZ,UAAI;AACF,cAAM,EAAE,OAAO,IAAI,MAAM,IAAI,UAAU,CAAC,WAAW,CAAC;AACpD,kBAAU,aAAa,MAAM;AAAA,MAC/B,QAAQ;AACN,kBAAU;AAAA,MACZ;AAAA,IACF;AAGA,QAAI,WAAW,YAAY,QAAQ;AAEjC,UAAI,uBAAuB,oBAAoB,WAAW,QAAQ;AAChE,8BAAsB;AAAA,MACxB;AACA,aAAO,EAAE,QAAQ,cAAc,SAAS,OAAO;AAAA,IACjD;AAGA,QAAI,iBAAiB,GAAG,CAAC,GAAG;AAC1B,UAAI,2EAAsE,MAAM,mBAAmB;AACnG,aAAO,EAAE,QAAQ,iBAAiB,OAAO;AAAA,IAC3C;AAMA,UAAM,oBAAoB,MAAM,yBAAyB,GAAG;AAC5D,QAAI,8CAA8C,WAAW,QAAQ,WAAM,MAAM,EAAE;AACnF,UAAM,IAAI,OAAO,CAAC,WAAW,MAAM,GAAG,OAAO,IAAI,MAAM,EAAE,CAAC;AAI1D,UAAM,eAAe,qBAAqB;AAC1C,QAAI,YAA2B;AAC/B,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,MAAM,IAAI,cAAc,CAAC,WAAW,CAAC;AACxD,kBAAY,aAAa,MAAM;AAAA,IACjC,QAAQ;AACN,kBAAY;AAAA,IACd;AAEA,QAAI,cAAc,QAAQ;AACxB;AAAA,QACE,gEAAgE,MAAM,SAAS,aAAa,SAAS,SAAS,YAAY;AAAA,MAC5H;AAKA,UAAI,CAAC,uBAAuB,oBAAoB,WAAW,QAAQ;AACjE,8BAAsB,EAAE,QAAQ,OAAO,GAAG,SAAS,MAAM;AAAA,MAC3D,OAAO;AACL,4BAAoB,SAAS;AAAA,MAC/B;AAEA,YAAM,YAAY,wBAAwB;AAC1C,UAAI,oBAAoB,SAAS,aAAa,CAAC,oBAAoB,SAAS;AAC1E,4BAAoB,UAAU;AAC9B;AAAA,UACE,sDAAsD,oBAAoB,KAAK,2CAA2C,MAAM,kBAAkB,YAAY;AAAA,QAChK;AACA,eAAO,EAAE,QAAQ,UAAU,QAAQ,4BAA4B;AAAA,MACjE;AACA,aAAO,EAAE,QAAQ,UAAU,QAAQ,kBAAkB;AAAA,IACvD;AAGA,0BAAsB;AACtB,QAAI,6CAA6C,WAAW,QAAQ,WAAM,SAAS,EAAE;AACrF,WAAO,EAAE,QAAQ,WAAW,MAAM,SAAS,IAAI,UAAU;AAAA,EAC3D,SAAS,KAAK;AAEZ,QAAI,gDAAgD,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AACtG,WAAO,EAAE,QAAQ,UAAU,QAAQ,YAAY;AAAA,EACjD;AACF;","names":[]}
@@ -1,3 +1,8 @@
1
+ import {
2
+ consumerTermsOffKeys,
3
+ selectRowKeys
4
+ } from "./chunk-DHWNVVX4.js";
5
+
1
6
  // src/lib/claude-pair-runtime.ts
2
7
  import { execFile } from "child_process";
3
8
  import { promisify } from "util";
@@ -94,6 +99,18 @@ async function sendKeys(session, ...keys) {
94
99
  async function sleep(ms) {
95
100
  return new Promise((r) => setTimeout(r, ms));
96
101
  }
102
+ async function answerConsumerTermsDialog(session, pane) {
103
+ const keys = consumerTermsOffKeys(pane);
104
+ if (!keys) return false;
105
+ await sendKeySequence(session, keys);
106
+ return true;
107
+ }
108
+ async function sendKeySequence(session, keys) {
109
+ for (let i = 0; i < keys.length; i++) {
110
+ if (i > 0) await sleep(300);
111
+ await sendKeys(session, keys[i]);
112
+ }
113
+ }
97
114
  function describeClaudeExit(exitStatus, pane) {
98
115
  const lines = pane.split("\n").map((l) => l.trimEnd()).filter((l) => l.length > 0 && !/^Pane is dead\b/.test(l));
99
116
  let output = lines.slice(-15).join("\n");
@@ -104,20 +121,33 @@ ${output}
104
121
  ` : "It printed nothing. ") + "Run `claude` on the host as the manager's user to reproduce, then Start over.";
105
122
  }
106
123
  async function checkPairPaneAlive(session) {
107
- let stdout;
108
- try {
109
- ({ stdout } = await execFileAsync("tmux", [
124
+ const read = async () => {
125
+ const { stdout } = await execFileAsync("tmux", [
110
126
  "display-message",
111
127
  "-p",
112
128
  "-t",
113
129
  session,
114
130
  "#{pane_dead} #{pane_dead_status}"
115
- ]));
131
+ ]);
132
+ const [dead = "", exitStatus2 = ""] = stdout.trim().split(" ");
133
+ return { dead, exitStatus: exitStatus2 };
134
+ };
135
+ let state;
136
+ try {
137
+ state = await read();
116
138
  } catch (err) {
117
139
  return classifyTmuxError(err);
118
140
  }
119
- const [dead = "", exitStatus = ""] = stdout.trim().split(" ");
120
- if (dead !== "1") return null;
141
+ if (state.dead !== "1") return null;
142
+ for (let i = 0; i < 10 && !/^\d+$/.test(state.exitStatus); i++) {
143
+ await sleep(100);
144
+ try {
145
+ state = await read();
146
+ } catch {
147
+ break;
148
+ }
149
+ }
150
+ const { exitStatus } = state;
121
151
  let pane = "";
122
152
  try {
123
153
  pane = await capturePane(session, { scrollback: -50 });
@@ -131,7 +161,7 @@ async function spawnPairSession(session) {
131
161
  return { ok: true };
132
162
  } catch {
133
163
  }
134
- const { resolveClaudeBinary } = await import("./persistent-session-523F5U4H.js");
164
+ const { resolveClaudeBinary } = await import("./persistent-session-VX26HP7Z.js");
135
165
  const claudeBin = resolveClaudeBinary();
136
166
  const pairEnv = {
137
167
  ...process.env,
@@ -198,6 +228,12 @@ async function finalizeClaudePairOnboarding(session, log, opts = {}) {
198
228
  log(`[claude-pair] finalize: capture-pane failed (${classified.kind}); aborting onboarding flow`);
199
229
  return { finalized: false, iterations: i };
200
230
  }
231
+ try {
232
+ if (await answerConsumerTermsDialog(session, pane)) continue;
233
+ } catch {
234
+ log("[claude-pair] finalize: send-keys failed on consumer-terms dialog; aborting onboarding flow");
235
+ return { finalized: false, iterations: i };
236
+ }
201
237
  if (pane.includes("Login successful") || pane.includes("Logged in as") || /Press Enter to continue/i.test(pane) || pane.includes("Security notes")) {
202
238
  try {
203
239
  await sendKeys(session, "C-m");
@@ -283,6 +319,17 @@ async function startClaudePair(opts) {
283
319
  }
284
320
  };
285
321
  }
322
+ if (consumerTermsOffKeys(pane)) {
323
+ if (Date.now() - lastDispatchAt >= 1500) {
324
+ lastDispatchAt = Date.now();
325
+ try {
326
+ await answerConsumerTermsDialog(session, pane);
327
+ } catch (err) {
328
+ return { kind: "error", error: classifyTmuxError(err) };
329
+ }
330
+ }
331
+ continue;
332
+ }
286
333
  if (/Select login method:/i.test(pane)) {
287
334
  await dispatchEnter();
288
335
  continue;
@@ -292,7 +339,15 @@ async function startClaudePair(opts) {
292
339
  continue;
293
340
  }
294
341
  if (isTrustDialogVisible(pane)) {
295
- await dispatchEnter();
342
+ const keys = selectRowKeys(pane, "Yes, I trust this folder");
343
+ if (keys && Date.now() - lastDispatchAt >= 1500) {
344
+ lastDispatchAt = Date.now();
345
+ try {
346
+ await sendKeySequence(session, keys);
347
+ } catch (err) {
348
+ return { kind: "error", error: classifyTmuxError(err) };
349
+ }
350
+ }
296
351
  continue;
297
352
  }
298
353
  if (/press\s+enter\s+to\s+continue/i.test(pane)) {
@@ -417,4 +472,4 @@ export {
417
472
  startClaudePair,
418
473
  submitClaudePairCode
419
474
  };
420
- //# sourceMappingURL=claude-pair-runtime-7WHDU2KR.js.map
475
+ //# sourceMappingURL=claude-pair-runtime-7JTAECL4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/claude-pair-runtime.ts","../src/lib/claude-pair-parser.ts"],"sourcesContent":["/**\n * ENG-4580: manager-side runtime for the Claude Code OAuth pairing flow.\n *\n * These functions own the actual tmux dance — sending `/login`,\n * polling the pane until Claude Code prints the OAuth URL, sending\n * the auth code, and detecting success/failure. They are the\n * counterpart to the pure parser in `claude-pair-parser.ts`.\n *\n * The API surface in ENG-4581 will wrap these — they don't include\n * any HTTP / DB code so the unit tests can target the parser layer\n * without spinning up a fake API. Errors are classified into the\n * `SessionError` shape so the API can translate them into structured\n * 4xx responses (e.g. `session_missing`).\n *\n * Architectural note: tmux capture-pane on a session that doesn't\n * exist exits non-zero with `can't find session`. Same goes for tmux\n * not being installed. classifyTmuxError covers both.\n */\n\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { existsSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir, userInfo } from 'node:os';\n\nimport {\n classifyTmuxError,\n detectAuthOutcome,\n extractOAuthUrl,\n isUrlPromptReady,\n type AuthOutcome,\n type SessionError,\n} from './claude-pair-parser.js';\nimport { consumerTermsOffKeys, selectRowKeys } from './claude-dialogs.js';\n\nconst execFileAsync = promisify(execFile);\n\n// Pair-scoped tmux session name. The pairing flow runs inside a\n// throwaway `claude` instance — never inside an agent's persistent\n// session — so first-time auth on a fresh host works (no chicken-and-\n// egg) and re-auth doesn't disrupt an in-flight agent conversation.\nexport function pairTmuxSession(pairId: string): string {\n return `agt-pair-${pairId.slice(0, 12)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Low-level helpers\n// ---------------------------------------------------------------------------\n\ninterface CapturePaneOpts {\n /** How many lines of scrollback to include (negative = lines back). Default -200. */\n scrollback?: number;\n}\n\nasync function capturePane(session: string, opts: CapturePaneOpts = {}): Promise<string> {\n const scrollback = opts.scrollback ?? -200;\n const { stdout } = await execFileAsync('tmux', [\n 'capture-pane',\n '-t',\n session,\n '-p',\n '-S',\n String(scrollback),\n ]);\n return stdout;\n}\n\nasync function sendKeys(session: string, ...keys: string[]): Promise<void> {\n await execFileAsync('tmux', ['send-keys', '-t', session, ...keys]);\n}\n\nasync function sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n/**\n * ENG-10619: answer Claude Code's consumer-terms dialog with\n * \"Help improve our AI models: OFF\" if it is focused. Returns true when\n * keys were sent. One send-keys call per key with a beat between them —\n * batching arrow keys into one call risks a bracketed paste (see\n * sendDialogKeys in claude-dialogs.ts).\n */\nasync function answerConsumerTermsDialog(session: string, pane: string): Promise<boolean> {\n const keys = consumerTermsOffKeys(pane);\n if (!keys) return false;\n await sendKeySequence(session, keys);\n return true;\n}\n\nasync function sendKeySequence(session: string, keys: readonly string[]): Promise<void> {\n for (let i = 0; i < keys.length; i++) {\n if (i > 0) await sleep(300);\n await sendKeys(session, keys[i]!);\n }\n}\n\n/**\n * ENG-10617: turn a dead pair pane into an operator-readable message.\n *\n * Pair sessions run with `remain-on-exit on`, so when claude exits the\n * pane stays behind holding its final screen plus tmux's own\n * `Pane is dead (status N, <date>)` banner. That screen is the only record\n * of why claude exited — without it the operator got a raw\n * `can't find pane` and nothing to act on.\n *\n * Exported for unit testing.\n */\nexport function describeClaudeExit(exitStatus: string, pane: string): string {\n const lines = pane\n .split('\\n')\n .map((l) => l.trimEnd())\n .filter((l) => l.length > 0 && !/^Pane is dead\\b/.test(l));\n let output = lines.slice(-15).join('\\n');\n if (output.length > 800) output = `…${output.slice(-800)}`;\n const status = /^\\d+$/.test(exitStatus.trim()) ? `exit status ${exitStatus.trim()}` : 'exit status unknown';\n return (\n `Claude Code exited on the host (${status}) before login finished. ` +\n (output ? `Its last output:\\n${output}\\n` : 'It printed nothing. ') +\n 'Run `claude` on the host as the manager\\'s user to reproduce, then Start over.'\n );\n}\n\n/**\n * ENG-10617: null while claude is still running in the pair pane; a\n * `claude-exited` error (with its last output) once it has exited; or the\n * classified tmux error if the session itself is gone.\n */\nasync function checkPairPaneAlive(session: string): Promise<SessionError | null> {\n const read = async (): Promise<{ dead: string; exitStatus: string }> => {\n const { stdout } = await execFileAsync('tmux', [\n 'display-message',\n '-p',\n '-t',\n session,\n '#{pane_dead} #{pane_dead_status}',\n ]);\n const [dead = '', exitStatus = ''] = stdout.trim().split(' ');\n return { dead, exitStatus };\n };\n let state: { dead: string; exitStatus: string };\n try {\n state = await read();\n } catch (err) {\n return classifyTmuxError(err);\n }\n if (state.dead !== '1') return null;\n // tmux marks a pane dead when the pty hits EOF, and only fills in\n // pane_dead_status once it has reaped the child — a separate event, so a\n // read in between sees the pane dead with an EMPTY status. Give the reap a\n // moment rather than reporting \"exit status unknown\" for a clean exit.\n for (let i = 0; i < 10 && !/^\\d+$/.test(state.exitStatus); i++) {\n await sleep(100);\n try { state = await read(); } catch { break; }\n }\n const { exitStatus } = state;\n let pane = '';\n try { pane = await capturePane(session, { scrollback: -50 }); } catch { /* best-effort */ }\n return { kind: 'claude-exited', message: describeClaudeExit(exitStatus, pane) };\n}\n\n// ---------------------------------------------------------------------------\n// Pair-scoped tmux session lifecycle\n// ---------------------------------------------------------------------------\n\n/**\n * Spawn a throwaway tmux session running `claude` for the pairing flow.\n * Idempotent — if the session already exists, returns success silently.\n * Caller is responsible for `killPairSession` once the pair reaches a\n * terminal state.\n *\n * Uses an absolute path to the claude binary so we don't depend on the\n * inherited PATH (the manager runs with cloud-init's minimal env on\n * EC2). resolveClaudeBinary checks CLAUDE_PATH, then `which`, then\n * canonical Linux/macOS Homebrew install dirs.\n */\nexport async function spawnPairSession(session: string): Promise<{ ok: true } | { ok: false; error: SessionError }> {\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n return { ok: true };\n } catch {\n // session doesn't exist yet — fall through to create it\n }\n\n const { resolveClaudeBinary } = await import('./persistent-session.js');\n const claudeBin = resolveClaudeBinary();\n\n // ENG-5070: backfill HOME/USER before spawning the pair tmux.\n // persistent-session.ts already does this for agent sessions\n // (ENG-4632) but the pair flow re-used execFileAsync's inherited\n // env, so the same SSM-launched-manager-strips-HOME trap fired the\n // moment an operator clicked \"Login via browser\" on a fresh host:\n // tmux inherits HOME='', the new claude process can't read or\n // write ~/.claude/, claude exits within milliseconds, and the\n // pairing UI surfaces the generic \"claude exited immediately\"\n // message. Treat empty-string as missing too — HOME=\"\" makes ~\n // resolve to cwd (whatever happens to be the manager's working\n // dir), which is the same broken outcome as no HOME, just better\n // hidden.\n const pairEnv: NodeJS.ProcessEnv = {\n ...process.env,\n HOME: (process.env.HOME?.trim()) || homedir(),\n USER: (process.env.USER?.trim()) || userInfo().username,\n };\n\n try {\n // -x 240 -y 50 — wide enough that Claude Code's OAuth URL (~350\n // chars after PKCE expansion) doesn't soft-wrap. The dewrap pass in\n // extractOAuthUrl handles any leftover wrapping, but giving the URL\n // a single line in the first place is more robust against future\n // claude UI changes.\n // ENG-10617: `remain-on-exit on` is chained into the SAME tmux\n // invocation (`;` is tmux's command separator) so it is applied before\n // the server can reap an early-exiting claude. Without it, claude\n // exiting destroys the session and its last output with it, and every\n // later tmux call fails with a bare `can't find pane`.\n await execFileAsync(\n 'tmux',\n [\n 'new-session',\n '-d',\n '-x',\n '240',\n '-y',\n '50',\n '-s',\n session,\n claudeBin,\n ';',\n 'set-option',\n '-w',\n '-t',\n session,\n 'remain-on-exit',\n 'on',\n ],\n { env: pairEnv },\n );\n } catch (err) {\n return { ok: false, error: classifyTmuxError(err) };\n }\n\n // `tmux new-session` returns 0 even if the command exited immediately\n // (binary missing, claude crashed, etc.) — the session is created and\n // then torn down. Without this verification we'd propagate a generic\n // `session_missing` to the operator on the next has-session call,\n // hiding the real failure. Sleep briefly to give claude a beat to\n // crash-or-stay-alive, then check.\n await sleep(500);\n const dead = await checkPairPaneAlive(session);\n if (dead?.kind === 'claude-exited') return { ok: false, error: dead };\n if (dead) {\n return {\n ok: false,\n error: {\n kind: 'unknown',\n message: `claude exited immediately after launch (binary at ${claudeBin}). Run \\`${claudeBin}\\` manually on the host to see why — likely missing TTY, missing HOME, or a startup error.`,\n },\n };\n }\n return { ok: true };\n}\n\n/**\n * ENG-4633: drive Claude Code through its post-OAuth dialogs so it\n * persists `~/.claude.json` (the device-state + onboarding-complete\n * file) before we tear down the pair tmux session.\n *\n * Without this step, the pair flow leaves the host with only\n * `~/.claude/.credentials.json` written. On the next agent launch,\n * Claude Code interactive mode sees no `~/.claude.json` and falls\n * back to the login picker — even though the OAuth tokens are\n * sitting right there. This was the root cause of the 2026-05-01\n * prod scout outage.\n *\n * After successful code submission, claude shows in sequence:\n * 1. \"Logged in as <email> / Login successful. Press Enter to continue…\"\n * 2. \"Security notes / Press Enter to continue…\"\n * 3. (depending on cwd) trust-folder prompt and/or bypass-permissions\n * warning. The pair tmux session has neither a project dir nor\n * `--dangerously-skip-permissions`, so we usually only see the\n * first two.\n *\n * For each iteration: capture the pane, match a known prompt, send\n * Enter, repeat. Bail when `~/.claude.json`'s mtime advances past\n * the snapshot we took at entry — proxy for \"claude has rewritten\n * the file with post-login state\". The actual contents (e.g.\n * `hasCompletedOnboarding: true`) are not parsed here; we trust\n * claude to write a coherent file once it gets the chance, and\n * mtime-only is enough to gate the success log line. Returns\n * `finalized` so the caller can warn if onboarding never flushed —\n * the pair is still considered successful (OAuth tokens are valid\n * regardless), the worst-case fallout is a one-time login picker\n * on the next agent launch.\n */\nexport async function finalizeClaudePairOnboarding(\n session: string,\n log: (msg: string) => void,\n opts: { maxIterations?: number; intervalMs?: number; claudeJsonPath?: string } = {},\n): Promise<{ finalized: boolean; iterations: number }> {\n const maxIterations = opts.maxIterations ?? 10;\n const intervalMs = opts.intervalMs ?? 1500;\n const claudeJsonPath = opts.claudeJsonPath ?? join(homedir(), '.claude.json');\n\n // Snapshot the file's existence + mtime so we can detect the moment\n // claude actually writes it. We can't simply check \"does the file\n // exist\" because a stale file from a prior pair attempt would\n // satisfy that on the first iteration.\n const initialMtime = existsSync(claudeJsonPath)\n ? statSync(claudeJsonPath).mtimeMs\n : 0;\n\n for (let i = 0; i < maxIterations; i++) {\n await sleep(intervalMs);\n const dead = await checkPairPaneAlive(session);\n if (dead) {\n log(`[claude-pair] finalize: pair session no longer running (${dead.kind}); aborting onboarding flow`);\n return { finalized: false, iterations: i };\n }\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n // Session likely dead. Reading it back as failure here is fine —\n // the upstream caller already considered the pair successful, so\n // we report finalized=false and let it decide whether to fail\n // soft.\n const classified = classifyTmuxError(err);\n log(`[claude-pair] finalize: capture-pane failed (${classified.kind}); aborting onboarding flow`);\n return { finalized: false, iterations: i };\n }\n\n // ENG-10619: must run before the generic Enter branch below — a bare\n // Enter on this dialog confirms its default row, which opts the\n // account into model training.\n try {\n if (await answerConsumerTermsDialog(session, pane)) continue;\n } catch {\n log('[claude-pair] finalize: send-keys failed on consumer-terms dialog; aborting onboarding flow');\n return { finalized: false, iterations: i };\n }\n\n // Pattern-match the post-OAuth dialogs claude shows in succession.\n // Keep these matchers loose — claude's wording shifts between\n // versions, and a missed match just means we send Enter on the\n // generic \"Press Enter to continue\" branch below.\n if (\n pane.includes('Login successful') ||\n pane.includes('Logged in as') ||\n /Press Enter to continue/i.test(pane) ||\n pane.includes('Security notes')\n ) {\n try {\n await sendKeys(session, 'C-m');\n } catch {\n // sendKeys failure → session dead. Same fallthrough as above.\n log('[claude-pair] finalize: send-keys failed; aborting onboarding flow');\n return { finalized: false, iterations: i };\n }\n continue;\n }\n\n // Has claude rewritten ~/.claude.json since we entered this\n // function? mtime advance is our proxy for \"post-login state has\n // been flushed to disk\" — we don't crack the JSON open to verify\n // hasCompletedOnboarding because claude's exact write timing is\n // version-dependent and we'd be guessing about which key to look\n // at. Mtime-only is good enough for the smoke contract.\n if (existsSync(claudeJsonPath)) {\n const mtime = statSync(claudeJsonPath).mtimeMs;\n if (mtime > initialMtime) {\n log(`[claude-pair] finalize: ~/.claude.json updated (after ${i + 1} dialog dismissal(s))`);\n return { finalized: true, iterations: i + 1 };\n }\n }\n }\n\n log(`[claude-pair] finalize: reached ${maxIterations} iterations without ~/.claude.json being updated`);\n return { finalized: false, iterations: maxIterations };\n}\n\n/**\n * Returns true if the session is gone afterwards (either kill succeeded\n * or it was already missing). Returns false only if `kill-session`\n * failed for a non-missing-session reason — caller should treat this\n * as \"still tracked, will retry next poll\".\n */\nexport async function killPairSession(session: string): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['kill-session', '-t', session]);\n return true;\n } catch (err) {\n // \"can't find session\" is success-equivalent — the desired end state\n // is \"this session does not exist\" and that's already true.\n if (classifyTmuxError(err).kind === 'no-session') return true;\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Result shapes\n// ---------------------------------------------------------------------------\n\nexport type ClaudePairStartResult =\n | { kind: 'url'; url: string }\n | { kind: 'timeout' }\n | { kind: 'error'; error: SessionError };\n\nexport type ClaudePairSubmitResult =\n | { kind: 'success'; rawMatch: string }\n | { kind: 'failure'; rawMatch: string }\n | { kind: 'timeout' }\n | { kind: 'error'; error: SessionError };\n\nexport type ClaudePairStatusResult =\n | { kind: 'idle' }\n | { kind: 'awaiting-code'; url: string }\n | { kind: 'success' }\n | { kind: 'failure'; rawMatch: string }\n | { kind: 'session-missing' }\n | { kind: 'error'; error: SessionError };\n\n/**\n * Detect Claude Code's workspace-trust dialog in a captured pane.\n *\n * ENG-5375: Claude Code 2.1.146 reworded the prompt from \"Do you trust\n * the files in this folder?\" to \"Quick safety check: Is this a project\n * you created or one you trust?\". Match on the option text instead of\n * the question — same shape as `acceptDialogs` in persistent-session.ts,\n * stable across Anthropic's question rewordings.\n *\n * Exported for unit testing.\n */\nexport function isTrustDialogVisible(pane: string): boolean {\n return /Yes,\\s+I\\s+trust\\s+this\\s+folder/i.test(pane);\n}\n\n/**\n * Detect Claude Code's regular interactive prompt in a captured pane.\n *\n * ENG-5377: Claude Code 2.1.146 replaced the older boxed `│ > ` cursor\n * with `❯ ` (U+276F) and the tip text is variable (\"Try \\\"fix lint\n * errors\\\"\" today — no \" to \"). Detect on the stable 2.1.146 markers:\n * the `❯` cursor and/or the `? for shortcuts` footer. Keep the legacy\n * shapes for back-compat with hosts that haven't picked up the newer\n * Claude Code binary yet.\n *\n * IMPORTANT: callers must check the dialog detectors (theme, trust,\n * login picker) BEFORE this, since `❯` also appears on those screens\n * as the selected-option indicator.\n *\n * Exported for unit testing.\n */\nexport function isInteractivePromptReady(pane: string): boolean {\n return (\n /❯/.test(pane) ||\n /\\?\\s+for\\s+shortcuts/i.test(pane) ||\n /[│|]\\s*>\\s/.test(pane) ||\n /^\\s*>\\s*$/m.test(pane)\n );\n}\n\n// ---------------------------------------------------------------------------\n// start — send `/login`, wait for the OAuth URL prompt\n// ---------------------------------------------------------------------------\n\nexport interface ClaudePairStartOpts {\n /** Pair-scoped tmux session name (see pairTmuxSession). */\n session: string;\n /** Total time to wait for Claude Code to print the URL prompt. Default 60s. */\n timeoutMs?: number;\n /** How often to re-capture and check the pane. Default 500ms. */\n pollIntervalMs?: number;\n}\n\nexport async function startClaudePair(opts: ClaudePairStartOpts): Promise<ClaudePairStartResult> {\n const { session } = opts;\n // Pair sessions cold-start `claude` from scratch — first-run on a fresh\n // host can take 10-30s before the prompt is interactive enough to\n // accept `/login`. Stay generous.\n const timeoutMs = opts.timeoutMs ?? 60_000;\n const pollIntervalMs = opts.pollIntervalMs ?? 500;\n\n // Quick precheck — fail fast if the tmux session doesn't exist\n // rather than blasting `/login` into an unrelated pane.\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Drive claude through its first-run onboarding to reach a state\n // where the OAuth URL is on screen. Possible paths:\n //\n // • Fresh host: theme picker → \"Trust folder?\" → login method picker\n // (option 1 = Claude subscription, already highlighted) → OAuth URL.\n // No `/login` needed — option 1 IS the login.\n // • Already onboarded: regular prompt → we send `/login` ourselves →\n // login method picker → OAuth URL.\n //\n // Auto-advance every onboarding screen we recognize. Keep going until\n // we either see the URL prompt or hit the deadline.\n const onboardingDeadline = Date.now() + Math.min(45_000, timeoutMs);\n let lastDispatchAt = 0;\n let loginCommandSent = false;\n const dispatchEnter = async (): Promise<void> => {\n if (Date.now() - lastDispatchAt < 1_500) return;\n lastDispatchAt = Date.now();\n try { await sendKeys(session, 'C-m'); } catch { /* keep polling */ }\n };\n\n while (Date.now() < onboardingDeadline) {\n await sleep(pollIntervalMs);\n const dead = await checkPairPaneAlive(session);\n if (dead) return { kind: 'error', error: dead };\n let pane: string;\n try {\n // VIEWPORT ONLY (no scrollback). With scrollback included, the\n // theme picker / login picker text lingers in the buffer after\n // claude advances to the URL-paste prompt. The match would then\n // dispatch Enter on the empty paste field — submitting an empty\n // code and triggering \"OAuth error: Invalid code\". Visible-only\n // capture ensures we only react to what's actually on screen.\n pane = await capturePane(session, { scrollback: 0 });\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Already at the URL prompt — done driving onboarding, fall through\n // to the URL-extraction loop below.\n if (isUrlPromptReady(pane)) {\n const url = extractOAuthUrl(pane);\n if (url) return { kind: 'url', url };\n }\n\n // Stuck on \"OAuth error: Invalid code. Press Enter to retry\" — usually\n // because claude has cached partial state from a previous failed login\n // attempt on this host. Pressing Enter would just resubmit the bad\n // code and loop forever, so bail with an actionable error.\n //\n // Capture WITH scrollback for this check: claude's TUI sometimes\n // re-renders the splash/banner over the OAuth error so the viewport\n // alone misses it (we'd then time out instead of giving the operator\n // an actionable error). OAuth-retry is a sticky state — claude waits\n // for input — so checking recent scrollback is safe; a false positive\n // from stale scrollback would only fire if a previous attempt actually\n // failed, in which case bailing is the right call.\n let scrollPane = pane;\n try {\n scrollPane = await capturePane(session, { scrollback: -50 });\n } catch { /* fall back to viewport-only check */ }\n const hasOAuthInvalidCode = /OAuth error[\\s\\S]*Invalid code/i.test(scrollPane);\n const hasOAuthRetryPrompt = /OAuth error/i.test(scrollPane) && /Press Enter to retry/i.test(scrollPane);\n if (hasOAuthInvalidCode || hasOAuthRetryPrompt) {\n return {\n kind: 'error',\n error: {\n kind: 'oauth-retry-stuck',\n message:\n 'claude is stuck on a previous failed-login retry prompt. SSH to the host and clear ~/.claude/.credentials.json (and any *.json next to it), then retry pair-via-browser.',\n },\n };\n }\n\n // ENG-10619: the consumer-terms dialog renders a `❯` cursor, so without\n // this the interactive-prompt branch below types `/login` into it.\n // Throttled like dispatchEnter: a relative arrow sequence re-sent before\n // claude re-renders would overshoot the row.\n if (consumerTermsOffKeys(pane)) {\n if (Date.now() - lastDispatchAt >= 1_500) {\n lastDispatchAt = Date.now();\n try {\n await answerConsumerTermsDialog(session, pane);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n }\n continue;\n }\n\n // Login method picker — option 1 (Claude subscription) is already\n // highlighted, Enter selects it.\n if (/Select login method:/i.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // First-run theme picker. Accept the highlighted default; operator\n // can change it later via /theme.\n if (/\\bDark mode\\b/.test(pane) && /\\bLight mode\\b/.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // \"Trust this folder?\" — select \"Yes, I trust this folder\" BY NAME.\n // ENG-10619: this used to be a bare Enter on the assumption the default\n // row is Yes. Since Claude Code 2.1.250 the dialog is an arrow-select\n // list whose cursor defaults to \"No, exit\", so Enter refused trust and\n // claude exited 1 — every re-login on a host whose workspace was not yet\n // trusted failed (surfaced as \"can't find pane\" before ENG-10617, and as\n // the trust dialog in the exit output after). Same defect sweepDialogs()\n // fixed for agents in ENG-9575. An unresolvable row sends nothing.\n if (isTrustDialogVisible(pane)) {\n const keys = selectRowKeys(pane, 'Yes, I trust this folder');\n if (keys && Date.now() - lastDispatchAt >= 1_500) {\n lastDispatchAt = Date.now();\n try {\n await sendKeySequence(session, keys);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n }\n continue;\n }\n // Onboarding \"Press Enter to continue\" splashes.\n if (/press\\s+enter\\s+to\\s+continue/i.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // Regular interactive prompt — this means claude was already\n // onboarded. Send `/login` once to surface the login picker, then\n // the next iteration handles it via the picker branch above.\n // ENG-5377: detect both the 2.1.146 `❯` cursor + `? for shortcuts`\n // footer AND the legacy boxed/bare prompt shapes. This branch must\n // come AFTER the dialog detectors above, since `❯` ALSO renders on\n // theme/trust/login dialogs.\n if (isInteractivePromptReady(pane) && !loginCommandSent) {\n loginCommandSent = true;\n try {\n await sendKeys(session, '/login', 'C-m');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n continue;\n }\n }\n\n // Onboarding deadline hit without seeing a URL — surface the pane so\n // operators can see what was on screen when we gave up.\n let lastPane = '';\n try { lastPane = await capturePane(session); } catch { /* best-effort */ }\n return {\n kind: 'error',\n error: {\n kind: 'unknown',\n message: `claude never reached OAuth URL prompt within ${timeoutMs}ms. Last pane: ${lastPane.slice(-500)}`,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// submit-code — paste the auth code, wait for outcome\n// ---------------------------------------------------------------------------\n\nexport interface ClaudePairSubmitOpts {\n /** Pair-scoped tmux session name (see pairTmuxSession). */\n session: string;\n code: string;\n /** Total time to wait for the success/failure marker. Default 30s. */\n timeoutMs?: number;\n pollIntervalMs?: number;\n}\n\nexport async function submitClaudePairCode(\n opts: ClaudePairSubmitOpts,\n): Promise<ClaudePairSubmitResult> {\n const { session } = opts;\n const timeoutMs = opts.timeoutMs ?? 30_000;\n const pollIntervalMs = opts.pollIntervalMs ?? 500;\n\n // Validate code shape minimally — Claude Code's auth codes are\n // alphanumeric with dashes, ~40-80 chars. Accept anything within\n // that envelope; reject blank or whitespace-only to avoid\n // accidentally submitting an empty buffer.\n if (!opts.code || !opts.code.trim()) {\n return {\n kind: 'error',\n error: { kind: 'unknown', message: 'empty auth code' },\n };\n }\n if (opts.code.length > 1024) {\n return {\n kind: 'error',\n error: { kind: 'unknown', message: 'auth code suspiciously long' },\n };\n }\n\n // Send the code + Enter. We use the literal value as one send-keys\n // argument; tmux handles spaces fine, but newlines would terminate\n // early so reject those as well.\n if (/[\\r\\n]/.test(opts.code)) {\n return {\n kind: 'error',\n error: { kind: 'unknown', message: 'auth code contains newline' },\n };\n }\n\n // Send the code as LITERAL text (-l) so tmux doesn't try to interpret\n // any chars as key tokens. Then a 250ms breather so claude's\n // ink/React render loop finishes processing the paste before Enter\n // lands. Without that wait the Enter often gets swallowed mid-render\n // and the code stays in the input box unsubmitted.\n try {\n await execFileAsync('tmux', ['send-keys', '-t', session, '-l', opts.code.trim()]);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n await sleep(250);\n try {\n // Use Enter (semantic) plus a fallback C-m on the next iteration if\n // claude still hasn't moved.\n await sendKeys(session, 'Enter');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n const deadline = Date.now() + timeoutMs;\n let enterRetried = false;\n let lastPane = '';\n while (Date.now() < deadline) {\n await sleep(pollIntervalMs);\n try {\n lastPane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n const outcome: AuthOutcome = detectAuthOutcome(lastPane);\n if (outcome.kind === 'success') return { kind: 'success', rawMatch: outcome.rawMatch };\n if (outcome.kind === 'failure') return { kind: 'failure', rawMatch: outcome.rawMatch };\n // ENG-10617: checked after the outcome so a success/failure marker\n // on claude's final screen still wins over \"it exited\".\n const dead = await checkPairPaneAlive(session);\n if (dead) return { kind: 'error', error: dead };\n\n // After ~5s with no outcome, retry Enter once — covers the case\n // where the first Enter landed mid-render and claude swallowed it.\n // The \"Paste code here\" prompt is still on screen if submission\n // didn't take; if it advanced, we'd already have an outcome above.\n if (!enterRetried && Date.now() - (deadline - timeoutMs) > 5_000 && /Paste code here/i.test(lastPane)) {\n enterRetried = true;\n try { await sendKeys(session, 'C-m'); } catch { /* keep polling */ }\n }\n }\n // Include pane snippet in the error path so operators can see what\n // claude was actually showing — outcome detection is brittle and the\n // session is killed immediately after this returns, so this is our\n // only chance to capture state for debugging regex updates.\n return {\n kind: 'error',\n error: {\n kind: 'unknown',\n message: `submit timed out after ${timeoutMs}ms — outcome regex didn't match. Last pane: ${lastPane.slice(-600)}`,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// status — non-mutating peek at the pane state\n// ---------------------------------------------------------------------------\n\nexport async function getClaudePairStatus(session: string): Promise<ClaudePairStatusResult> {\n const dead = await checkPairPaneAlive(session);\n if (dead?.kind === 'no-session') return { kind: 'session-missing' };\n if (dead) return { kind: 'error', error: dead };\n\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Outcome takes priority — if the pane already shows success/failure\n // from a recent submission, the API can short-circuit without\n // restarting the flow.\n const outcome = detectAuthOutcome(pane);\n if (outcome.kind === 'success') return { kind: 'success' };\n if (outcome.kind === 'failure') return { kind: 'failure', rawMatch: outcome.rawMatch };\n\n if (isUrlPromptReady(pane)) {\n const url = extractOAuthUrl(pane);\n if (url) return { kind: 'awaiting-code', url };\n }\n return { kind: 'idle' };\n}\n","/**\n * ENG-4580: pane-scrape parser for Claude Code's `/login` OAuth flow.\n *\n * The manager drives the flow by sending `/login` into the agent's\n * persistent tmux session, capturing the pane after a short poll, and\n * extracting the OAuth URL Claude Code prints. After the operator\n * pastes the auth code via the UI, the manager sends it back into the\n * pane and polls for a success / failure marker.\n *\n * Everything in this module is pure — no tmux calls, no fs I/O. The\n * runtime side (apps/cli/src/lib/manager-worker.ts) shells out and\n * feeds the captured pane string through these functions.\n *\n * Why a dedicated module: pane scraping is fragile across Claude Code\n * versions, terminal widths, and locale changes. Centralising the\n * regexes + the ANSI stripper makes them easy to fixture-test and\n * iterate on without touching the runtime path.\n */\n\n// ---------------------------------------------------------------------------\n// ANSI escape sequence stripper\n// ---------------------------------------------------------------------------\n\n/**\n * Strip the ANSI escape sequences a terminal emits for colour, cursor\n * movement, screen clears, and bracketed paste mode. The pattern below\n * covers:\n *\n * - CSI sequences: `ESC [ ... <final byte>` where the final byte is\n * in the 0x40-0x7E range (covers SGR colour, cursor-position,\n * erase-in-line/display, etc.)\n * - OSC sequences: `ESC ] ... BEL` or `ESC ] ... ESC \\` (used for\n * window titles and hyperlinks)\n * - Single-character `ESC <char>` two-byte escapes (e.g. `ESC =`,\n * `ESC >`, the `ESC c` reset)\n *\n * We keep newlines and printable text intact so pane content remains\n * matchable after stripping.\n *\n * The regex uses Unicode-friendly character classes; we explicitly\n * avoid `\\x1b` named escapes in source to keep the file ASCII-safe.\n */\nconst ANSI_ESC = String.fromCharCode(0x1b);\nconst ANSI_BEL = String.fromCharCode(0x07);\n\nconst CSI_RE = new RegExp(`${ANSI_ESC}\\\\[[0-?]*[ -/]*[@-~]`, 'g');\nconst OSC_RE = new RegExp(\n `${ANSI_ESC}\\\\][^${ANSI_BEL}${ANSI_ESC}]*(?:${ANSI_BEL}|${ANSI_ESC}\\\\\\\\)`,\n 'g',\n);\nconst TWO_BYTE_RE = new RegExp(`${ANSI_ESC}[=>cM78]`, 'g');\n\nexport function stripAnsi(text: string): string {\n return text.replace(CSI_RE, '').replace(OSC_RE, '').replace(TWO_BYTE_RE, '');\n}\n\n// ---------------------------------------------------------------------------\n// OAuth URL extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Anchored to Anthropic-owned domains that Claude Code's `/login`\n * actually prints. Adding more hosts is fine — keep them allowlisted\n * rather than matching arbitrary `https://` to avoid pulling random\n * URLs from the user's previous shell output.\n *\n * Note: Claude Code currently emits URLs at `claude.com/cai/oauth/...`\n * (the consumer-facing domain), but `claude.ai` and the console hosts\n * have appeared historically and are kept here for tolerance.\n */\nconst OAUTH_URL_RE =\n /https:\\/\\/(?:claude\\.com|claude\\.ai|platform\\.claude\\.com|console\\.anthropic\\.com|auth\\.anthropic\\.com)\\/[^\\s)\\]]*/;\n\n/**\n * Strip ANSI + reassemble URLs that were soft-wrapped across terminal\n * lines. tmux capture-pane emits hard newlines for wrapped lines, and\n * Claude Code's OAuth URL routinely runs >300 chars — much wider than\n * the manager's default tmux window. Iteratively strip newlines that\n * fall inside what looks like a URL until convergence (a single URL\n * can wrap 5+ times).\n *\n * Shared between extractOAuthUrl and isUrlPromptReady so the readiness\n * check sees the same string the extractor would.\n */\nfunction dewrapPane(rawPane: string): string {\n const stripped = stripAnsi(rawPane);\n let dewrapped = stripped;\n let prev = '';\n while (prev !== dewrapped) {\n prev = dewrapped;\n dewrapped = dewrapped.replace(/(https?:\\/\\/\\S+?)\\n(?=\\S)/, (_m, head: string) => head);\n }\n return dewrapped;\n}\n\nexport function extractOAuthUrl(rawPane: string): string | null {\n const match = OAUTH_URL_RE.exec(dewrapPane(rawPane));\n if (!match) return null;\n // Trim trailing punctuation that often clings to URLs in TUIs.\n return match[0].replace(/[.,;:!?]+$/, '');\n}\n\n// ---------------------------------------------------------------------------\n// Prompt readiness — \"we've printed the URL, now waiting for a code\"\n// ---------------------------------------------------------------------------\n\nconst URL_PROMPT_RE =\n /(?:Paste code here|Paste your code|Enter (?:the )?code|Authorization code)/i;\n\nexport function isUrlPromptReady(rawPane: string): boolean {\n const dewrapped = dewrapPane(rawPane);\n // Both anchors must be present: the URL itself AND the paste-code\n // prompt. The prompt alone could appear during a stale screen redraw;\n // the URL alone could be a stray match in command history. Use the\n // dewrapped pane so a wrapped URL still matches OAUTH_URL_RE.\n return OAUTH_URL_RE.test(dewrapped) && URL_PROMPT_RE.test(dewrapped);\n}\n\n// ---------------------------------------------------------------------------\n// Outcome detection after submitting the code\n// ---------------------------------------------------------------------------\n\n// Claude Code's success/failure copy has drifted across versions\n// (\"Logged in\" → \"Login successful\" → \"Signed in as ...\" → etc.).\n// Keep the alternations broad-but-specific — phrases that only appear\n// after a real auth roundtrip, never in welcome / tutorial text.\n// \"Welcome back\" was tried and rejected: it shows up in onboarding\n// help blurbs and produced false positives.\nconst SUCCESS_RE =\n /(?:Logged in|Login successful|Successfully (?:logged in|authenticated|signed in)|Authentication successful|Sign-?in (?:complete|successful)|You(?:'|’)?re signed in|Signed in as)/i;\nconst FAILURE_RE =\n /(?:Invalid (?:code|authorization code)|OAuth error|Authentication failed|Error (?:logging in|during authentication)|Login failed|Sign-?in failed|Failed to (?:authenticate|sign in|log in))/i;\n\nexport type AuthOutcome =\n | { kind: 'success'; rawMatch: string }\n | { kind: 'failure'; rawMatch: string }\n | { kind: 'pending' };\n\nexport function detectAuthOutcome(rawPane: string): AuthOutcome {\n const stripped = stripAnsi(rawPane);\n // Failure first — Claude Code sometimes prints a stale \"logged in\" from\n // a previous successful session above the new failure banner. The\n // most-recent line wins, so we scan from the end of the pane.\n const failureMatch = lastMatch(stripped, FAILURE_RE);\n const successMatch = lastMatch(stripped, SUCCESS_RE);\n\n if (failureMatch && successMatch) {\n // Whichever is later on the pane is the live state.\n if (failureMatch.index > successMatch.index) {\n return { kind: 'failure', rawMatch: failureMatch.match };\n }\n return { kind: 'success', rawMatch: successMatch.match };\n }\n if (failureMatch) return { kind: 'failure', rawMatch: failureMatch.match };\n if (successMatch) return { kind: 'success', rawMatch: successMatch.match };\n return { kind: 'pending' };\n}\n\nfunction lastMatch(haystack: string, re: RegExp): { match: string; index: number } | null {\n // Construct a sticky/global variant if needed. Most of our REs are\n // anchored to small phrases; iterating with a `g`-flagged RegExp is\n // cheap and correct.\n const globalRe = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);\n let last: RegExpExecArray | null = null;\n let m: RegExpExecArray | null;\n while ((m = globalRe.exec(haystack)) !== null) {\n last = m;\n // Prevent zero-length matches from looping.\n if (m.index === globalRe.lastIndex) globalRe.lastIndex++;\n }\n return last ? { match: last[0], index: last.index } : null;\n}\n\n// ---------------------------------------------------------------------------\n// \"Session not running / tmux missing\" — surface as a structured signal\n// ---------------------------------------------------------------------------\n\n/**\n * The runtime path will throw when `tmux capture-pane` fails. This\n * helper classifies the failure for the API layer so the UI can show\n * \"start a session first\" rather than a generic 500.\n */\nexport type SessionError =\n | { kind: 'no-session' }\n | { kind: 'tmux-missing' }\n | { kind: 'pane-empty' }\n /** Claude is sitting on the \"OAuth error: Invalid code. Press Enter to\n * retry.\" prompt from a previous failed login — pressing Enter would\n * resubmit the bad code, so the runtime bails with operator-actionable\n * guidance instead. Distinct from `unknown` so telemetry / UI can\n * surface a specific message rather than a generic 500. */\n | { kind: 'oauth-retry-stuck'; message: string }\n /** ENG-10617: the pair `claude` process exited mid-flow. The pair\n * session runs with `remain-on-exit on`, so the dead pane survives and\n * `message` carries claude's exit status and last output — the only\n * record of WHY it exited. */\n | { kind: 'claude-exited'; message: string }\n | { kind: 'unknown'; message: string };\n\nexport function classifyTmuxError(err: unknown): SessionError {\n const msg = err instanceof Error ? err.message : String(err);\n // ENG-10617: tmux 3.x phrases a `-t` miss by the target type it resolved\n // — `can't find pane` from capture-pane/send-keys, `can't find window` from\n // window-scoped commands — not only `can't find session`. Unmatched, those\n // fell to `unknown` and the raw command line reached the operator.\n if (/can't find (session|pane|window)|no server running/i.test(msg)) return { kind: 'no-session' };\n if (/command not found.*tmux|ENOENT.*tmux/i.test(msg)) return { kind: 'tmux-missing' };\n return { kind: 'unknown', message: msg };\n}\n"],"mappings":"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","names":["exitStatus"]}
@@ -63,7 +63,7 @@ import {
63
63
  safeWriteJsonAtomic,
64
64
  setConfigHash,
65
65
  tripClass
66
- } from "../chunk-372DZ2RQ.js";
66
+ } from "../chunk-MTRJTGJL.js";
67
67
  import {
68
68
  getProjectDir as getProjectDir2,
69
69
  getReadyTasks,
@@ -136,7 +136,7 @@ import {
136
136
  takeZombieDetection,
137
137
  toOpencodeModel,
138
138
  writeEgressAllowlist
139
- } from "../chunk-XN5MOKSV.js";
139
+ } from "../chunk-A3T3WOC2.js";
140
140
  import {
141
141
  ACCOUNT_ENFORCEMENT_MARKER_FILENAME,
142
142
  AnchorSessionClient,
@@ -232,7 +232,8 @@ import {
232
232
  subagentActivityAgeSeconds,
233
233
  sumTranscriptUsageInWindow,
234
234
  transcriptActivityAgeSeconds
235
- } from "../chunk-YZ5HGHPG.js";
235
+ } from "../chunk-JANAAFED.js";
236
+ import "../chunk-DHWNVVX4.js";
236
237
  import {
237
238
  reapOrphanChannelMcps
238
239
  } from "../chunk-XWVM4KPK.js";
@@ -13257,7 +13258,7 @@ var pendingDayRolloverReset = /* @__PURE__ */ new Set();
13257
13258
  var dayRolloverInboundHold = /* @__PURE__ */ new Map();
13258
13259
  var INBOUND_HOLD_EPISODE_GAP_MS = 12e4;
13259
13260
  async function channelInboundActivityAgeSecondsFor(codeName) {
13260
- const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-CHGET573.js");
13261
+ const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-5LUHQCKN.js");
13261
13262
  const newest = newestPendingInboundActivityMtimeMs(dirname11(paneLogPath(codeName)));
13262
13263
  if (newest === null) return null;
13263
13264
  return Math.max(0, Math.floor((Date.now() - newest) / 1e3));
@@ -13942,7 +13943,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
13942
13943
  var lastVersionCheckAt = 0;
13943
13944
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
13944
13945
  var lastResponsivenessProbeAt = 0;
13945
- var agtCliVersion = true ? "0.28.947" : "dev";
13946
+ var agtCliVersion = true ? "0.28.949" : "dev";
13946
13947
  function resolveBrewPath(execFileSync2) {
13947
13948
  try {
13948
13949
  const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -14272,6 +14273,7 @@ function claudeManagedSettingsPath() {
14272
14273
  return process.platform === "darwin" ? "/Library/Application Support/ClaudeCode/managed-settings.json" : "/etc/claude-code/managed-settings.json";
14273
14274
  }
14274
14275
  var MANAGED_SETTINGS_KEYS = ["channelsEnabled", "enableAllProjectMcpServers"];
14276
+ var MANAGED_SETTINGS_ENV = { DISABLE_AUTOUPDATER: "1" };
14275
14277
  function ensureClaudeManagedSettings(path = claudeManagedSettingsPath()) {
14276
14278
  try {
14277
14279
  let settings = {};
@@ -14291,12 +14293,23 @@ function ensureClaudeManagedSettings(path = claudeManagedSettingsPath()) {
14291
14293
  }
14292
14294
  }
14293
14295
  const missing = MANAGED_SETTINGS_KEYS.filter((key) => settings[key] !== true);
14294
- if (missing.length === 0) return "ok";
14296
+ const existingEnv = settings["env"];
14297
+ const env = existingEnv && typeof existingEnv === "object" && !Array.isArray(existingEnv) ? existingEnv : {};
14298
+ const missingEnv = Object.entries(MANAGED_SETTINGS_ENV).filter(([k, v]) => env[k] !== v);
14299
+ if (missing.length === 0 && missingEnv.length === 0) return "ok";
14295
14300
  for (const key of missing) settings[key] = true;
14301
+ if (missingEnv.length > 0) {
14302
+ for (const [k, v] of missingEnv) env[k] = v;
14303
+ settings["env"] = env;
14304
+ }
14296
14305
  mkdirSync15(dirname11(path), { recursive: true });
14297
14306
  writeFileSync18(path, `${JSON.stringify(settings, null, 2)}
14298
14307
  `);
14299
- log(`[managed-settings] set ${missing.map((k) => `${k}:true`).join(", ")} in ${path} (ENG-5786 unblocks Claude Code channels; ENG-9223 pre-approves project MCP servers so a headless session never freezes on the trust prompt)`);
14308
+ const changed = [
14309
+ ...missing.map((k) => `${k}:true`),
14310
+ ...missingEnv.map(([k, v]) => `env.${k}=${v}`)
14311
+ ];
14312
+ log(`[managed-settings] set ${changed.join(", ")} in ${path} (ENG-5786 unblocks Claude Code channels; ENG-9223 pre-approves project MCP servers so a headless session never freezes on the trust prompt; ENG-10620 disables Claude Code's self-updater for every launch so the fleet pin holds)`);
14300
14313
  return "ok";
14301
14314
  } catch (err) {
14302
14315
  log(`[managed-settings] could not ensure ${path}: ${err.message} \u2014 Claude Code channels may be blocked and a new agent may freeze on the MCP trust prompt; set ${MANAGED_SETTINGS_KEYS.map((k) => `${k}:true`).join(" and ")} manually`);
@@ -15532,7 +15545,7 @@ function flushRestartedAgentDiagnostics(hostId, codeNames) {
15532
15545
  if (codeNames.length === 0) return;
15533
15546
  void (async () => {
15534
15547
  try {
15535
- const { collectDiagnostics } = await import("../persistent-session-523F5U4H.js");
15548
+ const { collectDiagnostics } = await import("../persistent-session-VX26HP7Z.js");
15536
15549
  await api.post("/host/heartbeat", {
15537
15550
  host_id: hostId,
15538
15551
  agent_diagnostics: collectDiagnostics(codeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor)
@@ -15674,7 +15687,7 @@ async function pollCycleInner() {
15674
15687
  }
15675
15688
  try {
15676
15689
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
15677
- const { collectDiagnostics } = await import("../persistent-session-523F5U4H.js");
15690
+ const { collectDiagnostics } = await import("../persistent-session-VX26HP7Z.js");
15678
15691
  const diagCodeNames = [...agentState.persistentSessionAgents];
15679
15692
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor) : void 0;
15680
15693
  let tailscaleHostname;
@@ -15828,10 +15841,11 @@ async function pollCycleInner() {
15828
15841
  hostFlagStore().applyBooleanFlagsByAgent(hbResp?.feature_flags_by_agent);
15829
15842
  setConfigHash(hbResp?.config_hash ?? null);
15830
15843
  try {
15831
- if (decideMaintenanceWindowGate({ window: cachedMaintenanceWindow, now: /* @__PURE__ */ new Date() }) === "proceed") {
15832
- const { maybeUpdateClaudeCode } = await import("../claude-code-updater-4E5T2X3Z.js");
15844
+ const desiredClaudeCodeVersion = hbResp?.desired_claude_code_version?.trim();
15845
+ if (desiredClaudeCodeVersion && decideMaintenanceWindowGate({ window: cachedMaintenanceWindow, now: /* @__PURE__ */ new Date() }) === "proceed") {
15846
+ const { maybeUpdateClaudeCode } = await import("../claude-code-updater-NAHJ6O6C.js");
15833
15847
  await maybeUpdateClaudeCode({
15834
- desiredVersion: hbResp?.desired_claude_code_version ?? "latest",
15848
+ desiredVersion: desiredClaudeCodeVersion,
15835
15849
  currentVersion: cachedFrameworkVersion,
15836
15850
  log
15837
15851
  });
@@ -15849,7 +15863,7 @@ async function pollCycleInner() {
15849
15863
  collectPanelessActivityProbes,
15850
15864
  getResponsivenessIntervalMs,
15851
15865
  occupancyQualificationClassifications
15852
- } = await import("../responsiveness-probe-CHGET573.js");
15866
+ } = await import("../responsiveness-probe-5LUHQCKN.js");
15853
15867
  const probeIntervalMs = getResponsivenessIntervalMs();
15854
15868
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
15855
15869
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -15966,7 +15980,7 @@ async function pollCycleInner() {
15966
15980
  collectResponsivenessProbes,
15967
15981
  livePendingInboundOldestAgeSeconds,
15968
15982
  parkPendingInbound
15969
- } = await import("../responsiveness-probe-CHGET573.js");
15983
+ } = await import("../responsiveness-probe-5LUHQCKN.js");
15970
15984
  const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-NDP36U7O.js");
15971
15985
  const wedgeNow = /* @__PURE__ */ new Date();
15972
15986
  const liveAgents = agentState.persistentSessionAgents;
@@ -16122,7 +16136,7 @@ async function pollCycleInner() {
16122
16136
  }
16123
16137
  try {
16124
16138
  const { scrapeMcpFailedBannerCount } = await import("../pane-mcp-banner-scraper-JA437JIB.js");
16125
- const { probeSessionAuth } = await import("../session-auth-dead-T5SUK2HT.js");
16139
+ const { probeSessionAuth } = await import("../session-auth-dead-2WOMHFPP.js");
16126
16140
  const observations = [];
16127
16141
  const pendingCacheCommits = [];
16128
16142
  const modelApiErrorReportingOn = hostFlagStore().getBoolean("model-api-error-reporting");
@@ -20386,7 +20400,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
20386
20400
  try {
20387
20401
  const freshId = rotateSessionForWedge(codeName);
20388
20402
  consecutiveWedgeCycles.delete(codeName);
20389
- const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-CHGET573.js");
20403
+ const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-5LUHQCKN.js");
20390
20404
  const { parked, deadLettered } = parkForRestart(codeName, /* @__PURE__ */ new Date());
20391
20405
  const parkNote = parked > 0 ? `, ${parked} inbound parked` : "";
20392
20406
  const deadNote = deadLettered > 0 ? `, ${deadLettered} undeliverable dead-lettered` : "";
@@ -20429,7 +20443,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
20429
20443
  void api.post("/host/restart-ack", { host_id: hostId, agent_id: agentId, restart_requested_at: requestedAt }).catch((err) => log(`[restart-lane] ack failed for '${codeName}': ${err.message}`));
20430
20444
  void (async () => {
20431
20445
  try {
20432
- const { collectDiagnostics } = await import("../persistent-session-523F5U4H.js");
20446
+ const { collectDiagnostics } = await import("../persistent-session-VX26HP7Z.js");
20433
20447
  await api.post("/host/heartbeat", {
20434
20448
  host_id: hostId,
20435
20449
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor)
@@ -20480,7 +20494,7 @@ async function respawnAgentAfterMcpStop(codeName, reason) {
20480
20494
  }
20481
20495
  try {
20482
20496
  const hostId = await getHostId();
20483
- const { collectDiagnostics } = await import("../persistent-session-523F5U4H.js");
20497
+ const { collectDiagnostics } = await import("../persistent-session-VX26HP7Z.js");
20484
20498
  await api.post("/host/heartbeat", {
20485
20499
  host_id: hostId,
20486
20500
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor)
@@ -21122,7 +21136,7 @@ async function processClaudePairSessions(agents) {
21122
21136
  killPairSession,
21123
21137
  pairTmuxSession,
21124
21138
  finalizeClaudePairOnboarding
21125
- } = await import("../claude-pair-runtime-7WHDU2KR.js");
21139
+ } = await import("../claude-pair-runtime-7JTAECL4.js");
21126
21140
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
21127
21141
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
21128
21142
  const killed = await killPairSession(pairTmuxSession(pairId));