specweave 0.18.0 → 0.20.0

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 (428) hide show
  1. package/CLAUDE.md +229 -1817
  2. package/README.md +68 -0
  3. package/bin/specweave.js +62 -6
  4. package/dist/locales/de/.gitkeep +0 -0
  5. package/dist/locales/de/cli.json +108 -0
  6. package/dist/locales/en/cli.json +287 -0
  7. package/dist/locales/en/errors.json +7 -0
  8. package/dist/locales/en/templates.json +6 -0
  9. package/dist/locales/es/.gitkeep +0 -0
  10. package/dist/locales/es/cli.json +41 -0
  11. package/dist/locales/fr/.gitkeep +0 -0
  12. package/dist/locales/fr/cli.json +108 -0
  13. package/dist/locales/ja/.gitkeep +0 -0
  14. package/dist/locales/ja/cli.json +108 -0
  15. package/dist/locales/ko/.gitkeep +0 -0
  16. package/dist/locales/ko/cli.json +108 -0
  17. package/dist/locales/pt/.gitkeep +0 -0
  18. package/dist/locales/pt/cli.json +108 -0
  19. package/dist/locales/ru/.gitkeep +0 -0
  20. package/dist/locales/ru/cli.json +269 -0
  21. package/dist/locales/zh/.gitkeep +0 -0
  22. package/dist/locales/zh/cli.json +108 -0
  23. package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
  24. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +3 -0
  25. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
  26. package/dist/plugins/specweave/lib/hooks/update-ac-status.d.ts +21 -0
  27. package/dist/plugins/specweave/lib/hooks/update-ac-status.d.ts.map +1 -0
  28. package/dist/plugins/specweave/lib/hooks/update-ac-status.js +162 -0
  29. package/dist/plugins/specweave/lib/hooks/update-ac-status.js.map +1 -0
  30. package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.d.ts.map +1 -1
  31. package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js +65 -6
  32. package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js.map +1 -1
  33. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +25 -0
  34. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +1 -0
  35. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +191 -0
  36. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +1 -0
  37. package/dist/plugins/specweave-github/lib/completion-calculator.d.ts +112 -0
  38. package/dist/plugins/specweave-github/lib/completion-calculator.d.ts.map +1 -0
  39. package/dist/plugins/specweave-github/lib/completion-calculator.js +301 -0
  40. package/dist/plugins/specweave-github/lib/completion-calculator.js.map +1 -0
  41. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +3 -3
  42. package/dist/plugins/specweave-github/lib/duplicate-detector.js +3 -3
  43. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +70 -0
  44. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +1 -0
  45. package/dist/plugins/specweave-github/lib/epic-content-builder.js +258 -0
  46. package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +1 -0
  47. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +14 -0
  48. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
  49. package/dist/plugins/specweave-github/lib/github-client-v2.js +51 -0
  50. package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
  51. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +2 -2
  52. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -1
  53. package/dist/plugins/specweave-github/lib/github-epic-sync.js +20 -5
  54. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -1
  55. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +87 -0
  56. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -0
  57. package/dist/plugins/specweave-github/lib/github-feature-sync.js +412 -0
  58. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -0
  59. package/dist/plugins/specweave-github/lib/github-spec-content-sync.d.ts.map +1 -1
  60. package/dist/plugins/specweave-github/lib/github-spec-content-sync.js +64 -13
  61. package/dist/plugins/specweave-github/lib/github-spec-content-sync.js.map +1 -1
  62. package/dist/plugins/specweave-github/lib/progress-comment-builder.d.ts +78 -0
  63. package/dist/plugins/specweave-github/lib/progress-comment-builder.d.ts.map +1 -0
  64. package/dist/plugins/specweave-github/lib/progress-comment-builder.js +237 -0
  65. package/dist/plugins/specweave-github/lib/progress-comment-builder.js.map +1 -0
  66. package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts +97 -0
  67. package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts.map +1 -0
  68. package/dist/plugins/specweave-github/lib/user-story-content-builder.js +301 -0
  69. package/dist/plugins/specweave-github/lib/user-story-content-builder.js.map +1 -0
  70. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts +83 -0
  71. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -0
  72. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +386 -0
  73. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -0
  74. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +28 -0
  75. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -0
  76. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +156 -0
  77. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -0
  78. package/dist/plugins/specweave-kafka/lib/cli/kcat-wrapper.d.ts +57 -0
  79. package/dist/plugins/specweave-kafka/lib/cli/kcat-wrapper.d.ts.map +1 -0
  80. package/dist/plugins/specweave-kafka/lib/cli/kcat-wrapper.js +248 -0
  81. package/dist/plugins/specweave-kafka/lib/cli/kcat-wrapper.js.map +1 -0
  82. package/dist/plugins/specweave-kafka/lib/cli/types.d.ts +82 -0
  83. package/dist/plugins/specweave-kafka/lib/cli/types.d.ts.map +1 -0
  84. package/dist/plugins/specweave-kafka/lib/cli/types.js +13 -0
  85. package/dist/plugins/specweave-kafka/lib/cli/types.js.map +1 -0
  86. package/dist/plugins/specweave-kafka/lib/mcp/detector.d.ts +49 -0
  87. package/dist/plugins/specweave-kafka/lib/mcp/detector.d.ts.map +1 -0
  88. package/dist/plugins/specweave-kafka/lib/mcp/detector.js +316 -0
  89. package/dist/plugins/specweave-kafka/lib/mcp/detector.js.map +1 -0
  90. package/dist/plugins/specweave-kafka/lib/mcp/types.d.ts +70 -0
  91. package/dist/plugins/specweave-kafka/lib/mcp/types.d.ts.map +1 -0
  92. package/dist/plugins/specweave-kafka/lib/mcp/types.js +23 -0
  93. package/dist/plugins/specweave-kafka/lib/mcp/types.js.map +1 -0
  94. package/dist/plugins/specweave-kafka/lib/utils/partitioning.d.ts +85 -0
  95. package/dist/plugins/specweave-kafka/lib/utils/partitioning.d.ts.map +1 -0
  96. package/dist/plugins/specweave-kafka/lib/utils/partitioning.js +281 -0
  97. package/dist/plugins/specweave-kafka/lib/utils/partitioning.js.map +1 -0
  98. package/dist/plugins/specweave-kafka/lib/utils/sizing.d.ts +75 -0
  99. package/dist/plugins/specweave-kafka/lib/utils/sizing.d.ts.map +1 -0
  100. package/dist/plugins/specweave-kafka/lib/utils/sizing.js +238 -0
  101. package/dist/plugins/specweave-kafka/lib/utils/sizing.js.map +1 -0
  102. package/dist/spec-parser.js +629 -0
  103. package/dist/src/cli/commands/import-docs.js +4 -4
  104. package/dist/src/cli/commands/import-docs.js.map +1 -1
  105. package/dist/src/cli/commands/init-multiproject.d.ts.map +1 -1
  106. package/dist/src/cli/commands/init-multiproject.js +17 -18
  107. package/dist/src/cli/commands/init-multiproject.js.map +1 -1
  108. package/dist/src/cli/commands/init.d.ts.map +1 -1
  109. package/dist/src/cli/commands/init.js +107 -3
  110. package/dist/src/cli/commands/init.js.map +1 -1
  111. package/dist/src/cli/commands/migrate-to-multiproject.d.ts.map +1 -1
  112. package/dist/src/cli/commands/migrate-to-multiproject.js +8 -4
  113. package/dist/src/cli/commands/migrate-to-multiproject.js.map +1 -1
  114. package/dist/src/cli/commands/switch-project.d.ts.map +1 -1
  115. package/dist/src/cli/commands/switch-project.js +9 -26
  116. package/dist/src/cli/commands/switch-project.js.map +1 -1
  117. package/dist/src/cli/commands/sync-spec-content.js +3 -0
  118. package/dist/src/cli/commands/sync-spec-content.js.map +1 -1
  119. package/dist/src/core/deduplication/command-deduplicator.d.ts +166 -0
  120. package/dist/src/core/deduplication/command-deduplicator.d.ts.map +1 -0
  121. package/dist/src/core/deduplication/command-deduplicator.js +254 -0
  122. package/dist/src/core/deduplication/command-deduplicator.js.map +1 -0
  123. package/dist/src/core/increment/active-increment-manager.d.ts +42 -15
  124. package/dist/src/core/increment/active-increment-manager.d.ts.map +1 -1
  125. package/dist/src/core/increment/active-increment-manager.js +113 -46
  126. package/dist/src/core/increment/active-increment-manager.js.map +1 -1
  127. package/dist/src/core/increment/conflict-resolver.d.ts +40 -0
  128. package/dist/src/core/increment/conflict-resolver.d.ts.map +1 -0
  129. package/dist/src/core/increment/conflict-resolver.js +219 -0
  130. package/dist/src/core/increment/conflict-resolver.js.map +1 -0
  131. package/dist/src/core/increment/discipline-checker.d.ts.map +1 -1
  132. package/dist/src/core/increment/discipline-checker.js +7 -1
  133. package/dist/src/core/increment/discipline-checker.js.map +1 -1
  134. package/dist/src/core/increment/duplicate-detector.d.ts +52 -0
  135. package/dist/src/core/increment/duplicate-detector.d.ts.map +1 -0
  136. package/dist/src/core/increment/duplicate-detector.js +276 -0
  137. package/dist/src/core/increment/duplicate-detector.js.map +1 -0
  138. package/dist/src/core/increment/increment-archiver.d.ts +90 -0
  139. package/dist/src/core/increment/increment-archiver.d.ts.map +1 -0
  140. package/dist/src/core/increment/increment-archiver.js +368 -0
  141. package/dist/src/core/increment/increment-archiver.js.map +1 -0
  142. package/dist/src/core/increment/increment-reopener.d.ts +165 -0
  143. package/dist/src/core/increment/increment-reopener.d.ts.map +1 -0
  144. package/dist/src/core/increment/increment-reopener.js +390 -0
  145. package/dist/src/core/increment/increment-reopener.js.map +1 -0
  146. package/dist/src/core/increment/metadata-manager.d.ts +26 -1
  147. package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
  148. package/dist/src/core/increment/metadata-manager.js +143 -5
  149. package/dist/src/core/increment/metadata-manager.js.map +1 -1
  150. package/dist/src/core/increment/recent-work-scanner.d.ts +121 -0
  151. package/dist/src/core/increment/recent-work-scanner.d.ts.map +1 -0
  152. package/dist/src/core/increment/recent-work-scanner.js +303 -0
  153. package/dist/src/core/increment/recent-work-scanner.js.map +1 -0
  154. package/dist/src/core/increment/types.d.ts +1 -0
  155. package/dist/src/core/increment/types.d.ts.map +1 -1
  156. package/dist/src/core/increment-utils.d.ts +112 -0
  157. package/dist/src/core/increment-utils.d.ts.map +1 -0
  158. package/dist/src/core/increment-utils.js +210 -0
  159. package/dist/src/core/increment-utils.js.map +1 -0
  160. package/dist/src/core/living-docs/ac-project-specific-generator.d.ts +65 -0
  161. package/dist/src/core/living-docs/ac-project-specific-generator.d.ts.map +1 -0
  162. package/dist/src/core/living-docs/ac-project-specific-generator.js +175 -0
  163. package/dist/src/core/living-docs/ac-project-specific-generator.js.map +1 -0
  164. package/dist/src/core/living-docs/feature-archiver.d.ts +130 -0
  165. package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -0
  166. package/dist/src/core/living-docs/feature-archiver.js +549 -0
  167. package/dist/src/core/living-docs/feature-archiver.js.map +1 -0
  168. package/dist/src/core/living-docs/feature-id-manager.d.ts +81 -0
  169. package/dist/src/core/living-docs/feature-id-manager.d.ts.map +1 -0
  170. package/dist/src/core/living-docs/feature-id-manager.js +339 -0
  171. package/dist/src/core/living-docs/feature-id-manager.js.map +1 -0
  172. package/dist/src/core/living-docs/hierarchy-mapper.d.ts +144 -83
  173. package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -1
  174. package/dist/src/core/living-docs/hierarchy-mapper.js +488 -270
  175. package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -1
  176. package/dist/src/core/living-docs/index.d.ts +6 -0
  177. package/dist/src/core/living-docs/index.d.ts.map +1 -1
  178. package/dist/src/core/living-docs/index.js +6 -0
  179. package/dist/src/core/living-docs/index.js.map +1 -1
  180. package/dist/src/core/living-docs/project-detector.d.ts +6 -0
  181. package/dist/src/core/living-docs/project-detector.d.ts.map +1 -1
  182. package/dist/src/core/living-docs/project-detector.js +35 -1
  183. package/dist/src/core/living-docs/project-detector.js.map +1 -1
  184. package/dist/src/core/living-docs/spec-distributor.d.ts +100 -26
  185. package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -1
  186. package/dist/src/core/living-docs/spec-distributor.js +1275 -258
  187. package/dist/src/core/living-docs/spec-distributor.js.map +1 -1
  188. package/dist/src/core/living-docs/task-project-specific-generator.d.ts +109 -0
  189. package/dist/src/core/living-docs/task-project-specific-generator.d.ts.map +1 -0
  190. package/dist/src/core/living-docs/task-project-specific-generator.js +221 -0
  191. package/dist/src/core/living-docs/task-project-specific-generator.js.map +1 -0
  192. package/dist/src/core/living-docs/types.d.ts +143 -0
  193. package/dist/src/core/living-docs/types.d.ts.map +1 -1
  194. package/dist/src/core/project-manager.d.ts +2 -17
  195. package/dist/src/core/project-manager.d.ts.map +1 -1
  196. package/dist/src/core/project-manager.js +68 -48
  197. package/dist/src/core/project-manager.js.map +1 -1
  198. package/dist/src/core/spec-content-sync.d.ts +1 -1
  199. package/dist/src/core/spec-content-sync.d.ts.map +1 -1
  200. package/dist/src/core/sync/enhanced-content-builder.d.ts +32 -54
  201. package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -1
  202. package/dist/src/core/sync/enhanced-content-builder.js +142 -138
  203. package/dist/src/core/sync/enhanced-content-builder.js.map +1 -1
  204. package/dist/src/core/sync/performance-optimizer.d.ts +153 -0
  205. package/dist/src/core/sync/performance-optimizer.d.ts.map +1 -0
  206. package/dist/src/core/sync/performance-optimizer.js +220 -0
  207. package/dist/src/core/sync/performance-optimizer.js.map +1 -0
  208. package/dist/src/core/sync/retry-handler.d.ts +98 -0
  209. package/dist/src/core/sync/retry-handler.d.ts.map +1 -0
  210. package/dist/src/core/sync/retry-handler.js +196 -0
  211. package/dist/src/core/sync/retry-handler.js.map +1 -0
  212. package/dist/src/core/sync/spec-content-sync.d.ts +88 -0
  213. package/dist/src/core/sync/spec-content-sync.d.ts.map +1 -0
  214. package/dist/src/core/sync/spec-content-sync.js +5 -0
  215. package/dist/src/core/sync/spec-content-sync.js.map +1 -0
  216. package/dist/src/core/sync/types.d.ts +52 -0
  217. package/dist/src/core/sync/types.d.ts.map +1 -0
  218. package/dist/src/core/sync/types.js +5 -0
  219. package/dist/src/core/sync/types.js.map +1 -0
  220. package/dist/src/core/types/config.d.ts +125 -0
  221. package/dist/src/core/types/config.d.ts.map +1 -1
  222. package/dist/src/core/types/config.js +25 -0
  223. package/dist/src/core/types/config.js.map +1 -1
  224. package/dist/src/core/types/increment-metadata.d.ts +10 -0
  225. package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
  226. package/dist/src/core/types/increment-metadata.js +10 -1
  227. package/dist/src/core/types/increment-metadata.js.map +1 -1
  228. package/dist/src/integrations/jira/jira-incremental-mapper.d.ts.map +1 -1
  229. package/dist/src/integrations/jira/jira-incremental-mapper.js +4 -8
  230. package/dist/src/integrations/jira/jira-incremental-mapper.js.map +1 -1
  231. package/dist/src/integrations/jira/jira-mapper.d.ts.map +1 -1
  232. package/dist/src/integrations/jira/jira-mapper.js +4 -8
  233. package/dist/src/integrations/jira/jira-mapper.js.map +1 -1
  234. package/dist/tsconfig.tsbuildinfo +1 -0
  235. package/package.json +1 -1
  236. package/plugins/specweave/COMMANDS.md +13 -4
  237. package/plugins/specweave/agents/pm/AGENT.md +159 -12
  238. package/plugins/specweave/commands/specweave-abandon.md +22 -20
  239. package/plugins/specweave/commands/specweave-archive-features.md +121 -0
  240. package/plugins/specweave/commands/specweave-archive-increments.md +82 -0
  241. package/plugins/specweave/commands/specweave-archive.md +363 -0
  242. package/plugins/specweave/commands/specweave-backlog.md +211 -0
  243. package/plugins/specweave/commands/specweave-fix-duplicates.md +517 -0
  244. package/plugins/specweave/commands/specweave-increment.md +4 -3
  245. package/plugins/specweave/commands/specweave-progress.md +176 -27
  246. package/plugins/specweave/commands/specweave-reopen.md +391 -0
  247. package/plugins/specweave/commands/specweave-restore-feature.md +90 -0
  248. package/plugins/specweave/commands/specweave-restore.md +309 -0
  249. package/plugins/specweave/commands/specweave-resume.md +51 -23
  250. package/plugins/specweave/commands/specweave-status.md +41 -7
  251. package/plugins/specweave/commands/specweave-sync-specs.md +425 -0
  252. package/plugins/specweave/commands/specweave.md +70 -405
  253. package/plugins/specweave/hooks/hooks.json +4 -0
  254. package/plugins/specweave/hooks/lib/sync-spec-content.sh +2 -2
  255. package/plugins/specweave/hooks/post-increment-planning.sh +26 -2
  256. package/plugins/specweave/hooks/post-task-completion.sh +39 -0
  257. package/plugins/specweave/hooks/pre-command-deduplication.sh +83 -0
  258. package/plugins/specweave/hooks/user-prompt-submit.sh +1 -1
  259. package/plugins/specweave/lib/hooks/sync-living-docs.js +2 -0
  260. package/plugins/specweave/lib/hooks/sync-living-docs.ts +4 -0
  261. package/plugins/specweave/lib/hooks/update-ac-status.js +102 -0
  262. package/plugins/specweave/lib/hooks/update-ac-status.ts +192 -0
  263. package/plugins/specweave/skills/archive-increments/SKILL.md +198 -0
  264. package/plugins/specweave/skills/increment-planner/scripts/feature-utils.js +14 -0
  265. package/plugins/specweave/skills/smart-reopen-detector/SKILL.md +244 -0
  266. package/plugins/specweave-ado/lib/ado-spec-content-sync.js +49 -5
  267. package/plugins/specweave-ado/lib/ado-spec-content-sync.ts +72 -6
  268. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  269. package/plugins/specweave-confluent/.claude-plugin/plugin.json +23 -0
  270. package/plugins/specweave-confluent/README.md +375 -0
  271. package/plugins/specweave-confluent/agents/confluent-architect/AGENT.md +306 -0
  272. package/plugins/specweave-confluent/skills/confluent-kafka-connect/SKILL.md +453 -0
  273. package/plugins/specweave-confluent/skills/confluent-ksqldb/SKILL.md +470 -0
  274. package/plugins/specweave-confluent/skills/confluent-schema-registry/SKILL.md +316 -0
  275. package/plugins/specweave-github/agents/github-task-splitter/AGENT.md +2 -2
  276. package/plugins/specweave-github/agents/user-story-updater/AGENT.md +148 -0
  277. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +1 -1
  278. package/plugins/specweave-github/commands/specweave-github-update-user-story.md +156 -0
  279. package/plugins/specweave-github/hooks/post-task-completion.sh +42 -9
  280. package/plugins/specweave-github/lib/completion-calculator.js +262 -0
  281. package/plugins/specweave-github/lib/completion-calculator.ts +434 -0
  282. package/plugins/specweave-github/lib/duplicate-detector.js +3 -3
  283. package/plugins/specweave-github/lib/duplicate-detector.ts +4 -4
  284. package/plugins/specweave-github/lib/epic-content-builder.js +265 -0
  285. package/plugins/specweave-github/lib/epic-content-builder.ts +376 -0
  286. package/plugins/specweave-github/lib/github-client-v2.js +49 -0
  287. package/plugins/specweave-github/lib/github-client-v2.ts +59 -0
  288. package/plugins/specweave-github/lib/github-epic-sync.js +23 -24
  289. package/plugins/specweave-github/lib/github-epic-sync.ts +30 -5
  290. package/plugins/specweave-github/lib/github-feature-sync.js +381 -0
  291. package/plugins/specweave-github/lib/github-feature-sync.ts +568 -0
  292. package/plugins/specweave-github/lib/github-spec-content-sync.js +40 -10
  293. package/plugins/specweave-github/lib/github-spec-content-sync.ts +82 -14
  294. package/plugins/specweave-github/lib/progress-comment-builder.js +229 -0
  295. package/plugins/specweave-github/lib/progress-comment-builder.ts +324 -0
  296. package/plugins/specweave-github/lib/user-story-content-builder.js +299 -0
  297. package/plugins/specweave-github/lib/user-story-content-builder.ts +413 -0
  298. package/plugins/specweave-github/lib/user-story-issue-builder.js +344 -0
  299. package/plugins/specweave-github/lib/user-story-issue-builder.ts +543 -0
  300. package/plugins/specweave-github/skills/github-issue-standard/SKILL.md +189 -0
  301. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +134 -0
  302. package/plugins/specweave-jira/lib/{enhanced-jira-sync.ts.disabled → enhanced-jira-sync.ts} +26 -52
  303. package/plugins/specweave-kafka/.claude-plugin/plugin.json +26 -0
  304. package/plugins/specweave-kafka/IMPLEMENTATION-COMPLETE.md +483 -0
  305. package/plugins/specweave-kafka/README.md +242 -0
  306. package/plugins/specweave-kafka/agents/kafka-architect/AGENT.md +235 -0
  307. package/plugins/specweave-kafka/agents/kafka-devops/AGENT.md +209 -0
  308. package/plugins/specweave-kafka/agents/kafka-observability/AGENT.md +266 -0
  309. package/plugins/specweave-kafka/commands/deploy.md +99 -0
  310. package/plugins/specweave-kafka/commands/dev-env.md +176 -0
  311. package/plugins/specweave-kafka/commands/mcp-configure.md +101 -0
  312. package/plugins/specweave-kafka/commands/monitor-setup.md +96 -0
  313. package/plugins/specweave-kafka/docker/kafka-local/docker-compose.yml +187 -0
  314. package/plugins/specweave-kafka/docker/redpanda/docker-compose.yml +199 -0
  315. package/plugins/specweave-kafka/docker/templates/consumer-nodejs.js +225 -0
  316. package/plugins/specweave-kafka/docker/templates/consumer-python.py +220 -0
  317. package/plugins/specweave-kafka/docker/templates/producer-nodejs.js +168 -0
  318. package/plugins/specweave-kafka/docker/templates/producer-python.py +167 -0
  319. package/plugins/specweave-kafka/lib/adapters/apache-kafka-adapter.js +438 -0
  320. package/plugins/specweave-kafka/lib/adapters/apache-kafka-adapter.ts +541 -0
  321. package/plugins/specweave-kafka/lib/adapters/platform-adapter.js +47 -0
  322. package/plugins/specweave-kafka/lib/adapters/platform-adapter.ts +343 -0
  323. package/plugins/specweave-kafka/lib/cli/kcat-wrapper.js +258 -0
  324. package/plugins/specweave-kafka/lib/cli/kcat-wrapper.ts +298 -0
  325. package/plugins/specweave-kafka/lib/cli/types.js +10 -0
  326. package/plugins/specweave-kafka/lib/cli/types.ts +92 -0
  327. package/plugins/specweave-kafka/lib/connectors/connector-catalog.js +305 -0
  328. package/plugins/specweave-kafka/lib/connectors/connector-catalog.ts +528 -0
  329. package/plugins/specweave-kafka/lib/documentation/diagram-generator.js +114 -0
  330. package/plugins/specweave-kafka/lib/documentation/diagram-generator.ts +195 -0
  331. package/plugins/specweave-kafka/lib/documentation/exporter.js +210 -0
  332. package/plugins/specweave-kafka/lib/documentation/exporter.ts +338 -0
  333. package/plugins/specweave-kafka/lib/documentation/schema-catalog-generator.js +60 -0
  334. package/plugins/specweave-kafka/lib/documentation/schema-catalog-generator.ts +130 -0
  335. package/plugins/specweave-kafka/lib/documentation/topology-generator.js +143 -0
  336. package/plugins/specweave-kafka/lib/documentation/topology-generator.ts +290 -0
  337. package/plugins/specweave-kafka/lib/mcp/detector.js +298 -0
  338. package/plugins/specweave-kafka/lib/mcp/detector.ts +352 -0
  339. package/plugins/specweave-kafka/lib/mcp/types.js +21 -0
  340. package/plugins/specweave-kafka/lib/mcp/types.ts +77 -0
  341. package/plugins/specweave-kafka/lib/multi-cluster/cluster-config-manager.js +193 -0
  342. package/plugins/specweave-kafka/lib/multi-cluster/cluster-config-manager.ts +362 -0
  343. package/plugins/specweave-kafka/lib/multi-cluster/cluster-switcher.js +188 -0
  344. package/plugins/specweave-kafka/lib/multi-cluster/cluster-switcher.ts +359 -0
  345. package/plugins/specweave-kafka/lib/multi-cluster/health-aggregator.js +195 -0
  346. package/plugins/specweave-kafka/lib/multi-cluster/health-aggregator.ts +380 -0
  347. package/plugins/specweave-kafka/lib/observability/opentelemetry-kafka.js +209 -0
  348. package/plugins/specweave-kafka/lib/observability/opentelemetry-kafka.ts +358 -0
  349. package/plugins/specweave-kafka/lib/patterns/advanced-ksqldb-patterns.js +354 -0
  350. package/plugins/specweave-kafka/lib/patterns/advanced-ksqldb-patterns.ts +563 -0
  351. package/plugins/specweave-kafka/lib/patterns/circuit-breaker-resilience.js +259 -0
  352. package/plugins/specweave-kafka/lib/patterns/circuit-breaker-resilience.ts +516 -0
  353. package/plugins/specweave-kafka/lib/patterns/dead-letter-queue.js +233 -0
  354. package/plugins/specweave-kafka/lib/patterns/dead-letter-queue.ts +423 -0
  355. package/plugins/specweave-kafka/lib/patterns/exactly-once-semantics.js +266 -0
  356. package/plugins/specweave-kafka/lib/patterns/exactly-once-semantics.ts +445 -0
  357. package/plugins/specweave-kafka/lib/patterns/flink-kafka-integration.js +312 -0
  358. package/plugins/specweave-kafka/lib/patterns/flink-kafka-integration.ts +561 -0
  359. package/plugins/specweave-kafka/lib/patterns/multi-dc-replication.js +289 -0
  360. package/plugins/specweave-kafka/lib/patterns/multi-dc-replication.ts +607 -0
  361. package/plugins/specweave-kafka/lib/patterns/rate-limiting-backpressure.js +264 -0
  362. package/plugins/specweave-kafka/lib/patterns/rate-limiting-backpressure.ts +498 -0
  363. package/plugins/specweave-kafka/lib/patterns/stream-processing-optimization.js +263 -0
  364. package/plugins/specweave-kafka/lib/patterns/stream-processing-optimization.ts +549 -0
  365. package/plugins/specweave-kafka/lib/patterns/tiered-storage-compaction.js +205 -0
  366. package/plugins/specweave-kafka/lib/patterns/tiered-storage-compaction.ts +399 -0
  367. package/plugins/specweave-kafka/lib/performance/performance-optimizer.js +249 -0
  368. package/plugins/specweave-kafka/lib/performance/performance-optimizer.ts +427 -0
  369. package/plugins/specweave-kafka/lib/security/kafka-security.js +252 -0
  370. package/plugins/specweave-kafka/lib/security/kafka-security.ts +494 -0
  371. package/plugins/specweave-kafka/lib/utils/capacity-planner.js +203 -0
  372. package/plugins/specweave-kafka/lib/utils/capacity-planner.ts +469 -0
  373. package/plugins/specweave-kafka/lib/utils/config-validator.js +419 -0
  374. package/plugins/specweave-kafka/lib/utils/config-validator.ts +564 -0
  375. package/plugins/specweave-kafka/lib/utils/partitioning.js +329 -0
  376. package/plugins/specweave-kafka/lib/utils/partitioning.ts +473 -0
  377. package/plugins/specweave-kafka/lib/utils/sizing.js +221 -0
  378. package/plugins/specweave-kafka/lib/utils/sizing.ts +374 -0
  379. package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-broker-metrics.json +628 -0
  380. package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-cluster-overview.json +564 -0
  381. package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-consumer-lag.json +509 -0
  382. package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-jvm-metrics.json +674 -0
  383. package/plugins/specweave-kafka/monitoring/grafana/dashboards/kafka-topic-metrics.json +578 -0
  384. package/plugins/specweave-kafka/monitoring/grafana/provisioning/dashboards/kafka.yml +17 -0
  385. package/plugins/specweave-kafka/monitoring/grafana/provisioning/datasources/prometheus.yml +17 -0
  386. package/plugins/specweave-kafka/monitoring/prometheus/kafka-alerts.yml +415 -0
  387. package/plugins/specweave-kafka/monitoring/prometheus/kafka-jmx-exporter.yml +256 -0
  388. package/plugins/specweave-kafka/package.json +41 -0
  389. package/plugins/specweave-kafka/skills/kafka-architecture/SKILL.md +647 -0
  390. package/plugins/specweave-kafka/skills/kafka-cli-tools/SKILL.md +433 -0
  391. package/plugins/specweave-kafka/skills/kafka-iac-deployment/SKILL.md +449 -0
  392. package/plugins/specweave-kafka/skills/kafka-kubernetes/SKILL.md +667 -0
  393. package/plugins/specweave-kafka/skills/kafka-mcp-integration/SKILL.md +273 -0
  394. package/plugins/specweave-kafka/skills/kafka-observability/SKILL.md +576 -0
  395. package/plugins/specweave-kafka/templates/config/broker-production.properties +254 -0
  396. package/plugins/specweave-kafka/templates/config/consumer-low-latency.properties +112 -0
  397. package/plugins/specweave-kafka/templates/config/producer-high-throughput.properties +120 -0
  398. package/plugins/specweave-kafka/templates/migration/mirrormaker2-config.properties +234 -0
  399. package/plugins/specweave-kafka/templates/monitoring/grafana/multi-cluster-dashboard.json +686 -0
  400. package/plugins/specweave-kafka/terraform/apache-kafka/main.tf +347 -0
  401. package/plugins/specweave-kafka/terraform/apache-kafka/outputs.tf +107 -0
  402. package/plugins/specweave-kafka/terraform/apache-kafka/templates/kafka-broker-init.sh.tpl +216 -0
  403. package/plugins/specweave-kafka/terraform/apache-kafka/variables.tf +156 -0
  404. package/plugins/specweave-kafka/terraform/aws-msk/main.tf +362 -0
  405. package/plugins/specweave-kafka/terraform/aws-msk/outputs.tf +93 -0
  406. package/plugins/specweave-kafka/terraform/aws-msk/templates/server.properties.tpl +32 -0
  407. package/plugins/specweave-kafka/terraform/aws-msk/variables.tf +235 -0
  408. package/plugins/specweave-kafka/terraform/azure-event-hubs/main.tf +281 -0
  409. package/plugins/specweave-kafka/terraform/azure-event-hubs/outputs.tf +118 -0
  410. package/plugins/specweave-kafka/terraform/azure-event-hubs/variables.tf +148 -0
  411. package/plugins/specweave-kafka/tsconfig.json +21 -0
  412. package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +23 -0
  413. package/plugins/specweave-kafka-streams/README.md +310 -0
  414. package/plugins/specweave-kafka-streams/skills/kafka-streams-topology/SKILL.md +539 -0
  415. package/plugins/specweave-n8n/.claude-plugin/plugin.json +22 -0
  416. package/plugins/specweave-n8n/README.md +354 -0
  417. package/plugins/specweave-n8n/skills/n8n-kafka-workflows/SKILL.md +504 -0
  418. package/plugins/specweave-release/commands/specweave-release-platform.md +1 -1
  419. package/plugins/specweave-release/hooks/post-task-completion.sh +2 -2
  420. package/src/templates/AGENTS.md.template +601 -7
  421. package/src/templates/CLAUDE.md.template +188 -88
  422. package/plugins/specweave-ado/commands/specweave-ado-sync-spec.md +0 -255
  423. package/plugins/specweave-github/commands/specweave-github-sync-epic.md +0 -248
  424. package/plugins/specweave-github/commands/specweave-github-sync-from.md +0 -147
  425. package/plugins/specweave-github/commands/specweave-github-sync-spec.md +0 -208
  426. package/plugins/specweave-github/commands/specweave-github-sync-tasks.md +0 -530
  427. package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +0 -267
  428. package/plugins/specweave-jira/commands/specweave-jira-sync-spec.md +0 -240
