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,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
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontmatter Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for parsing, validating, and manipulating YAML frontmatter
|
|
5
|
+
* in markdown files. Used by Local Mode for PRD, Epic, and Task management.
|
|
6
|
+
*
|
|
7
|
+
* Documentation Source: Context7 - /eemeli/yaml
|
|
8
|
+
* Trust Score: 9.4, 100 code snippets
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { parse, stringify } = require('yaml');
|
|
12
|
+
const fs = require('fs').promises;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse YAML frontmatter from markdown content
|
|
16
|
+
*
|
|
17
|
+
* @param {string} content - Markdown content with optional frontmatter
|
|
18
|
+
* @returns {{frontmatter: Object, body: string}} Parsed frontmatter and body
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const { frontmatter, body } = parseFrontmatter(content);
|
|
22
|
+
* console.log(frontmatter.id); // 'task-001'
|
|
23
|
+
*/
|
|
24
|
+
function parseFrontmatter(content) {
|
|
25
|
+
if (!content || typeof content !== 'string') {
|
|
26
|
+
return { frontmatter: {}, body: content || '' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check for frontmatter delimiters
|
|
30
|
+
// Handles both empty (---\n---\n) and non-empty frontmatter
|
|
31
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)^---\r?\n?([\s\S]*)$/m;
|
|
32
|
+
const match = content.match(frontmatterRegex);
|
|
33
|
+
|
|
34
|
+
if (!match) {
|
|
35
|
+
// No frontmatter found, return entire content as body
|
|
36
|
+
return { frontmatter: {}, body: content };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const [, yamlContent, body] = match;
|
|
40
|
+
|
|
41
|
+
// Handle empty frontmatter
|
|
42
|
+
if (!yamlContent.trim()) {
|
|
43
|
+
return { frontmatter: {}, body: body || '' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Parse YAML using Context7-documented pattern
|
|
48
|
+
const frontmatter = parse(yamlContent);
|
|
49
|
+
return {
|
|
50
|
+
frontmatter: frontmatter || {},
|
|
51
|
+
body: body || ''
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Invalid YAML syntax
|
|
55
|
+
throw new Error(`Invalid YAML syntax in frontmatter: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Stringify frontmatter and body into markdown format
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} data - Frontmatter data object
|
|
63
|
+
* @param {string} body - Markdown body content
|
|
64
|
+
* @returns {string} Complete markdown with frontmatter
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const markdown = stringifyFrontmatter({ id: 'task-001' }, 'Body content');
|
|
68
|
+
*/
|
|
69
|
+
function stringifyFrontmatter(data, body = '') {
|
|
70
|
+
// Handle empty frontmatter - don't add empty object literal
|
|
71
|
+
if (!data || Object.keys(data).length === 0) {
|
|
72
|
+
return `---\n---\n${body}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Use Context7-documented stringify with default options
|
|
76
|
+
const yamlContent = stringify(data);
|
|
77
|
+
|
|
78
|
+
// Format: ---\nYAML\n---\nBODY
|
|
79
|
+
return `---\n${yamlContent}---\n${body}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update frontmatter fields in a file
|
|
84
|
+
*
|
|
85
|
+
* Supports nested field updates using dot notation:
|
|
86
|
+
* - 'status' → updates top-level field
|
|
87
|
+
* - 'providers.github.owner' → updates nested field
|
|
88
|
+
*
|
|
89
|
+
* @param {string} filePath - Path to markdown file
|
|
90
|
+
* @param {Object} updates - Fields to update (supports dot notation for nested)
|
|
91
|
+
* @returns {Promise<void>}
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* await updateFrontmatter('task.md', { status: 'done', 'metadata.updated': '2025-10-05' });
|
|
95
|
+
*/
|
|
96
|
+
async function updateFrontmatter(filePath, updates) {
|
|
97
|
+
// Read existing file
|
|
98
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
99
|
+
|
|
100
|
+
// Parse current frontmatter
|
|
101
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
102
|
+
|
|
103
|
+
// Apply updates (supports nested fields via dot notation)
|
|
104
|
+
const updatedFrontmatter = { ...frontmatter };
|
|
105
|
+
|
|
106
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
107
|
+
if (key.includes('.')) {
|
|
108
|
+
// Nested field update: 'providers.github.owner'
|
|
109
|
+
const keys = key.split('.');
|
|
110
|
+
let current = updatedFrontmatter;
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
113
|
+
const k = keys[i];
|
|
114
|
+
if (!current[k] || typeof current[k] !== 'object') {
|
|
115
|
+
current[k] = {};
|
|
116
|
+
}
|
|
117
|
+
current = current[k];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
current[keys[keys.length - 1]] = value;
|
|
121
|
+
} else {
|
|
122
|
+
// Top-level field update
|
|
123
|
+
updatedFrontmatter[key] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Write updated content
|
|
128
|
+
const updated = stringifyFrontmatter(updatedFrontmatter, body);
|
|
129
|
+
await fs.writeFile(filePath, updated, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate frontmatter against schema
|
|
134
|
+
*
|
|
135
|
+
* Schema format:
|
|
136
|
+
* {
|
|
137
|
+
* required: ['id', 'title', 'status'],
|
|
138
|
+
* fields: {
|
|
139
|
+
* id: { type: 'string', pattern: /^task-\d+$/ },
|
|
140
|
+
* status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },
|
|
141
|
+
* tasks_total: { type: 'number' }
|
|
142
|
+
* }
|
|
143
|
+
* }
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} data - Frontmatter data to validate
|
|
146
|
+
* @param {Object} schema - Validation schema
|
|
147
|
+
* @returns {{valid: boolean, errors: string[]}} Validation result
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* const result = validateFrontmatter(data, schema);
|
|
151
|
+
* if (!result.valid) console.error(result.errors);
|
|
152
|
+
*/
|
|
153
|
+
function validateFrontmatter(data, schema) {
|
|
154
|
+
const errors = [];
|
|
155
|
+
|
|
156
|
+
// Check required fields
|
|
157
|
+
if (schema.required) {
|
|
158
|
+
for (const field of schema.required) {
|
|
159
|
+
if (!(field in data)) {
|
|
160
|
+
errors.push(`Missing required field: ${field}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check field types and constraints
|
|
166
|
+
if (schema.fields) {
|
|
167
|
+
for (const [field, constraints] of Object.entries(schema.fields)) {
|
|
168
|
+
const value = data[field];
|
|
169
|
+
|
|
170
|
+
// Skip validation if field is not present and not required
|
|
171
|
+
if (value === undefined) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Type validation
|
|
176
|
+
if (constraints.type) {
|
|
177
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
178
|
+
if (actualType !== constraints.type) {
|
|
179
|
+
errors.push(`Field '${field}' must be of type ${constraints.type}, got ${actualType}`);
|
|
180
|
+
continue; // Skip other validations if type is wrong
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Enum validation
|
|
185
|
+
if (constraints.enum && !constraints.enum.includes(value)) {
|
|
186
|
+
errors.push(`Field '${field}' must be one of [${constraints.enum.join(', ')}], got '${value}' (invalid enum value)`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Pattern validation (for strings)
|
|
190
|
+
if (constraints.pattern && typeof value === 'string') {
|
|
191
|
+
if (!constraints.pattern.test(value)) {
|
|
192
|
+
errors.push(`Field '${field}' does not match required pattern (pattern validation failed)`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
valid: errors.length === 0,
|
|
200
|
+
errors
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Strip frontmatter and return only body content
|
|
206
|
+
*
|
|
207
|
+
* @param {string} content - Markdown content with optional frontmatter
|
|
208
|
+
* @returns {string} Body content only
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* const body = stripBody(content);
|
|
212
|
+
*/
|
|
213
|
+
function stripBody(content) {
|
|
214
|
+
const { body } = parseFrontmatter(content);
|
|
215
|
+
return body;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
parseFrontmatter,
|
|
220
|
+
stringifyFrontmatter,
|
|
221
|
+
updateFrontmatter,
|
|
222
|
+
validateFrontmatter,
|
|
223
|
+
stripBody
|
|
224
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for task ID generation and formatting.
|
|
5
|
+
* Ensures consistency across all task-related operations.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { generateTaskId, generateTaskNumber, generateTaskFilename } = require('./task-utils');
|
|
9
|
+
*
|
|
10
|
+
* const taskId = generateTaskId('epic-001', 5); // 'task-epic-001-005'
|
|
11
|
+
* const taskNum = generateTaskNumber(5); // '005'
|
|
12
|
+
* const filename = generateTaskFilename(5); // 'task-005.md'
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate zero-padded task number (001, 002, etc.)
|
|
17
|
+
*
|
|
18
|
+
* @param {number} index - Task index (1-based)
|
|
19
|
+
* @returns {string} Zero-padded task number
|
|
20
|
+
*/
|
|
21
|
+
function generateTaskNumber(index) {
|
|
22
|
+
return String(index).padStart(3, '0');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate full task ID with epic prefix
|
|
27
|
+
*
|
|
28
|
+
* @param {string} epicId - Epic ID (e.g., 'epic-001')
|
|
29
|
+
* @param {number} index - Task index (1-based)
|
|
30
|
+
* @returns {string} Full task ID (e.g., 'task-epic-001-005')
|
|
31
|
+
*/
|
|
32
|
+
function generateTaskId(epicId, index) {
|
|
33
|
+
const taskNum = generateTaskNumber(index);
|
|
34
|
+
return `task-${epicId}-${taskNum}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate short task ID without epic prefix (for dependency analyzer)
|
|
39
|
+
*
|
|
40
|
+
* @param {number} index - Task index (1-based)
|
|
41
|
+
* @returns {string} Short task ID (e.g., 'task-005')
|
|
42
|
+
*/
|
|
43
|
+
function generateShortTaskId(index) {
|
|
44
|
+
const taskNum = generateTaskNumber(index);
|
|
45
|
+
return `task-${taskNum}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate task filename
|
|
50
|
+
*
|
|
51
|
+
* @param {number} index - Task index (1-based)
|
|
52
|
+
* @returns {string} Task filename (e.g., 'task-005.md')
|
|
53
|
+
*/
|
|
54
|
+
function generateTaskFilename(index) {
|
|
55
|
+
const taskNum = generateTaskNumber(index);
|
|
56
|
+
return `task-${taskNum}.md`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
generateTaskNumber,
|
|
61
|
+
generateTaskId,
|
|
62
|
+
generateShortTaskId,
|
|
63
|
+
generateTaskFilename
|
|
64
|
+
};
|