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,489 @@
1
+ import { readdir, readFile, writeFile } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import * as path from "path";
4
+ import * as yaml from "yaml";
5
+ import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
6
+ import { DuplicateDetector } from "./duplicate-detector.js";
7
+ class GitHubEpicSync {
8
+ constructor(client, specsDir) {
9
+ this.client = client;
10
+ this.specsDir = specsDir;
11
+ }
12
+ /**
13
+ * Sync Epic folder to GitHub (Milestone + Issues)
14
+ */
15
+ async syncEpicToGitHub(epicId) {
16
+ console.log(`
17
+ \u{1F504} Syncing Epic ${epicId} to GitHub...`);
18
+ const epicFolder = await this.findEpicFolder(epicId);
19
+ if (!epicFolder) {
20
+ throw new Error(`Epic ${epicId} not found in ${this.specsDir}`);
21
+ }
22
+ const readmePath = path.join(epicFolder, "FEATURE.md");
23
+ const epicData = await this.parseEpicReadme(readmePath);
24
+ console.log(` \u{1F4E6} Epic: ${epicData.title}`);
25
+ console.log(` \u{1F4CA} Increments: ${epicData.total_increments}`);
26
+ let milestoneNumber = epicData.external_tools.github.id;
27
+ let milestoneUrl = epicData.external_tools.github.url;
28
+ if (!milestoneNumber) {
29
+ console.log(` \u{1F680} Creating GitHub Milestone...`);
30
+ const milestone = await this.createMilestone(epicData);
31
+ milestoneNumber = milestone.number;
32
+ milestoneUrl = milestone.url;
33
+ console.log(` \u2705 Created Milestone #${milestoneNumber}`);
34
+ await this.updateEpicReadme(readmePath, {
35
+ type: "milestone",
36
+ id: milestoneNumber,
37
+ url: milestoneUrl
38
+ });
39
+ } else {
40
+ console.log(` \u267B\uFE0F Updating existing Milestone #${milestoneNumber}...`);
41
+ await this.updateMilestone(milestoneNumber, epicData);
42
+ console.log(` \u2705 Updated Milestone #${milestoneNumber}`);
43
+ }
44
+ let issuesCreated = 0;
45
+ let issuesUpdated = 0;
46
+ let duplicatesDetected = 0;
47
+ console.log(`
48
+ \u{1F4DD} Syncing ${epicData.increments.length} increments...`);
49
+ for (const increment of epicData.increments) {
50
+ const incrementFile = path.join(epicFolder, `${increment.id}.md`);
51
+ if (!existsSync(incrementFile)) {
52
+ console.log(` \u26A0\uFE0F Increment file not found: ${increment.id}.md`);
53
+ continue;
54
+ }
55
+ const incrementData = await this.parseIncrementFile(incrementFile);
56
+ const existingIssue = increment.external.github;
57
+ if (!existingIssue) {
58
+ console.log(` \u{1F50D} Checking GitHub for existing issue: ${increment.id}...`);
59
+ const githubIssue = await this.findExistingIssue(epicData.id, increment.id);
60
+ if (githubIssue) {
61
+ console.log(` \u267B\uFE0F Found existing Issue #${githubIssue} for ${increment.id} (self-healing)`);
62
+ await this.updateIncrementExternalLink(
63
+ readmePath,
64
+ incrementFile,
65
+ increment.id,
66
+ githubIssue
67
+ );
68
+ issuesUpdated++;
69
+ duplicatesDetected++;
70
+ } else {
71
+ const issueNumber = await this.createIssue(
72
+ epicData.id,
73
+ incrementData,
74
+ milestoneNumber
75
+ );
76
+ issuesCreated++;
77
+ console.log(` \u2705 Created Issue #${issueNumber} for ${increment.id}`);
78
+ await this.updateIncrementExternalLink(
79
+ readmePath,
80
+ incrementFile,
81
+ increment.id,
82
+ issueNumber
83
+ );
84
+ }
85
+ } else {
86
+ await this.updateIssue(
87
+ epicData.id,
88
+ existingIssue,
89
+ incrementData,
90
+ milestoneNumber
91
+ );
92
+ issuesUpdated++;
93
+ console.log(` \u267B\uFE0F Updated Issue #${existingIssue} for ${increment.id}`);
94
+ }
95
+ }
96
+ console.log(`
97
+ \u2705 Epic sync complete!`);
98
+ console.log(` Milestone: ${milestoneUrl}`);
99
+ console.log(` Issues created: ${issuesCreated}`);
100
+ console.log(` Issues updated: ${issuesUpdated}`);
101
+ if (duplicatesDetected > 0) {
102
+ console.log(` \u{1F517} Self-healed: ${duplicatesDetected} (found existing issues)`);
103
+ }
104
+ console.log(`
105
+ \u{1F50D} Post-sync validation...`);
106
+ const validation = await this.validateSync(epicData.id);
107
+ if (validation.duplicatesFound > 0) {
108
+ console.warn(`
109
+ \u26A0\uFE0F WARNING: ${validation.duplicatesFound} duplicate(s) detected!`);
110
+ console.warn(` This may indicate a previous sync created duplicates.`);
111
+ console.warn(` Run cleanup command to resolve:`);
112
+ console.warn(` /specweave-github:cleanup-duplicates ${epicData.id}`);
113
+ console.warn(`
114
+ Duplicate groups:`);
115
+ for (const [title, numbers] of validation.duplicateGroups) {
116
+ console.warn(` - "${title}": Issues #${numbers.join(", #")}`);
117
+ }
118
+ } else {
119
+ console.log(` \u2705 No duplicates found`);
120
+ }
121
+ return {
122
+ milestoneNumber,
123
+ milestoneUrl,
124
+ issuesCreated,
125
+ issuesUpdated,
126
+ duplicatesDetected
127
+ };
128
+ }
129
+ /**
130
+ * Validate sync results - check for duplicate issues
131
+ *
132
+ * Searches GitHub for all issues with the Epic ID and detects duplicates
133
+ * (multiple issues with the same title).
134
+ *
135
+ * @param epicId - Epic ID (e.g., FS-031)
136
+ * @returns Validation result with duplicate count and groups
137
+ */
138
+ async validateSync(epicId) {
139
+ try {
140
+ const titlePattern = `[${epicId}]`;
141
+ const result = await execFileNoThrow("gh", [
142
+ "issue",
143
+ "list",
144
+ "--search",
145
+ `"${titlePattern}" in:title`,
146
+ "--json",
147
+ "number,title,state",
148
+ "--limit",
149
+ "100",
150
+ // Check up to 100 issues
151
+ "--state",
152
+ "all"
153
+ // Include both open and closed
154
+ ]);
155
+ if (result.exitCode !== 0 || !result.stdout) {
156
+ console.warn(` \u26A0\uFE0F Validation failed: ${result.stderr || "unknown error"}`);
157
+ return { totalIssues: 0, duplicatesFound: 0, duplicateGroups: [] };
158
+ }
159
+ const issues = JSON.parse(result.stdout);
160
+ const titleGroups = /* @__PURE__ */ new Map();
161
+ for (const issue of issues) {
162
+ const title = issue.title;
163
+ if (!titleGroups.has(title)) {
164
+ titleGroups.set(title, []);
165
+ }
166
+ titleGroups.get(title).push(issue.number);
167
+ }
168
+ const duplicateGroups = [];
169
+ for (const [title, numbers] of titleGroups.entries()) {
170
+ if (numbers.length > 1) {
171
+ duplicateGroups.push([title, numbers]);
172
+ }
173
+ }
174
+ return {
175
+ totalIssues: issues.length,
176
+ duplicatesFound: duplicateGroups.length,
177
+ duplicateGroups
178
+ };
179
+ } catch (error) {
180
+ console.warn(` \u26A0\uFE0F Validation error: ${error}`);
181
+ return { totalIssues: 0, duplicatesFound: 0, duplicateGroups: [] };
182
+ }
183
+ }
184
+ /**
185
+ * Find existing GitHub issue for increment (duplicate detection!)
186
+ *
187
+ * Searches GitHub for issues matching the Epic ID and Increment ID.
188
+ * This prevents creating duplicates when frontmatter is lost/corrupted.
189
+ *
190
+ * @param epicId - Epic ID (e.g., FS-031)
191
+ * @param incrementId - Increment ID (e.g., 0031-feature-name)
192
+ * @returns GitHub issue number if found, null otherwise
193
+ */
194
+ async findExistingIssue(epicId, incrementId) {
195
+ try {
196
+ const titlePattern = `[${epicId}]`;
197
+ const result = await execFileNoThrow("gh", [
198
+ "issue",
199
+ "list",
200
+ "--search",
201
+ `"${titlePattern}" in:title`,
202
+ "--json",
203
+ "number,title,body",
204
+ "--limit",
205
+ "50"
206
+ // Check up to 50 issues (should cover most Epics)
207
+ ]);
208
+ if (result.exitCode !== 0 || !result.stdout) {
209
+ console.warn(` \u26A0\uFE0F GitHub search failed: ${result.stderr || "unknown error"}`);
210
+ return null;
211
+ }
212
+ const issues = JSON.parse(result.stdout);
213
+ if (issues.length === 0) {
214
+ return null;
215
+ }
216
+ for (const issue of issues) {
217
+ if (issue.body && issue.body.includes(`**Increment**: ${incrementId}`)) {
218
+ console.log(
219
+ ` \u{1F517} Found existing issue #${issue.number} for ${incrementId}`
220
+ );
221
+ return issue.number;
222
+ }
223
+ }
224
+ for (const issue of issues) {
225
+ if (issue.title.toLowerCase().includes(incrementId.toLowerCase())) {
226
+ console.log(
227
+ ` \u{1F517} Found existing issue #${issue.number} for ${incrementId} (title match)`
228
+ );
229
+ return issue.number;
230
+ }
231
+ }
232
+ return null;
233
+ } catch (error) {
234
+ console.warn(` \u26A0\uFE0F Error searching for existing issue: ${error}`);
235
+ return null;
236
+ }
237
+ }
238
+ /**
239
+ * Find Epic folder by ID (FS-001 or just 001)
240
+ */
241
+ async findEpicFolder(epicId) {
242
+ const normalizedId = epicId.startsWith("FS-") ? epicId : `FS-${epicId.padStart(3, "0")}`;
243
+ const folders = await readdir(this.specsDir);
244
+ for (const folder of folders) {
245
+ if (folder.startsWith(normalizedId)) {
246
+ return path.join(this.specsDir, folder);
247
+ }
248
+ }
249
+ return null;
250
+ }
251
+ /**
252
+ * Parse Epic FEATURE.md to extract frontmatter
253
+ */
254
+ async parseEpicReadme(readmePath) {
255
+ const content = await readFile(readmePath, "utf-8");
256
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
257
+ if (!match) {
258
+ throw new Error("Epic FEATURE.md missing YAML frontmatter");
259
+ }
260
+ const frontmatter = yaml.parse(match[1]);
261
+ return frontmatter;
262
+ }
263
+ /**
264
+ * Parse increment file to extract title and overview
265
+ */
266
+ async parseIncrementFile(filePath) {
267
+ const content = await readFile(filePath, "utf-8");
268
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
269
+ let frontmatter = { id: "", epic: "" };
270
+ let bodyContent = content;
271
+ if (match) {
272
+ frontmatter = yaml.parse(match[1]);
273
+ bodyContent = content.slice(match[0].length).trim();
274
+ }
275
+ const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
276
+ const title = titleMatch ? titleMatch[1].trim() : frontmatter.id || path.basename(filePath, ".md");
277
+ const overviewMatch = bodyContent.match(/^#[^\n]+\n+([^\n]+)/);
278
+ const overview = overviewMatch ? overviewMatch[1].trim() : "No overview available";
279
+ return {
280
+ id: frontmatter.id,
281
+ title,
282
+ overview,
283
+ content: bodyContent,
284
+ frontmatter
285
+ };
286
+ }
287
+ /**
288
+ * Create GitHub Milestone
289
+ */
290
+ async createMilestone(epic) {
291
+ const title = `[${epic.id}] ${epic.title}`;
292
+ const description = `Epic: ${epic.title}
293
+
294
+ Progress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})
295
+
296
+ Priority: ${epic.priority}
297
+ Status: ${epic.status}`;
298
+ const state = epic.status === "complete" ? "closed" : "open";
299
+ const result = await execFileNoThrow("gh", [
300
+ "api",
301
+ "/repos/{owner}/{repo}/milestones",
302
+ "-X",
303
+ "POST",
304
+ "-f",
305
+ `title=${title}`,
306
+ "-f",
307
+ `description=${description}`,
308
+ "-f",
309
+ `state=${state}`
310
+ ]);
311
+ if (result.exitCode !== 0) {
312
+ throw new Error(
313
+ `Failed to create GitHub Milestone: ${result.stderr || result.stdout}`
314
+ );
315
+ }
316
+ const milestone = JSON.parse(result.stdout);
317
+ return {
318
+ number: milestone.number,
319
+ url: milestone.html_url
320
+ };
321
+ }
322
+ /**
323
+ * Update GitHub Milestone
324
+ */
325
+ async updateMilestone(milestoneNumber, epic) {
326
+ const title = `[${epic.id}] ${epic.title}`;
327
+ const description = `Epic: ${epic.title}
328
+
329
+ Progress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})
330
+
331
+ Priority: ${epic.priority}
332
+ Status: ${epic.status}`;
333
+ const state = epic.status === "complete" ? "closed" : "open";
334
+ const result = await execFileNoThrow("gh", [
335
+ "api",
336
+ `/repos/{owner}/{repo}/milestones/${milestoneNumber}`,
337
+ "-X",
338
+ "PATCH",
339
+ "-f",
340
+ `title=${title}`,
341
+ "-f",
342
+ `description=${description}`,
343
+ "-f",
344
+ `state=${state}`
345
+ ]);
346
+ if (result.exitCode !== 0) {
347
+ throw new Error(
348
+ `Failed to update GitHub Milestone: ${result.stderr || result.stdout}`
349
+ );
350
+ }
351
+ }
352
+ /**
353
+ * Create GitHub Issue for increment with FULL DUPLICATE PROTECTION
354
+ */
355
+ async createIssue(epicId, increment, milestoneNumber) {
356
+ const title = `[${epicId}] ${increment.title}`;
357
+ const body = `# ${increment.title}
358
+
359
+ ${increment.overview}
360
+
361
+ ---
362
+
363
+ **Increment**: ${increment.id}
364
+ **Epic**: ${epicId}
365
+ **Milestone**: See milestone for Epic progress
366
+
367
+ \u{1F916} Auto-created by SpecWeave Epic Sync`;
368
+ try {
369
+ const result = await DuplicateDetector.createWithProtection({
370
+ title,
371
+ body,
372
+ titlePattern: `[${epicId}]`,
373
+ incrementId: increment.id,
374
+ // For body matching
375
+ labels: ["increment", "epic-sync"],
376
+ milestone: milestoneNumber.toString()
377
+ });
378
+ if (result.wasReused) {
379
+ console.log(` \u267B\uFE0F Reused existing issue #${result.issue.number} (duplicate prevention)`);
380
+ }
381
+ if (result.duplicatesFound > 0) {
382
+ console.log(` \u{1F6E1}\uFE0F Duplicates: ${result.duplicatesFound} found, ${result.duplicatesClosed} closed`);
383
+ }
384
+ return result.issue.number;
385
+ } catch (error) {
386
+ throw new Error(`Failed to create GitHub Issue: ${error.message}`);
387
+ }
388
+ }
389
+ /**
390
+ * Update GitHub Issue for increment
391
+ */
392
+ async updateIssue(epicId, issueNumber, increment, milestoneNumber) {
393
+ const title = `[${epicId}] ${increment.title}`;
394
+ const body = `# ${increment.title}
395
+
396
+ ${increment.overview}
397
+
398
+ ---
399
+
400
+ **Increment**: ${increment.id}
401
+ **Epic**: ${epicId}
402
+ **Milestone**: See milestone for Epic progress
403
+
404
+ \u{1F916} Auto-updated by SpecWeave Epic Sync`;
405
+ const result = await execFileNoThrow("gh", [
406
+ "issue",
407
+ "edit",
408
+ issueNumber.toString(),
409
+ "--title",
410
+ title,
411
+ "--body",
412
+ body,
413
+ "--milestone",
414
+ milestoneNumber.toString()
415
+ ]);
416
+ if (result.exitCode !== 0) {
417
+ throw new Error(
418
+ `Failed to update GitHub Issue: ${result.stderr || result.stdout}`
419
+ );
420
+ }
421
+ }
422
+ /**
423
+ * Update Epic FEATURE.md with GitHub Milestone ID
424
+ */
425
+ async updateEpicReadme(readmePath, github) {
426
+ const content = await readFile(readmePath, "utf-8");
427
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
428
+ if (!match) {
429
+ throw new Error("Epic FEATURE.md missing YAML frontmatter");
430
+ }
431
+ const frontmatter = yaml.parse(match[1]);
432
+ frontmatter.external_tools.github = github;
433
+ const newFrontmatter = yaml.stringify(frontmatter);
434
+ const newContent = content.replace(
435
+ /^---\n[\s\S]*?\n---/,
436
+ `---
437
+ ${newFrontmatter}---`
438
+ );
439
+ await writeFile(readmePath, newContent, "utf-8");
440
+ }
441
+ /**
442
+ * Update increment external link in both Epic README and increment file
443
+ */
444
+ async updateIncrementExternalLink(readmePath, incrementFile, incrementId, issueNumber) {
445
+ const issueUrl = `https://github.com/{owner}/{repo}/issues/${issueNumber}`;
446
+ const readmeContent = await readFile(readmePath, "utf-8");
447
+ const readmeMatch = readmeContent.match(/^---\n([\s\S]*?)\n---/);
448
+ if (readmeMatch) {
449
+ const frontmatter = yaml.parse(readmeMatch[1]);
450
+ const increment = frontmatter.increments.find(
451
+ (inc) => inc.id === incrementId
452
+ );
453
+ if (increment) {
454
+ increment.external.github = issueNumber;
455
+ const newFrontmatter = yaml.stringify(frontmatter);
456
+ const newContent = readmeContent.replace(
457
+ /^---\n[\s\S]*?\n---/,
458
+ `---
459
+ ${newFrontmatter}---`
460
+ );
461
+ await writeFile(readmePath, newContent, "utf-8");
462
+ }
463
+ }
464
+ const incrementContent = await readFile(incrementFile, "utf-8");
465
+ const incrementMatch = incrementContent.match(/^---\n([\s\S]*?)\n---/);
466
+ if (incrementMatch) {
467
+ const frontmatter = yaml.parse(
468
+ incrementMatch[1]
469
+ );
470
+ if (!frontmatter.external) {
471
+ frontmatter.external = {};
472
+ }
473
+ frontmatter.external.github = {
474
+ issue: issueNumber,
475
+ url: issueUrl
476
+ };
477
+ const newFrontmatter = yaml.stringify(frontmatter);
478
+ const newContent = incrementContent.replace(
479
+ /^---\n[\s\S]*?\n---/,
480
+ `---
481
+ ${newFrontmatter}---`
482
+ );
483
+ await writeFile(incrementFile, newContent, "utf-8");
484
+ }
485
+ }
486
+ }
487
+ export {
488
+ GitHubEpicSync
489
+ };