specweave 0.28.17 → 0.28.19

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 (137) hide show
  1. package/dist/plugins/specweave-ado/lib/ado-board-resolver.d.ts +94 -0
  2. package/dist/plugins/specweave-ado/lib/ado-board-resolver.d.ts.map +1 -0
  3. package/dist/plugins/specweave-ado/lib/ado-board-resolver.js +219 -0
  4. package/dist/plugins/specweave-ado/lib/ado-board-resolver.js.map +1 -0
  5. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +6 -11
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.js +6 -11
  8. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  9. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +19 -0
  10. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +1 -0
  11. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +380 -0
  12. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +1 -0
  13. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +92 -0
  14. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +1 -0
  15. package/dist/plugins/specweave-github/lib/increment-issue-builder.js +349 -0
  16. package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +1 -0
  17. package/dist/plugins/specweave-jira/lib/jira-board-resolver.d.ts +50 -0
  18. package/dist/plugins/specweave-jira/lib/jira-board-resolver.d.ts.map +1 -0
  19. package/dist/plugins/specweave-jira/lib/jira-board-resolver.js +84 -0
  20. package/dist/plugins/specweave-jira/lib/jira-board-resolver.js.map +1 -0
  21. package/dist/src/cli/commands/import-external.d.ts.map +1 -1
  22. package/dist/src/cli/commands/import-external.js +12 -7
  23. package/dist/src/cli/commands/import-external.js.map +1 -1
  24. package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
  25. package/dist/src/cli/helpers/init/external-import.js +122 -17
  26. package/dist/src/cli/helpers/init/external-import.js.map +1 -1
  27. package/dist/src/cli/helpers/issue-tracker/ado-area-selection.d.ts +65 -0
  28. package/dist/src/cli/helpers/issue-tracker/ado-area-selection.d.ts.map +1 -0
  29. package/dist/src/cli/helpers/issue-tracker/ado-area-selection.js +278 -0
  30. package/dist/src/cli/helpers/issue-tracker/ado-area-selection.js.map +1 -0
  31. package/dist/src/cli/helpers/issue-tracker/jira-board-selection.d.ts +64 -0
  32. package/dist/src/cli/helpers/issue-tracker/jira-board-selection.d.ts.map +1 -0
  33. package/dist/src/cli/helpers/issue-tracker/jira-board-selection.js +251 -0
  34. package/dist/src/cli/helpers/issue-tracker/jira-board-selection.js.map +1 -0
  35. package/dist/src/core/ac-test-validator-cli.js +4 -1
  36. package/dist/src/core/ac-test-validator-cli.js.map +1 -1
  37. package/dist/src/core/ac-test-validator.d.ts.map +1 -1
  38. package/dist/src/core/ac-test-validator.js +4 -1
  39. package/dist/src/core/ac-test-validator.js.map +1 -1
  40. package/dist/src/core/types/increment-metadata.d.ts +75 -0
  41. package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
  42. package/dist/src/core/types/sync-profile.d.ts +137 -5
  43. package/dist/src/core/types/sync-profile.d.ts.map +1 -1
  44. package/dist/src/core/types/sync-profile.js +63 -0
  45. package/dist/src/core/types/sync-profile.js.map +1 -1
  46. package/dist/src/importers/external-importer.d.ts +25 -0
  47. package/dist/src/importers/external-importer.d.ts.map +1 -1
  48. package/dist/src/importers/github-importer.d.ts.map +1 -1
  49. package/dist/src/importers/github-importer.js +5 -3
  50. package/dist/src/importers/github-importer.js.map +1 -1
  51. package/dist/src/importers/item-converter.d.ts +51 -0
  52. package/dist/src/importers/item-converter.d.ts.map +1 -1
  53. package/dist/src/importers/item-converter.js +39 -12
  54. package/dist/src/importers/item-converter.js.map +1 -1
  55. package/dist/src/init/repo/types.d.ts +1 -1
  56. package/dist/src/living-docs/fs-id-allocator.d.ts +72 -3
  57. package/dist/src/living-docs/fs-id-allocator.d.ts.map +1 -1
  58. package/dist/src/living-docs/fs-id-allocator.js +142 -16
  59. package/dist/src/living-docs/fs-id-allocator.js.map +1 -1
  60. package/dist/src/locales/de/cli.json +14 -0
  61. package/dist/src/locales/es/cli.json +14 -0
  62. package/dist/src/locales/fr/cli.json +14 -0
  63. package/dist/src/locales/ja/cli.json +14 -0
  64. package/dist/src/locales/ko/cli.json +14 -0
  65. package/dist/src/locales/pt/cli.json +14 -0
  66. package/dist/src/locales/ru/cli.json +14 -0
  67. package/dist/src/locales/zh/cli.json +14 -0
  68. package/dist/src/utils/chalk-fallback.d.ts +38 -0
  69. package/dist/src/utils/chalk-fallback.d.ts.map +1 -0
  70. package/dist/src/utils/chalk-fallback.js +118 -0
  71. package/dist/src/utils/chalk-fallback.js.map +1 -0
  72. package/dist/src/utils/project-id-generator.d.ts +127 -0
  73. package/dist/src/utils/project-id-generator.d.ts.map +1 -0
  74. package/dist/src/utils/project-id-generator.js +228 -0
  75. package/dist/src/utils/project-id-generator.js.map +1 -0
  76. package/package.json +1 -1
  77. package/plugins/specweave/agents/pm/AGENT.md +202 -0
  78. package/plugins/specweave/commands/specweave-import-external.md +5 -3
  79. package/plugins/specweave/commands/specweave-sync-docs.md +6 -2
  80. package/plugins/specweave/hooks/pre-task-completion.sh +35 -17
  81. package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.d.ts +16 -0
  82. package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.js +121 -0
  83. package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.js.map +1 -0
  84. package/plugins/specweave/lib/vendor/core/ac-test-validator.d.ts +111 -0
  85. package/plugins/specweave/lib/vendor/core/ac-test-validator.js +295 -0
  86. package/plugins/specweave/lib/vendor/core/ac-test-validator.js.map +1 -0
  87. package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +75 -0
  88. package/plugins/specweave/lib/vendor/utils/chalk-fallback.d.ts +38 -0
  89. package/plugins/specweave/lib/vendor/utils/chalk-fallback.js +118 -0
  90. package/plugins/specweave/lib/vendor/utils/chalk-fallback.js.map +1 -0
  91. package/plugins/specweave/lib/vendor/utils/fs-native.d.ts +179 -0
  92. package/plugins/specweave/lib/vendor/utils/fs-native.js +319 -0
  93. package/plugins/specweave/lib/vendor/utils/fs-native.js.map +1 -0
  94. package/plugins/specweave/skills/code-reviewer/SKILL.md +1 -1
  95. package/plugins/specweave/skills/docs-updater/SKILL.md +61 -0
  96. package/plugins/specweave/skills/increment-planner/SKILL.md +10 -335
  97. package/plugins/specweave/skills/increment-planner/templates/metadata.json +13 -0
  98. package/plugins/specweave/skills/increment-planner/templates/plan.md +50 -0
  99. package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +86 -0
  100. package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +50 -0
  101. package/plugins/specweave/skills/increment-planner/templates/tasks-multi-project.md +86 -0
  102. package/plugins/specweave/skills/increment-planner/templates/tasks-single-project.md +48 -0
  103. package/plugins/specweave-ado/commands/specweave-ado-import-areas.md +358 -0
  104. package/plugins/specweave-alternatives/skills/architecture-alternatives/SKILL.md +1 -0
  105. package/plugins/specweave-alternatives/skills/bmad-method/SKILL.md +1 -0
  106. package/plugins/specweave-core/skills/code-quality/SKILL.md +1 -0
  107. package/plugins/specweave-core/skills/design-patterns/SKILL.md +1 -0
  108. package/plugins/specweave-core/skills/software-architecture/SKILL.md +1 -0
  109. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +14 -10
  110. package/plugins/specweave-github/commands/specweave-github-sync.md +57 -0
  111. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +68 -0
  112. package/plugins/specweave-github/lib/github-feature-sync.ts +6 -11
  113. package/plugins/specweave-github/lib/github-increment-sync-cli.js +343 -0
  114. package/plugins/specweave-github/lib/github-increment-sync-cli.ts +484 -0
  115. package/plugins/specweave-github/lib/increment-issue-builder.js +368 -0
  116. package/plugins/specweave-github/lib/increment-issue-builder.ts +471 -0
  117. package/plugins/specweave-github/skills/github-issue-standard/SKILL.md +19 -24
  118. package/plugins/specweave-infrastructure/agents/observability-engineer/AGENT.md +15 -23
  119. package/plugins/specweave-jira/commands/specweave-jira-import-boards.md +331 -0
  120. package/plugins/specweave-ml/agents/data-scientist/AGENT.md +16 -20
  121. package/plugins/specweave-ml/agents/ml-engineer/AGENT.md +18 -19
  122. package/plugins/specweave-ml/skills/{ml-pipeline-workflow → mlops-dag-builder}/SKILL.md +18 -14
  123. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +102 -0
  124. package/plugins/specweave-ui/skills/browser-automation/SKILL.md +1 -1
  125. package/plugins/specweave-ui/skills/ui-testing/SKILL.md +10 -122
  126. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +0 -70
  127. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +0 -1
  128. package/dist/plugins/specweave-github/lib/epic-content-builder.js +0 -258
  129. package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +0 -1
  130. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +0 -83
  131. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +0 -1
  132. package/dist/plugins/specweave-github/lib/github-epic-sync.js +0 -466
  133. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +0 -1
  134. package/plugins/specweave-github/lib/epic-content-builder.js +0 -265
  135. package/plugins/specweave-github/lib/epic-content-builder.ts +0 -376
  136. package/plugins/specweave-github/lib/github-epic-sync.js +0 -488
  137. package/plugins/specweave-github/lib/github-epic-sync.ts +0 -715
