crewly 1.2.1 → 1.2.3

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 (72) hide show
  1. package/config/roles/architect/prompt.md +7 -5
  2. package/config/roles/backend-developer/prompt.md +8 -4
  3. package/config/roles/content-strategist/prompt.md +12 -4
  4. package/config/roles/designer/prompt.md +8 -4
  5. package/config/roles/developer/prompt.md +7 -4
  6. package/config/roles/frontend-developer/prompt.md +8 -4
  7. package/config/roles/fullstack-dev/prompt.md +8 -4
  8. package/config/roles/generalist/prompt.md +7 -4
  9. package/config/roles/ops/prompt.md +7 -4
  10. package/config/roles/orchestrator/prompt.md +22 -1
  11. package/config/roles/product-manager/prompt.md +8 -4
  12. package/config/roles/qa/prompt.md +8 -4
  13. package/config/roles/qa-engineer/prompt.md +8 -4
  14. package/config/roles/sales/prompt.md +8 -4
  15. package/config/roles/support/prompt.md +8 -4
  16. package/config/roles/tpm/prompt.md +8 -4
  17. package/config/skills/orchestrator/delegate-task/execute.sh +2 -2
  18. package/config/templates/agent-claude-md.md +10 -5
  19. package/dist/backend/backend/src/constants.d.ts +7 -69
  20. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  21. package/dist/backend/backend/src/constants.js +7 -74
  22. package/dist/backend/backend/src/constants.js.map +1 -1
  23. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
  24. package/dist/backend/backend/src/controllers/chat/chat.controller.js +39 -0
  25. package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
  26. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +2 -13
  27. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  28. package/dist/backend/backend/src/services/agent/agent-registration.service.js +85 -133
  29. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  30. package/dist/backend/backend/src/services/agent/gemini-runtime.service.d.ts +1 -1
  31. package/dist/backend/backend/src/services/agent/gemini-runtime.service.js +8 -8
  32. package/dist/backend/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
  33. package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts +0 -7
  34. package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
  35. package/dist/backend/backend/src/services/event-bus/event-bus.service.js +0 -31
  36. package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
  37. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts +28 -4
  38. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
  39. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js +111 -58
  40. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
  41. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js +0 -3
  43. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js.map +1 -1
  44. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -1
  45. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  46. package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
  47. package/dist/backend/backend/src/services/session/session-command-helper.js +16 -4
  48. package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
  49. package/dist/backend/backend/src/utils/terminal-output.utils.d.ts +2 -1
  50. package/dist/backend/backend/src/utils/terminal-output.utils.d.ts.map +1 -1
  51. package/dist/backend/backend/src/utils/terminal-output.utils.js +2 -28
  52. package/dist/backend/backend/src/utils/terminal-output.utils.js.map +1 -1
  53. package/dist/backend/backend/src/utils/terminal-string-ops.d.ts +183 -0
  54. package/dist/backend/backend/src/utils/terminal-string-ops.d.ts.map +1 -0
  55. package/dist/backend/backend/src/utils/terminal-string-ops.js +717 -0
  56. package/dist/backend/backend/src/utils/terminal-string-ops.js.map +1 -0
  57. package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
  58. package/dist/backend/backend/src/websocket/terminal.gateway.js +22 -27
  59. package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
  60. package/dist/cli/backend/src/constants.d.ts +7 -69
  61. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  62. package/dist/cli/backend/src/constants.js +7 -74
  63. package/dist/cli/backend/src/constants.js.map +1 -1
  64. package/dist/cli/backend/src/utils/terminal-output.utils.d.ts +2 -1
  65. package/dist/cli/backend/src/utils/terminal-output.utils.d.ts.map +1 -1
  66. package/dist/cli/backend/src/utils/terminal-output.utils.js +2 -28
  67. package/dist/cli/backend/src/utils/terminal-output.utils.js.map +1 -1
  68. package/dist/cli/backend/src/utils/terminal-string-ops.d.ts +183 -0
  69. package/dist/cli/backend/src/utils/terminal-string-ops.d.ts.map +1 -0
  70. package/dist/cli/backend/src/utils/terminal-string-ops.js +717 -0
  71. package/dist/cli/backend/src/utils/terminal-string-ops.js.map +1 -0
  72. package/package.json +1 -1
