specweave 0.17.17 → 0.17.19

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 (61) hide show
  1. package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.d.ts.map +1 -1
  2. package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js +65 -6
  3. package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js.map +1 -1
  4. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +63 -0
  5. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +1 -0
  6. package/dist/plugins/specweave-github/lib/epic-content-builder.js +216 -0
  7. package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +1 -0
  8. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +2 -2
  9. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -1
  10. package/dist/plugins/specweave-github/lib/github-epic-sync.js +19 -4
  11. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -1
  12. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +8 -6
  13. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -1
  14. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +78 -117
  15. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -1
  16. package/dist/src/cli/commands/init.d.ts.map +1 -1
  17. package/dist/src/cli/commands/init.js +107 -3
  18. package/dist/src/cli/commands/init.js.map +1 -1
  19. package/dist/src/core/deduplication/command-deduplicator.d.ts +166 -0
  20. package/dist/src/core/deduplication/command-deduplicator.d.ts.map +1 -0
  21. package/dist/src/core/deduplication/command-deduplicator.js +254 -0
  22. package/dist/src/core/deduplication/command-deduplicator.js.map +1 -0
  23. package/dist/src/core/sync/enhanced-content-builder.d.ts +32 -54
  24. package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -1
  25. package/dist/src/core/sync/enhanced-content-builder.js +141 -138
  26. package/dist/src/core/sync/enhanced-content-builder.js.map +1 -1
  27. package/dist/src/core/sync/types.d.ts +52 -0
  28. package/dist/src/core/sync/types.d.ts.map +1 -0
  29. package/dist/src/core/sync/types.js +5 -0
  30. package/dist/src/core/sync/types.js.map +1 -0
  31. package/dist/src/core/types/config.d.ts +51 -0
  32. package/dist/src/core/types/config.d.ts.map +1 -1
  33. package/dist/src/core/types/config.js +16 -0
  34. package/dist/src/core/types/config.js.map +1 -1
  35. package/dist/src/core/types/increment-metadata.d.ts +4 -0
  36. package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
  37. package/dist/src/core/types/increment-metadata.js.map +1 -1
  38. package/package.json +1 -1
  39. package/plugins/specweave/agents/pm/AGENT.md +159 -12
  40. package/plugins/specweave/commands/specweave.md +70 -405
  41. package/plugins/specweave/hooks/hooks.json +4 -0
  42. package/plugins/specweave/hooks/lib/sync-spec-content.sh +2 -2
  43. package/plugins/specweave/hooks/post-increment-planning.sh +26 -2
  44. package/plugins/specweave/hooks/pre-command-deduplication.sh +86 -0
  45. package/plugins/specweave-ado/commands/specweave-ado-sync-spec.md +1 -1
  46. package/plugins/specweave-ado/lib/ado-spec-content-sync.js +49 -5
  47. package/plugins/specweave-ado/lib/ado-spec-content-sync.ts +72 -6
  48. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +1 -1
  49. package/plugins/specweave-github/commands/specweave-github-sync-epic.md +1 -1
  50. package/plugins/specweave-github/commands/specweave-github-sync-spec.md +1 -1
  51. package/plugins/specweave-github/hooks/post-task-completion.sh +32 -0
  52. package/plugins/specweave-github/lib/epic-content-builder.js +227 -0
  53. package/plugins/specweave-github/lib/epic-content-builder.ts +317 -0
  54. package/plugins/specweave-github/lib/github-epic-sync.js +23 -24
  55. package/plugins/specweave-github/lib/github-epic-sync.ts +29 -4
  56. package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +1 -1
  57. package/plugins/specweave-jira/commands/specweave-jira-sync-spec.md +1 -1
  58. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +134 -0
  59. package/plugins/specweave-jira/lib/{enhanced-jira-sync.ts.disabled → enhanced-jira-sync.ts} +26 -52
  60. package/plugins/specweave-release/commands/specweave-release-platform.md +1 -1
  61. package/plugins/specweave-release/hooks/post-task-completion.sh +2 -2
