@integrity-labs/agt-cli 0.15.11 → 0.15.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/agt.js CHANGED
@@ -46,7 +46,7 @@ import {
46
46
  success,
47
47
  table,
48
48
  warn
49
- } from "../chunk-LYWDRDJZ.js";
49
+ } from "../chunk-UHWDSCQX.js";
50
50
 
51
51
  // src/bin/agt.ts
52
52
  import { join as join10 } from "path";
@@ -3730,7 +3730,7 @@ import { execFileSync, execSync } from "child_process";
3730
3730
  import { existsSync as existsSync5, realpathSync } from "fs";
3731
3731
  import chalk17 from "chalk";
3732
3732
  import ora15 from "ora";
3733
- var cliVersion = true ? "0.15.11" : "dev";
3733
+ var cliVersion = true ? "0.15.13" : "dev";
3734
3734
  async function fetchLatestVersion() {
3735
3735
  const host2 = getHost();
3736
3736
  if (!host2) return null;
@@ -4179,7 +4179,7 @@ function handleError(err) {
4179
4179
  }
4180
4180
 
4181
4181
  // src/bin/agt.ts
4182
- var cliVersion2 = true ? "0.15.11" : "dev";
4182
+ var cliVersion2 = true ? "0.15.13" : "dev";
4183
4183
  var program = new Command();
4184
4184
  program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
4185
4185
  program.hook("preAction", (thisCommand) => {
@@ -7270,4 +7270,4 @@ export {
7270
7270
  managerStopCommand,
7271
7271
  managerStatusCommand
7272
7272
  };
7273
- //# sourceMappingURL=chunk-LYWDRDJZ.js.map
7273
+ //# sourceMappingURL=chunk-UHWDSCQX.js.map
@@ -61,8 +61,8 @@ function classifyTmuxError(err) {
61
61
 
62
62
  // src/lib/claude-pair-runtime.ts
63
63
  var execFileAsync = promisify(execFile);
64
- function tmuxSessionFor(codeName) {
65
- return `agt-${codeName}`;
64
+ function pairTmuxSession(pairId) {
65
+ return `agt-pair-${pairId.slice(0, 12)}`;
66
66
  }
67
67
  async function capturePane(session, opts = {}) {
68
68
  const scrollback = opts.scrollback ?? -200;
@@ -82,15 +82,78 @@ async function sendKeys(session, ...keys) {
82
82
  async function sleep(ms) {
83
83
  return new Promise((r) => setTimeout(r, ms));
84
84
  }
85
+ async function spawnPairSession(session) {
86
+ try {
87
+ await execFileAsync("tmux", ["has-session", "-t", session]);
88
+ return { ok: true };
89
+ } catch {
90
+ }
91
+ const { resolveClaudeBinary } = await import("./persistent-session-Q4X2KRS6.js");
92
+ const claudeBin = resolveClaudeBinary();
93
+ try {
94
+ await execFileAsync("tmux", [
95
+ "new-session",
96
+ "-d",
97
+ "-s",
98
+ session,
99
+ claudeBin
100
+ ]);
101
+ return { ok: true };
102
+ } catch (err) {
103
+ return { ok: false, error: classifyTmuxError(err) };
104
+ }
105
+ }
106
+ async function killPairSession(session) {
107
+ try {
108
+ await execFileAsync("tmux", ["kill-session", "-t", session]);
109
+ } catch {
110
+ }
111
+ }
85
112
  async function startClaudePair(opts) {
86
- const session = tmuxSessionFor(opts.codeName);
87
- const timeoutMs = opts.timeoutMs ?? 15e3;
113
+ const { session } = opts;
114
+ const timeoutMs = opts.timeoutMs ?? 6e4;
88
115
  const pollIntervalMs = opts.pollIntervalMs ?? 500;
89
116
  try {
90
117
  await execFileAsync("tmux", ["has-session", "-t", session]);
91
118
  } catch (err) {
92
119
  return { kind: "error", error: classifyTmuxError(err) };
93
120
  }
121
+ const promptDeadline = Date.now() + Math.min(45e3, timeoutMs);
122
+ let promptReady = false;
123
+ while (Date.now() < promptDeadline) {
124
+ await sleep(pollIntervalMs);
125
+ let pane;
126
+ try {
127
+ pane = await capturePane(session);
128
+ } catch (err) {
129
+ return { kind: "error", error: classifyTmuxError(err) };
130
+ }
131
+ if (/Do you trust the files in this folder\?/i.test(pane)) {
132
+ try {
133
+ await sendKeys(session, "C-m");
134
+ } catch {
135
+ }
136
+ continue;
137
+ }
138
+ if (/[│|]\s*>\s/.test(pane) || /Try .+ to .+/.test(pane) || /^\s*>\s*$/m.test(pane)) {
139
+ promptReady = true;
140
+ break;
141
+ }
142
+ }
143
+ if (!promptReady) {
144
+ let pane = "";
145
+ try {
146
+ pane = await capturePane(session);
147
+ } catch {
148
+ }
149
+ return {
150
+ kind: "error",
151
+ error: {
152
+ kind: "unknown",
153
+ message: `claude prompt never became interactive within ${promptDeadline - (Date.now() - timeoutMs)}ms. Last pane: ${pane.slice(-500)}`
154
+ }
155
+ };
156
+ }
94
157
  try {
95
158
  await sendKeys(session, "/login", "C-m");
96
159
  } catch (err) {
@@ -113,7 +176,7 @@ async function startClaudePair(opts) {
113
176
  return { kind: "timeout" };
114
177
  }
115
178
  async function submitClaudePairCode(opts) {
116
- const session = tmuxSessionFor(opts.codeName);
179
+ const { session } = opts;
117
180
  const timeoutMs = opts.timeoutMs ?? 3e4;
118
181
  const pollIntervalMs = opts.pollIntervalMs ?? 500;
119
182
  if (!opts.code || !opts.code.trim()) {
@@ -154,8 +217,7 @@ async function submitClaudePairCode(opts) {
154
217
  }
155
218
  return { kind: "timeout" };
156
219
  }
157
- async function getClaudePairStatus(codeName) {
158
- const session = tmuxSessionFor(codeName);
220
+ async function getClaudePairStatus(session) {
159
221
  try {
160
222
  await execFileAsync("tmux", ["has-session", "-t", session]);
161
223
  } catch (err) {
@@ -180,8 +242,10 @@ async function getClaudePairStatus(codeName) {
180
242
  }
181
243
  export {
182
244
  getClaudePairStatus,
245
+ killPairSession,
246
+ pairTmuxSession,
247
+ spawnPairSession,
183
248
  startClaudePair,
184
- submitClaudePairCode,
185
- tmuxSessionFor
249
+ submitClaudePairCode
186
250
  };
187
- //# sourceMappingURL=claude-pair-runtime-WGIKIPJV.js.map
251
+ //# sourceMappingURL=claude-pair-runtime-CM2NOTAE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/claude-pair-runtime.ts","../src/lib/claude-pair-parser.ts"],"sourcesContent":["/**\n * ENG-4580: manager-side runtime for the Claude Code OAuth pairing flow.\n *\n * These functions own the actual tmux dance — sending `/login`,\n * polling the pane until Claude Code prints the OAuth URL, sending\n * the auth code, and detecting success/failure. They are the\n * counterpart to the pure parser in `claude-pair-parser.ts`.\n *\n * The API surface in ENG-4581 will wrap these — they don't include\n * any HTTP / DB code so the unit tests can target the parser layer\n * without spinning up a fake API. Errors are classified into the\n * `SessionError` shape so the API can translate them into structured\n * 4xx responses (e.g. `session_missing`).\n *\n * Architectural note: tmux capture-pane on a session that doesn't\n * exist exits non-zero with `can't find session`. Same goes for tmux\n * not being installed. classifyTmuxError covers both.\n */\n\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nimport {\n classifyTmuxError,\n detectAuthOutcome,\n extractOAuthUrl,\n isUrlPromptReady,\n type AuthOutcome,\n type SessionError,\n} from './claude-pair-parser.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// Pair-scoped tmux session lifecycle\n// ---------------------------------------------------------------------------\n\n/**\n * Spawn a throwaway tmux session running `claude` for the pairing flow.\n * Idempotent — if the session already exists, returns success silently.\n * Caller is responsible for `killPairSession` once the pair reaches a\n * terminal state.\n *\n * Uses an absolute path to the claude binary so we don't depend on the\n * inherited PATH (the manager runs with cloud-init's minimal env on\n * EC2). resolveClaudeBinary checks CLAUDE_PATH, then `which`, then\n * canonical Linux/macOS Homebrew install dirs.\n */\nexport async function spawnPairSession(session: string): Promise<{ ok: true } | { ok: false; error: SessionError }> {\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n return { ok: true };\n } catch {\n // session doesn't exist yet — fall through to create it\n }\n\n const { resolveClaudeBinary } = await import('./persistent-session.js');\n const claudeBin = resolveClaudeBinary();\n\n try {\n await execFileAsync('tmux', [\n 'new-session',\n '-d',\n '-s',\n session,\n claudeBin,\n ]);\n return { ok: true };\n } catch (err) {\n return { ok: false, error: classifyTmuxError(err) };\n }\n}\n\nexport async function killPairSession(session: string): Promise<void> {\n try {\n await execFileAsync('tmux', ['kill-session', '-t', session]);\n } catch {\n // Best-effort cleanup; missing-session errors are fine.\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// 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 // Wait for claude to reach the interactive prompt before sending\n // `/login`. Cold-start splashes (\"Welcome to Claude Code\") and\n // \"Trust this directory?\" prompts can swallow keystrokes if we\n // send too early. Look for the prompt indicator (`>`) or the\n // explicit \"Try\" / \"Type\" hint Claude prints on the input line.\n const promptDeadline = Date.now() + Math.min(45_000, timeoutMs);\n let promptReady = false;\n while (Date.now() < promptDeadline) {\n await sleep(pollIntervalMs);\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n // Auto-accept \"Trust this folder?\" if we land on it.\n if (/Do you trust the files in this folder\\?/i.test(pane)) {\n try { await sendKeys(session, 'C-m'); } catch { /* keep polling */ }\n continue;\n }\n // The interactive input box draws with `>` followed by either a\n // cursor or the literal \"Try ...\" / \"Type ...\" hint that Claude\n // shows when idle.\n if (/[│|]\\s*>\\s/.test(pane) || /Try .+ to .+/.test(pane) || /^\\s*>\\s*$/m.test(pane)) {\n promptReady = true;\n break;\n }\n }\n if (!promptReady) {\n // Surface the pane in the error message — TUI scraping is brittle;\n // operators need to see what was actually on screen when we gave up.\n let pane = '';\n try { pane = await capturePane(session); } catch { /* best-effort */ }\n return {\n kind: 'error',\n error: {\n kind: 'unknown',\n message: `claude prompt never became interactive within ${promptDeadline - (Date.now() - timeoutMs)}ms. Last pane: ${pane.slice(-500)}`,\n },\n };\n }\n\n // Send the slash command. Most TUIs need a deliberate Enter after\n // the literal slash text to submit; tmux's Enter token is `C-m`.\n try {\n await sendKeys(session, '/login', 'C-m');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Poll until both the URL and \"Paste code here\" prompt are visible.\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n await sleep(pollIntervalMs);\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n if (isUrlPromptReady(pane)) {\n const url = extractOAuthUrl(pane);\n if (url) return { kind: 'url', url };\n }\n }\n return { kind: 'timeout' };\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 try {\n await sendKeys(session, opts.code.trim(), 'C-m');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n await sleep(pollIntervalMs);\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n const outcome: AuthOutcome = detectAuthOutcome(pane);\n if (outcome.kind === 'success') return { kind: 'success', rawMatch: outcome.rawMatch };\n if (outcome.kind === 'failure') return { kind: 'failure', rawMatch: outcome.rawMatch };\n }\n return { kind: 'timeout' };\n}\n\n// ---------------------------------------------------------------------------\n// status — non-mutating peek at the pane state\n// ---------------------------------------------------------------------------\n\nexport async function getClaudePairStatus(session: string): Promise<ClaudePairStatusResult> {\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n } catch (err) {\n const classified = classifyTmuxError(err);\n if (classified.kind === 'no-session') return { kind: 'session-missing' };\n return { kind: 'error', error: classified };\n }\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 */\nconst OAUTH_URL_RE =\n /https:\\/\\/(?:claude\\.ai|console\\.anthropic\\.com|auth\\.anthropic\\.com)\\/[^\\s)\\]]*/;\n\nexport function extractOAuthUrl(rawPane: string): string | null {\n const stripped = stripAnsi(rawPane);\n const match = OAUTH_URL_RE.exec(stripped);\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 stripped = stripAnsi(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.\n return OAUTH_URL_RE.test(stripped) && URL_PROMPT_RE.test(stripped);\n}\n\n// ---------------------------------------------------------------------------\n// Outcome detection after submitting the code\n// ---------------------------------------------------------------------------\n\nconst SUCCESS_RE =\n /(?:Logged in|Successfully (?:logged in|authenticated)|Authentication successful)/i;\nconst FAILURE_RE =\n /(?:Invalid (?:code|authorization code)|Authentication failed|Error (?:logging in|during authentication)|Login failed)/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 | { kind: 'unknown'; message: string };\n\nexport function classifyTmuxError(err: unknown): SessionError {\n const msg = err instanceof Error ? err.message : String(err);\n if (/can't find session|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":[]}
@@ -22,7 +22,7 @@ import {
22
22
  resolveChannels,
23
23
  resolveDmTarget,
24
24
  wrapScheduledTaskPrompt
25
- } from "../chunk-LYWDRDJZ.js";
25
+ } from "../chunk-UHWDSCQX.js";
26
26
  import {
27
27
  findTaskByTemplate,
28
28
  getProjectDir,
@@ -5250,14 +5250,42 @@ async function processClaudePairSessions(agents) {
5250
5250
  const codeNameByAgentId = new Map(agents.map((a) => [a.agentId, a.codeName]));
5251
5251
  const pendingResp = await api.post("/host/claude-pair/pending", { agent_ids: agentIds });
5252
5252
  if (!pendingResp.pending || pendingResp.pending.length === 0) return;
5253
- const { startClaudePair, submitClaudePairCode } = await import("../claude-pair-runtime-WGIKIPJV.js");
5253
+ const {
5254
+ startClaudePair,
5255
+ submitClaudePairCode,
5256
+ spawnPairSession,
5257
+ killPairSession,
5258
+ pairTmuxSession
5259
+ } = await import("../claude-pair-runtime-CM2NOTAE.js");
5260
+ const TERMINAL = /* @__PURE__ */ new Set([
5261
+ "success",
5262
+ "failure",
5263
+ "session_missing",
5264
+ "timeout"
5265
+ ]);
5266
+ async function reportAndCleanup(pairId, body) {
5267
+ await api.post("/host/claude-pair/result", { pair_id: pairId, ...body });
5268
+ if (typeof body.status === "string" && TERMINAL.has(body.status)) {
5269
+ await killPairSession(pairTmuxSession(pairId));
5270
+ }
5271
+ }
5254
5272
  for (const session of pendingResp.pending) {
5255
- const codeName = codeNameByAgentId.get(session.agent_id);
5256
- if (!codeName) continue;
5273
+ const codeName = codeNameByAgentId.get(session.agent_id) ?? "<unassigned>";
5274
+ const pairSession = pairTmuxSession(session.pair_id);
5257
5275
  try {
5258
5276
  if (session.status === "initiating") {
5259
- log(`[claude-pair] dispatching /login for '${codeName}' (pair ${session.pair_id.slice(0, 8)})`);
5260
- const result = await startClaudePair({ codeName });
5277
+ log(`[claude-pair] spawning pair session ${pairSession} for '${codeName}'`);
5278
+ const spawn = await spawnPairSession(pairSession);
5279
+ if (!spawn.ok) {
5280
+ await reportAndCleanup(session.pair_id, {
5281
+ status: "failure",
5282
+ error_code: spawn.error.kind,
5283
+ error_message: spawn.error.kind === "unknown" ? spawn.error.message : void 0
5284
+ });
5285
+ continue;
5286
+ }
5287
+ log(`[claude-pair] dispatching /login (pair ${session.pair_id.slice(0, 8)})`);
5288
+ const result = await startClaudePair({ session: pairSession });
5261
5289
  if (result.kind === "url") {
5262
5290
  await api.post("/host/claude-pair/result", {
5263
5291
  pair_id: session.pair_id,
@@ -5265,47 +5293,42 @@ async function processClaudePairSessions(agents) {
5265
5293
  url: result.url
5266
5294
  });
5267
5295
  } else if (result.kind === "timeout") {
5268
- await api.post("/host/claude-pair/result", {
5269
- pair_id: session.pair_id,
5296
+ await reportAndCleanup(session.pair_id, {
5270
5297
  status: "timeout",
5271
5298
  error_code: "url_prompt_not_seen",
5272
5299
  error_message: "Claude Code did not print the URL prompt within the deadline"
5273
5300
  });
5274
5301
  } else {
5275
5302
  const errKind = result.error.kind;
5276
- await api.post("/host/claude-pair/result", {
5277
- pair_id: session.pair_id,
5303
+ await reportAndCleanup(session.pair_id, {
5278
5304
  status: errKind === "no-session" ? "session_missing" : "failure",
5279
5305
  error_code: errKind,
5280
5306
  error_message: errKind === "unknown" ? result.error.message : void 0
5281
5307
  });
5282
5308
  }
5283
5309
  } else if (session.status === "code_submitted" && session.code) {
5284
- log(`[claude-pair] submitting code for '${codeName}' (pair ${session.pair_id.slice(0, 8)})`);
5285
- const result = await submitClaudePairCode({ codeName, code: session.code });
5310
+ log(`[claude-pair] submitting code (pair ${session.pair_id.slice(0, 8)})`);
5311
+ const result = await submitClaudePairCode({
5312
+ session: pairSession,
5313
+ code: session.code
5314
+ });
5286
5315
  if (result.kind === "success") {
5287
- await api.post("/host/claude-pair/result", {
5288
- pair_id: session.pair_id,
5289
- status: "success"
5290
- });
5316
+ await reportAndCleanup(session.pair_id, { status: "success" });
5291
5317
  } else if (result.kind === "failure") {
5292
- await api.post("/host/claude-pair/result", {
5293
- pair_id: session.pair_id,
5318
+ await reportAndCleanup(session.pair_id, {
5294
5319
  status: "failure",
5295
5320
  error_code: "invalid_code",
5296
5321
  error_message: result.rawMatch
5297
5322
  });
5298
5323
  } else if (result.kind === "timeout") {
5299
- await api.post("/host/claude-pair/result", {
5300
- pair_id: session.pair_id,
5324
+ await reportAndCleanup(session.pair_id, {
5301
5325
  status: "timeout",
5302
5326
  error_code: "outcome_not_seen",
5303
5327
  error_message: "Claude Code did not show a success/failure marker after submitting the code"
5304
5328
  });
5305
5329
  } else {
5306
5330
  const errKind = result.error.kind;
5307
- await api.post("/host/claude-pair/result", {
5308
- pair_id: session.pair_id,
5331
+ await reportAndCleanup(session.pair_id, {
5309
5332
  status: errKind === "no-session" ? "session_missing" : "failure",
5310
5333
  error_code: errKind,
5311
5334
  error_message: errKind === "unknown" ? result.error.message : void 0