@integrity-labs/agt-cli 0.28.1175 → 0.28.1177

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/bin/agt.js +7 -7
  2. package/dist/{chunk-P2B2SQHD.js → chunk-45UH24I2.js} +5 -5
  3. package/dist/{chunk-OAWK4OBN.js → chunk-6HBXEJHO.js} +2 -2
  4. package/dist/{chunk-EL5MWGGH.js → chunk-AIUIMEUS.js} +87 -3
  5. package/dist/chunk-AIUIMEUS.js.map +1 -0
  6. package/dist/{chunk-EK6JVOBH.js → chunk-H37WRW2E.js} +47 -8
  7. package/dist/chunk-H37WRW2E.js.map +1 -0
  8. package/dist/{chunk-JB23A42P.js → chunk-NHUXO5H2.js} +2 -2
  9. package/dist/{chunk-B6UYVEMD.js → chunk-OO36JBFI.js} +35 -1
  10. package/dist/chunk-OO36JBFI.js.map +1 -0
  11. package/dist/{claude-code-updater-PQYMHNQT.js → claude-code-updater-H7RNX6M7.js} +3 -3
  12. package/dist/{claude-pair-runtime-RKDNAYEU.js → claude-pair-runtime-53JY6TQX.js} +4 -4
  13. package/dist/{claude-signin-reset-EF3UO5IK.js → claude-signin-reset-6IJMMVYG.js} +3 -3
  14. package/dist/lib/manager-worker.js +20 -20
  15. package/dist/mcp/index.js +17 -8
  16. package/dist/{persistent-session-2365YL7P.js → persistent-session-HVYGIS5C.js} +5 -5
  17. package/dist/{responsiveness-probe-7HARR32R.js → responsiveness-probe-OGZ6ADVR.js} +5 -5
  18. package/package.json +1 -1
  19. package/dist/chunk-B6UYVEMD.js.map +0 -1
  20. package/dist/chunk-EK6JVOBH.js.map +0 -1
  21. package/dist/chunk-EL5MWGGH.js.map +0 -1
  22. /package/dist/{chunk-P2B2SQHD.js.map → chunk-45UH24I2.js.map} +0 -0
  23. /package/dist/{chunk-OAWK4OBN.js.map → chunk-6HBXEJHO.js.map} +0 -0
  24. /package/dist/{chunk-JB23A42P.js.map → chunk-NHUXO5H2.js.map} +0 -0
  25. /package/dist/{claude-code-updater-PQYMHNQT.js.map → claude-code-updater-H7RNX6M7.js.map} +0 -0
  26. /package/dist/{claude-pair-runtime-RKDNAYEU.js.map → claude-pair-runtime-53JY6TQX.js.map} +0 -0
  27. /package/dist/{claude-signin-reset-EF3UO5IK.js.map → claude-signin-reset-6IJMMVYG.js.map} +0 -0
  28. /package/dist/{persistent-session-2365YL7P.js.map → persistent-session-HVYGIS5C.js.map} +0 -0
  29. /package/dist/{responsiveness-probe-7HARR32R.js.map → responsiveness-probe-OGZ6ADVR.js.map} +0 -0
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-47P33SZF.js";
8
8
  import {
9
9
  effectiveClaudeJsonPath
10
- } from "./chunk-EL5MWGGH.js";
10
+ } from "./chunk-AIUIMEUS.js";
11
11
 
12
12
  // src/lib/claude-code-updater.ts
13
13
  import { execFile, execFileSync as execFileSync2 } from "child_process";
@@ -722,4 +722,4 @@ export {
722
722
  probeClaudeInstalls,
723
723
  maybeUpdateClaudeCode
724
724
  };
725
- //# sourceMappingURL=chunk-JB23A42P.js.map
725
+ //# sourceMappingURL=chunk-NHUXO5H2.js.map
@@ -26,6 +26,36 @@ function paneRemedy(codeName, consentDialogVisible) {
26
26
  }
27
27
  return attach;
28
28
  }
29
+ var CUSTOM_API_KEY_DIALOG_TEXT = "Detected a custom API key";
30
+ function isCustomApiKeyDialogVisible(screen) {
31
+ const block = focusedModalBlock(screen, CONSENT_BLOCK_LINES);
32
+ return !!block && block.some((line) => line.includes(CUSTOM_API_KEY_DIALOG_TEXT));
33
+ }
34
+ function promptShowsInjectedKey(screen, injectedApiKey) {
35
+ const tail = injectedApiKey.slice(-20);
36
+ const block = focusedModalBlock(screen, CONSENT_BLOCK_LINES) ?? [];
37
+ const heading = lastIndexWhere(block, (line) => line.includes(CUSTOM_API_KEY_DIALOG_TEXT));
38
+ if (heading < 0) return false;
39
+ const after = block.slice(heading + 1);
40
+ const question = after.findIndex((line) => line.includes("Do you want to use this API key"));
41
+ if (question < 0) return false;
42
+ return after.slice(0, question).some((line) => line.includes("ANTHROPIC_API_KEY") && line.trimEnd().endsWith(`...${tail}`));
43
+ }
44
+ function customApiKeyDialogKeys(screen, injectedApiKey) {
45
+ if (!injectedApiKey || !isCustomApiKeyDialogVisible(screen)) return null;
46
+ if (!promptShowsInjectedKey(screen, injectedApiKey)) return null;
47
+ return selectRowKeys(screen, "Yes");
48
+ }
49
+ function customApiKeyDialogRefusal(screen, injectedApiKey) {
50
+ if (!injectedApiKey) return "the manager did not inject a key for this session, so the key on screen came from elsewhere";
51
+ if (!promptShowsInjectedKey(screen, injectedApiKey)) {
52
+ return "the key on screen does not match the injected key, or Claude Code rendered it differently";
53
+ }
54
+ return "the key matched, but the Yes row or the cursor could not be read";
55
+ }
56
+ function loginPickerAdvice(apiKeyHost) {
57
+ return apiKeyHost ? "this is an API-key host, so do not re-pair or run /login. Claude Code did not accept the key: check that the host has a key set and that it is in the session" : "agent cannot start until ~/.claude.json is provisioned. Pair via the Hosts page or run 'claude /login' on the host";
58
+ }
29
59
  function isResumeModeDialogVisible(screen) {
30
60
  return screen.includes("Resume from summary") && screen.includes("Don't ask me again");
31
61
  }
