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,473 @@
1
+ /**
2
+ * GitHub Sync Upload - Local Mode
3
+ *
4
+ * Uploads local PRDs, Epics, and Tasks to GitHub Issues
5
+ * with bidirectional mapping and intelligent sync.
6
+ *
7
+ * Usage:
8
+ * const { syncPRDToGitHub, syncEpicToGitHub, syncTaskToGitHub,
9
+ * loadSyncMap, saveSyncMap } = require('./pm-sync-upload-local');
10
+ * const { Octokit } = require('@octokit/rest');
11
+ *
12
+ * // Initialize
13
+ * const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
14
+ * const repo = { owner: 'user', repo: 'repository' };
15
+ * const syncMap = await loadSyncMap('.claude/sync-map.json');
16
+ *
17
+ * // Sync a PRD
18
+ * await syncPRDToGitHub('.claude/prds/prd-001.md', repo, octokit, syncMap);
19
+ *
20
+ * // Sync an Epic
21
+ * await syncEpicToGitHub('.claude/epics/epic-001/epic.md', repo, octokit, syncMap);
22
+ *
23
+ * // Sync a Task
24
+ * await syncTaskToGitHub('.claude/epics/epic-001/task-001.md', repo, octokit, syncMap);
25
+ *
26
+ * // Save sync map
27
+ * await saveSyncMap('.claude/sync-map.json', syncMap);
28
+ */
29
+
30
+ const fs = require('fs').promises;
31
+ const path = require('path');
32
+ const { parseFrontmatter, stringifyFrontmatter } = require('../lib/frontmatter');
33
+
34
+ /**
35
+ * Sync PRD to GitHub Issue
36
+ *
37
+ * @param {string} prdPath - Path to PRD markdown file
38
+ * @param {Object} repo - Repository info {owner, repo}
39
+ * @param {Object} octokit - Octokit instance
40
+ * @param {Object} syncMap - Sync mapping object
41
+ * @param {boolean} dryRun - Dry run mode
42
+ * @returns {Promise<Object>} Sync result
43
+ */
44
+ async function syncPRDToGitHub(prdPath, repo, octokit, syncMap, dryRun = false) {
45
+ const content = await fs.readFile(prdPath, 'utf8');
46
+ const { frontmatter, body } = parseFrontmatter(content);
47
+
48
+ const title = `[PRD] ${frontmatter.title}`;
49
+ const labels = ['prd'];
50
+
51
+ if (frontmatter.priority) {
52
+ labels.push(frontmatter.priority);
53
+ }
54
+
55
+ const issueBody = buildPRDBody(frontmatter, body);
56
+
57
+ if (dryRun) {
58
+ console.log(` [DRY-RUN] Would create/update: ${title}`);
59
+ return { action: 'dry-run', title };
60
+ }
61
+
62
+ // Check if issue already exists
63
+ const existingIssue = frontmatter.github_issue || syncMap[frontmatter.id];
64
+
65
+ if (existingIssue) {
66
+ // Update existing issue
67
+ await octokit.issues.update({
68
+ owner: repo.owner,
69
+ repo: repo.repo,
70
+ issue_number: existingIssue,
71
+ title,
72
+ body: issueBody,
73
+ labels
74
+ });
75
+
76
+ console.log(` ✅ Updated PRD: ${title} (#${existingIssue})`);
77
+
78
+ // Ensure sync map is up-to-date
79
+ syncMap[frontmatter.id] = existingIssue;
80
+
81
+ // Update frontmatter if github_issue is missing or differs
82
+ if (frontmatter.github_issue !== existingIssue) {
83
+ frontmatter.github_issue = existingIssue;
84
+ frontmatter.updated = new Date().toISOString().split('T')[0];
85
+ const updatedContent = stringifyFrontmatter(frontmatter, body);
86
+ await fs.writeFile(prdPath, updatedContent);
87
+ }
88
+
89
+ return {
90
+ action: 'updated',
91
+ issueNumber: existingIssue,
92
+ title
93
+ };
94
+ } else {
95
+ // Create new issue
96
+ const response = await octokit.issues.create({
97
+ owner: repo.owner,
98
+ repo: repo.repo,
99
+ title,
100
+ body: issueBody,
101
+ labels
102
+ });
103
+
104
+ const issueNumber = response.data.number;
105
+
106
+ // Update local frontmatter
107
+ frontmatter.github_issue = issueNumber;
108
+ const updatedContent = stringifyFrontmatter(frontmatter, body);
109
+ await fs.writeFile(prdPath, updatedContent);
110
+
111
+ // Update sync map
112
+ syncMap[frontmatter.id] = issueNumber;
113
+
114
+ console.log(` ✅ Created PRD: ${title} (#${issueNumber})`);
115
+
116
+ return {
117
+ action: 'created',
118
+ issueNumber,
119
+ title
120
+ };
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Sync Epic to GitHub Issue
126
+ *
127
+ * @param {string} epicPath - Path to epic.md file
128
+ * @param {Object} repo - Repository info
129
+ * @param {Object} octokit - Octokit instance
130
+ * @param {Object} syncMap - Sync mapping
131
+ * @param {boolean} dryRun - Dry run mode
132
+ * @returns {Promise<Object>} Sync result
133
+ */
134
+ async function syncEpicToGitHub(epicPath, repo, octokit, syncMap, dryRun = false) {
135
+ const content = await fs.readFile(epicPath, 'utf8');
136
+ const { frontmatter, body } = parseFrontmatter(content);
137
+
138
+ const title = `[EPIC] ${frontmatter.title}`;
139
+ const labels = ['epic'];
140
+
141
+ if (frontmatter.priority) {
142
+ labels.push(frontmatter.priority);
143
+ }
144
+
145
+ const issueBody = buildEpicBody(frontmatter, body, syncMap);
146
+
147
+ if (dryRun) {
148
+ console.log(` [DRY-RUN] Would create/update: ${title}`);
149
+ return { action: 'dry-run', title };
150
+ }
151
+
152
+ const existingIssue = frontmatter.github_issue || syncMap[frontmatter.id];
153
+
154
+ if (existingIssue) {
155
+ await octokit.issues.update({
156
+ owner: repo.owner,
157
+ repo: repo.repo,
158
+ issue_number: existingIssue,
159
+ title,
160
+ body: issueBody,
161
+ labels
162
+ });
163
+
164
+ console.log(` ✅ Updated Epic: ${title} (#${existingIssue})`);
165
+
166
+ // Ensure sync map is up-to-date
167
+ syncMap[frontmatter.id] = existingIssue;
168
+
169
+ // Update frontmatter if github_issue is missing or differs
170
+ if (frontmatter.github_issue !== existingIssue) {
171
+ frontmatter.github_issue = existingIssue;
172
+ frontmatter.updated = new Date().toISOString().split('T')[0];
173
+ const updatedContent = stringifyFrontmatter(frontmatter, body);
174
+ await fs.writeFile(epicPath, updatedContent);
175
+ }
176
+
177
+ return {
178
+ action: 'updated',
179
+ issueNumber: existingIssue,
180
+ title
181
+ };
182
+ } else {
183
+ const response = await octokit.issues.create({
184
+ owner: repo.owner,
185
+ repo: repo.repo,
186
+ title,
187
+ body: issueBody,
188
+ labels
189
+ });
190
+
191
+ const issueNumber = response.data.number;
192
+
193
+ frontmatter.github_issue = issueNumber;
194
+ const updatedContent = stringifyFrontmatter(frontmatter, body);
195
+ await fs.writeFile(epicPath, updatedContent);
196
+
197
+ syncMap[frontmatter.id] = issueNumber;
198
+
199
+ console.log(` ✅ Created Epic: ${title} (#${issueNumber})`);
200
+
201
+ return {
202
+ action: 'created',
203
+ issueNumber,
204
+ title
205
+ };
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Sync Task to GitHub Issue
211
+ *
212
+ * @param {string} taskPath - Path to task.md file
213
+ * @param {Object} repo - Repository info
214
+ * @param {Object} octokit - Octokit instance
215
+ * @param {Object} syncMap - Sync mapping
216
+ * @param {boolean} dryRun - Dry run mode
217
+ * @returns {Promise<Object>} Sync result
218
+ */
219
+ async function syncTaskToGitHub(taskPath, repo, octokit, syncMap, dryRun = false) {
220
+ const content = await fs.readFile(taskPath, 'utf8');
221
+ const { frontmatter, body } = parseFrontmatter(content);
222
+
223
+ const title = `[TASK] ${frontmatter.title}`;
224
+ const labels = ['task'];
225
+
226
+ if (frontmatter.priority) {
227
+ labels.push(frontmatter.priority);
228
+ }
229
+
230
+ const issueBody = buildTaskBody(frontmatter, body, syncMap);
231
+
232
+ if (dryRun) {
233
+ console.log(` [DRY-RUN] Would create/update: ${title}`);
234
+ return { action: 'dry-run', title };
235
+ }
236
+
237
+ const existingIssue = frontmatter.github_issue || syncMap[frontmatter.id];
238
+
239
+ if (existingIssue) {
240
+ await octokit.issues.update({
241
+ owner: repo.owner,
242
+ repo: repo.repo,
243
+ issue_number: existingIssue,
244
+ title,
245
+ body: issueBody,
246
+ labels
247
+ });
248
+
249
+ console.log(` ✅ Updated Task: ${title} (#${existingIssue})`);
250
+
251
+ // Ensure sync map is up-to-date
252
+ syncMap[frontmatter.id] = existingIssue;
253
+
254
+ // Update frontmatter if github_issue is missing or differs
255
+ if (frontmatter.github_issue !== existingIssue) {
256
+ frontmatter.github_issue = existingIssue;
257
+ frontmatter.updated = new Date().toISOString().split('T')[0];
258
+ const updatedContent = stringifyFrontmatter(frontmatter, body);
259
+ await fs.writeFile(taskPath, updatedContent);
260
+ }
261
+
262
+ return {
263
+ action: 'updated',
264
+ issueNumber: existingIssue,
265
+ title
266
+ };
267
+ } else {
268
+ const response = await octokit.issues.create({
269
+ owner: repo.owner,
270
+ repo: repo.repo,
271
+ title,
272
+ body: issueBody,
273
+ labels
274
+ });
275
+
276
+ const issueNumber = response.data.number;
277
+
278
+ frontmatter.github_issue = issueNumber;
279
+ const updatedContent = stringifyFrontmatter(frontmatter, body);
280
+ await fs.writeFile(taskPath, updatedContent);
281
+
282
+ syncMap[frontmatter.id] = issueNumber;
283
+
284
+ console.log(` ✅ Created Task: ${title} (#${issueNumber})`);
285
+
286
+ return {
287
+ action: 'created',
288
+ issueNumber,
289
+ title
290
+ };
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Build PRD issue body
296
+ */
297
+ function buildPRDBody(frontmatter, body) {
298
+ let issueBody = '';
299
+
300
+ // Metadata
301
+ if (frontmatter.status != null) {
302
+ issueBody += `**Status:** ${frontmatter.status}\n`;
303
+ }
304
+ if (frontmatter.priority != null) {
305
+ issueBody += `**Priority:** ${frontmatter.priority}\n`;
306
+ }
307
+ if (frontmatter.created != null) {
308
+ issueBody += `**Created:** ${frontmatter.created}\n`;
309
+ }
310
+ issueBody += `\n---\n\n`;
311
+
312
+ // Body content
313
+ issueBody += body;
314
+
315
+ return issueBody;
316
+ }
317
+
318
+ /**
319
+ * Build Epic issue body
320
+ */
321
+ function buildEpicBody(frontmatter, body, syncMap) {
322
+ let issueBody = '';
323
+
324
+ // Link to parent PRD
325
+ if (frontmatter.prd_id && syncMap[frontmatter.prd_id]) {
326
+ issueBody += `**Parent PRD:** #${syncMap[frontmatter.prd_id]}\n`;
327
+ }
328
+
329
+ // Metadata
330
+ if (frontmatter.status != null) {
331
+ issueBody += `**Status:** ${frontmatter.status}\n`;
332
+ }
333
+ if (frontmatter.priority != null) {
334
+ issueBody += `**Priority:** ${frontmatter.priority}\n`;
335
+ }
336
+
337
+ if (frontmatter.tasks_total) {
338
+ const completion = frontmatter.tasks_completed || 0;
339
+ const total = frontmatter.tasks_total;
340
+ const percent = total > 0 ? Math.round((completion / total) * 100) : 0;
341
+ issueBody += `**Progress:** ${completion}/${total} tasks (${percent}%)\n`;
342
+ }
343
+
344
+ issueBody += `\n---\n\n`;
345
+
346
+ // Body content
347
+ issueBody += body;
348
+
349
+ return issueBody;
350
+ }
351
+
352
+ /**
353
+ * Build Task issue body
354
+ */
355
+ function buildTaskBody(frontmatter, body, syncMap) {
356
+ let issueBody = '';
357
+
358
+ // Link to parent epic
359
+ if (frontmatter.epic_id && syncMap[frontmatter.epic_id]) {
360
+ issueBody += `**Parent Epic:** #${syncMap[frontmatter.epic_id]}\n`;
361
+ }
362
+
363
+ // Metadata
364
+ if (frontmatter.status != null) {
365
+ issueBody += `**Status:** ${frontmatter.status}\n`;
366
+ }
367
+ if (frontmatter.priority != null) {
368
+ issueBody += `**Priority:** ${frontmatter.priority}\n`;
369
+ }
370
+ if (frontmatter.estimated_hours != null) {
371
+ issueBody += `**Estimated Hours:** ${frontmatter.estimated_hours}\n`;
372
+ }
373
+
374
+ if (frontmatter.dependencies && frontmatter.dependencies.length > 0) {
375
+ issueBody += `**Dependencies:** ${frontmatter.dependencies.join(', ')}\n`;
376
+ }
377
+
378
+ issueBody += `\n---\n\n`;
379
+
380
+ // Body content
381
+ issueBody += body;
382
+
383
+ return issueBody;
384
+ }
385
+
386
+ /**
387
+ * Load sync map from file
388
+ *
389
+ * @param {string} syncMapPath - Path to sync-map.json
390
+ * @returns {Promise<Object>} Sync map object
391
+ */
392
+ async function loadSyncMap(syncMapPath) {
393
+ try {
394
+ const content = await fs.readFile(syncMapPath, 'utf8');
395
+ return JSON.parse(content);
396
+ } catch (err) {
397
+ return {}; // New sync map
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Save sync map to file
403
+ *
404
+ * @param {string} syncMapPath - Path to sync-map.json
405
+ * @param {Object} syncMap - Sync map object
406
+ * @returns {Promise<void>}
407
+ */
408
+ async function saveSyncMap(syncMapPath, syncMap) {
409
+ await fs.writeFile(syncMapPath, JSON.stringify(syncMap, null, 2), 'utf8');
410
+ }
411
+
412
+ /**
413
+ * Orchestrator: Sync all PRDs, Epics, and Tasks to GitHub
414
+ *
415
+ * @param {Object} options
416
+ * - basePath: string, root directory containing PRDs, Epics, Tasks
417
+ * - owner: string, GitHub repo owner
418
+ * - repo: string, GitHub repo name
419
+ * - octokit: Octokit instance
420
+ * - dryRun: boolean, if true, do not write to GitHub
421
+ */
422
+ async function syncToGitHub({ basePath, owner, repo, octokit, dryRun = false }) {
423
+ const syncMapPath = path.join(basePath, 'sync-map.json');
424
+ let syncMap = await loadSyncMap(syncMapPath);
425
+
426
+ // Sync PRDs
427
+ const prdDir = path.join(basePath, 'prds');
428
+ let prdFiles = [];
429
+ try {
430
+ prdFiles = (await fs.readdir(prdDir))
431
+ .filter(f => f.endsWith('.md'))
432
+ .map(f => path.join(prdDir, f));
433
+ } catch (e) {}
434
+ for (const prdPath of prdFiles) {
435
+ await syncPRDToGitHub(prdPath, { owner, repo }, octokit, syncMap, dryRun);
436
+ }
437
+
438
+ // Sync Epics
439
+ const epicDir = path.join(basePath, 'epics');
440
+ let epicFiles = [];
441
+ try {
442
+ epicFiles = (await fs.readdir(epicDir))
443
+ .filter(f => f.endsWith('.md'))
444
+ .map(f => path.join(epicDir, f));
445
+ } catch (e) {}
446
+ for (const epicPath of epicFiles) {
447
+ await syncEpicToGitHub(epicPath, { owner, repo }, octokit, syncMap, dryRun);
448
+ }
449
+
450
+ // Sync Tasks
451
+ const taskDir = path.join(basePath, 'tasks');
452
+ let taskFiles = [];
453
+ try {
454
+ taskFiles = (await fs.readdir(taskDir))
455
+ .filter(f => f.endsWith('.md'))
456
+ .map(f => path.join(taskDir, f));
457
+ } catch (e) {}
458
+ for (const taskPath of taskFiles) {
459
+ await syncTaskToGitHub(taskPath, { owner, repo }, octokit, syncMap, dryRun);
460
+ }
461
+
462
+ // Save sync map
463
+ await saveSyncMap(syncMapPath, syncMap);
464
+ }
465
+
466
+ module.exports = {
467
+ syncPRDToGitHub,
468
+ syncEpicToGitHub,
469
+ syncTaskToGitHub,
470
+ loadSyncMap,
471
+ saveSyncMap,
472
+ syncToGitHub
473
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * List Local Tasks
3
+ *
4
+ * Lists all tasks for a specific epic with optional filtering.
5
+ * Tasks are read from `.claude/epics/<epic-id>/task-*.md` files.
6
+ *
7
+ * Usage:
8
+ * const { listLocalTasks } = require('./pm-task-list-local');
9
+ *
10
+ * // List all tasks for epic
11
+ * const tasks = await listLocalTasks('epic-001');
12
+ *
13
+ * // Filter by status
14
+ * const pending = await listLocalTasks('epic-001', { status: 'pending' });
15
+ */
16
+
17
+ const fs = require('fs').promises;
18
+ const path = require('path');
19
+ const { parseFrontmatter } = require('../lib/frontmatter');
20
+
21
+ /**
22
+ * List all tasks for an epic
23
+ *
24
+ * @param {string} epicId - Epic ID
25
+ * @param {Object} options - Filter options
26
+ * @param {string} [options.status] - Filter by task status
27
+ * @returns {Promise<Array>} Array of task objects with frontmatter
28
+ * @throws {Error} If epic not found
29
+ */
30
+ async function listLocalTasks(epicId, options = {}) {
31
+ const basePath = process.cwd();
32
+ const epicsDir = path.join(basePath, '.claude', 'epics');
33
+
34
+ // Find epic directory
35
+ const dirs = await fs.readdir(epicsDir);
36
+ const epicDir = dirs.find(dir => dir.startsWith(`${epicId}-`));
37
+
38
+ if (!epicDir) {
39
+ throw new Error(`Epic not found: ${epicId}`);
40
+ }
41
+
42
+ const epicDirPath = path.join(epicsDir, epicDir);
43
+
44
+ // Read all files in epic directory
45
+ const files = await fs.readdir(epicDirPath);
46
+ const taskFiles = files.filter(f => f.startsWith('task-') && f.endsWith('.md'));
47
+
48
+ const tasks = [];
49
+
50
+ // Process each task file
51
+ for (const taskFile of taskFiles) {
52
+ try {
53
+ const taskPath = path.join(epicDirPath, taskFile);
54
+ const content = await fs.readFile(taskPath, 'utf8');
55
+ const { frontmatter } = parseFrontmatter(content);
56
+
57
+ if (frontmatter && frontmatter.id) {
58
+ tasks.push({
59
+ ...frontmatter,
60
+ filename: taskFile
61
+ });
62
+ }
63
+ } catch (err) {
64
+ // Skip invalid task files
65
+ console.warn(`Warning: Could not process task ${taskFile}:`, err.message);
66
+ }
67
+ }
68
+
69
+ // Apply filters
70
+ let filtered = tasks;
71
+
72
+ if (options.status) {
73
+ filtered = filtered.filter(task => task.status === options.status);
74
+ }
75
+
76
+ // Sort by task number (task-001, task-002, etc.)
77
+ filtered.sort((a, b) => {
78
+ const numA = parseInt(a.filename.match(/task-(\d+)/)?.[1] || '0');
79
+ const numB = parseInt(b.filename.match(/task-(\d+)/)?.[1] || '0');
80
+ return numA - numB;
81
+ });
82
+
83
+ return filtered;
84
+ }
85
+
86
+ module.exports = { listLocalTasks };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Show Local Task
3
+ *
4
+ * Displays details of a specific task including blocking/blocked relationships.
5
+ * Provides epic context and dependency information.
6
+ *
7
+ * Usage:
8
+ * const { showLocalTask } = require('./pm-task-show-local');
9
+ *
10
+ * const task = await showLocalTask('epic-001', 'task-001');
11
+ * console.log(task.frontmatter.title);
12
+ * console.log(task.blocking); // Tasks this blocks
13
+ * console.log(task.blockedBy); // Tasks blocking this
14
+ */
15
+
16
+ const fs = require('fs').promises;
17
+ const path = require('path');
18
+ const { parseFrontmatter } = require('../lib/frontmatter');
19
+ const { showLocalEpic } = require('./pm-epic-show-local');
20
+ const { listLocalTasks } = require('./pm-task-list-local');
21
+
22
+ /**
23
+ * Get task details by ID
24
+ *
25
+ * @param {string} epicId - Epic ID containing the task
26
+ * @param {string} taskId - Task ID (e.g., 'task-001')
27
+ * @returns {Promise<Object>} Task object with frontmatter, body, and relationships
28
+ * @throws {Error} If task not found
29
+ */
30
+ async function showLocalTask(epicId, taskId) {
31
+ const basePath = process.cwd();
32
+ const epicsDir = path.join(basePath, '.claude', 'epics');
33
+
34
+ // Find epic directory
35
+ const dirs = await fs.readdir(epicsDir);
36
+ const epicDir = dirs.find(dir => dir.startsWith(`${epicId}-`));
37
+
38
+ if (!epicDir) {
39
+ throw new Error(`Epic not found: ${epicId}`);
40
+ }
41
+
42
+ const epicDirPath = path.join(epicsDir, epicDir);
43
+
44
+ // Find task file
45
+ const taskFilename = taskId.endsWith('.md') ? taskId : `${taskId}.md`;
46
+ const taskPath = path.join(epicDirPath, taskFilename);
47
+
48
+ try {
49
+ // Read task file
50
+ const content = await fs.readFile(taskPath, 'utf8');
51
+ const { frontmatter, body } = parseFrontmatter(content);
52
+
53
+ // Get epic context
54
+ const epic = await showLocalEpic(epicId);
55
+
56
+ // Get all tasks to find blocking relationships
57
+ const allTasks = await listLocalTasks(epicId);
58
+
59
+ // Find tasks this task blocks (tasks that depend on this one)
60
+ const taskFullId = frontmatter.id || taskId.replace('.md', '');
61
+ const blocking = allTasks
62
+ .filter(t => {
63
+ const deps = t.dependencies || [];
64
+ return deps.some(dep =>
65
+ dep === taskFullId ||
66
+ dep === taskId.replace('.md', '') ||
67
+ dep === `task-${taskId.replace('.md', '').replace('task-', '')}`
68
+ );
69
+ })
70
+ .map(t => t.id);
71
+
72
+ // Tasks blocking this task (dependencies)
73
+ const blockedBy = frontmatter.dependencies || [];
74
+
75
+ return {
76
+ frontmatter,
77
+ body,
78
+ epicTitle: epic.frontmatter.title,
79
+ epicId,
80
+ blocking,
81
+ blockedBy,
82
+ path: taskPath
83
+ };
84
+ } catch (err) {
85
+ if (err.code === 'ENOENT') {
86
+ throw new Error(`Task not found: ${taskId} in epic ${epicId}`);
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+
92
+ module.exports = { showLocalTask };