@@ -14,6 +14,8 @@ import { ContextWindowMonitorService } from './context-window-monitor.service.js
14
14
  import { SubAgentMessageQueue } from '../messaging/sub-agent-message-queue.service.js';
15
15
  import { AgentSuspendService } from './agent-suspend.service.js';
16
16
  import { stripAnsiCodes } from '../../utils/terminal-output.utils.js';
17
+ import { isPromptLine, containsSpinnerOrWorkingIndicator, containsProcessingIndicator, containsBusyStatusBar, containsRewindMode, containsGeminiProcessingKeywords, extractChatPrefix, stripTuiLineBorders, matchTuiPromptLine, } from '../../utils/terminal-string-ops.js';
18
+ import { PtyActivityTrackerService } from './pty-activity-tracker.service.js';
17
19
  /**
18
20
  * Service responsible for the complex, multi-step process of agent initialization and registration.
19
21
  * Isolates the complex state management of agent startup with progressive escalation.
@@ -47,16 +49,10 @@ export class AgentRegistrationService {
47
49
  // which causes multiple Ctrl+C presses that can crash the runtime.
48
50
  sessionDeliveryMutex = new Map();
49
51
  // Terminal patterns are now centralized in TERMINAL_PATTERNS constant
50
- // Keeping these as static getters for backwards compatibility within the class
52
+ // Keeping prompt chars as static getter for backwards compatibility within the class
51
53
  static get CLAUDE_PROMPT_INDICATORS() {
52
54
  return TERMINAL_PATTERNS.PROMPT_CHARS;
53
55
  }
54
- static get CLAUDE_PROMPT_STREAM_PATTERN() {
55
- return TERMINAL_PATTERNS.PROMPT_STREAM;
56
- }
57
- static get CLAUDE_PROCESSING_INDICATORS() {
58
- return TERMINAL_PATTERNS.PROCESSING_INDICATORS;
59
- }
60
56
  constructor(_legacyTmuxService, // Legacy parameter for backwards compatibility
61
57
  projectRoot, storageService) {
62
58
  this.logger = LoggerService.getInstance().createComponentLogger('AgentRegistrationService');
@@ -1508,6 +1504,14 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
1508
1504
  await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_SESSION_NAME, sessionName);
1509
1505
  await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_ROLE, role);
1510
1506
  await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_API_URL, `http://localhost:${WEB_CONSTANTS.PORTS.BACKEND}`);
1507
+ // Pass Gemini API key to gemini-cli agents so they authenticate
1508
+ // with the paid API key instead of the free-tier Google login.
1509
+ if (runtimeType === RUNTIME_TYPES.GEMINI_CLI) {
1510
+ const geminiApiKey = process.env[ENV_CONSTANTS.GEMINI_API_KEY];
1511
+ if (geminiApiKey) {
1512
+ await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.GEMINI_API_KEY, geminiApiKey);
1513
+ }
1514
+ }
1511
1515
  this.logger.info('Agent session created and environment variables set, initializing with registration', {
1512
1516
  sessionName,
1513
1517
  role,
@@ -1701,9 +1705,9 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
1701
1705
  if (!delivered) {
1702
1706
  // Check if the agent is actively processing (busy) — the queue
1703
1707
  // processor can re-queue instead of permanently failing the message.
1704
- const busyOutput = sessionHelper.capturePane(sessionName).slice(-2000);
1705
- const isBusy = TERMINAL_PATTERNS.BUSY_STATUS_BAR.test(busyOutput) ||
1706
- TERMINAL_PATTERNS.PROCESSING_WITH_TEXT.test(busyOutput);
1708
+ // Use PTY idle time instead of regex patterns for robust cross-runtime detection.
1709
+ const idleMs = PtyActivityTrackerService.getInstance().getIdleTimeMs(sessionName);
1710
+ const isBusy = idleMs < SESSION_COMMAND_DELAYS.AGENT_BUSY_IDLE_THRESHOLD_MS;
1707
1711
  return {
1708
1712
  success: false,
1709
1713
  error: isBusy
@@ -1768,9 +1772,6 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
1768
1772
  if (!session) {
1769
1773
  return false;
1770
1774
  }
1771
- // Use runtime-specific pattern for stream detection to avoid false positives
1772
- // (e.g. Gemini's `> ` pattern matching markdown blockquotes in Claude Code output)
1773
- const streamPattern = this.getPromptPatternForRuntime(runtimeType);
1774
1775
  return new Promise((resolve) => {
1775
1776
  let resolved = false;
1776
1777
  const pollInterval = EVENT_DELIVERY_CONSTANTS.AGENT_READY_POLL_INTERVAL;
@@ -1817,9 +1818,11 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
1817
1818
  if (resolved)
1818
1819
  return;
1819
1820
  // Strip ANSI escape sequences before testing — raw PTY data contains
1820
- // cursor positioning, color codes, etc. that break regex matching (#106)
1821
+ // cursor positioning, color codes, etc. that break pattern matching (#106)
1821
1822
  const cleanData = stripAnsiCodes(data);
1822
- if (streamPattern.test(cleanData)) {
1823
+ // Check each line for prompt pattern (string-based, no regex)
1824
+ const hasPromptInStream = cleanData.split('\n').some(line => line.trim().length > 0 && isPromptLine(line, runtimeType));
1825
+ if (hasPromptInStream) {
1823
1826
  // Double-check with capturePane to avoid false positives from partial data
1824
1827
  const output = sessionHelper.capturePane(sessionName);
1825
1828
  if (this.isClaudeAtPrompt(output, runtimeType)) {
@@ -2052,7 +2055,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2052
2055
  }
2053
2056
  // Phase 1: Wait for Claude to be at prompt before sending
2054
2057
  if (!messageSent) {
2055
- const isAtPrompt = AgentRegistrationService.CLAUDE_PROMPT_STREAM_PATTERN.test(buffer);
2058
+ const isAtPrompt = buffer.split('\n').some(line => line.trim().length > 0 && isPromptLine(line));
2056
2059
  if (isAtPrompt) {
2057
2060
  sendMessageNow();
2058
2061
  }
@@ -2063,9 +2066,9 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2063
2066
  return; // Wait for Enter to be sent
2064
2067
  }
2065
2068
  // Look for processing indicators confirming delivery
2066
- const hasProcessingIndicator = AgentRegistrationService.CLAUDE_PROCESSING_INDICATORS.some((pattern) => pattern.test(buffer));
2069
+ const hasProcessingIndicator = containsProcessingIndicator(buffer);
2067
2070
  // Also check if prompt disappeared (Claude is working)
2068
- const promptStillVisible = AgentRegistrationService.CLAUDE_PROMPT_STREAM_PATTERN.test(buffer);
2071
+ const promptStillVisible = buffer.split('\n').some(line => line.trim().length > 0 && isPromptLine(line));
2069
2072
  // Use constant for minimum buffer check (P3.2 fix)
2070
2073
  if (hasProcessingIndicator || (!promptStillVisible && buffer.length > EVENT_DELIVERY_CONSTANTS.MIN_BUFFER_FOR_PROCESSING_DETECTION)) {
2071
2074
  this.logger.debug('Message delivery confirmed (event-driven)', {
@@ -2114,12 +2117,10 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2114
2117
  if (!this.isClaudeAtPrompt(output, runtimeType)) {
2115
2118
  if (attempt === maxAttempts) {
2116
2119
  // On the final attempt, check if the agent is DEFINITELY busy
2117
- // before force-delivering. If we see "esc to interrupt" or
2118
- // processing indicators, the agent is actively working and
2119
- // force-delivery risks corrupting its current task.
2120
- const tailForBusyCheck = output.slice(-2000);
2121
- const isBusy = TERMINAL_PATTERNS.BUSY_STATUS_BAR.test(tailForBusyCheck) ||
2122
- TERMINAL_PATTERNS.PROCESSING_WITH_TEXT.test(tailForBusyCheck);
2120
+ // before force-delivering. Use PTY idle time for robust
2121
+ // cross-runtime detection instead of fragile regex patterns.
2122
+ const idleMs = PtyActivityTrackerService.getInstance().getIdleTimeMs(sessionName);
2123
+ const isBusy = idleMs < SESSION_COMMAND_DELAYS.AGENT_BUSY_IDLE_THRESHOLD_MS;
2123
2124
  if (isBusy) {
2124
2125
  this.logger.warn('Agent is busy (processing indicators detected), skipping force delivery', {
2125
2126
  sessionName,
@@ -2182,8 +2183,30 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2182
2183
  // for focus cycling and overlay dismissal.
2183
2184
  if (isClaudeCode) {
2184
2185
  if (attempt > 1) {
2186
+ // On retry: Ctrl+C to cancel stale input, then PTY resize to force
2187
+ // TUI re-render (SIGWINCH), then Tab to cycle Ink focus.
2185
2188
  await sessionHelper.sendCtrlC(sessionName);
2186
2189
  await delay(300);
2190
+ try {
2191
+ const session = sessionHelper.getSession(sessionName);
2192
+ if (session) {
2193
+ session.resize(81, 25);
2194
+ await delay(200);
2195
+ session.resize(80, 24);
2196
+ await delay(300);
2197
+ }
2198
+ }
2199
+ catch { /* non-fatal */ }
2200
+ await sessionHelper.sendKey(sessionName, 'Tab');
2201
+ await delay(300);
2202
+ }
2203
+ else {
2204
+ // First attempt: lightweight focus nudge — Tab key cycles Ink's
2205
+ // focusNext() to ensure the InputPrompt is active. This prevents
2206
+ // the "write succeeds but TUI ignores it" failure mode without
2207
+ // the overhead of Ctrl+C or PTY resize.
2208
+ await sessionHelper.sendKey(sessionName, 'Tab');
2209
+ await delay(200);
2187
2210
  }
