claude-autopm 1.25.0 → 1.27.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.
Files changed (40) hide show
  1. package/README.md +111 -0
  2. package/autopm/.claude/agents/frameworks/e2e-test-engineer.md +1 -18
  3. package/autopm/.claude/agents/frameworks/nats-messaging-expert.md +1 -18
  4. package/autopm/.claude/agents/frameworks/react-frontend-engineer.md +1 -18
  5. package/autopm/.claude/agents/frameworks/react-ui-expert.md +1 -18
  6. package/autopm/.claude/agents/frameworks/tailwindcss-expert.md +1 -18
  7. package/autopm/.claude/agents/frameworks/ux-design-expert.md +1 -18
  8. package/autopm/.claude/agents/languages/bash-scripting-expert.md +1 -18
  9. package/autopm/.claude/agents/languages/javascript-frontend-engineer.md +1 -18
  10. package/autopm/.claude/agents/languages/nodejs-backend-engineer.md +1 -18
  11. package/autopm/.claude/agents/languages/python-backend-engineer.md +1 -18
  12. package/autopm/.claude/agents/languages/python-backend-expert.md +1 -18
  13. package/autopm/.claude/commands/pm/epic-decompose.md +19 -5
  14. package/autopm/.claude/commands/pm/prd-new.md +14 -1
  15. package/autopm/.claude/includes/task-creation-excellence.md +18 -0
  16. package/autopm/.claude/lib/ai-task-generator.js +84 -0
  17. package/autopm/.claude/lib/cli-parser.js +148 -0
  18. package/autopm/.claude/lib/commands/pm/epicStatus.js +263 -0
  19. package/autopm/.claude/lib/dependency-analyzer.js +157 -0
  20. package/autopm/.claude/lib/frontmatter.js +224 -0
  21. package/autopm/.claude/lib/task-utils.js +64 -0
  22. package/autopm/.claude/scripts/pm-epic-decompose-local.js +158 -0
  23. package/autopm/.claude/scripts/pm-epic-list-local.js +103 -0
  24. package/autopm/.claude/scripts/pm-epic-show-local.js +70 -0
  25. package/autopm/.claude/scripts/pm-epic-update-local.js +56 -0
  26. package/autopm/.claude/scripts/pm-prd-list-local.js +111 -0
  27. package/autopm/.claude/scripts/pm-prd-new-local.js +196 -0
  28. package/autopm/.claude/scripts/pm-prd-parse-local.js +360 -0
  29. package/autopm/.claude/scripts/pm-prd-show-local.js +101 -0
  30. package/autopm/.claude/scripts/pm-prd-update-local.js +153 -0
  31. package/autopm/.claude/scripts/pm-sync-download-local.js +424 -0
  32. package/autopm/.claude/scripts/pm-sync-upload-local.js +473 -0
  33. package/autopm/.claude/scripts/pm-task-list-local.js +86 -0
  34. package/autopm/.claude/scripts/pm-task-show-local.js +92 -0
  35. package/autopm/.claude/scripts/pm-task-update-local.js +109 -0
  36. package/autopm/.claude/scripts/setup-local-mode.js +127 -0
  37. package/package.json +5 -3
  38. package/scripts/create-task-issues.sh +26 -0
  39. package/scripts/fix-invalid-command-refs.sh +4 -3
  40. package/scripts/fix-invalid-refs-simple.sh +8 -3
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Local PRD Listing Command
3
+ *
4
+ * Lists all Product Requirements Documents (PRDs) in local mode.
5
+ * Supports filtering and sorting.
6
+ *
7
+ * Usage:
8
+ * /pm:prd-list --local
9
+ * /pm:prd-list --local --status approved
10
+ *
11
+ * @module pm-prd-list-local
12
+ */
13
+
14
+ const fs = require('fs').promises;
15
+ const path = require('path');
16
+ const { parseFrontmatter } = require('../lib/frontmatter');
17
+
18
+ /**
19
+ * Lists all local PRDs with optional filtering
20
+ *
21
+ * @param {Object} options - Listing options
22
+ * @param {string} options.status - Filter by status (draft, approved, etc.)
23
+ * @returns {Promise<Array>} Array of PRD metadata objects
24
+ */
25
+ async function listLocalPRDs(options = {}) {
26
+ const prdsDir = path.join(process.cwd(), '.claude', 'prds');
27
+
28
+ // Ensure directory exists
29
+ try {
30
+ await fs.access(prdsDir);
31
+ } catch (err) {
32
+ if (err.code === 'ENOENT') {
33
+ return []; // No PRDs directory = no PRDs
34
+ }
35
+ throw err;
36
+ }
37
+
38
+ // Read directory
39
+ const files = await fs.readdir(prdsDir);
40
+ const mdFiles = files.filter(f => f.endsWith('.md'));
41
+
42
+ // Parallelize file reading/parsing with Promise.allSettled
43
+ const prdPromises = mdFiles.map(async (file) => {
44
+ try {
45
+ const filepath = path.join(prdsDir, file);
46
+ const content = await fs.readFile(filepath, 'utf8');
47
+ const { frontmatter } = parseFrontmatter(content);
48
+ // Only include files with valid frontmatter containing required fields
49
+ // A valid PRD must have at least an 'id' field
50
+ if (frontmatter && typeof frontmatter === 'object' && frontmatter.id) {
51
+ return {
52
+ filename: file,
53
+ ...frontmatter
54
+ };
55
+ }
56
+ } catch (err) {
57
+ // Skip files that can't be parsed
58
+ console.warn(`Warning: Could not parse ${file}:`, err.message);
59
+ }
60
+ return null;
61
+ });
62
+
63
+ const settled = await Promise.allSettled(prdPromises);
64
+ const prds = settled
65
+ .filter(r => r.status === 'fulfilled' && r.value)
66
+ .map(r => r.value);
67
+ // Filter by status if specified
68
+ let filtered = prds;
69
+ if (options.status) {
70
+ filtered = filtered.filter(p => p.status === options.status);
71
+ }
72
+
73
+ // Sort by creation timestamp (newest first)
74
+ filtered.sort((a, b) => {
75
+ // Use createdAt if available (full timestamp), fallback to created (date only)
76
+ const dateA = new Date(a.createdAt || a.created || 0);
77
+ const dateB = new Date(b.createdAt || b.created || 0);
78
+ return dateB - dateA;
79
+ });
80
+
81
+ return filtered;
82
+ }
83
+
84
+ /**
85
+ * Formats PRD list for display
86
+ *
87
+ * @param {Array} prds - Array of PRD objects
88
+ * @returns {string} Formatted string for display
89
+ */
90
+ function formatPRDList(prds) {
91
+ if (prds.length === 0) {
92
+ return 'No PRDs found.';
93
+ }
94
+
95
+ const lines = ['', 'Local PRDs:', ''];
96
+
97
+ prds.forEach((prd, index) => {
98
+ lines.push(
99
+ `${index + 1}. [${prd.id}] ${prd.title}`,
100
+ ` Status: ${prd.status} | Priority: ${prd.priority} | Created: ${prd.created}`,
101
+ ''
102
+ );
103
+ });
104
+
105
+ return lines.join('\n');
106
+ }
107
+
108
+ module.exports = {
109
+ listLocalPRDs,
110
+ formatPRDList
111
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Local PRD Creation Command
3
+ *
4
+ * Creates a new Product Requirements Document (PRD) in local mode.
5
+ * PRDs are stored in .claude/prds/ directory.
6
+ *
7
+ * Usage:
8
+ * /pm:prd-new --local "Feature Name"
9
+ *
10
+ * @module pm-prd-new-local
11
+ */
12
+
13
+ const fs = require('fs').promises;
14
+ const path = require('path');
15
+ const { stringifyFrontmatter } = require('../lib/frontmatter');
16
+
17
+ /**
18
+ * Creates a new PRD in the local .claude/prds/ directory
19
+ *
20
+ * @param {string} name - PRD title/name
21
+ * @param {Object} options - Optional configuration
22
+ * @param {string} options.id - Custom PRD ID (auto-generated if not provided)
23
+ * @param {string} options.author - Author name (default: 'ClaudeAutoPM')
24
+ * @param {string} options.priority - Priority level (default: 'medium')
25
+ * @returns {Promise<Object>} Created PRD metadata
26
+ * @throws {Error} If name is invalid or PRD already exists
27
+ */
28
+ async function createLocalPRD(name, options = {}) {
29
+ // Validate name
30
+ if (!name || typeof name !== 'string' || name.trim().length === 0) {
31
+ throw new Error('PRD name is required and must be a non-empty string');
32
+ }
33
+
34
+ const sanitizedName = name.trim();
35
+
36
+ // Generate unique ID
37
+ const id = options.id || generatePRDId();
38
+
39
+ // Create frontmatter
40
+ const now = new Date();
41
+ const frontmatter = {
42
+ id,
43
+ title: sanitizedName,
44
+ created: now.toISOString().split('T')[0],
45
+ createdAt: now.toISOString(), // Include full timestamp for sorting
46
+ author: options.author || 'ClaudeAutoPM',
47
+ status: 'draft',
48
+ priority: options.priority || 'medium',
49
+ version: '1.0'
50
+ };
51
+
52
+ // Create PRD body template
53
+ const body = createPRDTemplate(sanitizedName);
54
+
55
+ // Generate full markdown content
56
+ const content = stringifyFrontmatter(frontmatter, body);
57
+
58
+ // Generate filename (sanitize name + add ID for uniqueness)
59
+ const filename = `${id}-${sanitizeFilename(sanitizedName)}`;
60
+ const filepath = path.join(process.cwd(), '.claude', 'prds', filename);
61
+
62
+ // Check if file already exists
63
+ try {
64
+ await fs.access(filepath);
65
+ throw new Error(`PRD already exists: ${filename}`);
66
+ } catch (err) {
67
+ if (err.code !== 'ENOENT') {
68
+ throw err;
69
+ }
70
+ }
71
+
72
+ // Ensure directory exists
73
+ await fs.mkdir(path.dirname(filepath), { recursive: true });
74
+
75
+ // Write to file
76
+ await fs.writeFile(filepath, content, 'utf8');
77
+
78
+ return {
79
+ id,
80
+ filepath,
81
+ frontmatter
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Creates a PRD template with standard sections
87
+ *
88
+ * @param {string} name - PRD title
89
+ * @returns {string} PRD template content
90
+ */
91
+ function createPRDTemplate(name) {
92
+ return `# Product Requirements Document: ${name}
93
+
94
+ ## 1. Executive Summary
95
+
96
+ ### Overview
97
+ [Describe the feature/product in 2-3 sentences]
98
+
99
+ ### Business Value
100
+ [Why is this important?]
101
+
102
+ ### Success Metrics
103
+ [How will we measure success?]
104
+
105
+ ## 2. Background
106
+
107
+ ### Problem Statement
108
+ [What problem are we solving?]
109
+
110
+ ### Current State
111
+ [What exists today?]
112
+
113
+ ### Goals and Objectives
114
+ [What are we trying to achieve?]
115
+
116
+ ## 3. User Stories
117
+
118
+ [Epic-level user stories]
119
+
120
+ ## 4. Functional Requirements
121
+
122
+ [Detailed requirements]
123
+
124
+ ## 5. Non-Functional Requirements
125
+
126
+ [Performance, security, etc.]
127
+
128
+ ## 6. Out of Scope
129
+
130
+ [What we're NOT doing]
131
+
132
+ ## 7. Timeline
133
+
134
+ [Key milestones]
135
+ `;
136
+ }
137
+
138
+ // Counter for ensuring unique IDs even when created at the same millisecond
139
+ let idCounter = 0;
140
+
141
+ /**
142
+ * Generates a unique PRD ID
143
+ *
144
+ * Format: prd-XXX (3 digits from timestamp + counter)
145
+ *
146
+ * @returns {string} Generated PRD ID
147
+ */
148
+ function generatePRDId() {
149
+ const timestamp = Date.now();
150
+ const random = Math.floor(Math.random() * 900) + 100; // 100-999
151
+ const counter = (idCounter++) % 10; // 0-9 cycling counter
152
+
153
+ // Use last digit of timestamp + last 2 digits of random = 3 digits total
154
+ const suffix = (timestamp % 10).toString() +
155
+ (Math.floor(random / 10) % 10).toString() +
156
+ counter.toString();
157
+
158
+ return `prd-${suffix}`;
159
+ }
160
+
161
+ /**
162
+ * Sanitizes a PRD name for use as a filename
163
+ *
164
+ * - Converts to lowercase
165
+ * - Replaces spaces with hyphens
166
+ * - Removes special characters except hyphens
167
+ * - Truncates to safe length (max 100 chars before .md)
168
+ * - Adds .md extension
169
+ *
170
+ * @param {string} name - PRD name to sanitize
171
+ * @returns {string} Sanitized filename
172
+ */
173
+ function sanitizeFilename(name) {
174
+ const sanitized = name
175
+ .toLowerCase()
176
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
177
+ .replace(/[^a-z0-9-]/g, '') // Remove special characters
178
+ .replace(/-+/g, '-') // Collapse multiple hyphens
179
+ .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
180
+
181
+ // Truncate to safe length (100 chars + .md = 103 total)
182
+ // With prd-XXX- prefix (8 chars), max base name is ~92 chars
183
+ const maxLength = 92;
184
+ const truncated = sanitized.length > maxLength
185
+ ? sanitized.substring(0, maxLength)
186
+ : sanitized;
187
+
188
+ return truncated + '.md';
189
+ }
190
+
191
+ module.exports = {
192
+ createLocalPRD,
193
+ createPRDTemplate,
194
+ generatePRDId,
195
+ sanitizeFilename
196
+ };
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TASK-005: PRD-to-Epic Parser (Local Mode)
4
+ *
5
+ * Parse PRD markdown to Epic structure with proper section extraction
6
+ * Uses markdown-it for robust markdown parsing (Context7 verified)
7
+ *
8
+ * @module pm-prd-parse-local
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const MarkdownIt = require('markdown-it');
14
+ const { parseFrontmatter, stringifyFrontmatter } = require('../lib/frontmatter');
15
+ const { showLocalPRD } = require('./pm-prd-show-local');
16
+
17
+ /**
18
+ * Parse PRD to Epic structure
19
+ *
20
+ * @param {string} prdId - PRD identifier (e.g., 'prd-347')
21
+ * @returns {Promise<object>} Epic data with structure:
22
+ * - epicId: Generated epic ID
23
+ * - epicDir: Path to epic directory
24
+ * - epicPath: Path to epic.md file
25
+ * - frontmatter: Epic frontmatter object
26
+ * - sections: Extracted PRD sections
27
+ */
28
+ async function parseLocalPRD(prdId) {
29
+ // 1. Load PRD
30
+ const prd = await showLocalPRD(prdId);
31
+ const prdMeta = prd.frontmatter;
32
+ const prdBody = prd.body;
33
+
34
+ // 2. Parse markdown sections
35
+ const sections = extractSections(prdBody);
36
+
37
+ // 3. Generate Epic frontmatter
38
+ const epicId = generateEpicId(prdId);
39
+ const epicFrontmatter = {
40
+ id: epicId,
41
+ prd_id: prdId,
42
+ title: `${prdMeta.title} - Implementation Epic`,
43
+ created: new Date().toISOString().split('T')[0],
44
+ status: 'planning',
45
+ github_issue: null,
46
+ tasks_total: 0,
47
+ tasks_completed: 0
48
+ };
49
+
50
+ // 4. Build Epic body
51
+ const epicBody = buildEpicBody(sections, prdMeta);
52
+
53
+ // 5. Create epic directory
54
+ const epicDir = path.join(process.cwd(), '.claude', 'epics',
55
+ `${epicId}-${slugify(prdMeta.title)}`);
56
+ await fs.mkdir(epicDir, { recursive: true });
57
+
58
+ // 6. Write epic.md
59
+ const epicContent = stringifyFrontmatter(epicFrontmatter, epicBody);
60
+ const epicPath = path.join(epicDir, 'epic.md');
61
+ await fs.writeFile(epicPath, epicContent);
62
+
63
+ return {
64
+ epicId,
65
+ epicDir,
66
+ epicPath,
67
+ frontmatter: epicFrontmatter,
68
+ sections
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Extract sections from PRD markdown
74
+ * Uses markdown-it to parse headings and content
75
+ *
76
+ * @param {string} markdown - PRD markdown content
77
+ * @returns {object} Extracted sections:
78
+ * - overview: Project overview/summary
79
+ * - goals: Project goals/objectives
80
+ * - userStories: Array of user story objects
81
+ * - requirements: Technical requirements
82
+ * - timeline: Timeline/milestones
83
+ */
84
+ function extractSections(markdown) {
85
+ const md = new MarkdownIt();
86
+ const tokens = md.parse(markdown, {});
87
+
88
+ const sections = {
89
+ overview: '',
90
+ goals: '',
91
+ userStories: [],
92
+ requirements: '',
93
+ timeline: ''
94
+ };
95
+
96
+ let currentSection = null;
97
+ let currentContent = [];
98
+ let currentHeading = '';
99
+ let inList = false;
100
+
101
+ for (let i = 0; i < tokens.length; i++) {
102
+ const token = tokens[i];
103
+
104
+ if (token.type === 'heading_open') {
105
+ // Only process ## headings (level 2), not ### (level 3)
106
+ if (token.tag === 'h2') {
107
+ // Save previous section
108
+ if (currentSection) {
109
+ const content = currentContent.join('\n').trim();
110
+ if (currentSection === 'userStories') {
111
+ sections[currentSection] = parseUserStories(content);
112
+ } else {
113
+ sections[currentSection] = content;
114
+ }
115
+ }
116
+
117
+ // Get heading content from next token
118
+ const nextToken = tokens[i + 1];
119
+ if (nextToken && nextToken.type === 'inline') {
120
+ currentHeading = nextToken.content.toLowerCase();
121
+ }
122
+
123
+ // Start new section
124
+ currentContent = [];
125
+ inList = false;
126
+
127
+ // Determine section type from heading
128
+ if (currentHeading.includes('overview') || currentHeading.includes('summary')) {
129
+ currentSection = 'overview';
130
+ } else if (currentHeading.includes('goal') || currentHeading.includes('objective')) {
131
+ currentSection = 'goals';
132
+ } else if (currentHeading.includes('user stor')) {
133
+ currentSection = 'userStories';
134
+ } else if (currentHeading.includes('requirement')) {
135
+ currentSection = 'requirements';
136
+ } else if (currentHeading.includes('timeline') || currentHeading.includes('milestone')) {
137
+ currentSection = 'timeline';
138
+ } else {
139
+ currentSection = null;
140
+ }
141
+
142
+ // Skip the inline token (heading content) and closing tag
143
+ i += 2;
144
+ } else if (token.tag === 'h3' && currentSection) {
145
+ // Preserve ### headings within sections
146
+ const nextToken = tokens[i + 1];
147
+ if (nextToken && nextToken.type === 'inline') {
148
+ currentContent.push('');
149
+ currentContent.push('### ' + nextToken.content);
150
+ currentContent.push('');
151
+ }
152
+ i += 2; // Skip inline and closing tag
153
+ }
154
+ } else if (currentSection && token.type === 'inline' && token.content) {
155
+ currentContent.push(token.content);
156
+ } else if (currentSection && token.type === 'fence' && token.content) {
157
+ // Preserve code blocks
158
+ currentContent.push('');
159
+ currentContent.push('```' + (token.info || ''));
160
+ currentContent.push(token.content.trim());
161
+ currentContent.push('```');
162
+ currentContent.push('');
163
+ } else if (currentSection && token.type === 'bullet_list_open') {
164
+ // Handle bullet lists
165
+ if (currentContent.length > 0 && currentContent[currentContent.length - 1] !== '') {
166
+ currentContent.push('');
167
+ }
168
+ inList = true;
169
+ } else if (currentSection && token.type === 'bullet_list_close') {
170
+ inList = false;
171
+ if (currentContent.length > 0 && currentContent[currentContent.length - 1] !== '') {
172
+ currentContent.push('');
173
+ }
174
+ } else if (currentSection && token.type === 'list_item_open') {
175
+ // Add list item marker - look ahead for paragraph
176
+ let contentFound = false;
177
+ for (let j = i + 1; j < tokens.length && !contentFound; j++) {
178
+ if (tokens[j].type === 'inline' && tokens[j].content) {
179
+ currentContent.push('- ' + tokens[j].content);
180
+ contentFound = true;
181
+ } else if (tokens[j].type === 'list_item_close') {
182
+ break;
183
+ }
184
+ }
185
+ } else if (currentSection && token.type === 'paragraph_open' && !inList) {
186
+ // Add blank line before paragraph (except first one or in lists)
187
+ if (currentContent.length > 0 && currentContent[currentContent.length - 1] !== '') {
188
+ currentContent.push('');
189
+ }
190
+ }
191
+ }
192
+
193
+ // Save last section
194
+ if (currentSection) {
195
+ const content = currentContent.join('\n').trim();
196
+ if (currentSection === 'userStories') {
197
+ sections[currentSection] = parseUserStories(content);
198
+ } else {
199
+ sections[currentSection] = content;
200
+ }
201
+ }
202
+
203
+ return sections;
204
+ }
205
+
206
+ /**
207
+ * Parse user stories from text
208
+ * Looks for "As a...", "I want...", "So that..." patterns
209
+ *
210
+ * @param {string} text - Text containing user stories
211
+ * @returns {Array<object>} Array of user story objects with raw text
212
+ */
213
+ function parseUserStories(text) {
214
+ const stories = [];
215
+ const lines = text.split('\n');
216
+
217
+ let currentStory = null;
218
+
219
+ for (const line of lines) {
220
+ const trimmed = line.trim();
221
+
222
+ // Check if line starts a new user story (matches "As a" or "As an", with optional bold)
223
+ if (/^\*\*As an?\b/i.test(trimmed) || /^As an?\b/i.test(trimmed)) {
224
+ // Save previous story
225
+ if (currentStory) {
226
+ stories.push(currentStory);
227
+ }
228
+ // Start new story
229
+ currentStory = { raw: trimmed };
230
+ } else if (currentStory && trimmed) {
231
+ // Continue current story
232
+ currentStory.raw += '\n' + trimmed;
233
+ }
234
+ }
235
+
236
+ // Save last story
237
+ if (currentStory) {
238
+ stories.push(currentStory);
239
+ }
240
+
241
+ return stories;
242
+ }
243
+
244
+ /**
245
+ * Build Epic body from PRD sections
246
+ *
247
+ * @param {object} sections - Extracted PRD sections
248
+ * @param {object} prdMeta - PRD frontmatter metadata
249
+ * @returns {string} Epic markdown body
250
+ */
251
+ function buildEpicBody(sections, prdMeta) {
252
+ // Build user stories section
253
+ let userStoriesSection = '';
254
+ if (sections.userStories && sections.userStories.length > 0) {
255
+ userStoriesSection = sections.userStories
256
+ .map((s, i) => `${i + 1}. ${s.raw}`)
257
+ .join('\n\n');
258
+ } else {
259
+ userStoriesSection = 'To be defined from PRD user stories.';
260
+ }
261
+
262
+ return `# Epic: ${prdMeta.title}
263
+
264
+ ## Overview
265
+
266
+ ${sections.overview || 'To be defined based on PRD.'}
267
+
268
+ ## Technical Architecture
269
+
270
+ ### Goals
271
+ ${sections.goals || 'Extract from PRD goals and objectives.'}
272
+
273
+ ### User Stories
274
+ ${userStoriesSection}
275
+
276
+ ## Implementation Tasks
277
+
278
+ Tasks will be created via epic decomposition.
279
+
280
+ ## Dependencies
281
+
282
+ ### Between Tasks
283
+ To be determined during task breakdown.
284
+
285
+ ### External Dependencies
286
+ ${sections.requirements ? 'See PRD requirements section.' : 'To be identified.'}
287
+
288
+ ## Timeline
289
+
290
+ ${sections.timeline || 'See PRD for timeline and milestones.'}
291
+
292
+ ## Related Documents
293
+
294
+ - PRD: \`.claude/prds/${slugify(prdMeta.title)}.md\`
295
+ `;
296
+ }
297
+
298
+ /**
299
+ * Generate unique epic ID from PRD ID
300
+ *
301
+ * @param {string} prdId - PRD identifier (e.g., 'prd-347')
302
+ * @returns {string} Epic identifier (e.g., 'epic-347')
303
+ */
304
+ function generateEpicId(prdId) {
305
+ // prd-347 → epic-347
306
+ const num = prdId.replace('prd-', '');
307
+ return `epic-${num}`;
308
+ }
309
+
310
+ /**
311
+ * Slugify title for directory/file names
312
+ * Converts to lowercase, removes special chars, replaces spaces with hyphens
313
+ *
314
+ * @param {string} text - Text to slugify
315
+ * @returns {string} Slugified text
316
+ */
317
+ function slugify(text) {
318
+ return text
319
+ .toLowerCase()
320
+ .replace(/[^\w\s-]/g, '')
321
+ .replace(/\s+/g, '-')
322
+ .replace(/-+/g, '-');
323
+ }
324
+
325
+ module.exports = {
326
+ parseLocalPRD,
327
+ extractSections,
328
+ parseUserStories,
329
+ buildEpicBody,
330
+ generateEpicId,
331
+ slugify
332
+ };
333
+
334
+ // CLI execution
335
+ if (require.main === module) {
336
+ const args = process.argv.slice(2);
337
+ const prdId = args[0];
338
+
339
+ if (!prdId) {
340
+ console.error('Usage: pm-prd-parse-local.js <prd-id>');
341
+ process.exit(1);
342
+ }
343
+
344
+ parseLocalPRD(prdId)
345
+ .then(epic => {
346
+ console.log('✅ Epic created successfully!');
347
+ console.log(`Epic ID: ${epic.epicId}`);
348
+ console.log(`Epic Path: ${epic.epicPath}`);
349
+ console.log(`\nSections extracted:`);
350
+ console.log(`- Overview: ${epic.sections.overview ? '✓' : '✗'}`);
351
+ console.log(`- Goals: ${epic.sections.goals ? '✓' : '✗'}`);
352
+ console.log(`- User Stories: ${epic.sections.userStories.length} found`);
353
+ console.log(`- Requirements: ${epic.sections.requirements ? '✓' : '✗'}`);
354
+ console.log(`- Timeline: ${epic.sections.timeline ? '✓' : '✗'}`);
355
+ })
356
+ .catch(err => {
357
+ console.error('❌ Error parsing PRD:', err.message);
358
+ process.exit(1);
359
+ });
360
+ }