commandmate 0.2.10 → 0.2.12

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 (70) hide show
  1. package/.env.example +2 -1
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/app-build-manifest.json +12 -12
  4. package/.next/app-path-routes-manifest.json +1 -1
  5. package/.next/build-manifest.json +2 -2
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/cache/config.json +3 -3
  8. package/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/.next/cache/webpack/client-production/1.pack +0 -0
  10. package/.next/cache/webpack/client-production/2.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack +0 -0
  12. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  13. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  14. package/.next/cache/webpack/server-production/0.pack +0 -0
  15. package/.next/cache/webpack/server-production/index.pack +0 -0
  16. package/.next/next-server.js.nft.json +1 -1
  17. package/.next/prerender-manifest.json +1 -1
  18. package/.next/required-server-files.json +1 -1
  19. package/.next/routes-manifest.json +1 -1
  20. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/app/update-check/route.js +1 -1
  22. package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
  23. package/.next/server/app/api/repositories/clone/route.js +1 -1
  24. package/.next/server/app/api/repositories/route.js +2 -2
  25. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  26. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  27. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  28. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  29. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  30. package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js +1 -1
  31. package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
  32. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js +1 -1
  33. package/.next/server/app/page_client-reference-manifest.js +1 -1
  34. package/.next/server/app/worktrees/[id]/files/[...path]/page.js +1 -1
  35. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/worktrees/[id]/page.js +3 -3
  37. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  38. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app-paths-manifest.json +10 -10
  40. package/.next/server/chunks/2509.js +1 -0
  41. package/.next/server/chunks/369.js +1 -1
  42. package/.next/server/chunks/5488.js +4 -4
  43. package/.next/server/chunks/7808.js +1 -1
  44. package/.next/server/chunks/8693.js +1 -0
  45. package/.next/server/chunks/8744.js +1 -1
  46. package/.next/server/chunks/9238.js +14 -14
  47. package/.next/server/chunks/9367.js +2 -2
  48. package/.next/server/functions-config-manifest.json +1 -1
  49. package/.next/server/pages/500.html +1 -1
  50. package/.next/server/server-reference-manifest.json +1 -1
  51. package/.next/static/chunks/5970-2e18108d0cabd8af.js +1 -0
  52. package/.next/static/chunks/9178-88850a7c48deea07.js +1 -0
  53. package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-4a3c0861367e0391.js +1 -0
  54. package/.next/static/chunks/app/worktrees/[id]/page-c99258f57461962c.js +1 -0
  55. package/.next/static/css/897ffb669f47c97b.css +3 -0
  56. package/.next/trace +5 -5
  57. package/README.md +154 -181
  58. package/dist/server/src/config/auto-yes-config.js +44 -2
  59. package/dist/server/src/lib/auto-yes-manager.js +249 -41
  60. package/dist/server/src/lib/claude-session.js +60 -21
  61. package/dist/server/src/lib/prompt-key.js +30 -0
  62. package/package.json +19 -7
  63. package/.next/server/chunks/667.js +0 -1
  64. package/.next/static/chunks/5970-dc8fb1c8c0217636.js +0 -1
  65. package/.next/static/chunks/8864-2f60eadc8404fdd0.js +0 -1
  66. package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-ce9ac3658f2b7d91.js +0 -1
  67. package/.next/static/chunks/app/worktrees/[id]/page-1b8e4c49fbaf3f99.js +0 -1
  68. package/.next/static/css/a69d9c70fce558b4.css +0 -3
  69. /package/.next/static/{NGcx1ej6oVBba0MO0bwCg → ym6mA6Dl9wX62h3AoYO45}/_buildManifest.js +0 -0
  70. /package/.next/static/{NGcx1ej6oVBba0MO0bwCg → ym6mA6Dl9wX62h3AoYO45}/_ssgManifest.js +0 -0
@@ -9,16 +9,19 @@
9
9
  * auto-yes responses when browser tabs are in background.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.THINKING_CHECK_LINE_COUNT = exports.MAX_CONCURRENT_POLLERS = exports.MAX_CONSECUTIVE_ERRORS = exports.MAX_BACKOFF_MS = exports.POLLING_INTERVAL_MS = void 0;
12
+ exports.THINKING_CHECK_LINE_COUNT = exports.MAX_CONCURRENT_POLLERS = exports.MAX_CONSECUTIVE_ERRORS = exports.MAX_BACKOFF_MS = exports.COOLDOWN_INTERVAL_MS = exports.POLLING_INTERVAL_MS = void 0;
13
13
  exports.isValidWorktreeId = isValidWorktreeId;
14
14
  exports.calculateBackoffInterval = calculateBackoffInterval;
15
15
  exports.isAutoYesExpired = isAutoYesExpired;
