specweave 0.28.17 → 0.28.20

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