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,148 @@
1
+ /**
2
+ * CLI Parser for PM Commands
3
+ *
4
+ * Provides unified command-line argument parsing with --local flag support
5
+ * across all PM commands using yargs best practices.
6
+ *
7
+ * Following Context7 yargs documentation patterns:
8
+ * - .boolean() for boolean flags
9
+ * - .alias() for short flags
10
+ * - .parse() for argument processing
11
+ */
12
+
13
+ const yargs = require('yargs/yargs');
14
+ const { hideBin } = require('yargs/helpers');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ /**
19
+ * Read project configuration to determine default provider
20
+ * @returns {string} Default provider ('github', 'azure', or 'local')
21
+ */
22
+ function getDefaultProvider() {
23
+ try {
24
+ const configPath = path.join(process.cwd(), '.claude/config.json');
25
+ if (fs.existsSync(configPath)) {
26
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
27
+ return config.provider || 'github';
28
+ }
29
+ } catch (error) {
30
+ // Silently fall back to github if config cannot be read
31
+ }
32
+ return 'github';
33
+ }
34
+
35
+ /**
36
+ * Parse PM command arguments with --local flag support
37
+ *
38
+ * @param {string[]} args - Command arguments (usually process.argv)
39
+ * @returns {object} Parsed arguments with mode determined
40
+ *
41
+ * @example
42
+ * // Local mode
43
+ * parsePMCommand(['prd-new', 'feature', '--local'])
44
+ * // => { _: ['prd-new', 'feature'], local: true, mode: 'local' }
45
+ *
46
+ * @example
47
+ * // Remote mode (from config)
48
+ * parsePMCommand(['prd-list'])
49
+ * // => { _: ['prd-list'], local: false, mode: 'github' }
50
+ */
51
+ function parsePMCommand(args) {
52
+ let failureMessage = null;
53
+
54
+ // Don't use hideBin if args are already clean (for testing)
55
+ const cleanArgs = Array.isArray(args) && !args[0]?.includes('node') ? args : hideBin(args);
56
+
57
+ const argv = yargs(cleanArgs)
58
+ .option('local', {
59
+ alias: 'l',
60
+ type: 'boolean',
61
+ describe: 'Use local mode (offline, no GitHub/Azure)',
62
+ default: false
63
+ })
64
+ .option('github', {
65
+ type: 'boolean',
66
+ describe: 'Use GitHub provider',
67
+ default: false
68
+ })
69
+ .option('azure', {
70
+ type: 'boolean',
71
+ describe: 'Use Azure DevOps provider',
72
+ default: false
73
+ })
74
+ .option('verbose', {
75
+ alias: 'v',
76
+ type: 'boolean',
77
+ describe: 'Enable verbose output',
78
+ default: false
79
+ })
80
+ .option('force', {
81
+ alias: 'f',
82
+ type: 'boolean',
83
+ describe: 'Force operation',
84
+ default: false
85
+ })
86
+ .option('output', {
87
+ alias: 'o',
88
+ type: 'string',
89
+ describe: 'Output format (json, text)',
90
+ choices: ['json', 'text']
91
+ })
92
+ .check((argv) => {
93
+ // Validate: cannot use --local with --github or --azure
94
+ if (argv.local && (argv.github || argv.azure)) {
95
+ throw new Error('Cannot use both --local and remote provider (--github or --azure)');
96
+ }
97
+ return true;
98
+ })
99
+ .fail((msg, err) => {
100
+ // Capture failure for throwing
101
+ failureMessage = msg || (err && err.message) || 'Unknown error';
102
+ })
103
+ .exitProcess(false) // Don't call process.exit on error (for testing)
104
+ .parse();
105
+
106
+ // Throw error if validation failed
107
+ if (failureMessage) {
108
+ throw new Error(failureMessage);
109
+ }
110
+
111
+ // Determine mode based on flags and config
112
+ if (argv.local) {
113
+ argv.mode = 'local';
114
+ } else if (argv.github) {
115
+ argv.mode = 'github';
116
+ } else if (argv.azure) {
117
+ argv.mode = 'azure';
118
+ } else {
119
+ // Read from config
120
+ argv.mode = getDefaultProvider();
121
+ }
122
+
123
+ return argv;
124
+ }
125
+
126
+ /**
127
+ * Get help text for CLI parser
128
+ * Used for testing help text includes --local flag
129
+ *
130
+ * @returns {Promise<string>} Help text
131
+ */
132
+ async function getHelpText() {
133
+ const parser = yargs([])
134
+ .option('local', {
135
+ alias: 'l',
136
+ type: 'boolean',
137
+ describe: 'Use local mode (offline, no GitHub/Azure)',
138
+ default: false
139
+ });
140
+
141
+ return await parser.getHelp();
142
+ }
143
+
144
+ module.exports = {
145
+ parsePMCommand,
146
+ getHelpText,
147
+ getDefaultProvider
148
+ };
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Epic Status - Complete epic progress tracking
4
+ *
5
+ * Replaces epic-status.sh with clean, testable JavaScript
6
+ * - Counts tasks by status (completed/in-progress/pending)
7
+ * - Calculates progress percentage
8
+ * - Shows progress bar visualization
9
+ * - Provides sub-epic breakdown
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ /**
16
+ * Parse frontmatter from markdown file
17
+ */
18
+ function parseFrontmatter(filePath) {
19
+ try {
20
+ const content = fs.readFileSync(filePath, 'utf8');
21
+ const lines = content.split('\n');
22
+
23
+ let inFrontmatter = false;
24
+ let frontmatterCount = 0;
25
+ const frontmatter = {};
26
+
27
+ for (const line of lines) {
28
+ if (line === '---') {
29
+ frontmatterCount++;
30
+ if (frontmatterCount === 1) {
31
+ inFrontmatter = true;
32
+ continue;
33
+ } else if (frontmatterCount === 2) {
34
+ break;
35
+ }
36
+ }
37
+
38
+ if (inFrontmatter) {
39
+ const match = line.match(/^(\w+):\s*(.+)$/);
40
+ if (match) {
41
+ const [, key, value] = match;
42
+ frontmatter[key] = value;
43
+ }
44
+ }
45
+ }
46
+
47
+ return frontmatter;
48
+ } catch (error) {
49
+ return {};
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Find all task files in directory
55
+ */
56
+ function findTaskFiles(dir, maxDepth = 2, currentDepth = 0) {
57
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
58
+ return [];
59
+ }
60
+
61
+ if (currentDepth >= maxDepth) {
62
+ return [];
63
+ }
64
+
65
+ const files = [];
66
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
67
+
68
+ for (const entry of entries) {
69
+ const fullPath = path.join(dir, entry.name);
70
+
71
+ if (entry.isFile() && /^\d+\.md$/.test(entry.name)) {
72
+ files.push(fullPath);
73
+ } else if (entry.isDirectory() && currentDepth < maxDepth - 1) {
74
+ files.push(...findTaskFiles(fullPath, maxDepth, currentDepth + 1));
75
+ }
76
+ }
77
+
78
+ return files;
79
+ }
80
+
81
+ /**
82
+ * Count tasks by status
83
+ */
84
+ function countTasksByStatus(taskFiles) {
85
+ const counts = {
86
+ completed: 0,
87
+ in_progress: 0,
88
+ pending: 0,
89
+ total: taskFiles.length
90
+ };
91
+
92
+ for (const taskFile of taskFiles) {
93
+ const frontmatter = parseFrontmatter(taskFile);
94
+ const status = frontmatter.status || '';
95
+
96
+ if (status === 'completed') {
97
+ counts.completed++;
98
+ } else if (status === 'in-progress' || status === 'in_progress') {
99
+ counts.in_progress++;
100
+ } else {
101
+ counts.pending++;
102
+ }
103
+ }
104
+
105
+ return counts;
106
+ }
107
+
108
+ /**
109
+ * Generate progress bar
110
+ */
111
+ function generateProgressBar(percentage, length = 50) {
112
+ const filled = Math.round((percentage * length) / 100);
113
+ const empty = length - filled;
114
+
115
+ const bar = '='.repeat(filled) + '-'.repeat(empty);
116
+ return `[${bar}] ${percentage}%`;
117
+ }
118
+
119
+ /**
120
+ * Get sub-epic breakdown
121
+ */
122
+ function getSubEpicBreakdown(epicDir) {
123
+ const breakdown = [];
124
+
125
+ if (!fs.existsSync(epicDir)) {
126
+ return breakdown;
127
+ }
128
+
129
+ const entries = fs.readdirSync(epicDir, { withFileTypes: true });
130
+
131
+ for (const entry of entries) {
132
+ if (entry.isDirectory()) {
133
+ const subDir = path.join(epicDir, entry.name);
134
+ const taskFiles = findTaskFiles(subDir, 1);
135
+
136
+ if (taskFiles.length > 0) {
137
+ const counts = countTasksByStatus(taskFiles);
138
+ breakdown.push({
139
+ name: entry.name,
140
+ total: counts.total,
141
+ completed: counts.completed
142
+ });
143
+ }
144
+ }
145
+ }
146
+
147
+ return breakdown;
148
+ }
149
+
150
+ /**
151
+ * Format epic status report
152
+ */
153
+ function formatEpicStatus(epicName, epicDir) {
154
+ // Find all tasks
155
+ const taskFiles = findTaskFiles(epicDir);
156
+ const counts = countTasksByStatus(taskFiles);
157
+
158
+ // Calculate progress
159
+ const progress = counts.total > 0
160
+ ? Math.round((counts.completed * 100) / counts.total)
161
+ : 0;
162
+
163
+ // Build report
164
+ const lines = [];
165
+ lines.push(`Epic: ${epicName}`);
166
+ lines.push('='.repeat(20 + epicName.length));
167
+ lines.push('');
168
+ lines.push(`Total tasks: ${counts.total}`);
169
+ lines.push(`Completed: ${counts.completed} (${progress}%)`);
170
+ lines.push(`In Progress: ${counts.in_progress}`);
171
+ lines.push(`Pending: ${counts.pending}`);
172
+ lines.push('');
173
+
174
+ // Progress bar
175
+ if (counts.total > 0) {
176
+ lines.push(`Progress: ${generateProgressBar(progress)}`);
177
+ lines.push('');
178
+ }
179
+
180
+ // Sub-epic breakdown
181
+ const breakdown = getSubEpicBreakdown(epicDir);
182
+ if (breakdown.length > 0) {
183
+ lines.push('Sub-Epic Breakdown:');
184
+ lines.push('-'.repeat(19));
185
+
186
+ for (const sub of breakdown) {
187
+ const name = sub.name.padEnd(30);
188
+ lines.push(` ${name} ${sub.total.toString().padStart(3)} tasks (${sub.completed} completed)`);
189
+ }
190
+ }
191
+
192
+ return lines.join('\n');
193
+ }
194
+
195
+ /**
196
+ * List available epics
197
+ */
198
+ function listAvailableEpics(epicsDir) {
199
+ if (!fs.existsSync(epicsDir)) {
200
+ return [];
201
+ }
202
+
203
+ const entries = fs.readdirSync(epicsDir, { withFileTypes: true });
204
+ return entries
205
+ .filter(entry => entry.isDirectory())
206
+ .map(entry => entry.name);
207
+ }
208
+
209
+ /**
210
+ * Main function
211
+ */
212
+ function main() {
213
+ const args = process.argv.slice(2);
214
+ const epicName = args[0];
215
+
216
+ const epicsDir = path.join(process.cwd(), '.claude/epics');
217
+
218
+ if (!epicName) {
219
+ console.log('Usage: epicStatus.js <epic-name>');
220
+ console.log('');
221
+ console.log('Available epics:');
222
+
223
+ const epics = listAvailableEpics(epicsDir);
224
+ if (epics.length > 0) {
225
+ epics.forEach(epic => console.log(` ${epic}`));
226
+ } else {
227
+ console.log(' No epics found');
228
+ }
229
+
230
+ process.exit(1);
231
+ }
232
+
233
+ const epicDir = path.join(epicsDir, epicName);
234
+
235
+ if (!fs.existsSync(epicDir)) {
236
+ console.error(`Error: Epic '${epicName}' not found`);
237
+ console.log('');
238
+ console.log('Available epics:');
239
+
240
+ const epics = listAvailableEpics(epicsDir);
241
+ epics.forEach(epic => console.log(` ${epic}`));
242
+
243
+ process.exit(1);
244
+ }
245
+
246
+ // Generate and display status
247
+ const status = formatEpicStatus(epicName, epicDir);
248
+ console.log(status);
249
+ }
250
+
251
+ if (require.main === module) {
252
+ main();
253
+ }
254
+
255
+ module.exports = {
256
+ parseFrontmatter,
257
+ findTaskFiles,
258
+ countTasksByStatus,
259
+ generateProgressBar,
260
+ getSubEpicBreakdown,
261
+ formatEpicStatus,
262
+ listAvailableEpics
263
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Dependency Analyzer
3
+ *
4
+ * Analyzes task dependencies and validates dependency graphs.
5
+ * Detects circular dependencies and builds execution order.
6
+ *
7
+ * Usage:
8
+ * const { analyzeDependencies } = require('./dependency-analyzer');
9
+ *
10
+ * const result = analyzeDependencies(tasks);
11
+ * if (result.hasCircularDependencies) {
12
+ * console.error('Circular dependencies found:', result.cycles);
13
+ * }
14
+ */
15
+
16
+ const { generateShortTaskId } = require('./task-utils');
17
+
18
+ /**
19
+ * Analyze task dependencies
20
+ *
21
+ * @param {Array} tasks - Array of task objects with dependencies
22
+ * @returns {Object} Analysis result with cycles, order, and validation
23
+ */
24
+ function analyzeDependencies(tasks) {
25
+ const graph = buildDependencyGraph(tasks);
26
+ const cycles = detectCircularDependencies(graph);
27
+ const order = cycles.length === 0 ? topologicalSort(graph) : [];
28
+
29
+ return {
30
+ hasCircularDependencies: cycles.length > 0,
31
+ cycles,
32
+ executionOrder: order,
33
+ isValid: cycles.length === 0
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Build dependency graph from tasks
39
+ *
40
+ * @param {Array} tasks - Array of task objects
41
+ * @returns {Map} Dependency graph (task -> dependencies)
42
+ */
43
+ function buildDependencyGraph(tasks) {
44
+ const graph = new Map();
45
+
46
+ tasks.forEach((task, index) => {
47
+ const taskId = generateShortTaskId(index + 1);
48
+ const dependencies = task.dependencies || [];
49
+
50
+ graph.set(taskId, dependencies);
51
+ });
52
+
53
+ return graph;
54
+ }
55
+
56
+ /**
57
+ * Detect circular dependencies using DFS
58
+ *
59
+ * @param {Map} graph - Dependency graph
60
+ * @returns {Array} Array of circular dependency cycles
61
+ */
62
+ function detectCircularDependencies(graph) {
63
+ const visited = new Set();
64
+ const recursionStack = new Set();
65
+ const cycles = [];
66
+
67
+ function dfs(node, path = []) {
68
+ if (recursionStack.has(node)) {
69
+ // Found a cycle
70
+ const cycleStart = path.indexOf(node);
71
+ cycles.push(path.slice(cycleStart));
72
+ return;
73
+ }
74
+
75
+ if (visited.has(node)) {
76
+ return;
77
+ }
78
+
79
+ visited.add(node);
80
+ recursionStack.add(node);
81
+ path.push(node);
82
+
83
+ const dependencies = graph.get(node) || [];
84
+ for (const dep of dependencies) {
85
+ dfs(dep, path);
86
+ }
87
+
88
+ path.pop(); // Cleanup: remove node from path after exploring
89
+ recursionStack.delete(node);
90
+ }
91
+
92
+ for (const node of graph.keys()) {
93
+ if (!visited.has(node)) {
94
+ dfs(node);
95
+ }
96
+ }
97
+
98
+ return cycles;
99
+ }
100
+
101
+ /**
102
+ * Topological sort for task execution order
103
+ *
104
+ * @param {Map} graph - Dependency graph
105
+ * @returns {Array} Ordered array of task IDs
106
+ */
107
+ function topologicalSort(graph) {
108
+ const inDegree = new Map();
109
+ const order = [];
110
+
111
+ // Initialize in-degree for all nodes
112
+ for (const node of graph.keys()) {
113
+ inDegree.set(node, 0);
114
+ }
115
+
116
+ // Calculate in-degree
117
+ for (const deps of graph.values()) {
118
+ for (const dep of deps) {
119
+ if (graph.has(dep)) {
120
+ inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
121
+ }
122
+ }
123
+ }
124
+
125
+ // Queue nodes with in-degree 0
126
+ const queue = [];
127
+ for (const [node, degree] of inDegree) {
128
+ if (degree === 0) {
129
+ queue.push(node);
130
+ }
131
+ }
132
+
133
+ // Process queue
134
+ while (queue.length > 0) {
135
+ const node = queue.shift();
136
+ order.push(node);
137
+
138
+ const dependencies = graph.get(node) || [];
139
+ for (const dep of dependencies) {
140
+ const newDegree = inDegree.get(dep) - 1;
141
+ inDegree.set(dep, newDegree);
142
+
143
+ if (newDegree === 0) {
144
+ queue.push(dep);
145
+ }
146
+ }
147
+ }
148
+
149
+ return order;
150
+ }
151
+
152
+ module.exports = {
153
+ analyzeDependencies,
154
+ buildDependencyGraph,
155
+ detectCircularDependencies,
156
+ topologicalSort
157
+ };