specweave 0.17.15 → 0.17.17

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 (200) hide show
  1. package/CLAUDE.md +405 -2495
  2. package/README.md +92 -2
  3. package/dist/locales/de/.gitkeep +0 -0
  4. package/dist/locales/de/cli.json +108 -0
  5. package/dist/locales/en/cli.json +287 -0
  6. package/dist/locales/en/errors.json +7 -0
  7. package/dist/locales/en/templates.json +6 -0
  8. package/dist/locales/es/.gitkeep +0 -0
  9. package/dist/locales/es/cli.json +41 -0
  10. package/dist/locales/fr/.gitkeep +0 -0
  11. package/dist/locales/fr/cli.json +108 -0
  12. package/dist/locales/ja/.gitkeep +0 -0
  13. package/dist/locales/ja/cli.json +108 -0
  14. package/dist/locales/ko/.gitkeep +0 -0
  15. package/dist/locales/ko/cli.json +108 -0
  16. package/dist/locales/pt/.gitkeep +0 -0
  17. package/dist/locales/pt/cli.json +108 -0
  18. package/dist/locales/ru/.gitkeep +0 -0
  19. package/dist/locales/ru/cli.json +269 -0
  20. package/dist/locales/zh/.gitkeep +0 -0
  21. package/dist/locales/zh/cli.json +108 -0
  22. package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
  23. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +188 -36
  24. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
  25. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
  26. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
  27. package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
  28. package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
  29. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +25 -0
  30. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +1 -0
  31. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +191 -0
  32. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +1 -0
  33. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
  34. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
  35. package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
  36. package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
  37. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
  38. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
  39. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
  40. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
  41. package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
  42. package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
  43. package/dist/plugins/specweave-github/lib/github-client.js +25 -13
  44. package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
  45. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
  46. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
  47. package/dist/plugins/specweave-github/lib/github-epic-sync.js +451 -0
  48. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
  49. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
  50. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
  51. package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
  52. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
  53. package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
  54. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
  55. package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
  56. package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
  57. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +26 -0
  58. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -0
  59. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +195 -0
  60. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -0
  61. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
  62. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
  63. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
  64. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
  65. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
  66. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
  67. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
  68. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
  69. package/dist/spec-parser.js +629 -0
  70. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  71. package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
  72. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  73. package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
  74. package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
  75. package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
  76. package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
  77. package/dist/src/core/living-docs/index.d.ts +10 -84
  78. package/dist/src/core/living-docs/index.d.ts.map +1 -1
  79. package/dist/src/core/living-docs/index.js +10 -164
  80. package/dist/src/core/living-docs/index.js.map +1 -1
  81. package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
  82. package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
  83. package/dist/src/core/living-docs/spec-distributor.js +823 -0
  84. package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
  85. package/dist/src/core/living-docs/types.d.ts +201 -0
  86. package/dist/src/core/living-docs/types.d.ts.map +1 -0
  87. package/dist/src/core/living-docs/types.js +15 -0
  88. package/dist/src/core/living-docs/types.js.map +1 -0
  89. package/dist/src/core/logging/prompt-logger.d.ts +70 -0
  90. package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
  91. package/dist/src/core/logging/prompt-logger.js +247 -0
  92. package/dist/src/core/logging/prompt-logger.js.map +1 -0
  93. package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
  94. package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
  95. package/dist/src/core/status-line/status-line-manager.js +33 -70
  96. package/dist/src/core/status-line/status-line-manager.js.map +1 -1
  97. package/dist/src/core/status-line/types.d.ts +19 -31
  98. package/dist/src/core/status-line/types.d.ts.map +1 -1
  99. package/dist/src/core/status-line/types.js +5 -9
  100. package/dist/src/core/status-line/types.js.map +1 -1
  101. package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
  102. package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
  103. package/dist/src/core/sync/conflict-resolver.js +108 -0
  104. package/dist/src/core/sync/conflict-resolver.js.map +1 -0
  105. package/dist/src/core/sync/enhanced-content-builder.d.ts +77 -0
  106. package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
  107. package/dist/src/core/sync/enhanced-content-builder.js +199 -0
  108. package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
  109. package/dist/src/core/sync/label-detector.d.ts +66 -0
  110. package/dist/src/core/sync/label-detector.d.ts.map +1 -0
  111. package/dist/src/core/sync/label-detector.js +211 -0
  112. package/dist/src/core/sync/label-detector.js.map +1 -0
  113. package/dist/src/core/sync/retry-logic.d.ts +64 -0
  114. package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
  115. package/dist/src/core/sync/retry-logic.js +165 -0
  116. package/dist/src/core/sync/retry-logic.js.map +1 -0
  117. package/dist/src/core/sync/spec-content-sync.d.ts +88 -0
  118. package/dist/src/core/sync/spec-content-sync.d.ts.map +1 -0
  119. package/dist/src/core/sync/spec-content-sync.js +5 -0
  120. package/dist/src/core/sync/spec-content-sync.js.map +1 -0
  121. package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
  122. package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
  123. package/dist/src/core/sync/spec-increment-mapper.js +424 -0
  124. package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
  125. package/dist/src/core/sync/status-cache.d.ts +91 -0
  126. package/dist/src/core/sync/status-cache.d.ts.map +1 -0
  127. package/dist/src/core/sync/status-cache.js +140 -0
  128. package/dist/src/core/sync/status-cache.js.map +1 -0
  129. package/dist/src/core/sync/status-mapper.d.ts +69 -0
  130. package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
  131. package/dist/src/core/sync/status-mapper.js +90 -0
  132. package/dist/src/core/sync/status-mapper.js.map +1 -0
  133. package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
  134. package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
  135. package/dist/src/core/sync/status-sync-engine.js +347 -0
  136. package/dist/src/core/sync/status-sync-engine.js.map +1 -0
  137. package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
  138. package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
  139. package/dist/src/core/sync/sync-event-logger.js +103 -0
  140. package/dist/src/core/sync/sync-event-logger.js.map +1 -0
  141. package/dist/src/core/sync/workflow-detector.d.ts +95 -0
  142. package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
  143. package/dist/src/core/sync/workflow-detector.js +175 -0
  144. package/dist/src/core/sync/workflow-detector.js.map +1 -0
  145. package/dist/src/core/types/config.d.ts.map +1 -1
  146. package/dist/src/core/types/config.js +31 -0
  147. package/dist/src/core/types/config.js.map +1 -1
  148. package/dist/src/utils/github-url.d.ts +53 -0
  149. package/dist/src/utils/github-url.d.ts.map +1 -0
  150. package/dist/src/utils/github-url.js +90 -0
  151. package/dist/src/utils/github-url.js.map +1 -0
  152. package/dist/src/utils/plugin-validator.d.ts +9 -0
  153. package/dist/src/utils/plugin-validator.d.ts.map +1 -1
  154. package/dist/src/utils/plugin-validator.js +86 -19
  155. package/dist/src/utils/plugin-validator.js.map +1 -1
  156. package/dist/src/utils/spec-parser.d.ts +145 -0
  157. package/dist/src/utils/spec-parser.d.ts.map +1 -0
  158. package/dist/src/utils/spec-parser.js +640 -0
  159. package/dist/src/utils/spec-parser.js.map +1 -0
  160. package/dist/tsconfig.tsbuildinfo +1 -0
  161. package/package.json +1 -1
  162. package/plugins/specweave/agents/pm/AGENT.md +1 -1
  163. package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
  164. package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
  165. package/plugins/specweave/commands/specweave-done.md +163 -0
  166. package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
  167. package/plugins/specweave/hooks/post-increment-planning.sh +107 -35
  168. package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
  169. package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
  170. package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
  171. package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
  172. package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
  173. package/plugins/specweave/skills/plugin-validator/SKILL.md +16 -13
  174. package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
  175. package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
  176. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  177. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
  178. package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
  179. package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
  180. package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
  181. package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
  182. package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
  183. package/plugins/specweave-github/lib/github-client.js +21 -10
  184. package/plugins/specweave-github/lib/github-client.ts +27 -16
  185. package/plugins/specweave-github/lib/github-epic-sync.js +489 -0
  186. package/plugins/specweave-github/lib/github-epic-sync.ts +690 -0
  187. package/plugins/specweave-github/lib/github-status-sync.js +71 -0
  188. package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
  189. package/plugins/specweave-github/lib/task-sync.js +33 -2
  190. package/plugins/specweave-github/lib/task-sync.ts +44 -2
  191. package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
  192. package/plugins/specweave-jira/lib/enhanced-jira-sync.ts.disabled +222 -0
  193. package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
  194. package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
  195. package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
  196. package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
  197. package/src/templates/AGENTS.md.template +88 -1
  198. package/src/templates/CLAUDE.md.template +49 -0
  199. package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
  200. package/plugins/specweave/skills/plugin-installer/SKILL.md +0 -353
