speccrew 0.6.11 → 0.6.12
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-product-manager.md +18 -4
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +7 -7
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/apply-module-mapping.js +1 -1
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/reindex-modules.js +38 -38
- package/.speccrew/skills/speccrew-pm-module-initializer/SKILL.md +83 -39
- package/lib/commands/doctor.js +7 -7
- package/lib/commands/init.js +35 -35
- package/lib/commands/list.js +8 -8
- package/lib/commands/uninstall.js +20 -20
- package/lib/commands/update.js +41 -41
- package/lib/ide-adapters.js +47 -47
- package/lib/utils.js +12 -12
- package/package.json +1 -1
- package/workspace-template/scripts/update-progress.js +148 -147
|
@@ -1,75 +1,76 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* update-progress.js -
|
|
4
|
+
* update-progress.js - Universal Progress File Update Tool
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Provides a unified progress file update tool for all SpecCrew Agents,
|
|
7
|
+
* replacing manual PowerShell/Python inline operations.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
+
* Supported Commands:
|
|
9
10
|
*
|
|
10
|
-
* 1. init -
|
|
11
|
+
* 1. init - Initialize DISPATCH-PROGRESS.json
|
|
11
12
|
* 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>
|
|
16
|
-
* --tasks-file <path>
|
|
13
|
+
* Options:
|
|
14
|
+
* --file <path> Progress file path (required)
|
|
15
|
+
* --stage <name> Stage name (required)
|
|
16
|
+
* --tasks <json|file> Task list JSON or JSON file path
|
|
17
|
+
* --tasks-file <path> Read task list from file
|
|
17
18
|
*
|
|
18
|
-
* 2. read -
|
|
19
|
+
* 2. read - Read progress file
|
|
19
20
|
* node update-progress.js read --file <path> [options]
|
|
20
|
-
*
|
|
21
|
-
* --file <path>
|
|
22
|
-
* --task-id <id>
|
|
23
|
-
* --status <status>
|
|
24
|
-
* --summary
|
|
25
|
-
* --checkpoints
|
|
26
|
-
* --overview
|
|
21
|
+
* Options:
|
|
22
|
+
* --file <path> Progress file path (required)
|
|
23
|
+
* --task-id <id> Output only the specified task
|
|
24
|
+
* --status <status> Filter tasks by status (pending/in_progress/partial/completed/failed)
|
|
25
|
+
* --summary Output progress summary (total/completed/failed/partial/pending)
|
|
26
|
+
* --checkpoints Read all checkpoint statuses
|
|
27
|
+
* --overview Read workflow overview (stage summary)
|
|
27
28
|
*
|
|
28
|
-
* 3. update-task -
|
|
29
|
+
* 3. update-task - Update a single task status
|
|
29
30
|
* node update-progress.js update-task --file <path> --task-id <id> --status <status> [options]
|
|
30
|
-
*
|
|
31
|
-
* --file <path>
|
|
32
|
-
* --task-id <id>
|
|
33
|
-
* --status <status>
|
|
34
|
-
* --output <text>
|
|
35
|
-
* --error <text>
|
|
36
|
-
* --error-category <cat>
|
|
31
|
+
* Options:
|
|
32
|
+
* --file <path> Progress file path (required)
|
|
33
|
+
* --task-id <id> Task ID (required)
|
|
34
|
+
* --status <status> Task status: pending/in_progress/partial/completed/failed (required)
|
|
35
|
+
* --output <text> Task output (used when completed)
|
|
36
|
+
* --error <text> Error message (used when failed)
|
|
37
|
+
* --error-category <cat> Error category (used when failed)
|
|
37
38
|
*
|
|
38
|
-
* 4. update-counts -
|
|
39
|
+
* 4. update-counts - Force recalculate counts
|
|
39
40
|
* node update-progress.js update-counts --file <path>
|
|
40
41
|
*
|
|
41
|
-
* 5. write-checkpoint -
|
|
42
|
+
* 5. write-checkpoint - Write/update checkpoint
|
|
42
43
|
* node update-progress.js write-checkpoint --file <path> --stage <stage> --checkpoint <name> --passed <true|false> [--description <text>]
|
|
43
|
-
*
|
|
44
|
-
* --file <path>
|
|
45
|
-
* --stage <name>
|
|
46
|
-
* --checkpoint <name>
|
|
47
|
-
* --passed <true|false>
|
|
48
|
-
* --description <text>
|
|
44
|
+
* Options:
|
|
45
|
+
* --file <path> Progress file path (required)
|
|
46
|
+
* --stage <name> Stage name (required, creates file if not exists)
|
|
47
|
+
* --checkpoint <name> Checkpoint name (required)
|
|
48
|
+
* --passed <true|false> Whether passed (required)
|
|
49
|
+
* --description <text> Description (optional)
|
|
49
50
|
*
|
|
50
|
-
* 6. update-workflow -
|
|
51
|
+
* 6. update-workflow - Update WORKFLOW-PROGRESS stage status
|
|
51
52
|
* node update-progress.js update-workflow --file <path> --stage <name> --status <status> [--output <text>]
|
|
52
|
-
*
|
|
53
|
-
* --file <path>
|
|
54
|
-
* --stage <name>
|
|
55
|
-
* --status <status>
|
|
56
|
-
* --output <text>
|
|
53
|
+
* Options:
|
|
54
|
+
* --file <path> Progress file path (required)
|
|
55
|
+
* --stage <name> Stage name (required)
|
|
56
|
+
* --status <status> Status: pending/in_progress/completed/confirmed (required)
|
|
57
|
+
* --output <text> Output information (optional)
|
|
57
58
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* Output Format:
|
|
60
|
+
* Success: {"success": true, "message": "...", "data": {...}}
|
|
61
|
+
* Failure: {"success": false, "error": "..."} (output to stderr, exit code 1)
|
|
61
62
|
*/
|
|
62
63
|
|
|
63
64
|
const fs = require('fs');
|
|
64
65
|
const path = require('path');
|
|
65
66
|
|
|
66
67
|
// ============================================================================
|
|
67
|
-
//
|
|
68
|
+
// Utility Functions
|
|
68
69
|
// ============================================================================
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
72
|
+
* Generate ISO 8601 format timestamp in local timezone
|
|
73
|
+
* Example: 2026-04-10T20:38:21.978+08:00
|
|
73
74
|
*/
|
|
74
75
|
function getLocalISOString() {
|
|
75
76
|
const now = new Date();
|
|
@@ -87,14 +88,14 @@ function getLocalISOString() {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
|
-
*
|
|
91
|
+
* Generate ISO 8601 format timestamp (local timezone)
|
|
91
92
|
*/
|
|
92
93
|
function getTimestamp() {
|
|
93
94
|
return getLocalISOString();
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/**
|
|
97
|
-
*
|
|
98
|
+
* Output success result to stdout
|
|
98
99
|
*/
|
|
99
100
|
function outputSuccess(message, data = null) {
|
|
100
101
|
const result = { success: true, message };
|
|
@@ -105,7 +106,7 @@ function outputSuccess(message, data = null) {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
|
-
*
|
|
109
|
+
* Output error result to stderr and exit
|
|
109
110
|
*/
|
|
110
111
|
function outputError(error) {
|
|
111
112
|
console.error(JSON.stringify({ success: false, error }, null, 2));
|
|
@@ -113,9 +114,9 @@ function outputError(error) {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
116
|
-
*
|
|
117
|
-
* @param {string} filePath
|
|
118
|
-
* @returns {string}
|
|
117
|
+
* Acquire file lock (prevent concurrent conflicts)
|
|
118
|
+
* @param {string} filePath Target file path
|
|
119
|
+
* @returns {string} Lock file path
|
|
119
120
|
*/
|
|
120
121
|
function acquireLock(filePath) {
|
|
121
122
|
const lockPath = `${filePath}.lock`;
|
|
@@ -124,12 +125,12 @@ function acquireLock(filePath) {
|
|
|
124
125
|
|
|
125
126
|
while (retryCount < maxRetries) {
|
|
126
127
|
try {
|
|
127
|
-
//
|
|
128
|
+
// Attempt to exclusively create lock file
|
|
128
129
|
const fd = fs.openSync(lockPath, 'wx');
|
|
129
130
|
fs.closeSync(fd);
|
|
130
131
|
return lockPath;
|
|
131
132
|
} catch (error) {
|
|
132
|
-
//
|
|
133
|
+
// Check if error is lock file already exists
|
|
133
134
|
if (error.code === 'EEXIST') {
|
|
134
135
|
try {
|
|
135
136
|
const lockStat = fs.statSync(lockPath);
|
|
@@ -137,18 +138,18 @@ function acquireLock(filePath) {
|
|
|
137
138
|
if (ageSeconds > 60) {
|
|
138
139
|
console.error(`Warning: Stale lock file detected (age: ${Math.round(ageSeconds)}s), removing: ${lockPath}`);
|
|
139
140
|
fs.unlinkSync(lockPath);
|
|
140
|
-
//
|
|
141
|
+
// Do not consume retry count, continue to next loop attempt to acquire lock
|
|
141
142
|
continue;
|
|
142
143
|
}
|
|
143
144
|
} catch (statErr) {
|
|
144
|
-
//
|
|
145
|
+
// Lock file was deleted during stat, continue retrying
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
retryCount++;
|
|
148
149
|
if (retryCount >= maxRetries) {
|
|
149
150
|
throw new Error(`Failed to acquire file lock for '${filePath}' after ${maxRetries} attempts`);
|
|
150
151
|
}
|
|
151
|
-
//
|
|
152
|
+
// Wait 1 second before retry
|
|
152
153
|
const start = Date.now();
|
|
153
154
|
while (Date.now() - start < 1000) {
|
|
154
155
|
// Busy wait
|
|
@@ -158,8 +159,8 @@ function acquireLock(filePath) {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
/**
|
|
161
|
-
*
|
|
162
|
-
* @param {string} lockPath
|
|
162
|
+
* Release file lock
|
|
163
|
+
* @param {string} lockPath Lock file path
|
|
163
164
|
*/
|
|
164
165
|
function releaseLock(lockPath) {
|
|
165
166
|
try {
|
|
@@ -167,14 +168,14 @@ function releaseLock(lockPath) {
|
|
|
167
168
|
fs.unlinkSync(lockPath);
|
|
168
169
|
}
|
|
169
170
|
} catch (e) {
|
|
170
|
-
//
|
|
171
|
+
// Ignore cleanup errors
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
/**
|
|
175
|
-
*
|
|
176
|
-
* @param {string} filePath
|
|
177
|
-
* @param {object} data
|
|
176
|
+
* Atomically write JSON file
|
|
177
|
+
* @param {string} filePath Target file path
|
|
178
|
+
* @param {object} data Data to write
|
|
178
179
|
*/
|
|
179
180
|
function atomicWriteJson(filePath, data) {
|
|
180
181
|
const tempFile = `${filePath}.tmp`;
|
|
@@ -183,9 +184,9 @@ function atomicWriteJson(filePath, data) {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
/**
|
|
186
|
-
*
|
|
187
|
-
* @param {string} filePath
|
|
188
|
-
* @returns {object}
|
|
187
|
+
* Read JSON file
|
|
188
|
+
* @param {string} filePath File path
|
|
189
|
+
* @returns {object} Parsed JSON object
|
|
189
190
|
*/
|
|
190
191
|
function readJsonFile(filePath) {
|
|
191
192
|
if (!fs.existsSync(filePath)) {
|
|
@@ -200,9 +201,9 @@ function readJsonFile(filePath) {
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
/**
|
|
203
|
-
*
|
|
204
|
-
* @param {Array} tasks
|
|
205
|
-
* @returns {object}
|
|
204
|
+
* Calculate task counts
|
|
205
|
+
* @param {Array} tasks Task list
|
|
206
|
+
* @returns {object} Count results
|
|
206
207
|
*/
|
|
207
208
|
function calculateCounts(tasks) {
|
|
208
209
|
const total = tasks.length;
|
|
@@ -215,12 +216,12 @@ function calculateCounts(tasks) {
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
// ============================================================================
|
|
218
|
-
//
|
|
219
|
+
// Argument Parsing
|
|
219
220
|
// ============================================================================
|
|
220
221
|
|
|
221
222
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
223
|
+
* Parse command line arguments
|
|
224
|
+
* Supports both --flag value and -Flag value formats
|
|
224
225
|
*/
|
|
225
226
|
function parseArgs() {
|
|
226
227
|
const args = process.argv.slice(2);
|
|
@@ -246,7 +247,7 @@ function parseArgs() {
|
|
|
246
247
|
force: false
|
|
247
248
|
};
|
|
248
249
|
|
|
249
|
-
//
|
|
250
|
+
// First argument is the command
|
|
250
251
|
if (args.length > 0 && !args[0].startsWith('-')) {
|
|
251
252
|
result.command = args[0];
|
|
252
253
|
}
|
|
@@ -254,7 +255,7 @@ function parseArgs() {
|
|
|
254
255
|
for (let i = 0; i < args.length; i++) {
|
|
255
256
|
const arg = args[i];
|
|
256
257
|
|
|
257
|
-
//
|
|
258
|
+
// Skip the command itself
|
|
258
259
|
if (i === 0 && !arg.startsWith('-')) {
|
|
259
260
|
continue;
|
|
260
261
|
}
|
|
@@ -351,11 +352,11 @@ function parseArgs() {
|
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
// ============================================================================
|
|
354
|
-
//
|
|
355
|
+
// Command Implementations
|
|
355
356
|
// ============================================================================
|
|
356
357
|
|
|
357
358
|
/**
|
|
358
|
-
*
|
|
359
|
+
* Command: init - Initialize progress file
|
|
359
360
|
*/
|
|
360
361
|
function cmdInit(args) {
|
|
361
362
|
if (!args.file || !args.stage) {
|
|
@@ -365,9 +366,9 @@ function cmdInit(args) {
|
|
|
365
366
|
const filePath = path.resolve(args.file);
|
|
366
367
|
let tasks = [];
|
|
367
368
|
|
|
368
|
-
//
|
|
369
|
+
// Read task list from argument or file
|
|
369
370
|
if (args.tasksFile) {
|
|
370
|
-
//
|
|
371
|
+
// Read from file
|
|
371
372
|
const tasksContent = fs.readFileSync(path.resolve(args.tasksFile), 'utf8');
|
|
372
373
|
try {
|
|
373
374
|
tasks = JSON.parse(tasksContent);
|
|
@@ -375,7 +376,7 @@ function cmdInit(args) {
|
|
|
375
376
|
outputError(`Failed to parse tasks file: ${e.message}`);
|
|
376
377
|
}
|
|
377
378
|
} else if (args.tasks) {
|
|
378
|
-
//
|
|
379
|
+
// Parse JSON directly
|
|
379
380
|
try {
|
|
380
381
|
tasks = JSON.parse(args.tasks);
|
|
381
382
|
} catch (e) {
|
|
@@ -383,12 +384,12 @@ function cmdInit(args) {
|
|
|
383
384
|
}
|
|
384
385
|
}
|
|
385
386
|
|
|
386
|
-
//
|
|
387
|
+
// Validate tasks is an array
|
|
387
388
|
if (!Array.isArray(tasks)) {
|
|
388
389
|
outputError('Tasks must be an array');
|
|
389
390
|
}
|
|
390
391
|
|
|
391
|
-
//
|
|
392
|
+
// Ensure each task has required fields
|
|
392
393
|
tasks = tasks.map((task, index) => ({
|
|
393
394
|
id: task.id || `task-${index + 1}`,
|
|
394
395
|
name: task.name || task.id || `Task ${index + 1}`,
|
|
@@ -397,7 +398,7 @@ function cmdInit(args) {
|
|
|
397
398
|
...task
|
|
398
399
|
}));
|
|
399
400
|
|
|
400
|
-
//
|
|
401
|
+
// Create progress file structure
|
|
401
402
|
const progressData = {
|
|
402
403
|
stage: args.stage,
|
|
403
404
|
created_at: getTimestamp(),
|
|
@@ -407,13 +408,13 @@ function cmdInit(args) {
|
|
|
407
408
|
checkpoints: {}
|
|
408
409
|
};
|
|
409
410
|
|
|
410
|
-
//
|
|
411
|
+
// Ensure directory exists
|
|
411
412
|
const dir = path.dirname(filePath);
|
|
412
413
|
if (!fs.existsSync(dir)) {
|
|
413
414
|
fs.mkdirSync(dir, { recursive: true });
|
|
414
415
|
}
|
|
415
416
|
|
|
416
|
-
//
|
|
417
|
+
// Acquire lock and write
|
|
417
418
|
let lockPath = null;
|
|
418
419
|
try {
|
|
419
420
|
lockPath = acquireLock(filePath);
|
|
@@ -429,14 +430,14 @@ function cmdInit(args) {
|
|
|
429
430
|
}
|
|
430
431
|
|
|
431
432
|
/**
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
* -
|
|
435
|
-
* - --task-id
|
|
436
|
-
* - --status
|
|
437
|
-
* - --summary
|
|
438
|
-
* - --checkpoints
|
|
439
|
-
* - --overview
|
|
433
|
+
* Command: read - Read progress file (enhanced version)
|
|
434
|
+
* Supports multiple query modes:
|
|
435
|
+
* - Default: read entire file
|
|
436
|
+
* - --task-id: query single task
|
|
437
|
+
* - --status: filter tasks by status
|
|
438
|
+
* - --summary: output progress summary
|
|
439
|
+
* - --checkpoints: read checkpoint status
|
|
440
|
+
* - --overview: read workflow overview
|
|
440
441
|
*/
|
|
441
442
|
function cmdRead(args) {
|
|
442
443
|
if (!args.file) {
|
|
@@ -450,7 +451,7 @@ function cmdRead(args) {
|
|
|
450
451
|
lockPath = acquireLock(filePath);
|
|
451
452
|
const data = readJsonFile(filePath);
|
|
452
453
|
|
|
453
|
-
// 1. --summary
|
|
454
|
+
// 1. --summary mode: output progress summary
|
|
454
455
|
if (args.summary) {
|
|
455
456
|
const counts = data.counts || calculateCounts(data.tasks || []);
|
|
456
457
|
const summary = {
|
|
@@ -467,7 +468,7 @@ function cmdRead(args) {
|
|
|
467
468
|
return;
|
|
468
469
|
}
|
|
469
470
|
|
|
470
|
-
// 2. --checkpoints
|
|
471
|
+
// 2. --checkpoints mode: read checkpoint status
|
|
471
472
|
if (args.checkpoints) {
|
|
472
473
|
const checkpoints = data.checkpoints || {};
|
|
473
474
|
const checkpointList = Object.entries(checkpoints).map(([name, cp]) => ({
|
|
@@ -486,7 +487,7 @@ function cmdRead(args) {
|
|
|
486
487
|
return;
|
|
487
488
|
}
|
|
488
489
|
|
|
489
|
-
// 3. --overview
|
|
490
|
+
// 3. --overview mode: read workflow overview
|
|
490
491
|
if (args.overview) {
|
|
491
492
|
const stages = data.stages || {};
|
|
492
493
|
const stageList = Object.entries(stages).map(([name, stage]) => ({
|
|
@@ -515,7 +516,7 @@ function cmdRead(args) {
|
|
|
515
516
|
return;
|
|
516
517
|
}
|
|
517
518
|
|
|
518
|
-
// 4. --task-id
|
|
519
|
+
// 4. --task-id mode: query single task
|
|
519
520
|
if (args.taskId) {
|
|
520
521
|
const task = data.tasks?.find(t => t.id === args.taskId);
|
|
521
522
|
if (!task) {
|
|
@@ -525,7 +526,7 @@ function cmdRead(args) {
|
|
|
525
526
|
return;
|
|
526
527
|
}
|
|
527
528
|
|
|
528
|
-
// 5. --status
|
|
529
|
+
// 5. --status mode: filter tasks by status
|
|
529
530
|
if (args.status) {
|
|
530
531
|
const validStatuses = ['pending', 'in_progress', 'partial', 'completed', 'failed'];
|
|
531
532
|
if (!validStatuses.includes(args.status)) {
|
|
@@ -540,7 +541,7 @@ function cmdRead(args) {
|
|
|
540
541
|
return;
|
|
541
542
|
}
|
|
542
543
|
|
|
543
|
-
// 6.
|
|
544
|
+
// 6. Default mode: output entire file
|
|
544
545
|
outputSuccess(`Progress file: ${filePath}`, data);
|
|
545
546
|
} finally {
|
|
546
547
|
if (lockPath) releaseLock(lockPath);
|
|
@@ -548,7 +549,7 @@ function cmdRead(args) {
|
|
|
548
549
|
}
|
|
549
550
|
|
|
550
551
|
/**
|
|
551
|
-
*
|
|
552
|
+
* Command: update-task - Update a single task status
|
|
552
553
|
*/
|
|
553
554
|
function cmdUpdateTask(args) {
|
|
554
555
|
if (!args.file || !args.taskId || !args.status) {
|
|
@@ -567,7 +568,7 @@ function cmdUpdateTask(args) {
|
|
|
567
568
|
lockPath = acquireLock(filePath);
|
|
568
569
|
const data = readJsonFile(filePath);
|
|
569
570
|
|
|
570
|
-
//
|
|
571
|
+
// Find task
|
|
571
572
|
const taskIndex = data.tasks?.findIndex(t => t.id === args.taskId);
|
|
572
573
|
if (taskIndex === -1 || taskIndex === undefined) {
|
|
573
574
|
outputError(`Task not found: ${args.taskId}`);
|
|
@@ -576,15 +577,15 @@ function cmdUpdateTask(args) {
|
|
|
576
577
|
const task = data.tasks[taskIndex];
|
|
577
578
|
const now = getTimestamp();
|
|
578
579
|
|
|
579
|
-
//
|
|
580
|
+
// Update status
|
|
580
581
|
task.status = args.status;
|
|
581
582
|
task.updated_at = now;
|
|
582
583
|
|
|
583
|
-
//
|
|
584
|
+
// Set timestamps based on status (always use real timestamp generated by script, external parameters not accepted)
|
|
584
585
|
if (args.status === 'in_progress') {
|
|
585
586
|
task.started_at = now;
|
|
586
587
|
} else if (args.status === 'partial') {
|
|
587
|
-
// partial
|
|
588
|
+
// partial status: partially completed, may already have started_at, optionally set completed_at
|
|
588
589
|
if (!task.started_at) {
|
|
589
590
|
task.started_at = now;
|
|
590
591
|
}
|
|
@@ -606,14 +607,14 @@ function cmdUpdateTask(args) {
|
|
|
606
607
|
}
|
|
607
608
|
}
|
|
608
609
|
|
|
609
|
-
//
|
|
610
|
+
// Update task
|
|
610
611
|
data.tasks[taskIndex] = task;
|
|
611
612
|
data.updated_at = now;
|
|
612
613
|
|
|
613
|
-
//
|
|
614
|
+
// Recalculate counts
|
|
614
615
|
data.counts = calculateCounts(data.tasks);
|
|
615
616
|
|
|
616
|
-
//
|
|
617
|
+
// Atomic write
|
|
617
618
|
atomicWriteJson(filePath, data);
|
|
618
619
|
|
|
619
620
|
outputSuccess(`Task updated: ${args.taskId}`, {
|
|
@@ -627,7 +628,7 @@ function cmdUpdateTask(args) {
|
|
|
627
628
|
}
|
|
628
629
|
|
|
629
630
|
/**
|
|
630
|
-
*
|
|
631
|
+
* Command: update-counts - Force recalculate counts
|
|
631
632
|
*/
|
|
632
633
|
function cmdUpdateCounts(args) {
|
|
633
634
|
if (!args.file) {
|
|
@@ -645,11 +646,11 @@ function cmdUpdateCounts(args) {
|
|
|
645
646
|
outputError('No tasks array found in progress file');
|
|
646
647
|
}
|
|
647
648
|
|
|
648
|
-
//
|
|
649
|
+
// Recalculate counts
|
|
649
650
|
data.counts = calculateCounts(data.tasks);
|
|
650
651
|
data.updated_at = getTimestamp();
|
|
651
652
|
|
|
652
|
-
//
|
|
653
|
+
// Atomic write
|
|
653
654
|
atomicWriteJson(filePath, data);
|
|
654
655
|
|
|
655
656
|
outputSuccess('Counts updated', { counts: data.counts });
|
|
@@ -659,7 +660,7 @@ function cmdUpdateCounts(args) {
|
|
|
659
660
|
}
|
|
660
661
|
|
|
661
662
|
/**
|
|
662
|
-
*
|
|
663
|
+
* Command: write-checkpoint - Write/update checkpoint
|
|
663
664
|
*/
|
|
664
665
|
function cmdWriteCheckpoint(args) {
|
|
665
666
|
if (!args.file || !args.stage || !args.checkpoint || args.passed === null) {
|
|
@@ -678,7 +679,7 @@ function cmdWriteCheckpoint(args) {
|
|
|
678
679
|
if (fs.existsSync(filePath)) {
|
|
679
680
|
data = readJsonFile(filePath);
|
|
680
681
|
} else {
|
|
681
|
-
//
|
|
682
|
+
// Create new file
|
|
682
683
|
data = {
|
|
683
684
|
stage: args.stage,
|
|
684
685
|
created_at: now,
|
|
@@ -688,12 +689,12 @@ function cmdWriteCheckpoint(args) {
|
|
|
688
689
|
};
|
|
689
690
|
}
|
|
690
691
|
|
|
691
|
-
//
|
|
692
|
+
// Ensure checkpoints object exists
|
|
692
693
|
if (!data.checkpoints) {
|
|
693
694
|
data.checkpoints = {};
|
|
694
695
|
}
|
|
695
696
|
|
|
696
|
-
//
|
|
697
|
+
// Update or create checkpoint
|
|
697
698
|
data.checkpoints[args.checkpoint] = {
|
|
698
699
|
passed: passed,
|
|
699
700
|
checked_at: now,
|
|
@@ -703,13 +704,13 @@ function cmdWriteCheckpoint(args) {
|
|
|
703
704
|
|
|
704
705
|
data.updated_at = now;
|
|
705
706
|
|
|
706
|
-
//
|
|
707
|
+
// Ensure directory exists
|
|
707
708
|
const dir = path.dirname(filePath);
|
|
708
709
|
if (!fs.existsSync(dir)) {
|
|
709
710
|
fs.mkdirSync(dir, { recursive: true });
|
|
710
711
|
}
|
|
711
712
|
|
|
712
|
-
//
|
|
713
|
+
// Atomic write
|
|
713
714
|
atomicWriteJson(filePath, data);
|
|
714
715
|
|
|
715
716
|
outputSuccess(`Checkpoint updated: ${args.checkpoint}`, {
|
|
@@ -723,7 +724,7 @@ function cmdWriteCheckpoint(args) {
|
|
|
723
724
|
}
|
|
724
725
|
|
|
725
726
|
/**
|
|
726
|
-
*
|
|
727
|
+
* Command: update-workflow - Update WORKFLOW-PROGRESS stage status
|
|
727
728
|
*/
|
|
728
729
|
function cmdUpdateWorkflow(args) {
|
|
729
730
|
if (!args.file || !args.stage || !args.status) {
|
|
@@ -746,7 +747,7 @@ function cmdUpdateWorkflow(args) {
|
|
|
746
747
|
if (fs.existsSync(filePath)) {
|
|
747
748
|
data = readJsonFile(filePath);
|
|
748
749
|
} else {
|
|
749
|
-
//
|
|
750
|
+
// Create new file
|
|
750
751
|
data = {
|
|
751
752
|
created_at: now,
|
|
752
753
|
stages: {},
|
|
@@ -754,12 +755,12 @@ function cmdUpdateWorkflow(args) {
|
|
|
754
755
|
};
|
|
755
756
|
}
|
|
756
757
|
|
|
757
|
-
//
|
|
758
|
+
// Ensure stages object exists
|
|
758
759
|
if (!data.stages) {
|
|
759
760
|
data.stages = {};
|
|
760
761
|
}
|
|
761
762
|
|
|
762
|
-
//
|
|
763
|
+
// Get or create stage
|
|
763
764
|
if (!data.stages[args.stage]) {
|
|
764
765
|
data.stages[args.stage] = {
|
|
765
766
|
status: 'pending',
|
|
@@ -772,12 +773,12 @@ function cmdUpdateWorkflow(args) {
|
|
|
772
773
|
|
|
773
774
|
const stage = data.stages[args.stage];
|
|
774
775
|
|
|
775
|
-
//
|
|
776
|
+
// Update status
|
|
776
777
|
stage.status = args.status;
|
|
777
778
|
|
|
778
|
-
//
|
|
779
|
+
// Set timestamps based on status (always use real timestamp generated by script, external parameters not accepted)
|
|
779
780
|
if (args.status === 'in_progress') {
|
|
780
|
-
//
|
|
781
|
+
// Do not overwrite if started_at already has a value
|
|
781
782
|
if (!stage.started_at) {
|
|
782
783
|
stage.started_at = now;
|
|
783
784
|
}
|
|
@@ -787,22 +788,22 @@ function cmdUpdateWorkflow(args) {
|
|
|
787
788
|
stage.confirmed_at = now;
|
|
788
789
|
}
|
|
789
790
|
|
|
790
|
-
//
|
|
791
|
+
// Update output
|
|
791
792
|
if (args.output) {
|
|
792
793
|
stage.output = args.output;
|
|
793
794
|
}
|
|
794
795
|
|
|
795
|
-
//
|
|
796
|
+
// Update current stage
|
|
796
797
|
data.current_stage = args.stage;
|
|
797
798
|
data.updated_at = now;
|
|
798
799
|
|
|
799
|
-
//
|
|
800
|
+
// Ensure directory exists
|
|
800
801
|
const dir = path.dirname(filePath);
|
|
801
802
|
if (!fs.existsSync(dir)) {
|
|
802
803
|
fs.mkdirSync(dir, { recursive: true });
|
|
803
804
|
}
|
|
804
805
|
|
|
805
|
-
//
|
|
806
|
+
// Atomic write
|
|
806
807
|
atomicWriteJson(filePath, data);
|
|
807
808
|
|
|
808
809
|
outputSuccess(`Workflow stage updated: ${args.stage}`, {
|
|
@@ -816,10 +817,10 @@ function cmdUpdateWorkflow(args) {
|
|
|
816
817
|
}
|
|
817
818
|
|
|
818
819
|
/**
|
|
819
|
-
*
|
|
820
|
+
* Command: init-tasks - Scan feature-design directory to generate task list
|
|
820
821
|
*/
|
|
821
822
|
function cmdInitTasks(args) {
|
|
822
|
-
//
|
|
823
|
+
// Argument validation
|
|
823
824
|
if (!args.file || !args.stage || !args.featuresDir || !args.platforms) {
|
|
824
825
|
outputError('Usage: init-tasks --file <path> --stage <stage_name> --features-dir <dir> --platforms <comma-separated> [--force]');
|
|
825
826
|
}
|
|
@@ -828,17 +829,17 @@ function cmdInitTasks(args) {
|
|
|
828
829
|
const featuresDir = path.resolve(args.featuresDir);
|
|
829
830
|
const platforms = args.platforms.split(',').map(p => p.trim()).filter(p => p);
|
|
830
831
|
|
|
831
|
-
//
|
|
832
|
+
// Validate platforms is not empty
|
|
832
833
|
if (platforms.length === 0) {
|
|
833
834
|
outputError('Platforms list cannot be empty');
|
|
834
835
|
}
|
|
835
836
|
|
|
836
|
-
//
|
|
837
|
+
// Validate features-dir exists
|
|
837
838
|
if (!fs.existsSync(featuresDir)) {
|
|
838
839
|
outputError(`Features directory not found: ${featuresDir}`);
|
|
839
840
|
}
|
|
840
841
|
|
|
841
|
-
//
|
|
842
|
+
// Scan .feature-spec.md files
|
|
842
843
|
const featureFiles = [];
|
|
843
844
|
const files = fs.readdirSync(featuresDir);
|
|
844
845
|
for (const file of files) {
|
|
@@ -851,8 +852,8 @@ function cmdInitTasks(args) {
|
|
|
851
852
|
outputError(`No .feature-spec.md files found in: ${featuresDir}`);
|
|
852
853
|
}
|
|
853
854
|
|
|
854
|
-
//
|
|
855
|
-
//
|
|
855
|
+
// Extract feature info from filenames
|
|
856
|
+
// Format: F-{MODULE}-{NNN}-{feature-name}.feature-spec.md
|
|
856
857
|
const featurePattern = /^(F-([A-Z]+)-\d+)-(.+)\.feature-spec\.md$/;
|
|
857
858
|
const features = [];
|
|
858
859
|
|
|
@@ -862,7 +863,7 @@ function cmdInitTasks(args) {
|
|
|
862
863
|
features.push({
|
|
863
864
|
feature_id: match[1], // F-APPT-001
|
|
864
865
|
module: match[2], // APPT
|
|
865
|
-
name: match[3], //
|
|
866
|
+
name: match[3], // appointment-crud
|
|
866
867
|
file: file
|
|
867
868
|
});
|
|
868
869
|
}
|
|
@@ -872,10 +873,10 @@ function cmdInitTasks(args) {
|
|
|
872
873
|
outputError('No valid feature files found. Expected format: F-{MODULE}-{NNN}-{feature-name}.feature-spec.md');
|
|
873
874
|
}
|
|
874
875
|
|
|
875
|
-
//
|
|
876
|
+
// Sort by feature ID
|
|
876
877
|
features.sort((a, b) => a.feature_id.localeCompare(b.feature_id));
|
|
877
878
|
|
|
878
|
-
//
|
|
879
|
+
// Check if target file already has tasks
|
|
879
880
|
if (fs.existsSync(filePath)) {
|
|
880
881
|
const existingData = readJsonFile(filePath);
|
|
881
882
|
if (existingData.tasks && existingData.tasks.length > 0 && !args.force) {
|
|
@@ -883,18 +884,18 @@ function cmdInitTasks(args) {
|
|
|
883
884
|
}
|
|
884
885
|
}
|
|
885
886
|
|
|
886
|
-
//
|
|
887
|
+
// Generate task list
|
|
887
888
|
const tasks = [];
|
|
888
889
|
const now = getTimestamp();
|
|
889
890
|
|
|
890
|
-
// Module
|
|
891
|
+
// Module sort order
|
|
891
892
|
const moduleOrder = ['APPT', 'BASE', 'CUST', 'EMP', 'ITEM', 'KNW', 'REPORT', 'REV', 'SERV'];
|
|
892
893
|
const getModuleIndex = (module) => {
|
|
893
894
|
const idx = moduleOrder.indexOf(module);
|
|
894
895
|
return idx === -1 ? 999 : idx;
|
|
895
896
|
};
|
|
896
897
|
|
|
897
|
-
//
|
|
898
|
+
// Group by module
|
|
898
899
|
const featuresByModule = {};
|
|
899
900
|
for (const feature of features) {
|
|
900
901
|
if (!featuresByModule[feature.module]) {
|
|
@@ -903,12 +904,12 @@ function cmdInitTasks(args) {
|
|
|
903
904
|
featuresByModule[feature.module].push(feature);
|
|
904
905
|
}
|
|
905
906
|
|
|
906
|
-
//
|
|
907
|
+
// Sort by feature ID within each module
|
|
907
908
|
for (const module of Object.keys(featuresByModule)) {
|
|
908
909
|
featuresByModule[module].sort((a, b) => a.feature_id.localeCompare(b.feature_id));
|
|
909
910
|
}
|
|
910
911
|
|
|
911
|
-
//
|
|
912
|
+
// Generate tasks in module order
|
|
912
913
|
const sortedModules = Object.keys(featuresByModule).sort((a, b) => getModuleIndex(a) - getModuleIndex(b));
|
|
913
914
|
|
|
914
915
|
for (const module of sortedModules) {
|
|
@@ -927,7 +928,7 @@ function cmdInitTasks(args) {
|
|
|
927
928
|
}
|
|
928
929
|
}
|
|
929
930
|
|
|
930
|
-
//
|
|
931
|
+
// Create progress file structure
|
|
931
932
|
const progressData = {
|
|
932
933
|
stage: args.stage,
|
|
933
934
|
created_at: now,
|
|
@@ -937,13 +938,13 @@ function cmdInitTasks(args) {
|
|
|
937
938
|
checkpoints: {}
|
|
938
939
|
};
|
|
939
940
|
|
|
940
|
-
//
|
|
941
|
+
// Ensure directory exists
|
|
941
942
|
const dir = path.dirname(filePath);
|
|
942
943
|
if (!fs.existsSync(dir)) {
|
|
943
944
|
fs.mkdirSync(dir, { recursive: true });
|
|
944
945
|
}
|
|
945
946
|
|
|
946
|
-
//
|
|
947
|
+
// Acquire lock and write
|
|
947
948
|
let lockPath = null;
|
|
948
949
|
try {
|
|
949
950
|
lockPath = acquireLock(filePath);
|
|
@@ -964,13 +965,13 @@ function cmdInitTasks(args) {
|
|
|
964
965
|
}
|
|
965
966
|
|
|
966
967
|
// ============================================================================
|
|
967
|
-
//
|
|
968
|
+
// Main Entry
|
|
968
969
|
// ============================================================================
|
|
969
970
|
|
|
970
971
|
function main() {
|
|
971
972
|
const args = parseArgs();
|
|
972
973
|
|
|
973
|
-
//
|
|
974
|
+
// Show help when no command
|
|
974
975
|
if (!args.command) {
|
|
975
976
|
console.error('Usage: node update-progress.js <command> [options]');
|
|
976
977
|
console.error('');
|
|
@@ -981,13 +982,13 @@ function main() {
|
|
|
981
982
|
console.error(' update-counts Recalculate task counts');
|
|
982
983
|
console.error(' write-checkpoint Write or update a checkpoint');
|
|
983
984
|
console.error(' update-workflow Update a workflow stage status');
|
|
984
|
-
console.error(' init-tasks
|
|
985
|
+
console.error(' init-tasks Generate tasks from feature-spec files');
|
|
985
986
|
console.error('');
|
|
986
987
|
console.error('Run "node update-progress.js <command> --help" for more information.');
|
|
987
988
|
process.exit(1);
|
|
988
989
|
}
|
|
989
990
|
|
|
990
|
-
//
|
|
991
|
+
// Dispatch command
|
|
991
992
|
try {
|
|
992
993
|
switch (args.command) {
|
|
993
994
|
case 'init':
|