specweave 1.0.301 → 1.0.304

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 (74) hide show
  1. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -1
  2. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +44 -25
  3. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -1
  4. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +6 -0
  5. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +36 -1
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  8. package/dist/plugins/specweave-github/lib/github-feature-sync.js +266 -5
  9. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  10. package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts +2 -1
  11. package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts.map +1 -1
  12. package/dist/plugins/specweave-github/lib/user-story-content-builder.js +6 -4
  13. package/dist/plugins/specweave-github/lib/user-story-content-builder.js.map +1 -1
  14. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -1
  15. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +37 -17
  16. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -1
  17. package/dist/src/cli/commands/refresh-plugins.d.ts.map +1 -1
  18. package/dist/src/cli/commands/refresh-plugins.js +9 -0
  19. package/dist/src/cli/commands/refresh-plugins.js.map +1 -1
  20. package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
  21. package/dist/src/cli/commands/sync-progress.js +72 -2
  22. package/dist/src/cli/commands/sync-progress.js.map +1 -1
  23. package/dist/src/config/types.d.ts +2 -2
  24. package/dist/src/core/increment/increment-utils.d.ts +27 -4
  25. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  26. package/dist/src/core/increment/increment-utils.js +44 -17
  27. package/dist/src/core/increment/increment-utils.js.map +1 -1
  28. package/dist/src/core/increment/template-creator.d.ts +26 -0
  29. package/dist/src/core/increment/template-creator.d.ts.map +1 -1
  30. package/dist/src/core/increment/template-creator.js +179 -20
  31. package/dist/src/core/increment/template-creator.js.map +1 -1
  32. package/dist/src/importers/import-to-increment.d.ts +111 -0
  33. package/dist/src/importers/import-to-increment.d.ts.map +1 -0
  34. package/dist/src/importers/import-to-increment.js +223 -0
  35. package/dist/src/importers/import-to-increment.js.map +1 -0
  36. package/dist/src/importers/increment-external-ref-detector.d.ts +78 -0
  37. package/dist/src/importers/increment-external-ref-detector.d.ts.map +1 -0
  38. package/dist/src/importers/increment-external-ref-detector.js +130 -0
  39. package/dist/src/importers/increment-external-ref-detector.js.map +1 -0
  40. package/dist/src/init/research/types.d.ts +1 -1
  41. package/dist/src/sync/external-issue-auto-creator.d.ts.map +1 -1
  42. package/dist/src/sync/external-issue-auto-creator.js +28 -1
  43. package/dist/src/sync/external-issue-auto-creator.js.map +1 -1
  44. package/dist/src/sync/sync-coordinator.d.ts +6 -0
  45. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  46. package/dist/src/sync/sync-coordinator.js +42 -2
  47. package/dist/src/sync/sync-coordinator.js.map +1 -1
  48. package/package.json +1 -1
  49. package/plugins/specweave/hooks/lib/update-active-increment.sh +2 -2
  50. package/plugins/specweave/hooks/lib/update-status-line.sh +2 -2
  51. package/plugins/specweave/hooks/stop-auto-v5.sh +28 -8
  52. package/plugins/specweave/hooks/stop-sync.sh +10 -5
  53. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +8 -4
  54. package/plugins/specweave/hooks/user-prompt-submit.sh +130 -112
  55. package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +6 -3
  56. package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +4 -3
  57. package/plugins/specweave/skills/auto/SKILL.md +5 -3
  58. package/plugins/specweave/skills/done/SKILL.md +9 -3
  59. package/plugins/specweave/skills/import/SKILL.md +186 -0
  60. package/plugins/specweave/skills/increment/SKILL.md +30 -16
  61. package/plugins/specweave/skills/pm/SKILL.md +29 -2
  62. package/plugins/specweave/skills/pm/phases/00-deep-interview.md +12 -0
  63. package/plugins/specweave/skills/team-lead/SKILL.md +4 -2
  64. package/plugins/specweave/skills/team-merge/SKILL.md +2 -2
  65. package/plugins/specweave-github/lib/github-ac-comment-poster.js +31 -19
  66. package/plugins/specweave-github/lib/github-ac-comment-poster.ts +44 -27
  67. package/plugins/specweave-github/lib/github-feature-sync-cli.js +5 -0
  68. package/plugins/specweave-github/lib/github-feature-sync-cli.ts +7 -1
  69. package/plugins/specweave-github/lib/github-feature-sync.js +274 -6
  70. package/plugins/specweave-github/lib/github-feature-sync.ts +353 -5
  71. package/plugins/specweave-github/lib/user-story-content-builder.js +6 -4
  72. package/plugins/specweave-github/lib/user-story-content-builder.ts +6 -4
  73. package/plugins/specweave-github/lib/user-story-issue-builder.js +26 -11
  74. package/plugins/specweave-github/lib/user-story-issue-builder.ts +37 -19
