specweave 0.17.6 → 0.17.8

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 (108) hide show
  1. package/CLAUDE.md +365 -9
  2. package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts +9 -4
  3. package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
  4. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +82 -12
  5. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
  6. package/dist/plugins/specweave-github/lib/github-spec-content-sync.d.ts +2 -2
  7. package/dist/plugins/specweave-github/lib/github-spec-content-sync.d.ts.map +1 -1
  8. package/dist/plugins/specweave-github/lib/github-spec-content-sync.js +69 -10
  9. package/dist/plugins/specweave-github/lib/github-spec-content-sync.js.map +1 -1
  10. package/dist/src/cli/commands/detect-project.d.ts +9 -0
  11. package/dist/src/cli/commands/detect-project.d.ts.map +1 -0
  12. package/dist/src/cli/commands/detect-project.js +85 -0
  13. package/dist/src/cli/commands/detect-project.js.map +1 -0
  14. package/dist/src/cli/commands/detect-specs.d.ts +9 -0
  15. package/dist/src/cli/commands/detect-specs.d.ts.map +1 -0
  16. package/dist/src/cli/commands/detect-specs.js +73 -0
  17. package/dist/src/cli/commands/detect-specs.js.map +1 -0
  18. package/dist/src/cli/commands/init.d.ts.map +1 -1
  19. package/dist/src/cli/commands/init.js +8 -15
  20. package/dist/src/cli/commands/init.js.map +1 -1
  21. package/dist/src/core/living-docs/content-classifier.d.ts +83 -0
  22. package/dist/src/core/living-docs/content-classifier.d.ts.map +1 -0
  23. package/dist/src/core/living-docs/content-classifier.js +393 -0
  24. package/dist/src/core/living-docs/content-classifier.js.map +1 -0
  25. package/dist/src/core/living-docs/content-distributor.d.ts +126 -0
  26. package/dist/src/core/living-docs/content-distributor.d.ts.map +1 -0
  27. package/dist/src/core/living-docs/content-distributor.js +339 -0
  28. package/dist/src/core/living-docs/content-distributor.js.map +1 -0
  29. package/dist/src/core/living-docs/content-parser.d.ts +140 -0
  30. package/dist/src/core/living-docs/content-parser.d.ts.map +1 -0
  31. package/dist/src/core/living-docs/content-parser.js +316 -0
  32. package/dist/src/core/living-docs/content-parser.js.map +1 -0
  33. package/dist/src/core/living-docs/cross-linker.d.ts +126 -0
  34. package/dist/src/core/living-docs/cross-linker.d.ts.map +1 -0
  35. package/dist/src/core/living-docs/cross-linker.js +374 -0
  36. package/dist/src/core/living-docs/cross-linker.js.map +1 -0
  37. package/dist/src/core/living-docs/index.d.ts +89 -0
  38. package/dist/src/core/living-docs/index.d.ts.map +1 -0
  39. package/dist/src/core/living-docs/index.js +169 -0
  40. package/dist/src/core/living-docs/index.js.map +1 -0
  41. package/dist/src/core/living-docs/project-detector.d.ts +103 -0
  42. package/dist/src/core/living-docs/project-detector.d.ts.map +1 -0
  43. package/dist/src/core/living-docs/project-detector.js +314 -0
  44. package/dist/src/core/living-docs/project-detector.js.map +1 -0
  45. package/dist/src/core/repo-structure/prompt-consolidator.d.ts +1 -1
  46. package/dist/src/core/repo-structure/prompt-consolidator.d.ts.map +1 -1
  47. package/dist/src/core/repo-structure/prompt-consolidator.js +51 -22
  48. package/dist/src/core/repo-structure/prompt-consolidator.js.map +1 -1
  49. package/dist/src/core/repo-structure/repo-structure-manager.d.ts +2 -0
  50. package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
  51. package/dist/src/core/repo-structure/repo-structure-manager.js +126 -62
  52. package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
  53. package/dist/src/core/spec-content-sync.d.ts +6 -0
  54. package/dist/src/core/spec-content-sync.d.ts.map +1 -1
  55. package/dist/src/core/spec-content-sync.js +25 -7
  56. package/dist/src/core/spec-content-sync.js.map +1 -1
  57. package/dist/src/core/spec-detector.d.ts +54 -0
  58. package/dist/src/core/spec-detector.d.ts.map +1 -0
  59. package/dist/src/core/spec-detector.js +197 -0
  60. package/dist/src/core/spec-detector.js.map +1 -0
  61. package/dist/src/core/spec-identifier-detector.d.ts +49 -0
  62. package/dist/src/core/spec-identifier-detector.d.ts.map +1 -0
  63. package/dist/src/core/spec-identifier-detector.js +198 -0
  64. package/dist/src/core/spec-identifier-detector.js.map +1 -0
  65. package/dist/src/core/types/spec-identifier.d.ts +67 -0
  66. package/dist/src/core/types/spec-identifier.d.ts.map +1 -0
  67. package/dist/src/core/types/spec-identifier.js +23 -0
  68. package/dist/src/core/types/spec-identifier.js.map +1 -0
  69. package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
  70. package/dist/src/utils/docs-preview/config-generator.js +11 -0
  71. package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
  72. package/package.json +2 -1
  73. package/plugins/specweave/.claude-plugin/plugin.json +1 -1
  74. package/plugins/specweave/lib/hooks/sync-living-docs.js +61 -5
  75. package/plugins/specweave/lib/hooks/sync-living-docs.ts +102 -13
  76. package/plugins/specweave-ado/.claude-plugin/plugin.json +1 -1
  77. package/plugins/specweave-docs-preview/commands/preview.md +11 -11
  78. package/plugins/specweave-github/.claude-plugin/plugin.json +1 -1
  79. package/plugins/specweave-github/hooks/post-task-completion.sh +59 -30
  80. package/plugins/specweave-github/lib/github-spec-content-sync.js +55 -6
  81. package/plugins/specweave-github/lib/github-spec-content-sync.ts +84 -12
  82. package/plugins/specweave-jira/.claude-plugin/plugin.json +1 -1
  83. package/plugins/specweave-mobile/.claude-plugin/plugin.json +1 -4
  84. package/plugins/specweave-release/.claude-plugin/plugin.json +3 -15
  85. package/src/templates/AGENTS.md.template +297 -0
  86. package/dist/locales/de/.gitkeep +0 -0
  87. package/dist/locales/de/cli.json +0 -108
  88. package/dist/locales/en/cli.json +0 -287
  89. package/dist/locales/en/errors.json +0 -7
  90. package/dist/locales/en/templates.json +0 -6
  91. package/dist/locales/es/.gitkeep +0 -0
  92. package/dist/locales/es/cli.json +0 -41
  93. package/dist/locales/fr/.gitkeep +0 -0
  94. package/dist/locales/fr/cli.json +0 -108
  95. package/dist/locales/ja/.gitkeep +0 -0
  96. package/dist/locales/ja/cli.json +0 -108
  97. package/dist/locales/ko/.gitkeep +0 -0
  98. package/dist/locales/ko/cli.json +0 -108
  99. package/dist/locales/pt/.gitkeep +0 -0
  100. package/dist/locales/pt/cli.json +0 -108
  101. package/dist/locales/ru/.gitkeep +0 -0
  102. package/dist/locales/ru/cli.json +0 -269
  103. package/dist/locales/zh/.gitkeep +0 -0
  104. package/dist/locales/zh/cli.json +0 -108
  105. package/plugins/specweave/plugin.json +0 -22
  106. package/plugins/specweave-ado/plugin.json +0 -20
  107. package/plugins/specweave-github/plugin.json +0 -19
  108. package/plugins/specweave-jira/plugin.json +0 -20
