specweave 0.22.14 → 0.23.1

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 (211) hide show
  1. package/CLAUDE.md +178 -1
  2. package/dist/src/cli/commands/import-external.d.ts +22 -0
  3. package/dist/src/cli/commands/import-external.d.ts.map +1 -0
  4. package/dist/src/cli/commands/import-external.js +282 -0
  5. package/dist/src/cli/commands/import-external.js.map +1 -0
  6. package/dist/src/cli/commands/init.d.ts.map +1 -1
  7. package/dist/src/cli/commands/init.js +46 -0
  8. package/dist/src/cli/commands/init.js.map +1 -1
  9. package/dist/src/cli/helpers/github-repo-selector.d.ts +59 -0
  10. package/dist/src/cli/helpers/github-repo-selector.d.ts.map +1 -0
  11. package/dist/src/cli/helpers/github-repo-selector.js +265 -0
  12. package/dist/src/cli/helpers/github-repo-selector.js.map +1 -0
  13. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  14. package/dist/src/cli/helpers/issue-tracker/index.js +5 -17
  15. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  16. package/dist/src/config/types.d.ts +16 -16
  17. package/dist/src/core/increment/ac-status-manager.d.ts.map +1 -1
  18. package/dist/src/core/increment/ac-status-manager.js +4 -2
  19. package/dist/src/core/increment/ac-status-manager.js.map +1 -1
  20. package/dist/src/core/increment/completion-validator.d.ts +30 -1
  21. package/dist/src/core/increment/completion-validator.d.ts.map +1 -1
  22. package/dist/src/core/increment/completion-validator.js +151 -3
  23. package/dist/src/core/increment/completion-validator.js.map +1 -1
  24. package/dist/src/core/increment/increment-archiver.d.ts +25 -0
  25. package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
  26. package/dist/src/core/increment/increment-archiver.js +130 -3
  27. package/dist/src/core/increment/increment-archiver.js.map +1 -1
  28. package/dist/src/core/living-docs/feature-archiver.d.ts +37 -0
  29. package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
  30. package/dist/src/core/living-docs/feature-archiver.js +262 -18
  31. package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
  32. package/dist/src/core/living-docs/feature-id-manager.d.ts +17 -0
  33. package/dist/src/core/living-docs/feature-id-manager.d.ts.map +1 -1
  34. package/dist/src/core/living-docs/feature-id-manager.js +25 -0
  35. package/dist/src/core/living-docs/feature-id-manager.js.map +1 -1
  36. package/dist/src/core/living-docs/living-docs-sync.d.ts +14 -0
  37. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  38. package/dist/src/core/living-docs/living-docs-sync.js +46 -0
  39. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  40. package/dist/src/core/repo-structure/repo-id-generator.d.ts +20 -0
  41. package/dist/src/core/repo-structure/repo-id-generator.d.ts.map +1 -1
  42. package/dist/src/core/repo-structure/repo-id-generator.js +44 -0
  43. package/dist/src/core/repo-structure/repo-id-generator.js.map +1 -1
  44. package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
  45. package/dist/src/core/repo-structure/repo-structure-manager.js +5 -2
  46. package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
  47. package/dist/src/core/sync/sync-event-logger.d.ts +15 -1
  48. package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -1
  49. package/dist/src/core/sync/sync-event-logger.js +39 -1
  50. package/dist/src/core/sync/sync-event-logger.js.map +1 -1
  51. package/dist/src/core/types/sync-config-validator.d.ts +57 -0
  52. package/dist/src/core/types/sync-config-validator.d.ts.map +1 -0
  53. package/dist/src/core/types/sync-config-validator.js +116 -0
  54. package/dist/src/core/types/sync-config-validator.js.map +1 -0
  55. package/dist/src/importers/duplicate-detector.d.ts +107 -0
  56. package/dist/src/importers/duplicate-detector.d.ts.map +1 -0
  57. package/dist/src/importers/duplicate-detector.js +189 -0
  58. package/dist/src/importers/duplicate-detector.js.map +1 -0
  59. package/dist/src/importers/import-coordinator.d.ts +15 -0
  60. package/dist/src/importers/import-coordinator.d.ts.map +1 -1
  61. package/dist/src/importers/import-coordinator.js +43 -1
  62. package/dist/src/importers/import-coordinator.js.map +1 -1
  63. package/dist/src/importers/item-converter.d.ts +5 -0
  64. package/dist/src/importers/item-converter.d.ts.map +1 -1
  65. package/dist/src/importers/item-converter.js +27 -2
  66. package/dist/src/importers/item-converter.js.map +1 -1
  67. package/dist/src/importers/rate-limiter.d.ts +128 -0
  68. package/dist/src/importers/rate-limiter.d.ts.map +1 -0
  69. package/dist/src/importers/rate-limiter.js +200 -0
  70. package/dist/src/importers/rate-limiter.js.map +1 -0
  71. package/dist/src/init/compliance/types.d.ts +2 -2
  72. package/dist/src/integrations/ado/ado-client.d.ts +6 -0
  73. package/dist/src/integrations/ado/ado-client.d.ts.map +1 -1
  74. package/dist/src/integrations/ado/ado-client.js +23 -0
  75. package/dist/src/integrations/ado/ado-client.js.map +1 -1
  76. package/dist/src/integrations/jira/jira-client.d.ts +6 -0
  77. package/dist/src/integrations/jira/jira-client.d.ts.map +1 -1
  78. package/dist/src/integrations/jira/jira-client.js +38 -0
  79. package/dist/src/integrations/jira/jira-client.js.map +1 -1
  80. package/dist/src/sync/external-item-sync-service.d.ts +150 -0
  81. package/dist/src/sync/external-item-sync-service.d.ts.map +1 -0
  82. package/dist/src/sync/external-item-sync-service.js +241 -0
  83. package/dist/src/sync/external-item-sync-service.js.map +1 -0
  84. package/dist/src/sync/format-preservation-sync.d.ts +90 -0
  85. package/dist/src/sync/format-preservation-sync.d.ts.map +1 -0
  86. package/dist/src/sync/format-preservation-sync.js +173 -0
  87. package/dist/src/sync/format-preservation-sync.js.map +1 -0
  88. package/dist/src/sync/index.d.ts +8 -0
  89. package/dist/src/sync/index.d.ts.map +1 -0
  90. package/dist/src/sync/index.js +6 -0
  91. package/dist/src/sync/index.js.map +1 -0
  92. package/dist/src/sync/sync-coordinator.d.ts +49 -0
  93. package/dist/src/sync/sync-coordinator.d.ts.map +1 -0
  94. package/dist/src/sync/sync-coordinator.js +248 -0
  95. package/dist/src/sync/sync-coordinator.js.map +1 -0
  96. package/dist/src/sync/sync-metadata.d.ts +75 -0
  97. package/dist/src/sync/sync-metadata.d.ts.map +1 -0
  98. package/dist/src/sync/sync-metadata.js +100 -0
  99. package/dist/src/sync/sync-metadata.js.map +1 -0
  100. package/dist/src/types/living-docs-us-file.d.ts +63 -0
  101. package/dist/src/types/living-docs-us-file.d.ts.map +1 -0
  102. package/dist/src/types/living-docs-us-file.js +27 -0
  103. package/dist/src/types/living-docs-us-file.js.map +1 -0
  104. package/dist/src/validators/format-preservation-validator.d.ts +127 -0
  105. package/dist/src/validators/format-preservation-validator.d.ts.map +1 -0
  106. package/dist/src/validators/format-preservation-validator.js +187 -0
  107. package/dist/src/validators/format-preservation-validator.js.map +1 -0
  108. package/package.json +3 -2
  109. package/plugins/specweave/.claude-plugin/plugin.json +20 -0
  110. package/plugins/specweave/commands/specweave-archive-features.md +11 -1
  111. package/plugins/specweave/commands/specweave-archive.md +51 -15
  112. package/plugins/specweave/commands/specweave-import-docs.md +88 -278
  113. package/plugins/specweave/commands/specweave-import-external.md +407 -0
  114. package/plugins/specweave/hooks/post-edit-spec.sh +94 -0
  115. package/plugins/specweave/hooks/post-increment-completion.sh +0 -0
  116. package/plugins/specweave/hooks/post-spec-update.sh +0 -0
  117. package/plugins/specweave/hooks/post-task-completion.sh +13 -3
  118. package/plugins/specweave/hooks/post-write-spec.sh +91 -0
  119. package/plugins/specweave/lib/hooks/auto-transition.js +1 -1
  120. package/plugins/specweave/lib/hooks/auto-transition.js.bak +50 -0
  121. package/plugins/specweave/lib/hooks/auto-transition.ts +1 -1
  122. package/plugins/specweave/lib/hooks/auto-transition.ts.bak +84 -0
  123. package/plugins/specweave/lib/hooks/git-diff-analyzer.d.js.bak +0 -0
  124. package/plugins/specweave/lib/hooks/git-diff-analyzer.d.ts.bak +89 -0
  125. package/plugins/specweave/lib/hooks/git-diff-analyzer.js.bak +142 -0
  126. package/plugins/specweave/lib/hooks/git-diff-analyzer.ts.bak +269 -0
  127. package/plugins/specweave/lib/hooks/invoke-translator-skill.d.js.bak +0 -0
  128. package/plugins/specweave/lib/hooks/invoke-translator-skill.d.ts.bak +60 -0
  129. package/plugins/specweave/lib/hooks/invoke-translator-skill.js +1 -1
  130. package/plugins/specweave/lib/hooks/invoke-translator-skill.js.bak +155 -0
  131. package/plugins/specweave/lib/hooks/invoke-translator-skill.ts +1 -1
  132. package/plugins/specweave/lib/hooks/invoke-translator-skill.ts.bak +264 -0
  133. package/plugins/specweave/lib/hooks/prepare-reflection-context.d.js.bak +0 -0
  134. package/plugins/specweave/lib/hooks/prepare-reflection-context.d.ts.bak +42 -0
  135. package/plugins/specweave/lib/hooks/prepare-reflection-context.js.bak +110 -0
  136. package/plugins/specweave/lib/hooks/prepare-reflection-context.ts.bak +178 -0
  137. package/plugins/specweave/lib/hooks/reflection-config-loader.d.js.bak +0 -0
  138. package/plugins/specweave/lib/hooks/reflection-config-loader.d.ts.bak +45 -0
  139. package/plugins/specweave/lib/hooks/reflection-config-loader.js.bak +92 -0
  140. package/plugins/specweave/lib/hooks/reflection-config-loader.ts.bak +156 -0
  141. package/plugins/specweave/lib/hooks/reflection-parser.d.js.bak +0 -0
  142. package/plugins/specweave/lib/hooks/reflection-parser.d.ts.bak +33 -0
  143. package/plugins/specweave/lib/hooks/reflection-parser.js.bak +301 -0
  144. package/plugins/specweave/lib/hooks/reflection-parser.ts.bak +484 -0
  145. package/plugins/specweave/lib/hooks/reflection-prompt-builder.d.js.bak +0 -0
  146. package/plugins/specweave/lib/hooks/reflection-prompt-builder.d.ts.bak +56 -0
  147. package/plugins/specweave/lib/hooks/reflection-prompt-builder.js.bak +182 -0
  148. package/plugins/specweave/lib/hooks/reflection-prompt-builder.ts.bak +306 -0
  149. package/plugins/specweave/lib/hooks/reflection-storage.d.js.bak +0 -0
  150. package/plugins/specweave/lib/hooks/reflection-storage.d.ts.bak +64 -0
  151. package/plugins/specweave/lib/hooks/reflection-storage.js.bak +231 -0
  152. package/plugins/specweave/lib/hooks/reflection-storage.ts.bak +369 -0
  153. package/plugins/specweave/lib/hooks/run-self-reflection.d.js.bak +0 -0
  154. package/plugins/specweave/lib/hooks/run-self-reflection.d.ts.bak +43 -0
  155. package/plugins/specweave/lib/hooks/run-self-reflection.js.bak +132 -0
  156. package/plugins/specweave/lib/hooks/run-self-reflection.ts.bak +258 -0
  157. package/plugins/specweave/lib/hooks/sync-cache.js.bak +294 -0
  158. package/plugins/specweave/lib/hooks/sync-living-docs.d.js.bak +1 -0
  159. package/plugins/specweave/lib/hooks/sync-living-docs.d.ts.bak +27 -0
  160. package/plugins/specweave/lib/hooks/sync-living-docs.js +35 -1
  161. package/plugins/specweave/lib/hooks/sync-living-docs.js.bak +339 -0
  162. package/plugins/specweave/lib/hooks/sync-us-tasks.js +179 -3
  163. package/plugins/specweave/lib/hooks/sync-us-tasks.js.bak +476 -0
  164. package/plugins/specweave/lib/hooks/translate-file.d.js.bak +0 -0
  165. package/plugins/specweave/lib/hooks/translate-file.d.ts.bak +59 -0
  166. package/plugins/specweave/lib/hooks/translate-file.js +1 -1
  167. package/plugins/specweave/lib/hooks/translate-file.js.bak +289 -0
  168. package/plugins/specweave/lib/hooks/translate-file.ts +1 -1
  169. package/plugins/specweave/lib/hooks/translate-file.ts.bak +428 -0
  170. package/plugins/specweave/lib/hooks/translate-living-docs.d.js.bak +0 -0
  171. package/plugins/specweave/lib/hooks/translate-living-docs.d.ts.bak +13 -0
  172. package/plugins/specweave/lib/hooks/translate-living-docs.js.bak +119 -0
  173. package/plugins/specweave/lib/hooks/translate-living-docs.ts.bak +224 -0
  174. package/plugins/specweave/lib/hooks/update-ac-status.js +1 -1
  175. package/plugins/specweave/lib/hooks/update-ac-status.js.bak +51 -0
  176. package/plugins/specweave/lib/hooks/update-ac-status.ts +1 -1
  177. package/plugins/specweave/lib/hooks/update-ac-status.ts.bak +103 -0
  178. package/plugins/specweave/lib/hooks/update-tasks-md.d.js.bak +1 -0
  179. package/plugins/specweave/lib/hooks/update-tasks-md.d.ts.bak +29 -0
  180. package/plugins/specweave/lib/hooks/update-tasks-md.js.bak +296 -0
  181. package/plugins/specweave/lib/hooks/update-tasks-md.ts.bak +489 -0
  182. package/plugins/specweave/lib/vendor/core/increment/ac-status-manager.d.ts +115 -0
  183. package/plugins/specweave/lib/vendor/core/increment/ac-status-manager.js +345 -0
  184. package/plugins/specweave/lib/vendor/core/increment/ac-status-manager.js.map +1 -0
  185. package/plugins/specweave/lib/vendor/core/increment/active-increment-manager.d.ts +106 -0
  186. package/plugins/specweave/lib/vendor/core/increment/active-increment-manager.js +220 -0
  187. package/plugins/specweave/lib/vendor/core/increment/active-increment-manager.js.map +1 -0
  188. package/plugins/specweave/lib/vendor/core/increment/auto-transition-manager.d.ts +60 -0
  189. package/plugins/specweave/lib/vendor/core/increment/auto-transition-manager.js +192 -0
  190. package/plugins/specweave/lib/vendor/core/increment/auto-transition-manager.js.map +1 -0
  191. package/plugins/specweave/lib/vendor/core/increment/duplicate-detector.d.ts +52 -0
  192. package/plugins/specweave/lib/vendor/core/increment/duplicate-detector.js +276 -0
  193. package/plugins/specweave/lib/vendor/core/increment/duplicate-detector.js.map +1 -0
  194. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +163 -0
  195. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +541 -0
  196. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -0
  197. package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +157 -0
  198. package/plugins/specweave/lib/vendor/core/types/increment-metadata.js +191 -0
  199. package/plugins/specweave/lib/vendor/core/types/increment-metadata.js.map +1 -0
  200. package/plugins/specweave/lib/vendor/generators/spec/task-parser.d.ts +95 -0
  201. package/plugins/specweave/lib/vendor/generators/spec/task-parser.js +301 -0
  202. package/plugins/specweave/lib/vendor/generators/spec/task-parser.js.map +1 -0
  203. package/plugins/specweave/lib/vendor/utils/logger.d.ts +48 -0
  204. package/plugins/specweave/lib/vendor/utils/logger.js +53 -0
  205. package/plugins/specweave/lib/vendor/utils/logger.js.map +1 -0
  206. package/plugins/specweave/lib/vendor/utils/translation.d.ts +187 -0
  207. package/plugins/specweave/lib/vendor/utils/translation.js +414 -0
  208. package/plugins/specweave/lib/vendor/utils/translation.js.map +1 -0
  209. package/plugins/specweave-github/commands/specweave-github-update-user-story.md +1 -1
  210. package/plugins/specweave-github/skills/github-issue-standard/SKILL.md +1 -1
  211. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +531 -0