@@ -373,6 +403,10 @@ export {
373
403
  isConsumerTermsDialogVisible,
374
404
  consumerTermsOffKeys,
375
405
  paneRemedy,
406
+ isCustomApiKeyDialogVisible,
407
+ customApiKeyDialogKeys,
408
+ customApiKeyDialogRefusal,
409
+ loginPickerAdvice,
376
410
  isResumeModeDialogVisible,
377
411
  selectRowKeys,
378
412
  findUsageLimitResetHint,
@@ -387,4 +421,4 @@ export {
387
421
  classifyUnanswerablePane,
388
422
  summarizeUnanswerablePane
389
423
  };
390
- //# sourceMappingURL=chunk-B6UYVEMD.js.map
424
+ //# sourceMappingURL=chunk-OO36JBFI.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 /** ENG-11513: the manager's answer to a dangerous-rm permission prompt. */\n | 'dangerous-rm-backstop'\n /** ENG-11793: approving the key the manager itself injected. */\n | 'custom-api-key';\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 * ENG-10972: the operator-facing remedy for a session a human has to unstick at the\n * pane. EVERY \"needs a human\" line goes through this, and that is the point.\n *\n * On 2026-09-24 three Primo Cards agents sat blocked on the rate-limit prompt with\n * the consumer-terms dialog ALSO on the pane. The remedy text said only \"tmux\n * attach\". That dialog's cursor defaults to \"Help improve our AI models: ON\", which\n * opts the CUSTOMER's account into model training and stretches retention from 30\n * days to 5 years. The operator who cleared them by hand was one Enter away from\n * consenting on a customer's behalf, three times, and nothing he was shown said so.\n *\n * The blocking KIND never says \"consumer-terms\": on those panes the blocker was the\n * rate-limit prompt, and the consent dialog sat beside it. So the caller passes\n * whether a consent dialog is VISIBLE on the pane. That is a boolean, which is not\n * pane content, so it may leave the host in an alert row. The pane text itself may\n * not (see summarizeUnanswerablePane).\n *\n * Three states, because one site speaks before the pane has been read:\n * true -> name the safe row, and say not to press Enter\n * false -> the bare attach command; no warning on every hold, or it becomes noise\n * null -> we did not look, so a CONDITIONAL caution rather than silence\n *\n * The named row is `CONSUMER_TERMS_OFF_OPTION`, the same constant the automated\n * sweeper selects by name, so the human and the machine give the same answer.\n */\nexport function paneRemedy(codeName: string, consentDialogVisible: boolean | null): string {\n const attach = `tmux attach -t agt-${codeName}`;\n if (consentDialogVisible === true) {\n return (\n // Never glue punctuation to the command: `${attach}.` is copied as a session\n // named \"agt-<name>.\" that does not exist. Same \" (...)\" shape as the null\n // branch, and single-line because every caller embeds this mid-sentence.\n `${attach} (A CONSENT DIALOG IS ON THIS PANE: do NOT press Enter - the cursor defaults ` +\n `to opting this account into model training. Select \"${CONSUMER_TERMS_OFF_OPTION}\" by name, or Esc)`\n );\n }\n if (consentDialogVisible === null) {\n return (\n `${attach} (if the Consumer Terms dialog is showing, do NOT press Enter on the default - ` +\n `select \"${CONSUMER_TERMS_OFF_OPTION}\" by name)`\n );\n }\n return attach;\n}\n\nconst CUSTOM_API_KEY_DIALOG_TEXT = 'Detected a custom API key';\n\n/**\n * ENG-11793: Claude Code's \"Do you want to use this API key?\" prompt, measured on\n * 2.1.308. Its cursor defaults to \"No (recommended)\". Enter there saves the key as\n * rejected and lands on the login picker, so a bare Enter is the wrong answer.\n */\nexport function isCustomApiKeyDialogVisible(screen: string): boolean {\n const block = focusedModalBlock(screen, CONSENT_BLOCK_LINES);\n return !!block && block.some((line) => line.includes(CUSTOM_API_KEY_DIALOG_TEXT));\n}\n\n/**\n * ENG-11793: does the prompt's own key row show the injected key? The focused block also\n * holds lines of transcript above the prompt, so only the lines between the prompt's own\n * heading and its question count. No recognisable row there means no match.\n */\nfunction promptShowsInjectedKey(screen: string, injectedApiKey: string): boolean {\n // Must match CLAUDE_API_KEY_TAIL_CHARS in claude-config-location.ts: Claude Code stores 20.\n const tail = injectedApiKey.slice(-20);\n const block = focusedModalBlock(screen, CONSENT_BLOCK_LINES) ?? [];\n const heading = lastIndexWhere(block, (line) => line.includes(CUSTOM_API_KEY_DIALOG_TEXT));\n if (heading < 0) return false;\n const after = block.slice(heading + 1);\n const question = after.findIndex((line) => line.includes('Do you want to use this API key'));\n if (question < 0) return false;\n return after\n .slice(0, question)\n .some((line) => line.includes('ANTHROPIC_API_KEY') && line.trimEnd().endsWith(`...${tail}`));\n}\n\n/**\n * ENG-11793: the keys that approve the prompt, or null if we won't answer it.\n *\n * Only ever approves the key the manager injected, matched by the tail on screen.\n * This is the backstop for the config write in `approveApiKeyInClaudeConfig`, for a\n * Claude Code version that changes how approval is stored.\n */\nexport function customApiKeyDialogKeys(\n screen: string,\n injectedApiKey: string | null,\n): readonly string[] | null {\n if (!injectedApiKey || !isCustomApiKeyDialogVisible(screen)) return null;\n if (!promptShowsInjectedKey(screen, injectedApiKey)) return null;\n return selectRowKeys(screen, 'Yes');\n}\n\n/**\n * ENG-11793: why `customApiKeyDialogKeys` declined, in words for the log. These point\n * triage in different directions, so they must not share one message.\n */\nexport function customApiKeyDialogRefusal(screen: string, injectedApiKey: string | null): string {\n if (!injectedApiKey) return 'the manager did not inject a key for this session, so the key on screen came from elsewhere';\n if (!promptShowsInjectedKey(screen, injectedApiKey)) {\n return 'the key on screen does not match the injected key, or Claude Code rendered it differently';\n }\n return 'the key matched, but the Yes row or the cursor could not be read';\n}\n\n/**\n * ENG-11793: what to do about the login picker. An API-key host has no account to\n * pair, so \"pair\" or \"/login\" is the wrong advice there.\n */\nexport function loginPickerAdvice(apiKeyHost: boolean): string {\n return apiKeyHost\n ? 'this is an API-key host, so do not re-pair or run /login. Claude Code did not accept the key: ' +\n 'check that the host has a key set and that it is in the session'\n : \"agent cannot start until ~/.claude.json is provisioned. Pair via the Hosts page or run 'claude /login' on the host\";\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 const line = lines[i]!;\n if (line.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n if (isToolPermissionFooter(line)) {\n // Only a footer that sits under a numbered option row counts; see\n // isToolPermissionFooter for why the footer alone is not enough.\n // ...and only when no input box was rendered after it (Obi Wan\n // ceb92dbbc62b): a VERBATIM quote of the whole dialog - an agent reading\n // this ticket, which reproduces eliza's pane - passes both structural\n // checks, but the session is still live, so its input box sits below.\n const block = lines.slice(Math.max(0, i - USAGE_LIMIT_BLOCK_LINES), i + 1);\n const inputBoxAfter = lines.slice(i + 1).some((l) => INPUT_BOX_RULE.test(l.trim()));\n if (!inputBoxAfter && block.some((l) => NUMBERED_OPTION_ROW.test(l))) {\n footer = i;\n break;\n }\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 * ENG-10357: the footer Claude Code renders under a TOOL-PERMISSION prompt,\n * which is the dialog its dangerous-rm guard raises (and it raises it even with\n * bypass-permissions on):\n *\n * Dangerous rm operation on critical path: /tmp_rows.txt\n *\n * Do you want to proceed?\n * ❯ 1. Yes\n * 2. No\n * Esc to cancel · Tab to amend · ctrl+e to explain\n *\n * It does NOT say \"Enter to confirm\", so before this the whole blocked-prompt\n * stack (monitor, reaper, session gate) read that pane as clear. Five freezes\n * between 11 and 30 Sep (eliza, vigil, alfred, quinn, vera; up to ~15h each)\n * were every one found by a human reading a pane, never by an alert, while the\n * manager logged `decision=healthy` every cycle.\n *\n * Observed variants, both matched: with and without ` · ctrl+e to explain`.\n *\n * ANCHORED ON \"Tab to amend\", NOT \"Esc to cancel\". The usage-limit banner\n * renders `… · esc to cancel` on a line of its own, and it is not this dialog.\n *\n * QUOTES ARE THE HAZARD. Agents working this ticket (or reading an incident\n * write-up) will have this dialog in their own output, sometimes verbatim. The\n * caller requires three things, and each rejects a different quote shape:\n * - the footer STARTS the line -> rejects an inline prose mention\n * - a numbered option row sits above it -> rejects a bare footer line\n * - NO input-box rule is rendered after -> rejects a verbatim multi-line\n * quote, because a live session still shows its input box below it,\n * whereas a parked prompt has replaced the input box\n * A false positive is still only a noisy page, never a keystroke: nothing in\n * this module ANSWERS an unclaimed dialog (default-deny is unchanged).\n */\nexport function isToolPermissionFooter(line: string): boolean {\n return /^\\s*Esc to cancel · Tab to amend\\b/.test(line);\n}\n\n/** The Claude Code input box's top rule, as channel-input-watchdog reads it. */\nconst INPUT_BOX_RULE = /^[─━]{10,}/;\n\n/**\n * ENG-10357: true when the session is parked on a tool-permission prompt: the\n * pane's focused dialog is a permission footer under numbered options, and no\n * input box has been rendered after it (a quoted prompt has one below it).\n */\nexport function isParkedOnToolPermissionPrompt(screen: string): boolean {\n const block = focusedConfirmDialogBlock(screen);\n if (!block) return false;\n const footerLine = block[block.length - 1]!;\n if (!isToolPermissionFooter(footerLine)) return false;\n const lines = screen.split('\\n');\n let footerIdx = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i] === footerLine) {\n footerIdx = i;\n break;\n }\n }\n if (footerIdx < 0) return false;\n return !lines.slice(footerIdx + 1).some((l) => INPUT_BOX_RULE.test(l.trim()));\n}\n\n/** A numbered option row, e.g. `❯ 1. Yes` or ` 2. No`. */\nconst NUMBERED_OPTION_ROW = /^\\s*(?:❯\\s*)?\\d\\.\\s+\\S/;\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 * ENG-10972 hardening: the kind of the modal that OWNS THE BOTTOM of the pane -\n * the one the session is actually sitting on - or null when none does.\n *\n * `classifyUnanswerablePane` is a whole-screen substring test, and measured on\n * 2026-09-24 it classifies all four of these as blocked: an agent's message that\n * mentions \"Select login method\", a code line containing the picker's option\n * text, and a usage-limit or unknown modal QUOTED mid-transcript with a live input\n * box below it (`focusedConfirmDialogBlock` finds the LAST \"Enter to confirm\"\n * anywhere on screen). The reaper and spawn-dialogs can use it anyway because they\n * carry a corroborating prior (see `BlockedPromptSource` in\n * session-blocked-prompt.ts). A per-poll watchdog sweep has none, so it uses this.\n *\n * Grounded in the real panes, not guessed: a live confirm modal ends on its\n * footer, and the host-captured login picker (eng-9932's LOGIN_PICKER_PANE) ends\n * on an option row. Anything after that - an input box, more transcript - means\n * the session is not parked on it.\n *\n * The kind is read off WHAT owns the last line, never off the whole screen\n * (CodeRabbit, #6485). The first version checked \"some modal owns the bottom\" and\n * then classified the full pane, where the login-picker test runs first - so a\n * usage-limit modal parked at the bottom with \"Select login method\" anywhere above\n * it was reported as a login picker. So:\n * - a confirm footer at the bottom is classified from its own focused block, and\n * the login-picker substring test never runs (the picker has no such footer);\n * - a picker option row at the bottom is a login picker, and nothing else.\n * Even the 7-line focused block can carry a transcript line above a short modal,\n * which is why the confirm branch checks only the usage-limit and unclaimed kinds.\n * The one question it does NOT scope to the block is \"do we answer this dialog\n * ourselves?\" (sweepDialogs): that text can sit above the block (see below).\n *\n * Deliberately NOT folded into `classifyUnanswerablePane`: tightening that would\n * change the reaper's and spawn's behaviour without live-pane evidence that no\n * Claude Code version renders a hint line under its modals. For the watchdog's\n * busy-ack marker a false negative only restores the old generic notice, so this\n * is the safe place to be strict.\n */\nexport function parkedModalKind(screen: string | null | undefined): UnanswerablePaneKind | null {\n if (!screen) return null;\n const lines = screen.split('\\n').filter((l) => l.trim() !== '');\n const last = lines[lines.length - 1] ?? '';\n if (last.includes('Enter to confirm')) {\n const block = focusedConfirmDialogBlock(screen);\n if (!block) return null;\n const text = block.join('\\n');\n if (isUnanswerableUsageLimitDialog(text)) return 'usage-limit-unanswerable';\n // Obi Wan 9307eb1cbdd8: \"is this a dialog we ANSWER?\" is a whole-screen\n // question. sweepDialogs recognises dialogs by text that can sit far above the\n // footer - the real Bypass Permissions pane's is 11+ lines up, outside the\n // 7-line block - so a block-only check turned claimed dialogs into \"unclaimed\"\n // and marked an agent stuck on a prompt acceptDialogs was about to answer.\n // A claimed dialog merely QUOTED above an unknown modal now reads as claimed:\n // a false negative, which only restores the generic busy notice.\n if (sweepDialogs(screen) !== null) return null;\n if (isUnclaimedConfirmDialog(text)) return 'unclaimed-modal';\n return null;\n }\n // ENG-10357 (Obi Wan b0cdec049c2b): the tool-permission prompt does NOT end on\n // its footer - both real captures carry lines below it (eliza: `⎿ Waiting…`, a\n // spinner and the bypass-mode line; vera: queued direct-chat previews). So the\n // \"last line is the footer\" rule above never sees it, and the watchdog cleared\n // the blocked-prompt marker on every poll for exactly the pane that froze vera.\n // What separates a PARKED permission prompt from a QUOTED one is the thing the\n // 2026-09-24 measurement named: a quoted modal has a live input box below it,\n // and a parked one has replaced the input box. So: the last permission footer\n // (structurally checked by focusedConfirmDialogBlock), with no input-box divider\n // after it.\n if (isParkedOnToolPermissionPrompt(screen)) {\n if (sweepDialogs(screen) !== null) return null;\n return 'unclaimed-modal';\n }\n if (/Claude account with subscription|Anthropic Console account|3rd-party platform/.test(last)) {\n return isLoginPickerVisible(screen) ? 'login-picker' : null;\n }\n return null;\n}\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":[]}
@@ -13,10 +13,10 @@ import {
13
13
  probeClaudeInstalls,
14
14
  readsAsNativeInstall,
15
15
  shouldRunClaudeCodeUpdater
16
- } from "./chunk-JB23A42P.js";
16
+ } from "./chunk-NHUXO5H2.js";
17
17
  import "./chunk-XWVM4KPK.js";
