specweave 0.17.16 → 0.18.0

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 (161) hide show
  1. package/CLAUDE.md +405 -2495
  2. package/README.md +92 -2
  3. package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
  4. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +188 -36
  5. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
  6. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
  7. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
  8. package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
  9. package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
  10. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
  11. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
  12. package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
  13. package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
  14. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
  15. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
  16. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
  17. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
  18. package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
  19. package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
  20. package/dist/plugins/specweave-github/lib/github-client.js +25 -13
  21. package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
  22. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
  23. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
  24. package/dist/plugins/specweave-github/lib/github-epic-sync.js +451 -0
  25. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
  26. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
  27. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
  28. package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
  29. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
  30. package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
  31. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
  32. package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
  33. package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
  34. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
  35. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
  36. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
  37. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
  38. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
  39. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
  40. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
  41. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
  42. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  43. package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
  44. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  45. package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
  46. package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
  47. package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
  48. package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
  49. package/dist/src/core/living-docs/index.d.ts +10 -84
  50. package/dist/src/core/living-docs/index.d.ts.map +1 -1
  51. package/dist/src/core/living-docs/index.js +10 -164
  52. package/dist/src/core/living-docs/index.js.map +1 -1
  53. package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
  54. package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
  55. package/dist/src/core/living-docs/spec-distributor.js +823 -0
  56. package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
  57. package/dist/src/core/living-docs/types.d.ts +201 -0
  58. package/dist/src/core/living-docs/types.d.ts.map +1 -0
  59. package/dist/src/core/living-docs/types.js +15 -0
  60. package/dist/src/core/living-docs/types.js.map +1 -0
  61. package/dist/src/core/logging/prompt-logger.d.ts +70 -0
  62. package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
  63. package/dist/src/core/logging/prompt-logger.js +247 -0
  64. package/dist/src/core/logging/prompt-logger.js.map +1 -0
  65. package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
  66. package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
  67. package/dist/src/core/status-line/status-line-manager.js +33 -70
  68. package/dist/src/core/status-line/status-line-manager.js.map +1 -1
  69. package/dist/src/core/status-line/types.d.ts +19 -31
  70. package/dist/src/core/status-line/types.d.ts.map +1 -1
  71. package/dist/src/core/status-line/types.js +5 -9
  72. package/dist/src/core/status-line/types.js.map +1 -1
  73. package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
  74. package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
  75. package/dist/src/core/sync/conflict-resolver.js +108 -0
  76. package/dist/src/core/sync/conflict-resolver.js.map +1 -0
  77. package/dist/src/core/sync/enhanced-content-builder.d.ts +77 -0
  78. package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
  79. package/dist/src/core/sync/enhanced-content-builder.js +199 -0
  80. package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
  81. package/dist/src/core/sync/label-detector.d.ts +66 -0
  82. package/dist/src/core/sync/label-detector.d.ts.map +1 -0
  83. package/dist/src/core/sync/label-detector.js +211 -0
  84. package/dist/src/core/sync/label-detector.js.map +1 -0
  85. package/dist/src/core/sync/retry-logic.d.ts +64 -0
  86. package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
  87. package/dist/src/core/sync/retry-logic.js +165 -0
  88. package/dist/src/core/sync/retry-logic.js.map +1 -0
  89. package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
  90. package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
  91. package/dist/src/core/sync/spec-increment-mapper.js +424 -0
  92. package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
  93. package/dist/src/core/sync/status-cache.d.ts +91 -0
  94. package/dist/src/core/sync/status-cache.d.ts.map +1 -0
  95. package/dist/src/core/sync/status-cache.js +140 -0
  96. package/dist/src/core/sync/status-cache.js.map +1 -0
  97. package/dist/src/core/sync/status-mapper.d.ts +69 -0
  98. package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
  99. package/dist/src/core/sync/status-mapper.js +90 -0
  100. package/dist/src/core/sync/status-mapper.js.map +1 -0
  101. package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
  102. package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
  103. package/dist/src/core/sync/status-sync-engine.js +347 -0
  104. package/dist/src/core/sync/status-sync-engine.js.map +1 -0
  105. package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
  106. package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
  107. package/dist/src/core/sync/sync-event-logger.js +103 -0
  108. package/dist/src/core/sync/sync-event-logger.js.map +1 -0
  109. package/dist/src/core/sync/workflow-detector.d.ts +95 -0
  110. package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
  111. package/dist/src/core/sync/workflow-detector.js +175 -0
  112. package/dist/src/core/sync/workflow-detector.js.map +1 -0
  113. package/dist/src/core/types/config.d.ts.map +1 -1
  114. package/dist/src/core/types/config.js +31 -0
  115. package/dist/src/core/types/config.js.map +1 -1
  116. package/dist/src/utils/github-url.d.ts +53 -0
  117. package/dist/src/utils/github-url.d.ts.map +1 -0
  118. package/dist/src/utils/github-url.js +90 -0
  119. package/dist/src/utils/github-url.js.map +1 -0
  120. package/dist/src/utils/spec-parser.d.ts +145 -0
  121. package/dist/src/utils/spec-parser.d.ts.map +1 -0
  122. package/dist/src/utils/spec-parser.js +640 -0
  123. package/dist/src/utils/spec-parser.js.map +1 -0
  124. package/package.json +1 -1
  125. package/plugins/specweave/agents/pm/AGENT.md +1 -1
  126. package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
  127. package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
  128. package/plugins/specweave/commands/specweave-done.md +163 -0
  129. package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
  130. package/plugins/specweave/hooks/post-increment-planning.sh +107 -35
  131. package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
  132. package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
  133. package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
  134. package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
  135. package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
  136. package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
  137. package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
  138. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
  139. package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
  140. package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
  141. package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
  142. package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
  143. package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
  144. package/plugins/specweave-github/lib/github-client.js +21 -10
  145. package/plugins/specweave-github/lib/github-client.ts +27 -16
  146. package/plugins/specweave-github/lib/github-epic-sync.js +489 -0
  147. package/plugins/specweave-github/lib/github-epic-sync.ts +690 -0
  148. package/plugins/specweave-github/lib/github-status-sync.js +71 -0
  149. package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
  150. package/plugins/specweave-github/lib/task-sync.js +33 -2
  151. package/plugins/specweave-github/lib/task-sync.ts +44 -2
  152. package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
  153. package/plugins/specweave-jira/lib/enhanced-jira-sync.ts.disabled +222 -0
  154. package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
  155. package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
  156. package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
  157. package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
  158. package/src/templates/AGENTS.md.template +88 -1
  159. package/src/templates/CLAUDE.md.template +49 -0
  160. package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
  161. package/plugins/specweave/skills/plugin-installer/SKILL.md +0 -353
