specweave 1.0.18 → 1.0.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 (34) hide show
  1. package/CLAUDE.md +48 -0
  2. package/dist/src/core/config/types.d.ts +15 -0
  3. package/dist/src/core/config/types.d.ts.map +1 -1
  4. package/dist/src/core/config/types.js.map +1 -1
  5. package/dist/src/core/increment/increment-utils.d.ts +40 -0
  6. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  7. package/dist/src/core/increment/increment-utils.js +56 -2
  8. package/dist/src/core/increment/increment-utils.js.map +1 -1
  9. package/dist/src/core/increment/status-change-sync-trigger.d.ts +26 -2
  10. package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
  11. package/dist/src/core/increment/status-change-sync-trigger.js +70 -6
  12. package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
  13. package/dist/src/hooks/auto-create-external-issue.d.ts +19 -0
  14. package/dist/src/hooks/auto-create-external-issue.d.ts.map +1 -0
  15. package/dist/src/hooks/auto-create-external-issue.js +54 -0
  16. package/dist/src/hooks/auto-create-external-issue.js.map +1 -0
  17. package/dist/src/integrations/jira/jira-incremental-mapper.d.ts +3 -1
  18. package/dist/src/integrations/jira/jira-incremental-mapper.d.ts.map +1 -1
  19. package/dist/src/integrations/jira/jira-incremental-mapper.js +7 -1
  20. package/dist/src/integrations/jira/jira-incremental-mapper.js.map +1 -1
  21. package/dist/src/integrations/jira/jira-mapper.d.ts +3 -1
  22. package/dist/src/integrations/jira/jira-mapper.d.ts.map +1 -1
  23. package/dist/src/integrations/jira/jira-mapper.js +7 -1
  24. package/dist/src/integrations/jira/jira-mapper.js.map +1 -1
  25. package/dist/src/sync/external-issue-auto-creator.d.ts +156 -0
  26. package/dist/src/sync/external-issue-auto-creator.d.ts.map +1 -0
  27. package/dist/src/sync/external-issue-auto-creator.js +694 -0
  28. package/dist/src/sync/external-issue-auto-creator.js.map +1 -0
  29. package/dist/src/utils/feature-id-collision.d.ts +51 -0
  30. package/dist/src/utils/feature-id-collision.d.ts.map +1 -1
  31. package/dist/src/utils/feature-id-collision.js +149 -0
  32. package/dist/src/utils/feature-id-collision.js.map +1 -1
  33. package/package.json +1 -1
  34. package/plugins/specweave/commands/sync-progress.md +92 -0