18
18
  import "./chunk-47P33SZF.js";
19
- import "./chunk-EL5MWGGH.js";
19
+ import "./chunk-AIUIMEUS.js";
20
20
  import "./chunk-DGUM43GV.js";
21
21
  export {
22
22
  CLAUDE_CODE_SECURITY_FLOOR,
@@ -34,4 +34,4 @@ export {
34
34
  readsAsNativeInstall,
35
35
  shouldRunClaudeCodeUpdater
36
36
  };
37
- //# sourceMappingURL=claude-code-updater-PQYMHNQT.js.map
37
+ //# sourceMappingURL=claude-code-updater-H7RNX6M7.js.map
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  consumerTermsOffKeys,
6
6
  selectRowKeys
7
- } from "./chunk-B6UYVEMD.js";
7
+ } from "./chunk-OO36JBFI.js";
8
8
  import {
9
9
  redactPaneFrame,
10
10
  sanitizeTerminalFrame
@@ -15,7 +15,7 @@ import {
15
15
  claudeJsonPathFor,
16
16
  effectiveClaudeJsonPath,
17
17
  inspectClaudeConfig
18
- } from "./chunk-EL5MWGGH.js";
18
+ } from "./chunk-AIUIMEUS.js";
19
19
  import "./chunk-DGUM43GV.js";