@@ -10,7 +10,7 @@
10
10
  * Creates ONE issue PER user story file from specs/{project}/FS-XXX/us-*.md
11
11
  */
12
12
 
13
- import { readdir, readFile, writeFile } from 'fs/promises';
13
+ import { readdir, readFile, writeFile, mkdir } from 'fs/promises';
14
14
  import { existsSync } from 'fs';
15
15
  import * as path from 'path';
16
16
  import * as yaml from 'yaml';
@@ -54,6 +54,9 @@ export class GitHubFeatureSync {
54
54
  private calculator: CompletionCalculator;
55
55
  private token?: string;
56
56
 
57
+ // Cached default branch for the sync session (one API call per session)
58
+ private defaultBranch: string | null = null;
59
+
57
60
  // SYNC LOCK: Prevent concurrent syncs of the same feature
58
61
  // Maps featureId → last sync timestamp
59
62
  private static syncLocks: Map<string, number> = new Map();
@@ -68,6 +71,33 @@ export class GitHubFeatureSync {
68
71
  this.token = getGitHubAuthFromProject(projectRoot).token;
69
72
  }
70
73
 
74
+ /**
75
+ * Detect the default branch from the GitHub API.
76
+ * Caches the result per sync session to avoid repeated API calls.
77
+ * Falls back to 'main' if API call fails.
78
+ */
79
+ private async detectDefaultBranch(): Promise<string> {
80
+ if (this.defaultBranch) {
81
+ return this.defaultBranch;
82
+ }
83
+
84
+ const owner = this.client.getOwner();
85
+ const repo = this.client.getRepo();
86
+
87
+ const result = await execFileNoThrow('gh', [
88
+ 'api', `repos/${owner}/${repo}`, '--jq', '.default_branch'
89
+ ], { env: this.getGhEnv() });
90
+
91
+ if (result.exitCode === 0 && result.stdout.trim()) {
92
+ this.defaultBranch = result.stdout.trim();
93
+ } else {
94
+ console.warn(` ⚠️ Failed to detect default branch, falling back to 'main': ${result.stderr}`);
95
+ this.defaultBranch = 'main';
96
+ }
97
+
98
+ return this.defaultBranch;
99
+ }
100
+
71
101
  /**
72
102
  * Get environment object with GH_TOKEN for gh CLI commands.
73
103
  * This ensures the token from .env is passed to all gh operations,
@@ -163,6 +193,10 @@ export class GitHubFeatureSync {
163
193
  let issuesCreated = 0;
164
194
  let issuesUpdated = 0;
165
195
 
196
+ // Detect default branch once per sync session
197
+ const detectedBranch = await this.detectDefaultBranch();
198
+ console.log(` 🌿 Default branch: ${detectedBranch}`);
199
+
166
200
  for (const userStory of userStories) {
167
201
  console.log(`\n 🔹 Processing ${userStory.id}: ${userStory.title}`);
168
202
 
@@ -170,7 +204,7 @@ export class GitHubFeatureSync {
170
204
  const repoInfo = {
171
205
  owner: this.client.getOwner(),
172
206
  repo: this.client.getRepo(),
173
- branch: 'develop' // TODO: detect from git
207
+ branch: detectedBranch
174
208
  };
175
209
 
176
210
  const builder = new UserStoryIssueBuilder(
@@ -294,7 +328,8 @@ export class GitHubFeatureSync {
294
328
  }
295
329
 
296
330
  /**
297
- * Find Feature folder in specs directory
331
+ * Find Feature folder in specs directory.
332
+ * Falls back to auto-creating from increment spec.md if living docs don't exist.
298
333
  */
299
334
  private async findFeatureFolder(featureId: string): Promise<string | null> {
300
335
  // v5.0.0+: NO _features folder - features live in project folders
@@ -315,9 +350,296 @@ export class GitHubFeatureSync {
315
350
  return legacyFolder;
316
351
  }
317
352
 
353
+ // FALLBACK (v1.0.302): Auto-create feature folder from increment spec.md
354
+ // Most increments never get living docs created, so sync silently fails.
355
+ // This makes sync self-healing by creating minimal living docs on-the-fly.
356
+ console.log(` ℹ️ Feature folder not found in living docs, attempting auto-create from spec.md...`);
357
+ const created = await this.createFeatureFolderFromSpec(featureId, projectFolders);
358
+ if (created) {
359
+ return created;
360
+ }
361
+
318
362
  return null;
319
363
  }
320
364
 
365
+ /**
366
+ * Find the increment folder for a given feature ID.
367
+ * Converts FS-271 -> finds 0271-xxx-xxx/ in .specweave/increments/
368
+ */
369
+ private async findIncrementFolder(featureId: string): Promise<string | null> {
370
+ const numMatch = featureId.match(/FS-0*(\d+)E?/i);
371
+ if (!numMatch) return null;
372
+
373
+ const num = parseInt(numMatch[1], 10);
374
+ const paddedNum = String(num).padStart(4, '0');
375
+
376
+ const incrementsDir = path.join(this.projectRoot, '.specweave/increments');
377
+ if (!existsSync(incrementsDir)) return null;
378
+
379
+ const entries = await readdir(incrementsDir);
380
+ const match = entries.find(e => e.startsWith(paddedNum + '-'));
381
+ if (!match) return null;
382
+
383
+ return path.join(incrementsDir, match);
384
+ }
385
+
386
+ /**
387
+ * Auto-create a feature folder (FEATURE.md + us-NNN.md files) from an
388
+ * increment's spec.md. This enables GitHub sync even when the living docs
389
+ * builder hasn't run yet.
390
+ */
391
+ private async createFeatureFolderFromSpec(
392
+ featureId: string,
393
+ projectFolders: string[]
394
+ ): Promise<string | null> {
395
+ try {
396
+ const incrementFolder = await this.findIncrementFolder(featureId);
397
+ if (!incrementFolder) {
398
+ console.log(` ⚠️ No increment folder found for ${featureId}`);
399
+ return null;
400
+ }
401
+
402
+ const specPath = path.join(incrementFolder, 'spec.md');
403
+ if (!existsSync(specPath)) {
404
+ console.log(` ⚠️ No spec.md found in ${path.basename(incrementFolder)}`);
405
+ return null;
406
+ }
407
+
408
+ const specContent = await readFile(specPath, 'utf-8');
409
+
410
+ // Parse frontmatter
411
+ const fmMatch = specContent.match(/^---\n([\s\S]*?)\n---/);
412
+ if (!fmMatch) {
413
+ console.log(` ⚠️ spec.md has no YAML frontmatter`);
414
+ return null;
415
+ }
416
+
417
+ const frontmatter = yaml.parse(fmMatch[1]);
418
+ const title = frontmatter.title || path.basename(incrementFolder).replace(/^\d+-/, '');
419
+ const status = frontmatter.status || 'active';
420
+ const priority = frontmatter.priority || 'P2';
421
+ const created = frontmatter.created || new Date().toISOString().split('T')[0];
422
+ const incrementId = frontmatter.increment || path.basename(incrementFolder);
423
+
424
+ // Determine target project folder from spec.md user stories or first available
425
+ let targetProjectFolder = projectFolders[0]; // Default: first project folder
426
+ const projectMatch = specContent.match(/\*\*Project\*\*:\s*(\S+)/);
427
+ if (projectMatch) {
428
+ const projectName = projectMatch[1];
429
+ const matchingFolder = projectFolders.find(f => path.basename(f) === projectName);
430
+ if (matchingFolder) {
431
+ targetProjectFolder = matchingFolder;
432
+ }
433
+ }
434
+
435
+ if (!targetProjectFolder) {
436
+ console.log(` ⚠️ No project folder available for feature creation`);
437
+ return null;
438
+ }
439
+
440
+ // Create feature folder
441
+ const featureFolder = path.join(targetProjectFolder, featureId);
442
+ await mkdir(featureFolder, { recursive: true });
443
+
444
+ // Parse user stories from spec.md body
445
+ const userStories = this.parseUserStoriesFromSpec(specContent, featureId);
446
+
447
+ // Create FEATURE.md
448
+ const featureMd = this.buildFeatureMd(featureId, title, status, priority, created, incrementId, userStories);
449
+ await writeFile(path.join(featureFolder, 'FEATURE.md'), featureMd, 'utf-8');
450
+
451
+ // Create us-NNN.md files
452
+ for (const us of userStories) {
453
+ const usFilename = `us-${us.id.replace('US-', '').padStart(3, '0')}-${this.slugify(us.title)}.md`;
454
+ const usMd = this.buildUserStoryMd(us, featureId, incrementId);
455
+ await writeFile(path.join(featureFolder, usFilename), usMd, 'utf-8');
456
+ }
457
+
458
+ console.log(` ✅ Auto-created feature folder with ${userStories.length} user stories`);
459
+ return featureFolder;
460
+ } catch (error) {
461
+ console.log(` ⚠️ Failed to auto-create feature folder: ${(error as Error).message}`);
462
+ return null;
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Parse user stories from spec.md markdown content.
468
+ */
469
+ private parseUserStoriesFromSpec(
470
+ specContent: string,
471
+ featureId: string
472
+ ): Array<{
473
+ id: string;
474
+ title: string;
475
+ priority: string;
476
+ project: string;
477
+ storyText: string;
478
+ acceptanceCriteria: string[];
479
+ status: string;
480
+ }> {
481
+ const stories: Array<{
482
+ id: string;
483
+ title: string;
484
+ priority: string;
485
+ project: string;
486
+ storyText: string;
487
+ acceptanceCriteria: string[];
488
+ status: string;
489
+ }> = [];
490
+
491
+ // Match ### US-NNN: Title (Priority) sections
492
+ const usRegex = /### (US-\d+):\s*(.+?)(?:\s*\((P\d)\))?\s*\n([\s\S]*?)(?=\n### US-|\n## |\n---\s*\n### US-|$)/g;
493
+ let match;
494
+
495
+ while ((match = usRegex.exec(specContent)) !== null) {
496
+ const usId = match[1];
497
+ const rawTitle = match[2].trim();
498
+ const priority = match[3] || 'P2';
499
+ const body = match[4];
500
+
501
+ // Skip template placeholders
502
+ if (rawTitle === '[Story Title]') continue;
503
+
504
+ // Extract project
505
+ const projectMatch = body.match(/\*\*Project\*\*:\s*(\S+)/);
506
+ const project = projectMatch ? projectMatch[1] : 'specweave';
507
+
508
+ // Extract story text (As a... I want... So that...)
509
+ const storyMatch = body.match(/\*\*As a\*\*\s+([\s\S]*?)(?=\n\*\*Acceptance Criteria|$)/);
510
+ const storyText = storyMatch ? storyMatch[1].trim() : '';
511
+
512
+ // Extract acceptance criteria
513
+ const acs: string[] = [];
514
+ const acRegex = /- \[[ x]\] \*\*AC-[^*]+\*\*:\s*(.+)/g;
515
+ let acMatch;
516
+ while ((acMatch = acRegex.exec(body)) !== null) {
517
+ acs.push(acMatch[0]);
518
+ }
519
+
520
+ // Determine status from ACs
521
+ const totalAcs = acs.length;
522
+ const completedAcs = acs.filter(ac => ac.startsWith('- [x]')).length;
523
+ let status = 'not-started';
524
+ if (totalAcs > 0 && completedAcs === totalAcs) status = 'complete';
525
+ else if (completedAcs > 0) status = 'active';
526
+
527
+ stories.push({
528
+ id: usId,
529
+ title: rawTitle,
530
+ priority,
531
+ project,
532
+ storyText,
533
+ acceptanceCriteria: acs,
534
+ status,
535
+ });
536
+ }
537
+
538
+ return stories;
539
+ }
540
+
541
+ /**
542
+ * Build FEATURE.md content matching the living docs format.
543
+ */
544
+ private buildFeatureMd(
545
+ featureId: string,
546
+ title: string,
547
+ status: string,
548
+ priority: string,
549
+ created: string,
550
+ incrementId: string,
551
+ userStories: Array<{ id: string; title: string }>
552
+ ): string {
553
+ const now = new Date().toISOString().split('T')[0];
554
+ const mappedStatus = status === 'planned' ? 'planning'
555
+ : status === 'completed' || status === 'done' ? 'complete'
556
+ : status === 'active' || status === 'in-progress' ? 'active'
557
+ : 'planning';
558
+
559
+ const fm: Record<string, unknown> = {
560
+ id: featureId,
561
+ title,
562
+ type: 'feature',
563
+ status: mappedStatus,
564
+ priority,
565
+ created,
566
+ lastUpdated: now,
567
+ tldr: title,
568
+ complexity: 'medium',
569
+ auto_created: true,
570
+ };
571
+
572
+ const yamlFm = yaml.stringify(fm);
573
+
574
+ let body = `\n# ${title}\n\n## TL;DR\n\n**What**: ${title}\n**Status**: ${mappedStatus} | **Priority**: ${priority}\n**User Stories**: ${userStories.length}\n\n## Overview\n\n${title}\n\n## Implementation History\n\n| Increment | Status |\n|-----------|--------|\n| [${incrementId}](../../../../../increments/${incrementId}/spec.md) | ${mappedStatus} |\n\n## User Stories\n`;
575
+
576
+ for (const us of userStories) {
577
+ body += `\n- [${us.id}: ${us.title}](./${us.id.toLowerCase()}.md)`;
578
+ }
579
+
580
+ return `---\n${yamlFm}---${body}\n`;
581
+ }
582
+
583
+ /**
584
+ * Build us-NNN.md content matching the living docs format.
585
+ */
586
+ private buildUserStoryMd(
587
+ us: {
588
+ id: string;
589
+ title: string;
590
+ priority: string;
591
+ project: string;
592
+ storyText: string;
593
+ acceptanceCriteria: string[];
594
+ status: string;
595
+ },
596
+ featureId: string,
597
+ incrementId: string
598
+ ): string {
599
+ const now = new Date().toISOString().split('T')[0];
600
+
601
+ const fm: Record<string, unknown> = {
602
+ id: us.id,
603
+ feature: featureId,
604
+ title: us.title,
605
+ status: us.status,
606
+ priority: us.priority,
607
+ created: now,
608
+ project: us.project,
609
+ };
610
+
611
+ const yamlFm = yaml.stringify(fm);
612
+
613
+ let body = `\n# ${us.id}: ${us.title}\n\n**Feature**: [${featureId}](./FEATURE.md)\n\n`;
614
+
615
+ if (us.storyText) {
616
+ body += `${us.storyText}\n\n`;
617
+ }
618
+
619
+ body += `---\n\n## Acceptance Criteria\n\n`;
620
+
621
+ if (us.acceptanceCriteria.length > 0) {
622
+ body += us.acceptanceCriteria.join('\n') + '\n';
623
+ } else {
624
+ body += `- [ ] **AC-${us.id.replace('US-', 'US')}-01**: Pending specification\n`;
625
+ }
626
+
627
+ body += `\n---\n\n## Implementation\n\n**Increment**: [${incrementId}](../../../../../increments/${incrementId}/spec.md)\n`;
628
+
629
+ return `---\n${yamlFm}---${body}\n`;
630
+ }
631
+
632
+ /**
633
+ * Convert a title to a URL-safe slug.
634
+ */
635
+ private slugify(text: string): string {
636
+ return text
637
+ .toLowerCase()
638
+ .replace(/[^a-z0-9]+/g, '-')
639
+ .replace(/^-+|-+$/g, '')
640
+ .substring(0, 60);
641
+ }
642
+
321
643
  /**
322
644
  * Backfill increment metadata.json with GitHub issue reference (v1.0.240)
323
645
  *
@@ -467,9 +789,13 @@ export class GitHubFeatureSync {
467
789
  // CRITICAL: Check if milestone already exists before creating
468
790
  // NOTE: Must use per_page=100 to handle repos with 30+ milestones (GitHub default is 30)
469
791
  // BUG FIX: Without pagination, milestone #31+ won't be found → false "not found" → HTTP 422 duplicate error
792
+ // FIX (v1.0.302): Use explicit owner/repo from config, not :owner/:repo which resolves from git remote
793
+ const owner = this.client.getOwner();
794
+ const repo = this.client.getRepo();
795
+
470
796
  const existingResult = await execFileNoThrow('gh', [
471
797
  'api',
472
- 'repos/:owner/:repo/milestones?per_page=100&state=all',
798
+ `repos/${owner}/${repo}/milestones?per_page=100&state=all`,
473
799
  '--jq',
474
800
  `.[] | select(.title == "${title}") | {number, html_url}`,
475
801
  ], { env: this.getGhEnv() });
@@ -496,7 +822,7 @@ export class GitHubFeatureSync {
496
822
 
497
823
  const result = await execFileNoThrow('gh', [
498
824
  'api',
499
- 'repos/:owner/:repo/milestones',
825
+ `repos/${owner}/${repo}/milestones`,
500
826
  '-X',
501
827
  'POST',
502
828
  '-f',
@@ -677,7 +1003,9 @@ export class GitHubFeatureSync {
677
1003
  acsPercentage: number;
678
1004
  tasksPercentage: number;
679
1005
  acsTotal?: number;
1006
+ acsCompleted?: number;
680
1007
  tasksTotal?: number;
1008
+ tasksCompleted?: number;
681
1009
  frontmatterStatus?: string;
682
1010
  }
683
1011
  ): Promise<void> {
@@ -759,6 +1087,26 @@ export class GitHubFeatureSync {
759
1087
  } else {
760
1088
  console.warn(` ⚠️ Failed to add label ${newStatusLabel}: ${result.stderr}`);
761
1089
  }
1090
+
1091
+ // Step 3: Auto-close issue when status:complete and issue still OPEN
1092
+ // This ensures closure happens atomically with the label update,
1093
+ // preventing issues like #1198 where label is applied but issue stays open
1094
+ if (newStatusLabel === 'status:complete' && issueData.state.toLowerCase() !== 'closed') {
1095
+ try {
1096
+ const completionComment = this.calculator.buildCompletionComment(completion as any);
1097
+ await execFileNoThrow('gh', [
1098
+ 'issue',
1099
+ 'close',
1100
+ issueNumber.toString(),
1101
+ '--comment',
1102
+ completionComment,
1103
+ ], { env: this.getGhEnv() });
1104
+ console.log(` ✅ Auto-closed issue #${issueNumber} (status:complete)`);
1105
+ } catch (closeError) {
1106
+ // Non-blocking: close failure shouldn't break sync
1107
+ console.warn(` ⚠️ Failed to auto-close issue #${issueNumber}: ${(closeError as Error).message}`);
1108
+ }
1109
+ }
762
1110
  } catch (error) {
763
1111
  // Non-blocking: Label update failure shouldn't break sync
764
1112
  console.warn(` ⚠️ Failed to update status labels: ${(error as Error).message}`);
@@ -36,11 +36,13 @@ class UserStoryContentBuilder {
36
36
  * Build GitHub issue body from user story content
37
37
  *
38
38
  * @param githubRepo Optional GitHub repo in format "owner/repo" for generating URLs
39
+ * @param branch Optional branch name for URL generation (defaults to 'main')
39
40
  */
40
- async buildIssueBody(githubRepo) {
41
+ async buildIssueBody(githubRepo, branch) {
41
42
  const content = await this.parse();
42
43
  let body = "";
43
44
  const repo = githubRepo || await this.detectGitHubRepo();
45
+ const branchName = branch || "main";
44
46
  const completedACs = content.acceptanceCriteria.filter((ac) => ac.completed).length;
45
47
  const totalACs = content.acceptanceCriteria.length;
46
48
  const completedTasks = content.tasks.filter((t) => t.status).length;
@@ -83,7 +85,7 @@ class UserStoryContentBuilder {
83
85
  const usFilename = path.basename(this.userStoryPath);
84
86
  if (repo) {
85
87
  const relativePath = this.userStoryPath.replace(this.projectRoot, "").replace(/^\//, "");
86
- body += `\u{1F4C4} View full story: [\`${usFilename}\`](https://github.com/${repo}/tree/develop/${relativePath})
88
+ body += `\u{1F4C4} View full story: [\`${usFilename}\`](https://github.com/${repo}/tree/${branchName}/${relativePath})
87
89
 
88
90
  `;
89
91
  }
@@ -128,7 +130,7 @@ class UserStoryContentBuilder {
128
130
  `;
129
131
  if (content.incrementId) {
130
132
  if (repo) {
131
- body += `**Increment**: [${content.incrementId}](https://github.com/${repo}/tree/develop/.specweave/increments/${content.incrementId})
133
+ body += `**Increment**: [${content.incrementId}](https://github.com/${repo}/tree/${branchName}/.specweave/increments/${content.incrementId})
132
134
 
133
135
  `;
134
136
  } else {
@@ -142,7 +144,7 @@ class UserStoryContentBuilder {
142
144
  let taskLink = task.link;
143
145
  if (repo && taskLink.startsWith("../../")) {
144
146
  const relativePath = taskLink.replace(/^\.\.\/\.\.\//, ".specweave/");
145
- taskLink = `https://github.com/${repo}/tree/develop/${relativePath}`;
147
+ taskLink = `https://github.com/${repo}/tree/${branchName}/${relativePath}`;
146
148
  }
147
149
  body += `- ${checkbox} [${task.id}: ${task.title}](${taskLink})
148
150
  `;
@@ -110,14 +110,16 @@ export class UserStoryContentBuilder {
110
110
  * Build GitHub issue body from user story content
111
111
  *
112
112
  * @param githubRepo Optional GitHub repo in format "owner/repo" for generating URLs
113
+ * @param branch Optional branch name for URL generation (defaults to 'main')
113
114
  */
114
- async buildIssueBody(githubRepo?: string): Promise<string> {
115
+ async buildIssueBody(githubRepo?: string, branch?: string): Promise<string> {
115
116
  const content = await this.parse();
116
117
 
117
118
  let body = '';
118
119
 
119
120
  // Detect GitHub repo from git remote if not provided
120
121
  const repo = githubRepo || await this.detectGitHubRepo();
122
+ const branchName = branch || 'main';
121
123
 
122
124
  // ❌ REMOVED: Metadata header (Feature, Status, Priority)
123
125
  // WHY: GitHub has NATIVE fields for this (labels, milestones)
@@ -160,7 +162,7 @@ export class UserStoryContentBuilder {
160
162
  const relativePath = this.userStoryPath
161
163
  .replace(this.projectRoot, '')
162
164
  .replace(/^\//, '');
163
- body += `📄 View full story: [\`${usFilename}\`](https://github.com/${repo}/tree/develop/${relativePath})\n\n`;
165
+ body += `📄 View full story: [\`${usFilename}\`](https://github.com/${repo}/tree/${branchName}/${relativePath})\n\n`;
164
166
  }
165
167
 
166
168
  body += `---\n\n`;
@@ -196,7 +198,7 @@ export class UserStoryContentBuilder {
196
198
 
197
199
  if (content.incrementId) {
198
200
  if (repo) {
199
- body += `**Increment**: [${content.incrementId}](https://github.com/${repo}/tree/develop/.specweave/increments/${content.incrementId})\n\n`;
201
+ body += `**Increment**: [${content.incrementId}](https://github.com/${repo}/tree/${branchName}/.specweave/increments/${content.incrementId})\n\n`;
200
202
  } else {
201
203
  body += `**Increment**: ${content.incrementId}\n\n`;
202
204
  }
@@ -208,7 +210,7 @@ export class UserStoryContentBuilder {
208
210
  let taskLink = task.link;
209
211
  if (repo && taskLink.startsWith('../../')) {
210
212
  const relativePath = taskLink.replace(/^\.\.\/\.\.\//, '.specweave/');
211
- taskLink = `https://github.com/${repo}/tree/develop/${relativePath}`;
213
+ taskLink = `https://github.com/${repo}/tree/${branchName}/${relativePath}`;
212
214
  }
213
215
  body += `- ${checkbox} [${task.id}: ${task.title}](${taskLink})\n`;
214
216
  }
@@ -322,21 +322,36 @@ User Story ID: ${frontmatter.id}`
322
322
  sections.push("");
323
323
  if (this.repoOwner && this.repoName) {
324
324
  const baseUrl = `https://github.com/${this.repoOwner}/${this.repoName}/blob/${this.branch}`;
325
- const pathMatch = this.userStoryPath.match(/specs\/([^/]+)\/FS-\d+\//);
326
- const projectFolder = pathMatch ? pathMatch[1] : "default";
327
- sections.push(`- **Feature Spec**: [${this.featureId}](${baseUrl}/.specweave/docs/internal/specs/${projectFolder}/${this.featureId}/FEATURE.md)`);
328
- const relativeUSPath = path.relative(this.projectRoot, this.userStoryPath);
329
- sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${baseUrl}/${relativeUSPath})`);
330
325
  const incrementMatch = implMatch?.[1]?.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
331
- if (incrementMatch) {
332
- const incrementId = incrementMatch[1];
326
+ const incrementId = incrementMatch ? incrementMatch[1] : null;
327
+ if (incrementId) {
328
+ sections.push(`- **Feature Spec**: [${this.featureId}](${baseUrl}/.specweave/increments/${incrementId}/spec.md)`);
329
+ } else {
330
+ const pathMatch = this.userStoryPath.match(/specs\/([^/]+)\/FS-\d+\//);
331
+ const projectFolder = pathMatch ? pathMatch[1] : "default";
332
+ sections.push(`- **Feature Spec**: [${this.featureId}](${baseUrl}/.specweave/docs/internal/specs/${projectFolder}/${this.featureId}/FEATURE.md)`);
333
+ }
334
+ if (incrementId) {
335
+ sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${baseUrl}/.specweave/increments/${incrementId}/spec.md)`);
336
+ } else {
337
+ const relativeUSPath = path.relative(this.projectRoot, this.userStoryPath);
338
+ sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${baseUrl}/${relativeUSPath})`);
339
+ }
340
+ if (incrementId) {
333
341
  sections.push(`- **Increment**: [${incrementId}](${baseUrl}/.specweave/increments/${incrementId})`);
334
342
  }
335
343
  } else {
336
- const pathMatch = this.userStoryPath.match(/specs\/([^/]+)\/FS-\d+\//);
337
- const projectFolder = pathMatch ? pathMatch[1] : "default";
338
- sections.push(`- **Feature Spec**: [${this.featureId}](../.specweave/docs/internal/specs/${projectFolder}/${this.featureId}/FEATURE.md)`);
339
- sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${this.userStoryPath})`);
344
+ const incrementMatch = implMatch?.[1]?.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
345
+ const incrementId = incrementMatch ? incrementMatch[1] : null;
346
+ if (incrementId) {
347
+ sections.push(`- **Feature Spec**: [${this.featureId}](.specweave/increments/${incrementId}/spec.md)`);
348
+ sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](.specweave/increments/${incrementId}/spec.md)`);
349
+ } else {
350
+ const pathMatch = this.userStoryPath.match(/specs\/([^/]+)\/FS-\d+\//);
351
+ const projectFolder = pathMatch ? pathMatch[1] : "default";
352
+ sections.push(`- **Feature Spec**: [${this.featureId}](../.specweave/docs/internal/specs/${projectFolder}/${this.featureId}/FEATURE.md)`);
353
+ sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${this.userStoryPath})`);
354
+ }
340
355
  }
341
356
  sections.push("");
342
357
  sections.push("---");
@@ -520,34 +520,52 @@ export class UserStoryIssueBuilder {
520
520
  sections.push('');
521
521
 
522
522
  // Generate proper GitHub blob URLs
523
- // v5.0.0+: Features live in project folders, NOT _features
523
+ // v1.0.302 FIX: Use increment paths instead of living docs paths.
524
+ // Living docs (/.specweave/docs/internal/specs/) only exist in the umbrella repo,
525
+ // so links to them return 404 in the target repo. Increment spec.md is always pushed.
524
526
  if (this.repoOwner && this.repoName) {
525
527
  const baseUrl = `https://github.com/${this.repoOwner}/${this.repoName}/blob/${this.branch}`;
526
528
 
527
- // Extract project from user story path: specs/{project}/FS-XXX/us-*.md
528
- const pathMatch = this.userStoryPath.match(/specs\/([^/]+)\/FS-\d+\//);
529
- const projectFolder = pathMatch ? pathMatch[1] : 'default';
530
-
531
- // Feature Spec link
532
- sections.push(`- **Feature Spec**: [${this.featureId}](${baseUrl}/.specweave/docs/internal/specs/${projectFolder}/${this.featureId}/FEATURE.md)`);
529
+ // Extract increment ID from Implementation section (if available)
530
+ const incrementMatch = implMatch?.[1]?.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
531
+ const incrementId = incrementMatch ? incrementMatch[1] : null;
532
+
533
+ // Feature Spec link: point to increment spec.md (always exists in target repo)
534
+ if (incrementId) {
535
+ sections.push(`- **Feature Spec**: [${this.featureId}](${baseUrl}/.specweave/increments/${incrementId}/spec.md)`);
536
+ } else {
537
+ // Fallback to living docs path when no increment ID available
538
+ const pathMatch = this.userStoryPath.match(/specs\/([^/]+)\/FS-\d+\//);
539
+ const projectFolder = pathMatch ? pathMatch[1] : 'default';
540
+ sections.push(`- **Feature Spec**: [${this.featureId}](${baseUrl}/.specweave/docs/internal/specs/${projectFolder}/${this.featureId}/FEATURE.md)`);
541
+ }
533
542
 
534
- // User Story File link (relative to project root)
535
- const relativeUSPath = path.relative(this.projectRoot, this.userStoryPath);
536
- sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${baseUrl}/${relativeUSPath})`);
543
+ // User Story File link: point to increment spec.md (always exists in target repo)
544
+ if (incrementId) {
545
+ sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${baseUrl}/.specweave/increments/${incrementId}/spec.md)`);
546
+ } else {
547
+ const relativeUSPath = path.relative(this.projectRoot, this.userStoryPath);
548
+ sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${baseUrl}/${relativeUSPath})`);
549
+ }
537
550
 
538
- // Increment link (extracted from Implementation section)
539
- const incrementMatch = implMatch?.[1]?.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
540
- if (incrementMatch) {
541
- const incrementId = incrementMatch[1];
551
+ // Increment link (always points to the increment folder in the target repo)
552
+ if (incrementId) {
542
553
  sections.push(`- **Increment**: [${incrementId}](${baseUrl}/.specweave/increments/${incrementId})`);
543
554
  }
544
555
  } else {
545
556
  // Fallback to relative links if repo info not provided
546
- // v5.0.0+: Features live in project folders, NOT _features
547
- const pathMatch = this.userStoryPath.match(/specs\/([^/]+)\/FS-\d+\//);
548
- const projectFolder = pathMatch ? pathMatch[1] : 'default';
549
- sections.push(`- **Feature Spec**: [${this.featureId}](../.specweave/docs/internal/specs/${projectFolder}/${this.featureId}/FEATURE.md)`);
550
- sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${this.userStoryPath})`);
557
+ const incrementMatch = implMatch?.[1]?.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
558
+ const incrementId = incrementMatch ? incrementMatch[1] : null;
559
+
560
+ if (incrementId) {
561
+ sections.push(`- **Feature Spec**: [${this.featureId}](.specweave/increments/${incrementId}/spec.md)`);
562
+ sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](.specweave/increments/${incrementId}/spec.md)`);
563
+ } else {
564
+ const pathMatch = this.userStoryPath.match(/specs\/([^/]+)\/FS-\d+\//);
565
+ const projectFolder = pathMatch ? pathMatch[1] : 'default';
566
+ sections.push(`- **Feature Spec**: [${this.featureId}](../.specweave/docs/internal/specs/${projectFolder}/${this.featureId}/FEATURE.md)`);
567
+ sections.push(`- **User Story File**: [${path.basename(this.userStoryPath)}](${this.userStoryPath})`);
568
+ }
551
569
  }
552
570
 
553
571
  sections.push('');