@@ -0,0 +1,484 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GitHub Increment Sync CLI
4
+ *
5
+ * For brownfield projects without living docs structure.
6
+ * Creates GitHub issues directly from increment spec.md with proper format.
7
+ *
8
+ * CORRECT FORMAT:
9
+ * - Single issue per increment: [FS-XXX] Increment Title
10
+ * - With User Stories and ACs as sections
11
+ *
12
+ * Usage:
13
+ * node github-increment-sync-cli.js <increment-id>
14
+ * node github-increment-sync-cli.js 0063-fix-external-import
15
+ *
16
+ * @see ADR-0143 (GitHub Issue Format)
17
+ */
18
+
19
+ import { existsSync, readFileSync } from 'fs';
20
+ import * as fs from 'fs/promises';
21
+ import * as path from 'path';
22
+ import { IncrementIssueBuilder } from './increment-issue-builder.js';
23
+ import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
24
+
25
+ interface GitHubConfig {
26
+ owner: string;
27
+ repo: string;
28
+ token: string;
29
+ }
30
+
31
+ async function loadGitHubConfig(): Promise<GitHubConfig | null> {
32
+ const projectRoot = process.cwd();
33
+ const configPath = path.join(projectRoot, '.specweave/config.json');
34
+
35
+ let owner = process.env.GITHUB_OWNER || '';
36
+ let repo = process.env.GITHUB_REPO || '';
37
+ const token = process.env.GITHUB_TOKEN || '';
38
+
39
+ // Try to load from config.json
40
+ if (existsSync(configPath)) {
41
+ try {
42
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
43
+
44
+ // Method 1: sync.github
45
+ if (config.sync?.github?.owner && config.sync?.github?.repo) {
46
+ owner = config.sync.github.owner;
47
+ repo = config.sync.github.repo;
48
+ }
49
+ // Method 2: multiProject.projects[activeProject].externalTools.github
50
+ else if (config.multiProject?.enabled && config.multiProject?.activeProject) {
51
+ const activeProject = config.multiProject.activeProject;
52
+ const projectConfig = config.multiProject.projects?.[activeProject];
53
+ if (projectConfig?.externalTools?.github?.repository) {
54
+ const parts = projectConfig.externalTools.github.repository.split('/');
55
+ if (parts.length === 2) {
56
+ owner = parts[0];
57
+ repo = parts[1];
58
+ }
59
+ }
60
+ }
61
+ // Method 3: sync.profiles[activeProfile]
62
+ else if (config.sync?.activeProfile && config.sync?.profiles) {
63
+ const profile = config.sync.profiles[config.sync.activeProfile];
64
+ if (profile?.config?.owner && profile?.config?.repo) {
65
+ owner = profile.config.owner;
66
+ repo = profile.config.repo;
67
+ }
68
+ }
69
+ } catch (error) {
70
+ console.error('āš ļø Failed to parse config.json:', error);
71
+ }
72
+ }
73
+
74
+ // Fallback: detect from git remote
75
+ if (!owner || !repo) {
76
+ const result = await execFileNoThrow('git', ['remote', 'get-url', 'origin']);
77
+ if (result.exitCode === 0 && result.stdout) {
78
+ const remoteUrl = result.stdout.trim();
79
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
80
+ if (match) {
81
+ owner = owner || match[1];
82
+ repo = repo || match[2];
83
+ }
84
+ }
85
+ }
86
+
87
+ if (!token) {
88
+ console.error('āŒ GITHUB_TOKEN not set');
89
+ console.error(' Set it in .env file or export GITHUB_TOKEN=ghp_xxx');
90
+ return null;
91
+ }
92
+
93
+ if (!owner || !repo) {
94
+ console.error('āŒ Could not detect GitHub owner/repo');
95
+ console.error(' Set sync.github.owner and sync.github.repo in .specweave/config.json');
96
+ return null;
97
+ }
98
+
99
+ return { owner, repo, token };
100
+ }
101
+
102
+ /**
103
+ * Find increment folder by ID (supports partial matching)
104
+ */
105
+ async function findIncrementFolder(incrementId: string): Promise<string | null> {
106
+ const projectRoot = process.cwd();
107
+ const incrementsDir = path.join(projectRoot, '.specweave/increments');
108
+
109
+ if (!existsSync(incrementsDir)) {
110
+ return null;
111
+ }
112
+
113
+ const entries = await fs.readdir(incrementsDir, { withFileTypes: true });
114
+
115
+ for (const entry of entries) {
116
+ if (!entry.isDirectory()) continue;
117
+ if (entry.name.startsWith('_')) continue; // Skip _archive, _backlog
118
+
119
+ // Exact match
120
+ if (entry.name === incrementId) {
121
+ return path.join(incrementsDir, entry.name);
122
+ }
123
+
124
+ // Partial match (e.g., "0063" matches "0063-fix-external-import")
125
+ if (entry.name.startsWith(incrementId + '-')) {
126
+ return path.join(incrementsDir, entry.name);
127
+ }
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * Check if GitHub issue already exists for this increment
135
+ */
136
+ async function findExistingIssue(
137
+ owner: string,
138
+ repo: string,
139
+ featureId: string
140
+ ): Promise<number | null> {
141
+ // Search for issues with this feature ID in title
142
+ const result = await execFileNoThrow('gh', [
143
+ 'search', 'issues',
144
+ `repo:${owner}/${repo}`,
145
+ `"[${featureId}]" in:title`,
146
+ 'is:open',
147
+ '--json', 'number,title',
148
+ '--limit', '5'
149
+ ]);
150
+
151
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
152
+ return null;
153
+ }
154
+
155
+ try {
156
+ const issues = JSON.parse(result.stdout);
157
+ if (issues.length > 0) {
158
+ return issues[0].number;
159
+ }
160
+ } catch {
161
+ // Parse error, no existing issue
162
+ }
163
+
164
+ return null;
165
+ }
166
+
167
+ /**
168
+ * Create GitHub issue via gh CLI
169
+ */
170
+ async function createGitHubIssue(
171
+ owner: string,
172
+ repo: string,
173
+ title: string,
174
+ body: string,
175
+ labels: string[]
176
+ ): Promise<{ number: number; url: string }> {
177
+ const args = [
178
+ 'issue', 'create',
179
+ '--repo', `${owner}/${repo}`,
180
+ '--title', title,
181
+ '--body', body
182
+ ];
183
+
184
+ // Add labels
185
+ if (labels.length > 0) {
186
+ args.push('--label', labels.join(','));
187
+ }
188
+
189
+ const result = await execFileNoThrow('gh', args);
190
+
191
+ if (result.exitCode !== 0) {
192
+ throw new Error(`Failed to create issue: ${result.stderr || result.stdout}`);
193
+ }
194
+
195
+ // Parse issue URL from output
196
+ const urlMatch = result.stdout.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
197
+ if (!urlMatch) {
198
+ throw new Error('Could not parse issue URL from gh output');
199
+ }
200
+
201
+ return {
202
+ number: parseInt(urlMatch[1], 10),
203
+ url: urlMatch[0]
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Update existing GitHub issue
209
+ */
210
+ async function updateGitHubIssue(
211
+ owner: string,
212
+ repo: string,
213
+ issueNumber: number,
214
+ body: string
215
+ ): Promise<void> {
216
+ const result = await execFileNoThrow('gh', [
217
+ 'issue', 'edit',
218
+ String(issueNumber),
219
+ '--repo', `${owner}/${repo}`,
220
+ '--body', body
221
+ ]);
222
+
223
+ if (result.exitCode !== 0) {
224
+ throw new Error(`Failed to update issue: ${result.stderr || result.stdout}`);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Load existing GitHub issue link from metadata.json
230
+ * This is the PRIMARY source for existing issue detection
231
+ */
232
+ function loadExistingGitHubLink(incrementPath: string): { issue: number; url?: string } | null {
233
+ const metadataPath = path.join(incrementPath, 'metadata.json');
234
+
235
+ if (!existsSync(metadataPath)) {
236
+ return null;
237
+ }
238
+
239
+ try {
240
+ const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
241
+
242
+ // Check for github.issue (new format)
243
+ if (metadata.github?.issue) {
244
+ return {
245
+ issue: metadata.github.issue,
246
+ url: metadata.github.url
247
+ };
248
+ }
249
+
250
+ // Check for sync.issueNumber (alternative format)
251
+ if (metadata.sync?.issueNumber) {
252
+ return {
253
+ issue: metadata.sync.issueNumber,
254
+ url: metadata.sync.issueUrl
255
+ };
256
+ }
257
+
258
+ return null;
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Update increment metadata with GitHub issue link
266
+ */
267
+ async function updateIncrementMetadata(
268
+ incrementPath: string,
269
+ issueNumber: number,
270
+ issueUrl: string
271
+ ): Promise<void> {
272
+ const metadataPath = path.join(incrementPath, 'metadata.json');
273
+
274
+ let metadata: Record<string, unknown> = {};
275
+
276
+ if (existsSync(metadataPath)) {
277
+ try {
278
+ metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
279
+ } catch {
280
+ // Start fresh
281
+ }
282
+ }
283
+
284
+ // Update github section
285
+ metadata.github = {
286
+ issue: issueNumber,
287
+ url: issueUrl,
288
+ lastSync: new Date().toISOString()
289
+ };
290
+
291
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + '\n');
292
+ }
293
+
294
+ async function main() {
295
+ const args = process.argv.slice(2);
296
+
297
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
298
+ console.log('Usage: node github-increment-sync-cli.js <increment-id> [options]');
299
+ console.log('');
300
+ console.log('Arguments:');
301
+ console.log(' increment-id Increment ID (e.g., 0063 or 0063-fix-external-import)');
302
+ console.log('');
303
+ console.log('Options:');
304
+ console.log(' --force Force create even if issue exists');
305
+ console.log(' --dry-run Preview issue without creating');
306
+ console.log('');
307
+ console.log('Environment:');
308
+ console.log(' GITHUB_TOKEN Required - GitHub personal access token');
309
+ console.log('');
310
+ console.log('Example:');
311
+ console.log(' GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js 0063');
312
+ process.exit(args.length === 0 ? 1 : 0);
313
+ }
314
+
315
+ const incrementId = args[0];
316
+ const force = args.includes('--force');
317
+ const dryRun = args.includes('--dry-run');
318
+
319
+ console.log(`\nšŸ™ GitHub Increment Sync CLI`);
320
+ console.log(` Increment: ${incrementId}`);
321
+
322
+ // Find increment folder
323
+ const incrementPath = await findIncrementFolder(incrementId);
324
+ if (!incrementPath) {
325
+ console.error(`āŒ Increment not found: ${incrementId}`);
326
+ console.error(' Check: ls .specweave/increments/');
327
+ process.exit(1);
328
+ }
329
+
330
+ const fullIncrementId = path.basename(incrementPath);
331
+ console.log(` Found: ${fullIncrementId}`);
332
+
333
+ // For dry-run, we can skip config validation
334
+ let config: GitHubConfig | null = null;
335
+ if (!dryRun) {
336
+ config = await loadGitHubConfig();
337
+ if (!config) {
338
+ process.exit(1);
339
+ }
340
+ console.log(` Repository: ${config.owner}/${config.repo}`);
341
+ } else {
342
+ // Try to detect repo for dry-run preview (non-fatal)
343
+ config = await loadGitHubConfig().catch((): null => null);
344
+ if (config) {
345
+ console.log(` Repository: ${config.owner}/${config.repo}`);
346
+ } else {
347
+ console.log(` Repository: (not detected - dry run mode)`);
348
+ }
349
+ }
350
+
351
+ // Parse increment and build issue
352
+ const projectRoot = process.cwd();
353
+ const builder = new IncrementIssueBuilder(incrementPath, projectRoot);
354
+
355
+ try {
356
+ console.log(`\nšŸ”„ Parsing increment spec.md...`);
357
+ const incrementData = await builder.parse();
358
+
359
+ console.log(` šŸ“¦ Title: ${incrementData.title}`);
360
+ console.log(` šŸ“ User Stories: ${incrementData.userStories.length}`);
361
+
362
+ const totalACs = incrementData.userStories.reduce(
363
+ (sum, us) => sum + us.acceptanceCriteria.length, 0
364
+ );
365
+ console.log(` āœ“ Acceptance Criteria: ${totalACs}`);
366
+
367
+ // Build issue content
368
+ const githubRepo = config ? `${config.owner}/${config.repo}` : undefined;
369
+ const issue = builder.buildIncrementIssue(incrementData, githubRepo);
370
+
371
+ console.log(`\nšŸ“‹ Issue Preview:`);
372
+ console.log(` Title: ${issue.title}`);
373
+ console.log(` Labels: ${issue.labels.join(', ')}`);
374
+
375
+ if (dryRun) {
376
+ console.log(`\nšŸ“„ Issue Body Preview:\n`);
377
+ console.log(issue.body);
378
+ console.log(`\nāœ… Dry run complete (no issue created)`);
379
+ process.exit(0);
380
+ }
381
+
382
+ // After dry-run check, config is guaranteed to be non-null
383
+ if (!config) {
384
+ console.error('āŒ GitHub config not available');
385
+ process.exit(1);
386
+ }
387
+
388
+ // STEP 1: Check metadata.json for existing issue link (PRIMARY detection)
389
+ const metadataLink = loadExistingGitHubLink(incrementPath);
390
+
391
+ if (metadataLink) {
392
+ console.log(`\nšŸ“Ž Found existing issue link in metadata: #${metadataLink.issue}`);
393
+
394
+ // Update existing issue with new format
395
+ console.log(`šŸ”„ Updating issue #${metadataLink.issue} with new format...`);
396
+
397
+ // Update body
398
+ await updateGitHubIssue(config.owner, config.repo, metadataLink.issue, issue.body);
399
+ console.log(` āœ… Body updated with User Stories and ACs`);
400
+
401
+ // Update title to new format (fix the [0002] → [FS-XXX] issue)
402
+ const updateTitleResult = await execFileNoThrow('gh', [
403
+ 'issue', 'edit',
404
+ String(metadataLink.issue),
405
+ '--repo', `${config.owner}/${config.repo}`,
406
+ '--title', issue.title
407
+ ]);
408
+
409
+ if (updateTitleResult.exitCode === 0) {
410
+ console.log(` āœ… Title updated to: ${issue.title}`);
411
+ } else {
412
+ console.log(` āš ļø Could not update title (may need permissions)`);
413
+ }
414
+
415
+ // Update metadata with lastSync
416
+ await updateIncrementMetadata(
417
+ incrementPath,
418
+ metadataLink.issue,
419
+ `https://github.com/${config.owner}/${config.repo}/issues/${metadataLink.issue}`
420
+ );
421
+
422
+ console.log(`\nāœ… Sync complete!`);
423
+ console.log(` šŸ”— https://github.com/${config.owner}/${config.repo}/issues/${metadataLink.issue}`);
424
+ process.exit(0);
425
+ }
426
+
427
+ // STEP 2: Search GitHub by feature ID (fallback)
428
+ const featureId = incrementData.frontmatter.feature_id ||
429
+ `FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, '0') || 'UNKNOWN'}`;
430
+
431
+ console.log(`\nšŸ” Searching GitHub for existing issue [${featureId}]...`);
432
+ const existingIssue = await findExistingIssue(config.owner, config.repo, featureId);
433
+
434
+ if (existingIssue) {
435
+ console.log(` Found existing issue: #${existingIssue}`);
436
+
437
+ // Update existing issue
438
+ console.log(`šŸ”„ Updating issue #${existingIssue}...`);
439
+ await updateGitHubIssue(config.owner, config.repo, existingIssue, issue.body);
440
+ console.log(` āœ… Issue #${existingIssue} updated`);
441
+
442
+ await updateIncrementMetadata(
443
+ incrementPath,
444
+ existingIssue,
445
+ `https://github.com/${config.owner}/${config.repo}/issues/${existingIssue}`
446
+ );
447
+
448
+ console.log(`\nāœ… Sync complete!`);
449
+ console.log(` šŸ”— https://github.com/${config.owner}/${config.repo}/issues/${existingIssue}`);
450
+ process.exit(0);
451
+ }
452
+
453
+ // STEP 3: Create new issue (no existing found)
454
+ console.log(` No existing issue found`);
455
+ console.log(`\nšŸš€ Creating GitHub issue...`);
456
+ const created = await createGitHubIssue(
457
+ config.owner,
458
+ config.repo,
459
+ issue.title,
460
+ issue.body,
461
+ issue.labels
462
+ );
463
+
464
+ console.log(` āœ… Issue #${created.number} created`);
465
+
466
+ // Update metadata
467
+ await updateIncrementMetadata(incrementPath, created.number, created.url);
468
+ console.log(` šŸ“ Metadata updated`);
469
+
470
+ console.log(`\nāœ… Sync complete!`);
471
+ console.log(` šŸ”— ${created.url}`);
472
+
473
+ process.exit(0);
474
+ } catch (error) {
475
+ console.error(`\nāŒ Sync failed:`, error);
476
+ process.exit(1);
477
+ }
478
+ }
479
+
480
+ // Run CLI
481
+ main().catch(error => {
482
+ console.error('Fatal error:', error);
483
+ process.exit(1);
484
+ });