speccrew 0.2.1 → 0.2.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/.speccrew/agents/speccrew-feature-designer.md +21 -34
- package/.speccrew/agents/speccrew-system-designer.md +77 -82
- package/.speccrew/agents/speccrew-system-developer.md +227 -136
- package/.speccrew/agents/speccrew-test-manager.md +69 -172
- package/.speccrew/scripts/update-progress.js +826 -0
- package/.speccrew/skills/speccrew-dev-review/SKILL.md +442 -0
- package/package.json +1 -1
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* update-progress.js - 通用进度文件更新工具
|
|
5
|
+
*
|
|
6
|
+
* 为 SpecCrew 所有 Agent 提供统一的进度文件更新工具,替代手动 PowerShell/Python 内联操作。
|
|
7
|
+
*
|
|
8
|
+
* 支持的命令:
|
|
9
|
+
*
|
|
10
|
+
* 1. init - 初始化 DISPATCH-PROGRESS.json
|
|
11
|
+
* node update-progress.js init --file <path> --stage <stage_name> --tasks <tasks_json_or_file>
|
|
12
|
+
* 选项:
|
|
13
|
+
* --file <path> 进度文件路径(必需)
|
|
14
|
+
* --stage <name> 阶段名称(必需)
|
|
15
|
+
* --tasks <json|file> 任务列表 JSON 或 JSON 文件路径
|
|
16
|
+
* --tasks-file <path> 从文件读取任务列表
|
|
17
|
+
*
|
|
18
|
+
* 2. read - 读取进度文件
|
|
19
|
+
* node update-progress.js read --file <path> [options]
|
|
20
|
+
* 选项:
|
|
21
|
+
* --file <path> 进度文件路径(必需)
|
|
22
|
+
* --task-id <id> 仅输出指定任务
|
|
23
|
+
* --status <status> 按状态过滤任务列表(pending/in_progress/partial/completed/failed)
|
|
24
|
+
* --summary 输出进度摘要(总数/完成/失败/部分/待处理)
|
|
25
|
+
* --checkpoints 读取所有 checkpoint 状态
|
|
26
|
+
* --overview 读取 workflow 全景(阶段概览)
|
|
27
|
+
*
|
|
28
|
+
* 3. update-task - 更新单个任务状态
|
|
29
|
+
* node update-progress.js update-task --file <path> --task-id <id> --status <status> [options]
|
|
30
|
+
* 选项:
|
|
31
|
+
* --file <path> 进度文件路径(必需)
|
|
32
|
+
* --task-id <id> 任务 ID(必需)
|
|
33
|
+
* --status <status> 任务状态:pending/in_progress/partial/completed/failed(必需)
|
|
34
|
+
* --output <text> 任务输出(completed 时使用)
|
|
35
|
+
* --error <text> 错误信息(failed 时使用)
|
|
36
|
+
* --error-category <cat> 错误类别(failed 时使用)
|
|
37
|
+
* --started-at <iso> 覆盖 started_at 时间戳
|
|
38
|
+
* --completed-at <iso> 覆盖 completed_at 时间戳
|
|
39
|
+
*
|
|
40
|
+
* 4. update-counts - 强制重算计数
|
|
41
|
+
* node update-progress.js update-counts --file <path>
|
|
42
|
+
*
|
|
43
|
+
* 5. write-checkpoint - 写入/更新 checkpoint
|
|
44
|
+
* node update-progress.js write-checkpoint --file <path> --stage <stage> --checkpoint <name> --passed <true|false> [--description <text>]
|
|
45
|
+
* 选项:
|
|
46
|
+
* --file <path> 进度文件路径(必需)
|
|
47
|
+
* --stage <name> 阶段名称(必需,如文件不存在则创建)
|
|
48
|
+
* --checkpoint <name> 检查点名称(必需)
|
|
49
|
+
* --passed <true|false> 是否通过(必需)
|
|
50
|
+
* --description <text> 描述信息(可选)
|
|
51
|
+
*
|
|
52
|
+
* 6. update-workflow - 更新 WORKFLOW-PROGRESS 阶段状态
|
|
53
|
+
* node update-progress.js update-workflow --file <path> --stage <name> --status <status> [--output <text>]
|
|
54
|
+
* 选项:
|
|
55
|
+
* --file <path> 进度文件路径(必需)
|
|
56
|
+
* --stage <name> 阶段名称(必需)
|
|
57
|
+
* --status <status> 状态:pending/in_progress/completed/confirmed(必需)
|
|
58
|
+
* --output <text> 输出信息(可选)
|
|
59
|
+
* --started-at <iso> 覆盖 started_at 时间戳
|
|
60
|
+
* --completed-at <iso> 覆盖 completed_at 时间戳
|
|
61
|
+
* --confirmed-at <iso> 覆盖 confirmed_at 时间戳
|
|
62
|
+
*
|
|
63
|
+
* 输出格式:
|
|
64
|
+
* 成功:{"success": true, "message": "...", "data": {...}}
|
|
65
|
+
* 失败:{"success": false, "error": "..."}(输出到 stderr,exit code 1)
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
const fs = require('fs');
|
|
69
|
+
const path = require('path');
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// 工具函数
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 生成 ISO 8601 格式时间戳
|
|
77
|
+
*/
|
|
78
|
+
function getTimestamp() {
|
|
79
|
+
return new Date().toISOString();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 输出成功结果到 stdout
|
|
84
|
+
*/
|
|
85
|
+
function outputSuccess(message, data = null) {
|
|
86
|
+
const result = { success: true, message };
|
|
87
|
+
if (data !== null) {
|
|
88
|
+
result.data = data;
|
|
89
|
+
}
|
|
90
|
+
console.log(JSON.stringify(result, null, 2));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 输出错误结果到 stderr 并退出
|
|
95
|
+
*/
|
|
96
|
+
function outputError(error) {
|
|
97
|
+
console.error(JSON.stringify({ success: false, error }, null, 2));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 获取文件锁(防止并发冲突)
|
|
103
|
+
* @param {string} filePath 目标文件路径
|
|
104
|
+
* @returns {string} 锁文件路径
|
|
105
|
+
*/
|
|
106
|
+
function acquireLock(filePath) {
|
|
107
|
+
const lockPath = `${filePath}.lock`;
|
|
108
|
+
const maxRetries = 30;
|
|
109
|
+
let retryCount = 0;
|
|
110
|
+
|
|
111
|
+
while (retryCount < maxRetries) {
|
|
112
|
+
try {
|
|
113
|
+
// 尝试独占创建锁文件
|
|
114
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
115
|
+
fs.closeSync(fd);
|
|
116
|
+
return lockPath;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
retryCount++;
|
|
119
|
+
if (retryCount >= maxRetries) {
|
|
120
|
+
throw new Error(`Failed to acquire file lock for '${filePath}' after ${maxRetries} attempts`);
|
|
121
|
+
}
|
|
122
|
+
// 等待 1 秒后重试
|
|
123
|
+
const start = Date.now();
|
|
124
|
+
while (Date.now() - start < 1000) {
|
|
125
|
+
// Busy wait
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 释放文件锁
|
|
133
|
+
* @param {string} lockPath 锁文件路径
|
|
134
|
+
*/
|
|
135
|
+
function releaseLock(lockPath) {
|
|
136
|
+
try {
|
|
137
|
+
if (fs.existsSync(lockPath)) {
|
|
138
|
+
fs.unlinkSync(lockPath);
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// 忽略清理错误
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 原子写入 JSON 文件
|
|
147
|
+
* @param {string} filePath 目标文件路径
|
|
148
|
+
* @param {object} data 要写入的数据
|
|
149
|
+
*/
|
|
150
|
+
function atomicWriteJson(filePath, data) {
|
|
151
|
+
const tempFile = `${filePath}.tmp`;
|
|
152
|
+
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2), 'utf8');
|
|
153
|
+
fs.renameSync(tempFile, filePath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 读取 JSON 文件
|
|
158
|
+
* @param {string} filePath 文件路径
|
|
159
|
+
* @returns {object} 解析后的 JSON 对象
|
|
160
|
+
*/
|
|
161
|
+
function readJsonFile(filePath) {
|
|
162
|
+
if (!fs.existsSync(filePath)) {
|
|
163
|
+
throw new Error(`File not found: ${filePath}`);
|
|
164
|
+
}
|
|
165
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(content);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
throw new Error(`Failed to parse JSON from ${filePath}: ${e.message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 计算任务计数
|
|
175
|
+
* @param {Array} tasks 任务列表
|
|
176
|
+
* @returns {object} 计数结果
|
|
177
|
+
*/
|
|
178
|
+
function calculateCounts(tasks) {
|
|
179
|
+
const total = tasks.length;
|
|
180
|
+
const completed = tasks.filter(t => t.status === 'completed').length;
|
|
181
|
+
const failed = tasks.filter(t => t.status === 'failed').length;
|
|
182
|
+
const partial = tasks.filter(t => t.status === 'partial').length;
|
|
183
|
+
const pending = tasks.filter(t => t.status === 'pending' || !t.status).length;
|
|
184
|
+
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
|
|
185
|
+
return { total, completed, failed, partial, pending, in_progress: inProgress };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// 参数解析
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 解析命令行参数
|
|
194
|
+
* 支持 --flag value 和 -Flag value 两种格式
|
|
195
|
+
*/
|
|
196
|
+
function parseArgs() {
|
|
197
|
+
const args = process.argv.slice(2);
|
|
198
|
+
const result = {
|
|
199
|
+
command: null,
|
|
200
|
+
file: null,
|
|
201
|
+
stage: null,
|
|
202
|
+
tasks: null,
|
|
203
|
+
tasksFile: null,
|
|
204
|
+
taskId: null,
|
|
205
|
+
status: null,
|
|
206
|
+
output: null,
|
|
207
|
+
error: null,
|
|
208
|
+
errorCategory: null,
|
|
209
|
+
checkpoint: null,
|
|
210
|
+
passed: null,
|
|
211
|
+
description: null,
|
|
212
|
+
startedAt: null,
|
|
213
|
+
completedAt: null,
|
|
214
|
+
confirmedAt: null
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// 第一个参数是命令
|
|
218
|
+
if (args.length > 0 && !args[0].startsWith('-')) {
|
|
219
|
+
result.command = args[0];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < args.length; i++) {
|
|
223
|
+
const arg = args[i];
|
|
224
|
+
|
|
225
|
+
// 跳过命令本身
|
|
226
|
+
if (i === 0 && !arg.startsWith('-')) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
switch (arg) {
|
|
231
|
+
case '--file':
|
|
232
|
+
case '-File':
|
|
233
|
+
result.file = args[++i];
|
|
234
|
+
break;
|
|
235
|
+
case '--stage':
|
|
236
|
+
case '-Stage':
|
|
237
|
+
result.stage = args[++i];
|
|
238
|
+
break;
|
|
239
|
+
case '--tasks':
|
|
240
|
+
case '-Tasks':
|
|
241
|
+
result.tasks = args[++i];
|
|
242
|
+
break;
|
|
243
|
+
case '--tasks-file':
|
|
244
|
+
case '-Tasks-File':
|
|
245
|
+
result.tasksFile = args[++i];
|
|
246
|
+
break;
|
|
247
|
+
case '--task-id':
|
|
248
|
+
case '-Task-Id':
|
|
249
|
+
result.taskId = args[++i];
|
|
250
|
+
break;
|
|
251
|
+
case '--status':
|
|
252
|
+
case '-Status':
|
|
253
|
+
result.status = args[++i];
|
|
254
|
+
break;
|
|
255
|
+
case '--output':
|
|
256
|
+
case '-Output':
|
|
257
|
+
result.output = args[++i];
|
|
258
|
+
break;
|
|
259
|
+
case '--error':
|
|
260
|
+
case '-Error':
|
|
261
|
+
result.error = args[++i];
|
|
262
|
+
break;
|
|
263
|
+
case '--error-category':
|
|
264
|
+
case '-Error-Category':
|
|
265
|
+
result.errorCategory = args[++i];
|
|
266
|
+
break;
|
|
267
|
+
case '--checkpoint':
|
|
268
|
+
case '-Checkpoint':
|
|
269
|
+
result.checkpoint = args[++i];
|
|
270
|
+
break;
|
|
271
|
+
case '--passed':
|
|
272
|
+
case '-Passed':
|
|
273
|
+
result.passed = args[++i];
|
|
274
|
+
break;
|
|
275
|
+
case '--description':
|
|
276
|
+
case '-Description':
|
|
277
|
+
result.description = args[++i];
|
|
278
|
+
break;
|
|
279
|
+
case '--started-at':
|
|
280
|
+
case '-Started-At':
|
|
281
|
+
result.startedAt = args[++i];
|
|
282
|
+
break;
|
|
283
|
+
case '--completed-at':
|
|
284
|
+
case '-Completed-At':
|
|
285
|
+
result.completedAt = args[++i];
|
|
286
|
+
break;
|
|
287
|
+
case '--confirmed-at':
|
|
288
|
+
case '-Confirmed-At':
|
|
289
|
+
result.confirmedAt = args[++i];
|
|
290
|
+
break;
|
|
291
|
+
case '--summary':
|
|
292
|
+
case '-Summary':
|
|
293
|
+
result.summary = true;
|
|
294
|
+
break;
|
|
295
|
+
case '--checkpoints':
|
|
296
|
+
case '-Checkpoints':
|
|
297
|
+
result.checkpoints = true;
|
|
298
|
+
break;
|
|
299
|
+
case '--overview':
|
|
300
|
+
case '-Overview':
|
|
301
|
+
result.overview = true;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// 命令实现
|
|
311
|
+
// ============================================================================
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 命令:init - 初始化进度文件
|
|
315
|
+
*/
|
|
316
|
+
function cmdInit(args) {
|
|
317
|
+
if (!args.file || !args.stage) {
|
|
318
|
+
outputError('Usage: init --file <path> --stage <stage_name> [--tasks <json>] [--tasks-file <path>]');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const filePath = path.resolve(args.file);
|
|
322
|
+
let tasks = [];
|
|
323
|
+
|
|
324
|
+
// 从参数或文件读取任务列表
|
|
325
|
+
if (args.tasksFile) {
|
|
326
|
+
// 从文件读取
|
|
327
|
+
const tasksContent = fs.readFileSync(path.resolve(args.tasksFile), 'utf8');
|
|
328
|
+
try {
|
|
329
|
+
tasks = JSON.parse(tasksContent);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
outputError(`Failed to parse tasks file: ${e.message}`);
|
|
332
|
+
}
|
|
333
|
+
} else if (args.tasks) {
|
|
334
|
+
// 直接解析 JSON
|
|
335
|
+
try {
|
|
336
|
+
tasks = JSON.parse(args.tasks);
|
|
337
|
+
} catch (e) {
|
|
338
|
+
outputError(`Failed to parse tasks JSON: ${e.message}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 验证 tasks 是数组
|
|
343
|
+
if (!Array.isArray(tasks)) {
|
|
344
|
+
outputError('Tasks must be an array');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 确保每个任务有必要的字段
|
|
348
|
+
tasks = tasks.map((task, index) => ({
|
|
349
|
+
id: task.id || `task-${index + 1}`,
|
|
350
|
+
name: task.name || task.id || `Task ${index + 1}`,
|
|
351
|
+
status: task.status || 'pending',
|
|
352
|
+
created_at: task.created_at || getTimestamp(),
|
|
353
|
+
...task
|
|
354
|
+
}));
|
|
355
|
+
|
|
356
|
+
// 创建进度文件结构
|
|
357
|
+
const progressData = {
|
|
358
|
+
stage: args.stage,
|
|
359
|
+
created_at: getTimestamp(),
|
|
360
|
+
updated_at: getTimestamp(),
|
|
361
|
+
counts: calculateCounts(tasks),
|
|
362
|
+
tasks: tasks,
|
|
363
|
+
checkpoints: {}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// 确保目录存在
|
|
367
|
+
const dir = path.dirname(filePath);
|
|
368
|
+
if (!fs.existsSync(dir)) {
|
|
369
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 获取锁并写入
|
|
373
|
+
let lockPath = null;
|
|
374
|
+
try {
|
|
375
|
+
lockPath = acquireLock(filePath);
|
|
376
|
+
atomicWriteJson(filePath, progressData);
|
|
377
|
+
outputSuccess(`Progress file initialized: ${filePath}`, {
|
|
378
|
+
file: filePath,
|
|
379
|
+
stage: args.stage,
|
|
380
|
+
counts: progressData.counts
|
|
381
|
+
});
|
|
382
|
+
} finally {
|
|
383
|
+
if (lockPath) releaseLock(lockPath);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 命令:read - 读取进度文件(增强版)
|
|
389
|
+
* 支持多种查询模式:
|
|
390
|
+
* - 默认:读取整个文件
|
|
391
|
+
* - --task-id:查询单个任务
|
|
392
|
+
* - --status:按状态过滤任务
|
|
393
|
+
* - --summary:输出进度摘要
|
|
394
|
+
* - --checkpoints:读取 checkpoint 状态
|
|
395
|
+
* - --overview:读取 workflow 全景
|
|
396
|
+
*/
|
|
397
|
+
function cmdRead(args) {
|
|
398
|
+
if (!args.file) {
|
|
399
|
+
outputError('Usage: read --file <path> [--task-id <id>] [--status <status>] [--summary] [--checkpoints] [--overview]');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const filePath = path.resolve(args.file);
|
|
403
|
+
let lockPath = null;
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
lockPath = acquireLock(filePath);
|
|
407
|
+
const data = readJsonFile(filePath);
|
|
408
|
+
|
|
409
|
+
// 1. --summary 模式:输出进度摘要
|
|
410
|
+
if (args.summary) {
|
|
411
|
+
const counts = data.counts || calculateCounts(data.tasks || []);
|
|
412
|
+
const summary = {
|
|
413
|
+
file: filePath,
|
|
414
|
+
stage: data.stage || null,
|
|
415
|
+
current_stage: data.current_stage || null,
|
|
416
|
+
updated_at: data.updated_at || null,
|
|
417
|
+
counts: counts,
|
|
418
|
+
progress_percent: counts.total > 0
|
|
419
|
+
? Math.round((counts.completed / counts.total) * 100)
|
|
420
|
+
: 0
|
|
421
|
+
};
|
|
422
|
+
outputSuccess('Progress summary', summary);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 2. --checkpoints 模式:读取 checkpoint 状态
|
|
427
|
+
if (args.checkpoints) {
|
|
428
|
+
const checkpoints = data.checkpoints || {};
|
|
429
|
+
const checkpointList = Object.entries(checkpoints).map(([name, cp]) => ({
|
|
430
|
+
name,
|
|
431
|
+
passed: cp.passed,
|
|
432
|
+
checked_at: cp.checked_at,
|
|
433
|
+
confirmed_at: cp.confirmed_at,
|
|
434
|
+
description: cp.description
|
|
435
|
+
}));
|
|
436
|
+
outputSuccess('Checkpoints', {
|
|
437
|
+
total: checkpointList.length,
|
|
438
|
+
passed: checkpointList.filter(cp => cp.passed).length,
|
|
439
|
+
failed: checkpointList.filter(cp => !cp.passed).length,
|
|
440
|
+
checkpoints: checkpointList
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 3. --overview 模式:读取 workflow 全景
|
|
446
|
+
if (args.overview) {
|
|
447
|
+
const stages = data.stages || {};
|
|
448
|
+
const stageList = Object.entries(stages).map(([name, stage]) => ({
|
|
449
|
+
name,
|
|
450
|
+
status: stage.status,
|
|
451
|
+
started_at: stage.started_at,
|
|
452
|
+
completed_at: stage.completed_at,
|
|
453
|
+
confirmed_at: stage.confirmed_at,
|
|
454
|
+
output: stage.output
|
|
455
|
+
}));
|
|
456
|
+
|
|
457
|
+
const overview = {
|
|
458
|
+
current_stage: data.current_stage || null,
|
|
459
|
+
created_at: data.created_at,
|
|
460
|
+
updated_at: data.updated_at,
|
|
461
|
+
stages: stageList,
|
|
462
|
+
stage_summary: {
|
|
463
|
+
total: stageList.length,
|
|
464
|
+
pending: stageList.filter(s => s.status === 'pending').length,
|
|
465
|
+
in_progress: stageList.filter(s => s.status === 'in_progress').length,
|
|
466
|
+
completed: stageList.filter(s => s.status === 'completed').length,
|
|
467
|
+
confirmed: stageList.filter(s => s.status === 'confirmed').length
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
outputSuccess('Workflow overview', overview);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 4. --task-id 模式:查询单个任务
|
|
475
|
+
if (args.taskId) {
|
|
476
|
+
const task = data.tasks?.find(t => t.id === args.taskId);
|
|
477
|
+
if (!task) {
|
|
478
|
+
outputError(`Task not found: ${args.taskId}`);
|
|
479
|
+
}
|
|
480
|
+
outputSuccess(`Task: ${args.taskId}`, task);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 5. --status 模式:按状态过滤任务
|
|
485
|
+
if (args.status) {
|
|
486
|
+
const validStatuses = ['pending', 'in_progress', 'partial', 'completed', 'failed'];
|
|
487
|
+
if (!validStatuses.includes(args.status)) {
|
|
488
|
+
outputError(`Invalid status filter: ${args.status}. Must be one of: ${validStatuses.join(', ')}`);
|
|
489
|
+
}
|
|
490
|
+
const filteredTasks = (data.tasks || []).filter(t => t.status === args.status);
|
|
491
|
+
outputSuccess(`Tasks with status: ${args.status}`, {
|
|
492
|
+
status: args.status,
|
|
493
|
+
count: filteredTasks.length,
|
|
494
|
+
tasks: filteredTasks
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 6. 默认模式:输出整个文件
|
|
500
|
+
outputSuccess(`Progress file: ${filePath}`, data);
|
|
501
|
+
} finally {
|
|
502
|
+
if (lockPath) releaseLock(lockPath);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 命令:update-task - 更新单个任务状态
|
|
508
|
+
*/
|
|
509
|
+
function cmdUpdateTask(args) {
|
|
510
|
+
if (!args.file || !args.taskId || !args.status) {
|
|
511
|
+
outputError('Usage: update-task --file <path> --task-id <id> --status <status> [options]');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const validStatuses = ['pending', 'in_progress', 'partial', 'completed', 'failed'];
|
|
515
|
+
if (!validStatuses.includes(args.status)) {
|
|
516
|
+
outputError(`Invalid status: ${args.status}. Must be one of: ${validStatuses.join(', ')}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const filePath = path.resolve(args.file);
|
|
520
|
+
let lockPath = null;
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
lockPath = acquireLock(filePath);
|
|
524
|
+
const data = readJsonFile(filePath);
|
|
525
|
+
|
|
526
|
+
// 查找任务
|
|
527
|
+
const taskIndex = data.tasks?.findIndex(t => t.id === args.taskId);
|
|
528
|
+
if (taskIndex === -1 || taskIndex === undefined) {
|
|
529
|
+
outputError(`Task not found: ${args.taskId}`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const task = data.tasks[taskIndex];
|
|
533
|
+
const now = getTimestamp();
|
|
534
|
+
|
|
535
|
+
// 更新状态
|
|
536
|
+
task.status = args.status;
|
|
537
|
+
task.updated_at = now;
|
|
538
|
+
|
|
539
|
+
// 根据状态自动设置时间戳
|
|
540
|
+
if (args.status === 'in_progress') {
|
|
541
|
+
task.started_at = args.startedAt || now;
|
|
542
|
+
} else if (args.status === 'partial') {
|
|
543
|
+
// partial 状态:部分完成,可能已有 started_at,可选设置 completed_at
|
|
544
|
+
if (!task.started_at) {
|
|
545
|
+
task.started_at = args.startedAt || now;
|
|
546
|
+
}
|
|
547
|
+
if (args.output) {
|
|
548
|
+
task.output = args.output;
|
|
549
|
+
}
|
|
550
|
+
} else if (args.status === 'completed') {
|
|
551
|
+
task.completed_at = args.completedAt || now;
|
|
552
|
+
if (args.output) {
|
|
553
|
+
task.output = args.output;
|
|
554
|
+
}
|
|
555
|
+
} else if (args.status === 'failed') {
|
|
556
|
+
task.completed_at = args.completedAt || now;
|
|
557
|
+
if (args.error) {
|
|
558
|
+
task.error = args.error;
|
|
559
|
+
}
|
|
560
|
+
if (args.errorCategory) {
|
|
561
|
+
task.error_category = args.errorCategory;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 更新任务
|
|
566
|
+
data.tasks[taskIndex] = task;
|
|
567
|
+
data.updated_at = now;
|
|
568
|
+
|
|
569
|
+
// 重算计数
|
|
570
|
+
data.counts = calculateCounts(data.tasks);
|
|
571
|
+
|
|
572
|
+
// 原子写入
|
|
573
|
+
atomicWriteJson(filePath, data);
|
|
574
|
+
|
|
575
|
+
outputSuccess(`Task updated: ${args.taskId}`, {
|
|
576
|
+
task_id: args.taskId,
|
|
577
|
+
status: args.status,
|
|
578
|
+
counts: data.counts
|
|
579
|
+
});
|
|
580
|
+
} finally {
|
|
581
|
+
if (lockPath) releaseLock(lockPath);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* 命令:update-counts - 强制重算计数
|
|
587
|
+
*/
|
|
588
|
+
function cmdUpdateCounts(args) {
|
|
589
|
+
if (!args.file) {
|
|
590
|
+
outputError('Usage: update-counts --file <path>');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const filePath = path.resolve(args.file);
|
|
594
|
+
let lockPath = null;
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
lockPath = acquireLock(filePath);
|
|
598
|
+
const data = readJsonFile(filePath);
|
|
599
|
+
|
|
600
|
+
if (!data.tasks || !Array.isArray(data.tasks)) {
|
|
601
|
+
outputError('No tasks array found in progress file');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 重算计数
|
|
605
|
+
data.counts = calculateCounts(data.tasks);
|
|
606
|
+
data.updated_at = getTimestamp();
|
|
607
|
+
|
|
608
|
+
// 原子写入
|
|
609
|
+
atomicWriteJson(filePath, data);
|
|
610
|
+
|
|
611
|
+
outputSuccess('Counts updated', { counts: data.counts });
|
|
612
|
+
} finally {
|
|
613
|
+
if (lockPath) releaseLock(lockPath);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* 命令:write-checkpoint - 写入/更新 checkpoint
|
|
619
|
+
*/
|
|
620
|
+
function cmdWriteCheckpoint(args) {
|
|
621
|
+
if (!args.file || !args.stage || !args.checkpoint || args.passed === null) {
|
|
622
|
+
outputError('Usage: write-checkpoint --file <path> --stage <stage> --checkpoint <name> --passed <true|false> [--description <text>]');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const filePath = path.resolve(args.file);
|
|
626
|
+
const passed = ['true', '1', '$true', 'True', 'TRUE'].includes(args.passed);
|
|
627
|
+
const now = getTimestamp();
|
|
628
|
+
let lockPath = null;
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
lockPath = acquireLock(filePath);
|
|
632
|
+
|
|
633
|
+
let data;
|
|
634
|
+
if (fs.existsSync(filePath)) {
|
|
635
|
+
data = readJsonFile(filePath);
|
|
636
|
+
} else {
|
|
637
|
+
// 创建新文件
|
|
638
|
+
data = {
|
|
639
|
+
stage: args.stage,
|
|
640
|
+
created_at: now,
|
|
641
|
+
counts: { total: 0, completed: 0, failed: 0, pending: 0, in_progress: 0 },
|
|
642
|
+
tasks: [],
|
|
643
|
+
checkpoints: {}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// 确保 checkpoints 对象存在
|
|
648
|
+
if (!data.checkpoints) {
|
|
649
|
+
data.checkpoints = {};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// 更新或创建 checkpoint
|
|
653
|
+
data.checkpoints[args.checkpoint] = {
|
|
654
|
+
passed: passed,
|
|
655
|
+
checked_at: now,
|
|
656
|
+
confirmed_at: passed ? now : null,
|
|
657
|
+
description: args.description || null
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
data.updated_at = now;
|
|
661
|
+
|
|
662
|
+
// 确保目录存在
|
|
663
|
+
const dir = path.dirname(filePath);
|
|
664
|
+
if (!fs.existsSync(dir)) {
|
|
665
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// 原子写入
|
|
669
|
+
atomicWriteJson(filePath, data);
|
|
670
|
+
|
|
671
|
+
outputSuccess(`Checkpoint updated: ${args.checkpoint}`, {
|
|
672
|
+
checkpoint: args.checkpoint,
|
|
673
|
+
passed: passed,
|
|
674
|
+
confirmed_at: data.checkpoints[args.checkpoint].confirmed_at
|
|
675
|
+
});
|
|
676
|
+
} finally {
|
|
677
|
+
if (lockPath) releaseLock(lockPath);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* 命令:update-workflow - 更新 WORKFLOW-PROGRESS 阶段状态
|
|
683
|
+
*/
|
|
684
|
+
function cmdUpdateWorkflow(args) {
|
|
685
|
+
if (!args.file || !args.stage || !args.status) {
|
|
686
|
+
outputError('Usage: update-workflow --file <path> --stage <name> --status <status> [--output <text>]');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const validStatuses = ['pending', 'in_progress', 'completed', 'confirmed'];
|
|
690
|
+
if (!validStatuses.includes(args.status)) {
|
|
691
|
+
outputError(`Invalid status: ${args.status}. Must be one of: ${validStatuses.join(', ')}`);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const filePath = path.resolve(args.file);
|
|
695
|
+
const now = getTimestamp();
|
|
696
|
+
let lockPath = null;
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
lockPath = acquireLock(filePath);
|
|
700
|
+
|
|
701
|
+
let data;
|
|
702
|
+
if (fs.existsSync(filePath)) {
|
|
703
|
+
data = readJsonFile(filePath);
|
|
704
|
+
} else {
|
|
705
|
+
// 创建新文件
|
|
706
|
+
data = {
|
|
707
|
+
created_at: now,
|
|
708
|
+
stages: {},
|
|
709
|
+
current_stage: null
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// 确保 stages 对象存在
|
|
714
|
+
if (!data.stages) {
|
|
715
|
+
data.stages = {};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// 获取或创建阶段
|
|
719
|
+
if (!data.stages[args.stage]) {
|
|
720
|
+
data.stages[args.stage] = {
|
|
721
|
+
status: 'pending',
|
|
722
|
+
started_at: null,
|
|
723
|
+
completed_at: null,
|
|
724
|
+
confirmed_at: null,
|
|
725
|
+
output: null
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const stage = data.stages[args.stage];
|
|
730
|
+
|
|
731
|
+
// 更新状态
|
|
732
|
+
stage.status = args.status;
|
|
733
|
+
|
|
734
|
+
// 根据状态自动设置时间戳
|
|
735
|
+
if (args.status === 'in_progress') {
|
|
736
|
+
// 如 started_at 已有值则不覆盖
|
|
737
|
+
if (!stage.started_at) {
|
|
738
|
+
stage.started_at = args.startedAt || now;
|
|
739
|
+
}
|
|
740
|
+
} else if (args.status === 'completed') {
|
|
741
|
+
stage.completed_at = args.completedAt || now;
|
|
742
|
+
} else if (args.status === 'confirmed') {
|
|
743
|
+
stage.confirmed_at = args.confirmedAt || now;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// 更新输出
|
|
747
|
+
if (args.output) {
|
|
748
|
+
stage.output = args.output;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// 更新当前阶段
|
|
752
|
+
data.current_stage = args.stage;
|
|
753
|
+
data.updated_at = now;
|
|
754
|
+
|
|
755
|
+
// 确保目录存在
|
|
756
|
+
const dir = path.dirname(filePath);
|
|
757
|
+
if (!fs.existsSync(dir)) {
|
|
758
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// 原子写入
|
|
762
|
+
atomicWriteJson(filePath, data);
|
|
763
|
+
|
|
764
|
+
outputSuccess(`Workflow stage updated: ${args.stage}`, {
|
|
765
|
+
stage: args.stage,
|
|
766
|
+
status: args.status,
|
|
767
|
+
current_stage: data.current_stage
|
|
768
|
+
});
|
|
769
|
+
} finally {
|
|
770
|
+
if (lockPath) releaseLock(lockPath);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ============================================================================
|
|
775
|
+
// 主入口
|
|
776
|
+
// ============================================================================
|
|
777
|
+
|
|
778
|
+
function main() {
|
|
779
|
+
const args = parseArgs();
|
|
780
|
+
|
|
781
|
+
// 无命令时显示帮助
|
|
782
|
+
if (!args.command) {
|
|
783
|
+
console.error('Usage: node update-progress.js <command> [options]');
|
|
784
|
+
console.error('');
|
|
785
|
+
console.error('Commands:');
|
|
786
|
+
console.error(' init Initialize a progress file');
|
|
787
|
+
console.error(' read Read a progress file');
|
|
788
|
+
console.error(' update-task Update a task status');
|
|
789
|
+
console.error(' update-counts Recalculate task counts');
|
|
790
|
+
console.error(' write-checkpoint Write or update a checkpoint');
|
|
791
|
+
console.error(' update-workflow Update a workflow stage status');
|
|
792
|
+
console.error('');
|
|
793
|
+
console.error('Run "node update-progress.js <command> --help" for more information.');
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// 分发命令
|
|
798
|
+
try {
|
|
799
|
+
switch (args.command) {
|
|
800
|
+
case 'init':
|
|
801
|
+
cmdInit(args);
|
|
802
|
+
break;
|
|
803
|
+
case 'read':
|
|
804
|
+
cmdRead(args);
|
|
805
|
+
break;
|
|
806
|
+
case 'update-task':
|
|
807
|
+
cmdUpdateTask(args);
|
|
808
|
+
break;
|
|
809
|
+
case 'update-counts':
|
|
810
|
+
cmdUpdateCounts(args);
|
|
811
|
+
break;
|
|
812
|
+
case 'write-checkpoint':
|
|
813
|
+
cmdWriteCheckpoint(args);
|
|
814
|
+
break;
|
|
815
|
+
case 'update-workflow':
|
|
816
|
+
cmdUpdateWorkflow(args);
|
|
817
|
+
break;
|
|
818
|
+
default:
|
|
819
|
+
outputError(`Unknown command: ${args.command}`);
|
|
820
|
+
}
|
|
821
|
+
} catch (error) {
|
|
822
|
+
outputError(error.message);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
main();
|