@@ -0,0 +1,690 @@
1
+ /**
2
+ * GitHub Epic Sync - Hierarchical synchronization for Epic folder structure
3
+ *
4
+ * Architecture:
5
+ * - Epic (FS-001) → GitHub Milestone
6
+ * - Increment (0001-core-framework) → GitHub Issue (linked to Milestone)
7
+ *
8
+ * This implements the Universal Hierarchy architecture for GitHub.
9
+ */
10
+
11
+ import { readdir, readFile, writeFile } from 'fs/promises';
12
+ import { existsSync } from 'fs';
13
+ import * as path from 'path';
14
+ import * as yaml from 'yaml';
15
+ import { GitHubClientV2 } from './github-client-v2.js';
16
+ import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
17
+ import { DuplicateDetector } from './duplicate-detector.js';
18
+
19
+ interface EpicFrontmatter {
20
+ id: string;
21
+ title: string;
22
+ type: 'epic';
23
+ status: 'complete' | 'active' | 'planning' | 'archived';
24
+ priority: string;
25
+ created: string;
26
+ last_updated: string;
27
+ external_tools: {
28
+ github: {
29
+ type: 'milestone';
30
+ id: number | null;
31
+ url: string | null;
32
+ };
33
+ jira: {
34
+ type: 'epic';
35
+ key: string | null;
36
+ url: string | null;
37
+ };
38
+ ado: {
39
+ type: 'feature';
40
+ id: number | null;
41
+ url: string | null;
42
+ };
43
+ };
44
+ increments: Array<{
45
+ id: string;
46
+ status: string;
47
+ external: {
48
+ github: number | null;
49
+ jira: string | null;
50
+ ado: number | null;
51
+ };
52
+ }>;
53
+ total_increments: number;
54
+ completed_increments: number;
55
+ progress: string;
56
+ }
57
+
58
+ interface IncrementFrontmatter {
59
+ id: string;
60
+ epic: string;
61
+ type?: string;
62
+ status?: string;
63
+ external?: {
64
+ github?: {
65
+ issue: number | null;
66
+ url: string | null;
67
+ };
68
+ jira?: {
69
+ story: string | null;
70
+ url: string | null;
71
+ };
72
+ ado?: {
73
+ user_story: number | null;
74
+ url: string | null;
75
+ };
76
+ };
77
+ }
78
+
79
+ export class GitHubEpicSync {
80
+ private client: GitHubClientV2;
81
+ private specsDir: string;
82
+
83
+ constructor(client: GitHubClientV2, specsDir: string) {
84
+ this.client = client;
85
+ this.specsDir = specsDir;
86
+ }
87
+
88
+ /**
89
+ * Sync Epic folder to GitHub (Milestone + Issues)
90
+ */
91
+ async syncEpicToGitHub(epicId: string): Promise<{
92
+ milestoneNumber: number;
93
+ milestoneUrl: string;
94
+ issuesCreated: number;
95
+ issuesUpdated: number;
96
+ duplicatesDetected: number;
97
+ }> {
98
+ console.log(`\nšŸ”„ Syncing Epic ${epicId} to GitHub...`);
99
+
100
+ // 1. Load Epic FEATURE.md
101
+ const epicFolder = await this.findEpicFolder(epicId);
102
+ if (!epicFolder) {
103
+ throw new Error(`Epic ${epicId} not found in ${this.specsDir}`);
104
+ }
105
+
106
+ const readmePath = path.join(epicFolder, 'FEATURE.md');
107
+ const epicData = await this.parseEpicReadme(readmePath);
108
+
109
+ console.log(` šŸ“¦ Epic: ${epicData.title}`);
110
+ console.log(` šŸ“Š Increments: ${epicData.total_increments}`);
111
+
112
+ // 2. Create or update GitHub Milestone
113
+ let milestoneNumber = epicData.external_tools.github.id;
114
+ let milestoneUrl = epicData.external_tools.github.url;
115
+
116
+ if (!milestoneNumber) {
117
+ console.log(` šŸš€ Creating GitHub Milestone...`);
118
+ const milestone = await this.createMilestone(epicData);
119
+ milestoneNumber = milestone.number;
120
+ milestoneUrl = milestone.url;
121
+ console.log(` āœ… Created Milestone #${milestoneNumber}`);
122
+
123
+ // Update Epic README with Milestone ID
124
+ await this.updateEpicReadme(readmePath, {
125
+ type: 'milestone',
126
+ id: milestoneNumber,
127
+ url: milestoneUrl,
128
+ });
129
+ } else {
130
+ console.log(` ā™»ļø Updating existing Milestone #${milestoneNumber}...`);
131
+ await this.updateMilestone(milestoneNumber, epicData);
132
+ console.log(` āœ… Updated Milestone #${milestoneNumber}`);
133
+ }
134
+
135
+ // 3. Sync each increment as GitHub Issue (WITH DUPLICATE DETECTION!)
136
+ let issuesCreated = 0;
137
+ let issuesUpdated = 0;
138
+ let duplicatesDetected = 0;
139
+
140
+ console.log(`\n šŸ“ Syncing ${epicData.increments.length} increments...`);
141
+
142
+ for (const increment of epicData.increments) {
143
+ const incrementFile = path.join(epicFolder, `${increment.id}.md`);
144
+ if (!existsSync(incrementFile)) {
145
+ console.log(` āš ļø Increment file not found: ${increment.id}.md`);
146
+ continue;
147
+ }
148
+
149
+ const incrementData = await this.parseIncrementFile(incrementFile);
150
+ const existingIssue = increment.external.github;
151
+
152
+ if (!existingIssue) {
153
+ // NEW: Check GitHub FIRST before creating (duplicate detection!)
154
+ console.log(` šŸ” Checking GitHub for existing issue: ${increment.id}...`);
155
+ const githubIssue = await this.findExistingIssue(epicData.id, increment.id);
156
+
157
+ if (githubIssue) {
158
+ // Found existing issue! Re-link it instead of creating duplicate
159
+ console.log(` ā™»ļø Found existing Issue #${githubIssue} for ${increment.id} (self-healing)`);
160
+ await this.updateIncrementExternalLink(
161
+ readmePath,
162
+ incrementFile,
163
+ increment.id,
164
+ githubIssue
165
+ );
166
+ issuesUpdated++;
167
+ duplicatesDetected++;
168
+ } else {
169
+ // Truly new issue - create it
170
+ const issueNumber = await this.createIssue(
171
+ epicData.id,
172
+ incrementData,
173
+ milestoneNumber!
174
+ );
175
+ issuesCreated++;
176
+ console.log(` āœ… Created Issue #${issueNumber} for ${increment.id}`);
177
+
178
+ // Update Epic README and Increment file
179
+ await this.updateIncrementExternalLink(
180
+ readmePath,
181
+ incrementFile,
182
+ increment.id,
183
+ issueNumber
184
+ );
185
+ }
186
+ } else {
187
+ // Update existing issue
188
+ await this.updateIssue(
189
+ epicData.id,
190
+ existingIssue,
191
+ incrementData,
192
+ milestoneNumber!
193
+ );
194
+ issuesUpdated++;
195
+ console.log(` ā™»ļø Updated Issue #${existingIssue} for ${increment.id}`);
196
+ }
197
+ }
198
+
199
+ console.log(`\nāœ… Epic sync complete!`);
200
+ console.log(` Milestone: ${milestoneUrl}`);
201
+ console.log(` Issues created: ${issuesCreated}`);
202
+ console.log(` Issues updated: ${issuesUpdated}`);
203
+ if (duplicatesDetected > 0) {
204
+ console.log(` šŸ”— Self-healed: ${duplicatesDetected} (found existing issues)`);
205
+ }
206
+
207
+ // 4. Post-sync validation: Check for duplicates
208
+ console.log(`\nšŸ” Post-sync validation...`);
209
+ const validation = await this.validateSync(epicData.id);
210
+
211
+ if (validation.duplicatesFound > 0) {
212
+ console.warn(`\nāš ļø WARNING: ${validation.duplicatesFound} duplicate(s) detected!`);
213
+ console.warn(` This may indicate a previous sync created duplicates.`);
214
+ console.warn(` Run cleanup command to resolve:`);
215
+ console.warn(` /specweave-github:cleanup-duplicates ${epicData.id}`);
216
+ console.warn(`\n Duplicate groups:`);
217
+ for (const [title, numbers] of validation.duplicateGroups) {
218
+ console.warn(` - "${title}": Issues #${numbers.join(', #')}`);
219
+ }
220
+ } else {
221
+ console.log(` āœ… No duplicates found`);
222
+ }
223
+
224
+ return {
225
+ milestoneNumber: milestoneNumber!,
226
+ milestoneUrl: milestoneUrl!,
227
+ issuesCreated,
228
+ issuesUpdated,
229
+ duplicatesDetected,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Validate sync results - check for duplicate issues
235
+ *
236
+ * Searches GitHub for all issues with the Epic ID and detects duplicates
237
+ * (multiple issues with the same title).
238
+ *
239
+ * @param epicId - Epic ID (e.g., FS-031)
240
+ * @returns Validation result with duplicate count and groups
241
+ */
242
+ private async validateSync(epicId: string): Promise<{
243
+ totalIssues: number;
244
+ duplicatesFound: number;
245
+ duplicateGroups: Array<[string, number[]]>;
246
+ }> {
247
+ try {
248
+ // Search for all issues with Epic ID in title
249
+ const titlePattern = `[${epicId}]`;
250
+
251
+ const result = await execFileNoThrow('gh', [
252
+ 'issue',
253
+ 'list',
254
+ '--search',
255
+ `"${titlePattern}" in:title`,
256
+ '--json',
257
+ 'number,title,state',
258
+ '--limit',
259
+ '100', // Check up to 100 issues
260
+ '--state',
261
+ 'all', // Include both open and closed
262
+ ]);
263
+
264
+ if (result.exitCode !== 0 || !result.stdout) {
265
+ console.warn(` āš ļø Validation failed: ${result.stderr || 'unknown error'}`);
266
+ return { totalIssues: 0, duplicatesFound: 0, duplicateGroups: [] };
267
+ }
268
+
269
+ const issues = JSON.parse(result.stdout) as Array<{
270
+ number: number;
271
+ title: string;
272
+ state: string;
273
+ }>;
274
+
275
+ // Group issues by title
276
+ const titleGroups = new Map<string, number[]>();
277
+ for (const issue of issues) {
278
+ const title = issue.title;
279
+ if (!titleGroups.has(title)) {
280
+ titleGroups.set(title, []);
281
+ }
282
+ titleGroups.get(title)!.push(issue.number);
283
+ }
284
+
285
+ // Find duplicates (titles with more than one issue)
286
+ const duplicateGroups: Array<[string, number[]]> = [];
287
+ for (const [title, numbers] of titleGroups.entries()) {
288
+ if (numbers.length > 1) {
289
+ duplicateGroups.push([title, numbers]);
290
+ }
291
+ }
292
+
293
+ return {
294
+ totalIssues: issues.length,
295
+ duplicatesFound: duplicateGroups.length,
296
+ duplicateGroups,
297
+ };
298
+ } catch (error) {
299
+ console.warn(` āš ļø Validation error: ${error}`);
300
+ return { totalIssues: 0, duplicatesFound: 0, duplicateGroups: [] };
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Find existing GitHub issue for increment (duplicate detection!)
306
+ *
307
+ * Searches GitHub for issues matching the Epic ID and Increment ID.
308
+ * This prevents creating duplicates when frontmatter is lost/corrupted.
309
+ *
310
+ * @param epicId - Epic ID (e.g., FS-031)
311
+ * @param incrementId - Increment ID (e.g., 0031-feature-name)
312
+ * @returns GitHub issue number if found, null otherwise
313
+ */
314
+ private async findExistingIssue(
315
+ epicId: string,
316
+ incrementId: string
317
+ ): Promise<number | null> {
318
+ try {
319
+ // Search GitHub for issues with Epic ID in title
320
+ // Pattern: "[FS-031] Title" or "[INC-0031] Title"
321
+ const titlePattern = `[${epicId}]`;
322
+
323
+ const result = await execFileNoThrow('gh', [
324
+ 'issue',
325
+ 'list',
326
+ '--search',
327
+ `"${titlePattern}" in:title`,
328
+ '--json',
329
+ 'number,title,body',
330
+ '--limit',
331
+ '50', // Check up to 50 issues (should cover most Epics)
332
+ ]);
333
+
334
+ if (result.exitCode !== 0 || !result.stdout) {
335
+ console.warn(` āš ļø GitHub search failed: ${result.stderr || 'unknown error'}`);
336
+ return null;
337
+ }
338
+
339
+ const issues = JSON.parse(result.stdout) as Array<{
340
+ number: number;
341
+ title: string;
342
+ body: string;
343
+ }>;
344
+
345
+ if (issues.length === 0) {
346
+ return null; // No issues found for this Epic
347
+ }
348
+
349
+ // Find issue that mentions this increment ID in body
350
+ // Look for patterns like "**Increment**: 0031-feature-name"
351
+ for (const issue of issues) {
352
+ if (issue.body && issue.body.includes(`**Increment**: ${incrementId}`)) {
353
+ console.log(
354
+ ` šŸ”— Found existing issue #${issue.number} for ${incrementId}`
355
+ );
356
+ return issue.number;
357
+ }
358
+ }
359
+
360
+ // Fallback: Check if increment ID is in title
361
+ // Pattern: "[INC-0031-feature-name] Title"
362
+ for (const issue of issues) {
363
+ if (issue.title.toLowerCase().includes(incrementId.toLowerCase())) {
364
+ console.log(
365
+ ` šŸ”— Found existing issue #${issue.number} for ${incrementId} (title match)`
366
+ );
367
+ return issue.number;
368
+ }
369
+ }
370
+
371
+ return null; // No matching issue found
372
+ } catch (error) {
373
+ console.warn(` āš ļø Error searching for existing issue: ${error}`);
374
+ return null; // Fail gracefully - continue with sync
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Find Epic folder by ID (FS-001 or just 001)
380
+ */
381
+ private async findEpicFolder(epicId: string): Promise<string | null> {
382
+ const normalizedId = epicId.startsWith('FS-')
383
+ ? epicId
384
+ : `FS-${epicId.padStart(3, '0')}`;
385
+
386
+ const folders = await readdir(this.specsDir);
387
+ for (const folder of folders) {
388
+ if (folder.startsWith(normalizedId)) {
389
+ return path.join(this.specsDir, folder);
390
+ }
391
+ }
392
+
393
+ return null;
394
+ }
395
+
396
+ /**
397
+ * Parse Epic FEATURE.md to extract frontmatter
398
+ */
399
+ private async parseEpicReadme(
400
+ readmePath: string
401
+ ): Promise<EpicFrontmatter> {
402
+ const content = await readFile(readmePath, 'utf-8');
403
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
404
+
405
+ if (!match) {
406
+ throw new Error('Epic FEATURE.md missing YAML frontmatter');
407
+ }
408
+
409
+ const frontmatter = yaml.parse(match[1]) as EpicFrontmatter;
410
+ return frontmatter;
411
+ }
412
+
413
+ /**
414
+ * Parse increment file to extract title and overview
415
+ */
416
+ private async parseIncrementFile(
417
+ filePath: string
418
+ ): Promise<{
419
+ id: string;
420
+ title: string;
421
+ overview: string;
422
+ content: string;
423
+ frontmatter: IncrementFrontmatter;
424
+ }> {
425
+ const content = await readFile(filePath, 'utf-8');
426
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
427
+
428
+ let frontmatter: IncrementFrontmatter = { id: '', epic: '' };
429
+ let bodyContent = content;
430
+
431
+ if (match) {
432
+ frontmatter = yaml.parse(match[1]) as IncrementFrontmatter;
433
+ bodyContent = content.slice(match[0].length).trim();
434
+ }
435
+
436
+ // Extract title
437
+ const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
438
+ const title = titleMatch
439
+ ? titleMatch[1].trim()
440
+ : frontmatter.id || path.basename(filePath, '.md');
441
+
442
+ // Extract overview (first paragraph after title)
443
+ const overviewMatch = bodyContent.match(/^#[^\n]+\n+([^\n]+)/);
444
+ const overview = overviewMatch
445
+ ? overviewMatch[1].trim()
446
+ : 'No overview available';
447
+
448
+ return {
449
+ id: frontmatter.id,
450
+ title,
451
+ overview,
452
+ content: bodyContent,
453
+ frontmatter,
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Create GitHub Milestone
459
+ */
460
+ private async createMilestone(epic: EpicFrontmatter): Promise<{
461
+ number: number;
462
+ url: string;
463
+ }> {
464
+ const title = `[${epic.id}] ${epic.title}`;
465
+ const description = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
466
+
467
+ // Determine milestone state
468
+ const state = epic.status === 'complete' ? 'closed' : 'open';
469
+
470
+ // Use GitHub CLI to create milestone
471
+ const result = await execFileNoThrow('gh', [
472
+ 'api',
473
+ '/repos/{owner}/{repo}/milestones',
474
+ '-X',
475
+ 'POST',
476
+ '-f',
477
+ `title=${title}`,
478
+ '-f',
479
+ `description=${description}`,
480
+ '-f',
481
+ `state=${state}`,
482
+ ]);
483
+
484
+ if (result.exitCode !== 0) {
485
+ throw new Error(
486
+ `Failed to create GitHub Milestone: ${result.stderr || result.stdout}`
487
+ );
488
+ }
489
+
490
+ const milestone = JSON.parse(result.stdout);
491
+ return {
492
+ number: milestone.number,
493
+ url: milestone.html_url,
494
+ };
495
+ }
496
+
497
+ /**
498
+ * Update GitHub Milestone
499
+ */
500
+ private async updateMilestone(
501
+ milestoneNumber: number,
502
+ epic: EpicFrontmatter
503
+ ): Promise<void> {
504
+ const title = `[${epic.id}] ${epic.title}`;
505
+ const description = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
506
+ const state = epic.status === 'complete' ? 'closed' : 'open';
507
+
508
+ const result = await execFileNoThrow('gh', [
509
+ 'api',
510
+ `/repos/{owner}/{repo}/milestones/${milestoneNumber}`,
511
+ '-X',
512
+ 'PATCH',
513
+ '-f',
514
+ `title=${title}`,
515
+ '-f',
516
+ `description=${description}`,
517
+ '-f',
518
+ `state=${state}`,
519
+ ]);
520
+
521
+ if (result.exitCode !== 0) {
522
+ throw new Error(
523
+ `Failed to update GitHub Milestone: ${result.stderr || result.stdout}`
524
+ );
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Create GitHub Issue for increment with FULL DUPLICATE PROTECTION
530
+ */
531
+ private async createIssue(
532
+ epicId: string,
533
+ increment: {
534
+ id: string;
535
+ title: string;
536
+ overview: string;
537
+ content: string;
538
+ },
539
+ milestoneNumber: number
540
+ ): Promise<number> {
541
+ const title = `[${epicId}] ${increment.title}`;
542
+ const body = `# ${increment.title}\n\n${increment.overview}\n\n---\n\n**Increment**: ${increment.id}\n**Epic**: ${epicId}\n**Milestone**: See milestone for Epic progress\n\nšŸ¤– Auto-created by SpecWeave Epic Sync`;
543
+
544
+ try {
545
+ // Use DuplicateDetector for full 3-phase protection
546
+ const result = await DuplicateDetector.createWithProtection({
547
+ title,
548
+ body,
549
+ titlePattern: `[${epicId}]`,
550
+ incrementId: increment.id, // For body matching
551
+ labels: ['increment', 'epic-sync'],
552
+ milestone: milestoneNumber.toString()
553
+ });
554
+
555
+ // Log duplicate detection results
556
+ if (result.wasReused) {
557
+ console.log(` ā™»ļø Reused existing issue #${result.issue.number} (duplicate prevention)`);
558
+ }
559
+ if (result.duplicatesFound > 0) {
560
+ console.log(` šŸ›”ļø Duplicates: ${result.duplicatesFound} found, ${result.duplicatesClosed} closed`);
561
+ }
562
+
563
+ return result.issue.number;
564
+
565
+ } catch (error: any) {
566
+ throw new Error(`Failed to create GitHub Issue: ${error.message}`);
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Update GitHub Issue for increment
572
+ */
573
+ private async updateIssue(
574
+ epicId: string,
575
+ issueNumber: number,
576
+ increment: {
577
+ id: string;
578
+ title: string;
579
+ overview: string;
580
+ content: string;
581
+ },
582
+ milestoneNumber: number
583
+ ): Promise<void> {
584
+ const title = `[${epicId}] ${increment.title}`;
585
+ const body = `# ${increment.title}\n\n${increment.overview}\n\n---\n\n**Increment**: ${increment.id}\n**Epic**: ${epicId}\n**Milestone**: See milestone for Epic progress\n\nšŸ¤– Auto-updated by SpecWeave Epic Sync`;
586
+
587
+ const result = await execFileNoThrow('gh', [
588
+ 'issue',
589
+ 'edit',
590
+ issueNumber.toString(),
591
+ '--title',
592
+ title,
593
+ '--body',
594
+ body,
595
+ '--milestone',
596
+ milestoneNumber.toString(),
597
+ ]);
598
+
599
+ if (result.exitCode !== 0) {
600
+ throw new Error(
601
+ `Failed to update GitHub Issue: ${result.stderr || result.stdout}`
602
+ );
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Update Epic FEATURE.md with GitHub Milestone ID
608
+ */
609
+ private async updateEpicReadme(
610
+ readmePath: string,
611
+ github: { type: 'milestone'; id: number; url: string }
612
+ ): Promise<void> {
613
+ const content = await readFile(readmePath, 'utf-8');
614
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
615
+
616
+ if (!match) {
617
+ throw new Error('Epic FEATURE.md missing YAML frontmatter');
618
+ }
619
+
620
+ const frontmatter = yaml.parse(match[1]) as EpicFrontmatter;
621
+ frontmatter.external_tools.github = github;
622
+
623
+ const newFrontmatter = yaml.stringify(frontmatter);
624
+ const newContent = content.replace(
625
+ /^---\n[\s\S]*?\n---/,
626
+ `---\n${newFrontmatter}---`
627
+ );
628
+
629
+ await writeFile(readmePath, newContent, 'utf-8');
630
+ }
631
+
632
+ /**
633
+ * Update increment external link in both Epic README and increment file
634
+ */
635
+ private async updateIncrementExternalLink(
636
+ readmePath: string,
637
+ incrementFile: string,
638
+ incrementId: string,
639
+ issueNumber: number
640
+ ): Promise<void> {
641
+ const issueUrl = `https://github.com/{owner}/{repo}/issues/${issueNumber}`;
642
+
643
+ // 1. Update Epic FEATURE.md
644
+ const readmeContent = await readFile(readmePath, 'utf-8');
645
+ const readmeMatch = readmeContent.match(/^---\n([\s\S]*?)\n---/);
646
+
647
+ if (readmeMatch) {
648
+ const frontmatter = yaml.parse(readmeMatch[1]) as EpicFrontmatter;
649
+ const increment = frontmatter.increments.find(
650
+ (inc) => inc.id === incrementId
651
+ );
652
+
653
+ if (increment) {
654
+ increment.external.github = issueNumber;
655
+ const newFrontmatter = yaml.stringify(frontmatter);
656
+ const newContent = readmeContent.replace(
657
+ /^---\n[\s\S]*?\n---/,
658
+ `---\n${newFrontmatter}---`
659
+ );
660
+ await writeFile(readmePath, newContent, 'utf-8');
661
+ }
662
+ }
663
+
664
+ // 2. Update increment file frontmatter
665
+ const incrementContent = await readFile(incrementFile, 'utf-8');
666
+ const incrementMatch = incrementContent.match(/^---\n([\s\S]*?)\n---/);
667
+
668
+ if (incrementMatch) {
669
+ const frontmatter = yaml.parse(
670
+ incrementMatch[1]
671
+ ) as IncrementFrontmatter;
672
+
673
+ if (!frontmatter.external) {
674
+ frontmatter.external = {};
675
+ }
676
+
677
+ frontmatter.external.github = {
678
+ issue: issueNumber,
679
+ url: issueUrl,
680
+ };
681
+
682
+ const newFrontmatter = yaml.stringify(frontmatter);
683
+ const newContent = incrementContent.replace(
684
+ /^---\n[\s\S]*?\n---/,
685
+ `---\n${newFrontmatter}---`
686
+ );
687
+ await writeFile(incrementFile, newContent, 'utf-8');
688
+ }
689
+ }
690
+ }