claude-coder 1.10.0 → 1.10.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 +239 -236
- package/bin/cli.js +170 -170
- package/package.json +55 -55
- package/recipes/_shared/roles/developer.md +11 -11
- package/recipes/_shared/roles/product.md +12 -12
- package/recipes/_shared/roles/tester.md +12 -12
- package/recipes/_shared/test/report-format.md +86 -86
- package/recipes/backend/base.md +27 -27
- package/recipes/backend/components/auth.md +18 -18
- package/recipes/backend/components/crud-api.md +18 -18
- package/recipes/backend/components/file-service.md +15 -15
- package/recipes/backend/manifest.json +20 -20
- package/recipes/backend/test/api-test.md +25 -25
- package/recipes/console/base.md +37 -37
- package/recipes/console/components/modal-form.md +20 -20
- package/recipes/console/components/pagination.md +17 -17
- package/recipes/console/components/search.md +17 -17
- package/recipes/console/components/table-list.md +18 -18
- package/recipes/console/components/tabs.md +14 -14
- package/recipes/console/components/tree.md +15 -15
- package/recipes/console/components/upload.md +15 -15
- package/recipes/console/manifest.json +24 -24
- package/recipes/console/test/crud-e2e.md +47 -47
- package/recipes/h5/base.md +26 -26
- package/recipes/h5/components/animation.md +11 -11
- package/recipes/h5/components/countdown.md +11 -11
- package/recipes/h5/components/share.md +11 -11
- package/recipes/h5/components/swiper.md +11 -11
- package/recipes/h5/manifest.json +21 -21
- package/recipes/h5/test/h5-e2e.md +20 -20
- package/src/commands/auth.js +420 -420
- package/src/commands/setup-modules/helpers.js +100 -100
- package/src/commands/setup-modules/index.js +25 -25
- package/src/commands/setup-modules/mcp.js +115 -115
- package/src/commands/setup-modules/provider.js +260 -260
- package/src/commands/setup-modules/safety.js +47 -47
- package/src/commands/setup-modules/simplify.js +52 -52
- package/src/commands/setup.js +172 -172
- package/src/common/assets.js +259 -259
- package/src/common/config.js +147 -147
- package/src/common/constants.js +55 -55
- package/src/common/indicator.js +260 -260
- package/src/common/interaction.js +170 -170
- package/src/common/logging.js +77 -77
- package/src/common/sdk.js +48 -48
- package/src/common/tasks.js +88 -88
- package/src/common/utils.js +215 -214
- package/src/core/coding.js +35 -35
- package/src/core/design.js +268 -268
- package/src/core/go.js +264 -264
- package/src/core/hooks.js +514 -514
- package/src/core/init.js +175 -175
- package/src/core/plan.js +194 -194
- package/src/core/prompts.js +292 -292
- package/src/core/repair.js +36 -36
- package/src/core/runner.js +438 -471
- package/src/core/scan.js +94 -94
- package/src/core/session.js +294 -294
- package/src/core/simplify.js +76 -76
- package/src/core/state.js +120 -120
- package/src/index.js +80 -80
- package/templates/coding/system.md +65 -65
- package/templates/coding/user.md +18 -18
- package/templates/design/base.md +103 -103
- package/templates/design/fixSystem.md +71 -71
- package/templates/design/fixUser.md +3 -3
- package/templates/design/init.md +304 -304
- package/templates/design/system.md +108 -108
- package/templates/design/user.md +11 -11
- package/templates/go/system.md +130 -130
- package/templates/other/bash-process.md +12 -12
- package/templates/other/coreProtocol.md +30 -30
- package/templates/other/guidance.json +72 -72
- package/templates/other/requirements.example.md +57 -57
- package/templates/other/test_rule.md +192 -192
- package/templates/other/web-testing.md +17 -17
- package/templates/plan/system.md +78 -78
- package/templates/plan/user.md +9 -9
- package/templates/scan/system.md +120 -120
- package/templates/scan/user.md +10 -10
- package/types/index.d.ts +217 -217
package/src/core/runner.js
CHANGED
|
@@ -1,471 +1,438 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { execSync } = require('child_process');
|
|
4
|
-
const readline = require('readline');
|
|
5
|
-
const { log, COLOR, printModeBanner } = require('../common/config');
|
|
6
|
-
const { assets } = require('../common/assets');
|
|
7
|
-
const { loadTasks, saveTasks, getFeatures, getStats, printStats } = require('../common/tasks');
|
|
8
|
-
const { getGitHead, sleep, tryPush, killServices } = require('../common/utils');
|
|
9
|
-
const { RETRY } = require('../common/constants');
|
|
10
|
-
const {
|
|
11
|
-
loadState, saveState, selectNextTask, isAllDone,
|
|
12
|
-
appendProgress, incrementSession, markSimplifyDone,
|
|
13
|
-
} = require('./state');
|
|
14
|
-
|
|
15
|
-
const MAX_RETRY = RETRY.MAX_ATTEMPTS;
|
|
16
|
-
|
|
17
|
-
// ─── Display Helpers ──────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
function printBanner(dryRun, config, maxSessions) {
|
|
20
|
-
const mode = dryRun ? '预览模式' : `max: ${maxSessions}`;
|
|
21
|
-
printModeBanner('run', mode, config?.model);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function _progressBar(done, total, width = 24) {
|
|
25
|
-
if (total === 0) return `${COLOR.dim}[${'░'.repeat(width)}]${COLOR.reset}`;
|
|
26
|
-
const filled = Math.round(done / total * width);
|
|
27
|
-
return `${COLOR.green}[${'█'.repeat(filled)}${COLOR.dim}${'░'.repeat(width - filled)}${COLOR.reset}${COLOR.green}]${COLOR.reset}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function printSessionHeader(session, maxSessions, taskData, taskId) {
|
|
31
|
-
const stats = getStats(taskData);
|
|
32
|
-
const task = taskId ? getFeatures(taskData).find(f => f.id === taskId) : null;
|
|
33
|
-
const bar = _progressBar(stats.done, stats.total);
|
|
34
|
-
|
|
35
|
-
console.error('');
|
|
36
|
-
console.error(`${COLOR.cyan}┌─ Session ${session} / ${maxSessions} ${'─'.repeat(32)}┐${COLOR.reset}`);
|
|
37
|
-
if (task) {
|
|
38
|
-
console.error(`${COLOR.cyan}│${COLOR.reset} 任务: ${COLOR.bold}${task.id}${COLOR.reset} ${COLOR.dim}-${COLOR.reset} ${task.description || ''}`);
|
|
39
|
-
}
|
|
40
|
-
console.error(`${COLOR.cyan}│${COLOR.reset} 进度: ${bar} ${stats.done}/${stats.total} ${COLOR.green}✔${stats.done}${COLOR.reset} ${COLOR.yellow}○${stats.pending}${COLOR.reset} ${COLOR.red}✘${stats.failed}${COLOR.reset}`);
|
|
41
|
-
console.error(`${COLOR.cyan}└${'─'.repeat(46)}┘${COLOR.reset}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function printDryRun(taskData) {
|
|
45
|
-
const next = selectNextTask(taskData);
|
|
46
|
-
log('info', `[DRY-RUN] 下一个任务: ${next ? `${next.id} - ${next.description}` : '无待处理任务'}`);
|
|
47
|
-
|
|
48
|
-
if (!next) {
|
|
49
|
-
log('ok', '[DRY-RUN] 无可执行任务,预览结束');
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
console.log('');
|
|
54
|
-
log('info', '[DRY-RUN] 任务队列:');
|
|
55
|
-
const features = getFeatures(taskData);
|
|
56
|
-
for (const f of features) {
|
|
57
|
-
const st = f.status || 'unknown';
|
|
58
|
-
const icon = { done: '✓', in_progress: '▸', pending: '○', failed: '✗', testing: '◇' }[st] || '?';
|
|
59
|
-
const color = { done: COLOR.green, failed: COLOR.red, in_progress: COLOR.blue, testing: COLOR.yellow, pending: COLOR.dim }[st] || '';
|
|
60
|
-
log('info', ` ${color}${icon}${COLOR.reset} [${st.padEnd(11)}] ${f.id} - ${f.description || ''}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function printEndBanner() {
|
|
65
|
-
console.error('');
|
|
66
|
-
console.error(`${COLOR.cyan}╔══════════════════════════════════════════════╗${COLOR.reset}`);
|
|
67
|
-
console.error(`${COLOR.cyan}║${COLOR.reset} ${COLOR.bold}运行结束${COLOR.reset}`);
|
|
68
|
-
console.error(`${COLOR.cyan}╚══════════════════════════════════════════════╝${COLOR.reset}`);
|
|
69
|
-
console.error('');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function promptContinue() {
|
|
73
|
-
if (!process.stdin.isTTY) return true;
|
|
74
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
75
|
-
return new Promise(resolve => {
|
|
76
|
-
rl.question('是否继续?(y/n) ', answer => {
|
|
77
|
-
rl.close();
|
|
78
|
-
resolve(/^[Yy]/.test(answer.trim()));
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ─── Lifecycle: Snapshot ──────────────────────────────────
|
|
84
|
-
|
|
85
|
-
function snapshot(projectRoot, taskData) {
|
|
86
|
-
const nextTask = selectNextTask(taskData);
|
|
87
|
-
const taskId = nextTask?.id || 'unknown';
|
|
88
|
-
|
|
89
|
-
const state = loadState();
|
|
90
|
-
state.current_task_id = taskId;
|
|
91
|
-
saveState(state);
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
headBefore: getGitHead(projectRoot),
|
|
95
|
-
taskId,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ─── Lifecycle: Validation ────────────────────────────────
|
|
100
|
-
|
|
101
|
-
function _validateSessionResult() {
|
|
102
|
-
if (!assets.exists('sessionResult')) {
|
|
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
|
-
|
|
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
|
-
const projectRoot = assets.projectRoot;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
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
|
-
|
|
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
|
-
success,
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const validateResult = await validate(config, headBefore, taskId);
|
|
443
|
-
|
|
444
|
-
if (!validateResult.fatal) {
|
|
445
|
-
const level = validateResult.hasWarnings ? 'warn' : 'ok';
|
|
446
|
-
log(level, `Session ${session} ${validateResult.hasWarnings ? '校验通过 (有警告)' : '校验通过 ✓'}`);
|
|
447
|
-
state = await onSuccess(session, { taskId, sessionResult, validateResult });
|
|
448
|
-
|
|
449
|
-
if (shouldSimplify(config)) {
|
|
450
|
-
await tryRunSimplify(config);
|
|
451
|
-
}
|
|
452
|
-
} else {
|
|
453
|
-
state = await onFailure(session, { headBefore, taskId, sessionResult, validateResult, ...state });
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (pauseEvery > 0 && session % pauseEvery === 0) {
|
|
457
|
-
console.log('');
|
|
458
|
-
printStats();
|
|
459
|
-
if (!await promptContinue()) {
|
|
460
|
-
log('info', '手动停止');
|
|
461
|
-
break;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
killServices(projectRoot);
|
|
467
|
-
printEndBanner();
|
|
468
|
-
printStats();
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
module.exports = { executeRun };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const { log, COLOR, printModeBanner } = require('../common/config');
|
|
6
|
+
const { assets } = require('../common/assets');
|
|
7
|
+
const { loadTasks, saveTasks, getFeatures, getStats, printStats } = require('../common/tasks');
|
|
8
|
+
const { getGitHead, sleep, tryPush, killServices } = require('../common/utils');
|
|
9
|
+
const { RETRY } = require('../common/constants');
|
|
10
|
+
const {
|
|
11
|
+
loadState, saveState, selectNextTask, isAllDone,
|
|
12
|
+
appendProgress, incrementSession, markSimplifyDone,
|
|
13
|
+
} = require('./state');
|
|
14
|
+
|
|
15
|
+
const MAX_RETRY = RETRY.MAX_ATTEMPTS;
|
|
16
|
+
|
|
17
|
+
// ─── Display Helpers ──────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function printBanner(dryRun, config, maxSessions) {
|
|
20
|
+
const mode = dryRun ? '预览模式' : `max: ${maxSessions}`;
|
|
21
|
+
printModeBanner('run', mode, config?.model);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _progressBar(done, total, width = 24) {
|
|
25
|
+
if (total === 0) return `${COLOR.dim}[${'░'.repeat(width)}]${COLOR.reset}`;
|
|
26
|
+
const filled = Math.round(done / total * width);
|
|
27
|
+
return `${COLOR.green}[${'█'.repeat(filled)}${COLOR.dim}${'░'.repeat(width - filled)}${COLOR.reset}${COLOR.green}]${COLOR.reset}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function printSessionHeader(session, maxSessions, taskData, taskId) {
|
|
31
|
+
const stats = getStats(taskData);
|
|
32
|
+
const task = taskId ? getFeatures(taskData).find(f => f.id === taskId) : null;
|
|
33
|
+
const bar = _progressBar(stats.done, stats.total);
|
|
34
|
+
|
|
35
|
+
console.error('');
|
|
36
|
+
console.error(`${COLOR.cyan}┌─ Session ${session} / ${maxSessions} ${'─'.repeat(32)}┐${COLOR.reset}`);
|
|
37
|
+
if (task) {
|
|
38
|
+
console.error(`${COLOR.cyan}│${COLOR.reset} 任务: ${COLOR.bold}${task.id}${COLOR.reset} ${COLOR.dim}-${COLOR.reset} ${task.description || ''}`);
|
|
39
|
+
}
|
|
40
|
+
console.error(`${COLOR.cyan}│${COLOR.reset} 进度: ${bar} ${stats.done}/${stats.total} ${COLOR.green}✔${stats.done}${COLOR.reset} ${COLOR.yellow}○${stats.pending}${COLOR.reset} ${COLOR.red}✘${stats.failed}${COLOR.reset}`);
|
|
41
|
+
console.error(`${COLOR.cyan}└${'─'.repeat(46)}┘${COLOR.reset}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printDryRun(taskData) {
|
|
45
|
+
const next = selectNextTask(taskData);
|
|
46
|
+
log('info', `[DRY-RUN] 下一个任务: ${next ? `${next.id} - ${next.description}` : '无待处理任务'}`);
|
|
47
|
+
|
|
48
|
+
if (!next) {
|
|
49
|
+
log('ok', '[DRY-RUN] 无可执行任务,预览结束');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('');
|
|
54
|
+
log('info', '[DRY-RUN] 任务队列:');
|
|
55
|
+
const features = getFeatures(taskData);
|
|
56
|
+
for (const f of features) {
|
|
57
|
+
const st = f.status || 'unknown';
|
|
58
|
+
const icon = { done: '✓', in_progress: '▸', pending: '○', failed: '✗', testing: '◇' }[st] || '?';
|
|
59
|
+
const color = { done: COLOR.green, failed: COLOR.red, in_progress: COLOR.blue, testing: COLOR.yellow, pending: COLOR.dim }[st] || '';
|
|
60
|
+
log('info', ` ${color}${icon}${COLOR.reset} [${st.padEnd(11)}] ${f.id} - ${f.description || ''}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printEndBanner() {
|
|
65
|
+
console.error('');
|
|
66
|
+
console.error(`${COLOR.cyan}╔══════════════════════════════════════════════╗${COLOR.reset}`);
|
|
67
|
+
console.error(`${COLOR.cyan}║${COLOR.reset} ${COLOR.bold}运行结束${COLOR.reset}`);
|
|
68
|
+
console.error(`${COLOR.cyan}╚══════════════════════════════════════════════╝${COLOR.reset}`);
|
|
69
|
+
console.error('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function promptContinue() {
|
|
73
|
+
if (!process.stdin.isTTY) return true;
|
|
74
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
75
|
+
return new Promise(resolve => {
|
|
76
|
+
rl.question('是否继续?(y/n) ', answer => {
|
|
77
|
+
rl.close();
|
|
78
|
+
resolve(/^[Yy]/.test(answer.trim()));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Lifecycle: Snapshot ──────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function snapshot(projectRoot, taskData) {
|
|
86
|
+
const nextTask = selectNextTask(taskData);
|
|
87
|
+
const taskId = nextTask?.id || 'unknown';
|
|
88
|
+
|
|
89
|
+
const state = loadState();
|
|
90
|
+
state.current_task_id = taskId;
|
|
91
|
+
saveState(state);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
headBefore: getGitHead(projectRoot),
|
|
95
|
+
taskId,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Lifecycle: Validation ────────────────────────────────
|
|
100
|
+
|
|
101
|
+
function _validateSessionResult() {
|
|
102
|
+
if (!assets.exists('sessionResult')) {
|
|
103
|
+
return { valid: false, reason: 'session_result.json 不存在' };
|
|
104
|
+
}
|
|
105
|
+
const raw = assets.readJson('sessionResult', null);
|
|
106
|
+
if (!raw) {
|
|
107
|
+
return { valid: false, reason: 'JSON 解析失败', rawContent: assets.read('sessionResult') };
|
|
108
|
+
}
|
|
109
|
+
const data = raw.current && typeof raw.current === 'object' ? raw.current : raw;
|
|
110
|
+
if (!['success', 'failed'].includes(data.session_result)) {
|
|
111
|
+
return { valid: false, reason: `无效 session_result: ${data.session_result}`, data };
|
|
112
|
+
}
|
|
113
|
+
log(data.session_result === 'success' ? 'ok' : 'warn', `session_result: ${data.session_result}`);
|
|
114
|
+
return { valid: true, data };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function validate(config, headBefore, taskId) {
|
|
118
|
+
const projectRoot = assets.projectRoot;
|
|
119
|
+
log('info', '校验中...');
|
|
120
|
+
|
|
121
|
+
let srResult = _validateSessionResult();
|
|
122
|
+
const hasCommit = headBefore ? getGitHead(projectRoot) !== headBefore : false;
|
|
123
|
+
|
|
124
|
+
if (hasCommit) {
|
|
125
|
+
try {
|
|
126
|
+
const msg = execSync('git log --oneline -1', { cwd: projectRoot, encoding: 'utf8' }).trim();
|
|
127
|
+
log('ok', `检测到新提交: ${msg}`);
|
|
128
|
+
} catch { /* ignore */ }
|
|
129
|
+
} else if (headBefore) {
|
|
130
|
+
log('warn', '本次会话没有新的 git 提交');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!srResult.valid && srResult.rawContent) {
|
|
134
|
+
const srPath = assets.path('sessionResult');
|
|
135
|
+
if (srPath) {
|
|
136
|
+
const { executeRepair } = require('./repair');
|
|
137
|
+
await executeRepair(config, srPath);
|
|
138
|
+
srResult = _validateSessionResult();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let fatal = false;
|
|
143
|
+
let reason = '';
|
|
144
|
+
|
|
145
|
+
if (srResult.valid && srResult.data.session_result === 'failed') {
|
|
146
|
+
fatal = true;
|
|
147
|
+
reason = 'AI 报告任务失败';
|
|
148
|
+
} else if (!srResult.valid && !hasCommit) {
|
|
149
|
+
fatal = true;
|
|
150
|
+
reason = srResult.reason || 'session_result.json 异常';
|
|
151
|
+
} else if (!srResult.valid && hasCommit) {
|
|
152
|
+
log('warn', 'session_result.json 异常,但有新提交,降级为警告');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log(fatal ? 'error' : (hasCommit ? 'ok' : 'warn'),
|
|
156
|
+
fatal ? `校验失败: ${reason}` : `校验通过${hasCommit ? ' ✓' : ' (无新提交)'}`);
|
|
157
|
+
|
|
158
|
+
return { fatal, hasCommit, sessionData: srResult.data, reason };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Lifecycle: Rollback ──────────────────────────────────
|
|
162
|
+
|
|
163
|
+
async function rollback(headBefore, reason) {
|
|
164
|
+
if (!headBefore || headBefore === 'none') return;
|
|
165
|
+
|
|
166
|
+
const projectRoot = assets.projectRoot;
|
|
167
|
+
killServices(projectRoot);
|
|
168
|
+
if (process.platform === 'win32') await sleep(1500);
|
|
169
|
+
|
|
170
|
+
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
171
|
+
|
|
172
|
+
log('warn', `回滚到 ${headBefore} ...`);
|
|
173
|
+
|
|
174
|
+
let success = false;
|
|
175
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
176
|
+
try {
|
|
177
|
+
execSync(`git reset --hard ${headBefore}`, { cwd: projectRoot, stdio: 'pipe', env: gitEnv });
|
|
178
|
+
execSync('git clean -fd', { cwd: projectRoot, stdio: 'pipe', env: gitEnv });
|
|
179
|
+
log('ok', '回滚完成');
|
|
180
|
+
success = true;
|
|
181
|
+
break;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (attempt === 1) {
|
|
184
|
+
log('warn', `回滚首次失败,等待后重试: ${err.message}`);
|
|
185
|
+
await sleep(2000);
|
|
186
|
+
} else {
|
|
187
|
+
log('error', `回滚失败: ${err.message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
appendProgress({
|
|
193
|
+
type: 'rollback',
|
|
194
|
+
timestamp: _timestamp(),
|
|
195
|
+
reason: reason || 'harness 校验失败',
|
|
196
|
+
rollbackTo: headBefore,
|
|
197
|
+
success,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Lifecycle: Retry / Skip ──────────────────────────────
|
|
202
|
+
|
|
203
|
+
function _markTaskFailed(taskId) {
|
|
204
|
+
if (!taskId) return;
|
|
205
|
+
const data = loadTasks();
|
|
206
|
+
if (!data) return;
|
|
207
|
+
const features = getFeatures(data);
|
|
208
|
+
const task = features.find(f => f.id === taskId);
|
|
209
|
+
if (task && task.status !== 'done') {
|
|
210
|
+
task.status = 'failed';
|
|
211
|
+
saveTasks(data);
|
|
212
|
+
log('warn', `已将任务 ${taskId} 强制标记为 failed`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function _handleRetryOrSkip(session, {
|
|
217
|
+
headBefore, taskId, sessionResult, consecutiveFailures, result, reason, lastFailMsg, skipRollback,
|
|
218
|
+
}) {
|
|
219
|
+
const newFailures = consecutiveFailures + 1;
|
|
220
|
+
const exceeded = newFailures >= MAX_RETRY;
|
|
221
|
+
|
|
222
|
+
if (skipRollback) {
|
|
223
|
+
log('info', '已有提交,跳过回滚');
|
|
224
|
+
} else {
|
|
225
|
+
await rollback(headBefore, reason);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (exceeded) {
|
|
229
|
+
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
230
|
+
_markTaskFailed(taskId);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const entry = { session, timestamp: _timestamp(), result, cost: sessionResult.cost, taskId };
|
|
234
|
+
if (result === 'fatal') entry.reason = reason;
|
|
235
|
+
appendProgress(entry);
|
|
236
|
+
|
|
237
|
+
if (exceeded) return { consecutiveFailures: 0, lastFailReason: '' };
|
|
238
|
+
return { consecutiveFailures: newFailures, lastFailReason: lastFailMsg };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Lifecycle: Session Outcome ───────────────────────────
|
|
242
|
+
|
|
243
|
+
async function onSuccess(session, { taskId, sessionResult, validateResult }) {
|
|
244
|
+
incrementSession();
|
|
245
|
+
|
|
246
|
+
appendProgress({
|
|
247
|
+
session,
|
|
248
|
+
timestamp: _timestamp(),
|
|
249
|
+
result: 'success',
|
|
250
|
+
cost: sessionResult.cost,
|
|
251
|
+
taskId,
|
|
252
|
+
statusAfter: validateResult.sessionData?.status_after || null,
|
|
253
|
+
notes: validateResult.sessionData?.notes || null,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return { consecutiveFailures: 0, lastFailReason: '' };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function onFailure(session, { headBefore, taskId, sessionResult, validateResult, consecutiveFailures }) {
|
|
260
|
+
const reason = validateResult.reason || '校验失败';
|
|
261
|
+
log('error', `Session ${session} 失败 (连续: ${consecutiveFailures + 1}/${MAX_RETRY})`);
|
|
262
|
+
return _handleRetryOrSkip(session, {
|
|
263
|
+
headBefore, taskId, sessionResult, consecutiveFailures,
|
|
264
|
+
result: 'fatal', reason,
|
|
265
|
+
lastFailMsg: `上次失败: ${reason}${validateResult.hasCommit ? '' : ',代码已回滚'}`,
|
|
266
|
+
skipRollback: validateResult.hasCommit,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function onStall(session, { headBefore, taskId, sessionResult, consecutiveFailures, config }) {
|
|
271
|
+
log('warn', `Session ${session} 因停顿超时中断,尝试校验任务是否已完成...`);
|
|
272
|
+
|
|
273
|
+
const validateResult = await validate(config, headBefore, taskId);
|
|
274
|
+
|
|
275
|
+
if (!validateResult.fatal) {
|
|
276
|
+
log('ok', '停顿超时但任务已完成,按成功处理');
|
|
277
|
+
return onSuccess(session, { taskId, sessionResult, validateResult });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
log('warn', '停顿超时且校验未通过,回滚重试');
|
|
281
|
+
return _handleRetryOrSkip(session, {
|
|
282
|
+
headBefore, taskId, sessionResult, consecutiveFailures,
|
|
283
|
+
result: 'stalled', reason: '停顿超时',
|
|
284
|
+
lastFailMsg: '上次会话停顿超时,已回滚',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─── Lifecycle: Simplify Scheduling ───────────────────────
|
|
289
|
+
|
|
290
|
+
function shouldSimplify(config) {
|
|
291
|
+
const { simplifyInterval } = config;
|
|
292
|
+
if (simplifyInterval <= 0) return false;
|
|
293
|
+
const state = loadState();
|
|
294
|
+
return state.session_count % simplifyInterval === 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function needsFinalSimplify(config) {
|
|
298
|
+
const { simplifyInterval } = config;
|
|
299
|
+
if (simplifyInterval <= 0) return false;
|
|
300
|
+
const state = loadState();
|
|
301
|
+
return state.last_simplify_session < state.session_count;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function tryRunSimplify(config, msg) {
|
|
305
|
+
log('info', msg || `每 ${config.simplifyInterval} 个成功 session 运行代码审查...`);
|
|
306
|
+
try {
|
|
307
|
+
const { executeSimplify } = require('./simplify');
|
|
308
|
+
await executeSimplify(config, null, { n: config.simplifyCommits });
|
|
309
|
+
markSimplifyDone();
|
|
310
|
+
} catch (err) {
|
|
311
|
+
log('warn', `代码审查失败,跳过: ${err.message}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ─── Utilities ────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
function _timestamp() {
|
|
318
|
+
return new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Main Orchestration Loop ──────────────────────────────
|
|
322
|
+
|
|
323
|
+
async function executeRun(config, opts = {}) {
|
|
324
|
+
if (!assets.exists('tasks')) {
|
|
325
|
+
throw new Error('tasks.json 不存在,请先运行 claude-coder plan 生成任务');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const projectRoot = assets.projectRoot;
|
|
329
|
+
const dryRun = opts.dryRun || false;
|
|
330
|
+
const maxSessions = opts.max || 50;
|
|
331
|
+
const pauseEvery = opts.pause ?? 0;
|
|
332
|
+
printBanner(dryRun, config, maxSessions);
|
|
333
|
+
|
|
334
|
+
printStats();
|
|
335
|
+
|
|
336
|
+
log('info', `开始编码循环 (最多 ${maxSessions} 个会话) ...`);
|
|
337
|
+
|
|
338
|
+
let state = { consecutiveFailures: 0, lastFailReason: '' };
|
|
339
|
+
|
|
340
|
+
for (let session = 1; session <= maxSessions; session++) {
|
|
341
|
+
let taskData = loadTasks();
|
|
342
|
+
if (!taskData) {
|
|
343
|
+
const tasksPath = assets.path('tasks');
|
|
344
|
+
if (tasksPath) {
|
|
345
|
+
const { executeRepair } = require('./repair');
|
|
346
|
+
await executeRepair(config, tasksPath);
|
|
347
|
+
}
|
|
348
|
+
taskData = loadTasks();
|
|
349
|
+
if (!taskData) {
|
|
350
|
+
log('error', 'tasks.json 无法读取且修复失败,终止循环');
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (isAllDone(taskData)) {
|
|
356
|
+
if (!dryRun) {
|
|
357
|
+
if (needsFinalSimplify(config)) {
|
|
358
|
+
await tryRunSimplify(config, '所有任务完成,运行最终代码审查...');
|
|
359
|
+
}
|
|
360
|
+
tryPush(projectRoot);
|
|
361
|
+
}
|
|
362
|
+
console.error('');
|
|
363
|
+
log('ok', '所有任务已完成!');
|
|
364
|
+
printStats();
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const { headBefore, taskId } = dryRun ? { headBefore: null, taskId: null } : snapshot(projectRoot, taskData);
|
|
369
|
+
|
|
370
|
+
if (!dryRun && taskId === 'unknown') {
|
|
371
|
+
log('warn', '无可执行任务(剩余任务均已失败或依赖未满足),退出');
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const selectedTask = getFeatures(taskData).find(f => f.id === taskId);
|
|
376
|
+
if (!dryRun && selectedTask?.status === 'failed') {
|
|
377
|
+
const hasActionable = getFeatures(taskData).some(f =>
|
|
378
|
+
f.status === 'pending' || f.status === 'in_progress' || f.status === 'testing'
|
|
379
|
+
);
|
|
380
|
+
if (!hasActionable) {
|
|
381
|
+
log('warn', '所有可执行任务已完成或失败,退出(剩余 failed 任务需人工排查)');
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
printSessionHeader(session, maxSessions, taskData, taskId);
|
|
387
|
+
|
|
388
|
+
if (dryRun) {
|
|
389
|
+
printDryRun(taskData);
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const { executeCoding } = require('./coding');
|
|
394
|
+
const sessionResult = await executeCoding(config, session, {
|
|
395
|
+
projectRoot,
|
|
396
|
+
taskId,
|
|
397
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
398
|
+
maxSessions,
|
|
399
|
+
lastValidateLog: state.lastFailReason,
|
|
400
|
+
continue: true,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (sessionResult.stalled) {
|
|
404
|
+
state = await onStall(session, { headBefore, taskId, sessionResult, config, ...state });
|
|
405
|
+
if (state.consecutiveFailures === 0) {
|
|
406
|
+
if (shouldSimplify(config)) await tryRunSimplify(config);
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const validateResult = await validate(config, headBefore, taskId);
|
|
412
|
+
|
|
413
|
+
if (!validateResult.fatal) {
|
|
414
|
+
state = await onSuccess(session, { taskId, sessionResult, validateResult });
|
|
415
|
+
|
|
416
|
+
if (shouldSimplify(config)) {
|
|
417
|
+
await tryRunSimplify(config);
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
state = await onFailure(session, { headBefore, taskId, sessionResult, validateResult, ...state });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (pauseEvery > 0 && session % pauseEvery === 0) {
|
|
424
|
+
console.log('');
|
|
425
|
+
printStats();
|
|
426
|
+
if (!await promptContinue()) {
|
|
427
|
+
log('info', '手动停止');
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
killServices(projectRoot);
|
|
434
|
+
printEndBanner();
|
|
435
|
+
printStats();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
module.exports = { executeRun };
|