claude-coder 1.8.0 → 1.8.2
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/README.md +167 -177
- package/bin/cli.js +172 -159
- package/package.json +52 -52
- package/src/commands/auth.js +240 -294
- package/src/commands/setup-modules/helpers.js +99 -105
- package/src/commands/setup-modules/index.js +25 -25
- package/src/commands/setup-modules/mcp.js +94 -94
- package/src/commands/setup-modules/provider.js +260 -260
- package/src/commands/setup-modules/safety.js +61 -61
- package/src/commands/setup-modules/simplify.js +52 -52
- package/src/commands/setup.js +172 -172
- package/src/common/assets.js +236 -192
- package/src/common/config.js +125 -138
- package/src/common/constants.js +55 -56
- package/src/common/indicator.js +222 -222
- package/src/common/interaction.js +170 -170
- package/src/common/logging.js +77 -76
- package/src/common/sdk.js +50 -50
- package/src/common/tasks.js +88 -157
- package/src/common/utils.js +161 -146
- package/src/core/coding.js +55 -55
- package/src/core/context.js +117 -132
- package/src/core/go.js +310 -0
- package/src/core/harness.js +484 -0
- package/src/core/hooks.js +533 -528
- package/src/core/init.js +171 -163
- package/src/core/plan.js +325 -318
- package/src/core/prompts.js +227 -253
- package/src/core/query.js +49 -47
- package/src/core/repair.js +46 -58
- package/src/core/runner.js +195 -352
- package/src/core/scan.js +89 -89
- package/src/core/{base.js → session.js} +56 -53
- package/src/core/simplify.js +52 -59
- package/templates/bash-process.md +12 -5
- package/templates/codingSystem.md +65 -0
- package/templates/codingUser.md +17 -31
- package/templates/coreProtocol.md +29 -0
- package/templates/goSystem.md +130 -0
- package/templates/guidance.json +52 -34
- package/templates/planSystem.md +78 -0
- package/templates/planUser.md +9 -0
- package/templates/playwright.md +16 -16
- package/templates/requirements.example.md +57 -56
- package/templates/scanSystem.md +120 -0
- package/templates/scanUser.md +10 -17
- package/templates/test_rule.md +194 -194
- package/src/core/validator.js +0 -138
- package/templates/addGuide.md +0 -98
- package/templates/addUser.md +0 -26
- package/templates/agentProtocol.md +0 -195
- package/templates/scanProtocol.md +0 -118
package/src/core/hooks.js
CHANGED
|
@@ -1,529 +1,534 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { inferPhaseStep } = require('../common/indicator');
|
|
6
|
-
const { log } = require('../common/config');
|
|
7
|
-
const { EDIT_THRESHOLD, FILES } = require('../common/constants');
|
|
8
|
-
const { createAskUserQuestionHook } = require('../common/interaction');
|
|
9
|
-
const { assets } = require('../common/assets');
|
|
10
|
-
const { localTimestamp } = require('../common/utils');
|
|
11
|
-
// ─────────────────────────────────────────────────────────────
|
|
12
|
-
// Constants
|
|
13
|
-
// ─────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
const DEFAULT_EDIT_THRESHOLD = EDIT_THRESHOLD;
|
|
16
|
-
const SESSION_RESULT_FILENAME = FILES.SESSION_RESULT;
|
|
17
|
-
|
|
18
|
-
// Feature name constants
|
|
19
|
-
const FEATURES = Object.freeze({
|
|
20
|
-
GUIDANCE: 'guidance',
|
|
21
|
-
EDIT_GUARD: 'editGuard',
|
|
22
|
-
COMPLETION: 'completion',
|
|
23
|
-
STALL: 'stall',
|
|
24
|
-
INTERACTION: 'interaction',
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// ─────────────────────────────────────────────────────────────
|
|
28
|
-
//
|
|
29
|
-
// ─────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
function
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Create
|
|
282
|
-
*
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { inferPhaseStep } = require('../common/indicator');
|
|
6
|
+
const { log } = require('../common/config');
|
|
7
|
+
const { EDIT_THRESHOLD, FILES } = require('../common/constants');
|
|
8
|
+
const { createAskUserQuestionHook } = require('../common/interaction');
|
|
9
|
+
const { assets } = require('../common/assets');
|
|
10
|
+
const { localTimestamp } = require('../common/utils');
|
|
11
|
+
// ─────────────────────────────────────────────────────────────
|
|
12
|
+
// Constants
|
|
13
|
+
// ─────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const DEFAULT_EDIT_THRESHOLD = EDIT_THRESHOLD;
|
|
16
|
+
const SESSION_RESULT_FILENAME = FILES.SESSION_RESULT;
|
|
17
|
+
|
|
18
|
+
// Feature name constants
|
|
19
|
+
const FEATURES = Object.freeze({
|
|
20
|
+
GUIDANCE: 'guidance',
|
|
21
|
+
EDIT_GUARD: 'editGuard',
|
|
22
|
+
COMPLETION: 'completion',
|
|
23
|
+
STALL: 'stall',
|
|
24
|
+
INTERACTION: 'interaction',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────
|
|
28
|
+
// GuidanceInjector: JSON-based configurable guidance system
|
|
29
|
+
// ─────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
class GuidanceInjector {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.rules = [];
|
|
34
|
+
this.cache = {};
|
|
35
|
+
this.injectedRules = new Set(); // Track which rules have been injected once
|
|
36
|
+
this.loaded = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load rules from user's guidance.json and pre-compile regex patterns.
|
|
41
|
+
*/
|
|
42
|
+
load() {
|
|
43
|
+
if (this.loaded) return;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const content = assets.read('guidance');
|
|
47
|
+
const config = JSON.parse(content);
|
|
48
|
+
this.rules = config.rules || [];
|
|
49
|
+
} catch {
|
|
50
|
+
this.rules = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this._compiledMatchers = new Map();
|
|
54
|
+
this._compiledConditions = new Map();
|
|
55
|
+
for (const rule of this.rules) {
|
|
56
|
+
try {
|
|
57
|
+
this._compiledMatchers.set(rule.name, new RegExp(rule.matcher));
|
|
58
|
+
} catch {
|
|
59
|
+
this._compiledMatchers.set(rule.name, null);
|
|
60
|
+
}
|
|
61
|
+
if (rule.condition?.pattern !== undefined) {
|
|
62
|
+
try {
|
|
63
|
+
this._compiledConditions.set(rule.name, new RegExp(rule.condition.pattern, 'i'));
|
|
64
|
+
} catch {
|
|
65
|
+
this._compiledConditions.set(rule.name, null);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.loaded = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get nested field value from object
|
|
75
|
+
* @param {object} obj - Source object
|
|
76
|
+
* @param {string} fieldPath - Dot-separated path (e.g., "tool_input.command")
|
|
77
|
+
*/
|
|
78
|
+
getFieldValue(obj, fieldPath) {
|
|
79
|
+
return fieldPath.split('.').reduce((o, k) => o?.[k], obj);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if condition matches
|
|
84
|
+
* Supports: { field, pattern } or { any: [...] }
|
|
85
|
+
* @param {string} [ruleName] - Rule name for looking up pre-compiled regex
|
|
86
|
+
*/
|
|
87
|
+
matchCondition(input, condition, ruleName) {
|
|
88
|
+
if (!condition) return true;
|
|
89
|
+
|
|
90
|
+
if (condition.field && condition.pattern !== undefined) {
|
|
91
|
+
const value = this.getFieldValue(input, condition.field);
|
|
92
|
+
const re = (ruleName && this._compiledConditions?.get(ruleName)) ||
|
|
93
|
+
new RegExp(condition.pattern, 'i');
|
|
94
|
+
return re.test(String(value || ''));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (condition.any && Array.isArray(condition.any)) {
|
|
98
|
+
return condition.any.some(c => this.matchCondition(input, c));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract tool tip key from tool name
|
|
106
|
+
* @param {string} toolName - Full tool name (e.g., "mcp__playwright__browser_snapshot")
|
|
107
|
+
* @param {string} extractor - Regex pattern to extract key
|
|
108
|
+
*/
|
|
109
|
+
extractToolTipKey(toolName, extractor) {
|
|
110
|
+
if (!extractor) return null;
|
|
111
|
+
const match = toolName.match(new RegExp(extractor));
|
|
112
|
+
return match ? match[1] : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get rule file content
|
|
117
|
+
* @param {object|string} file - File config or path string
|
|
118
|
+
* @param {string} basePath - Base directory for relative paths
|
|
119
|
+
*/
|
|
120
|
+
getFileContent(file, basePath) {
|
|
121
|
+
if (!file) return null;
|
|
122
|
+
|
|
123
|
+
const filePath = typeof file === 'string' ? file : file.path;
|
|
124
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
125
|
+
? filePath
|
|
126
|
+
: path.join(basePath, filePath);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
return fs.readFileSync(absolutePath, 'utf8');
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Process a single rule and return guidance content
|
|
137
|
+
*/
|
|
138
|
+
processRule(rule, input, basePath) {
|
|
139
|
+
const matcherRe = this._compiledMatchers?.get(rule.name) ?? new RegExp(rule.matcher);
|
|
140
|
+
if (!matcherRe.test(input.tool_name)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!this.matchCondition(input, rule.condition, rule.name)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const result = { guidance: '', tip: '' };
|
|
149
|
+
|
|
150
|
+
// Process file content
|
|
151
|
+
if (rule.file) {
|
|
152
|
+
const fileConfig = typeof rule.file === 'object' ? rule.file : { path: rule.file };
|
|
153
|
+
const injectOnce = fileConfig.injectOnce === true;
|
|
154
|
+
const ruleKey = `${rule.name}_file`;
|
|
155
|
+
|
|
156
|
+
// Skip if already injected and injectOnce is true
|
|
157
|
+
if (injectOnce && this.injectedRules.has(ruleKey)) {
|
|
158
|
+
// Don't inject file content again
|
|
159
|
+
} else {
|
|
160
|
+
if (injectOnce) this.injectedRules.add(ruleKey);
|
|
161
|
+
|
|
162
|
+
// Get cached content or read file
|
|
163
|
+
const cacheKey = `${rule.name}_content`;
|
|
164
|
+
if (!this.cache[cacheKey]) {
|
|
165
|
+
this.cache[cacheKey] = this.getFileContent(fileConfig.path, basePath);
|
|
166
|
+
}
|
|
167
|
+
result.guidance = this.cache[cacheKey] || '';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Process tool tips
|
|
172
|
+
if (rule.toolTips && rule.toolTips.items) {
|
|
173
|
+
const tipKey = this.extractToolTipKey(input.tool_name, rule.toolTips.extractor);
|
|
174
|
+
if (tipKey && rule.toolTips.items[tipKey]) {
|
|
175
|
+
const tipInjectOnce = rule.toolTips.injectOnce !== false; // Default true
|
|
176
|
+
const tipRuleKey = `${rule.name}_tip_${tipKey}`;
|
|
177
|
+
|
|
178
|
+
if (!tipInjectOnce || !this.injectedRules.has(tipRuleKey)) {
|
|
179
|
+
if (tipInjectOnce) this.injectedRules.add(tipRuleKey);
|
|
180
|
+
result.tip = rule.toolTips.items[tipKey];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Reset per-session state for clean session boundaries.
|
|
190
|
+
* Also clears loaded flag so guidance.json is re-read on next hook call.
|
|
191
|
+
*/
|
|
192
|
+
reset() {
|
|
193
|
+
this.injectedRules.clear();
|
|
194
|
+
this.cache = {};
|
|
195
|
+
this.loaded = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create hook function for PreToolUse
|
|
200
|
+
*/
|
|
201
|
+
createHook() {
|
|
202
|
+
const basePath = assets.dir('loop');
|
|
203
|
+
|
|
204
|
+
return async (input, _toolUseID, _context) => {
|
|
205
|
+
this.load();
|
|
206
|
+
|
|
207
|
+
if (this.rules.length === 0) return {};
|
|
208
|
+
|
|
209
|
+
const guidanceParts = [];
|
|
210
|
+
const tipParts = [];
|
|
211
|
+
|
|
212
|
+
for (const rule of this.rules) {
|
|
213
|
+
const result = this.processRule(rule, input, basePath);
|
|
214
|
+
if (result) {
|
|
215
|
+
if (result.guidance) guidanceParts.push(result.guidance);
|
|
216
|
+
if (result.tip) tipParts.push(result.tip);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const allParts = [...guidanceParts, ...tipParts];
|
|
221
|
+
if (allParts.length === 0) return {};
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
hookSpecificOutput: {
|
|
225
|
+
hookEventName: 'PreToolUse',
|
|
226
|
+
additionalContext: allParts.join('\n\n'),
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Shared instance (reset per session via createGuidanceModule)
|
|
234
|
+
const guidanceInjector = new GuidanceInjector();
|
|
235
|
+
|
|
236
|
+
// ─────────────────────────────────────────────────────────────
|
|
237
|
+
// Utility Functions
|
|
238
|
+
// ─────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
function logToolCall(logStream, input) {
|
|
242
|
+
if (!logStream) return;
|
|
243
|
+
const target = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
244
|
+
const cmd = input.tool_input?.command || '';
|
|
245
|
+
const pattern = input.tool_input?.pattern || '';
|
|
246
|
+
const detail = target || cmd.slice(0, 200) || (pattern ? `pattern: ${pattern}` : '');
|
|
247
|
+
if (detail) {
|
|
248
|
+
logStream.write(`[${localTimestamp()}] ${input.tool_name}: ${detail}\n`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isSessionResultWrite(toolName, toolInput) {
|
|
253
|
+
if (toolName === 'Write') {
|
|
254
|
+
const target = toolInput?.file_path || toolInput?.path || '';
|
|
255
|
+
return target.endsWith(SESSION_RESULT_FILENAME);
|
|
256
|
+
}
|
|
257
|
+
if (toolName === 'Bash' || toolName === 'Shell') {
|
|
258
|
+
const cmd = toolInput?.command || '';
|
|
259
|
+
if (!cmd.includes(SESSION_RESULT_FILENAME)) return false;
|
|
260
|
+
return />\s*[^\s]*session_result/.test(cmd);
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─────────────────────────────────────────────────────────────
|
|
266
|
+
// Module Factories
|
|
267
|
+
// ─────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create guidance injection module.
|
|
271
|
+
* Resets the shared injector's per-session state to prevent cross-session leaks.
|
|
272
|
+
*/
|
|
273
|
+
function createGuidanceModule() {
|
|
274
|
+
guidanceInjector.reset();
|
|
275
|
+
return {
|
|
276
|
+
hook: guidanceInjector.createHook()
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Create edit guard module.
|
|
282
|
+
* Uses a sliding time window: edits older than cooldownMs are decayed,
|
|
283
|
+
* allowing the model to resume editing after a "thinking" break.
|
|
284
|
+
*/
|
|
285
|
+
function createEditGuardModule(options) {
|
|
286
|
+
const editTimestamps = {};
|
|
287
|
+
const threshold = options.editThreshold || DEFAULT_EDIT_THRESHOLD;
|
|
288
|
+
const cooldownMs = options.editCooldownMs || 60000;
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
hook: async (input, _toolUseID, _context) => {
|
|
292
|
+
if (!['Write', 'Edit', 'MultiEdit'].includes(input.tool_name)) return {};
|
|
293
|
+
const target = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
294
|
+
if (!target) return {};
|
|
295
|
+
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
if (!editTimestamps[target]) editTimestamps[target] = [];
|
|
298
|
+
|
|
299
|
+
editTimestamps[target] = editTimestamps[target].filter(t => now - t < cooldownMs);
|
|
300
|
+
editTimestamps[target].push(now);
|
|
301
|
+
|
|
302
|
+
if (editTimestamps[target].length > threshold) {
|
|
303
|
+
return {
|
|
304
|
+
hookSpecificOutput: {
|
|
305
|
+
hookEventName: 'PreToolUse',
|
|
306
|
+
permissionDecision: 'deny',
|
|
307
|
+
permissionDecisionReason:
|
|
308
|
+
`${cooldownMs / 1000}s 内对 ${target} 编辑 ${editTimestamps[target].length} 次(上限 ${threshold}),疑似死循环。请重新审视方案后再继续。`
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {};
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Create stall detection module (includes completion timeout handling)
|
|
319
|
+
*/
|
|
320
|
+
function createStallModule(indicator, logStream, options) {
|
|
321
|
+
let stallDetected = false;
|
|
322
|
+
let stallChecker = null;
|
|
323
|
+
const timeoutMs = options.stallTimeoutMs || 1200000;
|
|
324
|
+
const completionTimeoutMs = options.completionTimeoutMs || 300000;
|
|
325
|
+
const abortController = options.abortController;
|
|
326
|
+
let completionDetectedAt = 0;
|
|
327
|
+
|
|
328
|
+
const checkStall = () => {
|
|
329
|
+
const now = Date.now();
|
|
330
|
+
const idleMs = now - indicator.lastActivityTime;
|
|
331
|
+
|
|
332
|
+
// Priority: completion timeout
|
|
333
|
+
if (completionDetectedAt > 0) {
|
|
334
|
+
const sinceCompletion = now - completionDetectedAt;
|
|
335
|
+
if (sinceCompletion > completionTimeoutMs && !stallDetected) {
|
|
336
|
+
stallDetected = true;
|
|
337
|
+
const shortMin = Math.ceil(completionTimeoutMs / 60000);
|
|
338
|
+
const actualMin = Math.floor(sinceCompletion / 60000);
|
|
339
|
+
log('warn', `\nsession_result 已写入 ${actualMin} 分钟,超过 ${shortMin} 分钟上限,自动中断`);
|
|
340
|
+
if (logStream) {
|
|
341
|
+
logStream.write(`\n[${localTimestamp()}] STALL: session_result 写入后 ${actualMin} 分钟(上限 ${shortMin} 分钟),自动中断\n`);
|
|
342
|
+
}
|
|
343
|
+
if (abortController) {
|
|
344
|
+
abortController.abort();
|
|
345
|
+
log('warn', '\n已发送中断信号');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Normal stall detection
|
|
352
|
+
if (idleMs > timeoutMs && !stallDetected) {
|
|
353
|
+
stallDetected = true;
|
|
354
|
+
const idleMin = Math.floor(idleMs / 60000);
|
|
355
|
+
log('warn', `\n无响应超过 ${idleMin} 分钟,自动中断 session`);
|
|
356
|
+
if (logStream) {
|
|
357
|
+
logStream.write(`\n[${localTimestamp()}] STALL: 无响应 ${idleMin} 分钟,自动中断\n`);
|
|
358
|
+
}
|
|
359
|
+
if (abortController) {
|
|
360
|
+
abortController.abort();
|
|
361
|
+
log('warn', '\n已发送中断信号');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
stallChecker = setInterval(checkStall, 30000);
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
setCompletionDetected: () => { completionDetectedAt = Date.now(); },
|
|
370
|
+
onCompletionDetected: () => {
|
|
371
|
+
completionDetectedAt = Date.now();
|
|
372
|
+
const shortMin = Math.ceil(completionTimeoutMs / 60000);
|
|
373
|
+
indicator.setCompletionDetected(shortMin);
|
|
374
|
+
log('info', '');
|
|
375
|
+
log('info', `检测到 session_result 写入,${shortMin} 分钟内模型未终止将自动中断`);
|
|
376
|
+
if (logStream) {
|
|
377
|
+
logStream.write(`\n[${localTimestamp()}] COMPLETION_DETECTED: session_result.json written, ${shortMin}min grace period\n`);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
cleanup: () => { if (stallChecker) clearInterval(stallChecker); },
|
|
381
|
+
isStalled: () => stallDetected
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Create completion detection module (PostToolUse hook)
|
|
387
|
+
* endTool() resets toolRunning and refreshes lastActivityTime (countdown reset)
|
|
388
|
+
*/
|
|
389
|
+
function createCompletionModule(indicator, stallModule) {
|
|
390
|
+
return {
|
|
391
|
+
hook: async (input, _toolUseID, _context) => {
|
|
392
|
+
indicator.endTool();
|
|
393
|
+
indicator.updatePhase('thinking');
|
|
394
|
+
indicator.updateStep('');
|
|
395
|
+
indicator.toolTarget = '';
|
|
396
|
+
|
|
397
|
+
if (isSessionResultWrite(input.tool_name, input.tool_input)) {
|
|
398
|
+
stallModule.onCompletionDetected();
|
|
399
|
+
}
|
|
400
|
+
return {};
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Create PostToolUseFailure hook to ensure endTool on tool errors
|
|
407
|
+
*/
|
|
408
|
+
function createFailureHook(indicator) {
|
|
409
|
+
return async (_input, _toolUseID, _context) => {
|
|
410
|
+
indicator.endTool();
|
|
411
|
+
return {};
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Create logging hook
|
|
417
|
+
*/
|
|
418
|
+
function createLoggingHook(indicator, logStream) {
|
|
419
|
+
return async (input, _toolUseID, _context) => {
|
|
420
|
+
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
421
|
+
logToolCall(logStream, input);
|
|
422
|
+
return {};
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ─────────────────────────────────────────────────────────────
|
|
427
|
+
// Hook Factory: createHooks
|
|
428
|
+
// ─────────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
const FEATURE_MAP = {
|
|
431
|
+
coding: [FEATURES.GUIDANCE, FEATURES.EDIT_GUARD, FEATURES.COMPLETION, FEATURES.STALL],
|
|
432
|
+
plan: [FEATURES.STALL],
|
|
433
|
+
plan_interactive: [FEATURES.STALL, FEATURES.INTERACTION],
|
|
434
|
+
scan: [FEATURES.STALL],
|
|
435
|
+
add: [FEATURES.STALL],
|
|
436
|
+
simplify: [FEATURES.STALL, FEATURES.INTERACTION],
|
|
437
|
+
go: [FEATURES.STALL, FEATURES.INTERACTION],
|
|
438
|
+
custom: null
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Create hooks based on session type
|
|
443
|
+
*/
|
|
444
|
+
function createHooks(type, indicator, logStream, options = {}) {
|
|
445
|
+
const features = type === 'custom'
|
|
446
|
+
? (options.features || [FEATURES.STALL])
|
|
447
|
+
: (FEATURE_MAP[type] || [FEATURES.STALL]);
|
|
448
|
+
|
|
449
|
+
const modules = {};
|
|
450
|
+
|
|
451
|
+
if (features.includes(FEATURES.STALL)) {
|
|
452
|
+
modules.stall = createStallModule(indicator, logStream, options);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (features.includes(FEATURES.EDIT_GUARD)) {
|
|
456
|
+
modules.editGuard = createEditGuardModule(options);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (features.includes(FEATURES.COMPLETION) && modules.stall) {
|
|
460
|
+
modules.completion = createCompletionModule(indicator, modules.stall);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (features.includes(FEATURES.GUIDANCE)) {
|
|
464
|
+
modules.guidance = createGuidanceModule();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (features.includes(FEATURES.INTERACTION)) {
|
|
468
|
+
modules.interaction = { hook: createAskUserQuestionHook(indicator) };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Assemble PreToolUse hooks
|
|
472
|
+
const preToolUseHooks = [];
|
|
473
|
+
preToolUseHooks.push(createLoggingHook(indicator, logStream));
|
|
474
|
+
|
|
475
|
+
if (modules.editGuard) {
|
|
476
|
+
preToolUseHooks.push(modules.editGuard.hook);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (modules.guidance) {
|
|
480
|
+
preToolUseHooks.push(modules.guidance.hook);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (modules.interaction) {
|
|
484
|
+
preToolUseHooks.push(modules.interaction.hook);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Assemble PostToolUse hooks (always include endTool for countdown reset)
|
|
488
|
+
const postToolUseHooks = [];
|
|
489
|
+
if (modules.completion) {
|
|
490
|
+
postToolUseHooks.push(modules.completion.hook);
|
|
491
|
+
} else {
|
|
492
|
+
postToolUseHooks.push(createFailureHook(indicator));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// PostToolUseFailure hook: ensure endTool even on tool errors
|
|
496
|
+
const failureHook = createFailureHook(indicator);
|
|
497
|
+
|
|
498
|
+
// Build hooks object
|
|
499
|
+
const hooks = {};
|
|
500
|
+
if (preToolUseHooks.length > 0) {
|
|
501
|
+
hooks.PreToolUse = [{ matcher: '*', hooks: preToolUseHooks }];
|
|
502
|
+
}
|
|
503
|
+
if (postToolUseHooks.length > 0) {
|
|
504
|
+
hooks.PostToolUse = [{ matcher: '*', hooks: postToolUseHooks }];
|
|
505
|
+
}
|
|
506
|
+
hooks.PostToolUseFailure = [{ matcher: '*', hooks: [failureHook] }];
|
|
507
|
+
|
|
508
|
+
// Cleanup functions
|
|
509
|
+
const cleanupFns = [];
|
|
510
|
+
if (modules.stall) {
|
|
511
|
+
cleanupFns.push(modules.stall.cleanup);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
hooks,
|
|
516
|
+
cleanup: () => cleanupFns.forEach(fn => fn()),
|
|
517
|
+
isStalled: () => modules.stall?.isStalled() || false
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ─────────────────────────────────────────────────────────────
|
|
522
|
+
// Exports
|
|
523
|
+
// ─────────────────────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
module.exports = {
|
|
526
|
+
createHooks,
|
|
527
|
+
GuidanceInjector,
|
|
528
|
+
createGuidanceModule,
|
|
529
|
+
createEditGuardModule,
|
|
530
|
+
createCompletionModule,
|
|
531
|
+
createStallModule,
|
|
532
|
+
FEATURES,
|
|
533
|
+
isSessionResultWrite,
|
|
529
534
|
};
|