specweave 1.0.255 → 1.0.256

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 (92) hide show
  1. package/CLAUDE.md +24 -24
  2. package/README.md +138 -203
  3. package/dist/src/core/ac-checkbox-formatter.d.ts +24 -0
  4. package/dist/src/core/ac-checkbox-formatter.d.ts.map +1 -0
  5. package/dist/src/core/ac-checkbox-formatter.js +35 -0
  6. package/dist/src/core/ac-checkbox-formatter.js.map +1 -0
  7. package/dist/src/core/ac-progress-sync.d.ts +116 -0
  8. package/dist/src/core/ac-progress-sync.d.ts.map +1 -0
  9. package/dist/src/core/ac-progress-sync.js +272 -0
  10. package/dist/src/core/ac-progress-sync.js.map +1 -0
  11. package/dist/src/core/fabric/registry-schema.d.ts +79 -0
  12. package/dist/src/core/fabric/registry-schema.d.ts.map +1 -0
  13. package/dist/src/core/fabric/registry-schema.js +6 -0
  14. package/dist/src/core/fabric/registry-schema.js.map +1 -0
  15. package/dist/src/core/fabric/security-scanner.d.ts +12 -0
  16. package/dist/src/core/fabric/security-scanner.d.ts.map +1 -0
  17. package/dist/src/core/fabric/security-scanner.js +219 -0
  18. package/dist/src/core/fabric/security-scanner.js.map +1 -0
  19. package/dist/src/core/types/sync-profile.d.ts +44 -0
  20. package/dist/src/core/types/sync-profile.d.ts.map +1 -1
  21. package/dist/src/core/types/sync-profile.js.map +1 -1
  22. package/package.json +1 -1
  23. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +4 -4
  24. package/plugins/{specweave-github/hooks/github-ac-sync-handler.sh → specweave/hooks/v2/handlers/ac-sync-dispatcher.sh} +96 -92
  25. package/plugins/specweave/skills/architect/SKILL.md +1 -1
  26. package/plugins/specweave/skills/auto/SKILL.md +1 -1
  27. package/plugins/specweave/skills/cancel-auto/SKILL.md +1 -1
  28. package/plugins/specweave/skills/code-simplifier/SKILL.md +1 -1
  29. package/plugins/specweave/skills/do/SKILL.md +1 -1
  30. package/plugins/specweave/skills/docs/SKILL.md +1 -1
  31. package/plugins/specweave/skills/docs-updater/SKILL.md +1 -1
  32. package/plugins/specweave/skills/done/SKILL.md +13 -70
  33. package/plugins/specweave/skills/framework/SKILL.md +1 -1
  34. package/plugins/specweave/skills/grill/SKILL.md +1 -1
  35. package/plugins/specweave/skills/increment/SKILL.md +1 -1
  36. package/plugins/specweave/skills/increment-planner/SKILL.md +1 -1
  37. package/plugins/specweave/skills/lsp/SKILL.md +1 -1
  38. package/plugins/specweave/skills/pm/SKILL.md +1 -1
  39. package/plugins/specweave/skills/progress/SKILL.md +1 -1
  40. package/plugins/specweave/skills/save/SKILL.md +1 -1
  41. package/plugins/specweave/skills/security/SKILL.md +1 -1
  42. package/plugins/specweave/skills/security-patterns/SKILL.md +1 -1
  43. package/plugins/specweave/skills/tdd-cycle/SKILL.md +1 -1
  44. package/plugins/specweave/skills/tdd-green/SKILL.md +1 -1
  45. package/plugins/specweave/skills/tdd-orchestrator/SKILL.md +1 -1
  46. package/plugins/specweave/skills/tdd-red/SKILL.md +1 -1
  47. package/plugins/specweave/skills/validate/SKILL.md +1 -1
  48. package/plugins/specweave-github/commands/sync.md +1 -22
  49. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts +0 -205
  50. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts.map +0 -1
  51. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js +0 -685
  52. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js.map +0 -1
  53. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts +0 -12
  54. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts.map +0 -1
  55. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.js +0 -28
  56. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.js.map +0 -1
  57. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +0 -21
  58. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +0 -1
  59. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +0 -471
  60. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +0 -1
  61. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +0 -53
  62. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +0 -1
  63. package/dist/plugins/specweave-github/lib/github-status-sync.js +0 -120
  64. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +0 -1
  65. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.d.ts +0 -18
  66. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.d.ts.map +0 -1
  67. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.js +0 -297
  68. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.js.map +0 -1
  69. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +0 -94
  70. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +0 -1
  71. package/dist/plugins/specweave-github/lib/increment-issue-builder.js +0 -385
  72. package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +0 -1
  73. package/plugins/specweave-github/lib/ThreeLayerSyncManager.js +0 -611
  74. package/plugins/specweave-github/lib/ThreeLayerSyncManager.ts +0 -909
  75. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.js +0 -1
  76. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts +0 -12
  77. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts.map +0 -1
  78. package/plugins/specweave-github/lib/cli-sync-increment-changes.js +0 -17
  79. package/plugins/specweave-github/lib/cli-sync-increment-changes.js.map +0 -1
  80. package/plugins/specweave-github/lib/cli-sync-increment-changes.ts +0 -33
  81. package/plugins/specweave-github/lib/github-increment-sync-cli.js +0 -474
  82. package/plugins/specweave-github/lib/github-increment-sync-cli.ts +0 -616
  83. package/plugins/specweave-github/lib/github-status-sync.js +0 -107
  84. package/plugins/specweave-github/lib/github-status-sync.ts +0 -163
  85. package/plugins/specweave-github/lib/github-sync-increment-changes.d.js +0 -0
  86. package/plugins/specweave-github/lib/github-sync-increment-changes.d.ts +0 -18
  87. package/plugins/specweave-github/lib/github-sync-increment-changes.d.ts.map +0 -1
  88. package/plugins/specweave-github/lib/github-sync-increment-changes.js +0 -253
  89. package/plugins/specweave-github/lib/github-sync-increment-changes.js.map +0 -1
  90. package/plugins/specweave-github/lib/github-sync-increment-changes.ts +0 -391
  91. package/plugins/specweave-github/lib/increment-issue-builder.js +0 -402
  92. package/plugins/specweave-github/lib/increment-issue-builder.ts +0 -520
