specweave 0.13.6 → 0.14.0

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 (35) hide show
  1. package/CLAUDE.md +189 -0
  2. package/dist/cli/commands/init.js +1 -1
  3. package/dist/cli/commands/init.js.map +1 -1
  4. package/dist/cli/commands/status-line.d.ts +14 -0
  5. package/dist/cli/commands/status-line.d.ts.map +1 -0
  6. package/dist/cli/commands/status-line.js +75 -0
  7. package/dist/cli/commands/status-line.js.map +1 -0
  8. package/dist/core/status-line/status-line-manager.d.ts +62 -0
  9. package/dist/core/status-line/status-line-manager.d.ts.map +1 -0
  10. package/dist/core/status-line/status-line-manager.js +169 -0
  11. package/dist/core/status-line/status-line-manager.js.map +1 -0
  12. package/dist/core/status-line/types.d.ts +50 -0
  13. package/dist/core/status-line/types.d.ts.map +1 -0
  14. package/dist/core/status-line/types.js +17 -0
  15. package/dist/core/status-line/types.js.map +1 -0
  16. package/dist/utils/project-mapper.d.ts +74 -0
  17. package/dist/utils/project-mapper.d.ts.map +1 -0
  18. package/dist/utils/project-mapper.js +273 -0
  19. package/dist/utils/project-mapper.js.map +1 -0
  20. package/dist/utils/spec-splitter.d.ts +68 -0
  21. package/dist/utils/spec-splitter.d.ts.map +1 -0
  22. package/dist/utils/spec-splitter.js +314 -0
  23. package/dist/utils/spec-splitter.js.map +1 -0
  24. package/package.json +1 -1
  25. package/plugins/specweave/hooks/lib/update-status-line.sh +138 -0
  26. package/plugins/specweave/hooks/post-task-completion.sh +10 -0
  27. package/plugins/specweave/skills/multi-project-spec-mapper/SKILL.md +399 -0
  28. package/plugins/specweave-ado/lib/ado-multi-project-sync.js +453 -0
  29. package/plugins/specweave-ado/lib/ado-multi-project-sync.ts +633 -0
  30. package/plugins/specweave-docs/skills/docusaurus/SKILL.md +17 -3
  31. package/plugins/specweave-docs-preview/commands/preview.md +29 -4
  32. package/plugins/specweave-github/lib/github-multi-project-sync.js +340 -0
  33. package/plugins/specweave-github/lib/github-multi-project-sync.ts +461 -0
  34. package/plugins/specweave-jira/lib/jira-multi-project-sync.js +244 -0
  35. package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +358 -0
