fe-harness 1.0.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 +55 -0
- package/agents/fe-codebase-mapper.md +945 -0
- package/agents/fe-design-scanner.md +47 -0
- package/agents/fe-executor.md +221 -0
- package/agents/fe-fix-loop.md +310 -0
- package/agents/fe-fixer.md +153 -0
- package/agents/fe-project-scanner.md +95 -0
- package/agents/fe-reviewer.md +141 -0
- package/agents/fe-verifier.md +231 -0
- package/agents/fe-wave-runner.md +477 -0
- package/bin/install.js +292 -0
- package/commands/fe/complete.md +35 -0
- package/commands/fe/execute.md +46 -0
- package/commands/fe/help.md +17 -0
- package/commands/fe/map-codebase.md +60 -0
- package/commands/fe/plan.md +36 -0
- package/commands/fe/status.md +39 -0
- package/fe-harness/bin/browser.cjs +271 -0
- package/fe-harness/bin/fe-tools.cjs +317 -0
- package/fe-harness/bin/lib/__tests__/browser.test.cjs +422 -0
- package/fe-harness/bin/lib/__tests__/config.test.cjs +93 -0
- package/fe-harness/bin/lib/__tests__/core.test.cjs +127 -0
- package/fe-harness/bin/lib/__tests__/scoring.test.cjs +130 -0
- package/fe-harness/bin/lib/__tests__/tasks.test.cjs +698 -0
- package/fe-harness/bin/lib/browser-core.cjs +365 -0
- package/fe-harness/bin/lib/config.cjs +34 -0
- package/fe-harness/bin/lib/core.cjs +135 -0
- package/fe-harness/bin/lib/logger.cjs +93 -0
- package/fe-harness/bin/lib/scoring.cjs +219 -0
- package/fe-harness/bin/lib/tasks.cjs +632 -0
- package/fe-harness/references/model-profiles.md +44 -0
- package/fe-harness/templates/config.jsonc +31 -0
- package/fe-harness/vendor/.gitkeep +0 -0
- package/fe-harness/vendor/puppeteer-core.cjs +445 -0
- package/fe-harness/workflows/complete.md +143 -0
- package/fe-harness/workflows/execute.md +227 -0
- package/fe-harness/workflows/help.md +89 -0
- package/fe-harness/workflows/map-codebase.md +331 -0
- package/fe-harness/workflows/plan.md +244 -0
- package/package.json +35 -0
- package/scripts/bundle-puppeteer.js +38 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getRuntimeDir, getContextDir, getLogsDir, ensureDir, readJSON, writeJSON, autoParse, timestamp } = require('./core.cjs');
|
|
6
|
+
|
|
7
|
+
function tasksPath(root) {
|
|
8
|
+
return path.join(getRuntimeDir(root), 'tasks.json');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function loadTasks(root) {
|
|
12
|
+
const tasks = readJSON(tasksPath(root));
|
|
13
|
+
if (!tasks) return [];
|
|
14
|
+
return Array.isArray(tasks) ? tasks : [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function saveTasks(root, tasks) {
|
|
18
|
+
writeJSON(tasksPath(root), tasks);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resetTaskFields(task) {
|
|
22
|
+
task.status = 'pending';
|
|
23
|
+
task.verifyPassed = false;
|
|
24
|
+
task.retryCount = 0;
|
|
25
|
+
task.bestScore = 0;
|
|
26
|
+
task.bestScoresJSON = null;
|
|
27
|
+
task.lastError = '';
|
|
28
|
+
task.startedAt = '';
|
|
29
|
+
task.completedAt = '';
|
|
30
|
+
task.executorFinishedAt = '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function listTasks(root) {
|
|
34
|
+
return loadTasks(root);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getTask(root, id) {
|
|
38
|
+
const tasks = loadTasks(root);
|
|
39
|
+
const task = tasks.find(t => t.id === Number(id));
|
|
40
|
+
return task || { error: `Task #${id} not found` };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function updateTask(root, id, field, value) {
|
|
44
|
+
const tasks = loadTasks(root);
|
|
45
|
+
const idx = tasks.findIndex(t => t.id === Number(id));
|
|
46
|
+
if (idx === -1) return { error: `Task #${id} not found` };
|
|
47
|
+
|
|
48
|
+
tasks[idx][field] = autoParse(value);
|
|
49
|
+
if (field === 'status' && value === 'in_progress' && !tasks[idx].startedAt) {
|
|
50
|
+
tasks[idx].startedAt = timestamp();
|
|
51
|
+
}
|
|
52
|
+
if (field === 'status' && value === 'done') {
|
|
53
|
+
tasks[idx].completedAt = timestamp();
|
|
54
|
+
}
|
|
55
|
+
saveTasks(root, tasks);
|
|
56
|
+
return { ok: true, id: Number(id), field, value };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Next task selection (dependency-aware) ---
|
|
60
|
+
|
|
61
|
+
function getNextTask(root) {
|
|
62
|
+
const tasks = loadTasks(root);
|
|
63
|
+
|
|
64
|
+
// First: any in_progress task
|
|
65
|
+
const inProgress = tasks.find(t => t.status === 'in_progress');
|
|
66
|
+
if (inProgress) return inProgress;
|
|
67
|
+
|
|
68
|
+
// Then: first pending task with all dependencies satisfied
|
|
69
|
+
for (const task of tasks) {
|
|
70
|
+
if (task.status !== 'pending') continue;
|
|
71
|
+
|
|
72
|
+
const deps = task.dependsOn || [];
|
|
73
|
+
const allSatisfied = deps.every(depId => {
|
|
74
|
+
const dep = tasks.find(t => t.id === depId);
|
|
75
|
+
return dep && dep.status === 'done';
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (allSatisfied) return task;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { done: true, message: 'All tasks completed or no executable task available' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Failure propagation ---
|
|
85
|
+
|
|
86
|
+
function propagateFailure(root, failedId) {
|
|
87
|
+
const tasks = loadTasks(root);
|
|
88
|
+
const failedTask = tasks.find(t => t.id === Number(failedId));
|
|
89
|
+
if (!failedTask) return { error: `Task #${failedId} not found` };
|
|
90
|
+
|
|
91
|
+
const skipped = [];
|
|
92
|
+
|
|
93
|
+
function markDependents(id) {
|
|
94
|
+
for (const task of tasks) {
|
|
95
|
+
if (task.status !== 'pending') continue;
|
|
96
|
+
const deps = task.dependsOn || [];
|
|
97
|
+
if (deps.includes(Number(id))) {
|
|
98
|
+
task.status = 'skipped';
|
|
99
|
+
task.lastError = `依赖任务失败: ${failedTask.name} (#${failedId})`;
|
|
100
|
+
skipped.push(task.id);
|
|
101
|
+
markDependents(task.id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
markDependents(Number(failedId));
|
|
107
|
+
saveTasks(root, tasks);
|
|
108
|
+
return { ok: true, skipped };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- Batch operations ---
|
|
112
|
+
|
|
113
|
+
function failTask(root, id, errorMsg) {
|
|
114
|
+
const tasks = loadTasks(root);
|
|
115
|
+
const failedId = Number(id);
|
|
116
|
+
const failedTask = tasks.find(t => t.id === failedId);
|
|
117
|
+
if (!failedTask) return { error: `Task #${id} not found` };
|
|
118
|
+
|
|
119
|
+
failedTask.status = 'failed';
|
|
120
|
+
failedTask.lastError = errorMsg || '';
|
|
121
|
+
|
|
122
|
+
const skipped = [];
|
|
123
|
+
function markDependents(tid) {
|
|
124
|
+
for (const task of tasks) {
|
|
125
|
+
if (task.status !== 'pending') continue;
|
|
126
|
+
if ((task.dependsOn || []).includes(tid)) {
|
|
127
|
+
task.status = 'skipped';
|
|
128
|
+
task.lastError = `依赖任务失败: ${failedTask.name} (#${failedId})`;
|
|
129
|
+
skipped.push(task.id);
|
|
130
|
+
markDependents(task.id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
markDependents(failedId);
|
|
135
|
+
|
|
136
|
+
saveTasks(root, tasks);
|
|
137
|
+
return { ok: true, id: failedId, skipped };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function completeTasks(root, ids) {
|
|
141
|
+
const tasks = loadTasks(root);
|
|
142
|
+
const completed = [];
|
|
143
|
+
for (const id of ids) {
|
|
144
|
+
const idx = tasks.findIndex(t => t.id === Number(id));
|
|
145
|
+
if (idx === -1) continue;
|
|
146
|
+
tasks[idx].status = 'done';
|
|
147
|
+
tasks[idx].verifyPassed = true;
|
|
148
|
+
tasks[idx].completedAt = timestamp();
|
|
149
|
+
completed.push(Number(id));
|
|
150
|
+
}
|
|
151
|
+
saveTasks(root, tasks);
|
|
152
|
+
return { ok: true, completed };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function saveRetryState(root, id, data) {
|
|
156
|
+
const tasks = loadTasks(root);
|
|
157
|
+
const idx = tasks.findIndex(t => t.id === Number(id));
|
|
158
|
+
if (idx === -1) return { error: `Task #${id} not found` };
|
|
159
|
+
|
|
160
|
+
if (data.retryCount !== undefined) tasks[idx].retryCount = data.retryCount;
|
|
161
|
+
if (data.bestScore !== undefined) tasks[idx].bestScore = data.bestScore;
|
|
162
|
+
if (data.bestScoresJSON !== undefined) tasks[idx].bestScoresJSON = data.bestScoresJSON;
|
|
163
|
+
saveTasks(root, tasks);
|
|
164
|
+
return { ok: true, id: Number(id) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- Reset ---
|
|
168
|
+
|
|
169
|
+
function resetTask(root, id) {
|
|
170
|
+
const tasks = loadTasks(root);
|
|
171
|
+
const idx = tasks.findIndex(t => t.id === Number(id));
|
|
172
|
+
if (idx === -1) return { error: `Task #${id} not found` };
|
|
173
|
+
|
|
174
|
+
resetTaskFields(tasks[idx]);
|
|
175
|
+
saveTasks(root, tasks);
|
|
176
|
+
return { ok: true, id: Number(id) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resetAllFailed(root) {
|
|
180
|
+
const tasks = loadTasks(root);
|
|
181
|
+
const resetIds = [];
|
|
182
|
+
|
|
183
|
+
for (const task of tasks) {
|
|
184
|
+
if (task.status === 'failed' || task.status === 'skipped') {
|
|
185
|
+
resetTaskFields(task);
|
|
186
|
+
resetIds.push(task.id);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
saveTasks(root, tasks);
|
|
191
|
+
return { ok: true, reset: resetIds };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Wave grouping (topological level sort) ---
|
|
195
|
+
|
|
196
|
+
function getWaves(root, preloadedTasks) {
|
|
197
|
+
const tasks = preloadedTasks || loadTasks(root);
|
|
198
|
+
if (tasks.length === 0) return { waves: {}, waveOrder: [], taskCount: 0 };
|
|
199
|
+
|
|
200
|
+
const taskMap = new Map(tasks.map(t => [t.id, t]));
|
|
201
|
+
|
|
202
|
+
// Assign wave numbers via topological-level sort
|
|
203
|
+
const waveOf = new Map();
|
|
204
|
+
const circularDeps = [];
|
|
205
|
+
|
|
206
|
+
function computeWave(id, visited) {
|
|
207
|
+
if (waveOf.has(id)) return waveOf.get(id);
|
|
208
|
+
if (visited.has(id)) {
|
|
209
|
+
// Circular dependency detected — record warning and break cycle
|
|
210
|
+
const cycle = [...visited, id].map(Number);
|
|
211
|
+
circularDeps.push(cycle);
|
|
212
|
+
waveOf.set(id, 1);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
visited.add(id);
|
|
216
|
+
|
|
217
|
+
const task = taskMap.get(id);
|
|
218
|
+
const deps = (task && task.dependsOn) || [];
|
|
219
|
+
if (deps.length === 0) {
|
|
220
|
+
waveOf.set(id, 1);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let maxDepWave = 0;
|
|
225
|
+
for (const depId of deps) {
|
|
226
|
+
if (!taskMap.has(depId)) continue; // skip missing deps
|
|
227
|
+
maxDepWave = Math.max(maxDepWave, computeWave(depId, visited));
|
|
228
|
+
}
|
|
229
|
+
// If waveOf was already set during cycle detection inside the recursive
|
|
230
|
+
// calls above, keep that value — do not overwrite it.
|
|
231
|
+
if (waveOf.has(id)) return waveOf.get(id);
|
|
232
|
+
const wave = maxDepWave + 1;
|
|
233
|
+
waveOf.set(id, wave);
|
|
234
|
+
return wave;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const task of tasks) {
|
|
238
|
+
computeWave(task.id, new Set());
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Group tasks by wave
|
|
242
|
+
const waves = {};
|
|
243
|
+
for (const task of tasks) {
|
|
244
|
+
const wave = waveOf.get(task.id);
|
|
245
|
+
if (!waves[wave]) waves[wave] = [];
|
|
246
|
+
waves[wave].push({
|
|
247
|
+
id: task.id,
|
|
248
|
+
name: task.name,
|
|
249
|
+
status: task.status,
|
|
250
|
+
type: task.figmaUrl ? 'design' : 'logic',
|
|
251
|
+
dependsOn: task.dependsOn || [],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const waveOrder = Object.keys(waves).map(Number).sort((a, b) => a - b);
|
|
256
|
+
|
|
257
|
+
// Summary for each wave
|
|
258
|
+
const waveSummary = {};
|
|
259
|
+
let completedWaves = 0;
|
|
260
|
+
let completedTasks = 0;
|
|
261
|
+
for (const w of waveOrder) {
|
|
262
|
+
const waveTasks = waves[w];
|
|
263
|
+
const done = waveTasks.filter(t => t.status === 'done').length;
|
|
264
|
+
const allDone = done === waveTasks.length;
|
|
265
|
+
if (allDone) completedWaves++;
|
|
266
|
+
completedTasks += done;
|
|
267
|
+
waveSummary[w] = {
|
|
268
|
+
total: waveTasks.length,
|
|
269
|
+
pending: waveTasks.filter(t => t.status === 'pending').length,
|
|
270
|
+
done,
|
|
271
|
+
failed: waveTasks.filter(t => t.status === 'failed' || t.status === 'skipped').length,
|
|
272
|
+
allDone,
|
|
273
|
+
tasks: waveTasks,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const remaining = tasks.length - completedTasks;
|
|
278
|
+
|
|
279
|
+
// Circular dependency warning
|
|
280
|
+
let circularWarning = null;
|
|
281
|
+
if (circularDeps.length > 0) {
|
|
282
|
+
const cycles = circularDeps.map(c => c.join(' → ')).join('; ');
|
|
283
|
+
circularWarning = `⚠️ 检测到循环依赖(已自动打破): ${cycles}。受影响的任务可能在同一 wave 中并行执行,请检查 dependsOn 配置。`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
waves: waveSummary,
|
|
288
|
+
waveOrder,
|
|
289
|
+
taskCount: tasks.length,
|
|
290
|
+
completedWaves,
|
|
291
|
+
completedTasks,
|
|
292
|
+
remaining,
|
|
293
|
+
circularWarning,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- File conflict detection for parallel execution ---
|
|
298
|
+
|
|
299
|
+
function checkConflicts(root, preloadedTasks) {
|
|
300
|
+
const tasks = preloadedTasks || loadTasks(root);
|
|
301
|
+
if (tasks.length === 0) return { conflicts: [], hasConflicts: false };
|
|
302
|
+
|
|
303
|
+
const waveResult = getWaves(root, tasks);
|
|
304
|
+
const conflicts = [];
|
|
305
|
+
|
|
306
|
+
for (const waveNum of waveResult.waveOrder) {
|
|
307
|
+
const waveTasks = waveResult.waves[waveNum].tasks;
|
|
308
|
+
if (waveTasks.length <= 1) continue;
|
|
309
|
+
|
|
310
|
+
// Build file-to-tasks map for this wave
|
|
311
|
+
const fileOwners = new Map();
|
|
312
|
+
for (const wt of waveTasks) {
|
|
313
|
+
const task = tasks.find(t => t.id === wt.id);
|
|
314
|
+
const files = (task && task.filesModified) || [];
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
if (!fileOwners.has(file)) fileOwners.set(file, []);
|
|
317
|
+
fileOwners.get(file).push(wt.id);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Find conflicts (files owned by multiple tasks in same wave)
|
|
322
|
+
for (const [file, owners] of fileOwners) {
|
|
323
|
+
if (owners.length > 1) {
|
|
324
|
+
conflicts.push({
|
|
325
|
+
wave: waveNum,
|
|
326
|
+
file,
|
|
327
|
+
tasks: owners,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
conflicts,
|
|
335
|
+
hasConflicts: conflicts.length > 0,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// --- Auto-resolve file conflicts by adding dependencies ---
|
|
340
|
+
|
|
341
|
+
function resolveConflicts(root) {
|
|
342
|
+
const tasks = loadTasks(root);
|
|
343
|
+
const { conflicts, hasConflicts } = checkConflicts(root, tasks);
|
|
344
|
+
if (!hasConflicts) return { ok: true, resolved: 0 };
|
|
345
|
+
const added = [];
|
|
346
|
+
|
|
347
|
+
// Group conflicts by wave
|
|
348
|
+
const conflictsByWave = new Map();
|
|
349
|
+
for (const c of conflicts) {
|
|
350
|
+
if (!conflictsByWave.has(c.wave)) conflictsByWave.set(c.wave, []);
|
|
351
|
+
conflictsByWave.get(c.wave).push(c);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const [, waveConflicts] of conflictsByWave) {
|
|
355
|
+
// Collect all conflicting task pairs — sort first to build a
|
|
356
|
+
// deterministic chain (1→2→3) so all tasks end up in different waves.
|
|
357
|
+
const pairs = new Set();
|
|
358
|
+
for (const c of waveConflicts) {
|
|
359
|
+
const sorted = [...c.tasks].sort((a, b) => a - b);
|
|
360
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
361
|
+
pairs.add(`${sorted[i]}:${sorted[i + 1]}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const pair of pairs) {
|
|
366
|
+
const [earlierId, laterId] = pair.split(':').map(Number);
|
|
367
|
+
const laterTask = tasks.find(t => t.id === laterId);
|
|
368
|
+
if (laterTask) {
|
|
369
|
+
if (!laterTask.dependsOn) laterTask.dependsOn = [];
|
|
370
|
+
if (!laterTask.dependsOn.includes(earlierId)) {
|
|
371
|
+
laterTask.dependsOn.push(earlierId);
|
|
372
|
+
added.push({ from: laterId, dependsOn: earlierId });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
saveTasks(root, tasks);
|
|
379
|
+
return { ok: true, resolved: added.length, added };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// --- Status overview ---
|
|
383
|
+
|
|
384
|
+
function getStatus(root) {
|
|
385
|
+
const tasks = loadTasks(root);
|
|
386
|
+
const summary = {
|
|
387
|
+
total: tasks.length,
|
|
388
|
+
pending: 0,
|
|
389
|
+
in_progress: 0,
|
|
390
|
+
done: 0,
|
|
391
|
+
failed: 0,
|
|
392
|
+
skipped: 0,
|
|
393
|
+
tasks: [],
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
for (const task of tasks) {
|
|
397
|
+
summary[task.status] = (summary[task.status] || 0) + 1;
|
|
398
|
+
summary.tasks.push({
|
|
399
|
+
id: task.id,
|
|
400
|
+
name: task.name,
|
|
401
|
+
status: task.status,
|
|
402
|
+
type: task.figmaUrl ? 'design' : 'logic',
|
|
403
|
+
retryCount: task.retryCount || 0,
|
|
404
|
+
verifyPassed: task.verifyPassed || false,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return summary;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function updateTaskJSON(root, id, field, jsonValue) {
|
|
412
|
+
const tasks = loadTasks(root);
|
|
413
|
+
const idx = tasks.findIndex(t => t.id === Number(id));
|
|
414
|
+
if (idx === -1) return { error: `Task #${id} not found` };
|
|
415
|
+
|
|
416
|
+
tasks[idx][field] = jsonValue;
|
|
417
|
+
saveTasks(root, tasks);
|
|
418
|
+
return { ok: true, id: Number(id), field };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// --- Completion summary ---
|
|
422
|
+
|
|
423
|
+
function getCompletionSummary(root) {
|
|
424
|
+
const tasks = loadTasks(root);
|
|
425
|
+
if (tasks.length === 0) return { error: 'No tasks found' };
|
|
426
|
+
|
|
427
|
+
const pending = tasks.filter(t => t.status === 'pending');
|
|
428
|
+
const inProgress = tasks.filter(t => t.status === 'in_progress');
|
|
429
|
+
const done = tasks.filter(t => t.status === 'done');
|
|
430
|
+
const failed = tasks.filter(t => t.status === 'failed');
|
|
431
|
+
const skipped = tasks.filter(t => t.status === 'skipped');
|
|
432
|
+
|
|
433
|
+
const unfinished = [...pending, ...inProgress];
|
|
434
|
+
const isAllFinished = unfinished.length === 0;
|
|
435
|
+
|
|
436
|
+
// Score stats from done tasks
|
|
437
|
+
const scores = done.filter(t => t.bestScore > 0).map(t => t.bestScore);
|
|
438
|
+
const avgScore = scores.length > 0
|
|
439
|
+
? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
|
|
440
|
+
: 0;
|
|
441
|
+
const minScore = scores.length > 0 ? Math.min(...scores) : 0;
|
|
442
|
+
const maxScore = scores.length > 0 ? Math.max(...scores) : 0;
|
|
443
|
+
|
|
444
|
+
// Task type breakdown
|
|
445
|
+
const designTasks = tasks.filter(t => !!t.figmaUrl);
|
|
446
|
+
const logicTasks = tasks.filter(t => !t.figmaUrl);
|
|
447
|
+
const designDone = designTasks.filter(t => t.status === 'done').length;
|
|
448
|
+
const logicDone = logicTasks.filter(t => t.status === 'done').length;
|
|
449
|
+
|
|
450
|
+
// Total retries
|
|
451
|
+
const totalRetries = tasks.reduce((sum, t) => sum + (t.retryCount || 0), 0);
|
|
452
|
+
|
|
453
|
+
// Duration calculation
|
|
454
|
+
const startTimes = tasks
|
|
455
|
+
.filter(t => t.startedAt)
|
|
456
|
+
.map(t => new Date(t.startedAt).getTime())
|
|
457
|
+
.filter(t => !isNaN(t));
|
|
458
|
+
const endTimes = tasks
|
|
459
|
+
.filter(t => t.completedAt)
|
|
460
|
+
.map(t => new Date(t.completedAt).getTime())
|
|
461
|
+
.filter(t => !isNaN(t));
|
|
462
|
+
const earliestStart = startTimes.length > 0 ? Math.min(...startTimes) : 0;
|
|
463
|
+
const latestEnd = endTimes.length > 0 ? Math.max(...endTimes) : 0;
|
|
464
|
+
const totalDurationMs = earliestStart && latestEnd ? latestEnd - earliestStart : 0;
|
|
465
|
+
|
|
466
|
+
// Format duration as human readable
|
|
467
|
+
function formatDuration(ms) {
|
|
468
|
+
if (ms <= 0) return '-';
|
|
469
|
+
const totalSec = Math.floor(ms / 1000);
|
|
470
|
+
const h = Math.floor(totalSec / 3600);
|
|
471
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
472
|
+
const s = totalSec % 60;
|
|
473
|
+
const parts = [];
|
|
474
|
+
if (h > 0) parts.push(`${h}h`);
|
|
475
|
+
if (m > 0) parts.push(`${m}m`);
|
|
476
|
+
parts.push(`${s}s`);
|
|
477
|
+
return parts.join(' ');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Per-task durations
|
|
481
|
+
const taskDurations = tasks.map(t => {
|
|
482
|
+
const s = t.startedAt ? new Date(t.startedAt).getTime() : 0;
|
|
483
|
+
const e = t.completedAt ? new Date(t.completedAt).getTime() : 0;
|
|
484
|
+
return (s && e) ? e - s : 0;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Warnings for failed/skipped tasks
|
|
488
|
+
const warnings = [];
|
|
489
|
+
for (const t of failed) {
|
|
490
|
+
warnings.push({ id: t.id, name: t.name, status: 'failed', error: t.lastError || '' });
|
|
491
|
+
}
|
|
492
|
+
for (const t of skipped) {
|
|
493
|
+
warnings.push({ id: t.id, name: t.name, status: 'skipped', error: t.lastError || '' });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
isAllFinished,
|
|
498
|
+
total: tasks.length,
|
|
499
|
+
done: done.length,
|
|
500
|
+
failed: failed.length,
|
|
501
|
+
skipped: skipped.length,
|
|
502
|
+
pending: pending.length,
|
|
503
|
+
inProgress: inProgress.length,
|
|
504
|
+
designTasks: { total: designTasks.length, done: designDone },
|
|
505
|
+
logicTasks: { total: logicTasks.length, done: logicDone },
|
|
506
|
+
scores: { avg: avgScore, min: minScore, max: maxScore },
|
|
507
|
+
totalRetries,
|
|
508
|
+
duration: {
|
|
509
|
+
totalMs: totalDurationMs,
|
|
510
|
+
totalFormatted: formatDuration(totalDurationMs),
|
|
511
|
+
startedAt: earliestStart ? new Date(earliestStart).toISOString() : '',
|
|
512
|
+
completedAt: latestEnd ? new Date(latestEnd).toISOString() : '',
|
|
513
|
+
},
|
|
514
|
+
warnings,
|
|
515
|
+
hasWarnings: warnings.length > 0,
|
|
516
|
+
completedAt: timestamp(),
|
|
517
|
+
tasks: tasks.map((t, i) => {
|
|
518
|
+
const s = t.startedAt ? new Date(t.startedAt).getTime() : 0;
|
|
519
|
+
const ef = t.executorFinishedAt ? new Date(t.executorFinishedAt).getTime() : 0;
|
|
520
|
+
const execDurationMs = (s && ef) ? ef - s : 0;
|
|
521
|
+
return {
|
|
522
|
+
id: t.id,
|
|
523
|
+
name: t.name,
|
|
524
|
+
status: t.status,
|
|
525
|
+
type: t.figmaUrl ? 'design' : 'logic',
|
|
526
|
+
bestScore: t.bestScore || 0,
|
|
527
|
+
retryCount: t.retryCount || 0,
|
|
528
|
+
startedAt: t.startedAt || '',
|
|
529
|
+
executorFinishedAt: t.executorFinishedAt || '',
|
|
530
|
+
completedAt: t.completedAt || '',
|
|
531
|
+
executorDuration: formatDuration(execDurationMs),
|
|
532
|
+
duration: formatDuration(taskDurations[i]),
|
|
533
|
+
};
|
|
534
|
+
}),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// --- Archive tasks ---
|
|
539
|
+
|
|
540
|
+
function archiveTasks(root) {
|
|
541
|
+
const srcPath = tasksPath(root);
|
|
542
|
+
if (!fs.existsSync(srcPath)) return { error: 'No tasks.json to archive' };
|
|
543
|
+
|
|
544
|
+
const now = new Date();
|
|
545
|
+
const stamp = now.toISOString().replace(/[:.]/g, '-').replace('T', '_').replace('Z', '');
|
|
546
|
+
const historyDir = path.join(getRuntimeDir(root), 'history');
|
|
547
|
+
const archiveDir = path.join(historyDir, stamp);
|
|
548
|
+
|
|
549
|
+
ensureDir(archiveDir);
|
|
550
|
+
|
|
551
|
+
// Copy tasks.json
|
|
552
|
+
fs.copyFileSync(srcPath, path.join(archiveDir, 'tasks.json'));
|
|
553
|
+
|
|
554
|
+
// Copy progress.md if it exists
|
|
555
|
+
const progressPath = path.join(getRuntimeDir(root), 'progress.md');
|
|
556
|
+
if (fs.existsSync(progressPath)) {
|
|
557
|
+
fs.copyFileSync(progressPath, path.join(archiveDir, 'progress.md'));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Copy context files if they exist
|
|
561
|
+
const contextDir = getContextDir(root);
|
|
562
|
+
if (fs.existsSync(contextDir)) {
|
|
563
|
+
const contextFiles = fs.readdirSync(contextDir);
|
|
564
|
+
if (contextFiles.length > 0) {
|
|
565
|
+
const archiveContextDir = path.join(archiveDir, 'context');
|
|
566
|
+
ensureDir(archiveContextDir);
|
|
567
|
+
for (const f of contextFiles) {
|
|
568
|
+
fs.copyFileSync(path.join(contextDir, f), path.join(archiveContextDir, f));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Copy log files if they exist
|
|
574
|
+
const logsDir = getLogsDir(root);
|
|
575
|
+
if (fs.existsSync(logsDir)) {
|
|
576
|
+
const logFiles = fs.readdirSync(logsDir);
|
|
577
|
+
if (logFiles.length > 0) {
|
|
578
|
+
const archiveLogsDir = path.join(archiveDir, 'logs');
|
|
579
|
+
ensureDir(archiveLogsDir);
|
|
580
|
+
for (const f of logFiles) {
|
|
581
|
+
fs.copyFileSync(path.join(logsDir, f), path.join(archiveLogsDir, f));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Clean up: remove tasks.json, progress.md, context files, and log files
|
|
587
|
+
fs.unlinkSync(srcPath);
|
|
588
|
+
if (fs.existsSync(progressPath)) {
|
|
589
|
+
fs.unlinkSync(progressPath);
|
|
590
|
+
}
|
|
591
|
+
const contextDirPath = getContextDir(root);
|
|
592
|
+
if (fs.existsSync(contextDirPath)) {
|
|
593
|
+
for (const f of fs.readdirSync(contextDirPath)) {
|
|
594
|
+
fs.unlinkSync(path.join(contextDirPath, f));
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Clean log files
|
|
598
|
+
if (fs.existsSync(logsDir)) {
|
|
599
|
+
for (const f of fs.readdirSync(logsDir)) {
|
|
600
|
+
fs.unlinkSync(path.join(logsDir, f));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
ok: true,
|
|
606
|
+
archiveDir: path.relative(root, archiveDir),
|
|
607
|
+
timestamp: stamp,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
module.exports = {
|
|
612
|
+
loadTasks,
|
|
613
|
+
saveTasks,
|
|
614
|
+
tasksPath,
|
|
615
|
+
listTasks,
|
|
616
|
+
getTask,
|
|
617
|
+
updateTask,
|
|
618
|
+
updateTaskJSON,
|
|
619
|
+
getNextTask,
|
|
620
|
+
getWaves,
|
|
621
|
+
checkConflicts,
|
|
622
|
+
resolveConflicts,
|
|
623
|
+
propagateFailure,
|
|
624
|
+
failTask,
|
|
625
|
+
completeTasks,
|
|
626
|
+
saveRetryState,
|
|
627
|
+
resetTask,
|
|
628
|
+
resetAllFailed,
|
|
629
|
+
getStatus,
|
|
630
|
+
getCompletionSummary,
|
|
631
|
+
archiveTasks,
|
|
632
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 模型分配策略
|
|
2
|
+
|
|
3
|
+
## 分配原则
|
|
4
|
+
|
|
5
|
+
| 模型 | 适用场景 | 特点 |
|
|
6
|
+
|------|---------|------|
|
|
7
|
+
| **opus** | 需要精确推理和创造性判断的任务 | 最强推理能力,成本最高 |
|
|
8
|
+
| **sonnet** | 执行类、验证类任务 | 平衡性能与成本 |
|
|
9
|
+
| **haiku** | 纯读取、探索、结构化输出 | 最快最便宜 |
|
|
10
|
+
|
|
11
|
+
## Agent 模型映射表
|
|
12
|
+
|
|
13
|
+
| Agent 角色 | 模型 | 理由 |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `fe-executor` | **opus** | 代码实现是核心环节,复杂组件需要深度架构推理 |
|
|
16
|
+
| `fe-verifier` | sonnet | 结构化视觉对比与评分,中等复杂度 |
|
|
17
|
+
| `fe-reviewer` | sonnet | 结构化代码审查与评分,中等复杂度 |
|
|
18
|
+
| `fe-fixer` (visual) | **opus** | 视觉修复需要精确判断 CSS/布局细节以匹配 Figma 设计稿 |
|
|
19
|
+
| `fe-fixer` (logic) | sonnet | 跟随审查报告的明确指引修复代码问题 |
|
|
20
|
+
| `fe-fixer` (backpressure) | sonnet | 跟随构建/lint 错误信息修复,模式明确 |
|
|
21
|
+
| `fe-design-scanner` | haiku | 纯 API 调用获取 Figma 截图,数据提取 |
|
|
22
|
+
| `fe-project-scanner` | haiku | 文件探索与结构化输出,无复杂推理 |
|
|
23
|
+
| `fe-codebase-mapper` (x4) | haiku | 纯代码库探索与文档生成,无复杂推理 |
|
|
24
|
+
|
|
25
|
+
## 使用方式
|
|
26
|
+
|
|
27
|
+
在 Agent 调用中通过 `model` 参数指定:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# opus — 需要精确推理
|
|
31
|
+
Agent(subagent_type="fe-fixer", model="opus", prompt="...")
|
|
32
|
+
|
|
33
|
+
# sonnet — 执行与验证
|
|
34
|
+
Agent(subagent_type="fe-executor", model="sonnet", prompt="...")
|
|
35
|
+
|
|
36
|
+
# haiku — 探索与扫描
|
|
37
|
+
Agent(subagent_type="general-purpose", model="haiku", prompt="...")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 调优建议
|
|
41
|
+
|
|
42
|
+
- 如果 `fe-executor` 在复杂组件实现上质量不足,可升级为 opus
|
|
43
|
+
- 如果 `fe-fixer` (visual) 的修复效果已经很好,可降级为 sonnet 节省成本
|
|
44
|
+
- 如果 `fe-verifier` 的评分不够精准,可升级为 opus
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
// 单个任务最大重试次数
|
|
3
|
+
"maxRetries": 5,
|
|
4
|
+
|
|
5
|
+
// 开发服务器启动命令,例如 "npm run dev", "pnpm dev"
|
|
6
|
+
// 启动时自动分配随机端口(通过 PORT 环境变量传入)
|
|
7
|
+
"devServerCommand": "",
|
|
8
|
+
|
|
9
|
+
// 截图前等待时间(毫秒),留足页面渲染时间
|
|
10
|
+
"screenshotWaitMs": 10000,
|
|
11
|
+
|
|
12
|
+
// 视觉验证通过阈值(0-100),低于此分数会触发修复
|
|
13
|
+
"verifyThreshold": 80,
|
|
14
|
+
|
|
15
|
+
// 代码审查通过阈值(0-100)
|
|
16
|
+
"reviewThreshold": 80,
|
|
17
|
+
|
|
18
|
+
// 尺寸偏差容忍度(像素),超出则扣分(0-10)
|
|
19
|
+
"dimensionThreshold": 6,
|
|
20
|
+
|
|
21
|
+
// 分数下降容忍度,连续低于此差值触发回滚(0-10)
|
|
22
|
+
"scoreDropTolerance": 3,
|
|
23
|
+
|
|
24
|
+
// 硬性校验命令,例如 "npm run typecheck && npm run lint"
|
|
25
|
+
// 每轮修复后执行,不通过则阻断
|
|
26
|
+
"backpressureCommand": "",
|
|
27
|
+
|
|
28
|
+
// 设计任务验证时并行启动的最大浏览器实例数
|
|
29
|
+
// 过多的 Chrome 进程会消耗大量内存,建议 2-4
|
|
30
|
+
"maxParallelBrowsers": 3
|
|
31
|
+
}
|
|
File without changes
|