@@ -54,7 +54,9 @@ async function syncLivingDocs(incrementId) {
54
54
  return;
55
55
  }
56
56
 
57
- await syncToGitHub(incrementId, changedDocs);
57
+ // T-034E: Use FormatPreservationSyncService for origin-aware sync
58
+ await syncWithFormatPreservation(incrementId);
59
+
58
60
  console.log("\u2705 Living docs sync complete\n");
59
61
  } catch (error) {
60
62
  console.error("\u274C Error syncing living docs:", error);
@@ -334,6 +336,38 @@ if (isMainModule) {
334
336
  console.error("\u274C Fatal error:", error);
335
337
  });
336
338
  }
339
+
340
+ /**
341
+ * Sync with format preservation (T-034E)
342
+ * Routes sync based on origin metadata (external vs internal)
343
+ */
344
+ async function syncWithFormatPreservation(incrementId) {
345
+ try {
346
+ console.log("\n🔄 Using format-preserving sync...");
347
+
348
+ const { SyncCoordinator } = await import("../../../../dist/src/sync/sync-coordinator.js");
349
+
350
+ const coordinator = new SyncCoordinator({
351
+ projectRoot: process.cwd(),
352
+ incrementId
353
+ });
354
+
355
+ const result = await coordinator.syncIncrementCompletion();
356
+
357
+ if (result.success) {
358
+ console.log(`✅ Format-preserving sync complete (${result.syncMode} mode)`);
359
+ console.log(` ${result.userStoriesSynced} user story/stories synced`);
360
+ } else {
361
+ console.log(`⚠️ Sync completed with ${result.errors.length} error(s)`);
362
+ result.errors.forEach(err => console.error(` - ${err}`));
363
+ }
364
+ } catch (error) {
365
+ console.error("⚠️ Format-preserving sync error:", error);
366
+ console.log(" Falling back to legacy sync...");
367
+ // Don't fail - just log and continue
368
+ }
369
+ }
370
+
337
371
  export {
338
372
  syncLivingDocs
339
373
  };
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs-extra";
3
+ import path from "path";
4
+ import { execSync } from "child_process";
5
+ async function syncLivingDocs(incrementId) {
6
+ try {
7
+ console.log(`
8
+ \u{1F4DA} Checking living docs sync for increment: ${incrementId}`);
9
+ const configPath = path.join(process.cwd(), ".specweave", "config.json");
10
+ let config = {};
11
+ if (fs.existsSync(configPath)) {
12
+ config = JSON.parse(await fs.readFile(configPath, "utf-8"));
13
+ }
14
+ const syncEnabled = config.hooks?.post_task_completion?.sync_living_docs ?? false;
15
+ if (!syncEnabled) {
16
+ console.log("\u2139\uFE0F Living docs sync disabled in config");
17
+ console.log(" To enable: Set hooks.post_task_completion.sync_living_docs = true");
18
+ return;
19
+ }
20
+ console.log("\u2705 Living docs sync enabled");
21
+ const intelligentEnabled = config.livingDocs?.intelligent?.enabled ?? false;
22
+ let specCopied = false;
23
+ let changedDocs = [];
24
+ if (intelligentEnabled) {
25
+ console.log("\u{1F9E0} Using intelligent sync mode (v0.18.0+)");
26
+ const result = await intelligentSyncLivingDocs(incrementId, config);
27
+ specCopied = result.success;
28
+ changedDocs = result.changedFiles;
29
+ } else {
30
+ console.log("\u{1F4CA} Using hierarchical distribution mode (v2.1 - Epic + User Stories)");
31
+ const result = await hierarchicalDistribution(incrementId);
32
+ specCopied = result.success;
33
+ changedDocs = result.changedFiles;
34
+ }
35
+ if (changedDocs.length === 0 && !specCopied) {
36
+ console.log("\u2139\uFE0F No living docs changed");
37
+ return;
38
+ }
39
+ console.log(`\u{1F4C4} Changed/created ${changedDocs.length} file(s)`);
40
+
41
+ // ========================================================================
42
+ // CHECK PERMISSION: canUpdateExternalItems (v0.24.0 - Three-Permission Architecture)
43
+ // ========================================================================
44
+ // This permission controls whether SpecWeave can UPDATE externally-created items
45
+ // (full content: title, description, ACs, tasks, comments).
46
+ // If false, living docs sync happens locally but doesn't push to external tools.
47
+ const canUpdateExternal = config.sync?.settings?.canUpdateExternalItems ?? false;
48
+
49
+ if (!canUpdateExternal) {
50
+ console.log("\u2139\uFE0F GitHub sync skipped (canUpdateExternalItems = false)");
51
+ console.log(" Living docs updated locally only");
52
+ console.log(" To enable: Set sync.settings.canUpdateExternalItems = true in config.json");
53
+ console.log("\u2705 Living docs sync complete (local only)\n");
54
+ return;
55
+ }
56
+
57
+ await syncToGitHub(incrementId, changedDocs);
58
+ console.log("\u2705 Living docs sync complete\n");
59
+ } catch (error) {
60
+ console.error("\u274C Error syncing living docs:", error);
61
+ }
62
+ }
63
+ async function intelligentSyncLivingDocs(incrementId, config) {
64
+ console.log(" \u26A0\uFE0F Intelligent sync not yet fully implemented");
65
+ console.log(" Falling back to hierarchical distribution mode...");
66
+ return await hierarchicalDistribution(incrementId);
67
+ }
68
+ async function hierarchicalDistribution(incrementId) {
69
+ try {
70
+ // ============================================================================
71
+ // LONG-TERM FIX (2025-11-19): Use LivingDocsSync instead of old SpecDistributor
72
+ // ============================================================================
73
+ //
74
+ // Why this change:
75
+ // - Old SpecDistributor.distribute() method no longer exists (removed in v3.0.0)
76
+ // - LivingDocsSync is the official, stable API for syncing increments
77
+ // - Used by /specweave:sync-docs command - battle-tested and maintained
78
+ // - Future-proof: Won't break when internal APIs change
79
+ //
80
+ // Architecture:
81
+ // - LivingDocsSync delegates to FeatureIDManager, TaskProjectSpecificGenerator, etc.
82
+ // - Handles greenfield/brownfield detection automatically
83
+ // - Returns consistent SyncResult interface
84
+ //
85
+ // Previous broken code:
86
+ // const { SpecDistributor } = await import("../../../../dist/src/core/living-docs/index.js");
87
+ // const distributor = new SpecDistributor(projectRoot, { overwriteExisting: false, createBackups: true });
88
+ // const result = await distributor.distribute(incrementId); // ❌ Method doesn't exist
89
+ //
90
+ // ============================================================================
91
+
92
+ const { LivingDocsSync } = await import("../../../../dist/src/core/living-docs/living-docs-sync.js");
93
+
94
+ console.log(" \u{1F4CA} Syncing increment to living docs structure...");
95
+ const projectRoot = process.cwd();
96
+
97
+ // ========================================================================
98
+ // USE FEATURE ID FROM ENVIRONMENT (NEW in v0.23.0 - Increment 0047)
99
+ // ========================================================================
100
+ // If FEATURE_ID is provided via environment variable (extracted from spec.md),
101
+ // use it directly instead of auto-generating. This ensures correct traceability.
102
+ const explicitFeatureId = process.env.FEATURE_ID;
103
+ if (explicitFeatureId) {
104
+ console.log(` \u{1F4CE} Using explicit feature ID from spec.md: ${explicitFeatureId}`);
105
+ } else {
106
+ console.log(" \u{1F504} Feature ID will be auto-generated from increment number");
107
+ }
108
+
109
+ // Create logger adapter for LivingDocsSync
110
+ const logger = {
111
+ log: (msg) => console.log(` ${msg}`),
112
+ error: (msg, err) => console.error(` ${msg}`, err || ''),
113
+ warn: (msg) => console.warn(` ${msg}`)
114
+ };
115
+
116
+ const sync = new LivingDocsSync(projectRoot, { logger });
117
+ const result = await sync.syncIncrement(incrementId, {
118
+ dryRun: false,
119
+ force: false,
120
+ // Pass explicit feature ID if available (v0.23.0+)
121
+ explicitFeatureId: explicitFeatureId || undefined
122
+ });
123
+
124
+ if (!result.success) {
125
+ console.error(` \u274C Sync failed with errors:`);
126
+ for (const error of result.errors) {
127
+ console.error(` - ${error}`);
128
+ }
129
+ return { success: false, changedFiles: [] };
130
+ }
131
+
132
+ console.log(` \u2705 Living docs sync complete:`);
133
+ console.log(` Feature ID: ${result.featureId}`);
134
+ console.log(` Files created/updated: ${result.filesCreated.length + result.filesUpdated.length}`);
135
+
136
+ const changedFiles = [...result.filesCreated, ...result.filesUpdated];
137
+
138
+ // NEW (v0.23.0): Sync tasks from tasks.md to living docs US files
139
+ // Part of increment 0047-us-task-linkage implementation
140
+ try {
141
+ const { syncUSTasksToLivingDocs } = await import("./sync-us-tasks.js");
142
+ const taskSyncResult = await syncUSTasksToLivingDocs(
143
+ incrementId,
144
+ projectRoot,
145
+ result.featureId,
146
+ {}
147
+ );
148
+
149
+ if (taskSyncResult.success && taskSyncResult.updatedFiles.length > 0) {
150
+ changedFiles.push(...taskSyncResult.updatedFiles);
151
+ }
152
+ } catch (error) {
153
+ // Don't fail entire sync if US-Task sync fails (graceful degradation)
154
+ console.log(` \u26A0\uFE0F US-Task sync failed (non-fatal):`, error.message);
155
+ console.log(` \u{1F4A1} Tip: This is a new feature (v0.23.0) - may need task parser build`);
156
+ }
157
+
158
+ return {
159
+ success: true,
160
+ changedFiles
161
+ };
162
+ } catch (error) {
163
+ console.error(` \u274C Living docs sync failed: ${error}`);
164
+ console.error(error.stack);
165
+ console.error(" \u26A0\uFE0F Living docs sync skipped due to error");
166
+ console.error(" \u{1F4A1} Tip: Run /specweave:sync-docs manually to retry");
167
+ return {
168
+ success: false,
169
+ changedFiles: []
170
+ };
171
+ }
172
+ }
173
+ async function extractAndMergeLivingDocs(incrementId) {
174
+ try {
175
+ const {
176
+ parseIncrementSpec,
177
+ parseLivingDocsSpec,
178
+ extractSpecId,
179
+ mergeUserStories,
180
+ generateRelatedDocsLinks,
181
+ writeLivingDocsSpec
182
+ } = await import("../../../../dist/src/utils/spec-parser.js");
183
+ const projectRoot = process.cwd();
184
+ const incrementSpecPath = path.join(projectRoot, ".specweave", "increments", incrementId, "spec.md");
185
+ if (!fs.existsSync(incrementSpecPath)) {
186
+ console.log(`\u26A0\uFE0F Increment spec not found: ${incrementSpecPath}`);
187
+ return false;
188
+ }
189
+ console.log(` \u{1F4D6} Parsing increment spec: ${incrementId}`);
190
+ const incrementSpec = await parseIncrementSpec(incrementSpecPath);
191
+ if (incrementSpec.userStories.length === 0) {
192
+ console.log(`\u2139\uFE0F No user stories found in increment spec, skipping sync`);
193
+ return false;
194
+ }
195
+ console.log(` \u2705 Found ${incrementSpec.userStories.length} user stories in increment`);
196
+ const specId = incrementSpec.implementsSpec || extractSpecId(incrementId);
197
+ const livingDocsDir = path.join(projectRoot, ".specweave", "docs", "internal", "specs", "default");
198
+ const livingDocsPath = path.join(livingDocsDir, `${specId}-${incrementId.replace(/^\d+-/, "")}.md`);
199
+ const livingDocsExists = fs.existsSync(livingDocsPath);
200
+ if (livingDocsExists) {
201
+ console.log(` \u{1F4DA} Living docs spec exists, merging user stories...`);
202
+ const livingSpec = await parseLivingDocsSpec(livingDocsPath);
203
+ const mergedStories = mergeUserStories(
204
+ livingSpec.userStories,
205
+ incrementSpec.userStories,
206
+ incrementId
207
+ );
208
+ const newStoriesCount = mergedStories.length - livingSpec.userStories.length;
209
+ const existingEntry = livingSpec.implementationHistory.find((e) => e.increment === incrementId);
210
+ if (!existingEntry) {
211
+ livingSpec.implementationHistory.push({
212
+ increment: incrementId,
213
+ stories: incrementSpec.userStories.map((s) => s.id),
214
+ status: "complete",
215
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
216
+ });
217
+ } else {
218
+ existingEntry.status = "complete";
219
+ existingEntry.date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
220
+ }
221
+ livingSpec.userStories = mergedStories;
222
+ livingSpec.lastUpdated = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
223
+ await writeLivingDocsSpec(livingDocsPath, livingSpec);
224
+ console.log(` \u2705 Merged ${newStoriesCount} new user stories into living docs`);
225
+ console.log(` \u2705 Updated implementation history for ${incrementId}`);
226
+ } else {
227
+ console.log(` \u{1F4DD} Creating new living docs spec: ${specId}`);
228
+ const relatedDocs = generateRelatedDocsLinks(incrementSpec, projectRoot);
229
+ const livingSpec = {
230
+ id: specId,
231
+ title: incrementSpec.title,
232
+ featureArea: extractFeatureArea(incrementSpec.title),
233
+ overview: incrementSpec.overview,
234
+ userStories: incrementSpec.userStories.map((story) => ({
235
+ ...story,
236
+ implementedIn: incrementId,
237
+ status: "complete"
238
+ })),
239
+ implementationHistory: [
240
+ {
241
+ increment: incrementId,
242
+ stories: incrementSpec.userStories.map((s) => s.id),
243
+ status: "complete",
244
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
245
+ }
246
+ ],
247
+ relatedDocs,
248
+ externalLinks: {},
249
+ priority: incrementSpec.priority,
250
+ status: "active",
251
+ created: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
252
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
253
+ };
254
+ await fs.ensureDir(livingDocsDir);
255
+ await writeLivingDocsSpec(livingDocsPath, livingSpec);
256
+ console.log(` \u2705 Created new living docs spec: ${specId}`);
257
+ console.log(` \u2705 Added ${incrementSpec.userStories.length} user stories`);
258
+ console.log(` \u2705 Generated links to ${relatedDocs.architecture.length} architecture docs`);
259
+ console.log(` \u2705 Generated links to ${relatedDocs.adrs.length} ADRs`);
260
+ }
261
+ return true;
262
+ } catch (error) {
263
+ console.error(`\u274C Error extracting/merging living docs: ${error}`);
264
+ console.error(error.stack);
265
+ return false;
266
+ }
267
+ }
268
+ function extractFeatureArea(title) {
269
+ return title.replace(/^(Increment \d+:\s*)?/, "").trim();
270
+ }
271
+ function detectChangedDocs() {
272
+ try {
273
+ const output = execSync("git diff --name-only .specweave/docs/ 2>/dev/null || true", {
274
+ encoding: "utf-8",
275
+ cwd: process.cwd()
276
+ }).trim();
277
+ if (!output) {
278
+ return [];
279
+ }
280
+ const files = output.split("\n").filter((f) => f.endsWith(".md")).filter((f) => f.length > 0);
281
+ return files;
282
+ } catch (error) {
283
+ console.warn("\u26A0\uFE0F Could not detect git changes:", error);
284
+ return [];
285
+ }
286
+ }
287
+ async function syncToGitHub(incrementId, changedDocs) {
288
+ try {
289
+ console.log("\n\u{1F504} Syncing to GitHub...");
290
+ const {
291
+ loadIncrementMetadata,
292
+ detectRepo,
293
+ collectLivingDocs,
294
+ updateIssueLivingDocs,
295
+ postArchitectureComment
296
+ } = await import("../../../../dist/plugins/specweave-github/lib/github-issue-updater.js");
297
+ const metadata = await loadIncrementMetadata(incrementId);
298
+ if (!metadata?.github?.issue) {
299
+ console.log("\u2139\uFE0F No GitHub issue linked, skipping GitHub sync");
300
+ return;
301
+ }
302
+ const repoInfo = await detectRepo();
303
+ if (!repoInfo) {
304
+ console.log("\u26A0\uFE0F Could not detect GitHub repository, skipping GitHub sync");
305
+ return;
306
+ }
307
+ const { owner, repo } = repoInfo;
308
+ const issueNumber = metadata.github.issue;
309
+ console.log(` Syncing to ${owner}/${repo}#${issueNumber}`);
310
+ const livingDocs = await collectLivingDocs(incrementId);
311
+ if (livingDocs.specs.length > 0 || livingDocs.architecture.length > 0 || livingDocs.diagrams.length > 0) {
312
+ await updateIssueLivingDocs(issueNumber, livingDocs, owner, repo);
313
+ }
314
+ for (const docPath of changedDocs) {
315
+ if (docPath.includes("/architecture/")) {
316
+ await postArchitectureComment(issueNumber, docPath, owner, repo);
317
+ }
318
+ }
319
+ console.log("\u2705 GitHub sync complete");
320
+ } catch (error) {
321
+ console.error("\u274C Error syncing to GitHub:", error);
322
+ console.error(" (Non-blocking - continuing...)");
323
+ }
324
+ }
325
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
326
+ if (isMainModule) {
327
+ const incrementId = process.argv[2];
328
+ if (!incrementId) {
329
+ console.error("\u274C Usage: sync-living-docs <incrementId>");
330
+ console.error(" Example: sync-living-docs 0006-llm-native-i18n");
331
+ process.exit(1);
332
+ }
333
+ syncLivingDocs(incrementId).catch((error) => {
334
+ console.error("\u274C Fatal error:", error);
335
+ });
336
+ }
337
+ export {
338
+ syncLivingDocs
339
+ };
@@ -12,7 +12,7 @@
12
12
 
