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.
- package/.env.example +2 -1
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +12 -12
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
- package/.next/server/app/api/repositories/clone/route.js +1 -1
- package/.next/server/app/api/repositories/route.js +2 -2
- package/.next/server/app/api/repositories/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +3 -3
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +10 -10
- package/.next/server/chunks/2509.js +1 -0
- package/.next/server/chunks/369.js +1 -1
- package/.next/server/chunks/5488.js +4 -4
- package/.next/server/chunks/7808.js +1 -1
- package/.next/server/chunks/8693.js +1 -0
- package/.next/server/chunks/8744.js +1 -1
- package/.next/server/chunks/9238.js +14 -14
- package/.next/server/chunks/9367.js +2 -2
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/5970-2e18108d0cabd8af.js +1 -0
- package/.next/static/chunks/9178-88850a7c48deea07.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-4a3c0861367e0391.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-c99258f57461962c.js +1 -0
- package/.next/static/css/897ffb669f47c97b.css +3 -0
- package/.next/trace +5 -5
- package/README.md +154 -181
- package/dist/server/src/config/auto-yes-config.js +44 -2
- package/dist/server/src/lib/auto-yes-manager.js +249 -41
- package/dist/server/src/lib/claude-session.js +60 -21
- package/dist/server/src/lib/prompt-key.js +30 -0
- package/package.json +19 -7
- package/.next/server/chunks/667.js +0 -1
- package/.next/static/chunks/5970-dc8fb1c8c0217636.js +0 -1
- package/.next/static/chunks/8864-2f60eadc8404fdd0.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/files/[...path]/page-ce9ac3658f2b7d91.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-1b8e4c49fbaf3f99.js +0 -1
- package/.next/static/css/a69d9c70fce558b4.css +0 -3
- /package/.next/static/{NGcx1ej6oVBba0MO0bwCg → ym6mA6Dl9wX62h3AoYO45}/_buildManifest.js +0 -0
- /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
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|