2188
2211
  }
2189
2212
  else {
@@ -2318,7 +2341,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2318
2341
  // definitive proof the agent accepted and is working.
2319
2342
  // Only check spinner/⏺ chars — NOT text words like
2320
2343
  // "thinking" which appear in historical response text.
2321
- if (TERMINAL_PATTERNS.PROCESSING.test(currentOutput)) {
2344
+ if (containsSpinnerOrWorkingIndicator(currentOutput)) {
2322
2345
  this.logger.debug('Processing indicators detected — message accepted', {
2323
2346
  sessionName,
2324
2347
  attempt,
@@ -2353,7 +2376,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2353
2376
  await delay(SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY);
2354
2377
  // Verify recovery
2355
2378
  const postEnterOutput = sessionHelper.capturePane(sessionName);
2356
- if (TERMINAL_PATTERNS.PROCESSING.test(postEnterOutput) ||
2379
+ if (containsSpinnerOrWorkingIndicator(postEnterOutput) ||
2357
2380
  !this.isClaudeAtPrompt(postEnterOutput, runtimeType)) {
2358
2381
  this.logger.info('Enter recovery from prompt successful', {
2359
2382
  sessionName,
@@ -2400,7 +2423,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2400
2423
  await delay(SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY);
2401
2424
  // Verify recovery: check if processing started
2402
2425
  const recoveryOutput = sessionHelper.capturePane(sessionName);
2403
- if (TERMINAL_PATTERNS.PROCESSING.test(recoveryOutput)) {
2426
+ if (containsSpinnerOrWorkingIndicator(recoveryOutput)) {
2404
2427
  this.logger.info('Enter recovery successful — processing started', {
2405
2428
  sessionName,
2406
2429
  attempt,
@@ -2487,9 +2510,9 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2487
2510
  .filter((line) => !beforeLines.has(line))
2488
2511
  .join('\n');
2489
2512
  }
2490
- const hasProcessingIndicators = TERMINAL_PATTERNS.PROCESSING_WITH_TEXT.test(newContent || afterOutput.slice(-500));
2513
+ const hasProcessingIndicators = containsProcessingIndicator(newContent || afterOutput.slice(-500));
2491
2514
  const hasGeminiIndicators = newContent.length > 0
2492
- && /reading|thinking|processing|analyzing|generating|searching/i.test(newContent);
2515
+ && containsGeminiProcessingKeywords(newContent);
2493
2516
  const significantLengthChange = Math.abs(lengthDiff) > 10;
2494
2517
  // For Gemini CLI, contentChanged alone is sufficient evidence of
2495
2518
  // delivery. The TUI redraws minimally (lengthDiff can be as low as
@@ -2568,19 +2591,11 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2568
2591
  }
2569
2592
  }
2570
2593
  }
2571
- // Verification failed, but the message was physically written to the PTY.
2572
- // If the session is still alive, the agent will likely process it — return
2573
- // true to avoid false "Failed to deliver" errors shown to users (#99).
2574
- const backend = getSessionBackendSync();
2575
- const childAlive = backend?.isChildProcessAlive?.(sessionName);
2576
- if (childAlive !== false) {
2577
- this.logger.warn('Message delivery verification inconclusive but session alive — assuming success', {
2578
- sessionName,
2579
- maxAttempts,
2580
- messageLength: message.length,
2581
- });
2582
- return true;
2583
- }
2594
+ this.logger.warn('Message delivery verification failed all attempts exhausted', {
2595
+ sessionName,
2596
+ maxAttempts,
2597
+ messageLength: message.length,
2598
+ });
2584
2599
  this.logger.error('Message delivery failed after all retry attempts', {
2585
2600
  sessionName,
2586
2601
  maxAttempts,
@@ -2608,13 +2623,13 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2608
2623
  }
2609
2624
  // Extract a search token from the message:
2610
2625
  // Strip [CHAT:uuid] prefix if present, then take the first 40 chars
2611
- const chatPrefixMatch = message.match(/^\[CHAT:[^\]]+\]\s*/);
2612
- const contentAfterPrefix = chatPrefixMatch
2613
- ? message.slice(chatPrefixMatch[0].length)
2626
+ const { prefixLength } = extractChatPrefix(message);
2627
+ const contentAfterPrefix = prefixLength > 0
2628
+ ? message.slice(prefixLength)
2614
2629
  : message;
2615
2630
  const searchToken = contentAfterPrefix.slice(0, 40).trim();
2616
2631
  // Also use [CHAT: as a secondary token if message has a CHAT prefix
2617
- const chatToken = chatPrefixMatch ? '[CHAT:' : null;
2632
+ const chatToken = prefixLength > 0 ? '[CHAT:' : null;
2618
2633
  // Check last 20 non-empty lines for either token.
2619
2634
  // Gemini CLI TUI has status bars at the bottom (branch, sandbox, model info)
2620
2635
  // that push input content further up. 5 lines was insufficient.
@@ -2622,7 +2637,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2622
2637
  const linesToCheck = lines.slice(-20);
2623
2638
  const isStuck = linesToCheck.some((line) => {
2624
2639
  // Strip TUI box-drawing borders before checking (Gemini CLI wraps content in │...│)
2625
- const stripped = line.replace(/^[│┃║|\s]+/, '').replace(/[│┃║|\s]+$/, '');
2640
+ const stripped = stripTuiLineBorders(line);
2626
2641
  if (searchToken && (line.includes(searchToken) || stripped.includes(searchToken)))
2627
2642
  return true;
2628
2643
  if (chatToken && (line.includes(chatToken) || stripped.includes(chatToken)))
@@ -2672,9 +2687,9 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2672
2687
  return false;
2673
2688
  // Extract search token: first 30 chars of the message content
2674
2689
  // (after stripping any [CHAT:uuid] prefix)
2675
- const chatPrefixMatch = message.match(/^\[CHAT:[^\]]+\]\s*/);
2676
- const contentAfterPrefix = chatPrefixMatch
2677
- ? message.slice(chatPrefixMatch[0].length)
2690
+ const { prefixLength } = extractChatPrefix(message);
2691
+ const contentAfterPrefix = prefixLength > 0
2692
+ ? message.slice(prefixLength)
2678
2693
  : message;
2679
2694
  const searchToken = contentAfterPrefix.slice(0, 30).trim();
2680
2695
  if (!searchToken)
@@ -2684,14 +2699,13 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2684
2699
  // Or without borders: > text
2685
2700
  // The prompt line is the line with `> ` followed by actual content.
2686
2701
  const lines = output.split('\n');
2687
- const promptLineRegex = /^[│┃║|\s]*>\s+(.+)/;
2688
2702
  for (let i = lines.length - 1; i >= Math.max(0, lines.length - 25); i--) {
2689
2703
  const line = lines[i];
2690
- const match = line.match(promptLineRegex);
2691
- if (match) {
2692
- const promptContent = match[1].replace(/[│┃║|\s]+$/, '').trim();
2704
+ const promptContent = matchTuiPromptLine(line);
2705
+ if (promptContent !== null) {
2706
+ const trimmedContent = stripTuiLineBorders(promptContent).trim();
2693
2707
  // Check if the prompt line content contains our message text
2694
- if (promptContent.length > 5 && promptContent.includes(searchToken)) {
2708
+ if (trimmedContent.length > 5 && trimmedContent.includes(searchToken)) {
2695
2709
  this.logger.warn('Text stuck at TUI prompt — Enter was not pressed', {
2696
2710
  sessionName,
2697
2711
  searchToken: searchToken.slice(0, 20),
@@ -2797,7 +2811,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2797
2811
  for (const sessionName of sessionHelper.listSessions()) {
2798
2812
  try {
2799
2813
  const output = sessionHelper.capturePane(sessionName);
2800
- if (TERMINAL_PATTERNS.REWIND_MODE.test(output)) {
2814
+ if (containsRewindMode(output)) {
2801
2815
  this.logger.warn('Rewind mode detected, sending q to exit', { sessionName });
2802
2816
  sessionHelper.writeRaw(sessionName, 'q');
2803
2817
  await delay(500);
@@ -2820,19 +2834,19 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2820
2834
  continue;
2821
2835
  // Look for any text sitting on the prompt line
2822
2836
  const lines = output.split('\n');
2823
- const promptLineRegex = /^[│┃║|\s]*>\s+(.+)/;
2824
2837
  for (let i = lines.length - 1; i >= Math.max(0, lines.length - 25); i--) {
2825
- const match = lines[i].match(promptLineRegex);
2826
- if (match) {
2827
- const promptContent = match[1].replace(/[│┃║|\s]+$/, '').trim();
2838
+ const promptContent = matchTuiPromptLine(lines[i]);
2839
+ if (promptContent !== null) {
2840
+ const trimmedContent = stripTuiLineBorders(promptContent).trim();
2828
2841
  // Only act on substantial text (> 10 chars) to avoid false positives
2829
2842
  // from TUI rendering artifacts or short status text
2830
- if (promptContent.length > 10) {
2843
+ if (trimmedContent.length > 10) {
2831
2844
  // Skip known Gemini CLI idle placeholder text that sits at
2832
2845
  // the `> ` prompt when no user input is present. These are
2833
2846
  // NOT stuck messages — they are TUI decoration.
2834
- const isPlaceholder = /^Type your message/i.test(promptContent) ||
2835
- /^@[\w/.]+/.test(promptContent); // e.g., "@path/to/file"
2847
+ const lowerContent = trimmedContent.toLowerCase();
2848
+ const isPlaceholder = lowerContent.startsWith('type your message') ||
2849
+ trimmedContent.startsWith('@'); // e.g., "@path/to/file"
2836
2850
  if (isPlaceholder) {
2837
2851
  break;
2838
2852
  }
@@ -2927,8 +2941,8 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2927
2941
  */
2928
2942
  trackSentMessage(sessionName, message) {
2929
2943
  // Extract a search snippet: skip [CHAT:uuid] prefix, take first 80 chars
2930
- const prefixMatch = message.match(/^\[CHAT:[^\]]+\]\s*/);
2931
- const contentStart = prefixMatch ? prefixMatch[0].length : 0;
2944
+ const { prefixLength } = extractChatPrefix(message);
2945
+ const contentStart = prefixLength;
2932
2946
  // Normalize whitespace: messages may contain \n from enhanced templates.
2933
2947
  // Terminal bottom text is join(' '), so \n in snippet would never match.
2934
2948
  const snippet = message.slice(contentStart, contentStart + 80).replace(/\s+/g, ' ').trim();
@@ -2942,27 +2956,10 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2942
2956
  // Ensure the background scanner is running
2943
2957
  this.startStuckMessageDetector();
2944
2958
  }
2945
- /**
2946
- * Get the runtime-specific prompt regex pattern.
2947
- * Avoids false positives by using narrow patterns when runtime is known.
2948
- *
2949
- * @param runtimeType - The runtime type (claude-code, gemini-cli, etc.)
2950
- * @returns The appropriate prompt detection regex
2951
- */
2952
- getPromptPatternForRuntime(runtimeType) {
2953
- if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE)
2954
- return TERMINAL_PATTERNS.CLAUDE_CODE_PROMPT;
2955
- if (runtimeType === RUNTIME_TYPES.GEMINI_CLI)
2956
- return TERMINAL_PATTERNS.GEMINI_CLI_PROMPT;
2957
- if (runtimeType === RUNTIME_TYPES.CODEX_CLI)
2958
- return TERMINAL_PATTERNS.CODEX_CLI_PROMPT;
2959
- return TERMINAL_PATTERNS.PROMPT_STREAM;
2960
- }
2961
2959
  /**
2962
2960
  * Check if the agent appears to be at an input prompt.
2963
- * Looks for prompt indicators (❯, ⏵, $, ❯❯, ⏵⏵) in terminal output.
2964
- * Also checks for busy indicators (esc to interrupt, spinners, ⏺)
2965
- * to avoid false negatives when the agent is processing.
2961
+ * Delegates to the regex-free isAgentAtPrompt() from terminal-string-ops,
2962
+ * with additional logging and per-line prompt detection using isPromptLine().
2966
2963
  *
2967
2964
  * @param terminalOutput - The terminal output to check
2968
2965
  * @param runtimeType - The runtime type for pattern selection
@@ -2979,57 +2976,12 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
2979
2976
  // Use 5000 chars to accommodate large tool outputs that push the prompt
2980
2977
  // further back in the buffer (#106).
2981
2978
  const tailSection = terminalOutput.slice(-5000);
2982
- const isGemini = runtimeType === RUNTIME_TYPES.GEMINI_CLI;
2983
- const isClaudeCode = runtimeType === RUNTIME_TYPES.CLAUDE_CODE;
2984
- const isCodex = runtimeType === RUNTIME_TYPES.CODEX_CLI;
2985
- const streamPattern = this.getPromptPatternForRuntime(runtimeType);
2986
2979
  // Check for prompt FIRST. Processing indicators like "thinking" or "analyzing"
2987
2980
  // can appear in the agent's previous response text and persist in the terminal
2988
2981
  // scroll buffer, causing false negatives if checked before the prompt.
2989
- if (streamPattern.test(tailSection)) {
2990
- return true;
2991
- }
2992
- // Fallback: check last several lines for prompt indicators.
2993
- // The prompt may not be on the very last line due to status bars,
2994
- // notifications, or terminal wrapping below the prompt.
2995
2982
  const lines = tailSection.split('\n').filter((line) => line.trim().length > 0);
2996
2983
  const linesToCheck = lines.slice(-10);
2997
- const hasPrompt = linesToCheck.some((line) => {
2998
- const trimmed = line.trim();
2999
- // Strip TUI box-drawing borders that Gemini CLI and other TUI frameworks
3000
- // wrap around prompts. Covers full Unicode box-drawing range (#106).
3001
- const stripped = trimmed
3002
- .replace(/^[\u2500-\u257F|+\-═║╭╮╰╯]+\s*/, '')
3003
- .replace(/\s*[\u2500-\u257F|+\-═║╭╮╰╯]+$/, '');
3004
- // Claude Code prompts: ❯, ⏵, $ alone on a line
3005
- if (!isGemini && !isCodex) {
3006
- if (['❯', '⏵', '$'].some(ch => trimmed === ch || stripped === ch)) {
3007
- return true;
3008
- }
3009
- // ❯❯ = bypass permissions prompt (idle).
3010
- // Matches "❯❯", "❯❯ ", and "❯❯ bypass permissions on (shift+tab to cycle)".
3011
- // Note: ⏵⏵ appears in the status bar but is visible both when idle AND
3012
- // busy, so it cannot be used as a reliable prompt indicator.
3013
- if (trimmed.startsWith('❯❯')) {
3014
- return true;
3015
- }
3016
- }
3017
- // Gemini CLI prompts: > or ! followed by space
3018
- if (!isClaudeCode) {
3019
- if (isCodex) {
3020
- // Codex prompt uses `›`; avoid plain `> ` to prevent false-positives
3021
- // from markdown blockquotes in agent output.
3022
- if (trimmed.startsWith('› ') || stripped.startsWith('› ')) {
3023
- return true;
3024
- }
3025
- }
3026
- else if (trimmed.startsWith('> ') || trimmed.startsWith('! ') ||
3027
- stripped.startsWith('> ') || stripped.startsWith('! ')) {
3028
- return true;
3029
- }
3030
- }
3031
- return false;
3032
- });
2984
+ const hasPrompt = linesToCheck.some(line => isPromptLine(line, runtimeType));
3033
2985
  if (hasPrompt) {
3034
2986
  return true;
3035
2987
  }
@@ -3037,14 +2989,14 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
3037
2989
  // Check last 10 lines (not just 5) because tool output can push processing
3038
2990
  // indicators further up while the status bar stays at the bottom.
3039
2991
  const recentLines = linesToCheck.join('\n');
3040
- if (TERMINAL_PATTERNS.PROCESSING_WITH_TEXT.test(recentLines)) {
2992
+ if (containsProcessingIndicator(recentLines)) {
3041
2993
  this.logger.debug('Processing indicators present near bottom of output');
3042
2994
  return false;
3043
2995
  }
3044
2996
  // Check for "esc to interrupt" in the status bar — this is a definitive
3045
2997
  // busy signal. Claude Code only shows this text while actively processing.
3046
2998
  // It disappears when the agent returns to idle at the prompt.
3047
- if (TERMINAL_PATTERNS.BUSY_STATUS_BAR.test(recentLines)) {
2999
+ if (containsBusyStatusBar(recentLines)) {
3048
3000
  this.logger.debug('Busy status bar detected (esc to interrupt)');
3049
3001
  return false;
3050
3002
  }
@@ -3302,7 +3254,7 @@ After checking in, just say "Ready for tasks" and wait for me to send you work.`
3302
3254
  return false;
3303
3255
  const currentOutput = sessionHelper.capturePane(sessionName);
3304
3256
  // Processing indicators (spinners) = definitive success
3305
- if (TERMINAL_PATTERNS.PROCESSING.test(currentOutput)) {
3257
+ if (containsSpinnerOrWorkingIndicator(currentOutput)) {
3306
3258
  this.logger.debug('Kickoff delivered — processing indicators detected', {
3307
3259
  sessionName, checkIndex: i,
3308
3260
  });