@@ -18,20 +18,76 @@ async function syncLivingDocs(incrementId) {
18
18
  return;
19
19
  }
20
20
  console.log("\u2705 Living docs sync enabled");
21
- const specCopied = await copyIncrementSpecToLivingDocs(incrementId);
22
- const changedDocs = detectChangedDocs();
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{1F4CB} Using simple sync mode (legacy)");
31
+ specCopied = await copyIncrementSpecToLivingDocs(incrementId);
32
+ changedDocs = detectChangedDocs();
33
+ }
23
34
  if (changedDocs.length === 0 && !specCopied) {
24
- console.log("\u2139\uFE0F No living docs changed (no git diff in .specweave/docs/)");
35
+ console.log("\u2139\uFE0F No living docs changed");
25
36
  return;
26
37
  }
27
- console.log(`\u{1F4C4} Detected ${changedDocs.length} changed doc(s):`);
28
- changedDocs.forEach((doc) => console.log(` - ${doc}`));
38
+ console.log(`\u{1F4C4} Changed/created ${changedDocs.length} file(s)`);
29
39
  await syncToGitHub(incrementId, changedDocs);
30
40
  console.log("\u2705 Living docs sync complete\n");
31
41
  } catch (error) {
32
42
  console.error("\u274C Error syncing living docs:", error);
33
43
  }
34
44
  }
