specweave 0.21.3 → 0.22.2

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 (235) hide show
  1. package/CLAUDE.md +409 -6
  2. package/README.md +38 -8
  3. package/bin/specweave.js +5 -8
  4. package/dist/plugins/specweave-github/lib/CodeValidator.d.ts +101 -0
  5. package/dist/plugins/specweave-github/lib/CodeValidator.d.ts.map +1 -0
  6. package/dist/plugins/specweave-github/lib/CodeValidator.js +219 -0
  7. package/dist/plugins/specweave-github/lib/CodeValidator.js.map +1 -0
  8. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts +182 -0
  9. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts.map +1 -0
  10. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js +603 -0
  11. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js.map +1 -0
  12. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +10 -0
  13. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
  14. package/dist/plugins/specweave-github/lib/github-client-v2.js +26 -0
  15. package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
  16. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
  17. package/dist/plugins/specweave-github/lib/task-sync.js +7 -0
  18. package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
  19. package/dist/plugins/specweave-github/lib/types.d.ts +34 -0
  20. package/dist/plugins/specweave-github/lib/types.d.ts.map +1 -1
  21. package/dist/src/cli/commands/init.d.ts.map +1 -1
  22. package/dist/src/cli/commands/init.js +60 -5
  23. package/dist/src/cli/commands/init.js.map +1 -1
  24. package/dist/src/cli/commands/migrate-to-profiles.d.ts +1 -0
  25. package/dist/src/cli/commands/migrate-to-profiles.d.ts.map +1 -1
  26. package/dist/src/cli/commands/migrate-to-profiles.js +12 -1
  27. package/dist/src/cli/commands/migrate-to-profiles.js.map +1 -1
  28. package/dist/src/cli/commands/next-command.d.ts +52 -0
  29. package/dist/src/cli/commands/next-command.d.ts.map +1 -0
  30. package/dist/src/cli/commands/next-command.js +204 -0
  31. package/dist/src/cli/commands/next-command.js.map +1 -0
  32. package/dist/src/cli/commands/sync-specs.d.ts +16 -0
  33. package/dist/src/cli/commands/sync-specs.d.ts.map +1 -0
  34. package/dist/src/cli/commands/sync-specs.js +130 -0
  35. package/dist/src/cli/commands/sync-specs.js.map +1 -0
  36. package/dist/src/cli/count-tasks.d.ts +20 -0
  37. package/dist/src/cli/count-tasks.d.ts.map +1 -0
  38. package/dist/src/cli/count-tasks.js +50 -0
  39. package/dist/src/cli/count-tasks.js.map +1 -0
  40. package/dist/src/config/ConfigManager.d.ts.map +1 -1
  41. package/dist/src/config/ConfigManager.js +2 -1
  42. package/dist/src/config/ConfigManager.js.map +1 -1
  43. package/dist/src/config/types.d.ts +58 -58
  44. package/dist/src/core/cicd/state-manager.d.ts +8 -0
  45. package/dist/src/core/cicd/state-manager.d.ts.map +1 -1
  46. package/dist/src/core/cicd/state-manager.js +60 -15
  47. package/dist/src/core/cicd/state-manager.js.map +1 -1
  48. package/dist/src/core/cost-tracker.d.ts.map +1 -1
  49. package/dist/src/core/cost-tracker.js +2 -1
  50. package/dist/src/core/cost-tracker.js.map +1 -1
  51. package/dist/src/core/iac/template-engine.d.ts.map +1 -1
  52. package/dist/src/core/iac/template-engine.js +28 -0
  53. package/dist/src/core/iac/template-engine.js.map +1 -1
  54. package/dist/src/core/iac/template-generator.d.ts +53 -0
  55. package/dist/src/core/iac/template-generator.d.ts.map +1 -0
  56. package/dist/src/core/iac/template-generator.js +125 -0
  57. package/dist/src/core/iac/template-generator.js.map +1 -0
  58. package/dist/src/core/increment/status-auto-transition.js +3 -3
  59. package/dist/src/core/increment/status-auto-transition.js.map +1 -1
  60. package/dist/src/core/living-docs/CodeValidator.js +1 -1
  61. package/dist/src/core/living-docs/CodeValidator.js.map +1 -1
  62. package/dist/src/core/living-docs/CompletionPropagator.d.ts.map +1 -1
  63. package/dist/src/core/living-docs/CompletionPropagator.js +4 -3
  64. package/dist/src/core/living-docs/CompletionPropagator.js.map +1 -1
  65. package/dist/src/core/living-docs/SpecDistributor.d.ts +5 -0
  66. package/dist/src/core/living-docs/SpecDistributor.d.ts.map +1 -1
  67. package/dist/src/core/living-docs/SpecDistributor.js +12 -0
  68. package/dist/src/core/living-docs/SpecDistributor.js.map +1 -1
  69. package/dist/src/core/living-docs/content-distributor.d.ts.map +1 -1
  70. package/dist/src/core/living-docs/content-distributor.js +11 -1
  71. package/dist/src/core/living-docs/content-distributor.js.map +1 -1
  72. package/dist/src/core/living-docs/living-docs-sync.d.ts +166 -0
  73. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -0
  74. package/dist/src/core/living-docs/living-docs-sync.js +726 -0
  75. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -0
  76. package/dist/src/core/living-docs/project-detector.d.ts.map +1 -1
  77. package/dist/src/core/living-docs/project-detector.js +38 -0
  78. package/dist/src/core/living-docs/project-detector.js.map +1 -1
  79. package/dist/src/core/living-docs/task-project-specific-generator.d.ts +7 -3
  80. package/dist/src/core/living-docs/task-project-specific-generator.d.ts.map +1 -1
  81. package/dist/src/core/living-docs/task-project-specific-generator.js +40 -24
  82. package/dist/src/core/living-docs/task-project-specific-generator.js.map +1 -1
  83. package/dist/src/core/plugin-loader.d.ts +7 -0
  84. package/dist/src/core/plugin-loader.d.ts.map +1 -1
  85. package/dist/src/core/plugin-loader.js +18 -1
  86. package/dist/src/core/plugin-loader.js.map +1 -1
  87. package/dist/src/core/serverless/platform-data-loader.d.ts +8 -0
  88. package/dist/src/core/serverless/platform-data-loader.d.ts.map +1 -1
  89. package/dist/src/core/serverless/platform-data-loader.js +14 -0
  90. package/dist/src/core/serverless/platform-data-loader.js.map +1 -1
  91. package/dist/src/core/serverless/types.d.ts +1 -1
  92. package/dist/src/core/serverless/types.d.ts.map +1 -1
  93. package/dist/src/core/status-line/status-line-manager.d.ts +6 -2
  94. package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
  95. package/dist/src/core/status-line/status-line-manager.js +11 -5
  96. package/dist/src/core/status-line/status-line-manager.js.map +1 -1
  97. package/dist/src/core/status-line/task-counter.d.ts +69 -0
  98. package/dist/src/core/status-line/task-counter.d.ts.map +1 -0
  99. package/dist/src/core/status-line/task-counter.js +107 -0
  100. package/dist/src/core/status-line/task-counter.js.map +1 -0
  101. package/dist/src/core/types/config.d.ts +23 -0
  102. package/dist/src/core/types/config.d.ts.map +1 -1
  103. package/dist/src/core/types/config.js +10 -0
  104. package/dist/src/core/types/config.js.map +1 -1
  105. package/dist/src/core/workflow/autonomous-executor.d.ts +111 -0
  106. package/dist/src/core/workflow/autonomous-executor.d.ts.map +1 -0
  107. package/dist/src/core/workflow/autonomous-executor.js +275 -0
  108. package/dist/src/core/workflow/autonomous-executor.js.map +1 -0
  109. package/dist/src/core/workflow/backlog-scanner.d.ts +94 -0
  110. package/dist/src/core/workflow/backlog-scanner.d.ts.map +1 -0
  111. package/dist/src/core/workflow/backlog-scanner.js +170 -0
  112. package/dist/src/core/workflow/backlog-scanner.js.map +1 -0
  113. package/dist/src/core/workflow/command-invoker.d.ts +86 -0
  114. package/dist/src/core/workflow/command-invoker.d.ts.map +1 -0
  115. package/dist/src/core/workflow/command-invoker.js +131 -0
  116. package/dist/src/core/workflow/command-invoker.js.map +1 -0
  117. package/dist/src/core/workflow/cost-estimator.d.ts +120 -0
  118. package/dist/src/core/workflow/cost-estimator.d.ts.map +1 -0
  119. package/dist/src/core/workflow/cost-estimator.js +222 -0
  120. package/dist/src/core/workflow/cost-estimator.js.map +1 -0
  121. package/dist/src/core/workflow/index.d.ts +20 -0
  122. package/dist/src/core/workflow/index.d.ts.map +1 -0
  123. package/dist/src/core/workflow/index.js +24 -0
  124. package/dist/src/core/workflow/index.js.map +1 -0
  125. package/dist/src/core/workflow/state-manager.d.ts +107 -0
  126. package/dist/src/core/workflow/state-manager.d.ts.map +1 -0
  127. package/dist/src/core/workflow/state-manager.js +126 -0
  128. package/dist/src/core/workflow/state-manager.js.map +1 -0
  129. package/dist/src/core/workflow/workflow-orchestrator.d.ts +93 -0
  130. package/dist/src/core/workflow/workflow-orchestrator.d.ts.map +1 -0
  131. package/dist/src/core/workflow/workflow-orchestrator.js +195 -0
  132. package/dist/src/core/workflow/workflow-orchestrator.js.map +1 -0
  133. package/dist/src/init/ArchitecturePresenter.d.ts +47 -0
  134. package/dist/src/init/ArchitecturePresenter.d.ts.map +1 -0
  135. package/dist/src/init/ArchitecturePresenter.js +180 -0
  136. package/dist/src/init/ArchitecturePresenter.js.map +1 -0
  137. package/dist/src/init/InitFlow.d.ts.map +1 -1
  138. package/dist/src/init/InitFlow.js +30 -1
  139. package/dist/src/init/InitFlow.js.map +1 -1
  140. package/dist/src/init/architecture/CostEstimator.d.ts +52 -0
  141. package/dist/src/init/architecture/CostEstimator.d.ts.map +1 -0
  142. package/dist/src/init/architecture/CostEstimator.js +107 -0
  143. package/dist/src/init/architecture/CostEstimator.js.map +1 -0
  144. package/dist/src/init/architecture/InfrastructureMapper.d.ts +41 -0
  145. package/dist/src/init/architecture/InfrastructureMapper.d.ts.map +1 -0
  146. package/dist/src/init/architecture/InfrastructureMapper.js +140 -0
  147. package/dist/src/init/architecture/InfrastructureMapper.js.map +1 -0
  148. package/dist/src/init/architecture/ProjectGenerator.d.ts +44 -0
  149. package/dist/src/init/architecture/ProjectGenerator.d.ts.map +1 -0
  150. package/dist/src/init/architecture/ProjectGenerator.js +216 -0
  151. package/dist/src/init/architecture/ProjectGenerator.js.map +1 -0
  152. package/dist/src/init/architecture/types.d.ts +10 -10
  153. package/dist/src/init/research/src/config/types.d.ts +8 -8
  154. package/dist/src/metrics/dora-calculator.js +2 -2
  155. package/dist/src/metrics/dora-calculator.js.map +1 -1
  156. package/dist/src/utils/pricing-constants.d.ts +5 -2
  157. package/dist/src/utils/pricing-constants.d.ts.map +1 -1
  158. package/dist/src/utils/pricing-constants.js +3 -2
  159. package/dist/src/utils/pricing-constants.js.map +1 -1
  160. package/package.json +9 -8
  161. package/plugins/specweave/agents/infrastructure/AGENT.md +88 -46
  162. package/plugins/specweave/agents/pm/AGENT.md +58 -1
  163. package/plugins/specweave/commands/specweave-archive-features.md +1 -1
  164. package/plugins/specweave/commands/specweave-archive-increments.md +1 -1
  165. package/plugins/specweave/commands/specweave-check-hooks.md +5 -0
  166. package/plugins/specweave/commands/specweave-done.md +12 -0
  167. package/plugins/specweave/commands/specweave-plan.md +1 -1
  168. package/plugins/specweave/commands/specweave-progress.md +108 -379
  169. package/plugins/specweave/commands/specweave-reopen.md +1 -1
  170. package/plugins/specweave/commands/specweave-restore-feature.md +1 -1
  171. package/plugins/specweave/commands/specweave-sync-specs.md +20 -48
  172. package/plugins/specweave/hooks/lib/update-status-line.sh +44 -35
  173. package/plugins/specweave/hooks/lib/validate-spec-status.sh +163 -0
  174. package/plugins/specweave/hooks/user-prompt-submit.sh +17 -35
  175. package/plugins/specweave/lib/hooks/update-tasks-md.js +52 -9
  176. package/plugins/specweave/lib/hooks/update-tasks-md.ts +77 -16
  177. package/plugins/specweave/templates/iac/aws-lambda/defaults.json +24 -0
  178. package/plugins/specweave/templates/iac/aws-lambda/templates/README.md.hbs +260 -0
  179. package/plugins/specweave/templates/iac/aws-lambda/templates/environments/dev.tfvars.hbs +34 -0
  180. package/plugins/specweave/templates/iac/aws-lambda/templates/environments/prod.tfvars.hbs +37 -0
  181. package/plugins/specweave/templates/iac/aws-lambda/templates/environments/staging.tfvars.hbs +35 -0
  182. package/plugins/specweave/templates/iac/aws-lambda/templates/outputs.tf.hbs +77 -0
  183. package/plugins/specweave/templates/iac/aws-lambda/templates/providers.tf.hbs +36 -0
  184. package/plugins/specweave/templates/iac/aws-lambda/templates/variables.tf.hbs +115 -0
  185. package/plugins/specweave/templates/iac/azure-functions/defaults.json +25 -0
  186. package/plugins/specweave/templates/iac/azure-functions/templates/README.md.hbs +268 -0
  187. package/plugins/specweave/templates/iac/azure-functions/templates/environments/dev.tfvars.hbs +34 -0
  188. package/plugins/specweave/templates/iac/azure-functions/templates/environments/prod.tfvars.hbs +46 -0
  189. package/plugins/specweave/templates/iac/azure-functions/templates/environments/staging.tfvars.hbs +34 -0
  190. package/plugins/specweave/templates/iac/azure-functions/templates/main.tf.hbs +225 -0
  191. package/plugins/specweave/templates/iac/azure-functions/templates/outputs.tf.hbs +89 -0
  192. package/plugins/specweave/templates/iac/azure-functions/templates/provider.tf.hbs +27 -0
  193. package/plugins/specweave/templates/iac/azure-functions/templates/providers.tf.hbs +35 -0
  194. package/plugins/specweave/templates/iac/azure-functions/templates/variables.tf.hbs +124 -0
  195. package/plugins/specweave/templates/iac/firebase/defaults.json +29 -0
  196. package/plugins/specweave/templates/iac/firebase/templates/README.md.hbs +35 -0
  197. package/plugins/specweave/templates/iac/firebase/templates/environments/dev.tfvars.hbs +7 -0
  198. package/plugins/specweave/templates/iac/firebase/templates/environments/prod.tfvars.hbs +7 -0
  199. package/plugins/specweave/templates/iac/firebase/templates/environments/staging.tfvars.hbs +7 -0
  200. package/plugins/specweave/templates/iac/firebase/templates/main.tf.hbs +90 -0
  201. package/plugins/specweave/templates/iac/firebase/templates/outputs.tf.hbs +15 -0
  202. package/plugins/specweave/templates/iac/firebase/templates/providers.tf.hbs +23 -0
  203. package/plugins/specweave/templates/iac/firebase/templates/variables.tf.hbs +42 -0
  204. package/plugins/specweave/templates/iac/gcp-cloud-functions/defaults.json +26 -0
  205. package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/README.md.hbs +299 -0
  206. package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/environments/dev.tfvars.hbs +36 -0
  207. package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/environments/prod.tfvars.hbs +48 -0
  208. package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/environments/staging.tfvars.hbs +41 -0
  209. package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/main.tf.hbs +192 -0
  210. package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/outputs.tf.hbs +66 -0
  211. package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/providers.tf.hbs +25 -0
  212. package/plugins/specweave/templates/iac/gcp-cloud-functions/templates/variables.tf.hbs +119 -0
  213. package/plugins/specweave/templates/iac/supabase/defaults.json +15 -0
  214. package/plugins/specweave/templates/iac/supabase/templates/README.md.hbs +46 -0
  215. package/plugins/specweave/templates/iac/supabase/templates/main.tf.hbs +50 -0
  216. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  217. package/plugins/specweave-github/agents/github-manager/AGENT.md +39 -7
  218. package/plugins/specweave-github/commands/specweave-github-create-issue.md +5 -5
  219. package/plugins/specweave-github/lib/CodeValidator.js +195 -0
  220. package/plugins/specweave-github/lib/CodeValidator.ts +284 -0
  221. package/plugins/specweave-github/lib/ThreeLayerSyncManager.js +545 -0
  222. package/plugins/specweave-github/lib/ThreeLayerSyncManager.ts +809 -0
  223. package/plugins/specweave-github/lib/github-client-v2.js +29 -0
  224. package/plugins/specweave-github/lib/github-client-v2.ts +30 -0
  225. package/plugins/specweave-github/lib/task-sync.js +4 -0
  226. package/plugins/specweave-github/lib/task-sync.ts +7 -0
  227. package/plugins/specweave-github/lib/types.ts +38 -0
  228. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
  229. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +3222 -0
  230. package/src/templates/AGENTS.md.template +22 -1
  231. package/src/templates/CLAUDE.md.template +31 -0
  232. package/dist/src/core/living-docs/ThreeLayerSyncManager.d.ts +0 -116
  233. package/dist/src/core/living-docs/ThreeLayerSyncManager.d.ts.map +0 -1
  234. package/dist/src/core/living-docs/ThreeLayerSyncManager.js +0 -356
  235. package/dist/src/core/living-docs/ThreeLayerSyncManager.js.map +0 -1