16
16
  exports.getAutoYesState = getAutoYesState;
17
17
  exports.setAutoYesEnabled = setAutoYesEnabled;
18
+ exports.disableAutoYes = disableAutoYes;
18
19
  exports.clearAllAutoYesStates = clearAllAutoYesStates;
19
20
  exports.getActivePollerCount = getActivePollerCount;
20
21
  exports.clearAllPollerStates = clearAllPollerStates;
21
22
  exports.getLastServerResponseTimestamp = getLastServerResponseTimestamp;
23
+ exports.executeRegexWithTimeout = executeRegexWithTimeout;
24
+ exports.checkStopCondition = checkStopCondition;
22
25
  exports.startAutoYesPolling = startAutoYesPolling;
23
26
  exports.stopAutoYesPolling = stopAutoYesPolling;
24
27
  exports.stopAllAutoYesPolling = stopAllAutoYesPolling;
@@ -29,11 +32,14 @@ const prompt_answer_sender_1 = require("./prompt-answer-sender");
29
32
  const manager_1 = require("./cli-tools/manager");
30
33
  const cli_patterns_1 = require("./cli-patterns");
31
34
  const auto_yes_config_1 = require("../config/auto-yes-config");
35
+ const prompt_key_1 = require("./prompt-key");
32
36
  // =============================================================================
33
37
  // Constants (Issue #138)
34
38
  // =============================================================================
35
39
  /** Polling interval in milliseconds */
36
40
  exports.POLLING_INTERVAL_MS = 2000;
41
+ /** Cooldown interval after successful response (milliseconds) (Issue #306) */
42
+ exports.COOLDOWN_INTERVAL_MS = 5000;
37
43
  /** Maximum backoff interval in milliseconds (60 seconds) */
38
44
  exports.MAX_BACKOFF_MS = 60000;
39
45
  /** Number of consecutive errors before applying backoff */
@@ -52,6 +58,16 @@ exports.MAX_CONCURRENT_POLLERS = 50;
52
58
  exports.THINKING_CHECK_LINE_COUNT = 50;
53
59
  /** Worktree ID validation pattern (security: prevent command injection) */
54
60
  const WORKTREE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
61
+ /**
62
+ * Extract error message from unknown error type.
63
+ * Provides consistent error message extraction across the module (DRY).
64
+ *
65
+ * @param error - Unknown error object
66
+ * @returns Error message string, or 'Unknown error' for non-Error values
67
+ */
68
+ function getErrorMessage(error) {
69
+ return error instanceof Error ? error.message : 'Unknown error';
70
+ }
55
71
  /** In-memory storage for auto-yes states (globalThis for hot reload persistence) */
56
72
  const autoYesStates = globalThis.__autoYesStates ??
57
73
  (globalThis.__autoYesStates = new Map());
@@ -62,8 +78,11 @@ const autoYesPollerStates = globalThis.__autoYesPollerStates ??
62
78
  // Utility Functions
63
79
  // =============================================================================
64
80
  /**
65
- * Validate worktree ID format (security measure)
81
+ * Validate worktree ID format (security measure).
66
82
  * Only allows alphanumeric characters, hyphens, and underscores.
83
+ *
84
+ * @param worktreeId - Worktree ID to validate
85
+ * @returns true if the ID matches the allowed pattern
67
86
  */
