specweave 0.17.15 → 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.
- package/CLAUDE.md +405 -2495
- package/README.md +92 -2
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +188 -36
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.js +25 -13
- package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.js +451 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
- package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
- package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
- package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
- package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
- package/dist/src/core/living-docs/index.d.ts +10 -84
- package/dist/src/core/living-docs/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/index.js +10 -164
- package/dist/src/core/living-docs/index.js.map +1 -1
- package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
- package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
- package/dist/src/core/living-docs/spec-distributor.js +823 -0
- package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
- package/dist/src/core/living-docs/types.d.ts +201 -0
- package/dist/src/core/living-docs/types.d.ts.map +1 -0
- package/dist/src/core/living-docs/types.js +15 -0
- package/dist/src/core/living-docs/types.js.map +1 -0
- package/dist/src/core/logging/prompt-logger.d.ts +70 -0
- package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
- package/dist/src/core/logging/prompt-logger.js +247 -0
- package/dist/src/core/logging/prompt-logger.js.map +1 -0
- package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
- package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-manager.js +33 -70
- package/dist/src/core/status-line/status-line-manager.js.map +1 -1
- package/dist/src/core/status-line/types.d.ts +19 -31
- package/dist/src/core/status-line/types.d.ts.map +1 -1
- package/dist/src/core/status-line/types.js +5 -9
- package/dist/src/core/status-line/types.js.map +1 -1
- package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
- package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
- package/dist/src/core/sync/conflict-resolver.js +108 -0
- package/dist/src/core/sync/conflict-resolver.js.map +1 -0
- package/dist/src/core/sync/enhanced-content-builder.d.ts +77 -0
- package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
- package/dist/src/core/sync/enhanced-content-builder.js +199 -0
- package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
- package/dist/src/core/sync/label-detector.d.ts +66 -0
- package/dist/src/core/sync/label-detector.d.ts.map +1 -0
- package/dist/src/core/sync/label-detector.js +211 -0
- package/dist/src/core/sync/label-detector.js.map +1 -0
- package/dist/src/core/sync/retry-logic.d.ts +64 -0
- package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
- package/dist/src/core/sync/retry-logic.js +165 -0
- package/dist/src/core/sync/retry-logic.js.map +1 -0
- package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
- package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
- package/dist/src/core/sync/spec-increment-mapper.js +424 -0
- package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
- package/dist/src/core/sync/status-cache.d.ts +91 -0
- package/dist/src/core/sync/status-cache.d.ts.map +1 -0
- package/dist/src/core/sync/status-cache.js +140 -0
- package/dist/src/core/sync/status-cache.js.map +1 -0
- package/dist/src/core/sync/status-mapper.d.ts +69 -0
- package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
- package/dist/src/core/sync/status-mapper.js +90 -0
- package/dist/src/core/sync/status-mapper.js.map +1 -0
- package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
- package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
- package/dist/src/core/sync/status-sync-engine.js +347 -0
- package/dist/src/core/sync/status-sync-engine.js.map +1 -0
- package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
- package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
- package/dist/src/core/sync/sync-event-logger.js +103 -0
- package/dist/src/core/sync/sync-event-logger.js.map +1 -0
- package/dist/src/core/sync/workflow-detector.d.ts +95 -0
- package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
- package/dist/src/core/sync/workflow-detector.js +175 -0
- package/dist/src/core/sync/workflow-detector.js.map +1 -0
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js +31 -0
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/utils/github-url.d.ts +53 -0
- package/dist/src/utils/github-url.d.ts.map +1 -0
- package/dist/src/utils/github-url.js +90 -0
- package/dist/src/utils/github-url.js.map +1 -0
- package/dist/src/utils/plugin-validator.d.ts +9 -0
- package/dist/src/utils/plugin-validator.d.ts.map +1 -1
- package/dist/src/utils/plugin-validator.js +86 -19
- package/dist/src/utils/plugin-validator.js.map +1 -1
- package/dist/src/utils/spec-parser.d.ts +145 -0
- package/dist/src/utils/spec-parser.d.ts.map +1 -0
- package/dist/src/utils/spec-parser.js +640 -0
- package/dist/src/utils/spec-parser.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/agents/pm/AGENT.md +1 -1
- package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
- package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
- package/plugins/specweave/commands/specweave-done.md +163 -0
- package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
- package/plugins/specweave/hooks/post-increment-planning.sh +107 -35
- package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
- package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
- package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
- package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
- package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
- package/plugins/specweave/skills/plugin-validator/SKILL.md +16 -13
- package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
- package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
- package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
- package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
- package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
- package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
- package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
- package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
- package/plugins/specweave-github/lib/github-client.js +21 -10
- package/plugins/specweave-github/lib/github-client.ts +27 -16
- package/plugins/specweave-github/lib/github-epic-sync.js +489 -0
- package/plugins/specweave-github/lib/github-epic-sync.ts +690 -0
- package/plugins/specweave-github/lib/github-status-sync.js +71 -0
- package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
- package/plugins/specweave-github/lib/task-sync.js +33 -2
- package/plugins/specweave-github/lib/task-sync.ts +44 -2
- package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.ts.disabled +222 -0
- package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
- package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
- package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
- package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
- package/src/templates/AGENTS.md.template +88 -1
- package/src/templates/CLAUDE.md.template +49 -0
- package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
- 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
|
+
}
|