specweave 0.17.16 → 0.17.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 (232) 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-spec-content-sync.d.ts.map +1 -1
  26. package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js +65 -6
  27. package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js.map +1 -1
  28. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
  29. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
  30. package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
  31. package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
  32. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +25 -0
  33. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +1 -0
  34. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +191 -0
  35. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +1 -0
  36. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
  37. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
  38. package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
  39. package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
  40. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
  41. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
  42. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
  43. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
  44. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +63 -0
  45. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +1 -0
  46. package/dist/plugins/specweave-github/lib/epic-content-builder.js +216 -0
  47. package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +1 -0
  48. package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
  49. package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
  50. package/dist/plugins/specweave-github/lib/github-client.js +25 -13
  51. package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
  52. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
  53. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
  54. package/dist/plugins/specweave-github/lib/github-epic-sync.js +466 -0
  55. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
  56. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
  57. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
  58. package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
  59. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
  60. package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
  61. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
  62. package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
  63. package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
  64. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +28 -0
  65. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -0
  66. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +156 -0
  67. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -0
  68. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
  69. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
  70. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
  71. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
  72. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
  73. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
  74. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
  75. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
  76. package/dist/spec-parser.js +629 -0
  77. package/dist/src/cli/commands/init.d.ts.map +1 -1
  78. package/dist/src/cli/commands/init.js +107 -3
  79. package/dist/src/cli/commands/init.js.map +1 -1
  80. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  81. package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
  82. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  83. package/dist/src/core/deduplication/command-deduplicator.d.ts +166 -0
  84. package/dist/src/core/deduplication/command-deduplicator.d.ts.map +1 -0
  85. package/dist/src/core/deduplication/command-deduplicator.js +254 -0
  86. package/dist/src/core/deduplication/command-deduplicator.js.map +1 -0
  87. package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
  88. package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
  89. package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
  90. package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
  91. package/dist/src/core/living-docs/index.d.ts +10 -84
  92. package/dist/src/core/living-docs/index.d.ts.map +1 -1
  93. package/dist/src/core/living-docs/index.js +10 -164
  94. package/dist/src/core/living-docs/index.js.map +1 -1
  95. package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
  96. package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
  97. package/dist/src/core/living-docs/spec-distributor.js +823 -0
  98. package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
  99. package/dist/src/core/living-docs/types.d.ts +201 -0
  100. package/dist/src/core/living-docs/types.d.ts.map +1 -0
  101. package/dist/src/core/living-docs/types.js +15 -0
  102. package/dist/src/core/living-docs/types.js.map +1 -0
  103. package/dist/src/core/logging/prompt-logger.d.ts +70 -0
  104. package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
  105. package/dist/src/core/logging/prompt-logger.js +247 -0
  106. package/dist/src/core/logging/prompt-logger.js.map +1 -0
  107. package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
  108. package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
  109. package/dist/src/core/status-line/status-line-manager.js +33 -70
  110. package/dist/src/core/status-line/status-line-manager.js.map +1 -1
  111. package/dist/src/core/status-line/types.d.ts +19 -31
  112. package/dist/src/core/status-line/types.d.ts.map +1 -1
  113. package/dist/src/core/status-line/types.js +5 -9
  114. package/dist/src/core/status-line/types.js.map +1 -1
  115. package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
  116. package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
  117. package/dist/src/core/sync/conflict-resolver.js +108 -0
  118. package/dist/src/core/sync/conflict-resolver.js.map +1 -0
  119. package/dist/src/core/sync/enhanced-content-builder.d.ts +55 -0
  120. package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
  121. package/dist/src/core/sync/enhanced-content-builder.js +202 -0
  122. package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
  123. package/dist/src/core/sync/label-detector.d.ts +66 -0
  124. package/dist/src/core/sync/label-detector.d.ts.map +1 -0
  125. package/dist/src/core/sync/label-detector.js +211 -0
  126. package/dist/src/core/sync/label-detector.js.map +1 -0
  127. package/dist/src/core/sync/retry-logic.d.ts +64 -0
  128. package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
  129. package/dist/src/core/sync/retry-logic.js +165 -0
  130. package/dist/src/core/sync/retry-logic.js.map +1 -0
  131. package/dist/src/core/sync/spec-content-sync.d.ts +88 -0
  132. package/dist/src/core/sync/spec-content-sync.d.ts.map +1 -0
  133. package/dist/src/core/sync/spec-content-sync.js +5 -0
  134. package/dist/src/core/sync/spec-content-sync.js.map +1 -0
  135. package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
  136. package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
  137. package/dist/src/core/sync/spec-increment-mapper.js +424 -0
  138. package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
  139. package/dist/src/core/sync/status-cache.d.ts +91 -0
  140. package/dist/src/core/sync/status-cache.d.ts.map +1 -0
  141. package/dist/src/core/sync/status-cache.js +140 -0
  142. package/dist/src/core/sync/status-cache.js.map +1 -0
  143. package/dist/src/core/sync/status-mapper.d.ts +69 -0
  144. package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
  145. package/dist/src/core/sync/status-mapper.js +90 -0
  146. package/dist/src/core/sync/status-mapper.js.map +1 -0
  147. package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
  148. package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
  149. package/dist/src/core/sync/status-sync-engine.js +347 -0
  150. package/dist/src/core/sync/status-sync-engine.js.map +1 -0
  151. package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
  152. package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
  153. package/dist/src/core/sync/sync-event-logger.js +103 -0
  154. package/dist/src/core/sync/sync-event-logger.js.map +1 -0
  155. package/dist/src/core/sync/types.d.ts +52 -0
  156. package/dist/src/core/sync/types.d.ts.map +1 -0
  157. package/dist/src/core/sync/types.js +5 -0
  158. package/dist/src/core/sync/types.js.map +1 -0
  159. package/dist/src/core/sync/workflow-detector.d.ts +95 -0
  160. package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
  161. package/dist/src/core/sync/workflow-detector.js +175 -0
  162. package/dist/src/core/sync/workflow-detector.js.map +1 -0
  163. package/dist/src/core/types/config.d.ts +51 -0
  164. package/dist/src/core/types/config.d.ts.map +1 -1
  165. package/dist/src/core/types/config.js +47 -0
  166. package/dist/src/core/types/config.js.map +1 -1
  167. package/dist/src/core/types/increment-metadata.d.ts +4 -0
  168. package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
  169. package/dist/src/core/types/increment-metadata.js.map +1 -1
  170. package/dist/src/utils/github-url.d.ts +53 -0
  171. package/dist/src/utils/github-url.d.ts.map +1 -0
  172. package/dist/src/utils/github-url.js +90 -0
  173. package/dist/src/utils/github-url.js.map +1 -0
  174. package/dist/src/utils/spec-parser.d.ts +145 -0
  175. package/dist/src/utils/spec-parser.d.ts.map +1 -0
  176. package/dist/src/utils/spec-parser.js +640 -0
  177. package/dist/src/utils/spec-parser.js.map +1 -0
  178. package/dist/tsconfig.tsbuildinfo +1 -0
  179. package/package.json +1 -1
  180. package/plugins/specweave/agents/pm/AGENT.md +160 -13
  181. package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
  182. package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
  183. package/plugins/specweave/commands/specweave-done.md +163 -0
  184. package/plugins/specweave/commands/specweave.md +70 -405
  185. package/plugins/specweave/hooks/hooks.json +4 -0
  186. package/plugins/specweave/hooks/lib/sync-spec-content.sh +2 -2
  187. package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
  188. package/plugins/specweave/hooks/post-increment-planning.sh +133 -37
  189. package/plugins/specweave/hooks/pre-command-deduplication.sh +86 -0
  190. package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
  191. package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
  192. package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
  193. package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
  194. package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
  195. package/plugins/specweave-ado/commands/specweave-ado-sync-spec.md +1 -1
  196. package/plugins/specweave-ado/lib/ado-spec-content-sync.js +49 -5
  197. package/plugins/specweave-ado/lib/ado-spec-content-sync.ts +72 -6
  198. package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
  199. package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
  200. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  201. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
  202. package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
  203. package/plugins/specweave-github/commands/specweave-github-sync-spec.md +1 -1
  204. package/plugins/specweave-github/hooks/post-task-completion.sh +32 -0
  205. package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
  206. package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
  207. package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
  208. package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
  209. package/plugins/specweave-github/lib/epic-content-builder.js +227 -0
  210. package/plugins/specweave-github/lib/epic-content-builder.ts +317 -0
  211. package/plugins/specweave-github/lib/github-client.js +21 -10
  212. package/plugins/specweave-github/lib/github-client.ts +27 -16
  213. package/plugins/specweave-github/lib/github-epic-sync.js +488 -0
  214. package/plugins/specweave-github/lib/github-epic-sync.ts +715 -0
  215. package/plugins/specweave-github/lib/github-status-sync.js +71 -0
  216. package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
  217. package/plugins/specweave-github/lib/task-sync.js +33 -2
  218. package/plugins/specweave-github/lib/task-sync.ts +44 -2
  219. package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
  220. package/plugins/specweave-jira/commands/specweave-jira-sync-spec.md +1 -1
  221. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +134 -0
  222. package/plugins/specweave-jira/lib/enhanced-jira-sync.ts +196 -0
  223. package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
  224. package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
  225. package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
  226. package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
  227. package/plugins/specweave-release/commands/specweave-release-platform.md +1 -1
  228. package/plugins/specweave-release/hooks/post-task-completion.sh +2 -2
  229. package/src/templates/AGENTS.md.template +88 -1
  230. package/src/templates/CLAUDE.md.template +49 -0
  231. package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
  232. package/plugins/specweave/skills/plugin-installer/SKILL.md +0 -353
