claude-coder 1.8.2 → 1.8.3
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 +167 -167
- package/bin/cli.js +172 -172
- package/package.json +52 -52
- package/src/commands/auth.js +290 -240
- package/src/commands/setup-modules/helpers.js +99 -99
- package/src/commands/setup-modules/index.js +25 -25
- package/src/commands/setup-modules/mcp.js +94 -94
- package/src/commands/setup-modules/provider.js +260 -260
- package/src/commands/setup-modules/safety.js +61 -61
- package/src/commands/setup-modules/simplify.js +52 -52
- package/src/commands/setup.js +172 -172
- package/src/common/assets.js +236 -236
- package/src/common/config.js +125 -125
- package/src/common/constants.js +55 -55
- package/src/common/indicator.js +222 -222
- package/src/common/interaction.js +170 -170
- package/src/common/logging.js +77 -77
- package/src/common/sdk.js +50 -50
- package/src/common/tasks.js +88 -88
- package/src/common/utils.js +161 -161
- package/src/core/coding.js +55 -55
- package/src/core/context.js +117 -117
- package/src/core/go.js +310 -310
- package/src/core/harness.js +484 -484
- package/src/core/hooks.js +533 -533
- package/src/core/init.js +171 -171
- package/src/core/plan.js +325 -325
- package/src/core/prompts.js +227 -227
- package/src/core/query.js +49 -49
- package/src/core/repair.js +46 -46
- package/src/core/runner.js +195 -195
- package/src/core/scan.js +89 -89
- package/src/core/session.js +56 -56
- package/src/core/simplify.js +53 -52
- package/templates/bash-process.md +12 -12
- package/templates/codingSystem.md +65 -65
- package/templates/codingUser.md +17 -17
- package/templates/coreProtocol.md +29 -29
- package/templates/goSystem.md +130 -130
- package/templates/guidance.json +52 -52
- package/templates/planSystem.md +78 -78
- package/templates/planUser.md +8 -8
- package/templates/playwright.md +16 -16
- package/templates/requirements.example.md +57 -57
- package/templates/scanSystem.md +120 -120
- package/templates/scanUser.md +10 -10
- package/templates/test_rule.md +194 -194
package/src/core/harness.js
CHANGED
|
@@ -1,484 +1,484 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { execSync } = require('child_process');
|
|
4
|
-
const { log } = require('../common/config');
|
|
5
|
-
const { assets } = require('../common/assets');
|
|
6
|
-
const { getGitHead, isGitRepo, sleep, ensureGitignore } = require('../common/utils');
|
|
7
|
-
const { RETRY, TASK_STATUSES } = require('../common/constants');
|
|
8
|
-
const { loadTasks, saveTasks, getFeatures } = require('../common/tasks');
|
|
9
|
-
|
|
10
|
-
const MAX_RETRY = RETRY.MAX_ATTEMPTS;
|
|
11
|
-
|
|
12
|
-
// ─── Harness State (harness_state.json) ───────────────────────
|
|
13
|
-
|
|
14
|
-
const DEFAULT_STATE = Object.freeze({
|
|
15
|
-
version: 1,
|
|
16
|
-
next_task_id: 1,
|
|
17
|
-
next_priority: 1,
|
|
18
|
-
session_count: 0,
|
|
19
|
-
last_simplify_session: 0,
|
|
20
|
-
current_task_id: null,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
function loadState() {
|
|
24
|
-
return assets.readJson('harnessState', { ...DEFAULT_STATE });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function saveState(data) {
|
|
28
|
-
assets.writeJson('harnessState', data);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function extractIdNum(id) {
|
|
32
|
-
const m = String(id).match(/(\d+)$/);
|
|
33
|
-
return m ? parseInt(m[1], 10) : 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* plan session 结束后调用:扫描 tasks.json,同步 next_task_id 和 next_priority
|
|
38
|
-
*/
|
|
39
|
-
function syncAfterPlan() {
|
|
40
|
-
const state = loadState();
|
|
41
|
-
const tasks = assets.readJson('tasks', null);
|
|
42
|
-
if (!tasks || !tasks.features) return state;
|
|
43
|
-
|
|
44
|
-
const features = tasks.features;
|
|
45
|
-
state.next_task_id = features.reduce((max, f) => Math.max(max, extractIdNum(f.id)), 0) + 1;
|
|
46
|
-
state.next_priority = features.reduce((max, f) => Math.max(max, f.priority || 0), 0) + 1;
|
|
47
|
-
saveState(state);
|
|
48
|
-
return state;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ─── Task Scheduling ──────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 任务调度算法:failed(优先重试) > pending(依赖就绪) > in_progress
|
|
55
|
-
*/
|
|
56
|
-
function selectNextTask(taskData) {
|
|
57
|
-
const features = getFeatures(taskData);
|
|
58
|
-
|
|
59
|
-
const failed = features.filter(f => f.status === 'failed')
|
|
60
|
-
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
61
|
-
if (failed.length > 0) return failed[0];
|
|
62
|
-
|
|
63
|
-
const pending = features.filter(f => f.status === 'pending')
|
|
64
|
-
.filter(f => {
|
|
65
|
-
const deps = f.depends_on || [];
|
|
66
|
-
return deps.every(depId => {
|
|
67
|
-
const dep = features.find(x => x.id === depId);
|
|
68
|
-
return dep && dep.status === 'done';
|
|
69
|
-
});
|
|
70
|
-
})
|
|
71
|
-
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
72
|
-
if (pending.length > 0) return pending[0];
|
|
73
|
-
|
|
74
|
-
const inProgress = features.filter(f => f.status === 'in_progress')
|
|
75
|
-
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
76
|
-
return inProgress[0] || null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ─── Validation ───────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
function _validateSessionResult() {
|
|
82
|
-
if (!assets.exists('sessionResult')) {
|
|
83
|
-
log('error', 'Agent 未生成 session_result.json');
|
|
84
|
-
return { valid: false, reason: 'session_result.json 不存在' };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const raw = assets.readJson('sessionResult', null);
|
|
88
|
-
if (raw === null) {
|
|
89
|
-
log('warn', 'session_result.json 解析失败');
|
|
90
|
-
return { valid: false, reason: 'JSON 解析失败', rawContent: assets.read('sessionResult') };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const data = raw.current && typeof raw.current === 'object' ? raw.current : raw;
|
|
94
|
-
|
|
95
|
-
const required = ['session_result', 'status_after'];
|
|
96
|
-
const missing = required.filter(k => !(k in data));
|
|
97
|
-
if (missing.length > 0) {
|
|
98
|
-
log('warn', `session_result.json 缺少字段: ${missing.join(', ')}`);
|
|
99
|
-
return { valid: false, reason: `缺少字段: ${missing.join(', ')}`, data };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!['success', 'failed'].includes(data.session_result)) {
|
|
103
|
-
return { valid: false, reason: `无效 session_result: ${data.session_result}`, data };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!TASK_STATUSES.includes(data.status_after)) {
|
|
107
|
-
return { valid: false, reason: `无效 status_after: ${data.status_after}`, data };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const level = data.session_result === 'success' ? 'ok' : 'warn';
|
|
111
|
-
log(level, `session_result.json 合法 (${data.session_result})`);
|
|
112
|
-
return { valid: true, data };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function _checkGitProgress(headBefore) {
|
|
116
|
-
if (!headBefore) {
|
|
117
|
-
log('info', '未提供 head_before,跳过 git 检查');
|
|
118
|
-
return { hasCommit: false, warning: false };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const projectRoot = assets.projectRoot;
|
|
122
|
-
const headAfter = getGitHead(projectRoot);
|
|
123
|
-
|
|
124
|
-
if (headBefore === headAfter) {
|
|
125
|
-
log('warn', '本次会话没有新的 git 提交');
|
|
126
|
-
return { hasCommit: false, warning: true };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const msg = execSync('git log --oneline -1', { cwd: projectRoot, encoding: 'utf8' }).trim();
|
|
131
|
-
log('ok', `检测到新提交: ${msg}`);
|
|
132
|
-
} catch { /* ignore */ }
|
|
133
|
-
|
|
134
|
-
return { hasCommit: true, warning: false };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function _inferFromTasks(taskId) {
|
|
138
|
-
if (!taskId) return null;
|
|
139
|
-
const data = loadTasks();
|
|
140
|
-
if (!data) return null;
|
|
141
|
-
const task = getFeatures(data).find(f => f.id === taskId);
|
|
142
|
-
return task ? task.status : null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ─── Harness Lifecycle Class ──────────────────────────────────
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Harness 生命周期管理
|
|
149
|
-
*
|
|
150
|
-
* 职责:环境准备、状态持久化、任务调度、校验、回滚、清理
|
|
151
|
-
*
|
|
152
|
-
* 方法 阶段 说明
|
|
153
|
-
* ensureEnvironment() 循环前 目录创建、gitignore、git 初始化
|
|
154
|
-
* checkPrerequisites() 循环前 检查 profile/tasks 是否存在
|
|
155
|
-
* snapshot(taskData) 会话前 快照 HEAD、选取并持久化当前任务
|
|
156
|
-
* isAllDone(taskData) 会话前 判断是否全部完成
|
|
157
|
-
* validate(...) 会话后 校验 session_result + git 进度(含 AI 修复)
|
|
158
|
-
* onSuccess(...) 会话后 递增计数、记录进度
|
|
159
|
-
* onFailure(...) 会话后 回滚、超限跳过、记录进度
|
|
160
|
-
* onStall(...) 会话后 回滚、超限跳过、记录进度
|
|
161
|
-
* shouldSimplify() 会话后 判断是否需要周期性 simplify
|
|
162
|
-
* needsFinalSimplify() 全部完成 判断是否需要最终 simplify
|
|
163
|
-
* afterSimplify(msg) simplify后 标记状态 + commit
|
|
164
|
-
* tryPush() 推送 推送代码到远程
|
|
165
|
-
* cleanup() 循环后 杀停服务进程
|
|
166
|
-
*/
|
|
167
|
-
class Harness {
|
|
168
|
-
constructor(config) {
|
|
169
|
-
this.config = config;
|
|
170
|
-
this.projectRoot = assets.projectRoot;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ─── Phase: Pre-loop Setup ──────────────────────────────────
|
|
174
|
-
|
|
175
|
-
ensureEnvironment() {
|
|
176
|
-
assets.ensureDirs();
|
|
177
|
-
ensureGitignore(this.projectRoot);
|
|
178
|
-
|
|
179
|
-
if (!isGitRepo(this.projectRoot)) {
|
|
180
|
-
log('info', '初始化 git 仓库...');
|
|
181
|
-
execSync('git init', { cwd: this.projectRoot, stdio: 'inherit' });
|
|
182
|
-
execSync('git add -A && git commit -m "init: 项目初始化" --allow-empty', {
|
|
183
|
-
cwd: this.projectRoot,
|
|
184
|
-
stdio: 'inherit',
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
checkPrerequisites() {
|
|
190
|
-
if (!assets.exists('profile')) {
|
|
191
|
-
return { ok: false, msg: 'profile 不存在,请先运行 claude-coder init 初始化项目' };
|
|
192
|
-
}
|
|
193
|
-
if (!assets.exists('tasks')) {
|
|
194
|
-
return { ok: false, msg: 'tasks.json 不存在,请先运行 claude-coder plan 生成任务' };
|
|
195
|
-
}
|
|
196
|
-
return { ok: true };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ─── Phase: Per-session Snapshot ────────────────────────────
|
|
200
|
-
|
|
201
|
-
snapshot(taskData) {
|
|
202
|
-
const nextTask = selectNextTask(taskData);
|
|
203
|
-
const taskId = nextTask?.id || 'unknown';
|
|
204
|
-
|
|
205
|
-
const state = loadState();
|
|
206
|
-
state.current_task_id = taskId;
|
|
207
|
-
saveState(state);
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
headBefore: getGitHead(this.projectRoot),
|
|
211
|
-
taskId,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
isAllDone(taskData) {
|
|
216
|
-
const features = getFeatures(taskData);
|
|
217
|
-
return features.length > 0 && features.every(f => f.status === 'done');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ─── Phase: Post-session Lifecycle ──────────────────────────
|
|
221
|
-
|
|
222
|
-
async onSuccess(session, { taskId, sessionResult, validateResult }) {
|
|
223
|
-
this._incrementSession();
|
|
224
|
-
|
|
225
|
-
this._appendProgress({
|
|
226
|
-
session,
|
|
227
|
-
timestamp: this._timestamp(),
|
|
228
|
-
result: 'success',
|
|
229
|
-
cost: sessionResult.cost,
|
|
230
|
-
taskId,
|
|
231
|
-
statusAfter: validateResult.sessionData?.status_after || null,
|
|
232
|
-
notes: validateResult.sessionData?.notes || null,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
return { consecutiveFailures: 0, lastFailReason: '' };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async onFailure(session, { headBefore, taskId, sessionResult, validateResult, consecutiveFailures }) {
|
|
239
|
-
const reason = validateResult.reason || '校验失败';
|
|
240
|
-
log('error', `Session ${session} 校验失败 (连续失败: ${consecutiveFailures + 1}/${MAX_RETRY})`);
|
|
241
|
-
return this._handleRetryOrSkip(session, {
|
|
242
|
-
headBefore, taskId, sessionResult, consecutiveFailures,
|
|
243
|
-
result: 'fatal', reason,
|
|
244
|
-
lastFailMsg: `上次校验失败: ${reason},代码已回滚`,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async onStall(session, { headBefore, taskId, sessionResult, consecutiveFailures }) {
|
|
249
|
-
log('warn', `Session ${session} 因停顿超时中断,跳过校验直接重试`);
|
|
250
|
-
return this._handleRetryOrSkip(session, {
|
|
251
|
-
headBefore, taskId, sessionResult, consecutiveFailures,
|
|
252
|
-
result: 'stalled', reason: '停顿超时',
|
|
253
|
-
lastFailMsg: '上次会话停顿超时,已回滚',
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
cleanup() {
|
|
258
|
-
this._killServicesByProfile();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// ─── Phase: Post-session Validation ─────────────────────────
|
|
262
|
-
|
|
263
|
-
async validate(headBefore, taskId) {
|
|
264
|
-
log('info', '========== 开始校验 ==========');
|
|
265
|
-
|
|
266
|
-
let srResult = _validateSessionResult();
|
|
267
|
-
const gitResult = _checkGitProgress(headBefore);
|
|
268
|
-
|
|
269
|
-
if (!srResult.valid && srResult.rawContent) {
|
|
270
|
-
const srPath = assets.path('sessionResult');
|
|
271
|
-
if (srPath) {
|
|
272
|
-
const { repairJsonFile } = require('./repair');
|
|
273
|
-
await repairJsonFile(srPath);
|
|
274
|
-
srResult = _validateSessionResult();
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
let fatal = false;
|
|
279
|
-
let hasWarnings = false;
|
|
280
|
-
|
|
281
|
-
if (srResult.valid) {
|
|
282
|
-
hasWarnings = gitResult.warning;
|
|
283
|
-
} else {
|
|
284
|
-
if (gitResult.hasCommit) {
|
|
285
|
-
const taskStatus = _inferFromTasks(taskId);
|
|
286
|
-
if (taskStatus === 'done' || taskStatus === 'testing') {
|
|
287
|
-
log('warn', `session_result.json 异常,但 tasks.json 显示 ${taskId} 已 ${taskStatus},且有新提交,降级为警告`);
|
|
288
|
-
} else {
|
|
289
|
-
log('warn', 'session_result.json 异常,但有新提交,降级为警告(不回滚代码)');
|
|
290
|
-
}
|
|
291
|
-
hasWarnings = true;
|
|
292
|
-
} else {
|
|
293
|
-
log('error', '无新提交且 session_result.json 异常,视为致命');
|
|
294
|
-
fatal = true;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (fatal) {
|
|
299
|
-
log('error', '========== 校验失败 (致命) ==========');
|
|
300
|
-
} else if (hasWarnings) {
|
|
301
|
-
log('warn', '========== 校验通过 (有警告) ==========');
|
|
302
|
-
} else {
|
|
303
|
-
log('ok', '========== 校验全部通过 ==========');
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const reason = fatal ? (srResult.reason || '无新提交且 session_result.json 异常') : '';
|
|
307
|
-
return { fatal, hasWarnings, sessionData: srResult.data, reason };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ─── Internal: Retry / Skip ─────────────────────────────────
|
|
311
|
-
|
|
312
|
-
async _handleRetryOrSkip(session, { headBefore, taskId, sessionResult, consecutiveFailures, result, reason, lastFailMsg }) {
|
|
313
|
-
const newFailures = consecutiveFailures + 1;
|
|
314
|
-
const exceeded = newFailures >= MAX_RETRY;
|
|
315
|
-
|
|
316
|
-
await this._rollback(headBefore, reason);
|
|
317
|
-
|
|
318
|
-
if (exceeded) {
|
|
319
|
-
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
320
|
-
this._markTaskFailed(taskId);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const entry = { session, timestamp: this._timestamp(), result, cost: sessionResult.cost, taskId };
|
|
324
|
-
if (result === 'fatal') entry.reason = reason;
|
|
325
|
-
this._appendProgress(entry);
|
|
326
|
-
|
|
327
|
-
if (exceeded) return { consecutiveFailures: 0, lastFailReason: '' };
|
|
328
|
-
return { consecutiveFailures: newFailures, lastFailReason: lastFailMsg };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// ─── Internal: Utilities ────────────────────────────────────
|
|
332
|
-
|
|
333
|
-
_timestamp() {
|
|
334
|
-
return new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ─── Internal: State Operations ─────────────────────────────
|
|
338
|
-
|
|
339
|
-
_incrementSession() {
|
|
340
|
-
const state = loadState();
|
|
341
|
-
state.session_count++;
|
|
342
|
-
saveState(state);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
_markSimplifyDone() {
|
|
346
|
-
const state = loadState();
|
|
347
|
-
state.last_simplify_session = state.session_count;
|
|
348
|
-
saveState(state);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ─── Internal: Git Operations ───────────────────────────────
|
|
352
|
-
|
|
353
|
-
async _rollback(headBefore, reason) {
|
|
354
|
-
if (!headBefore || headBefore === 'none') return;
|
|
355
|
-
|
|
356
|
-
this._killServicesByProfile();
|
|
357
|
-
if (process.platform === 'win32') await sleep(1500);
|
|
358
|
-
|
|
359
|
-
const cwd = this.projectRoot;
|
|
360
|
-
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
361
|
-
|
|
362
|
-
log('warn', `回滚到 ${headBefore} ...`);
|
|
363
|
-
|
|
364
|
-
let success = false;
|
|
365
|
-
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
366
|
-
try {
|
|
367
|
-
execSync(`git reset --hard ${headBefore}`, { cwd, stdio: 'pipe', env: gitEnv });
|
|
368
|
-
execSync('git clean -fd', { cwd, stdio: 'pipe', env: gitEnv });
|
|
369
|
-
log('ok', '回滚完成');
|
|
370
|
-
success = true;
|
|
371
|
-
break;
|
|
372
|
-
} catch (err) {
|
|
373
|
-
if (attempt === 1) {
|
|
374
|
-
log('warn', `回滚首次失败,等待后重试: ${err.message}`);
|
|
375
|
-
await sleep(2000);
|
|
376
|
-
} else {
|
|
377
|
-
log('error', `回滚失败: ${err.message}`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
this._appendProgress({
|
|
383
|
-
type: 'rollback',
|
|
384
|
-
timestamp: this._timestamp(),
|
|
385
|
-
reason: reason || 'harness 校验失败',
|
|
386
|
-
rollbackTo: headBefore,
|
|
387
|
-
success,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
tryPush() {
|
|
392
|
-
try {
|
|
393
|
-
const cwd = this.projectRoot;
|
|
394
|
-
const remotes = execSync('git remote', { cwd, encoding: 'utf8' }).trim();
|
|
395
|
-
if (!remotes) return;
|
|
396
|
-
log('info', '正在推送代码...');
|
|
397
|
-
execSync('git push', { cwd, stdio: 'inherit' });
|
|
398
|
-
log('ok', '推送成功');
|
|
399
|
-
} catch {
|
|
400
|
-
log('warn', '推送失败 (请检查网络或权限),继续执行...');
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
_commitIfDirty(message) {
|
|
405
|
-
try {
|
|
406
|
-
execSync('git diff --quiet HEAD', { cwd: this.projectRoot, stdio: 'pipe' });
|
|
407
|
-
} catch {
|
|
408
|
-
execSync(`git add -A && git commit -m "${message}"`, { cwd: this.projectRoot, stdio: 'pipe' });
|
|
409
|
-
log('ok', '代码优化已提交');
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// ─── Simplify Scheduling (called by runner) ────────────────
|
|
414
|
-
|
|
415
|
-
shouldSimplify() {
|
|
416
|
-
const { simplifyInterval } = this.config;
|
|
417
|
-
if (simplifyInterval <= 0) return false;
|
|
418
|
-
const state = loadState();
|
|
419
|
-
return state.session_count % simplifyInterval === 0;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
needsFinalSimplify() {
|
|
423
|
-
const { simplifyInterval } = this.config;
|
|
424
|
-
if (simplifyInterval <= 0) return false;
|
|
425
|
-
const state = loadState();
|
|
426
|
-
return state.last_simplify_session < state.session_count;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
afterSimplify(commitMsg = 'style: simplify optimization') {
|
|
430
|
-
this._markSimplifyDone();
|
|
431
|
-
this._commitIfDirty(commitMsg);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// ─── Internal: Task & Progress ──────────────────────────────
|
|
435
|
-
|
|
436
|
-
_markTaskFailed(taskId) {
|
|
437
|
-
if (!taskId) return;
|
|
438
|
-
const data = loadTasks();
|
|
439
|
-
if (!data) return;
|
|
440
|
-
const features = getFeatures(data);
|
|
441
|
-
const task = features.find(f => f.id === taskId);
|
|
442
|
-
if (task && task.status !== 'done') {
|
|
443
|
-
task.status = 'failed';
|
|
444
|
-
saveTasks(data);
|
|
445
|
-
log('warn', `已将任务 ${taskId} 强制标记为 failed`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
_appendProgress(entry) {
|
|
450
|
-
let progress = assets.readJson('progress', { sessions: [] });
|
|
451
|
-
if (!Array.isArray(progress.sessions)) progress.sessions = [];
|
|
452
|
-
progress.sessions.push(entry);
|
|
453
|
-
assets.writeJson('progress', progress);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// ─── Internal: Process Management ──────────────────────────
|
|
457
|
-
|
|
458
|
-
_killServicesByProfile() {
|
|
459
|
-
const profile = assets.readJson('profile', null);
|
|
460
|
-
if (!profile) return;
|
|
461
|
-
const ports = (profile.services || []).map(s => s.port).filter(Boolean);
|
|
462
|
-
if (ports.length === 0) return;
|
|
463
|
-
|
|
464
|
-
for (const port of ports) {
|
|
465
|
-
try {
|
|
466
|
-
if (process.platform === 'win32') {
|
|
467
|
-
const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
468
|
-
const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
|
|
469
|
-
for (const pid of pids) { try { execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'pipe' }); } catch { /* ignore */ } }
|
|
470
|
-
} else {
|
|
471
|
-
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
|
|
472
|
-
}
|
|
473
|
-
} catch { /* no process on port */ }
|
|
474
|
-
}
|
|
475
|
-
log('info', `已停止端口 ${ports.join(', ')} 上的服务`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
module.exports = {
|
|
480
|
-
Harness,
|
|
481
|
-
loadState,
|
|
482
|
-
syncAfterPlan,
|
|
483
|
-
selectNextTask,
|
|
484
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { log } = require('../common/config');
|
|
5
|
+
const { assets } = require('../common/assets');
|
|
6
|
+
const { getGitHead, isGitRepo, sleep, ensureGitignore } = require('../common/utils');
|
|
7
|
+
const { RETRY, TASK_STATUSES } = require('../common/constants');
|
|
8
|
+
const { loadTasks, saveTasks, getFeatures } = require('../common/tasks');
|
|
9
|
+
|
|
10
|
+
const MAX_RETRY = RETRY.MAX_ATTEMPTS;
|
|
11
|
+
|
|
12
|
+
// ─── Harness State (harness_state.json) ───────────────────────
|
|
13
|
+
|
|
14
|
+
const DEFAULT_STATE = Object.freeze({
|
|
15
|
+
version: 1,
|
|
16
|
+
next_task_id: 1,
|
|
17
|
+
next_priority: 1,
|
|
18
|
+
session_count: 0,
|
|
19
|
+
last_simplify_session: 0,
|
|
20
|
+
current_task_id: null,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function loadState() {
|
|
24
|
+
return assets.readJson('harnessState', { ...DEFAULT_STATE });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function saveState(data) {
|
|
28
|
+
assets.writeJson('harnessState', data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractIdNum(id) {
|
|
32
|
+
const m = String(id).match(/(\d+)$/);
|
|
33
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* plan session 结束后调用:扫描 tasks.json,同步 next_task_id 和 next_priority
|
|
38
|
+
*/
|
|
39
|
+
function syncAfterPlan() {
|
|
40
|
+
const state = loadState();
|
|
41
|
+
const tasks = assets.readJson('tasks', null);
|
|
42
|
+
if (!tasks || !tasks.features) return state;
|
|
43
|
+
|
|
44
|
+
const features = tasks.features;
|
|
45
|
+
state.next_task_id = features.reduce((max, f) => Math.max(max, extractIdNum(f.id)), 0) + 1;
|
|
46
|
+
state.next_priority = features.reduce((max, f) => Math.max(max, f.priority || 0), 0) + 1;
|
|
47
|
+
saveState(state);
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Task Scheduling ──────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 任务调度算法:failed(优先重试) > pending(依赖就绪) > in_progress
|
|
55
|
+
*/
|
|
56
|
+
function selectNextTask(taskData) {
|
|
57
|
+
const features = getFeatures(taskData);
|
|
58
|
+
|
|
59
|
+
const failed = features.filter(f => f.status === 'failed')
|
|
60
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
61
|
+
if (failed.length > 0) return failed[0];
|
|
62
|
+
|
|
63
|
+
const pending = features.filter(f => f.status === 'pending')
|
|
64
|
+
.filter(f => {
|
|
65
|
+
const deps = f.depends_on || [];
|
|
66
|
+
return deps.every(depId => {
|
|
67
|
+
const dep = features.find(x => x.id === depId);
|
|
68
|
+
return dep && dep.status === 'done';
|
|
69
|
+
});
|
|
70
|
+
})
|
|
71
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
72
|
+
if (pending.length > 0) return pending[0];
|
|
73
|
+
|
|
74
|
+
const inProgress = features.filter(f => f.status === 'in_progress')
|
|
75
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
76
|
+
return inProgress[0] || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Validation ───────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function _validateSessionResult() {
|
|
82
|
+
if (!assets.exists('sessionResult')) {
|
|
83
|
+
log('error', 'Agent 未生成 session_result.json');
|
|
84
|
+
return { valid: false, reason: 'session_result.json 不存在' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const raw = assets.readJson('sessionResult', null);
|
|
88
|
+
if (raw === null) {
|
|
89
|
+
log('warn', 'session_result.json 解析失败');
|
|
90
|
+
return { valid: false, reason: 'JSON 解析失败', rawContent: assets.read('sessionResult') };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = raw.current && typeof raw.current === 'object' ? raw.current : raw;
|
|
94
|
+
|
|
95
|
+
const required = ['session_result', 'status_after'];
|
|
96
|
+
const missing = required.filter(k => !(k in data));
|
|
97
|
+
if (missing.length > 0) {
|
|
98
|
+
log('warn', `session_result.json 缺少字段: ${missing.join(', ')}`);
|
|
99
|
+
return { valid: false, reason: `缺少字段: ${missing.join(', ')}`, data };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!['success', 'failed'].includes(data.session_result)) {
|
|
103
|
+
return { valid: false, reason: `无效 session_result: ${data.session_result}`, data };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!TASK_STATUSES.includes(data.status_after)) {
|
|
107
|
+
return { valid: false, reason: `无效 status_after: ${data.status_after}`, data };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const level = data.session_result === 'success' ? 'ok' : 'warn';
|
|
111
|
+
log(level, `session_result.json 合法 (${data.session_result})`);
|
|
112
|
+
return { valid: true, data };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _checkGitProgress(headBefore) {
|
|
116
|
+
if (!headBefore) {
|
|
117
|
+
log('info', '未提供 head_before,跳过 git 检查');
|
|
118
|
+
return { hasCommit: false, warning: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const projectRoot = assets.projectRoot;
|
|
122
|
+
const headAfter = getGitHead(projectRoot);
|
|
123
|
+
|
|
124
|
+
if (headBefore === headAfter) {
|
|
125
|
+
log('warn', '本次会话没有新的 git 提交');
|
|
126
|
+
return { hasCommit: false, warning: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const msg = execSync('git log --oneline -1', { cwd: projectRoot, encoding: 'utf8' }).trim();
|
|
131
|
+
log('ok', `检测到新提交: ${msg}`);
|
|
132
|
+
} catch { /* ignore */ }
|
|
133
|
+
|
|
134
|
+
return { hasCommit: true, warning: false };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _inferFromTasks(taskId) {
|
|
138
|
+
if (!taskId) return null;
|
|
139
|
+
const data = loadTasks();
|
|
140
|
+
if (!data) return null;
|
|
141
|
+
const task = getFeatures(data).find(f => f.id === taskId);
|
|
142
|
+
return task ? task.status : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Harness Lifecycle Class ──────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Harness 生命周期管理
|
|
149
|
+
*
|
|
150
|
+
* 职责:环境准备、状态持久化、任务调度、校验、回滚、清理
|
|
151
|
+
*
|
|
152
|
+
* 方法 阶段 说明
|
|
153
|
+
* ensureEnvironment() 循环前 目录创建、gitignore、git 初始化
|
|
154
|
+
* checkPrerequisites() 循环前 检查 profile/tasks 是否存在
|
|
155
|
+
* snapshot(taskData) 会话前 快照 HEAD、选取并持久化当前任务
|
|
156
|
+
* isAllDone(taskData) 会话前 判断是否全部完成
|
|
157
|
+
* validate(...) 会话后 校验 session_result + git 进度(含 AI 修复)
|
|
158
|
+
* onSuccess(...) 会话后 递增计数、记录进度
|
|
159
|
+
* onFailure(...) 会话后 回滚、超限跳过、记录进度
|
|
160
|
+
* onStall(...) 会话后 回滚、超限跳过、记录进度
|
|
161
|
+
* shouldSimplify() 会话后 判断是否需要周期性 simplify
|
|
162
|
+
* needsFinalSimplify() 全部完成 判断是否需要最终 simplify
|
|
163
|
+
* afterSimplify(msg) simplify后 标记状态 + commit
|
|
164
|
+
* tryPush() 推送 推送代码到远程
|
|
165
|
+
* cleanup() 循环后 杀停服务进程
|
|
166
|
+
*/
|
|
167
|
+
class Harness {
|
|
168
|
+
constructor(config) {
|
|
169
|
+
this.config = config;
|
|
170
|
+
this.projectRoot = assets.projectRoot;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Phase: Pre-loop Setup ──────────────────────────────────
|
|
174
|
+
|
|
175
|
+
ensureEnvironment() {
|
|
176
|
+
assets.ensureDirs();
|
|
177
|
+
ensureGitignore(this.projectRoot);
|
|
178
|
+
|
|
179
|
+
if (!isGitRepo(this.projectRoot)) {
|
|
180
|
+
log('info', '初始化 git 仓库...');
|
|
181
|
+
execSync('git init', { cwd: this.projectRoot, stdio: 'inherit' });
|
|
182
|
+
execSync('git add -A && git commit -m "init: 项目初始化" --allow-empty', {
|
|
183
|
+
cwd: this.projectRoot,
|
|
184
|
+
stdio: 'inherit',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
checkPrerequisites() {
|
|
190
|
+
if (!assets.exists('profile')) {
|
|
191
|
+
return { ok: false, msg: 'profile 不存在,请先运行 claude-coder init 初始化项目' };
|
|
192
|
+
}
|
|
193
|
+
if (!assets.exists('tasks')) {
|
|
194
|
+
return { ok: false, msg: 'tasks.json 不存在,请先运行 claude-coder plan 生成任务' };
|
|
195
|
+
}
|
|
196
|
+
return { ok: true };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Phase: Per-session Snapshot ────────────────────────────
|
|
200
|
+
|
|
201
|
+
snapshot(taskData) {
|
|
202
|
+
const nextTask = selectNextTask(taskData);
|
|
203
|
+
const taskId = nextTask?.id || 'unknown';
|
|
204
|
+
|
|
205
|
+
const state = loadState();
|
|
206
|
+
state.current_task_id = taskId;
|
|
207
|
+
saveState(state);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
headBefore: getGitHead(this.projectRoot),
|
|
211
|
+
taskId,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
isAllDone(taskData) {
|
|
216
|
+
const features = getFeatures(taskData);
|
|
217
|
+
return features.length > 0 && features.every(f => f.status === 'done');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── Phase: Post-session Lifecycle ──────────────────────────
|
|
221
|
+
|
|
222
|
+
async onSuccess(session, { taskId, sessionResult, validateResult }) {
|
|
223
|
+
this._incrementSession();
|
|
224
|
+
|
|
225
|
+
this._appendProgress({
|
|
226
|
+
session,
|
|
227
|
+
timestamp: this._timestamp(),
|
|
228
|
+
result: 'success',
|
|
229
|
+
cost: sessionResult.cost,
|
|
230
|
+
taskId,
|
|
231
|
+
statusAfter: validateResult.sessionData?.status_after || null,
|
|
232
|
+
notes: validateResult.sessionData?.notes || null,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return { consecutiveFailures: 0, lastFailReason: '' };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async onFailure(session, { headBefore, taskId, sessionResult, validateResult, consecutiveFailures }) {
|
|
239
|
+
const reason = validateResult.reason || '校验失败';
|
|
240
|
+
log('error', `Session ${session} 校验失败 (连续失败: ${consecutiveFailures + 1}/${MAX_RETRY})`);
|
|
241
|
+
return this._handleRetryOrSkip(session, {
|
|
242
|
+
headBefore, taskId, sessionResult, consecutiveFailures,
|
|
243
|
+
result: 'fatal', reason,
|
|
244
|
+
lastFailMsg: `上次校验失败: ${reason},代码已回滚`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async onStall(session, { headBefore, taskId, sessionResult, consecutiveFailures }) {
|
|
249
|
+
log('warn', `Session ${session} 因停顿超时中断,跳过校验直接重试`);
|
|
250
|
+
return this._handleRetryOrSkip(session, {
|
|
251
|
+
headBefore, taskId, sessionResult, consecutiveFailures,
|
|
252
|
+
result: 'stalled', reason: '停顿超时',
|
|
253
|
+
lastFailMsg: '上次会话停顿超时,已回滚',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
cleanup() {
|
|
258
|
+
this._killServicesByProfile();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Phase: Post-session Validation ─────────────────────────
|
|
262
|
+
|
|
263
|
+
async validate(headBefore, taskId) {
|
|
264
|
+
log('info', '========== 开始校验 ==========');
|
|
265
|
+
|
|
266
|
+
let srResult = _validateSessionResult();
|
|
267
|
+
const gitResult = _checkGitProgress(headBefore);
|
|
268
|
+
|
|
269
|
+
if (!srResult.valid && srResult.rawContent) {
|
|
270
|
+
const srPath = assets.path('sessionResult');
|
|
271
|
+
if (srPath) {
|
|
272
|
+
const { repairJsonFile } = require('./repair');
|
|
273
|
+
await repairJsonFile(srPath);
|
|
274
|
+
srResult = _validateSessionResult();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let fatal = false;
|
|
279
|
+
let hasWarnings = false;
|
|
280
|
+
|
|
281
|
+
if (srResult.valid) {
|
|
282
|
+
hasWarnings = gitResult.warning;
|
|
283
|
+
} else {
|
|
284
|
+
if (gitResult.hasCommit) {
|
|
285
|
+
const taskStatus = _inferFromTasks(taskId);
|
|
286
|
+
if (taskStatus === 'done' || taskStatus === 'testing') {
|
|
287
|
+
log('warn', `session_result.json 异常,但 tasks.json 显示 ${taskId} 已 ${taskStatus},且有新提交,降级为警告`);
|
|
288
|
+
} else {
|
|
289
|
+
log('warn', 'session_result.json 异常,但有新提交,降级为警告(不回滚代码)');
|
|
290
|
+
}
|
|
291
|
+
hasWarnings = true;
|
|
292
|
+
} else {
|
|
293
|
+
log('error', '无新提交且 session_result.json 异常,视为致命');
|
|
294
|
+
fatal = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (fatal) {
|
|
299
|
+
log('error', '========== 校验失败 (致命) ==========');
|
|
300
|
+
} else if (hasWarnings) {
|
|
301
|
+
log('warn', '========== 校验通过 (有警告) ==========');
|
|
302
|
+
} else {
|
|
303
|
+
log('ok', '========== 校验全部通过 ==========');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const reason = fatal ? (srResult.reason || '无新提交且 session_result.json 异常') : '';
|
|
307
|
+
return { fatal, hasWarnings, sessionData: srResult.data, reason };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Internal: Retry / Skip ─────────────────────────────────
|
|
311
|
+
|
|
312
|
+
async _handleRetryOrSkip(session, { headBefore, taskId, sessionResult, consecutiveFailures, result, reason, lastFailMsg }) {
|
|
313
|
+
const newFailures = consecutiveFailures + 1;
|
|
314
|
+
const exceeded = newFailures >= MAX_RETRY;
|
|
315
|
+
|
|
316
|
+
await this._rollback(headBefore, reason);
|
|
317
|
+
|
|
318
|
+
if (exceeded) {
|
|
319
|
+
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
320
|
+
this._markTaskFailed(taskId);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const entry = { session, timestamp: this._timestamp(), result, cost: sessionResult.cost, taskId };
|
|
324
|
+
if (result === 'fatal') entry.reason = reason;
|
|
325
|
+
this._appendProgress(entry);
|
|
326
|
+
|
|
327
|
+
if (exceeded) return { consecutiveFailures: 0, lastFailReason: '' };
|
|
328
|
+
return { consecutiveFailures: newFailures, lastFailReason: lastFailMsg };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Internal: Utilities ────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
_timestamp() {
|
|
334
|
+
return new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ─── Internal: State Operations ─────────────────────────────
|
|
338
|
+
|
|
339
|
+
_incrementSession() {
|
|
340
|
+
const state = loadState();
|
|
341
|
+
state.session_count++;
|
|
342
|
+
saveState(state);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
_markSimplifyDone() {
|
|
346
|
+
const state = loadState();
|
|
347
|
+
state.last_simplify_session = state.session_count;
|
|
348
|
+
saveState(state);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ─── Internal: Git Operations ───────────────────────────────
|
|
352
|
+
|
|
353
|
+
async _rollback(headBefore, reason) {
|
|
354
|
+
if (!headBefore || headBefore === 'none') return;
|
|
355
|
+
|
|
356
|
+
this._killServicesByProfile();
|
|
357
|
+
if (process.platform === 'win32') await sleep(1500);
|
|
358
|
+
|
|
359
|
+
const cwd = this.projectRoot;
|
|
360
|
+
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
361
|
+
|
|
362
|
+
log('warn', `回滚到 ${headBefore} ...`);
|
|
363
|
+
|
|
364
|
+
let success = false;
|
|
365
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
366
|
+
try {
|
|
367
|
+
execSync(`git reset --hard ${headBefore}`, { cwd, stdio: 'pipe', env: gitEnv });
|
|
368
|
+
execSync('git clean -fd', { cwd, stdio: 'pipe', env: gitEnv });
|
|
369
|
+
log('ok', '回滚完成');
|
|
370
|
+
success = true;
|
|
371
|
+
break;
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (attempt === 1) {
|
|
374
|
+
log('warn', `回滚首次失败,等待后重试: ${err.message}`);
|
|
375
|
+
await sleep(2000);
|
|
376
|
+
} else {
|
|
377
|
+
log('error', `回滚失败: ${err.message}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this._appendProgress({
|
|
383
|
+
type: 'rollback',
|
|
384
|
+
timestamp: this._timestamp(),
|
|
385
|
+
reason: reason || 'harness 校验失败',
|
|
386
|
+
rollbackTo: headBefore,
|
|
387
|
+
success,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
tryPush() {
|
|
392
|
+
try {
|
|
393
|
+
const cwd = this.projectRoot;
|
|
394
|
+
const remotes = execSync('git remote', { cwd, encoding: 'utf8' }).trim();
|
|
395
|
+
if (!remotes) return;
|
|
396
|
+
log('info', '正在推送代码...');
|
|
397
|
+
execSync('git push', { cwd, stdio: 'inherit' });
|
|
398
|
+
log('ok', '推送成功');
|
|
399
|
+
} catch {
|
|
400
|
+
log('warn', '推送失败 (请检查网络或权限),继续执行...');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
_commitIfDirty(message) {
|
|
405
|
+
try {
|
|
406
|
+
execSync('git diff --quiet HEAD', { cwd: this.projectRoot, stdio: 'pipe' });
|
|
407
|
+
} catch {
|
|
408
|
+
execSync(`git add -A && git commit -m "${message}"`, { cwd: this.projectRoot, stdio: 'pipe' });
|
|
409
|
+
log('ok', '代码优化已提交');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ─── Simplify Scheduling (called by runner) ────────────────
|
|
414
|
+
|
|
415
|
+
shouldSimplify() {
|
|
416
|
+
const { simplifyInterval } = this.config;
|
|
417
|
+
if (simplifyInterval <= 0) return false;
|
|
418
|
+
const state = loadState();
|
|
419
|
+
return state.session_count % simplifyInterval === 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
needsFinalSimplify() {
|
|
423
|
+
const { simplifyInterval } = this.config;
|
|
424
|
+
if (simplifyInterval <= 0) return false;
|
|
425
|
+
const state = loadState();
|
|
426
|
+
return state.last_simplify_session < state.session_count;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
afterSimplify(commitMsg = 'style: simplify optimization') {
|
|
430
|
+
this._markSimplifyDone();
|
|
431
|
+
this._commitIfDirty(commitMsg);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── Internal: Task & Progress ──────────────────────────────
|
|
435
|
+
|
|
436
|
+
_markTaskFailed(taskId) {
|
|
437
|
+
if (!taskId) return;
|
|
438
|
+
const data = loadTasks();
|
|
439
|
+
if (!data) return;
|
|
440
|
+
const features = getFeatures(data);
|
|
441
|
+
const task = features.find(f => f.id === taskId);
|
|
442
|
+
if (task && task.status !== 'done') {
|
|
443
|
+
task.status = 'failed';
|
|
444
|
+
saveTasks(data);
|
|
445
|
+
log('warn', `已将任务 ${taskId} 强制标记为 failed`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_appendProgress(entry) {
|
|
450
|
+
let progress = assets.readJson('progress', { sessions: [] });
|
|
451
|
+
if (!Array.isArray(progress.sessions)) progress.sessions = [];
|
|
452
|
+
progress.sessions.push(entry);
|
|
453
|
+
assets.writeJson('progress', progress);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ─── Internal: Process Management ──────────────────────────
|
|
457
|
+
|
|
458
|
+
_killServicesByProfile() {
|
|
459
|
+
const profile = assets.readJson('profile', null);
|
|
460
|
+
if (!profile) return;
|
|
461
|
+
const ports = (profile.services || []).map(s => s.port).filter(Boolean);
|
|
462
|
+
if (ports.length === 0) return;
|
|
463
|
+
|
|
464
|
+
for (const port of ports) {
|
|
465
|
+
try {
|
|
466
|
+
if (process.platform === 'win32') {
|
|
467
|
+
const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
468
|
+
const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
|
|
469
|
+
for (const pid of pids) { try { execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'pipe' }); } catch { /* ignore */ } }
|
|
470
|
+
} else {
|
|
471
|
+
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
|
|
472
|
+
}
|
|
473
|
+
} catch { /* no process on port */ }
|
|
474
|
+
}
|
|
475
|
+
log('info', `已停止端口 ${ports.join(', ')} 上的服务`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
module.exports = {
|
|
480
|
+
Harness,
|
|
481
|
+
loadState,
|
|
482
|
+
syncAfterPlan,
|
|
483
|
+
selectNextTask,
|
|
484
|
+
};
|