45
+ async function intelligentSyncLivingDocs(incrementId, config) {
46
+ try {
47
+ const { syncIncrement } = await import("../../../../src/core/living-docs/index.js");
48
+ console.log(" \u{1F4D6} Parsing and classifying spec sections...");
49
+ const result = await syncIncrement(incrementId, {
50
+ verbose: false,
51
+ // We'll log our own summary
52
+ dryRun: false,
53
+ parser: {
54
+ preserveCodeBlocks: true,
55
+ preserveLinks: true,
56
+ preserveImages: true
57
+ },
58
+ distributor: {
59
+ generateFrontmatter: true,
60
+ preserveOriginal: config.livingDocs?.intelligent?.preserveOriginal ?? true
61
+ },
62
+ linker: {
63
+ generateBacklinks: config.livingDocs?.intelligent?.generateCrossLinks ?? true,
64
+ updateExisting: true
65
+ }
66
+ });
67
+ console.log(` \u2705 Intelligent sync complete:`);
68
+ console.log(` Project: ${result.project.name} (${(result.project.confidence * 100).toFixed(0)}% confidence)`);
69
+ console.log(` Files created: ${result.distribution.summary.filesCreated}`);
70
+ console.log(` Files updated: ${result.distribution.summary.filesUpdated}`);
71
+ console.log(` Cross-links: ${result.links.length}`);
72
+ console.log(` Duration: ${result.duration}ms`);
73
+ const changedFiles = [
74
+ ...result.distribution.created.map((f) => f.path),
75
+ ...result.distribution.updated.map((f) => f.path)
76
+ ];
77
+ return {
78
+ success: result.success,
79
+ changedFiles
80
+ };
81
+ } catch (error) {
82
+ console.error(` \u274C Intelligent sync failed: ${error}`);
83
+ console.error(" Falling back to simple sync mode...");
84
+ const copied = await copyIncrementSpecToLivingDocs(incrementId);
85
+ return {
86
+ success: copied,
87
+ changedFiles: copied ? [path.join(process.cwd(), ".specweave", "docs", "internal", "specs", `spec-${incrementId}.md`)] : []
88
+ };
89
+ }
90
+ }
35
91
  async function copyIncrementSpecToLivingDocs(incrementId) {
36
92
  try {
37
93
  const incrementSpecPath = path.join(process.cwd(), ".specweave", "increments", incrementId, "spec.md");
@@ -5,6 +5,10 @@
5
5
  *
6
6
  * Automatically syncs living documentation after task completion.
7
7
  *
8
+ * Supports two modes:
9
+ * 1. Simple mode: Copy spec.md to living docs (legacy)
10
+ * 2. Intelligent mode: Parse, classify, and distribute content (v0.18.0+)
11
+ *
8
12
  * Usage:
9
13
  * node dist/hooks/lib/sync-living-docs.js <incrementId>
10
14
  *
@@ -13,12 +17,13 @@
13
17
  *
14
18
  * What it does:
15
19
  * 1. Checks if sync_living_docs enabled in config
16
- * 2. Detects changed docs via git diff
17
- * 3. Invokes /sync-docs update command (future implementation)
18
- * 4. Logs sync actions
20
+ * 2. Detects sync mode (simple or intelligent)
21
+ * 3. Runs appropriate sync strategy
22
+ * 4. Syncs to external tools (GitHub/Jira/ADO)
23
+ * 5. Logs sync actions
19
24
  *
20
25
  * @author SpecWeave Team
21
- * @version 1.0.0
26
+ * @version 2.0.0
22
27
  */
23
28
 
24
29
  import fs from 'fs-extra';
@@ -31,6 +36,16 @@ interface Config {
31
36
  sync_living_docs?: boolean;
32
37
  };
33
38
  };
39
+ livingDocs?: {
40
+ intelligent?: {
41
+ enabled?: boolean;
42
+ splitByCategory?: boolean;
43
+ generateCrossLinks?: boolean;
44
+ preserveOriginal?: boolean;
45
+ classificationConfidenceThreshold?: number;
46
+ fallbackProject?: string;
47
+ };
48
+ };
34
49
  }
35
50
 
