specweave 0.17.16 → 0.17.17

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 (195) hide show
  1. package/CLAUDE.md +405 -2495
  2. package/README.md +92 -2
  3. package/dist/locales/de/.gitkeep +0 -0
  4. package/dist/locales/de/cli.json +108 -0
  5. package/dist/locales/en/cli.json +287 -0
  6. package/dist/locales/en/errors.json +7 -0
  7. package/dist/locales/en/templates.json +6 -0
  8. package/dist/locales/es/.gitkeep +0 -0
  9. package/dist/locales/es/cli.json +41 -0
  10. package/dist/locales/fr/.gitkeep +0 -0
  11. package/dist/locales/fr/cli.json +108 -0
  12. package/dist/locales/ja/.gitkeep +0 -0
  13. package/dist/locales/ja/cli.json +108 -0
  14. package/dist/locales/ko/.gitkeep +0 -0
  15. package/dist/locales/ko/cli.json +108 -0
  16. package/dist/locales/pt/.gitkeep +0 -0
  17. package/dist/locales/pt/cli.json +108 -0
  18. package/dist/locales/ru/.gitkeep +0 -0
  19. package/dist/locales/ru/cli.json +269 -0
  20. package/dist/locales/zh/.gitkeep +0 -0
  21. package/dist/locales/zh/cli.json +108 -0
  22. package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
  23. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +188 -36
  24. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
  25. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
  26. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
  27. package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
  28. package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
  29. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +25 -0
  30. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +1 -0
  31. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +191 -0
  32. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +1 -0
  33. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
  34. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
  35. package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
  36. package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
  37. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
  38. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
  39. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
  40. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
  41. package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
  42. package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
  43. package/dist/plugins/specweave-github/lib/github-client.js +25 -13
  44. package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
  45. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
  46. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
  47. package/dist/plugins/specweave-github/lib/github-epic-sync.js +451 -0
  48. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
  49. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
  50. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
  51. package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
  52. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
  53. package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
  54. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
  55. package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
  56. package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
  57. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +26 -0
  58. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -0
  59. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +195 -0
  60. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -0
  61. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
  62. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
  63. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
  64. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
  65. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
  66. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
  67. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
  68. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
  69. package/dist/spec-parser.js +629 -0
  70. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  71. package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
  72. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  73. package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
  74. package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
  75. package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
  76. package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
  77. package/dist/src/core/living-docs/index.d.ts +10 -84
  78. package/dist/src/core/living-docs/index.d.ts.map +1 -1
  79. package/dist/src/core/living-docs/index.js +10 -164
  80. package/dist/src/core/living-docs/index.js.map +1 -1
  81. package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
  82. package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
  83. package/dist/src/core/living-docs/spec-distributor.js +823 -0
  84. package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
  85. package/dist/src/core/living-docs/types.d.ts +201 -0
  86. package/dist/src/core/living-docs/types.d.ts.map +1 -0
  87. package/dist/src/core/living-docs/types.js +15 -0
  88. package/dist/src/core/living-docs/types.js.map +1 -0
  89. package/dist/src/core/logging/prompt-logger.d.ts +70 -0
  90. package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
  91. package/dist/src/core/logging/prompt-logger.js +247 -0
  92. package/dist/src/core/logging/prompt-logger.js.map +1 -0
  93. package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
  94. package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
  95. package/dist/src/core/status-line/status-line-manager.js +33 -70
  96. package/dist/src/core/status-line/status-line-manager.js.map +1 -1
  97. package/dist/src/core/status-line/types.d.ts +19 -31
  98. package/dist/src/core/status-line/types.d.ts.map +1 -1
  99. package/dist/src/core/status-line/types.js +5 -9
  100. package/dist/src/core/status-line/types.js.map +1 -1
  101. package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
  102. package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
  103. package/dist/src/core/sync/conflict-resolver.js +108 -0
  104. package/dist/src/core/sync/conflict-resolver.js.map +1 -0
  105. package/dist/src/core/sync/enhanced-content-builder.d.ts +77 -0
  106. package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
  107. package/dist/src/core/sync/enhanced-content-builder.js +199 -0
  108. package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
  109. package/dist/src/core/sync/label-detector.d.ts +66 -0
  110. package/dist/src/core/sync/label-detector.d.ts.map +1 -0
  111. package/dist/src/core/sync/label-detector.js +211 -0
  112. package/dist/src/core/sync/label-detector.js.map +1 -0
  113. package/dist/src/core/sync/retry-logic.d.ts +64 -0
  114. package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
  115. package/dist/src/core/sync/retry-logic.js +165 -0
  116. package/dist/src/core/sync/retry-logic.js.map +1 -0
  117. package/dist/src/core/sync/spec-content-sync.d.ts +88 -0
  118. package/dist/src/core/sync/spec-content-sync.d.ts.map +1 -0
  119. package/dist/src/core/sync/spec-content-sync.js +5 -0
  120. package/dist/src/core/sync/spec-content-sync.js.map +1 -0
  121. package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
  122. package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
  123. package/dist/src/core/sync/spec-increment-mapper.js +424 -0
  124. package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
  125. package/dist/src/core/sync/status-cache.d.ts +91 -0
  126. package/dist/src/core/sync/status-cache.d.ts.map +1 -0
  127. package/dist/src/core/sync/status-cache.js +140 -0
  128. package/dist/src/core/sync/status-cache.js.map +1 -0
  129. package/dist/src/core/sync/status-mapper.d.ts +69 -0
  130. package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
  131. package/dist/src/core/sync/status-mapper.js +90 -0
  132. package/dist/src/core/sync/status-mapper.js.map +1 -0
  133. package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
  134. package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
  135. package/dist/src/core/sync/status-sync-engine.js +347 -0
  136. package/dist/src/core/sync/status-sync-engine.js.map +1 -0
  137. package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
  138. package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
  139. package/dist/src/core/sync/sync-event-logger.js +103 -0
  140. package/dist/src/core/sync/sync-event-logger.js.map +1 -0
  141. package/dist/src/core/sync/workflow-detector.d.ts +95 -0
  142. package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
  143. package/dist/src/core/sync/workflow-detector.js +175 -0
  144. package/dist/src/core/sync/workflow-detector.js.map +1 -0
  145. package/dist/src/core/types/config.d.ts.map +1 -1
  146. package/dist/src/core/types/config.js +31 -0
  147. package/dist/src/core/types/config.js.map +1 -1
  148. package/dist/src/utils/github-url.d.ts +53 -0
  149. package/dist/src/utils/github-url.d.ts.map +1 -0
  150. package/dist/src/utils/github-url.js +90 -0
  151. package/dist/src/utils/github-url.js.map +1 -0
  152. package/dist/src/utils/spec-parser.d.ts +145 -0
  153. package/dist/src/utils/spec-parser.d.ts.map +1 -0
  154. package/dist/src/utils/spec-parser.js +640 -0
  155. package/dist/src/utils/spec-parser.js.map +1 -0
  156. package/dist/tsconfig.tsbuildinfo +1 -0
  157. package/package.json +1 -1
  158. package/plugins/specweave/agents/pm/AGENT.md +1 -1
  159. package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
  160. package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
  161. package/plugins/specweave/commands/specweave-done.md +163 -0
  162. package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
  163. package/plugins/specweave/hooks/post-increment-planning.sh +107 -35
  164. package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
  165. package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
  166. package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
  167. package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
  168. package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
  169. package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
  170. package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
  171. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  172. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
  173. package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
  174. package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
  175. package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
  176. package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
  177. package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
  178. package/plugins/specweave-github/lib/github-client.js +21 -10
  179. package/plugins/specweave-github/lib/github-client.ts +27 -16
  180. package/plugins/specweave-github/lib/github-epic-sync.js +489 -0
  181. package/plugins/specweave-github/lib/github-epic-sync.ts +690 -0
  182. package/plugins/specweave-github/lib/github-status-sync.js +71 -0
  183. package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
  184. package/plugins/specweave-github/lib/task-sync.js +33 -2
  185. package/plugins/specweave-github/lib/task-sync.ts +44 -2
  186. package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
  187. package/plugins/specweave-jira/lib/enhanced-jira-sync.ts.disabled +222 -0
  188. package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
  189. package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
  190. package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
  191. package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
  192. package/src/templates/AGENTS.md.template +88 -1
  193. package/src/templates/CLAUDE.md.template +49 -0
  194. package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
  195. package/plugins/specweave/skills/plugin-installer/SKILL.md +0 -353