@@ -13,6 +13,8 @@ import fs from 'fs-extra';
13
13
  import path from 'path';
14
14
  import { HierarchyMapper } from './hierarchy-mapper.js';
15
15
  import { detectPrimaryGitHubRemote } from '../../utils/git-detector.js';
16
+ import { ACProjectSpecificGenerator } from './ac-project-specific-generator.js';
17
+ import { TaskProjectSpecificGenerator } from './task-project-specific-generator.js';
16
18
  /**
17
19
  * SpecDistributor - Distributes increment specs into hierarchical living docs
18
20
  */
@@ -36,15 +38,15 @@ export class SpecDistributor {
36
38
  createBackups: true,
37
39
  ...config,
38
40
  };
39
- // Initialize HierarchyMapper with same project config
40
- this.hierarchyMapper = new HierarchyMapper(projectRoot, {
41
- projectId: config?.specsDir?.includes('/specs/')
42
- ? config.specsDir.split('/specs/')[1]?.split('/')[0] || 'default'
43
- : 'default'
44
- });
41
+ // Initialize HierarchyMapper
42
+ this.hierarchyMapper = new HierarchyMapper(projectRoot);
43
+ // Initialize AC Generator
44
+ this.acGenerator = new ACProjectSpecificGenerator();
45
+ // ✅ NEW: Initialize Task Generator
46
+ this.taskGenerator = new TaskProjectSpecificGenerator(projectRoot);
45
47
  }
