claude-coder 1.8.4 → 1.9.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 +59 -12
- package/bin/cli.js +20 -37
- package/package.json +3 -1
- package/src/commands/auth.js +87 -15
- package/src/commands/setup-modules/helpers.js +4 -3
- package/src/commands/setup-modules/mcp.js +44 -24
- package/src/commands/setup-modules/safety.js +1 -15
- package/src/commands/setup.js +8 -8
- package/src/common/assets.js +10 -1
- package/src/common/config.js +2 -2
- package/src/common/indicator.js +158 -120
- package/src/common/utils.js +60 -8
- package/src/core/coding.js +16 -38
- package/src/core/go.js +31 -77
- package/src/core/hooks.js +56 -89
- package/src/core/init.js +94 -100
- package/src/core/plan.js +85 -223
- package/src/core/prompts.js +36 -16
- package/src/core/repair.js +7 -17
- package/src/core/runner.js +306 -43
- package/src/core/scan.js +38 -34
- package/src/core/session.js +253 -39
- package/src/core/simplify.js +45 -24
- package/src/core/state.js +105 -0
- package/src/index.js +76 -0
- package/templates/codingSystem.md +2 -2
- package/templates/codingUser.md +1 -1
- package/templates/guidance.json +22 -3
- package/templates/planSystem.md +2 -2
- package/templates/scanSystem.md +3 -3
- package/templates/scanUser.md +1 -1
- package/templates/web-testing.md +17 -0
- package/types/index.d.ts +217 -0
- package/src/core/context.js +0 -117
- package/src/core/harness.js +0 -484
- package/src/core/query.js +0 -50
- package/templates/playwright.md +0 -17
package/src/core/prompts.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { loadConfig } = require('../common/config');
|
|
6
6
|
const { assets } = require('../common/assets');
|
|
7
7
|
const { loadTasks, getStats } = require('../common/tasks');
|
|
8
|
-
const { loadState, selectNextTask } = require('./
|
|
8
|
+
const { loadState, selectNextTask } = require('./state');
|
|
9
9
|
|
|
10
10
|
// --------------- System Prompt ---------------
|
|
11
11
|
|
|
@@ -36,8 +36,11 @@ function needsWebTools(task) {
|
|
|
36
36
|
// --------------- Hint Builders ---------------
|
|
37
37
|
|
|
38
38
|
function buildMcpHint(config, task) {
|
|
39
|
-
if (!config.
|
|
39
|
+
if (!config.webTestTool) return '';
|
|
40
40
|
if (!needsWebTools(task)) return '';
|
|
41
|
+
if (config.webTestTool === 'chrome-devtools') {
|
|
42
|
+
return '前端/全栈任务可用 Chrome DevTools MCP(navigate、click、type_text、screenshot 等)做端到端测试和调试。';
|
|
43
|
+
}
|
|
41
44
|
return '前端/全栈任务可用 Playwright MCP(browser_navigate、browser_snapshot、browser_click 等)做端到端测试。';
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -77,21 +80,33 @@ function buildTaskContext(projectRoot, taskId) {
|
|
|
77
80
|
|
|
78
81
|
if (!task) return '无待处理任务。';
|
|
79
82
|
|
|
80
|
-
const steps =
|
|
83
|
+
const { id, description, status, category, priority, depends_on, steps, ...rest } = task;
|
|
84
|
+
|
|
85
|
+
const stepLines = (steps || [])
|
|
81
86
|
.map((s, i) => ` ${i + 1}. ${s}`)
|
|
82
87
|
.join('\n');
|
|
83
88
|
|
|
84
|
-
const deps = (
|
|
85
|
-
? `depends_on: [${
|
|
89
|
+
const deps = (depends_on || []).length > 0
|
|
90
|
+
? `depends_on: [${depends_on.join(', ')}]`
|
|
86
91
|
: '';
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
`**${
|
|
90
|
-
`状态: ${
|
|
91
|
-
`步骤:\n${
|
|
93
|
+
const lines = [
|
|
94
|
+
`**${id}**: "${description}"`,
|
|
95
|
+
`状态: ${status}, category: ${category}, priority: ${priority || 'N/A'} ${deps}`,
|
|
96
|
+
`步骤:\n${stepLines}`,
|
|
92
97
|
`进度: ${stats.done}/${stats.total} done, ${stats.failed} failed`,
|
|
93
98
|
`项目路径: ${projectRoot}`,
|
|
94
|
-
]
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const extras = Object.entries(rest).filter(([, v]) => v != null && v !== '');
|
|
102
|
+
if (extras.length > 0) {
|
|
103
|
+
lines.push('补充信息:');
|
|
104
|
+
for (const [k, v] of extras) {
|
|
105
|
+
lines.push(` ${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return lines.join('\n');
|
|
95
110
|
} catch {
|
|
96
111
|
return '任务上下文加载失败,请读取 .claude-coder/tasks.json 自行确认。';
|
|
97
112
|
}
|
|
@@ -104,10 +119,15 @@ function buildTestEnvHint(projectRoot) {
|
|
|
104
119
|
return '';
|
|
105
120
|
}
|
|
106
121
|
|
|
107
|
-
function
|
|
108
|
-
if (!config.
|
|
122
|
+
function buildWebTestHint(config, task) {
|
|
123
|
+
if (!config.webTestTool) return '';
|
|
109
124
|
if (!needsWebTools(task)) return '';
|
|
110
|
-
|
|
125
|
+
|
|
126
|
+
if (config.webTestTool === 'chrome-devtools') {
|
|
127
|
+
return 'Chrome DevTools MCP 已启用,通过 autoConnect 连接已打开的 Chrome 浏览器,直接复用已有登录态。';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const mode = config.webTestMode;
|
|
111
131
|
switch (mode) {
|
|
112
132
|
case 'persistent':
|
|
113
133
|
return 'Playwright MCP 使用 persistent 模式,浏览器登录状态持久保存,无需额外登录操作。';
|
|
@@ -127,7 +147,7 @@ function buildMemoryHint() {
|
|
|
127
147
|
if (!sr?.session_result) return '';
|
|
128
148
|
const base = `上次会话 ${sr.session_result}(${sr.status_before || '?'} → ${sr.status_after || '?'})。`;
|
|
129
149
|
if (!sr.notes || !sr.notes.trim()) return base;
|
|
130
|
-
return `${base}遗留: ${sr.notes
|
|
150
|
+
return `${base}遗留: ${sr.notes}`;
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
function buildServiceHint(maxSessions) {
|
|
@@ -164,7 +184,7 @@ function buildCodingContext(sessionNum, opts = {}) {
|
|
|
164
184
|
envHint: buildEnvHint(consecutiveFailures, sessionNum),
|
|
165
185
|
docsHint: buildDocsHint(),
|
|
166
186
|
testEnvHint: buildTestEnvHint(projectRoot),
|
|
167
|
-
|
|
187
|
+
webTestHint: buildWebTestHint(config, task),
|
|
168
188
|
memoryHint: buildMemoryHint(),
|
|
169
189
|
serviceHint: buildServiceHint(opts.maxSessions || 50),
|
|
170
190
|
});
|
|
@@ -203,7 +223,7 @@ function buildPlanPrompt(planPath) {
|
|
|
203
223
|
|
|
204
224
|
let testRuleHint = '';
|
|
205
225
|
if (assets.exists('testRule') && assets.exists('mcpConfig')) {
|
|
206
|
-
testRuleHint = '
|
|
226
|
+
testRuleHint = '【浏览器测试规则】项目已配置浏览器测试工具(.mcp.json),' +
|
|
207
227
|
'`.claude-coder/assets/test_rule.md` 包含测试规范(Smart Snapshot、等待策略、步骤模板等)。' +
|
|
208
228
|
'前端页面 test 类任务 steps 首步加入 `【规则】阅读 .claude-coder/assets/test_rule.md`。';
|
|
209
229
|
}
|
package/src/core/repair.js
CHANGED
|
@@ -2,16 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { runSession } = require('./session');
|
|
6
|
-
const { buildQueryOptions } = require('./query');
|
|
7
5
|
const { log } = require('../common/config');
|
|
6
|
+
const { Session } = require('./session');
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
* 使用 AI 修复损坏的 JSON 文件
|
|
11
|
-
* @param {string} filePath - 文件绝对路径
|
|
12
|
-
* @param {object} [opts] - 透传给 runSession 的选项
|
|
13
|
-
*/
|
|
14
|
-
async function repairJsonFile(filePath, opts = {}) {
|
|
8
|
+
async function executeRepair(config, filePath, opts = {}) {
|
|
15
9
|
if (!fs.existsSync(filePath)) return;
|
|
16
10
|
|
|
17
11
|
const rawContent = fs.readFileSync(filePath, 'utf8');
|
|
@@ -23,17 +17,13 @@ async function repairJsonFile(filePath, opts = {}) {
|
|
|
23
17
|
const prompt = `文件 ${filePath} 的 JSON 格式已损坏,请修复并用 Write 工具写入原路径。\n\n当前损坏内容:\n${rawContent}`;
|
|
24
18
|
|
|
25
19
|
try {
|
|
26
|
-
await
|
|
27
|
-
opts,
|
|
28
|
-
sessionNum: 0,
|
|
20
|
+
await Session.run('repair', config, {
|
|
29
21
|
logFileName: `repair_${fileName.replace('.json', '')}.log`,
|
|
30
22
|
label: `repair:${fileName}`,
|
|
31
23
|
|
|
32
|
-
async execute(
|
|
33
|
-
const queryOpts = buildQueryOptions(
|
|
34
|
-
|
|
35
|
-
queryOpts.abortController = ctx.abortController;
|
|
36
|
-
await ctx.runQuery(sdk, prompt, queryOpts);
|
|
24
|
+
async execute(session) {
|
|
25
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
26
|
+
await session.runQuery(prompt, queryOpts);
|
|
37
27
|
log('ok', `AI 修复 ${fileName} 完成`);
|
|
38
28
|
return {};
|
|
39
29
|
},
|
|
@@ -43,4 +33,4 @@ async function repairJsonFile(filePath, opts = {}) {
|
|
|
43
33
|
}
|
|
44
34
|
}
|
|
45
35
|
|
|
46
|
-
module.exports = {
|
|
36
|
+
module.exports = { executeRepair };
|
package/src/core/runner.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { execSync } = require('child_process');
|
|
3
4
|
const readline = require('readline');
|
|
4
|
-
const { log
|
|
5
|
+
const { log } = require('../common/config');
|
|
5
6
|
const { assets } = require('../common/assets');
|
|
6
|
-
const { loadTasks, getFeatures, getStats, printStats } = require('../common/tasks');
|
|
7
|
-
const {
|
|
8
|
-
const {
|
|
9
|
-
const {
|
|
10
|
-
|
|
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');
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
const MAX_RETRY = RETRY.MAX_ATTEMPTS;
|
|
16
|
+
|
|
17
|
+
// ─── Display Helpers ──────────────────────────────────────
|
|
13
18
|
|
|
14
19
|
function printBanner(dryRun) {
|
|
15
20
|
console.log('');
|
|
@@ -69,42 +74,296 @@ async function promptContinue() {
|
|
|
69
74
|
});
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
// ───
|
|
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 ───────────────────────
|
|
73
321
|
|
|
74
|
-
|
|
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) {
|
|
75
337
|
log('info', msg || `每 ${config.simplifyInterval} 个成功 session 运行代码审查...`);
|
|
76
338
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
339
|
+
const { executeSimplify } = require('./simplify');
|
|
340
|
+
await executeSimplify(config, null, { n: config.simplifyCommits });
|
|
341
|
+
markSimplifyDone();
|
|
79
342
|
} catch (err) {
|
|
80
343
|
log('warn', `代码审查失败,跳过: ${err.message}`);
|
|
81
344
|
}
|
|
82
345
|
}
|
|
83
346
|
|
|
84
|
-
// ───
|
|
347
|
+
// ─── Utilities ────────────────────────────────────────────
|
|
85
348
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
349
|
+
function _timestamp() {
|
|
350
|
+
return new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─── Main Orchestration Loop ──────────────────────────────
|
|
89
354
|
|
|
90
|
-
|
|
355
|
+
async function executeRun(config, opts = {}) {
|
|
356
|
+
if (!assets.exists('tasks')) {
|
|
357
|
+
throw new Error('tasks.json 不存在,请先运行 claude-coder plan 生成任务');
|
|
358
|
+
}
|
|
91
359
|
|
|
360
|
+
const projectRoot = assets.projectRoot;
|
|
92
361
|
const dryRun = opts.dryRun || false;
|
|
93
362
|
const maxSessions = opts.max || 50;
|
|
94
363
|
const pauseEvery = opts.pause ?? 0;
|
|
95
364
|
|
|
96
365
|
printBanner(dryRun);
|
|
97
366
|
|
|
98
|
-
if (config.provider !== 'claude' && config.baseUrl) {
|
|
99
|
-
log('ok', `模型配置已加载: ${config.provider}${config.model ? ` (${config.model})` : ''}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const prereq = harness.checkPrerequisites();
|
|
103
|
-
if (!prereq.ok) {
|
|
104
|
-
log('error', prereq.msg);
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
367
|
printStats();
|
|
109
368
|
|
|
110
369
|
log('info', `开始编码循环 (最多 ${maxSessions} 个会话) ...`);
|
|
@@ -118,7 +377,10 @@ async function run(opts = {}) {
|
|
|
118
377
|
let taskData = loadTasks();
|
|
119
378
|
if (!taskData) {
|
|
120
379
|
const tasksPath = assets.path('tasks');
|
|
121
|
-
if (tasksPath)
|
|
380
|
+
if (tasksPath) {
|
|
381
|
+
const { executeRepair } = require('./repair');
|
|
382
|
+
await executeRepair(config, tasksPath);
|
|
383
|
+
}
|
|
122
384
|
taskData = loadTasks();
|
|
123
385
|
if (!taskData) {
|
|
124
386
|
log('error', 'tasks.json 无法读取且修复失败,终止循环');
|
|
@@ -126,12 +388,12 @@ async function run(opts = {}) {
|
|
|
126
388
|
}
|
|
127
389
|
}
|
|
128
390
|
|
|
129
|
-
if (
|
|
391
|
+
if (isAllDone(taskData)) {
|
|
130
392
|
if (!dryRun) {
|
|
131
|
-
if (
|
|
132
|
-
await tryRunSimplify(
|
|
393
|
+
if (needsFinalSimplify(config)) {
|
|
394
|
+
await tryRunSimplify(config, '所有任务完成,运行最终代码审查...');
|
|
133
395
|
}
|
|
134
|
-
|
|
396
|
+
tryPush(projectRoot);
|
|
135
397
|
}
|
|
136
398
|
console.log('');
|
|
137
399
|
log('ok', '所有任务已完成!');
|
|
@@ -146,10 +408,11 @@ async function run(opts = {}) {
|
|
|
146
408
|
break;
|
|
147
409
|
}
|
|
148
410
|
|
|
149
|
-
const { headBefore, taskId } =
|
|
411
|
+
const { headBefore, taskId } = snapshot(projectRoot, taskData);
|
|
150
412
|
|
|
151
|
-
const
|
|
152
|
-
|
|
413
|
+
const { executeCoding } = require('./coding');
|
|
414
|
+
const sessionResult = await executeCoding(config, session, {
|
|
415
|
+
projectRoot,
|
|
153
416
|
taskId,
|
|
154
417
|
consecutiveFailures: state.consecutiveFailures,
|
|
155
418
|
maxSessions,
|
|
@@ -157,24 +420,24 @@ async function run(opts = {}) {
|
|
|
157
420
|
});
|
|
158
421
|
|
|
159
422
|
if (sessionResult.stalled) {
|
|
160
|
-
state = await
|
|
423
|
+
state = await onStall(session, { headBefore, taskId, sessionResult, ...state });
|
|
161
424
|
continue;
|
|
162
425
|
}
|
|
163
426
|
|
|
164
427
|
log('info', '开始 harness 校验 ...');
|
|
165
|
-
const validateResult = await
|
|
428
|
+
const validateResult = await validate(config, headBefore, taskId);
|
|
166
429
|
|
|
167
430
|
if (!validateResult.fatal) {
|
|
168
431
|
const level = validateResult.hasWarnings ? 'warn' : 'ok';
|
|
169
432
|
log(level, `Session ${session} 校验通过${validateResult.hasWarnings ? ' (有警告)' : ''}`);
|
|
170
|
-
state = await
|
|
433
|
+
state = await onSuccess(session, { taskId, sessionResult, validateResult });
|
|
171
434
|
|
|
172
|
-
if (
|
|
173
|
-
await tryRunSimplify(
|
|
435
|
+
if (shouldSimplify(config)) {
|
|
436
|
+
await tryRunSimplify(config);
|
|
174
437
|
}
|
|
175
|
-
|
|
438
|
+
tryPush(projectRoot);
|
|
176
439
|
} else {
|
|
177
|
-
state = await
|
|
440
|
+
state = await onFailure(session, { headBefore, taskId, sessionResult, validateResult, ...state });
|
|
178
441
|
}
|
|
179
442
|
|
|
180
443
|
if (pauseEvery > 0 && session % pauseEvery === 0) {
|
|
@@ -187,9 +450,9 @@ async function run(opts = {}) {
|
|
|
187
450
|
}
|
|
188
451
|
}
|
|
189
452
|
|
|
190
|
-
|
|
453
|
+
killServices(projectRoot);
|
|
191
454
|
printEndBanner();
|
|
192
455
|
printStats();
|
|
193
456
|
}
|
|
194
457
|
|
|
195
|
-
module.exports = {
|
|
458
|
+
module.exports = { executeRun };
|