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/runner.js
CHANGED
|
@@ -1,394 +1,396 @@
|
|
|
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(
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
log('
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (!fs.existsSync(p.
|
|
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
|
-
console.log(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
printStats();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (
|
|
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(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
|
+
try {
|
|
172
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd: projectRoot, stdio: 'ignore' });
|
|
173
|
+
} catch {
|
|
174
|
+
log('info', '初始化 git 仓库...');
|
|
175
|
+
execSync('git init', { cwd: projectRoot, stdio: 'inherit' });
|
|
176
|
+
execSync('git add -A && git commit -m "init: 项目初始化" --allow-empty', {
|
|
177
|
+
cwd: projectRoot,
|
|
178
|
+
stdio: 'inherit',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 检查前置条件
|
|
183
|
+
if (!fs.existsSync(p.profile)) {
|
|
184
|
+
log('error', 'profile.json 不存在,请先运行 claude-coder add 添加任务');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!fs.existsSync(p.tasksFile)) {
|
|
189
|
+
log('error', 'tasks.json 不存在,请先运行 claude-coder add 添加任务');
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
printStats();
|
|
194
|
+
|
|
195
|
+
if (!dryRun) await loadSDK();
|
|
196
|
+
log('info', `开始编码循环 (最多 ${maxSessions} 个会话) ...`);
|
|
197
|
+
console.log('');
|
|
198
|
+
|
|
199
|
+
let consecutiveFailures = 0;
|
|
200
|
+
|
|
201
|
+
for (let session = 1; session <= maxSessions; session++) {
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log('--------------------------------------------');
|
|
204
|
+
log('info', `Session ${session} / ${maxSessions}`);
|
|
205
|
+
console.log('--------------------------------------------');
|
|
206
|
+
|
|
207
|
+
const taskData = loadTasks();
|
|
208
|
+
if (!taskData) {
|
|
209
|
+
log('error', 'tasks.json 无法读取,终止循环');
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (allTasksDone()) {
|
|
214
|
+
console.log('');
|
|
215
|
+
log('ok', '所有任务已完成!');
|
|
216
|
+
printStats();
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
printStats();
|
|
221
|
+
|
|
222
|
+
if (dryRun) {
|
|
223
|
+
const next = findNextTask(loadTasks());
|
|
224
|
+
log('info', `[DRY-RUN] 下一个任务: ${next ? `${next.id} - ${next.description}` : '无'}`);
|
|
225
|
+
if (!next) break;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const headBefore = getHead();
|
|
230
|
+
const nextTask = findNextTask(taskData);
|
|
231
|
+
const taskId = nextTask?.id || 'unknown';
|
|
232
|
+
|
|
233
|
+
const sessionResult = await runCodingSession(session, {
|
|
234
|
+
projectRoot,
|
|
235
|
+
taskId,
|
|
236
|
+
consecutiveFailures,
|
|
237
|
+
maxSessions,
|
|
238
|
+
lastValidateLog: consecutiveFailures > 0 ? '上次校验失败' : '',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (sessionResult.stalled) {
|
|
242
|
+
log('warn', `Session ${session} 因停顿超时中断,跳过校验直接重试`);
|
|
243
|
+
consecutiveFailures++;
|
|
244
|
+
await rollback(headBefore, '停顿超时');
|
|
245
|
+
if (consecutiveFailures >= MAX_RETRY) {
|
|
246
|
+
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
247
|
+
markTaskFailed();
|
|
248
|
+
consecutiveFailures = 0;
|
|
249
|
+
}
|
|
250
|
+
appendProgress({
|
|
251
|
+
session,
|
|
252
|
+
timestamp: new Date().toISOString(),
|
|
253
|
+
result: 'stalled',
|
|
254
|
+
cost: sessionResult.cost,
|
|
255
|
+
taskId,
|
|
256
|
+
});
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
log('info', '开始 harness 校验 ...');
|
|
261
|
+
const validateResult = await validate(headBefore, taskId);
|
|
262
|
+
|
|
263
|
+
if (!validateResult.fatal) {
|
|
264
|
+
if (validateResult.hasWarnings) {
|
|
265
|
+
log('warn', `Session ${session} 校验通过 (有自动修复或警告)`);
|
|
266
|
+
} else {
|
|
267
|
+
log('ok', `Session ${session} 校验通过`);
|
|
268
|
+
}
|
|
269
|
+
tryPush();
|
|
270
|
+
consecutiveFailures = 0;
|
|
271
|
+
|
|
272
|
+
appendProgress({
|
|
273
|
+
session,
|
|
274
|
+
timestamp: new Date().toISOString(),
|
|
275
|
+
result: 'success',
|
|
276
|
+
cost: sessionResult.cost,
|
|
277
|
+
taskId,
|
|
278
|
+
statusAfter: validateResult.sessionData?.status_after || null,
|
|
279
|
+
notes: validateResult.sessionData?.notes || null,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
} else {
|
|
283
|
+
consecutiveFailures++;
|
|
284
|
+
log('error', `Session ${session} 校验失败 (连续失败: ${consecutiveFailures}/${MAX_RETRY})`);
|
|
285
|
+
|
|
286
|
+
appendProgress({
|
|
287
|
+
session,
|
|
288
|
+
timestamp: new Date().toISOString(),
|
|
289
|
+
result: 'fatal',
|
|
290
|
+
cost: sessionResult.cost,
|
|
291
|
+
taskId,
|
|
292
|
+
reason: validateResult.sessionData?.reason || '校验失败',
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await rollback(headBefore, '校验失败');
|
|
296
|
+
|
|
297
|
+
if (consecutiveFailures >= MAX_RETRY) {
|
|
298
|
+
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
299
|
+
markTaskFailed();
|
|
300
|
+
consecutiveFailures = 0;
|
|
301
|
+
log('warn', '已将任务标记为 failed,继续下一个任务');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (pauseEvery > 0 && session % pauseEvery === 0) {
|
|
306
|
+
console.log('');
|
|
307
|
+
printStats();
|
|
308
|
+
const shouldContinue = await promptContinue();
|
|
309
|
+
if (!shouldContinue) {
|
|
310
|
+
log('info', '手动停止');
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
killServicesByProfile();
|
|
317
|
+
|
|
318
|
+
console.log('');
|
|
319
|
+
console.log('============================================');
|
|
320
|
+
console.log(' 运行结束');
|
|
321
|
+
console.log('============================================');
|
|
322
|
+
console.log('');
|
|
323
|
+
printStats();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function promptAutoRun() {
|
|
327
|
+
if (!process.stdin.isTTY) return false;
|
|
328
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
329
|
+
return new Promise(resolve => {
|
|
330
|
+
rl.question('任务分解完成后是否自动开始执行?(y/n) ', answer => {
|
|
331
|
+
rl.close();
|
|
332
|
+
resolve(/^[Yy]/.test(answer.trim()));
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function add(instruction, opts = {}) {
|
|
338
|
+
await loadSDK();
|
|
339
|
+
const p = paths();
|
|
340
|
+
const projectRoot = getProjectRoot();
|
|
341
|
+
ensureLoopDir();
|
|
342
|
+
|
|
343
|
+
const config = loadConfig();
|
|
344
|
+
|
|
345
|
+
if (!opts.model) {
|
|
346
|
+
if (config.defaultOpus) {
|
|
347
|
+
opts.model = config.defaultOpus;
|
|
348
|
+
} else if (config.model) {
|
|
349
|
+
opts.model = config.model;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const displayModel = opts.model || config.model || '(default)';
|
|
354
|
+
log('ok', `模型配置已加载: ${config.provider || 'claude'} (add 使用: ${displayModel})`);
|
|
355
|
+
|
|
356
|
+
// 如果 profile 不存在,先执行项目扫描
|
|
357
|
+
if (!fs.existsSync(p.profile)) {
|
|
358
|
+
log('info', '首次使用,正在执行项目扫描...');
|
|
359
|
+
const scanResult = await scan(instruction, { projectRoot });
|
|
360
|
+
if (!scanResult.success) {
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
363
|
+
console.log(`${COLOR.yellow} 若出现 "Credit balance is too low",请运行:${COLOR.reset}`);
|
|
364
|
+
console.log(` ${COLOR.green}claude-coder setup${COLOR.reset}`);
|
|
365
|
+
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 询问用户是否在完成后自动运行
|
|
371
|
+
const shouldAutoRun = await promptAutoRun();
|
|
372
|
+
|
|
373
|
+
deployTestRule(p);
|
|
374
|
+
|
|
375
|
+
await runAddSession(instruction, { projectRoot, ...opts });
|
|
376
|
+
printStats();
|
|
377
|
+
|
|
378
|
+
// 如果用户选择自动运行,则调用 run()
|
|
379
|
+
if (shouldAutoRun) {
|
|
380
|
+
console.log('');
|
|
381
|
+
log('info', '开始自动执行任务...');
|
|
382
|
+
await run(opts);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function deployTestRule(p) {
|
|
387
|
+
const dest = path.join(p.loopDir, 'test_rule.md');
|
|
388
|
+
if (fs.existsSync(dest)) return;
|
|
389
|
+
if (!fs.existsSync(p.testRuleTemplate)) return;
|
|
390
|
+
try {
|
|
391
|
+
fs.copyFileSync(p.testRuleTemplate, dest);
|
|
392
|
+
log('ok', '已部署测试指导规则 → .claude-coder/test_rule.md');
|
|
393
|
+
} catch { /* ignore */ }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = { run, add };
|