specweave 0.26.4 â 0.26.9
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/CLAUDE.md +154 -4
- package/bin/specweave.js +15 -0
- package/dist/plugins/specweave-github/lib/completion-calculator.js +2 -2
- package/dist/plugins/specweave-github/lib/completion-calculator.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +28 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +191 -19
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts +3 -0
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +25 -2
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -1
- package/dist/src/cli/commands/archive.d.ts +10 -0
- package/dist/src/cli/commands/archive.d.ts.map +1 -0
- package/dist/src/cli/commands/archive.js +78 -0
- package/dist/src/cli/commands/archive.js.map +1 -0
- package/dist/src/cli/commands/init.js +2 -2
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/init/initial-increment-generator.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/initial-increment-generator.js +48 -8
- package/dist/src/cli/helpers/init/initial-increment-generator.js.map +1 -1
- package/dist/src/core/increment/increment-reopener.d.ts.map +1 -1
- package/dist/src/core/increment/increment-reopener.js +13 -14
- package/dist/src/core/increment/increment-reopener.js.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +19 -0
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts +85 -0
- package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -0
- package/dist/src/core/increment/status-change-sync-trigger.js +137 -0
- package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -0
- package/dist/src/core/increment/sync-circuit-breaker.d.ts +64 -0
- package/dist/src/core/increment/sync-circuit-breaker.d.ts.map +1 -0
- package/dist/src/core/increment/sync-circuit-breaker.js +95 -0
- package/dist/src/core/increment/sync-circuit-breaker.js.map +1 -0
- package/dist/src/core/living-docs/living-docs-sync.d.ts +12 -0
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +157 -24
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/init/repo/types.d.ts +1 -1
- package/package.json +2 -2
- package/plugins/specweave/agents/pm/AGENT.md +13 -7
- package/plugins/specweave/commands/sync-diagnostics.md +227 -0
- package/plugins/specweave/hooks/docs-changed.sh.backup +79 -0
- package/plugins/specweave/hooks/human-input-required.sh.backup +75 -0
- package/plugins/specweave/hooks/post-first-increment.sh.backup +61 -0
- package/plugins/specweave/hooks/post-increment-change.sh.backup +98 -0
- package/plugins/specweave/hooks/post-increment-completion.sh.backup +231 -0
- package/plugins/specweave/hooks/post-increment-planning.sh.backup +1048 -0
- package/plugins/specweave/hooks/post-increment-status-change.sh.backup +147 -0
- package/plugins/specweave/hooks/post-spec-update.sh.backup +158 -0
- package/plugins/specweave/hooks/post-user-story-complete.sh.backup +179 -0
- package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +83 -0
- package/plugins/specweave/hooks/pre-implementation.sh.backup +67 -0
- package/plugins/specweave/hooks/pre-task-completion.sh.backup +194 -0
- package/plugins/specweave/hooks/pre-tool-use.sh.backup +133 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +20 -8
- package/plugins/specweave/hooks/user-prompt-submit.sh.backup +386 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +19 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/skills/brownfield-analyzer/SKILL.md +267 -868
- package/plugins/specweave/skills/increment-planner/SKILL.md +379 -1245
- package/plugins/specweave/skills/role-orchestrator/SKILL.md +293 -969
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +353 -0
- package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +172 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-docs/skills/technical-writing/SKILL.md +333 -839
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +1080 -0
- package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
- package/plugins/specweave-github/lib/completion-calculator.js +1 -1
- package/plugins/specweave-github/lib/completion-calculator.ts +2 -2
- package/plugins/specweave-github/lib/github-feature-sync.js +152 -18
- package/plugins/specweave-github/lib/github-feature-sync.ts +225 -22
- package/plugins/specweave-github/lib/user-story-issue-builder.js +21 -1
- package/plugins/specweave-github/lib/user-story-issue-builder.ts +31 -3
- package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +172 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +981 -0
- package/plugins/specweave-release/hooks/post-task-completion.sh.backup +110 -0
- package/plugins/specweave-testing/skills/tdd-expert/SKILL.md +269 -749
- package/plugins/specweave-testing/skills/unit-testing-expert/SKILL.md +318 -810
|
@@ -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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
'
|
|
629
|
+
'edit',
|
|
518
630
|
issueNumber.toString(),
|
|
519
|
-
'--
|
|
520
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|