@@ -0,0 +1,694 @@
1
+ /**
2
+ * External Issue Auto-Creator (v1.0.19)
3
+ *
4
+ * Automatically creates external issues (GitHub/JIRA/ADO) for increments
5
+ * when they don't have linked issues.
6
+ *
7
+ * TRIGGER POINTS:
8
+ * 1. After increment creation (post-increment-planning hook)
9
+ * 2. During sync-progress if issue is missing
10
+ * 3. On status change (active → completed)
11
+ *
12
+ * SAFETY FEATURES:
13
+ * - 3-layer idempotency (frontmatter → metadata → API)
14
+ * - Duplicate detection before creation
15
+ * - Rate limiting (circuit breaker)
16
+ * - Non-blocking (failures don't crash the workflow)
17
+ *
18
+ * @see ADR-0139 (Unified Post-Increment GitHub Sync)
19
+ */
20
+ import { promises as fs, existsSync } from 'fs';
21
+ import path from 'path';
22
+ import yaml from 'yaml';
23
+ import { consoleLogger } from '../utils/logger.js';
24
+ import { deriveFeatureId } from '../utils/feature-id-derivation.js';
25
+ import { ConfigManager } from '../core/config/config-manager.js';
26
+ /**
27
+ * Auto-creates external issues for increments
28
+ *
29
+ * Usage:
30
+ * ```typescript
31
+ * const creator = new ExternalIssueAutoCreator({ projectRoot: process.cwd() });
32
+ * const result = await creator.createForIncrement('0001-feature');
33
+ * ```
34
+ */
35
+ export class ExternalIssueAutoCreator {
36
+ constructor(options) {
37
+ this.projectRoot = options.projectRoot;
38
+ this.logger = options.logger ?? consoleLogger;
39
+ this.configManager = new ConfigManager(this.projectRoot);
40
+ }
41
+ /**
42
+ * Create external issues for an increment (if configured and not already created)
43
+ *
44
+ * This method:
45
+ * 1. Checks if auto-create is enabled in config
46
+ * 2. Detects which provider to use (GitHub/JIRA/ADO)
47
+ * 3. Checks if issues already exist (3-layer idempotency)
48
+ * 4. Creates issues if missing
49
+ * 5. Updates metadata with issue links
50
+ */
51
+ async createForIncrement(incrementId) {
52
+ try {
53
+ // Load config
54
+ const config = await this.configManager.read();
55
+ // Check if auto-create is enabled
56
+ const autoCreateEnabled = this.isAutoCreateEnabled(config);
57
+ if (!autoCreateEnabled) {
58
+ return {
59
+ success: true,
60
+ provider: 'none',
61
+ skipped: true,
62
+ skipReason: 'Auto-create disabled in config (sync.autoCreateOnIncrement = false)',
63
+ };
64
+ }
65
+ // Detect provider
66
+ const provider = this.detectProvider(config);
67
+ if (!provider) {
68
+ return {
69
+ success: true,
70
+ provider: 'none',
71
+ skipped: true,
72
+ skipReason: 'No external provider configured (GitHub/JIRA/ADO)',
73
+ };
74
+ }
75
+ // Load increment info
76
+ const incrementInfo = await this.loadIncrementInfo(incrementId);
77
+ if (!incrementInfo) {
78
+ return {
79
+ success: false,
80
+ provider,
81
+ error: `Increment ${incrementId} not found or missing spec.md`,
82
+ };
83
+ }
84
+ // Check if already has external issue (Layer 1 & 2)
85
+ const existingIssue = await this.checkExistingIssue(incrementId, provider);
86
+ if (existingIssue) {
87
+ this.logger.log(`✅ ${incrementId} already has ${provider} issue: ${existingIssue}`);
88
+ return {
89
+ success: true,
90
+ provider,
91
+ skipped: true,
92
+ skipReason: `Issue already exists: ${existingIssue}`,
93
+ };
94
+ }
95
+ // Create issues based on provider
96
+ switch (provider) {
97
+ case 'github':
98
+ return await this.createGitHubIssues(incrementId, incrementInfo, config);
99
+ case 'jira':
100
+ return await this.createJiraIssues(incrementId, incrementInfo, config);
101
+ case 'ado':
102
+ return await this.createAdoIssues(incrementId, incrementInfo, config);
103
+ default:
104
+ return {
105
+ success: false,
106
+ provider: 'none',
107
+ error: `Unknown provider: ${provider}`,
108
+ };
109
+ }
110
+ }
111
+ catch (error) {
112
+ const errorMessage = error instanceof Error ? error.message : String(error);
113
+ this.logger.error(`❌ Auto-create failed for ${incrementId}: ${errorMessage}`);
114
+ return {
115
+ success: false,
116
+ provider: 'none',
117
+ error: errorMessage,
118
+ };
119
+ }
120
+ }
121
+ /**
122
+ * Check if auto-create is enabled in config
123
+ *
124
+ * Checks (in order):
125
+ * 1. sync.autoCreateOnIncrement (new, explicit)
126
+ * 2. sync.autoSync (legacy)
127
+ * 3. sync.settings.canUpsertInternalItems (legacy)
128
+ */
129
+ isAutoCreateEnabled(config) {
130
+ // New explicit option (recommended)
131
+ if (config.sync?.autoCreateOnIncrement !== undefined) {
132
+ return config.sync.autoCreateOnIncrement === true;
133
+ }
134
+ // Legacy: autoSync
135
+ if (config.sync?.autoSync === true) {
136
+ return true;
137
+ }
138
+ // Legacy: canUpsertInternalItems
139
+ if (config.sync?.settings?.canUpsertInternalItems === true) {
140
+ return true;
141
+ }
142
+ // Default: false (opt-in)
143
+ return false;
144
+ }
145
+ /**
146
+ * Detect which provider to use for external sync
147
+ *
148
+ * Priority:
149
+ * 1. GitHub (if sync.github.enabled = true)
150
+ * 2. JIRA (if sync.jira.enabled = true)
151
+ * 3. ADO (if sync.ado.enabled = true)
152
+ * 4. GitHub (if issueTracker.provider = 'github')
153
+ * 5. JIRA (if issueTracker.provider = 'jira')
154
+ * 6. ADO (if issueTracker.provider = 'ado')
155
+ */
156
+ detectProvider(config) {
157
+ // Check explicit provider enablement in sync config
158
+ if (config.sync?.github?.enabled === true) {
159
+ return 'github';
160
+ }
161
+ if (config.sync?.jira?.enabled === true) {
162
+ return 'jira';
163
+ }
164
+ if (config.sync?.ado?.enabled === true) {
165
+ return 'ado';
166
+ }
167
+ // Check issueTracker provider
168
+ const trackerProvider = config.issueTracker?.provider;
169
+ if (trackerProvider === 'github') {
170
+ return 'github';
171
+ }
172
+ if (trackerProvider === 'jira') {
173
+ return 'jira';
174
+ }
175
+ if (trackerProvider === 'ado') {
176
+ return 'ado';
177
+ }
178
+ // Check sync profiles for provider hints
179
+ const profiles = config.sync?.profiles || {};
180
+ for (const profile of Object.values(profiles)) {
181
+ if (profile.provider === 'github') {
182
+ return 'github';
183
+ }
184
+ if (profile.provider === 'jira') {
185
+ return 'jira';
186
+ }
187
+ if (profile.provider === 'ado' || profile.provider === 'azure-devops') {
188
+ return 'ado';
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+ /**
194
+ * Load increment information from spec.md and metadata.json
195
+ */
196
+ async loadIncrementInfo(incrementId) {
197
+ const incrementPath = path.join(this.projectRoot, '.specweave/increments', incrementId);
198
+ const specPath = path.join(incrementPath, 'spec.md');
199
+ const metadataPath = path.join(incrementPath, 'metadata.json');
200
+ if (!existsSync(specPath)) {
201
+ return null;
202
+ }
203
+ try {
204
+ const specContent = await fs.readFile(specPath, 'utf-8');
205
+ // Parse frontmatter
206
+ const frontmatterMatch = specContent.match(/^---\n([\s\S]*?)\n---/);
207
+ let frontmatter = {};
208
+ if (frontmatterMatch) {
209
+ frontmatter = yaml.parse(frontmatterMatch[1]) || {};
210
+ }
211
+ // Derive feature ID
212
+ let featureId = frontmatter.feature_id || frontmatter.epic || frontmatter.feature || '';
213
+ if (!featureId) {
214
+ try {
215
+ featureId = deriveFeatureId(incrementId);
216
+ }
217
+ catch {
218
+ featureId = `FS-${incrementId.substring(0, 4)}`;
219
+ }
220
+ }
221
+ // Parse user stories from spec.md body
222
+ const userStories = this.parseUserStories(specContent);
223
+ // Load status from metadata
224
+ let status = 'active';
225
+ if (existsSync(metadataPath)) {
226
+ const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
227
+ status = metadata.status || 'active';
228
+ }
229
+ return {
230
+ id: incrementId,
231
+ featureId,
232
+ title: frontmatter.title || incrementId,
233
+ status,
234
+ userStories,
235
+ };
236
+ }
237
+ catch (error) {
238
+ this.logger.error(`Failed to load increment info: ${error}`);
239
+ return null;
240
+ }
241
+ }
242
+ /**
243
+ * Parse user stories from spec.md body
244
+ */
245
+ parseUserStories(specContent) {
246
+ const userStories = [];
247
+ // Match ### US-XXX: Title patterns
248
+ const usRegex = /^### (US-\d+):?\s*(.+)$/gm;
249
+ let match;
250
+ while ((match = usRegex.exec(specContent)) !== null) {
251
+ const usId = match[1];
252
+ const title = match[2].trim();
253
+ // Try to find **Project**: field after the US header
254
+ const usStartIndex = match.index;
255
+ const nextUsMatch = specContent.substring(usStartIndex + match[0].length).match(/^### US-\d+/m);
256
+ const usEndIndex = nextUsMatch
257
+ ? usStartIndex + match[0].length + (nextUsMatch.index || 0)
258
+ : specContent.length;
259
+ const usSection = specContent.substring(usStartIndex, usEndIndex);
260
+ const projectMatch = usSection.match(/\*\*Project\*\*:\s*(\S+)/);
261
+ const project = projectMatch ? projectMatch[1] : undefined;
262
+ userStories.push({ id: usId, title, project });
263
+ }
264
+ return userStories;
265
+ }
266
+ /**
267
+ * Check if increment already has external issue linked
268
+ *
269
+ * Checks:
270
+ * 1. metadata.json github/jira/ado fields
271
+ * 2. spec.md frontmatter external links
272
+ */
273
+ async checkExistingIssue(incrementId, provider) {
274
+ const metadataPath = path.join(this.projectRoot, '.specweave/increments', incrementId, 'metadata.json');
275
+ if (!existsSync(metadataPath)) {
276
+ return null;
277
+ }
278
+ try {
279
+ const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
280
+ switch (provider) {
281
+ case 'github':
282
+ if (metadata.github?.issue) {
283
+ return `#${metadata.github.issue}`;
284
+ }
285
+ if (metadata.github?.issues?.length > 0) {
286
+ return `#${metadata.github.issues[0].number}`;
287
+ }
288
+ break;
289
+ case 'jira':
290
+ if (metadata.jira?.issue) {
291
+ return metadata.jira.issue;
292
+ }
293
+ if (metadata.jira?.issues?.length > 0) {
294
+ return metadata.jira.issues[0].key;
295
+ }
296
+ break;
297
+ case 'ado':
298
+ if (metadata.ado?.workItem) {
299
+ return `#${metadata.ado.workItem}`;
300
+ }
301
+ if (metadata.ado?.workItems?.length > 0) {
302
+ return `#${metadata.ado.workItems[0].id}`;
303
+ }
304
+ break;
305
+ }
306
+ return null;
307
+ }
308
+ catch {
309
+ return null;
310
+ }
311
+ }
312
+ /**
313
+ * Create GitHub issues for the increment
314
+ */
315
+ async createGitHubIssues(incrementId, incrementInfo, config) {
316
+ try {
317
+ // Import GitHub client dynamically to avoid circular deps
318
+ const { GitHubClientV2 } = await import('../../plugins/specweave-github/lib/github-client-v2.js');
319
+ // Detect repo from config
320
+ const repoInfo = await this.detectGitHubRepo(config);
321
+ if (!repoInfo) {
322
+ return {
323
+ success: false,
324
+ provider: 'github',
325
+ error: 'GitHub repository not configured. Set sync.github.owner and sync.github.repo in config.json',
326
+ };
327
+ }
328
+ const client = GitHubClientV2.fromRepo(repoInfo.owner, repoInfo.repo);
329
+ // Check if GitHub issue already exists via API (Layer 3)
330
+ const searchTitle = `[${incrementInfo.featureId}]`;
331
+ const existingIssue = await client.searchIssueByTitle(searchTitle);
332
+ if (existingIssue) {
333
+ // Found existing issue - update metadata and return
334
+ await this.updateMetadataWithGitHubIssue(incrementId, existingIssue.number, existingIssue.html_url);
335
+ return {
336
+ success: true,
337
+ provider: 'github',
338
+ issueNumber: existingIssue.number,
339
+ issueUrl: existingIssue.html_url,
340
+ skipped: true,
341
+ skipReason: `Issue already exists on GitHub: #${existingIssue.number}`,
342
+ };
343
+ }
344
+ // Create issues for each user story
345
+ const createdIssues = [];
346
+ for (const us of incrementInfo.userStories) {
347
+ const body = this.buildGitHubIssueBody(incrementId, incrementInfo, us);
348
+ try {
349
+ const issue = await client.createUserStoryIssue({
350
+ featureId: incrementInfo.featureId,
351
+ userStoryId: us.id,
352
+ title: us.title,
353
+ body,
354
+ labels: ['specweave', 'auto-created'],
355
+ });
356
+ createdIssues.push(issue.number);
357
+ this.logger.log(` ✅ Created GitHub issue #${issue.number} for ${us.id}`);
358
+ }
359
+ catch (error) {
360
+ this.logger.warn(` ⚠️ Failed to create issue for ${us.id}: ${error}`);
361
+ }
362
+ }
363
+ if (createdIssues.length === 0) {
364
+ // Fallback: create a single feature-level issue using createEpicIssue
365
+ const title = `[${incrementInfo.featureId}] ${incrementInfo.title}`;
366
+ const body = this.buildFeatureLevelIssueBody(incrementId, incrementInfo);
367
+ const issue = await client.createEpicIssue(title, body, undefined, ['specweave', 'auto-created']);
368
+ await this.updateMetadataWithGitHubIssue(incrementId, issue.number, issue.html_url);
369
+ return {
370
+ success: true,
371
+ provider: 'github',
372
+ issueNumber: issue.number,
373
+ issueUrl: issue.html_url,
374
+ };
375
+ }
376
+ // Update metadata with first created issue
377
+ await this.updateMetadataWithGitHubIssue(incrementId, createdIssues[0], `https://github.com/${repoInfo.owner}/${repoInfo.repo}/issues/${createdIssues[0]}`);
378
+ return {
379
+ success: true,
380
+ provider: 'github',
381
+ issueNumber: createdIssues[0],
382
+ issueUrl: `https://github.com/${repoInfo.owner}/${repoInfo.repo}/issues/${createdIssues[0]}`,
383
+ };
384
+ }
385
+ catch (error) {
386
+ const errorMessage = error instanceof Error ? error.message : String(error);
387
+ return {
388
+ success: false,
389
+ provider: 'github',
390
+ error: errorMessage,
391
+ };
392
+ }
393
+ }
394
+ /**
395
+ * Create JIRA issues for the increment
396
+ */
397
+ async createJiraIssues(incrementId, incrementInfo, config) {
398
+ try {
399
+ // Import JIRA client dynamically
400
+ const { JiraClient } = await import('../integrations/jira/jira-client.js');
401
+ const jiraConfig = config.issueTracker || config.sync?.jira || {};
402
+ const domain = jiraConfig.domain;
403
+ const projectKey = jiraConfig.projects?.[0]?.key || jiraConfig.projectKey;
404
+ if (!domain || !projectKey) {
405
+ return {
406
+ success: false,
407
+ provider: 'jira',
408
+ error: 'JIRA not configured. Set issueTracker.domain and issueTracker.projects in config.json',
409
+ };
410
+ }
411
+ const client = new JiraClient();
412
+ // Create Epic for the feature
413
+ const epicSummary = `[${incrementInfo.featureId}] ${incrementInfo.title}`;
414
+ const epicDescription = this.buildJiraEpicDescription(incrementId, incrementInfo);
415
+ const epic = await client.createIssue({
416
+ issueType: 'Epic',
417
+ summary: epicSummary,
418
+ description: epicDescription,
419
+ labels: ['specweave', 'auto-created'],
420
+ }, projectKey);
421
+ // Update metadata
422
+ await this.updateMetadataWithJiraIssue(incrementId, epic.key, epic.self);
423
+ this.logger.log(`✅ Created JIRA Epic: ${epic.key}`);
424
+ return {
425
+ success: true,
426
+ provider: 'jira',
427
+ issueNumber: epic.key,
428
+ issueUrl: `https://${domain}/browse/${epic.key}`,
429
+ };
430
+ }
431
+ catch (error) {
432
+ const errorMessage = error instanceof Error ? error.message : String(error);
433
+ return {
434
+ success: false,
435
+ provider: 'jira',
436
+ error: errorMessage,
437
+ };
438
+ }
439
+ }
440
+ /**
441
+ * Create Azure DevOps work items for the increment
442
+ */
443
+ async createAdoIssues(incrementId, incrementInfo, config) {
444
+ try {
445
+ const { AdoClient } = await import('../integrations/ado/ado-client.js');
446
+ const { getAdoPat } = await import('../integrations/ado/ado-pat-provider.js');
447
+ const adoConfig = config.issueTracker || config.sync?.ado || {};
448
+ const organization = adoConfig.organization_ado || adoConfig.organization;
449
+ const project = adoConfig.project;
450
+ if (!organization || !project) {
451
+ return {
452
+ success: false,
453
+ provider: 'ado',
454
+ error: 'Azure DevOps not configured. Set issueTracker.organization_ado and issueTracker.project in config.json',
455
+ };
456
+ }
457
+ const pat = getAdoPat(organization);
458
+ if (!pat) {
459
+ return {
460
+ success: false,
461
+ provider: 'ado',
462
+ error: 'AZURE_DEVOPS_PAT not set in environment',
463
+ };
464
+ }
465
+ const client = new AdoClient({
466
+ pat,
467
+ organization,
468
+ project,
469
+ });
470
+ // Create Feature work item
471
+ const title = `[${incrementInfo.featureId}] ${incrementInfo.title}`;
472
+ const description = this.buildAdoDescription(incrementId, incrementInfo);
473
+ const workItem = await client.createWorkItem({
474
+ workItemType: 'Feature',
475
+ title,
476
+ description,
477
+ tags: ['specweave', 'auto-created'],
478
+ });
479
+ // Update metadata
480
+ await this.updateMetadataWithAdoWorkItem(incrementId, workItem.id, workItem.url);
481
+ this.logger.log(`✅ Created ADO Feature: #${workItem.id}`);
482
+ return {
483
+ success: true,
484
+ provider: 'ado',
485
+ issueNumber: workItem.id,
486
+ issueUrl: workItem.url,
487
+ };
488
+ }
489
+ catch (error) {
490
+ const errorMessage = error instanceof Error ? error.message : String(error);
491
+ return {
492
+ success: false,
493
+ provider: 'ado',
494
+ error: errorMessage,
495
+ };
496
+ }
497
+ }
498
+ /**
499
+ * Detect GitHub repo from config
500
+ */
501
+ async detectGitHubRepo(config) {
502
+ // Check sync.github config
503
+ if (config.sync?.github?.owner && config.sync?.github?.repo) {
504
+ return {
505
+ owner: config.sync.github.owner,
506
+ repo: config.sync.github.repo,
507
+ };
508
+ }
509
+ // Check profiles
510
+ const profiles = config.sync?.profiles || {};
511
+ for (const profile of Object.values(profiles)) {
512
+ if (profile.provider === 'github' && profile.config?.owner && profile.config?.repo) {
513
+ return {
514
+ owner: profile.config.owner,
515
+ repo: profile.config.repo,
516
+ };
517
+ }
518
+ }
519
+ // Check issueTracker
520
+ if (config.issueTracker?.owner && config.issueTracker?.repo) {
521
+ return {
522
+ owner: config.issueTracker.owner,
523
+ repo: config.issueTracker.repo,
524
+ };
525
+ }
526
+ // Try git remote detection
527
+ try {
528
+ const { execSync } = await import('child_process');
529
+ const remoteUrl = execSync('git remote get-url origin', {
530
+ cwd: this.projectRoot,
531
+ encoding: 'utf-8',
532
+ }).trim();
533
+ // Parse GitHub URL
534
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
535
+ if (match) {
536
+ return {
537
+ owner: match[1],
538
+ repo: match[2],
539
+ };
540
+ }
541
+ }
542
+ catch {
543
+ // Git detection failed
544
+ }
545
+ return null;
546
+ }
547
+ /**
548
+ * Build GitHub issue body for a user story
549
+ */
550
+ buildGitHubIssueBody(incrementId, incrementInfo, us) {
551
+ return `## ${us.title}
552
+
553
+ **Feature**: ${incrementInfo.featureId}
554
+ **Increment**: ${incrementId}
555
+ **User Story**: ${us.id}
556
+ ${us.project ? `**Project**: ${us.project}` : ''}
557
+
558
+ ---
559
+
560
+ 📋 See [\`spec.md\`](../../tree/develop/.specweave/increments/${incrementId}/spec.md) for full acceptance criteria.
561
+
562
+ ---
563
+
564
+ 🤖 Auto-created by SpecWeave | Updates automatically on task completion
565
+ `;
566
+ }
567
+ /**
568
+ * Build GitHub issue body for feature-level issue
569
+ */
570
+ buildFeatureLevelIssueBody(incrementId, incrementInfo) {
571
+ const userStoriesList = incrementInfo.userStories
572
+ .map((us) => `- [ ] ${us.id}: ${us.title}`)
573
+ .join('\n');
574
+ return `## ${incrementInfo.title}
575
+
576
+ **Feature**: ${incrementInfo.featureId}
577
+ **Status**: ${incrementInfo.status}
578
+
579
+ ### User Stories
580
+
581
+ ${userStoriesList || '_No user stories defined_'}
582
+
583
+ ### Links
584
+
585
+ - **Spec**: [\`spec.md\`](../../tree/develop/.specweave/increments/${incrementId}/spec.md)
586
+ - **Tasks**: [\`tasks.md\`](../../tree/develop/.specweave/increments/${incrementId}/tasks.md)
587
+
588
+ ---
589
+
590
+ 🤖 Auto-created by SpecWeave | Updates automatically on task completion
591
+ `;
592
+ }
593
+ /**
594
+ * Build JIRA Epic description
595
+ */
596
+ buildJiraEpicDescription(incrementId, incrementInfo) {
597
+ const userStoriesList = incrementInfo.userStories
598
+ .map((us) => `* ${us.id}: ${us.title}`)
599
+ .join('\n');
600
+ return `h2. ${incrementInfo.title}
601
+
602
+ *Feature*: ${incrementInfo.featureId}
603
+ *Increment*: ${incrementId}
604
+
605
+ h3. User Stories
606
+
607
+ ${userStoriesList || '_No user stories defined_'}
608
+
609
+ ----
610
+
611
+ 🤖 Auto-created by SpecWeave
612
+ `;
613
+ }
614
+ /**
615
+ * Build ADO work item description
616
+ */
617
+ buildAdoDescription(incrementId, incrementInfo) {
618
+ const userStoriesList = incrementInfo.userStories
619
+ .map((us) => `<li>${us.id}: ${us.title}</li>`)
620
+ .join('\n');
621
+ return `<h2>${incrementInfo.title}</h2>
622
+
623
+ <p><strong>Feature</strong>: ${incrementInfo.featureId}<br/>
624
+ <strong>Increment</strong>: ${incrementId}</p>
625
+
626
+ <h3>User Stories</h3>
627
+ <ul>
628
+ ${userStoriesList || '<li><em>No user stories defined</em></li>'}
629
+ </ul>
630
+
631
+ <hr/>
632
+ <p>🤖 Auto-created by SpecWeave</p>
633
+ `;
634
+ }
635
+ /**
636
+ * Update metadata.json with GitHub issue
637
+ */
638
+ async updateMetadataWithGitHubIssue(incrementId, issueNumber, issueUrl) {
639
+ const metadataPath = path.join(this.projectRoot, '.specweave/increments', incrementId, 'metadata.json');
640
+ let metadata = {};
641
+ if (existsSync(metadataPath)) {
642
+ metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
643
+ }
644
+ metadata.github = {
645
+ issue: issueNumber,
646
+ url: issueUrl,
647
+ synced: new Date().toISOString(),
648
+ };
649
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
650
+ }
651
+ /**
652
+ * Update metadata.json with JIRA issue
653
+ */
654
+ async updateMetadataWithJiraIssue(incrementId, issueKey, issueUrl) {
655
+ const metadataPath = path.join(this.projectRoot, '.specweave/increments', incrementId, 'metadata.json');
656
+ let metadata = {};
657
+ if (existsSync(metadataPath)) {
658
+ metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
659
+ }
660
+ metadata.jira = {
661
+ issue: issueKey,
662
+ url: issueUrl,
663
+ synced: new Date().toISOString(),
664
+ };
665
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
666
+ }
667
+ /**
668
+ * Update metadata.json with ADO work item
669
+ */
670
+ async updateMetadataWithAdoWorkItem(incrementId, workItemId, workItemUrl) {
671
+ const metadataPath = path.join(this.projectRoot, '.specweave/increments', incrementId, 'metadata.json');
672
+ let metadata = {};
673
+ if (existsSync(metadataPath)) {
674
+ metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
675
+ }
676
+ metadata.ado = {
677
+ workItem: workItemId,
678
+ url: workItemUrl,
679
+ synced: new Date().toISOString(),
680
+ };
681
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
682
+ }
683
+ }
684
+ /**
685
+ * Convenience function to auto-create issues for an increment
686
+ */
687
+ export async function autoCreateExternalIssue(projectRoot, incrementId, logger) {
688
+ const creator = new ExternalIssueAutoCreator({
689
+ projectRoot,
690
+ logger,
691
+ });
692
+ return creator.createForIncrement(incrementId);
693
+ }
694
+ //# sourceMappingURL=external-issue-auto-creator.js.map