specweave 0.17.16 → 0.18.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 (161) hide show
  1. package/CLAUDE.md +405 -2495
  2. package/README.md +92 -2
  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 +188 -36
  5. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
  6. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
  7. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
  8. package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
  9. package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
  10. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
  11. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
  12. package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
  13. package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
  14. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
  15. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
  16. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
  17. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
  18. package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
  19. package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
  20. package/dist/plugins/specweave-github/lib/github-client.js +25 -13
  21. package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
  22. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
  23. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
  24. package/dist/plugins/specweave-github/lib/github-epic-sync.js +451 -0
  25. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
  26. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
  27. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
  28. package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
  29. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
  30. package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
  31. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
  32. package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
  33. package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
  34. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
  35. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
  36. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
  37. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
  38. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
  39. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
  40. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
  41. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
  42. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  43. package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
  44. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  45. package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
  46. package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
  47. package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
  48. package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
  49. package/dist/src/core/living-docs/index.d.ts +10 -84
  50. package/dist/src/core/living-docs/index.d.ts.map +1 -1
  51. package/dist/src/core/living-docs/index.js +10 -164
  52. package/dist/src/core/living-docs/index.js.map +1 -1
  53. package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
  54. package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
  55. package/dist/src/core/living-docs/spec-distributor.js +823 -0
  56. package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
  57. package/dist/src/core/living-docs/types.d.ts +201 -0
  58. package/dist/src/core/living-docs/types.d.ts.map +1 -0
  59. package/dist/src/core/living-docs/types.js +15 -0
  60. package/dist/src/core/living-docs/types.js.map +1 -0
  61. package/dist/src/core/logging/prompt-logger.d.ts +70 -0
  62. package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
  63. package/dist/src/core/logging/prompt-logger.js +247 -0
  64. package/dist/src/core/logging/prompt-logger.js.map +1 -0
  65. package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
  66. package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
  67. package/dist/src/core/status-line/status-line-manager.js +33 -70
  68. package/dist/src/core/status-line/status-line-manager.js.map +1 -1
  69. package/dist/src/core/status-line/types.d.ts +19 -31
  70. package/dist/src/core/status-line/types.d.ts.map +1 -1
  71. package/dist/src/core/status-line/types.js +5 -9
  72. package/dist/src/core/status-line/types.js.map +1 -1
  73. package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
  74. package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
  75. package/dist/src/core/sync/conflict-resolver.js +108 -0
  76. package/dist/src/core/sync/conflict-resolver.js.map +1 -0
  77. package/dist/src/core/sync/enhanced-content-builder.d.ts +77 -0
  78. package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
  79. package/dist/src/core/sync/enhanced-content-builder.js +199 -0
  80. package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
  81. package/dist/src/core/sync/label-detector.d.ts +66 -0
  82. package/dist/src/core/sync/label-detector.d.ts.map +1 -0
  83. package/dist/src/core/sync/label-detector.js +211 -0
  84. package/dist/src/core/sync/label-detector.js.map +1 -0
  85. package/dist/src/core/sync/retry-logic.d.ts +64 -0
  86. package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
  87. package/dist/src/core/sync/retry-logic.js +165 -0
  88. package/dist/src/core/sync/retry-logic.js.map +1 -0
  89. package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
  90. package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
  91. package/dist/src/core/sync/spec-increment-mapper.js +424 -0
  92. package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
  93. package/dist/src/core/sync/status-cache.d.ts +91 -0
  94. package/dist/src/core/sync/status-cache.d.ts.map +1 -0
  95. package/dist/src/core/sync/status-cache.js +140 -0
  96. package/dist/src/core/sync/status-cache.js.map +1 -0
  97. package/dist/src/core/sync/status-mapper.d.ts +69 -0
  98. package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
  99. package/dist/src/core/sync/status-mapper.js +90 -0
  100. package/dist/src/core/sync/status-mapper.js.map +1 -0
  101. package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
  102. package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
  103. package/dist/src/core/sync/status-sync-engine.js +347 -0
  104. package/dist/src/core/sync/status-sync-engine.js.map +1 -0
  105. package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
  106. package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
  107. package/dist/src/core/sync/sync-event-logger.js +103 -0
  108. package/dist/src/core/sync/sync-event-logger.js.map +1 -0
  109. package/dist/src/core/sync/workflow-detector.d.ts +95 -0
  110. package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
  111. package/dist/src/core/sync/workflow-detector.js +175 -0
  112. package/dist/src/core/sync/workflow-detector.js.map +1 -0
  113. package/dist/src/core/types/config.d.ts.map +1 -1
  114. package/dist/src/core/types/config.js +31 -0
  115. package/dist/src/core/types/config.js.map +1 -1
  116. package/dist/src/utils/github-url.d.ts +53 -0
  117. package/dist/src/utils/github-url.d.ts.map +1 -0
  118. package/dist/src/utils/github-url.js +90 -0
  119. package/dist/src/utils/github-url.js.map +1 -0
  120. package/dist/src/utils/spec-parser.d.ts +145 -0
  121. package/dist/src/utils/spec-parser.d.ts.map +1 -0
  122. package/dist/src/utils/spec-parser.js +640 -0
  123. package/dist/src/utils/spec-parser.js.map +1 -0
  124. package/package.json +1 -1
  125. package/plugins/specweave/agents/pm/AGENT.md +1 -1
  126. package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
  127. package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
  128. package/plugins/specweave/commands/specweave-done.md +163 -0
  129. package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
  130. package/plugins/specweave/hooks/post-increment-planning.sh +107 -35
  131. package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
  132. package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
  133. package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
  134. package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
  135. package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
  136. package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
  137. package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
  138. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
  139. package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
  140. package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
  141. package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
  142. package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
  143. package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
  144. package/plugins/specweave-github/lib/github-client.js +21 -10
  145. package/plugins/specweave-github/lib/github-client.ts +27 -16
  146. package/plugins/specweave-github/lib/github-epic-sync.js +489 -0
  147. package/plugins/specweave-github/lib/github-epic-sync.ts +690 -0
  148. package/plugins/specweave-github/lib/github-status-sync.js +71 -0
  149. package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
  150. package/plugins/specweave-github/lib/task-sync.js +33 -2
  151. package/plugins/specweave-github/lib/task-sync.ts +44 -2
  152. package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
  153. package/plugins/specweave-jira/lib/enhanced-jira-sync.ts.disabled +222 -0
  154. package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
  155. package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
  156. package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
  157. package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
  158. package/src/templates/AGENTS.md.template +88 -1
  159. package/src/templates/CLAUDE.md.template +49 -0
  160. package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
  161. package/plugins/specweave/skills/plugin-installer/SKILL.md +0 -353
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Enhanced JIRA Spec Content Sync
3
+ *
4
+ * Uses EnhancedContentBuilder and SpecIncrementMapper for rich epic descriptions.
5
+ */
6
+
7
+ import { JiraClientV2 } from './jira-client-v2.js';
8
+ import { EnhancedContentBuilder, EnhancedSpecContent } from '../../../src/core/sync/enhanced-content-builder.js';
9
+ import { SpecIncrementMapper, TaskInfo } from '../../../src/core/sync/spec-increment-mapper.js';
10
+ import { parseSpecContent } from '../../../src/core/spec-content-sync.js';
11
+ import path from 'path';
12
+ import fs from 'fs/promises';
13
+
14
+ export interface EnhancedJiraSyncOptions {
15
+ specPath: string;
16
+ domain?: string;
17
+ project?: string;
18
+ dryRun?: boolean;
19
+ verbose?: boolean;
20
+ }
21
+
22
+ export interface EnhancedJiraSyncResult {
23
+ success: boolean;
24
+ action: 'created' | 'updated' | 'no-change' | 'error';
25
+ epicKey?: string;
26
+ epicUrl?: string;
27
+ error?: string;
28
+ tasksLinked?: number;
29
+ }
30
+
31
+ /**
32
+ * Enhanced sync with rich content including task mappings
33
+ */
34
+ export async function syncSpecToJiraWithEnhancedContent(
35
+ options: EnhancedJiraSyncOptions
36
+ ): Promise<EnhancedJiraSyncResult> {
37
+ const { specPath, domain, project, dryRun = false, verbose = false } = options;
38
+
39
+ try {
40
+ // 1. Parse spec content
41
+ const baseSpec = await parseSpecContent(specPath);
42
+ if (!baseSpec) {
43
+ return {
44
+ success: false,
45
+ action: 'error',
46
+ error: 'Failed to parse spec content',
47
+ };
48
+ }
49
+
50
+ if (verbose) {
51
+ console.log(`📄 Parsed spec: ${baseSpec.identifier.compact}`);
52
+ }
53
+
54
+ // 2. Build enhanced spec with task mappings
55
+ const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
56
+ const rootDir = await findSpecWeaveRoot(specPath);
57
+ const mapper = new SpecIncrementMapper(rootDir);
58
+ const mapping = await mapper.mapSpecToIncrements(specId);
59
+
60
+ if (verbose) {
61
+ console.log(`🔗 Found ${mapping.increments.length} related increments`);
62
+ }
63
+
64
+ // 3. Build enhanced spec content
65
+ const taskMapping = buildTaskMapping(mapping.increments);
66
+ const architectureDocs = await findArchitectureDocs(rootDir, specId);
67
+
68
+ const enhancedSpec: EnhancedSpecContent = {
69
+ ...baseSpec,
70
+ summary: baseSpec.description,
71
+ taskMapping,
72
+ architectureDocs
73
+ };
74
+
75
+ // 4. Build external description
76
+ const builder = new EnhancedContentBuilder();
77
+ const description = builder.buildExternalDescription(enhancedSpec);
78
+
79
+ if (verbose) {
80
+ console.log(`📝 Generated description: ${description.length} characters`);
81
+ }
82
+
83
+ if (dryRun) {
84
+ console.log('🔍 DRY RUN - Would create/update epic with:');
85
+ console.log(` Summary: ${baseSpec.title}`);
86
+ console.log(` Description length: ${description.length}`);
87
+ return {
88
+ success: true,
89
+ action: 'no-change',
90
+ tasksLinked: taskMapping?.tasks.length || 0
91
+ };
92
+ }
93
+
94
+ // 5. Create or update JIRA epic
95
+ if (!domain || !project) {
96
+ return {
97
+ success: false,
98
+ action: 'error',
99
+ error: 'JIRA domain/project not specified',
100
+ };
101
+ }
102
+
103
+ const client = new JiraClientV2({ domain, project });
104
+
105
+ // Check if epic already exists
106
+ const existingEpic = await findExistingEpic(client, baseSpec.identifier.compact);
107
+
108
+ let result: EnhancedJiraSyncResult;
109
+
110
+ if (existingEpic) {
111
+ // Update existing epic
112
+ await client.updateEpic(existingEpic.key, {
113
+ summary: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
114
+ description
115
+ });
116
+
117
+ result = {
118
+ success: true,
119
+ action: 'updated',
120
+ epicKey: existingEpic.key,
121
+ epicUrl: `https://${domain}/browse/${existingEpic.key}`,
122
+ tasksLinked: taskMapping?.tasks.length || 0
123
+ };
124
+ } else {
125
+ // Create new epic
126
+ const epic = await client.createEpic({
127
+ summary: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
128
+ description,
129
+ labels: ['spec', 'external-tool-sync']
130
+ });
131
+
132
+ result = {
133
+ success: true,
134
+ action: 'created',
135
+ epicKey: epic.key,
136
+ epicUrl: `https://${domain}/browse/${epic.key}`,
137
+ tasksLinked: taskMapping?.tasks.length || 0
138
+ };
139
+ }
140
+
141
+ if (verbose) {
142
+ console.log(`✅ ${result.action === 'created' ? 'Created' : 'Updated'} epic ${result.epicKey}`);
143
+ }
144
+
145
+ return result;
146
+ } catch (error: any) {
147
+ return {
148
+ success: false,
149
+ action: 'error',
150
+ error: error.message
151
+ };
152
+ }
153
+ }
154
+
155
+ // Helper functions (similar to GitHub sync)
156
+
157
+ async function findSpecWeaveRoot(specPath: string): Promise<string> {
158
+ let currentDir = path.dirname(specPath);
159
+
160
+ while (true) {
161
+ const specweaveDir = path.join(currentDir, '.specweave');
162
+ try {
163
+ await fs.access(specweaveDir);
164
+ return currentDir;
165
+ } catch {
166
+ const parentDir = path.dirname(currentDir);
167
+ if (parentDir === currentDir) {
168
+ throw new Error('.specweave directory not found');
169
+ }
170
+ currentDir = parentDir;
171
+ }
172
+ }
173
+ }
174
+
175
+ function buildTaskMapping(increments: any[]): any {
176
+ if (increments.length === 0) return undefined;
177
+
178
+ const firstIncrement = increments[0];
179
+ const tasks = firstIncrement.tasks.map((task: TaskInfo) => ({
180
+ id: task.id,
181
+ title: task.title,
182
+ userStories: task.userStories
183
+ }));
184
+
185
+ return {
186
+ incrementId: firstIncrement.id,
187
+ tasks,
188
+ tasksUrl: `tasks.md` // JIRA doesn't support external links in same way
189
+ };
190
+ }
191
+
192
+ async function findArchitectureDocs(rootDir: string, specId: string): Promise<any[]> {
193
+ const docs: any[] = [];
194
+ const archDir = path.join(rootDir, '.specweave/docs/internal/architecture');
195
+
196
+ try {
197
+ const adrDir = path.join(archDir, 'adr');
198
+ try {
199
+ const adrs = await fs.readdir(adrDir);
200
+ const relatedAdrs = adrs.filter(file => file.includes(specId.replace('spec-', '')));
201
+
202
+ for (const adr of relatedAdrs) {
203
+ docs.push({
204
+ type: 'adr',
205
+ path: path.join(adrDir, adr),
206
+ title: adr.replace('.md', '').replace(/-/g, ' ')
207
+ });
208
+ }
209
+ } catch {}
210
+ } catch {}
211
+
212
+ return docs;
213
+ }
214
+
215
+ async function findExistingEpic(client: JiraClientV2, specId: string): Promise<any | null> {
216
+ try {
217
+ const epics = await client.searchEpics(`summary ~ "[${specId}]"`);
218
+ return epics[0] || null;
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
@@ -0,0 +1,304 @@
1
+ import * as fs from "fs-extra";
2
+ import * as path from "path";
3
+ import * as yaml from "yaml";
4
+ class JiraEpicSync {
5
+ constructor(client, specsDir, projectKey) {
6
+ this.client = client;
7
+ this.specsDir = specsDir;
8
+ this.projectKey = projectKey;
9
+ }
10
+ /**
11
+ * Sync Epic folder to JIRA (Epic + Stories)
12
+ */
13
+ async syncEpicToJira(epicId) {
14
+ console.log(`
15
+ \u{1F504} Syncing Epic ${epicId} to JIRA...`);
16
+ const epicFolder = await this.findEpicFolder(epicId);
17
+ if (!epicFolder) {
18
+ throw new Error(`Epic ${epicId} not found in ${this.specsDir}`);
19
+ }
20
+ const readmePath = path.join(epicFolder, "README.md");
21
+ const epicData = await this.parseEpicReadme(readmePath);
22
+ console.log(` \u{1F4E6} Epic: ${epicData.title}`);
23
+ console.log(` \u{1F4CA} Increments: ${epicData.total_increments}`);
24
+ let epicKey = epicData.external_tools.jira.key;
25
+ let epicUrl = epicData.external_tools.jira.url;
26
+ if (!epicKey) {
27
+ console.log(` \u{1F680} Creating JIRA Epic...`);
28
+ const epic = await this.createEpic(epicData);
29
+ epicKey = epic.key;
30
+ epicUrl = epic.url;
31
+ console.log(` \u2705 Created Epic ${epicKey}`);
32
+ await this.updateEpicReadme(readmePath, {
33
+ type: "epic",
34
+ key: epicKey,
35
+ url: epicUrl
36
+ });
37
+ } else {
38
+ console.log(` \u267B\uFE0F Updating existing Epic ${epicKey}...`);
39
+ await this.updateEpic(epicKey, epicData);
40
+ console.log(` \u2705 Updated Epic ${epicKey}`);
41
+ }
42
+ let storiesCreated = 0;
43
+ let storiesUpdated = 0;
44
+ console.log(`
45
+ \u{1F4DD} Syncing ${epicData.increments.length} increments...`);
46
+ for (const increment of epicData.increments) {
47
+ const incrementFile = path.join(epicFolder, `${increment.id}.md`);
48
+ if (!await fs.pathExists(incrementFile)) {
49
+ console.log(` \u26A0\uFE0F Increment file not found: ${increment.id}.md`);
50
+ continue;
51
+ }
52
+ const incrementData = await this.parseIncrementFile(incrementFile);
53
+ const existingStory = increment.external.jira;
54
+ if (!existingStory) {
55
+ const storyKey = await this.createStory(
56
+ epicData.id,
57
+ incrementData,
58
+ epicKey
59
+ );
60
+ storiesCreated++;
61
+ console.log(` \u2705 Created Story ${storyKey} for ${increment.id}`);
62
+ await this.updateIncrementExternalLink(
63
+ readmePath,
64
+ incrementFile,
65
+ increment.id,
66
+ storyKey
67
+ );
68
+ } else {
69
+ await this.updateStory(
70
+ epicData.id,
71
+ existingStory,
72
+ incrementData,
73
+ epicKey
74
+ );
75
+ storiesUpdated++;
76
+ console.log(` \u267B\uFE0F Updated Story ${existingStory} for ${increment.id}`);
77
+ }
78
+ }
79
+ console.log(`
80
+ \u2705 Epic sync complete!`);
81
+ console.log(` Epic: ${epicUrl}`);
82
+ console.log(` Stories created: ${storiesCreated}`);
83
+ console.log(` Stories updated: ${storiesUpdated}`);
84
+ return {
85
+ epicKey,
86
+ epicUrl,
87
+ storiesCreated,
88
+ storiesUpdated
89
+ };
90
+ }
91
+ /**
92
+ * Find Epic folder by ID (FS-001 or just 001)
93
+ */
94
+ async findEpicFolder(epicId) {
95
+ const normalizedId = epicId.startsWith("FS-") ? epicId : `FS-${epicId.padStart(3, "0")}`;
96
+ const folders = await fs.readdir(this.specsDir);
97
+ for (const folder of folders) {
98
+ if (folder.startsWith(normalizedId)) {
99
+ return path.join(this.specsDir, folder);
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ /**
105
+ * Parse Epic README.md to extract frontmatter
106
+ */
107
+ async parseEpicReadme(readmePath) {
108
+ const content = await fs.readFile(readmePath, "utf-8");
109
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
110
+ if (!match) {
111
+ throw new Error("Epic README.md missing YAML frontmatter");
112
+ }
113
+ const frontmatter = yaml.parse(match[1]);
114
+ return frontmatter;
115
+ }
116
+ /**
117
+ * Parse increment file to extract title and overview
118
+ */
119
+ async parseIncrementFile(filePath) {
120
+ const content = await fs.readFile(filePath, "utf-8");
121
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
122
+ let frontmatter = { id: "", epic: "" };
123
+ let bodyContent = content;
124
+ if (match) {
125
+ frontmatter = yaml.parse(match[1]);
126
+ bodyContent = content.slice(match[0].length).trim();
127
+ }
128
+ const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
129
+ const title = titleMatch ? titleMatch[1].trim() : frontmatter.id || path.basename(filePath, ".md");
130
+ const overviewMatch = bodyContent.match(/^#[^\n]+\n+([^\n]+)/);
131
+ const overview = overviewMatch ? overviewMatch[1].trim() : "No overview available";
132
+ return {
133
+ id: frontmatter.id,
134
+ title,
135
+ overview,
136
+ content: bodyContent,
137
+ frontmatter
138
+ };
139
+ }
140
+ /**
141
+ * Create JIRA Epic
142
+ */
143
+ async createEpic(epic) {
144
+ const summary = `[${epic.id}] ${epic.title}`;
145
+ const description = `Epic: ${epic.title}
146
+
147
+ Progress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})
148
+
149
+ Priority: ${epic.priority}
150
+ Status: ${epic.status}`;
151
+ const issueData = {
152
+ issueType: "Epic",
153
+ summary,
154
+ description,
155
+ priority: this.mapPriorityToJira(epic.priority),
156
+ labels: ["epic-sync", epic.id.toLowerCase()]
157
+ };
158
+ const issue = await this.client.createIssue(issueData, this.projectKey);
159
+ return {
160
+ key: issue.key,
161
+ url: issue.self.replace("/rest/api/3/issue/", "/browse/")
162
+ };
163
+ }
164
+ /**
165
+ * Update JIRA Epic
166
+ */
167
+ async updateEpic(epicKey, epic) {
168
+ const summary = `[${epic.id}] ${epic.title}`;
169
+ const description = `Epic: ${epic.title}
170
+
171
+ Progress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})
172
+
173
+ Priority: ${epic.priority}
174
+ Status: ${epic.status}`;
175
+ await this.client.updateIssue({
176
+ key: epicKey,
177
+ summary,
178
+ description,
179
+ priority: this.mapPriorityToJira(epic.priority),
180
+ labels: ["epic-sync", epic.id.toLowerCase()]
181
+ });
182
+ }
183
+ /**
184
+ * Create JIRA Story for increment
185
+ */
186
+ async createStory(epicId, increment, epicKey) {
187
+ const summary = `[${epicId}] ${increment.title}`;
188
+ const description = `${increment.overview}
189
+
190
+ ---
191
+
192
+ **Increment**: ${increment.id}
193
+ **Epic**: ${epicId} (${epicKey})
194
+
195
+ \u{1F916} Auto-created by SpecWeave Epic Sync`;
196
+ const issueData = {
197
+ issueType: "Story",
198
+ summary,
199
+ description,
200
+ epicKey,
201
+ // Link to Epic via Epic Link field
202
+ labels: ["increment", "epic-sync"]
203
+ };
204
+ const issue = await this.client.createIssue(issueData, this.projectKey);
205
+ return issue.key;
206
+ }
207
+ /**
208
+ * Update JIRA Story for increment
209
+ */
210
+ async updateStory(epicId, storyKey, increment, epicKey) {
211
+ const summary = `[${epicId}] ${increment.title}`;
212
+ const description = `${increment.overview}
213
+
214
+ ---
215
+
216
+ **Increment**: ${increment.id}
217
+ **Epic**: ${epicId} (${epicKey})
218
+
219
+ \u{1F916} Auto-updated by SpecWeave Epic Sync`;
220
+ await this.client.updateIssue({
221
+ key: storyKey,
222
+ summary,
223
+ description,
224
+ labels: ["increment", "epic-sync"]
225
+ });
226
+ }
227
+ /**
228
+ * Map SpecWeave priority to JIRA priority
229
+ */
230
+ mapPriorityToJira(priority) {
231
+ const map = {
232
+ P0: "Highest",
233
+ P1: "High",
234
+ P2: "Medium",
235
+ P3: "Low"
236
+ };
237
+ return map[priority] || "Medium";
238
+ }
239
+ /**
240
+ * Update Epic README.md with JIRA Epic key
241
+ */
242
+ async updateEpicReadme(readmePath, jira) {
243
+ const content = await fs.readFile(readmePath, "utf-8");
244
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
245
+ if (!match) {
246
+ throw new Error("Epic README.md missing YAML frontmatter");
247
+ }
248
+ const frontmatter = yaml.parse(match[1]);
249
+ frontmatter.external_tools.jira = jira;
250
+ const newFrontmatter = yaml.stringify(frontmatter);
251
+ const newContent = content.replace(
252
+ /^---\n[\s\S]*?\n---/,
253
+ `---
254
+ ${newFrontmatter}---`
255
+ );
256
+ await fs.writeFile(readmePath, newContent, "utf-8");
257
+ }
258
+ /**
259
+ * Update increment external link in both Epic README and increment file
260
+ */
261
+ async updateIncrementExternalLink(readmePath, incrementFile, incrementId, storyKey) {
262
+ const storyUrl = `https://${this.client["credentials"].domain}/browse/${storyKey}`;
263
+ const readmeContent = await fs.readFile(readmePath, "utf-8");
264
+ const readmeMatch = readmeContent.match(/^---\n([\s\S]*?)\n---/);
265
+ if (readmeMatch) {
266
+ const frontmatter = yaml.parse(readmeMatch[1]);
267
+ const increment = frontmatter.increments.find(
268
+ (inc) => inc.id === incrementId
269
+ );
270
+ if (increment) {
271
+ increment.external.jira = storyKey;
272
+ const newFrontmatter = yaml.stringify(frontmatter);
273
+ const newContent = readmeContent.replace(
274
+ /^---\n[\s\S]*?\n---/,
275
+ `---
276
+ ${newFrontmatter}---`
277
+ );
278
+ await fs.writeFile(readmePath, newContent, "utf-8");
279
+ }
280
+ }
281
+ const incrementContent = await fs.readFile(incrementFile, "utf-8");
282
+ const incrementMatch = incrementContent.match(/^---\n([\s\S]*?)\n---/);
283
+ if (incrementMatch) {
284
+ const frontmatter = yaml.parse(incrementMatch[1]);
285
+ if (!frontmatter.external) {
286
+ frontmatter.external = {};
287
+ }
288
+ frontmatter.external.jira = {
289
+ story: storyKey,
290
+ url: storyUrl
291
+ };
292
+ const newFrontmatter = yaml.stringify(frontmatter);
293
+ const newContent = incrementContent.replace(
294
+ /^---\n[\s\S]*?\n---/,
295
+ `---
296
+ ${newFrontmatter}---`
297
+ );
298
+ await fs.writeFile(incrementFile, newContent, "utf-8");
299
+ }
300
+ }
301
+ }
302
+ export {
303
+ JiraEpicSync
304
+ };