specweave 0.22.0 → 0.22.3

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