46
48
  /**
47
- * Distribute increment spec into epic + user story files
49
+ * Distribute increment spec into universal hierarchy (epic + feature + user stories)
48
50
  */
49
51
  async distribute(incrementId) {
50
52
  const errors = [];
@@ -54,46 +56,80 @@ export class SpecDistributor {
54
56
  if (!this.githubRemote) {
55
57
  this.githubRemote = await detectPrimaryGitHubRemote(this.projectRoot);
56
58
  }
57
- // Step 0a: Parse increment spec to detect project ID
59
+ // Step 1: Parse increment spec (with epic and project detection)
58
60
  const parsed = await this.parseIncrementSpec(incrementId);
59
- // Step 0b: Update config if project ID is specified in frontmatter
60
- let projectId = this.config.specsDir.split('/specs/')[1]?.split('/')[0] || 'default';
61
- if (parsed.project) {
62
- projectId = parsed.project;
63
- // Update config paths for correct project
64
- this.config.specsDir = path.join(this.projectRoot, '.specweave', 'docs', 'internal', 'specs', projectId);
65
- // Recreate hierarchy mapper with correct project ID
66
- this.hierarchyMapper = new HierarchyMapper(this.projectRoot, {
67
- projectId,
68
- specsBaseDir: this.config.specsDir,
69
- });
61
+ // Step 1.5: Filter abandoned/archived increments (CRITICAL: prevent pollution)
62
+ if (parsed.status === 'abandoned' || parsed.status === 'archived') {
63
+ console.log(` ⚠️ Skipping distribution for ${parsed.status} increment: ${incrementId}`);
64
+ console.log(` 💡 Living docs are NOT updated for ${parsed.status} increments`);
65
+ return {
66
+ epic: {},
67
+ userStories: [],
68
+ incrementId,
69
+ specId: `${parsed.status.toUpperCase()}-${incrementId}`,
70
+ totalStories: 0,
71
+ totalFiles: 0,
72
+ epicPath: '',
73
+ userStoryPaths: [],
74
+ success: false,
75
+ errors: [`Increment ${incrementId} is ${parsed.status} - skipping living docs distribution`],
76
+ warnings: [`To preserve history, archived content remains in the increment folder`],
77
+ };
78
+ }
79
+ // Step 2: Check if we should create epics (skip for GitHub)
80
+ const config = await this.hierarchyMapper.getSpecweaveConfig();
81
+ const syncProvider = config.sync?.provider || config.sync?.activeProfile ?
82
+ config.sync.profiles?.[config.sync.activeProfile]?.provider : undefined;
83
+ const shouldCreateEpic = syncProvider !== 'github'; // GitHub doesn't have epics concept
84
+ // Step 3: Detect epic mapping (OPTIONAL - skip for GitHub)
85
+ let epicMapping = null;
86
+ if (shouldCreateEpic) {
87
+ epicMapping = await this.hierarchyMapper.detectEpicMapping(incrementId);
88
+ if (epicMapping) {
89
+ console.log(` 🎯 Mapped to epic ${epicMapping.epicId} (confidence: ${epicMapping.confidence}%)`);
90
+ }
91
+ }
92
+ else {
93
+ console.log(` ⚡ Skipping epic creation (GitHub integration)`);
70
94
  }
71
- // Step 0c: Detect feature folder using HierarchyMapper (NEW: feature-based naming)
95
+ // Step 4: Detect feature mapping (REQUIRED)
72
96
  console.log(` 🔍 Detecting feature folder for ${incrementId}...`);
73
- const epicMapping = await this.hierarchyMapper.detectFeatureMapping(incrementId);
74
- console.log(` 📁 Mapped to ${epicMapping.featureFolder} (confidence: ${epicMapping.confidence}%, method: ${epicMapping.detectionMethod})`);
75
- // Ensure specs base directory exists (for multi-project support)
76
- await fs.ensureDir(this.config.specsDir);
77
- // Step 2: Classify content (pass epicMapping to use feature folder as ID)
78
- const classified = await this.classifyContent(parsed, epicMapping);
79
- // Step 3: Generate epic file
80
- const epic = await this.generateEpicFile(classified, incrementId);
81
- // Step 4: Generate user story files
82
- const userStories = await this.generateUserStoryFiles(classified, incrementId);
83
- // Step 5: Write files (using epicMapping paths)
84
- const epicPath = await this.writeEpicFile(epic, epicMapping);
85
- const userStoryPaths = await this.writeUserStoryFiles(userStories, epicMapping);
86
- // Step 6: Update tasks.md with bidirectional links to user stories (CRITICAL!)
87
- await this.updateTasksWithUserStoryLinks(incrementId, userStories, epicMapping);
97
+ const featureMapping = await this.hierarchyMapper.detectFeatureMapping(incrementId);
98
+ console.log(` 📁 Mapped to feature ${featureMapping.featureId} (confidence: ${featureMapping.confidence}%, method: ${featureMapping.detectionMethod})`);
99
+ console.log(` 📦 Projects: ${featureMapping.projects.join(', ')}`);
100
+ // Step 5: Classify content by project (NEW)
101
+ const storiesByProject = await this.classifyContentByProject(parsed, featureMapping);
102
+ console.log(` 📊 Classified ${parsed.userStories.length} user stories across ${storiesByProject.size} project(s)`);
103
+ // Step 6: Generate epic file (OPTIONAL - only if not GitHub)
104
+ const epicFile = epicMapping && shouldCreateEpic ? await this.generateEpicFile(parsed, epicMapping, featureMapping) : null;
105
+ // Step 7: Generate feature file (REQUIRED)
106
+ const featureFile = await this.generateFeatureFile(parsed, featureMapping, storiesByProject, incrementId);
107
+ // Step 8: Generate project context files (REQUIRED)
108
+ const projectContextFiles = await this.generateProjectContextFiles(featureMapping, parsed);
109
+ // Step 9: Generate user story files by project (REQUIRED)
110
+ const userStoryFilesByProject = await this.generateUserStoryFilesByProject(storiesByProject, featureMapping, incrementId);
111
+ // Step 10: Write epic file (if exists and not GitHub)
112
+ const epicPath = epicFile && epicMapping && shouldCreateEpic ? await this.writeEpicFile(epicFile, epicMapping) : null;
113
+ // Step 11: Write feature file
114
+ const featurePath = await this.writeFeatureFile(featureFile, featureMapping);
115
+ // Step 12: Write project context files
116
+ const contextPaths = await this.writeProjectContextFiles(projectContextFiles, featureMapping);
117
+ // Step 13: Write user story files by project
118
+ const storyPathsByProject = await this.writeUserStoryFilesByProject(userStoryFilesByProject, featureMapping, incrementId);
119
+ // Step 14: Update tasks.md with bidirectional links (project-aware)
120
+ await this.updateTasksWithUserStoryLinks(incrementId, userStoryFilesByProject, featureMapping);
121
+ // Prepare legacy result (for backward compatibility)
122
+ const allUserStories = Array.from(userStoryFilesByProject.values()).flat();
123
+ const allStoryPaths = Array.from(storyPathsByProject.values()).flat();
88
124
  return {
89
- epic,
90
- userStories,
125
+ epic: featureFile, // Type compatibility hack
126
+ userStories: allUserStories,
91
127
  incrementId,
92
- specId: epic.id,
93
- totalStories: userStories.length,
94
- totalFiles: 1 + userStories.length,
95
- epicPath,
96
- userStoryPaths,
128
+ specId: featureFile.id,
129
+ totalStories: allUserStories.length,
130
+ totalFiles: 1 + (epicFile ? 1 : 0) + contextPaths.length + allStoryPaths.length,
131
+ epicPath: featurePath, // Feature is the new "epic"
132
+ userStoryPaths: allStoryPaths,
97
133
  success: true,
98
134
  errors,
99
135
  warnings,
@@ -164,18 +200,34 @@ export class SpecDistributor {
164
200
  // Extract overview (try multiple sections)
165
201
  let overview = '';
166
202
  // Try "Quick Overview" or "Executive Summary"
167
- let overviewMatch = bodyContent.match(/##\s+(?:Quick\s+)?(?:Overview|Executive\s+Summary)\s*\n+([\s\S]*?)(?=\n##|\n---|\Z)/i);
168
- if (overviewMatch)
203
+ let overviewMatch = bodyContent.match(/##\s+(?:Quick\s+)?(?:Overview|Executive\s+Summary)\s*\n+([\s\S]*?)(?=\n\*\*Business Value\*\*:|\n##|\n---|$)/im);
204
+ if (overviewMatch) {
169
205
  overview = overviewMatch[1].trim();
206
+ // Remove any trailing fragments or incomplete sentences
207
+ if (overview && !overview.endsWith('.') && !overview.endsWith('!') && !overview.endsWith('?')) {
208
+ // Try to complete the sentence by looking ahead
209
+ const extendedMatch = bodyContent.match(/##\s+(?:Quick\s+)?(?:Overview|Executive\s+Summary)\s*\n+([\s\S]{0,1000}?)(?:\n\n|\n\*\*|\n##|\n---|$)/im);
210
+ if (extendedMatch) {
211
+ const extended = extendedMatch[1].trim();
212
+ // Find the last complete sentence
213
+ const sentences = extended.match(/[^.!?]+[.!?]/g);
214
+ if (sentences) {
215
+ overview = sentences.join('').trim();
216
+ }
217
+ }
218
+ }
219
+ }
170
220
  if (!overview) {
171
221
  // Try "Overview" section
172
- overviewMatch = bodyContent.match(/##\s+Overview\s*\n+([\s\S]*?)(?=\n##|\n---|\Z)/i);
173
- if (overviewMatch)
174
- overview = overviewMatch[1].trim();
222
+ overviewMatch = bodyContent.match(/##\s+Overview\s*\n+([\s\S]*?)(?=\n##|\n---|$)/im);
223
+ if (overviewMatch) {
224
+ const parts = overviewMatch[1].trim().split(/\n\n|\n---/);
225
+ overview = parts[0].trim();
226
+ }
175
227
  }
176
228
  if (!overview) {
177
229
  // Try "Problem Statement" section
178
- const problemMatch = bodyContent.match(/##\s+Problem\s+Statement\s*\n+([\s\S]*?)(?=\n##|\n---|\Z)/i);
230
+ const problemMatch = bodyContent.match(/##\s+Problem\s+Statement\s*\n+([\s\S]*?)(?=\n##|\n---|$)/im);
179
231
  if (problemMatch) {
180
232
  // Take first paragraph only
181
233
  const firstPara = problemMatch[1].trim().split('\n\n')[0];
@@ -202,14 +254,23 @@ export class SpecDistributor {
202
254
  }
203
255
  // Extract user stories
204
256
  const userStories = await this.extractUserStories(content, incrementId);
257
+ // Extract priority, status, and created date from frontmatter
258
+ const priority = frontmatter.priority || 'P1';
259
+ const status = frontmatter.status || 'planning';
260
+ const created = frontmatter.created || new Date().toISOString();
205
261
  return {
206
262
  incrementId,
207
263
  title,
208
264
  overview,
209
265
  businessValue,
266
+ epic: frontmatter.epic, // Epic ID from frontmatter (optional)
210
267
  project: frontmatter.project, // Project ID from frontmatter (if present)
268
+ projects: frontmatter.projects || [], // Multiple projects (for cross-project features)
269
+ priority,
270
+ status,
271
+ created,
211
272
  userStories,
212
- externalLinks, // NEW: External links from metadata.json
273
+ externalLinks, // External links from metadata.json
213
274
  };
214
275
  }
215
276
  /**
@@ -241,30 +302,98 @@ export class SpecDistributor {
241
302
  }
242
303
  return links;
243
304
  }
305
+ /**
306
+ * Detect external tool mapping from metadata.json
307
+ *
308
+ * Maps SpecWeave hierarchy to external tool hierarchy with clear indicators.
309
+ * Examples:
310
+ * - SpecWeave Feature (FS-031) → JIRA Epic (AUTH-100)
311
+ * - SpecWeave Feature (FS-031) → GitHub Issue (#45)
312
+ * - SpecWeave Feature (FS-031) → ADO Feature (12345)
313
+ */
314
+ async detectExternalToolMapping(incrementId) {
315
+ const metadataPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'metadata.json');
316
+ if (!fs.existsSync(metadataPath)) {
317
+ return undefined;
318
+ }
319
+ try {
320
+ const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
321
+ // Priority order: JIRA > ADO > GitHub
322
+ // (JIRA has most divergence, GitHub has least)
323
+ // JIRA mapping (Epic → Feature divergence)
324
+ if (metadata.jira?.epicKey) {
325
+ return {
326
+ provider: 'jira',
327
+ externalType: 'epic',
328
+ externalId: metadata.jira.epicKey,
329
+ externalUrl: metadata.jira.url || `https://jira.atlassian.com/browse/${metadata.jira.epicKey}`,
330
+ hierarchyLevel: 'feature',
331
+ mappingNote: 'JIRA Epic maps to SpecWeave Feature',
332
+ };
333
+ }
334
+ // Azure DevOps mapping (Feature → Feature same level)
335
+ if (metadata.ado?.workItemId) {
336
+ return {
337
+ provider: 'ado',
338
+ externalType: 'feature',
339
+ externalId: String(metadata.ado.workItemId),
340
+ externalUrl: metadata.ado.workItemUrl,
341
+ hierarchyLevel: 'feature',
342
+ mappingNote: 'ADO Feature maps to SpecWeave Feature',
343
+ };
344
+ }
345
+ // GitHub mapping - Features should NOT map to issues
346
+ // According to Universal Hierarchy:
347
+ // - SpecWeave Feature (FS-*) → GitHub Milestone
348
+ // - SpecWeave User Story (US-*) → GitHub Issue
349
+ // We should NOT show GitHub issue mapping at feature level
350
+ // This will be handled at user story level instead
351
+ }
352
+ catch (error) {
353
+ console.warn(` ⚠️ Failed to parse metadata.json for external mapping: ${error}`);
354
+ }
355
+ return undefined;
356
+ }
244
357
  /**
245
358
  * Extract user stories from increment spec
246
359
  */
247
360
  async extractUserStories(content, incrementId) {
248
361
  const userStories = [];
249
- // Find all user story sections (supports both ### and #### patterns, with or without blank line)
250
- const userStoryPattern = /^###+\s+(US-\d+):\s+(.+?)\s*\n([\s\S]*?)(?=^###+\s+US-|\n---\n|$)/gm;
251
- let match;
252
- while ((match = userStoryPattern.exec(content)) !== null) {
253
- const id = match[1]; // US-001
254
- const title = match[2];
255
- const storyContent = match[3];
362
+ // Split content into individual user stories
363
+ // Each story runs from "### US-XXX:" or "#### US-XXX:" until the next user story or end of content
364
+ const storyParts = content.split(/(?=^#{3,4}\s+US-\d+:)/m);
365
+ for (const part of storyParts) {
366
+ // Skip empty parts or parts that don't start with US-
367
+ if (!part.trim() || !part.match(/^#{3,4}\s+US-\d+:/))
368
+ continue;
369
+ // Extract the user story ID and title from the first line (supports both ### and ####)
370
+ const headerMatch = part.match(/^#{3,4}\s+(US-\d+):\s+(.+?)$/m);
371
+ if (!headerMatch)
372
+ continue;
373
+ const id = headerMatch[1];
374
+ const title = headerMatch[2];
375
+ // Get everything after the header line, removing trailing ---
376
+ const storyContent = part
377
+ .substring(part.indexOf('\n') + 1)
378
+ .replace(/\n---\s*$/, '')
379
+ .trim();
256
380
  // Extract description (As a... I want... So that...) - supports both inline and separate line formats
257
- const descMatch = storyContent.match(/\*\*As a\*\*\s+(.*?)\s*\n\*\*I want\*\*\s+(.*?)\s*\n\*\*So that\*\*\s+(.*?)(?=\n\n|\*\*Acceptance)/is);
381
+ const descMatch = storyContent.match(/\*\*As a\*\*\s+(.*?)\n\*\*I want\*\*\s+(.*?)\n\*\*So that\*\*\s+(.*?)(?:\n|$)/is);
258
382
  const description = descMatch
259
383
  ? `**As a** ${descMatch[1].trim()}\n**I want** ${descMatch[2].trim()}\n**So that** ${descMatch[3].trim()}`
260
384
  : '';
261
- // Extract acceptance criteria
262
- const acceptanceCriteria = this.extractAcceptanceCriteria(storyContent);
385
+ // Extract acceptance criteria from the Acceptance Criteria section
386
+ // Try full content first (works for #### headings), then story content (works for ### headings)
387
+ let acceptanceCriteria = this.extractAcceptanceCriteriaFromSection(content, id);
388
+ if (!acceptanceCriteria || acceptanceCriteria.length === 0) {
389
+ acceptanceCriteria = this.extractAcceptanceCriteria(storyContent);
390
+ }
263
391
  // Extract business rationale
264
392
  const rationaleMatch = storyContent.match(/\*\*Business Rationale\*\*:\s+(.*?)(?=\n\n---|\n\n##|$)/is);
265
393
  const businessRationale = rationaleMatch ? rationaleMatch[1].trim() : undefined;
266
- // Extract phase
267
- const phaseMatch = content.substring(0, match.index).match(/###\s+(Phase\s+\d+:.*?)$/im);
394
+ // Extract phase (look for phase header before this story in the original content)
395
+ const storyIndex = content.indexOf(part);
396
+ const phaseMatch = content.substring(0, storyIndex).match(/###\s+(Phase\s+\d+:.*?)$/im);
268
397
  const phase = phaseMatch ? phaseMatch[1] : undefined;
269
398
  // Determine status (assume complete if in completed increment)
270
399
  const status = 'complete'; // Can be enhanced later
@@ -286,87 +415,145 @@ export class SpecDistributor {
286
415
  */
287
416
  extractAcceptanceCriteria(content) {
288
417
  const criteria = [];
289
- // Pattern: - [x] **AC-US1-01**: Description (P1, testable)
290
- const acPattern = /^[-*]\s+\[([ x])\]\s+\*\*(.+?)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm;
291
- let match;
292
- while ((match = acPattern.exec(content)) !== null) {
293
- const completed = match[1] === 'x';
294
- const id = match[2]; // AC-US1-01
295
- const description = match[3];
296
- const metaString = match[4] || ''; // "P1, testable"
297
- const priority = metaString.match(/P\d/)?.[0];
298
- const testable = metaString.includes('testable');
299
- criteria.push({
300
- id,
301
- description,
302
- priority,
303
- testable,
304
- completed,
305
- });
418
+ // Pattern 1: - [x] **AC-US1-01**: Description (P1, testable)
419
+ // Pattern 2: - [ ] **AC-001**: Description (P0, testable)
420
+ // Pattern 3: - AC-001: Description (P0, testable) [without checkbox]
421
+ // Also handles both ** bold ** and plain text formats
422
+ const acPatterns = [
423
+ // With checkbox and bold
424
+ /^[-*]\s+\[([ x])\]\s+\*\*(AC-[^:]+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
425
+ // Without checkbox but with bold
426
+ /^[-*]\s+\*\*(AC-[^:]+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
427
+ // Without checkbox and without bold (common in specs)
428
+ /^[-*]\s+(AC-\d+):\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
429
+ ];
430
+ // Try each pattern
431
+ for (const pattern of acPatterns) {
432
+ const contentCopy = content; // Work with a copy for each pattern
433
+ pattern.lastIndex = 0; // Reset regex state
434
+ let match;
435
+ while ((match = pattern.exec(contentCopy)) !== null) {
436
+ let completed = false;
437
+ let id = '';
438
+ let description = '';
439
+ let metaString = '';
440
+ // Handle different match groups based on pattern
441
+ if (match.length === 5) {
442
+ // Pattern with checkbox: [1]=checkbox, [2]=id, [3]=desc, [4]=meta
443
+ completed = match[1] === 'x';
444
+ id = match[2];
445
+ description = match[3];
446
+ metaString = match[4] || '';
447
+ }
448
+ else if (match.length === 4) {
449
+ // Pattern without checkbox: [1]=id, [2]=desc, [3]=meta
450
+ completed = false; // Default to not completed
451
+ id = match[1];
452
+ description = match[2];
453
+ metaString = match[3] || '';
454
+ }
455
+ // Skip if already added (avoid duplicates from multiple patterns)
456
+ if (criteria.some(c => c.id === id)) {
457
+ continue;
458
+ }
459
+ const priority = metaString.match(/P\d/)?.[0];
460
+ const testable = metaString.includes('testable');
461
+ criteria.push({
462
+ id,
463
+ description,
464
+ priority,
465
+ testable,
466
+ completed,
467
+ });
468
+ }
306
469
  }
307
470
  return criteria;
308
471
  }
309
472
  /**
310
- * Classify content into epic vs user-story level
311
- *
312
- * NEW (v0.18.0): Uses feature folder name as ID (e.g., FS-25-11-14-release-management)
313
- */
314
- async classifyContent(parsed, epicMapping) {
315
- // Use feature folder name as ID (e.g., FS-25-11-14-release-management)
316
- // This ensures ID matches folder name
317
- const specId = epicMapping.featureFolder;
318
- return {
319
- epic: {
320
- id: specId,
321
- title: parsed.title,
322
- overview: parsed.overview,
323
- businessValue: parsed.businessValue,
324
- status: 'complete',
325
- },
326
- userStories: parsed.userStories,
327
- implementationHistory: [
328
- {
329
- increment: parsed.incrementId,
330
- stories: parsed.userStories.map((us) => us.id),
331
- status: 'complete',
332
- date: new Date().toISOString().split('T')[0],
333
- },
334
- ],
335
- externalLinks: parsed.externalLinks || {},
336
- relatedDocs: [],
337
- };
338
- }
339
- /**
340
- * Generate epic file
473
+ * Extract acceptance criteria from the full spec content for a specific user story
474
+ * This handles cases where AC is in a separate section after the user story
341
475
  */
342
- async generateEpicFile(classified, incrementId) {
343
- // Generate user story summaries
344
- const userStorySummaries = classified.userStories.map((us) => ({
345
- id: us.id,
346
- title: us.title,
347
- status: us.status,
348
- phase: us.phase,
349
- filePath: this.generateUserStoryFilename(us.id, us.title), // User stories are directly in FS folder
350
- }));
351
- const completedStories = classified.userStories.filter((us) => us.status === 'complete').length;
352
- return {
353
- id: classified.epic.id,
354
- title: classified.epic.title,
355
- type: 'epic',
356
- status: 'complete',
357
- priority: classified.epic.priority,
358
- created: new Date().toISOString().split('T')[0],
359
- lastUpdated: new Date().toISOString().split('T')[0],
360
- overview: classified.epic.overview,
361
- businessValue: classified.epic.businessValue,
362
- implementationHistory: classified.implementationHistory,
363
- userStories: userStorySummaries,
364
- externalLinks: classified.externalLinks,
365
- relatedDocs: classified.relatedDocs,
366
- totalStories: classified.userStories.length,
367
- completedStories,
368
- overallProgress: Math.round((completedStories / classified.userStories.length) * 100),
369
- };
476
+ extractAcceptanceCriteriaFromSection(content, userStoryId) {
477
+ const criteria = [];
478
+ // Find the user story section and its acceptance criteria
479
+ const usNumber = userStoryId.replace('US-', '');
480
+ // Try multiple patterns to find AC section (supports both ### and #### headings)
481
+ const patterns = [
482
+ // Pattern 1: AC in "**Acceptance Criteria**:" section (most common - matches spec format)
483
+ // This matches: **Acceptance Criteria**:\n- AC-XXX: description
484
+ new RegExp(`####+?\\s+${userStoryId}:.*?\\n[\\s\\S]*?\\*\\*Acceptance Criteria\\*\\*:\\s*\\n+((?:[-*]\\s+AC-\\d+:[^\\n]+\\n?)+)`, 'im'),
485
+ // Pattern 2: AC with checkboxes format
486
+ new RegExp(`####+?\\s+${userStoryId}:.*?\\n[\\s\\S]*?\\*\\*Acceptance Criteria\\*\\*:\\s*\\n+((?:[-*]\\s+\\[[ x]\\]\\s+\\*\\*AC-[^\\n]+\\n?)+)`, 'im'),
487
+ // Pattern 3: AC directly after user story (no separate section)
488
+ new RegExp(`####+?\\s+${userStoryId}:.*?\\n[\\s\\S]*?(?:\\n\\n|\\n)+([-*]\\s+\\[[ x]\\]\\s+\\*\\*AC-US${usNumber}-\\d+[\\s\\S]*?)(?=\\n\\s*\\*\\*Business|\\n###|\\n---|$)`, 'im'),
489
+ // Pattern 4: AC in a dedicated "### Acceptance Criteria" subsection
490
+ new RegExp(`####+?\\s+${userStoryId}:.*?\\n[\\s\\S]*?####+?\\s+Acceptance Criteria\\s*\\n+([\\s\\S]*?)(?=\\n\\s*\\*\\*Business|\\n###|\\n---|$)`, 'im'),
491
+ ];
492
+ let acContent = '';
493
+ for (const pattern of patterns) {
494
+ const match = content.match(pattern);
495
+ if (match && match[1]) {
496
+ acContent = match[1];
497
+ break;
498
+ }
499
+ }
500
+ if (!acContent) {
501
+ return criteria;
502
+ }
503
+ // Multiple patterns to handle different AC formats
504
+ // Pattern 1: - [ ] **AC-USX-XX**: Description (P1, testable)
505
+ // Pattern 2: - [x] **AC-001**: Description (P0, testable)
506
+ // Pattern 3: - AC-001: Description (P0, testable) [without checkbox]
507
+ const acPatterns = [
508
+ // With checkbox and bold (specific user story format)
509
+ /^[-*]\s+\[([ x])\]\s+\*\*(AC-US\d+-\d+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
510
+ // With checkbox and bold (general format)
511
+ /^[-*]\s+\[([ x])\]\s+\*\*(AC-\d+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
512
+ // Without checkbox but with bold
513
+ /^[-*]\s+\*\*(AC-\d+)\*\*:\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
514
+ // Without checkbox and without bold (common in specs)
515
+ /^[-*]\s+(AC-\d+):\s+(.+?)(?:\s+\(([^)]+)\))?$/gm,
516
+ ];
517
+ // Try each pattern
518
+ for (const pattern of acPatterns) {
519
+ pattern.lastIndex = 0; // Reset regex state
520
+ let match;
521
+ while ((match = pattern.exec(acContent)) !== null) {
522
+ let completed = false;
523
+ let id = '';
524
+ let description = '';
525
+ let metaString = '';
526
+ // Handle different match groups based on pattern
527
+ if (match.length === 5) {
528
+ // Pattern with checkbox: [1]=checkbox, [2]=id, [3]=desc, [4]=meta
529
+ completed = match[1] === 'x';
530
+ id = match[2];
531
+ description = match[3];
532
+ metaString = match[4] || '';
533
+ }
534
+ else if (match.length === 4) {
535
+ // Pattern without checkbox: [1]=id, [2]=desc, [3]=meta
536
+ completed = false; // Default to not completed
537
+ id = match[1];
538
+ description = match[2];
539
+ metaString = match[3] || '';
540
+ }
541
+ // Skip if already added (avoid duplicates from multiple patterns)
542
+ if (criteria.some(c => c.id === id)) {
543
+ continue;
544
+ }
545
+ const priority = metaString.match(/P\d/)?.[0];
546
+ const testable = metaString.includes('testable');
547
+ criteria.push({
548
+ id,
549
+ description,
550
+ priority,
551
+ testable,
552
+ completed,
553
+ });
554
+ }
555
+ }
556
+ return criteria;
370
557
  }
371
558
  /**
372
559
  * Generate user story files
@@ -489,36 +676,6 @@ export class SpecDistributor {
489
676
  .replace(/^-|-$/g, '');
490
677
  return `${id.toLowerCase()}-${slug}.md`;
491
678
  }
492
- /**
493
- * Write feature file to disk (NEW: writes to FEATURE.md instead of README.md)
494
- */
495
- async writeEpicFile(epic, epicMapping) {
496
- // Write to feature-folder/FEATURE.md (feature overview - high-level summary)
497
- const featurePath = path.join(epicMapping.featurePath, 'FEATURE.md');
498
- const content = this.formatEpicFile(epic);
499
- await fs.ensureDir(path.dirname(featurePath));
500
- await fs.writeFile(featurePath, content, 'utf-8');
501
- console.log(` ✅ Written feature overview to ${epicMapping.featureFolder}/FEATURE.md`);
502
- return featurePath;
503
- }
504
- /**
505
- * Write user story files to disk (NEW: writes directly to feature folder)
506
- */
507
- async writeUserStoryFiles(userStories, epicMapping) {
508
- // Write user stories directly to feature folder (not in subfolder)
509
- const featureDir = epicMapping.featurePath;
510
- await fs.ensureDir(featureDir);
511
- const paths = [];
512
- for (const userStory of userStories) {
513
- const filename = this.generateUserStoryFilename(userStory.id, userStory.title);
514
- const filePath = path.join(featureDir, filename);
515
- const content = this.formatUserStoryFile(userStory);
516
- await fs.writeFile(filePath, content, 'utf-8');
517
- paths.push(filePath);
518
- }
519
- console.log(` ✅ Written ${userStories.length} user stories directly to ${epicMapping.featureFolder}/`);
520
- return paths;
521
- }
522
679
  /**
523
680
  * Format epic file as markdown
524
681
  */
@@ -651,7 +808,7 @@ export class SpecDistributor {
651
808
  // Frontmatter
652
809
  lines.push('---');
653
810
  lines.push(`id: ${userStory.id}`);
654
- lines.push(`epic: ${userStory.epic}`);
811
+ lines.push(`feature: ${userStory.epic}`); // ✅ FIX: Use 'feature:' not 'epic:' (Universal Hierarchy)
655
812
  lines.push(`title: "${userStory.title}"`);
656
813
  lines.push(`status: ${userStory.status}`);
657
814
  if (userStory.priority)
@@ -659,40 +816,81 @@ export class SpecDistributor {
659
816
  lines.push(`created: ${userStory.created}`);
660
817
  if (userStory.completed)
661
818
  lines.push(`completed: ${userStory.completed}`);
819
+ // Add external tool mapping to frontmatter
820
+ if (userStory.externalToolMapping) {
821
+ lines.push('externalTool:');
822
+ lines.push(` provider: ${userStory.externalToolMapping.provider}`);
823
+ lines.push(` type: ${userStory.externalToolMapping.externalType}`);
824
+ lines.push(` id: "${userStory.externalToolMapping.externalId}"`);
825
+ lines.push(` url: "${userStory.externalToolMapping.externalUrl}"`);
826
+ }
662
827
  lines.push('---');
663
828
  lines.push('');
664
- // Title
665
- lines.push(`# ${userStory.id}: ${userStory.title}`);
829
+ // Title with external tool indicator
830
+ let titleLine = `# ${userStory.id}: ${userStory.title}`;
831
+ if (userStory.externalToolMapping) {
832
+ const provider = userStory.externalToolMapping.provider.toUpperCase();
833
+ const type = userStory.externalToolMapping.externalType.charAt(0).toUpperCase() +
834
+ userStory.externalToolMapping.externalType.slice(1);
835
+ titleLine += ` (${provider} ${type}: ${userStory.externalToolMapping.externalId})`;
836
+ }
837
+ lines.push(titleLine);
666
838
  lines.push('');
667
- // Feature link (FEATURE.md in same folder)
668
- lines.push(`**Feature**: [${userStory.epic}](./FEATURE.md)`);
839
+ // Feature link (to _features folder)
840
+ // User story is at: .specweave/docs/internal/specs/{project}/{FS-XXX}/us-*.md
841
+ // Feature is at: .specweave/docs/internal/specs/_features/{FS-XXX}/FEATURE.md
842
+ // From {project}/{FS-XXX}/ we need to go up 2 levels to specs/, then into _features/
843
+ // Relative path: ../../_features/{FS-XXX}/FEATURE.md
844
+ const featureRelativePath = `../../_features/${userStory.epic}/FEATURE.md`;
845
+ lines.push(`**Feature**: [${userStory.epic}](${featureRelativePath})`);
669
846
  lines.push('');
670
847
  // Description
671
- lines.push(userStory.description);
672
- lines.push('');
848
+ if (userStory.description) {
849
+ lines.push(userStory.description);
850
+ lines.push('');
851
+ }
673
852
  lines.push('---');
674
853
  lines.push('');
675
854
  // Acceptance Criteria
676
855
  lines.push('## Acceptance Criteria');
677
856
  lines.push('');
678
- for (const ac of userStory.acceptanceCriteria) {
679
- const checkbox = ac.completed ? '[x]' : '[ ]';
680
- const priorityText = ac.priority ? ` (${ac.priority}, testable)` : '';
681
- lines.push(`- ${checkbox} **${ac.id}**: ${ac.description}${priorityText}`);
857
+ if (userStory.acceptanceCriteria && userStory.acceptanceCriteria.length > 0) {
858
+ for (const ac of userStory.acceptanceCriteria) {
859
+ const checkbox = ac.completed ? '[x]' : '[ ]';
860
+ const priorityText = ac.priority ? ` (${ac.priority}, testable)` : '';
861
+ lines.push(`- ${checkbox} **${ac.id}**: ${ac.description}${priorityText}`);
862
+ }
863
+ }
864
+ else {
865
+ lines.push('*Acceptance criteria to be extracted from increment specification*');
682
866
  }
683
867
  lines.push('');
684
868
  lines.push('---');
685
869
  lines.push('');
686
- // Implementation
870
+ // ✅ NEW: Tasks section with checkboxes (project-specific)
871
+ if (userStory.tasks && userStory.tasks.length > 0) {
872
+ lines.push('## Tasks');
873
+ lines.push('');
874
+ for (const task of userStory.tasks) {
875
+ const checkbox = task.completed ? '[x]' : '[ ]';
876
+ lines.push(`- ${checkbox} **${task.id}**: ${task.title}`);
877
+ }
878
+ lines.push('');
879
+ lines.push('> **Note**: Tasks are project-specific. For the full increment task list, see [increment tasks.md](../../../../../increments/${userStory.implementation.increment}/tasks.md)');
880
+ lines.push('');
881
+ lines.push('---');
882
+ lines.push('');
883
+ }
884
+ // Implementation (source reference)
687
885
  lines.push('## Implementation');
688
886
  lines.push('');
689
- lines.push(`**Increment**: [${userStory.implementation.increment}](${userStory.implementation.tasks[0]?.path.replace(/#.*$/, '')})`);
887
+ const incrementLink = userStory.implementation.tasks[0]?.path.replace(/#.*$/, '') || `../../../../../increments/${userStory.implementation.increment}/tasks.md`;
888
+ lines.push(`**Increment**: [${userStory.implementation.increment}](${incrementLink})`);
690
889
  lines.push('');
691
- lines.push('**Tasks**:');
692
- for (const task of userStory.implementation.tasks) {
693
- lines.push(`- [${task.id}: ${task.title}](${task.path})`);
890
+ if (userStory.implementation.tasks.length > 0) {
891
+ lines.push('**Source Tasks**: See increment tasks.md for complete task breakdown');
892
+ lines.push('');
694
893
  }
695
- lines.push('');
696
894
  // Business Rationale
697
895
  if (userStory.businessRationale) {
698
896
  lines.push('---');
@@ -726,98 +924,917 @@ export class SpecDistributor {
726
924
  * Update tasks.md with bidirectional links to user stories (CRITICAL!)
727
925
  *
728
926
  * This creates bidirectional traceability:
729
- * - User Story → Tasks (already done in us-*.md files)
730
- * - Tasks → User Story (NEW - added here)
731
- *
732
- * When a task implements a user story, this adds a link in tasks.md:
733
- * **User Story**: [US-001: Title](../../docs/internal/specs/{project}/{feature}/us-001-*.md)
927
+
928
+ /**
929
+ * Classify content by project (NEW for universal hierarchy)
930
+ * Split user stories across projects based on keywords and frontmatter
931
+ */
932
+ async classifyContentByProject(parsed, featureMapping) {
933
+ const storiesByProject = new Map();
934
+ for (const story of parsed.userStories) {
935
+ // Detect which project(s) this story belongs to
936
+ const storyProjects = await this.detectUserStoryProjects(story, featureMapping.projects);
937
+ for (const project of storyProjects) {
938
+ if (!storiesByProject.has(project)) {
939
+ storiesByProject.set(project, []);
940
+ }
941
+ storiesByProject.get(project).push(story);
942
+ }
943
+ }
944
+ // If no stories were classified to any project, add all to default/first project
945
+ if (storiesByProject.size === 0 && parsed.userStories.length > 0) {
946
+ const defaultProject = featureMapping.projects[0] || 'default';
947
+ storiesByProject.set(defaultProject, parsed.userStories);
948
+ }
949
+ return storiesByProject;
950
+ }
951
+ /**
952
+ * Detect which projects a user story belongs to
734
953
  */
735
- async updateTasksWithUserStoryLinks(incrementId, userStories, epicMapping) {
954
+ async detectUserStoryProjects(story, availableProjects) {
955
+ // If only one project available, return it
956
+ if (availableProjects.length === 1) {
957
+ return availableProjects;
958
+ }
959
+ const projects = [];
960
+ const config = await this.hierarchyMapper.getSpecweaveConfig();
961
+ // Method 1: Check if story has explicit project field
962
+ if (story.project) {
963
+ if (availableProjects.includes(story.project)) {
964
+ return [story.project];
965
+ }
966
+ }
967
+ // Method 2: Keyword detection in title and description
968
+ const text = `${story.title} ${story.description}`.toLowerCase();
969
+ for (const projectId of availableProjects) {
970
+ if (projectId === 'default') {
971
+ continue; // Skip default for keyword matching
972
+ }
973
+ // Get project config for keywords
974
+ const projectConfig = config.multiProject?.projects?.[projectId];
975
+ if (projectConfig?.keywords) {
976
+ const hasKeyword = projectConfig.keywords.some(keyword => text.includes(keyword.toLowerCase()));
977
+ if (hasKeyword) {
978
+ projects.push(projectId);
979
+ }
980
+ }
981
+ // Also check if project name is mentioned
982
+ if (text.includes(projectId.toLowerCase())) {
983
+ if (!projects.includes(projectId)) {
984
+ projects.push(projectId);
985
+ }
986
+ }
987
+ }
988
+ // Method 3: Fallback - if no projects detected, assign to all
989
+ if (projects.length === 0) {
990
+ return availableProjects;
991
+ }
992
+ return projects;
993
+ }
994
+ /**
995
+ * Generate epic file (OPTIONAL - for strategic themes)
996
+ */
997
+ async generateEpicFile(parsed, epicMapping, featureMapping) {
998
+ if (!epicMapping)
999
+ return null;
1000
+ return {
1001
+ id: epicMapping.epicId,
1002
+ title: parsed.title,
1003
+ type: 'epic',
1004
+ status: parsed.status || 'in-progress',
1005
+ created: parsed.created || new Date().toISOString(),
1006
+ lastUpdated: new Date().toISOString(),
1007
+ strategicOverview: parsed.overview,
1008
+ features: [featureMapping.featureId],
1009
+ successMetrics: [],
1010
+ timeline: {
1011
+ start: parsed.created || new Date().toISOString(),
1012
+ targetCompletion: 'TBD',
1013
+ currentPhase: 'Phase 1',
1014
+ },
1015
+ stakeholders: {},
1016
+ externalLinks: parsed.externalLinks || {},
1017
+ };
1018
+ }
1019
+ /**
1020
+ * Generate feature file (REQUIRED - cross-project overview)
1021
+ */
1022
+ async generateFeatureFile(parsed, featureMapping, storiesByProject, incrementId) {
1023
+ // Convert stories to summaries grouped by project
1024
+ const userStoriesByProject = new Map();
1025
+ for (const [project, stories] of storiesByProject.entries()) {
1026
+ const summaries = stories.map(s => ({
1027
+ id: s.id,
1028
+ title: s.title,
1029
+ status: s.status,
1030
+ phase: s.phase,
1031
+ filePath: `../../${project}/${featureMapping.featureFolder}/${this.generateUserStoryFilename(s.id, s.title)}`,
1032
+ }));
1033
+ userStoriesByProject.set(project, summaries);
1034
+ }
1035
+ // Count completed stories
1036
+ let completedStories = 0;
1037
+ for (const stories of storiesByProject.values()) {
1038
+ completedStories += stories.filter(s => s.status === 'complete').length;
1039
+ }
1040
+ // Detect external tool mapping
1041
+ const externalToolMapping = incrementId ? await this.detectExternalToolMapping(incrementId) : undefined;
1042
+ return {
1043
+ id: featureMapping.featureId,
1044
+ title: parsed.title,
1045
+ type: 'feature',
1046
+ status: parsed.status || 'in-progress',
1047
+ priority: parsed.priority || 'P1',
1048
+ created: parsed.created || new Date().toISOString(),
1049
+ lastUpdated: new Date().toISOString(),
1050
+ epic: featureMapping.epic,
1051
+ projects: featureMapping.projects,
1052
+ overview: parsed.overview,
1053
+ businessValue: parsed.businessValue || [],
1054
+ implementationHistory: [],
1055
+ userStoriesByProject,
1056
+ externalLinks: parsed.externalLinks || {},
1057
+ totalStories: parsed.userStories.length,
1058
+ completedStories,
1059
+ overallProgress: parsed.userStories.length > 0
1060
+ ? Math.round((completedStories / parsed.userStories.length) * 100)
1061
+ : 0,
1062
+ sourceIncrement: incrementId, // Track the source increment
1063
+ externalToolMapping, // Include external tool mapping
1064
+ };
1065
+ }
1066
+ /**
1067
+ * Generate project context files (README.md for each project)
1068
+ */
1069
+ async generateProjectContextFiles(featureMapping, parsed) {
1070
+ const contextFiles = new Map();
1071
+ for (const project of featureMapping.projects) {
1072
+ const projectContext = await this.hierarchyMapper.getProjectContext(project);
1073
+ if (!projectContext)
1074
+ continue;
1075
+ const content = this.formatProjectContextFile(featureMapping, projectContext, parsed);
1076
+ contextFiles.set(project, content);
1077
+ }
1078
+ return contextFiles;
1079
+ }
1080
+ /**
1081
+ * Format project context file (README.md)
1082
+ * IMPORTANT: This README is created for ALL increments, even without user stories!
1083
+ */
1084
+ formatProjectContextFile(featureMapping, projectContext, parsed) {
1085
+ const featurePathUp = featureMapping.projects.length > 1 ? '../../../' : '../../';
1086
+ // Determine if this increment has user stories
1087
+ const hasUserStories = parsed.userStories && parsed.userStories.length > 0;
1088
+ const statusNote = parsed.status === 'abandoned' ? 'abandoned' :
1089
+ parsed.status === 'complete' ? 'complete' : 'in-progress';
1090
+ return `---
1091
+ id: ${featureMapping.featureId}-${projectContext.projectId}
1092
+ title: "${parsed.title} - ${projectContext.projectName} Implementation"
1093
+ feature: ${featureMapping.featureId}
1094
+ project: ${projectContext.projectId}
1095
+ type: feature-context
1096
+ status: ${statusNote}
1097
+ sourceIncrement: ${parsed.incrementId}
1098
+ ---
1099
+
1100
+ # ${projectContext.projectName} Implementation: ${parsed.title}
1101
+
1102
+ **Feature**: [${featureMapping.featureId}](${featurePathUp}_features/${featureMapping.featureFolder}/FEATURE.md)
1103
+
1104
+ ## Overview
1105
+
1106
+ ${parsed.overview}
1107
+
1108
+ ## ${projectContext.projectName}-Specific Context
1109
+
1110
+ This document contains the ${projectContext.projectName} implementation details for the ${parsed.title} feature.
1111
+
1112
+ ## Tech Stack
1113
+
1114
+ ${projectContext.techStack.map(t => `- ${t}`).join('\n')}
1115
+
1116
+ ## User Stories (${projectContext.projectName})
1117
+
1118
+ ${hasUserStories ? 'User stories for this project are listed below.' : `_This increment has no user stories. See [FEATURE.md](${featurePathUp}_features/${featureMapping.featureFolder}/FEATURE.md) for overview and implementation details._`}
1119
+
1120
+ ## Dependencies
1121
+
1122
+ [Project-specific dependencies will be documented here]
1123
+
1124
+ ## Architecture Considerations
1125
+
1126
+ [${projectContext.projectName}-specific architecture notes]
1127
+
1128
+ ---
1129
+
1130
+ **Source**: [Increment ${parsed.incrementId}](../../../../../increments/${parsed.incrementId})
1131
+ `;
1132
+ }
1133
+ /**
1134
+ * Generate user story files by project
1135
+ */
1136
+ async generateUserStoryFilesByProject(storiesByProject, featureMapping, incrementId) {
1137
+ const filesByProject = new Map();
1138
+ // Load tasks for linking (LEGACY - for backward compatibility)
1139
+ const taskMap = await this.loadTaskReferences(incrementId);
1140
+ for (const [project, stories] of storiesByProject.entries()) {
1141
+ const userStoryFiles = [];
1142
+ // Get project context for AC and task transformation
1143
+ const rawProjectContext = await this.hierarchyMapper.getProjectContext(project);
1144
+ for (const userStory of stories) {
1145
+ // LEGACY: Find tasks that implement this user story (for backward compatibility)
1146
+ const taskReferences = this.findTasksForUserStory(userStory.id, taskMap);
1147
+ // ✅ NEW: Generate project-specific tasks with completion status
1148
+ const projectSpecificTasks = await this.taskGenerator.generateProjectSpecificTasks(incrementId, userStory.id, rawProjectContext ? {
1149
+ id: rawProjectContext.projectId,
1150
+ name: rawProjectContext.projectName,
1151
+ type: this.detectProjectType(rawProjectContext),
1152
+ techStack: rawProjectContext.techStack,
1153
+ keywords: rawProjectContext.keywords,
1154
+ } : undefined);
1155
+ // Find related user stories (same project, same phase)
1156
+ const relatedStories = stories
1157
+ .filter((us) => us.id !== userStory.id && us.phase === userStory.phase)
1158
+ .map((us) => ({
1159
+ id: us.id,
1160
+ title: us.title,
1161
+ status: us.status,
1162
+ phase: us.phase,
1163
+ filePath: this.generateUserStoryFilename(us.id, us.title),
1164
+ }));
1165
+ // ✨ Apply project-specific AC transformation
1166
+ let projectSpecificACs = userStory.acceptanceCriteria;
1167
+ if (rawProjectContext) {
1168
+ // Map ProjectContext to AC generator's expected format
1169
+ const acGeneratorContext = {
1170
+ id: rawProjectContext.projectId,
1171
+ name: rawProjectContext.projectName,
1172
+ type: this.detectProjectType(rawProjectContext),
1173
+ techStack: rawProjectContext.techStack,
1174
+ keywords: rawProjectContext.keywords,
1175
+ };
1176
+ projectSpecificACs = this.acGenerator.makeProjectSpecific(userStory.acceptanceCriteria, userStory.id, acGeneratorContext);
1177
+ }
1178
+ userStoryFiles.push({
1179
+ id: userStory.id,
1180
+ epic: featureMapping.featureId, // Feature is the parent
1181
+ title: userStory.title,
1182
+ status: userStory.status,
1183
+ priority: userStory.priority,
1184
+ created: new Date().toISOString().split('T')[0],
1185
+ completed: userStory.status === 'complete' ? new Date().toISOString().split('T')[0] : undefined,
1186
+ description: userStory.description,
1187
+ acceptanceCriteria: projectSpecificACs, // ✅ Use project-specific AC
1188
+ tasks: projectSpecificTasks, // ✅ NEW: Project-specific tasks with completion status
1189
+ implementation: {
1190
+ increment: incrementId,
1191
+ tasks: taskReferences, // LEGACY: Keep for backward compatibility
1192
+ },
1193
+ businessRationale: userStory.businessRationale,
1194
+ relatedStories,
1195
+ phase: userStory.phase,
1196
+ project, // Add project field
1197
+ });
1198
+ }
1199
+ filesByProject.set(project, userStoryFiles);
1200
+ }
1201
+ return filesByProject;
1202
+ }
1203
+ /**
1204
+ * Write epic file to _epics/ folder
1205
+ */
1206
+ async writeEpicFile(epic, epicMapping) {
1207
+ if (!epic || !epicMapping)
1208
+ return null;
1209
+ const epicPath = path.join(epicMapping.epicPath, 'EPIC.md');
1210
+ const content = this.formatEpicThemeFile(epic);
1211
+ await fs.ensureDir(path.dirname(epicPath));
1212
+ await fs.writeFile(epicPath, content, 'utf-8');
1213
+ console.log(` ✅ Written epic overview to _epics/${epicMapping.epicFolder}/EPIC.md`);
1214
+ return epicPath;
1215
+ }
1216
+ /**
1217
+ * Format epic theme file content
1218
+ */
1219
+ formatEpicThemeFile(epic) {
1220
+ const yaml = `---
1221
+ id: ${epic.id}
1222
+ title: "${epic.title}"
1223
+ type: ${epic.type}
1224
+ status: ${epic.status}
1225
+ ---`;
1226
+ return `${yaml}
1227
+
1228
+ # ${epic.title}
1229
+
1230
+ ## Strategic Overview
1231
+
1232
+ ${epic.strategicOverview}
1233
+
1234
+ ## Features
1235
+
1236
+ ${epic.features.map(f => `- ${f}`).join('\n')}
1237
+
1238
+ ## Timeline
1239
+
1240
+ - **Start**: ${epic.timeline.start}
1241
+ - **Target Completion**: ${epic.timeline.targetCompletion}
1242
+ - **Current Phase**: ${epic.timeline.currentPhase}
1243
+ `;
1244
+ }
1245
+ /**
1246
+ * Write feature file to _features/ folder
1247
+ */
1248
+ async writeFeatureFile(feature, featureMapping) {
1249
+ const featurePath = path.join(featureMapping.featurePath, 'FEATURE.md');
1250
+ const content = this.formatFeatureFile(feature);
1251
+ await fs.ensureDir(path.dirname(featurePath));
1252
+ await fs.writeFile(featurePath, content, 'utf-8');
1253
+ console.log(` ✅ Written feature overview to _features/${featureMapping.featureFolder}/FEATURE.md`);
1254
+ return featurePath;
1255
+ }
1256
+ /**
1257
+ * Format feature file content
1258
+ */
1259
+ formatFeatureFile(feature) {
1260
+ // Build YAML frontmatter with external tool mapping
1261
+ let yaml = `---
1262
+ id: ${feature.id}
1263
+ title: "${feature.title}"
1264
+ type: ${feature.type}
1265
+ status: ${feature.status}
1266
+ priority: ${feature.priority}
1267
+ created: ${feature.created}
1268
+ lastUpdated: ${feature.lastUpdated}
1269
+ projects: [${feature.projects.map(p => `"${p}"`).join(', ')}]
1270
+ ${feature.epic ? `epic: ${feature.epic}` : ''}
1271
+ ${feature.sourceIncrement ? `sourceIncrement: ${feature.sourceIncrement}` : ''}`;
1272
+ // Add external tool mapping to frontmatter
1273
+ if (feature.externalToolMapping) {
1274
+ yaml += `
1275
+ externalTool:
1276
+ provider: ${feature.externalToolMapping.provider}
1277
+ type: ${feature.externalToolMapping.externalType}
1278
+ id: "${feature.externalToolMapping.externalId}"
1279
+ url: "${feature.externalToolMapping.externalUrl}"`;
1280
+ }
1281
+ yaml += '\n---';
1282
+ const storiesByProjectSection = Array.from(feature.userStoriesByProject.entries())
1283
+ .map(([project, stories]) => `
1284
+ ### ${project}
1285
+
1286
+ ${stories.map(s => `- [${s.id}: ${s.title}](${s.filePath}) - ${s.status}`).join('\n')}`)
1287
+ .join('\n');
1288
+ // Build title with external tool indicator
1289
+ let titleLine = `# ${feature.title}`;
1290
+ if (feature.externalToolMapping) {
1291
+ const provider = feature.externalToolMapping.provider.toUpperCase();
1292
+ const type = feature.externalToolMapping.externalType.charAt(0).toUpperCase() +
1293
+ feature.externalToolMapping.externalType.slice(1);
1294
+ titleLine += ` (${provider} ${type}: ${feature.externalToolMapping.externalId})`;
1295
+ }
1296
+ // Add external tool mapping section
1297
+ let externalMappingSection = '';
1298
+ if (feature.externalToolMapping) {
1299
+ externalMappingSection = `
1300
+ ## External Tool Mapping
1301
+
1302
+ **Mapped from**: ${feature.externalToolMapping.provider.toUpperCase()} ${feature.externalToolMapping.externalType.charAt(0).toUpperCase() + feature.externalToolMapping.externalType.slice(1)} [${feature.externalToolMapping.externalId}](${feature.externalToolMapping.externalUrl})
1303
+
1304
+ > **Note**: ${feature.externalToolMapping.mappingNote}
1305
+
1306
+ This SpecWeave Feature corresponds to a ${feature.externalToolMapping.externalType} in ${feature.externalToolMapping.provider.toUpperCase()}. The hierarchy mapping is:
1307
+ - **SpecWeave**: Feature (${feature.id})
1308
+ - **${feature.externalToolMapping.provider.toUpperCase()}**: ${feature.externalToolMapping.externalType.charAt(0).toUpperCase() + feature.externalToolMapping.externalType.slice(1)} (${feature.externalToolMapping.externalId})
1309
+
1310
+ `;
1311
+ }
1312
+ // Add source section based on where the feature came from
1313
+ let sourceSection = '';
1314
+ if (feature.sourceIncrement) {
1315
+ sourceSection = `
1316
+ ## Source
1317
+
1318
+ This feature was created from increment: [\`${feature.sourceIncrement}\`](../../../../../increments/${feature.sourceIncrement})
1319
+ `;
1320
+ }
1321
+ else if (feature.externalLinks?.github || feature.externalLinks?.jira || feature.externalLinks?.ado) {
1322
+ sourceSection = `
1323
+ ## Source
1324
+
1325
+ This feature was imported from external tool:
1326
+ ${feature.externalLinks?.github ? `- GitHub: ${feature.externalLinks.github}` : ''}
1327
+ ${feature.externalLinks?.jira ? `- JIRA: ${feature.externalLinks.jira}` : ''}
1328
+ ${feature.externalLinks?.ado ? `- Azure DevOps: ${feature.externalLinks.ado}` : ''}
1329
+
1330
+ Note: No explicit feature was specified during import.
1331
+ `;
1332
+ }
1333
+ return `${yaml}
1334
+
1335
+ ${titleLine}
1336
+ ${externalMappingSection}
1337
+ ## Overview
1338
+
1339
+ ${feature.overview}
1340
+ ${sourceSection}
1341
+ ## Business Value
1342
+
1343
+ ${feature.businessValue.map(v => `- ${v}`).join('\n') || 'See overview above'}
1344
+
1345
+ ## Projects
1346
+
1347
+ This feature spans the following projects:
1348
+ ${feature.projects.map(p => `- ${p}`).join('\n')}
1349
+
1350
+ ## User Stories by Project
1351
+ ${storiesByProjectSection}
1352
+
1353
+ ## Progress
1354
+
1355
+ - **Total Stories**: ${feature.totalStories}
1356
+ - **Completed**: ${feature.completedStories}
1357
+ - **Progress**: ${feature.overallProgress}%
1358
+ `;
1359
+ }
1360
+ /**
1361
+ * Write project context files (README.md for each project)
1362
+ * IMPORTANT: Always create README.md even if increment has no user stories!
1363
+ * This ensures every increment is represented in living docs, preventing gaps.
1364
+ */
1365
+ async writeProjectContextFiles(contextFiles, featureMapping) {
1366
+ const writtenPaths = [];
1367
+ // Write README.md for EVERY project, even if no user stories exist
1368
+ for (const [project, content] of contextFiles.entries()) {
1369
+ const projectPath = featureMapping.projectPaths.get(project);
1370
+ if (!projectPath) {
1371
+ console.warn(` ⚠️ No project path found for ${project}, skipping README`);
1372
+ continue;
1373
+ }
1374
+ // Ensure directory exists
1375
+ await fs.ensureDir(projectPath);
1376
+ // Write README.md
1377
+ const readmePath = path.join(projectPath, 'README.md');
1378
+ await fs.writeFile(readmePath, content, 'utf-8');
1379
+ writtenPaths.push(readmePath);
1380
+ }
1381
+ if (writtenPaths.length > 0) {
1382
+ console.log(` ✅ Written README.md to ${writtenPaths.length} project folder(s)`);
1383
+ }
1384
+ return writtenPaths;
1385
+ }
1386
+ /**
1387
+ * Write user story files by project
1388
+ * IMPORTANT: Ensures project folders exist even if no user stories!
1389
+ */
1390
+ async writeUserStoryFilesByProject(userStoryFilesByProject, featureMapping, incrementId) {
1391
+ const pathsByProject = new Map();
1392
+ // CRITICAL FIX: Ensure ALL project folders exist, even without user stories
1393
+ for (const project of featureMapping.projects) {
1394
+ const projectPath = featureMapping.projectPaths.get(project);
1395
+ if (!projectPath) {
1396
+ console.warn(` ⚠️ No project path found for ${project}, skipping`);
1397
+ continue;
1398
+ }
1399
+ // Ensure directory exists (even if no stories to write)
1400
+ await fs.ensureDir(projectPath);
1401
+ const stories = userStoryFilesByProject.get(project) || [];
1402
+ const paths = [];
1403
+ // Write user story files (if any exist)
1404
+ for (const story of stories) {
1405
+ const filename = this.generateUserStoryFilename(story.id, story.title);
1406
+ const filePath = path.join(projectPath, filename);
1407
+ const content = this.formatUserStoryFile(story);
1408
+ await fs.writeFile(filePath, content, 'utf-8');
1409
+ paths.push(filePath);
1410
+ }
1411
+ pathsByProject.set(project, paths);
1412
+ }
1413
+ const totalStories = Array.from(userStoryFilesByProject.values())
1414
+ .reduce((sum, stories) => sum + stories.length, 0);
1415
+ if (totalStories > 0) {
1416
+ console.log(` ✅ Written ${totalStories} user stories to ${featureMapping.projects.length} project(s)`);
1417
+ }
1418
+ else {
1419
+ console.log(` ℹ️ No user stories to write, but created ${featureMapping.projects.length} project folder(s)`);
1420
+ }
1421
+ return pathsByProject;
1422
+ }
1423
+ /**
1424
+ * Update tasks.md with bidirectional links (project-aware)
1425
+ */
1426
+ async updateTasksWithUserStoryLinks(incrementId, userStoryFilesByProject, featureMapping) {
736
1427
  const tasksPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'tasks.md');
737
- // Check if tasks.md exists
738
1428
  if (!fs.existsSync(tasksPath)) {
739
- console.log(` ⚠️ tasks.md not found for ${incrementId}, skipping bidirectional link update`);
1429
+ console.warn(` ⚠️ tasks.md not found for ${incrementId}, skipping bidirectional linking`);
740
1430
  return;
741
1431
  }
742
- try {
743
- const tasksContent = await fs.readFile(tasksPath, 'utf-8');
744
- // Parse tasks to create task → user story mapping
745
- const taskToUSMapping = this.mapTasksToUserStories(tasksContent, userStories);
746
- if (Object.keys(taskToUSMapping).length === 0) {
747
- console.log(` ℹ️ No AC-based task-to-US mapping found, skipping bidirectional links`);
748
- return;
749
- }
750
- // Update tasks.md content with user story links
751
- let updatedContent = tasksContent;
752
- let linksAdded = 0;
753
- for (const [taskId, userStory] of Object.entries(taskToUSMapping)) {
754
- // Generate relative path from tasks.md to user story file
755
- const projectId = epicMapping.featurePath.split('/specs/')[1]?.split('/')[0] || 'default';
756
- const featureFolder = epicMapping.featureFolder;
757
- const userStoryFile = this.generateUserStoryFilename(userStory.id, userStory.title);
758
- const relativePath = `../../docs/internal/specs/${projectId}/${featureFolder}/${userStoryFile}`;
759
- // Find task section and add link if not already present (supports both ## and ### headings)
760
- // CRITICAL: Remove 'g' flag to prevent multiple matches of the same task
761
- const taskPattern = new RegExp(`(^##+ ${taskId}:.*?$\\n)([\\s\\S]*?)(?=^##+ T-|^---$|$)`, 'm');
762
- // Only replace once per task
763
- let replaced = false;
764
- updatedContent = updatedContent.replace(taskPattern, (match, heading, body) => {
765
- // Prevent multiple replacements
766
- if (replaced) {
767
- return match;
1432
+ let content = await fs.readFile(tasksPath, 'utf-8');
1433
+ // Build a map of all user stories across all projects
1434
+ const allUserStories = new Map();
1435
+ for (const [project, stories] of userStoryFilesByProject.entries()) {
1436
+ for (const story of stories) {
1437
+ allUserStories.set(story.id, { story, project });
1438
+ }
1439
+ }
1440
+ // Pattern to find task headings and their AC fields
1441
+ const taskSections = content.split(/(?=^##+ T-\d+:)/m);
1442
+ const updatedSections = [];
1443
+ for (const section of taskSections) {
1444
+ if (!section.trim()) {
1445
+ updatedSections.push(section);
1446
+ continue;
1447
+ }
1448
+ // Extract task ID and AC list
1449
+ const taskMatch = section.match(/^(##+ T-\d+:.+?)$/m);
1450
+ const acMatch = section.match(/\*\*AC\*\*:\s*([^\n]+)/);
1451
+ if (!taskMatch || !acMatch) {
1452
+ updatedSections.push(section);
1453
+ continue;
1454
+ }
1455
+ const taskHeading = taskMatch[1];
1456
+ const acList = acMatch[1];
1457
+ // Find which user story this task belongs to based on AC-IDs
1458
+ let linkedStory = null;
1459
+ const acPattern = /AC-US(\d+)-\d+/g;
1460
+ let acIdMatch;
1461
+ while ((acIdMatch = acPattern.exec(acList)) !== null) {
1462
+ const usNumber = acIdMatch[1];
1463
+ const usId = `US-${usNumber.padStart(3, '0')}`;
1464
+ if (allUserStories.has(usId)) {
1465
+ linkedStory = allUserStories.get(usId);
1466
+ break;
1467
+ }
1468
+ }
1469
+ if (linkedStory) {
1470
+ const { story, project } = linkedStory;
1471
+ // Use the FS-XXX feature ID from featureMapping, not featureFolder
1472
+ const newRelativePath = `../../docs/internal/specs/${project}/${featureMapping.featureId}/${this.generateUserStoryFilename(story.id, story.title)}`;
1473
+ const newLinkLine = `**User Story**: [${story.id}: ${story.title}](${newRelativePath})`;
1474
+ // Check for existing user story links (old or new format)
1475
+ const userStoryLinkPattern = /\*\*User Story\*\*:.*/g;
1476
+ const existingLinks = section.match(userStoryLinkPattern);
1477
+ if (!existingLinks || existingLinks.length === 0) {
1478
+ // No link exists - add new one
1479
+ const lines = section.split('\n');
1480
+ const headingIndex = lines.findIndex(line => line.match(/^##+ T-\d+:/));
1481
+ if (headingIndex >= 0) {
1482
+ lines.splice(headingIndex + 1, 0, '', newLinkLine);
1483
+ updatedSections.push(lines.join('\n'));
768
1484
  }
769
- // Check if link already exists
770
- if (body.includes('**User Story**:')) {
771
- return match; // Link already exists
1485
+ else {
1486
+ updatedSections.push(section);
772
1487
  }
773
- // Insert link right after the heading (before any content)
774
- const linkLine = `**User Story**: [${userStory.id}: ${userStory.title}](${relativePath})\n\n`;
775
- replaced = true;
776
- linksAdded++;
777
- return heading + linkLine + body;
778
- });
779
- }
780
- // Write updated tasks.md
781
- if (linksAdded > 0) {
782
- await fs.writeFile(tasksPath, updatedContent, 'utf-8');
783
- console.log(` 🔗 Added ${linksAdded} bidirectional links to tasks.md`);
1488
+ }
1489
+ else {
1490
+ // Link(s) exist - update them and remove duplicates/blank lines
1491
+ const lines = section.split('\n');
1492
+ let foundFirst = false;
1493
+ const cleanedLines = [];
1494
+ let consecutiveBlankLines = 0;
1495
+ for (let i = 0; i < lines.length; i++) {
1496
+ const line = lines[i];
1497
+ // Handle user story links
1498
+ if (line.includes('**User Story**:')) {
1499
+ if (!foundFirst) {
1500
+ foundFirst = true;
1501
+ cleanedLines.push(newLinkLine); // Replace with updated link
1502
+ consecutiveBlankLines = 0;
1503
+ }
1504
+ // Skip duplicate links
1505
+ continue;
1506
+ }
1507
+ // Handle blank lines (keep max 1 consecutive)
1508
+ if (line.trim() === '') {
1509
+ consecutiveBlankLines++;
1510
+ if (consecutiveBlankLines <= 1) {
1511
+ cleanedLines.push(line);
1512
+ }
1513
+ }
1514
+ else {
1515
+ consecutiveBlankLines = 0;
1516
+ cleanedLines.push(line);
1517
+ }
1518
+ }
1519
+ updatedSections.push(cleanedLines.join('\n'));
1520
+ }
784
1521
  }
785
1522
  else {
786
- console.log(` ℹ️ All tasks already have user story links`);
1523
+ updatedSections.push(section);
787
1524
  }
788
1525
  }
789
- catch (error) {
790
- console.warn(` ⚠️ Failed to update tasks.md with bidirectional links: ${error}`);
1526
+ const updatedContent = updatedSections.join('');
1527
+ if (content !== updatedContent) {
1528
+ await fs.writeFile(tasksPath, updatedContent, 'utf-8');
1529
+ console.log(` ✅ Updated tasks.md with bidirectional user story links`);
791
1530
  }
792
1531
  }
793
1532
  /**
794
- * Map tasks to user stories using AC-IDs
795
- *
796
- * Extracts AC-IDs from tasks (e.g., AC-US1-01) and maps them to user stories (e.g., US-001)
1533
+ * Update acceptance criteria status in user stories based on completed tasks
1534
+ * This method synchronizes AC checkboxes with task completion status
797
1535
  */
798
- mapTasksToUserStories(tasksContent, userStories) {
799
- const mapping = {};
800
- // Extract all tasks with their AC-IDs (supports both ## and ### headings)
801
- const taskPattern = /^##+ (T-\d+):.*?$\n[\s\S]*?\*\*AC\*\*:\s*([^\n]+)/gm;
802
- let match;
803
- while ((match = taskPattern.exec(tasksContent)) !== null) {
804
- const taskId = match[1]; // T-001
805
- const acList = match[2]; // AC-US1-01, AC-US1-02
806
- // Extract user story IDs from AC-IDs (AC-US1-01 → US-001)
807
- const acPattern = /AC-US(\d+)-\d+/g;
808
- let acMatch;
809
- while ((acMatch = acPattern.exec(acList)) !== null) {
810
- const usNumber = acMatch[1]; // "1"
811
- const usId = `US-${usNumber.padStart(3, '0')}`; // "US-001"
812
- // Find matching user story
813
- const userStory = userStories.find(us => us.id === usId);
814
- if (userStory) {
815
- mapping[taskId] = userStory;
816
- break; // One task can only map to one primary user story
1536
+ async updateAcceptanceCriteriaStatus(incrementId) {
1537
+ console.log(`\n📊 Updating acceptance criteria status for increment: ${incrementId}`);
1538
+ const incrementPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId);
1539
+ const tasksPath = path.join(incrementPath, 'tasks.md');
1540
+ const specPath = path.join(incrementPath, 'spec.md');
1541
+ if (!await fs.pathExists(tasksPath)) {
1542
+ console.log(` ⚠️ No tasks.md found, skipping AC status update`);
1543
+ return;
1544
+ }
1545
+ const tasksContent = await fs.readFile(tasksPath, 'utf-8');
1546
+ // Parse completed tasks and their AC references
1547
+ const completedACs = this.extractCompletedAcceptanceCriteria(tasksContent);
1548
+ if (completedACs.size === 0) {
1549
+ console.log(` ℹ️ No completed acceptance criteria found`);
1550
+ return;
1551
+ }
1552
+ console.log(` 📝 Found ${completedACs.size} user stories with completed acceptance criteria`);
1553
+ // Try to detect the feature folder this increment maps to
1554
+ let featureId = null;
1555
+ if (await fs.pathExists(specPath)) {
1556
+ const specContent = await fs.readFile(specPath, 'utf-8');
1557
+ // Look for feature mapping in frontmatter or content
1558
+ const featureMatch = specContent.match(/feature:\s*(FS-[^\s\n]+)/i);
1559
+ if (featureMatch) {
1560
+ const declaredFeature = featureMatch[1];
1561
+ // Try to find the actual feature folder that exists
1562
+ featureId = await this.findActualFeatureFolder(incrementId, declaredFeature);
1563
+ console.log(` 📁 Feature mapping: ${declaredFeature} → ${featureId || 'not found'}`);
1564
+ }
1565
+ }
1566
+ // Update user story files with completed ACs
1567
+ await this.updateUserStoryACStatus(completedACs, incrementId, featureId);
1568
+ }
1569
+ /**
1570
+ * Extract completed acceptance criteria from tasks
1571
+ */
1572
+ extractCompletedAcceptanceCriteria(tasksContent) {
1573
+ const completedACs = new Map(); // US-ID -> Set of AC-IDs
1574
+ // Split into task sections
1575
+ const taskSections = tasksContent.split(/^#{2,3}\s+T-\d+:/m);
1576
+ for (const section of taskSections) {
1577
+ if (!section.trim())
1578
+ continue;
1579
+ // Check if task is completed
1580
+ const statusMatch = section.match(/\*\*Status\*\*:\s*\[x\]/i);
1581
+ if (!statusMatch)
1582
+ continue; // Skip incomplete tasks
1583
+ // Extract AC field
1584
+ const acMatch = section.match(/\*\*AC\*\*:\s*([^\n]+)/);
1585
+ if (!acMatch)
1586
+ continue;
1587
+ // Parse AC-IDs
1588
+ const acIds = acMatch[1].split(/[,\s]+/).filter(id => id.match(/^AC-/));
1589
+ for (const acId of acIds) {
1590
+ // Determine user story from AC-ID
1591
+ // Patterns: AC-US1-01 -> US-001, AC-001 -> US-001 (assume single story)
1592
+ let userStoryId = '';
1593
+ const usMatch = acId.match(/AC-US(\d+)-/);
1594
+ if (usMatch) {
1595
+ userStoryId = `US-${usMatch[1].padStart(3, '0')}`;
1596
+ }
1597
+ else {
1598
+ // For generic AC-XXX format, try to infer from task's user story link
1599
+ const usLinkMatch = section.match(/\[US-(\d+):/);
1600
+ if (usLinkMatch) {
1601
+ userStoryId = `US-${usLinkMatch[1].padStart(3, '0')}`;
1602
+ }
1603
+ }
1604
+ if (userStoryId) {
1605
+ if (!completedACs.has(userStoryId)) {
1606
+ completedACs.set(userStoryId, new Set());
1607
+ }
1608
+ completedACs.get(userStoryId).add(acId);
1609
+ }
1610
+ }
1611
+ }
1612
+ return completedACs;
1613
+ }
1614
+ /**
1615
+ * Update user story files with completed AC status
1616
+ */
1617
+ async updateUserStoryACStatus(completedACs, incrementId, featureId = null) {
1618
+ const specsPath = path.join(this.projectRoot, '.specweave', 'docs', 'internal', 'specs');
1619
+ // Find all user story files
1620
+ const projects = await this.getProjectFolders();
1621
+ let updatedCount = 0;
1622
+ for (const project of projects) {
1623
+ const projectSpecsPath = path.join(specsPath, project);
1624
+ if (!await fs.pathExists(projectSpecsPath))
1625
+ continue;
1626
+ // Find all feature folders
1627
+ const entries = await fs.readdir(projectSpecsPath, { withFileTypes: true });
1628
+ let featureFolders = entries.filter(e => e.isDirectory() && e.name.startsWith('FS-'));
1629
+ // If we have a specific feature ID, only look in that folder
1630
+ if (featureId) {
1631
+ featureFolders = featureFolders.filter(e => e.name === featureId || e.name.includes(featureId));
1632
+ if (featureFolders.length === 0) {
1633
+ console.log(` ⚠️ Feature folder ${featureId} not found in ${project}`);
1634
+ }
1635
+ }
1636
+ for (const folder of featureFolders) {
1637
+ const featurePath = path.join(projectSpecsPath, folder.name);
1638
+ // Find all user story files
1639
+ const files = await fs.readdir(featurePath);
1640
+ const userStoryFiles = files.filter(f => f.match(/^us-\d+-.+\.md$/));
1641
+ for (const file of userStoryFiles) {
1642
+ const filePath = path.join(featurePath, file);
1643
+ const content = await fs.readFile(filePath, 'utf-8');
1644
+ // Extract user story ID from content or filename
1645
+ const idMatch = content.match(/^id:\s*(US-\d+)/m) || file.match(/us-(\d+)/);
1646
+ if (!idMatch)
1647
+ continue;
1648
+ const userStoryId = idMatch[1].startsWith('US') ? idMatch[1] : `US-${idMatch[1].padStart(3, '0')}`;
1649
+ const acsToComplete = completedACs.get(userStoryId);
1650
+ if (!acsToComplete || acsToComplete.size === 0)
1651
+ continue;
1652
+ // Update AC checkboxes
1653
+ let updatedContent = content;
1654
+ let hasUpdates = false;
1655
+ for (const acId of acsToComplete) {
1656
+ // Find and update the AC checkbox
1657
+ // Pattern: - [ ] **AC-XXX**: Description
1658
+ const acPattern = new RegExp(`^(\\s*-\\s*)\\[\\s\\](\\s*\\*\\*${acId}\\*\\*:.*)$`, 'gm');
1659
+ // Check if pattern exists before replacing
1660
+ const originalContent = updatedContent;
1661
+ updatedContent = updatedContent.replace(acPattern, '$1[x]$2');
1662
+ // Check if replacement actually happened
1663
+ if (originalContent !== updatedContent) {
1664
+ hasUpdates = true;
1665
+ console.log(` ✅ Updated ${acId} in ${userStoryId} (${project}/${folder.name})`);
1666
+ }
1667
+ else {
1668
+ // Debug: pattern didn't match
1669
+ console.log(` ⚠️ Could not find ${acId} in ${userStoryId} (${project}/${folder.name})`);
1670
+ // Try to find what format the AC actually has
1671
+ const lineWithAc = updatedContent.split('\n').find(line => line.includes(acId));
1672
+ if (lineWithAc) {
1673
+ console.log(` Found line: "${lineWithAc}"`);
1674
+ }
1675
+ }
1676
+ }
1677
+ if (hasUpdates) {
1678
+ await fs.writeFile(filePath, updatedContent, 'utf-8');
1679
+ updatedCount++;
1680
+ }
817
1681
  }
818
1682
  }
819
1683
  }
820
- return mapping;
1684
+ if (updatedCount > 0) {
1685
+ console.log(` ✅ Updated ${updatedCount} user story file(s) with completed acceptance criteria`);
1686
+ }
1687
+ }
1688
+ /**
1689
+ * Get all project folders from specs
1690
+ */
1691
+ async getProjectFolders() {
1692
+ const specsPath = path.join(this.projectRoot, '.specweave', 'docs', 'internal', 'specs');
1693
+ if (!await fs.pathExists(specsPath)) {
1694
+ return ['default'];
1695
+ }
1696
+ const entries = await fs.readdir(specsPath, { withFileTypes: true });
1697
+ const projectFolders = entries
1698
+ .filter(e => e.isDirectory() && !e.name.startsWith('_'))
1699
+ .map(e => e.name);
1700
+ return projectFolders.length > 0 ? projectFolders : ['default'];
1701
+ }
1702
+ /**
1703
+ * Detect project type for AC generation (backend, frontend, mobile, infrastructure, generic)
1704
+ */
1705
+ detectProjectType(context) {
1706
+ const projectId = context.projectId.toLowerCase();
1707
+ const keywords = context.keywords.map(k => k.toLowerCase());
1708
+ const techStack = context.techStack.map(t => t.toLowerCase());
1709
+ // Direct match on project ID
1710
+ if (projectId.includes('backend') || projectId.includes('api') || projectId.includes('service')) {
1711
+ return 'backend';
1712
+ }
1713
+ if (projectId.includes('frontend') || projectId.includes('web') || projectId.includes('ui')) {
1714
+ return 'frontend';
1715
+ }
1716
+ if (projectId.includes('mobile') || projectId.includes('ios') || projectId.includes('android')) {
1717
+ return 'mobile';
1718
+ }
1719
+ if (projectId.includes('infra') || projectId.includes('devops') || projectId.includes('deployment')) {
1720
+ return 'infrastructure';
1721
+ }
1722
+ // Keyword-based detection
1723
+ const backendKeywords = ['api', 'backend', 'service', 'server', 'database'];
1724
+ const frontendKeywords = ['frontend', 'ui', 'component', 'react', 'web'];
1725
+ const mobileKeywords = ['mobile', 'ios', 'android', 'react-native'];
1726
+ const infraKeywords = ['infrastructure', 'devops', 'deployment', 'ci/cd', 'kubernetes'];
1727
+ if (keywords.some(k => backendKeywords.includes(k)))
1728
+ return 'backend';
1729
+ if (keywords.some(k => frontendKeywords.includes(k)))
1730
+ return 'frontend';
1731
+ if (keywords.some(k => mobileKeywords.includes(k)))
1732
+ return 'mobile';
1733
+ if (keywords.some(k => infraKeywords.includes(k)))
1734
+ return 'infrastructure';
1735
+ // Tech stack-based detection
1736
+ const backendTech = ['node.js', 'express', 'postgresql', 'mongodb', 'redis'];
1737
+ const frontendTech = ['react', 'next.js', 'vue', 'angular', 'typescript'];
1738
+ const mobileTech = ['react native', 'swift', 'kotlin', 'flutter'];
1739
+ const infraTech = ['docker', 'kubernetes', 'terraform', 'ansible'];
1740
+ if (techStack.some(t => backendTech.some(bt => t.includes(bt))))
1741
+ return 'backend';
1742
+ if (techStack.some(t => frontendTech.some(ft => t.includes(ft))))
1743
+ return 'frontend';
1744
+ if (techStack.some(t => mobileTech.some(mt => t.includes(mt))))
1745
+ return 'mobile';
1746
+ if (techStack.some(t => infraTech.some(it => t.includes(it))))
1747
+ return 'infrastructure';
1748
+ // Default to generic if no match
1749
+ return 'generic';
1750
+ }
1751
+ /**
1752
+ * Find the actual feature folder that matches an increment
1753
+ * Handles mapping between different naming conventions
1754
+ */
1755
+ async findActualFeatureFolder(incrementId, declaredFeature) {
1756
+ const specsPath = path.join(this.projectRoot, '.specweave', 'docs', 'internal', 'specs');
1757
+ const projects = await this.getProjectFolders();
1758
+ // Extract increment number
1759
+ const incrementNum = incrementId.match(/^(\d+)/)?.[1];
1760
+ for (const project of projects) {
1761
+ const projectPath = path.join(specsPath, project);
1762
+ if (!await fs.pathExists(projectPath))
1763
+ continue;
1764
+ const entries = await fs.readdir(projectPath);
1765
+ const featureFolders = entries.filter(e => e.startsWith('FS-'));
1766
+ // Strategy 1: Direct match
1767
+ if (featureFolders.includes(declaredFeature)) {
1768
+ return declaredFeature;
1769
+ }
1770
+ // Strategy 2: Match by increment number (FS-031 for increment 0031)
1771
+ if (incrementNum) {
1772
+ const paddedNum = incrementNum.padStart(3, '0');
1773
+ const numericFeature = `FS-${paddedNum}`;
1774
+ if (featureFolders.includes(numericFeature)) {
1775
+ return numericFeature;
1776
+ }
1777
+ }
1778
+ // Strategy 3: Match by suffix (e.g., FS-XXX-external-tool-sync matches external-tool-sync)
1779
+ const incrementSuffix = incrementId.replace(/^\d+-/, '');
1780
+ for (const folder of featureFolders) {
1781
+ if (folder.includes(incrementSuffix)) {
1782
+ return folder;
1783
+ }
1784
+ }
1785
+ // Strategy 4: Match by date pattern
1786
+ if (declaredFeature.match(/^FS-\d{2}-\d{2}-\d{2}-/)) {
1787
+ // Find folder with same date prefix
1788
+ const datePrefix = declaredFeature.match(/^FS-\d{2}-\d{2}-\d{2}/)?.[0];
1789
+ for (const folder of featureFolders) {
1790
+ if (folder.startsWith(datePrefix)) {
1791
+ return folder;
1792
+ }
1793
+ }
1794
+ }
1795
+ // Strategy 5: Smart content-based matching
1796
+ // Look for feature folders containing user stories that reference this increment
1797
+ for (const folder of featureFolders) {
1798
+ const featurePath = path.join(projectPath, folder);
1799
+ const files = await fs.readdir(featurePath);
1800
+ const userStoryFiles = files.filter(f => f.match(/^us-\d+-.+\.md$/));
1801
+ for (const file of userStoryFiles) {
1802
+ const filePath = path.join(featurePath, file);
1803
+ const content = await fs.readFile(filePath, 'utf-8');
1804
+ // Check if this user story references our increment
1805
+ if (content.includes(`increments/${incrementId}/`) ||
1806
+ content.includes(`Increment**: [${incrementId}]`)) {
1807
+ console.log(` 🎯 Found feature folder ${folder} by content match in ${file}`);
1808
+ return folder;
1809
+ }
1810
+ }
1811
+ }
1812
+ // Strategy 6: Fuzzy match on increment name parts
1813
+ // Split increment name into words and look for feature folders with matching words
1814
+ const incrementWords = incrementId.toLowerCase().split(/[-_]/);
1815
+ let bestMatch = null;
1816
+ for (const folder of featureFolders) {
1817
+ const folderWords = folder.toLowerCase().split(/[-_]/);
1818
+ let matchScore = 0;
1819
+ // Count matching words (ignoring numbers and 'FS' prefix)
1820
+ for (const word of incrementWords) {
1821
+ if (word.match(/^\d+$/) || word === 'fs')
1822
+ continue;
1823
+ if (folderWords.includes(word)) {
1824
+ matchScore++;
1825
+ }
1826
+ }
1827
+ // Update best match if this score is better
1828
+ if (matchScore > 0 && (!bestMatch || matchScore > bestMatch.score)) {
1829
+ bestMatch = { folder, score: matchScore };
1830
+ }
1831
+ }
1832
+ if (bestMatch && bestMatch.score >= 2) { // Require at least 2 word matches
1833
+ console.log(` 🎯 Found feature folder ${bestMatch.folder} by fuzzy match (score: ${bestMatch.score})`);
1834
+ return bestMatch.folder;
1835
+ }
1836
+ }
1837
+ return null;
821
1838
  }
822
1839
  }
823
1840
  //# sourceMappingURL=spec-distributor.js.map