36
51
  /**
@@ -59,19 +74,31 @@ async function syncLivingDocs(incrementId: string): Promise<void> {
59
74
 
60
75
  console.log('✅ Living docs sync enabled');
61
76
 
62
- // 3. Copy increment spec to living docs
63
- const specCopied = await copyIncrementSpecToLivingDocs(incrementId);
64
-
65
- // 4. Detect changed docs via git diff
66
- const changedDocs = detectChangedDocs();
77
+ // 3. Determine sync mode (simple or intelligent)
78
+ const intelligentEnabled = config.livingDocs?.intelligent?.enabled ?? false;
79
+
80
+ let specCopied = false;
81
+ let changedDocs: string[] = [];
82
+
83
+ if (intelligentEnabled) {
84
+ // Intelligent mode: Parse, classify, and distribute
85
+ console.log('🧠 Using intelligent sync mode (v0.18.0+)');
86
+ const result = await intelligentSyncLivingDocs(incrementId, config);
87
+ specCopied = result.success;
88
+ changedDocs = result.changedFiles;
89
+ } else {
90
+ // Simple mode: Copy spec to living docs (legacy)
91
+ console.log('📋 Using simple sync mode (legacy)');
92
+ specCopied = await copyIncrementSpecToLivingDocs(incrementId);
93
+ changedDocs = detectChangedDocs();
94
+ }
67
95
 
68
96
  if (changedDocs.length === 0 && !specCopied) {
69
- console.log('â„šī¸ No living docs changed (no git diff in .specweave/docs/)');
97
+ console.log('â„šī¸ No living docs changed');
70
98
  return;
71
99
  }
72
100
 
73
- console.log(`📄 Detected ${changedDocs.length} changed doc(s):`);
74
- changedDocs.forEach((doc) => console.log(` - ${doc}`));
101
+ console.log(`📄 Changed/created ${changedDocs.length} file(s)`);
75
102
 
76
103
  // 4. Sync to GitHub if configured
77
104
  await syncToGitHub(incrementId, changedDocs);
@@ -85,7 +112,69 @@ async function syncLivingDocs(incrementId: string): Promise<void> {
85
112
  }
86
113
 
87
114
  /**
88
- * Copy increment spec to living docs
115
+ * Intelligent sync using the new architecture (v0.18.0+)
116
+ */
117
+ async function intelligentSyncLivingDocs(
118
+ incrementId: string,
119
+ config: Config
120
+ ): Promise<{ success: boolean; changedFiles: string[] }> {
121
+ try {
122
+ // Dynamic import to avoid circular dependencies
123
+ const { syncIncrement } = await import('../../../../src/core/living-docs/index.js');
124
+
125
+ console.log(' 📖 Parsing and classifying spec sections...');
126
+
127
+ const result = await syncIncrement(incrementId, {
128
+ verbose: false, // We'll log our own summary
129
+ dryRun: false,
130
+ parser: {
131
+ preserveCodeBlocks: true,
132
+ preserveLinks: true,
133
+ preserveImages: true,
134
+ },
135
+ distributor: {
136
+ generateFrontmatter: true,
137
+ preserveOriginal: config.livingDocs?.intelligent?.preserveOriginal ?? true,
138
+ },
139
+ linker: {
140
+ generateBacklinks: config.livingDocs?.intelligent?.generateCrossLinks ?? true,
141
+ updateExisting: true,
142
+ },
143
+ });
144
+
145
+ // Log summary
146
+ console.log(` ✅ Intelligent sync complete:`);
147
+ console.log(` Project: ${result.project.name} (${(result.project.confidence * 100).toFixed(0)}% confidence)`);
148
+ console.log(` Files created: ${result.distribution.summary.filesCreated}`);
149
+ console.log(` Files updated: ${result.distribution.summary.filesUpdated}`);
150
+ console.log(` Cross-links: ${result.links.length}`);
151
+ console.log(` Duration: ${result.duration}ms`);
152
+
153
+ // Collect changed file paths
154
+ const changedFiles = [
155
+ ...result.distribution.created.map((f) => f.path),
156
+ ...result.distribution.updated.map((f) => f.path),
157
+ ];
158
+
159
+ return {
160
+ success: result.success,
161
+ changedFiles,
162
+ };
163
+ } catch (error) {
164
+ console.error(` ❌ Intelligent sync failed: ${error}`);
165
+ console.error(' Falling back to simple sync mode...');
166
+
167
+ // Fallback to simple mode
168
+ const copied = await copyIncrementSpecToLivingDocs(incrementId);
169
+ return {
170
+ success: copied,
171
+ changedFiles: copied ? [path.join(process.cwd(), '.specweave', 'docs', 'internal', 'specs', `spec-${incrementId}.md`)] : [],
172
+ };
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Copy increment spec to living docs (legacy/simple mode)
89
178
  * Returns true if spec was copied, false if skipped
90
179
  */
91
180
  async function copyIncrementSpecToLivingDocs(incrementId: string): Promise<boolean> {
@@ -17,5 +17,5 @@
17
17
  "integration"
18
18
  ],
19
19
  "license": "MIT",
20
- "hooks": "hooks/hooks.json"
20
+ "hooks": "./hooks/hooks.json"
21
21
  }
@@ -51,7 +51,7 @@ if (fs.existsSync(configPath)) {
51
51
  const docsConfig = config.documentation?.preview || {
52
52
  enabled: true,
53
53
  autoInstall: true,
54
- port: 3015,
54
+ port: 3016,
55
55
  openBrowser: true,
56
56
  theme: 'default',
57
57
  excludeFolders: ['legacy', 'node_modules']
@@ -103,7 +103,7 @@ if (setupNeeded) {
103
103
  ```typescript
104
104
  try {
105
105
  const options = {
106
- port: docsConfig.port || 3015,
106
+ port: docsConfig.port || 3016,
107
107
  openBrowser: docsConfig.openBrowser !== false,
108
108
  theme: docsConfig.theme || 'default',
109
109
  excludeFolders: docsConfig.excludeFolders || ['legacy', 'node_modules']
@@ -135,8 +135,8 @@ try {
135
135
  } else if (error.message.includes('port')) {
136
136
  console.log('💡 Solution:');
137
137
  console.log(' â€ĸ Change port in .specweave/config.json');
138
- console.log(' â€ĸ Or stop the service using port ' + (docsConfig.port || 3015));
139
- console.log(' â€ĸ Check with: lsof -i :' + (docsConfig.port || 3015) + '\n');
138
+ console.log(' â€ĸ Or stop the service using port ' + (docsConfig.port || 3016));
139
+ console.log(' â€ĸ Check with: lsof -i :' + (docsConfig.port || 3016) + '\n');
140
140
  } else {
141
141
  console.log('💡 Troubleshooting:');
142
142
  console.log(' â€ĸ Check Node.js version (18+ required): node --version');
@@ -160,7 +160,7 @@ The command uses settings from `.specweave/config.json`:
160
160
  "preview": {
161
161
  "enabled": true,
162
162
  "autoInstall": true,
163
- "port": 3015,
163
+ "port": 3016,
164
164
  "openBrowser": true,
165
165
  "theme": "default",
166
166
  "excludeFolders": ["legacy", "node_modules"]
@@ -250,11 +250,11 @@ Changes to markdown files in `.specweave/docs/internal/` are detected automatica
250
250
  ✓ Packages installed successfully
251
251
  ✓ Configuration generated
252
252
  ✓ Sidebar generated (42 documents, 8 categories)
253
- ✓ Server started on http://localhost:3015
253
+ ✓ Server started on http://localhost:3016
254
254
 
255
255
  ✅ Documentation server started successfully!
256
256
 
257
- 🌐 URL: http://localhost:3015
257
+ 🌐 URL: http://localhost:3016
258
258
  🔄 Hot reload enabled - edit markdown files to see changes instantly
259
259
  đŸ—‚ī¸ Sidebar auto-generated from folder structure
260
260
  📊 Mermaid diagrams supported
@@ -270,11 +270,11 @@ Changes to markdown files in `.specweave/docs/internal/` are detected automatica
270
270
  ✓ Configuration up-to-date
271
271
 
272
272
  ✓ Sidebar generated (42 documents, 8 categories)
273
- ✓ Server started on http://localhost:3015
273
+ ✓ Server started on http://localhost:3016
274
274
 
275
275
  ✅ Documentation server started successfully!
276
276
 
277
- 🌐 URL: http://localhost:3015
277
+ 🌐 URL: http://localhost:3016
278
278
  🔄 Hot reload enabled - edit markdown files to see changes instantly
279
279
  đŸ—‚ī¸ Sidebar auto-generated from folder structure
280
280
  📊 Mermaid diagrams supported
@@ -288,11 +288,11 @@ Changes to markdown files in `.specweave/docs/internal/` are detected automatica
288
288
 
289
289
  **Port Already in Use:**
290
290
  ```
291
- Error: Port 3015 is already in use
291
+ Error: Port 3016 is already in use
292
292
  ```
293
293
  Solution:
294
294
  1. Change port in `.specweave/config.json` → `documentation.preview.port`
295
- 2. Or stop the service: `lsof -i :3015` then `kill -9 <PID>`
295
+ 2. Or stop the service: `lsof -i :3016` then `kill -9 <PID>`
296
296
 
297
297
  **Node.js Version:**
298
298
  ```
@@ -16,5 +16,5 @@
16
16
  "sync",
17
17
  "specweave"
18
18
  ],
19
- "hooks": "hooks/hooks.json"
19
+ "hooks": "./hooks/hooks.json"
20
20
  }
@@ -95,10 +95,10 @@ EOF
95
95
  fi
96
96
 
97
97
  # ============================================================================
98
- # DETECT CURRENT SPEC
98
+ # DETECT ALL SPECS (Multi-Spec Support)
99
99
  # ============================================================================
100
100
 
101
- # Strategy: Find current increment, then find which spec it belongs to
101
+ # Strategy: Use multi-spec detector to find ALL specs referenced in current increment
102
102
 
103
103
  # 1. Detect current increment (temporary context)
104
104
  CURRENT_INCREMENT=$(ls -t .specweave/increments/ 2>/dev/null | grep -v "_backlog" | head -1)
@@ -108,49 +108,78 @@ if [ -z "$CURRENT_INCREMENT" ]; then
108
108
  # Fall through to sync all changed specs
109
109
  fi
110
110
 
111
- SPEC_ID=""
111
+ # 2. Use TypeScript CLI to detect all specs
112
+ DETECT_CLI="$PROJECT_ROOT/dist/src/cli/commands/detect-specs.js"
112
113
 
113
- if [ -n "$CURRENT_INCREMENT" ]; then
114
- # 2. Try to find spec reference in increment
115
- SPEC_FILE=".specweave/increments/$CURRENT_INCREMENT/spec.md"
114
+ if [ -f "$DETECT_CLI" ]; then
115
+ echo "[$(date)] [GitHub] 🔍 Detecting all specs in increment $CURRENT_INCREMENT..." >> "$DEBUG_LOG" 2>/dev/null || true
116
116
 
117
- if [ -f "$SPEC_FILE" ]; then
118
- # Look for "Implements: SPEC-XXX" or "See: SPEC-XXX" patterns
119
- SPEC_REF=$(grep -E "^(Implements|See|References).*SPEC-[0-9]+" "$SPEC_FILE" 2>/dev/null | head -1 || echo "")
117
+ # Call detect-specs CLI and capture JSON output
118
+ DETECTION_RESULT=$(node "$DETECT_CLI" 2>> "$DEBUG_LOG" || echo "{}")
120
119
 
121
- if [ -n "$SPEC_REF" ]; then
122
- # Extract spec ID (e.g., "SPEC-001" → "spec-001")
123
- SPEC_ID=$(echo "$SPEC_REF" | grep -oE "SPEC-[0-9]+" | tr 'A-Z' 'a-z' | head -1)
124
- echo "[$(date)] [GitHub] 📋 Detected spec: $SPEC_ID (from increment $CURRENT_INCREMENT)" >> "$DEBUG_LOG" 2>/dev/null || true
125
- fi
126
- fi
120
+ # Extract spec count
121
+ SPEC_COUNT=$(echo "$DETECTION_RESULT" | node -e "const fs=require('fs'); const data=JSON.parse(fs.readFileSync(0,'utf-8')); console.log(data.specs?.length || 0)")
122
+
123
+ echo "[$(date)] [GitHub] 📋 Detected $SPEC_COUNT spec(s)" >> "$DEBUG_LOG" 2>/dev/null || true
124
+
125
+ # Store detection result for later use
126
+ echo "$DETECTION_RESULT" > /tmp/specweave-detected-specs.json
127
+ else
128
+ echo "[$(date)] [GitHub] âš ī¸ detect-specs CLI not found at $DETECT_CLI, falling back to git diff" >> "$DEBUG_LOG" 2>/dev/null || true
129
+ SPEC_COUNT=0
127
130
  fi
128
131
 
129
132
  # ============================================================================
130
- # SYNC SPEC TO GITHUB
133
+ # SYNC ALL DETECTED SPECS TO GITHUB (Multi-Spec Support)
131
134
  # ============================================================================
132
135
 
133
- if [ -n "$SPEC_ID" ]; then
134
- # Convert SPEC_ID to spec file path
135
- SPEC_FILE=$(find .specweave/docs/internal/specs -name "${SPEC_ID}*.md" -o -name "${SPEC_ID}.md" 2>/dev/null | head -1)
136
+ if [ -f /tmp/specweave-detected-specs.json ] && [ "$SPEC_COUNT" -gt 0 ]; then
137
+ # Multi-spec sync: Loop through all detected specs
138
+ echo "[$(date)] [GitHub] 🔄 Syncing $SPEC_COUNT spec(s) to GitHub..." >> "$DEBUG_LOG" 2>/dev/null || true
139
+
140
+ # Extract spec paths using Node.js
141
+ SPEC_PATHS=$(node -e "
142
+ const fs = require('fs');
143
+ const data = JSON.parse(fs.readFileSync('/tmp/specweave-detected-specs.json', 'utf-8'));
144
+ const syncable = data.specs.filter(s => s.syncEnabled && s.project !== '_parent');
145
+ syncable.forEach(s => console.log(s.path));
146
+ " 2>> "$DEBUG_LOG")
147
+
148
+ # Count syncable specs
149
+ SYNCABLE_COUNT=$(echo "$SPEC_PATHS" | grep -v '^$' | wc -l | tr -d ' ')
136
150
 
137
- if [ -n "$SPEC_FILE" ]; then
138
- # Sync specific spec
139
- echo "[$(date)] [GitHub] 🔄 Syncing spec $SPEC_ID ($SPEC_FILE) to GitHub..." >> "$DEBUG_LOG" 2>/dev/null || true
151
+ if [ "$SYNCABLE_COUNT" -gt 0 ]; then
152
+ echo "[$(date)] [GitHub] 📋 Syncing $SYNCABLE_COUNT syncable spec(s) (excluding _parent)" >> "$DEBUG_LOG" 2>/dev/null || true
140
153
 
141
- (cd "$PROJECT_ROOT" && node "$SYNC_CLI" --spec "$SPEC_FILE" --provider github) 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
142
- echo "[$(date)] [GitHub] âš ī¸ Spec sync failed for $SPEC_ID (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
143
- }
154
+ # Sync each spec
155
+ echo "$SPEC_PATHS" | while read -r SPEC_FILE; do
156
+ if [ -n "$SPEC_FILE" ] && [ -f "$SPEC_FILE" ]; then
157
+ # Extract project and spec ID from path
158
+ SPEC_NAME=$(basename "$SPEC_FILE" .md)
159
+ PROJECT=$(basename "$(dirname "$SPEC_FILE")")
144
160
 
145
- echo "[$(date)] [GitHub] ✅ Spec sync complete for $SPEC_ID" >> "$DEBUG_LOG" 2>/dev/null || true
161
+ echo "[$(date)] [GitHub] 🔄 Syncing $PROJECT/$SPEC_NAME..." >> "$DEBUG_LOG" 2>/dev/null || true
162
+
163
+ (cd "$PROJECT_ROOT" && node "$SYNC_CLI" --spec "$SPEC_FILE" --provider github) 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
164
+ echo "[$(date)] [GitHub] âš ī¸ Spec sync failed for $PROJECT/$SPEC_NAME (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
165
+ }
166
+
167
+ echo "[$(date)] [GitHub] ✅ Synced $PROJECT/$SPEC_NAME" >> "$DEBUG_LOG" 2>/dev/null || true
168
+ fi
169
+ done
170
+
171
+ echo "[$(date)] [GitHub] ✅ Multi-spec sync complete ($SYNCABLE_COUNT synced)" >> "$DEBUG_LOG" 2>/dev/null || true
146
172
  else
147
- echo "[$(date)] [GitHub] âš ī¸ Spec file not found for $SPEC_ID" >> "$DEBUG_LOG" 2>/dev/null || true
173
+ echo "[$(date)] [GitHub] â„šī¸ No syncable specs (all specs are _parent or syncEnabled=false)" >> "$DEBUG_LOG" 2>/dev/null || true
148
174
  fi
175
+
176
+ # Cleanup temp file
177
+ rm -f /tmp/specweave-detected-specs.json 2>/dev/null || true
149
178
  else
150
- # Sync all modified specs (check git diff)
179
+ # Fallback: Sync all modified specs (check git diff)
151
180
  echo "[$(date)] [GitHub] 🔄 Checking for modified specs..." >> "$DEBUG_LOG" 2>/dev/null || true
152
181
 
153
- MODIFIED_SPECS=$(git diff --name-only HEAD .specweave/docs/internal/specs/*.md 2>/dev/null || echo "")
182
+ MODIFIED_SPECS=$(git diff --name-only HEAD .specweave/docs/internal/specs/**/*.md 2>/dev/null || echo "")
154
183
 
155
184
  if [ -n "$MODIFIED_SPECS" ]; then
156
185
  echo "[$(date)] [GitHub] 📝 Found modified specs:" >> "$DEBUG_LOG" 2>/dev/null || true
@@ -158,7 +187,7 @@ else
158
187
 
159
188
  # Sync each modified spec
160
189
  echo "$MODIFIED_SPECS" | while read -r SPEC_FILE; do
161
- if [ -n "$SPEC_FILE" ]; then
190
+ if [ -n "$SPEC_FILE" ] && [ -f "$SPEC_FILE" ]; then
162
191
  echo "[$(date)] [GitHub] 🔄 Syncing $SPEC_FILE..." >> "$DEBUG_LOG" 2>/dev/null || true
163
192
  (cd "$PROJECT_ROOT" && node "$SYNC_CLI" --spec "$SPEC_FILE" --provider github) 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || true
164
193
  fi
@@ -8,8 +8,34 @@ import {
8
8
  } from "../../../src/core/spec-content-sync.js";
9
9
  import path from "path";
10
10
  import fs from "fs/promises";
11
+ async function getGitHubRepoForProject(project, specPath) {
12
+ try {
13
+ let currentDir = path.dirname(specPath);
14
+ let configPath = path.join(currentDir, ".specweave", "config.json");
15
+ while (!await fs.access(configPath).then(() => true).catch(() => false)) {
16
+ const parentDir = path.dirname(currentDir);
17
+ if (parentDir === currentDir) {
18
+ return null;
19
+ }
20
+ currentDir = parentDir;
21
+ configPath = path.join(currentDir, ".specweave", "config.json");
22
+ }
23
+ const configContent = await fs.readFile(configPath, "utf-8");
24
+ const config = JSON.parse(configContent);
25
+ const projectConfig = config.specs?.projects?.[project];
26
+ if (!projectConfig?.github) {
27
+ return null;
28
+ }
29
+ return {
30
+ owner: projectConfig.github.owner,
31
+ repo: projectConfig.github.repo
32
+ };
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
11
37
  async function syncSpecContentToGitHub(options) {
12
- const { specPath, owner, repo, dryRun = false, verbose = false } = options;
38
+ let { specPath, owner, repo, dryRun = false, verbose = false } = options;
13
39
  try {
14
40
  const spec = await parseSpecContent(specPath);
15
41
  if (!spec) {
@@ -20,10 +46,26 @@ async function syncSpecContentToGitHub(options) {
20
46
  };
21
47
  }
22
48
  if (verbose) {
23
- console.log(`\u{1F4C4} Parsed spec: ${spec.id}`);
49
+ console.log(`\u{1F4C4} Parsed spec: ${spec.identifier.compact}`);
50
+ console.log(` Project: ${spec.project}`);
24
51
  console.log(` Title: ${spec.title}`);
25
52
  console.log(` User Stories: ${spec.userStories.length}`);
26
53
  }
54
+ if (!owner || !repo) {
55
+ const repoConfig = await getGitHubRepoForProject(spec.project, specPath);
56
+ if (!repoConfig) {
57
+ return {
58
+ success: false,
59
+ action: "error",
60
+ error: `No GitHub repository configured for project "${spec.project}". Add specs.projects.${spec.project}.github in config.json`
61
+ };
62
+ }
63
+ owner = repoConfig.owner;
64
+ repo = repoConfig.repo;
65
+ if (verbose) {
66
+ console.log(` Auto-detected repo: ${owner}/${repo}`);
67
+ }
68
+ }
27
69
  const existingIssueNumber = await hasExternalLink(specPath, "github");
28
70
  const client = GitHubClientV2.fromRepo(owner, repo);
29
71
  if (existingIssueNumber) {
@@ -42,7 +84,7 @@ async function syncSpecContentToGitHub(options) {
42
84
  async function createGitHubIssue(client, spec, options) {
43
85
  const { specPath, dryRun, verbose } = options;
44
86
  try {
45
- const title = `[${spec.id.toUpperCase()}] ${spec.title}`;
87
+ const title = `[${spec.identifier.compact}] ${spec.title}`;
46
88
  const body = buildExternalDescription(spec);
47
89
  if (verbose) {
48
90
  console.log(`
@@ -62,7 +104,13 @@ ${body}`);
62
104
  externalUrl: "https://github.com/DRY-RUN"
63
105
  };
64
106
  }
65
- const labels = ["specweave", "spec", spec.metadata.priority || "P2"].filter(Boolean);
107
+ const labels = [
108
+ "specweave",
109
+ "spec",
110
+ spec.project,
111
+ // backend, frontend, mobile, etc.
112
+ spec.metadata.priority || "P2"
113
+ ].filter(Boolean);
66
114
  const issue = await client.createEpicIssue(title, body, void 0, labels);
67
115
  if (verbose) {
68
116
  console.log(`\u2705 Created issue #${issue.number}`);
@@ -96,8 +144,9 @@ async function updateGitHubIssue(client, spec, issueNumber, options) {
96
144
  console.log(`
97
145
  \u{1F504} Checking for changes in issue #${issueNumber}`);
98
146
  }
147
+ const cleanTitle = issue.title.replace(/^\[[A-Z]{2,4}-[A-Z0-9-]+\]\s*/, "");
99
148
  const changes = detectContentChanges(spec, {
100
- title: issue.title.replace(/^\[SPEC-\d+\]\s*/, ""),
149
+ title: cleanTitle,
101
150
  description: issue.body || "",
102
151
  userStoryCount: countUserStoriesInBody(issue.body || "")
103
152
  });
@@ -118,7 +167,7 @@ async function updateGitHubIssue(client, spec, issueNumber, options) {
118
167
  console.log(` - ${change}`);
119
168
  }
120
169
  }
121
- const newTitle = `[${spec.id.toUpperCase()}] ${spec.title}`;
170
+ const newTitle = `[${spec.identifier.compact}] ${spec.title}`;
122
171
  const newBody = buildExternalDescription(spec);
123
172
  if (dryRun) {
124
173
  console.log("\n\u{1F50D} Dry run - would update issue:");