@@ -0,0 +1,227 @@
1
+ import { readdir, readFile } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import * as path from "path";
4
+ import * as yaml from "yaml";
5
+ class EpicContentBuilder {
6
+ constructor(epicFolder, projectRoot) {
7
+ this.epicFolder = epicFolder;
8
+ this.projectRoot = projectRoot;
9
+ }
10
+ /**
11
+ * Build hierarchical GitHub issue body
12
+ *
13
+ * Format:
14
+ * - Epic overview
15
+ * - User Stories section (checkable, with status + increment)
16
+ * - Tasks section (grouped by User Story)
17
+ */
18
+ async buildIssueBody() {
19
+ const epicData = await this.readEpicMetadata();
20
+ const userStories = await this.readUserStories();
21
+ const overview = this.buildOverviewSection(epicData);
22
+ const userStoriesSection = this.buildUserStoriesSection(userStories);
23
+ const tasksSection = this.buildTasksSection(userStories);
24
+ return `${overview}
25
+
26
+ ---
27
+
28
+ ${userStoriesSection}
29
+
30
+ ---
31
+
32
+ ${tasksSection}
33
+
34
+ ---
35
+
36
+ \u{1F916} Auto-created by SpecWeave Epic Sync`;
37
+ }
38
+ /**
39
+ * Read Epic FEATURE.md frontmatter
40
+ */
41
+ async readEpicMetadata() {
42
+ const featurePath = path.join(this.epicFolder, "FEATURE.md");
43
+ const content = await readFile(featurePath, "utf-8");
44
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
45
+ if (!match) {
46
+ throw new Error("FEATURE.md missing YAML frontmatter");
47
+ }
48
+ return yaml.parse(match[1]);
49
+ }
50
+ /**
51
+ * Read all user stories from us-*.md files
52
+ */
53
+ async readUserStories() {
54
+ const files = await readdir(this.epicFolder);
55
+ const usFiles = files.filter((f) => f.startsWith("us-") && f.endsWith(".md"));
56
+ const userStories = [];
57
+ for (const file of usFiles.sort()) {
58
+ const filePath = path.join(this.epicFolder, file);
59
+ const content = await readFile(filePath, "utf-8");
60
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
61
+ if (!match) {
62
+ console.warn(` \u26A0\uFE0F ${file} missing frontmatter, skipping`);
63
+ continue;
64
+ }
65
+ const frontmatter = yaml.parse(match[1]);
66
+ const bodyContent = content.slice(match[0].length).trim();
67
+ const incrementMatch = bodyContent.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
68
+ const increment = incrementMatch ? incrementMatch[1] : null;
69
+ const tasks = await this.extractTasksForUserStory(
70
+ frontmatter.id,
71
+ increment,
72
+ bodyContent
73
+ );
74
+ userStories.push({
75
+ id: frontmatter.id,
76
+ title: frontmatter.title,
77
+ status: this.normalizeStatus(frontmatter.status),
78
+ increment,
79
+ tasks
80
+ });
81
+ }
82
+ return userStories;
83
+ }
84
+ /**
85
+ * Extract tasks for a user story from its Implementation section
86
+ */
87
+ async extractTasksForUserStory(userStoryId, incrementId, content) {
88
+ if (!incrementId) {
89
+ return [];
90
+ }
91
+ const incrementFolder = path.join(
92
+ this.projectRoot,
93
+ ".specweave",
94
+ "increments",
95
+ incrementId
96
+ );
97
+ if (!existsSync(incrementFolder)) {
98
+ console.warn(` \u26A0\uFE0F Increment folder not found: ${incrementId}`);
99
+ return [];
100
+ }
101
+ const tasksPath = path.join(incrementFolder, "tasks.md");
102
+ if (!existsSync(tasksPath)) {
103
+ console.warn(` \u26A0\uFE0F tasks.md not found in ${incrementId}`);
104
+ return [];
105
+ }
106
+ const tasksContent = await readFile(tasksPath, "utf-8");
107
+ const taskLinkPattern = /- \[([T-\d]+):\s*([^\]]+)\]/g;
108
+ const taskLinks = [];
109
+ let match;
110
+ while ((match = taskLinkPattern.exec(content)) !== null) {
111
+ taskLinks.push({
112
+ id: match[1],
113
+ // e.g., "T-001"
114
+ title: match[2].trim()
115
+ });
116
+ }
117
+ const tasks = [];
118
+ for (const taskLink of taskLinks) {
119
+ const taskPattern = new RegExp(
120
+ `###\\s+${taskLink.id}:\\s*([^\\n]+)[\\s\\S]*?\\*\\*Status\\*\\*:\\s*\\[([x\\s])\\]`,
121
+ "i"
122
+ );
123
+ const taskMatch = tasksContent.match(taskPattern);
124
+ const isCompleted = taskMatch ? taskMatch[2] === "x" : false;
125
+ tasks.push({
126
+ id: taskLink.id,
127
+ title: taskLink.title,
128
+ status: isCompleted,
129
+ userStoryId
130
+ });
131
+ }
132
+ return tasks;
133
+ }
134
+ /**
135
+ * Build overview section
136
+ */
137
+ buildOverviewSection(epic) {
138
+ return `# [${epic.id}] ${epic.title}
139
+
140
+ **Status**: ${epic.status}
141
+ **Created**: ${epic.created}
142
+ **Last Updated**: ${epic.last_updated}`;
143
+ }
144
+ /**
145
+ * Build User Stories section
146
+ */
147
+ buildUserStoriesSection(userStories) {
148
+ const total = userStories.length;
149
+ const completed = userStories.filter((us) => us.status === "complete").length;
150
+ const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
151
+ let section = `## User Stories
152
+
153
+ Progress: ${completed}/${total} user stories complete (${percentage}%)
154
+
155
+ `;
156
+ for (const us of userStories) {
157
+ const checkbox = us.status === "complete" ? "[x]" : "[ ]";
158
+ const statusEmoji = this.getStatusEmoji(us.status);
159
+ const incrementLink = us.increment ? `[${us.increment}](../../increments/${us.increment}/)` : "TBD";
160
+ section += `- ${checkbox} **${us.id}: ${us.title}** (${statusEmoji} ${us.status} | Increment: ${incrementLink})
161
+ `;
162
+ }
163
+ return section;
164
+ }
165
+ /**
166
+ * Build Tasks section (grouped by User Story)
167
+ */
168
+ buildTasksSection(userStories) {
169
+ const totalTasks = userStories.reduce((sum, us) => sum + us.tasks.length, 0);
170
+ const completedTasks = userStories.reduce(
171
+ (sum, us) => sum + us.tasks.filter((t) => t.status).length,
172
+ 0
173
+ );
174
+ const percentage = totalTasks > 0 ? Math.round(completedTasks / totalTasks * 100) : 0;
175
+ let section = `## Tasks by User Story
176
+
177
+ Progress: ${completedTasks}/${totalTasks} tasks complete (${percentage}%)
178
+
179
+ `;
180
+ for (const us of userStories) {
181
+ if (us.tasks.length === 0) {
182
+ continue;
183
+ }
184
+ const incrementLink = us.increment ? `[${us.increment}](../../increments/${us.increment}/tasks.md)` : "TBD";
185
+ section += `### ${us.id}: ${us.title} (Increment: ${incrementLink})
186
+
187
+ `;
188
+ for (const task of us.tasks) {
189
+ const checkbox = task.status ? "[x]" : "[ ]";
190
+ section += `- ${checkbox} ${task.id}: ${task.title}
191
+ `;
192
+ }
193
+ section += "\n";
194
+ }
195
+ return section;
196
+ }
197
+ /**
198
+ * Normalize status values
199
+ */
200
+ normalizeStatus(status) {
201
+ const normalized = status.toLowerCase();
202
+ if (normalized === "complete" || normalized === "completed") return "complete";
203
+ if (normalized === "active" || normalized === "in-progress") return "active";
204
+ if (normalized === "planning") return "planning";
205
+ return "not-started";
206
+ }
207
+ /**
208
+ * Get status emoji
209
+ */
210
+ getStatusEmoji(status) {
211
+ switch (status) {
212
+ case "complete":
213
+ return "\u2705";
214
+ case "active":
215
+ return "\u{1F6A7}";
216
+ case "planning":
217
+ return "\u{1F4CB}";
218
+ case "not-started":
219
+ return "\u23F3";
220
+ default:
221
+ return "\u2753";
222
+ }
223
+ }
224
+ }
225
+ export {
226
+ EpicContentBuilder
227
+ };
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Epic Content Builder - Hierarchical GitHub issue content for Feature Specs
3
+ *
4
+ * Architecture:
5
+ * - Reads FS-* folder (FEATURE.md + us-*.md files)
6
+ * - Reads increment tasks.md files to map tasks to user stories
7
+ * - Generates hierarchical issue body:
8
+ * 1. User Stories section (with status + increment)
9
+ * 2. Tasks section (grouped by User Story)
10
+ *
11
+ * Key Features:
12
+ * - NO single "Increment" field (epics span multiple increments)
13
+ * - User Stories are checkable with status and increment link
14
+ * - Tasks grouped under their User Story
15
+ * - Shows which increment each US/task belongs to
16
+ */
17
+
18
+ import { readdir, readFile } from 'fs/promises';
19
+ import { existsSync } from 'fs';
20
+ import * as path from 'path';
21
+ import * as yaml from 'yaml';
22
+
23
+ interface UserStory {
24
+ id: string;
25
+ title: string;
26
+ status: 'complete' | 'active' | 'planning' | 'not-started';
27
+ increment: string | null; // e.g., "0031-external-tool-status-sync"
28
+ tasks: Task[];
29
+ }
30
+
31
+ interface Task {
32
+ id: string;
33
+ title: string;
34
+ status: boolean; // true = completed, false = not started
35
+ userStoryId: string; // e.g., "US-001"
36
+ }
37
+
38
+ interface EpicFrontmatter {
39
+ id: string;
40
+ title: string;
41
+ status: string;
42
+ created: string;
43
+ last_updated: string;
44
+ }
45
+
46
+ interface UserStoryFrontmatter {
47
+ id: string;
48
+ epic: string;
49
+ title: string;
50
+ status: string;
51
+ created: string;
52
+ completed?: string;
53
+ }
54
+
55
+ export class EpicContentBuilder {
56
+ private epicFolder: string;
57
+ private projectRoot: string;
58
+
59
+ constructor(epicFolder: string, projectRoot: string) {
60
+ this.epicFolder = epicFolder;
61
+ this.projectRoot = projectRoot;
62
+ }
63
+
64
+ /**
65
+ * Build hierarchical GitHub issue body
66
+ *
67
+ * Format:
68
+ * - Epic overview
69
+ * - User Stories section (checkable, with status + increment)
70
+ * - Tasks section (grouped by User Story)
71
+ */
72
+ async buildIssueBody(): Promise<string> {
73
+ // 1. Read Epic metadata
74
+ const epicData = await this.readEpicMetadata();
75
+
76
+ // 2. Read User Stories
77
+ const userStories = await this.readUserStories();
78
+
79
+ // 3. Build sections
80
+ const overview = this.buildOverviewSection(epicData);
81
+ const userStoriesSection = this.buildUserStoriesSection(userStories);
82
+ const tasksSection = this.buildTasksSection(userStories);
83
+
84
+ // 4. Combine
85
+ return `${overview}\n\n---\n\n${userStoriesSection}\n\n---\n\n${tasksSection}\n\n---\n\n🤖 Auto-created by SpecWeave Epic Sync`;
86
+ }
87
+
88
+ /**
89
+ * Read Epic FEATURE.md frontmatter
90
+ */
91
+ private async readEpicMetadata(): Promise<EpicFrontmatter> {
92
+ const featurePath = path.join(this.epicFolder, 'FEATURE.md');
93
+ const content = await readFile(featurePath, 'utf-8');
94
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
95
+
96
+ if (!match) {
97
+ throw new Error('FEATURE.md missing YAML frontmatter');
98
+ }
99
+
100
+ return yaml.parse(match[1]) as EpicFrontmatter;
101
+ }
102
+
103
+ /**
104
+ * Read all user stories from us-*.md files
105
+ */
106
+ private async readUserStories(): Promise<UserStory[]> {
107
+ const files = await readdir(this.epicFolder);
108
+ const usFiles = files.filter((f) => f.startsWith('us-') && f.endsWith('.md'));
109
+
110
+ const userStories: UserStory[] = [];
111
+
112
+ for (const file of usFiles.sort()) {
113
+ const filePath = path.join(this.epicFolder, file);
114
+ const content = await readFile(filePath, 'utf-8');
115
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
116
+
117
+ if (!match) {
118
+ console.warn(` ⚠️ ${file} missing frontmatter, skipping`);
119
+ continue;
120
+ }
121
+
122
+ const frontmatter = yaml.parse(match[1]) as UserStoryFrontmatter;
123
+ const bodyContent = content.slice(match[0].length).trim();
124
+
125
+ // Extract increment from Implementation section
126
+ const incrementMatch = bodyContent.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
127
+ const increment = incrementMatch ? incrementMatch[1] : null;
128
+
129
+ // Extract tasks from Implementation section
130
+ const tasks = await this.extractTasksForUserStory(
131
+ frontmatter.id,
132
+ increment,
133
+ bodyContent
134
+ );
135
+
136
+ userStories.push({
137
+ id: frontmatter.id,
138
+ title: frontmatter.title,
139
+ status: this.normalizeStatus(frontmatter.status),
140
+ increment,
141
+ tasks,
142
+ });
143
+ }
144
+
145
+ return userStories;
146
+ }
147
+
148
+ /**
149
+ * Extract tasks for a user story from its Implementation section
150
+ */
151
+ private async extractTasksForUserStory(
152
+ userStoryId: string,
153
+ incrementId: string | null,
154
+ content: string
155
+ ): Promise<Task[]> {
156
+ if (!incrementId) {
157
+ return []; // No increment yet, no tasks
158
+ }
159
+
160
+ // Find increment folder
161
+ const incrementFolder = path.join(
162
+ this.projectRoot,
163
+ '.specweave',
164
+ 'increments',
165
+ incrementId
166
+ );
167
+
168
+ if (!existsSync(incrementFolder)) {
169
+ console.warn(` ⚠️ Increment folder not found: ${incrementId}`);
170
+ return [];
171
+ }
172
+
173
+ const tasksPath = path.join(incrementFolder, 'tasks.md');
174
+ if (!existsSync(tasksPath)) {
175
+ console.warn(` ⚠️ tasks.md not found in ${incrementId}`);
176
+ return [];
177
+ }
178
+
179
+ // Read tasks.md
180
+ const tasksContent = await readFile(tasksPath, 'utf-8');
181
+
182
+ // Extract task links from user story's Implementation section
183
+ // Format: - [T-001: Title](link#t-001-title)
184
+ const taskLinkPattern = /- \[([T-\d]+):\s*([^\]]+)\]/g;
185
+ const taskLinks: Array<{ id: string; title: string }> = [];
186
+
187
+ let match;
188
+ while ((match = taskLinkPattern.exec(content)) !== null) {
189
+ taskLinks.push({
190
+ id: match[1], // e.g., "T-001"
191
+ title: match[2].trim(),
192
+ });
193
+ }
194
+
195
+ // Parse tasks from tasks.md to get completion status
196
+ const tasks: Task[] = [];
197
+
198
+ for (const taskLink of taskLinks) {
199
+ // Find task in tasks.md by heading pattern: ### T-001: Title
200
+ const taskPattern = new RegExp(
201
+ `###\\s+${taskLink.id}:\\s*([^\\n]+)[\\s\\S]*?\\*\\*Status\\*\\*:\\s*\\[([x\\s])\\]`,
202
+ 'i'
203
+ );
204
+ const taskMatch = tasksContent.match(taskPattern);
205
+
206
+ const isCompleted = taskMatch ? taskMatch[2] === 'x' : false;
207
+
208
+ tasks.push({
209
+ id: taskLink.id,
210
+ title: taskLink.title,
211
+ status: isCompleted,
212
+ userStoryId,
213
+ });
214
+ }
215
+
216
+ return tasks;
217
+ }
218
+
219
+ /**
220
+ * Build overview section
221
+ */
222
+ private buildOverviewSection(epic: EpicFrontmatter): string {
223
+ return `# [${epic.id}] ${epic.title}\n\n**Status**: ${epic.status}\n**Created**: ${epic.created}\n**Last Updated**: ${epic.last_updated}`;
224
+ }
225
+
226
+ /**
227
+ * Build User Stories section
228
+ */
229
+ private buildUserStoriesSection(userStories: UserStory[]): string {
230
+ const total = userStories.length;
231
+ const completed = userStories.filter((us) => us.status === 'complete').length;
232
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
233
+
234
+ let section = `## User Stories\n\nProgress: ${completed}/${total} user stories complete (${percentage}%)\n\n`;
235
+
236
+ for (const us of userStories) {
237
+ const checkbox = us.status === 'complete' ? '[x]' : '[ ]';
238
+ const statusEmoji = this.getStatusEmoji(us.status);
239
+ const incrementLink = us.increment
240
+ ? `[${us.increment}](../../increments/${us.increment}/)`
241
+ : 'TBD';
242
+
243
+ section += `- ${checkbox} **${us.id}: ${us.title}** (${statusEmoji} ${us.status} | Increment: ${incrementLink})\n`;
244
+ }
245
+
246
+ return section;
247
+ }
248
+
249
+ /**
250
+ * Build Tasks section (grouped by User Story)
251
+ */
252
+ private buildTasksSection(userStories: UserStory[]): string {
253
+ const totalTasks = userStories.reduce((sum, us) => sum + us.tasks.length, 0);
254
+ const completedTasks = userStories.reduce(
255
+ (sum, us) => sum + us.tasks.filter((t) => t.status).length,
256
+ 0
257
+ );
258
+ const percentage =
259
+ totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
260
+
261
+ let section = `## Tasks by User Story\n\nProgress: ${completedTasks}/${totalTasks} tasks complete (${percentage}%)\n\n`;
262
+
263
+ for (const us of userStories) {
264
+ if (us.tasks.length === 0) {
265
+ continue; // Skip user stories with no tasks yet
266
+ }
267
+
268
+ const incrementLink = us.increment
269
+ ? `[${us.increment}](../../increments/${us.increment}/tasks.md)`
270
+ : 'TBD';
271
+
272
+ section += `### ${us.id}: ${us.title} (Increment: ${incrementLink})\n\n`;
273
+
274
+ for (const task of us.tasks) {
275
+ const checkbox = task.status ? '[x]' : '[ ]';
276
+ section += `- ${checkbox} ${task.id}: ${task.title}\n`;
277
+ }
278
+
279
+ section += '\n';
280
+ }
281
+
282
+ return section;
283
+ }
284
+
285
+ /**
286
+ * Normalize status values
287
+ */
288
+ private normalizeStatus(
289
+ status: string
290
+ ): 'complete' | 'active' | 'planning' | 'not-started' {
291
+ const normalized = status.toLowerCase();
292
+ if (normalized === 'complete' || normalized === 'completed') return 'complete';
293
+ if (normalized === 'active' || normalized === 'in-progress') return 'active';
294
+ if (normalized === 'planning') return 'planning';
295
+ return 'not-started';
296
+ }
297
+
298
+ /**
299
+ * Get status emoji
300
+ */
301
+ private getStatusEmoji(
302
+ status: 'complete' | 'active' | 'planning' | 'not-started'
303
+ ): string {
304
+ switch (status) {
305
+ case 'complete':
306
+ return '✅';
307
+ case 'active':
308
+ return '🚧';
309
+ case 'planning':
310
+ return '📋';
311
+ case 'not-started':
312
+ return '⏳';
313
+ default:
314
+ return '❓';
315
+ }
316
+ }
317
+ }
@@ -1,4 +1,5 @@
1
1
  import { execSync } from "child_process";
