specweave 0.18.0 → 0.18.1

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 (69) hide show
  1. package/dist/locales/de/.gitkeep +0 -0
  2. package/dist/locales/de/cli.json +108 -0
  3. package/dist/locales/en/cli.json +287 -0
  4. package/dist/locales/en/errors.json +7 -0
  5. package/dist/locales/en/templates.json +6 -0
  6. package/dist/locales/es/.gitkeep +0 -0
  7. package/dist/locales/es/cli.json +41 -0
  8. package/dist/locales/fr/.gitkeep +0 -0
  9. package/dist/locales/fr/cli.json +108 -0
  10. package/dist/locales/ja/.gitkeep +0 -0
  11. package/dist/locales/ja/cli.json +108 -0
  12. package/dist/locales/ko/.gitkeep +0 -0
  13. package/dist/locales/ko/cli.json +108 -0
  14. package/dist/locales/pt/.gitkeep +0 -0
  15. package/dist/locales/pt/cli.json +108 -0
  16. package/dist/locales/ru/.gitkeep +0 -0
  17. package/dist/locales/ru/cli.json +269 -0
  18. package/dist/locales/zh/.gitkeep +0 -0
  19. package/dist/locales/zh/cli.json +108 -0
  20. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +25 -0
  21. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +1 -0
  22. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +191 -0
  23. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +1 -0
  24. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +63 -0
  25. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +1 -0
  26. package/dist/plugins/specweave-github/lib/epic-content-builder.js +216 -0
  27. package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +1 -0
  28. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +2 -2
  29. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -1
  30. package/dist/plugins/specweave-github/lib/github-epic-sync.js +19 -4
  31. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -1
  32. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +26 -0
  33. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -0
  34. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +195 -0
  35. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -0
  36. package/dist/spec-parser.js +629 -0
  37. package/dist/src/cli/commands/init.d.ts.map +1 -1
  38. package/dist/src/cli/commands/init.js +107 -3
  39. package/dist/src/cli/commands/init.js.map +1 -1
  40. package/dist/src/core/sync/enhanced-content-builder.d.ts +32 -54
  41. package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -1
  42. package/dist/src/core/sync/enhanced-content-builder.js +141 -138
  43. package/dist/src/core/sync/enhanced-content-builder.js.map +1 -1
  44. package/dist/src/core/sync/spec-content-sync.d.ts +88 -0
  45. package/dist/src/core/sync/spec-content-sync.d.ts.map +1 -0
  46. package/dist/src/core/sync/spec-content-sync.js +5 -0
  47. package/dist/src/core/sync/spec-content-sync.js.map +1 -0
  48. package/dist/src/core/sync/types.d.ts +52 -0
  49. package/dist/src/core/sync/types.d.ts.map +1 -0
  50. package/dist/src/core/sync/types.js +5 -0
  51. package/dist/src/core/sync/types.js.map +1 -0
  52. package/dist/src/core/types/config.d.ts +31 -0
  53. package/dist/src/core/types/config.d.ts.map +1 -1
  54. package/dist/src/core/types/config.js +9 -0
  55. package/dist/src/core/types/config.js.map +1 -1
  56. package/dist/src/core/types/increment-metadata.d.ts +4 -0
  57. package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
  58. package/dist/src/core/types/increment-metadata.js.map +1 -1
  59. package/dist/tsconfig.tsbuildinfo +1 -0
  60. package/package.json +1 -1
  61. package/plugins/specweave/agents/pm/AGENT.md +159 -12
  62. package/plugins/specweave/commands/specweave.md +70 -405
  63. package/plugins/specweave/hooks/post-increment-planning.sh +26 -2
  64. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  65. package/plugins/specweave-github/hooks/post-task-completion.sh +32 -0
  66. package/plugins/specweave-github/lib/epic-content-builder.js +227 -0
  67. package/plugins/specweave-github/lib/epic-content-builder.ts +317 -0
  68. package/plugins/specweave-github/lib/github-epic-sync.js +23 -24
  69. package/plugins/specweave-github/lib/github-epic-sync.ts +29 -4
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Epic Content Builder - Hierarchical GitHub issue content for Feature Specs
3
+ *
4
+ * Architecture:
5
+ * - Reads FS-* folder (FEATURE.md + us-*.md files)
6
+ * - Reads increment tasks.md files to map tasks to user stories
7
+ * - Generates hierarchical issue body:
8
+ * 1. User Stories section (with status + increment)
9
+ * 2. Tasks section (grouped by User Story)
10
+ *
11
+ * Key Features:
12
+ * - NO single "Increment" field (epics span multiple increments)
13
+ * - User Stories are checkable with status and increment link
14
+ * - Tasks grouped under their User Story
15
+ * - Shows which increment each US/task belongs to
16
+ */
17
+
18
+ import { readdir, readFile } from 'fs/promises';
19
+ import { existsSync } from 'fs';
20
+ import * as path from 'path';
21
+ import * as yaml from 'yaml';
22
+
23
+ interface UserStory {
24
+ id: string;
25
+ title: string;
26
+ status: 'complete' | 'active' | 'planning' | 'not-started';
27
+ increment: string | null; // e.g., "0031-external-tool-status-sync"
28
+ tasks: Task[];
29
+ }
30
+
31
+ interface Task {
32
+ id: string;
33
+ title: string;
34
+ status: boolean; // true = completed, false = not started
35
+ userStoryId: string; // e.g., "US-001"
36
+ }
37
+
38
+ interface EpicFrontmatter {
39
+ id: string;
40
+ title: string;
41
+ status: string;
42
+ created: string;
43
+ last_updated: string;
44
+ }
45
+
46
+ interface UserStoryFrontmatter {
47
+ id: string;
48
+ epic: string;
49
+ title: string;
50
+ status: string;
51
+ created: string;
52
+ completed?: string;
53
+ }
54
+
55
+ export class EpicContentBuilder {
56
+ private epicFolder: string;
57
+ private projectRoot: string;
58
+
59
+ constructor(epicFolder: string, projectRoot: string) {
60
+ this.epicFolder = epicFolder;
61
+ this.projectRoot = projectRoot;
62
+ }
63
+
64
+ /**
65
+ * Build hierarchical GitHub issue body
66
+ *
67
+ * Format:
68
+ * - Epic overview
69
+ * - User Stories section (checkable, with status + increment)
70
+ * - Tasks section (grouped by User Story)
71
+ */
72
+ async buildIssueBody(): Promise<string> {
73
+ // 1. Read Epic metadata
74
+ const epicData = await this.readEpicMetadata();
75
+
76
+ // 2. Read User Stories
77
+ const userStories = await this.readUserStories();
78
+
79
+ // 3. Build sections
80
+ const overview = this.buildOverviewSection(epicData);
81
+ const userStoriesSection = this.buildUserStoriesSection(userStories);
82
+ const tasksSection = this.buildTasksSection(userStories);
83
+
84
+ // 4. Combine
85
+ return `${overview}\n\n---\n\n${userStoriesSection}\n\n---\n\n${tasksSection}\n\n---\n\nšŸ¤– Auto-created by SpecWeave Epic Sync`;
86
+ }
87
+
88
+ /**
89
+ * Read Epic FEATURE.md frontmatter
90
+ */
91
+ private async readEpicMetadata(): Promise<EpicFrontmatter> {
92
+ const featurePath = path.join(this.epicFolder, 'FEATURE.md');
93
+ const content = await readFile(featurePath, 'utf-8');
94
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
95
+
96
+ if (!match) {
97
+ throw new Error('FEATURE.md missing YAML frontmatter');
98
+ }
99
+
100
+ return yaml.parse(match[1]) as EpicFrontmatter;
101
+ }
102
+
103
+ /**
104
+ * Read all user stories from us-*.md files
105
+ */
106
+ private async readUserStories(): Promise<UserStory[]> {
107
+ const files = await readdir(this.epicFolder);
108
+ const usFiles = files.filter((f) => f.startsWith('us-') && f.endsWith('.md'));
109
+
110
+ const userStories: UserStory[] = [];
111
+
112
+ for (const file of usFiles.sort()) {
113
+ const filePath = path.join(this.epicFolder, file);
114
+ const content = await readFile(filePath, 'utf-8');
115
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
116
+
117
+ if (!match) {
118
+ console.warn(` āš ļø ${file} missing frontmatter, skipping`);
119
+ continue;
120
+ }
121
+
122
+ const frontmatter = yaml.parse(match[1]) as UserStoryFrontmatter;
123
+ const bodyContent = content.slice(match[0].length).trim();
124
+
125
+ // Extract increment from Implementation section
126
+ const incrementMatch = bodyContent.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
127
+ const increment = incrementMatch ? incrementMatch[1] : null;
128
+
129
+ // Extract tasks from Implementation section
130
+ const tasks = await this.extractTasksForUserStory(
131
+ frontmatter.id,
132
+ increment,
133
+ bodyContent
134
+ );
135
+
136
+ userStories.push({
137
+ id: frontmatter.id,
138
+ title: frontmatter.title,
139
+ status: this.normalizeStatus(frontmatter.status),
140
+ increment,
141
+ tasks,
142
+ });
143
+ }
144
+
145
+ return userStories;
146
+ }
147
+
148
+ /**
149
+ * Extract tasks for a user story from its Implementation section
150
+ */
151
+ private async extractTasksForUserStory(
152
+ userStoryId: string,
153
+ incrementId: string | null,
154
+ content: string
155
+ ): Promise<Task[]> {
156
+ if (!incrementId) {
157
+ return []; // No increment yet, no tasks
158
+ }
159
+
160
+ // Find increment folder
161
+ const incrementFolder = path.join(
162
+ this.projectRoot,
163
+ '.specweave',
164
+ 'increments',
165
+ incrementId
166
+ );
167
+
168
+ if (!existsSync(incrementFolder)) {
169
+ console.warn(` āš ļø Increment folder not found: ${incrementId}`);
170
+ return [];
171
+ }
172
+
173
+ const tasksPath = path.join(incrementFolder, 'tasks.md');
174
+ if (!existsSync(tasksPath)) {
175
+ console.warn(` āš ļø tasks.md not found in ${incrementId}`);
176
+ return [];
177
+ }
178
+
179
+ // Read tasks.md
180
+ const tasksContent = await readFile(tasksPath, 'utf-8');
181
+
182
+ // Extract task links from user story's Implementation section
183
+ // Format: - [T-001: Title](link#t-001-title)
184
+ const taskLinkPattern = /- \[([T-\d]+):\s*([^\]]+)\]/g;
185
+ const taskLinks: Array<{ id: string; title: string }> = [];
186
+
187
+ let match;
188
+ while ((match = taskLinkPattern.exec(content)) !== null) {
189
+ taskLinks.push({
190
+ id: match[1], // e.g., "T-001"
191
+ title: match[2].trim(),
192
+ });
193
+ }
194
+
195
+ // Parse tasks from tasks.md to get completion status
196
+ const tasks: Task[] = [];
197
+
198
+ for (const taskLink of taskLinks) {
199
+ // Find task in tasks.md by heading pattern: ### T-001: Title
200
+ const taskPattern = new RegExp(
201
+ `###\\s+${taskLink.id}:\\s*([^\\n]+)[\\s\\S]*?\\*\\*Status\\*\\*:\\s*\\[([x\\s])\\]`,
202
+ 'i'
203
+ );
204
+ const taskMatch = tasksContent.match(taskPattern);
205
+
206
+ const isCompleted = taskMatch ? taskMatch[2] === 'x' : false;
207
+
208
+ tasks.push({
209
+ id: taskLink.id,
210
+ title: taskLink.title,
211
+ status: isCompleted,
212
+ userStoryId,
213
+ });
214
+ }
215
+
216
+ return tasks;
217
+ }
218
+
219
+ /**
220
+ * Build overview section
221
+ */
222
+ private buildOverviewSection(epic: EpicFrontmatter): string {
223
+ return `# [${epic.id}] ${epic.title}\n\n**Status**: ${epic.status}\n**Created**: ${epic.created}\n**Last Updated**: ${epic.last_updated}`;
224
+ }
225
+
226
+ /**
227
+ * Build User Stories section
228
+ */
229
+ private buildUserStoriesSection(userStories: UserStory[]): string {
230
+ const total = userStories.length;
231
+ const completed = userStories.filter((us) => us.status === 'complete').length;
232
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
233
+
234
+ let section = `## User Stories\n\nProgress: ${completed}/${total} user stories complete (${percentage}%)\n\n`;
235
+
236
+ for (const us of userStories) {
237
+ const checkbox = us.status === 'complete' ? '[x]' : '[ ]';
238
+ const statusEmoji = this.getStatusEmoji(us.status);
239
+ const incrementLink = us.increment
240
+ ? `[${us.increment}](../../increments/${us.increment}/)`
241
+ : 'TBD';
242
+
243
+ section += `- ${checkbox} **${us.id}: ${us.title}** (${statusEmoji} ${us.status} | Increment: ${incrementLink})\n`;
244
+ }
245
+
246
+ return section;
247
+ }
248
+
249
+ /**
250
+ * Build Tasks section (grouped by User Story)
251
+ */
252
+ private buildTasksSection(userStories: UserStory[]): string {
253
+ const totalTasks = userStories.reduce((sum, us) => sum + us.tasks.length, 0);
254
+ const completedTasks = userStories.reduce(
255
+ (sum, us) => sum + us.tasks.filter((t) => t.status).length,
256
+ 0
257
+ );
258
+ const percentage =
259
+ totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
260
+
261
+ let section = `## Tasks by User Story\n\nProgress: ${completedTasks}/${totalTasks} tasks complete (${percentage}%)\n\n`;
262
+
263
+ for (const us of userStories) {
264
+ if (us.tasks.length === 0) {
265
+ continue; // Skip user stories with no tasks yet
266
+ }
267
+
268
+ const incrementLink = us.increment
269
+ ? `[${us.increment}](../../increments/${us.increment}/tasks.md)`
270
+ : 'TBD';
271
+
272
+ section += `### ${us.id}: ${us.title} (Increment: ${incrementLink})\n\n`;
273
+
274
+ for (const task of us.tasks) {
275
+ const checkbox = task.status ? '[x]' : '[ ]';
276
+ section += `- ${checkbox} ${task.id}: ${task.title}\n`;
277
+ }
278
+
279
+ section += '\n';
280
+ }
281
+
282
+ return section;
283
+ }
284
+
285
+ /**
286
+ * Normalize status values
287
+ */
288
+ private normalizeStatus(
289
+ status: string
290
+ ): 'complete' | 'active' | 'planning' | 'not-started' {
291
+ const normalized = status.toLowerCase();
292
+ if (normalized === 'complete' || normalized === 'completed') return 'complete';
293
+ if (normalized === 'active' || normalized === 'in-progress') return 'active';
294
+ if (normalized === 'planning') return 'planning';
295
+ return 'not-started';
296
+ }
297
+
298
+ /**
299
+ * Get status emoji
300
+ */
301
+ private getStatusEmoji(
302
+ status: 'complete' | 'active' | 'planning' | 'not-started'
303
+ ): string {
304
+ switch (status) {
305
+ case 'complete':
306
+ return 'āœ…';
307
+ case 'active':
308
+ return '🚧';
309
+ case 'planning':
310
+ return 'šŸ“‹';
311
+ case 'not-started':
312
+ return 'ā³';
313
+ default:
314
+ return 'ā“';
315
+ }
316
+ }
317
+ }
@@ -4,6 +4,7 @@ import * as path from "path";
4
4
  import * as yaml from "yaml";
