musubi-sdd 0.8.3 → 0.8.4
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.ja.md +9 -0
- package/README.md +9 -0
- package/bin/musubi-tasks.js +365 -0
- package/package.json +3 -2
- package/src/generators/tasks.js +485 -0
package/README.ja.md
CHANGED
|
@@ -120,6 +120,15 @@ musubi-design add-c4 container --format plantuml # PlantUMLでコンテナ図
|
|
|
120
120
|
musubi-design add-adr "JWTトークン使用" # アーキテクチャ決定記録追加
|
|
121
121
|
musubi-design validate # 設計完全性を検証
|
|
122
122
|
musubi-design trace # 要件トレーサビリティ表示
|
|
123
|
+
|
|
124
|
+
# 設計をタスクに分解(v0.8.4)
|
|
125
|
+
musubi-tasks init "ユーザー認証" # タスク分解を初期化
|
|
126
|
+
musubi-tasks add "データベーススキーマ" # インタラクティブにタスク追加
|
|
127
|
+
musubi-tasks list # 全タスクリスト表示
|
|
128
|
+
musubi-tasks list --priority P0 # 重要タスクのみ表示
|
|
129
|
+
musubi-tasks update 001 "In Progress" # タスクステータス更新
|
|
130
|
+
musubi-tasks validate # タスク完全性を検証
|
|
131
|
+
musubi-tasks graph # 依存関係グラフ表示
|
|
123
132
|
```
|
|
124
133
|
|
|
125
134
|
### プロジェクトタイプ
|
package/README.md
CHANGED
|
@@ -124,6 +124,15 @@ musubi-design add-c4 container --format plantuml # Add Container with PlantUML
|
|
|
124
124
|
musubi-design add-adr "Use JWT for tokens" # Add Architecture Decision
|
|
125
125
|
musubi-design validate # Validate design completeness
|
|
126
126
|
musubi-design trace # Show requirements traceability
|
|
127
|
+
|
|
128
|
+
# Break down design into tasks (v0.8.4)
|
|
129
|
+
musubi-tasks init "User Authentication" # Initialize task breakdown
|
|
130
|
+
musubi-tasks add "Database Schema" # Add task interactively
|
|
131
|
+
musubi-tasks list # List all tasks
|
|
132
|
+
musubi-tasks list --priority P0 # List critical tasks
|
|
133
|
+
musubi-tasks update 001 "In Progress" # Update task status
|
|
134
|
+
musubi-tasks validate # Validate task completeness
|
|
135
|
+
musubi-tasks graph # Show dependency graph
|
|
127
136
|
```
|
|
128
137
|
|
|
129
138
|
### Project Types
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MUSUBI Task Breakdown System CLI
|
|
5
|
+
*
|
|
6
|
+
* Breaks down design into actionable implementation tasks
|
|
7
|
+
* Complies with Article I-IX and P0-P3 priority labels
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* musubi-tasks init <feature> # Initialize task breakdown
|
|
11
|
+
* musubi-tasks add <title> # Add task with interactive prompts
|
|
12
|
+
* musubi-tasks list # List all tasks
|
|
13
|
+
* musubi-tasks update <id> <status> # Update task status
|
|
14
|
+
* musubi-tasks validate # Validate task breakdown
|
|
15
|
+
* musubi-tasks graph # Generate dependency graph
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { Command } = require('commander');
|
|
19
|
+
const chalk = require('chalk');
|
|
20
|
+
const TasksGenerator = require('../src/generators/tasks');
|
|
21
|
+
|
|
22
|
+
let inquirer;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize inquirer (ESM module in v9+)
|
|
26
|
+
*/
|
|
27
|
+
async function getInquirer() {
|
|
28
|
+
if (!inquirer) {
|
|
29
|
+
inquirer = (await import('inquirer')).default;
|
|
30
|
+
}
|
|
31
|
+
return inquirer;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const program = new Command();
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.name('musubi-tasks')
|
|
38
|
+
.description('MUSUBI Task Breakdown System - Break down design into actionable tasks')
|
|
39
|
+
.version('0.8.4');
|
|
40
|
+
|
|
41
|
+
// Initialize task breakdown
|
|
42
|
+
program
|
|
43
|
+
.command('init <feature>')
|
|
44
|
+
.description('Initialize task breakdown document from design')
|
|
45
|
+
.option('-o, --output <path>', 'Output directory', 'docs/tasks')
|
|
46
|
+
.option('-a, --author <name>', 'Author name')
|
|
47
|
+
.option('--project <name>', 'Project name')
|
|
48
|
+
.option('-d, --design <path>', 'Design document path')
|
|
49
|
+
.option('-r, --requirements <path>', 'Requirements document path')
|
|
50
|
+
.action(async (feature, options) => {
|
|
51
|
+
try {
|
|
52
|
+
console.log(chalk.bold(`\n📋 Initializing task breakdown for: ${feature}\n`));
|
|
53
|
+
|
|
54
|
+
const generator = new TasksGenerator(process.cwd());
|
|
55
|
+
const result = await generator.init(feature, options);
|
|
56
|
+
|
|
57
|
+
console.log(chalk.green('\n✓ Task breakdown document created'));
|
|
58
|
+
console.log(chalk.dim(` ${result.path}`));
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(chalk.bold('Next steps:'));
|
|
61
|
+
console.log(chalk.dim(` 1. Edit ${result.path}`));
|
|
62
|
+
console.log(chalk.dim(' 2. Add tasks: musubi-tasks add <title>'));
|
|
63
|
+
console.log(chalk.dim(' 3. Validate: musubi-tasks validate'));
|
|
64
|
+
console.log(chalk.dim(' 4. Generate graph: musubi-tasks graph'));
|
|
65
|
+
console.log();
|
|
66
|
+
|
|
67
|
+
process.exit(0);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Add task
|
|
75
|
+
program
|
|
76
|
+
.command('add <title>')
|
|
77
|
+
.description('Add new task with interactive prompts')
|
|
78
|
+
.option('-f, --file <path>', 'Task breakdown file path')
|
|
79
|
+
.action(async (title, options) => {
|
|
80
|
+
try {
|
|
81
|
+
console.log(chalk.bold(`\n📝 Adding task: ${title}\n`));
|
|
82
|
+
|
|
83
|
+
const generator = new TasksGenerator(process.cwd());
|
|
84
|
+
|
|
85
|
+
// Find task file
|
|
86
|
+
let taskFile = options.file;
|
|
87
|
+
if (!taskFile) {
|
|
88
|
+
const files = await generator.findTaskFiles();
|
|
89
|
+
if (files.length === 0) {
|
|
90
|
+
console.error(chalk.red('✗ No task files found'));
|
|
91
|
+
console.log(chalk.dim(' Run: musubi-tasks init <feature>'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (files.length === 1) {
|
|
96
|
+
taskFile = files[0];
|
|
97
|
+
} else {
|
|
98
|
+
const inquirerInst = await getInquirer();
|
|
99
|
+
const answer = await inquirerInst.prompt([{
|
|
100
|
+
type: 'list',
|
|
101
|
+
name: 'file',
|
|
102
|
+
message: 'Select task file:',
|
|
103
|
+
choices: files
|
|
104
|
+
}]);
|
|
105
|
+
taskFile = answer.file;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Interactive prompts for task details
|
|
110
|
+
const inquirerInst2 = await getInquirer();
|
|
111
|
+
const answers = await inquirerInst2.prompt([
|
|
112
|
+
{
|
|
113
|
+
type: 'list',
|
|
114
|
+
name: 'priority',
|
|
115
|
+
message: 'Task priority:',
|
|
116
|
+
choices: [
|
|
117
|
+
{ name: 'P0 (Critical - Launch Blocker)', value: 'P0' },
|
|
118
|
+
{ name: 'P1 (High - Important)', value: 'P1' },
|
|
119
|
+
{ name: 'P2 (Medium - Nice to have)', value: 'P2' },
|
|
120
|
+
{ name: 'P3 (Low - Future)', value: 'P3' }
|
|
121
|
+
],
|
|
122
|
+
default: 'P1'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'list',
|
|
126
|
+
name: 'storyPoints',
|
|
127
|
+
message: 'Story points (Fibonacci):',
|
|
128
|
+
choices: ['1', '2', '3', '5', '8', '13'],
|
|
129
|
+
default: '3'
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: 'input',
|
|
133
|
+
name: 'estimatedHours',
|
|
134
|
+
message: 'Estimated hours:',
|
|
135
|
+
default: '4',
|
|
136
|
+
validate: (input) => !isNaN(input) || 'Must be a number'
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: 'input',
|
|
140
|
+
name: 'assignee',
|
|
141
|
+
message: 'Assignee (optional):',
|
|
142
|
+
default: '[Unassigned]'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'input',
|
|
146
|
+
name: 'description',
|
|
147
|
+
message: 'Task description:',
|
|
148
|
+
validate: (input) => input.length > 0 || 'Description is required'
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: 'input',
|
|
152
|
+
name: 'requirements',
|
|
153
|
+
message: 'Requirements (comma-separated REQ-XXX-NNN):',
|
|
154
|
+
filter: (input) => input.split(',').map(s => s.trim()).filter(s => s.length > 0)
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: 'input',
|
|
158
|
+
name: 'acceptance',
|
|
159
|
+
message: 'Acceptance criteria (semicolon-separated):',
|
|
160
|
+
filter: (input) => input.split(';').map(s => s.trim()).filter(s => s.length > 0)
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'input',
|
|
164
|
+
name: 'dependencies',
|
|
165
|
+
message: 'Dependencies (comma-separated TASK-XXX):',
|
|
166
|
+
filter: (input) => input.split(',').map(s => s.trim()).filter(s => s.length > 0)
|
|
167
|
+
}
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const task = {
|
|
171
|
+
title,
|
|
172
|
+
priority: answers.priority,
|
|
173
|
+
storyPoints: parseInt(answers.storyPoints),
|
|
174
|
+
estimatedHours: parseFloat(answers.estimatedHours),
|
|
175
|
+
assignee: answers.assignee,
|
|
176
|
+
status: 'Not Started',
|
|
177
|
+
description: answers.description,
|
|
178
|
+
requirements: answers.requirements,
|
|
179
|
+
acceptance: answers.acceptance,
|
|
180
|
+
dependencies: answers.dependencies
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const result = await generator.addTask(taskFile, task);
|
|
184
|
+
|
|
185
|
+
console.log(chalk.green('\n✓ Task added:'));
|
|
186
|
+
console.log(chalk.dim(` TASK-${result.id}: ${result.title}`));
|
|
187
|
+
console.log(chalk.dim(` Priority: ${result.priority}, Points: ${result.storyPoints}, Hours: ${result.estimatedHours}`));
|
|
188
|
+
console.log();
|
|
189
|
+
|
|
190
|
+
process.exit(0);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// List all tasks
|
|
198
|
+
program
|
|
199
|
+
.command('list')
|
|
200
|
+
.description('List all tasks')
|
|
201
|
+
.option('-f, --file <path>', 'Specific task file')
|
|
202
|
+
.option('--format <type>', 'Output format (table|json|markdown)', 'table')
|
|
203
|
+
.option('--priority <level>', 'Filter by priority (P0|P1|P2|P3)')
|
|
204
|
+
.option('--status <status>', 'Filter by status')
|
|
205
|
+
.action(async (options) => {
|
|
206
|
+
try {
|
|
207
|
+
console.log(chalk.bold('\n📋 Task List\n'));
|
|
208
|
+
|
|
209
|
+
const generator = new TasksGenerator(process.cwd());
|
|
210
|
+
const result = await generator.list(options);
|
|
211
|
+
|
|
212
|
+
if (result.tasks.length === 0) {
|
|
213
|
+
console.log(chalk.yellow('No tasks found'));
|
|
214
|
+
console.log(chalk.dim(' Run: musubi-tasks add <title>'));
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (options.format === 'json') {
|
|
219
|
+
console.log(JSON.stringify(result, null, 2));
|
|
220
|
+
} else if (options.format === 'markdown') {
|
|
221
|
+
result.tasks.forEach(t => {
|
|
222
|
+
console.log(`### TASK-${t.id}: ${t.title}`);
|
|
223
|
+
console.log(`**Priority**: ${t.priority} | **Points**: ${t.storyPoints} | **Status**: ${t.status}`);
|
|
224
|
+
console.log();
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
// Table format
|
|
228
|
+
console.log(chalk.bold('Summary:'));
|
|
229
|
+
console.log(chalk.dim(` Total: ${result.summary.total} tasks`));
|
|
230
|
+
console.log(chalk.dim(` P0: ${result.summary.p0}, P1: ${result.summary.p1}, P2: ${result.summary.p2}, P3: ${result.summary.p3}`));
|
|
231
|
+
console.log(chalk.dim(` Story Points: ${result.summary.totalPoints}, Hours: ${result.summary.totalHours}`));
|
|
232
|
+
console.log();
|
|
233
|
+
|
|
234
|
+
result.tasks.forEach(t => {
|
|
235
|
+
const priorityColor = {
|
|
236
|
+
'P0': chalk.red,
|
|
237
|
+
'P1': chalk.yellow,
|
|
238
|
+
'P2': chalk.blue,
|
|
239
|
+
'P3': chalk.gray
|
|
240
|
+
}[t.priority] || chalk.white;
|
|
241
|
+
|
|
242
|
+
console.log(priorityColor(` TASK-${t.id}: ${t.title}`));
|
|
243
|
+
console.log(chalk.dim(` ${t.priority} | ${t.storyPoints}pts | ${t.estimatedHours}h | ${t.status}`));
|
|
244
|
+
});
|
|
245
|
+
console.log();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
process.exit(0);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Update task status
|
|
256
|
+
program
|
|
257
|
+
.command('update <id> <status>')
|
|
258
|
+
.description('Update task status')
|
|
259
|
+
.option('-f, --file <path>', 'Task breakdown file path')
|
|
260
|
+
.action(async (id, status, options) => {
|
|
261
|
+
try {
|
|
262
|
+
console.log(chalk.bold(`\n📝 Updating TASK-${id}\n`));
|
|
263
|
+
|
|
264
|
+
const generator = new TasksGenerator(process.cwd());
|
|
265
|
+
const result = await generator.updateStatus(id, status, options.file);
|
|
266
|
+
|
|
267
|
+
console.log(chalk.green('✓ Task updated:'));
|
|
268
|
+
console.log(chalk.dim(` TASK-${result.id}: ${result.title}`));
|
|
269
|
+
console.log(chalk.dim(` Status: ${result.oldStatus} → ${result.newStatus}`));
|
|
270
|
+
console.log();
|
|
271
|
+
|
|
272
|
+
process.exit(0);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Validate task breakdown
|
|
280
|
+
program
|
|
281
|
+
.command('validate')
|
|
282
|
+
.description('Validate task breakdown completeness')
|
|
283
|
+
.option('-f, --file <path>', 'Specific task file')
|
|
284
|
+
.option('-v, --verbose', 'Show detailed validation results')
|
|
285
|
+
.action(async (options) => {
|
|
286
|
+
try {
|
|
287
|
+
console.log(chalk.bold('\n🔍 Validating Task Breakdown\n'));
|
|
288
|
+
|
|
289
|
+
const generator = new TasksGenerator(process.cwd());
|
|
290
|
+
const results = await generator.validate(options.file);
|
|
291
|
+
|
|
292
|
+
if (results.passed) {
|
|
293
|
+
console.log(chalk.green('✓ All tasks valid\n'));
|
|
294
|
+
} else {
|
|
295
|
+
console.log(chalk.red('✗ Validation failed\n'));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log(chalk.bold('Summary:'));
|
|
299
|
+
console.log(chalk.dim(` Total: ${results.total}`));
|
|
300
|
+
console.log(chalk.green(` Valid: ${results.valid}`));
|
|
301
|
+
console.log(chalk.red(` Invalid: ${results.invalid}`));
|
|
302
|
+
console.log();
|
|
303
|
+
|
|
304
|
+
if (results.violations.length > 0) {
|
|
305
|
+
console.log(chalk.bold.red('Violations:'));
|
|
306
|
+
results.violations.forEach(v => {
|
|
307
|
+
console.log(chalk.red(` • ${v}`));
|
|
308
|
+
});
|
|
309
|
+
console.log();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (options.verbose && results.details) {
|
|
313
|
+
console.log(chalk.bold('Details:'));
|
|
314
|
+
results.details.forEach(d => {
|
|
315
|
+
const icon = d.valid ? chalk.green('✓') : chalk.red('✗');
|
|
316
|
+
console.log(` ${icon} ${d.file}: ${d.message}`);
|
|
317
|
+
});
|
|
318
|
+
console.log();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
process.exit(results.passed ? 0 : 1);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Generate dependency graph
|
|
329
|
+
program
|
|
330
|
+
.command('graph')
|
|
331
|
+
.description('Generate task dependency graph')
|
|
332
|
+
.option('-f, --file <path>', 'Task breakdown file path')
|
|
333
|
+
.option('--format <type>', 'Output format (mermaid|dot)', 'mermaid')
|
|
334
|
+
.action(async (options) => {
|
|
335
|
+
try {
|
|
336
|
+
console.log(chalk.bold('\n📊 Generating Dependency Graph\n'));
|
|
337
|
+
|
|
338
|
+
const generator = new TasksGenerator(process.cwd());
|
|
339
|
+
const result = await generator.generateGraph(options);
|
|
340
|
+
|
|
341
|
+
console.log(chalk.green('✓ Dependency graph generated\n'));
|
|
342
|
+
console.log(chalk.bold('Graph:'));
|
|
343
|
+
console.log(chalk.cyan(result.graph));
|
|
344
|
+
console.log();
|
|
345
|
+
|
|
346
|
+
if (result.parallelGroups) {
|
|
347
|
+
console.log(chalk.bold('Parallel Execution Groups:'));
|
|
348
|
+
result.parallelGroups.forEach((group, i) => {
|
|
349
|
+
console.log(chalk.dim(` Group ${i + 1}: ${group.join(', ')}`));
|
|
350
|
+
});
|
|
351
|
+
console.log();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
process.exit(0);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
program.parse(process.argv);
|
|
362
|
+
|
|
363
|
+
if (!process.argv.slice(2).length) {
|
|
364
|
+
program.outputHelp();
|
|
365
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musubi-sdd",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Ultimate Specification Driven Development Tool with 25 Agents for 7 AI Coding Platforms (Claude Code, GitHub Copilot, Cursor, Gemini CLI, Windsurf, Codex, Qwen Code)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"musubi-share": "bin/musubi-share.js",
|
|
14
14
|
"musubi-validate": "bin/musubi-validate.js",
|
|
15
15
|
"musubi-requirements": "bin/musubi-requirements.js",
|
|
16
|
-
"musubi-design": "bin/musubi-design.js"
|
|
16
|
+
"musubi-design": "bin/musubi-design.js",
|
|
17
|
+
"musubi-tasks": "bin/musubi-tasks.js"
|
|
17
18
|
},
|
|
18
19
|
"scripts": {
|
|
19
20
|
"test": "jest",
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUSUBI Tasks Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates task breakdown documents with P0-P3 priority labels
|
|
5
|
+
* Supports dependency graphs and parallel execution planning
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const glob = require('glob');
|
|
11
|
+
|
|
12
|
+
class TasksGenerator {
|
|
13
|
+
constructor(workspaceRoot) {
|
|
14
|
+
this.workspaceRoot = workspaceRoot;
|
|
15
|
+
this.templatePath = path.join(__dirname, '../templates/shared/documents/tasks.md');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize task breakdown document
|
|
20
|
+
*/
|
|
21
|
+
async init(feature, options = {}) {
|
|
22
|
+
const outputDir = path.join(this.workspaceRoot, options.output || 'docs/tasks');
|
|
23
|
+
await fs.ensureDir(outputDir);
|
|
24
|
+
|
|
25
|
+
const fileName = this.slugify(feature) + '.md';
|
|
26
|
+
const filePath = path.join(outputDir, fileName);
|
|
27
|
+
|
|
28
|
+
// Read template
|
|
29
|
+
const template = await fs.readFile(this.templatePath, 'utf-8');
|
|
30
|
+
|
|
31
|
+
// Replace template variables
|
|
32
|
+
const content = template
|
|
33
|
+
.replace(/\{\{FEATURE_NAME\}\}/g, feature)
|
|
34
|
+
.replace(/\{\{PROJECT_NAME\}\}/g, options.project || '[Project Name]')
|
|
35
|
+
.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0])
|
|
36
|
+
.replace(/\{\{AUTHOR\}\}/g, options.author || '[Author]')
|
|
37
|
+
.replace(/\{\{COMPONENT\}\}/g, this.slugify(feature));
|
|
38
|
+
|
|
39
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
40
|
+
|
|
41
|
+
return { path: filePath, feature };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find all task breakdown files
|
|
46
|
+
*/
|
|
47
|
+
async findTaskFiles() {
|
|
48
|
+
const patterns = [
|
|
49
|
+
'docs/tasks/**/*.md',
|
|
50
|
+
'tasks/**/*.md'
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const files = [];
|
|
54
|
+
for (const pattern of patterns) {
|
|
55
|
+
const matches = glob.sync(pattern, { cwd: this.workspaceRoot, absolute: true });
|
|
56
|
+
files.push(...matches);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return [...new Set(files)];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add task to breakdown document
|
|
64
|
+
*/
|
|
65
|
+
async addTask(filePath, task) {
|
|
66
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
67
|
+
|
|
68
|
+
// Generate task ID
|
|
69
|
+
const taskId = this.generateTaskId(content);
|
|
70
|
+
|
|
71
|
+
// Format task section
|
|
72
|
+
const taskSection = this.formatTaskSection(taskId, task);
|
|
73
|
+
|
|
74
|
+
// Find insertion point based on priority
|
|
75
|
+
const insertionPoint = this.findTaskInsertionPoint(content, task.priority);
|
|
76
|
+
const updatedContent = content.slice(0, insertionPoint) + '\n' + taskSection + '\n' + content.slice(insertionPoint);
|
|
77
|
+
|
|
78
|
+
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
|
79
|
+
|
|
80
|
+
return { id: taskId, ...task };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate next task ID
|
|
85
|
+
*/
|
|
86
|
+
generateTaskId(content) {
|
|
87
|
+
const taskIds = [];
|
|
88
|
+
const regex = /### TASK-(\d{3}):/g;
|
|
89
|
+
let match;
|
|
90
|
+
|
|
91
|
+
while ((match = regex.exec(content)) !== null) {
|
|
92
|
+
taskIds.push(parseInt(match[1]));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (taskIds.length === 0) {
|
|
96
|
+
return '001';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const maxId = Math.max(...taskIds);
|
|
100
|
+
return String(maxId + 1).padStart(3, '0');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Format task section
|
|
105
|
+
*/
|
|
106
|
+
formatTaskSection(taskId, task) {
|
|
107
|
+
let section = `### TASK-${taskId}: ${task.title}\n\n`;
|
|
108
|
+
section += `**Priority**: ${task.priority}\n`;
|
|
109
|
+
section += `**Story Points**: ${task.storyPoints}\n`;
|
|
110
|
+
section += `**Estimated Hours**: ${task.estimatedHours}\n`;
|
|
111
|
+
section += `**Assignee**: ${task.assignee}\n`;
|
|
112
|
+
section += `**Status**: ${task.status}\n\n`;
|
|
113
|
+
section += `**Description**:\n${task.description}\n\n`;
|
|
114
|
+
|
|
115
|
+
if (task.requirements && task.requirements.length > 0) {
|
|
116
|
+
section += '**Requirements Coverage**:\n';
|
|
117
|
+
task.requirements.forEach(req => {
|
|
118
|
+
section += `- ${req}\n`;
|
|
119
|
+
});
|
|
120
|
+
section += '\n';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (task.acceptance && task.acceptance.length > 0) {
|
|
124
|
+
section += '**Acceptance Criteria**:\n';
|
|
125
|
+
task.acceptance.forEach(criterion => {
|
|
126
|
+
section += `- [ ] ${criterion}\n`;
|
|
127
|
+
});
|
|
128
|
+
section += '\n';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (task.dependencies && task.dependencies.length > 0) {
|
|
132
|
+
section += '**Dependencies**:\n';
|
|
133
|
+
task.dependencies.forEach(dep => {
|
|
134
|
+
section += `- ${dep}\n`;
|
|
135
|
+
});
|
|
136
|
+
section += '\n';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
section += '**Test-First Checklist** (Article III):\n';
|
|
140
|
+
section += '- [ ] Tests written BEFORE implementation\n';
|
|
141
|
+
section += '- [ ] Red: Failing test committed\n';
|
|
142
|
+
section += '- [ ] Green: Minimal implementation passes test\n';
|
|
143
|
+
section += '- [ ] Blue: Refactored with confidence\n\n';
|
|
144
|
+
|
|
145
|
+
section += '**Validation**:\n';
|
|
146
|
+
section += '```bash\n';
|
|
147
|
+
section += '# Verify task completion\n';
|
|
148
|
+
section += 'npm test\n';
|
|
149
|
+
section += 'npm run lint\n';
|
|
150
|
+
section += '```\n';
|
|
151
|
+
|
|
152
|
+
return section;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Find insertion point for task based on priority
|
|
157
|
+
*/
|
|
158
|
+
findTaskInsertionPoint(content, priority) {
|
|
159
|
+
const priorityHeaders = {
|
|
160
|
+
'P0': '## P0 Tasks',
|
|
161
|
+
'P1': '## P1 Tasks',
|
|
162
|
+
'P2': '## P2 Tasks',
|
|
163
|
+
'P3': '## P2 Tasks'
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const headerPattern = priorityHeaders[priority];
|
|
167
|
+
if (!headerPattern) {
|
|
168
|
+
return content.length;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const headerIndex = content.indexOf(headerPattern);
|
|
172
|
+
if (headerIndex === -1) {
|
|
173
|
+
// Priority section doesn't exist, add at end
|
|
174
|
+
return content.length;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Find end of section (before next ## header or end of document)
|
|
178
|
+
const nextHeaderRegex = /\n## /g;
|
|
179
|
+
nextHeaderRegex.lastIndex = headerIndex + headerPattern.length;
|
|
180
|
+
const nextMatch = nextHeaderRegex.exec(content);
|
|
181
|
+
|
|
182
|
+
if (nextMatch) {
|
|
183
|
+
return nextMatch.index + 1; // Include the newline
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return content.length;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* List all tasks
|
|
191
|
+
*/
|
|
192
|
+
async list(options = {}) {
|
|
193
|
+
const files = options.file ? [options.file] : await this.findTaskFiles();
|
|
194
|
+
const allTasks = [];
|
|
195
|
+
|
|
196
|
+
for (const file of files) {
|
|
197
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
198
|
+
const tasks = this.parseTasks(content);
|
|
199
|
+
allTasks.push(...tasks);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Filter by priority
|
|
203
|
+
let filtered = allTasks;
|
|
204
|
+
if (options.priority) {
|
|
205
|
+
filtered = filtered.filter(t => t.priority === options.priority);
|
|
206
|
+
}
|
|
207
|
+
if (options.status) {
|
|
208
|
+
filtered = filtered.filter(t => t.status === options.status);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Calculate summary
|
|
212
|
+
const summary = {
|
|
213
|
+
total: filtered.length,
|
|
214
|
+
p0: filtered.filter(t => t.priority === 'P0').length,
|
|
215
|
+
p1: filtered.filter(t => t.priority === 'P1').length,
|
|
216
|
+
p2: filtered.filter(t => t.priority === 'P2').length,
|
|
217
|
+
p3: filtered.filter(t => t.priority === 'P3').length,
|
|
218
|
+
totalPoints: filtered.reduce((sum, t) => sum + (t.storyPoints || 0), 0),
|
|
219
|
+
totalHours: filtered.reduce((sum, t) => sum + (t.estimatedHours || 0), 0)
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return { tasks: filtered, summary };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parse tasks from content
|
|
227
|
+
*/
|
|
228
|
+
parseTasks(content) {
|
|
229
|
+
const tasks = [];
|
|
230
|
+
const taskRegex = /### TASK-(\d{3}): (.+?)\n\n\*\*Priority\*\*: (P[0-3])\n\*\*Story Points\*\*: (\d+)\n\*\*Estimated Hours\*\*: ([\d.]+)\n\*\*Assignee\*\*: (.+?)\n\*\*Status\*\*: (.+?)\n/g;
|
|
231
|
+
let match;
|
|
232
|
+
|
|
233
|
+
while ((match = taskRegex.exec(content)) !== null) {
|
|
234
|
+
tasks.push({
|
|
235
|
+
id: match[1],
|
|
236
|
+
title: match[2],
|
|
237
|
+
priority: match[3],
|
|
238
|
+
storyPoints: parseInt(match[4]),
|
|
239
|
+
estimatedHours: parseFloat(match[5]),
|
|
240
|
+
assignee: match[6],
|
|
241
|
+
status: match[7]
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return tasks;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Update task status
|
|
250
|
+
*/
|
|
251
|
+
async updateStatus(taskId, newStatus, filePath = null) {
|
|
252
|
+
const files = filePath ? [filePath] : await this.findTaskFiles();
|
|
253
|
+
|
|
254
|
+
for (const file of files) {
|
|
255
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
256
|
+
const taskPattern = new RegExp(`(### TASK-${taskId}: .+?\\n\\n\\*\\*Priority\\*\\*: .+?\\n\\*\\*Story Points\\*\\*: .+?\\n\\*\\*Estimated Hours\\*\\*: .+?\\n\\*\\*Assignee\\*\\*: .+?\\n\\*\\*Status\\*\\*: )(.+?)(\\n)`, 'g');
|
|
257
|
+
|
|
258
|
+
const match = taskPattern.exec(content);
|
|
259
|
+
if (match) {
|
|
260
|
+
const oldStatus = match[2];
|
|
261
|
+
const updatedContent = content.replace(taskPattern, `$1${newStatus}$3`);
|
|
262
|
+
await fs.writeFile(file, updatedContent, 'utf-8');
|
|
263
|
+
|
|
264
|
+
// Extract task title
|
|
265
|
+
const titleMatch = content.match(new RegExp(`### TASK-${taskId}: (.+?)\\n`));
|
|
266
|
+
const title = titleMatch ? titleMatch[1] : 'Unknown';
|
|
267
|
+
|
|
268
|
+
return { id: taskId, title, oldStatus, newStatus };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
throw new Error(`Task TASK-${taskId} not found`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Validate task breakdown
|
|
277
|
+
*/
|
|
278
|
+
async validate(filePath = null) {
|
|
279
|
+
const files = filePath ? [filePath] : await this.findTaskFiles();
|
|
280
|
+
const violations = [];
|
|
281
|
+
let total = 0;
|
|
282
|
+
let valid = 0;
|
|
283
|
+
|
|
284
|
+
for (const file of files) {
|
|
285
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
286
|
+
const tasks = this.parseTasks(content);
|
|
287
|
+
total += tasks.length;
|
|
288
|
+
|
|
289
|
+
tasks.forEach(task => {
|
|
290
|
+
let isValid = true;
|
|
291
|
+
|
|
292
|
+
// Validate required fields
|
|
293
|
+
if (!task.title || task.title.length === 0) {
|
|
294
|
+
violations.push(`TASK-${task.id}: Missing title`);
|
|
295
|
+
isValid = false;
|
|
296
|
+
}
|
|
297
|
+
if (!['P0', 'P1', 'P2', 'P3'].includes(task.priority)) {
|
|
298
|
+
violations.push(`TASK-${task.id}: Invalid priority ${task.priority}`);
|
|
299
|
+
isValid = false;
|
|
300
|
+
}
|
|
301
|
+
if (!task.storyPoints || task.storyPoints <= 0) {
|
|
302
|
+
violations.push(`TASK-${task.id}: Invalid story points`);
|
|
303
|
+
isValid = false;
|
|
304
|
+
}
|
|
305
|
+
if (!task.estimatedHours || task.estimatedHours <= 0) {
|
|
306
|
+
violations.push(`TASK-${task.id}: Invalid estimated hours`);
|
|
307
|
+
isValid = false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (isValid) {
|
|
311
|
+
valid++;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
passed: violations.length === 0,
|
|
318
|
+
total,
|
|
319
|
+
valid,
|
|
320
|
+
invalid: total - valid,
|
|
321
|
+
violations
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Generate dependency graph
|
|
327
|
+
*/
|
|
328
|
+
async generateGraph(options = {}) {
|
|
329
|
+
const files = options.file ? [options.file] : await this.findTaskFiles();
|
|
330
|
+
const tasks = [];
|
|
331
|
+
const dependencies = new Map();
|
|
332
|
+
|
|
333
|
+
for (const file of files) {
|
|
334
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
335
|
+
const fileTasks = this.parseTasksWithDependencies(content);
|
|
336
|
+
tasks.push(...fileTasks);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Build dependency map
|
|
340
|
+
tasks.forEach(task => {
|
|
341
|
+
dependencies.set(task.id, task.dependencies || []);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Generate graph based on format
|
|
345
|
+
let graph = '';
|
|
346
|
+
if (options.format === 'dot') {
|
|
347
|
+
graph = this.generateDotGraph(tasks, dependencies);
|
|
348
|
+
} else {
|
|
349
|
+
graph = this.generateMermaidGraph(tasks, dependencies);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Calculate parallel execution groups
|
|
353
|
+
const parallelGroups = this.calculateParallelGroups(tasks, dependencies);
|
|
354
|
+
|
|
355
|
+
return { graph, parallelGroups };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Parse tasks with dependencies
|
|
360
|
+
*/
|
|
361
|
+
parseTasksWithDependencies(content) {
|
|
362
|
+
const tasks = [];
|
|
363
|
+
const taskSections = content.split(/### TASK-(\d{3}):/);
|
|
364
|
+
|
|
365
|
+
for (let i = 1; i < taskSections.length; i += 2) {
|
|
366
|
+
const id = taskSections[i];
|
|
367
|
+
const section = taskSections[i + 1];
|
|
368
|
+
|
|
369
|
+
const titleMatch = section.match(/^(.+?)\n/);
|
|
370
|
+
const title = titleMatch ? titleMatch[1].trim() : 'Unknown';
|
|
371
|
+
|
|
372
|
+
const depsMatch = section.match(/\*\*Dependencies\*\*:\n((?:- TASK-\d{3}.+?\n)+)/);
|
|
373
|
+
const dependencies = [];
|
|
374
|
+
if (depsMatch) {
|
|
375
|
+
const depLines = depsMatch[1].match(/TASK-(\d{3})/g);
|
|
376
|
+
if (depLines) {
|
|
377
|
+
dependencies.push(...depLines.map(d => d.replace('TASK-', '')));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
tasks.push({ id, title, dependencies });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return tasks;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Generate Mermaid graph
|
|
389
|
+
*/
|
|
390
|
+
generateMermaidGraph(tasks, dependencies) {
|
|
391
|
+
let graph = 'graph TD\n';
|
|
392
|
+
|
|
393
|
+
tasks.forEach(task => {
|
|
394
|
+
graph += ` TASK${task.id}["TASK-${task.id}: ${task.title}"]\n`;
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
dependencies.forEach((deps, taskId) => {
|
|
398
|
+
deps.forEach(depId => {
|
|
399
|
+
graph += ` TASK${depId} --> TASK${taskId}\n`;
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return graph;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Generate DOT graph
|
|
408
|
+
*/
|
|
409
|
+
generateDotGraph(tasks, dependencies) {
|
|
410
|
+
let graph = 'digraph Tasks {\n';
|
|
411
|
+
graph += ' rankdir=TB;\n';
|
|
412
|
+
graph += ' node [shape=box];\n\n';
|
|
413
|
+
|
|
414
|
+
tasks.forEach(task => {
|
|
415
|
+
graph += ` TASK${task.id} [label="TASK-${task.id}\\n${task.title}"];\n`;
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
dependencies.forEach((deps, taskId) => {
|
|
419
|
+
deps.forEach(depId => {
|
|
420
|
+
graph += ` TASK${depId} -> TASK${taskId};\n`;
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
graph += '}';
|
|
425
|
+
return graph;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Calculate parallel execution groups
|
|
430
|
+
*/
|
|
431
|
+
calculateParallelGroups(tasks, dependencies) {
|
|
432
|
+
const groups = [];
|
|
433
|
+
const processed = new Set();
|
|
434
|
+
const iterations = tasks.length + 1; // Prevent infinite loop
|
|
435
|
+
let iteration = 0;
|
|
436
|
+
|
|
437
|
+
while (processed.size < tasks.length && iteration < iterations) {
|
|
438
|
+
iteration++;
|
|
439
|
+
const group = [];
|
|
440
|
+
|
|
441
|
+
tasks.forEach(task => {
|
|
442
|
+
if (processed.has(task.id)) return;
|
|
443
|
+
|
|
444
|
+
// Check if all dependencies are processed
|
|
445
|
+
const deps = dependencies.get(task.id) || [];
|
|
446
|
+
const allDepsProcessed = deps.every(depId => processed.has(depId) || depId === task.id);
|
|
447
|
+
|
|
448
|
+
if (allDepsProcessed) {
|
|
449
|
+
group.push(`TASK-${task.id}`);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (group.length === 0 && processed.size < tasks.length) {
|
|
454
|
+
// Circular dependency detected
|
|
455
|
+
const remaining = tasks.filter(t => !processed.has(t.id));
|
|
456
|
+
group.push(...remaining.map(t => `TASK-${t.id} (circular)`));
|
|
457
|
+
remaining.forEach(t => processed.add(t.id));
|
|
458
|
+
} else {
|
|
459
|
+
// Mark this group as processed
|
|
460
|
+
group.forEach(taskId => {
|
|
461
|
+
const id = taskId.replace('TASK-', '').replace(' (circular)', '');
|
|
462
|
+
processed.add(id);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (group.length > 0) {
|
|
467
|
+
groups.push(group);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return groups;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Slugify feature name
|
|
476
|
+
*/
|
|
477
|
+
slugify(str) {
|
|
478
|
+
return str
|
|
479
|
+
.toLowerCase()
|
|
480
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
481
|
+
.replace(/^-+|-+$/g, '');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
module.exports = TasksGenerator;
|