claude-coder 1.6.2 → 1.7.1
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 +125 -127
- package/bin/cli.js +161 -197
- package/package.json +47 -44
- package/prompts/ADD_GUIDE.md +98 -0
- package/{templates → prompts}/CLAUDE.md +199 -238
- package/{templates → prompts}/SCAN_PROTOCOL.md +118 -123
- package/prompts/add_user.md +24 -0
- package/prompts/coding_user.md +31 -0
- package/prompts/scan_user.md +17 -0
- package/src/auth.js +245 -245
- package/src/config.js +201 -223
- package/src/hooks.js +166 -96
- package/src/indicator.js +233 -160
- package/src/init.js +144 -144
- package/src/prompts.js +295 -339
- package/src/runner.js +396 -394
- package/src/scanner.js +62 -62
- package/src/session.js +354 -320
- package/src/setup.js +579 -397
- package/src/tasks.js +172 -172
- package/src/validator.js +181 -170
- package/templates/requirements.example.md +56 -56
- package/templates/test_rule.md +194 -157
- package/docs/ARCHITECTURE.md +0 -516
- package/docs/PLAYWRIGHT_CREDENTIALS.md +0 -178
- package/docs/README.en.md +0 -103
package/src/hooks.js
CHANGED
|
@@ -1,96 +1,166 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { inferPhaseStep } = require('./indicator');
|
|
4
|
-
const { log } = require('./config');
|
|
5
|
-
|
|
6
|
-
const DEFAULT_EDIT_THRESHOLD =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { inferPhaseStep } = require('./indicator');
|
|
4
|
+
const { log } = require('./config');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_EDIT_THRESHOLD = 15;
|
|
7
|
+
const SESSION_RESULT_FILENAME = 'session_result.json';
|
|
8
|
+
|
|
9
|
+
function logToolCall(logStream, input) {
|
|
10
|
+
if (!logStream) return;
|
|
11
|
+
const target = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
12
|
+
const cmd = input.tool_input?.command || '';
|
|
13
|
+
const pattern = input.tool_input?.pattern || '';
|
|
14
|
+
const detail = target || cmd.slice(0, 200) || (pattern ? `pattern: ${pattern}` : '');
|
|
15
|
+
if (detail) {
|
|
16
|
+
logStream.write(`[${new Date().toISOString()}] ${input.tool_name}: ${detail}\n`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect whether a tool call writes to session_result.json.
|
|
22
|
+
* Covers both the Write tool (exact path match) and Bash redirect writes.
|
|
23
|
+
*/
|
|
24
|
+
function isSessionResultWrite(toolName, toolInput) {
|
|
25
|
+
if (toolName === 'Write') {
|
|
26
|
+
const target = toolInput?.file_path || toolInput?.path || '';
|
|
27
|
+
return target.endsWith(SESSION_RESULT_FILENAME);
|
|
28
|
+
}
|
|
29
|
+
if (toolName === 'Bash') {
|
|
30
|
+
const cmd = toolInput?.command || '';
|
|
31
|
+
if (!cmd.includes(SESSION_RESULT_FILENAME)) return false;
|
|
32
|
+
return />\s*[^\s]*session_result/.test(cmd);
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create unified session hooks with stall detection, edit guard,
|
|
39
|
+
* and completion detection (auto-shorten timeout after session_result written).
|
|
40
|
+
* @param {Indicator} indicator
|
|
41
|
+
* @param {WriteStream|null} logStream
|
|
42
|
+
* @param {object} [options]
|
|
43
|
+
* @param {boolean} [options.enableStallDetection=false]
|
|
44
|
+
* @param {number} [options.stallTimeoutMs=1200000]
|
|
45
|
+
* @param {AbortController} [options.abortController]
|
|
46
|
+
* @param {boolean} [options.enableEditGuard=false]
|
|
47
|
+
* @param {boolean} [options.enableCompletionDetection=true]
|
|
48
|
+
* @param {number} [options.completionTimeoutMs=300000]
|
|
49
|
+
* @returns {{ hooks: object, cleanup: () => void, isStalled: () => boolean }}
|
|
50
|
+
*/
|
|
51
|
+
function createSessionHooks(indicator, logStream, options = {}) {
|
|
52
|
+
const {
|
|
53
|
+
enableStallDetection = false,
|
|
54
|
+
stallTimeoutMs = 1200000,
|
|
55
|
+
abortController = null,
|
|
56
|
+
enableEditGuard = false,
|
|
57
|
+
editThreshold = DEFAULT_EDIT_THRESHOLD,
|
|
58
|
+
enableCompletionDetection = true,
|
|
59
|
+
completionTimeoutMs = 300000,
|
|
60
|
+
} = options;
|
|
61
|
+
|
|
62
|
+
const editCounts = {};
|
|
63
|
+
let stallDetected = false;
|
|
64
|
+
let stallChecker = null;
|
|
65
|
+
let completionDetectedAt = 0;
|
|
66
|
+
|
|
67
|
+
if (enableStallDetection) {
|
|
68
|
+
stallChecker = setInterval(() => {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const idleMs = now - indicator.lastActivityTime; // 使用活动时间而非工具调用时间
|
|
71
|
+
|
|
72
|
+
// 优先检测 completion 超时(session_result 写入后的缩短超时)
|
|
73
|
+
if (completionDetectedAt > 0) {
|
|
74
|
+
const sinceCompletion = now - completionDetectedAt;
|
|
75
|
+
if (sinceCompletion > completionTimeoutMs && !stallDetected) {
|
|
76
|
+
stallDetected = true;
|
|
77
|
+
const shortMin = Math.ceil(completionTimeoutMs / 60000);
|
|
78
|
+
const actualMin = Math.floor(sinceCompletion / 60000);
|
|
79
|
+
log('warn', `\nsession_result 已写入 ${actualMin} 分钟,超过 ${shortMin} 分钟上限,自动中断`);
|
|
80
|
+
if (logStream) {
|
|
81
|
+
logStream.write(`\n[${new Date().toISOString()}] STALL: session_result 写入后 ${actualMin} 分钟(上限 ${shortMin} 分钟),自动中断\n`);
|
|
82
|
+
}
|
|
83
|
+
if (abortController) {
|
|
84
|
+
abortController.abort();
|
|
85
|
+
log('warn', '\n已发送中断信号');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 已检测到 completion,不再执行 stall 检测,等待 completion 超时
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 正常 stall 检测(仅在未检测到 completion 时执行)
|
|
93
|
+
if (idleMs > stallTimeoutMs && !stallDetected) {
|
|
94
|
+
stallDetected = true;
|
|
95
|
+
const idleMin = Math.floor(idleMs / 60000);
|
|
96
|
+
log('warn', `\n无响应超过 ${idleMin} 分钟,自动中断 session`);
|
|
97
|
+
if (logStream) {
|
|
98
|
+
logStream.write(`\n[${new Date().toISOString()}] STALL: 无响应 ${idleMin} 分钟,自动中断\n`);
|
|
99
|
+
}
|
|
100
|
+
if (abortController) {
|
|
101
|
+
abortController.abort();
|
|
102
|
+
log('warn', '\n已发送中断信号');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}, 30000);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hooks = {
|
|
109
|
+
PreToolUse: [{
|
|
110
|
+
matcher: '*',
|
|
111
|
+
hooks: [async (input) => {
|
|
112
|
+
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
113
|
+
logToolCall(logStream, input);
|
|
114
|
+
|
|
115
|
+
if (enableEditGuard) {
|
|
116
|
+
const target = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
117
|
+
if (['Write', 'Edit', 'MultiEdit'].includes(input.tool_name) && target) {
|
|
118
|
+
editCounts[target] = (editCounts[target] || 0) + 1;
|
|
119
|
+
if (editCounts[target] > editThreshold) {
|
|
120
|
+
return {
|
|
121
|
+
hookSpecificOutput: {
|
|
122
|
+
hookEventName: 'PreToolUse',
|
|
123
|
+
permissionDecision: 'deny',
|
|
124
|
+
permissionDecisionReason: `已对 ${target} 编辑 ${editCounts[target]} 次,疑似死循环。请重新审视方案后再继续。`,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {};
|
|
132
|
+
}]
|
|
133
|
+
}],
|
|
134
|
+
PostToolUse: [{
|
|
135
|
+
matcher: '*',
|
|
136
|
+
hooks: [async (input) => {
|
|
137
|
+
indicator.updatePhase('thinking');
|
|
138
|
+
indicator.updateStep('');
|
|
139
|
+
indicator.toolTarget = '';
|
|
140
|
+
|
|
141
|
+
if (enableCompletionDetection && !completionDetectedAt) {
|
|
142
|
+
if (isSessionResultWrite(input.tool_name, input.tool_input)) {
|
|
143
|
+
completionDetectedAt = Date.now();
|
|
144
|
+
const shortMin = Math.ceil(completionTimeoutMs / 60000);
|
|
145
|
+
indicator.setCompletionDetected(shortMin);
|
|
146
|
+
log('info', '');
|
|
147
|
+
log('info', `检测到 session_result 写入,${shortMin} 分钟内模型未终止将自动中断`);
|
|
148
|
+
if (logStream) {
|
|
149
|
+
logStream.write(`\n[${new Date().toISOString()}] COMPLETION_DETECTED: session_result.json written, ${shortMin}min grace period\n`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {};
|
|
155
|
+
}]
|
|
156
|
+
}],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
hooks,
|
|
161
|
+
cleanup() { if (stallChecker) clearInterval(stallChecker); },
|
|
162
|
+
isStalled() { return stallDetected; },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { createSessionHooks };
|