5
5
  import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
6
6
  import { DuplicateDetector } from "./duplicate-detector.js";
7
+ import { EpicContentBuilder } from "./epic-content-builder.js";
7
8
  class GitHubEpicSync {
8
9
  constructor(client, specsDir) {
9
10
  this.client = client;
@@ -350,21 +351,20 @@ Status: ${epic.status}`;
350
351
  }
351
352
  }
352
353
  /**
353
- * Create GitHub Issue for increment with FULL DUPLICATE PROTECTION
354
+ * Create GitHub Issue for Epic with hierarchical content (US → Tasks)
354
355
  */
355
356
  async createIssue(epicId, increment, milestoneNumber) {
356
357
  const title = `[${epicId}] ${increment.title}`;
357
- const body = `# ${increment.title}
358
-
359
- ${increment.overview}
360
-
361
- ---
362
-
363
- **Increment**: ${increment.id}
364
- **Epic**: ${epicId}
365
- **Milestone**: See milestone for Epic progress
366
-
367
- \u{1F916} Auto-created by SpecWeave Epic Sync`;
358
+ const epicFolder = await this.findEpicFolder(epicId);
359
+ if (!epicFolder) {
360
+ throw new Error(`Epic folder not found for ${epicId}`);
361
+ }
362
+ const contentBuilder = new EpicContentBuilder(
363
+ epicFolder,
364
+ path.dirname(this.specsDir)
365
+ // Project root
366
+ );
367
+ const body = await contentBuilder.buildIssueBody();
368
368
  try {
369
369
  const result = await DuplicateDetector.createWithProtection({
370
370
  title,
@@ -387,21 +387,20 @@ ${increment.overview}
387
387
  }
388
388
  }
389
389
  /**
390
- * Update GitHub Issue for increment
390
+ * Update GitHub Issue for Epic with hierarchical content (US → Tasks)
391
391
  */
392
392
  async updateIssue(epicId, issueNumber, increment, milestoneNumber) {
393
393
  const title = `[${epicId}] ${increment.title}`;
394
- const body = `# ${increment.title}
395
-
396
- ${increment.overview}
397
-
398
- ---
399
-
400
- **Increment**: ${increment.id}
401
- **Epic**: ${epicId}
402
- **Milestone**: See milestone for Epic progress
403
-
404
- \u{1F916} Auto-updated by SpecWeave Epic Sync`;
394
+ const epicFolder = await this.findEpicFolder(epicId);
395
+ if (!epicFolder) {
396
+ throw new Error(`Epic folder not found for ${epicId}`);
397
+ }
398
+ const contentBuilder = new EpicContentBuilder(
399
+ epicFolder,
400
+ path.dirname(this.specsDir)
401
+ // Project root
402
+ );
403
+ const body = await contentBuilder.buildIssueBody();
405
404
  const result = await execFileNoThrow("gh", [
406
405
  "issue",
407
406
  "edit",
@@ -15,6 +15,7 @@ import * as yaml from 'yaml';
15
15
  import { GitHubClientV2 } from './github-client-v2.js';
16
16
  import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
17
17
  import { DuplicateDetector } from './duplicate-detector.js';
18
+ import { EpicContentBuilder } from './epic-content-builder.js';
18
19
 
19
20
  interface EpicFrontmatter {
20
21
  id: string;
@@ -526,7 +527,7 @@ export class GitHubEpicSync {
526
527
  }
527
528
 
528
529
  /**
529
- * Create GitHub Issue for increment with FULL DUPLICATE PROTECTION
530
+ * Create GitHub Issue for Epic with hierarchical content (US → Tasks)
530
531
  */
531
532
  private async createIssue(
532
533
  epicId: string,
@@ -539,7 +540,19 @@ export class GitHubEpicSync {
539
540
  milestoneNumber: number
540
541
  ): Promise<number> {
541
542
  const title = `[${epicId}] ${increment.title}`;
542
- const body = `# ${increment.title}\n\n${increment.overview}\n\n---\n\n**Increment**: ${increment.id}\n**Epic**: ${epicId}\n**Milestone**: See milestone for Epic progress\n\nšŸ¤– Auto-created by SpecWeave Epic Sync`;
543
+
544
+ // Build hierarchical issue body using EpicContentBuilder
545
+ const epicFolder = await this.findEpicFolder(epicId);
546
+ if (!epicFolder) {
547
+ throw new Error(`Epic folder not found for ${epicId}`);
548
+ }
549
+
550
+ const contentBuilder = new EpicContentBuilder(
551
+ epicFolder,
552
+ path.dirname(this.specsDir) // Project root
553
+ );
554
+
555
+ const body = await contentBuilder.buildIssueBody();
543
556
 
544
557
  try {
545
558
  // Use DuplicateDetector for full 3-phase protection
@@ -568,7 +581,7 @@ export class GitHubEpicSync {
568
581
  }
569
582
 
570
583
  /**
571
- * Update GitHub Issue for increment
584
+ * Update GitHub Issue for Epic with hierarchical content (US → Tasks)
572
585
  */
573
586
  private async updateIssue(
574
587
  epicId: string,
@@ -582,7 +595,19 @@ export class GitHubEpicSync {
582
595
  milestoneNumber: number
583
596
  ): Promise<void> {
584
597
  const title = `[${epicId}] ${increment.title}`;
585
- const body = `# ${increment.title}\n\n${increment.overview}\n\n---\n\n**Increment**: ${increment.id}\n**Epic**: ${epicId}\n**Milestone**: See milestone for Epic progress\n\nšŸ¤– Auto-updated by SpecWeave Epic Sync`;
598
+
599
+ // Build hierarchical issue body using EpicContentBuilder
600
+ const epicFolder = await this.findEpicFolder(epicId);
601
+ if (!epicFolder) {
602
+ throw new Error(`Epic folder not found for ${epicId}`);
603
+ }
604
+
605
+ const contentBuilder = new EpicContentBuilder(
606
+ epicFolder,
607
+ path.dirname(this.specsDir) // Project root
608
+ );
609
+
610
+ const body = await contentBuilder.buildIssueBody();
586
611
 
587
612
  const result = await execFileNoThrow('gh', [
588
613
  'issue',