speccrew 0.7.38 → 0.7.40

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.38",
3
+ "version": "0.7.40",
4
4
  "description": "Spec-Driven Development toolkit for AI-powered IDEs",
5
5
  "author": "charlesmu99",
6
6
  "repository": {
@@ -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 > 120) {
155
+ if (ageSeconds > 30) {
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
@@ -161,10 +161,32 @@ function acquireLock(filePath) {
161
161
  } catch (statErr) {
162
162
  // Lock file was deleted during stat, continue retrying
163
163
  }
164
+ } else {
165
+ // Non-EEXIST error (EACCES, EPERM, etc.) - log for debugging
166
+ if (retryCount === 0) {
167
+ console.error(`Warning: Lock creation failed with ${error.code} for: ${lockPath}`);
168
+ }
164
169
  }
165
170
  retryCount++;
166
171
  if (retryCount >= maxRetries) {
167
- throw new Error(`Failed to acquire file lock for '${filePath}' after ${maxRetries} attempts`);
172
+ // Force acquire as last resort
173
+ try {
174
+ // Remove any existing lock file regardless of age
175
+ if (fs.existsSync(lockPath)) {
176
+ fs.unlinkSync(lockPath);
177
+ }
178
+ // Brief random delay to avoid thundering herd on force acquire
179
+ const forceDelay = Math.floor(Math.random() * 500);
180
+ const forceStart = Date.now();
181
+ while (Date.now() - forceStart < forceDelay) {}
182
+
183
+ const fd = fs.openSync(lockPath, 'wx');
184
+ fs.closeSync(fd);
185
+ console.error(`Warning: Lock acquired by force after ${maxRetries} retries for: ${filePath}`);
186
+ return lockPath;
187
+ } catch (forceErr) {
188
+ throw new Error(`Failed to acquire file lock for '${filePath}' after ${maxRetries} attempts (force acquire also failed: ${forceErr.code})`);
189
+ }
168
190
  }
169
191
  // Retry with jitter to avoid thundering herd
170
192
  const delay = 200 + Math.floor(Math.random() * 300);
@@ -616,13 +638,38 @@ function cmdUpdateTask(args) {
616
638
  lockPath = acquireLock(filePath);
617
639
  const data = readJsonFile(filePath);
618
640
 
619
- // Find task
620
- const taskIndex = data.tasks?.findIndex(t => t.id === args.taskId);
621
- if (taskIndex === -1 || taskIndex === undefined) {
622
- outputError(`Task not found: ${args.taskId}`);
641
+ // Find task (support both flat structure and nested stages structure)
642
+ let task = null;
643
+ let taskIndex = -1;
644
+ let taskArray = null;
645
+ let isStageMode = false;
646
+ let targetStage = null;
647
+
648
+ if (args.stage) {
649
+ // Nested structure: stages.{stage}.features
650
+ isStageMode = true;
651
+ if (!data.stages || !data.stages[args.stage]) {
652
+ outputError(`Stage not found: ${args.stage}`);
653
+ }
654
+ targetStage = data.stages[args.stage];
655
+ if (!targetStage.features || !Array.isArray(targetStage.features)) {
656
+ outputError(`Stage has no features array: ${args.stage}`);
657
+ }
658
+ taskArray = targetStage.features;
659
+ taskIndex = taskArray.findIndex(t => t.id === args.taskId);
660
+ if (taskIndex === -1) {
661
+ outputError(`Task not found in stage ${args.stage}: ${args.taskId}`);
662
+ }
663
+ } else {
664
+ // Flat structure: data.tasks
665
+ taskArray = data.tasks;
666
+ taskIndex = taskArray?.findIndex(t => t.id === args.taskId);
667
+ if (taskIndex === -1 || taskIndex === undefined) {
668
+ outputError(`Task not found: ${args.taskId}`);
669
+ }
623
670
  }
624
671
 
625
- const task = data.tasks[taskIndex];
672
+ task = taskArray[taskIndex];
626
673
  const now = getTimestamp();
627
674
 
628
675
  // Update status
@@ -668,11 +715,17 @@ function cmdUpdateTask(args) {
668
715
  }
669
716
 
670
717
  // Update task
671
- data.tasks[taskIndex] = task;
718
+ taskArray[taskIndex] = task;
672
719
  data.updated_at = now;
673
720
 
674
721
  // Recalculate counts
675
- data.counts = calculateCounts(data.tasks);
722
+ if (isStageMode && targetStage.counts) {
723
+ // Update stage-level counts
724
+ targetStage.counts = calculateCounts(taskArray);
725
+ } else {
726
+ // Update global counts
727
+ data.counts = calculateCounts(data.tasks);
728
+ }
676
729
 
677
730
  // Atomic write
678
731
  atomicWriteJson(filePath, data);