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.
Files changed (41) hide show
  1. package/README.md +55 -0
  2. package/agents/fe-codebase-mapper.md +945 -0
  3. package/agents/fe-design-scanner.md +47 -0
  4. package/agents/fe-executor.md +221 -0
  5. package/agents/fe-fix-loop.md +310 -0
  6. package/agents/fe-fixer.md +153 -0
  7. package/agents/fe-project-scanner.md +95 -0
  8. package/agents/fe-reviewer.md +141 -0
  9. package/agents/fe-verifier.md +231 -0
  10. package/agents/fe-wave-runner.md +477 -0
  11. package/bin/install.js +292 -0
  12. package/commands/fe/complete.md +35 -0
  13. package/commands/fe/execute.md +46 -0
  14. package/commands/fe/help.md +17 -0
  15. package/commands/fe/map-codebase.md +60 -0
  16. package/commands/fe/plan.md +36 -0
  17. package/commands/fe/status.md +39 -0
  18. package/fe-harness/bin/browser.cjs +271 -0
  19. package/fe-harness/bin/fe-tools.cjs +317 -0
  20. package/fe-harness/bin/lib/__tests__/browser.test.cjs +422 -0
  21. package/fe-harness/bin/lib/__tests__/config.test.cjs +93 -0
  22. package/fe-harness/bin/lib/__tests__/core.test.cjs +127 -0
  23. package/fe-harness/bin/lib/__tests__/scoring.test.cjs +130 -0
  24. package/fe-harness/bin/lib/__tests__/tasks.test.cjs +698 -0
  25. package/fe-harness/bin/lib/browser-core.cjs +365 -0
  26. package/fe-harness/bin/lib/config.cjs +34 -0
  27. package/fe-harness/bin/lib/core.cjs +135 -0
  28. package/fe-harness/bin/lib/logger.cjs +93 -0
  29. package/fe-harness/bin/lib/scoring.cjs +219 -0
  30. package/fe-harness/bin/lib/tasks.cjs +632 -0
  31. package/fe-harness/references/model-profiles.md +44 -0
  32. package/fe-harness/templates/config.jsonc +31 -0
  33. package/fe-harness/vendor/.gitkeep +0 -0
  34. package/fe-harness/vendor/puppeteer-core.cjs +445 -0
  35. package/fe-harness/workflows/complete.md +143 -0
  36. package/fe-harness/workflows/execute.md +227 -0
  37. package/fe-harness/workflows/help.md +89 -0
  38. package/fe-harness/workflows/map-codebase.md +331 -0
  39. package/fe-harness/workflows/plan.md +244 -0
  40. package/package.json +35 -0
  41. package/scripts/bundle-puppeteer.js +38 -0
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { ensureDir, findProjectRoot } = require('./lib/core.cjs');
7
+ const {
8
+ listSessions,
9
+ launch,
10
+ connect,
11
+ stop,
12
+ cleanup,
13
+ formatA11yNode,
14
+ } = require('./lib/browser-core.cjs');
15
+ const { initLogger, log, closeLogger } = require('./lib/logger.cjs');
16
+
17
+ const args = process.argv.slice(2);
18
+ const command = args[0];
19
+
20
+ function output(data) {
21
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
22
+ }
23
+
24
+ function parseFlags(args) {
25
+ const flags = {};
26
+ for (let i = 0; i < args.length; i++) {
27
+ if (args[i].startsWith('--')) {
28
+ const key = args[i].slice(2);
29
+ const next = args[i + 1];
30
+ if (next && !next.startsWith('--')) {
31
+ flags[key] = next;
32
+ i++;
33
+ } else {
34
+ flags[key] = true;
35
+ }
36
+ }
37
+ }
38
+ return flags;
39
+ }
40
+
41
+ async function main() {
42
+ try { initLogger(findProjectRoot()); } catch (_) { /* best-effort */ }
43
+
44
+ try {
45
+ switch (command) {
46
+ case 'start': {
47
+ const flags = parseFlags(args.slice(1));
48
+ const opts = {};
49
+
50
+ if (flags.viewport) {
51
+ const [w, h] = flags.viewport.split('x').map(Number);
52
+ if (w && h) {
53
+ opts.viewportWidth = w;
54
+ opts.viewportHeight = h;
55
+ }
56
+ }
57
+
58
+ if (flags.maximized) {
59
+ opts.maximized = true;
60
+ }
61
+
62
+ const session = await launch(opts);
63
+
64
+ log('INFO', 'browser', `浏览器启动`, { sessionId: session.sessionId });
65
+ if (flags['session-id-only']) {
66
+ process.stdout.write(session.sessionId);
67
+ } else {
68
+ output({ ok: true, ...session });
69
+ }
70
+ break;
71
+ }
72
+
73
+ case 'navigate': {
74
+ const sessionId = args[1];
75
+ const url = args[2];
76
+ if (!sessionId || !url) {
77
+ output({ error: 'Usage: browser.cjs navigate <sessionId> <url> [--wait-for text] [--timeout ms]' });
78
+ process.exit(1);
79
+ }
80
+
81
+ const flags = parseFlags(args.slice(3));
82
+ const timeout = parseInt(flags.timeout) || 30000;
83
+
84
+ const { browser, page } = await connect(sessionId);
85
+ try {
86
+ await page.goto(url, { waitUntil: 'networkidle2', timeout });
87
+
88
+ if (flags['wait-for']) {
89
+ await page.waitForFunction(
90
+ (text) => document.body?.innerText?.includes(text),
91
+ { timeout },
92
+ flags['wait-for']
93
+ );
94
+ }
95
+
96
+ const title = await page.title();
97
+ log('INFO', 'browser', `页面导航`, { url, title });
98
+ output({ ok: true, url, title });
99
+ } finally {
100
+ browser.disconnect();
101
+ }
102
+ break;
103
+ }
104
+
105
+ case 'screenshot': {
106
+ const sessionId = args[1];
107
+ const outputPath = args[2];
108
+ if (!sessionId || !outputPath) {
109
+ output({ error: 'Usage: browser.cjs screenshot <sessionId> <outputPath> [--full-page] [--selector css]' });
110
+ process.exit(1);
111
+ }
112
+
113
+ const flags = parseFlags(args.slice(3));
114
+ const resolvedPath = path.resolve(outputPath);
115
+
116
+ // Ensure output directory exists
117
+ ensureDir(path.dirname(resolvedPath));
118
+
119
+ const { browser, page } = await connect(sessionId);
120
+ try {
121
+ const screenshotOpts = { path: resolvedPath, type: 'png' };
122
+
123
+ if (flags.selector) {
124
+ const element = await page.$(flags.selector);
125
+ if (!element) {
126
+ output({ error: `Element not found: ${flags.selector}` });
127
+ break;
128
+ }
129
+ await element.screenshot(screenshotOpts);
130
+ } else {
131
+ screenshotOpts.fullPage = !!flags['full-page'];
132
+ await page.screenshot(screenshotOpts);
133
+ }
134
+
135
+ const viewport = page.viewport();
136
+ log('INFO', 'browser', `截图完成`, { path: resolvedPath });
137
+ output({
138
+ ok: true,
139
+ path: resolvedPath,
140
+ width: viewport?.width || 0,
141
+ height: viewport?.height || 0,
142
+ });
143
+ } finally {
144
+ browser.disconnect();
145
+ }
146
+ break;
147
+ }
148
+
149
+ case 'eval': {
150
+ const sessionId = args[1];
151
+ if (!sessionId) {
152
+ output({ error: 'Usage: browser.cjs eval <sessionId> <expression> | --stdin' });
153
+ process.exit(1);
154
+ }
155
+
156
+ const flags = parseFlags(args.slice(2));
157
+ let expression;
158
+
159
+ if (flags.stdin || flags.stdin === true) {
160
+ expression = fs.readFileSync('/dev/stdin', 'utf8').trim();
161
+ } else {
162
+ // Collect all remaining non-flag args as expression
163
+ expression = args.slice(2).filter(a => !a.startsWith('--')).join(' ');
164
+ }
165
+
166
+ if (!expression) {
167
+ output({ error: 'No expression provided' });
168
+ process.exit(1);
169
+ }
170
+
171
+ const { browser, page } = await connect(sessionId);
172
+ try {
173
+ // Wrap in async IIFE if it looks like a function
174
+ let result;
175
+ if (expression.trim().startsWith('(') || expression.trim().startsWith('async')) {
176
+ result = await page.evaluate(expression);
177
+ } else {
178
+ result = await page.evaluate(`(() => { return ${expression}; })()`);
179
+ }
180
+ output({ ok: true, result });
181
+ } finally {
182
+ browser.disconnect();
183
+ }
184
+ break;
185
+ }
186
+
187
+ case 'snapshot': {
188
+ const sessionId = args[1];
189
+ if (!sessionId) {
190
+ output({ error: 'Usage: browser.cjs snapshot <sessionId> [--file path]' });
191
+ process.exit(1);
192
+ }
193
+
194
+ const flags = parseFlags(args.slice(2));
195
+
196
+ const { browser, page } = await connect(sessionId);
197
+ try {
198
+ const snapshot = await page.accessibility.snapshot();
199
+ const formatted = formatA11yNode(snapshot);
200
+
201
+ if (flags.file) {
202
+ const resolvedPath = path.resolve(flags.file);
203
+ const dir = path.dirname(resolvedPath);
204
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
205
+ fs.writeFileSync(resolvedPath, formatted, 'utf8');
206
+ output({ ok: true, path: resolvedPath });
207
+ } else {
208
+ output({ ok: true, snapshot: formatted });
209
+ }
210
+ } finally {
211
+ browser.disconnect();
212
+ }
213
+ break;
214
+ }
215
+
216
+ case 'stop': {
217
+ const sessionId = args[1];
218
+ if (!sessionId) {
219
+ output({ error: 'Usage: browser.cjs stop <sessionId>' });
220
+ process.exit(1);
221
+ }
222
+
223
+ const result = await stop(sessionId);
224
+ log('INFO', 'browser', `浏览器停止`, { sessionId });
225
+ output({ ok: true, sessionId, ...result });
226
+ break;
227
+ }
228
+
229
+ case 'cleanup': {
230
+ const flags = parseFlags(args.slice(1));
231
+ const maxAge = parseInt(flags['max-age']) || 60;
232
+ const result = await cleanup({ maxAge });
233
+ log('INFO', 'browser', `浏览器清理`, { maxAge, ...result });
234
+ output({ ok: true, ...result });
235
+ break;
236
+ }
237
+
238
+ case 'list': {
239
+ const sessions = listSessions();
240
+ output({ ok: true, sessions });
241
+ break;
242
+ }
243
+
244
+ default: {
245
+ output({
246
+ error: 'Unknown command',
247
+ usage: {
248
+ start: 'browser.cjs start [--viewport WxH] [--session-id-only]',
249
+ navigate: 'browser.cjs navigate <sid> <url> [--wait-for text] [--timeout ms]',
250
+ screenshot: 'browser.cjs screenshot <sid> <outputPath> [--full-page] [--selector css]',
251
+ eval: 'browser.cjs eval <sid> <expression> | --stdin',
252
+ snapshot: 'browser.cjs snapshot <sid> [--file path]',
253
+ stop: 'browser.cjs stop <sid>',
254
+ cleanup: 'browser.cjs cleanup [--max-age minutes]',
255
+ list: 'browser.cjs list',
256
+ },
257
+ });
258
+ process.exit(1);
259
+ }
260
+ }
261
+ } catch (err) {
262
+ log('ERROR', 'browser', `浏览器操作失败: ${err.message}`, { command });
263
+ closeLogger();
264
+ output({ error: err.message });
265
+ process.exit(1);
266
+ } finally {
267
+ closeLogger();
268
+ }
269
+ }
270
+
271
+ main();
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { findProjectRoot, getContextDir, ensureDir } = require('./lib/core.cjs');
7
+ const { listTasks, getTask, updateTask, updateTaskJSON, getNextTask, getWaves, checkConflicts, resolveConflicts, propagateFailure, failTask, completeTasks, saveRetryState, resetTask, resetAllFailed, getStatus, getCompletionSummary, archiveTasks } = require('./lib/tasks.cjs');
8
+ const { getConfig, setConfig, initConfig } = require('./lib/config.cjs');
9
+ const { calculateScore, checkRegression } = require('./lib/scoring.cjs');
10
+ const { initLogger, log, getLogPath, closeLogger } = require('./lib/logger.cjs');
11
+
12
+ const args = process.argv.slice(2);
13
+ const command = args[0];
14
+ const subcommand = args[1];
15
+
16
+ const root = findProjectRoot();
17
+
18
+ // Initialize logger (silent no-op if .fe-runtime doesn't exist yet)
19
+ try { initLogger(root); } catch (_) { /* ignore — logging is best-effort */ }
20
+
21
+ function output(data) {
22
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
23
+ }
24
+
25
+ /**
26
+ * Read JSON from stdin (for --stdin flag).
27
+ * Reads all remaining args after --stdin as space-separated JSON objects,
28
+ * or reads from actual stdin if no inline data follows.
29
+ */
30
+ function readStdinSync() {
31
+ return fs.readFileSync('/dev/stdin', 'utf8').trim();
32
+ }
33
+
34
+ function usage() {
35
+ output({
36
+ commands: {
37
+ 'tasks list': 'List all tasks',
38
+ 'tasks waves': 'Get wave-grouped task execution plan (dependency-based)',
39
+ 'tasks check-conflicts': 'Check for file ownership conflicts within waves',
40
+ 'tasks resolve-conflicts': 'Auto-resolve conflicts by adding dependencies',
41
+ 'tasks next': 'Get next executable task (dependency-aware)',
42
+ 'tasks get <id>': 'Get task by ID',
43
+ 'tasks update <id> <field> <value>': 'Update a task field',
44
+ 'tasks update-json <id> <field> <json>': 'Update a task field with JSON value',
45
+ 'tasks propagate-failure <id>': 'Propagate failure to dependent tasks',
46
+ 'tasks reset <id>': 'Reset a task to pending',
47
+ 'tasks reset-all-failed': 'Reset all failed/skipped tasks',
48
+ 'tasks fail <id> [--error "msg"]': 'Mark task failed + set error + propagate failure (atomic)',
49
+ 'tasks complete <id1> [id2 ...]': 'Batch-mark tasks as done with verifyPassed=true',
50
+ 'tasks summary': 'Get completion summary with stats, scores and warnings',
51
+ 'tasks archive': 'Archive tasks.json + context to .fe-runtime/history/ and clean up',
52
+ 'tasks save-retry <id> --stdin': 'Update retryCount + bestScore + bestScoresJSON atomically (JSON stdin)',
53
+ 'tasks status': 'Get status overview',
54
+ 'scoring calculate <type> <scores-json|--stdin>': 'Calculate weighted score (type: design|logic)',
55
+ 'scoring check-regression <current-json> <best-json> | --stdin': 'Check for score regression',
56
+ 'config get': 'Get project config',
57
+ 'config set <key> <value>': 'Set a config value',
58
+ 'config init <config-json>': 'Initialize config',
59
+ 'init execute': 'Pre-execution checks',
60
+ 'init context': 'Ensure context directory exists and is clean',
61
+ },
62
+ });
63
+ }
64
+
65
+ try {
66
+ switch (command) {
67
+ case 'tasks': {
68
+ switch (subcommand) {
69
+ case 'list':
70
+ output(listTasks(root));
71
+ break;
72
+ case 'next':
73
+ output(getNextTask(root));
74
+ break;
75
+ case 'waves': {
76
+ const wavesResult = getWaves(root);
77
+ log('INFO', 'wave', `Wave 计划: ${wavesResult.waveOrder?.length || 0} 个 wave, ${wavesResult.taskCount || 0} 个任务`, { remaining: wavesResult.remaining, completedWaves: wavesResult.completedWaves });
78
+ output(wavesResult);
79
+ break;
80
+ }
81
+ case 'check-conflicts':
82
+ output(checkConflicts(root));
83
+ break;
84
+ case 'resolve-conflicts': {
85
+ const resolveResult = resolveConflicts(root);
86
+ if (resolveResult.resolved > 0) {
87
+ log('WARN', 'task', `文件冲突已自动解决`, { resolved: resolveResult.resolved, added: resolveResult.added });
88
+ }
89
+ output(resolveResult);
90
+ break;
91
+ }
92
+ case 'get':
93
+ output(getTask(root, args[2]));
94
+ break;
95
+ case 'update': {
96
+ const updateResult = updateTask(root, args[2], args[3], args[4]);
97
+ if (args[3] === 'status') {
98
+ log('INFO', 'task', `任务状态变更 #${args[2]} ${args[3]}=${args[4]}`);
99
+ }
100
+ output(updateResult);
101
+ break;
102
+ }
103
+ case 'update-json': {
104
+ const field = args[3] === '--stdin' ? args[4] : args[3];
105
+ const jsonValue = args[3] === '--stdin' ? JSON.parse(readStdinSync()) : JSON.parse(args[4]);
106
+ output(updateTaskJSON(root, args[2], field, jsonValue));
107
+ break;
108
+ }
109
+ case 'propagate-failure':
110
+ output(propagateFailure(root, args[2]));
111
+ break;
112
+ case 'reset':
113
+ output(resetTask(root, args[2]));
114
+ break;
115
+ case 'reset-all-failed':
116
+ output(resetAllFailed(root));
117
+ break;
118
+ case 'fail': {
119
+ const errIdx = args.indexOf('--error');
120
+ const errorMsg = errIdx >= 0 ? args.slice(errIdx + 1).join(' ') : '';
121
+ const failResult = failTask(root, args[2], errorMsg);
122
+ log('WARN', 'task', `任务失败 #${args[2]}`, { error: errorMsg, skipped: failResult.skipped });
123
+ output(failResult);
124
+ break;
125
+ }
126
+ case 'complete': {
127
+ const completeResult = completeTasks(root, args.slice(2));
128
+ log('INFO', 'task', `任务完成`, { completed: completeResult.completed });
129
+ output(completeResult);
130
+ break;
131
+ }
132
+ case 'save-retry': {
133
+ const retryData = JSON.parse(readStdinSync());
134
+ output(saveRetryState(root, args[2], retryData));
135
+ break;
136
+ }
137
+ case 'status':
138
+ output(getStatus(root));
139
+ break;
140
+ case 'summary':
141
+ output(getCompletionSummary(root));
142
+ break;
143
+ case 'archive':
144
+ output(archiveTasks(root));
145
+ break;
146
+ default:
147
+ output({ error: `Unknown tasks subcommand: ${subcommand}` });
148
+ }
149
+ break;
150
+ }
151
+
152
+ case 'scoring': {
153
+ const cfg = getConfig(root);
154
+ const thresholds = {
155
+ verifyThreshold: cfg.verifyThreshold || 80,
156
+ reviewThreshold: cfg.reviewThreshold || 80,
157
+ dimensionThreshold: cfg.dimensionThreshold || 6,
158
+ scoreDropTolerance: cfg.scoreDropTolerance || 3,
159
+ };
160
+
161
+ switch (subcommand) {
162
+ case 'calculate': {
163
+ const type = args[2]; // 'design' or 'logic'
164
+ let scores;
165
+ if (args[3] === '--stdin') {
166
+ scores = JSON.parse(readStdinSync());
167
+ } else {
168
+ scores = JSON.parse(args[3]);
169
+ }
170
+ const scoreResult = calculateScore(scores, type, thresholds);
171
+ log('INFO', 'scoring', `评分计算 type=${type}`, { score: scoreResult.score, passed: scoreResult.passed });
172
+ output(scoreResult);
173
+ break;
174
+ }
175
+ case 'check-regression': {
176
+ let current, best;
177
+ if (args[2] === '--stdin') {
178
+ // Read two JSON objects from stdin (newline separated or as array)
179
+ const stdinData = readStdinSync();
180
+ const parts = stdinData.split('\n').filter(Boolean);
181
+ if (parts.length >= 2) {
182
+ current = JSON.parse(parts[0]);
183
+ best = JSON.parse(parts[1]);
184
+ } else {
185
+ const arr = JSON.parse(stdinData);
186
+ current = arr[0];
187
+ best = arr[1];
188
+ }
189
+ } else {
190
+ current = JSON.parse(args[2]);
191
+ best = JSON.parse(args[3]);
192
+ }
193
+ const regResult = checkRegression(current, best, thresholds);
194
+ if (regResult.regressed) {
195
+ log('WARN', 'scoring', `检测到评分回归`, { regressed: regResult.regressed, details: regResult.regressions });
196
+ }
197
+ output(regResult);
198
+ break;
199
+ }
200
+ default:
201
+ output({ error: `Unknown scoring subcommand: ${subcommand}` });
202
+ }
203
+ break;
204
+ }
205
+
206
+ case 'config': {
207
+ switch (subcommand) {
208
+ case 'get':
209
+ output(getConfig(root));
210
+ break;
211
+ case 'set':
212
+ output(setConfig(root, args[2], args[3]));
213
+ break;
214
+ case 'init':
215
+ output(initConfig(root, JSON.parse(args[2])));
216
+ break;
217
+ default:
218
+ output({ error: `Unknown config subcommand: ${subcommand}` });
219
+ }
220
+ break;
221
+ }
222
+
223
+ case 'init': {
224
+ switch (subcommand) {
225
+ case 'execute': {
226
+ // Pre-execution checks
227
+ const cfg = getConfig(root);
228
+ if (cfg.error) { output(cfg); break; }
229
+ const tasks = listTasks(root);
230
+ if (tasks.length === 0) { output({ error: 'No tasks found. Run /fe:plan first.' }); break; }
231
+
232
+ // Check if any design tasks exist (need dev server)
233
+ const hasDesignTasks = tasks.some(t => !!t.figmaUrl);
234
+
235
+ const missing = [];
236
+ if (hasDesignTasks) {
237
+ if (!cfg.devServerCommand) missing.push('devServerCommand');
238
+ }
239
+ if (missing.length > 0) {
240
+ output({ error: `Config incomplete (design tasks require dev server). Missing: ${missing.join(', ')}` });
241
+ break;
242
+ }
243
+
244
+ const initResult = {
245
+ ok: true,
246
+ config: cfg,
247
+ taskCount: tasks.length,
248
+ hasDesignTasks,
249
+ pending: tasks.filter(t => t.status === 'pending').length,
250
+ in_progress: tasks.filter(t => t.status === 'in_progress').length,
251
+ done: tasks.filter(t => t.status === 'done').length,
252
+ };
253
+ log('INFO', 'init', `执行初始化完成`, { taskCount: initResult.taskCount, hasDesignTasks, pending: initResult.pending, done: initResult.done, logPath: getLogPath() });
254
+ output(initResult);
255
+ break;
256
+ }
257
+ case 'context': {
258
+ const contextDir = getContextDir(root);
259
+ ensureDir(contextDir);
260
+ // Optional: pass task IDs to only clean files for specific tasks
261
+ // Usage: fe-tools.cjs init context [id1 id2 ...]
262
+ const taskIds = args.slice(2).map(Number).filter(n => !isNaN(n));
263
+
264
+ // Build patterns: if task IDs provided, only match those IDs; otherwise match all
265
+ const idPattern = taskIds.length > 0
266
+ ? `(${taskIds.join('|')})`
267
+ : '\\d+';
268
+ const contextPatterns = [
269
+ new RegExp(`^impl-result-${idPattern}\\.json$`),
270
+ new RegExp(`^verify-result-${idPattern}\\.json$`),
271
+ new RegExp(`^verify-analysis-${idPattern}\\.md$`),
272
+ new RegExp(`^review-result-${idPattern}\\.json$`),
273
+ new RegExp(`^review-analysis-${idPattern}\\.md$`),
274
+ new RegExp(`^fix-result-${idPattern}\\.json$`),
275
+ new RegExp(`^(impl-screenshot|self-check|fix-check)-${idPattern}\\.png$`),
276
+ new RegExp(`^a11y-snapshot-${idPattern}\\.txt$`),
277
+ ];
278
+ // Always clean non-ID-suffixed legacy files and backpressure results
279
+ const legacyPatterns = [
280
+ /^impl-result\.json$/,
281
+ /^verify-result\.json$/,
282
+ /^verify-analysis\.md$/,
283
+ /^review-result\.json$/,
284
+ /^review-analysis\.md$/,
285
+ /^fix-result\.json$/,
286
+ /^fix-result-bp\.json$/,
287
+ ];
288
+ const allPatterns = [...contextPatterns, ...legacyPatterns];
289
+
290
+ const files = fs.readdirSync(contextDir);
291
+ let cleaned = 0;
292
+ for (const f of files) {
293
+ if (allPatterns.some(p => p.test(f))) {
294
+ fs.unlinkSync(path.join(contextDir, f));
295
+ cleaned++;
296
+ }
297
+ }
298
+ output({ ok: true, contextDir, cleaned, taskIds: taskIds.length > 0 ? taskIds : 'all' });
299
+ break;
300
+ }
301
+ default:
302
+ output({ error: `Unknown init subcommand: ${subcommand}` });
303
+ }
304
+ break;
305
+ }
306
+
307
+ default:
308
+ usage();
309
+ }
310
+ } catch (err) {
311
+ log('ERROR', 'system', `未捕获异常: ${err.message}`, { stack: err.stack });
312
+ closeLogger();
313
+ output({ error: err.message, stack: err.stack });
314
+ process.exit(1);
315
+ } finally {
316
+ closeLogger();
317
+ }