20
20
 
21
21
  // src/lib/claude-pair-runtime.ts
@@ -321,7 +321,7 @@ async function spawnPairSession(session) {
321
321
  return { ok: true };
322
322
  } catch {
323
323
  }
324
- const { resolveClaudeBinary } = await import("./persistent-session-2365YL7P.js");
324
+ const { resolveClaudeBinary } = await import("./persistent-session-HVYGIS5C.js");
325
325
  const claudeBin = resolveClaudeBinary();
326
326
  const pairEnv = {
327
327
  ...process.env,
@@ -836,4 +836,4 @@ export {
836
836
  startClaudePair,
837
837
  submitClaudePairCode
838
838
  };
839
- //# sourceMappingURL=claude-pair-runtime-RKDNAYEU.js.map
839
+ //# sourceMappingURL=claude-pair-runtime-53JY6TQX.js.map
@@ -7,8 +7,8 @@ import {
7
7
  resetClaudeSignin,
8
8
  signinResetBackupPath,
9
9
  signinResetInForce
10
- } from "./chunk-OAWK4OBN.js";
11
- import "./chunk-EL5MWGGH.js";
10
+ } from "./chunk-6HBXEJHO.js";
11
+ import "./chunk-AIUIMEUS.js";
12
12
  import "./chunk-DGUM43GV.js";
