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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speccrew",
3
- "version": "0.7.37",
3
+ "version": "0.7.39",
4
4
  "description": "Spec-Driven Development toolkit for AI-powered IDEs",
5
5
  "author": "charlesmu99",
6
6
  "repository": {
@@ -137,7 +137,7 @@ function outputError(error) {
137
137
  */
138
138
  function acquireLock(filePath) {
139
139
  const lockPath = `${filePath}.lock`;
140
- const maxRetries = 30;
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 > 60) {
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
- // Wait 1 second before retry
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 < 1000) {
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
- const content = fs.readFileSync(filePath, 'utf8');
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
- const taskIndex = data.tasks?.findIndex(t => t.id === args.taskId);
614
- if (taskIndex === -1 || taskIndex === undefined) {
615
- outputError(`Task not found: ${args.taskId}`);
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
- const task = data.tasks[taskIndex];
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
- data.tasks[taskIndex] = task;
696
+ taskArray[taskIndex] = task;
663
697
  data.updated_at = now;
664
698
 
665
699
  // Recalculate counts
666
- data.counts = calculateCounts(data.tasks);
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);