2
+ import { DuplicateDetector } from "./duplicate-detector.js";
2
3
  class GitHubClient {
3
4
  constructor(repo) {
4
5
  this.repo = repo || this.detectRepo();
@@ -70,21 +71,31 @@ class GitHubClient {
70
71
  }
71
72
  }
72
73
  /**
73
- * Create epic issue (increment-level)
74
+ * Create epic issue (increment-level) with FULL DUPLICATE PROTECTION
74
75
  */
75
76
  async createEpicIssue(title, body, milestone, labels = []) {
76
- const labelArgs = labels.map((l) => `-l "${l}"`).join(" ");
77
- const milestoneArg = milestone ? `-m "${milestone}"` : "";
78
- const createCmd = `gh issue create --repo ${this.repo} --title "${this.escapeQuotes(title)}" --body "${this.escapeQuotes(body)}" ${labelArgs} ${milestoneArg}`;
77
+ const titlePattern = DuplicateDetector.extractTitlePattern(title);
78
+ if (!titlePattern) {
79
+ throw new Error(`Epic issue title must start with pattern like [FS-XXX] or [INC-XXXX]: ${title}`);
80
+ }
79
81
  try {
80
- const issueUrl = execSync(createCmd, { encoding: "utf-8" }).trim();
81
- const issueNumber = parseInt(issueUrl.split("/").pop() || "0", 10);
82
- if (!issueNumber) {
83
- throw new Error("Failed to extract issue number from URL: " + issueUrl);
84
- }
85
- const viewCmd = `gh issue view ${issueNumber} --repo ${this.repo} --json number,title,body,state,url,labels,milestone`;
82
+ const result = await DuplicateDetector.createWithProtection({
83
+ title,
84
+ body,
85
+ titlePattern,
86
+ labels: labels.length > 0 ? labels : ["specweave", "increment"],
87
+ milestone: milestone?.toString(),
88
+ repo: this.repo
89
+ });
90
+ const viewCmd = `gh issue view ${result.issue.number} --repo ${this.repo} --json number,title,body,state,url,labels,milestone`;
86
91
  const output = execSync(viewCmd, { encoding: "utf-8" });
87
92
  const issue = JSON.parse(output);
93
+ if (result.wasReused) {
94
+ console.log(` \u267B\uFE0F Reused existing issue #${result.issue.number} (duplicate prevention)`);
95
+ }
96
+ if (result.duplicatesFound > 0) {
97
+ console.log(` \u{1F6E1}\uFE0F Duplicates detected: ${result.duplicatesFound} (auto-closed: ${result.duplicatesClosed})`);
98
+ }
88
99
  return {
89
100
  ...issue,
90
101
  html_url: issue.url,
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { execSync } from 'child_process';
7
7
  import { GitHubIssue, GitHubMilestone } from './types';
8
+ import { DuplicateDetector } from './duplicate-detector.js';
8
9
 
9
10
  export class GitHubClient {
10
11
  private repo: string;
@@ -96,7 +97,7 @@ export class GitHubClient {
96
97
  }
97
98
 
98
99
  /**
99
- * Create epic issue (increment-level)
100
+ * Create epic issue (increment-level) with FULL DUPLICATE PROTECTION
100
101
  */
101
102
  async createEpicIssue(
102
103
  title: string,
@@ -104,27 +105,37 @@ export class GitHubClient {
104
105
  milestone?: number | string,
105
106
  labels: string[] = []
106
107
  ): Promise<GitHubIssue> {
107
- const labelArgs = labels.map(l => `-l "${l}"`).join(' ');
108
- const milestoneArg = milestone ? `-m "${milestone}"` : '';
108
+ // Extract title pattern for duplicate detection (e.g., "[FS-031]" from "[FS-031] Feature Title")
109
+ const titlePattern = DuplicateDetector.extractTitlePattern(title);
109
110
 
110
- // Create issue (returns URL)
111
- const createCmd = `gh issue create --repo ${this.repo} --title "${this.escapeQuotes(title)}" --body "${this.escapeQuotes(body)}" ${labelArgs} ${milestoneArg}`;
111
+ if (!titlePattern) {
112
+ throw new Error(`Epic issue title must start with pattern like [FS-XXX] or [INC-XXXX]: ${title}`);
113
+ }
112
114
 
113
115
  try {
114
- const issueUrl = execSync(createCmd, { encoding: 'utf-8' }).trim();
115
-
116
- // Extract issue number from URL (e.g., https://github.com/owner/repo/issues/123)
117
- const issueNumber = parseInt(issueUrl.split('/').pop() || '0', 10);
118
-
119
- if (!issueNumber) {
120
- throw new Error('Failed to extract issue number from URL: ' + issueUrl);
121
- }
122
-
123
- // Fetch issue details
124
- const viewCmd = `gh issue view ${issueNumber} --repo ${this.repo} --json number,title,body,state,url,labels,milestone`;
116
+ // Use DuplicateDetector for full 3-phase protection
117
+ const result = await DuplicateDetector.createWithProtection({
118
+ title,
119
+ body,
120
+ titlePattern,
121
+ labels: labels.length > 0 ? labels : ['specweave', 'increment'],
122
+ milestone: milestone?.toString(),
123
+ repo: this.repo
124
+ });
125
+
126
+ // Fetch full issue details (DuplicateDetector returns minimal info)
127
+ const viewCmd = `gh issue view ${result.issue.number} --repo ${this.repo} --json number,title,body,state,url,labels,milestone`;
125
128
  const output = execSync(viewCmd, { encoding: 'utf-8' });
126
129
  const issue = JSON.parse(output);
127
130
 
131
+ // Log duplicate detection results if any
132
+ if (result.wasReused) {
133
+ console.log(` ♻️ Reused existing issue #${result.issue.number} (duplicate prevention)`);
134
+ }
135
+ if (result.duplicatesFound > 0) {
136
+ console.log(` 🛡️ Duplicates detected: ${result.duplicatesFound} (auto-closed: ${result.duplicatesClosed})`);
137
+ }
138
+
128
139
  return {
129
140
  ...issue,
130
141
  html_url: issue.url,