@@ -0,0 +1,461 @@
1
+ /**
2
+ * GitHub Multi-Project Sync
3
+ *
4
+ * Supports two patterns:
5
+ * 1. **Multiple Repos**: Separate repos for each project (FE, BE, MOBILE)
6
+ * 2. **Master + Nested**: Master repo (epics) + nested repos (detailed tasks)
7
+ *
8
+ * @module github-multi-project-sync
9
+ */
10
+
11
+ import { Octokit } from '@octokit/rest';
12
+ import { UserStory, getPrimaryProject, ProjectMapping } from '../../../src/utils/project-mapper.js';
13
+ import { parseSpecFile } from '../../../src/utils/spec-splitter.js';
14
+
15
+ export interface GitHubMultiProjectConfig {
16
+ owner: string;
17
+ token: string;
18
+
19
+ // Pattern 1: Multiple repos (simple)
20
+ repos?: string[]; // ['frontend-web', 'backend-api', 'mobile-app']
21
+
22
+ // Pattern 2: Master + nested repos (advanced)
23
+ masterRepo?: string; // 'master-project' (high-level epics)
24
+ nestedRepos?: string[]; // ['frontend-web', 'backend-api', 'mobile-app'] (detailed tasks)
25
+
26
+ // Settings
27
+ masterRepoLevel?: 'epic'; // Master repo contains epics
28
+ nestedRepoLevel?: 'story-task'; // Nested repos contain stories/tasks
29
+ crossLinking?: boolean; // Enable epic → issue links
30
+ }
31
+
32
+ export interface GitHubIssue {
33
+ number: number;
34
+ title: string;
35
+ body: string;
36
+ state: 'open' | 'closed';
37
+ html_url: string;
38
+ labels: Array<{ name: string }>;
39
+ }
40
+
41
+ export interface SyncResult {
42
+ project: string;
43
+ repo: string;
44
+ issueNumber: number;
45
+ url: string;
46
+ action: 'created' | 'updated' | 'skipped';
47
+ }
48
+
49
+ /**
50
+ * GitHub Multi-Project Sync Client
51
+ */
52
+ export class GitHubMultiProjectSync {
53
+ private octokit: Octokit;
54
+ private config: GitHubMultiProjectConfig;
55
+
56
+ constructor(config: GitHubMultiProjectConfig) {
57
+ this.config = config;
58
+ this.octokit = new Octokit({ auth: config.token });
59
+ }
60
+
61
+ /**
62
+ * Sync spec to appropriate GitHub repos based on project mapping
63
+ *
64
+ * @param specPath Path to spec file
65
+ * @returns Array of sync results
66
+ */
67
+ async syncSpec(specPath: string): Promise<SyncResult[]> {
68
+ const results: SyncResult[] = [];
69
+
70
+ // Parse spec
71
+ const parsedSpec = await parseSpecFile(specPath);
72
+
73
+ // Determine sync pattern
74
+ const isMasterNested = !!this.config.masterRepo && !!this.config.nestedRepos;
75
+
76
+ if (isMasterNested) {
77
+ // Pattern 2: Master + Nested
78
+ results.push(...await this.syncMasterNested(parsedSpec));
79
+ } else if (this.config.repos) {
80
+ // Pattern 1: Multiple Repos
81
+ results.push(...await this.syncMultipleRepos(parsedSpec));
82
+ } else {
83
+ throw new Error('Invalid config: Must specify repos[] or masterRepo+nestedRepos[]');
84
+ }
85
+
86
+ return results;
87
+ }
88
+
89
+ /**
90
+ * Pattern 1: Sync to multiple repos (simple)
91
+ *
92
+ * Each project → separate repo
93
+ * - FE user stories → company/frontend-web
94
+ * - BE user stories → company/backend-api
95
+ * - MOBILE user stories → company/mobile-app
96
+ */
97
+ private async syncMultipleRepos(parsedSpec: any): Promise<SyncResult[]> {
98
+ const results: SyncResult[] = [];
99
+
100
+ // Classify user stories by project
101
+ const projectStories = new Map<string, UserStory[]>();
102
+
103
+ for (const userStory of parsedSpec.userStories) {
104
+ const primaryProject = getPrimaryProject(userStory);
105
+
106
+ if (primaryProject) {
107
+ const existing = projectStories.get(primaryProject.projectId) || [];
108
+ existing.push(userStory);
109
+ projectStories.set(primaryProject.projectId, existing);
110
+ } else {
111
+ // No confident match - skip or assign to default repo
112
+ console.warn(`⚠️ No confident project match for ${userStory.id} - skipping`);
113
+ }
114
+ }
115
+
116
+ // Sync each project to its repo
117
+ for (const [projectId, stories] of projectStories.entries()) {
118
+ const repo = this.findRepoForProject(projectId);
119
+
120
+ if (!repo) {
121
+ console.warn(`⚠️ No GitHub repo configured for project ${projectId} - skipping`);
122
+ continue;
123
+ }
124
+
125
+ // Create or update issue for each story
126
+ for (const story of stories) {
127
+ const result = await this.createOrUpdateIssue(repo, story, projectId);
128
+ results.push(result);
129
+ }
130
+ }
131
+
132
+ return results;
133
+ }
134
+
135
+ /**
136
+ * Pattern 2: Sync to master + nested repos (advanced)
137
+ *
138
+ * - Master repo (epic-level): High-level overview
139
+ * - Nested repos (task-level): Detailed implementation
140
+ *
141
+ * Example:
142
+ * Master (company/master-project):
143
+ * Epic #10: User Authentication
144
+ * → Links to: frontend-web#42, backend-api#15, mobile-app#8
145
+ *
146
+ * Nested (company/frontend-web):
147
+ * Issue #42: Implement Login UI
148
+ * Task 1: Create login component
149
+ * Task 2: Add form validation
150
+ */
151
+ private async syncMasterNested(parsedSpec: any): Promise<SyncResult[]> {
152
+ const results: SyncResult[] = [];
153
+
154
+ if (!this.config.masterRepo || !this.config.nestedRepos) {
155
+ throw new Error('Master+nested mode requires masterRepo and nestedRepos');
156
+ }
157
+
158
+ // Step 1: Create epic in master repo
159
+ const epicResult = await this.createEpicInMasterRepo(parsedSpec);
160
+ results.push(epicResult);
161
+
162
+ // Step 2: Classify user stories by project
163
+ const projectStories = new Map<string, UserStory[]>();
164
+
165
+ for (const userStory of parsedSpec.userStories) {
166
+ const primaryProject = getPrimaryProject(userStory);
167
+
168
+ if (primaryProject) {
169
+ const existing = projectStories.get(primaryProject.projectId) || [];
170
+ existing.push(userStory);
171
+ projectStories.set(primaryProject.projectId, existing);
172
+ }
173
+ }
174
+
175
+ // Step 3: Create issues in nested repos + link to epic
176
+ for (const [projectId, stories] of projectStories.entries()) {
177
+ const repo = this.findNestedRepoForProject(projectId);
178
+
179
+ if (!repo) {
180
+ console.warn(`⚠️ No nested repo for project ${projectId} - skipping`);
181
+ continue;
182
+ }
183
+
184
+ for (const story of stories) {
185
+ const result = await this.createIssueInNestedRepo(repo, story, projectId, epicResult.issueNumber);
186
+ results.push(result);
187
+ }
188
+ }
189
+
190
+ // Step 4: Update epic with links to nested issues
191
+ if (this.config.crossLinking) {
192
+ await this.updateEpicWithLinks(epicResult.issueNumber, results.filter(r => r.repo !== this.config.masterRepo));
193
+ }
194
+
195
+ return results;
196
+ }
197
+
198
+ /**
199
+ * Create epic issue in master repo
200
+ */
201
+ private async createEpicInMasterRepo(parsedSpec: any): Promise<SyncResult> {
202
+ const title = `Epic: ${parsedSpec.metadata.title}`;
203
+
204
+ const body = `# ${parsedSpec.metadata.title}
205
+
206
+ **Status**: ${parsedSpec.metadata.status}
207
+ **Priority**: ${parsedSpec.metadata.priority}
208
+ **Estimated Effort**: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
209
+
210
+ ## Executive Summary
211
+
212
+ ${parsedSpec.executiveSummary}
213
+
214
+ ## User Stories (${parsedSpec.userStories.length} total)
215
+
216
+ ${parsedSpec.userStories.map((s: UserStory, i: number) => `${i + 1}. ${s.id}: ${s.title}`).join('\n')}
217
+
218
+ ---
219
+
220
+ 📊 **This is a high-level epic** - detailed implementation tracked in nested repos
221
+ 🔗 **Links to implementation issues** (will be added automatically)
222
+
223
+ 🤖 Auto-generated by SpecWeave
224
+ `;
225
+
226
+ const response = await this.octokit.issues.create({
227
+ owner: this.config.owner,
228
+ repo: this.config.masterRepo!,
229
+ title,
230
+ body,
231
+ labels: ['epic', 'specweave']
232
+ });
233
+
234
+ return {
235
+ project: 'MASTER',
236
+ repo: this.config.masterRepo!,
237
+ issueNumber: response.data.number,
238
+ url: response.data.html_url,
239
+ action: 'created'
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Create issue in nested repo + link to epic
245
+ */
246
+ private async createIssueInNestedRepo(
247
+ repo: string,
248
+ userStory: UserStory,
249
+ projectId: string,
250
+ epicNumber?: number
251
+ ): Promise<SyncResult> {
252
+ const title = `${userStory.id}: ${userStory.title}`;
253
+
254
+ let body = `# ${userStory.title}
255
+
256
+ ${userStory.description}
257
+
258
+ ## Acceptance Criteria
259
+
260
+ ${userStory.acceptanceCriteria.map((ac, i) => `- [ ] ${ac}`).join('\n')}
261
+ `;
262
+
263
+ // Add technical context if present
264
+ if (userStory.technicalContext) {
265
+ body += `\n## Technical Context\n\n${userStory.technicalContext}\n`;
266
+ }
267
+
268
+ // Add link to epic if cross-linking enabled
269
+ if (this.config.crossLinking && epicNumber) {
270
+ body += `\n---\n\n📊 Part of Epic: ${this.config.owner}/${this.config.masterRepo}#${epicNumber}\n`;
271
+ }
272
+
273
+ body += `\n🤖 Auto-generated by SpecWeave\n`;
274
+
275
+ const response = await this.octokit.issues.create({
276
+ owner: this.config.owner,
277
+ repo,
278
+ title,
279
+ body,
280
+ labels: ['story', 'specweave', projectId.toLowerCase()]
281
+ });
282
+
283
+ return {
284
+ project: projectId,
285
+ repo,
286
+ issueNumber: response.data.number,
287
+ url: response.data.html_url,
288
+ action: 'created'
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Create or update issue (for simple multi-repo pattern)
294
+ */
295
+ private async createOrUpdateIssue(repo: string, userStory: UserStory, projectId: string): Promise<SyncResult> {
296
+ const title = `${userStory.id}: ${userStory.title}`;
297
+
298
+ const body = `# ${userStory.title}
299
+
300
+ ${userStory.description}
301
+
302
+ ## Acceptance Criteria
303
+
304
+ ${userStory.acceptanceCriteria.map((ac, i) => `- [ ] ${ac}`).join('\n')}
305
+
306
+ ${userStory.technicalContext ? `\n## Technical Context\n\n${userStory.technicalContext}\n` : ''}
307
+
308
+ 🤖 Auto-generated by SpecWeave
309
+ `;
310
+
311
+ // Check if issue already exists (search by title)
312
+ const existingIssues = await this.octokit.issues.listForRepo({
313
+ owner: this.config.owner,
314
+ repo,
315
+ labels: 'specweave',
316
+ state: 'all'
317
+ });
318
+
319
+ const existing = existingIssues.data.find(issue => issue.title === title);
320
+
321
+ if (existing) {
322
+ // Update existing issue
323
+ const response = await this.octokit.issues.update({
324
+ owner: this.config.owner,
325
+ repo,
326
+ issue_number: existing.number,
327
+ body
328
+ });
329
+
330
+ return {
331
+ project: projectId,
332
+ repo,
333
+ issueNumber: response.data.number,
334
+ url: response.data.html_url,
335
+ action: 'updated'
336
+ };
337
+ } else {
338
+ // Create new issue
339
+ const response = await this.octokit.issues.create({
340
+ owner: this.config.owner,
341
+ repo,
342
+ title,
343
+ body,
344
+ labels: ['specweave', projectId.toLowerCase()]
345
+ });
346
+
347
+ return {
348
+ project: projectId,
349
+ repo,
350
+ issueNumber: response.data.number,
351
+ url: response.data.html_url,
352
+ action: 'created'
353
+ };
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Update epic with links to nested issues
359
+ */
360
+ private async updateEpicWithLinks(epicNumber: number, nestedResults: SyncResult[]): Promise<void> {
361
+ if (!this.config.masterRepo) return;
362
+
363
+ const linksSection = `\n\n## Implementation Issues\n\n${nestedResults
364
+ .map(r => `- ${r.project}: ${this.config.owner}/${r.repo}#${r.issueNumber} - ${r.url}`)
365
+ .join('\n')}\n`;
366
+
367
+ // Get current epic body
368
+ const epic = await this.octokit.issues.get({
369
+ owner: this.config.owner,
370
+ repo: this.config.masterRepo,
371
+ issue_number: epicNumber
372
+ });
373
+
374
+ // Append links section
375
+ const updatedBody = epic.data.body + linksSection;
376
+
377
+ // Update epic
378
+ await this.octokit.issues.update({
379
+ owner: this.config.owner,
380
+ repo: this.config.masterRepo,
381
+ issue_number: epicNumber,
382
+ body: updatedBody
383
+ });
384
+ }
385
+
386
+ /**
387
+ * Find GitHub repo for project ID
388
+ *
389
+ * Maps project IDs to repo names:
390
+ * - FE → frontend-web
391
+ * - BE → backend-api
392
+ * - MOBILE → mobile-app
393
+ */
394
+ private findRepoForProject(projectId: string): string | undefined {
395
+ if (!this.config.repos) return undefined;
396
+
397
+ // Try exact match first
398
+ let match = this.config.repos.find(repo => repo.toLowerCase().includes(projectId.toLowerCase()));
399
+
400
+ if (!match) {
401
+ // Try fuzzy match (FE → frontend, BE → backend, MOBILE → mobile)
402
+ const fuzzyMap: Record<string, string[]> = {
403
+ FE: ['frontend', 'web', 'ui', 'client'],
404
+ BE: ['backend', 'api', 'server'],
405
+ MOBILE: ['mobile', 'app', 'ios', 'android'],
406
+ INFRA: ['infra', 'infrastructure', 'devops', 'platform']
407
+ };
408
+
409
+ const keywords = fuzzyMap[projectId] || [];
410
+
411
+ match = this.config.repos.find(repo =>
412
+ keywords.some(keyword => repo.toLowerCase().includes(keyword))
413
+ );
414
+ }
415
+
416
+ return match;
417
+ }
418
+
419
+ /**
420
+ * Find nested repo for project ID (same logic as findRepoForProject)
421
+ */
422
+ private findNestedRepoForProject(projectId: string): string | undefined {
423
+ if (!this.config.nestedRepos) return undefined;
424
+
425
+ // Reuse same logic
426
+ const tempConfig = { ...this.config, repos: this.config.nestedRepos };
427
+ const tempSync = new GitHubMultiProjectSync(tempConfig);
428
+ return tempSync.findRepoForProject(projectId);
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Format sync results for display
434
+ */
435
+ export function formatSyncResults(results: SyncResult[]): string {
436
+ const lines: string[] = [];
437
+
438
+ lines.push('📊 GitHub Multi-Project Sync Results:\n');
439
+
440
+ const byProject = new Map<string, SyncResult[]>();
441
+
442
+ for (const result of results) {
443
+ const existing = byProject.get(result.project) || [];
444
+ existing.push(result);
445
+ byProject.set(result.project, existing);
446
+ }
447
+
448
+ for (const [project, projectResults] of byProject.entries()) {
449
+ lines.push(`\n**${project}**:`);
450
+
451
+ for (const result of projectResults) {
452
+ const icon = result.action === 'created' ? '✅' : result.action === 'updated' ? '🔄' : '⏭️';
453
+ lines.push(` ${icon} ${result.repo}#${result.issueNumber} (${result.action})`);
454
+ lines.push(` ${result.url}`);
455
+ }
456
+ }
457
+
458
+ lines.push(`\n✅ Total: ${results.length} issues synced\n`);
459
+
460
+ return lines.join('\n');
461
+ }
@@ -0,0 +1,244 @@
1
+ import { JiraClient } from "../../../src/integrations/jira/jira-client.js";
2
+ import {
3
+ suggestJiraItemType,
4
+ mapUserStoryToProjects
5
+ } from "../../../src/utils/project-mapper.js";
6
+ import { parseSpecFile } from "../../../src/utils/spec-splitter.js";
7
+ class JiraMultiProjectSync {
8
+ constructor(config) {
9
+ this.config = config;
10
+ this.client = new JiraClient({
11
+ domain: config.domain,
12
+ email: config.email,
13
+ apiToken: config.apiToken
14
+ });
15
+ }
16
+ /**
17
+ * Sync spec to JIRA projects with intelligent mapping
18
+ *
19
+ * @param specPath Path to spec file
20
+ * @returns Array of sync results
21
+ */
22
+ async syncSpec(specPath) {
23
+ const results = [];
24
+ const parsedSpec = await parseSpecFile(specPath);
25
+ const epicsByProject = /* @__PURE__ */ new Map();
26
+ if (this.config.autoCreateEpics !== false) {
27
+ for (const project of this.config.projects) {
28
+ const epicResult = await this.createEpicForProject(parsedSpec, project);
29
+ epicsByProject.set(project, epicResult.issueKey);
30
+ results.push(epicResult);
31
+ }
32
+ }
33
+ const projectStories = /* @__PURE__ */ new Map();
34
+ for (const userStory of parsedSpec.userStories) {
35
+ if (this.config.intelligentMapping !== false) {
36
+ const mappings = mapUserStoryToProjects(userStory);
37
+ if (mappings.length > 0 && mappings[0].confidence >= 0.3) {
38
+ const primary = mappings[0];
39
+ const existing = projectStories.get(primary.projectId) || [];
40
+ existing.push({ story: userStory, confidence: primary.confidence });
41
+ projectStories.set(primary.projectId, existing);
42
+ } else {
43
+ console.warn(`\u26A0\uFE0F Low confidence for ${userStory.id} (${(mappings[0]?.confidence || 0) * 100}%) - assigning to ${this.config.projects[0]}`);
44
+ const fallback = this.config.projects[0];
45
+ const existing = projectStories.get(fallback) || [];
46
+ existing.push({ story: userStory, confidence: mappings[0]?.confidence || 0 });
47
+ projectStories.set(fallback, existing);
48
+ }
49
+ } else {
50
+ const projectHint = this.extractProjectHint(userStory);
51
+ if (projectHint && this.config.projects.includes(projectHint)) {
52
+ const existing = projectStories.get(projectHint) || [];
53
+ existing.push({ story: userStory, confidence: 1 });
54
+ projectStories.set(projectHint, existing);
55
+ } else {
56
+ const fallback = this.config.projects[0];
57
+ const existing = projectStories.get(fallback) || [];
58
+ existing.push({ story: userStory, confidence: 0 });
59
+ projectStories.set(fallback, existing);
60
+ }
61
+ }
62
+ }
63
+ for (const [projectId, stories] of projectStories.entries()) {
64
+ const epicKey = epicsByProject.get(projectId);
65
+ for (const { story, confidence } of stories) {
66
+ const result = await this.createIssueForUserStory(projectId, story, epicKey, confidence);
67
+ results.push(result);
68
+ }
69
+ }
70
+ return results;
71
+ }
72
+ /**
73
+ * Create epic for project
74
+ */
75
+ async createEpicForProject(parsedSpec, projectId) {
76
+ const summary = `${parsedSpec.metadata.title} - ${projectId}`;
77
+ const description = `h2. ${projectId} Implementation
78
+
79
+ *Status*: ${parsedSpec.metadata.status}
80
+ *Priority*: ${parsedSpec.metadata.priority}
81
+ *Estimated Effort*: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
82
+
83
+ h3. Executive Summary
84
+
85
+ ${parsedSpec.executiveSummary}
86
+
87
+ h3. Scope (${projectId})
88
+
89
+ This epic covers all ${projectId}-related user stories for "${parsedSpec.metadata.title}".
90
+
91
+ User stories will be added as child issues.
92
+
93
+ ---
94
+
95
+ \u{1F916} Auto-generated by SpecWeave
96
+ `;
97
+ const issue = await this.client.createIssue({
98
+ project: { key: projectId },
99
+ summary,
100
+ description,
101
+ issuetype: { name: this.config.itemTypeMapping?.epic || "Epic" }
102
+ });
103
+ return {
104
+ project: projectId,
105
+ issueKey: issue.key,
106
+ issueType: "Epic",
107
+ summary,
108
+ url: `https://${this.config.domain}/browse/${issue.key}`,
109
+ action: "created"
110
+ };
111
+ }
112
+ /**
113
+ * Create issue for user story
114
+ */
115
+ async createIssueForUserStory(projectId, userStory, epicKey, confidence) {
116
+ const summary = `${userStory.id}: ${userStory.title}`;
117
+ const itemType = suggestJiraItemType(userStory);
118
+ const description = `h3. ${userStory.title}
119
+
120
+ ${userStory.description}
121
+
122
+ h4. Acceptance Criteria
123
+
124
+ ${userStory.acceptanceCriteria.map((ac, i) => `* ${ac}`).join("\n")}
125
+
126
+ ${userStory.technicalContext ? `
127
+ h4. Technical Context
128
+
129
+ ${userStory.technicalContext}
130
+ ` : ""}
131
+
132
+ ${confidence !== void 0 ? `
133
+ _Classification confidence: ${(confidence * 100).toFixed(0)}%_
134
+ ` : ""}
135
+
136
+ \u{1F916} Auto-generated by SpecWeave
137
+ `;
138
+ const issueData = {
139
+ project: { key: projectId },
140
+ summary,
141
+ description,
142
+ issuetype: { name: this.getIssueTypeName(itemType) }
143
+ };
144
+ if (epicKey) {
145
+ issueData.parent = { key: epicKey };
146
+ }
147
+ const issue = await this.client.createIssue(issueData);
148
+ return {
149
+ project: projectId,
150
+ issueKey: issue.key,
151
+ issueType: itemType,
152
+ summary,
153
+ url: `https://${this.config.domain}/browse/${issue.key}`,
154
+ action: "created",
155
+ confidence
156
+ };
157
+ }
158
+ /**
159
+ * Get JIRA issue type name from suggested type
160
+ */
161
+ getIssueTypeName(itemType) {
162
+ const mapping = this.config.itemTypeMapping || {};
163
+ switch (itemType) {
164
+ case "Epic":
165
+ return mapping.epic || "Epic";
166
+ case "Story":
167
+ return mapping.story || "Story";
168
+ case "Task":
169
+ return mapping.task || "Task";
170
+ case "Subtask":
171
+ return mapping.subtask || "Sub-task";
172
+ default:
173
+ return "Story";
174
+ }
175
+ }
176
+ /**
177
+ * Extract project hint from user story (manual override)
178
+ *
179
+ * Looks for hints like:
180
+ * - Title: "[FE] Login UI"
181
+ * - Description: "Project: FE"
182
+ * - Technical context: "Frontend: React"
183
+ */
184
+ extractProjectHint(userStory) {
185
+ const titleMatch = userStory.title.match(/^\[([A-Z]+)\]/);
186
+ if (titleMatch) {
187
+ return titleMatch[1];
188
+ }
189
+ const descMatch = userStory.description.match(/Project:\s*([A-Z]+)/i);
190
+ if (descMatch) {
191
+ return descMatch[1].toUpperCase();
192
+ }
193
+ return void 0;
194
+ }
195
+ }
196
+ function formatJiraSyncResults(results) {
197
+ const lines = [];
198
+ lines.push("\u{1F4CA} JIRA Multi-Project Sync Results:\n");
199
+ const byProject = /* @__PURE__ */ new Map();
200
+ for (const result of results) {
201
+ const existing = byProject.get(result.project) || [];
202
+ existing.push(result);
203
+ byProject.set(result.project, existing);
204
+ }
205
+ for (const [project, projectResults] of byProject.entries()) {
206
+ lines.push(`
207
+ **JIRA Project ${project}**:`);
208
+ for (const result of projectResults) {
209
+ const icon = result.action === "created" ? "\u2705" : result.action === "updated" ? "\u{1F504}" : "\u23ED\uFE0F";
210
+ const confidence = result.confidence !== void 0 ? ` (${(result.confidence * 100).toFixed(0)}% confidence)` : "";
211
+ lines.push(` ${icon} ${result.issueKey} [${result.issueType}]: ${result.summary}${confidence}`);
212
+ lines.push(` ${result.url}`);
213
+ }
214
+ }
215
+ lines.push(`
216
+ \u2705 Total: ${results.length} issues synced
217
+ `);
218
+ const epicCount = results.filter((r) => r.issueType === "Epic").length;
219
+ const storyCount = results.filter((r) => r.issueType === "Story").length;
220
+ const taskCount = results.filter((r) => r.issueType === "Task").length;
221
+ const subtaskCount = results.filter((r) => r.issueType === "Subtask").length;
222
+ lines.push("\u{1F4C8} Item Type Distribution:");
223
+ if (epicCount > 0) lines.push(` - Epics: ${epicCount}`);
224
+ if (storyCount > 0) lines.push(` - Stories: ${storyCount}`);
225
+ if (taskCount > 0) lines.push(` - Tasks: ${taskCount}`);
226
+ if (subtaskCount > 0) lines.push(` - Subtasks: ${subtaskCount}`);
227
+ return lines.join("\n");
228
+ }
229
+ async function validateJiraProjects(client, projectKeys) {
230
+ const missing = [];
231
+ for (const key of projectKeys) {
232
+ try {
233
+ await client.getProject(key);
234
+ } catch (error) {
235
+ missing.push(key);
236
+ }
237
+ }
238
+ return missing;
239
+ }
240
+ export {
241
+ JiraMultiProjectSync,
242
+ formatJiraSyncResults,
243
+ validateJiraProjects
244
+ };