@@ -0,0 +1,459 @@
1
+ /**
2
+ * JIRA Epic Sync - Hierarchical synchronization for Epic folder structure
3
+ *
4
+ * Architecture:
5
+ * - Epic (FS-001) → JIRA Epic
6
+ * - Increment (0001-core-framework) → JIRA Story (with Epic Link field)
7
+ *
8
+ * This implements the Universal Hierarchy architecture for JIRA.
9
+ */
10
+
11
+ import * as fs from 'fs-extra';
12
+ import * as path from 'path';
13
+ import * as yaml from 'yaml';
14
+ import { JiraClient, JiraIssue, JiraIssueCreate } from '../../../src/integrations/jira/jira-client.js';
15
+
16
+ interface EpicFrontmatter {
17
+ id: string;
18
+ title: string;
19
+ type: 'epic';
20
+ status: 'complete' | 'active' | 'planning' | 'archived';
21
+ priority: string;
22
+ created: string;
23
+ last_updated: string;
24
+ external_tools: {
25
+ github: {
26
+ type: 'milestone';
27
+ id: number | null;
28
+ url: string | null;
29
+ };
30
+ jira: {
31
+ type: 'epic';
32
+ key: string | null;
33
+ url: string | null;
34
+ };
35
+ ado: {
36
+ type: 'feature';
37
+ id: number | null;
38
+ url: string | null;
39
+ };
40
+ };
41
+ increments: Array<{
42
+ id: string;
43
+ status: string;
44
+ external: {
45
+ github: number | null;
46
+ jira: string | null;
47
+ ado: number | null;
48
+ };
49
+ }>;
50
+ total_increments: number;
51
+ completed_increments: number;
52
+ progress: string;
53
+ }
54
+
55
+ interface IncrementFrontmatter {
56
+ id: string;
57
+ epic: string;
58
+ type?: string;
59
+ status?: string;
60
+ external?: {
61
+ github?: {
62
+ issue: number | null;
63
+ url: string | null;
64
+ };
65
+ jira?: {
66
+ story: string | null;
67
+ url: string | null;
68
+ };
69
+ ado?: {
70
+ user_story: number | null;
71
+ url: string | null;
72
+ };
73
+ };
74
+ }
75
+
76
+ export class JiraEpicSync {
77
+ private client: JiraClient;
78
+ private specsDir: string;
79
+ private projectKey: string;
80
+
81
+ constructor(client: JiraClient, specsDir: string, projectKey: string) {
82
+ this.client = client;
83
+ this.specsDir = specsDir;
84
+ this.projectKey = projectKey;
85
+ }
86
+
87
+ /**
88
+ * Sync Epic folder to JIRA (Epic + Stories)
89
+ */
90
+ async syncEpicToJira(epicId: string): Promise<{
91
+ epicKey: string;
92
+ epicUrl: string;
93
+ storiesCreated: number;
94
+ storiesUpdated: number;
95
+ }> {
96
+ console.log(`\n🔄 Syncing Epic ${epicId} to JIRA...`);
97
+
98
+ // 1. Load Epic README.md
99
+ const epicFolder = await this.findEpicFolder(epicId);
100
+ if (!epicFolder) {
101
+ throw new Error(`Epic ${epicId} not found in ${this.specsDir}`);
102
+ }
103
+
104
+ const readmePath = path.join(epicFolder, 'README.md');
105
+ const epicData = await this.parseEpicReadme(readmePath);
106
+
107
+ console.log(` 📦 Epic: ${epicData.title}`);
108
+ console.log(` 📊 Increments: ${epicData.total_increments}`);
109
+
110
+ // 2. Create or update JIRA Epic
111
+ let epicKey = epicData.external_tools.jira.key;
112
+ let epicUrl = epicData.external_tools.jira.url;
113
+
114
+ if (!epicKey) {
115
+ console.log(` 🚀 Creating JIRA Epic...`);
116
+ const epic = await this.createEpic(epicData);
117
+ epicKey = epic.key;
118
+ epicUrl = epic.url;
119
+ console.log(` ✅ Created Epic ${epicKey}`);
120
+
121
+ // Update Epic README with JIRA key
122
+ await this.updateEpicReadme(readmePath, {
123
+ type: 'epic',
124
+ key: epicKey,
125
+ url: epicUrl,
126
+ });
127
+ } else {
128
+ console.log(` ♻️ Updating existing Epic ${epicKey}...`);
129
+ await this.updateEpic(epicKey, epicData);
130
+ console.log(` ✅ Updated Epic ${epicKey}`);
131
+ }
132
+
133
+ // 3. Sync each increment as JIRA Story
134
+ let storiesCreated = 0;
135
+ let storiesUpdated = 0;
136
+
137
+ console.log(`\n 📝 Syncing ${epicData.increments.length} increments...`);
138
+
139
+ for (const increment of epicData.increments) {
140
+ const incrementFile = path.join(epicFolder, `${increment.id}.md`);
141
+ if (!(await fs.pathExists(incrementFile))) {
142
+ console.log(` ⚠️ Increment file not found: ${increment.id}.md`);
143
+ continue;
144
+ }
145
+
146
+ const incrementData = await this.parseIncrementFile(incrementFile);
147
+ const existingStory = increment.external.jira;
148
+
149
+ if (!existingStory) {
150
+ // Create new Story
151
+ const storyKey = await this.createStory(
152
+ epicData.id,
153
+ incrementData,
154
+ epicKey!
155
+ );
156
+ storiesCreated++;
157
+ console.log(` ✅ Created Story ${storyKey} for ${increment.id}`);
158
+
159
+ // Update Epic README and Increment file
160
+ await this.updateIncrementExternalLink(
161
+ readmePath,
162
+ incrementFile,
163
+ increment.id,
164
+ storyKey
165
+ );
166
+ } else {
167
+ // Update existing Story
168
+ await this.updateStory(
169
+ epicData.id,
170
+ existingStory,
171
+ incrementData,
172
+ epicKey!
173
+ );
174
+ storiesUpdated++;
175
+ console.log(` ♻️ Updated Story ${existingStory} for ${increment.id}`);
176
+ }
177
+ }
178
+
179
+ console.log(`\n✅ Epic sync complete!`);
180
+ console.log(` Epic: ${epicUrl}`);
181
+ console.log(` Stories created: ${storiesCreated}`);
182
+ console.log(` Stories updated: ${storiesUpdated}`);
183
+
184
+ return {
185
+ epicKey: epicKey!,
186
+ epicUrl: epicUrl!,
187
+ storiesCreated,
188
+ storiesUpdated,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Find Epic folder by ID (FS-001 or just 001)
194
+ */
195
+ private async findEpicFolder(epicId: string): Promise<string | null> {
196
+ const normalizedId = epicId.startsWith('FS-')
197
+ ? epicId
198
+ : `FS-${epicId.padStart(3, '0')}`;
199
+
200
+ const folders = await fs.readdir(this.specsDir);
201
+ for (const folder of folders) {
202
+ if (folder.startsWith(normalizedId)) {
203
+ return path.join(this.specsDir, folder);
204
+ }
205
+ }
206
+
207
+ return null;
208
+ }
209
+
210
+ /**
211
+ * Parse Epic README.md to extract frontmatter
212
+ */
213
+ private async parseEpicReadme(readmePath: string): Promise<EpicFrontmatter> {
214
+ const content = await fs.readFile(readmePath, 'utf-8');
215
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
216
+
217
+ if (!match) {
218
+ throw new Error('Epic README.md missing YAML frontmatter');
219
+ }
220
+
221
+ const frontmatter = yaml.parse(match[1]) as EpicFrontmatter;
222
+ return frontmatter;
223
+ }
224
+
225
+ /**
226
+ * Parse increment file to extract title and overview
227
+ */
228
+ private async parseIncrementFile(
229
+ filePath: string
230
+ ): Promise<{
231
+ id: string;
232
+ title: string;
233
+ overview: string;
234
+ content: string;
235
+ frontmatter: IncrementFrontmatter;
236
+ }> {
237
+ const content = await fs.readFile(filePath, 'utf-8');
238
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
239
+
240
+ let frontmatter: IncrementFrontmatter = { id: '', epic: '' };
241
+ let bodyContent = content;
242
+
243
+ if (match) {
244
+ frontmatter = yaml.parse(match[1]) as IncrementFrontmatter;
245
+ bodyContent = content.slice(match[0].length).trim();
246
+ }
247
+
248
+ // Extract title
249
+ const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
250
+ const title = titleMatch
251
+ ? titleMatch[1].trim()
252
+ : frontmatter.id || path.basename(filePath, '.md');
253
+
254
+ // Extract overview (first paragraph after title)
255
+ const overviewMatch = bodyContent.match(/^#[^\n]+\n+([^\n]+)/);
256
+ const overview = overviewMatch
257
+ ? overviewMatch[1].trim()
258
+ : 'No overview available';
259
+
260
+ return {
261
+ id: frontmatter.id,
262
+ title,
263
+ overview,
264
+ content: bodyContent,
265
+ frontmatter,
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Create JIRA Epic
271
+ */
272
+ private async createEpic(epic: EpicFrontmatter): Promise<{
273
+ key: string;
274
+ url: string;
275
+ }> {
276
+ const summary = `[${epic.id}] ${epic.title}`;
277
+ const description = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
278
+
279
+ const issueData: JiraIssueCreate = {
280
+ issueType: 'Epic',
281
+ summary,
282
+ description,
283
+ priority: this.mapPriorityToJira(epic.priority),
284
+ labels: ['epic-sync', epic.id.toLowerCase()],
285
+ };
286
+
287
+ const issue = await this.client.createIssue(issueData, this.projectKey);
288
+
289
+ return {
290
+ key: issue.key,
291
+ url: issue.self.replace('/rest/api/3/issue/', '/browse/'),
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Update JIRA Epic
297
+ */
298
+ private async updateEpic(epicKey: string, epic: EpicFrontmatter): Promise<void> {
299
+ const summary = `[${epic.id}] ${epic.title}`;
300
+ const description = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
301
+
302
+ await this.client.updateIssue({
303
+ key: epicKey,
304
+ summary,
305
+ description,
306
+ priority: this.mapPriorityToJira(epic.priority),
307
+ labels: ['epic-sync', epic.id.toLowerCase()],
308
+ });
309
+ }
310
+
311
+ /**
312
+ * Create JIRA Story for increment
313
+ */
314
+ private async createStory(
315
+ epicId: string,
316
+ increment: {
317
+ id: string;
318
+ title: string;
319
+ overview: string;
320
+ content: string;
321
+ },
322
+ epicKey: string
323
+ ): Promise<string> {
324
+ const summary = `[${epicId}] ${increment.title}`;
325
+ const description = `${increment.overview}\n\n---\n\n**Increment**: ${increment.id}\n**Epic**: ${epicId} (${epicKey})\n\n🤖 Auto-created by SpecWeave Epic Sync`;
326
+
327
+ const issueData: JiraIssueCreate = {
328
+ issueType: 'Story',
329
+ summary,
330
+ description,
331
+ epicKey, // Link to Epic via Epic Link field
332
+ labels: ['increment', 'epic-sync'],
333
+ };
334
+
335
+ const issue = await this.client.createIssue(issueData, this.projectKey);
336
+ return issue.key;
337
+ }
338
+
339
+ /**
340
+ * Update JIRA Story for increment
341
+ */
342
+ private async updateStory(
343
+ epicId: string,
344
+ storyKey: string,
345
+ increment: {
346
+ id: string;
347
+ title: string;
348
+ overview: string;
349
+ content: string;
350
+ },
351
+ epicKey: string
352
+ ): Promise<void> {
353
+ const summary = `[${epicId}] ${increment.title}`;
354
+ const description = `${increment.overview}\n\n---\n\n**Increment**: ${increment.id}\n**Epic**: ${epicId} (${epicKey})\n\n🤖 Auto-updated by SpecWeave Epic Sync`;
355
+
356
+ await this.client.updateIssue({
357
+ key: storyKey,
358
+ summary,
359
+ description,
360
+ labels: ['increment', 'epic-sync'],
361
+ });
362
+ }
363
+
364
+ /**
365
+ * Map SpecWeave priority to JIRA priority
366
+ */
367
+ private mapPriorityToJira(priority: string): string {
368
+ const map: Record<string, string> = {
369
+ P0: 'Highest',
370
+ P1: 'High',
371
+ P2: 'Medium',
372
+ P3: 'Low',
373
+ };
374
+ return map[priority] || 'Medium';
375
+ }
376
+
377
+ /**
378
+ * Update Epic README.md with JIRA Epic key
379
+ */
380
+ private async updateEpicReadme(
381
+ readmePath: string,
382
+ jira: { type: 'epic'; key: string; url: string }
383
+ ): Promise<void> {
384
+ const content = await fs.readFile(readmePath, 'utf-8');
385
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
386
+
387
+ if (!match) {
388
+ throw new Error('Epic README.md missing YAML frontmatter');
389
+ }
390
+
391
+ const frontmatter = yaml.parse(match[1]) as EpicFrontmatter;
392
+ frontmatter.external_tools.jira = jira;
393
+
394
+ const newFrontmatter = yaml.stringify(frontmatter);
395
+ const newContent = content.replace(
396
+ /^---\n[\s\S]*?\n---/,
397
+ `---\n${newFrontmatter}---`
398
+ );
399
+
400
+ await fs.writeFile(readmePath, newContent, 'utf-8');
401
+ }
402
+
403
+ /**
404
+ * Update increment external link in both Epic README and increment file
405
+ */
406
+ private async updateIncrementExternalLink(
407
+ readmePath: string,
408
+ incrementFile: string,
409
+ incrementId: string,
410
+ storyKey: string
411
+ ): Promise<void> {
412
+ const storyUrl = `https://${this.client['credentials'].domain}/browse/${storyKey}`;
413
+
414
+ // 1. Update Epic README.md
415
+ const readmeContent = await fs.readFile(readmePath, 'utf-8');
416
+ const readmeMatch = readmeContent.match(/^---\n([\s\S]*?)\n---/);
417
+
418
+ if (readmeMatch) {
419
+ const frontmatter = yaml.parse(readmeMatch[1]) as EpicFrontmatter;
420
+ const increment = frontmatter.increments.find(
421
+ (inc) => inc.id === incrementId
422
+ );
423
+
424
+ if (increment) {
425
+ increment.external.jira = storyKey;
426
+ const newFrontmatter = yaml.stringify(frontmatter);
427
+ const newContent = readmeContent.replace(
428
+ /^---\n[\s\S]*?\n---/,
429
+ `---\n${newFrontmatter}---`
430
+ );
431
+ await fs.writeFile(readmePath, newContent, 'utf-8');
432
+ }
433
+ }
434
+
435
+ // 2. Update increment file frontmatter
436
+ const incrementContent = await fs.readFile(incrementFile, 'utf-8');
437
+ const incrementMatch = incrementContent.match(/^---\n([\s\S]*?)\n---/);
438
+
439
+ if (incrementMatch) {
440
+ const frontmatter = yaml.parse(incrementMatch[1]) as IncrementFrontmatter;
441
+
442
+ if (!frontmatter.external) {
443
+ frontmatter.external = {};
444
+ }
445
+
446
+ frontmatter.external.jira = {
447
+ story: storyKey,
448
+ url: storyUrl,
449
+ };
450
+
451
+ const newFrontmatter = yaml.stringify(frontmatter);
452
+ const newContent = incrementContent.replace(
453
+ /^---\n[\s\S]*?\n---/,
454
+ `---\n${newFrontmatter}---`
455
+ );
456
+ await fs.writeFile(incrementFile, newContent, 'utf-8');
457
+ }
458
+ }
459
+ }
@@ -0,0 +1,79 @@
1
+ import axios from "axios";
2
+ class JiraStatusSync {
3
+ constructor(domain, email, apiToken, projectKey) {
4
+ this.domain = domain;
5
+ this.projectKey = projectKey;
6
+ this.client = axios.create({
7
+ baseURL: `https://${domain}/rest/api/3`,
8
+ auth: {
9
+ username: email,
10
+ password: apiToken
11
+ },
12
+ headers: {
13
+ "Accept": "application/json",
14
+ "Content-Type": "application/json"
15
+ }
16
+ });
17
+ }
18
+ /**
19
+ * Get current status from JIRA issue
20
+ *
21
+ * @param issueKey - JIRA issue key (e.g., PROJ-123)
22
+ * @returns Current issue status
23
+ */
24
+ async getStatus(issueKey) {
25
+ const response = await this.client.get(`/issue/${issueKey}`);
26
+ return {
27
+ state: response.data.fields.status.name
28
+ };
29
+ }
30
+ /**
31
+ * Update JIRA issue status via transitions
32
+ *
33
+ * JIRA requires using transitions to change status.
34
+ * Cannot directly set status field.
35
+ *
36
+ * @param issueKey - JIRA issue key (e.g., PROJ-123)
37
+ * @param status - Desired status
38
+ */
39
+ async updateStatus(issueKey, status) {
40
+ const transitionsResponse = await this.client.get(`/issue/${issueKey}/transitions`);
41
+ const transitions = transitionsResponse.data.transitions;
42
+ const targetTransition = transitions.find(
43
+ (t) => t.to.name === status.state
44
+ );
45
+ if (!targetTransition) {
46
+ throw new Error(
47
+ `Transition to ${status.state} not available for ${issueKey}. Available transitions: ${transitions.map((t) => t.to.name).join(", ")}`
48
+ );
49
+ }
50
+ await this.client.post(`/issue/${issueKey}/transitions`, {
51
+ transition: {
52
+ id: targetTransition.id
53
+ }
54
+ });
55
+ }
56
+ /**
57
+ * Post comment about status change to JIRA issue
58
+ *
59
+ * @param issueKey - JIRA issue key (e.g., PROJ-123)
60
+ * @param oldStatus - Previous SpecWeave status
61
+ * @param newStatus - New SpecWeave status
62
+ */
63
+ async postStatusComment(issueKey, oldStatus, newStatus) {
64
+ const body = `\u{1F504} *Status Update*
65
+
66
+ SpecWeave status changed:
67
+ \u2022 *From*: ${oldStatus}
68
+ \u2022 *To*: ${newStatus}
69
+ \u2022 *When*: ${(/* @__PURE__ */ new Date()).toISOString()}
70
+
71
+ _Synced from SpecWeave_`;
72
+ await this.client.post(`/issue/${issueKey}/comment`, {
73
+ body
74
+ });
75
+ }
76
+ }
77
+ export {
78
+ JiraStatusSync
79
+ };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * JIRA Status Sync
3
+ *
4
+ * Synchronizes SpecWeave increment statuses with JIRA issue statuses.
5
+ *
6
+ * JIRA Status Transitions:
7
+ * - Uses JIRA transitions API to change issue status
8
+ * - Available transitions depend on workflow configuration
9
+ * - Must fetch available transitions before applying
10
+ *
11
+ * @module jira-status-sync
12
+ */
13
+
14
+ import axios, { AxiosInstance } from 'axios';
15
+
16
+ /**
17
+ * External status representation (JIRA-specific)
18
+ */
19
+ export interface ExternalStatus {
20
+ state: string; // e.g., "To Do", "In Progress", "Done"
21
+ labels?: string[]; // Optional labels (JIRA supports labels)
22
+ }
23
+
24
+ /**
25
+ * JIRA transition representation
26
+ */
27
+ interface JiraTransition {
28
+ id: string;
29
+ name: string;
30
+ to: {
31
+ name: string;
32
+ };
33
+ }
34
+
35
+ /**
36
+ * JIRA Status Sync
37
+ *
38
+ * Handles status synchronization with JIRA issues.
39
+ */
40
+ export class JiraStatusSync {
41
+ private client: AxiosInstance;
42
+ private domain: string;
43
+ private projectKey: string;
44
+
45
+ constructor(
46
+ domain: string,
47
+ email: string,
48
+ apiToken: string,
49
+ projectKey: string
50
+ ) {
51
+ this.domain = domain;
52
+ this.projectKey = projectKey;
53
+
54
+ // Create JIRA API client
55
+ this.client = axios.create({
56
+ baseURL: `https://${domain}/rest/api/3`,
57
+ auth: {
58
+ username: email,
59
+ password: apiToken
60
+ },
61
+ headers: {
62
+ 'Accept': 'application/json',
63
+ 'Content-Type': 'application/json'
64
+ }
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Get current status from JIRA issue
70
+ *
71
+ * @param issueKey - JIRA issue key (e.g., PROJ-123)
72
+ * @returns Current issue status
73
+ */
74
+ async getStatus(issueKey: string): Promise<ExternalStatus> {
75
+ const response = await this.client.get(`/issue/${issueKey}`);
76
+
77
+ return {
78
+ state: response.data.fields.status.name
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Update JIRA issue status via transitions
84
+ *
85
+ * JIRA requires using transitions to change status.
86
+ * Cannot directly set status field.
87
+ *
88
+ * @param issueKey - JIRA issue key (e.g., PROJ-123)
89
+ * @param status - Desired status
90
+ */
91
+ async updateStatus(issueKey: string, status: ExternalStatus): Promise<void> {
92
+ // 1. Get available transitions for this issue
93
+ const transitionsResponse = await this.client.get(`/issue/${issueKey}/transitions`);
94
+ const transitions: JiraTransition[] = transitionsResponse.data.transitions;
95
+
96
+ // 2. Find transition that leads to desired status
97
+ const targetTransition = transitions.find(
98
+ (t) => t.to.name === status.state
99
+ );
100
+
101
+ if (!targetTransition) {
102
+ throw new Error(
103
+ `Transition to ${status.state} not available for ${issueKey}. ` +
104
+ `Available transitions: ${transitions.map(t => t.to.name).join(', ')}`
105
+ );
106
+ }
107
+
108
+ // 3. Execute transition
109
+ await this.client.post(`/issue/${issueKey}/transitions`, {
110
+ transition: {
111
+ id: targetTransition.id
112
+ }
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Post comment about status change to JIRA issue
118
+ *
119
+ * @param issueKey - JIRA issue key (e.g., PROJ-123)
120
+ * @param oldStatus - Previous SpecWeave status
121
+ * @param newStatus - New SpecWeave status
122
+ */
123
+ async postStatusComment(
124
+ issueKey: string,
125
+ oldStatus: string,
126
+ newStatus: string
127
+ ): Promise<void> {
128
+ const body = `🔄 *Status Update*\n\n` +
129
+ `SpecWeave status changed:\n` +
130
+ `• *From*: ${oldStatus}\n` +
131
+ `• *To*: ${newStatus}\n` +
132
+ `• *When*: ${new Date().toISOString()}\n\n` +
133
+ `_Synced from SpecWeave_`;
134
+
135
+ await this.client.post(`/issue/${issueKey}/comment`, {
136
+ body
137
+ });
138
+ }
139
+ }