@@ -0,0 +1,86 @@
1
+ #!/bin/bash
2
+
3
+ # SpecWeave Pre-Command Deduplication Hook
4
+ # Fires BEFORE any command executes (UserPromptSubmit hook)
5
+ # Purpose: Prevent duplicate command invocations within configurable time window
6
+
7
+ set -euo pipefail
8
+
9
+ # Read input JSON from stdin
10
+ INPUT=$(cat)
11
+
12
+ # Extract prompt from JSON
13
+ PROMPT=$(echo "$INPUT" | node -e "
14
+ const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
15
+ console.log(input.prompt || '');
16
+ ")
17
+
18
+ # ==============================================================================
19
+ # DEDUPLICATION CHECK: Block duplicate commands within 1 second
20
+ # ==============================================================================
21
+
22
+ # Extract command name from prompt (if slash command)
23
+ COMMAND=$(echo "$PROMPT" | grep -oE "^/[a-z0-9:-]+" | head -1 || echo "")
24
+
25
+ if [[ -n "$COMMAND" ]]; then
26
+ # Check deduplication using TypeScript module
27
+ if command -v node >/dev/null 2>&1 && [[ -f "dist/src/core/deduplication/command-deduplicator.js" ]]; then
28
+ # Run deduplication check
29
+ DEDUP_RESULT=$(node -e "
30
+ (async () => {
31
+ try {
32
+ const { CommandDeduplicator } = require('./dist/src/core/deduplication/command-deduplicator.js');
33
+ const dedup = new CommandDeduplicator({ debug: false });
34
+
35
+ // Parse command and args
36
+ const fullCommand = '${COMMAND}';
37
+ const args = '${PROMPT}'.replace(fullCommand, '').trim().split(/\\s+/).filter(Boolean);
38
+
39
+ // Check for duplicate
40
+ const isDuplicate = await dedup.checkDuplicate(fullCommand, args);
41
+
42
+ if (isDuplicate) {
43
+ const stats = dedup.getStats();
44
+ console.log('DUPLICATE');
45
+ console.log(JSON.stringify(stats));
46
+ } else {
47
+ // Record invocation
48
+ await dedup.recordInvocation(fullCommand, args);
49
+ console.log('OK');
50
+ }
51
+ } catch (e) {
52
+ console.error('Error in deduplication:', e.message);
53
+ console.log('OK'); // Fail-open (don't block on errors)
54
+ }
55
+ })();
56
+ " 2>/dev/null || echo "OK")
57
+
58
+ # Parse result
59
+ STATUS=$(echo "$DEDUP_RESULT" | head -1)
60
+
61
+ if [[ "$STATUS" == "DUPLICATE" ]]; then
62
+ # Get stats
63
+ STATS=$(echo "$DEDUP_RESULT" | tail -1)
64
+
65
+ cat <<EOF
66
+ {
67
+ "decision": "block",
68
+ "reason": "🚫 DUPLICATE COMMAND DETECTED\\n\\nCommand: \`$COMMAND\`\\nTime window: 1 second\\n\\nThis command was just executed! To prevent unintended duplicates, this invocation has been blocked.\\n\\n💡 If you meant to run this command again:\\n 1. Wait 1 second\\n 2. Run the command again\\n\\n📊 Deduplication Stats:\\n$STATS\\n\\n🔧 To adjust the time window, edit \`.specweave/config.json\`:\\n\`\`\`json\\n{\\n \\\"deduplication\\\": {\\n \\\"windowMs\\\": 2000 // Increase to 2 seconds\\n }\\n}\\n\`\`\`"
69
+ }
70
+ EOF
71
+ exit 0
72
+ fi
73
+ fi
74
+ fi
75
+
76
+ # ==============================================================================
77
+ # PASS THROUGH: No duplicate detected, proceed with command
78
+ # ==============================================================================
79
+
80
+ cat <<EOF
81
+ {
82
+ "decision": "approve"
83
+ }
84
+ EOF
85
+
86
+ exit 0
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: specweave-ado-sync-spec
2
+ name: specweave-ado:sync-spec
3
3
  description: Sync SpecWeave spec to Azure DevOps Feature (bidirectional). Use when syncing .specweave/docs/internal/specs/spec-*.md files with ADO Features for permanent feature tracking.
4
4
  ---
5
5
 
@@ -4,8 +4,9 @@ import {
4
4
  hasExternalLink,
5
5
  updateSpecWithExternalLink
6
6
  } from "../../../src/core/spec-content-sync.js";
7
- import path from "path";
8
- import fs from "fs/promises";
7
+ import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
8
+ import * as path from "path";
9
+ import * as fs from "fs/promises";
9
10
  async function syncSpecContentToAdo(options) {
10
11
  const { specPath, client, dryRun = false, verbose = false } = options;
11
12
  try {
@@ -44,8 +45,9 @@ async function syncSpecContentToAdo(options) {
44
45
  async function createAdoFeature(client, spec, options) {
45
46
  const { specPath, dryRun, verbose } = options;
46
47
  try {
48
+ const tasks = await getTaskMappings(specPath, spec.id);
47
49
  const title = `[${spec.id.toUpperCase()}] ${spec.title}`;
48
- const description = buildAdoDescription(spec);
50
+ const description = buildAdoDescription(spec, tasks);
49
51
  if (verbose) {
50
52
  console.log(`
51
53
  \u{1F4DD} Creating ADO feature:`);
@@ -125,8 +127,9 @@ async function updateAdoFeature(client, spec, workItemId, options) {
125
127
  console.log(` - ${change}`);
126
128
  }
127
129
  }
130
+ const tasks = await getTaskMappings(specPath, spec.id);
128
131
  const newTitle = `[${spec.id.toUpperCase()}] ${spec.title}`;
129
- const newDescription = buildAdoDescription(spec);
132
+ const newDescription = buildAdoDescription(spec, tasks);
130
133
  if (dryRun) {
131
134
  console.log("\n\u{1F50D} Dry run - would update feature:");
132
135
  console.log(` Title: ${newTitle}`);
@@ -160,7 +163,7 @@ ${newDescription}`);
160
163
  };
161
164
  }
162
165
  }
163
- function buildAdoDescription(spec) {
166
+ function buildAdoDescription(spec, tasks) {
164
167
  let html = "";
165
168
  if (spec.description) {
166
169
  html += `<p>${escapeHtml(spec.description)}</p>`;
@@ -179,11 +182,52 @@ function buildAdoDescription(spec) {
179
182
  }
180
183
  }
181
184
  }
185
+ if (tasks && tasks.length > 0) {
186
+ html += "<h2>Implementation Tasks</h2>";
187
+ html += "<ul>";
188
+ for (const task of tasks) {
189
+ html += `<li><strong>${task.id}</strong>: ${escapeHtml(task.title)}`;
190
+ if (task.userStories && task.userStories.length > 0) {
191
+ html += ` (${task.userStories.join(", ")})`;
192
+ }
193
+ html += "</li>";
194
+ }
195
+ html += "</ul>";
196
+ }
182
197
  if (spec.metadata.priority) {
183
198
  html += `<p><strong>Priority:</strong> ${spec.metadata.priority}</p>`;
184
199
  }
185
200
  return html;
186
201
  }
202
+ async function getTaskMappings(specPath, specId) {
203
+ try {
204
+ const rootDir = await findSpecWeaveRoot(specPath);
205
+ const mapper = new SpecIncrementMapper(rootDir);
206
+ const mapping = await mapper.mapSpecToIncrements(specId);
207
+ if (mapping.increments.length > 0) {
208
+ return mapping.increments[0].tasks;
209
+ }
210
+ return void 0;
211
+ } catch (error) {
212
+ return void 0;
213
+ }
214
+ }
215
+ async function findSpecWeaveRoot(specPath) {
216
+ let currentDir = path.dirname(specPath);
217
+ while (true) {
218
+ const specweaveDir = path.join(currentDir, ".specweave");
219
+ try {
220
+ await fs.access(specweaveDir);
221
+ return currentDir;
222
+ } catch {
223
+ const parentDir = path.dirname(currentDir);
224
+ if (parentDir === currentDir) {
225
+ throw new Error(".specweave directory not found");
226
+ }
227
+ currentDir = parentDir;
228
+ }
229
+ }
230
+ }
187
231
  function escapeHtml(text) {
188
232
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
189
233
  }
@@ -20,8 +20,9 @@ import {
20
20
  ContentSyncResult,
21
21
  } from '../../../src/core/spec-content-sync.js';
22
22
  import { SyncProfile } from '../../../src/core/types/sync-profile.js';
23
- import path from 'path';
24
- import fs from 'fs/promises';
23
+ import { SpecIncrementMapper, TaskInfo } from '../../../src/core/sync/spec-increment-mapper.js';
24
+ import * as path from 'path';
25
+ import * as fs from 'fs/promises';
25
26
 
26
27
  export interface AdoContentSyncOptions {
27
28
  specPath: string;
@@ -91,9 +92,12 @@ async function createAdoFeature(
91
92
  const { specPath, dryRun, verbose } = options;
92
93
 
93
94
  try {
95
+ // Get task mappings (if available)
96
+ const tasks = await getTaskMappings(specPath, spec.id);
97
+
94
98
  // Build feature title and description
95
99
  const title = `[${spec.id.toUpperCase()}] ${spec.title}`;
96
- const description = buildAdoDescription(spec);
100
+ const description = buildAdoDescription(spec, tasks);
97
101
 
98
102
  if (verbose) {
99
103
  console.log(`\n📝 Creating ADO feature:`);
@@ -196,9 +200,12 @@ async function updateAdoFeature(
196
200
  }
197
201
  }
198
202
 
203
+ // Get task mappings (if available)
204
+ const tasks = await getTaskMappings(specPath, spec.id);
205
+
199
206
  // Build updated content
200
207
  const newTitle = `[${spec.id.toUpperCase()}] ${spec.title}`;
201
- const newDescription = buildAdoDescription(spec);
208
+ const newDescription = buildAdoDescription(spec, tasks);
202
209
 
203
210
  if (dryRun) {
204
211
  console.log('\n🔍 Dry run - would update feature:');
@@ -242,10 +249,10 @@ async function updateAdoFeature(
242
249
  }
243
250
 
244
251
  /**
245
- * Build ADO description from spec content
252
+ * Build ADO description from spec content with optional task mappings
246
253
  * ADO supports HTML in description
247
254
  */
248
- function buildAdoDescription(spec: SpecContent): string {
255
+ function buildAdoDescription(spec: SpecContent, tasks?: TaskInfo[]): string {
249
256
  let html = '';
250
257
 
251
258
  // Add spec description
@@ -271,6 +278,20 @@ function buildAdoDescription(spec: SpecContent): string {
271
278
  }
272
279
  }
273
280
 
281
+ // Add task mappings (if provided)
282
+ if (tasks && tasks.length > 0) {
283
+ html += '<h2>Implementation Tasks</h2>';
284
+ html += '<ul>';
285
+ for (const task of tasks) {
286
+ html += `<li><strong>${task.id}</strong>: ${escapeHtml(task.title)}`;
287
+ if (task.userStories && task.userStories.length > 0) {
288
+ html += ` (${task.userStories.join(', ')})`;
289
+ }
290
+ html += '</li>';
291
+ }
292
+ html += '</ul>';
293
+ }
294
+
274
295
  // Add metadata
275
296
  if (spec.metadata.priority) {
276
297
  html += `<p><strong>Priority:</strong> ${spec.metadata.priority}</p>`;
@@ -279,6 +300,51 @@ function buildAdoDescription(spec: SpecContent): string {
279
300
  return html;
280
301
  }
281
302
 
303
+ /**
304
+ * Get task mappings for a spec (if available)
305
+ */
306
+ async function getTaskMappings(specPath: string, specId: string): Promise<TaskInfo[] | undefined> {
307
+ try {
308
+ // Find SpecWeave root
309
+ const rootDir = await findSpecWeaveRoot(specPath);
310
+
311
+ // Use SpecIncrementMapper to get task mappings
312
+ const mapper = new SpecIncrementMapper(rootDir);
313
+ const mapping = await mapper.mapSpecToIncrements(specId);
314
+
315
+ if (mapping.increments.length > 0) {
316
+ // Return tasks from the first (most recent) increment
317
+ return mapping.increments[0].tasks;
318
+ }
319
+
320
+ return undefined;
321
+ } catch (error) {
322
+ // If mapping fails, just return undefined (not critical)
323
+ return undefined;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Find SpecWeave root directory from spec path
329
+ */
330
+ async function findSpecWeaveRoot(specPath: string): Promise<string> {
331
+ let currentDir = path.dirname(specPath);
332
+
333
+ while (true) {
334
+ const specweaveDir = path.join(currentDir, '.specweave');
335
+ try {
336
+ await fs.access(specweaveDir);
337
+ return currentDir;
338
+ } catch {
339
+ const parentDir = path.dirname(currentDir);
340
+ if (parentDir === currentDir) {
341
+ throw new Error('.specweave directory not found');
342
+ }
343
+ currentDir = parentDir;
344
+ }
345
+ }
346
+ }
347
+
282
348
  /**
283
349
  * Escape HTML special characters
284
350
  */
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: specweave-github-cleanup-duplicates
2
+ name: specweave-github:cleanup-duplicates
3
3
  description: Clean up duplicate GitHub issues for an Epic. Finds issues with duplicate titles and closes all except the first created issue.
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: specweave-github-sync-epic
2
+ name: specweave-github:sync-epic
3
3
  description: Sync SpecWeave Epic folder to GitHub (Milestone + Issues). Implements Universal Hierarchy architecture - Epic → Milestone, Increments → Issues.
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: specweave-github-sync-spec
2
+ name: specweave-github:sync-spec
3
3
  description: Sync SpecWeave spec to GitHub Project (bidirectional). Use when syncing .specweave/docs/internal/specs/spec-*.md files with GitHub Projects for permanent feature tracking.
4
4
  ---
5
5
 
@@ -199,6 +199,38 @@ else
199
199
  fi
200
200
  fi
201
201
 
202
+ # ============================================================================
203
+ # EPIC GITHUB ISSUE SYNC (Update Epic issue with fresh task progress)
204
+ # ============================================================================
205
+
206
+ echo "[$(date)] [GitHub] 🔄 Checking for Epic GitHub issue update..." >> "$DEBUG_LOG" 2>/dev/null || true
207
+
208
+ # Find active increment ID
209
+ ACTIVE_INCREMENT=$(ls -t .specweave/increments/ | grep -v '^\.' | while read inc; do
210
+ if [ -f ".specweave/increments/$inc/metadata.json" ]; then
211
+ STATUS=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' ".specweave/increments/$inc/metadata.json" 2>/dev/null | sed 's/.*"\([^"]*\)".*/\1/' || true)
212
+ if [ "$STATUS" = "active" ]; then
213
+ echo "$inc"
214
+ break
215
+ fi
216
+ fi
217
+ done | head -1)
218
+
219
+ if [ -n "$ACTIVE_INCREMENT" ]; then
220
+ echo "[$(date)] [GitHub] 🎯 Active increment: $ACTIVE_INCREMENT" >> "$DEBUG_LOG" 2>/dev/null || true
221
+
222
+ # Run Epic sync script (silently, errors logged to debug log)
223
+ if [ -f "$PROJECT_ROOT/scripts/update-epic-github-issue.sh" ]; then
224
+ echo "[$(date)] [GitHub] 🚀 Updating Epic GitHub issue..." >> "$DEBUG_LOG" 2>/dev/null || true
225
+ "$PROJECT_ROOT/scripts/update-epic-github-issue.sh" "$ACTIVE_INCREMENT" >> "$DEBUG_LOG" 2>&1 || true
226
+ echo "[$(date)] [GitHub] ✅ Epic sync complete (see logs for details)" >> "$DEBUG_LOG" 2>/dev/null || true
227
+ else
228
+ echo "[$(date)] [GitHub] ⚠️ Epic sync script not found, skipping" >> "$DEBUG_LOG" 2>/dev/null || true
229
+ fi
230
+ else
231
+ echo "[$(date)] [GitHub] ℹ️ No active increment found, skipping Epic sync" >> "$DEBUG_LOG" 2>/dev/null || true
232
+ fi
233
+
202
234
  # ============================================================================
203
235
  # OUTPUT TO CLAUDE
204
236
  # ============================================================================
@@ -0,0 +1,227 @@
1
+ import { readdir, readFile } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import * as path from "path";
4
+ import * as yaml from "yaml";
5
+ class EpicContentBuilder {
6
+ constructor(epicFolder, projectRoot) {
7
+ this.epicFolder = epicFolder;
8
+ this.projectRoot = projectRoot;
9
+ }
10
+ /**
11
+ * Build hierarchical GitHub issue body
12
+ *
13
+ * Format:
14
+ * - Epic overview
15
+ * - User Stories section (checkable, with status + increment)
16
+ * - Tasks section (grouped by User Story)
17
+ */
18
+ async buildIssueBody() {
19
+ const epicData = await this.readEpicMetadata();
20
+ const userStories = await this.readUserStories();
21
+ const overview = this.buildOverviewSection(epicData);
22
+ const userStoriesSection = this.buildUserStoriesSection(userStories);
23
+ const tasksSection = this.buildTasksSection(userStories);
24
+ return `${overview}
25
+
26
+ ---
27
+
28
+ ${userStoriesSection}
29
+
30
+ ---
31
+
32
+ ${tasksSection}
33
+
34
+ ---
35
+
36
+ \u{1F916} Auto-created by SpecWeave Epic Sync`;
37
+ }
38
+ /**
39
+ * Read Epic FEATURE.md frontmatter
40
+ */
41
+ async readEpicMetadata() {
42
+ const featurePath = path.join(this.epicFolder, "FEATURE.md");
43
+ const content = await readFile(featurePath, "utf-8");
44
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
45
+ if (!match) {
46
+ throw new Error("FEATURE.md missing YAML frontmatter");
47
+ }
48
+ return yaml.parse(match[1]);
49
+ }
50
+ /**
51
+ * Read all user stories from us-*.md files
52
+ */
53
+ async readUserStories() {
54
+ const files = await readdir(this.epicFolder);
55
+ const usFiles = files.filter((f) => f.startsWith("us-") && f.endsWith(".md"));
56
+ const userStories = [];
57
+ for (const file of usFiles.sort()) {
58
+ const filePath = path.join(this.epicFolder, file);
59
+ const content = await readFile(filePath, "utf-8");
60
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
61
+ if (!match) {
62
+ console.warn(` \u26A0\uFE0F ${file} missing frontmatter, skipping`);
63
+ continue;
64
+ }
65
+ const frontmatter = yaml.parse(match[1]);
66
+ const bodyContent = content.slice(match[0].length).trim();
67
+ const incrementMatch = bodyContent.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
68
+ const increment = incrementMatch ? incrementMatch[1] : null;
69
+ const tasks = await this.extractTasksForUserStory(
70
+ frontmatter.id,
71
+ increment,
72
+ bodyContent
73
+ );
74
+ userStories.push({
75
+ id: frontmatter.id,
76
+ title: frontmatter.title,
77
+ status: this.normalizeStatus(frontmatter.status),
78
+ increment,
79
+ tasks
80
+ });
81
+ }
82
+ return userStories;
83
+ }
84
+ /**
85
+ * Extract tasks for a user story from its Implementation section
86
+ */
87
+ async extractTasksForUserStory(userStoryId, incrementId, content) {
88
+ if (!incrementId) {
89
+ return [];
90
+ }
91
+ const incrementFolder = path.join(
92
+ this.projectRoot,
93
+ ".specweave",
94
+ "increments",
95
+ incrementId
96
+ );
97
+ if (!existsSync(incrementFolder)) {
98
+ console.warn(` \u26A0\uFE0F Increment folder not found: ${incrementId}`);
99
+ return [];
100
+ }
101
+ const tasksPath = path.join(incrementFolder, "tasks.md");
102
+ if (!existsSync(tasksPath)) {
103
+ console.warn(` \u26A0\uFE0F tasks.md not found in ${incrementId}`);
104
+ return [];
105
+ }
106
+ const tasksContent = await readFile(tasksPath, "utf-8");
107
+ const taskLinkPattern = /- \[([T-\d]+):\s*([^\]]+)\]/g;
108
+ const taskLinks = [];
109
+ let match;
110
+ while ((match = taskLinkPattern.exec(content)) !== null) {
111
+ taskLinks.push({
112
+ id: match[1],
113
+ // e.g., "T-001"
114
+ title: match[2].trim()
115
+ });
116
+ }
117
+ const tasks = [];
118
+ for (const taskLink of taskLinks) {
119
+ const taskPattern = new RegExp(
120
+ `###\\s+${taskLink.id}:\\s*([^\\n]+)[\\s\\S]*?\\*\\*Status\\*\\*:\\s*\\[([x\\s])\\]`,
121
+ "i"
122
+ );
123
+ const taskMatch = tasksContent.match(taskPattern);
124
+ const isCompleted = taskMatch ? taskMatch[2] === "x" : false;
125
+ tasks.push({
126
+ id: taskLink.id,
127
+ title: taskLink.title,
128
+ status: isCompleted,
129
+ userStoryId
130
+ });
131
+ }
132
+ return tasks;
133
+ }
134
+ /**
135
+ * Build overview section
136
+ */
137
+ buildOverviewSection(epic) {
138
+ return `# [${epic.id}] ${epic.title}
139
+
140
+ **Status**: ${epic.status}
141
+ **Created**: ${epic.created}
142
+ **Last Updated**: ${epic.last_updated}`;
143
+ }
144
+ /**
145
+ * Build User Stories section
146
+ */
147
+ buildUserStoriesSection(userStories) {
148
+ const total = userStories.length;
149
+ const completed = userStories.filter((us) => us.status === "complete").length;
150
+ const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
151
+ let section = `## User Stories
152
+
153
+ Progress: ${completed}/${total} user stories complete (${percentage}%)
154
+
155
+ `;
156
+ for (const us of userStories) {
157
+ const checkbox = us.status === "complete" ? "[x]" : "[ ]";
158
+ const statusEmoji = this.getStatusEmoji(us.status);
159
+ const incrementLink = us.increment ? `[${us.increment}](../../increments/${us.increment}/)` : "TBD";
160
+ section += `- ${checkbox} **${us.id}: ${us.title}** (${statusEmoji} ${us.status} | Increment: ${incrementLink})
161
+ `;
162
+ }
163
+ return section;
164
+ }
165
+ /**
166
+ * Build Tasks section (grouped by User Story)
167
+ */
168
+ buildTasksSection(userStories) {
169
+ const totalTasks = userStories.reduce((sum, us) => sum + us.tasks.length, 0);
170
+ const completedTasks = userStories.reduce(
171
+ (sum, us) => sum + us.tasks.filter((t) => t.status).length,
172
+ 0
173
+ );
174
+ const percentage = totalTasks > 0 ? Math.round(completedTasks / totalTasks * 100) : 0;
175
+ let section = `## Tasks by User Story
176
+
177
+ Progress: ${completedTasks}/${totalTasks} tasks complete (${percentage}%)
178
+
179
+ `;
180
+ for (const us of userStories) {
181
+ if (us.tasks.length === 0) {
182
+ continue;
183
+ }
184
+ const incrementLink = us.increment ? `[${us.increment}](../../increments/${us.increment}/tasks.md)` : "TBD";
185
+ section += `### ${us.id}: ${us.title} (Increment: ${incrementLink})
186
+
187
+ `;
188
+ for (const task of us.tasks) {
189
+ const checkbox = task.status ? "[x]" : "[ ]";
190
+ section += `- ${checkbox} ${task.id}: ${task.title}
191
+ `;
192
+ }
193
+ section += "\n";
194
+ }
195
+ return section;
196
+ }
197
+ /**
198
+ * Normalize status values
199
+ */
200
+ normalizeStatus(status) {
201
+ const normalized = status.toLowerCase();
202
+ if (normalized === "complete" || normalized === "completed") return "complete";
203
+ if (normalized === "active" || normalized === "in-progress") return "active";
204
+ if (normalized === "planning") return "planning";
205
+ return "not-started";
206
+ }
207
+ /**
208
+ * Get status emoji
209
+ */
210
+ getStatusEmoji(status) {
211
+ switch (status) {
212
+ case "complete":
213
+ return "\u2705";
214
+ case "active":
215
+ return "\u{1F6A7}";
216
+ case "planning":
217
+ return "\u{1F4CB}";
218
+ case "not-started":
219
+ return "\u23F3";
220
+ default:
221
+ return "\u2753";
222
+ }
223
+ }
224
+ }
225
+ export {
226
+ EpicContentBuilder
227
+ };