musubi-sdd 0.8.2 → 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 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
@@ -16,9 +16,20 @@
16
16
 
17
17
  const { Command } = require('commander');
18
18
  const chalk = require('chalk');
19
- const inquirer = require('inquirer');
20
19
  const DesignGenerator = require('../src/generators/design');
21
20
 
21
+ let inquirer;
22
+
23
+ /**
24
+ * Initialize inquirer (ESM module in v9+)
25
+ */
26
+ async function getInquirer() {
27
+ if (!inquirer) {
28
+ inquirer = (await import('inquirer')).default;
29
+ }
30
+ return inquirer;
31
+ }
32
+
22
33
  const program = new Command();
23
34
 
24
35
  program
@@ -83,7 +94,8 @@ program
83
94
  if (files.length === 1) {
84
95
  designFile = files[0];
85
96
  } else {
86
- const answer = await inquirer.prompt([{
97
+ const inquirerInst = await getInquirer();
98
+ const answer = await inquirerInst.prompt([{
87
99
  type: 'list',
88
100
  name: 'file',
89
101
  message: 'Select design file:',
@@ -94,7 +106,8 @@ program
94
106
  }
95
107
 
96
108
  // Interactive prompts for C4 diagram
97
- const answers = await inquirer.prompt([
109
+ const inquirerInst2 = await getInquirer();
110
+ const answers = await inquirerInst2.prompt([
98
111
  {
99
112
  type: 'input',
100
113
  name: 'title',
@@ -157,7 +170,8 @@ program
157
170
  if (files.length === 1) {
158
171
  designFile = files[0];
159
172
  } else {
160
- const answer = await inquirer.prompt([{
173
+ const inquirerInst = await getInquirer();
174
+ const answer = await inquirerInst.prompt([{
161
175
  type: 'list',
162
176
  name: 'file',
163
177
  message: 'Select design file:',
@@ -168,7 +182,8 @@ program
168
182
  }
169
183
 
170
184
  // Interactive prompts for ADR
171
- const answers = await inquirer.prompt([
185
+ const inquirerInst2 = await getInquirer();
186
+ const answers = await inquirerInst2.prompt([
172
187
  {
173
188
  type: 'input',
174
189
  name: 'context',
File without changes
@@ -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.2",
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;