13
13
  export {
14
14
  CLAUDE_CREDENTIALS_FILE,
@@ -20,4 +20,4 @@ export {
20
20
  signinResetBackupPath,
21
21
  signinResetInForce
22
22
  };
23
- //# sourceMappingURL=claude-signin-reset-EF3UO5IK.js.map
23
+ //# sourceMappingURL=claude-signin-reset-6IJMMVYG.js.map
@@ -77,7 +77,7 @@ import {
77
77
  setConfigHash,
78
78
  shellGuard,
79
79
  tripClass
80
- } from "../chunk-P2B2SQHD.js";
80
+ } from "../chunk-45UH24I2.js";
81
81
  import {
82
82
  CLAUDE_CODE_SECURITY_FLOOR,
83
83
  SELF_UPDATE_TIMEOUTS,
@@ -91,7 +91,7 @@ import {
91
91
  runSafeCleanup,
92
92
  tightestDiskStat,
93
93
  withPackageCacheLock
94
- } from "../chunk-JB23A42P.js";
94
+ } from "../chunk-NHUXO5H2.js";
95
95
  import {
96
96
  getProjectDir as getProjectDir2,
97
97
  getReadyTasks,
@@ -178,7 +178,7 @@ import {
178
178
  webFetchBaselineValue,
179
179
  webFetchRestartNeeded,
180
180
  writeEgressAllowlist
181
- } from "../chunk-EK6JVOBH.js";
181
+ } from "../chunk-H37WRW2E.js";
182
182
  import {
183
183
  isAgentIdle,
184
184
  isStaleForToday,
@@ -195,8 +195,8 @@ import {
195
195
  paneRemedy,
196
196
  sendDialogKeys,
197
197
  simpleTextHash
198
- } from "../chunk-B6UYVEMD.js";
199
- import "../chunk-OAWK4OBN.js";
198
+ } from "../chunk-OO36JBFI.js";
199
+ import "../chunk-6HBXEJHO.js";
200
200
  import {
201
201
  parsePsRows,
202
202
  reapOrphanChannelMcps
@@ -312,7 +312,7 @@ import {
312
312
  claudeConfigDirIsReady,
313
313
  effectiveClaudeJsonPath,
314
314
  seedClaudeConfigFromLegacy
315
- } from "../chunk-EL5MWGGH.js";
315
+ } from "../chunk-AIUIMEUS.js";
316
316
  import "../chunk-DGUM43GV.js";
317
317
 
318
318
  // src/lib/manager-worker.ts
@@ -15131,7 +15131,7 @@ var pendingDayRolloverReset = /* @__PURE__ */ new Set();
15131
15131
  var dayRolloverInboundHold = /* @__PURE__ */ new Map();
15132
15132
  var INBOUND_HOLD_EPISODE_GAP_MS = 12e4;
