speccrew 0.7.37 → 0.7.39
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/package.json
CHANGED
|
@@ -137,7 +137,7 @@ function outputError(error) {
|
|
|
137
137
|
*/
|
|
138
138
|
function acquireLock(filePath) {
|
|
139
139
|
const lockPath = `${filePath}.lock`;
|
|
140
|
-
const maxRetries =
|
|
140
|
+
const maxRetries = 50;
|
|
141
141
|
let retryCount = 0;
|
|
142
142
|
|
|
143
143
|
while (retryCount < maxRetries) {
|
|
@@ -152,7 +152,7 @@ function acquireLock(filePath) {
|
|
|
152
152
|
try {
|
|
153
153
|
const lockStat = fs.statSync(lockPath);
|
|
154
154
|
const ageSeconds = (Date.now() - lockStat.mtimeMs) / 1000;
|
|
155
|
-
if (ageSeconds >
|
|
155
|
+
if (ageSeconds > 120) {
|
|
156
156
|
console.error(`Warning: Stale lock file detected (age: ${Math.round(ageSeconds)}s), removing: ${lockPath}`);
|
|
157
157
|
fs.unlinkSync(lockPath);
|
|
158
158
|
// Do not consume retry count, continue to next loop attempt to acquire lock
|
|
@@ -166,9 +166,10 @@ function acquireLock(filePath) {
|
|
|
166
166
|
if (retryCount >= maxRetries) {
|
|
167
167
|
throw new Error(`Failed to acquire file lock for '${filePath}' after ${maxRetries} attempts`);
|
|
168
168
|
}
|
|
169
|
-
//
|
|
169
|
+
// Retry with jitter to avoid thundering herd
|
|
170
|
+
const delay = 200 + Math.floor(Math.random() * 300);
|
|
170
171
|
const start = Date.now();
|
|
171
|
-
while (Date.now() - start <
|
|
172
|
+
while (Date.now() - start < delay) {
|
|
172
173
|
// Busy wait
|
|
173
174
|
}
|
|
174
175
|
}
|
|
@@ -209,7 +210,13 @@ function readJsonFile(filePath) {
|
|
|
209
210
|
if (!fs.existsSync(filePath)) {
|
|
210
211
|
throw new Error(`File not found: ${filePath}`);
|
|
211
212
|
}
|
|
212
|
-
|
|
213
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
214
|
+
|
|
215
|
+
// Remove UTF-8 BOM if present
|
|
216
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
217
|
+
content = content.slice(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
213
220
|
try {
|
|
214
221
|
return JSON.parse(content);
|
|
215
222
|
} catch (e) {
|
|
@@ -569,7 +576,7 @@ function cmdRead(args) {
|
|
|
569
576
|
|
|
570
577
|
// 5. --status mode: filter tasks by status
|
|
571
578
|
if (args.status) {
|
|
572
|
-
const validStatuses = ['pending', 'in_progress', 'partial', 'completed', 'failed'];
|
|
579
|
+
const validStatuses = ['pending', 'in_progress', 'partial', 'completed', 'failed', 'confirmed'];
|
|
573
580
|
if (!validStatuses.includes(args.status)) {
|
|
574
581
|
outputError(`Invalid status filter: ${args.status}. Must be one of: ${validStatuses.join(', ')}`);
|
|
575
582
|
}
|
|
@@ -597,7 +604,7 @@ function cmdUpdateTask(args) {
|
|
|
597
604
|
outputError('Usage: update-task --file <path> --task-id <id> --status <status> [options]');
|
|
598
605
|
}
|
|
599
606
|
|
|
600
|
-
const validStatuses = ['pending', 'in_progress', 'partial', 'completed', 'failed'];
|
|
607
|
+
const validStatuses = ['pending', 'in_progress', 'partial', 'completed', 'failed', 'confirmed'];
|
|
601
608
|
if (!validStatuses.includes(args.status)) {
|
|
602
609
|
outputError(`Invalid status: ${args.status}. Must be one of: ${validStatuses.join(', ')}`);
|
|
603
610
|
}
|
|
@@ -609,13 +616,38 @@ function cmdUpdateTask(args) {
|
|
|
609
616
|
lockPath = acquireLock(filePath);
|
|
610
617
|
const data = readJsonFile(filePath);
|
|
611
618
|
|
|
612
|
-
// Find task
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
619
|
+
// Find task (support both flat structure and nested stages structure)
|
|
620
|
+
let task = null;
|
|
621
|
+
let taskIndex = -1;
|
|
622
|
+
let taskArray = null;
|
|
623
|
+
let isStageMode = false;
|
|
624
|
+
let targetStage = null;
|
|
625
|
+
|
|
626
|
+
if (args.stage) {
|
|
627
|
+
// Nested structure: stages.{stage}.features
|
|
628
|
+
isStageMode = true;
|
|
629
|
+
if (!data.stages || !data.stages[args.stage]) {
|
|
630
|
+
outputError(`Stage not found: ${args.stage}`);
|
|
631
|
+
}
|
|
632
|
+
targetStage = data.stages[args.stage];
|
|
633
|
+
if (!targetStage.features || !Array.isArray(targetStage.features)) {
|
|
634
|
+
outputError(`Stage has no features array: ${args.stage}`);
|
|
635
|
+
}
|
|
636
|
+
taskArray = targetStage.features;
|
|
637
|
+
taskIndex = taskArray.findIndex(t => t.id === args.taskId);
|
|
638
|
+
if (taskIndex === -1) {
|
|
639
|
+
outputError(`Task not found in stage ${args.stage}: ${args.taskId}`);
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
// Flat structure: data.tasks
|
|
643
|
+
taskArray = data.tasks;
|
|
644
|
+
taskIndex = taskArray?.findIndex(t => t.id === args.taskId);
|
|
645
|
+
if (taskIndex === -1 || taskIndex === undefined) {
|
|
646
|
+
outputError(`Task not found: ${args.taskId}`);
|
|
647
|
+
}
|
|
616
648
|
}
|
|
617
649
|
|
|
618
|
-
|
|
650
|
+
task = taskArray[taskIndex];
|
|
619
651
|
const now = getTimestamp();
|
|
620
652
|
|
|
621
653
|
// Update status
|
|
@@ -638,6 +670,8 @@ function cmdUpdateTask(args) {
|
|
|
638
670
|
if (args.output) {
|
|
639
671
|
task.output = args.output;
|
|
640
672
|
}
|
|
673
|
+
} else if (args.status === 'confirmed') {
|
|
674
|
+
task.confirmed_at = now;
|
|
641
675
|
} else if (args.status === 'failed') {
|
|
642
676
|
task.completed_at = now;
|
|
643
677
|
if (args.error) {
|
|
@@ -659,11 +693,17 @@ function cmdUpdateTask(args) {
|
|
|
659
693
|
}
|
|
660
694
|
|
|
661
695
|
// Update task
|
|
662
|
-
|
|
696
|
+
taskArray[taskIndex] = task;
|
|
663
697
|
data.updated_at = now;
|
|
664
698
|
|
|
665
699
|
// Recalculate counts
|
|
666
|
-
|
|
700
|
+
if (isStageMode && targetStage.counts) {
|
|
701
|
+
// Update stage-level counts
|
|
702
|
+
targetStage.counts = calculateCounts(taskArray);
|
|
703
|
+
} else {
|
|
704
|
+
// Update global counts
|
|
705
|
+
data.counts = calculateCounts(data.tasks);
|
|
706
|
+
}
|
|
667
707
|
|
|
668
708
|
// Atomic write
|
|
669
709
|
atomicWriteJson(filePath, data);
|