13
13
  import fs from 'fs-extra';
14
14
  import path from 'path';
15
- import { parseTasksWithUSLinks, getAllTasks } from '../../../../dist/src/generators/spec/task-parser.js';
15
+ import { parseTasksWithUSLinks, getAllTasks } from '../vendor/generators/spec/task-parser.js';
16
16
  import { glob } from 'glob';
17
17
  import { getCachedTasks, needsSync, recordSync, batchFileUpdates } from './sync-cache.js';
18
18
 
@@ -165,7 +165,14 @@ async function updateUSFile(usFilePath, tasks, incrementId) {
165
165
  let content = await fs.readFile(usFilePath, 'utf-8');
166
166
  let updated = false;
167
167
 
168
- // 1. Update task list section
168
+ // 1. Update origin badge (NEW - T-034)
169
+ const updatedOrigin = updateOriginBadge(content, usFilePath);
170
+ if (updatedOrigin !== content) {
171
+ content = updatedOrigin;
172
+ updated = true;
173
+ }
174
+
175
+ // 2. Update task list section
169
176
  const taskList = generateTaskList(tasks, incrementId);
170
177
  const newTasksSection = `## Tasks\n\n${taskList}`;
171
178
 
@@ -181,7 +188,7 @@ async function updateUSFile(usFilePath, tasks, incrementId) {
181
188
  }
182
189
  }