15133
15133
  async function channelInboundActivityAgeSecondsFor(codeName) {
15134
- const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-7HARR32R.js");
15134
+ const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-OGZ6ADVR.js");
15135
15135
  const newest = newestPendingInboundActivityMtimeMs(dirname11(paneLogPath(codeName)));
15136
15136
  if (newest === null) return null;
15137
15137
  return Math.max(0, Math.floor((Date.now() - newest) / 1e3));
@@ -15928,7 +15928,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
15928
15928
  var lastVersionCheckAt = 0;
15929
15929
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
15930
15930
  var lastResponsivenessProbeAt = 0;
15931
- var agtCliVersion = true ? "0.28.1175" : "dev";
15931
+ var agtCliVersion = true ? "0.28.1177" : "dev";
15932
15932
  function resolveBrewPath(execFileSync3) {
15933
15933
  try {
15934
15934
  const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -17740,7 +17740,7 @@ function flushRestartedAgentDiagnostics(hostId, codeNames) {
17740
17740
  if (codeNames.length === 0) return;
17741
17741
  void (async () => {
17742
17742
  try {
17743
- const { collectDiagnostics } = await import("../persistent-session-2365YL7P.js");
17743
+ const { collectDiagnostics } = await import("../persistent-session-HVYGIS5C.js");
17744
17744
  await api.post("/host/heartbeat", {
17745
17745
  host_id: hostId,
17746
17746
  agent_diagnostics: collectDiagnostics(codeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
@@ -17883,7 +17883,7 @@ async function pollCycleInner() {
17883
17883
  }
17884
17884
  try {
17885
17885
  const { detectHostSecurity } = await import("../host-security-KQYYZZVW.js");
17886
- const { collectDiagnostics } = await import("../persistent-session-2365YL7P.js");
17886
+ const { collectDiagnostics } = await import("../persistent-session-HVYGIS5C.js");
17887
17887
  const diagCodeNames = [...agentState.persistentSessionAgents];
17888
17888
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor) : void 0;
17889
17889
  let tailscaleHostname;
@@ -18087,7 +18087,7 @@ async function pollCycleInner() {
18087
18087
  setConfigHash(hbResp?.config_hash ?? null);
18088
18088
  try {
18089
18089
  const desiredClaudeCodeVersion = hbResp?.desired_claude_code_version?.trim();
18090
- const { maybeUpdateClaudeCode, shouldRunClaudeCodeUpdater } = await import("../claude-code-updater-PQYMHNQT.js");
18090
+ const { maybeUpdateClaudeCode, shouldRunClaudeCodeUpdater } = await import("../claude-code-updater-H7RNX6M7.js");
18091
18091
  const securityFloor = currentSecurityFloor();
18092
18092
  claudeUpdaterInFlight = true;
18093
18093
  if (desiredClaudeCodeVersion && (claudePairLoop.busy || claudePairingInProgress())) {
@@ -18122,7 +18122,7 @@ async function pollCycleInner() {
18122
18122
  collectPanelessActivityProbes,
18123
18123
  getResponsivenessIntervalMs,
18124
18124
  occupancyQualificationClassifications
18125
- } = await import("../responsiveness-probe-7HARR32R.js");
18125
+ } = await import("../responsiveness-probe-OGZ6ADVR.js");
18126
18126
  const probeIntervalMs = getResponsivenessIntervalMs();
18127
18127
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
18128
18128
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -18239,7 +18239,7 @@ async function pollCycleInner() {
18239
18239
  collectResponsivenessProbes,
18240
18240
  livePendingInboundOldestAgeSeconds,
18241
18241
  parkPendingInbound
18242
- } = await import("../responsiveness-probe-7HARR32R.js");
18242
+ } = await import("../responsiveness-probe-OGZ6ADVR.js");
18243
18243
  const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-M6AFZWNU.js");
18244
18244
  const wedgeNow = /* @__PURE__ */ new Date();
18245
18245
  const liveAgents = agentState.persistentSessionAgents;
@@ -18525,7 +18525,7 @@ async function pollCycleInner() {
18525
18525
  }
18526
18526
  }
18527
18527
  );
18528
- const { parkPendingInbound } = await import("../responsiveness-probe-7HARR32R.js");
18528
+ const { parkPendingInbound } = await import("../responsiveness-probe-OGZ6ADVR.js");
18529
18529
  const { parked, deadLettered } = parkPendingInbound(codeName, new Date(fatalNowMs));
18530
18530
  paneFatalStreak.delete(codeName);
18531
18531
  paneFatalRestarts.set(codeName, [...history, fatalNowMs]);
@@ -18556,7 +18556,7 @@ async function pollCycleInner() {
18556
18556
  TRANSCRIPT_MOUNT_ESCALATION_REFRESH_MS,
18557
18557
  bindSourceEscalations
18558
18558
  } = await import("../transcript-mount-health-OHE6IVJV.js");
18559
- const { transcriptBindSourcesFor, ensureTranscriptBindSources } = await import("../persistent-session-2365YL7P.js");
18559
+ const { transcriptBindSourcesFor, ensureTranscriptBindSources } = await import("../persistent-session-HVYGIS5C.js");
18560
18560
  const mountConfig = resolveTranscriptMountConfig();
18561
18561
  const liveAgents = agentState.persistentSessionAgents;
18562
18562
  for (const tracked of [
@@ -23410,7 +23410,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
23410
23410
  try {
23411
23411
  const freshId = rotateSessionForWedge(codeName);
23412
23412
  consecutiveWedgeCycles.delete(codeName);
23413
- const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-7HARR32R.js");
23413
+ const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-OGZ6ADVR.js");
23414
23414
  const { parked, deadLettered } = parkForRestart(codeName, /* @__PURE__ */ new Date());
23415
23415
  const parkNote = parked > 0 ? `, ${parked} inbound parked` : "";
23416
23416
  const deadNote = deadLettered > 0 ? `, ${deadLettered} undeliverable dead-lettered` : "";
@@ -23453,7 +23453,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
23453
23453
  void api.post("/host/restart-ack", { host_id: hostId, agent_id: agentId, restart_requested_at: requestedAt }).catch((err) => log(`[restart-lane] ack failed for '${codeName}': ${err.message}`));
23454
23454
  void (async () => {
23455
23455
  try {
23456
- const { collectDiagnostics } = await import("../persistent-session-2365YL7P.js");
23456
+ const { collectDiagnostics } = await import("../persistent-session-HVYGIS5C.js");
23457
23457
  await api.post("/host/heartbeat", {
23458
23458
  host_id: hostId,
23459
23459
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
@@ -23504,7 +23504,7 @@ async function respawnAgentAfterMcpStop(codeName, reason) {
23504
23504
  }
23505
23505
  try {
23506
23506
  const hostId = await getHostId();
23507
- const { collectDiagnostics } = await import("../persistent-session-2365YL7P.js");
23507
+ const { collectDiagnostics } = await import("../persistent-session-HVYGIS5C.js");
23508
23508
  await api.post("/host/heartbeat", {
23509
23509
  host_id: hostId,
23510
23510
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
@@ -24206,8 +24206,8 @@ async function processClaudePairSessions(agents) {
24206
24206
  snapshotPairConfig,
24207
24207
  // ENG-11659: result -> report body + the manager.log line that records it.
24208
24208
  planPairStartReport
24209
- } = await import("../claude-pair-runtime-RKDNAYEU.js");
24210
- const { resetClaudeSignin, describeSigninResetForLog, describeSigninResetFailure } = await import("../claude-signin-reset-EF3UO5IK.js");
24209
+ } = await import("../claude-pair-runtime-53JY6TQX.js");
24210
+ const { resetClaudeSignin, describeSigninResetForLog, describeSigninResetFailure } = await import("../claude-signin-reset-6IJMMVYG.js");
24211
24211
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
24212
24212
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
24213
24213
  const killed = await killPairSession(pairTmuxSession(pairId));
package/dist/mcp/index.js CHANGED
@@ -47678,13 +47678,14 @@ function cardTextAuthor(card) {
47678
47678
  // src/kanban-list-render.ts
47679
47679
  var KANBAN_LIST_DISPLAY_ORDER = [
47680
47680
  "in_progress",
47681
- "waiting",
47682
- "needs_attention",
47683
47681
  "todo",
47684
47682
  "backlog",
47683
+ "waiting",
47684
+ "needs_attention",
47685
47685
  "done",
47686
47686
  "failed"
47687
47687
  ];
47688
+ var KANBAN_SEARCH_STATUSES = [...KANBAN_LIST_DISPLAY_ORDER, "all"];
47688
47689
  function kanbanStatusLabel(status) {
47689
47690
  const labels = {
47690
47691
  backlog: "Backlog",
@@ -47801,7 +47802,9 @@ ${desc}` : head;
47801
47802
  function renderTruncationNotice(t) {
47802
47803
  if (!t?.truncated) return null;
47803
47804
  const of = typeof t.total === "number" && typeof t.shown === "number" ? `showing ${t.shown} of ${t.total} active cards` : typeof t.total === "number" ? `${t.total} active cards exist` : "this board is capped";
47804
- return `[BOARD TRUNCATED \u2014 ${of}. The counts below are a FLOOR, not a total. Do not report them as your full board; use kanban_search for history, and treat any per-status count here as "at least N".]`;
47805
+ const dropped = Object.entries(t.droppedByStatus ?? {});
47806
+ const cut = dropped.length ? ` Not shown: ${dropped.map(([status, n]) => `${n} ${status.replaceAll("_", " ")}`).join(", ")}.` : "";
47807
+ return `[BOARD TRUNCATED \u2014 ${of}. The counts below are a FLOOR, not a total. Do not report them as your full board; use kanban_search for history, and treat any per-status count here as "at least N".${cut}]`;
47805
47808
  }
47806
47809
  function renderAssignerClause(item) {
47807
47810
  const by = describeKanbanAssigner(item, { always: false });
@@ -47842,8 +47845,9 @@ ${OWED_REPLY_UNAVAILABLE_NOTE}` : empty;
47842
47845
  for (const status of order) {
47843
47846
  const group = grouped[status];
47844
47847
  if (!group?.length) continue;
47848
+ const cut = truncation?.droppedByStatus?.[status] ?? 0;
47845
47849
  lines.push(`
47846
- ## ${kanbanStatusLabel(status)} (${group.length})`);
47850
+ ## ${kanbanStatusLabel(status)} (${cut ? `${group.length} shown, ${cut} more: kanban_search with status="${status}"` : group.length})`);
47847
47851
  for (const item of group) {
47848
47852
  const owedSuffix = isOwedReplyByMe(item) ? ` [reply owed to ${item.owed_reply_from_label}]` : void 0;
47849
47853
  lines.push(renderKanbanItemLine(item, nowMs, owedSuffix));
@@ -47870,8 +47874,11 @@ ${OWED_REPLY_UNAVAILABLE_NOTE}` : empty;
47870
47874
  if (truncation?.owedReplies) lines.push(OWED_REPLY_TRUNCATED_NOTE);
47871
47875
  if (truncation?.owedRepliesUnavailable) lines.push(OWED_REPLY_UNAVAILABLE_NOTE);
47872
47876
  if (humanAssigned.length) {
47873
- lines.push(`
47874
- ## ${HUMAN_ASSIGNED_SECTION_HEADING} (${humanAssigned.length})`);
47877
+ const cutAssigned = truncation?.droppedByStatus?.["assigned_to_person"] ?? 0;
47878
+ lines.push(
47879
+ `
47880
+ ## ${HUMAN_ASSIGNED_SECTION_HEADING} (${cutAssigned ? `${humanAssigned.length} shown, ${cutAssigned} more not shown` : humanAssigned.length})`
47881
+ );
47875
47882
  lines.push(HUMAN_ASSIGNED_SECTION_NOTE);
47876
47883
  for (const item of humanAssigned) {
47877
47884
  const who = item.assignee_name ? ` [with ${item.assignee_name}]` : "";
@@ -49500,7 +49507,9 @@ server.tool(
49500
49507
  owedRepliesUnavailable: data.owed_reply_unavailable === true,
49501
49508
  truncated: data.active_truncated === true,
49502
49509
  total: data.active_total,
49503
- shown: data.active_limit
49510
+ // ENG-11547: `active_shown` can pass the limit; an older API only sends the limit.
49511
+ shown: data.active_shown ?? data.active_limit,
49512
+ droppedByStatus: data.active_dropped_by_status
49504
49513
  })
49505
49514
  }
49506
49515
  ]
@@ -49513,7 +49522,7 @@ server.tool(
49513
49522
  `Search your FULL kanban history by keyword \u2014 including done cards older than 24h that kanban_list omits. Use this BEFORE telling anyone you have "no record" of work they say you did: the board only shows the last 24h of completed cards, so "it's not on my board" does NOT mean you never did it. Each hit includes the card's result (the actual deliverable), so you can recite what you produced. An empty query returns your most recent cards across all statuses.`,
49514
49523
  {
49515
49524
  query: external_exports.string().optional().describe('Keyword to match across title, description, result, deliverable and notes (e.g. "carousels", "Caribbean Park"). Omit to get your most recent cards.'),
49516
- status: external_exports.enum(["backlog", "todo", "in_progress", "done", "failed", "all"]).optional().describe('Restrict to one status. Default "all" searches every status \u2014 leave it unset to reach old done/failed work.'),
49525
+ status: external_exports.enum(KANBAN_SEARCH_STATUSES).optional().describe('Restrict to one status. Default "all" searches every status \u2014 leave it unset to reach old done/failed work.'),
49517
49526
  since: external_exports.string().optional().describe('ISO date lower bound on created_at (e.g. "2026-06-06").'),
49518
49527
  until: external_exports.string().optional().describe("ISO date upper bound on created_at."),
49519
49528
  limit: external_exports.number().int().min(1).max(50).optional().describe("Max results (default 20, max 50).")
@@ -68,13 +68,13 @@ import {
68
68
  writeDirectChatSessionState,
69
69
  writeEgressAllowlist,
70
70
  writePersistentClaudeWrapper
71
- } from "./chunk-EK6JVOBH.js";
71
+ } from "./chunk-H37WRW2E.js";
72
72
  import "./chunk-DJNCIWVG.js";
73
- import "./chunk-B6UYVEMD.js";
74
- import "./chunk-OAWK4OBN.js";
73
+ import "./chunk-OO36JBFI.js";
74
+ import "./chunk-6HBXEJHO.js";
75
75
  import "./chunk-XWVM4KPK.js";
76
76
  import "./chunk-47P33SZF.js";
77
- import "./chunk-EL5MWGGH.js";
77
+ import "./chunk-AIUIMEUS.js";
78
78
  import "./chunk-DGUM43GV.js";
79
79
  export {
80
80
  CLAUDE_HOST_WIDE,
@@ -147,4 +147,4 @@ export {
147
147
  writeEgressAllowlist,
148
148
  writePersistentClaudeWrapper
149
149
  };
150
- //# sourceMappingURL=persistent-session-2365YL7P.js.map
150
+ //# sourceMappingURL=persistent-session-HVYGIS5C.js.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  paneLogPath
3
- } from "./chunk-EK6JVOBH.js";
3
+ } from "./chunk-H37WRW2E.js";
4
4
  import "./chunk-DJNCIWVG.js";
5
- import "./chunk-B6UYVEMD.js";
6
- import "./chunk-OAWK4OBN.js";
5
+ import "./chunk-OO36JBFI.js";
6
+ import "./chunk-6HBXEJHO.js";
7
7
  import "./chunk-XWVM4KPK.js";
8
8
  import "./chunk-47P33SZF.js";
9
- import "./chunk-EL5MWGGH.js";
9
+ import "./chunk-AIUIMEUS.js";
10
10
  import "./chunk-DGUM43GV.js";
11
11
 
12
12
  // src/lib/responsiveness-probe.ts
@@ -804,4 +804,4 @@ export {
804
804
  readAndResetSlackReplyBindingClassifications,
805
805
  readAndResetSlackReplyTargetClassifications
806
806
  };
807
- //# sourceMappingURL=responsiveness-probe-7HARR32R.js.map
807
+ //# sourceMappingURL=responsiveness-probe-OGZ6ADVR.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.1175",
3
+ "version": "0.28.1177",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {