claude-autopm 1.26.0 → 1.28.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 (49) hide show
  1. package/README.md +40 -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/dependency-analyzer.js +157 -0
  19. package/autopm/.claude/lib/frontmatter.js +224 -0
  20. package/autopm/.claude/lib/task-utils.js +64 -0
  21. package/autopm/.claude/scripts/pm/prd-new.js +292 -2
  22. package/autopm/.claude/scripts/pm/template-list.js +119 -0
  23. package/autopm/.claude/scripts/pm/template-new.js +344 -0
  24. package/autopm/.claude/scripts/pm-epic-decompose-local.js +158 -0
  25. package/autopm/.claude/scripts/pm-epic-list-local.js +103 -0
  26. package/autopm/.claude/scripts/pm-epic-show-local.js +70 -0
  27. package/autopm/.claude/scripts/pm-epic-update-local.js +56 -0
  28. package/autopm/.claude/scripts/pm-prd-list-local.js +111 -0
  29. package/autopm/.claude/scripts/pm-prd-new-local.js +196 -0
  30. package/autopm/.claude/scripts/pm-prd-parse-local.js +360 -0
  31. package/autopm/.claude/scripts/pm-prd-show-local.js +101 -0
  32. package/autopm/.claude/scripts/pm-prd-update-local.js +153 -0
  33. package/autopm/.claude/scripts/pm-sync-download-local.js +424 -0
  34. package/autopm/.claude/scripts/pm-sync-upload-local.js +473 -0
  35. package/autopm/.claude/scripts/pm-task-list-local.js +86 -0
  36. package/autopm/.claude/scripts/pm-task-show-local.js +92 -0
  37. package/autopm/.claude/scripts/pm-task-update-local.js +109 -0
  38. package/autopm/.claude/scripts/setup-local-mode.js +127 -0
  39. package/autopm/.claude/templates/prds/README.md +334 -0
  40. package/autopm/.claude/templates/prds/api-feature.md +306 -0
  41. package/autopm/.claude/templates/prds/bug-fix.md +413 -0
  42. package/autopm/.claude/templates/prds/data-migration.md +483 -0
  43. package/autopm/.claude/templates/prds/documentation.md +439 -0
  44. package/autopm/.claude/templates/prds/ui-feature.md +365 -0
  45. package/lib/template-engine.js +347 -0
  46. package/package.json +5 -3
  47. package/scripts/create-task-issues.sh +26 -0
  48. package/scripts/fix-invalid-command-refs.sh +4 -3
  49. package/scripts/fix-invalid-refs-simple.sh +8 -3
@@ -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
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Local PRD Show Command
3
+ *
4
+ * Displays a specific Product Requirements Document (PRD) by ID.
5
+ *
6
+ * Usage:
7
+ * /pm:prd-show --local <id>
8
+ *
9
+ * @module pm-prd-show-local
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const path = require('path');
14
+ const { parseFrontmatter } = require('../lib/frontmatter');
15
+
16
+ /**
17
+ * Shows a specific PRD by ID
18
+ *
19
+ * @param {string} id - PRD ID to display
20
+ * @returns {Promise<Object>} PRD data including frontmatter and body
21
+ * @throws {Error} If PRD not found or ID is invalid
22
+ */
23
+ async function showLocalPRD(id) {
24
+ if (!id || typeof id !== 'string' || id.trim().length === 0) {
25
+ throw new Error('PRD ID is required');
26
+ }
27
+
28
+ const prdsDir = path.join(process.cwd(), '.claude', 'prds');
29
+
30
+ // Ensure directory exists
31
+ try {
32
+ await fs.access(prdsDir);
33
+ } catch (err) {
34
+ if (err.code === 'ENOENT') {
35
+ throw new Error(`PRD not found: ${id}`);
36
+ }
37
+ throw err;
38
+ }
39
+
40
+ // Read all PRD files
41
+ const files = await fs.readdir(prdsDir);
42
+
43
+ // Search for PRD by ID
44
+ for (const file of files) {
45
+ if (!file.endsWith('.md')) continue;
46
+
47
+ const filepath = path.join(prdsDir, file);
48
+
49
+ try {
50
+ const content = await fs.readFile(filepath, 'utf8');
51
+ const { frontmatter, body } = parseFrontmatter(content);
52
+
53
+ if (frontmatter && frontmatter.id === id) {
54
+ return {
55
+ filepath,
56
+ filename: file,
57
+ frontmatter,
58
+ body,
59
+ content
60
+ };
61
+ }
62
+ } catch (err) {
63
+ // Skip files that can't be parsed
64
+ continue;
65
+ }
66
+ }
67
+
68
+ // PRD not found
69
+ throw new Error(`PRD not found: ${id}`);
70
+ }
71
+
72
+ /**
73
+ * Formats PRD for display
74
+ *
75
+ * @param {Object} prd - PRD object from showLocalPRD
76
+ * @returns {string} Formatted PRD for display
77
+ */
78
+ function formatPRD(prd) {
79
+ const { frontmatter, body } = prd;
80
+
81
+ const header = [
82
+ '',
83
+ `PRD: ${frontmatter.title}`,
84
+ `ID: ${frontmatter.id}`,
85
+ `Status: ${frontmatter.status}`,
86
+ `Priority: ${frontmatter.priority}`,
87
+ `Created: ${frontmatter.created}`,
88
+ `Author: ${frontmatter.author}`,
89
+ `Version: ${frontmatter.version}`,
90
+ '',
91
+ '─'.repeat(80),
92
+ ''
93
+ ].join('\n');
94
+
95
+ return header + body;
96
+ }
97
+
98
+ module.exports = {
99
+ showLocalPRD,
100
+ formatPRD
101
+ };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Local PRD Update Command
3
+ *
4
+ * Updates frontmatter fields in a Product Requirements Document (PRD).
5
+ *
6
+ * Usage:
7
+ * /pm:prd-update --local <id> <field> <value>
8
+ *
9
+ * @module pm-prd-update-local
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const path = require('path');
14
+ const { parseFrontmatter, stringifyFrontmatter } = require('../lib/frontmatter');
15
+
16
+ /**
17
+ * Updates a frontmatter field in a PRD
18
+ *
19
+ * @param {string} id - PRD ID
20
+ * @param {string} field - Frontmatter field to update
21
+ * @param {*} value - New value for the field
22
+ * @returns {Promise<Object>} Updated PRD data
23
+ * @throws {Error} If PRD not found or parameters invalid
24
+ */
25
+ async function updateLocalPRD(id, field, value) {
26
+ // Validate parameters
27
+ if (!id || typeof id !== 'string' || id.trim().length === 0) {
28
+ throw new Error('PRD ID is required');
29
+ }
30
+
31
+ if (!field || typeof field !== 'string' || field.trim().length === 0) {
32
+ throw new Error('Field is required');
33
+ }
34
+
35
+ const prdsDir = path.join(process.cwd(), '.claude', 'prds');
36
+
37
+ // Ensure directory exists
38
+ try {
39
+ await fs.access(prdsDir);
40
+ } catch (err) {
41
+ if (err.code === 'ENOENT') {
42
+ throw new Error(`PRD not found: ${id}`);
43
+ }
44
+ throw err;
45
+ }
46
+
47
+ // Read all PRD files to find the target
48
+ const files = await fs.readdir(prdsDir);
49
+
50
+ for (const file of files) {
51
+ if (!file.endsWith('.md')) continue;
52
+
53
+ const filepath = path.join(prdsDir, file);
54
+
55
+ try {
56
+ const content = await fs.readFile(filepath, 'utf8');
57
+ const { frontmatter, body } = parseFrontmatter(content);
58
+
59
+ if (frontmatter && frontmatter.id === id) {
60
+ // Update the field
61
+ const updatedFrontmatter = {
62
+ ...frontmatter,
63
+ [field]: value
64
+ };
65
+
66
+ // Reconstruct the file
67
+ const updatedContent = stringifyFrontmatter(updatedFrontmatter, body);
68
+
69
+ // Write back to file
70
+ await fs.writeFile(filepath, updatedContent, 'utf8');
71
+
72
+ return {
73
+ filepath,
74
+ filename: file,
75
+ frontmatter: updatedFrontmatter,
76
+ body
77
+ };
78
+ }
79
+ } catch (err) {
80
+ // Skip files that can't be parsed
81
+ continue;
82
+ }
83
+ }
84
+
85
+ // PRD not found
86
+ throw new Error(`PRD not found: ${id}`);
87
+ }
88
+
89
+ /**
90
+ * Updates multiple fields at once
91
+ *
92
+ * @param {string} id - PRD ID
93
+ * @param {Object} updates - Object with field: value pairs
94
+ * @returns {Promise<Object>} Updated PRD data
95
+ */
96
+ async function updateMultipleFields(id, updates) {
97
+ // Validate parameters
98
+ if (!id || typeof id !== 'string' || id.trim().length === 0) {
99
+ throw new Error('PRD ID is required');
100
+ }
101
+ if (!updates || typeof updates !== 'object') {
102
+ throw new Error('Updates must be an object');
103
+ }
104
+
105
+ const prdsDir = path.join(process.cwd(), '.claude', 'prds');
106
+ let files;
107
+ try {
108
+ files = await fs.readdir(prdsDir);
109
+ } catch (err) {
110
+ if (err.code === 'ENOENT') {
111
+ throw new Error(`PRD not found: ${id}`);
112
+ }
113
+ throw err;
114
+ }
115
+
116
+ for (const file of files) {
117
+ if (!file.endsWith('.md')) continue;
118
+
119
+ const filepath = path.join(prdsDir, file);
120
+
121
+ try {
122
+ const content = await fs.readFile(filepath, 'utf8');
123
+ const { frontmatter, body } = parseFrontmatter(content);
124
+
125
+ if (frontmatter && frontmatter.id === id) {
126
+ // Apply all updates
127
+ const updatedFrontmatter = {
128
+ ...frontmatter,
129
+ ...updates
130
+ };
131
+
132
+ const updatedContent = stringifyFrontmatter(updatedFrontmatter, body);
133
+ await fs.writeFile(filepath, updatedContent, 'utf8');
134
+
135
+ return {
136
+ filepath,
137
+ filename: file,
138
+ frontmatter: updatedFrontmatter,
139
+ body
140
+ };
141
+ }
142
+ } catch (err) {
143
+ continue;
144
+ }
145
+ }
146
+
147
+ throw new Error(`PRD not found: ${id}`);
148
+ }
149
+
150
+ module.exports = {
151
+ updateLocalPRD,
152
+ updateMultipleFields
153
+ };