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,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
+ };
@@ -0,0 +1,424 @@
1
+ /**
2
+ * GitHub Sync Download - Local Mode
3
+ *
4
+ * Downloads GitHub Issues to local PRDs, Epics, and Tasks
5
+ * with intelligent conflict resolution and mapping.
6
+ *
7
+ * Usage:
8
+ * const { syncFromGitHub } = require('./pm-sync-download-local');
9
+ *
10
+ * await syncFromGitHub({
11
+ * basePath: '.claude',
12
+ * owner: 'user',
13
+ * repo: 'repository',
14
+ * octokit: octokitInstance,
15
+ * dryRun: false
16
+ * });
17
+ */
18
+
19
+ const fs = require('fs').promises;
20
+ const path = require('path');
21
+ const { parseFrontmatter, stringifyFrontmatter } = require('../lib/frontmatter');
22
+
23
+ /**
24
+ * Download PRD from GitHub Issue
25
+ *
26
+ * @param {Object} issue - GitHub issue object
27
+ * @param {string} prdsDir - PRDs directory path
28
+ * @param {Object} reverseMap - Reverse mapping (GitHub → local)
29
+ * @param {boolean} dryRun - Dry run mode
30
+ * @param {string} conflictMode - Conflict resolution: 'merge', 'github', 'local'
31
+ * @returns {Promise<Object>} Download result
32
+ */
33
+ async function downloadPRDFromGitHub(issue, prdsDir, reverseMap, dryRun = false, conflictMode = 'merge') {
34
+ // Parse title to get PRD name
35
+ const title = issue.title.replace(/^\[PRD\]\s*/, '');
36
+
37
+ if (dryRun) {
38
+ console.log(` [DRY-RUN] Would download: [PRD] ${title} (#${issue.number})`);
39
+ return { action: 'dry-run', title };
40
+ }
41
+
42
+ // Check if PRD already exists locally
43
+ const existingPrdId = reverseMap[issue.number];
44
+ let prdPath;
45
+ let frontmatter;
46
+ let body;
47
+
48
+ if (existingPrdId) {
49
+ // Update existing PRD
50
+ const files = await fs.readdir(prdsDir);
51
+ const existingFile = files.find(f => f.startsWith(`${existingPrdId}-`));
52
+
53
+ if (existingFile) {
54
+ prdPath = path.join(prdsDir, existingFile);
55
+ const existingContent = await fs.readFile(prdPath, 'utf8');
56
+ const parsed = parseFrontmatter(existingContent);
57
+
58
+ // Check for conflicts
59
+ let hasConflict = false;
60
+ if (parsed.frontmatter.updated && issue.updated_at) {
61
+ const localUpdated = new Date(parsed.frontmatter.updated);
62
+ const githubUpdated = new Date(issue.updated_at);
63
+
64
+ if (localUpdated > githubUpdated) {
65
+ hasConflict = true;
66
+
67
+ if (conflictMode === 'local') {
68
+ console.log(` ⚠️ Conflict: Local PRD newer than GitHub (#${issue.number}) - Keeping local`);
69
+ return { action: 'conflict-skipped', conflict: true, title };
70
+ } else if (conflictMode === 'merge') {
71
+ console.log(` ⚠️ Conflict: Local PRD newer than GitHub (#${issue.number}) - Merging`);
72
+ }
73
+ }
74
+ }
75
+
76
+ frontmatter = parsed.frontmatter;
77
+ body = parsed.body;
78
+
79
+ // If conflict in merge mode, return conflict indicator
80
+ if (hasConflict && conflictMode === 'merge') {
81
+ // Continue with update but flag conflict
82
+ const { metadata, content } = parseIssueBody(issue.body);
83
+ frontmatter.title = title;
84
+ frontmatter.status = metadata.status || frontmatter.status;
85
+ frontmatter.priority = extractPriority(issue.labels);
86
+ frontmatter.github_issue = issue.number;
87
+ frontmatter.updated = new Date().toISOString();
88
+
89
+ body = content;
90
+
91
+ const updatedContent = stringifyFrontmatter(frontmatter, body);
92
+ await fs.writeFile(prdPath, updatedContent);
93
+
94
+ console.log(` ✅ Updated PRD (conflict resolved): ${title} (#${issue.number})`);
95
+
96
+ return {
97
+ action: 'conflict-merged',
98
+ prdId: frontmatter.id,
99
+ title,
100
+ conflict: true
101
+ };
102
+ }
103
+ } else {
104
+ // File was deleted locally, recreate
105
+ return await createNewPRD(issue, prdsDir, reverseMap);
106
+ }
107
+ } else {
108
+ // Create new PRD
109
+ return await createNewPRD(issue, prdsDir, reverseMap);
110
+ }
111
+
112
+ // Update frontmatter from GitHub issue
113
+ const { metadata, content } = parseIssueBody(issue.body);
114
+
115
+ frontmatter.title = title;
116
+ frontmatter.status = metadata.status || 'draft';
117
+ frontmatter.priority = extractPriority(issue.labels);
118
+ frontmatter.github_issue = issue.number;
119
+ frontmatter.updated = new Date().toISOString();
120
+
121
+ // Update body
122
+ body = content;
123
+
124
+ // Write updated PRD
125
+ const updatedContent = stringifyFrontmatter(frontmatter, body);
126
+ await fs.writeFile(prdPath, updatedContent);
127
+
128
+ console.log(` ✅ Updated PRD: ${title} (#${issue.number})`);
129
+
130
+ return {
131
+ action: 'updated',
132
+ prdId: frontmatter.id,
133
+ title,
134
+ conflict: false
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Create new PRD from GitHub issue
140
+ */
141
+ async function createNewPRD(issue, prdsDir, reverseMap) {
142
+ const title = issue.title.replace(/^\[PRD\]\s*/, '');
143
+ const { metadata, content } = parseIssueBody(issue.body);
144
+
145
+ // Generate PRD ID
146
+ const existingFiles = await fs.readdir(prdsDir);
147
+ const prdNumbers = existingFiles
148
+ .filter(f => f.startsWith('prd-'))
149
+ .map(f => parseInt(f.match(/prd-(\d+)/)?.[1] || '0'))
150
+ .filter(n => !isNaN(n));
151
+
152
+ const nextNum = prdNumbers.length > 0 ? Math.max(...prdNumbers) + 1 : 1;
153
+ const prdId = `prd-${String(nextNum).padStart(3, '0')}`;
154
+
155
+ // Build frontmatter
156
+ const frontmatter = {
157
+ id: prdId,
158
+ title,
159
+ status: metadata.status || 'draft',
160
+ priority: extractPriority(issue.labels),
161
+ created: metadata.created || new Date(issue.created_at).toISOString().split('T')[0],
162
+ github_issue: issue.number
163
+ };
164
+
165
+ // Create PRD file
166
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
167
+ const prdFilename = `${prdId}-${slug}.md`;
168
+ const prdPath = path.join(prdsDir, prdFilename);
169
+
170
+ const prdContent = stringifyFrontmatter(frontmatter, content);
171
+ await fs.writeFile(prdPath, prdContent);
172
+
173
+ // Update reverse map
174
+ reverseMap[issue.number] = prdId;
175
+
176
+ console.log(` ✅ Created PRD: ${title} (#${issue.number})`);
177
+
178
+ return {
179
+ action: 'created',
180
+ prdId,
181
+ title
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Download Epic from GitHub Issue
187
+ *
188
+ * @param {Object} issue - GitHub issue object
189
+ * @param {string} epicsDir - Epics directory path
190
+ * @param {Object} reverseMap - Reverse mapping
191
+ * @param {boolean} dryRun - Dry run mode
192
+ * @returns {Promise<Object>} Download result
193
+ */
194
+ async function downloadEpicFromGitHub(issue, epicsDir, reverseMap, dryRun = false) {
195
+ const title = issue.title.replace(/^\[EPIC\]\s*/, '');
196
+
197
+ if (dryRun) {
198
+ console.log(` [DRY-RUN] Would download: [EPIC] ${title} (#${issue.number})`);
199
+ return { action: 'dry-run', title };
200
+ }
201
+
202
+ // Parse issue body
203
+ const { metadata, content } = parseIssueBody(issue.body);
204
+
205
+ // Extract parent PRD from metadata
206
+ const prdMatch = issue.body.match(/\*\*Parent PRD:\*\*\s*#(\d+)/);
207
+ const parentPrdIssue = prdMatch ? parseInt(prdMatch[1]) : null;
208
+ const prdId = parentPrdIssue && reverseMap[parentPrdIssue] ? reverseMap[parentPrdIssue] : null;
209
+
210
+ // Generate Epic ID
211
+ const existingDirs = await fs.readdir(epicsDir).catch(() => []);
212
+ const epicNumbers = existingDirs
213
+ .filter(d => d.startsWith('epic-'))
214
+ .map(d => parseInt(d.match(/epic-(\d+)/)?.[1] || '0'))
215
+ .filter(n => !isNaN(n));
216
+
217
+ const nextNum = epicNumbers.length > 0 ? Math.max(...epicNumbers) + 1 : 1;
218
+ const epicId = `epic-${String(nextNum).padStart(3, '0')}`;
219
+
220
+ // Build frontmatter
221
+ const frontmatter = {
222
+ id: epicId,
223
+ title,
224
+ status: metadata.status || 'pending',
225
+ priority: extractPriority(issue.labels),
226
+ created: new Date(issue.created_at).toISOString().split('T')[0],
227
+ github_issue: issue.number
228
+ };
229
+
230
+ if (prdId) {
231
+ frontmatter.prd_id = prdId;
232
+ }
233
+
234
+ // Parse progress if present
235
+ const progressMatch = issue.body.match(/\*\*Progress:\*\*\s*(\d+)\/(\d+)/);
236
+ if (progressMatch) {
237
+ frontmatter.tasks_completed = parseInt(progressMatch[1]);
238
+ frontmatter.tasks_total = parseInt(progressMatch[2]);
239
+ }
240
+
241
+ // Create epic directory and file
242
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
243
+ const epicDirName = `${epicId}-${slug}`;
244
+ const epicDirPath = path.join(epicsDir, epicDirName);
245
+
246
+ await fs.mkdir(epicDirPath, { recursive: true });
247
+
248
+ const epicFilePath = path.join(epicDirPath, 'epic.md');
249
+ const epicContent = stringifyFrontmatter(frontmatter, content);
250
+ await fs.writeFile(epicFilePath, epicContent);
251
+
252
+ // Update reverse map
253
+ reverseMap[issue.number] = epicId;
254
+
255
+ console.log(` ✅ Created Epic: ${title} (#${issue.number})`);
256
+
257
+ return {
258
+ action: 'created',
259
+ epicId,
260
+ title
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Download Task from GitHub Issue
266
+ *
267
+ * @param {Object} issue - GitHub issue object
268
+ * @param {string} epicsDir - Epics directory path
269
+ * @param {Object} reverseMap - Reverse mapping
270
+ * @param {boolean} dryRun - Dry run mode
271
+ * @returns {Promise<Object>} Download result
272
+ */
273
+ async function downloadTaskFromGitHub(issue, epicsDir, reverseMap, dryRun = false) {
274
+ const title = issue.title.replace(/^\[TASK\]\s*/, '');
275
+
276
+ if (dryRun) {
277
+ console.log(` [DRY-RUN] Would download: [TASK] ${title} (#${issue.number})`);
278
+ return { action: 'dry-run', title };
279
+ }
280
+
281
+ // Parse issue body
282
+ const { metadata, content } = parseIssueBody(issue.body);
283
+
284
+ // Extract parent epic
285
+ const epicMatch = issue.body.match(/\*\*Parent Epic:\*\*\s*#(\d+)/);
286
+ const parentEpicIssue = epicMatch ? parseInt(epicMatch[1]) : null;
287
+ const epicId = parentEpicIssue && reverseMap[parentEpicIssue] ? reverseMap[parentEpicIssue] : null;
288
+
289
+ if (!epicId) {
290
+ console.log(` ⚠️ Skipped task: No parent epic found for #${issue.number}`);
291
+ return { action: 'skipped', reason: 'no-parent-epic' };
292
+ }
293
+
294
+ // Find epic directory
295
+ const epicDirs = await fs.readdir(epicsDir);
296
+ const epicDirName = epicDirs.find(d => d.startsWith(`${epicId}-`));
297
+
298
+ if (!epicDirName) {
299
+ console.log(` ⚠️ Skipped task: Epic directory not found for ${epicId}`);
300
+ return { action: 'skipped', reason: 'epic-not-found' };
301
+ }
302
+
303
+ const epicDirPath = path.join(epicsDir, epicDirName);
304
+
305
+ // Generate task number
306
+ const existingTasks = await fs.readdir(epicDirPath);
307
+ const taskNumbers = existingTasks
308
+ .filter(f => f.startsWith('task-'))
309
+ .map(f => parseInt(f.match(/task-(\d+)/)?.[1] || '0'))
310
+ .filter(n => !isNaN(n));
311
+
312
+ const nextNum = taskNumbers.length > 0 ? Math.max(...taskNumbers) + 1 : 1;
313
+ const taskNum = String(nextNum).padStart(3, '0');
314
+ const taskId = `task-${epicId}-${taskNum}`;
315
+
316
+ // Build frontmatter
317
+ const frontmatter = {
318
+ id: taskId,
319
+ epic_id: epicId,
320
+ title,
321
+ status: metadata.status || 'pending',
322
+ priority: extractPriority(issue.labels),
323
+ estimated_hours: metadata.estimated_hours ? parseInt(metadata.estimated_hours) : 4,
324
+ created: new Date(issue.created_at).toISOString().split('T')[0],
325
+ github_issue: issue.number,
326
+ dependencies: []
327
+ };
328
+
329
+ if (metadata.dependencies) {
330
+ frontmatter.dependencies = metadata.dependencies.split(',').map(d => d.trim());
331
+ }
332
+
333
+ // Create task file
334
+ const taskFilePath = path.join(epicDirPath, `task-${taskNum}.md`);
335
+ const taskContent = stringifyFrontmatter(frontmatter, content);
336
+ await fs.writeFile(taskFilePath, taskContent);
337
+
338
+ // Update reverse map
339
+ reverseMap[issue.number] = taskId;
340
+
341
+ console.log(` ✅ Created Task: ${title} (#${issue.number})`);
342
+
343
+ return {
344
+ action: 'created',
345
+ taskId,
346
+ title
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Parse GitHub issue body into metadata and content
352
+ */
353
+ function parseIssueBody(body) {
354
+ const lines = body.split('\n');
355
+ const metadata = {};
356
+ let content = '';
357
+ let inMetadata = true;
358
+
359
+ for (const line of lines) {
360
+ if (line.trim() === '---') {
361
+ inMetadata = false;
362
+ continue;
363
+ }
364
+
365
+ if (inMetadata) {
366
+ // Parse metadata line
367
+ const match = line.match(/\*\*(.+?):\*\*\s*(.+)/);
368
+ if (match) {
369
+ const key = match[1].toLowerCase().replace(/\s+/g, '_');
370
+ metadata[key] = match[2].trim();
371
+ }
372
+ } else {
373
+ content += line + '\n';
374
+ }
375
+ }
376
+
377
+ return {
378
+ metadata,
379
+ content: content.trim()
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Extract priority from issue labels
385
+ */
386
+ function extractPriority(labels) {
387
+ const priorityLabels = ['critical', 'high', 'medium', 'low'];
388
+
389
+ for (const label of labels) {
390
+ const labelName = typeof label === 'string' ? label : label.name;
391
+ if (priorityLabels.includes(labelName)) {
392
+ return labelName;
393
+ }
394
+ }
395
+
396
+ return 'medium'; // default
397
+ }
398
+
399
+ /**
400
+ * Load sync map from file
401
+ */
402
+ async function loadSyncMap(syncMapPath) {
403
+ try {
404
+ const content = await fs.readFile(syncMapPath, 'utf8');
405
+ return JSON.parse(content);
406
+ } catch (err) {
407
+ return {};
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Save sync map to file
413
+ */
414
+ async function saveSyncMap(syncMapPath, syncMap) {
415
+ await fs.writeFile(syncMapPath, JSON.stringify(syncMap, null, 2), 'utf8');
416
+ }
417
+
418
+ module.exports = {
419
+ downloadPRDFromGitHub,
420
+ downloadEpicFromGitHub,
421
+ downloadTaskFromGitHub,
422
+ loadSyncMap,
423
+ saveSyncMap
424
+ };