specweave 0.28.15 → 0.28.19

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