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.
- package/README.md +40 -0
- 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/prd-new.js +292 -2
- package/autopm/.claude/scripts/pm/template-list.js +119 -0
- package/autopm/.claude/scripts/pm/template-new.js +344 -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/autopm/.claude/templates/prds/README.md +334 -0
- package/autopm/.claude/templates/prds/api-feature.md +306 -0
- package/autopm/.claude/templates/prds/bug-fix.md +413 -0
- package/autopm/.claude/templates/prds/data-migration.md +483 -0
- package/autopm/.claude/templates/prds/documentation.md +439 -0
- package/autopm/.claude/templates/prds/ui-feature.md +365 -0
- package/lib/template-engine.js +347 -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,344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Template New - Create custom template
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* autopm template:new prd my-custom-template
|
|
7
|
+
* autopm template:new epic my-sprint-template
|
|
8
|
+
* autopm template:new task my-task-template
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
// Dynamically resolve template engine path
|
|
16
|
+
let TemplateEngine;
|
|
17
|
+
try {
|
|
18
|
+
TemplateEngine = require(path.join(__dirname, '..', '..', '..', '..', 'lib', 'template-engine'));
|
|
19
|
+
} catch (err) {
|
|
20
|
+
try {
|
|
21
|
+
TemplateEngine = require(path.join(process.cwd(), 'lib', 'template-engine'));
|
|
22
|
+
} catch (err2) {
|
|
23
|
+
TemplateEngine = require('../../../lib/template-engine');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class TemplateCreator {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.templateEngine = new TemplateEngine();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get base template for type
|
|
34
|
+
*/
|
|
35
|
+
getBaseTemplate(type) {
|
|
36
|
+
const templates = {
|
|
37
|
+
prd: `---
|
|
38
|
+
id: {{id}}
|
|
39
|
+
title: {{title}}
|
|
40
|
+
type: prd
|
|
41
|
+
status: draft
|
|
42
|
+
priority: {{priority}}
|
|
43
|
+
created: {{timestamp}}
|
|
44
|
+
author: {{author}}
|
|
45
|
+
timeline: {{timeline}}
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
# PRD: {{title}}
|
|
49
|
+
|
|
50
|
+
## Executive Summary
|
|
51
|
+
|
|
52
|
+
{{executive_summary}}
|
|
53
|
+
|
|
54
|
+
## Problem Statement
|
|
55
|
+
|
|
56
|
+
### Background
|
|
57
|
+
{{problem_background}}
|
|
58
|
+
|
|
59
|
+
### Current State
|
|
60
|
+
{{current_state}}
|
|
61
|
+
|
|
62
|
+
### Desired State
|
|
63
|
+
{{desired_state}}
|
|
64
|
+
|
|
65
|
+
## Target Users
|
|
66
|
+
|
|
67
|
+
{{target_users}}
|
|
68
|
+
|
|
69
|
+
## Key Features
|
|
70
|
+
|
|
71
|
+
### Must Have (P0)
|
|
72
|
+
{{#if must_have_features}}
|
|
73
|
+
{{#each must_have_features}}
|
|
74
|
+
- [ ] {{this}}
|
|
75
|
+
{{/each}}
|
|
76
|
+
{{/if}}
|
|
77
|
+
|
|
78
|
+
### Should Have (P1)
|
|
79
|
+
{{#if should_have_features}}
|
|
80
|
+
{{#each should_have_features}}
|
|
81
|
+
- [ ] {{this}}
|
|
82
|
+
{{/each}}
|
|
83
|
+
{{/if}}
|
|
84
|
+
|
|
85
|
+
## Success Metrics
|
|
86
|
+
|
|
87
|
+
{{success_metrics}}
|
|
88
|
+
|
|
89
|
+
## Technical Requirements
|
|
90
|
+
|
|
91
|
+
{{technical_requirements}}
|
|
92
|
+
|
|
93
|
+
## Implementation Plan
|
|
94
|
+
|
|
95
|
+
### Phase 1: Design (Week 1)
|
|
96
|
+
- [ ] Requirements finalized
|
|
97
|
+
- [ ] Technical design review
|
|
98
|
+
- [ ] Development environment setup
|
|
99
|
+
|
|
100
|
+
### Phase 2: Development (Week 2-3)
|
|
101
|
+
- [ ] Implement core features
|
|
102
|
+
- [ ] Write tests (TDD)
|
|
103
|
+
- [ ] Code review
|
|
104
|
+
|
|
105
|
+
### Phase 3: Testing (Week 4)
|
|
106
|
+
- [ ] Integration testing
|
|
107
|
+
- [ ] User acceptance testing
|
|
108
|
+
- [ ] Performance testing
|
|
109
|
+
|
|
110
|
+
### Phase 4: Release (Week 5)
|
|
111
|
+
- [ ] Documentation
|
|
112
|
+
- [ ] Deployment
|
|
113
|
+
- [ ] Monitoring setup
|
|
114
|
+
|
|
115
|
+
## Risks and Mitigation
|
|
116
|
+
|
|
117
|
+
{{risks_and_mitigation}}
|
|
118
|
+
|
|
119
|
+
## Open Questions
|
|
120
|
+
|
|
121
|
+
- [ ] {{open_question_1}}
|
|
122
|
+
- [ ] {{open_question_2}}
|
|
123
|
+
|
|
124
|
+
## Appendix
|
|
125
|
+
|
|
126
|
+
### Changelog
|
|
127
|
+
- {{timestamp}}: Initial PRD created by {{author}}
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
*Custom PRD Template*
|
|
132
|
+
`,
|
|
133
|
+
epic: `---
|
|
134
|
+
id: {{id}}
|
|
135
|
+
title: {{title}}
|
|
136
|
+
type: epic
|
|
137
|
+
status: planning
|
|
138
|
+
priority: {{priority}}
|
|
139
|
+
created: {{timestamp}}
|
|
140
|
+
author: {{author}}
|
|
141
|
+
start_date: {{start_date}}
|
|
142
|
+
end_date: {{end_date}}
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
# Epic: {{title}}
|
|
146
|
+
|
|
147
|
+
## Overview
|
|
148
|
+
|
|
149
|
+
{{overview}}
|
|
150
|
+
|
|
151
|
+
## Goals
|
|
152
|
+
|
|
153
|
+
{{#each goals}}
|
|
154
|
+
- {{this}}
|
|
155
|
+
{{/each}}
|
|
156
|
+
|
|
157
|
+
## User Stories
|
|
158
|
+
|
|
159
|
+
{{#each user_stories}}
|
|
160
|
+
- As a {{role}}, I want to {{action}}, so that {{benefit}}
|
|
161
|
+
{{/each}}
|
|
162
|
+
|
|
163
|
+
## Tasks
|
|
164
|
+
|
|
165
|
+
{{#each tasks}}
|
|
166
|
+
- [ ] {{this}}
|
|
167
|
+
{{/each}}
|
|
168
|
+
|
|
169
|
+
## Success Criteria
|
|
170
|
+
|
|
171
|
+
{{success_criteria}}
|
|
172
|
+
|
|
173
|
+
## Dependencies
|
|
174
|
+
|
|
175
|
+
{{dependencies}}
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
*Custom Epic Template*
|
|
180
|
+
`,
|
|
181
|
+
task: `---
|
|
182
|
+
id: {{id}}
|
|
183
|
+
title: {{title}}
|
|
184
|
+
type: task
|
|
185
|
+
status: todo
|
|
186
|
+
priority: {{priority}}
|
|
187
|
+
created: {{timestamp}}
|
|
188
|
+
author: {{author}}
|
|
189
|
+
assigned_to: {{assigned_to}}
|
|
190
|
+
estimated_hours: {{estimated_hours}}
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
# Task: {{title}}
|
|
194
|
+
|
|
195
|
+
## Description
|
|
196
|
+
|
|
197
|
+
{{description}}
|
|
198
|
+
|
|
199
|
+
## Acceptance Criteria
|
|
200
|
+
|
|
201
|
+
{{#each acceptance_criteria}}
|
|
202
|
+
- [ ] {{this}}
|
|
203
|
+
{{/each}}
|
|
204
|
+
|
|
205
|
+
## Technical Details
|
|
206
|
+
|
|
207
|
+
{{technical_details}}
|
|
208
|
+
|
|
209
|
+
## Testing
|
|
210
|
+
|
|
211
|
+
{{testing_notes}}
|
|
212
|
+
|
|
213
|
+
## Notes
|
|
214
|
+
|
|
215
|
+
{{notes}}
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
*Custom Task Template*
|
|
220
|
+
`
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return templates[type] || templates.prd;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create new template
|
|
228
|
+
*/
|
|
229
|
+
create(type, name) {
|
|
230
|
+
console.log(`\n📝 Creating Custom Template`);
|
|
231
|
+
console.log(`${'═'.repeat(50)}\n`);
|
|
232
|
+
|
|
233
|
+
// Validate type
|
|
234
|
+
const validTypes = ['prd', 'epic', 'task'];
|
|
235
|
+
if (!validTypes.includes(type)) {
|
|
236
|
+
console.error(`❌ Invalid type: ${type}`);
|
|
237
|
+
console.log(`Valid types: ${validTypes.join(', ')}`);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Ensure template directory exists
|
|
242
|
+
const typeDir = type === 'prd' ? 'prds' : type === 'epic' ? 'epics' : 'tasks';
|
|
243
|
+
this.templateEngine.ensureTemplateDir(typeDir);
|
|
244
|
+
|
|
245
|
+
const templatePath = path.join('.claude', 'templates', typeDir, `${name}.md`);
|
|
246
|
+
|
|
247
|
+
// Check if template already exists
|
|
248
|
+
if (fs.existsSync(templatePath)) {
|
|
249
|
+
console.error(`❌ Template already exists: ${templatePath}`);
|
|
250
|
+
console.log(`💡 Edit file directly or choose a different name`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Get base template
|
|
255
|
+
const baseTemplate = this.getBaseTemplate(type);
|
|
256
|
+
|
|
257
|
+
// Write template file
|
|
258
|
+
fs.writeFileSync(templatePath, baseTemplate);
|
|
259
|
+
|
|
260
|
+
console.log(`✅ Template created: ${templatePath}`);
|
|
261
|
+
console.log(`\n📋 Template Structure:`);
|
|
262
|
+
console.log(` - Frontmatter: Define metadata variables`);
|
|
263
|
+
console.log(` - Variables: Use {{variable_name}} for substitution`);
|
|
264
|
+
console.log(` - Conditionals: {{#if var}}...{{/if}}`);
|
|
265
|
+
console.log(` - Loops: {{#each items}}...{{/each}}`);
|
|
266
|
+
|
|
267
|
+
console.log(`\n🛠️ Next Steps:`);
|
|
268
|
+
console.log(` 1. Edit template: nano ${templatePath}`);
|
|
269
|
+
console.log(` 2. Add custom variables and sections`);
|
|
270
|
+
console.log(` 3. Test template: autopm ${type}:new --template ${name} "Test"`);
|
|
271
|
+
|
|
272
|
+
// Try to open in editor
|
|
273
|
+
this.openInEditor(templatePath);
|
|
274
|
+
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Open template in editor
|
|
280
|
+
*/
|
|
281
|
+
openInEditor(templatePath) {
|
|
282
|
+
const editors = ['code', 'nano', 'vim', 'vi'];
|
|
283
|
+
|
|
284
|
+
for (const editor of editors) {
|
|
285
|
+
try {
|
|
286
|
+
// Check if editor exists
|
|
287
|
+
execSync(`which ${editor}`, { stdio: 'ignore' });
|
|
288
|
+
|
|
289
|
+
console.log(`\n📝 Opening in ${editor}...`);
|
|
290
|
+
console.log(` Edit the template, save, and exit`);
|
|
291
|
+
|
|
292
|
+
// Open editor (blocking)
|
|
293
|
+
execSync(`${editor} ${templatePath}`, { stdio: 'inherit' });
|
|
294
|
+
|
|
295
|
+
// Validate template after editing
|
|
296
|
+
const content = fs.readFileSync(templatePath, 'utf8');
|
|
297
|
+
const validation = this.templateEngine.validate(content);
|
|
298
|
+
|
|
299
|
+
if (!validation.valid) {
|
|
300
|
+
console.log(`\n⚠️ Template validation warnings:`);
|
|
301
|
+
validation.errors.forEach(err => console.log(` - ${err}`));
|
|
302
|
+
console.log(`\n💡 These are suggestions. Template will still work.`);
|
|
303
|
+
} else {
|
|
304
|
+
console.log(`\n✅ Template is valid!`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return true;
|
|
308
|
+
} catch (err) {
|
|
309
|
+
// Editor not found, try next
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// No editor found
|
|
315
|
+
console.log(`\n💡 Edit manually: ${templatePath}`);
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
run(args) {
|
|
320
|
+
if (args.length < 2) {
|
|
321
|
+
console.error('❌ Usage: autopm template:new <type> <name>');
|
|
322
|
+
console.log('\nExamples:');
|
|
323
|
+
console.log(' autopm template:new prd my-custom-prd');
|
|
324
|
+
console.log(' autopm template:new epic my-sprint');
|
|
325
|
+
console.log(' autopm template:new task my-development-task');
|
|
326
|
+
console.log('\nTypes: prd, epic, task');
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const type = args[0];
|
|
331
|
+
const name = args[1];
|
|
332
|
+
|
|
333
|
+
const success = this.create(type, name);
|
|
334
|
+
process.exit(success ? 0 : 1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Main execution
|
|
339
|
+
if (require.main === module) {
|
|
340
|
+
const creator = new TemplateCreator();
|
|
341
|
+
creator.run(process.argv.slice(2));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = TemplateCreator;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic Decomposition - Local Mode
|
|
3
|
+
*
|
|
4
|
+
* AI-powered decomposition of epics into right-sized tasks (4-8h each).
|
|
5
|
+
* Generates task files with frontmatter, dependencies, and acceptance criteria.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { decomposeLocalEpic } = require('./pm-epic-decompose-local');
|
|
9
|
+
*
|
|
10
|
+
* const result = await decomposeLocalEpic('epic-001', {
|
|
11
|
+
* aiProvider: new OpenAIProvider(),
|
|
12
|
+
* maxTasks: 10
|
|
13
|
+
* });
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs').promises;
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { showLocalEpic } = require('./pm-epic-show-local');
|
|
19
|
+
const { updateLocalEpic } = require('./pm-epic-update-local');
|
|
20
|
+
const { stringifyFrontmatter } = require('../lib/frontmatter');
|
|
21
|
+
const { TaskGenerator } = require('../lib/ai-task-generator');
|
|
22
|
+
const { analyzeDependencies } = require('../lib/dependency-analyzer');
|
|
23
|
+
const { generateTaskId, generateTaskNumber, generateTaskFilename } = require('../lib/task-utils');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Decompose epic into tasks using AI
|
|
27
|
+
*
|
|
28
|
+
* @param {string} epicId - Epic ID to decompose
|
|
29
|
+
* @param {Object} options - Decomposition options
|
|
30
|
+
* @param {Object} [options.aiProvider] - AI provider instance (for testing)
|
|
31
|
+
* @param {number} [options.maxTasks=15] - Maximum tasks to generate
|
|
32
|
+
* @param {boolean} [options.validateDependencies=false] - Validate dependency graph
|
|
33
|
+
* @returns {Promise<Object>} Decomposition result
|
|
34
|
+
*/
|
|
35
|
+
async function decomposeLocalEpic(epicId, options = {}) {
|
|
36
|
+
const {
|
|
37
|
+
aiProvider = null,
|
|
38
|
+
maxTasks = 15,
|
|
39
|
+
validateDependencies = false
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
// 1. Load epic
|
|
43
|
+
const epic = await showLocalEpic(epicId);
|
|
44
|
+
const epicDir = path.dirname(epic.path);
|
|
45
|
+
|
|
46
|
+
// 2. Generate tasks using AI
|
|
47
|
+
const generator = new TaskGenerator(aiProvider);
|
|
48
|
+
const tasks = await generator.generate(epic.body, { maxTasks });
|
|
49
|
+
|
|
50
|
+
// Handle empty response
|
|
51
|
+
if (tasks.length === 0) {
|
|
52
|
+
return {
|
|
53
|
+
epicId,
|
|
54
|
+
tasksCreated: 0,
|
|
55
|
+
warning: 'No tasks generated by AI provider'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Validate dependencies if requested
|
|
60
|
+
if (validateDependencies) {
|
|
61
|
+
const analysis = analyzeDependencies(tasks);
|
|
62
|
+
if (analysis.hasCircularDependencies) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Circular dependency detected: ${analysis.cycles.map(c => c.join(' -> ')).join(', ')}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. Create task files
|
|
70
|
+
const taskIds = [];
|
|
71
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
72
|
+
const task = tasks[i];
|
|
73
|
+
const taskId = generateTaskId(epicId, i + 1);
|
|
74
|
+
const taskFilename = generateTaskFilename(i + 1);
|
|
75
|
+
const taskPath = path.join(epicDir, taskFilename);
|
|
76
|
+
|
|
77
|
+
// Build task frontmatter
|
|
78
|
+
const taskFrontmatter = {
|
|
79
|
+
id: taskId,
|
|
80
|
+
epic_id: epicId,
|
|
81
|
+
title: task.title,
|
|
82
|
+
status: 'pending',
|
|
83
|
+
priority: task.priority || 'medium',
|
|
84
|
+
estimated_hours: task.estimated_hours || 4,
|
|
85
|
+
dependencies: task.dependencies || [],
|
|
86
|
+
created: new Date().toISOString().split('T')[0]
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Build task body
|
|
90
|
+
const taskBody = buildTaskBody(task);
|
|
91
|
+
|
|
92
|
+
// Write task file
|
|
93
|
+
const taskContent = stringifyFrontmatter(taskFrontmatter, taskBody);
|
|
94
|
+
await fs.writeFile(taskPath, taskContent, 'utf8');
|
|
95
|
+
|
|
96
|
+
taskIds.push(taskId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 5. Update epic with task count
|
|
100
|
+
await updateLocalEpic(epicId, {
|
|
101
|
+
tasks_total: tasks.length,
|
|
102
|
+
tasks_completed: 0,
|
|
103
|
+
task_ids: taskIds
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
epicId,
|
|
108
|
+
tasksCreated: tasks.length,
|
|
109
|
+
taskIds
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build task body content from AI-generated task
|
|
115
|
+
*
|
|
116
|
+
* @param {Object} task - Task object from AI
|
|
117
|
+
* @returns {string} Markdown body content
|
|
118
|
+
*/
|
|
119
|
+
function buildTaskBody(task) {
|
|
120
|
+
let body = `# ${task.title}\n\n`;
|
|
121
|
+
|
|
122
|
+
// Description
|
|
123
|
+
if (task.description) {
|
|
124
|
+
body += `## Description\n\n${task.description}\n\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Acceptance Criteria
|
|
128
|
+
body += `## Acceptance Criteria\n\n`;
|
|
129
|
+
if (task.acceptance_criteria && task.acceptance_criteria.length > 0) {
|
|
130
|
+
task.acceptance_criteria.forEach(criterion => {
|
|
131
|
+
body += `- [ ] ${criterion}\n`;
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
body += `- [ ] Implementation complete\n`;
|
|
135
|
+
body += `- [ ] Tests passing\n`;
|
|
136
|
+
body += `- [ ] Code reviewed\n`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
body += `\n`;
|
|
140
|
+
|
|
141
|
+
// Technical Notes (if provided)
|
|
142
|
+
if (task.technical_notes) {
|
|
143
|
+
body += `## Technical Notes\n\n${task.technical_notes}\n\n`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Dependencies
|
|
147
|
+
if (task.dependencies && task.dependencies.length > 0) {
|
|
148
|
+
body += `## Dependencies\n\n`;
|
|
149
|
+
task.dependencies.forEach(dep => {
|
|
150
|
+
body += `- ${dep}\n`;
|
|
151
|
+
});
|
|
152
|
+
body += `\n`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return body.trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { decomposeLocalEpic };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Local Epics
|
|
3
|
+
*
|
|
4
|
+
* Lists all epics in the local `.claude/epics/` directory
|
|
5
|
+
* with optional filtering by status or PRD ID.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { listLocalEpics } = require('./pm-epic-list-local');
|
|
9
|
+
*
|
|
10
|
+
* // List all epics
|
|
11
|
+
* const epics = await listLocalEpics();
|
|
12
|
+
*
|
|
13
|
+
* // Filter by status
|
|
14
|
+
* const inProgress = await listLocalEpics({ status: 'in_progress' });
|
|
15
|
+
*
|
|
16
|
+
* // Filter by PRD
|
|
17
|
+
* const prdEpics = await listLocalEpics({ prd_id: 'prd-001' });
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs').promises;
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { parseFrontmatter } = require('../lib/frontmatter');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* List all local epics with optional filtering
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} options - Filter options
|
|
28
|
+
* @param {string} [options.status] - Filter by epic status (planning, in_progress, completed, etc.)
|
|
29
|
+
* @param {string} [options.prd_id] - Filter by PRD ID
|
|
30
|
+
* @returns {Promise<Array>} Array of epic objects with frontmatter
|
|
31
|
+
*/
|
|
32
|
+
async function listLocalEpics(options = {}) {
|
|
33
|
+
const basePath = process.cwd();
|
|
34
|
+
const epicsDir = path.join(basePath, '.claude', 'epics');
|
|
35
|
+
|
|
36
|
+
// Check if epics directory exists
|
|
37
|
+
try {
|
|
38
|
+
await fs.access(epicsDir);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err.code === 'ENOENT') {
|
|
41
|
+
return []; // No epics directory = no epics
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Read all epic directories
|
|
47
|
+
const dirs = await fs.readdir(epicsDir);
|
|
48
|
+
const epics = [];
|
|
49
|
+
|
|
50
|
+
// Process each epic directory
|
|
51
|
+
for (const dir of dirs) {
|
|
52
|
+
// Skip hidden directories and files
|
|
53
|
+
if (dir.startsWith('.')) continue;
|
|
54
|
+
|
|
55
|
+
const epicDir = path.join(epicsDir, dir);
|
|
56
|
+
const epicPath = path.join(epicDir, 'epic.md');
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Check if it's a directory with epic.md
|
|
60
|
+
const stat = await fs.stat(epicDir);
|
|
61
|
+
if (!stat.isDirectory()) continue;
|
|
62
|
+
|
|
63
|
+
// Read and parse epic.md
|
|
64
|
+
const content = await fs.readFile(epicPath, 'utf8');
|
|
65
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
66
|
+
|
|
67
|
+
// Only include valid epics with required fields
|
|
68
|
+
if (frontmatter && frontmatter.id) {
|
|
69
|
+
epics.push({
|
|
70
|
+
...frontmatter,
|
|
71
|
+
directory: dir
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
// Skip invalid epic directories (missing epic.md, parse errors, etc.)
|
|
76
|
+
if (err.code !== 'ENOENT') {
|
|
77
|
+
console.warn(`Warning: Could not process epic in ${dir}:`, err.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Apply filters
|
|
83
|
+
let filtered = epics;
|
|
84
|
+
|
|
85
|
+
if (options.status) {
|
|
86
|
+
filtered = filtered.filter(epic => epic.status === options.status);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (options.prd_id) {
|
|
90
|
+
filtered = filtered.filter(epic => epic.prd_id === options.prd_id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Sort by creation date (newest first)
|
|
94
|
+
filtered.sort((a, b) => {
|
|
95
|
+
const dateA = new Date(a.created || 0);
|
|
96
|
+
const dateB = new Date(b.created || 0);
|
|
97
|
+
return dateB - dateA; // Descending order (newest first)
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return filtered;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { listLocalEpics };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show Local Epic
|
|
3
|
+
*
|
|
4
|
+
* Displays details of a specific epic including frontmatter,
|
|
5
|
+
* body content, and directory information.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { showLocalEpic } = require('./pm-epic-show-local');
|
|
9
|
+
*
|
|
10
|
+
* const epic = await showLocalEpic('epic-001');
|
|
11
|
+
* console.log(epic.frontmatter.title);
|
|
12
|
+
* console.log(epic.body);
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs').promises;
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { parseFrontmatter } = require('../lib/frontmatter');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get epic details by ID
|
|
21
|
+
*
|
|
22
|
+
* @param {string} epicId - Epic ID to retrieve
|
|
23
|
+
* @returns {Promise<Object>} Epic object with frontmatter, body, and directory
|
|
24
|
+
* @throws {Error} If epic not found
|
|
25
|
+
*/
|
|
26
|
+
async function showLocalEpic(epicId) {
|
|
27
|
+
const basePath = process.cwd();
|
|
28
|
+
const epicsDir = path.join(basePath, '.claude', 'epics');
|
|
29
|
+
|
|
30
|
+
// Check if epics directory exists
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(epicsDir);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err.code === 'ENOENT') {
|
|
35
|
+
throw new Error(`Epic not found: ${epicId} (epics directory does not exist)`);
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Find epic directory by ID
|
|
41
|
+
const dirs = await fs.readdir(epicsDir);
|
|
42
|
+
const epicDir = dirs.find(dir => dir.startsWith(`${epicId}-`));
|
|
43
|
+
|
|
44
|
+
if (!epicDir) {
|
|
45
|
+
throw new Error(`Epic not found: ${epicId}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const epicDirPath = path.join(epicsDir, epicDir);
|
|
49
|
+
const epicPath = path.join(epicDirPath, 'epic.md');
|
|
50
|
+
|
|
51
|
+
// Read and parse epic file
|
|
52
|
+
try {
|
|
53
|
+
const content = await fs.readFile(epicPath, 'utf8');
|
|
54
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
frontmatter,
|
|
58
|
+
body,
|
|
59
|
+
directory: epicDir,
|
|
60
|
+
path: epicPath
|
|
61
|
+
};
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err.code === 'ENOENT') {
|
|
64
|
+
throw new Error(`Epic not found: ${epicId} (epic.md missing in ${epicDir})`);
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { showLocalEpic };
|