claude-autopm 1.26.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 (38) hide show
  1. package/autopm/.claude/agents/frameworks/e2e-test-engineer.md +1 -18
  2. package/autopm/.claude/agents/frameworks/nats-messaging-expert.md +1 -18
  3. package/autopm/.claude/agents/frameworks/react-frontend-engineer.md +1 -18
  4. package/autopm/.claude/agents/frameworks/react-ui-expert.md +1 -18
  5. package/autopm/.claude/agents/frameworks/tailwindcss-expert.md +1 -18
  6. package/autopm/.claude/agents/frameworks/ux-design-expert.md +1 -18
  7. package/autopm/.claude/agents/languages/bash-scripting-expert.md +1 -18
  8. package/autopm/.claude/agents/languages/javascript-frontend-engineer.md +1 -18
  9. package/autopm/.claude/agents/languages/nodejs-backend-engineer.md +1 -18
  10. package/autopm/.claude/agents/languages/python-backend-engineer.md +1 -18
  11. package/autopm/.claude/agents/languages/python-backend-expert.md +1 -18
  12. package/autopm/.claude/commands/pm/epic-decompose.md +19 -5
  13. package/autopm/.claude/commands/pm/prd-new.md +14 -1
  14. package/autopm/.claude/includes/task-creation-excellence.md +18 -0
  15. package/autopm/.claude/lib/ai-task-generator.js +84 -0
  16. package/autopm/.claude/lib/cli-parser.js +148 -0
  17. package/autopm/.claude/lib/dependency-analyzer.js +157 -0
  18. package/autopm/.claude/lib/frontmatter.js +224 -0
  19. package/autopm/.claude/lib/task-utils.js +64 -0
  20. package/autopm/.claude/scripts/pm-epic-decompose-local.js +158 -0
  21. package/autopm/.claude/scripts/pm-epic-list-local.js +103 -0
  22. package/autopm/.claude/scripts/pm-epic-show-local.js +70 -0
  23. package/autopm/.claude/scripts/pm-epic-update-local.js +56 -0
  24. package/autopm/.claude/scripts/pm-prd-list-local.js +111 -0
  25. package/autopm/.claude/scripts/pm-prd-new-local.js +196 -0
  26. package/autopm/.claude/scripts/pm-prd-parse-local.js +360 -0
  27. package/autopm/.claude/scripts/pm-prd-show-local.js +101 -0
  28. package/autopm/.claude/scripts/pm-prd-update-local.js +153 -0
  29. package/autopm/.claude/scripts/pm-sync-download-local.js +424 -0
  30. package/autopm/.claude/scripts/pm-sync-upload-local.js +473 -0
  31. package/autopm/.claude/scripts/pm-task-list-local.js +86 -0
  32. package/autopm/.claude/scripts/pm-task-show-local.js +92 -0
  33. package/autopm/.claude/scripts/pm-task-update-local.js +109 -0
  34. package/autopm/.claude/scripts/setup-local-mode.js +127 -0
  35. package/package.json +5 -3
  36. package/scripts/create-task-issues.sh +26 -0
  37. package/scripts/fix-invalid-command-refs.sh +4 -3
  38. package/scripts/fix-invalid-refs-simple.sh +8 -3
@@ -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
+ };