@integrity-labs/agt-cli 0.28.946 → 0.28.948

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.
@@ -0,0 +1,319 @@
1
+ // src/lib/claude-dialogs.ts
2
+ import { execFileSync } from "child_process";
3
+ function isLoginPickerVisible(screen) {
4
+ return screen.includes("Select login method") || screen.includes("Claude account with subscription") && screen.includes("Anthropic Console account");
5
+ }
6
+ var CONSUMER_TERMS_OFF_OPTION = "Accept terms \xB7 Help improve our AI models: OFF";
7
+ function isConsumerTermsDialogVisible(screen) {
8
+ return screen.includes("Updates to Consumer Terms") && screen.includes(CONSUMER_TERMS_OFF_OPTION);
9
+ }
10
+ function consumerTermsOffKeys(screen) {
11
+ if (!isConsumerTermsDialogVisible(screen)) return null;
12
+ return selectRowKeys(screen, CONSUMER_TERMS_OFF_OPTION);
13
+ }
14
+ function isResumeModeDialogVisible(screen) {
15
+ return screen.includes("Resume from summary") && screen.includes("Don't ask me again");
16
+ }
17
+ function isSessionFeedbackDialogVisible(screen) {
18
+ return screen.includes("How is Claude doing this session") && screen.includes("0: Dismiss");
19
+ }
20
+ var USAGE_LIMIT_SAFE_OPTION = "Stop and wait for limit to reset";
21
+ var USAGE_LIMIT_CONTINUE_OPTION = "Wait here, then continue automatically";
22
+ var USAGE_LIMIT_SAFE_OPTIONS = [
23
+ USAGE_LIMIT_CONTINUE_OPTION,
24
+ USAGE_LIMIT_SAFE_OPTION
25
+ ];
26
+ var USAGE_LIMIT_KNOWN_OPTIONS = [
27
+ ...USAGE_LIMIT_SAFE_OPTIONS,
28
+ "Switch to usage credits",
29
+ "Switch to Team plan",
30
+ // ENG-9006: the two-option variant Claude Code shipped ~2026-08-17. Filed
31
+ // with a wedged pane and four unanswered customer Telegram messages queued
32
+ // behind it.
33
+ "Upgrade your plan",
34
+ "Ask your admin for more usage"
35
+ ];
36
+ var USAGE_LIMIT_BLOCK_LINES = 6;
37
+ function optionRowDigit(line, label) {
38
+ const m = line.match(
39
+ new RegExp(
40
+ String.raw`^[^\S\n]*(?:❯[^\S\n]*)?(\d)\.[^\S\n]*` + label.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
41
+ )
42
+ );
43
+ return m?.[1] ?? null;
44
+ }
45
+ function focusedModalBlock(screen, blockLines) {
46
+ const lines = screen.split("\n");
47
+ let footer = -1;
48
+ for (let i = lines.length - 1; i >= 0; i--) {
49
+ if (lines[i].includes("Enter to confirm")) {
50
+ footer = i;
51
+ break;
52
+ }
53
+ }
54
+ if (footer < 0) return null;
55
+ return lines.slice(Math.max(0, footer - blockLines), footer);
56
+ }
57
+ var MCP_DIALOG_OPTIONS = [
58
+ "Use this and all future MCP servers in this project",
59
+ "Continue without using these MCP servers"
60
+ ];
61
+ function isMcpServersDialogVisible(screen) {
62
+ const block = focusedModalBlock(screen, USAGE_LIMIT_BLOCK_LINES);
63
+ if (!block) return false;
64
+ return block.some((l) => MCP_DIALOG_OPTIONS.some((label) => optionRowDigit(l, label) !== null));
65
+ }
66
+ var MCP_CHECKBOX_HEADER = /new MCP servers? found in this project/i;
67
+ var MCP_CHECKED_ROW = /^[^\S\n]*(?:❯[^\S\n]*)?\[[✓x]\][^\S\n]*\S/iu;
68
+ var MCP_CHECKBOX_BLOCK_LINES = 20;
69
+ function isMcpCheckboxDialogVisible(screen) {
70
+ const block = focusedModalBlock(screen, MCP_CHECKBOX_BLOCK_LINES);
71
+ if (!block) return false;
72
+ if (!block.some((l) => MCP_CHECKBOX_HEADER.test(l))) return false;
73
+ return block.some((l) => MCP_CHECKED_ROW.test(l));
74
+ }
75
+ function usageLimitModalBlock(screen) {
76
+ const lines = screen.split("\n");
77
+ let footer = -1;
78
+ for (let i = lines.length - 1; i >= 0; i--) {
79
+ if (lines[i].includes("Enter to confirm")) {
80
+ footer = i;
81
+ break;
82
+ }
83
+ }
84
+ if (footer < 0) return null;
85
+ const block = lines.slice(Math.max(0, footer - USAGE_LIMIT_BLOCK_LINES), footer);
86
+ const hasKnownOptionRow = block.some(
87
+ (l) => USAGE_LIMIT_KNOWN_OPTIONS.some((label) => optionRowDigit(l, label) !== null)
88
+ );
89
+ return hasKnownOptionRow ? block : null;
90
+ }
91
+ function isUsageLimitChoiceDialogVisible(screen) {
92
+ return usageLimitModalBlock(screen) !== null;
93
+ }
94
+ function findUsageLimitSafeOption(screen) {
95
+ const block = usageLimitModalBlock(screen);
96
+ if (!block) return null;
97
+ for (const label of USAGE_LIMIT_SAFE_OPTIONS) {
98
+ for (const line of block) {
99
+ const digit = optionRowDigit(line, label);
100
+ if (digit !== null) return { key: digit, label };
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ function findUsageLimitSafeOptionKey(screen) {
106
+ return findUsageLimitSafeOption(screen)?.key ?? null;
107
+ }
108
+ var CONSENT_BLOCK_LINES = 12;
109
+ function lastIndexWhere(xs, pred) {
110
+ for (let i = xs.length - 1; i >= 0; i--) {
111
+ if (pred(xs[i])) return i;
112
+ }
113
+ return -1;
114
+ }
115
+ function isCursorRow(line) {
116
+ return /^[^\S\n]*❯[^\S\n]+\S/.test(line);
117
+ }
118
+ function isOptionRowFor(line, label) {
119
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
120
+ return new RegExp(
121
+ String.raw`^[^\S\n]*(?:❯[^\S\n]*)?(?:\d\.[^\S\n]*)?` + escaped + String.raw`[^\S\n]*$`
122
+ ).test(line);
123
+ }
124
+ function selectRowKeys(screen, label) {
125
+ const block = focusedModalBlock(screen, CONSENT_BLOCK_LINES);
126
+ if (!block) return null;
127
+ const target = lastIndexWhere(block, (l) => isOptionRowFor(l, label));
128
+ if (target < 0) return null;
129
+ const digit = optionRowDigit(block[target], label);
130
+ if (digit !== null) return [digit, "Enter"];
131
+ const cursors = [];
132
+ for (let i = 0; i < block.length; i++) {
133
+ if (isCursorRow(block[i])) cursors.push(i);
134
+ }
135
+ if (cursors.length === 0) return null;
136
+ let cursor = cursors[0];
137
+ let tied = false;
138
+ for (const i of cursors.slice(1)) {
139
+ const d = Math.abs(i - target);
140
+ const best = Math.abs(cursor - target);
141
+ if (d < best) {
142
+ cursor = i;
143
+ tied = false;
144
+ } else if (d === best) {
145
+ tied = true;
146
+ }
147
+ }
148
+ if (tied) return null;
149
+ const distance = target - cursor;
150
+ if (distance === 0) return ["Enter"];
151
+ const step = distance > 0 ? "Down" : "Up";
152
+ return [...Array(Math.abs(distance)).fill(step), "Enter"];
153
+ }
154
+ function findUsageLimitResetHint(screen) {
155
+ const block = usageLimitModalBlock(screen);
156
+ if (!block) return null;
157
+ for (const line of block) {
158
+ if (optionRowDigit(line, USAGE_LIMIT_CONTINUE_OPTION) === null) continue;
159
+ const idx = line.indexOf(USAGE_LIMIT_CONTINUE_OPTION);
160
+ const tail = line.slice(idx + USAGE_LIMIT_CONTINUE_OPTION.length).trim();
161
+ const when = tail.replace(/^at\s+/i, "").trim();
162
+ if (!when) return null;
163
+ return when.slice(0, 40);
164
+ }
165
+ return null;
166
+ }
167
+ function isUnanswerableUsageLimitDialog(screen) {
168
+ return isUsageLimitChoiceDialogVisible(screen) && findUsageLimitSafeOptionKey(screen) === null;
169
+ }
170
+ function sweepDialogs(screen) {
171
+ if (isUsageLimitChoiceDialogVisible(screen)) {
172
+ const safe = findUsageLimitSafeOption(screen);
173
+ if (!safe) return null;
174
+ return {
175
+ kind: "usage-limit-choice",
176
+ // The log names the option ACTUALLY picked, not a fixed literal. With two
177
+ // selectable rows now, a constant string would make a fleet sweep unable
178
+ // to tell an agent that will resume by itself from one parked until a
179
+ // human touches it.
180
+ keys: [safe.key, "Enter"],
181
+ interKeyDelayMs: 300,
182
+ logMessage: `Auto-answered usage-limit choice dialog (picked '${safe.label}')`
183
+ };
184
+ }
185
+ const termsKeys = consumerTermsOffKeys(screen);
186
+ if (termsKeys) {
187
+ return {
188
+ kind: "consumer-terms",
189
+ keys: termsKeys,
190
+ interKeyDelayMs: 300,
191
+ logMessage: "Auto-answered consumer-terms dialog (picked 'Help improve our AI models: OFF')"
192
+ };
193
+ }
194
+ if (screen.includes("Choose the text style") || screen.includes("Dark mode") && screen.includes("Light mode")) {
195
+ return {
196
+ kind: "theme-picker",
197
+ keys: ["Enter"],
198
+ interKeyDelayMs: 0,
199
+ logMessage: "Auto-accepted theme picker"
200
+ };
201
+ }
202
+ const trustKeys = screen.includes("Yes, I trust this folder") ? selectRowKeys(screen, "Yes, I trust this folder") : null;
203
+ if (trustKeys) {
204
+ return {
205
+ kind: "folder-trust",
206
+ keys: trustKeys,
207
+ interKeyDelayMs: 300,
208
+ logMessage: "Auto-accepted folder trust"
209
+ };
210
+ }
211
+ const resumeKeys = isResumeModeDialogVisible(screen) ? selectRowKeys(screen, "Don't ask me again") : null;
212
+ if (resumeKeys) {
213
+ return {
214
+ kind: "resume-mode",
215
+ keys: resumeKeys,
216
+ interKeyDelayMs: 300,
217
+ logMessage: "Auto-dismissed resume-mode dialog (picked 'Don't ask me again')"
218
+ };
219
+ }
220
+ if (screen.includes("I am using this for local development")) {
221
+ return {
222
+ kind: "dev-channels",
223
+ keys: ["Enter"],
224
+ interKeyDelayMs: 0,
225
+ logMessage: "Auto-accepted dev channels"
226
+ };
227
+ }
228
+ if (isMcpServersDialogVisible(screen) || isMcpCheckboxDialogVisible(screen)) {
229
+ return {
230
+ kind: "mcp-servers",
231
+ keys: ["Enter"],
232
+ interKeyDelayMs: 0,
233
+ logMessage: "Auto-accepted MCP servers"
234
+ };
235
+ }
236
+ const bypassKeys = screen.includes("Yes, I accept") && screen.includes("Bypass Permissions") ? selectRowKeys(screen, "Yes, I accept") : null;
237
+ if (bypassKeys) {
238
+ return {
239
+ kind: "bypass-permissions",
240
+ keys: bypassKeys,
241
+ interKeyDelayMs: 300,
242
+ logMessage: "Auto-accepted bypass permissions"
243
+ };
244
+ }
245
+ if (isSessionFeedbackDialogVisible(screen)) {
246
+ return {
247
+ kind: "session-feedback",
248
+ keys: ["0"],
249
+ interKeyDelayMs: 0,
250
+ logMessage: "Auto-dismissed session-feedback dialog"
251
+ };
252
+ }
253
+ return null;
254
+ }
255
+ async function sendDialogKeys(tmuxSession, action) {
256
+ for (let i = 0; i < action.keys.length; i++) {
257
+ if (i > 0 && action.interKeyDelayMs > 0) {
258
+ await new Promise((r) => setTimeout(r, action.interKeyDelayMs));
259
+ }
260
+ execFileSync("tmux", ["send-keys", "-t", tmuxSession, action.keys[i]], {
261
+ stdio: "ignore"
262
+ });
263
+ }
264
+ }
265
+ function focusedConfirmDialogBlock(screen) {
266
+ const lines = screen.split("\n");
267
+ let footer = -1;
268
+ for (let i = lines.length - 1; i >= 0; i--) {
269
+ if (lines[i].includes("Enter to confirm")) {
270
+ footer = i;
271
+ break;
272
+ }
273
+ }
274
+ if (footer < 0) return null;
275
+ return lines.slice(Math.max(0, footer - USAGE_LIMIT_BLOCK_LINES), footer + 1);
276
+ }
277
+ function isUnclaimedConfirmDialog(screen) {
278
+ if (focusedConfirmDialogBlock(screen) === null) return false;
279
+ if (sweepDialogs(screen) !== null) return false;
280
+ if (isUnanswerableUsageLimitDialog(screen)) return false;
281
+ return true;
282
+ }
283
+ function simpleTextHash(s) {
284
+ let h = 0;
285
+ for (let i = 0; i < s.length; i++) {
286
+ h = (h << 5) - h + s.charCodeAt(i) | 0;
287
+ }
288
+ return h.toString(16);
289
+ }
290
+ function classifyUnanswerablePane(screen) {
291
+ if (!screen) return null;
292
+ if (isLoginPickerVisible(screen)) return "login-picker";
293
+ if (isUnanswerableUsageLimitDialog(screen)) return "usage-limit-unanswerable";
294
+ if (isUnclaimedConfirmDialog(screen)) return "unclaimed-modal";
295
+ return null;
296
+ }
297
+ function summarizeUnanswerablePane(screen) {
298
+ const block = focusedConfirmDialogBlock(screen);
299
+ if (!block) return null;
300
+ const flat = block.join("\n");
301
+ return { hash: simpleTextHash(flat), length: flat.length };
302
+ }
303
+
304
+ export {
305
+ isLoginPickerVisible,
306
+ consumerTermsOffKeys,
307
+ isResumeModeDialogVisible,
308
+ selectRowKeys,
309
+ findUsageLimitResetHint,
310
+ isUnanswerableUsageLimitDialog,
311
+ sweepDialogs,
312
+ sendDialogKeys,
313
+ focusedConfirmDialogBlock,
314
+ isUnclaimedConfirmDialog,
315
+ simpleTextHash,
316
+ classifyUnanswerablePane,
317
+ summarizeUnanswerablePane
318
+ };
319
+ //# sourceMappingURL=chunk-DHWNVVX4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/claude-dialogs.ts"],"sourcesContent":["/**\n * ENG-6017: shared Claude Code TUI dialog detection + dismissal.\n *\n * Extracted from persistent-session.ts's acceptDialogs() cascade so the\n * dialog knowledge has exactly one home, consumable from:\n *\n * - `acceptDialogs()` in persistent-session.ts — the spawn-time loop that\n * walks an agent through the first-run dialog cascade (theme → trust →\n * MCP → bypass …).\n * - `channel-input-watchdog.ts` — the per-poll-cycle watchdog that fires\n * Enter at stuck channel input. Before ENG-6017 the watchdog was\n * dialog-blind: when Claude Code's session-feedback dialog (\"How is\n * Claude doing this session?\") overlaid the pane, the watchdog's\n * single-shot Enter went into the dialog instead of the input box and\n * the inbound message sat unsubmitted for 40+ minutes (koda,\n * 2026-06-04) while every health metric stayed green.\n * - `injectMessageWithStatus()` — pre-send pane hygiene on the tmux\n * send-keys fallback path.\n *\n * Living in its own module (rather than persistent-session.ts) breaks the\n * import cycle persistent-session → channel-input-watchdog →\n * persistent-session that a shared export would otherwise create.\n *\n * DEFAULT-DENY: `sweepDialogs()` only ever returns an action for an\n * explicitly recognised dialog. An unknown dialog gets `null` — never a\n * blind Enter — because a future dialog's default option could be\n * destructive. Callers that want visibility into unknown overlays should\n * log the pane themselves.\n */\n\nimport { execFileSync } from 'node:child_process';\n\n/**\n * A recognised dialog plus the keystrokes that dismiss it.\n *\n * `keys` are tmux key names sent as individual `tmux send-keys` calls —\n * never batched into one call, so a multi-byte sequence can't get wrapped\n * into a single bracketed paste (the CSI-u trap documented on\n * defaultArmSender in persistent-session.ts).\n */\nexport interface DialogAction {\n kind:\n | 'theme-picker'\n | 'folder-trust'\n | 'resume-mode'\n | 'dev-channels'\n | 'mcp-servers'\n | 'bypass-permissions'\n | 'session-feedback'\n | 'usage-limit-choice'\n | 'consumer-terms';\n /** tmux key names, one send-keys invocation each. */\n keys: readonly string[];\n /** Delay between consecutive key sends (selector dialogs need a beat\n * between picking an option and confirming it). */\n interKeyDelayMs: number;\n /** Past-tense log fragment, e.g. \"Auto-accepted theme picker\". Callers\n * append their own context (`for '<codeName>'` etc.). */\n logMessage: string;\n}\n\n/**\n * Detect whether Claude Code is showing the **login picker** dialog.\n *\n * ENG-4634: this dialog appears when ~/.claude.json is missing or\n * Claude Code can't validate the saved session. Pressing Enter on\n * the default (1. Claude account with subscription) kicks off a\n * browser-based OAuth flow that an unattended agent can't complete —\n * the helper used to fall through to the generic `❯ no Enter to\n * confirm` exit branch and declare the session \"ready\" while the\n * actual claude REPL was still on the picker. Without explicit\n * detection, every manager respawn would silently flip the agent\n * back to the picker and never recover.\n *\n * Pattern matches the literal option strings claude renders. Both\n * 'Claude account with subscription' and 'Anthropic Console account'\n * are present on the picker (and not in the post-login UI), so the\n * conjunction is unambiguous.\n *\n * NOT part of sweepDialogs() — the picker must never be keyed past\n * (it needs an operator OAuth); acceptDialogs() handles it specially.\n */\nexport function isLoginPickerVisible(screen: string): boolean {\n return (\n screen.includes('Select login method') ||\n (screen.includes('Claude account with subscription') &&\n screen.includes('Anthropic Console account'))\n );\n}\n\n/**\n * ENG-10619: the row we ALWAYS pick on Claude Code's consumer-terms dialog.\n *\n * Updates to Consumer Terms and Policies\n * …\n * ❯ 1. Accept terms · Help improve our AI models: ON\n * 2. Accept terms · Help improve our AI models: OFF\n *\n * Enter to confirm · Esc to cancel\n *\n * The cursor defaults to ON, which opts the account's chats and coding\n * sessions into model training and extends retention from 30 days to 5\n * years. A bare Enter would therefore consent on the customer's behalf, so\n * this row is selected by NAME (selectRowKeys) and never by position or\n * default. The `·` is U+00B7, the same separator Claude Code uses in its\n * confirm footer.\n */\nexport const CONSUMER_TERMS_OFF_OPTION =\n 'Accept terms \\u00b7 Help improve our AI models: OFF';\n\n/**\n * ENG-10619: the consumer-terms dialog is on screen. Keyed on the\n * conjunction of the heading and the OFF row's own text so prose that\n * mentions either alone does not match; selectRowKeys still requires a\n * focused modal and a complete option row before anything is keyed.\n */\nexport function isConsumerTermsDialogVisible(screen: string): boolean {\n return (\n screen.includes('Updates to Consumer Terms') &&\n screen.includes(CONSUMER_TERMS_OFF_OPTION)\n );\n}\n\n/**\n * ENG-10619: keys that pick \"Help improve our AI models: OFF\" on a focused\n * consumer-terms dialog, or null (answer nothing) when that row cannot be\n * resolved. Shared by sweepDialogs() and the Claude pair/re-login flow,\n * which drives its own dialog loop.\n */\nexport function consumerTermsOffKeys(screen: string): readonly string[] | null {\n if (!isConsumerTermsDialogVisible(screen)) return null;\n return selectRowKeys(screen, CONSUMER_TERMS_OFF_OPTION);\n}\n\n/**\n * Detect Claude Code's resume-mode dialog (ENG-5364).\n *\n * On `claude --resume <uuid>` against a transcript large enough to\n * trigger Claude Code 2.1.x's context-management heuristic, the agent\n * lands on an interactive picker offering:\n *\n * ❯ 1. Resume from summary (recommended)\n * 2. Resume full session as-is\n * 3. Don't ask me again\n *\n * Without auto-dismissal the agent sits silently waiting for keyboard\n * input on every manager respawn — channel inbounds stack up while\n * health metrics stay green. Surfaced fleet-wide on 2026-05-20\n * (don/stirling/maven hit it on a single manager restart).\n *\n * Match on the conjunction of two distinct option strings so a\n * passing mention of \"Resume\" in a transcript doesn't false-positive.\n */\nexport function isResumeModeDialogVisible(screen: string): boolean {\n return (\n screen.includes('Resume from summary') &&\n screen.includes(\"Don't ask me again\")\n );\n}\n\n/**\n * Detect Claude Code's session-feedback dialog (ENG-6017).\n *\n * After some turns Claude Code renders an optional rating prompt:\n *\n * ● How is Claude doing this session? (optional)\n * 1: Bad 2: Fine 3: Good 0: Dismiss\n *\n * It waits for a digit — Enter does nothing — so any injected message\n * sits in the input box unsubmitted, and the channel-input-watchdog's\n * Enter is swallowed too. Observed live on koda (agt-aws-1) 2026-06-04:\n * an operator Slack DM sat typed-but-unsubmitted for 40+ minutes behind\n * this dialog while pane-activity / synthetic-probe / heartbeat all\n * stayed green.\n *\n * Match the question text together with the literal `0: Dismiss` option\n * so a transcript merely *quoting* the question doesn't false-positive.\n */\nexport function isSessionFeedbackDialogVisible(screen: string): boolean {\n return (\n screen.includes('How is Claude doing this session') &&\n screen.includes('0: Dismiss')\n );\n}\n\n/**\n * The stop-the-session safe option (ENG-8213). Exported so tests and callers\n * assert against the same literal the matcher uses, rather than a copy that\n * could drift.\n *\n * Kept as the name it has always had because callers and tests reference it,\n * but it is no longer the ONLY option this module selects — see\n * `USAGE_LIMIT_SAFE_OPTIONS` for the full preference order.\n */\nexport const USAGE_LIMIT_SAFE_OPTION = 'Stop and wait for limit to reset';\n\n/**\n * The auto-continue option, added by Claude Code on the Team seat spend-limit\n * variant of this modal:\n *\n * 2. Wait here, then continue automatically at Aug 21, 10am\n *\n * A PREFIX, deliberately: the row carries a reset date that changes every\n * time, so the literal can only ever be the stable leading text.\n */\nexport const USAGE_LIMIT_CONTINUE_OPTION = 'Wait here, then continue automatically';\n\n/**\n * Options this module is willing to press, MOST PREFERRED FIRST.\n *\n * `Wait here, then continue automatically` outranks `Stop and wait for limit to\n * reset` because the two differ in exactly the way that matters to an\n * unattended agent: both idle until the cap lifts, but only the first RESUMES\n * by itself afterwards. \"Stop and wait\" leaves the session parked until a human\n * touches it, which is the state this whole handler exists to avoid — we would\n * have answered the modal and still needed the human.\n *\n * Neither spends money, which is the invariant that governs this list. Every\n * other row on either variant of this modal is excluded on purpose:\n *\n * - `Switch to usage credits` — starts pay-as-you-go spend.\n * - `Switch to Team plan` — a plan change.\n * - `Ask your admin for more usage` — spends nothing, but it pages a human\n * and asks them to raise a budget. That is a decision with a cost attached\n * and it is not ours to make automatically; a human choosing to raise the\n * cap is a different act from a fleet of agents asking them to.\n */\nexport const USAGE_LIMIT_SAFE_OPTIONS: readonly string[] = [\n USAGE_LIMIT_CONTINUE_OPTION,\n USAGE_LIMIT_SAFE_OPTION,\n];\n\n/**\n * Every option label known to appear on this modal, including the ones we would\n * never press. This is the RECOGNITION vocabulary, and it is deliberately wider\n * than the selection vocabulary above.\n *\n * ENG-8213 anchored recognition on the BILLING rows alone, which tied \"is this\n * the usage-limit modal?\" to options that only exist on the subscription\n * variant. Claude Code's Team seat spend-limit variant renders\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Wait here, then continue automatically at Aug 21, 10am\n * 3. Ask your admin for more usage\n *\n * with no billing row at all, so the modal was not recognised — and the failure\n * was not a no-op. This pane renders \"Enter to confirm\", and the `mcp-servers`\n * branch below fires on `'Enter to confirm' && 'MCP'` with \"MCP\" trivially\n * present in any agent's scrollback. So the unrecognised variant fell through to\n * a BARE ENTER, confirming whichever row the cursor sat on — row 1, \"Stop and\n * wait\", the one option that guarantees a human is needed. Observed on\n * acquire-intelligence, 2026-08-19.\n *\n * Recognising by \"an option row this modal is known to render\" rather than by\n * billing vocabulary keeps the structural guard (it must still be a numbered\n * ROW inside the block adjacent to the nearest footer) while covering both\n * variants and any future one whose rows we add here.\n */\nconst USAGE_LIMIT_KNOWN_OPTIONS: readonly string[] = [\n ...USAGE_LIMIT_SAFE_OPTIONS,\n 'Switch to usage credits',\n 'Switch to Team plan',\n // ENG-9006: the two-option variant Claude Code shipped ~2026-08-17. Filed\n // with a wedged pane and four unanswered customer Telegram messages queued\n // behind it.\n 'Upgrade your plan',\n 'Ask your admin for more usage',\n];\n\n/**\n * Detect Claude Code's usage-limit choice dialog (ENG-8213).\n *\n * On hitting the plan limit, Claude Code blocks the TUI on:\n *\n * What do you want to do?\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Switch to usage credits\n * 3. Switch to Team plan\n *\n * Enter to confirm · Esc to cancel\n *\n * Nothing answered it, so the session sat on the modal and the agent was\n * unavailable until a human attached to the pane.\n *\n * Matched STRUCTURALLY: a KNOWN option (USAGE_LIMIT_KNOWN_OPTIONS) must appear\n * as a numbered option ROW (`<indent>[❯ ]<digit>. <text>`), not merely\n * somewhere in the capture, and the modal's confirm affordance must be present.\n *\n * The anchor was originally the BILLING rows specifically, which silently made\n * recognition subscription-only and let the Team seat spend-limit variant fall\n * through to a bare Enter. See USAGE_LIMIT_KNOWN_OPTIONS. The structural\n * requirement below is unchanged and is what actually does the safety work; only\n * the vocabulary widened.\n *\n * The looser \"billing phrase anywhere && 'Enter to confirm'\" version was\n * actively dangerous, and in the opposite direction to the bug this fixes.\n * \"Enter to confirm\" is rendered by OTHER dialogs (the MCP confirm among\n * them), and an agent that merely writes the words \"Switch to usage credits\"\n * - discussing a capped teammate, quoting this very file - leaves them in the\n * scrollback. Both true at once and this predicate fires on a pane that is\n * not this dialog. Because sweepDialogs() checks it FIRST and then finds no\n * safe option row, it would return null AND isUnanswerableUsageLimitDialog()\n * would tell every caller to hold the pane: the real dialog never gets\n * answered and the agent wedges. That is the ENG-8194 self-gating shape with\n * a worse blast radius, so the match has to key on layout, not vocabulary.\n */\n/**\n * How far above the \"Enter to confirm\" footer an option row may sit and still\n * count as part of the SAME modal.\n *\n * The real dialog puts its furthest option 4 lines above the footer, so 6\n * carries the whole block with room for a wrap. Deliberately tight: every line\n * of slack here is scrollback that a numbered write-up could occupy, and the\n * cost of being too generous (wedging the pane on a false positive) is worse\n * than the cost of being too tight (failing to answer, which is the status quo\n * this handler improves on).\n */\nconst USAGE_LIMIT_BLOCK_LINES = 6;\n\nfunction optionRowDigit(line: string, label: string): string | null {\n const m = line.match(\n new RegExp(\n String.raw`^[^\\S\\n]*(?:❯[^\\S\\n]*)?(\\d)\\.[^\\S\\n]*` +\n label.replace(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`),\n ),\n );\n return m?.[1] ?? null;\n}\n\n/**\n * Return the modal's own lines, or null when this dialog is not on screen.\n *\n * The match is bound to a REGION, not to the capture as a whole: a known\n * option row and the confirm footer must belong to the same block. Two earlier\n * versions of this predicate were too loose, in the same direction each time:\n *\n * 1. \"billing phrase anywhere && 'Enter to confirm' anywhere\" - satisfied by\n * prose about a capped teammate sitting behind an unrelated dialog.\n * 2. \"billing option ROW anywhere && 'Enter to confirm' anywhere\" - still\n * satisfied by a numbered list in prose (`1. Switch to Team plan`, which\n * is exactly how an agent writes up this very ticket) plus a confirm\n * footer from a different dialog.\n *\n * Both misfire the same way, and it is the dangerous way: sweepDialogs() checks\n * this branch FIRST (so the mcp-servers branch cannot bare-Enter the real\n * modal), so a false positive with no safe option row returns null AND makes\n * isUnanswerableUsageLimitDialog() tell every caller to hold the pane. The\n * dialog actually on screen never gets answered and the agent wedges.\n */\n/**\n * The lines of the FOCUSED modal — the block immediately above the nearest\n * \"Enter to confirm\" footer — or null when no modal is focused.\n *\n * Extracted from usageLimitModalBlock so every structural predicate reads the\n * same region by the same rule. \"Nearest footer\" is load-bearing and was\n * arrived at the hard way: with a newer dialog on screen and an older one still\n * in the scrollback, a backwards search skips the live dialog's footer and\n * matches the STALE one, so we would classify the pane as a dialog that is not\n * focused and key whatever actually is.\n */\nfunction focusedModalBlock(screen: string, blockLines: number): string[] | null {\n const lines = screen.split('\\n');\n let footer = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i]!.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n }\n if (footer < 0) return null;\n return lines.slice(Math.max(0, footer - blockLines), footer);\n}\n\n/**\n * The MCP-servers dialog's own option rows.\n *\n * New MCP servers found in .mcp.json\n * ❯ 1. Use this and all future MCP servers in this project\n * 2. Continue without using these MCP servers\n *\n * Enter to confirm · Esc to cancel\n */\nconst MCP_DIALOG_OPTIONS: readonly string[] = [\n 'Use this and all future MCP servers in this project',\n 'Continue without using these MCP servers',\n];\n\n/**\n * Detect the MCP-servers dialog STRUCTURALLY.\n *\n * This branch used to read `screen.includes('Enter to confirm') &&\n * screen.includes('MCP')`, which is not a detector — it is a catch-all. Every\n * agent pane carries the string \"MCP\" somewhere (`Connected to MCP server:\n * slack` is printed at startup and never scrolls out of a quiet pane), so the\n * condition reduced to \"any focused confirm dialog at all\", and the action is a\n * BARE ENTER on whatever row the cursor sits on.\n *\n * That is not a hypothetical. It is the mechanism behind the acquire-intelligence\n * wedge on 2026-08-19: the spend-limit modal was not recognised by the\n * usage-limit branch, fell to this one, and got an Enter on the pre-selected\n * \"Stop and wait for limit to reset\". ENG-8213's fix for the SAME hazard was to\n * order the usage-limit branch first — correct, and only a fix for the one\n * dialog anybody had thought of.\n *\n * It also silently defeats the ENG-9006 backstop: an unclaimed modal cannot be\n * reported as unclaimed if this branch claims everything. Tightening here is\n * what lets an unrecognised dialog reach `isUnclaimedConfirmDialog` and become\n * visible instead of being blindly confirmed.\n */\nexport function isMcpServersDialogVisible(screen: string): boolean {\n const block = focusedModalBlock(screen, USAGE_LIMIT_BLOCK_LINES);\n if (!block) return false;\n return block.some((l) => MCP_DIALOG_OPTIONS.some((label) => optionRowDigit(l, label) !== null));\n}\n\n/**\n * ENG-9224 — the SAME dialog after Claude Code 2.1.237 replaced the widget.\n *\n * `isMcpServersDialogVisible` above matches a numbered option list. 2.1.237\n * ships a checkbox multi-select instead, with neither the digits nor either of\n * the `MCP_DIALOG_OPTIONS` labels:\n *\n * 3 new MCP servers found in this project\n * Select any you wish to enable.\n *\n * MCP servers may execute code or access system resources. All tool calls\n * require approval. Learn more in the MCP documentation.\n *\n * ❯ [✓] augmented\n * [✓] direct-chat\n * [✓] composio_googlesheets\n * Space to select · Enter to confirm · Esc to reject all\n *\n * So the branch stopped firing, agents froze on it, and the presence reaper\n * restarted them into the identical prompt until the breaker paused them. Both\n * customer orgs onboarded on 2026-08-20 died this way.\n *\n * This is the second rewrite of this dialog we have chased: ENG-5375 moved the\n * match onto the option TEXT when 2.1.146 reworded the prompt. Matching on text\n * is what left us exposed when the widget itself changed, so this one keys on\n * STRUCTURE — a header, a checked row, a confirm footer — which survives\n * renaming the servers or reordering the list.\n *\n * ## Why a checked row and not just any row\n *\n * `Enter` accepts the CURRENT selection. If Claude Code ever renders this with\n * nothing pre-selected, confirming it enables nothing: the servers stay unbound,\n * the reaper loop continues, and the log says `Auto-accepted MCP servers` while\n * no progress was made. That is strictly worse than today, because it is\n * invisible.\n *\n * So an all-unchecked dialog is deliberately NOT claimed. It falls through to\n * `isUnclaimedConfirmDialog` and becomes a human's problem — the correct outcome\n * for a state we cannot safely resolve by pressing one key.\n */\nconst MCP_CHECKBOX_HEADER = /new MCP servers? found in this project/i;\n\n/**\n * A CHECKED checkbox row: `❯ [✓] augmented`, ` [x] direct-chat`.\n *\n * `[ ]` is excluded on purpose — see the \"why a checked row\" note above. The\n * optional `❯` mirrors `optionRowDigit`'s handling of the focus caret.\n */\nconst MCP_CHECKED_ROW = /^[^\\S\\n]*(?:❯[^\\S\\n]*)?\\[[✓x]\\][^\\S\\n]*\\S/iu;\n\n/**\n * Wider than `USAGE_LIMIT_BLOCK_LINES` because this dialog puts its header\n * above two lines of prose, a blank line, and one row PER SERVER. Six lines\n * reaches the checkboxes but not the header. Twenty covers a comfortable\n * server count while still binding the match to a region rather than the whole\n * capture — the discipline that keeps this file's detectors from becoming the\n * catch-all described above.\n */\nconst MCP_CHECKBOX_BLOCK_LINES = 20;\n\nexport function isMcpCheckboxDialogVisible(screen: string): boolean {\n const block = focusedModalBlock(screen, MCP_CHECKBOX_BLOCK_LINES);\n if (!block) return false;\n if (!block.some((l) => MCP_CHECKBOX_HEADER.test(l))) return false;\n return block.some((l) => MCP_CHECKED_ROW.test(l));\n}\n\nfunction usageLimitModalBlock(screen: string): string[] | null {\n const lines = screen.split('\\n');\n // ONLY the nearest footer. Scanning further back was the fourth version of\n // this bug: with a newer dialog on screen and an older usage-limit block\n // still in the scrollback, a backwards search skips the live dialog's footer\n // (no billing row) and matches the STALE one - so we would classify the pane\n // as the usage-limit modal and send a digit + Enter into whatever dialog is\n // actually focused. The live dialog is always the last footer; treating this\n // as \"search the capture\" rather than \"read the focused dialog\" is what kept\n // producing near-misses.\n let footer = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i]!.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n }\n if (footer < 0) return null;\n const block = lines.slice(Math.max(0, footer - USAGE_LIMIT_BLOCK_LINES), footer);\n const hasKnownOptionRow = block.some((l) =>\n USAGE_LIMIT_KNOWN_OPTIONS.some((label) => optionRowDigit(l, label) !== null),\n );\n return hasKnownOptionRow ? block : null;\n}\n\n/**\n * Detect Claude Code's usage-limit choice dialog (ENG-8213).\n *\n * On hitting the plan limit, Claude Code blocks the TUI on:\n *\n * What do you want to do?\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Switch to usage credits\n * 3. Switch to Team plan\n *\n * Enter to confirm · Esc to cancel\n *\n * ...or, on a Claude Team seat that has hit its individual spend limit:\n *\n * What do you want to do?\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Wait here, then continue automatically at Aug 21, 10am\n * 3. Ask your admin for more usage\n *\n * Enter to confirm · Esc to cancel\n *\n * Nothing answered it, so the session sat on the modal and the agent was\n * unavailable until a human attached to the pane.\n */\nexport function isUsageLimitChoiceDialogVisible(screen: string): boolean {\n return usageLimitModalBlock(screen) !== null;\n}\n\n/**\n * Locate the option to press by READING ITS NUMBER off the line that carries\n * the option's text — never by assuming it is option 1.\n *\n * This is the whole safety design (ENG-8213). Several rows on this modal cost\n * money or a human's attention: \"Switch to usage credits\" starts pay-as-you-go\n * spend, \"Switch to Team plan\" is a plan change, \"Ask your admin for more\n * usage\" pages a person to raise a budget. Selecting by position would mean a\n * future reordering of this menu silently turns an auto-answer into a purchase.\n * Deriving the digit from the matched text means a reorder just changes which\n * digit we send, and an unrecognised menu yields `null` (answer nothing) rather\n * than a guess.\n *\n * Returns the chosen row's digit (as a tmux key name) and the label it matched,\n * or `null` when no selectable option is present — failing to clear a modal is\n * recoverable; buying credits is not.\n */\nexport function findUsageLimitSafeOption(\n screen: string,\n): { key: string; label: string } | null {\n // Searched inside the modal's own block, so a numbered line elsewhere on the\n // pane can never supply the digit we are about to press.\n const block = usageLimitModalBlock(screen);\n if (!block) return null;\n // Preference order comes from USAGE_LIMIT_SAFE_OPTIONS, not from row order:\n // the OUTER loop is the preference list so `Wait here, then continue\n // automatically` wins wherever it sits on the menu. Iterating rows first\n // would make the answer depend on Claude Code's row ordering, which is the\n // select-by-position mistake this function exists to avoid — just one level\n // up.\n for (const label of USAGE_LIMIT_SAFE_OPTIONS) {\n for (const line of block) {\n const digit = optionRowDigit(line, label);\n if (digit !== null) return { key: digit, label };\n }\n }\n return null;\n}\n\n/** The digit alone, as before. */\nexport function findUsageLimitSafeOptionKey(screen: string): string | null {\n return findUsageLimitSafeOption(screen)?.key ?? null;\n}\n\n/**\n * How many option rows a two-choice consent dialog can span. Wider than the\n * rows themselves so the block still reaches the cursor row when Claude Code\n * pads the modal with blank lines (it does, between the prose and the options).\n */\nconst CONSENT_BLOCK_LINES = 12;\n\n/** Index of the LAST element satisfying `pred`, or -1. */\nfunction lastIndexWhere<T>(xs: readonly T[], pred: (x: T) => boolean): number {\n for (let i = xs.length - 1; i >= 0; i--) {\n if (pred(xs[i]!)) return i;\n }\n return -1;\n}\n\n/**\n * True when `line` is a COMPLETE option row for `label` — optionally preceded by\n * the `❯` cursor and/or a `N.` digit, and carrying nothing else.\n *\n * A substring test is not good enough, and the bypass dialog is exactly where\n * that bites. Its detector is `includes('Yes, I accept') && includes('Bypass\n * Permissions')`, and both strings show up in ordinary prose — an agent that\n * reads a manager log, or discusses this very incident, puts them in its own\n * transcript. A substring match would then find the prose line, measure the\n * cursor distance to it, and key that many Downs into whatever dialog is\n * actually focused. Anchoring to the whole row means prose can never be\n * mistaken for a selectable option; at worst we answer nothing.\n *\n * Anchored at both ends deliberately: every label this is called with names a\n * full row ('Yes, I accept', 'Yes, I trust this folder', \"Don't ask me again\"),\n * unlike USAGE_LIMIT_CONTINUE_OPTION, which is a prefix and is matched by\n * findUsageLimitSafeOption's own looser rule.\n */\n/**\n * True when `line` is a menu CURSOR row — the `❯` opens the line (leading\n * whitespace aside) and something follows it.\n *\n * A bare `includes('❯')` is not good enough: the agent's own input prompt\n * renders the same glyph, and so does any prose quoting a menu. Requiring the\n * glyph to START the row is what separates \"this is the selected option\" from\n * \"this line happens to contain an arrow\".\n */\nfunction isCursorRow(line: string): boolean {\n return /^[^\\S\\n]*❯[^\\S\\n]+\\S/.test(line);\n}\n\nfunction isOptionRowFor(line: string, label: string): boolean {\n const escaped = label.replace(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\n return new RegExp(\n String.raw`^[^\\S\\n]*(?:❯[^\\S\\n]*)?(?:\\d\\.[^\\S\\n]*)?` +\n escaped +\n String.raw`[^\\S\\n]*$`,\n ).test(line);\n}\n\n/**\n * ENG-9575: keystrokes that select `label` on the FOCUSED modal, for either\n * shape Claude Code renders — or null when the row isn't there.\n *\n * The bug this exists to kill: every branch below used to answer by DIGIT\n * (`['2','Enter']`) or by BARE ENTER, both of which encode an assumption about\n * how the menu is drawn. Claude Code 2.1.250 draws these consent dialogs as an\n * arrow-select list with NO numbered rows, and — this is the part that turns a\n * cosmetic change into an outage — it defaults the cursor to the REFUSING row:\n *\n * ❯ No, exit\n * Yes, I accept\n * Enter to confirm · Esc to cancel\n *\n * A digit is swallowed (verified on 2.1.250: sending `2` does not move the\n * cursor), so `['2','Enter']` confirms whatever is selected — `No, exit`. Claude\n * exits 0, the manager sees a dead session and respawns, and the next spawn\n * lands on the same dialog. On the two hosts provisioned 2026-08-28 that ran\n * 847 and 1,974 times, every agent down from first boot, while the manager\n * logged `Auto-accepted bypass permissions` on each pass.\n *\n * It is self-perpetuating in a way worth naming: Claude records the consent\n * only after a SUCCESSFUL start, so answering it wrongly also prevents the\n * write that would stop it being asked again. One correct answer ends it for\n * good; a host can never get there on its own.\n *\n * Reading the row instead of assuming its position keeps both shapes working\n * and survives a reorder — the same reasoning as findUsageLimitSafeOption,\n * which derives the digit from the matched text rather than trusting row order.\n * Preserves DEFAULT-DENY: an unrecognised menu returns null, so the caller\n * answers nothing rather than keying a row it cannot see.\n */\nexport function selectRowKeys(\n screen: string,\n label: string,\n): readonly string[] | null {\n // FAIL CLOSED without a focused modal, for BOTH shapes. Every one of these\n // dialogs renders `Enter to confirm` (the numbered shape too — see\n // MCP_DIALOG_OPTIONS above), so no footer means no dialog we are entitled to\n // key. The alternative, searching the whole pane, reads lines the agent\n // itself wrote: a transcript listing the options verbatim (\" 2. Yes, I\n // accept\") is a complete option row by any structural test, and answering it\n // types into the REPL.\n const block = focusedModalBlock(screen, CONSENT_BLOCK_LINES);\n if (!block) return null;\n\n // Last match, not first: with an older render of the same dialog still in\n // the block, the live one is the most recent.\n const target = lastIndexWhere(block, (l) => isOptionRowFor(l, label));\n if (target < 0) return null;\n\n // Numbered shape (pre-2.1.250): the row carries its own digit, so press it.\n // Tried first so a host still on an older Claude Code answers as it always\n // has, and so this fix needs no knowledge of which version a host is running.\n const digit = optionRowDigit(block[target]!, label);\n if (digit !== null) return [digit, 'Enter'];\n\n // Arrow-select shape: RELATIVE movement, so we need the menu's own cursor.\n // Only a row whose `❯` STARTS the line is a cursor — prose that merely\n // contains the glyph is not, and neither is the agent's input prompt.\n const cursors: number[] = [];\n for (let i = 0; i < block.length; i++) {\n if (isCursorRow(block[i]!)) cursors.push(i);\n }\n // No cursor means we cannot know what Enter would confirm. Answer nothing:\n // on these dialogs the unknown row is the destructive one, and DEFAULT-DENY\n // is the module's contract.\n if (cursors.length === 0) return null;\n\n // Nearest cursor to the target — option rows are adjacent, so nearest is the\n // menu this row belongs to. A TIE means two candidate menus are equally\n // close and we cannot tell which owns the row; that is ambiguity, and\n // ambiguity is answered with silence rather than a guess.\n let cursor = cursors[0]!;\n let tied = false;\n for (const i of cursors.slice(1)) {\n const d = Math.abs(i - target);\n const best = Math.abs(cursor - target);\n if (d < best) {\n cursor = i;\n tied = false;\n } else if (d === best) {\n tied = true;\n }\n }\n if (tied) return null;\n\n const distance = target - cursor;\n if (distance === 0) return ['Enter'];\n const step = distance > 0 ? 'Down' : 'Up';\n return [...Array<string>(Math.abs(distance)).fill(step), 'Enter'];\n}\n\n/**\n * ENG-9006: the reset moment the modal names, verbatim, or null.\n *\n * The Team seat spend-limit variant renders its auto-continue row as\n *\n * 2. Wait here, then continue automatically at Aug 21, 10am\n *\n * and that tail is the single most authoritative reset string we ever get from\n * Claude Code. Everywhere else we RECONSTRUCT one: the saturated banner often\n * names only an hour (\"resets 1am (UTC)\") and `banner-parser.ts` resolves it to\n * the next occurrence of that hour, which can never be more than 24h out even\n * when the real reset is days away — the ENG-8901 defect, which had operators\n * scheduling against an instant that was three days early.\n *\n * Returned as the RAW STRING on purpose. Parsing it would mean inventing a\n * second date grammar that can drift from the banner parser's, and guessing a\n * timezone the modal does not state. The one consumer is a sentence shown to a\n * human, and \"until Aug 21, 10am\" is exactly as useful to them as a Date would\n * be, while being impossible to get subtly wrong.\n *\n * Bounded so a mis-parse cannot push an arbitrary pane line into a message that\n * reaches a customer.\n */\nexport function findUsageLimitResetHint(screen: string): string | null {\n const block = usageLimitModalBlock(screen);\n if (!block) return null;\n for (const line of block) {\n if (optionRowDigit(line, USAGE_LIMIT_CONTINUE_OPTION) === null) continue;\n const idx = line.indexOf(USAGE_LIMIT_CONTINUE_OPTION);\n const tail = line.slice(idx + USAGE_LIMIT_CONTINUE_OPTION.length).trim();\n // The row reads \"… continue automatically at <when>\". Drop the connective\n // so the caller can compose \"until <when>\" without doubling it up.\n const when = tail.replace(/^at\\s+/i, '').trim();\n if (!when) return null;\n return when.slice(0, 40);\n }\n return null;\n}\n\n/**\n * The usage-limit dialog is on screen but its safe option could not be\n * located, so there is no keystroke we are willing to send (ENG-8213).\n *\n * Callers MUST treat this as \"do not key this pane at all\" and log it: the\n * generic fallbacks (the watchdog's bounded Enter, acceptDialogs' `❯`\n * readiness branch) would otherwise press Enter on whatever row the cursor\n * happens to be sitting on, and on this dialog most of the rows either spend\n * money or page a human.\n */\nexport function isUnanswerableUsageLimitDialog(screen: string): boolean {\n return (\n isUsageLimitChoiceDialogVisible(screen) &&\n findUsageLimitSafeOptionKey(screen) === null\n );\n}\n\n/**\n * Single-pass dialog recognition. Returns the dismissal action for the\n * first recognised dialog on screen, or `null` when no known dialog is\n * visible (including for the login picker, which must never be keyed\n * past — see isLoginPickerVisible).\n *\n * Branch order mirrors the original acceptDialogs() cascade: the theme\n * picker check must run before any generic `❯`-based readiness logic in\n * callers, since picker rows also render with `❯`.\n */\nexport function sweepDialogs(screen: string): DialogAction | null {\n // ENG-8213: FIRST, and it must stay first. The usage-limit dialog renders\n // \"Enter to confirm\", and the mcp-servers branch below fires on\n // `'Enter to confirm' && 'MCP'` — with \"MCP\" trivially present in the\n // scrollback behind the modal, that branch would match this dialog and\n // send a BARE ENTER, confirming whichever row the cursor is on. Two of\n // those three rows are billing actions. Ordering is the guard.\n if (isUsageLimitChoiceDialogVisible(screen)) {\n const safe = findUsageLimitSafeOption(screen);\n // No recognised safe option => answer nothing. isUnanswerableUsageLimitDialog()\n // lets callers detect this state and stop their own fallbacks keying the pane.\n if (!safe) return null;\n return {\n kind: 'usage-limit-choice',\n // The log names the option ACTUALLY picked, not a fixed literal. With two\n // selectable rows now, a constant string would make a fleet sweep unable\n // to tell an agent that will resume by itself from one parked until a\n // human touches it.\n keys: [safe.key, 'Enter'],\n interKeyDelayMs: 300,\n logMessage: `Auto-answered usage-limit choice dialog (picked '${safe.label}')`,\n };\n }\n // ENG-10619: before the generic branches. The dialog renders\n // `Enter to confirm`, and its default row opts the account INTO model\n // training — so nothing below may get the chance to send a bare Enter.\n // Resolve before claiming (see the folder-trust branch): an unresolvable\n // OFF row falls through rather than suppressing later handlers, and\n // isUnclaimedConfirmDialog() still reports the pane.\n const termsKeys = consumerTermsOffKeys(screen);\n if (termsKeys) {\n return {\n kind: 'consumer-terms',\n keys: termsKeys,\n interKeyDelayMs: 300,\n logMessage: \"Auto-answered consumer-terms dialog (picked 'Help improve our AI models: OFF')\",\n };\n }\n if (\n screen.includes('Choose the text style') ||\n (screen.includes('Dark mode') && screen.includes('Light mode'))\n ) {\n return {\n kind: 'theme-picker',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted theme picker',\n };\n }\n // ENG-9575: was a bare Enter. On 2.1.250 this dialog's default row is\n // \"No, exit\", so the bare Enter REFUSED the trust prompt and exited Claude.\n // Latent rather than firing on the hosts that broke (their agent projects\n // were already trusted), but it fires the first time an agent runs in a\n // project directory Claude has not seen — i.e. on every newly provisioned\n // agent. Select the row by name instead.\n //\n // RESOLVE BEFORE CLAIMING. These predicates are whole-pane substring tests,\n // so scrollback or the agent's own prose can match them while a DIFFERENT\n // dialog is focused. Returning null on an unresolved row would cancel the\n // sweep and suppress every branch below — so trust-folder prose sitting\n // behind a real bypass dialog would leave that dialog unanswered, which is\n // the exact wedge this change exists to prevent. Falling through instead\n // keeps the later handlers reachable and lets isUnclaimedConfirmDialog()\n // still report a pane nobody claimed. Same hazard the usage-limit branch\n // documents as the self-gating trap.\n const trustKeys = screen.includes('Yes, I trust this folder')\n ? selectRowKeys(screen, 'Yes, I trust this folder')\n : null;\n if (trustKeys) {\n return {\n kind: 'folder-trust',\n keys: trustKeys,\n interKeyDelayMs: 300,\n logMessage: 'Auto-accepted folder trust',\n };\n }\n // ENG-5364: picks \"Don't ask me again\", which Claude Code persists in config\n // so subsequent resumes skip the dialog entirely.\n // ENG-9575: was ['3','Enter']. The digit is swallowed on 2.1.250's\n // arrow-select shape, leaving Enter to confirm the default row — here that is\n // \"Resume from summary\", which is harmless but is NOT the option that stops\n // the dialog recurring, so the agent re-answered it on every single respawn.\n // Resolve before claiming — see the folder-trust branch above.\n const resumeKeys = isResumeModeDialogVisible(screen)\n ? selectRowKeys(screen, \"Don't ask me again\")\n : null;\n if (resumeKeys) {\n return {\n kind: 'resume-mode',\n keys: resumeKeys,\n interKeyDelayMs: 300,\n logMessage: \"Auto-dismissed resume-mode dialog (picked 'Don't ask me again')\",\n };\n }\n if (screen.includes('I am using this for local development')) {\n return {\n kind: 'dev-channels',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted dev channels',\n };\n }\n // ENG-9224: both shapes of the same dialog — the pre-2.1.237 numbered list\n // and the 2.1.237 checkbox multi-select. Kept in ONE branch, in the position\n // the numbered one already held, so the ordering hazard ENG-8213 fixed (the\n // usage-limit branch must be evaluated first) is untouched by this change.\n if (isMcpServersDialogVisible(screen) || isMcpCheckboxDialogVisible(screen)) {\n return {\n kind: 'mcp-servers',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted MCP servers',\n };\n }\n // ENG-9575: was ['2','Enter'] — the incident branch. See selectRowKeys.\n // Resolve before claiming — see the folder-trust branch above. This one\n // matters most: its two substrings are the ones an agent is likeliest to\n // have written itself, and suppressing the session-feedback branch below\n // leaves a rating prompt swallowing the input box (the koda wedge, ENG-6017).\n const bypassKeys =\n screen.includes('Yes, I accept') && screen.includes('Bypass Permissions')\n ? selectRowKeys(screen, 'Yes, I accept')\n : null;\n if (bypassKeys) {\n return {\n kind: 'bypass-permissions',\n keys: bypassKeys,\n interKeyDelayMs: 300,\n logMessage: 'Auto-accepted bypass permissions',\n };\n }\n // ENG-6017: the rating prompt acts on the bare digit — no Enter needed.\n if (isSessionFeedbackDialogVisible(screen)) {\n return {\n kind: 'session-feedback',\n keys: ['0'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-dismissed session-feedback dialog',\n };\n }\n return null;\n}\n\n/**\n * Send a DialogAction's keystrokes to a tmux session, one send-keys call\n * per key with the action's inter-key delay. execFileSync (not execSync)\n * so the session name is an argv entry rather than shell-interpolated.\n */\nexport async function sendDialogKeys(\n tmuxSession: string,\n action: DialogAction,\n): Promise<void> {\n for (let i = 0; i < action.keys.length; i++) {\n if (i > 0 && action.interKeyDelayMs > 0) {\n await new Promise((r) => setTimeout(r, action.interKeyDelayMs));\n }\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, action.keys[i]!], {\n stdio: 'ignore',\n });\n }\n}\n\n/**\n * ENG-9006 criterion 3: the block of a focused confirm-footer dialog that NO\n * handler in this module claimed, or null.\n *\n * This is the backstop for the failure mode that has now recurred three times\n * in three weeks, each time the same way: Claude Code reworded a modal, every\n * predicate above went false, the pane wedged, and the fleet found out because\n * a human looked at a screenshot.\n *\n * 2026-07-28 ENG-8213 \"Switch to usage credits\" / \"Switch to Team plan\"\n * 2026-08-17 ENG-9006 \"Upgrade your plan\" (two-option variant)\n * 2026-08-19 this \"Wait here, then continue automatically at …\"\n * / \"Ask your admin for more usage\" (Team seat spend)\n *\n * Adding each new string is necessary and is not sufficient — the detector is\n * anchored on vendor-authored copy that the vendor keeps changing, so the NEXT\n * rewording is already scheduled. What does not depend on the wording is the\n * shape: a modal is focused (it renders its confirm affordance) and nothing\n * claimed it.\n *\n * DEFAULT-DENY IS PRESERVED. This function keys nothing and returns no action;\n * it exists purely so an unclaimed modal becomes VISIBLE instead of silent.\n * Answering an unrecognised dialog is still forbidden — a future dialog's\n * default row could be destructive, which is the whole reason sweepDialogs()\n * returns null rather than guessing.\n *\n * Deliberately excludes `isUnanswerableUsageLimitDialog`: that state is\n * recognised, is refused on purpose, and already emits its own loud signal.\n * Counting it here too would double-report one pane.\n */\nexport function focusedConfirmDialogBlock(screen: string): string[] | null {\n const lines = screen.split('\\n');\n // The LIVE dialog is the last footer, same rule as usageLimitModalBlock():\n // an older modal still in the scrollback is not what is focused now.\n let footer = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i]!.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n }\n if (footer < 0) return null;\n return lines.slice(Math.max(0, footer - USAGE_LIMIT_BLOCK_LINES), footer + 1);\n}\n\n/**\n * True when a focused confirm-footer dialog is on screen and no handler in\n * this module claimed it. See focusedConfirmDialogBlock for why this exists.\n */\nexport function isUnclaimedConfirmDialog(screen: string): boolean {\n if (focusedConfirmDialogBlock(screen) === null) return false;\n if (sweepDialogs(screen) !== null) return false;\n // Already has its own signal — see the doc above.\n if (isUnanswerableUsageLimitDialog(screen)) return false;\n return true;\n}\n\n/**\n * Tiny non-cryptographic hash for hash-only logging of channel input\n * (prod logging policy: input may contain PII/secrets, so log hash+len,\n * never content). Shared by the watchdog and the inject-time hygiene.\n */\nexport function simpleTextHash(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) {\n h = ((h << 5) - h + s.charCodeAt(i)) | 0;\n }\n return h.toString(16);\n}\n\n/**\n * ENG-9932: a pane state that NO keystroke this module is willing to send can\n * clear, and that therefore a session RESTART cannot clear either.\n *\n * The distinction this type draws is the whole point of the ticket. On\n * agt-aws-1 (2026-09-03) eight agents were auto-paused because\n * `mcp-presence-reaper` saw \"declared MCP(s) have no live children\" and\n * restarted the session — three times in ten minutes, into the circuit breaker\n * (ENG-5441). The MCP servers were not broken. Claude Code had never finished\n * booting, because it was sitting on the login picker, so no MCP child had ever\n * been spawned. Every restart re-presented the same modal.\n *\n * `no live MCP children` is therefore a SYMPTOM with (at least) two causes that\n * want opposite remedies:\n *\n * - an MCP server actually died -> restart the session (correct)\n * - the pane is blocked on a modal -> restarting is futile by\n * construction; surface it\n *\n * Nothing distinguished them, so the reaper applied the first remedy to the\n * second cause and the diagnosis landed two subsystems away from the fault.\n */\n/**\n * Every pane state a restart provably cannot recover.\n *\n * A RUNTIME array, with the type DERIVED from it, deliberately. The obvious\n * shape is a bare union plus a hand-written list in the test — but a\n * three-element `UnanswerablePaneKind[]` stays a valid `UnanswerablePaneKind[]`\n * after the union grows to four, so the coverage assertion would keep passing\n * while the new kind shipped untested. A `Record<UnanswerablePaneKind, true>` in\n * the test does not fix it either: `apps/cli/tsconfig.json` excludes\n * `**\\/*.test.ts`, and vitest strips types without checking them, so a\n * compile-time guard written in a test file is never evaluated by anything.\n *\n * Exporting the values and deriving the type means adding a kind here makes the\n * suite's coverage check fail at RUNTIME, which is the only mechanism that\n * actually runs.\n */\nexport const UNANSWERABLE_PANE_KINDS = [\n /** The login picker (ENG-4634). Needs an operator OAuth; no keystroke and no\n * respawn can complete it, and `acceptDialogs()` deliberately never keys\n * past it. This is the state marlow was found in. */\n 'login-picker',\n /** The usage-limit modal is up but no option we are willing to press is on\n * it (ENG-8213). Recognised and refused ON PURPOSE — every remaining row\n * either spends money or pages a human. A respawn re-presents it. */\n 'usage-limit-unanswerable',\n /** A modal is focused and NO handler in this module claimed it (ENG-9006).\n * Unrecognised means neither the channel-input watchdog nor the spawn-time\n * `acceptDialogs()` cascade can answer it, so a respawn lands on the same\n * prompt. This is the branch that catches the NEXT dialog Claude Code adds,\n * which is the one we cannot enumerate in advance. */\n 'unclaimed-modal',\n] as const;\n\nexport type UnanswerablePaneKind = (typeof UNANSWERABLE_PANE_KINDS)[number];\n\n/**\n * Classify a pane that a session restart provably cannot recover, or null.\n *\n * ## Why a RECOGNISED, answerable dialog is deliberately not in this set\n *\n * The tempting version of this function returns non-null for any dialog at\n * all. That would be wrong, and in the dangerous direction: it would add a new\n * way to suppress a restart that is genuinely needed.\n *\n * A dialog `sweepDialogs()` claims gets answered by two independent paths — the\n * per-poll `channel-input-watchdog`, and the spawn-time `acceptDialogs()`\n * cascade in persistent-session.ts. The second one means a restart is a real\n * remedy for an answerable dialog: the fresh session hits the same prompt and\n * the cascade keys past it. So an answerable dialog must keep the pre-ENG-9932\n * behaviour and fall through to the restart.\n *\n * Every kind above is the opposite case: no path answers it, in this session or\n * the next. Holding the restart costs nothing that a restart would have gained,\n * and saves the breaker budget that today gets burned discovering that.\n *\n * DEFAULT-DENY IS PRESERVED, and note the direction. This function keys\n * nothing and returns no `DialogAction`; a pane it does not recognise returns\n * null, which means \"carry on with the restart\" — i.e. unrecognised falls\n * through to the EXISTING behaviour, never to a new suppression.\n */\nexport function classifyUnanswerablePane(\n screen: string | null | undefined,\n): UnanswerablePaneKind | null {\n // A pane we could not read is not evidence of a blocked pane. Fail open to\n // the legacy restart path: a tmux hiccup must never become a silent,\n // fleet-wide suppression of MCP recovery.\n if (!screen) return null;\n if (isLoginPickerVisible(screen)) return 'login-picker';\n if (isUnanswerableUsageLimitDialog(screen)) return 'usage-limit-unanswerable';\n if (isUnclaimedConfirmDialog(screen)) return 'unclaimed-modal';\n return null;\n}\n\n/**\n * A CONTENT-FREE fingerprint of the blocking modal: a stable hash plus the\n * block's length — or null when there is no focused modal block.\n *\n * ## Why a hash and not the text (CodeRabbit, PR #5504)\n *\n * The first version of this returned a 200-character excerpt of the pane. The\n * bound was the wrong control. The modal this fires on most often is the\n * UNCLAIMED one, whose content is by definition unknown to us, and the block is\n * simply the six lines above a confirm footer — which on a busy agent is the\n * transcript it was mid-way through writing. Truncating a customer's message to\n * 200 characters still forwards a customer's message, and `onPaneBlocked`\n * persists this into a control-plane alert row, so it would leave the host.\n *\n * This module's own `simpleTextHash` already states the policy this has to\n * follow — \"input may contain PII/secrets, so log hash+len, never content\" —\n * and `channel-input-watchdog` already reports exactly this data, the unclaimed\n * dialog block, as `unclaimedDialogHash`. The decision was made; this just\n * failed to follow it.\n *\n * Nothing diagnostic is lost. The `UnanswerablePaneKind` is the actual\n * diagnosis, and the operator-facing line names the one action that resolves\n * it (`tmux attach`), where the real pane is a keystroke away. The hash adds\n * the thing an excerpt could not: correlation. Eight agents blocked on the same\n * unknown modal share a hash, which is how you tell one wedged agent from a\n * fleet-wide dialog rollout.\n */\nexport function summarizeUnanswerablePane(\n screen: string,\n): { hash: string; length: number } | null {\n const block = focusedConfirmDialogBlock(screen);\n if (!block) return null;\n const flat = block.join('\\n');\n return { hash: simpleTextHash(flat), length: flat.length };\n}\n"],"mappings":"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","names":[]}