183
190
 
184
- // 2. Update AC checkboxes based on task completion
191
+ // 3. Update AC checkboxes based on task completion
185
192
  const updatedACs = updateACCheckboxes(content, tasks);
186
193
  if (updatedACs !== content) {
187
194
  content = updatedACs;
@@ -191,6 +198,175 @@ async function updateUSFile(usFilePath, tasks, incrementId) {
191
198
  return { updated, content };
192
199
  }
193
200
 
201
+ /**
202
+ * Update origin badge in User Story file
203
+ * Detects origin from US ID (E suffix = external)
204
+ *
205
+ * @param {string} content - US file content
206
+ * @param {string} usFilePath - Path to US file
207
+ * @returns {string} Updated content with origin badge
208
+ */
209
+ function updateOriginBadge(content, usFilePath) {
210
+ // Extract US ID from filename (e.g., us-001e-title.md -> US-001E)
211
+ const filename = path.basename(usFilePath);
212
+ const usIdMatch = filename.match(/^(us-\d{3}e?)-/i);
213
+
214
+ if (!usIdMatch) {
215
+ return content; // Can't determine US ID, skip
216
+ }
217
+
218
+ const usId = usIdMatch[1].toUpperCase();
219
+ const isExternal = usId.endsWith('E');
220
+
221
+ // Validate origin immutability (prevent internal ↔ external changes)
222
+ const existingOrigin = extractExistingOrigin(content);
223
+ if (existingOrigin) {
224
+ const existingIsExternal = existingOrigin !== 'internal';
225
+ if (isExternal !== existingIsExternal) {
226
+ console.log(` ⚠️ Origin immutable: Cannot change ${usId} from ${existingOrigin} to ${isExternal ? 'external' : 'internal'}`);
227
+ return content; // Don't modify origin
228
+ }
229
+ }
230
+
231
+ // Determine origin badge
232
+ let originBadge;
233
+ if (isExternal) {
234
+ // Try to detect external source from metadata
235
+ const externalSource = detectExternalSource(content);
236
+ originBadge = getExternalOriginBadge(externalSource, content);
237
+ } else {
238
+ originBadge = '🏠 **Internal**';
239
+ }
240
+
241
+ // Check if origin badge already exists
242
+ const originPattern = /\*\*Origin\*\*:\s*.*/;
243
+ if (originPattern.test(content)) {
244
+ // Replace existing origin badge
245
+ return content.replace(originPattern, `**Origin**: ${originBadge}`);
246
+ }
247
+
248
+ // Add origin badge after frontmatter (if exists) or at beginning
249
+ const frontmatterEnd = content.match(/^---\n.*?\n---\n/s);
250
+ if (frontmatterEnd) {
251
+ const insertPos = frontmatterEnd[0].length;
252
+ return content.slice(0, insertPos) + `\n**Origin**: ${originBadge}\n\n` + content.slice(insertPos);
253
+ }
254
+
255
+ // No frontmatter, add at beginning after title
256
+ const titleMatch = content.match(/^#\s+.+\n/);
257
+ if (titleMatch) {
258
+ const insertPos = titleMatch[0].length;
259
+ return content.slice(0, insertPos) + `\n**Origin**: ${originBadge}\n\n` + content.slice(insertPos);
260
+ }
261
+
262
+ // Fallback: add at very beginning
263
+ return `**Origin**: ${originBadge}\n\n` + content;
264
+ }
265
+
266
+ /**
267
+ * Extract existing origin from content (for immutability validation)
268
+ *
269
+ * @param {string} content - US file content
270
+ * @returns {string|null} Existing origin (internal, github, jira, ado) or null
271
+ */
272
+ function extractExistingOrigin(content) {
273
+ const originMatch = content.match(/\*\*Origin\*\*:\s*(.+)/);
274
+ if (!originMatch) {
275
+ return null;
276
+ }
277
+
278
+ const originText = originMatch[1].toLowerCase();
279
+ if (originText.includes('internal')) {
280
+ return 'internal';
281
+ }
282
+ if (originText.includes('github')) {
283
+ return 'github';
284
+ }
285
+ if (originText.includes('jira')) {
286
+ return 'jira';
287
+ }
288
+ if (originText.includes('ado') || originText.includes('azure')) {
289
+ return 'ado';
290
+ }
291
+ if (originText.includes('external')) {
292
+ return 'external'; // Generic external
293
+ }
294
+
295
+ return null;
296
+ }
297
+
298
+ /**
299
+ * Detect external source from content metadata
300
+ *
301
+ * @param {string} content - US file content
302
+ * @returns {string} External source (github, jira, ado) or 'unknown'
303
+ */
304
+ function detectExternalSource(content) {
305
+ // Check frontmatter for external_source or externalSource
306
+ const frontmatterMatch = content.match(/^---\n(.*?)\n---/s);
307
+ if (frontmatterMatch) {
308
+ const frontmatter = frontmatterMatch[1];
309
+ if (frontmatter.includes('external_source: github') || frontmatter.includes('externalSource: github')) {
310
+ return 'github';
311
+ }
312
+ if (frontmatter.includes('external_source: jira') || frontmatter.includes('externalSource: jira')) {
313
+ return 'jira';
314
+ }
315
+ if (frontmatter.includes('external_source: ado') || frontmatter.includes('externalSource: ado')) {
316
+ return 'ado';
317
+ }
318
+ }
319
+
320
+ // Check for external ID patterns
321
+ if (content.includes('externalId: GH-') || content.includes('external_id: GH-')) {
322
+ return 'github';
323
+ }
324
+ if (content.includes('externalId: JIRA-') || content.includes('external_id: JIRA-')) {
325
+ return 'jira';
326
+ }
327
+ if (content.includes('externalId: ADO-') || content.includes('external_id: ADO-')) {
328
+ return 'ado';
329
+ }
330
+
331
+ return 'unknown';
332
+ }
333
+
334
+ /**
335
+ * Get external origin badge with link if available
336
+ *
337
+ * @param {string} source - External source (github, jira, ado)
338
+ * @param {string} content - US file content
339
+ * @returns {string} Origin badge markdown
340
+ */
341
+ function getExternalOriginBadge(source, content) {
342
+ // Extract external ID and URL from content
343
+ const externalIdMatch = content.match(/external_?[iI]d:\s*([^\n]+)/);
344
+ const externalUrlMatch = content.match(/external_?[uU]rl:\s*([^\n]+)/);
345
+
346
+ const externalId = externalIdMatch ? externalIdMatch[1].trim() : null;
347
+ const externalUrl = externalUrlMatch ? externalUrlMatch[1].trim() : null;
348
+
349
+ switch (source) {
350
+ case 'github':
351
+ if (externalId && externalUrl) {
352
+ return `🔗 [GitHub ${externalId}](${externalUrl})`;
353
+ }
354
+ return '🔗 **GitHub**';
355
+ case 'jira':
356
+ if (externalId && externalUrl) {
357
+ return `🎫 [JIRA ${externalId}](${externalUrl})`;
358
+ }
359
+ return '🎫 **JIRA**';
360
+ case 'ado':
361
+ if (externalId && externalUrl) {
362
+ return `📋 [ADO ${externalId}](${externalUrl})`;
363
+ }
364
+ return '📋 **Azure DevOps**';
365
+ default:
366
+ return '🔗 **External**';
367
+ }
368
+ }
369
+
194
370
  /**
195
371
  * Generate task list markdown
196
372
  *