specweave 0.26.5 → 0.26.10

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.
Files changed (66) hide show
  1. package/CLAUDE.md +35 -5
  2. package/bin/specweave.js +15 -0
  3. package/dist/plugins/specweave-github/lib/completion-calculator.js +2 -2
  4. package/dist/plugins/specweave-github/lib/completion-calculator.js.map +1 -1
  5. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +28 -1
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.js +191 -19
  8. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  9. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts +3 -0
  10. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -1
  11. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +25 -2
  12. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -1
  13. package/dist/src/cli/commands/archive.d.ts +10 -0
  14. package/dist/src/cli/commands/archive.d.ts.map +1 -0
  15. package/dist/src/cli/commands/archive.js +78 -0
  16. package/dist/src/cli/commands/archive.js.map +1 -0
  17. package/dist/src/cli/commands/init.js +2 -2
  18. package/dist/src/cli/commands/init.js.map +1 -1
  19. package/dist/src/cli/helpers/init/initial-increment-generator.d.ts.map +1 -1
  20. package/dist/src/cli/helpers/init/initial-increment-generator.js +48 -8
  21. package/dist/src/cli/helpers/init/initial-increment-generator.js.map +1 -1
  22. package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
  23. package/dist/src/core/increment/metadata-manager.js +15 -12
  24. package/dist/src/core/increment/metadata-manager.js.map +1 -1
  25. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  26. package/dist/src/core/living-docs/living-docs-sync.js +43 -6
  27. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  28. package/dist/src/init/repo/types.d.ts +1 -1
  29. package/package.json +1 -1
  30. package/plugins/specweave/agents/pm/AGENT.md +13 -7
  31. package/plugins/specweave/commands/specweave-archive.md +13 -1
  32. package/plugins/specweave/commands/sync-diagnostics.md +227 -0
  33. package/plugins/specweave/hooks/docs-changed.sh.backup +79 -0
  34. package/plugins/specweave/hooks/human-input-required.sh.backup +75 -0
  35. package/plugins/specweave/hooks/post-first-increment.sh.backup +61 -0
  36. package/plugins/specweave/hooks/post-increment-change.sh.backup +98 -0
  37. package/plugins/specweave/hooks/post-increment-completion.sh.backup +231 -0
  38. package/plugins/specweave/hooks/post-increment-planning.sh.backup +1048 -0
  39. package/plugins/specweave/hooks/post-increment-status-change.sh.backup +147 -0
  40. package/plugins/specweave/hooks/post-spec-update.sh.backup +158 -0
  41. package/plugins/specweave/hooks/post-user-story-complete.sh.backup +179 -0
  42. package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +83 -0
  43. package/plugins/specweave/hooks/pre-implementation.sh.backup +67 -0
  44. package/plugins/specweave/hooks/pre-task-completion.sh.backup +194 -0
  45. package/plugins/specweave/hooks/pre-tool-use.sh.backup +133 -0
  46. package/plugins/specweave/hooks/user-prompt-submit.sh +20 -8
  47. package/plugins/specweave/hooks/user-prompt-submit.sh.backup +386 -0
  48. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +15 -12
  49. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
  50. package/plugins/specweave/skills/increment-planner/SKILL.md +57 -7
  51. package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +353 -0
  52. package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +172 -0
  53. package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
  54. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  55. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +1080 -0
  56. package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
  57. package/plugins/specweave-github/lib/completion-calculator.js +1 -1
  58. package/plugins/specweave-github/lib/completion-calculator.ts +2 -2
  59. package/plugins/specweave-github/lib/github-feature-sync.js +152 -18
  60. package/plugins/specweave-github/lib/github-feature-sync.ts +225 -22
  61. package/plugins/specweave-github/lib/user-story-issue-builder.js +21 -1
  62. package/plugins/specweave-github/lib/user-story-issue-builder.ts +31 -3
  63. package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +172 -0
  64. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
  65. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +981 -0
  66. package/plugins/specweave-release/hooks/post-task-completion.sh.backup +110 -0
