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