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
@@ -20,8 +20,9 @@ import {
20
20
  ContentSyncResult,
21
21
  } from '../../../src/core/spec-content-sync.js';
22
22
  import { SyncProfile } from '../../../src/core/types/sync-profile.js';
23
- import path from 'path';
24
- import fs from 'fs/promises';
23
+ import { SpecIncrementMapper, TaskInfo } from '../../../src/core/sync/spec-increment-mapper.js';
24
+ import * as path from 'path';
25
+ import * as fs from 'fs/promises';
25
26
 
26
27
  export interface AdoContentSyncOptions {
27
28
  specPath: string;
@@ -91,9 +92,12 @@ async function createAdoFeature(
91
92
  const { specPath, dryRun, verbose } = options;
92
93
 
93
94
  try {
95
+ // Get task mappings (if available)
96
+ const tasks = await getTaskMappings(specPath, spec.id);
97
+
94
98
  // Build feature title and description
95
99
  const title = `[${spec.id.toUpperCase()}] ${spec.title}`;
96
- const description = buildAdoDescription(spec);
100
+ const description = buildAdoDescription(spec, tasks);
97
101
 
98
102
  if (verbose) {
99
103
  console.log(`\n📝 Creating ADO feature:`);
@@ -196,9 +200,12 @@ async function updateAdoFeature(
196
200
  }
197
201
  }
198
202
 
203
+ // Get task mappings (if available)
204
+ const tasks = await getTaskMappings(specPath, spec.id);
205
+
199
206
  // Build updated content
200
207
  const newTitle = `[${spec.id.toUpperCase()}] ${spec.title}`;
201
- const newDescription = buildAdoDescription(spec);
208
+ const newDescription = buildAdoDescription(spec, tasks);
202
209
 
203
210
  if (dryRun) {
204
211
  console.log('\n🔍 Dry run - would update feature:');
@@ -242,10 +249,10 @@ async function updateAdoFeature(
242
249
  }
243
250
 
244
251
  /**
245
- * Build ADO description from spec content
252
+ * Build ADO description from spec content with optional task mappings
246
253
  * ADO supports HTML in description
247
254
  */
248
- function buildAdoDescription(spec: SpecContent): string {
255
+ function buildAdoDescription(spec: SpecContent, tasks?: TaskInfo[]): string {
249
256
  let html = '';
250
257
 
251
258
  // Add spec description
@@ -271,6 +278,20 @@ function buildAdoDescription(spec: SpecContent): string {
271
278
  }
272
279
  }
273
280
 
281
+ // Add task mappings (if provided)
282
+ if (tasks && tasks.length > 0) {
283
+ html += '<h2>Implementation Tasks</h2>';
284
+ html += '<ul>';
285
+ for (const task of tasks) {
286
+ html += `<li><strong>${task.id}</strong>: ${escapeHtml(task.title)}`;
287
+ if (task.userStories && task.userStories.length > 0) {
288
+ html += ` (${task.userStories.join(', ')})`;
289
+ }
290
+ html += '</li>';
291
+ }
292
+ html += '</ul>';
293
+ }
294
+
274
295
  // Add metadata
275
296
  if (spec.metadata.priority) {
276
297
  html += `<p><strong>Priority:</strong> ${spec.metadata.priority}</p>`;
@@ -279,6 +300,51 @@ function buildAdoDescription(spec: SpecContent): string {
279
300
  return html;
280
301
  }
281
302
 
303
+ /**
304
+ * Get task mappings for a spec (if available)
305
+ */
306
+ async function getTaskMappings(specPath: string, specId: string): Promise<TaskInfo[] | undefined> {
307
+ try {
308
+ // Find SpecWeave root
309
+ const rootDir = await findSpecWeaveRoot(specPath);
310
+
311
+ // Use SpecIncrementMapper to get task mappings
312
+ const mapper = new SpecIncrementMapper(rootDir);
313
+ const mapping = await mapper.mapSpecToIncrements(specId);
314
+
315
+ if (mapping.increments.length > 0) {
316
+ // Return tasks from the first (most recent) increment
317
+ return mapping.increments[0].tasks;
318
+ }
319
+
320
+ return undefined;
321
+ } catch (error) {
322
+ // If mapping fails, just return undefined (not critical)
323
+ return undefined;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Find SpecWeave root directory from spec path
329
+ */
330
+ async function findSpecWeaveRoot(specPath: string): Promise<string> {
331
+ let currentDir = path.dirname(specPath);
332
+
333
+ while (true) {
334
+ const specweaveDir = path.join(currentDir, '.specweave');
335
+ try {
336
+ await fs.access(specweaveDir);
337
+ return currentDir;
338
+ } catch {
339
+ const parentDir = path.dirname(currentDir);
340
+ if (parentDir === currentDir) {
341
+ throw new Error('.specweave directory not found');
342
+ }
343
+ currentDir = parentDir;
344
+ }
345
+ }
346
+ }
347
+
282
348
  /**
283
349
  * Escape HTML special characters
284
350
  */
@@ -0,0 +1,80 @@
1
+ import axios from "axios";
2
+ class AdoStatusSync {
3
+ constructor(organization, project, personalAccessToken) {
4
+ this.organization = organization;
5
+ this.project = project;
6
+ this.client = axios.create({
7
+ baseURL: `https://dev.azure.com/${organization}/${project}/_apis`,
8
+ auth: {
9
+ username: "",
10
+ // Empty for PAT auth
11
+ password: personalAccessToken
12
+ },
13
+ headers: {
14
+ "Content-Type": "application/json-patch+json",
15
+ "Accept": "application/json"
16
+ }
17
+ });
18
+ }
19
+ /**
20
+ * Get current status from ADO work item
21
+ *
22
+ * @param workItemId - ADO work item ID (e.g., 123)
23
+ * @returns Current work item state
24
+ */
25
+ async getStatus(workItemId) {
26
+ const response = await this.client.get(
27
+ `/wit/workitems/${workItemId}?api-version=7.0`
28
+ );
29
+ return {
30
+ state: response.data.fields["System.State"]
31
+ };
32
+ }
33
+ /**
34
+ * Update ADO work item state
35
+ *
36
+ * Uses JSON Patch format to update System.State field.
37
+ *
38
+ * @param workItemId - ADO work item ID (e.g., 123)
39
+ * @param status - Desired status
40
+ */
41
+ async updateStatus(workItemId, status) {
42
+ const patch = [
43
+ {
44
+ op: "add",
45
+ path: "/fields/System.State",
46
+ value: status.state
47
+ }
48
+ ];
49
+ await this.client.patch(
50
+ `/wit/workitems/${workItemId}?api-version=7.0`,
51
+ patch
52
+ );
53
+ }
54
+ /**
55
+ * Post comment about status change to ADO work item
56
+ *
57
+ * @param workItemId - ADO work item ID (e.g., 123)
58
+ * @param oldStatus - Previous SpecWeave status
59
+ * @param newStatus - New SpecWeave status
60
+ */
61
+ async postStatusComment(workItemId, oldStatus, newStatus) {
62
+ const text = `\u{1F504} Status Update
63
+
64
+ SpecWeave status changed:
65
+ \u2022 From: ${oldStatus}
66
+ \u2022 To: ${newStatus}
67
+ \u2022 When: ${(/* @__PURE__ */ new Date()).toISOString()}
68
+
69
+ Synced from SpecWeave`;
70
+ await this.client.post(
71
+ `/wit/workitems/${workItemId}/comments?api-version=7.0-preview.3`,
72
+ {
73
+ text
74
+ }
75
+ );
76
+ }
77
+ }
78
+ export {
79
+ AdoStatusSync
80
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Azure DevOps Status Sync
3
+ *
4
+ * Synchronizes SpecWeave increment statuses with ADO work item states.
5
+ *
6
+ * ADO Work Item State Updates:
7
+ * - Uses JSON Patch format for updates
8
+ * - System.State field controls work item state
9
+ * - Available states: New, Active, On Hold, Resolved, Closed, Removed
10
+ *
11
+ * @module ado-status-sync
12
+ */
13
+
14
+ import axios, { AxiosInstance } from 'axios';
15
+
16
+ /**
17
+ * External status representation (ADO-specific)
18
+ */
19
+ export interface ExternalStatus {
20
+ state: string; // e.g., "New", "Active", "Closed"
21
+ }
22
+
23
+ /**
24
+ * Azure DevOps Status Sync
25
+ *
26
+ * Handles status synchronization with ADO work items.
27
+ */
28
+ export class AdoStatusSync {
29
+ private client: AxiosInstance;
30
+ private organization: string;
31
+ private project: string;
32
+
33
+ constructor(
34
+ organization: string,
35
+ project: string,
36
+ personalAccessToken: string
37
+ ) {
38
+ this.organization = organization;
39
+ this.project = project;
40
+
41
+ // Create ADO API client
42
+ this.client = axios.create({
43
+ baseURL: `https://dev.azure.com/${organization}/${project}/_apis`,
44
+ auth: {
45
+ username: '', // Empty for PAT auth
46
+ password: personalAccessToken
47
+ },
48
+ headers: {
49
+ 'Content-Type': 'application/json-patch+json',
50
+ 'Accept': 'application/json'
51
+ }
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Get current status from ADO work item
57
+ *
58
+ * @param workItemId - ADO work item ID (e.g., 123)
59
+ * @returns Current work item state
60
+ */
61
+ async getStatus(workItemId: number): Promise<ExternalStatus> {
62
+ const response = await this.client.get(
63
+ `/wit/workitems/${workItemId}?api-version=7.0`
64
+ );
65
+
66
+ return {
67
+ state: response.data.fields['System.State']
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Update ADO work item state
73
+ *
74
+ * Uses JSON Patch format to update System.State field.
75
+ *
76
+ * @param workItemId - ADO work item ID (e.g., 123)
77
+ * @param status - Desired status
78
+ */
79
+ async updateStatus(workItemId: number, status: ExternalStatus): Promise<void> {
80
+ // ADO uses JSON Patch format for updates
81
+ const patch = [
82
+ {
83
+ op: 'add',
84
+ path: '/fields/System.State',
85
+ value: status.state
86
+ }
87
+ ];
88
+
89
+ await this.client.patch(
90
+ `/wit/workitems/${workItemId}?api-version=7.0`,
91
+ patch
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Post comment about status change to ADO work item
97
+ *
98
+ * @param workItemId - ADO work item ID (e.g., 123)
99
+ * @param oldStatus - Previous SpecWeave status
100
+ * @param newStatus - New SpecWeave status
101
+ */
102
+ async postStatusComment(
103
+ workItemId: number,
104
+ oldStatus: string,
105
+ newStatus: string
106
+ ): Promise<void> {
107
+ const text = `🔄 Status Update\n\n` +
108
+ `SpecWeave status changed:\n` +
109
+ `• From: ${oldStatus}\n` +
110
+ `• To: ${newStatus}\n` +
111
+ `• When: ${new Date().toISOString()}\n\n` +
112
+ `Synced from SpecWeave`;
113
+
114
+ await this.client.post(
115
+ `/wit/workitems/${workItemId}/comments?api-version=7.0-preview.3`,
116
+ {
117
+ text
118
+ }
119
+ );
120
+ }
121
+ }
@@ -0,0 +1,170 @@
1
+ import { AdoClientV2 } from "./ado-client-v2.js";
2
+ import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
3
+ import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
4
+ import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
5
+ import path from "path";
6
+ import fs from "fs/promises";
7
+ async function syncSpecToAdoWithEnhancedContent(options) {
8
+ const { specPath, organization, project, dryRun = false, verbose = false } = options;
9
+ try {
10
+ const baseSpec = await parseSpecContent(specPath);
11
+ if (!baseSpec) {
12
+ return {
13
+ success: false,
14
+ action: "error",
15
+ error: "Failed to parse spec content"
16
+ };
17
+ }
18
+ if (verbose) {
19
+ console.log(`\u{1F4C4} Parsed spec: ${baseSpec.identifier.compact}`);
20
+ }
21
+ const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
22
+ const rootDir = await findSpecWeaveRoot(specPath);
23
+ const mapper = new SpecIncrementMapper(rootDir);
24
+ const mapping = await mapper.mapSpecToIncrements(specId);
25
+ if (verbose) {
26
+ console.log(`\u{1F517} Found ${mapping.increments.length} related increments`);
27
+ }
28
+ const taskMapping = buildTaskMapping(mapping.increments, organization, project);
29
+ const architectureDocs = await findArchitectureDocs(rootDir, specId);
30
+ const enhancedSpec = {
31
+ ...baseSpec,
32
+ summary: baseSpec.description,
33
+ taskMapping,
34
+ architectureDocs
35
+ };
36
+ const builder = new EnhancedContentBuilder();
37
+ const description = builder.buildExternalDescription(enhancedSpec);
38
+ if (verbose) {
39
+ console.log(`\u{1F4DD} Generated description: ${description.length} characters`);
40
+ }
41
+ if (dryRun) {
42
+ console.log("\u{1F50D} DRY RUN - Would create/update feature with:");
43
+ console.log(` Title: ${baseSpec.title}`);
44
+ console.log(` Description length: ${description.length}`);
45
+ return {
46
+ success: true,
47
+ action: "no-change",
48
+ tasksLinked: taskMapping?.tasks.length || 0
49
+ };
50
+ }
51
+ if (!organization || !project) {
52
+ return {
53
+ success: false,
54
+ action: "error",
55
+ error: "Azure DevOps organization/project not specified"
56
+ };
57
+ }
58
+ const profile = {
59
+ provider: "ado",
60
+ displayName: `${organization}/${project}`,
61
+ config: {
62
+ organization,
63
+ project
64
+ },
65
+ timeRange: { default: "1M", max: "6M" }
66
+ };
67
+ const pat = process.env.AZURE_DEVOPS_PAT || "";
68
+ const client = new AdoClientV2(profile, pat);
69
+ const existingFeature = await findExistingFeature(client, baseSpec.identifier.compact);
70
+ let result;
71
+ if (existingFeature) {
72
+ await client.updateWorkItem(existingFeature.id, {
73
+ title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
74
+ description
75
+ });
76
+ result = {
77
+ success: true,
78
+ action: "updated",
79
+ featureId: existingFeature.id,
80
+ featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${existingFeature.id}`,
81
+ tasksLinked: taskMapping?.tasks.length || 0
82
+ };
83
+ } else {
84
+ const feature = await client.createEpic({
85
+ title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
86
+ description,
87
+ tags: ["spec", "external-tool-sync"]
88
+ });
89
+ result = {
90
+ success: true,
91
+ action: "created",
92
+ featureId: feature.id,
93
+ featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${feature.id}`,
94
+ tasksLinked: taskMapping?.tasks.length || 0
95
+ };
96
+ }
97
+ if (verbose) {
98
+ console.log(`\u2705 ${result.action === "created" ? "Created" : "Updated"} feature #${result.featureId}`);
99
+ }
100
+ return result;
101
+ } catch (error) {
102
+ return {
103
+ success: false,
104
+ action: "error",
105
+ error: error.message
106
+ };
107
+ }
108
+ }
109
+ async function findSpecWeaveRoot(specPath) {
110
+ let currentDir = path.dirname(specPath);
111
+ while (true) {
112
+ const specweaveDir = path.join(currentDir, ".specweave");
113
+ try {
114
+ await fs.access(specweaveDir);
115
+ return currentDir;
116
+ } catch {
117
+ const parentDir = path.dirname(currentDir);
118
+ if (parentDir === currentDir) {
119
+ throw new Error(".specweave directory not found");
120
+ }
121
+ currentDir = parentDir;
122
+ }
123
+ }
124
+ }
125
+ function buildTaskMapping(increments, organization, project) {
126
+ if (increments.length === 0) return void 0;
127
+ const firstIncrement = increments[0];
128
+ const tasks = firstIncrement.tasks.map((task) => ({
129
+ id: task.id,
130
+ title: task.title,
131
+ userStories: task.userStories
132
+ }));
133
+ return {
134
+ incrementId: firstIncrement.id,
135
+ tasks,
136
+ tasksUrl: `https://dev.azure.com/${organization}/${project}/_git/repo?path=/.specweave/increments/${firstIncrement.id}/tasks.md`
137
+ };
138
+ }
139
+ async function findArchitectureDocs(rootDir, specId) {
140
+ const docs = [];
141
+ const archDir = path.join(rootDir, ".specweave/docs/internal/architecture");
142
+ try {
143
+ const adrDir = path.join(archDir, "adr");
144
+ try {
145
+ const adrs = await fs.readdir(adrDir);
146
+ const relatedAdrs = adrs.filter((file) => file.includes(specId.replace("spec-", "")));
147
+ for (const adr of relatedAdrs) {
148
+ docs.push({
149
+ type: "adr",
150
+ path: path.join(adrDir, adr),
151
+ title: adr.replace(".md", "").replace(/-/g, " ")
152
+ });
153
+ }
154
+ } catch {
155
+ }
156
+ } catch {
157
+ }
158
+ return docs;
159
+ }
160
+ async function findExistingFeature(client, specId) {
161
+ try {
162
+ const features = await client.queryWorkItems(`[System.Title] Contains '[${specId}]' AND [System.WorkItemType] = 'Feature'`);
163
+ return features[0] || null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+ export {
169
+ syncSpecToAdoWithEnhancedContent
170
+ };
@@ -0,0 +1,205 @@
1
+ ---
2
+ name: specweave-github:cleanup-duplicates
3
+ description: Clean up duplicate GitHub issues for an Epic. Finds issues with duplicate titles and closes all except the first created issue.
4
+ ---
5
+
6
+ # Clean Up Duplicate GitHub Issues
7
+
8
+ **CRITICAL**: This command detects and closes duplicate GitHub issues created by multiple syncs.
9
+
10
+ ## Usage
11
+
12
+ ```bash
13
+ /specweave-github:cleanup-duplicates <epic-id> [--dry-run]
14
+ ```
15
+
16
+ ## What It Does
17
+
18
+ **Duplicate Detection & Cleanup**:
19
+
20
+ 1. **Find all issues** for the Epic (searches by Epic ID in title)
21
+ 2. **Group by title** (detect duplicates)
22
+ 3. **For each duplicate group**:
23
+ - Keep the **FIRST created** issue (lowest number)
24
+ - Close all **LATER** issues with comment: "Duplicate of #XXX"
25
+ 4. **Update Epic README** with correct issue numbers
26
+
27
+ ## Examples
28
+
29
+ ### Dry Run (No Changes)
30
+
31
+ ```bash
32
+ /specweave-github:cleanup-duplicates FS-031 --dry-run
33
+ ```
34
+
35
+ **Output**:
36
+ ```
37
+ 🔍 Scanning for duplicates in Epic FS-031...
38
+ Found 25 total issues
39
+ Detected 10 duplicate groups:
40
+
41
+ 📋 Group 1: "[FS-031] External Tool Status Synchronization"
42
+ - #250 (KEEP) - Created 2025-11-10
43
+ - #255 (CLOSE) - Created 2025-11-11 - DUPLICATE
44
+ - #260 (CLOSE) - Created 2025-11-12 - DUPLICATE
45
+
46
+ 📋 Group 2: "[FS-031] Multi-Project GitHub Sync"
47
+ - #251 (KEEP) - Created 2025-11-10
48
+ - #256 (CLOSE) - Created 2025-11-11 - DUPLICATE
49
+
50
+ ...
51
+
52
+ ✅ Dry run complete!
53
+ Total issues: 25
54
+ Duplicate groups: 10
55
+ Issues to close: 15
56
+
57
+ ⚠️ This was a DRY RUN - no changes made.
58
+ Run without --dry-run to actually close duplicates.
59
+ ```
60
+
61
+ ### Actual Cleanup
62
+
63
+ ```bash
64
+ /specweave-github:cleanup-duplicates FS-031
65
+ ```
66
+
67
+ **Output**:
68
+ ```
69
+ 🔍 Scanning for duplicates in Epic FS-031...
70
+ Found 25 total issues
71
+ Detected 10 duplicate groups
72
+
73
+ ⚠️ CONFIRM: Close 15 duplicate issues? [y/N]
74
+ > y
75
+
76
+ 🗑️ Closing duplicates...
77
+ ✅ Closed #255 (duplicate of #250)
78
+ ✅ Closed #256 (duplicate of #251)
79
+ ✅ Closed #260 (duplicate of #250)
80
+ ...
81
+
82
+ 📝 Updating Epic README frontmatter...
83
+ ✅ Updated frontmatter with correct issue numbers
84
+
85
+ ✅ Cleanup complete!
86
+ Closed: 15 duplicates
87
+ Kept: 10 original issues
88
+ ```
89
+
90
+ ## Arguments
91
+
92
+ - `<epic-id>` - Epic ID (e.g., `FS-031` or just `031`)
93
+ - `--dry-run` - Preview changes without actually closing issues (optional)
94
+
95
+ ## Safety Features
96
+
97
+ ✅ **Confirmation prompt**: Asks before closing issues (unless --dry-run)
98
+ ✅ **Dry run mode**: Preview changes safely
99
+ ✅ **Keeps oldest issue**: Preserves the first created issue
100
+ ✅ **Adds closure comment**: Links to the original issue
101
+ ✅ **Updates metadata**: Fixes Epic README frontmatter
102
+
103
+ ## What Gets Closed
104
+
105
+ **Closed issues**:
106
+ - ✅ Duplicate titles (second, third, etc. occurrences)
107
+ - ✅ Closed with comment: "Duplicate of #XXX"
108
+ - ✅ Original issue kept open (or maintains its status)
109
+
110
+ **Example comment on closed duplicate**:
111
+ ```markdown
112
+ Duplicate of #250
113
+
114
+ This issue was automatically closed by SpecWeave cleanup because it is a duplicate.
115
+
116
+ The original issue (#250) contains the same content and should be used for tracking instead.
117
+
118
+ 🤖 Auto-closed by SpecWeave Duplicate Cleanup
119
+ ```
120
+
121
+ ## Requirements
122
+
123
+ 1. **GitHub CLI** (`gh`) installed and authenticated
124
+ 2. **Write access** to repository (for closing issues)
125
+ 3. **Epic folder exists** at `.specweave/docs/internal/specs/FS-XXX-name/`
126
+
127
+ ## When to Use
128
+
129
+ **Use this command when**:
130
+ - ✅ You see multiple issues with the same title in GitHub
131
+ - ✅ Epic sync ran multiple times and created duplicates
132
+ - ✅ Epic README frontmatter got corrupted and reset
133
+ - ✅ Post-sync validation warns about duplicates
134
+
135
+ **Example warning that triggers this**:
136
+ ```
137
+ ⚠️ WARNING: 10 duplicate(s) detected!
138
+ Run cleanup command to resolve:
139
+ /specweave-github:cleanup-duplicates FS-031
140
+ ```
141
+
142
+ ## Troubleshooting
143
+
144
+ **"No duplicates found"**:
145
+ - Good! Your Epic has no duplicate issues
146
+ - Run epic sync is working correctly with duplicate detection
147
+
148
+ **"GitHub CLI not authenticated"**:
149
+ - Run: `gh auth login`
150
+ - Ensure you have write access to the repository
151
+
152
+ **"Could not find Epic folder"**:
153
+ - Check Epic exists: `ls .specweave/docs/internal/specs/`
154
+ - Verify Epic ID format: `FS-031-epic-name/`
155
+
156
+ **"Error closing issue"**:
157
+ - Check GitHub CLI: `gh auth status`
158
+ - Verify write permissions: `gh repo view`
159
+
160
+ ## Architecture
161
+
162
+ **Duplicate Detection Logic**:
163
+ 1. Group issues by **exact title match**
164
+ 2. Within each group, sort by **issue number** (ascending)
165
+ 3. Keep **first issue** (lowest number = oldest)
166
+ 4. Close **all others** as duplicates
167
+
168
+ **Why lowest number?**:
169
+ - Lower issue numbers were created first
170
+ - Preserves chronological order
171
+ - Maintains links from old documentation
172
+
173
+ ## Related Commands
174
+
175
+ - `/specweave-github:sync-epic` - Sync Epic (now with duplicate detection!)
176
+ - `/specweave:validate` - Validate increment completeness
177
+ - `gh issue list` - View all issues (GitHub CLI)
178
+
179
+ ## Implementation
180
+
181
+ **File**: `plugins/specweave-github/lib/github-epic-sync.ts`
182
+
183
+ **Method**: `cleanupDuplicates(epicId: string, dryRun: boolean)`
184
+
185
+ **Algorithm**:
186
+ 1. Search GitHub for all issues with Epic ID
187
+ 2. Group by title (Map<string, number[]>)
188
+ 3. Filter groups with >1 issue (duplicates)
189
+ 4. For each duplicate group:
190
+ - Keep first issue (lowest number)
191
+ - Close others with gh CLI
192
+ 5. Update Epic README frontmatter
193
+
194
+ ## Next Steps
195
+
196
+ After cleanup:
197
+
198
+ 1. **Verify cleanup**: `gh issue list --search "[FS-031]"`
199
+ 2. **Check Epic README**: Verify frontmatter has correct issue numbers
200
+ 3. **Re-run sync**: `/specweave-github:sync-epic FS-031` (should show no duplicates)
201
+ 4. **Enable duplicate detection**: Already enabled in v0.18.0+
202
+
203
+ ---
204
+
205
+ **✅ SAFE TO USE**: This command is idempotent and safe to run multiple times.