@@ -0,0 +1,726 @@
1
+ /**
2
+ * Living Docs Sync - Simplified sync mechanism
3
+ *
4
+ * Syncs increment specs to living docs structure:
5
+ * - .specweave/docs/internal/specs/_features/FS-XXX/FEATURE.md
6
+ * - .specweave/docs/internal/specs/specweave/FS-XXX/README.md
7
+ * - .specweave/docs/internal/specs/specweave/FS-XXX/us-*.md
8
+ *
9
+ * Uses FeatureIDManager for automatic feature ID assignment (greenfield vs brownfield)
10
+ */
11
+ import fs from 'fs-extra';
12
+ import path from 'path';
13
+ import yaml from 'yaml';
14
+ import { FeatureIDManager } from './feature-id-manager.js';
15
+ import { TaskProjectSpecificGenerator } from './task-project-specific-generator.js';
16
+ export class LivingDocsSync {
17
+ constructor(projectRoot) {
18
+ this.projectRoot = projectRoot;
19
+ this.featureIdManager = new FeatureIDManager(projectRoot);
20
+ }
21
+ /**
22
+ * Sync an increment to living docs
23
+ */
24
+ async syncIncrement(incrementId, options = {}) {
25
+ const result = {
26
+ success: false,
27
+ featureId: '',
28
+ incrementId,
29
+ filesCreated: [],
30
+ filesUpdated: [],
31
+ errors: []
32
+ };
33
+ try {
34
+ // Step 1: Build feature registry (auto-generates IDs for greenfield)
35
+ await this.featureIdManager.buildRegistry();
36
+ // Step 2: Get/assign feature ID
37
+ const featureId = await this.getFeatureIdForIncrement(incrementId);
38
+ result.featureId = featureId;
39
+ console.log(`📚 Syncing ${incrementId} → ${featureId}...`);
40
+ // Step 3: Parse increment spec
41
+ const parsed = await this.parseIncrementSpec(incrementId);
42
+ // Step 4: Create living docs structure
43
+ const basePath = path.join(this.projectRoot, '.specweave/docs/internal/specs');
44
+ // Create _features/FS-XXX/FEATURE.md
45
+ const featurePath = path.join(basePath, '_features', featureId);
46
+ const featureFile = path.join(featurePath, 'FEATURE.md');
47
+ if (!options.dryRun) {
48
+ await fs.ensureDir(featurePath);
49
+ const featureContent = this.generateFeatureFile(featureId, parsed, incrementId);
50
+ await fs.writeFile(featureFile, featureContent, 'utf-8');
51
+ result.filesCreated.push(featureFile);
52
+ }
53
+ else {
54
+ result.filesCreated.push(featureFile + ' (dry-run)');
55
+ }
56
+ // Create specweave/FS-XXX/README.md
57
+ const projectPath = path.join(basePath, 'specweave', featureId);
58
+ const readmePath = path.join(projectPath, 'README.md');
59
+ if (!options.dryRun) {
60
+ await fs.ensureDir(projectPath);
61
+ const readmeContent = this.generateReadmeFile(featureId, parsed, incrementId);
62
+ await fs.writeFile(readmePath, readmeContent, 'utf-8');
63
+ result.filesCreated.push(readmePath);
64
+ }
65
+ else {
66
+ result.filesCreated.push(readmePath + ' (dry-run)');
67
+ }
68
+ // Create user story files
69
+ for (const story of parsed.userStories) {
70
+ // CRITICAL: Find existing file by US-ID first to prevent duplicates
71
+ const existingFile = await this.findExistingUserStoryFile(projectPath, story.id);
72
+ let storyFile;
73
+ if (existingFile) {
74
+ // Reuse existing file (prevent duplicate creation)
75
+ storyFile = path.join(projectPath, existingFile);
76
+ console.log(` ♻️ Reusing existing file: ${existingFile}`);
77
+ }
78
+ else {
79
+ // Create new file with standardized naming
80
+ const storySlug = story.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
81
+ storyFile = path.join(projectPath, `${story.id.toLowerCase()}-${storySlug}.md`);
82
+ }
83
+ if (!options.dryRun) {
84
+ const storyContent = this.generateUserStoryFile(story, featureId, incrementId, parsed);
85
+ await fs.writeFile(storyFile, storyContent, 'utf-8');
86
+ result.filesCreated.push(storyFile);
87
+ }
88
+ else {
89
+ result.filesCreated.push(storyFile + ' (dry-run)');
90
+ }
91
+ }
92
+ // Step 5: Clean up duplicates and temp files BEFORE syncing tasks
93
+ if (!options.dryRun) {
94
+ await this.cleanupDuplicateFiles(featureId, projectPath);
95
+ await this.cleanupTempFiles(projectPath);
96
+ }
97
+ // Step 6: Sync tasks from increment to user stories
98
+ if (!options.dryRun) {
99
+ await this.syncTasksToUserStories(incrementId, featureId, parsed.userStories, projectPath);
100
+ }
101
+ // Step 7: Sync to external tools (GitHub, JIRA, ADO)
102
+ if (!options.dryRun) {
103
+ await this.syncToExternalTools(incrementId, featureId, projectPath);
104
+ }
105
+ // Step 8: Final cleanup (remove any temp files created during sync)
106
+ if (!options.dryRun) {
107
+ await this.cleanupTempFiles(projectPath);
108
+ }
109
+ result.success = true;
110
+ console.log(`✅ Synced ${incrementId} → ${featureId}`);
111
+ console.log(` Created: ${result.filesCreated.length} files`);
112
+ return result;
113
+ }
114
+ catch (error) {
115
+ result.errors.push(`Sync failed: ${error}`);
116
+ console.error(`❌ Sync failed for ${incrementId}:`, error);
117
+ return result;
118
+ }
119
+ }
120
+ /**
121
+ * Get feature ID for increment (auto-generates for greenfield)
122
+ *
123
+ * CRITICAL: Validates feature ID format matches increment type (greenfield vs brownfield)
124
+ * - Greenfield (SpecWeave-native): FS-XXX (e.g., FS-031, FS-043)
125
+ * - Brownfield (imported): FS-YY-MM-DD-name (e.g., FS-25-11-14-jira-epic)
126
+ */
127
+ async getFeatureIdForIncrement(incrementId) {
128
+ // Extract increment number (e.g., "0040-name" → 40)
129
+ const match = incrementId.match(/^(\d{4})-/);
130
+ if (!match) {
131
+ throw new Error(`Invalid increment ID format: ${incrementId}`);
132
+ }
133
+ const num = parseInt(match[1], 10);
134
+ // Check if increment has explicit feature ID
135
+ const metadataPath = path.join(this.projectRoot, '.specweave/increments', incrementId, 'metadata.json');
136
+ if (await fs.pathExists(metadataPath)) {
137
+ const metadata = await fs.readJson(metadataPath);
138
+ // Check if brownfield (imported from external tool)
139
+ const isBrownfield = metadata.imported === true || metadata.source === 'external';
140
+ if (metadata.feature) {
141
+ // Validate format matches increment type
142
+ const isDateFormat = /^FS-\d{2}-\d{2}-\d{2}/.test(metadata.feature);
143
+ const isIncrementFormat = /^FS-\d{3}$/.test(metadata.feature);
144
+ if (isBrownfield && isDateFormat) {
145
+ // ✅ Brownfield with correct date format
146
+ return metadata.feature;
147
+ }
148
+ else if (!isBrownfield && isIncrementFormat) {
149
+ // ✅ Greenfield with correct increment format
150
+ return metadata.feature;
151
+ }
152
+ else {
153
+ // ⚠️ Format mismatch - log warning and auto-generate correct format
154
+ console.warn(`⚠️ Feature ID format mismatch for ${incrementId}:`);
155
+ console.warn(` Found: ${metadata.feature}`);
156
+ console.warn(` Expected: ${isBrownfield ? 'FS-YY-MM-DD-name (brownfield)' : 'FS-XXX (greenfield)'}`);
157
+ console.warn(` Auto-generating correct format...`);
158
+ // Fall through to auto-generation
159
+ }
160
+ }
161
+ }
162
+ // Auto-generate for greenfield: FS-040, FS-041, etc.
163
+ const autoGenId = `FS-${String(num).padStart(3, '0')}`;
164
+ console.log(` 📝 Generated feature ID: ${autoGenId}`);
165
+ return autoGenId;
166
+ }
167
+ /**
168
+ * Parse increment spec.md
169
+ */
170
+ async parseIncrementSpec(incrementId) {
171
+ const specPath = path.join(this.projectRoot, '.specweave/increments', incrementId, 'spec.md');
172
+ if (!await fs.pathExists(specPath)) {
173
+ throw new Error(`Spec file not found: ${specPath}`);
174
+ }
175
+ const content = await fs.readFile(specPath, 'utf-8');
176
+ // Extract frontmatter
177
+ let frontmatter = {};
178
+ let bodyContent = content;
179
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
180
+ if (frontmatterMatch) {
181
+ try {
182
+ frontmatter = yaml.parse(frontmatterMatch[1]) || {};
183
+ bodyContent = content.slice(frontmatterMatch[0].length).trim();
184
+ }
185
+ catch (error) {
186
+ console.warn(`Failed to parse frontmatter for ${incrementId}`);
187
+ }
188
+ }
189
+ // Extract title
190
+ let title = frontmatter.title || '';
191
+ if (!title) {
192
+ const headingMatch = bodyContent.match(/^#\s+(.+)$/m);
193
+ if (headingMatch) {
194
+ title = headingMatch[1].replace(/^(SPEC-\d+:|Increment\s+\d+:)\s*/, '').trim();
195
+ }
196
+ }
197
+ if (!title) {
198
+ title = incrementId.replace(/^\d+-/, '').split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
199
+ }
200
+ // Extract overview
201
+ let overview = '';
202
+ const overviewMatch = bodyContent.match(/##\s+(?:Overview|Problem Statement|Quick Overview)\s*\n+([\s\S]*?)(?=\n##|\Z)/i);
203
+ if (overviewMatch) {
204
+ overview = overviewMatch[1].trim().split('\n\n')[0];
205
+ }
206
+ // Extract user stories
207
+ const userStories = this.extractUserStories(bodyContent);
208
+ // Extract acceptance criteria
209
+ const acceptanceCriteria = this.extractAcceptanceCriteria(bodyContent);
210
+ return {
211
+ title,
212
+ overview,
213
+ status: frontmatter.status || 'planning',
214
+ priority: frontmatter.priority || 'P1',
215
+ created: frontmatter.created || new Date().toISOString().split('T')[0],
216
+ userStories,
217
+ acceptanceCriteria,
218
+ frontmatter
219
+ };
220
+ }
221
+ /**
222
+ * Extract user stories from spec content
223
+ */
224
+ extractUserStories(content) {
225
+ const stories = [];
226
+ const lines = content.split('\n');
227
+ for (let i = 0; i < lines.length; i++) {
228
+ const headingMatch = lines[i].match(/^###+\s+(US-\d+):\s+(.+)/);
229
+ if (!headingMatch)
230
+ continue;
231
+ const id = headingMatch[1];
232
+ const title = headingMatch[2];
233
+ // Collect all lines until next US heading or end
234
+ const storyLines = [];
235
+ for (let j = i + 1; j < lines.length; j++) {
236
+ if (lines[j].match(/^###+\s+US-\d+:/)) {
237
+ break; // Found next US heading
238
+ }
239
+ storyLines.push(lines[j]);
240
+ }
241
+ const storyContent = storyLines.join('\n');
242
+ // Extract description
243
+ let description = '';
244
+ const descMatch = storyContent.match(/\*\*As a\*\*\s*([^\n]+)\s*\n\*\*I want\*\*\s*([^\n]+)\s*\n\*\*So that\*\*\s*([^\n]+)/i);
245
+ if (descMatch) {
246
+ description = `**As a** ${descMatch[1].trim()}\n**I want** ${descMatch[2].trim()}\n**So that** ${descMatch[3].trim()}`;
247
+ }
248
+ // Extract acceptance criteria IDs
249
+ const acIds = [];
250
+ const acPattern = /AC-US\d+-\d+/g;
251
+ let acMatch;
252
+ while ((acMatch = acPattern.exec(storyContent)) !== null) {
253
+ if (!acIds.includes(acMatch[0])) {
254
+ acIds.push(acMatch[0]);
255
+ }
256
+ }
257
+ stories.push({
258
+ id,
259
+ title,
260
+ description,
261
+ acceptanceCriteria: acIds
262
+ });
263
+ }
264
+ return stories;
265
+ }
266
+ /**
267
+ * Extract acceptance criteria from spec content
268
+ */
269
+ extractAcceptanceCriteria(content) {
270
+ const criteria = [];
271
+ // Pattern: - [x] AC-US1-01: Description
272
+ const acPattern = /^[-*]\s+\[([ x])\]\s+(AC-US\d+-\d+):\s+(.+?)$/gm;
273
+ let match;
274
+ while ((match = acPattern.exec(content)) !== null) {
275
+ const completed = match[1] === 'x';
276
+ const id = match[2];
277
+ const description = match[3];
278
+ // Extract user story ID (AC-US1-01 → US-001)
279
+ const usMatch = id.match(/AC-US(\d+)-\d+/);
280
+ const userStoryId = usMatch ? `US-${usMatch[1].padStart(3, '0')}` : '';
281
+ criteria.push({
282
+ id,
283
+ userStoryId,
284
+ description,
285
+ completed
286
+ });
287
+ }
288
+ return criteria;
289
+ }
290
+ /**
291
+ * Generate FEATURE.md content
292
+ */
293
+ generateFeatureFile(featureId, parsed, incrementId) {
294
+ const lines = [];
295
+ // Frontmatter
296
+ lines.push('---');
297
+ lines.push(`id: ${featureId}`);
298
+ lines.push(`title: "${parsed.title}"`);
299
+ lines.push(`type: feature`);
300
+ lines.push(`status: ${parsed.status}`);
301
+ lines.push(`priority: ${parsed.priority}`);
302
+ lines.push(`created: ${parsed.created}`);
303
+ lines.push(`lastUpdated: ${new Date().toISOString().split('T')[0]}`);
304
+ lines.push('---');
305
+ lines.push('');
306
+ // Title
307
+ lines.push(`# ${parsed.title}`);
308
+ lines.push('');
309
+ // Overview
310
+ if (parsed.overview) {
311
+ lines.push('## Overview');
312
+ lines.push('');
313
+ lines.push(parsed.overview);
314
+ lines.push('');
315
+ }
316
+ // Implementation History
317
+ lines.push('## Implementation History');
318
+ lines.push('');
319
+ lines.push('| Increment | Status | Completion Date |');
320
+ lines.push('|-----------|--------|----------------|');
321
+ const statusEmoji = parsed.status === 'completed' ? '✅' : '⏳';
322
+ lines.push(`| [${incrementId}](../../../../increments/${incrementId}/spec.md) | ${statusEmoji} ${parsed.status} | ${parsed.created} |`);
323
+ lines.push('');
324
+ // User Stories
325
+ if (parsed.userStories.length > 0) {
326
+ lines.push('## User Stories');
327
+ lines.push('');
328
+ for (const story of parsed.userStories) {
329
+ const storySlug = story.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
330
+ const storyFile = `../../specweave/${featureId}/${story.id.toLowerCase()}-${storySlug}.md`;
331
+ lines.push(`- [${story.id}: ${story.title}](${storyFile})`);
332
+ }
333
+ lines.push('');
334
+ }
335
+ return lines.join('\n');
336
+ }
337
+ /**
338
+ * Generate README.md content
339
+ */
340
+ generateReadmeFile(featureId, parsed, incrementId) {
341
+ const lines = [];
342
+ lines.push('---');
343
+ lines.push(`id: ${featureId}-specweave`);
344
+ lines.push(`title: "${parsed.title} - SpecWeave Implementation"`);
345
+ lines.push(`feature: ${featureId}`);
346
+ lines.push(`project: specweave`);
347
+ lines.push(`type: feature-context`);
348
+ lines.push(`status: ${parsed.status}`);
349
+ lines.push('---');
350
+ lines.push('');
351
+ lines.push(`# ${parsed.title}`);
352
+ lines.push('');
353
+ lines.push(`**Feature**: [${featureId}](../../_features/${featureId}/FEATURE.md)`);
354
+ lines.push('');
355
+ if (parsed.overview) {
356
+ lines.push('## Overview');
357
+ lines.push('');
358
+ lines.push(parsed.overview);
359
+ lines.push('');
360
+ }
361
+ lines.push('## User Stories');
362
+ lines.push('');
363
+ lines.push('See user story files in this directory.');
364
+ lines.push('');
365
+ return lines.join('\n');
366
+ }
367
+ /**
368
+ * Generate user story file content
369
+ */
370
+ generateUserStoryFile(story, featureId, incrementId, parsed) {
371
+ const lines = [];
372
+ // Frontmatter
373
+ lines.push('---');
374
+ lines.push(`id: ${story.id}`);
375
+ lines.push(`feature: ${featureId}`);
376
+ lines.push(`title: "${story.title}"`);
377
+ lines.push(`status: ${parsed.status}`);
378
+ lines.push(`priority: ${parsed.priority}`);
379
+ lines.push(`created: ${parsed.created}`);
380
+ lines.push('---');
381
+ lines.push('');
382
+ // Title
383
+ lines.push(`# ${story.id}: ${story.title}`);
384
+ lines.push('');
385
+ // Feature link
386
+ lines.push(`**Feature**: [${featureId}](../../_features/${featureId}/FEATURE.md)`);
387
+ lines.push('');
388
+ // Description
389
+ if (story.description) {
390
+ lines.push(story.description);
391
+ lines.push('');
392
+ }
393
+ lines.push('---');
394
+ lines.push('');
395
+ // Acceptance Criteria
396
+ lines.push('## Acceptance Criteria');
397
+ lines.push('');
398
+ const storyCriteria = parsed.acceptanceCriteria.filter(ac => ac.userStoryId === story.id);
399
+ if (storyCriteria.length > 0) {
400
+ for (const ac of storyCriteria) {
401
+ const checkbox = ac.completed ? '[x]' : '[ ]';
402
+ lines.push(`- ${checkbox} **${ac.id}**: ${ac.description}`);
403
+ }
404
+ }
405
+ else {
406
+ lines.push('No acceptance criteria defined.');
407
+ }
408
+ lines.push('');
409
+ lines.push('---');
410
+ lines.push('');
411
+ // Implementation
412
+ lines.push('## Implementation');
413
+ lines.push('');
414
+ lines.push(`**Increment**: [${incrementId}](../../../../increments/${incrementId}/spec.md)`);
415
+ lines.push('');
416
+ lines.push('**Tasks**: See increment tasks.md for implementation details.');
417
+ lines.push('');
418
+ return lines.join('\n');
419
+ }
420
+ /**
421
+ * Sync tasks from increment to user story files
422
+ *
423
+ * Populates the ## Tasks section in each user story file with tasks from increment tasks.md
424
+ */
425
+ async syncTasksToUserStories(incrementId, featureId, userStories, projectPath) {
426
+ const taskGenerator = new TaskProjectSpecificGenerator(this.projectRoot);
427
+ for (const story of userStories) {
428
+ try {
429
+ // Generate project-specific tasks for this user story
430
+ const tasks = await taskGenerator.generateProjectSpecificTasks(incrementId, story.id, // e.g., "US-001"
431
+ undefined // No project filter (use all tasks mapped to this user story)
432
+ );
433
+ // Format tasks as markdown
434
+ const tasksMarkdown = taskGenerator.formatTasksAsMarkdown(tasks);
435
+ // Update user story file
436
+ const storySlug = story.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
437
+ const storyFile = path.join(projectPath, `${story.id.toLowerCase()}-${storySlug}.md`);
438
+ await this.updateTasksSection(storyFile, tasksMarkdown);
439
+ console.log(` ✅ Synced ${tasks.length} tasks to ${story.id}`);
440
+ }
441
+ catch (error) {
442
+ console.error(` ⚠️ Failed to sync tasks for ${story.id}:`, error);
443
+ // Continue with other user stories even if one fails
444
+ }
445
+ }
446
+ }
447
+ /**
448
+ * Update ## Tasks section in user story file
449
+ */
450
+ async updateTasksSection(userStoryFile, tasksMarkdown) {
451
+ const content = await fs.readFile(userStoryFile, 'utf-8');
452
+ // Replace existing ## Tasks section
453
+ const tasksRegex = /##\s+Tasks\s+([\s\S]*?)(?=\n##|$)/;
454
+ if (tasksRegex.test(content)) {
455
+ // Replace existing section
456
+ const updatedContent = content.replace(tasksRegex, `## Tasks\n\n${tasksMarkdown}\n`);
457
+ await fs.writeFile(userStoryFile, updatedContent, 'utf-8');
458
+ }
459
+ else {
460
+ // Add new section before "Related" section or at the end
461
+ const relatedRegex = /\n---\n\n\*\*Related\*\*:/;
462
+ if (relatedRegex.test(content)) {
463
+ // Insert before "Related" section
464
+ const updatedContent = content.replace(relatedRegex, `\n\n## Tasks\n\n${tasksMarkdown}\n\n---\n\n**Related**:`);
465
+ await fs.writeFile(userStoryFile, updatedContent, 'utf-8');
466
+ }
467
+ else {
468
+ // Append at the end
469
+ const updatedContent = content + `\n\n## Tasks\n\n${tasksMarkdown}\n`;
470
+ await fs.writeFile(userStoryFile, updatedContent, 'utf-8');
471
+ }
472
+ }
473
+ }
474
+ /**
475
+ * Sync to external tools (GitHub, JIRA, ADO)
476
+ *
477
+ * AC-US5-01: Detect external tool configuration from metadata.json
478
+ * AC-US5-02: When GitHub configured, trigger GitHub sync
479
+ * AC-US5-03: When no external tools configured, skip
480
+ * AC-US5-05: External tool failures don't break living docs sync
481
+ */
482
+ async syncToExternalTools(incrementId, featureId, projectPath) {
483
+ try {
484
+ // 1. Detect external tool configuration from metadata.json
485
+ const externalTools = await this.detectExternalTools(incrementId);
486
+ if (externalTools.length === 0) {
487
+ // AC-US5-03: No external tools configured, skip
488
+ return;
489
+ }
490
+ console.log(`\n📡 Syncing to external tools: ${externalTools.join(', ')}`);
491
+ // 2. Sync to each configured external tool
492
+ for (const tool of externalTools) {
493
+ try {
494
+ switch (tool) {
495
+ case 'github':
496
+ await this.syncToGitHub(featureId, projectPath);
497
+ break;
498
+ case 'jira':
499
+ await this.syncToJira(featureId, projectPath);
500
+ break;
501
+ case 'ado':
502
+ await this.syncToADO(featureId, projectPath);
503
+ break;
504
+ default:
505
+ console.warn(` ⚠️ Unknown external tool: ${tool}`);
506
+ }
507
+ }
508
+ catch (error) {
509
+ // AC-US5-05: External tool failures are logged but don't break living docs sync
510
+ console.error(` ⚠️ Failed to sync to ${tool}:`, error);
511
+ console.error(` Living docs sync will continue...`);
512
+ }
513
+ }
514
+ }
515
+ catch (error) {
516
+ // AC-US5-05: External tool failures don't break living docs sync
517
+ console.error(` ⚠️ External tool sync failed:`, error);
518
+ console.error(` Living docs sync completed successfully despite external tool errors`);
519
+ }
520
+ }
521
+ /**
522
+ * Detect external tool configuration from increment metadata.json
523
+ *
524
+ * AC-US5-01: Detect external tool configuration from metadata.json
525
+ *
526
+ * Returns: Array of tool names (['github'], ['github', 'jira'], or [])
527
+ */
528
+ async detectExternalTools(incrementId) {
529
+ const metadataPath = path.join(this.projectRoot, '.specweave', 'increments', incrementId, 'metadata.json');
530
+ if (!fs.existsSync(metadataPath)) {
531
+ return [];
532
+ }
533
+ try {
534
+ const metadata = await fs.readJson(metadataPath);
535
+ const tools = [];
536
+ // Check for GitHub configuration
537
+ if (metadata.github && (metadata.github.milestone || metadata.github.user_story_issues)) {
538
+ tools.push('github');
539
+ }
540
+ // Check for JIRA configuration
541
+ if (metadata.jira) {
542
+ tools.push('jira');
543
+ }
544
+ // Check for ADO configuration
545
+ if (metadata.ado || metadata.azure_devops) {
546
+ tools.push('ado');
547
+ }
548
+ return tools;
549
+ }
550
+ catch (error) {
551
+ console.warn(` ⚠️ Failed to read metadata.json: ${error}`);
552
+ return [];
553
+ }
554
+ }
555
+ /**
556
+ * Sync to GitHub Issues
557
+ *
558
+ * AC-US5-02: When GitHub configured, trigger GitHub sync
559
+ *
560
+ * Uses GitHubFeatureSync.syncFeatureToGitHub() which is idempotent:
561
+ * - Uses existing milestone if it exists
562
+ * - Updates existing issues (triple idempotency check)
563
+ * - Only creates new issues if they don't exist
564
+ */
565
+ async syncToGitHub(featureId, projectPath) {
566
+ try {
567
+ console.log(` 🔄 Syncing to GitHub...`);
568
+ // Dynamic import to avoid circular dependencies
569
+ const { GitHubClientV2 } = await import('../../../plugins/specweave-github/lib/github-client-v2.js');
570
+ const { GitHubFeatureSync } = await import('../../../plugins/specweave-github/lib/github-feature-sync.js');
571
+ // Load GitHub config from environment
572
+ const profile = {
573
+ provider: 'github',
574
+ displayName: 'GitHub',
575
+ config: {
576
+ owner: process.env.GITHUB_OWNER || '',
577
+ repo: process.env.GITHUB_REPO || '',
578
+ token: process.env.GITHUB_TOKEN || ''
579
+ },
580
+ timeRange: {
581
+ default: '1M', // 1 month
582
+ max: '3M' // 3 months
583
+ }
584
+ };
585
+ if (!profile.config.token || !profile.config.owner || !profile.config.repo) {
586
+ console.warn(` ⚠️ GitHub credentials not configured (GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO)`);
587
+ return;
588
+ }
589
+ // Initialize GitHub client and sync
590
+ const client = new GitHubClientV2(profile);
591
+ const specsDir = path.join(this.projectRoot, '.specweave/docs/internal/specs');
592
+ const sync = new GitHubFeatureSync(client, specsDir, this.projectRoot);
593
+ // Sync feature to GitHub (idempotent - safe to run multiple times)
594
+ const result = await sync.syncFeatureToGitHub(featureId);
595
+ console.log(` ✅ Synced to GitHub: ${result.issuesUpdated} updated, ${result.issuesCreated} created`);
596
+ }
597
+ catch (error) {
598
+ if (error instanceof Error && error.message.includes('Cannot find module')) {
599
+ console.warn(` ⚠️ GitHub plugin not installed - skipping GitHub sync`);
600
+ }
601
+ else {
602
+ throw error;
603
+ }
604
+ }
605
+ }
606
+ /**
607
+ * Sync to JIRA (placeholder for future implementation)
608
+ */
609
+ async syncToJira(featureId, projectPath) {
610
+ console.log(` ⚠️ JIRA sync not yet implemented - skipping`);
611
+ // TODO: Implement JIRA sync when specweave-jira plugin is available
612
+ }
613
+ /**
614
+ * Sync to Azure DevOps (placeholder for future implementation)
615
+ */
616
+ async syncToADO(featureId, projectPath) {
617
+ console.log(` ⚠️ ADO sync not yet implemented - skipping`);
618
+ // TODO: Implement ADO sync when specweave-ado plugin is available
619
+ }
620
+ /**
621
+ * Find existing user story file by US-ID
622
+ *
623
+ * Searches for any file matching pattern: us-{id}-*.md
624
+ * Example: US-001 matches us-001-status-line.md or us-001-status-line-priority-p1.md
625
+ *
626
+ * @param projectPath - Path to feature folder (e.g., .specweave/docs/internal/specs/specweave/FS-043)
627
+ * @param userStoryId - User story ID (e.g., "US-001")
628
+ * @returns Filename if found, null otherwise
629
+ */
630
+ async findExistingUserStoryFile(projectPath, userStoryId) {
631
+ try {
632
+ const files = await fs.readdir(projectPath);
633
+ // Look for file matching: us-001-*.md (case insensitive)
634
+ const usIdLower = userStoryId.toLowerCase(); // us-001
635
+ const matchingFiles = files.filter(f => {
636
+ const match = f.match(/^(us-\d+)-/);
637
+ return match && match[1] === usIdLower;
638
+ });
639
+ if (matchingFiles.length === 0) {
640
+ return null; // No existing file found
641
+ }
642
+ if (matchingFiles.length === 1) {
643
+ return matchingFiles[0]; // Exactly one file found ✅
644
+ }
645
+ // Multiple files found - return most recent
646
+ console.warn(` ⚠️ Found ${matchingFiles.length} files for ${userStoryId}, using most recent`);
647
+ const fileTimes = await Promise.all(matchingFiles.map(async (f) => ({
648
+ file: f,
649
+ mtime: (await fs.stat(path.join(projectPath, f))).mtime.getTime()
650
+ })));
651
+ fileTimes.sort((a, b) => b.mtime - a.mtime); // Newest first
652
+ return fileTimes[0].file;
653
+ }
654
+ catch (error) {
655
+ // Directory doesn't exist yet (first sync)
656
+ return null;
657
+ }
658
+ }
659
+ /**
660
+ * Clean up duplicate user story files
661
+ *
662
+ * Strategy:
663
+ * 1. List all user story files in feature folder
664
+ * 2. Group by user story ID (US-001, US-002, etc.)
665
+ * 3. If multiple files found for same US:
666
+ * - Keep the file WITH most recent modification time
667
+ * - Delete older files
668
+ * - Log warning
669
+ */
670
+ async cleanupDuplicateFiles(featureId, projectPath) {
671
+ const files = await fs.readdir(projectPath);
672
+ // Group files by user story ID
673
+ const filesByStory = new Map();
674
+ for (const file of files) {
675
+ // Match pattern: us-001-*, us-002-*, etc.
676
+ const match = file.match(/^(us-\d+)-/);
677
+ if (match) {
678
+ const storyId = match[1].toUpperCase(); // US-001
679
+ if (!filesByStory.has(storyId)) {
680
+ filesByStory.set(storyId, []);
681
+ }
682
+ filesByStory.get(storyId).push(file);
683
+ }
684
+ }
685
+ // Check for duplicates
686
+ for (const [storyId, storyFiles] of filesByStory.entries()) {
687
+ if (storyFiles.length > 1) {
688
+ console.warn(` ⚠️ Found ${storyFiles.length} duplicate files for ${storyId}`);
689
+ // Find the most recent file
690
+ const fileTimes = await Promise.all(storyFiles.map(async (f) => ({
691
+ file: f,
692
+ mtime: (await fs.stat(path.join(projectPath, f))).mtime.getTime()
693
+ })));
694
+ fileTimes.sort((a, b) => b.mtime - a.mtime); // Newest first
695
+ const keepFile = fileTimes[0].file;
696
+ const deleteFiles = fileTimes.slice(1).map(f => f.file);
697
+ console.warn(` → Keeping: ${keepFile} (most recent)`);
698
+ // Delete older files
699
+ for (const file of deleteFiles) {
700
+ const filePath = path.join(projectPath, file);
701
+ await fs.remove(filePath);
702
+ console.warn(` ✅ Deleted: ${file}`);
703
+ }
704
+ }
705
+ }
706
+ }
707
+ /**
708
+ * Clean up temporary files left behind by sync operations
709
+ *
710
+ * Removes:
711
+ * - .tmp files (temporary update files)
712
+ * - .backup files (backup files from updates)
713
+ * - Any other temporary files
714
+ */
715
+ async cleanupTempFiles(projectPath) {
716
+ const files = await fs.readdir(projectPath);
717
+ for (const file of files) {
718
+ if (file.endsWith('.tmp') || file.endsWith('.backup')) {
719
+ const filePath = path.join(projectPath, file);
720
+ await fs.remove(filePath);
721
+ console.log(` 🧹 Cleaned up: ${file}`);
722
+ }
723
+ }
724
+ }
725
+ }
726
+ //# sourceMappingURL=living-docs-sync.js.map