@integrity-labs/agt-cli 0.28.1124 → 0.28.1126

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 (26) hide show
  1. package/dist/bin/agt.js +5 -5
  2. package/dist/{chunk-5N6EPYZQ.js → chunk-C5YNHEST.js} +2 -2
  3. package/dist/{chunk-CHNTTWQ7.js → chunk-DIOP5LSU.js} +4 -4
  4. package/dist/chunk-JEC3BUC6.js +29 -0
  5. package/dist/chunk-JEC3BUC6.js.map +1 -0
  6. package/dist/{chunk-KHRTN324.js → chunk-JI2D2TA2.js} +7 -1
  7. package/dist/{chunk-KHRTN324.js.map → chunk-JI2D2TA2.js.map} +1 -1
  8. package/dist/{claude-pair-runtime-ZESFCZ7Q.js → claude-pair-runtime-5OBFRZWS.js} +8 -3
  9. package/dist/claude-pair-runtime-5OBFRZWS.js.map +1 -0
  10. package/dist/lib/manager-worker.js +75 -62
  11. package/dist/lib/manager-worker.js.map +1 -1
  12. package/dist/mcp/direct-chat-channel.js +66 -20
  13. package/dist/mcp/index.js +6 -0
  14. package/dist/mcp/origami.js +6 -0
  15. package/dist/mcp/slack-channel.js +6 -0
  16. package/dist/mcp/telegram-channel.js +6 -0
  17. package/dist/{persistent-session-HLPFSYTU.js → persistent-session-E5IR3RBF.js} +3 -3
  18. package/dist/{responsiveness-probe-DS7RXU6E.js → responsiveness-probe-KCICZE6E.js} +3 -3
  19. package/dist/{session-auth-dead-XOJCPP4F.js → session-auth-dead-VYP4VCMX.js} +2 -2
  20. package/package.json +1 -1
  21. package/dist/claude-pair-runtime-ZESFCZ7Q.js.map +0 -1
  22. /package/dist/{chunk-5N6EPYZQ.js.map → chunk-C5YNHEST.js.map} +0 -0
  23. /package/dist/{chunk-CHNTTWQ7.js.map → chunk-DIOP5LSU.js.map} +0 -0
  24. /package/dist/{persistent-session-HLPFSYTU.js.map → persistent-session-E5IR3RBF.js.map} +0 -0
  25. /package/dist/{responsiveness-probe-DS7RXU6E.js.map → responsiveness-probe-KCICZE6E.js.map} +0 -0
  26. /package/dist/{session-auth-dead-XOJCPP4F.js.map → session-auth-dead-VYP4VCMX.js.map} +0 -0
@@ -37044,6 +37044,12 @@ var FLAG_REGISTRY = [
37044
37044
  flagType: "boolean",
37045
37045
  defaultValue: false
37046
37046
  },