@@ -57,6 +57,11 @@ export class GitHubFeatureSync {
57
57
  private projectRoot: string;
58
58
  private calculator: CompletionCalculator;
59
59
 
60
+ // SYNC LOCK: Prevent concurrent syncs of the same feature
61
+ // Maps featureId → last sync timestamp
62
+ private static syncLocks: Map<string, number> = new Map();
63
+ private static readonly LOCK_DURATION_MS = 30000; // 30 seconds
64
+
60
65
  constructor(client: GitHubClientV2, specsDir: string, projectRoot: string) {
61
66
  this.client = client;
62
67
  this.specsDir = specsDir;
@@ -80,6 +85,30 @@ export class GitHubFeatureSync {
80
85
  issuesUpdated: number;
81
86
  userStoriesProcessed: number;
82
87
  }> {
88
+ // SYNC LOCK CHECK: Prevent concurrent/rapid syncs of the same feature
89
+ // Root cause: Two sync paths (task completion + status change) can fire simultaneously
90
+ // Result: Duplicate GitHub comments due to race condition
91
+ const now = Date.now();
92
+ const lastSync = GitHubFeatureSync.syncLocks.get(featureId);
93
+
94
+ if (lastSync && (now - lastSync) < GitHubFeatureSync.LOCK_DURATION_MS) {
95
+ const secondsRemaining = Math.ceil((GitHubFeatureSync.LOCK_DURATION_MS - (now - lastSync)) / 1000);
96
+ console.log(`\n⏭️ Sync already in progress for ${featureId} (or completed ${Math.floor((now - lastSync) / 1000)}s ago)`);
97
+ console.log(` ℹ️ Sync will be available in ${secondsRemaining}s to prevent duplicates`);
98
+ console.log(` 💡 This prevents race conditions between task completion and status change syncs`);
99
+
100
+ // Return placeholder result (sync was skipped, not failed)
101
+ return {
102
+ milestoneNumber: 0,
103
+ milestoneUrl: '',
104
+ issuesCreated: 0,
105
+ issuesUpdated: 0,
106
+ userStoriesProcessed: 0
107
+ };
108
+ }
109
+
110
+ // Acquire lock
111
+ GitHubFeatureSync.syncLocks.set(featureId, now);
83
112
  console.log(`\n🔄 Syncing Feature ${featureId} to GitHub...`);
84
113
 
85
114
  // 1. Load Feature FEATURE.md
@@ -216,13 +245,20 @@ export class GitHubFeatureSync {
216
245
  // Update User Story frontmatter with issue link
217
246
  await this.updateUserStoryFrontmatter(userStory.filePath, issueNumber);
218
247
 
248
+ // ✅ CRITICAL FIX (2025-11-24): Check completion for ALL issues (new AND reused)
249
+ // BUG: Previously only checked completion for reused issues, not new ones
250
+ // RESULT: New issues stayed OPEN even if status:complete
251
+ //
252
+ // Now we always call updateUserStoryIssue() which:
253
+ // 1. Calculates ACTUAL completion from [x] checkboxes
254
+ // 2. Closes issue if all ACs and tasks verified complete
255
+ // 3. Updates status labels automatically
256
+ await this.updateUserStoryIssue(issueNumber, issueContent, userStory.filePath);
257
+
219
258
  // Update completion tracking
220
259
  if (result.wasReused) {
221
- // Update existing issue with latest content
222
- await this.updateUserStoryIssue(issueNumber, issueContent, userStory.filePath);
223
260
  issuesUpdated++;
224
261
  } else {
225
- // New issue created
226
262
  issuesCreated++;
227
263
  }
228
264
  }
@@ -336,13 +372,40 @@ export class GitHubFeatureSync {
336
372
  }
337
373
 
338
374
  /**
339
- * Create GitHub Milestone for Feature
375
+ * Create GitHub Milestone for Feature (with duplicate detection)
340
376
  */
341
377
  private async createMilestone(featureData: FeatureFrontmatter): Promise<{
342
378
  number: number;
343
379
  url: string;
344
380
  }> {
345
381
  const title = `${featureData.id}: ${featureData.title}`;
382
+
383
+ // CRITICAL: Check if milestone already exists before creating
384
+ const existingResult = await execFileNoThrow('gh', [
385
+ 'api',
386
+ 'repos/:owner/:repo/milestones',
387
+ '--jq',
388
+ `.[] | select(.title == "${title}") | {number, html_url}`,
389
+ ]);
390
+
391
+ // DEBUG: Log detection result
392
+ console.log(` 🔍 Milestone detection: exitCode=${existingResult.exitCode}, stdout length=${existingResult.stdout.length}`);
393
+ if (existingResult.exitCode !== 0) {
394
+ console.log(` ⚠️ Detection failed: ${existingResult.stderr}`);
395
+ }
396
+
397
+ if (existingResult.exitCode === 0 && existingResult.stdout.trim()) {
398
+ const existing = JSON.parse(existingResult.stdout);
399
+ console.log(` ♻️ Reusing existing Milestone #${existing.number}`);
400
+ return {
401
+ number: existing.number,
402
+ url: existing.html_url,
403
+ };
404
+ }
405
+
406
+ console.log(` ℹ️ No existing milestone found, creating new one...`);
407
+
408
+ // Milestone doesn't exist, create new one
346
409
  const description = `Feature ${featureData.id}\n\nStatus: ${featureData.status}\nCreated: ${featureData.created}`;
347
410
 
348
411
  const result = await execFileNoThrow('gh', [
@@ -428,17 +491,10 @@ export class GitHubFeatureSync {
428
491
  ` ✅ Created and verified complete: ${completion.acsCompleted}/${completion.acsTotal} ACs, ${completion.tasksCompleted}/${completion.tasksTotal} tasks`
429
492
  );
430
493
  } else {
431
- // ⚠️ INCOMPLETE - Leave open with progress comment
432
- await execFileNoThrow('gh', [
433
- 'issue',
434
- 'comment',
435
- issueNumber.toString(),
436
- '--body',
437
- this.calculator.buildProgressComment(completion),
438
- ]);
439
- console.log(
440
- ` 📊 Created: ${completion.acsPercentage.toFixed(0)}% ACs, ${completion.tasksPercentage.toFixed(0)}% tasks`
441
- );
494
+ // ⚠️ INCOMPLETE - Leave open with progress comment (with deduplication)
495
+ // Note: For newly created issues, this is the first comment so deduplication
496
+ // will likely pass through, but the logic is here for consistency
497
+ await this.postProgressCommentIfChanged(issueNumber, completion);
442
498
  }
443
499
 
444
500
  return issueNumber;
@@ -511,21 +567,168 @@ export class GitHubFeatureSync {
511
567
  ` ⚠️ Reopened: ${completion.blockingAcs.length + completion.blockingTasks.length} items incomplete`
512
568
  );
513
569
  } else {
514
- // Update progress comment
570
+ // Update progress comment (with deduplication)
571
+ await this.postProgressCommentIfChanged(issueNumber, completion);
572
+ }
573
+ }
574
+
575
+ // **NEW (2025-11-24)**: Update status labels based on completion
576
+ await this.updateStatusLabels(issueNumber, completion);
577
+ }
578
+
579
+ /**
580
+ * Update status labels on GitHub issue based on completion state
581
+ *
582
+ * SMART LABEL MANAGEMENT:
583
+ * - Only manages status:* labels (status:not_started, status:in-progress, status:completed)
584
+ * - Preserves all other labels (priority, type, custom labels)
585
+ * - Ensures exactly one status label is present
586
+ */
587
+ private async updateStatusLabels(
588
+ issueNumber: number,
589
+ completion: {
590
+ overallComplete: boolean;
591
+ acsPercentage: number;
592
+ tasksPercentage: number;
593
+ }
594
+ ): Promise<void> {
595
+ try {
596
+ // Get current issue labels
597
+ const issueData = await this.client.getIssue(issueNumber);
598
+ const currentLabels = issueData.labels || [];
599
+
600
+ // Separate status labels from other labels
601
+ const statusLabels = currentLabels.filter((label: string) => label.startsWith('status:'));
602
+ const otherLabels = currentLabels.filter((label: string) => !label.startsWith('status:'));
603
+
604
+ // Determine correct status label based on completion
605
+ // NOTE: Label names must match repository labels exactly
606
+ let newStatusLabel: string;
607
+ if (completion.overallComplete) {
608
+ newStatusLabel = 'status:complete'; // Repository uses "complete" not "completed"
609
+ } else if (completion.acsPercentage > 0 || completion.tasksPercentage > 0) {
610
+ newStatusLabel = 'status:active'; // Repository uses "active" not "in-progress"
611
+ } else {
612
+ newStatusLabel = 'status:not_started';
613
+ }
614
+
615
+ // Check if update needed
616
+ const needsUpdate = statusLabels.length !== 1 || !statusLabels.includes(newStatusLabel);
617
+
618
+ if (!needsUpdate) {
619
+ return; // Status label already correct
620
+ }
621
+
622
+ // Update labels using gh CLI
623
+ // Strategy: Remove old status labels first (if any), then add new one
624
+
625
+ // Step 1: Remove old status labels (only if they exist)
626
+ if (statusLabels.length > 0) {
515
627
  await execFileNoThrow('gh', [
516
628
  'issue',
517
- 'comment',
629
+ 'edit',
518
630
  issueNumber.toString(),
519
- '--body',
520
- this.calculator.buildProgressComment(completion),
631
+ '--remove-label',
632
+ ...statusLabels,
521
633
  ]);
634
+ }
635
+
636
+ // Step 2: Add new status label
637
+ const result = await execFileNoThrow('gh', [
638
+ 'issue',
639
+ 'edit',
640
+ issueNumber.toString(),
641
+ '--add-label',
642
+ newStatusLabel,
643
+ ]);
644
+
645
+ if (result.exitCode === 0) {
646
+ console.log(` 🏷️ Updated label: ${newStatusLabel}`);
647
+ } else {
648
+ console.warn(` ⚠️ Failed to add label ${newStatusLabel}: ${result.stderr}`);
649
+ }
650
+ } catch (error) {
651
+ // Non-blocking: Label update failure shouldn't break sync
652
+ console.warn(` ⚠️ Failed to update status labels: ${(error as Error).message}`);
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Post progress comment only if it differs from the last comment
658
+ *
659
+ * DEDUPLICATION FIX (2025-11-24):
660
+ * - Prevents posting identical consecutive comments
661
+ * - Fetches last comment from issue
662
+ * - Compares content (ignoring timestamps)
663
+ * - Only posts if progress has changed
664
+ *
665
+ * Root Cause: updateUserStoryIssue() was posting progress comments on EVERY sync,
666
+ * even when progress hadn't changed, causing 4+ duplicate comments.
667
+ *
668
+ * @param issueNumber - GitHub issue number
669
+ * @param completion - Completion status with AC/task metrics
670
+ */
671
+ private async postProgressCommentIfChanged(
672
+ issueNumber: number,
673
+ completion: any
674
+ ): Promise<void> {
675
+ try {
676
+ // 1. Fetch last comment from the issue
677
+ const commentsResult = await execFileNoThrow('gh', [
678
+ 'api',
679
+ 'repos/:owner/:repo/issues/' + issueNumber + '/comments',
680
+ '--jq',
681
+ '.[-1] | {body: .body, created_at: .created_at}', // Get last comment only
682
+ ]);
683
+
684
+ let lastCommentBody = '';
685
+ if (commentsResult.exitCode === 0 && commentsResult.stdout.trim()) {
686
+ try {
687
+ const lastComment = JSON.parse(commentsResult.stdout);
688
+ lastCommentBody = lastComment.body || '';
689
+ } catch {
690
+ // No valid last comment, proceed with posting
691
+ }
692
+ }
693
+
694
+ // 2. Build new progress comment
695
+ const newCommentBody = this.calculator.buildProgressComment(completion);
696
+
697
+ // 3. Normalize both comments for comparison (remove timestamps, whitespace differences)
698
+ const normalizeComment = (text: string): string => {
699
+ return text
700
+ .replace(/🤖 Auto-updated by SpecWeave AC Completion Gate/g, '')
701
+ .replace(/\s+/g, ' ')
702
+ .trim();
703
+ };
704
+
705
+ const normalizedLast = normalizeComment(lastCommentBody);
706
+ const normalizedNew = normalizeComment(newCommentBody);
707
+
708
+ // 4. Check if comments are identical (ignoring formatting differences)
709
+ if (normalizedLast === normalizedNew) {
522
710
  console.log(
523
- ` 📊 Progress: ${completion.acsPercentage.toFixed(0)}% ACs, ${completion.tasksPercentage.toFixed(0)}% tasks`
711
+ ` ⏭️ Progress unchanged (${completion.acsPercentage.toFixed(0)}% ACs, ${completion.tasksPercentage.toFixed(0)}% tasks) - skipping duplicate comment`
524
712
  );
713
+ return;
525
714
  }
526
- }
527
715
 
528
- // Note: Labels are not updated to avoid overwriting manually added labels
716
+ // 5. Post new comment only if progress has changed
717
+ await execFileNoThrow('gh', [
718
+ 'issue',
719
+ 'comment',
720
+ issueNumber.toString(),
721
+ '--body',
722
+ newCommentBody,
723
+ ]);
724
+ console.log(
725
+ ` 📊 Progress: ${completion.acsPercentage.toFixed(0)}% ACs, ${completion.tasksPercentage.toFixed(0)}% tasks (updated)`
726
+ );
727
+
728
+ } catch (error) {
729
+ // Non-blocking: Log error but don't break sync
730
+ console.error(` ⚠️ Failed to check/post progress comment: ${(error as Error).message}`);
731
+ }
529
732
  }
530
733
 
531
734
  /**
@@ -349,11 +349,31 @@ User Story ID: ${frontmatter.id}`
349
349
  }
350
350
  /**
351
351
  * Build labels for the issue
352
+ *
353
+ * CRITICAL: Label names must match repository labels exactly!
354
+ * Repository uses: status:complete, status:active, status:not_started
352
355
  */
353
356
  buildLabels(frontmatter) {
354
357
  const labels = ["user-story", "specweave"];
355
358
  if (frontmatter.status) {
356
- labels.push(`status:${frontmatter.status}`);
359
+ let statusLabel;
360
+ switch (frontmatter.status) {
361
+ case "completed":
362
+ case "complete":
363
+ statusLabel = "status:complete";
364
+ break;
365
+ case "active":
366
+ case "in-progress":
367
+ statusLabel = "status:active";
368
+ break;
369
+ case "planning":
370
+ case "not-started":
371
+ statusLabel = "status:not_started";
372
+ break;
373
+ default:
374
+ statusLabel = `status:${frontmatter.status}`;
375
+ }
376
+ labels.push(statusLabel);
357
377
  }
358
378
  if (frontmatter.priority) {
359
379
  labels.push(frontmatter.priority.toLowerCase());
@@ -23,7 +23,7 @@ interface UserStoryFrontmatter {
23
23
  id: string;
24
24
  feature: string;
25
25
  title: string;
26
- status: 'complete' | 'active' | 'planning' | 'not-started';
26
+ status: 'complete' | 'completed' | 'active' | 'in-progress' | 'planning' | 'not-started';
27
27
  project?: string; // ✅ Optional - not all user stories specify project
28
28
  priority?: string;
29
29
  created: string;
@@ -557,13 +557,41 @@ export class UserStoryIssueBuilder {
557
557
 
558
558
  /**
559
559
  * Build labels for the issue
560
+ *
561
+ * CRITICAL: Label names must match repository labels exactly!
562
+ * Repository uses: status:complete, status:active, status:not_started
560
563
  */
561
564
  private buildLabels(frontmatter: UserStoryFrontmatter): string[] {
562
565
  const labels: string[] = ['user-story', 'specweave'];
563
566
 
564
- // Add status label
567
+ // Add status label with proper mapping
568
+ // Map living docs status values to GitHub repository label names
565
569
  if (frontmatter.status) {
566
- labels.push(`status:${frontmatter.status}`);
570
+ let statusLabel: string;
571
+
572
+ // Map status values to correct GitHub labels
573
+ switch (frontmatter.status) {
574
+ case 'completed':
575
+ case 'complete':
576
+ statusLabel = 'status:complete'; // Repository uses "complete" not "completed"
577
+ break;
578
+
579
+ case 'active':
580
+ case 'in-progress':
581
+ statusLabel = 'status:active'; // Repository uses "active" not "in-progress"
582
+ break;
583
+
584
+ case 'planning':
585
+ case 'not-started':
586
+ statusLabel = 'status:not_started'; // Note: underscore, not dash!
587
+ break;
588
+
589
+ default:
590
+ // Defensive: Use original value if unknown
591
+ statusLabel = `status:${frontmatter.status}`;
592
+ }
593
+
594
+ labels.push(statusLabel);
567
595
  }
568
596
 
569
597
  // Add priority label
@@ -0,0 +1,172 @@
1
+ #!/bin/bash
2
+
3
+ # SpecWeave JIRA Sync Hook
4
+ # Runs after task completion to sync progress to JIRA Issues
5
+ #
6
+ # This hook is part of the specweave-jira plugin and handles:
7
+ # - Syncing task completion state to JIRA issues
8
+ # - Updating JIRA issue status based on increment progress
9
+ #
10
+ # Dependencies:
11
+ # - Node.js for running sync scripts
12
+ # - jq for JSON parsing
13
+ # - metadata.json must have .jira.issue field
14
+ # - JIRA API credentials in .env
15
+
16
+ set -e
17
+
18
+ # ============================================================================
19
+ # PROJECT ROOT DETECTION
20
+ # ============================================================================
21
+
22
+ # Find project root by searching upward for .specweave/ directory
23
+ find_project_root() {
24
+ local dir="$1"
25
+ while [ "$dir" != "/" ]; do
26
+ if [ -d "$dir/.specweave" ]; then
27
+ echo "$dir"
28
+ return 0
29
+ fi
30
+ dir="$(dirname "$dir")"
31
+ done
32
+ # Fallback: try current directory
33
+ if [ -d "$(pwd)/.specweave" ]; then
34
+ pwd
35
+ else
36
+ echo "$(pwd)"
37
+ fi
38
+ }
39
+
40
+ PROJECT_ROOT="$(find_project_root "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")"
41
+ cd "$PROJECT_ROOT" 2>/dev/null || true
42
+
43
+ # ============================================================================
44
+ # CONFIGURATION
45
+ # ============================================================================
46
+
47
+ LOGS_DIR=".specweave/logs"
48
+ DEBUG_LOG="$LOGS_DIR/hooks-debug.log"
49
+
50
+ mkdir -p "$LOGS_DIR" 2>/dev/null || true
51
+
52
+ # ============================================================================
53
+ # PRECONDITIONS CHECK
54
+ # ============================================================================
55
+
56
+ echo "[$(date)] [JIRA] 🔗 JIRA sync hook fired" >> "$DEBUG_LOG" 2>/dev/null || true
57
+
58
+ # Detect current increment
59
+ CURRENT_INCREMENT=$(ls -td .specweave/increments/*/ 2>/dev/null | xargs -n1 basename | grep -v "_backlog" | grep -v "_archive" | grep -v "_working" | head -1)
60
+
61
+ if [ -z "$CURRENT_INCREMENT" ]; then
62
+ echo "[$(date)] [JIRA] ℹ️ No active increment, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
63
+ cat <<EOF
64
+ {
65
+ "continue": true
66
+ }
67
+ EOF
68
+ exit 0
69
+ fi
70
+
71
+ # Check for metadata.json
72
+ METADATA_FILE=".specweave/increments/$CURRENT_INCREMENT/metadata.json"
73
+
74
+ if [ ! -f "$METADATA_FILE" ]; then
75
+ echo "[$(date)] [JIRA] ℹ️ No metadata.json for $CURRENT_INCREMENT, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
76
+ cat <<EOF
77
+ {
78
+ "continue": true
79
+ }
80
+ EOF
81
+ exit 0
82
+ fi
83
+
84
+ # Check for JIRA issue link
85
+ if ! command -v jq &> /dev/null; then
86
+ echo "[$(date)] [JIRA] ⚠️ jq not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
87
+ cat <<EOF
88
+ {
89
+ "continue": true
90
+ }
91
+ EOF
92
+ exit 0
93
+ fi
94
+
95
+ JIRA_ISSUE=$(jq -r '.jira.issue // empty' "$METADATA_FILE" 2>/dev/null)
96
+
97
+ if [ -z "$JIRA_ISSUE" ]; then
98
+ echo "[$(date)] [JIRA] ℹ️ No JIRA issue linked to $CURRENT_INCREMENT, skipping sync" >> "$DEBUG_LOG" 2>/dev/null || true
99
+ cat <<EOF
100
+ {
101
+ "continue": true
102
+ }
103
+ EOF
104
+ exit 0
105
+ fi
106
+
107
+ # Check for Node.js
108
+ if ! command -v node &> /dev/null; then
109
+ echo "[$(date)] [JIRA] ⚠️ Node.js not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
110
+ cat <<EOF
111
+ {
112
+ "continue": true
113
+ }
114
+ EOF
115
+ exit 0
116
+ fi
117
+
118
+ # Check for JIRA sync script
119
+ if [ ! -f "dist/commands/jira-sync.js" ]; then
120
+ echo "[$(date)] [JIRA] ⚠️ jira-sync.js not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
121
+ cat <<EOF
122
+ {
123
+ "continue": true
124
+ }
125
+ EOF
126
+ exit 0
127
+ fi
128
+
129
+ # ============================================================================
130
+ # JIRA SYNC LOGIC
131
+ # ============================================================================
132
+
133
+ echo "[$(date)] [JIRA] 🔄 Syncing to JIRA issue $JIRA_ISSUE" >> "$DEBUG_LOG" 2>/dev/null || true
134
+
135
+ # Run JIRA sync command (non-blocking)
136
+ node dist/commands/jira-sync.js "$CURRENT_INCREMENT" 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
137
+ echo "[$(date)] [JIRA] ⚠️ Failed to sync to JIRA (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
138
+ }
139
+
140
+ echo "[$(date)] [JIRA] ✅ JIRA sync complete" >> "$DEBUG_LOG" 2>/dev/null || true
141
+
142
+ # ============================================================================
143
+ # SPEC COMMIT SYNC (NEW!)
144
+ # ============================================================================
145
+
146
+ echo "[$(date)] [JIRA] 🔗 Checking for spec commit sync..." >> "$DEBUG_LOG" 2>/dev/null || true
147
+
148
+ # Call TypeScript CLI to sync commits
149
+ if command -v node &> /dev/null && [ -f "$PROJECT_ROOT/dist/cli/commands/sync-spec-commits.js" ]; then
150
+ echo "[$(date)] [JIRA] 🚀 Running spec commit sync..." >> "$DEBUG_LOG" 2>/dev/null || true
151
+
152
+ node "$PROJECT_ROOT/dist/cli/commands/sync-spec-commits.js" \
153
+ --increment "$PROJECT_ROOT/.specweave/increments/$CURRENT_INCREMENT" \
154
+ --provider jira \
155
+ 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
156
+ echo "[$(date)] [JIRA] ⚠️ Spec commit sync failed (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
157
+ }
158
+
159
+ echo "[$(date)] [JIRA] ✅ Spec commit sync complete" >> "$DEBUG_LOG" 2>/dev/null || true
160
+ else
161
+ echo "[$(date)] [JIRA] ℹ️ Spec commit sync not available (node or script not found)" >> "$DEBUG_LOG" 2>/dev/null || true
162
+ fi
163
+
164
+ # ============================================================================
165
+ # OUTPUT TO CLAUDE
166
+ # ============================================================================
167
+
168
+ cat <<EOF
169
+ {
170
+ "continue": true
171
+ }
172
+ EOF
@@ -1,6 +1,6 @@
1
- import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
2
- import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
3
- import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
1
+ import { EnhancedContentBuilder } from "../../../dist/src/core/sync/enhanced-content-builder.js";
2
+ import { SpecIncrementMapper } from "../../../dist/src/core/sync/spec-increment-mapper.js";
3
+ import { parseSpecContent } from "../../../dist/src/core/spec-content-sync.js";
4
4
  import * as path from "path";
5
5
  import * as fs from "fs/promises";
6
6
  async function syncSpecToJiraWithEnhancedContent(options) {