@@ -0,0 +1,823 @@
1
+ /**
2
+ * SpecWeave Spec Distributor
3
+ *
4
+ * Distributes increment specs into hierarchical living docs structure:
5
+ * - Epic (SPEC-###.md) - High-level summary
6
+ * - User Stories (us-###.md) - Detailed requirements
7
+ * - Tasks (tasks.md) - Implementation details (already exists)
8
+ *
9
+ * @author SpecWeave Team
10
+ * @version 2.0.0
11
+ */
12
+ import fs from 'fs-extra';
13
+ import path from 'path';
14
+ import { HierarchyMapper } from './hierarchy-mapper.js';
15
+ import { detectPrimaryGitHubRemote } from '../../utils/git-detector.js';
16
+ /**
17
+ * SpecDistributor - Distributes increment specs into hierarchical living docs
18
+ */
19
+ export class SpecDistributor {
20
+ constructor(projectRoot, config) {
21
+ this.githubRemote = null;
22
+ this.projectRoot = projectRoot;
23
+ // Detect project ID from config or use default
24
+ const projectId = config?.specsDir?.includes('/specs/')
25
+ ? config.specsDir.split('/specs/')[1]?.split('/')[0] || 'default'
26
+ : 'default';
27
+ this.config = {
28
+ specsDir: path.join(projectRoot, '.specweave', 'docs', 'internal', 'specs', projectId),
29
+ userStoriesSubdir: 'user-stories',
30
+ epicFilePattern: 'SPEC-{id}-{name}.md',
31
+ userStoryFilePattern: 'us-{id}-{name}.md',
32
+ generateFrontmatter: true,
33
+ generateCrossLinks: true,
34
+ preserveOriginal: true,
35
+ overwriteExisting: false,
36
+ createBackups: true,
37
+ ...config,
38
+ };
39
+ // Initialize HierarchyMapper with same project config
40
+ this.hierarchyMapper = new HierarchyMapper(projectRoot, {
41
+ projectId: config?.specsDir?.includes('/specs/')
42
+ ? config.specsDir.split('/specs/')[1]?.split('/')[0] || 'default'
43
+ : 'default'
44
+ });
45
+ }
46
+ /**
47
+ * Distribute increment spec into epic + user story files
48
+ */
49
+ async distribute(incrementId) {
50
+ const errors = [];
51
+ const warnings = [];
52
+ try {
53
+ // Detect GitHub remote for generating GitHub URLs (if not already detected)
54
+ if (!this.githubRemote) {
55
+ this.githubRemote = await detectPrimaryGitHubRemote(this.projectRoot);
56
+ }
57
+ // Step 0a: Parse increment spec to detect project ID
58
+ const parsed = await this.parseIncrementSpec(incrementId);
59
+ // Step 0b: Update config if project ID is specified in frontmatter
60
+ let projectId = this.config.specsDir.split('/specs/')[1]?.split('/')[0] || 'default';
61
+ if (parsed.project) {
62
+ projectId = parsed.project;
63
+ // Update config paths for correct project
64
+ this.config.specsDir = path.join(this.projectRoot, '.specweave', 'docs', 'internal', 'specs', projectId);
65
+ // Recreate hierarchy mapper with correct project ID
66
+ this.hierarchyMapper = new HierarchyMapper(this.projectRoot, {
67
+ projectId,
68
+ specsBaseDir: this.config.specsDir,
69
+ });
70
+ }
71
+ // Step 0c: Detect feature folder using HierarchyMapper (NEW: feature-based naming)
72
+ console.log(` 🔍 Detecting feature folder for ${incrementId}...`);
73
+ const epicMapping = await this.hierarchyMapper.detectFeatureMapping(incrementId);
74
+ console.log(` 📁 Mapped to ${epicMapping.featureFolder} (confidence: ${epicMapping.confidence}%, method: ${epicMapping.detectionMethod})`);
75
+ // Ensure specs base directory exists (for multi-project support)
76
+ await fs.ensureDir(this.config.specsDir);
77
+ // Step 2: Classify content (pass epicMapping to use feature folder as ID)
78
+ const classified = await this.classifyContent(parsed, epicMapping);
79
+ // Step 3: Generate epic file
80
+ const epic = await this.generateEpicFile(classified, incrementId);
81
+ // Step 4: Generate user story files
82
+ const userStories = await this.generateUserStoryFiles(classified, incrementId);
83
+ // Step 5: Write files (using epicMapping paths)
84
+ const epicPath = await this.writeEpicFile(epic, epicMapping);
85
+ const userStoryPaths = await this.writeUserStoryFiles(userStories, epicMapping);
86
+ // Step 6: Update tasks.md with bidirectional links to user stories (CRITICAL!)
87
+ await this.updateTasksWithUserStoryLinks(incrementId, userStories, epicMapping);
88
+ return {
89
+ epic,
90
+ userStories,
91
+ incrementId,
92
+ specId: epic.id,
93
+ totalStories: userStories.length,
94
+ totalFiles: 1 + userStories.length,
95
+ epicPath,
96
+ userStoryPaths,
97
+ success: true,
98
+ errors,
99
+ warnings,
100
+ };
101
+ }
102
+ catch (error) {
103
+ errors.push(`Distribution failed: ${error}`);
104
+ throw new Error(`Failed to distribute increment ${incrementId}: ${error}`);
105
+ }
106
+ }
107
+ /**
108
+ * Parse increment spec into structured data
109
+ */
110
+ async parseIncrementSpec(incrementId) {
111
+ const specPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'spec.md');
112
+ if (!fs.existsSync(specPath)) {
113
+ throw new Error(`Increment spec not found: ${specPath}`);
114
+ }
115
+ const content = await fs.readFile(specPath, 'utf-8');
116
+ // Load external links from metadata.json (NEW: source of truth for external integrations)
117
+ const externalLinks = await this.loadExternalLinks(incrementId);
118
+ // Extract YAML frontmatter if present
119
+ let frontmatter = {};
120
+ let bodyContent = content;
121
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
122
+ if (frontmatterMatch) {
123
+ try {
124
+ const yaml = await import('yaml');
125
+ frontmatter = yaml.parse(frontmatterMatch[1]);
126
+ bodyContent = content.slice(frontmatterMatch[0].length).trim();
127
+ }
128
+ catch (error) {
129
+ console.warn(` ⚠️ Failed to parse frontmatter for ${incrementId}`);
130
+ }
131
+ }
132
+ // Extract title (try multiple patterns)
133
+ let title = frontmatter.title || '';
134
+ if (!title) {
135
+ // Pattern 1: # SPEC-####: Title
136
+ const specTitleMatch = bodyContent.match(/^#\s+SPEC-\d+:\s+(.+)$/m);
137
+ if (specTitleMatch)
138
+ title = specTitleMatch[1].trim();
139
+ }
140
+ if (!title) {
141
+ // Pattern 2: # Increment ####: Title
142
+ const incTitleMatch = bodyContent.match(/^#\s+Increment\s+\d+:\s+(.+)$/m);
143
+ if (incTitleMatch)
144
+ title = incTitleMatch[1].trim();
145
+ }
146
+ if (!title) {
147
+ // Pattern 3: First # heading
148
+ const headingMatch = bodyContent.match(/^#\s+(.+)$/m);
149
+ if (headingMatch) {
150
+ title = headingMatch[1]
151
+ .replace(/^SPEC-\d+:\s*/, '')
152
+ .replace(/^Increment\s+\d+:\s*/, '')
153
+ .trim();
154
+ }
155
+ }
156
+ if (!title) {
157
+ // Fallback: Use increment ID
158
+ title = incrementId
159
+ .replace(/^\d+-/, '')
160
+ .split('-')
161
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
162
+ .join(' ');
163
+ }
164
+ // Extract overview (try multiple sections)
165
+ let overview = '';
166
+ // Try "Quick Overview" or "Executive Summary"
167
+ let overviewMatch = bodyContent.match(/##\s+(?:Quick\s+)?(?:Overview|Executive\s+Summary)\s*\n+([\s\S]*?)(?=\n##|\n---|\Z)/i);
168
+ if (overviewMatch)
169
+ overview = overviewMatch[1].trim();
170
+ if (!overview) {
171
+ // Try "Overview" section
172
+ overviewMatch = bodyContent.match(/##\s+Overview\s*\n+([\s\S]*?)(?=\n##|\n---|\Z)/i);
173
+ if (overviewMatch)
174
+ overview = overviewMatch[1].trim();
175
+ }
176
+ if (!overview) {
177
+ // Try "Problem Statement" section
178
+ const problemMatch = bodyContent.match(/##\s+Problem\s+Statement\s*\n+([\s\S]*?)(?=\n##|\n---|\Z)/i);
179
+ if (problemMatch) {
180
+ // Take first paragraph only
181
+ const firstPara = problemMatch[1].trim().split('\n\n')[0];
182
+ overview = firstPara;
183
+ }
184
+ }
185
+ if (!overview) {
186
+ // Fallback: First paragraph after title
187
+ const firstParaMatch = bodyContent.match(/^#[^\n]+\n+([^\n]+)/);
188
+ if (firstParaMatch)
189
+ overview = firstParaMatch[1].trim();
190
+ }
191
+ // Extract business value
192
+ const businessValue = [];
193
+ const businessValueMatch = content.match(/\*\*Business Value\*\*:\s*\n([\s\S]*?)(?=\n---|\n##|\Z)/i);
194
+ if (businessValueMatch) {
195
+ const lines = businessValueMatch[1].split('\n');
196
+ for (const line of lines) {
197
+ const bulletMatch = line.match(/^[-*]\s+\*\*(.+?)\*\*:\s+(.+)$/);
198
+ if (bulletMatch) {
199
+ businessValue.push(`${bulletMatch[1]}: ${bulletMatch[2]}`);
200
+ }
201
+ }
202
+ }
203
+ // Extract user stories
204
+ const userStories = await this.extractUserStories(content, incrementId);
205
+ return {
206
+ incrementId,
207
+ title,
208
+ overview,
209
+ businessValue,
210
+ project: frontmatter.project, // Project ID from frontmatter (if present)
211
+ userStories,
212
+ externalLinks, // NEW: External links from metadata.json
213
+ };
214
+ }
215
+ /**
216
+ * Load external links from metadata.json (source of truth)
217
+ */
218
+ async loadExternalLinks(incrementId) {
219
+ const metadataPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'metadata.json');
220
+ const links = {};
221
+ if (!fs.existsSync(metadataPath)) {
222
+ return links;
223
+ }
224
+ try {
225
+ const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
226
+ // Extract GitHub link
227
+ if (metadata.github?.url) {
228
+ links.github = metadata.github.url;
229
+ }
230
+ // Extract JIRA link
231
+ if (metadata.jira?.epicKey) {
232
+ links.jira = metadata.jira.epicKey; // Store just the key, can be converted to URL in template
233
+ }
234
+ // Extract Azure DevOps link
235
+ if (metadata.ado?.workItemUrl) {
236
+ links.ado = metadata.ado.workItemUrl;
237
+ }
238
+ }
239
+ catch (error) {
240
+ console.warn(` ⚠️ Failed to parse metadata.json for ${incrementId}: ${error}`);
241
+ }
242
+ return links;
243
+ }
244
+ /**
245
+ * Extract user stories from increment spec
246
+ */
247
+ async extractUserStories(content, incrementId) {
248
+ const userStories = [];
249
+ // Find all user story sections (supports both ### and #### patterns, with or without blank line)
250
+ const userStoryPattern = /^###+\s+(US-\d+):\s+(.+?)\s*\n([\s\S]*?)(?=^###+\s+US-|\n---\n|$)/gm;
251
+ let match;
252
+ while ((match = userStoryPattern.exec(content)) !== null) {
253
+ const id = match[1]; // US-001
254
+ const title = match[2];
255
+ const storyContent = match[3];
256
+ // Extract description (As a... I want... So that...) - supports both inline and separate line formats
257
+ const descMatch = storyContent.match(/\*\*As a\*\*\s+(.*?)\s*\n\*\*I want\*\*\s+(.*?)\s*\n\*\*So that\*\*\s+(.*?)(?=\n\n|\*\*Acceptance)/is);
258
+ const description = descMatch
259
+ ? `**As a** ${descMatch[1].trim()}\n**I want** ${descMatch[2].trim()}\n**So that** ${descMatch[3].trim()}`
260
+ : '';
261
+ // Extract acceptance criteria
262
+ const acceptanceCriteria = this.extractAcceptanceCriteria(storyContent);
263
+ // Extract business rationale
264
+ const rationaleMatch = storyContent.match(/\*\*Business Rationale\*\*:\s+(.*?)(?=\n\n---|\n\n##|$)/is);
265
+ const businessRationale = rationaleMatch ? rationaleMatch[1].trim() : undefined;
266
+ // Extract phase
267
+ const phaseMatch = content.substring(0, match.index).match(/###\s+(Phase\s+\d+:.*?)$/im);
268
+ const phase = phaseMatch ? phaseMatch[1] : undefined;
269
+ // Determine status (assume complete if in completed increment)
270
+ const status = 'complete'; // Can be enhanced later
271
+ userStories.push({
272
+ id,
273
+ title,
274
+ description,
275
+ acceptanceCriteria,
276
+ tasks: [], // Will be populated later
277
+ businessRationale,
278
+ status,
279
+ phase,
280
+ });
281
+ }
282
+ return userStories;
283
+ }
284
+ /**
285
+ * Extract acceptance criteria from user story content
286
+ */
287
+ extractAcceptanceCriteria(content) {
288
+ const criteria = [];
289
+ // Pattern: - [x] **AC-US1-01**: Description (P1, testable)
290
+ const acPattern = /^[-*]\s+\[([ x])\]\s+\*\*(.+?)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm;
291
+ let match;
292
+ while ((match = acPattern.exec(content)) !== null) {
293
+ const completed = match[1] === 'x';
294
+ const id = match[2]; // AC-US1-01
295
+ const description = match[3];
296
+ const metaString = match[4] || ''; // "P1, testable"
297
+ const priority = metaString.match(/P\d/)?.[0];
298
+ const testable = metaString.includes('testable');
299
+ criteria.push({
300
+ id,
301
+ description,
302
+ priority,
303
+ testable,
304
+ completed,
305
+ });
306
+ }
307
+ return criteria;
308
+ }
309
+ /**
310
+ * Classify content into epic vs user-story level
311
+ *
312
+ * NEW (v0.18.0): Uses feature folder name as ID (e.g., FS-25-11-14-release-management)
313
+ */
314
+ async classifyContent(parsed, epicMapping) {
315
+ // Use feature folder name as ID (e.g., FS-25-11-14-release-management)
316
+ // This ensures ID matches folder name
317
+ const specId = epicMapping.featureFolder;
318
+ return {
319
+ epic: {
320
+ id: specId,
321
+ title: parsed.title,
322
+ overview: parsed.overview,
323
+ businessValue: parsed.businessValue,
324
+ status: 'complete',
325
+ },
326
+ userStories: parsed.userStories,
327
+ implementationHistory: [
328
+ {
329
+ increment: parsed.incrementId,
330
+ stories: parsed.userStories.map((us) => us.id),
331
+ status: 'complete',
332
+ date: new Date().toISOString().split('T')[0],
333
+ },
334
+ ],
335
+ externalLinks: parsed.externalLinks || {},
336
+ relatedDocs: [],
337
+ };
338
+ }
339
+ /**
340
+ * Generate epic file
341
+ */
342
+ async generateEpicFile(classified, incrementId) {
343
+ // Generate user story summaries
344
+ const userStorySummaries = classified.userStories.map((us) => ({
345
+ id: us.id,
346
+ title: us.title,
347
+ status: us.status,
348
+ phase: us.phase,
349
+ filePath: this.generateUserStoryFilename(us.id, us.title), // User stories are directly in FS folder
350
+ }));
351
+ const completedStories = classified.userStories.filter((us) => us.status === 'complete').length;
352
+ return {
353
+ id: classified.epic.id,
354
+ title: classified.epic.title,
355
+ type: 'epic',
356
+ status: 'complete',
357
+ priority: classified.epic.priority,
358
+ created: new Date().toISOString().split('T')[0],
359
+ lastUpdated: new Date().toISOString().split('T')[0],
360
+ overview: classified.epic.overview,
361
+ businessValue: classified.epic.businessValue,
362
+ implementationHistory: classified.implementationHistory,
363
+ userStories: userStorySummaries,
364
+ externalLinks: classified.externalLinks,
365
+ relatedDocs: classified.relatedDocs,
366
+ totalStories: classified.userStories.length,
367
+ completedStories,
368
+ overallProgress: Math.round((completedStories / classified.userStories.length) * 100),
369
+ };
370
+ }
371
+ /**
372
+ * Generate user story files
373
+ */
374
+ async generateUserStoryFiles(classified, incrementId) {
375
+ const userStoryFiles = [];
376
+ // Load tasks from tasks.md to extract task references
377
+ const taskMap = await this.loadTaskReferences(incrementId);
378
+ for (const userStory of classified.userStories) {
379
+ // Find tasks that implement this user story
380
+ const tasks = this.findTasksForUserStory(userStory.id, taskMap);
381
+ // Find related user stories (same phase)
382
+ const relatedStories = classified.userStories
383
+ .filter((us) => us.id !== userStory.id && us.phase === userStory.phase)
384
+ .map((us) => ({
385
+ id: us.id,
386
+ title: us.title,
387
+ status: us.status,
388
+ phase: us.phase,
389
+ filePath: this.generateUserStoryFilename(us.id, us.title),
390
+ }));
391
+ userStoryFiles.push({
392
+ id: userStory.id,
393
+ epic: classified.epic.id,
394
+ title: userStory.title,
395
+ status: userStory.status,
396
+ priority: userStory.priority,
397
+ created: new Date().toISOString().split('T')[0],
398
+ completed: userStory.status === 'complete' ? new Date().toISOString().split('T')[0] : undefined,
399
+ description: userStory.description,
400
+ acceptanceCriteria: userStory.acceptanceCriteria,
401
+ implementation: {
402
+ increment: incrementId,
403
+ tasks,
404
+ },
405
+ businessRationale: userStory.businessRationale,
406
+ relatedStories,
407
+ phase: userStory.phase,
408
+ });
409
+ }
410
+ return userStoryFiles;
411
+ }
412
+ /**
413
+ * Load task references from tasks.md (with AC-ID extraction)
414
+ */
415
+ async loadTaskReferences(incrementId) {
416
+ const tasksPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'tasks.md');
417
+ const taskMap = new Map();
418
+ if (!fs.existsSync(tasksPath)) {
419
+ return taskMap;
420
+ }
421
+ const content = await fs.readFile(tasksPath, 'utf-8');
422
+ // Pattern: ### T-001: Task Title followed by **AC**: field
423
+ // Supports both ## and ### headings
424
+ const taskPattern = /^##+ (T-\d+):\s+(.+?)$[\s\S]*?\*\*AC\*\*:\s*([^\n]+)?/gm;
425
+ let match;
426
+ while ((match = taskPattern.exec(content)) !== null) {
427
+ const taskId = match[1]; // T-001
428
+ const taskTitle = match[2];
429
+ const acList = match[3] || ''; // AC-US1-01, AC-US1-02
430
+ const anchor = this.generateTaskAnchor(taskId, taskTitle);
431
+ // Extract AC-IDs from the list
432
+ const acIds = [];
433
+ const acPattern = /AC-US\d+-\d+/g;
434
+ let acMatch;
435
+ while ((acMatch = acPattern.exec(acList)) !== null) {
436
+ acIds.push(acMatch[0]); // AC-US1-01
437
+ }
438
+ taskMap.set(taskId, {
439
+ id: taskId,
440
+ title: taskTitle,
441
+ anchor,
442
+ path: `../../../../../increments/${incrementId}/tasks.md${anchor}`,
443
+ acIds,
444
+ });
445
+ }
446
+ return taskMap;
447
+ }
448
+ /**
449
+ * Find tasks that implement a user story (using AC-ID based filtering)
450
+ */
451
+ findTasksForUserStory(userStoryId, taskMap) {
452
+ const tasks = [];
453
+ // Extract US number from userStoryId (US-001 → "1")
454
+ const usMatch = userStoryId.match(/US-(\d+)/);
455
+ if (!usMatch) {
456
+ return tasks;
457
+ }
458
+ const usNumber = parseInt(usMatch[1], 10); // 1
459
+ // Find tasks that reference this user story's AC-IDs
460
+ for (const task of taskMap.values()) {
461
+ // Check if task has AC-IDs for this user story (AC-US1-01, AC-US1-02, etc.)
462
+ const hasMatchingAC = task.acIds.some((acId) => {
463
+ const acMatch = acId.match(/AC-US(\d+)-\d+/);
464
+ return acMatch && parseInt(acMatch[1], 10) === usNumber;
465
+ });
466
+ if (hasMatchingAC) {
467
+ tasks.push(task);
468
+ }
469
+ }
470
+ return tasks;
471
+ }
472
+ /**
473
+ * Generate task anchor
474
+ */
475
+ generateTaskAnchor(taskId, taskTitle) {
476
+ const slug = taskTitle
477
+ .toLowerCase()
478
+ .replace(/[^a-z0-9]+/g, '-')
479
+ .replace(/^-|-$/g, '');
480
+ return `#${taskId.toLowerCase()}-${slug}`;
481
+ }
482
+ /**
483
+ * Generate user story filename
484
+ */
485
+ generateUserStoryFilename(id, title) {
486
+ const slug = title
487
+ .toLowerCase()
488
+ .replace(/[^a-z0-9]+/g, '-')
489
+ .replace(/^-|-$/g, '');
490
+ return `${id.toLowerCase()}-${slug}.md`;
491
+ }
492
+ /**
493
+ * Write feature file to disk (NEW: writes to FEATURE.md instead of README.md)
494
+ */
495
+ async writeEpicFile(epic, epicMapping) {
496
+ // Write to feature-folder/FEATURE.md (feature overview - high-level summary)
497
+ const featurePath = path.join(epicMapping.featurePath, 'FEATURE.md');
498
+ const content = this.formatEpicFile(epic);
499
+ await fs.ensureDir(path.dirname(featurePath));
500
+ await fs.writeFile(featurePath, content, 'utf-8');
501
+ console.log(` ✅ Written feature overview to ${epicMapping.featureFolder}/FEATURE.md`);
502
+ return featurePath;
503
+ }
504
+ /**
505
+ * Write user story files to disk (NEW: writes directly to feature folder)
506
+ */
507
+ async writeUserStoryFiles(userStories, epicMapping) {
508
+ // Write user stories directly to feature folder (not in subfolder)
509
+ const featureDir = epicMapping.featurePath;
510
+ await fs.ensureDir(featureDir);
511
+ const paths = [];
512
+ for (const userStory of userStories) {
513
+ const filename = this.generateUserStoryFilename(userStory.id, userStory.title);
514
+ const filePath = path.join(featureDir, filename);
515
+ const content = this.formatUserStoryFile(userStory);
516
+ await fs.writeFile(filePath, content, 'utf-8');
517
+ paths.push(filePath);
518
+ }
519
+ console.log(` ✅ Written ${userStories.length} user stories directly to ${epicMapping.featureFolder}/`);
520
+ return paths;
521
+ }
522
+ /**
523
+ * Format epic file as markdown
524
+ */
525
+ formatEpicFile(epic) {
526
+ const lines = [];
527
+ // Frontmatter
528
+ lines.push('---');
529
+ lines.push(`id: ${epic.id}`);
530
+ lines.push(`title: "${epic.title}"`);
531
+ lines.push(`type: epic`);
532
+ lines.push(`status: ${epic.status}`);
533
+ if (epic.priority)
534
+ lines.push(`priority: ${epic.priority}`);
535
+ lines.push(`created: ${epic.created}`);
536
+ lines.push(`last_updated: ${epic.lastUpdated}`);
537
+ lines.push('---');
538
+ lines.push('');
539
+ // Title
540
+ lines.push(`# ${epic.id}: ${epic.title}`);
541
+ lines.push('');
542
+ lines.push(epic.overview);
543
+ lines.push('');
544
+ // Business Value
545
+ if (epic.businessValue.length > 0) {
546
+ lines.push('**Business Value**:');
547
+ lines.push('');
548
+ for (const value of epic.businessValue) {
549
+ lines.push(`- **${value.split(':')[0]}**: ${value.split(':').slice(1).join(':').trim()}`);
550
+ }
551
+ lines.push('');
552
+ }
553
+ lines.push('---');
554
+ lines.push('');
555
+ // Implementation History
556
+ if (epic.implementationHistory.length > 0) {
557
+ lines.push('## Implementation History');
558
+ lines.push('');
559
+ lines.push('| Increment | User Stories | Status | Completion Date |');
560
+ lines.push('|-----------|--------------|--------|----------------|');
561
+ for (const entry of epic.implementationHistory) {
562
+ const statusEmoji = entry.status === 'complete' ? '✅' : entry.status === 'in-progress' ? '⏳' : '📋';
563
+ // Generate increment link (prefer GitHub URL for deployed version, fallback to relative path)
564
+ let incrementLink;
565
+ if (this.githubRemote && this.githubRemote.owner && this.githubRemote.repo) {
566
+ // GitHub URL (works on deployed version)
567
+ incrementLink = `[${entry.increment}](https://github.com/${this.githubRemote.owner}/${this.githubRemote.repo}/tree/develop/.specweave/increments/${entry.increment})`;
568
+ }
569
+ else {
570
+ // Fallback to relative path (5 levels up, not 4!)
571
+ incrementLink = `[${entry.increment}](../../../../../increments/${entry.increment}/tasks.md)`;
572
+ }
573
+ // Handle empty stories array (no user stories in spec)
574
+ let storiesText;
575
+ if (entry.stories.length === 0) {
576
+ storiesText = 'Implementation only (no user stories)';
577
+ }
578
+ else if (entry.stories.length === 1) {
579
+ storiesText = entry.stories[0];
580
+ }
581
+ else {
582
+ const firstStory = entry.stories[0];
583
+ const lastStory = entry.stories[entry.stories.length - 1];
584
+ storiesText = `${firstStory} through ${lastStory} (all)`;
585
+ }
586
+ lines.push(`| ${incrementLink} | ${storiesText} | ${statusEmoji} ${entry.status.charAt(0).toUpperCase() + entry.status.slice(1)} | ${entry.date || '-'} |`);
587
+ }
588
+ lines.push('');
589
+ // Handle division by zero (no user stories)
590
+ const progressText = epic.totalStories === 0
591
+ ? 'No user stories (implementation only)'
592
+ : `${epic.completedStories}/${epic.totalStories} user stories complete (${epic.overallProgress}%)`;
593
+ lines.push(`**Overall Progress**: ${progressText}`);
594
+ lines.push('');
595
+ lines.push('---');
596
+ lines.push('');
597
+ }
598
+ // User Stories
599
+ lines.push('## User Stories');
600
+ lines.push('');
601
+ // Group by phase
602
+ const phases = new Map();
603
+ for (const story of epic.userStories) {
604
+ const phase = story.phase || 'General';
605
+ if (!phases.has(phase)) {
606
+ phases.set(phase, []);
607
+ }
608
+ phases.get(phase).push(story);
609
+ }
610
+ for (const [phase, stories] of phases) {
611
+ if (phase !== 'General') {
612
+ lines.push(`### ${phase}`);
613
+ lines.push('');
614
+ }
615
+ for (const story of stories) {
616
+ const statusEmoji = story.status === 'complete' ? '✅' : story.status === 'in-progress' ? '⏳' : '📋';
617
+ lines.push(`- [${story.id}: ${story.title}](${story.filePath}) - ${statusEmoji} ${story.status.charAt(0).toUpperCase() + story.status.slice(1)}`);
618
+ }
619
+ lines.push('');
620
+ }
621
+ lines.push('---');
622
+ lines.push('');
623
+ // External Tool Integration (only if there are actual links)
624
+ const hasExternalLinks = epic.externalLinks.github || epic.externalLinks.jira || epic.externalLinks.ado;
625
+ if (hasExternalLinks) {
626
+ lines.push('## External Tool Integration');
627
+ lines.push('');
628
+ // Only show tools that have actual links
629
+ if (epic.externalLinks.github) {
630
+ lines.push(`**GitHub Project**: [${epic.externalLinks.github}](${epic.externalLinks.github})`);
631
+ }
632
+ if (epic.externalLinks.jira) {
633
+ // Convert JIRA key to URL (if it's just a key like SCRUM-23)
634
+ const jiraUrl = epic.externalLinks.jira.startsWith('http')
635
+ ? epic.externalLinks.jira
636
+ : `https://jira.atlassian.com/browse/${epic.externalLinks.jira}`;
637
+ lines.push(`**JIRA Epic**: [${epic.externalLinks.jira}](${jiraUrl})`);
638
+ }
639
+ if (epic.externalLinks.ado) {
640
+ lines.push(`**Azure DevOps**: [${epic.externalLinks.ado}](${epic.externalLinks.ado})`);
641
+ }
642
+ lines.push('');
643
+ }
644
+ return lines.join('\n');
645
+ }
646
+ /**
647
+ * Format user story file as markdown
648
+ */
649
+ formatUserStoryFile(userStory) {
650
+ const lines = [];
651
+ // Frontmatter
652
+ lines.push('---');
653
+ lines.push(`id: ${userStory.id}`);
654
+ lines.push(`epic: ${userStory.epic}`);
655
+ lines.push(`title: "${userStory.title}"`);
656
+ lines.push(`status: ${userStory.status}`);
657
+ if (userStory.priority)
658
+ lines.push(`priority: ${userStory.priority}`);
659
+ lines.push(`created: ${userStory.created}`);
660
+ if (userStory.completed)
661
+ lines.push(`completed: ${userStory.completed}`);
662
+ lines.push('---');
663
+ lines.push('');
664
+ // Title
665
+ lines.push(`# ${userStory.id}: ${userStory.title}`);
666
+ lines.push('');
667
+ // Feature link (FEATURE.md in same folder)
668
+ lines.push(`**Feature**: [${userStory.epic}](./FEATURE.md)`);
669
+ lines.push('');
670
+ // Description
671
+ lines.push(userStory.description);
672
+ lines.push('');
673
+ lines.push('---');
674
+ lines.push('');
675
+ // Acceptance Criteria
676
+ lines.push('## Acceptance Criteria');
677
+ lines.push('');
678
+ for (const ac of userStory.acceptanceCriteria) {
679
+ const checkbox = ac.completed ? '[x]' : '[ ]';
680
+ const priorityText = ac.priority ? ` (${ac.priority}, testable)` : '';
681
+ lines.push(`- ${checkbox} **${ac.id}**: ${ac.description}${priorityText}`);
682
+ }
683
+ lines.push('');
684
+ lines.push('---');
685
+ lines.push('');
686
+ // Implementation
687
+ lines.push('## Implementation');
688
+ lines.push('');
689
+ lines.push(`**Increment**: [${userStory.implementation.increment}](${userStory.implementation.tasks[0]?.path.replace(/#.*$/, '')})`);
690
+ lines.push('');
691
+ lines.push('**Tasks**:');
692
+ for (const task of userStory.implementation.tasks) {
693
+ lines.push(`- [${task.id}: ${task.title}](${task.path})`);
694
+ }
695
+ lines.push('');
696
+ // Business Rationale
697
+ if (userStory.businessRationale) {
698
+ lines.push('---');
699
+ lines.push('');
700
+ lines.push('## Business Rationale');
701
+ lines.push('');
702
+ lines.push(userStory.businessRationale);
703
+ lines.push('');
704
+ }
705
+ // Related User Stories
706
+ if (userStory.relatedStories.length > 0) {
707
+ lines.push('---');
708
+ lines.push('');
709
+ lines.push('## Related User Stories');
710
+ lines.push('');
711
+ for (const related of userStory.relatedStories) {
712
+ lines.push(`- [${related.id}: ${related.title}](${related.filePath})`);
713
+ }
714
+ lines.push('');
715
+ }
716
+ lines.push('---');
717
+ lines.push('');
718
+ lines.push(`**Status**: ${userStory.status === 'complete' ? '✅' : '⏳'} ${userStory.status.charAt(0).toUpperCase() + userStory.status.slice(1)}`);
719
+ if (userStory.completed) {
720
+ lines.push(`**Completed**: ${userStory.completed}`);
721
+ }
722
+ lines.push('');
723
+ return lines.join('\n');
724
+ }
725
+ /**
726
+ * Update tasks.md with bidirectional links to user stories (CRITICAL!)
727
+ *
728
+ * This creates bidirectional traceability:
729
+ * - User Story → Tasks (already done in us-*.md files)
730
+ * - Tasks → User Story (NEW - added here)
731
+ *
732
+ * When a task implements a user story, this adds a link in tasks.md:
733
+ * **User Story**: [US-001: Title](../../docs/internal/specs/{project}/{feature}/us-001-*.md)
734
+ */
735
+ async updateTasksWithUserStoryLinks(incrementId, userStories, epicMapping) {
736
+ const tasksPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'tasks.md');
737
+ // Check if tasks.md exists
738
+ if (!fs.existsSync(tasksPath)) {
739
+ console.log(` ⚠️ tasks.md not found for ${incrementId}, skipping bidirectional link update`);
740
+ return;
741
+ }
742
+ try {
743
+ const tasksContent = await fs.readFile(tasksPath, 'utf-8');
744
+ // Parse tasks to create task → user story mapping
745
+ const taskToUSMapping = this.mapTasksToUserStories(tasksContent, userStories);
746
+ if (Object.keys(taskToUSMapping).length === 0) {
747
+ console.log(` ℹ️ No AC-based task-to-US mapping found, skipping bidirectional links`);
748
+ return;
749
+ }
750
+ // Update tasks.md content with user story links
751
+ let updatedContent = tasksContent;
752
+ let linksAdded = 0;
753
+ for (const [taskId, userStory] of Object.entries(taskToUSMapping)) {
754
+ // Generate relative path from tasks.md to user story file
755
+ const projectId = epicMapping.featurePath.split('/specs/')[1]?.split('/')[0] || 'default';
756
+ const featureFolder = epicMapping.featureFolder;
757
+ const userStoryFile = this.generateUserStoryFilename(userStory.id, userStory.title);
758
+ const relativePath = `../../docs/internal/specs/${projectId}/${featureFolder}/${userStoryFile}`;
759
+ // Find task section and add link if not already present (supports both ## and ### headings)
760
+ // CRITICAL: Remove 'g' flag to prevent multiple matches of the same task
761
+ const taskPattern = new RegExp(`(^##+ ${taskId}:.*?$\\n)([\\s\\S]*?)(?=^##+ T-|^---$|$)`, 'm');
762
+ // Only replace once per task
763
+ let replaced = false;
764
+ updatedContent = updatedContent.replace(taskPattern, (match, heading, body) => {
765
+ // Prevent multiple replacements
766
+ if (replaced) {
767
+ return match;
768
+ }
769
+ // Check if link already exists
770
+ if (body.includes('**User Story**:')) {
771
+ return match; // Link already exists
772
+ }
773
+ // Insert link right after the heading (before any content)
774
+ const linkLine = `**User Story**: [${userStory.id}: ${userStory.title}](${relativePath})\n\n`;
775
+ replaced = true;
776
+ linksAdded++;
777
+ return heading + linkLine + body;
778
+ });
779
+ }
780
+ // Write updated tasks.md
781
+ if (linksAdded > 0) {
782
+ await fs.writeFile(tasksPath, updatedContent, 'utf-8');
783
+ console.log(` 🔗 Added ${linksAdded} bidirectional links to tasks.md`);
784
+ }
785
+ else {
786
+ console.log(` ℹ️ All tasks already have user story links`);
787
+ }
788
+ }
789
+ catch (error) {
790
+ console.warn(` ⚠️ Failed to update tasks.md with bidirectional links: ${error}`);
791
+ }
792
+ }
793
+ /**
794
+ * Map tasks to user stories using AC-IDs
795
+ *
796
+ * Extracts AC-IDs from tasks (e.g., AC-US1-01) and maps them to user stories (e.g., US-001)
797
+ */
798
+ mapTasksToUserStories(tasksContent, userStories) {
799
+ const mapping = {};
800
+ // Extract all tasks with their AC-IDs (supports both ## and ### headings)
801
+ const taskPattern = /^##+ (T-\d+):.*?$\n[\s\S]*?\*\*AC\*\*:\s*([^\n]+)/gm;
802
+ let match;
803
+ while ((match = taskPattern.exec(tasksContent)) !== null) {
804
+ const taskId = match[1]; // T-001
805
+ const acList = match[2]; // AC-US1-01, AC-US1-02
806
+ // Extract user story IDs from AC-IDs (AC-US1-01 → US-001)
807
+ const acPattern = /AC-US(\d+)-\d+/g;
808
+ let acMatch;
809
+ while ((acMatch = acPattern.exec(acList)) !== null) {
810
+ const usNumber = acMatch[1]; // "1"
811
+ const usId = `US-${usNumber.padStart(3, '0')}`; // "US-001"
812
+ // Find matching user story
813
+ const userStory = userStories.find(us => us.id === usId);
814
+ if (userStory) {
815
+ mapping[taskId] = userStory;
816
+ break; // One task can only map to one primary user story
817
+ }
818
+ }
819
+ }
820
+ return mapping;
821
+ }
822
+ }
823
+ //# sourceMappingURL=spec-distributor.js.map