37047
+ {
37048
+ key: "broker-read-jobs",
37049
+ description: `Slow broker READ tools finish in a background job (ENG-11476, ADR-0129). When on for an org, an opted-in read tool (Pronto Xi first) that misses an 18s grace window answers "still running, call again with the same arguments" and finishes in the broker-read-job worker instead of timing out. Off = today's synchronous call, so false is the safe value. Evaluated in the API per org, team and agent.`,
37050
+ flagType: "boolean",
37051
+ defaultValue: false
37052
+ },
37047
37053
  {
37048
37054
  key: "id-keyed-layout-migration",
37049
37055
  description: "Dark migration engine (ENG-7891 / ADR-0049 slice 3): convert an EXISTING legacy codename-keyed agent host dir to the agent_id-keyed layout (real ~/.augmented/{agent_id} dir + codename compatibility symlink) at the next pre-spawn tick, moving the Claude transcript store so the agent keeps its history. Resolved PER-AGENT (getBooleanForAgent) so a one-way-door migration can be armed one agent at a time. Ships dark; NOT armed until a test-host rehearsal passes (the last ENG-7891 acceptance gate). Crash recovery + the per-agent preconditions (session-down, WhatsApp-writer quiesce, id-aware hook regen by the spawn funnel) are always-on and flag-independent; this flag only gates INITIATING a fresh conversion. Deliberately no host-wide env override - arming is per-agent to avoid flipping every legacy agent on a host at once.",
@@ -40791,11 +40797,22 @@ function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = D
40791
40797
  // src/direct-chat-poll-guard.ts
40792
40798
  function evaluatePollGuard(state, nowMs, stuckMs) {
40793
40799
  if (!state.inFlight) return { run: true, stuck: false };
40800
+ if (state.leaseUntilMs != null && nowMs < state.leaseUntilMs) return { run: false, stuck: false };
40794
40801
  if (state.inFlightSinceMs != null && nowMs - state.inFlightSinceMs >= stuckMs) {
40795
40802
  return { run: true, stuck: true };
40796
40803
  }
40797
40804
  return { run: false, stuck: false };
40798
40805
  }
40806
+ async function withPollGuardLease(state, leaseUntilMs, work, nowMs = Date.now) {
40807
+ const previous = state.leaseUntilMs ?? null;
40808
+ state.leaseUntilMs = Math.max(previous ?? 0, leaseUntilMs);
40809
+ try {
40810
+ return await work();
40811
+ } finally {
40812
+ state.leaseUntilMs = previous;
40813
+ if (state.inFlight) state.inFlightSinceMs = nowMs();
40814
+ }
40815
+ }
40799
40816
  async function fetchWithTimeout(input, init, timeoutMs, fetchImpl = fetch) {
40800
40817
  const controller = new AbortController();
40801
40818
  const timer = setTimeout(() => {
@@ -40939,7 +40956,44 @@ import {
40939
40956
 
40940
40957
  // src/direct-chat-inbound-attachments.ts
40941
40958
  import { join as join14 } from "path";
40942
- var MAX_INBOUND_ATTACHMENT_BYTES = 10 * 1024 * 1024;
40959
+ var MAX_INBOUND_ATTACHMENT_BYTES = DIRECT_CHAT_UPLOAD_MAX_BYTES;
40960
+ var INBOUND_DOWNLOAD_FLOOR_MS = 2e4;
40961
+ var INBOUND_DOWNLOAD_MIN_BYTES_PER_SEC = 512 * 1024;
40962
+ function inboundDownloadTimeoutMs(expectedBytes) {
40963
+ const bytes = typeof expectedBytes === "number" && Number.isFinite(expectedBytes) && expectedBytes > 0 ? Math.min(expectedBytes, MAX_INBOUND_ATTACHMENT_BYTES) : MAX_INBOUND_ATTACHMENT_BYTES;
40964
+ return INBOUND_DOWNLOAD_FLOOR_MS + Math.ceil(bytes / INBOUND_DOWNLOAD_MIN_BYTES_PER_SEC * 1e3);
40965
+ }
40966
+ function attachmentsDownloadBudgetMs(attachments) {
40967
+ return attachments.reduce((sum, a) => sum + inboundDownloadTimeoutMs(a.byte_size), 0);
40968
+ }
40969
+ async function fetchWithTransferDeadline(url, budgetMs, fetchImpl = fetch) {
40970
+ const controller = new AbortController();
40971
+ const timer = setTimeout(() => {
40972
+ controller.abort(new Error(`inbound attachment download timed out after ${budgetMs}ms`));
40973
+ }, budgetMs);
40974
+ timer.unref?.();
40975
+ let res;
40976
+ try {
40977
+ res = await fetchImpl(url, { method: "GET", signal: controller.signal });
40978
+ } catch (err) {
40979
+ clearTimeout(timer);
40980
+ throw err;
40981
+ }
40982
+ const len = res.headers.get("content-length");
40983
+ return {
40984
+ ok: res.ok,
40985
+ status: res.status,
40986
+ contentLength: len != null ? Number(len) : null,
40987
+ bytes: async () => {
40988
+ try {
40989
+ return new Uint8Array(await res.arrayBuffer());
40990
+ } finally {
40991
+ clearTimeout(timer);
40992
+ }
40993
+ }
40994
+ };
40995
+ }
40996
+ var OVER_CAP_ERROR = `exceeds the ${formatUploadSize(MAX_INBOUND_ATTACHMENT_BYTES)} inbound limit`;
40943
40997
  var INBOUND_ATTACHMENTS_SUBDIR = "direct-chat-inbound";
40944
40998
  function resolveInboundAttachmentsDir(input) {
40945
40999
  const alias = resolveAgentDirAlias(input);
@@ -41011,10 +41065,10 @@ async function downloadInboundAttachment(att, dir, deps) {
41011
41065
  isImage: isImageContentType(att.content_type)
41012
41066
  };
41013
41067
  if (att.byte_size > MAX_INBOUND_ATTACHMENT_BYTES) {
41014
- return { ...base, error: "exceeds the 10 MB inbound limit" };
41068
+ return { ...base, error: OVER_CAP_ERROR };
41015
41069
  }
41016
41070
  try {
41017
- const res = await deps.fetchUrl(att.download_url);
41071
+ const res = await deps.fetchUrl(att.download_url, att.byte_size);
41018
41072
  if (!res.ok) {
41019
41073
  let s3Body = "";
41020
41074
  try {
@@ -41028,11 +41082,11 @@ async function downloadInboundAttachment(att, dir, deps) {
41028
41082
  return { ...base, error: `download HTTP ${res.status}` };
41029
41083
  }
41030
41084
  if (res.contentLength != null && res.contentLength > MAX_INBOUND_ATTACHMENT_BYTES) {
41031
- return { ...base, error: "exceeds the 10 MB inbound limit" };
41085
+ return { ...base, error: OVER_CAP_ERROR };
41032
41086
  }
41033
41087
  const bytes = await res.bytes();
41034
41088
  if (bytes.byteLength > MAX_INBOUND_ATTACHMENT_BYTES) {
41035
- return { ...base, error: "exceeds the 10 MB inbound limit" };
41089
+ return { ...base, error: OVER_CAP_ERROR };
41036
41090
  }
41037
41091
  deps.ensureDir(dir);
41038
41092
  const path = deps.joinPath(dir, safeInboundFilename(att.upload_id, att.filename));
@@ -42227,16 +42281,7 @@ async function maybeSendDirectChatAccountWarn(sessionId) {
42227
42281
  }
42228
42282
  }
42229
42283
  var inboundAttachmentDeps = {
42230
- fetchUrl: async (url) => {
42231
- const res = await fetchWithTimeout(url, { method: "GET" }, HTTP_TIMEOUT_MS);
42232
- const len = res.headers.get("content-length");
42233
- return {
42234
- ok: res.ok,
42235
- status: res.status,
42236
- contentLength: len != null ? Number(len) : null,
42237
- bytes: async () => new Uint8Array(await res.arrayBuffer())
42238
- };
42239
- },
42284
+ fetchUrl: (url, expectedBytes) => fetchWithTransferDeadline(url, inboundDownloadTimeoutMs(expectedBytes)),
42240
42285
  ensureDir: (dir) => mkdirSync7(dir, { recursive: true }),
42241
42286
  writeFile: (path, bytes) => writeFileSync8(path, bytes, { mode: 384 }),
42242
42287
  joinPath: (...parts) => join17(...parts),
@@ -43104,10 +43149,10 @@ async function pollForMessages(sinceMs) {
43104
43149
  }
43105
43150
  const inboundAttachments = msg.attachments ?? [];
43106
43151
  if (!isNotice && inboundAttachments.length > 0 && INBOUND_ATTACHMENTS_DIR) {
43107
- const processed = await downloadInboundAttachments(
43108
- inboundAttachments,
43109
- INBOUND_ATTACHMENTS_DIR,
43110
- inboundAttachmentDeps
43152
+ const processed = await withPollGuardLease(
43153
+ pollGuard,
43154
+ Date.now() + attachmentsDownloadBudgetMs(inboundAttachments) + POLL_STUCK_MS,
43155
+ () => downloadInboundAttachments(inboundAttachments, INBOUND_ATTACHMENTS_DIR, inboundAttachmentDeps)
43111
43156
  );
43112
43157
  const annotation = buildAttachmentAnnotation(processed);
43113
43158
  if (annotation) injectContent = `${injectContent}${annotation}`;
@@ -43267,7 +43312,7 @@ var DOORBELL_ENABLED = resolveHostBooleanFlag({
43267
43312
  var DOORBELL_FILE = "direct-chat-doorbell";
43268
43313
  var SAFETY_NET_POLL_MS = 3e4;
43269
43314
  var POLL_STUCK_MS = 9e4;
43270
- var pollGuard = { inFlight: false, inFlightSinceMs: null };
43315
+ var pollGuard = { inFlight: false, inFlightSinceMs: null, leaseUntilMs: null };
43271
43316
  async function pollOnce(sinceMs) {
43272
43317
  const decision = evaluatePollGuard(pollGuard, Date.now(), POLL_STUCK_MS);
43273
43318
  if (!decision.run) return { ran: false, ok: false };
@@ -43285,6 +43330,7 @@ async function pollOnce(sinceMs) {
43285
43330
  } finally {
43286
43331
  pollGuard.inFlight = false;
43287
43332
  pollGuard.inFlightSinceMs = null;
43333
+ pollGuard.leaseUntilMs = null;
43288
43334
  }
43289
43335
  }
43290
43336
  var doorbellWatcher = null;
package/dist/mcp/index.js CHANGED
@@ -29127,6 +29127,12 @@ var FLAG_REGISTRY = [
29127
29127
  flagType: "boolean",
29128
29128
  defaultValue: false
29129
29129
  },
29130
+ {
29131
+ key: "broker-read-jobs",
29132
+ description: `Slow broker READ tools finish in a background job (ENG-11476, ADR-0129). When on for an org, an opted-in read tool (Pronto Xi first) that misses an 18s grace window answers "still running, call again with the same arguments" and finishes in the broker-read-job worker instead of timing out. Off = today's synchronous call, so false is the safe value. Evaluated in the API per org, team and agent.`,
29133
+ flagType: "boolean",
29134
+ defaultValue: false
29135
+ },
29130
29136
  {
29131
29137
  key: "id-keyed-layout-migration",
29132
29138
  description: "Dark migration engine (ENG-7891 / ADR-0049 slice 3): convert an EXISTING legacy codename-keyed agent host dir to the agent_id-keyed layout (real ~/.augmented/{agent_id} dir + codename compatibility symlink) at the next pre-spawn tick, moving the Claude transcript store so the agent keeps its history. Resolved PER-AGENT (getBooleanForAgent) so a one-way-door migration can be armed one agent at a time. Ships dark; NOT armed until a test-host rehearsal passes (the last ENG-7891 acceptance gate). Crash recovery + the per-agent preconditions (session-down, WhatsApp-writer quiesce, id-aware hook regen by the spawn funnel) are always-on and flag-independent; this flag only gates INITIATING a fresh conversion. Deliberately no host-wide env override - arming is per-agent to avoid flipping every legacy agent on a host at once.",
@@ -43056,6 +43056,12 @@ var FLAG_REGISTRY = [
43056
43056
  flagType: "boolean",
43057
43057
  defaultValue: false
43058
43058
  },
43059
+ {
43060
+ key: "broker-read-jobs",
43061
+ description: `Slow broker READ tools finish in a background job (ENG-11476, ADR-0129). When on for an org, an opted-in read tool (Pronto Xi first) that misses an 18s grace window answers "still running, call again with the same arguments" and finishes in the broker-read-job worker instead of timing out. Off = today's synchronous call, so false is the safe value. Evaluated in the API per org, team and agent.`,
43062
+ flagType: "boolean",
43063
+ defaultValue: false
43064
+ },
43059
43065
  {
43060
43066
  key: "id-keyed-layout-migration",
43061
43067
  description: "Dark migration engine (ENG-7891 / ADR-0049 slice 3): convert an EXISTING legacy codename-keyed agent host dir to the agent_id-keyed layout (real ~/.augmented/{agent_id} dir + codename compatibility symlink) at the next pre-spawn tick, moving the Claude transcript store so the agent keeps its history. Resolved PER-AGENT (getBooleanForAgent) so a one-way-door migration can be armed one agent at a time. Ships dark; NOT armed until a test-host rehearsal passes (the last ENG-7891 acceptance gate). Crash recovery + the per-agent preconditions (session-down, WhatsApp-writer quiesce, id-aware hook regen by the spawn funnel) are always-on and flag-independent; this flag only gates INITIATING a fresh conversion. Deliberately no host-wide env override - arming is per-agent to avoid flipping every legacy agent on a host at once.",
@@ -37865,6 +37865,12 @@ var FLAG_REGISTRY = [
37865
37865
  flagType: "boolean",
37866
37866
  defaultValue: false
37867
37867
  },
37868
+ {
37869
+ key: "broker-read-jobs",
37870
+ description: `Slow broker READ tools finish in a background job (ENG-11476, ADR-0129). When on for an org, an opted-in read tool (Pronto Xi first) that misses an 18s grace window answers "still running, call again with the same arguments" and finishes in the broker-read-job worker instead of timing out. Off = today's synchronous call, so false is the safe value. Evaluated in the API per org, team and agent.`,
37871
+ flagType: "boolean",
37872
+ defaultValue: false
37873
+ },
37868
37874
  {
37869
37875
  key: "id-keyed-layout-migration",
37870
37876
  description: "Dark migration engine (ENG-7891 / ADR-0049 slice 3): convert an EXISTING legacy codename-keyed agent host dir to the agent_id-keyed layout (real ~/.augmented/{agent_id} dir + codename compatibility symlink) at the next pre-spawn tick, moving the Claude transcript store so the agent keeps its history. Resolved PER-AGENT (getBooleanForAgent) so a one-way-door migration can be armed one agent at a time. Ships dark; NOT armed until a test-host rehearsal passes (the last ENG-7891 acceptance gate). Crash recovery + the per-agent preconditions (session-down, WhatsApp-writer quiesce, id-aware hook regen by the spawn funnel) are always-on and flag-independent; this flag only gates INITIATING a fresh conversion. Deliberately no host-wide env override - arming is per-agent to avoid flipping every legacy agent on a host at once.",
@@ -38093,6 +38093,12 @@ var FLAG_REGISTRY = [
38093
38093
  flagType: "boolean",
38094
38094
  defaultValue: false
38095
38095
  },
38096
+ {
38097
+ key: "broker-read-jobs",
38098
+ description: `Slow broker READ tools finish in a background job (ENG-11476, ADR-0129). When on for an org, an opted-in read tool (Pronto Xi first) that misses an 18s grace window answers "still running, call again with the same arguments" and finishes in the broker-read-job worker instead of timing out. Off = today's synchronous call, so false is the safe value. Evaluated in the API per org, team and agent.`,
38099
+ flagType: "boolean",
38100
+ defaultValue: false
38101
+ },
38096
38102
  {
38097
38103
  key: "id-keyed-layout-migration",
38098
38104
  description: "Dark migration engine (ENG-7891 / ADR-0049 slice 3): convert an EXISTING legacy codename-keyed agent host dir to the agent_id-keyed layout (real ~/.augmented/{agent_id} dir + codename compatibility symlink) at the next pre-spawn tick, moving the Claude transcript store so the agent keeps its history. Resolved PER-AGENT (getBooleanForAgent) so a one-way-door migration can be armed one agent at a time. Ships dark; NOT armed until a test-host rehearsal passes (the last ENG-7891 acceptance gate). Crash recovery + the per-agent preconditions (session-down, WhatsApp-writer quiesce, id-aware hook regen by the spawn funnel) are always-on and flag-independent; this flag only gates INITIATING a fresh conversion. Deliberately no host-wide env override - arming is per-agent to avoid flipping every legacy agent on a host at once.",
@@ -68,8 +68,8 @@ import {
68
68
  writeDirectChatSessionState,
69
69
  writeEgressAllowlist,
70
70
  writePersistentClaudeWrapper
71
- } from "./chunk-5N6EPYZQ.js";
72
- import "./chunk-KHRTN324.js";
71
+ } from "./chunk-C5YNHEST.js";
72
+ import "./chunk-JI2D2TA2.js";
73
73
  import "./chunk-OCMJ5E6J.js";
74
74
  import "./chunk-XWVM4KPK.js";
75
75
  import "./chunk-EL5MWGGH.js";
@@ -145,4 +145,4 @@ export {
145
145
  writeEgressAllowlist,
146
146
  writePersistentClaudeWrapper
147
147
  };
148
- //# sourceMappingURL=persistent-session-HLPFSYTU.js.map
148
+ //# sourceMappingURL=persistent-session-E5IR3RBF.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  paneLogPath
3
- } from "./chunk-5N6EPYZQ.js";
4
- import "./chunk-KHRTN324.js";
3
+ } from "./chunk-C5YNHEST.js";
4
+ import "./chunk-JI2D2TA2.js";
5
5
  import "./chunk-OCMJ5E6J.js";
6
6
  import "./chunk-XWVM4KPK.js";
7
7
  import "./chunk-EL5MWGGH.js";
@@ -802,4 +802,4 @@ export {
802
802
  readAndResetSlackReplyBindingClassifications,
803
803
  readAndResetSlackReplyTargetClassifications
804
804
  };
805
- //# sourceMappingURL=responsiveness-probe-DS7RXU6E.js.map
805
+ //# sourceMappingURL=responsiveness-probe-KCICZE6E.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sessionTranscriptDir
3
- } from "./chunk-KHRTN324.js";
3
+ } from "./chunk-JI2D2TA2.js";
4
4
  import "./chunk-DGUM43GV.js";
5
5
 
6
6
  // src/lib/session-auth-dead.ts
@@ -204,4 +204,4 @@ export {
204
204
  decideSessionAuthState,
205
205
  probeSessionAuth
206
206
  };
207
- //# sourceMappingURL=session-auth-dead-XOJCPP4F.js.map
207
+ //# sourceMappingURL=session-auth-dead-VYP4VCMX.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.1124",
3
+ "version": "0.28.1126",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1 +0,0 @@
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';\n// ENG-10862: `readFileSync` so finalize can tell a written config from a torn\n// one — an mtime that advances onto unparseable JSON is not a success.\nimport { existsSync, readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir, userInfo } from 'node:os';\nimport {\n claudeConfigDir as claudeConfigDirFor,\n claudeJsonPath as claudeJsonPathFor,\n effectiveClaudeJsonPath,\n inspectClaudeConfig,\n CLAUDE_CONFIG_DIR_ENV,\n} from './claude-config-location.js';\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-10653: how long to wait, after a pane goes dead, for tmux to publish its\n * exit status. A wall-clock deadline, deliberately not an iteration count —\n * see the bound's derivation below for why that distinction is not cosmetic.\n *\n * MEASURED (`scripts/measure-tmux-dead-status.mjs`, macOS 15, tmux 3.7b,\n * 2026-09-18, across three load regimes — unloaded, 12-way CPU saturation and\n * a fork storm): **not once** did a poll see the `Pane is dead (...)` banner\n * drawn while `pane_dead_status` was still empty.\n *\n * That is stated as a count of a single-instant observation, deliberately, and\n * not as a timing gap. An earlier draft of this comment claimed \"gap 0 ms at\n * p100\"; that was an artefact of polling — two values read in the same poll\n * bound the gap by one poll width (~9–15 ms here), they do not measure it as\n * zero — and the companion claim that the banner \"followed by 3–24 ms\" was\n * just the cost of the second tmux call in the same iteration. Both are gone.\n *\n * Two consequences, and the second is why ENG-10631 did not close this:\n *\n * 1. The banner is not an independent source of the status. tmux draws it\n * only once it has reaped the child, so the state ENG-10631's fallback\n * exists to exploit — banner present, status absent — is one this tmux\n * never actually entered. The fallback is still right and stays (it\n * recovers a status the format read itself lost), but it could not win the\n * reap race it was written for, which is why ENG-10643's queue run still\n * read \"exit status unknown\" with all three of its fixes in the tree.\n * 2. The banner IS a completion marker. Once drawn, tmux has published\n * everything it is going to, so it ends the wait as definitively as a\n * numeric status does.\n *\n * The BOUND is therefore not derived from that measurement — a 0 ms gap would\n * justify no wait at all. It is derived from the CI tail, the only place the\n * gap has ever opened. ENG-10631 measured >1 s. The ENG-10643 ejection (run\n * 35223527037, job `tests (packages)`) then exhausted the 30 × 100 ms loop\n * that replaced it: the test took 4624 ms, which decomposes to the 500 ms\n * spawn settle + 3000 ms of sleeps + ~33 tmux client spawns at ~35 ms each.\n * That is the point about wall clock — the loop's nominal \"3 s\" was really\n * ~4.1 s, because each iteration also paid to spawn a tmux client, and the\n * number in the code did not say so.\n *\n * 15 s is ~3.7× the bound that was observed losing. It is affordable because\n * this wait is on a TERMINAL path — the pane is dead, the pair flow is over —\n * and it sits inside a single poll of callers budgeting 60 s\n * (`startClaudePair`) and 30 s (`submitClaudePairCode`). An alive pane never\n * reaches it.\n *\n * ENG-10714 — \"sits inside\" is true of the DEFAULT budgets and not of the\n * mechanism, so read it as a statement about today's callers rather than a\n * bound this wait respects. `checkPairPaneAlive` is called from INSIDE\n * `startClaudePair`'s poll loop, and that loop tests its deadline only\n * BETWEEN iterations — so one poll can overrun a caller's whole budget by up\n * to `DEAD_PANE_STATUS_WAIT_MS`, and the caller has no way to cut it short.\n * Measured 2026-09-19: a caller passing `timeoutMs: 10_000` returned after\n * 15050 ms. Nothing is broken by that today (the path is terminal and the\n * overrun buys the answer the caller wanted), but a future caller with a\n * genuinely tight budget needs `waitMs` threading through\n * `DeadPaneWaitOpts` — which `checkPairPaneAlive` already accepts and no\n * caller yet passes — rather than a smaller global bound. Raising this\n * constant without that is what would turn the overrun into a real one.\n */\nexport const DEAD_PANE_STATUS_WAIT_MS = 15_000;\n\n/** One `#{pane_dead}|#{pane_dead_status}|#{pane_dead_signal}` reading. */\nexport interface DeadPaneReading {\n dead: string;\n exitStatus: string;\n /** Empty string on tmux builds with no `pane_dead_signal` format. */\n signal: string;\n}\n\nexport interface DeadPaneWaitOpts {\n waitMs?: number;\n pollMs?: number;\n /** How often to spend a capture-pane looking for the completion banner. */\n captureEveryMs?: number;\n /**\n * ENG-10714: how long the FINAL capture may keep retrying while the dead\n * pane still shows nothing. Bounded and only entered when there is nothing\n * to show — a pane that already has content costs one capture, as before.\n */\n captureRetryMs?: number;\n /** Gap between those retries. */\n captureRetryPollMs?: number;\n}\n\nexport interface DeadPaneVerdict {\n exitStatus: string;\n pane: string;\n /**\n * How the wait ended. Four of these are answers; only `timeout` means tmux\n * never told us. `signal` is a genuinely status-less death, not a missing\n * status — the distinction the operator message depends on.\n */\n resolved: 'status' | 'signal' | 'banner' | 'session-gone' | 'timeout';\n waitedMs: number;\n polls: number;\n readFailures: number;\n /**\n * ENG-10714: transient `capture-pane` failures. These used to be swallowed\n * by a bare `catch {}`, which made \"the capture failed\" and \"claude printed\n * nothing\" the same observable — so the message could not tell the operator\n * which had happened, and the after-window could not tell which was fixed.\n */\n captureFailures: number;\n /** ENG-10714: extra captures spent waiting for a dead pane to show content. */\n recaptures: number;\n /** Filled in only on `timeout`, where it is worth knowing. */\n tmuxVersion?: string;\n}\n\n/**\n * The pane lines that are claude's OUTPUT — everything except tmux's own\n * dead-pane banner and blank padding.\n *\n * Shared by the capture retry and the operator message deliberately: they are\n * answering the same question (\"is there anything to show?\") and a second copy\n * of the filter would let them disagree about it, so the retry could give up\n * on a pane the message then renders, or keep retrying one it would discard.\n */\nfunction paneOutputLines(pane: string): string[] {\n return pane\n .split('\\n')\n .map((l) => l.trimEnd())\n .filter((l) => l.length > 0 && !/^Pane is dead\\b/.test(l));\n}\n\n/**\n * ENG-10653: wait for tmux to publish a dead pane's exit status, and report\n * how the wait ended.\n *\n * tmux marks a pane dead when the pty hits EOF but only fills in\n * `pane_dead_status` once it has reaped the child — separate events, so a read\n * in between sees a dead pane with an EMPTY status. Ending the wait on the\n * first of those and reporting \"unknown\" is the defect this closes.\n *\n * The I/O is injected so the wait can be proven against a fake that publishes\n * late or fails transiently. It cannot be proven against real tmux on a\n * developer machine: the gap there is 0 ms (see `DEAD_PANE_STATUS_WAIT_MS`),\n * so the race simply does not occur.\n *\n * ENG-10714: it DOES occur in CI, and harder than this comment implied — on\n * tmux 3.4 the full 15 s bound has been seen to expire with no status, no\n * signal and no banner, i.e. tmux published nothing at all rather than\n * publishing late. So the real-tmux test above asserts only what does not\n * depend on the race; anything that asserts the status is recovered belongs\n * against the injected fake, where the timing is the test's to choose.\n */\nexport async function awaitDeadPaneVerdict(\n io: { read: () => Promise<DeadPaneReading>; capture: () => Promise<string> },\n initial: DeadPaneReading,\n opts: DeadPaneWaitOpts = {},\n): Promise<DeadPaneVerdict> {\n const waitMs = opts.waitMs ?? DEAD_PANE_STATUS_WAIT_MS;\n const pollMs = opts.pollMs ?? 100;\n const captureEveryMs = opts.captureEveryMs ?? 1_000;\n\n const numeric = (v: string): boolean => /^\\d+$/.test(v.trim());\n // NOT `numeric`: tmux reports the signal by NAME, not number. Measured on\n // tmux 3.7b, `#{pane_dead_signal}` for a SIGTERM death is the string `term`\n // — so a numeric test never matches, and a signal death would sit out the\n // whole bound before reporting the unknown it already knew about. Any\n // non-empty value is the answer; builds without the format expand it to ''.\n //\n // The `#{` rejection is the fail-safe direction. A build that echoed the\n // format back unexpanded would otherwise make EVERY dead pane resolve as a\n // signal on the first read — skipping the status wait entirely and turning\n // this fix into a faster version of the bug it closes.\n const signalled = (v: string): boolean => v.trim().length > 0 && !v.includes('#{');\n const started = Date.now();\n const deadline = started + waitMs;\n\n let reading = initial;\n let pane = '';\n let polls = 0;\n let readFailures = 0;\n // ENG-10714 (CodeRabbit): declared HERE, not beside the retry below, because\n // the periodic banner capture inside the loop can fail too — and if it does\n // while the pane is still empty, a later final capture that succeeds-and-is-\n // empty would report zero failures and `describeClaudeExit` would go back to\n // claiming \"It printed nothing\". That is this change's own defect, one loop up.\n let captureFailures = 0;\n let nextCaptureAt = started + captureEveryMs;\n let resolved: DeadPaneVerdict['resolved'] = 'timeout';\n if (numeric(reading.exitStatus)) resolved = 'status';\n else if (signalled(reading.signal)) resolved = 'signal';\n\n while (resolved === 'timeout' && Date.now() < deadline) {\n await sleep(pollMs);\n polls++;\n try {\n reading = await io.read();\n } catch (err) {\n readFailures++;\n // A transient tmux client spawn failure under load must NOT collapse the\n // wait. The previous `catch { break }` here turned one such failure into\n // an immediate \"exit status unknown\", which is the same wrong answer\n // this whole wait exists to avoid — and it left no trace of having\n // happened. Only a session that has genuinely gone away ends the wait,\n // and even then the pane was already dead, so the verdict is still\n // `claude-exited` rather than a bare `no-session`.\n if (classifyTmuxError(err).kind !== 'unknown') { resolved = 'session-gone'; break; }\n continue;\n }\n if (numeric(reading.exitStatus)) { resolved = 'status'; break; }\n if (signalled(reading.signal)) { resolved = 'signal'; break; }\n if (Date.now() >= nextCaptureAt) {\n nextCaptureAt = Date.now() + captureEveryMs;\n try { pane = await io.capture(); } catch { captureFailures++; }\n // Costs one capture per second, and covers the tmux builds that publish\n // no `pane_dead_signal`: on those, a signal death would otherwise sit\n // here for the full bound before admitting it has no status.\n if (/^Pane is dead \\(/m.test(pane)) { resolved = 'banner'; break; }\n }\n }\n\n // Capture last, so the pane read is the freshest one available — on `status`\n // the banner is 3–24 ms behind and may only now have landed. Attempted even\n // on `session-gone`: a `display-message` that classified as no-session does\n // not prove `capture-pane` fails in the same instant, and the pane content\n // is the last thing recoverable about why claude exited.\n //\n // ENG-10714: this was one `try { pane = await io.capture(); } catch {}`, and\n // that single line produced a quarter of the CI failures on this path — 5 of\n // 20 over the test file's first 38.7 h were an EMPTY pane, once on a pane\n // whose exit status had already been published. The operator was told\n // \"It printed nothing\" about a pane that had printed.\n //\n // Two candidate causes, and the swallowing is what made them\n // indistinguishable: tmux had not yet drained the pty into the pane buffer\n // (the pane is marked dead at EOF, which is earlier), or the capture's own\n // tmux client spawn failed transiently under load — the very class the read\n // loop above already retries and COUNTS. Retrying fixes both; counting is\n // what lets the next occurrence say which one it was.\n //\n // Bounded, and entered only when there is nothing to show: a pane with\n // content costs exactly one capture, exactly as before.\n // Snapshot BEFORE the capture retry: `waitedMs` is how long tmux was given\n // to publish a STATUS, and the operator message quotes it as exactly that.\n // Letting the capture retry inflate it would make the reported wait disagree\n // with the bound it is being compared against.\n const waitedMs = Date.now() - started;\n const captureDeadline = Date.now() + (opts.captureRetryMs ?? 1_000);\n const captureRetryPollMs = opts.captureRetryPollMs ?? 200;\n let recaptures = 0;\n for (;;) {\n try {\n const fresh = await io.capture();\n // Never trade content away for a later, emptier read: an in-loop banner\n // capture may already hold claude's output, and on `session-gone` the\n // pane can legitimately go blank between reads.\n //\n // Compared by CHARACTER length of the filtered output, not by line count\n // (CodeRabbit): two captures can both hold one line while the later one\n // is a truncated version of the earlier, and a line-count test would\n // swap the diagnostic for the fragment.\n const freshOutput = paneOutputLines(fresh).join('\\n');\n const savedOutput = paneOutputLines(pane).join('\\n');\n if (freshOutput.length >= savedOutput.length) pane = fresh;\n } catch (err) {\n captureFailures++;\n // Same rule as the read loop: only a session that has genuinely gone\n // away ends this, because retrying a vanished session cannot succeed.\n if (classifyTmuxError(err).kind !== 'unknown') break;\n }\n if (paneOutputLines(pane).length > 0 || Date.now() >= captureDeadline) break;\n recaptures++;\n await sleep(captureRetryPollMs);\n }\n return {\n exitStatus: reading.exitStatus,\n pane,\n resolved,\n waitedMs,\n polls,\n readFailures,\n captureFailures,\n recaptures,\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, wait?: DeadPaneVerdict): string {\n // ENG-10631: `pane_dead_status` can still be empty after the reap wait under\n // heavy load (seen in CI with a 1s wait), but tmux's own banner in the pane\n // carries the same number. Use it rather than report \"exit status unknown\".\n // The LAST banner: claude's own output above it could contain banner-shaped\n // text, and tmux always writes the real one after everything claude printed.\n // Take the final banner of ANY kind first (a signal death writes\n // `Pane is dead (signal N, ...)`), and only then read a status out of it.\n const finalBanner = [...pane.matchAll(/^Pane is dead \\(.*$/gm)].at(-1)?.[0] ?? '';\n const bannerStatus = /^Pane is dead \\(status (\\d+)/.exec(finalBanner)?.[1] ?? '';\n if (!/^\\d+$/.test(exitStatus.trim()) && bannerStatus) exitStatus = bannerStatus;\n const lines = paneOutputLines(pane);\n let output = lines.slice(-15).join('\\n');\n if (output.length > 800) output = `…${output.slice(-800)}`;\n const known = /^\\d+$/.test(exitStatus.trim());\n // ENG-10653: \"we waited and tmux never said\" and \"tmux said there is no exit\n // status\" are different facts, and only the first is a defect. A signal death\n // is a real status-less exit and keeps the plain wording; a wait that ran out\n // says so, and carries what it saw — otherwise the next occurrence of this\n // flake is as undiagnosable as the one that opened this issue.\n let unresolved = '';\n if (wait?.resolved === 'timeout') {\n unresolved = ` — tmux had not published one after ${wait.waitedMs}ms (${wait.polls} polls, ` +\n `${wait.readFailures} read failure(s), ` +\n `${finalBanner ? 'banner present' : 'no dead-pane banner'}` +\n `${wait.tmuxVersion ? `; ${wait.tmuxVersion}` : ''})`;\n } else if (wait?.resolved === 'session-gone') {\n // Also unanswered, just for a different reason, and it needs saying for\n // the same one: without it this reads as a plain status-less exit, which\n // is the bare-diff shape this whole change exists to remove.\n unresolved = ` — the tmux session disappeared after ${wait.waitedMs}ms ` +\n `(${wait.polls} polls) before tmux published one`;\n }\n const status = known ? `exit status ${exitStatus.trim()}` : `exit status unknown${unresolved}`;\n // ENG-10714: \"It printed nothing\" is a CLAIM about claude, and it must only\n // be made when the capture actually succeeded and came back empty — which,\n // after the bounded retry above, means several clean reads spread over a\n // second. If a capture FAILED we do not know what claude printed, and saying\n // it printed nothing is the same shape of confident falsehood ENG-10617\n // exists to remove: the operator stops looking for output that was there.\n const nothing = (wait?.captureFailures ?? 0) > 0\n ? `Its output could not be read back from the dead pane ` +\n `(${wait?.captureFailures} capture failure(s)), so it may have printed something. `\n : 'It printed nothing. ';\n return (\n `Claude Code exited on the host (${status}) before login finished. ` +\n (output ? `Its last output:\\n${output}\\n` : 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, opts: DeadPaneWaitOpts = {}): Promise<SessionError | null> {\n const read = async (): Promise<DeadPaneReading> => {\n const { stdout } = await execFileAsync('tmux', [\n 'display-message',\n '-p',\n '-t',\n session,\n // `|`-separated, not space-separated: an unset format expands to the\n // empty string, so splitting three fields on a space would shift them\n // left and read the signal as the status.\n '#{pane_dead}|#{pane_dead_status}|#{pane_dead_signal}',\n ]);\n const [dead = '', exitStatus = '', signal = ''] = stdout.trim().split('|');\n return { dead, exitStatus, signal };\n };\n let initial: DeadPaneReading;\n try {\n initial = await read();\n } catch (err) {\n return classifyTmuxError(err);\n }\n // Fast path: an alive pane costs exactly one tmux call and never captures.\n if (initial.dead !== '1') return null;\n const verdict = await awaitDeadPaneVerdict(\n { read, capture: () => capturePane(session, { scrollback: -50 }) },\n initial,\n opts,\n );\n if (verdict.resolved === 'timeout') {\n try {\n const { stdout } = await execFileAsync('tmux', ['-V']);\n verdict.tmuxVersion = stdout.trim();\n } catch { /* best-effort: the version is a diagnostic, not a requirement */ }\n }\n return { kind: 'claude-exited', message: describeClaudeExit(verdict.exitStatus, verdict.pane, verdict) };\n}\n\n// ---------------------------------------------------------------------------\n// Pair-scoped tmux session lifecycle\n// ---------------------------------------------------------------------------\n\n/**\n * ENG-10653: the dead-pane banner, pinned rather than inherited.\n *\n * The banner text is an OPTION (`remain-on-exit-format`, tmux 3.3+), not a\n * constant, and a pair session is created on whatever tmux server the host\n * already runs — so it inherits the host user's `~/.tmux.conf`. A host that\n * sets its own format silently disables BOTH readers of that banner:\n * ENG-10631's status fallback and ENG-10653's completion marker. Verified:\n * with `remain-on-exit-format \"child gone: #{pane_dead_status}\"` the pane\n * holds `child gone: 3` and `/^Pane is dead \\(/` matches nothing, while the\n * status sits in plain view. No error, no log — the shape this whole issue is\n * about, one level down.\n *\n * So the pair session states the format it intends to parse.\n *\n * The `#{!=:...,}` guard is not decoration, and tmux's own default carries it\n * for the same reason: `#{?pane_dead_status,...}` would test TRUTHINESS, and\n * tmux reads `0` as false — so a clean `exit 0` would render as a signal\n * death. Comparing against the empty string asks the question actually meant,\n * \"did tmux publish a status\".\n */\nexport const PAIR_REMAIN_ON_EXIT_FORMAT =\n 'Pane is dead (#{?#{!=:#{pane_dead_status},},status #{pane_dead_status},signal #{pane_dead_signal}})';\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 // ENG-10869 — the pair pane needs CLAUDE_CONFIG_DIR passed BY ARGV.\n //\n // `{ env: pairEnv }` below sets the environment of the tmux CLIENT, and a\n // tmux pane does not inherit that: it gets the SERVER's global environment,\n // merged with `-e` entries, plus only the names in `update-environment`\n // (ENG-8800 measured this on a live host — see the long note in\n // persistent-session.ts, which exists because a per-agent variable passed\n // this way leaked one agent's identity to every other agent on the box).\n //\n // The consequence here is specific and bad. Without the `-e`, `claude` in\n // this pane writes `<home>/.claude.json` while `finalizeClaudePairOnboarding`\n // polls `<configDir>/.claude.json` for an mtime that can never move. Every\n // pairing on every host then exhausts its dialog budget — which under\n // ENG-10862 is no longer a warning but a REPORTED FAILURE, an alert and an\n // instruction to start over, for pairings that actually worked. Meanwhile the\n // containers keep reading the pre-pair copy and the next spawn archives the\n // freshly-paired file.\n //\n // Only when the config dir already holds a usable config: on a host that has\n // never paired there is nothing there yet, and the bootstrap path is exactly\n // that claude writes the legacy file and the next spawn seeds from it.\n const pairConfigDir = claudeConfigDirFor();\n const pairConfigEnvArgs =\n inspectClaudeConfig(claudeJsonPathFor()) === 'usable'\n ? ['-e', `${CLAUDE_CONFIG_DIR_ENV}=${pairConfigDir}`]\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 ...pairConfigEnvArgs,\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 // ENG-10653: pin the banner, BEST-EFFORT AND DELIBERATELY NOT CHAINED.\n //\n // `remain-on-exit-format` is tmux 3.3+, and tmux exits 1 on an unknown\n // option while still creating the session — measured. Chained into the\n // new-session above, that would turn \"this host runs tmux 3.2\" into a failed\n // pair AND an orphaned session, trading a silent degradation for an outage.\n // Separate and swallowed, an old tmux simply keeps the host's banner, which\n // is exactly the behaviour it had before this change.\n //\n // The window this opens — claude dying between the two calls — resolves to\n // that same pre-existing behaviour, not to anything worse.\n try {\n await execFileAsync(\n 'tmux',\n ['set-option', '-w', '-t', session, 'remain-on-exit-format', PAIR_REMAIN_ON_EXIT_FORMAT],\n { env: pairEnv },\n );\n } catch { /* tmux < 3.3, or the option is gone: fall back to the host's banner */ }\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 * Has a USABLE config landed at `path` since `sinceMtimeMs`?\n *\n * Both halves matter: the mtime advance distinguishes this pairing's write from\n * a file left by a previous attempt, and the parse distinguishes a write from a\n * tear (ENG-10140 truncations are a plausible size).\n *\n * Exported because finalize is not the only thing that has to ask. The caller\n * re-asks before it reports a failure — see {@link confirmPairFailure} — and a\n * second hand-written copy of this rule is how the two would come to disagree\n * about what \"landed\" means.\n */\nexport function pairConfigLandedSince(path: string, sinceMtimeMs: number): boolean {\n try {\n if (!existsSync(path)) return false;\n if (statSync(path).mtimeMs <= sinceMtimeMs) return false;\n // USABLE, not merely parseable.\n //\n // This was `JSON.parse(...)` and that is a false success. Measured on\n // dominet 2026-09-22T21:26Z, on a host running the ENG-10862 fix: claude\n // wrote a 45KB config carrying a real `oauthAccount` for dom@dominet.com,\n // `hasCompletedOnboarding` ABSENT. It parsed, so finalize reported\n // \"onboarding persisted\" and all five agents stayed on the login picker.\n // That is the exact condition ENG-10862 exists to end, surviving its own\n // fix because the fix corrected WHEN the check runs and not WHAT it tests.\n //\n // It is not only a mis-report, it is a CAUSE. The log read\n // `iterations=1 dialogEnter=0 consumerTerms=0`: finalize saw the first\n // write, declared success, and tore down the pane having dismissed ZERO\n // dialogs — leaving claude sitting on the onboarding prompt that would have\n // written the flag. Requiring the flag keeps the loop pressing Enter until\n // onboarding actually completes, which is what the loop is for.\n //\n // `inspectClaudeConfig` is the same rule the config-dir readiness and mount\n // gates use. Two definitions of \"a good config\" in one codebase is how this\n // got through; there is now one.\n return inspectClaudeConfig(path) === 'usable';\n } catch {\n return false;\n }\n}\n\n/**\n * ENG-10862 — give a late write one last chance before calling a pairing failed.\n *\n * `finalizeClaudePairOnboarding` can return `send-failed` or `pane-gone`\n * IMMEDIATELY after its last config check: those branches abort on a tmux error\n * rather than falling through to the grace pass, so unlike `not-flushed` they\n * have observed the file exactly once, milliseconds earlier. A write already in\n * flight then lands after the verdict.\n *\n * That matters more than it used to. Before this issue the verdict was a log\n * line; now it gates the report, and `reportAndCleanup` POSTs the result BEFORE\n * its three-second beat — so a pairing that actually worked would raise a\n * `warning` alert and tell the operator to start over.\n *\n * So the caller waits and re-asks. The wait is NOT the same as the beat in\n * `reportAndCleanup`: that one exists to let Claude Code finish persisting\n * `.credentials.json` before the pane is killed, and it happens after the POST.\n * This one exists to make the POST correct.\n */\nexport async function confirmPairFailure(opts: {\n configPath: string;\n sinceMtimeMs: number;\n graceMs?: number;\n sleep?: (ms: number) => Promise<void>;\n log?: (msg: string) => void;\n}): Promise<'landed-after-all' | 'confirmed'> {\n const graceMs = opts.graceMs ?? 3_000;\n const nap = opts.sleep ?? sleep;\n const log = opts.log ?? (() => {});\n if (pairConfigLandedSince(opts.configPath, opts.sinceMtimeMs)) {\n log(`[claude-pair] a usable config was already present at ${opts.configPath} — not a failure`);\n return 'landed-after-all';\n }\n await nap(graceMs);\n if (pairConfigLandedSince(opts.configPath, opts.sinceMtimeMs)) {\n log(\n `[claude-pair] ${opts.configPath} landed during the ${graceMs}ms confirm window — ` +\n `the pairing worked after all, reporting success`,\n );\n return 'landed-after-all';\n }\n return 'confirmed';\n}\n\n/**\n * Why finalize concluded that onboarding did not persist. ENG-10862.\n *\n * One WARN covered all of these before, which is what made the alyve incident\n * unreadable: the message asserted a timeout (\"reached 10 iterations\") for a\n * host whose config was simply unwritable. Only `not-flushed` is a timeout.\n */\nexport type PairFinalizeFailure =\n /** The watched path holds a file that does not parse — the ENG-10140 fault. */\n | 'config-corrupt'\n /** Nothing at the watched path at all. Claude Code never wrote one. */\n | 'config-absent'\n /** A parseable config is there, but its mtime never advanced in the window. */\n | 'not-flushed'\n /**\n * A DIRECTORY sits where the config file belongs.\n *\n * Not hypothetical, and not a corruption: `docker run -v /nonexistent:/path`\n * creates a directory at the target, which is how `/root/.claude.json` became\n * one on isolated hosts. `readFileSync` gives EISDIR, so without its own\n * reason this reported `config-corrupt` and told the operator to re-pair —\n * which cannot fix it, so they would loop.\n */\n | 'config-path-is-directory'\n /** The pane died, so nothing was left that could write the file. */\n | 'pane-gone'\n /** tmux refused a keystroke we needed to send to clear a dialog. */\n | 'send-failed';\n\n/**\n * ENG-10862 AC5: how the dialog loop actually spent its iterations.\n *\n * The question \"do real pairings need more than 10 dismissals?\" was\n * unanswerable because nothing recorded the breakdown. Counts only — never the\n * pane text, which carries `Logged in as <email>`.\n */\nexport interface PairDialogCounts {\n /** ENG-10619 consumer-terms dialog answered (option 2, training OFF). */\n consumerTerms: number;\n /** A generic post-OAuth dialog dismissed with Enter. */\n dialogEnter: number;\n /** Pane matched no known dialog — iterations spent purely waiting. */\n noMatch: number;\n}\n\nexport interface PairFinalizeResult {\n finalized: boolean;\n /**\n * Dialog-loop iterations only, capped at `maxIterations`. Deliberately NOT\n * inclusive of the grace pass below, so the number keeps the meaning callers\n * and tests already depend on.\n */\n iterations: number;\n /** Grace-pass waits consumed (ENG-10862). Zero unless the dialog loop ran out. */\n graceWaits: number;\n counts: PairDialogCounts;\n /** Set if and only if `finalized` is false. */\n reason?: PairFinalizeFailure;\n /**\n * The config path this call actually watched. Returned so the caller can\n * name it in the operator-facing message without recomputing the rule — the\n * two would drift, and the symptom would be an error message about a file\n * the flow never looked at.\n */\n configPath: string;\n}\n\n/** Stable machine-readable code per failure, for `claude_pair_sessions.error_code`. */\nexport const PAIR_FINALIZE_ERROR_CODES: Record<PairFinalizeFailure, string> = {\n 'config-corrupt': 'onboarding_config_corrupt',\n 'config-absent': 'onboarding_config_absent',\n 'config-path-is-directory': 'onboarding_config_path_is_directory',\n 'not-flushed': 'onboarding_not_flushed',\n 'pane-gone': 'onboarding_pane_gone',\n 'send-failed': 'onboarding_send_failed',\n};\n\n/**\n * Operator-facing text for a finalize failure.\n *\n * **Every message says the OAuth tokens are valid**, because the pairing is now\n * reported as a `failure` and the console renders this string in a danger box.\n * Without that sentence the operator reads \"failed\" and goes hunting a token\n * fault that does not exist — the tokens were written to\n * `~/.claude/.credentials.json` before finalize ever ran. What failed is the\n * onboarding state that stops the next agent spawn landing on the login picker.\n */\nexport function describePairFinalizeFailure(\n reason: PairFinalizeFailure,\n configPath: string,\n): string {\n const valid =\n 'The OAuth tokens themselves are valid, so this is not a credential problem.';\n const retry = 'Start over to re-run pairing.';\n switch (reason) {\n case 'config-corrupt':\n // Deliberately carries NO ticket id: this string renders in the console,\n // which customers use to pair their own hosts, and they cannot read\n // Linear. The ENG-10140 pointer is in the host's log line instead, where\n // the audience is us.\n return `Signed in, but ${configPath} on this host is unreadable and Claude Code did not replace it, so agents here would start on the login picker. ${valid} ${retry} If it recurs on this host, contact support — the host's config is being corrupted by something outside pairing.`;\n case 'config-path-is-directory':\n // The remedy is genuinely different, so the message has to be too:\n // re-pairing cannot help until the directory is gone.\n return `Signed in, but ${configPath} on this host is a DIRECTORY, not a file, so Claude Code cannot write its config there and agents here would start on the login picker. ${valid} Re-pairing will NOT fix this on its own: remove that directory on the host, then pair again.`;\n case 'config-absent':\n return `Signed in, but Claude Code never wrote ${configPath}, so agents on this host would start on the login picker. ${valid} ${retry}`;\n case 'not-flushed':\n // Covers BOTH \"never written\" and the dominet case: written, parseable,\n // carrying a real account, with `hasCompletedOnboarding` absent. The old\n // wording claimed the file was not written, which on that host was false\n // and sent the operator looking for a disk problem.\n return `Signed in, but Claude Code did not finish onboarding on this host, so ${configPath} is not usable yet and agents here would start on the login picker. ${valid} ${retry}`;\n case 'pane-gone':\n return `Signed in, but the temporary Claude Code session on the host closed before it wrote ${configPath}, so agents here would start on the login picker. ${valid} ${retry}`;\n case 'send-failed':\n return `Signed in, but tmux refused a keystroke needed to clear Claude Code's post-login dialogs, so ${configPath} was never written and agents here would start on the login picker. ${valid} ${retry}`;\n }\n}\n\n/**\n * The `.claude.json` the pair flow watches.\n *\n * Exported so a caller can snapshot the SAME path finalize will watch. Two\n * independent spellings of this rule is how the baseline below would silently\n * stop applying.\n */\n/**\n * The path finalize watches, reconciling ENG-10862 and ENG-10869.\n *\n * An explicitly-set CLAUDE_CONFIG_DIR wins because the pair process inherits\n * it and WILL write there — including before that directory holds a usable\n * config, which is precisely the window `effectiveClaudeJsonPath()` still\n * resolves to the legacy path. Everywhere else, the shared resolution, so\n * finalize and the pane's environment cannot disagree about which file this\n * pairing is writing.\n */\nexport function finalizeWatchPath(env: NodeJS.ProcessEnv = process.env): string {\n if (env['CLAUDE_CONFIG_DIR']?.trim()) return pairClaudeJsonPath(env);\n return effectiveClaudeJsonPath();\n}\n\nexport function pairClaudeJsonPath(env: NodeJS.ProcessEnv = process.env): string {\n // CLAUDE_CONFIG_DIR FIRST, because that is what the pair process itself\n // obeys. `spawnPairSession` builds the pair env as `{...process.env, HOME}`,\n // so a CLAUDE_CONFIG_DIR in the manager's environment is inherited and\n // Claude Code writes `$CLAUDE_CONFIG_DIR/.claude.json`. Watching\n // `~/.claude.json` unconditionally would watch a file nothing writes.\n //\n // Before ENG-10862 that mismatch cost a WARN and a `success` report. It is\n // strictly worse now: `flushed()` never fires, the give-up classifier says\n // `config-absent`, the re-ask confirms against the same wrong path, and the\n // pairing reports `failure` — a page, plus a console box telling the\n // operator to start over, which re-runs the identical mismatch. Every\n // pairing on such a host would fail forever with no way out from the\n // console. Precedent for the precedence: `claudeConfigCandidatePaths` in\n // `claude-account-fingerprint.ts` puts CLAUDE_CONFIG_DIR first for the same\n // reason.\n const configDir = env['CLAUDE_CONFIG_DIR']?.trim();\n if (configDir) return join(configDir, '.claude.json');\n return join(homedir(), '.claude.json');\n}\n\n/** A path plus the mtime it had at some earlier instant. */\nexport interface PairConfigBaseline {\n path: string;\n mtimeMs: number;\n}\n\n/**\n * ENG-10862 — snapshot the config's mtime BEFORE the auth code is submitted.\n *\n * ## Why the caller has to take this, and finalize cannot\n *\n * finalize runs only after `submitClaudePairCode` has seen the success marker\n * in the pane, so a snapshot taken at its entry is already too late: Claude\n * Code may have written the config between the marker appearing and finalize\n * starting, and those are concurrent events, not ordered ones. The mtime has\n * then ALREADY advanced, finalize compares against the post-write value, sees\n * no further change, and reports a failure for a pairing that worked.\n *\n * Under the old code that produced a WARN and a `success` report, so the\n * mistake was invisible. Now that the report is gated on this verdict it would\n * be a page, a red console and an instruction to start over, against a host\n * that is fine — a strictly worse failure than the one being fixed, and\n * introduced by fixing it.\n */\nexport function snapshotPairConfig(claudeJsonPath = finalizeWatchPath()): PairConfigBaseline {\n return {\n path: claudeJsonPath,\n mtimeMs: existsSync(claudeJsonPath) ? statSync(claudeJsonPath).mtimeMs : 0,\n };\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 * ## ENG-10862: the success check has to come FIRST\n *\n * This loop used to stat the config at the BOTTOM, after the dialog matchers —\n * and both matcher branches `continue`. The matchers are deliberately loose\n * (claude's wording shifts between versions) and `Logged in as <email>` stays\n * visible in the pane after login, so on any run where that text persisted the\n * loop sent Enter ten times and **never looked at the file at all**. Exhausting\n * the cap was then guaranteed whether or not Claude Code had written the\n * config, and the give-up message blamed a timeout either way.\n *\n * That is the alyve-host-1 failure of 2026-09-21: one pairing in three gave up\n * at the cap, four agents latched on the login picker for up to 9h50m, and the\n * only signal was a WARN nobody read for nine hours. The runs that succeed log\n * `after 3 dialog dismissal(s)`, which establishes that the pane *usually*\n * clears by the third iteration — not that it must.\n *\n * So the file is checked at the top of every iteration, before any matcher runs\n * and before the pane-liveness probe: a config that landed is a success even if\n * the pane has since died, and no amount of residual dialog text can mask it.\n *\n * ## Success now requires the file to PARSE, not merely to change\n *\n * mtime-only was the old contract, on the stated grounds that we trust claude\n * to write a coherent file. ENG-10140 falsified that: on a containerised host\n * the shared config is routinely torn by concurrent truncate-in-place writes,\n * and an mtime that advances onto a corrupt file is not onboarding having\n * succeeded — it is the next spawn's login picker with a fresher timestamp. A\n * transient mid-write read is covered by simply continuing to poll.\n *\n * ## The grace pass, and where its budget comes from\n *\n * `maxIterations` reads like a dialog count and is really a timer: 10 × 1500ms\n * is a **15-second** total budget. When it runs out, this makes one longer,\n * quieter pass before returning, because the caller's next act is to kill the\n * pane and the pane is the only thing left that can still write the file. The\n * grace pass sends no blind Enter — ten have already gone — but it does keep\n * answering the consumer-terms dialog, since a bare Enter on that one opts the\n * account into model training (ENG-10619) and leaving it unanswered is itself a\n * way to never get the write.\n *\n * **10 more intervals, i.e. 30s total.** An earlier version of this comment\n * derived that number from a heartbeat constraint, and the constraint does not\n * exist: it argued that because `processClaudePairSessions` is awaited inside\n * the refresh cycle, a longer finalize starves `last_seen_at` and a failing\n * pairing could make its own host look stale. ENG-7273 decoupled those —\n * `livenessTimer` is an independent 30s `setInterval` that POSTs\n * `/host/heartbeat` regardless of how long a cycle takes, and finalize only\n * ever awaits `sleep()` and `execFileAsync`, so the interval keeps firing\n * throughout. Reported by a reviewer, and confirmed against\n * `LIVENESS_HEARTBEAT_INTERVAL_MS` before this was rewritten.\n *\n * So the budget is a JUDGEMENT, not a derivation, and it should be read as one:\n * 30s is long enough to catch a write that the 15s dialog loop missed, short\n * enough that an operator watching the pairing UI is not left on a spinner, and\n * it delays only a pairing that is already failing. The real constraint is the\n * console's patience, and nobody has measured that. Anyone extending it is not\n * fighting a heartbeat bound — that reason was wrong.\n *\n * Nothing upstream races the longer window — checked rather than assumed. The\n * console polls `code_submitted` with a spinner and no deadline, and no cron\n * sweeps a stale pair row, so the only cost of waiting is the spinner running\n * longer on a pairing that was going to fail anyway.\n *\n * Returns `finalized` plus a `reason` the caller reports as a FAILED pairing.\n * The OAuth tokens are valid regardless, which every operator-facing message\n * says explicitly — see {@link describePairFinalizeFailure}.\n */\nexport async function finalizeClaudePairOnboarding(\n session: string,\n log: (msg: string) => void,\n opts: {\n maxIterations?: number;\n intervalMs?: number;\n claudeJsonPath?: string;\n graceIterations?: number;\n /**\n * ENG-10862: the mtime to measure against, taken by the caller BEFORE the\n * auth code was submitted. See {@link snapshotPairConfig} for why a\n * snapshot taken at entry here is already too late.\n */\n baseline?: PairConfigBaseline;\n } = {},\n): Promise<PairFinalizeResult> {\n const maxIterations = opts.maxIterations ?? 10;\n const graceIterations = opts.graceIterations ?? 10;\n const intervalMs = opts.intervalMs ?? 1500;\n // ENG-10869 x ENG-10862: which file finalize watches. BOTH sides of this\n // merge were needed; neither alone is correct.\n //\n // finalize polls ONE path's mtime to decide whether onboarding took, so it\n // has to watch the file `claude` is actually writing.\n //\n // - ENG-10869 resolves the config dir only once it holds a USABLE config,\n // falling back to the legacy file. That keeps finalize in step with the\n // pane's environment, which is gated on the same condition.\n // - ENG-10862 honours an explicitly-set CLAUDE_CONFIG_DIR, because\n // `spawnPairSession` builds the pair env as `{...process.env, HOME}` and\n // the pair process therefore INHERITS it.\n //\n // They disagree in exactly one window, and it is the window that matters:\n // CLAUDE_CONFIG_DIR set while the config dir does not YET hold a usable\n // config — the first pairing on a migrated host. There, `claude` writes into\n // the config dir while the effective resolution still points at the legacy\n // file, so finalize would watch a file nothing writes. Under ENG-10862 that\n // is not a WARN any more: it is a reported FAILURE, an alert, and a console\n // box telling the operator to start over, which re-runs the identical\n // mismatch. Every pairing on such a host, forever.\n //\n // So: an explicit CLAUDE_CONFIG_DIR wins outright, and the shared resolution\n // answers everywhere else.\n //\n // `opts.baseline?.path` is deliberately NOT in this chain, though the first\n // draft of this merge put it there. Deriving the watch path FROM the baseline\n // does two bad things at once. It silently adopts whatever file the caller\n // happened to snapshot — and `snapshotPairConfig()` defaulted to\n // `pairClaudeJsonPath()`, so on a migrated host whose MANAGER env carries no\n // CLAUDE_CONFIG_DIR it returns the legacy file while ENG-10869 hands the pane\n // `-e CLAUDE_CONFIG_DIR=<dir>` and `claude` writes the config dir: finalize\n // watching a file nothing writes, which is the exact failure both these\n // tickets exist to remove. And it makes the mismatch guard below unfalsifiable,\n // since a path derived from the baseline can never disagree with it.\n //\n // Resolve independently; let the guard compare. The caller's snapshot is\n // still what the mtime is measured against — that is ENG-10862's late-write\n // fix and it is untouched — but only once the two agree about the file.\n const claudeJsonPath = opts.claudeJsonPath ?? finalizeWatchPath();\n\n // The mtime we measure against. Existence alone will not do: a stale file\n // from a previous pairing would satisfy that on the first iteration.\n //\n // The caller's baseline wins when it describes the same path, because it was\n // taken before the code was submitted and this one is taken after the login\n // already happened. A baseline for a DIFFERENT path is refused rather than\n // quietly ignored — ignoring it would reinstate exactly the too-late\n // snapshot it exists to prevent, with nothing to show for it.\n let initialMtime: number;\n if (opts.baseline && opts.baseline.path === claudeJsonPath) {\n initialMtime = opts.baseline.mtimeMs;\n } else {\n if (opts.baseline) {\n log(\n `[claude-pair] finalize: IGNORING a baseline for ${opts.baseline.path} — this run watches ` +\n `${claudeJsonPath}. A write that landed before the code was submitted may now read as a failure.`,\n );\n }\n initialMtime = existsSync(claudeJsonPath) ? statSync(claudeJsonPath).mtimeMs : 0;\n }\n\n const counts: PairDialogCounts = { consumerTerms: 0, dialogEnter: 0, noMatch: 0 };\n\n /**\n * Has claude written a USABLE config since we entered? Both halves matter:\n * the mtime advance distinguishes this pairing's write from a stale file left\n * by a previous attempt, and the parse distinguishes a write from a tear.\n */\n // Delegated, not reimplemented: the caller re-asks the same question before it\n // reports a failure, and two copies of \"did a usable config land\" would drift.\n // A parse failure here is either a mid-write or a genuine tear; keep polling,\n // and let the give-up classification below re-read and say which.\n const flushed = (): boolean => pairConfigLandedSince(claudeJsonPath, initialMtime);\n\n /** Fresh observation of the watched path, made at give-up time. */\n const classifyAtGiveUp = (): PairFinalizeFailure => {\n if (!existsSync(claudeJsonPath)) return 'config-absent';\n try {\n // Checked BEFORE the read, because a directory throws EISDIR from\n // `readFileSync` and would otherwise be reported as corruption — with a\n // remedy (re-pair) that cannot clear it.\n if (statSync(claudeJsonPath).isDirectory()) return 'config-path-is-directory';\n JSON.parse(readFileSync(claudeJsonPath, 'utf-8'));\n return 'not-flushed';\n } catch {\n return 'config-corrupt';\n }\n };\n\n const describeCounts = () =>\n `consumerTerms=${counts.consumerTerms} dialogEnter=${counts.dialogEnter} noMatch=${counts.noMatch}`;\n\n for (let i = 0; i < maxIterations; i++) {\n await sleep(intervalMs);\n\n // ENG-10862: FIRST, before the matchers and before the liveness probe. A\n // config that landed is a success even if the pane has since exited, and\n // residual dialog text must not be able to skip this.\n if (flushed()) {\n log(\n `[claude-pair] finalize: ${claudeJsonPath} updated (after ${i + 1} dialog dismissal(s); ${describeCounts()})`,\n );\n return { finalized: true, iterations: i + 1, graceWaits: 0, counts, configPath: claudeJsonPath };\n }\n\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, graceWaits: 0, counts, reason: 'pane-gone', configPath: claudeJsonPath };\n }\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n const classified = classifyTmuxError(err);\n log(`[claude-pair] finalize: capture-pane failed (${classified.kind}); aborting onboarding flow`);\n return { finalized: false, iterations: i, graceWaits: 0, counts, reason: 'pane-gone', configPath: claudeJsonPath };\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)) {\n counts.consumerTerms += 1;\n continue;\n }\n } catch {\n log('[claude-pair] finalize: send-keys failed on consumer-terms dialog; aborting onboarding flow');\n return { finalized: false, iterations: i, graceWaits: 0, counts, reason: 'send-failed', configPath: claudeJsonPath };\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 wait out this iteration\n // and re-check the file at the top of the next one.\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, graceWaits: 0, counts, reason: 'send-failed', configPath: claudeJsonPath };\n }\n counts.dialogEnter += 1;\n continue;\n }\n\n counts.noMatch += 1;\n }\n\n // ENG-10862 AC4: one longer, quieter pass before the caller kills the pane.\n // No blind Enter here (ten have gone already); consumer-terms is still\n // answered because leaving it up is itself a reason the write never comes.\n log(\n `[claude-pair] finalize: ${maxIterations} dialog iterations elapsed (${describeCounts()}); waiting up to ${graceIterations} more intervals for the config`,\n );\n let graceWaits = 0;\n /**\n * Why the grace pass stopped early, if it did.\n *\n * It outranks the OBSERVED state for everything except a config that names a\n * specific fault. \"The session died before writing\" tells the operator\n * something that neither \"no config\" nor \"not written in time\" does.\n *\n * The first version let it win only over `config-absent`, which threw it away\n * on every host that has ever been paired: those have a parseable\n * `~/.claude.json`, so `classifyAtGiveUp()` returns `not-flushed` and a pane\n * that DIED during the grace pass was reported as a timeout — with the\n * message \"Claude Code did not write … within the onboarding window\" for a\n * session that had exited, and the wrong kind in the alert payload. The main\n * loop reports `pane-gone` for the identical event, so the two paths\n * disagreed. That is the timeout-for-everything conflation this type exists\n * to remove; `PairFinalizeFailure`'s own docblock says only `not-flushed` is\n * a timeout.\n *\n * A CORRUPT config, or a DIRECTORY at the path, still wins: those name an\n * actionable fault the pane's fate does not explain, and each carries its own\n * remedy.\n */\n let graceAborted: PairFinalizeFailure | null = null;\n for (let g = 0; g < graceIterations; g++) {\n await sleep(intervalMs);\n graceWaits = g + 1;\n if (flushed()) {\n log(\n `[claude-pair] finalize: ${claudeJsonPath} updated during grace pass (wait ${graceWaits}; ${describeCounts()})`,\n );\n return { finalized: true, iterations: maxIterations, graceWaits, counts, configPath: claudeJsonPath };\n }\n const dead = await checkPairPaneAlive(session);\n if (dead) {\n // Nothing is left that could write the file; waiting longer is theatre.\n log(`[claude-pair] finalize: pair session ended during grace pass (${dead.kind})`);\n graceAborted = 'pane-gone';\n break;\n }\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch {\n graceAborted = 'pane-gone';\n break;\n }\n try {\n if (await answerConsumerTermsDialog(session, pane)) counts.consumerTerms += 1;\n } catch {\n graceAborted = 'send-failed';\n break;\n }\n }\n\n const observed = classifyAtGiveUp();\n // A config that names its own fault wins; otherwise an abort beats the\n // observed state, including `not-flushed`.\n const selfExplaining = observed === 'config-corrupt' || observed === 'config-path-is-directory';\n const reason = !selfExplaining && graceAborted ? graceAborted : observed;\n // The ticket pointers live HERE, not in the operator-facing message: this\n // line goes to manager.log, whose audience is us.\n const hint =\n reason === 'config-corrupt'\n ? ' — see ENG-10140 (shared-config corruption on containerised hosts)'\n : reason === 'config-path-is-directory'\n ? ' — a docker bind created a directory at this path; see ENG-10869'\n : '';\n log(\n `[claude-pair] finalize: giving up after ${maxIterations} dialog iterations + ${graceWaits} grace wait(s) — ` +\n `reason=${reason} (${describeCounts()}); ${claudeJsonPath} was not updated${hint}`,\n );\n return { finalized: false, iterations: maxIterations, graceWaits, counts, reason, configPath: claudeJsonPath };\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":[]}