@@ -1,520 +0,0 @@
1
- /**
2
- * Increment Issue Builder - Generate GitHub issues from increment spec.md
3
- *
4
- * For brownfield projects without full living docs structure,
5
- * this builder extracts User Stories and ACs directly from spec.md
6
- * and generates properly formatted GitHub issues.
7
- *
8
- * Correct format: [FS-XXX][US-YYY] User Story Title
9
- * With ACs as checkable items in issue body.
10
- *
11
- * @see ADR-0143 (GitHub Issue Format)
12
- */
13
-
14
- import { readFile } from 'fs/promises';
15
- import { existsSync } from 'fs';
16
- import * as path from 'path';
17
- import * as yaml from 'yaml';
18
-
19
- export interface IncrementFrontmatter {
20
- increment: string;
21
- feature_id?: string;
22
- type?: string;
23
- status?: string;
24
- created?: string;
25
- priority?: string;
26
- }
27
-
28
- export interface AcceptanceCriterion {
29
- id: string;
30
- description: string;
31
- completed: boolean;
32
- }
33
-
34
- export interface UserStory {
35
- id: string;
36
- title: string;
37
- asA: string;
38
- iWant: string;
39
- soThat: string;
40
- acceptanceCriteria: AcceptanceCriterion[];
41
- priority?: string;
42
- }
43
-
44
- export interface Task {
45
- id: string;
46
- title: string;
47
- completed: boolean;
48
- userStories: string[];
49
- priority?: string;
50
- description?: string;
51
- }
52
-
53
- export interface IncrementData {
54
- frontmatter: IncrementFrontmatter;
55
- title: string;
56
- problemStatement: string;
57
- userStories: UserStory[];
58
- tasks: Task[];
59
- outOfScope: string[];
60
- }
61
-
62
- export interface GitHubIssueContent {
63
- title: string;
64
- body: string;
65
- labels: string[];
66
- }
67
-
68
- export class IncrementIssueBuilder {
69
- private incrementPath: string;
70
- private projectRoot: string;
71
-
72
- constructor(incrementPath: string, projectRoot: string) {
73
- this.incrementPath = incrementPath;
74
- this.projectRoot = projectRoot;
75
- }
76
-
77
- /**
78
- * Parse increment spec.md and extract all data
79
- */
80
- async parse(): Promise<IncrementData> {
81
- const specPath = path.join(this.incrementPath, 'spec.md');
82
-
83
- if (!existsSync(specPath)) {
84
- throw new Error(`spec.md not found at ${specPath}`);
85
- }
86
-
87
- const content = await readFile(specPath, 'utf-8');
88
-
89
- // Extract YAML frontmatter
90
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
91
- if (!frontmatterMatch) {
92
- throw new Error('spec.md missing YAML frontmatter');
93
- }
94
-
95
- const frontmatter = yaml.parse(frontmatterMatch[1]) as IncrementFrontmatter;
96
- const bodyContent = content.slice(frontmatterMatch[0].length).trim();
97
-
98
- // Extract main title
99
- const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
100
- const title = titleMatch ? titleMatch[1].trim() : frontmatter.increment;
101
-
102
- // Extract problem statement
103
- const problemStatement = this.extractSection(bodyContent, 'Problem Statement');
104
-
105
- // Extract user stories
106
- const userStories = this.extractUserStories(bodyContent);
107
-
108
- // Extract out of scope
109
- const outOfScopeSection = this.extractSection(bodyContent, 'Out of Scope');
110
- const outOfScope = outOfScopeSection
111
- .split('\n')
112
- .filter(line => line.startsWith('-'))
113
- .map(line => line.replace(/^-\s*/, '').trim());
114
-
115
- // Extract tasks from tasks.md
116
- const tasks = await this.extractTasks();
117
-
118
- return {
119
- frontmatter,
120
- title,
121
- problemStatement,
122
- userStories,
123
- tasks,
124
- outOfScope,
125
- };
126
- }
127
-
128
- /**
129
- * Extract tasks from tasks.md
130
- */
131
- private async extractTasks(): Promise<Task[]> {
132
- const tasksPath = path.join(this.incrementPath, 'tasks.md');
133
-
134
- if (!existsSync(tasksPath)) {
135
- return [];
136
- }
137
-
138
- try {
139
- const content = await readFile(tasksPath, 'utf-8');
140
- const tasks: Task[] = [];
141
-
142
- // Split by task headers: ### T-XXX: Title
143
- const taskBlocks = content.split(/(?=###\s+T-\d+)/);
144
-
145
- for (const block of taskBlocks) {
146
- const headerMatch = block.match(/###\s+(T-\d+):\s*(.+)/);
147
- if (!headerMatch) continue;
148
-
149
- const id = headerMatch[1];
150
- const title = headerMatch[2].trim();
151
-
152
- // Extract status: **Status**: [x] completed or **Status**: [ ] pending
153
- const statusMatch = block.match(/\*\*Status\*\*:\s*\[([x\s])\]/i);
154
- const completed = statusMatch ? statusMatch[1].toLowerCase() === 'x' : false;
155
-
156
- // Extract user stories: **User Story**: US-001 or **Satisfies ACs**: AC-US1-01
157
- const userStoryMatch = block.match(/\*\*User Story\*\*:\s*([^\n]+)/i);
158
- const satisfiesMatch = block.match(/\*\*Satisfies ACs?\*\*:\s*([^\n]+)/i);
159
-
160
- let userStories: string[] = [];
161
- if (userStoryMatch) {
162
- userStories = userStoryMatch[1].split(',').map(s => s.trim());
163
- } else if (satisfiesMatch) {
164
- // Extract US IDs from AC IDs (AC-US1-01 → US-001)
165
- const acIds = satisfiesMatch[1].split(',').map(s => s.trim());
166
- const usIds = new Set<string>();
167
- for (const ac of acIds) {
168
- const usMatch = ac.match(/AC-(US\d+)-/i);
169
- if (usMatch) {
170
- usIds.add(usMatch[1]);
171
- }
172
- }
173
- userStories = Array.from(usIds);
174
- }
175
-
176
- // Extract priority
177
- const priorityMatch = block.match(/\*\*Priority\*\*:\s*(P\d)/i);
178
- const priority = priorityMatch ? priorityMatch[1] : undefined;
179
-
180
- tasks.push({
181
- id,
182
- title,
183
- completed,
184
- userStories,
185
- priority,
186
- });
187
- }
188
-
189
- return tasks;
190
- } catch {
191
- return [];
192
- }
193
- }
194
-
195
- /**
196
- * Extract a section by heading
197
- */
198
- private extractSection(content: string, heading: string): string {
199
- const regex = new RegExp(`##\\s+${heading}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, 'i');
200
- const match = content.match(regex);
201
- return match ? match[1].trim() : '';
202
- }
203
-
204
- /**
205
- * Extract user stories from spec.md
206
- */
207
- private extractUserStories(content: string): UserStory[] {
208
- const stories: UserStory[] = [];
209
-
210
- // Find User Stories section
211
- const userStoriesSection = this.extractSection(content, 'User Stories');
212
- if (!userStoriesSection) {
213
- return stories;
214
- }
215
-
216
- // Split by ### US-XXX headers
217
- const storyBlocks = userStoriesSection.split(/(?=###\s+US-\d+)/);
218
-
219
- for (const block of storyBlocks) {
220
- const headerMatch = block.match(/###\s+(US-\d+):\s*(.+)/);
221
- if (!headerMatch) continue;
222
-
223
- const id = headerMatch[1];
224
- const title = headerMatch[2].trim();
225
-
226
- // Extract As a/I want/So that
227
- const asAMatch = block.match(/\*\*As a\*\*\s+([^\n]+)/i);
228
- const iWantMatch = block.match(/\*\*I want\*\*\s+([^\n]+)/i);
229
- const soThatMatch = block.match(/\*\*So that\*\*\s+([^\n]+)/i);
230
-
231
- // Extract Acceptance Criteria
232
- const acceptanceCriteria = this.extractAcceptanceCriteria(block, id);
233
-
234
- stories.push({
235
- id,
236
- title,
237
- asA: asAMatch ? asAMatch[1].trim().replace(/,$/, '') : '',
238
- iWant: iWantMatch ? iWantMatch[1].trim().replace(/,$/, '') : '',
239
- soThat: soThatMatch ? soThatMatch[1].trim().replace(/\.$/, '') : '',
240
- acceptanceCriteria,
241
- });
242
- }
243
-
244
- return stories;
245
- }
246
-
247
- /**
248
- * Extract acceptance criteria from a user story block
249
- */
250
- private extractAcceptanceCriteria(block: string, userStoryId: string): AcceptanceCriterion[] {
251
- const criteria: AcceptanceCriterion[] = [];
252
-
253
- // Pattern: - [x] **AC-US1-01**: Description or - [ ] **AC-US1-01**: Description
254
- const acPattern = /-\s*\[([ x])\]\s*\*\*AC-(US\d+)-(\d+)\*\*:\s*(.+)/gi;
255
-
256
- let match;
257
- while ((match = acPattern.exec(block)) !== null) {
258
- const completed = match[1].toLowerCase() === 'x';
259
- const usNum = match[2];
260
- const acNum = match[3];
261
- const description = match[4].trim();
262
-
263
- criteria.push({
264
- id: `AC-${usNum}-${acNum}`,
265
- description,
266
- completed,
267
- });
268
- }
269
-
270
- return criteria;
271
- }
272
-
273
- /**
274
- * Build GitHub issue for a single user story
275
- */
276
- buildUserStoryIssue(
277
- story: UserStory,
278
- incrementData: IncrementData,
279
- githubRepo?: string
280
- ): GitHubIssueContent {
281
- const featureId = incrementData.frontmatter.feature_id || this.generateFeatureId(incrementData);
282
- const incrementId = incrementData.frontmatter.increment;
283
-
284
- // Title format: [FS-XXX][US-YYY] User Story Title
285
- const title = `[${featureId}][${story.id}] ${story.title}`;
286
-
287
- // Build body
288
- let body = '';
289
-
290
- // ❌ REMOVED: Metadata header (Feature, Status, Priority)
291
- // WHY: GitHub has NATIVE fields for this (labels, milestones)
292
- // Body should contain ONLY actual work content (ACs, tasks, user story)
293
- // See: .specweave/docs/internal/troubleshooting/CRITICAL-remove-metadata-header-from-github-issues.md
294
-
295
- // Progress section (consistency with user-story-issue-builder.ts)
296
- const completedACs = story.acceptanceCriteria.filter(ac => ac.completed).length;
297
- const totalACs = story.acceptanceCriteria.length;
298
- const storyTasks = incrementData.tasks.filter(t =>
299
- t.userStories.includes(story.id) ||
300
- t.userStories.some(us => us.toUpperCase() === story.id.toUpperCase())
301
- );
302
- const completedTasks = storyTasks.filter(t => t.completed).length;
303
- const totalTasks = storyTasks.length;
304
- const overallPercentage = (totalACs + totalTasks) > 0
305
- ? Math.round(((completedACs + completedTasks) / (totalACs + totalTasks)) * 100)
306
- : 0;
307
-
308
- body += `## Progress\n\n`;
309
- body += `**Acceptance Criteria**: ${completedACs}/${totalACs} (${totalACs > 0 ? Math.round((completedACs / totalACs) * 100) : 0}%)\n`;
310
- body += `**Tasks**: ${completedTasks}/${totalTasks} (${totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0}%)\n`;
311
- body += `**Overall**: ${overallPercentage}%\n\n`;
312
-
313
- // Progress bar
314
- const filledBlocks = Math.floor(overallPercentage / 5);
315
- const emptyBlocks = 20 - filledBlocks;
316
- body += `${'█'.repeat(filledBlocks)}${'░'.repeat(emptyBlocks)} ${overallPercentage}%\n\n`;
317
- body += `---\n\n`;
318
-
319
- // User Story description
320
- body += `## User Story\n\n`;
321
- if (story.asA && story.iWant && story.soThat) {
322
- body += `**As a** ${story.asA}\n`;
323
- body += `**I want** ${story.iWant}\n`;
324
- body += `**So that** ${story.soThat}\n\n`;
325
- } else {
326
- body += `${story.title}\n\n`;
327
- }
328
-
329
- body += `---\n\n`;
330
-
331
- // Acceptance Criteria (checkable!)
332
- body += `## Acceptance Criteria\n\n`;
333
- if (story.acceptanceCriteria.length > 0) {
334
- const completed = story.acceptanceCriteria.filter(ac => ac.completed).length;
335
- const total = story.acceptanceCriteria.length;
336
- const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
337
- body += `Progress: ${completed}/${total} criteria met (${percentage}%)\n\n`;
338
-
339
- for (const ac of story.acceptanceCriteria) {
340
- const checkbox = ac.completed ? '[x]' : '[ ]';
341
- body += `- ${checkbox} **${ac.id}**: ${ac.description}\n`;
342
- }
343
- body += '\n';
344
- } else {
345
- body += `*No acceptance criteria defined*\n\n`;
346
- }
347
-
348
- body += `---\n\n`;
349
-
350
- // Tasks for this user story (already calculated above for Progress section)
351
- if (storyTasks.length > 0) {
352
- body += `## Tasks\n\n`;
353
- const completedTasks = storyTasks.filter(t => t.completed).length;
354
- body += `Progress: ${completedTasks}/${storyTasks.length} tasks\n\n`;
355
-
356
- for (const task of storyTasks) {
357
- const checkbox = task.completed ? '[x]' : '[ ]';
358
- body += `- ${checkbox} **${task.id}**: ${task.title}\n`;
359
- }
360
- body += '\n---\n\n';
361
- }
362
-
363
- // Link to increment
364
- body += `## Implementation\n\n`;
365
- if (githubRepo) {
366
- body += `**Increment**: [${incrementId}](https://github.com/${githubRepo}/tree/develop/.specweave/increments/${incrementId})\n\n`;
367
- } else {
368
- body += `**Increment**: ${incrementId}\n\n`;
369
- }
370
-
371
- body += `---\n\n`;
372
- body += `🤖 Auto-synced by SpecWeave Increment Sync`;
373
-
374
- // Labels
375
- const labels = ['specweave', 'user-story'];
376
-
377
- // Add type label if available
378
- if (incrementData.frontmatter.type) {
379
- labels.push(incrementData.frontmatter.type.toLowerCase());
380
- }
381
-
382
- // Add priority label (from story or default to p2)
383
- const priority = story.priority?.toLowerCase() || incrementData.frontmatter.priority?.toLowerCase() || 'p2';
384
- labels.push(priority);
385
-
386
- return { title, body, labels };
387
- }
388
-
389
- /**
390
- * Build GitHub issue for the entire increment (epic-style)
391
- */
392
- buildIncrementIssue(
393
- incrementData: IncrementData,
394
- githubRepo?: string
395
- ): GitHubIssueContent {
396
- const featureId = incrementData.frontmatter.feature_id || this.generateFeatureId(incrementData);
397
- const incrementId = incrementData.frontmatter.increment;
398
-
399
- // Title format: [FS-XXX] Increment Title
400
- const title = `[${featureId}] ${incrementData.title}`;
401
-
402
- // Build body
403
- let body = '';
404
-
405
- // Header with metadata
406
- body += `**Increment**: ${incrementId}\n`;
407
- body += `**Status**: ${incrementData.frontmatter.status || 'planning'}\n`;
408
- body += `**Priority**: P0 (Critical)\n`;
409
-
410
- // Calculate progress
411
- const totalACs = incrementData.userStories.reduce((sum, us) => sum + us.acceptanceCriteria.length, 0);
412
- const completedACs = incrementData.userStories.reduce(
413
- (sum, us) => sum + us.acceptanceCriteria.filter(ac => ac.completed).length, 0
414
- );
415
- const percentage = totalACs > 0 ? Math.round((completedACs / totalACs) * 100) : 0;
416
- body += `**Progress**: ${completedACs}/${totalACs} ACs (${percentage}%)\n`;
417
-
418
- body += `\n---\n\n`;
419
-
420
- // Overview
421
- body += `## Overview\n\n`;
422
- body += incrementData.problemStatement || incrementData.title;
423
- body += `\n\n---\n\n`;
424
-
425
- // User Stories with ACs
426
- body += `## User Stories\n\n`;
427
- for (const story of incrementData.userStories) {
428
- const usCompleted = story.acceptanceCriteria.filter(ac => ac.completed).length;
429
- const usTotal = story.acceptanceCriteria.length;
430
- const usCheckbox = usCompleted === usTotal && usTotal > 0 ? '[x]' : '[ ]';
431
-
432
- body += `### ${usCheckbox} ${story.id}: ${story.title}\n\n`;
433
-
434
- if (story.asA && story.iWant && story.soThat) {
435
- body += `> **As a** ${story.asA}, **I want** ${story.iWant}, **So that** ${story.soThat}\n\n`;
436
- }
437
-
438
- body += `**Acceptance Criteria:**\n`;
439
- for (const ac of story.acceptanceCriteria) {
440
- const checkbox = ac.completed ? '[x]' : '[ ]';
441
- body += `- ${checkbox} **${ac.id}**: ${ac.description}\n`;
442
- }
443
- body += '\n';
444
- }
445
-
446
- body += `---\n\n`;
447
-
448
- // Tasks section
449
- if (incrementData.tasks.length > 0) {
450
- body += `## Tasks\n\n`;
451
- const completedTasks = incrementData.tasks.filter(t => t.completed).length;
452
- const totalTasks = incrementData.tasks.length;
453
- const taskPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
454
- body += `Progress: ${completedTasks}/${totalTasks} tasks (${taskPercentage}%)\n\n`;
455
-
456
- for (const task of incrementData.tasks) {
457
- const checkbox = task.completed ? '[x]' : '[ ]';
458
- body += `- ${checkbox} **${task.id}**: ${task.title}\n`;
459
- if (task.priority || task.userStories.length > 0) {
460
- const parts: string[] = [];
461
- if (task.priority) parts.push(`Priority: ${task.priority}`);
462
- if (task.userStories.length > 0) parts.push(`User ${task.userStories.length === 1 ? 'Story' : 'Stories'}: ${task.userStories.join(', ')}`);
463
- body += ` - ${parts.join(' | ')}\n`;
464
- }
465
- }
466
- body += '\n---\n\n';
467
- }
468
-
469
- // Links to increment files
470
- body += `## SpecWeave Increment\n\n`;
471
- if (githubRepo) {
472
- body += `- **Spec**: [\`spec.md\`](https://github.com/${githubRepo}/blob/develop/.specweave/increments/${incrementId}/spec.md)\n`;
473
- body += `- **Plan**: [\`plan.md\`](https://github.com/${githubRepo}/blob/develop/.specweave/increments/${incrementId}/plan.md)\n`;
474
- body += `- **Tasks**: [\`tasks.md\`](https://github.com/${githubRepo}/blob/develop/.specweave/increments/${incrementId}/tasks.md)\n`;
475
- } else {
476
- body += `- **Spec**: \`spec.md\`\n`;
477
- body += `- **Plan**: \`plan.md\`\n`;
478
- body += `- **Tasks**: \`tasks.md\`\n`;
479
- }
480
-
481
- body += `\n---\n\n`;
482
- body += `🤖 Auto-synced by SpecWeave Increment Sync`;
483
-
484
- // Labels
485
- const labels = ['specweave', 'increment'];
486
-
487
- // Add type label (default to 'enhancement' if not specified)
488
- const typeLabel = incrementData.frontmatter.type?.toLowerCase() || 'enhancement';
489
- labels.push(typeLabel);
490
-
491
- // Add priority label (default to 'p2' if not specified)
492
- const priority = incrementData.frontmatter.priority?.toLowerCase() || 'p2';
493
- labels.push(priority);
494
-
495
- return { title, body, labels };
496
- }
497
-
498
- /**
499
- * Generate a feature ID if not present in frontmatter
500
- * Uses date-based format: FS-YY-MM-DD
501
- */
502
- private generateFeatureId(incrementData: IncrementData): string {
503
- // Try to extract increment number
504
- const incrementNum = incrementData.frontmatter.increment.match(/^(\d+)/)?.[1];
505
- if (incrementNum) {
506
- return `FS-${incrementNum.padStart(3, '0')}`;
507
- }
508
-
509
- // Fallback to date-based
510
- const created = incrementData.frontmatter.created;
511
- if (created) {
512
- const match = created.match(/^(\d{4})-(\d{2})-(\d{2})/);
513
- if (match) {
514
- return `FS-${match[1].slice(2)}-${match[2]}-${match[3]}`;
515
- }
516
- }
517
-
518
- return 'FS-UNKNOWN';
519
- }
520
- }