68
87
  function isValidWorktreeId(worktreeId) {
69
88
  if (!worktreeId || worktreeId.length === 0)
@@ -71,7 +90,12 @@ function isValidWorktreeId(worktreeId) {
71
90
  return WORKTREE_ID_PATTERN.test(worktreeId);
72
91
  }
73
92
  /**
74
- * Calculate backoff interval based on consecutive errors
93
+ * Calculate backoff interval based on consecutive errors.
94
+ * Returns the normal polling interval when errors are below the threshold,
95
+ * and applies exponential backoff (capped at MAX_BACKOFF_MS) above it.
96
+ *
97
+ * @param consecutiveErrors - Number of consecutive errors encountered
98
+ * @returns Polling interval in milliseconds
75
99
  */
76
100
  function calculateBackoffInterval(consecutiveErrors) {
77
101
  if (consecutiveErrors < exports.MAX_CONSECUTIVE_ERRORS) {
@@ -89,27 +113,29 @@ function calculateBackoffInterval(consecutiveErrors) {
89
113
  // Auto-Yes State Management (Existing)
90
114
  // =============================================================================
91
115
  /**
92
- * Check if an auto-yes state has expired
116
+ * Check if an auto-yes state has expired.
117
+ * Compares current time against the expiresAt timestamp.
118
+ *
119
+ * @param state - Auto-yes state to check
120
+ * @returns true if the current time is past the expiration time
93
121
  */
94
122
  function isAutoYesExpired(state) {
95
123
  return Date.now() > state.expiresAt;
96
124
  }
97
125
  /**
98
- * Get the auto-yes state for a worktree
99
- * Returns null if no state exists or if expired (auto-disables on expiry)
126
+ * Get the auto-yes state for a worktree.
127
+ * Returns null if no state exists. If expired, auto-disables and returns the disabled state.
128
+ *
129
+ * @param worktreeId - Worktree identifier
130
+ * @returns Current auto-yes state, or null if no state exists
100
131
  */
101
132
  function getAutoYesState(worktreeId) {
102
133
  const state = autoYesStates.get(worktreeId);
103
134
  if (!state)
104
135
  return null;
105
- // Auto-disable if expired
136
+ // Auto-disable if expired (Issue #314: delegate to disableAutoYes)
106
137
  if (isAutoYesExpired(state)) {
107
- const disabledState = {
108
- ...state,
109
- enabled: false,
110
- };
111
- autoYesStates.set(worktreeId, disabledState);
112
- return disabledState;
138
+ return disableAutoYes(worktreeId, 'expired');
113
139
  }
114
140
  return state;
115
141
  }
@@ -117,8 +143,10 @@ function getAutoYesState(worktreeId) {
117
143
  * Set the auto-yes enabled state for a worktree
118
144
  * @param duration - Optional duration in milliseconds (must be an ALLOWED_DURATIONS value).
119
145
  * Defaults to DEFAULT_AUTO_YES_DURATION (1 hour) when omitted.
146
+ * @param stopPattern - Optional regex pattern for stop condition (Issue #314).
147
+ * When terminal output matches this pattern, auto-yes is automatically disabled.
120
148
  */
121
- function setAutoYesEnabled(worktreeId, enabled, duration) {
149
+ function setAutoYesEnabled(worktreeId, enabled, duration, stopPattern) {
122
150
  if (enabled) {
123
151
  const now = Date.now();
124
152
  const effectiveDuration = duration ?? auto_yes_config_1.DEFAULT_AUTO_YES_DURATION;
@@ -126,23 +154,41 @@ function setAutoYesEnabled(worktreeId, enabled, duration) {
126
154
  enabled: true,
127
155
  enabledAt: now,
128
156
  expiresAt: now + effectiveDuration,
157
+ stopPattern,
129
158
  };
130
159
  autoYesStates.set(worktreeId, state);
131
160
  return state;
132
161
  }
133
162
  else {
134
- const existing = autoYesStates.get(worktreeId);
135
- const state = {
136
- enabled: false,
137
- enabledAt: existing?.enabledAt ?? 0,
138
- expiresAt: existing?.expiresAt ?? 0,
139
- };
140
- autoYesStates.set(worktreeId, state);
141
- return state;
163
+ // Issue #314: Delegate disable path to disableAutoYes()
164
+ return disableAutoYes(worktreeId);
142
165
  }
143
166
  }
144
167
  /**
145
- * Clear all auto-yes states (for testing)
168
+ * Disable auto-yes for a worktree with an optional reason.
169
+ * Preserves existing state fields (enabledAt, expiresAt, stopPattern) for inspection.
170
+ *
171
+ * Issue #314: Centralized disable logic for expiration, stop pattern match, and manual disable.
172
+ *
173
+ * @param worktreeId - Worktree identifier
174
+ * @param reason - Optional reason for disabling ('expired' | 'stop_pattern_matched')
175
+ * @returns Updated auto-yes state
176
+ */
177
+ function disableAutoYes(worktreeId, reason) {
178
+ const existing = autoYesStates.get(worktreeId);
179
+ const state = {
180
+ enabled: false,
181
+ enabledAt: existing?.enabledAt ?? 0,
182
+ expiresAt: existing?.expiresAt ?? 0,
183
+ stopPattern: existing?.stopPattern,
184
+ stopReason: reason,
185
+ };
186
+ autoYesStates.set(worktreeId, state);
187
+ return state;
188
+ }
189
+ /**
190
+ * Clear all auto-yes states.
191
+ * @internal Exported for testing purposes only.
146
192
  */
147
193
  function clearAllAutoYesStates() {
148
194
  autoYesStates.clear();
@@ -151,28 +197,38 @@ function clearAllAutoYesStates() {
151
197
  // Server-side Polling (Issue #138)
152
198
  // =============================================================================
153
199
  /**
154
- * Get the number of active pollers
200
+ * Get the number of active pollers.
201
+ *
202
+ * @returns Count of currently active polling instances
155
203
  */
156
204
  function getActivePollerCount() {
157
205
  return autoYesPollerStates.size;
158
206
  }
159
207
  /**
160
- * Clear all poller states (for testing)
208
+ * Clear all poller states.
209
+ * Stops all active pollers before clearing state.
210
+ * @internal Exported for testing purposes only.
161
211
  */
162
212
  function clearAllPollerStates() {
163
213
  stopAllAutoYesPolling();
164
214
  autoYesPollerStates.clear();
165
215
  }
166
216
  /**
167
- * Get the last server response timestamp for a worktree
168
- * Used by clients to prevent duplicate responses
217
+ * Get the last server response timestamp for a worktree.
218
+ * Used by clients to prevent duplicate responses.
219
+ *
220
+ * @param worktreeId - Worktree identifier
221
+ * @returns Timestamp (Date.now()) of the last server response, or null if none
169
222
  */
170
223
  function getLastServerResponseTimestamp(worktreeId) {
171
224
  const pollerState = autoYesPollerStates.get(worktreeId);
172
225
  return pollerState?.lastServerResponseTimestamp ?? null;
173
226
  }
174
227
  /**
175
- * Update the last server response timestamp
228
+ * Update the last server response timestamp.
229
+ *
230
+ * @param worktreeId - Worktree identifier
231
+ * @param timestamp - Timestamp value (Date.now())
176
232
  */
177
233
  function updateLastServerResponseTimestamp(worktreeId, timestamp) {
178
234
  const pollerState = autoYesPollerStates.get(worktreeId);
@@ -181,7 +237,9 @@ function updateLastServerResponseTimestamp(worktreeId, timestamp) {
181
237
  }
182
238
  }
183
239
  /**
184
- * Reset error count for a poller
240
+ * Reset error count for a poller and restore the default polling interval.
241
+ *
242
+ * @param worktreeId - Worktree identifier
185
243
  */
186
244
  function resetErrorCount(worktreeId) {
187
245
  const pollerState = autoYesPollerStates.get(worktreeId);
@@ -191,7 +249,9 @@ function resetErrorCount(worktreeId) {
191
249
  }
192
250
  }
193
251
  /**
194
- * Increment error count and apply backoff if needed
252
+ * Increment error count and apply backoff if the threshold is exceeded.
253
+ *
254
+ * @param worktreeId - Worktree identifier
195
255
  */
196
256
  function incrementErrorCount(worktreeId) {
197
257
  const pollerState = autoYesPollerStates.get(worktreeId);
@@ -201,7 +261,97 @@ function incrementErrorCount(worktreeId) {
201
261
  }
202
262
  }
203
263
  /**
204
- * Internal polling function (setTimeout recursive)
264
+ * Check if the given prompt has already been answered.
265
+ * Extracted from pollAutoYes() to reduce responsibility concentration (F005/SRP).
266
+ *
267
+ * @param pollerState - Current poller state containing the last answered prompt key
268
+ * @param promptKey - Composite key of the current prompt (generated by generatePromptKey)
269
+ * @returns true if the prompt key matches the last answered prompt key
270
+ */
271
+ function isDuplicatePrompt(pollerState, promptKey) {
272
+ return pollerState.lastAnsweredPromptKey === promptKey;
273
+ }
274
+ // =============================================================================
275
+ // Stop Condition (Issue #314)
276
+ // =============================================================================
277
+ /**
278
+ * Execute a regex test with timeout protection.
279
+ * Uses synchronous execution with safe-regex2 pre-validation as the primary defense.
280
+ *
281
+ * Note: Node.js is single-threaded, so true async timeout requires Worker threads.
282
+ * The safe-regex2 pre-validation in validateStopPattern() prevents catastrophic
283
+ * backtracking patterns from reaching this function. The timeoutMs parameter is
284
+ * reserved for future Worker thread implementation.
285
+ *
286
+ * @internal Exported for testing purposes only.
287
+ * @param regex - Pre-compiled RegExp to test
288
+ * @param text - Text to test against
289
+ * @param _timeoutMs - Reserved for future timeout implementation (default: 100ms)
290
+ * @returns true/false for match result, null if execution failed
291
+ */
292
+ function executeRegexWithTimeout(regex, text,
293
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
294
+ _timeoutMs = 100) {
295
+ try {
296
+ return regex.test(text);
297
+ }
298
+ catch {
299
+ return null;
300
+ }
301
+ }
302
+ /**
303
+ * Check if the terminal output matches the stop condition pattern.
304
+ * If matched, disables auto-yes and stops polling for the worktree.
305
+ *
306
+ * Security: Pattern is re-validated before execution to handle cases where
307
+ * a previously valid pattern becomes invalid (e.g., state corruption).
308
+ *
309
+ * @internal Exported for testing purposes only.
310
+ * @param worktreeId - Worktree identifier
311
+ * @param cleanOutput - ANSI-stripped terminal output to check
312
+ * @returns true if stop condition matched and auto-yes was disabled
313
+ */
314
+ function checkStopCondition(worktreeId, cleanOutput) {
315
+ const autoYesState = getAutoYesState(worktreeId);
316
+ if (!autoYesState?.stopPattern)
317
+ return false;
318
+ const validation = (0, auto_yes_config_1.validateStopPattern)(autoYesState.stopPattern);
319
+ if (!validation.valid) {
320
+ console.warn('[Auto-Yes] Invalid stop pattern, disabling', { worktreeId });
321
+ disableAutoYes(worktreeId);
322
+ return false;
323
+ }
324
+ try {
325
+ const regex = new RegExp(autoYesState.stopPattern);
326
+ const matched = executeRegexWithTimeout(regex, cleanOutput);
327
+ if (matched === null) {
328
+ // Execution failed - disable to prevent future errors
329
+ console.warn('[Auto-Yes] Stop condition check failed, disabling pattern', { worktreeId });
330
+ disableAutoYes(worktreeId);
331
+ return false;
332
+ }
333
+ if (matched) {
334
+ disableAutoYes(worktreeId, 'stop_pattern_matched');
335
+ stopAutoYesPolling(worktreeId);
336
+ console.warn('[Auto-Yes] Stop condition matched, auto-yes disabled', { worktreeId });
337
+ return true;
338
+ }
339
+ }
340
+ catch {
341
+ console.warn('[Auto-Yes] Stop condition check error', { worktreeId });
342
+ }
343
+ return false;
344
+ }
345
+ /**
346
+ * Internal polling function that recursively schedules itself via setTimeout.
347
+ * Captures tmux output, detects prompts, and sends auto-responses when appropriate.
348
+ *
349
+ * Includes duplicate prevention (Issue #306): skips prompts that have already
350
+ * been answered (tracked via lastAnsweredPromptKey) and applies a cooldown
351
+ * interval (COOLDOWN_INTERVAL_MS) after successful responses.
352
+ *
353
+ * @param worktreeId - Worktree identifier
354
+ * @param cliToolId - CLI tool type being polled
205
355
  */
206
356
  async function pollAutoYes(worktreeId, cliToolId) {
207
357
  // Check if poller was stopped
@@ -224,7 +374,7 @@ async function pollAutoYes(worktreeId, cliToolId) {
224
374
  // while Claude is actively processing (thinking/planning).
225
375
  //
226
376
  // Issue #191: Apply windowing to detectThinking() to prevent stale thinking
227
- // summary lines (e.g., "· Simmering") from blocking prompt detection.
377
+ // summary lines (e.g., "· Simmering...") from blocking prompt detection.
228
378
  // Window size matches detectPrompt()'s multiple_choice scan range (50 lines).
229
379
  //
230
380
  // Safety: Claude CLI does not emit prompts during thinking, so narrowing
@@ -242,11 +392,48 @@ async function pollAutoYes(worktreeId, cliToolId) {
242
392
  scheduleNextPoll(worktreeId, cliToolId);
243
393
  return;
244
394
  }
395
+ // 2.7. Check stop condition (Issue #314)
396
+ // After thinking check, before prompt detection: if terminal output matches
397
+ // the stop pattern, disable auto-yes and stop polling immediately.
398
+ //
399
+ // Delta-based check: Only check NEW output since Auto-Yes was enabled.
400
+ // On the first poll, establish the baseline output length (skip check to
401
+ // avoid matching pre-existing terminal content like shell prompts or paths).
402
+ // On subsequent polls, check only the output delta (new content appended).
403
+ // If the buffer shrank (tmux scrollback shifted), reset baseline and skip
404
+ // that cycle to avoid false positives from old content.
405
+ if (pollerState.stopCheckBaselineLength < 0) {
406
+ // First poll: set baseline, skip stop condition check
407
+ pollerState.stopCheckBaselineLength = cleanOutput.length;
408
+ }
409
+ else {
410
+ const baseline = pollerState.stopCheckBaselineLength;
411
+ if (cleanOutput.length > baseline) {
412
+ // Output grew: check only new content (delta)
413
+ const newContent = cleanOutput.substring(baseline);
414
+ pollerState.stopCheckBaselineLength = cleanOutput.length;
415
+ if (checkStopCondition(worktreeId, newContent)) {
416
+ return;
417
+ }
418
+ }
419
+ else if (cleanOutput.length < baseline) {
420
+ // Buffer shrank (old lines dropped from scrollback): reset baseline
421
+ pollerState.stopCheckBaselineLength = cleanOutput.length;
422
+ }
423
+ // If length unchanged: no new content, skip check
424
+ }
245
425
  // 3. Detect prompt
246
426
  const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
247
427
  const promptDetection = (0, prompt_detector_1.detectPrompt)(cleanOutput, promptOptions);
248
428
  if (!promptDetection.isPrompt || !promptDetection.promptData) {
249
- // No prompt detected, schedule next poll
429
+ // No prompt detected - reset lastAnsweredPromptKey (Issue #306)
430
+ pollerState.lastAnsweredPromptKey = null;
431
+ scheduleNextPoll(worktreeId, cliToolId);
432
+ return;
433
+ }
434
+ // Issue #306: Check for duplicate prompt before responding
435
+ const promptKey = (0, prompt_key_1.generatePromptKey)(promptDetection.promptData);
436
+ if (isDuplicatePrompt(pollerState, promptKey)) {
250
437
  scheduleNextPoll(worktreeId, cliToolId);
251
438
  return;
252
439
  }
@@ -274,32 +461,47 @@ async function pollAutoYes(worktreeId, cliToolId) {
274
461
  updateLastServerResponseTimestamp(worktreeId, Date.now());
275
462
  // 7. Reset error count on success
276
463
  resetErrorCount(worktreeId);
464
+ // Issue #306: Record answered prompt key and apply cooldown
465
+ pollerState.lastAnsweredPromptKey = promptKey;
277
466
  // Log success (without sensitive content)
278
467
  console.info(`[Auto-Yes Poller] Sent response for worktree: ${worktreeId}`);
468
+ // Issue #306: Apply cooldown interval after successful response (early return)
469
+ scheduleNextPoll(worktreeId, cliToolId, exports.COOLDOWN_INTERVAL_MS);
470
+ return;
279
471
  }
280
472
  catch (error) {
281
473
  // Increment error count on failure
282
474
  incrementErrorCount(worktreeId);
283
475
  // Log error (without sensitive details)
284
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
285
- console.warn(`[Auto-Yes Poller] Error for worktree ${worktreeId}: ${errorMessage}`);
476
+ console.warn(`[Auto-Yes Poller] Error for worktree ${worktreeId}: ${getErrorMessage(error)}`);
286
477
  }
287
- // Schedule next poll
478
+ // Schedule next poll (catch block fallthrough or other paths)
288
479
  scheduleNextPoll(worktreeId, cliToolId);
289
480
  }
290
481
  /**
291
482
  * Schedule the next polling iteration
483
+ * @param overrideInterval - Optional interval in milliseconds (S2-F009: type definition).
484
+ * When provided (e.g., COOLDOWN_INTERVAL_MS), overrides pollerState.currentInterval.
485
+ * Type: number | undefined (optional parameter).
292
486
  */
293
- function scheduleNextPoll(worktreeId, cliToolId) {
487
+ function scheduleNextPoll(worktreeId, cliToolId, overrideInterval) {
294
488
  const pollerState = autoYesPollerStates.get(worktreeId);
295
489
  if (!pollerState)
296
490
  return;
491
+ // S4-F003: Floor guard - polling interval must not be below POLLING_INTERVAL_MS
492
+ const interval = Math.max(overrideInterval ?? pollerState.currentInterval, exports.POLLING_INTERVAL_MS);
297
493
  pollerState.timerId = setTimeout(() => {
298
494
  pollAutoYes(worktreeId, cliToolId);
299
- }, pollerState.currentInterval);
495
+ }, interval);
300
496
  }
301
497
  /**
302
- * Start server-side auto-yes polling for a worktree
498
+ * Start server-side auto-yes polling for a worktree.
499
+ * Validates the worktree ID, checks auto-yes state, enforces concurrent poller limits,
500
+ * and begins the polling loop.
501
+ *
502
+ * @param worktreeId - Worktree identifier (must match WORKTREE_ID_PATTERN)
503
+ * @param cliToolId - CLI tool type to poll for
504
+ * @returns Result indicating whether the poller was started, with reason if not
303
505
  */
304
506
  function startAutoYesPolling(worktreeId, cliToolId) {
305
507
  // Validate worktree ID (security)
@@ -328,6 +530,8 @@ function startAutoYesPolling(worktreeId, cliToolId) {
328
530
  consecutiveErrors: 0,
329
531
  currentInterval: exports.POLLING_INTERVAL_MS,
330
532
  lastServerResponseTimestamp: null,
533
+ lastAnsweredPromptKey: null, // S2-F003: initialized to null
534
+ stopCheckBaselineLength: -1, // Issue #314 fix: -1 = first poll (baseline not set)
331
535
  };
332
536
  autoYesPollerStates.set(worktreeId, pollerState);
333
537
  // Start polling immediately
@@ -338,7 +542,10 @@ function startAutoYesPolling(worktreeId, cliToolId) {
338
542
  return { started: true };
339
543
  }
340
544
  /**
341
- * Stop server-side auto-yes polling for a worktree
545
+ * Stop server-side auto-yes polling for a worktree.
546
+ * Clears the timer and removes the poller state.
547
+ *
548
+ * @param worktreeId - Worktree identifier
342
549
  */
343
550
  function stopAutoYesPolling(worktreeId) {
344
551
  const pollerState = autoYesPollerStates.get(worktreeId);
@@ -353,7 +560,8 @@ function stopAutoYesPolling(worktreeId) {
353
560
  console.info(`[Auto-Yes Poller] Stopped for worktree: ${worktreeId}`);
354
561
  }
355
562
  /**
356
- * Stop all server-side auto-yes polling (graceful shutdown)
563
+ * Stop all server-side auto-yes polling (graceful shutdown).
564
+ * Clears all timers and removes all poller states.
357
565
  */
358
566
  function stopAllAutoYesPolling() {
359
567
  for (const [worktreeId, pollerState] of autoYesPollerStates.entries()) {
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.CLAUDE_PROMPT_POLL_INTERVAL = exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT = exports.CLAUDE_PROMPT_WAIT_TIMEOUT = exports.CLAUDE_POST_PROMPT_DELAY = exports.CLAUDE_INIT_POLL_INTERVAL = exports.CLAUDE_INIT_TIMEOUT = void 0;
8
8
  exports.clearCachedClaudePath = clearCachedClaudePath;
9
+ exports.isSessionHealthy = isSessionHealthy;
9
10
  exports.getSessionName = getSessionName;
10
11
  exports.isClaudeInstalled = isClaudeInstalled;
11
12
  exports.isClaudeRunning = isClaudeRunning;
@@ -124,6 +125,17 @@ exports.CLAUDE_SEND_PROMPT_WAIT_TIMEOUT = 10000;
124
125
  * 200ms provides quick response while minimizing CPU usage.
125
126
  */
126
127
  exports.CLAUDE_PROMPT_POLL_INTERVAL = 200;
128
+ /**
129
+ * Maximum expected length of a shell prompt line (characters)
130
+ *
131
+ * Shell prompts are typically under 40 characters (e.g., "user@host:~/project$" ~30 chars).
132
+ * Lines at or above this threshold are not considered shell prompts, preventing
133
+ * false positives from Claude CLI output that happens to end with $, %, or #.
134
+ *
135
+ * Used by isSessionHealthy() to distinguish shell prompts from CLI output.
136
+ * 40 is an empirical threshold with safety margin.
137
+ */
138
+ const MAX_SHELL_PROMPT_LENGTH = 40;
127
139
  /**
128
140
  * Cached Claude CLI path
129
141
  */
@@ -229,45 +241,69 @@ async function getCleanPaneOutput(sessionName, lines = 50) {
229
241
  const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -lines });
230
242
  return (0, cli_patterns_1.stripAnsi)(output);
231
243
  }
232
- // ----- Health Check Functions (Bug 2) -----
233
244
  /**
234
245
  * Verify that Claude CLI is actually running inside a tmux session
235
246
  * Detects broken sessions where tmux exists but Claude failed to start
236
247
  *
248
+ * @internal Exported for testing purposes only.
249
+ * Follows clearCachedClaudePath() precedent (L148-156).
250
+ *
237
251
  * @param sessionName - tmux session name
238
- * @returns true if Claude CLI is responsive (prompt detected or initializing)
252
+ * @returns HealthCheckResult with healthy status and optional reason
239
253
  */
240
254
  async function isSessionHealthy(sessionName) {
241
255
  try {
242
256
  // SF-001: Use shared helper instead of inline capturePane + stripAnsi
243
257
  const cleanOutput = await getCleanPaneOutput(sessionName);
244
- // MF-001: Check error patterns from cli-patterns.ts (SRP - pattern management centralized)
245
- for (const pattern of cli_patterns_1.CLAUDE_SESSION_ERROR_PATTERNS) {
246
- if (cleanOutput.includes(pattern)) {
247
- return false;
248
- }
249
- }
250
- for (const regex of cli_patterns_1.CLAUDE_SESSION_ERROR_REGEX_PATTERNS) {
251
- if (regex.test(cleanOutput)) {
252
- return false;
253
- }
254
- }
255
258
  // MF-002: Check shell prompt endings from extensible array (OCP)
256
259
  const trimmed = cleanOutput.trim();
260
+ // S2-F010: Empty output judgment (HealthCheckResult format)
257
261
  // C-S2-001: Empty output means tmux session exists but Claude CLI has no output.
258
262
  // This is treated as unhealthy because a properly running Claude CLI always
259
263
  // produces output (prompt, spinner, or response). An empty pane indicates
260
264
  // the CLI process has exited or failed to start.
261
265
  if (trimmed === '') {
262
- return false;
266
+ return { healthy: false, reason: 'empty output' };
263
267
  }
264
- if (SHELL_PROMPT_ENDINGS.some(ending => trimmed.endsWith(ending))) {
265
- return false;
268
+ // S2-F010: Error pattern detection (HealthCheckResult format)
269
+ // MF-001: Check error patterns from cli-patterns.ts (SRP - pattern management centralized)
270
+ for (const pattern of cli_patterns_1.CLAUDE_SESSION_ERROR_PATTERNS) {
271
+ if (trimmed.includes(pattern)) {
272
+ return { healthy: false, reason: `error pattern: ${pattern}` };
273
+ }
266
274
  }
267
- return true;
275
+ for (const regex of cli_patterns_1.CLAUDE_SESSION_ERROR_REGEX_PATTERNS) {
276
+ if (regex.test(trimmed)) {
277
+ return { healthy: false, reason: `error pattern: ${regex.source}` };
278
+ }
279
+ }
280
+ // S2-F002: Extract last line after empty line filtering
281
+ const lines = trimmed.split('\n').filter(line => line.trim() !== '');
282
+ const lastLine = lines[lines.length - 1]?.trim() ?? '';
283
+ // F006: Line length check BEFORE SHELL_PROMPT_ENDINGS check (early return)
284
+ if (lastLine.length >= MAX_SHELL_PROMPT_LENGTH) {
285
+ // Long lines are not shell prompts -> treat as healthy (early return)
286
+ return { healthy: true };
287
+ }
288
+ // F003: Individual pattern exclusions for SHELL_PROMPT_ENDINGS
289
+ // NOTE(F003): If new false positive patterns are found in the future,
290
+ // consider refactoring to a structure that associates exclusionPattern
291
+ // with each SHELL_PROMPT_ENDINGS entry. Currently only % needs exclusion (YAGNI).
292
+ if (SHELL_PROMPT_ENDINGS.some(ending => {
293
+ if (!lastLine.endsWith(ending))
294
+ return false;
295
+ // Exclude N% pattern (e.g., "Context left until auto-compact: 7%")
296
+ if (ending === '%' && /\d+%$/.test(lastLine))
297
+ return false;
298
+ return true;
299
+ })) {
300
+ return { healthy: false, reason: `shell prompt ending detected: ${lastLine}` };
301
+ }
302
+ return { healthy: true };
268
303
  }
269
304
  catch {
270
- return false;
305
+ // S3-F001: Catch block also returns HealthCheckResult format
306
+ return { healthy: false, reason: 'capture error' };
271
307
  }
272
308
  }
273
309
  /**
@@ -279,8 +315,9 @@ async function isSessionHealthy(sessionName) {
279
315
  * @returns true if session is healthy and can be reused, false if it was killed
280
316
  */
281
317
  async function ensureHealthySession(sessionName) {
282
- const healthy = await isSessionHealthy(sessionName);
283
- if (!healthy) {
318
+ const result = await isSessionHealthy(sessionName);
319
+ if (!result.healthy) {
320
+ console.warn(`[health-check] Session ${sessionName} unhealthy: ${result.reason}`);
284
321
  await (0, tmux_1.killSession)(sessionName);
285
322
  return false;
286
323
  }
@@ -376,7 +413,9 @@ async function isClaudeRunning(worktreeId) {
376
413
  return false;
377
414
  }
378
415
  // MF-S3-001: Verify session health to avoid reporting broken sessions as running
379
- return isSessionHealthy(sessionName);
416
+ // S2-F001: await + extract .healthy to maintain boolean return type
417
+ const result = await isSessionHealthy(sessionName);
418
+ return result.healthy;
380
419
  }
381
420
  /**
382
421
  * Get Claude session state
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generatePromptKey = generatePromptKey;
4
+ /**
5
+ * Generate a composite key for prompt deduplication.
6
+ *
7
+ * Used by both client-side (useAutoYes.ts) and server-side (auto-yes-manager.ts)
8
+ * to ensure consistent prompt identification across the duplicate prevention system.
9
+ *
10
+ * The key format is `{type}:{question}`, which uniquely identifies a prompt
11
+ * by combining its type and question text.
12
+ *
13
+ * @param promptData - Prompt data containing type and question fields
14
+ * @returns Composite key string in the format "type:question"
15
+ *
16
+ * @internal Used for in-memory comparison only. Do NOT use for logging,
17
+ * persistence, or external output. If the return value is ever used in
18
+ * log output, DB storage, or HTML rendering, apply appropriate sanitization
19
+ * (CR/LF escaping, prepared statements, HTML escaping respectively).
20
+ * See SEC: S4-F001.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const key = generatePromptKey({ type: 'yes_no', question: 'Continue?' });
25
+ * // Returns 'yes_no:Continue?'
26
+ * ```
27
+ */
28
+ function generatePromptKey(promptData) {
29
+ return `${promptData.type}:${promptData.question}`;
30
+ }