claude-coder 1.6.2 → 1.7.0
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 +23 -0
- package/prompts/scan_user.md +17 -0
- package/src/auth.js +245 -245
- package/src/config.js +201 -223
- package/src/hooks.js +160 -96
- package/src/indicator.js +217 -160
- package/src/init.js +144 -144
- package/src/prompts.js +295 -339
- package/src/runner.js +420 -394
- package/src/scanner.js +62 -62
- package/src/session.js +352 -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/runner.js
CHANGED
|
@@ -1,394 +1,420 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
7
|
-
const { paths, log, COLOR, loadConfig, ensureLoopDir, getProjectRoot } = require('./config');
|
|
8
|
-
const { loadTasks, getFeatures, getStats, findNextTask, forceStatus } = require('./tasks');
|
|
9
|
-
const { validate } = require('./validator');
|
|
10
|
-
const { scan } = require('./scanner');
|
|
11
|
-
const { loadSDK, runCodingSession, runAddSession } = require('./session');
|
|
12
|
-
|
|
13
|
-
const MAX_RETRY = 3;
|
|
14
|
-
|
|
15
|
-
function sleep(ms) {
|
|
16
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function getHead() {
|
|
20
|
-
try {
|
|
21
|
-
return execSync('git rev-parse HEAD', { cwd: getProjectRoot(), encoding: 'utf8' }).trim();
|
|
22
|
-
} catch {
|
|
23
|
-
return 'none';
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function allTasksDone() {
|
|
28
|
-
const data = loadTasks();
|
|
29
|
-
if (!data) return false;
|
|
30
|
-
const features = getFeatures(data);
|
|
31
|
-
if (features.length === 0) return true;
|
|
32
|
-
return features.every(f => f.status === 'done');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function killServicesByProfile() {
|
|
36
|
-
const p = paths();
|
|
37
|
-
if (!fs.existsSync(p.profile)) return;
|
|
38
|
-
try {
|
|
39
|
-
const profile = JSON.parse(fs.readFileSync(p.profile, 'utf8'));
|
|
40
|
-
const services = profile.services || [];
|
|
41
|
-
const ports = services.map(s => s.port).filter(Boolean);
|
|
42
|
-
if (ports.length === 0) return;
|
|
43
|
-
|
|
44
|
-
const isWin = process.platform === 'win32';
|
|
45
|
-
for (const port of ports) {
|
|
46
|
-
try {
|
|
47
|
-
if (isWin) {
|
|
48
|
-
const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
49
|
-
const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
|
|
50
|
-
for (const pid of pids) { try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' }); } catch { /* ignore */ } }
|
|
51
|
-
} else {
|
|
52
|
-
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
|
|
53
|
-
}
|
|
54
|
-
} catch { /* no process on port */ }
|
|
55
|
-
}
|
|
56
|
-
log('info', `已停止端口 ${ports.join(', ')} 上的服务`);
|
|
57
|
-
} catch { /* ignore profile read errors */ }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function rollback(headBefore, reason) {
|
|
61
|
-
if (!headBefore || headBefore === 'none') return;
|
|
62
|
-
|
|
63
|
-
killServicesByProfile();
|
|
64
|
-
|
|
65
|
-
if (process.platform === 'win32') await sleep(1500);
|
|
66
|
-
|
|
67
|
-
const cwd = getProjectRoot();
|
|
68
|
-
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
69
|
-
|
|
70
|
-
log('warn', `回滚到 ${headBefore} ...`);
|
|
71
|
-
|
|
72
|
-
let success = false;
|
|
73
|
-
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
74
|
-
try {
|
|
75
|
-
execSync(`git reset --hard ${headBefore}`, { cwd, stdio: 'pipe', env: gitEnv });
|
|
76
|
-
log('ok', '回滚完成');
|
|
77
|
-
success = true;
|
|
78
|
-
break;
|
|
79
|
-
} catch (err) {
|
|
80
|
-
if (attempt === 1) {
|
|
81
|
-
log('warn', `回滚首次失败,等待后重试: ${err.message}`);
|
|
82
|
-
await sleep(2000);
|
|
83
|
-
} else {
|
|
84
|
-
log('error', `回滚失败: ${err.message}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
appendProgress({
|
|
90
|
-
type: 'rollback',
|
|
91
|
-
timestamp: new Date().toISOString(),
|
|
92
|
-
reason: reason || 'harness 校验失败',
|
|
93
|
-
rollbackTo: headBefore,
|
|
94
|
-
success,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function markTaskFailed() {
|
|
99
|
-
const data = loadTasks();
|
|
100
|
-
if (!data) return;
|
|
101
|
-
const result = forceStatus(data, 'failed');
|
|
102
|
-
if (result) {
|
|
103
|
-
log('warn', `已将任务 ${result.id} 强制标记为 failed`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function tryPush() {
|
|
108
|
-
try {
|
|
109
|
-
const remotes = execSync('git remote', { cwd: getProjectRoot(), encoding: 'utf8' }).trim();
|
|
110
|
-
if (!remotes) return;
|
|
111
|
-
log('info', '正在推送代码...');
|
|
112
|
-
execSync('git push', { cwd: getProjectRoot(), stdio: 'inherit' });
|
|
113
|
-
log('ok', '推送成功');
|
|
114
|
-
} catch {
|
|
115
|
-
log('warn', '推送失败 (请检查网络或权限),继续执行...');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function appendProgress(entry) {
|
|
120
|
-
const p = paths();
|
|
121
|
-
let progress = { sessions: [] };
|
|
122
|
-
if (fs.existsSync(p.progressFile)) {
|
|
123
|
-
try {
|
|
124
|
-
const text = fs.readFileSync(p.progressFile, 'utf8');
|
|
125
|
-
progress = JSON.parse(text);
|
|
126
|
-
} catch { /* reset */ }
|
|
127
|
-
}
|
|
128
|
-
if (!Array.isArray(progress.sessions)) progress.sessions = [];
|
|
129
|
-
progress.sessions.push(entry);
|
|
130
|
-
fs.writeFileSync(p.progressFile, JSON.stringify(progress, null, 2) + '\n', 'utf8');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function printStats() {
|
|
134
|
-
const data = loadTasks();
|
|
135
|
-
if (!data) return;
|
|
136
|
-
const stats = getStats(data);
|
|
137
|
-
log('info', `进度: ${stats.done}/${stats.total} done, ${stats.in_progress} in_progress, ${stats.testing} testing, ${stats.failed} failed, ${stats.pending} pending`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function promptContinue() {
|
|
141
|
-
if (!process.stdin.isTTY) return true;
|
|
142
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
143
|
-
return new Promise(resolve => {
|
|
144
|
-
rl.question('是否继续?(y/n) ', answer => {
|
|
145
|
-
rl.close();
|
|
146
|
-
resolve(/^[Yy]/.test(answer.trim()));
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function run(requirement, opts = {}) {
|
|
152
|
-
const p = paths();
|
|
153
|
-
const projectRoot = getProjectRoot();
|
|
154
|
-
ensureLoopDir();
|
|
155
|
-
|
|
156
|
-
const maxSessions = opts.max || 50;
|
|
157
|
-
const pauseEvery = opts.pause ?? 0;
|
|
158
|
-
const dryRun = opts.dryRun || false;
|
|
159
|
-
|
|
160
|
-
console.log('');
|
|
161
|
-
console.log('============================================');
|
|
162
|
-
console.log(` Claude Coder${dryRun ? ' (预览模式)' : ''}`);
|
|
163
|
-
console.log('============================================');
|
|
164
|
-
console.log('');
|
|
165
|
-
|
|
166
|
-
const config = loadConfig();
|
|
167
|
-
if (config.provider !== 'claude' && config.baseUrl) {
|
|
168
|
-
log('ok', `模型配置已加载: ${config.provider}${config.model ? ` (${config.model})` : ''}`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const reqFile = path.join(projectRoot, 'requirements.md');
|
|
172
|
-
if (fs.existsSync(reqFile) && !requirement) {
|
|
173
|
-
requirement = fs.readFileSync(reqFile, 'utf8');
|
|
174
|
-
log('ok', '已读取需求文件: requirements.md');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
execSync('git rev-parse --is-inside-work-tree', { cwd: projectRoot, stdio: 'ignore' });
|
|
179
|
-
} catch {
|
|
180
|
-
log('info', '初始化 git 仓库...');
|
|
181
|
-
execSync('git init', { cwd: projectRoot, stdio: 'inherit' });
|
|
182
|
-
execSync('git add -A && git commit -m "init: 项目初始化" --allow-empty', {
|
|
183
|
-
cwd: projectRoot,
|
|
184
|
-
stdio: 'inherit',
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.log('
|
|
193
|
-
console.log('
|
|
194
|
-
console.log('
|
|
195
|
-
console.log('');
|
|
196
|
-
console.log('
|
|
197
|
-
console.log('
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.log(
|
|
213
|
-
console.log(`${COLOR.yellow}
|
|
214
|
-
console.log(
|
|
215
|
-
console.log(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
const { paths, log, COLOR, loadConfig, ensureLoopDir, getProjectRoot } = require('./config');
|
|
8
|
+
const { loadTasks, getFeatures, getStats, findNextTask, forceStatus } = require('./tasks');
|
|
9
|
+
const { validate } = require('./validator');
|
|
10
|
+
const { scan } = require('./scanner');
|
|
11
|
+
const { loadSDK, runCodingSession, runAddSession } = require('./session');
|
|
12
|
+
|
|
13
|
+
const MAX_RETRY = 3;
|
|
14
|
+
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getHead() {
|
|
20
|
+
try {
|
|
21
|
+
return execSync('git rev-parse HEAD', { cwd: getProjectRoot(), encoding: 'utf8' }).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return 'none';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function allTasksDone() {
|
|
28
|
+
const data = loadTasks();
|
|
29
|
+
if (!data) return false;
|
|
30
|
+
const features = getFeatures(data);
|
|
31
|
+
if (features.length === 0) return true;
|
|
32
|
+
return features.every(f => f.status === 'done');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function killServicesByProfile() {
|
|
36
|
+
const p = paths();
|
|
37
|
+
if (!fs.existsSync(p.profile)) return;
|
|
38
|
+
try {
|
|
39
|
+
const profile = JSON.parse(fs.readFileSync(p.profile, 'utf8'));
|
|
40
|
+
const services = profile.services || [];
|
|
41
|
+
const ports = services.map(s => s.port).filter(Boolean);
|
|
42
|
+
if (ports.length === 0) return;
|
|
43
|
+
|
|
44
|
+
const isWin = process.platform === 'win32';
|
|
45
|
+
for (const port of ports) {
|
|
46
|
+
try {
|
|
47
|
+
if (isWin) {
|
|
48
|
+
const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
49
|
+
const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
|
|
50
|
+
for (const pid of pids) { try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' }); } catch { /* ignore */ } }
|
|
51
|
+
} else {
|
|
52
|
+
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
|
|
53
|
+
}
|
|
54
|
+
} catch { /* no process on port */ }
|
|
55
|
+
}
|
|
56
|
+
log('info', `已停止端口 ${ports.join(', ')} 上的服务`);
|
|
57
|
+
} catch { /* ignore profile read errors */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function rollback(headBefore, reason) {
|
|
61
|
+
if (!headBefore || headBefore === 'none') return;
|
|
62
|
+
|
|
63
|
+
killServicesByProfile();
|
|
64
|
+
|
|
65
|
+
if (process.platform === 'win32') await sleep(1500);
|
|
66
|
+
|
|
67
|
+
const cwd = getProjectRoot();
|
|
68
|
+
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
69
|
+
|
|
70
|
+
log('warn', `回滚到 ${headBefore} ...`);
|
|
71
|
+
|
|
72
|
+
let success = false;
|
|
73
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
execSync(`git reset --hard ${headBefore}`, { cwd, stdio: 'pipe', env: gitEnv });
|
|
76
|
+
log('ok', '回滚完成');
|
|
77
|
+
success = true;
|
|
78
|
+
break;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (attempt === 1) {
|
|
81
|
+
log('warn', `回滚首次失败,等待后重试: ${err.message}`);
|
|
82
|
+
await sleep(2000);
|
|
83
|
+
} else {
|
|
84
|
+
log('error', `回滚失败: ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
appendProgress({
|
|
90
|
+
type: 'rollback',
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
reason: reason || 'harness 校验失败',
|
|
93
|
+
rollbackTo: headBefore,
|
|
94
|
+
success,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function markTaskFailed() {
|
|
99
|
+
const data = loadTasks();
|
|
100
|
+
if (!data) return;
|
|
101
|
+
const result = forceStatus(data, 'failed');
|
|
102
|
+
if (result) {
|
|
103
|
+
log('warn', `已将任务 ${result.id} 强制标记为 failed`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function tryPush() {
|
|
108
|
+
try {
|
|
109
|
+
const remotes = execSync('git remote', { cwd: getProjectRoot(), encoding: 'utf8' }).trim();
|
|
110
|
+
if (!remotes) return;
|
|
111
|
+
log('info', '正在推送代码...');
|
|
112
|
+
execSync('git push', { cwd: getProjectRoot(), stdio: 'inherit' });
|
|
113
|
+
log('ok', '推送成功');
|
|
114
|
+
} catch {
|
|
115
|
+
log('warn', '推送失败 (请检查网络或权限),继续执行...');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function appendProgress(entry) {
|
|
120
|
+
const p = paths();
|
|
121
|
+
let progress = { sessions: [] };
|
|
122
|
+
if (fs.existsSync(p.progressFile)) {
|
|
123
|
+
try {
|
|
124
|
+
const text = fs.readFileSync(p.progressFile, 'utf8');
|
|
125
|
+
progress = JSON.parse(text);
|
|
126
|
+
} catch { /* reset */ }
|
|
127
|
+
}
|
|
128
|
+
if (!Array.isArray(progress.sessions)) progress.sessions = [];
|
|
129
|
+
progress.sessions.push(entry);
|
|
130
|
+
fs.writeFileSync(p.progressFile, JSON.stringify(progress, null, 2) + '\n', 'utf8');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function printStats() {
|
|
134
|
+
const data = loadTasks();
|
|
135
|
+
if (!data) return;
|
|
136
|
+
const stats = getStats(data);
|
|
137
|
+
log('info', `进度: ${stats.done}/${stats.total} done, ${stats.in_progress} in_progress, ${stats.testing} testing, ${stats.failed} failed, ${stats.pending} pending`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function promptContinue() {
|
|
141
|
+
if (!process.stdin.isTTY) return true;
|
|
142
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
143
|
+
return new Promise(resolve => {
|
|
144
|
+
rl.question('是否继续?(y/n) ', answer => {
|
|
145
|
+
rl.close();
|
|
146
|
+
resolve(/^[Yy]/.test(answer.trim()));
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function run(requirement, opts = {}) {
|
|
152
|
+
const p = paths();
|
|
153
|
+
const projectRoot = getProjectRoot();
|
|
154
|
+
ensureLoopDir();
|
|
155
|
+
|
|
156
|
+
const maxSessions = opts.max || 50;
|
|
157
|
+
const pauseEvery = opts.pause ?? 0;
|
|
158
|
+
const dryRun = opts.dryRun || false;
|
|
159
|
+
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log('============================================');
|
|
162
|
+
console.log(` Claude Coder${dryRun ? ' (预览模式)' : ''}`);
|
|
163
|
+
console.log('============================================');
|
|
164
|
+
console.log('');
|
|
165
|
+
|
|
166
|
+
const config = loadConfig();
|
|
167
|
+
if (config.provider !== 'claude' && config.baseUrl) {
|
|
168
|
+
log('ok', `模型配置已加载: ${config.provider}${config.model ? ` (${config.model})` : ''}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const reqFile = path.join(projectRoot, 'requirements.md');
|
|
172
|
+
if (fs.existsSync(reqFile) && !requirement) {
|
|
173
|
+
requirement = fs.readFileSync(reqFile, 'utf8');
|
|
174
|
+
log('ok', '已读取需求文件: requirements.md');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd: projectRoot, stdio: 'ignore' });
|
|
179
|
+
} catch {
|
|
180
|
+
log('info', '初始化 git 仓库...');
|
|
181
|
+
execSync('git init', { cwd: projectRoot, stdio: 'inherit' });
|
|
182
|
+
execSync('git add -A && git commit -m "init: 项目初始化" --allow-empty', {
|
|
183
|
+
cwd: projectRoot,
|
|
184
|
+
stdio: 'inherit',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Phase 1: 项目扫描(生成 profile) ---
|
|
189
|
+
if (!fs.existsSync(p.profile)) {
|
|
190
|
+
if (!requirement) {
|
|
191
|
+
log('error', '首次运行需要提供需求描述');
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log('用法(二选一):');
|
|
194
|
+
console.log(' 方式 1: 在项目根目录创建 requirements.md');
|
|
195
|
+
console.log(' claude-coder run');
|
|
196
|
+
console.log('');
|
|
197
|
+
console.log(' 方式 2: 直接传入一句话需求');
|
|
198
|
+
console.log(' claude-coder run "你的需求描述"');
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (dryRun) {
|
|
203
|
+
log('info', '[DRY-RUN] 将执行初始化扫描(跳过)');
|
|
204
|
+
const reqPreview = (requirement || '').slice(0, 100);
|
|
205
|
+
log('info', `[DRY-RUN] 需求: ${reqPreview}${reqPreview.length >= 100 ? '...' : ''}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await loadSDK();
|
|
210
|
+
const scanResult = await scan(requirement, { projectRoot });
|
|
211
|
+
if (!scanResult.success) {
|
|
212
|
+
console.log('');
|
|
213
|
+
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
214
|
+
console.log(`${COLOR.yellow} 若出现 "Credit balance is too low",请运行:${COLOR.reset}`);
|
|
215
|
+
console.log(` ${COLOR.green}claude-coder setup${COLOR.reset}`);
|
|
216
|
+
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- Phase 2: 任务分解(scan 后衔接 add) ---
|
|
222
|
+
if (!fs.existsSync(p.tasksFile)) {
|
|
223
|
+
if (requirement) {
|
|
224
|
+
console.log('');
|
|
225
|
+
log('ok', '项目扫描完成,是否根据需求分解任务?');
|
|
226
|
+
const shouldAdd = await promptContinue();
|
|
227
|
+
if (shouldAdd) {
|
|
228
|
+
if (!dryRun) await loadSDK();
|
|
229
|
+
deployTestRule(p);
|
|
230
|
+
log('info', '正在分解任务...');
|
|
231
|
+
await runAddSession(requirement, { projectRoot });
|
|
232
|
+
} else {
|
|
233
|
+
log('info', '跳过任务分解。后续可通过 claude-coder add 手动添加任务。');
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
log('warn', 'tasks.json 不存在且无需求描述,请运行 claude-coder add 添加任务');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!fs.existsSync(p.tasksFile)) {
|
|
241
|
+
log('error', 'tasks.json 不存在,无法进入编码循环。请先运行 claude-coder add 添加任务。');
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (fs.existsSync(p.profile) && fs.existsSync(p.tasksFile)) {
|
|
246
|
+
printStats();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!dryRun) await loadSDK();
|
|
250
|
+
log('info', `开始编码循环 (最多 ${maxSessions} 个会话) ...`);
|
|
251
|
+
console.log('');
|
|
252
|
+
|
|
253
|
+
let consecutiveFailures = 0;
|
|
254
|
+
|
|
255
|
+
for (let session = 1; session <= maxSessions; session++) {
|
|
256
|
+
console.log('');
|
|
257
|
+
console.log('--------------------------------------------');
|
|
258
|
+
log('info', `Session ${session} / ${maxSessions}`);
|
|
259
|
+
console.log('--------------------------------------------');
|
|
260
|
+
|
|
261
|
+
const taskData = loadTasks();
|
|
262
|
+
if (!taskData) {
|
|
263
|
+
log('error', 'tasks.json 无法读取,终止循环');
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (allTasksDone()) {
|
|
268
|
+
console.log('');
|
|
269
|
+
log('ok', '所有任务已完成!');
|
|
270
|
+
printStats();
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
printStats();
|
|
275
|
+
|
|
276
|
+
if (dryRun) {
|
|
277
|
+
const next = findNextTask(loadTasks());
|
|
278
|
+
log('info', `[DRY-RUN] 下一个任务: ${next ? `${next.id} - ${next.description}` : '无'}`);
|
|
279
|
+
if (!next) break;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const headBefore = getHead();
|
|
284
|
+
const nextTask = findNextTask(taskData);
|
|
285
|
+
const taskId = nextTask?.id || 'unknown';
|
|
286
|
+
|
|
287
|
+
const sessionResult = await runCodingSession(session, {
|
|
288
|
+
projectRoot,
|
|
289
|
+
taskId,
|
|
290
|
+
consecutiveFailures,
|
|
291
|
+
maxSessions,
|
|
292
|
+
lastValidateLog: consecutiveFailures > 0 ? '上次校验失败' : '',
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (sessionResult.stalled) {
|
|
296
|
+
log('warn', `Session ${session} 因停顿超时中断,跳过校验直接重试`);
|
|
297
|
+
consecutiveFailures++;
|
|
298
|
+
await rollback(headBefore, '停顿超时');
|
|
299
|
+
if (consecutiveFailures >= MAX_RETRY) {
|
|
300
|
+
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
301
|
+
markTaskFailed();
|
|
302
|
+
consecutiveFailures = 0;
|
|
303
|
+
}
|
|
304
|
+
appendProgress({
|
|
305
|
+
session,
|
|
306
|
+
timestamp: new Date().toISOString(),
|
|
307
|
+
result: 'stalled',
|
|
308
|
+
cost: sessionResult.cost,
|
|
309
|
+
taskId,
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
log('info', '开始 harness 校验 ...');
|
|
315
|
+
const validateResult = await validate(headBefore, taskId);
|
|
316
|
+
|
|
317
|
+
if (!validateResult.fatal) {
|
|
318
|
+
if (validateResult.hasWarnings) {
|
|
319
|
+
log('warn', `Session ${session} 校验通过 (有自动修复或警告)`);
|
|
320
|
+
} else {
|
|
321
|
+
log('ok', `Session ${session} 校验通过`);
|
|
322
|
+
}
|
|
323
|
+
tryPush();
|
|
324
|
+
consecutiveFailures = 0;
|
|
325
|
+
|
|
326
|
+
appendProgress({
|
|
327
|
+
session,
|
|
328
|
+
timestamp: new Date().toISOString(),
|
|
329
|
+
result: 'success',
|
|
330
|
+
cost: sessionResult.cost,
|
|
331
|
+
taskId,
|
|
332
|
+
statusAfter: validateResult.sessionData?.status_after || null,
|
|
333
|
+
notes: validateResult.sessionData?.notes || null,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
} else {
|
|
337
|
+
consecutiveFailures++;
|
|
338
|
+
log('error', `Session ${session} 校验失败 (连续失败: ${consecutiveFailures}/${MAX_RETRY})`);
|
|
339
|
+
|
|
340
|
+
appendProgress({
|
|
341
|
+
session,
|
|
342
|
+
timestamp: new Date().toISOString(),
|
|
343
|
+
result: 'fatal',
|
|
344
|
+
cost: sessionResult.cost,
|
|
345
|
+
taskId,
|
|
346
|
+
reason: validateResult.sessionData?.reason || '校验失败',
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await rollback(headBefore, '校验失败');
|
|
350
|
+
|
|
351
|
+
if (consecutiveFailures >= MAX_RETRY) {
|
|
352
|
+
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
353
|
+
markTaskFailed();
|
|
354
|
+
consecutiveFailures = 0;
|
|
355
|
+
log('warn', '已将任务标记为 failed,继续下一个任务');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (pauseEvery > 0 && session % pauseEvery === 0) {
|
|
360
|
+
console.log('');
|
|
361
|
+
printStats();
|
|
362
|
+
const shouldContinue = await promptContinue();
|
|
363
|
+
if (!shouldContinue) {
|
|
364
|
+
log('info', '手动停止');
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
killServicesByProfile();
|
|
371
|
+
|
|
372
|
+
console.log('');
|
|
373
|
+
console.log('============================================');
|
|
374
|
+
console.log(' 运行结束');
|
|
375
|
+
console.log('============================================');
|
|
376
|
+
console.log('');
|
|
377
|
+
printStats();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function add(instruction, opts = {}) {
|
|
381
|
+
await loadSDK();
|
|
382
|
+
const p = paths();
|
|
383
|
+
const projectRoot = getProjectRoot();
|
|
384
|
+
ensureLoopDir();
|
|
385
|
+
|
|
386
|
+
const config = loadConfig();
|
|
387
|
+
|
|
388
|
+
if (!opts.model) {
|
|
389
|
+
if (config.defaultOpus) {
|
|
390
|
+
opts.model = config.defaultOpus;
|
|
391
|
+
} else if (config.model) {
|
|
392
|
+
opts.model = config.model;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const displayModel = opts.model || config.model || '(default)';
|
|
397
|
+
log('ok', `模型配置已加载: ${config.provider || 'claude'} (add 使用: ${displayModel})`);
|
|
398
|
+
|
|
399
|
+
if (!fs.existsSync(p.profile)) {
|
|
400
|
+
log('error', 'add 需要先完成项目扫描(至少运行一次 claude-coder run)');
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
deployTestRule(p);
|
|
405
|
+
|
|
406
|
+
await runAddSession(instruction, { projectRoot, ...opts });
|
|
407
|
+
printStats();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function deployTestRule(p) {
|
|
411
|
+
const dest = path.join(p.loopDir, 'test_rule.md');
|
|
412
|
+
if (fs.existsSync(dest)) return;
|
|
413
|
+
if (!fs.existsSync(p.testRuleTemplate)) return;
|
|
414
|
+
try {
|
|
415
|
+
fs.copyFileSync(p.testRuleTemplate, dest);
|
|
416
|
+
log('ok', '已部署测试指导规则 → .claude-coder/test_rule.md');
|
|
417
|
+
} catch { /* ignore */ }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
module.exports = { run, add };
|