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.
- package/autopm/.claude/agents/frameworks/e2e-test-engineer.md +1 -18
- package/autopm/.claude/agents/frameworks/nats-messaging-expert.md +1 -18
- package/autopm/.claude/agents/frameworks/react-frontend-engineer.md +1 -18
- package/autopm/.claude/agents/frameworks/react-ui-expert.md +1 -18
- package/autopm/.claude/agents/frameworks/tailwindcss-expert.md +1 -18
- package/autopm/.claude/agents/frameworks/ux-design-expert.md +1 -18
- package/autopm/.claude/agents/languages/bash-scripting-expert.md +1 -18
- package/autopm/.claude/agents/languages/javascript-frontend-engineer.md +1 -18
- package/autopm/.claude/agents/languages/nodejs-backend-engineer.md +1 -18
- package/autopm/.claude/agents/languages/python-backend-engineer.md +1 -18
- package/autopm/.claude/agents/languages/python-backend-expert.md +1 -18
- package/autopm/.claude/commands/pm/epic-decompose.md +19 -5
- package/autopm/.claude/commands/pm/prd-new.md +14 -1
- package/autopm/.claude/includes/task-creation-excellence.md +18 -0
- package/autopm/.claude/lib/ai-task-generator.js +84 -0
- package/autopm/.claude/lib/cli-parser.js +148 -0
- package/autopm/.claude/lib/dependency-analyzer.js +157 -0
- package/autopm/.claude/lib/frontmatter.js +224 -0
- package/autopm/.claude/lib/task-utils.js +64 -0
- package/autopm/.claude/scripts/pm-epic-decompose-local.js +158 -0
- package/autopm/.claude/scripts/pm-epic-list-local.js +103 -0
- package/autopm/.claude/scripts/pm-epic-show-local.js +70 -0
- package/autopm/.claude/scripts/pm-epic-update-local.js +56 -0
- package/autopm/.claude/scripts/pm-prd-list-local.js +111 -0
- package/autopm/.claude/scripts/pm-prd-new-local.js +196 -0
- package/autopm/.claude/scripts/pm-prd-parse-local.js +360 -0
- package/autopm/.claude/scripts/pm-prd-show-local.js +101 -0
- package/autopm/.claude/scripts/pm-prd-update-local.js +153 -0
- package/autopm/.claude/scripts/pm-sync-download-local.js +424 -0
- package/autopm/.claude/scripts/pm-sync-upload-local.js +473 -0
- package/autopm/.claude/scripts/pm-task-list-local.js +86 -0
- package/autopm/.claude/scripts/pm-task-show-local.js +92 -0
- package/autopm/.claude/scripts/pm-task-update-local.js +109 -0
- package/autopm/.claude/scripts/setup-local-mode.js +127 -0
- package/package.json +5 -3
- package/scripts/create-task-issues.sh +26 -0
- package/scripts/fix-invalid-command-refs.sh +4 -3
- 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 };
|