@polymorphism-tech/morph-spec 2.2.0 → 2.3.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/CLAUDE.md +65 -8
- package/bin/morph-spec.js +121 -0
- package/bin/task-manager.js +368 -0
- package/bin/validate.js +268 -0
- package/content/.claude/skills/specialists/ef-modeler.md +11 -0
- package/content/.claude/skills/specialists/hangfire-orchestrator.md +10 -0
- package/content/.claude/skills/specialists/ui-ux-designer.md +40 -0
- package/content/.claude/skills/stacks/dotnet-blazor.md +18 -0
- package/content/.morph/examples/state-v3.json +188 -0
- package/package.json +1 -1
- package/src/commands/task.js +75 -0
- package/src/lib/continuous-validator.js +440 -0
- package/src/lib/learning-system.js +520 -0
- package/src/lib/mockup-generator.js +366 -0
- package/src/lib/ui-detector.js +350 -0
- package/src/lib/validators/architecture-validator.js +387 -0
- package/src/lib/validators/package-validator.js +360 -0
- package/src/lib/validators/ui-contrast-validator.js +422 -0
package/CLAUDE.md
CHANGED
|
@@ -570,17 +570,74 @@ Formato: Sempre as 3 ações acima (padrão para esta fase)
|
|
|
570
570
|
|
|
571
571
|
### FASE 5: IMPLEMENT
|
|
572
572
|
```
|
|
573
|
-
Gatilho: Tasks aprovadas
|
|
573
|
+
Gatilho: Tasks aprovadas (FASE 4 concluída)
|
|
574
|
+
|
|
574
575
|
Ações:
|
|
575
|
-
1. Implementar task por task
|
|
576
|
-
2.
|
|
577
|
-
|
|
578
|
-
|
|
576
|
+
1. Implementar task por task seguindo tasks em state.json
|
|
577
|
+
2. **SEMPRE chamar CLI após completar cada task:**
|
|
578
|
+
```bash
|
|
579
|
+
npx morph-spec task done {feature} {task-id}
|
|
580
|
+
```
|
|
581
|
+
3. Aguardar confirmação do CLI (validação de dependencies)
|
|
582
|
+
4. Framework atualiza state.json automaticamente:
|
|
583
|
+
- Marca task como completed
|
|
584
|
+
- Atualiza progress (percentage, counters)
|
|
585
|
+
- Detecta checkpoints automaticamente (a cada 3 tasks ou flag checkpoint)
|
|
586
|
+
- Mostra próxima task sugerida
|
|
587
|
+
5. Executar testes quando task requer
|
|
588
|
+
6. Gerar recap.md ao final de todas as tasks
|
|
589
|
+
|
|
590
|
+
**Workflow de implementação:**
|
|
591
|
+
|
|
592
|
+
# Claude implementa T001
|
|
593
|
+
Claude: "Vou criar a entidade ScheduledReport..."
|
|
594
|
+
[Gera Domain/Entities/ScheduledReport.cs]
|
|
595
|
+
[Gera Infrastructure/Persistence/Config/ScheduledReportConfig.cs]
|
|
596
|
+
|
|
597
|
+
# Claude chama CLI explicitamente
|
|
598
|
+
Claude executa:
|
|
599
|
+
npx morph-spec task done scheduled-reports T001
|
|
600
|
+
|
|
601
|
+
# CLI valida e atualiza state.json
|
|
602
|
+
CLI output:
|
|
603
|
+
✅ Task T001 completed!
|
|
604
|
+
📊 Progress: 11% (1/9)
|
|
605
|
+
[█████░░░░░░░░░░░░░░░░░░░░░░░░] 11%
|
|
606
|
+
⏭️ Next: T002 - Create CreateReportCommand + Handler
|
|
607
|
+
|
|
608
|
+
# Claude confirma ao usuário
|
|
609
|
+
Claude: "✅ T001 concluída!
|
|
610
|
+
Entidade ScheduledReport criada com EF Core mapping.
|
|
611
|
+
Progresso: 11% (1/9)
|
|
612
|
+
Próxima: T002 - Create CreateReportCommand + Handler"
|
|
613
|
+
|
|
614
|
+
# Após completar 3 tasks
|
|
615
|
+
npx morph-spec task done scheduled-reports T003
|
|
616
|
+
|
|
617
|
+
CLI output:
|
|
618
|
+
✅ Task T003 completed!
|
|
619
|
+
🎉 Auto-checkpoint reached! 3 tasks completed
|
|
620
|
+
📊 Progress: 33% (3/9)
|
|
621
|
+
⏭️ Next: T004 - Create ReportList component
|
|
622
|
+
|
|
623
|
+
**NUNCA:**
|
|
624
|
+
- ❌ Atualizar state.json manualmente
|
|
625
|
+
- ❌ Atualizar tasks.json (não existe mais em schema 3.0.0)
|
|
626
|
+
- ❌ Criar scripts PowerShell para atualizar tasks
|
|
627
|
+
- ❌ Pular tasks com dependencies não completadas
|
|
628
|
+
- ❌ Esquecer de chamar CLI após implementar task
|
|
629
|
+
|
|
630
|
+
**SEMPRE:**
|
|
631
|
+
- ✅ Chamar `npx morph-spec task done` após cada task
|
|
632
|
+
- ✅ Verificar output do CLI (validações, próxima task)
|
|
633
|
+
- ✅ Respeitar ordem de dependencies
|
|
634
|
+
- ✅ Deixar framework gerenciar checkpoints automaticamente
|
|
579
635
|
|
|
580
636
|
Outputs:
|
|
581
|
-
- Código implementado
|
|
582
|
-
- Testes criados
|
|
583
|
-
- .
|
|
637
|
+
- Código implementado (task por task)
|
|
638
|
+
- Testes criados (quando task requer)
|
|
639
|
+
- state.json atualizado automaticamente pelo CLI
|
|
640
|
+
- .morph/project/outputs/{feature}/recap.md (ao final)
|
|
584
641
|
```
|
|
585
642
|
|
|
586
643
|
### FASE 6: SYNC (condicional)
|
package/bin/morph-spec.js
CHANGED
|
@@ -15,9 +15,12 @@ import { createStoryCommand } from '../src/commands/create-story.js';
|
|
|
15
15
|
import { shardSpecCommand } from '../src/commands/shard-spec.js';
|
|
16
16
|
import { sprintStatusCommand } from '../src/commands/sprint-status.js';
|
|
17
17
|
import { stateCommand } from '../src/commands/state.js';
|
|
18
|
+
import { taskDoneCommand, taskStartCommand, taskNextCommand } from '../src/commands/task.js';
|
|
18
19
|
import { costCommand } from '../src/commands/cost.js';
|
|
19
20
|
import { generateDesignSystemCommand } from '../src/commands/generate.js';
|
|
20
21
|
import { updateResourcePricing, showPricing, validatePricing } from '../src/commands/update-pricing.js';
|
|
22
|
+
import { validateCommand } from './validate.js';
|
|
23
|
+
import { LearningSystem } from '../src/lib/learning-system.js';
|
|
21
24
|
|
|
22
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
26
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -111,6 +114,26 @@ program
|
|
|
111
114
|
.option('--json', 'Output as JSON (get command)')
|
|
112
115
|
.action((action, args, options) => stateCommand(action, args, options));
|
|
113
116
|
|
|
117
|
+
// Task management commands (MORPH-SPEC 3.0)
|
|
118
|
+
const taskCommand = program
|
|
119
|
+
.command('task')
|
|
120
|
+
.description('Manage feature tasks (done | start | next)');
|
|
121
|
+
|
|
122
|
+
taskCommand
|
|
123
|
+
.command('done <feature> <task-ids...>')
|
|
124
|
+
.description('Mark tasks as completed')
|
|
125
|
+
.action((feature, taskIds, options) => taskDoneCommand(feature, taskIds, options));
|
|
126
|
+
|
|
127
|
+
taskCommand
|
|
128
|
+
.command('start <feature> <task-id>')
|
|
129
|
+
.description('Start a task (mark as in_progress)')
|
|
130
|
+
.action((feature, taskId, options) => taskStartCommand(feature, taskId, options));
|
|
131
|
+
|
|
132
|
+
taskCommand
|
|
133
|
+
.command('next <feature>')
|
|
134
|
+
.description('Show next suggested task')
|
|
135
|
+
.action((feature, options) => taskNextCommand(feature, options));
|
|
136
|
+
|
|
114
137
|
// Cost calculation command
|
|
115
138
|
program
|
|
116
139
|
.command('cost <bicep-files>')
|
|
@@ -170,4 +193,102 @@ generateCommand
|
|
|
170
193
|
.option('--dry-run', 'Preview without writing files')
|
|
171
194
|
.action(generateDesignSystemCommand);
|
|
172
195
|
|
|
196
|
+
// Validation commands (Sprint 4: Continuous Validation)
|
|
197
|
+
program
|
|
198
|
+
.command('validate [validator]')
|
|
199
|
+
.description('Run project validations (all | packages | architecture | contrast)')
|
|
200
|
+
.option('-v, --verbose', 'Show detailed output')
|
|
201
|
+
.option('--auto-fix, --fix', 'Auto-fix issues where possible')
|
|
202
|
+
.option('--no-fail', 'Don\'t exit with error code')
|
|
203
|
+
.option('-i, --insights', 'Show learning insights')
|
|
204
|
+
.option('--wcag-aaa', 'Use WCAG AAA standard (stricter)')
|
|
205
|
+
.action((validator, options) => {
|
|
206
|
+
const args = validator ? [validator] : ['all'];
|
|
207
|
+
if (options.verbose) args.push('--verbose');
|
|
208
|
+
if (options.autoFix) args.push('--auto-fix');
|
|
209
|
+
if (options.noFail) args.push('--no-fail');
|
|
210
|
+
if (options.insights) args.push('--insights');
|
|
211
|
+
if (options.wcagAaa) args.push('--wcag-aaa');
|
|
212
|
+
validateCommand(args);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Learning commands (Sprint 4: Learning System)
|
|
216
|
+
const learnCommand = program
|
|
217
|
+
.command('learn')
|
|
218
|
+
.description('AI learning system commands');
|
|
219
|
+
|
|
220
|
+
learnCommand
|
|
221
|
+
.command('analyze')
|
|
222
|
+
.description('Learn from project history (decisions.md files)')
|
|
223
|
+
.option('-v, --verbose', 'Show detailed learning progress')
|
|
224
|
+
.action(async (options) => {
|
|
225
|
+
const learner = new LearningSystem('.');
|
|
226
|
+
await learner.learnFromProject();
|
|
227
|
+
|
|
228
|
+
if (options.verbose) {
|
|
229
|
+
learner.formatInsights();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
learnCommand
|
|
234
|
+
.command('insights')
|
|
235
|
+
.description('Show AI insights and patterns')
|
|
236
|
+
.option('--json', 'Output as JSON')
|
|
237
|
+
.action((options) => {
|
|
238
|
+
const learner = new LearningSystem('.');
|
|
239
|
+
|
|
240
|
+
if (options.json) {
|
|
241
|
+
console.log(JSON.stringify(learner.getInsightsSummary(), null, 2));
|
|
242
|
+
} else {
|
|
243
|
+
learner.formatInsights();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
learnCommand
|
|
248
|
+
.command('suggest [category]')
|
|
249
|
+
.description('Get AI suggestions for a category (uiLibrary | architecture | infrastructure | authentication | stateManagement | testing)')
|
|
250
|
+
.option('--json', 'Output as JSON')
|
|
251
|
+
.action((category, options) => {
|
|
252
|
+
const learner = new LearningSystem('.');
|
|
253
|
+
|
|
254
|
+
if (category) {
|
|
255
|
+
const suggestion = learner.getSuggestion(category);
|
|
256
|
+
if (options.json) {
|
|
257
|
+
console.log(JSON.stringify(suggestion, null, 2));
|
|
258
|
+
} else {
|
|
259
|
+
console.log(chalk.cyan(`\n💡 Suggestion for ${category}:\n`));
|
|
260
|
+
if (suggestion.confidence === 'none') {
|
|
261
|
+
console.log(chalk.yellow(` ${suggestion.reason}`));
|
|
262
|
+
} else {
|
|
263
|
+
console.log(chalk.white(` → ${suggestion.suggestion} (${suggestion.percentage}% confidence)`));
|
|
264
|
+
console.log(chalk.gray(` ${suggestion.reason}`));
|
|
265
|
+
}
|
|
266
|
+
console.log('');
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
const suggestions = learner.getAllSuggestions();
|
|
270
|
+
if (options.json) {
|
|
271
|
+
console.log(JSON.stringify(suggestions, null, 2));
|
|
272
|
+
} else {
|
|
273
|
+
learner.formatSuggestions(suggestions);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
learnCommand
|
|
279
|
+
.command('reset')
|
|
280
|
+
.description('Reset knowledge base (clear all learned patterns)')
|
|
281
|
+
.option('--force', 'Skip confirmation')
|
|
282
|
+
.action((options) => {
|
|
283
|
+
if (!options.force) {
|
|
284
|
+
console.log(chalk.yellow('\n⚠️ This will delete all learned patterns and preferences.'));
|
|
285
|
+
console.log(chalk.gray(' Run with --force to confirm.\n'));
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const learner = new LearningSystem('.');
|
|
290
|
+
learner.reset();
|
|
291
|
+
console.log(chalk.green('\n✅ Knowledge base reset\n'));
|
|
292
|
+
});
|
|
293
|
+
|
|
173
294
|
program.parse();
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MORPH-SPEC Task Manager
|
|
5
|
+
*
|
|
6
|
+
* Manages task completion, dependencies, checkpoints, and progress tracking.
|
|
7
|
+
* Part of MORPH-SPEC 3.0 - Event-driven state management.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs').promises;
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
|
|
14
|
+
class TaskManager {
|
|
15
|
+
constructor(statePath = '.morph/state.json') {
|
|
16
|
+
this.statePath = statePath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load state.json
|
|
21
|
+
*/
|
|
22
|
+
async loadState() {
|
|
23
|
+
try {
|
|
24
|
+
const content = await fs.readFile(this.statePath, 'utf-8');
|
|
25
|
+
return JSON.parse(content);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT') {
|
|
28
|
+
throw new Error(`State file not found: ${this.statePath}. Run 'npx morph-spec state init' first.`);
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Save state.json
|
|
36
|
+
*/
|
|
37
|
+
async saveState(state) {
|
|
38
|
+
state.project.updatedAt = new Date().toISOString();
|
|
39
|
+
await fs.writeFile(this.statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Complete one or more tasks
|
|
44
|
+
*/
|
|
45
|
+
async completeTasks(featureName, taskIds) {
|
|
46
|
+
const state = await this.loadState();
|
|
47
|
+
const feature = state.features[featureName];
|
|
48
|
+
|
|
49
|
+
if (!feature) {
|
|
50
|
+
throw new Error(`Feature '${featureName}' not found in state.json`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const results = [];
|
|
54
|
+
|
|
55
|
+
for (const taskId of taskIds) {
|
|
56
|
+
const task = feature.tasks.find(t => t.id === taskId);
|
|
57
|
+
|
|
58
|
+
if (!task) {
|
|
59
|
+
console.error(chalk.red(`❌ Task ${taskId} not found`));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (task.status === 'completed') {
|
|
64
|
+
console.log(chalk.yellow(`⚠️ Task ${taskId} already completed`));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate dependencies
|
|
69
|
+
const missingDeps = this.checkDependencies(task, feature.tasks);
|
|
70
|
+
if (missingDeps.length > 0) {
|
|
71
|
+
console.error(chalk.red(`❌ Cannot complete ${taskId}: missing dependencies: ${missingDeps.join(', ')}`));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Mark as completed
|
|
76
|
+
task.status = 'completed';
|
|
77
|
+
task.completedAt = new Date().toISOString();
|
|
78
|
+
task.completedBy = 'claude';
|
|
79
|
+
|
|
80
|
+
results.push(task);
|
|
81
|
+
|
|
82
|
+
console.log(chalk.green(`✅ Task ${taskId} completed!`));
|
|
83
|
+
|
|
84
|
+
// Register checkpoint if task has one
|
|
85
|
+
if (task.checkpoint) {
|
|
86
|
+
this.registerCheckpoint(feature, task);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Update progress
|
|
91
|
+
feature.progress = this.calculateProgress(feature.tasks);
|
|
92
|
+
|
|
93
|
+
// Auto-checkpoint every 3 tasks
|
|
94
|
+
const recentCompleted = this.getRecentCompleted(feature.tasks, 3);
|
|
95
|
+
if (recentCompleted.length === 3) {
|
|
96
|
+
const lastCheckpoint = feature.checkpoints[feature.checkpoints.length - 1];
|
|
97
|
+
const shouldAutoCheckpoint = !lastCheckpoint ||
|
|
98
|
+
!recentCompleted.every(t =>
|
|
99
|
+
lastCheckpoint.tasksCompleted.includes(t.id)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (shouldAutoCheckpoint) {
|
|
103
|
+
this.autoCheckpoint(feature, recentCompleted);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Save state
|
|
108
|
+
await this.saveState(state);
|
|
109
|
+
|
|
110
|
+
// Display progress
|
|
111
|
+
this.displayProgress(feature);
|
|
112
|
+
|
|
113
|
+
// Suggest next task
|
|
114
|
+
const nextTask = this.getNextTask(feature.tasks);
|
|
115
|
+
if (nextTask) {
|
|
116
|
+
console.log(chalk.cyan(`\n⏭️ Next: ${nextTask.id} - ${nextTask.title}`));
|
|
117
|
+
if (nextTask.dependencies && nextTask.dependencies.length > 0) {
|
|
118
|
+
console.log(chalk.gray(` Dependencies: ${nextTask.dependencies.join(', ')}`));
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.green('\n🎉 All tasks completed!'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if task dependencies are met
|
|
129
|
+
*/
|
|
130
|
+
checkDependencies(task, allTasks) {
|
|
131
|
+
if (!task.dependencies || task.dependencies.length === 0) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return task.dependencies.filter(depId => {
|
|
136
|
+
const dep = allTasks.find(t => t.id === depId);
|
|
137
|
+
return !dep || dep.status !== 'completed';
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Calculate progress
|
|
143
|
+
*/
|
|
144
|
+
calculateProgress(tasks) {
|
|
145
|
+
const total = tasks.length;
|
|
146
|
+
const completed = tasks.filter(t => t.status === 'completed').length;
|
|
147
|
+
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
|
|
148
|
+
const pending = tasks.filter(t => t.status === 'pending').length;
|
|
149
|
+
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
total,
|
|
153
|
+
completed,
|
|
154
|
+
inProgress,
|
|
155
|
+
pending,
|
|
156
|
+
percentage
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Register checkpoint (from task flag)
|
|
162
|
+
*/
|
|
163
|
+
registerCheckpoint(feature, task) {
|
|
164
|
+
const checkpoint = {
|
|
165
|
+
id: task.checkpoint,
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
tasksCompleted: [task.id],
|
|
168
|
+
note: `Checkpoint: ${task.title}`
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
feature.checkpoints = feature.checkpoints || [];
|
|
172
|
+
feature.checkpoints.push(checkpoint);
|
|
173
|
+
|
|
174
|
+
console.log(chalk.magenta(`\n🎯 ${task.checkpoint} reached!`));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Auto-checkpoint every 3 tasks
|
|
179
|
+
*/
|
|
180
|
+
autoCheckpoint(feature, tasks) {
|
|
181
|
+
const checkpoint = {
|
|
182
|
+
id: `CHECKPOINT_AUTO_${Date.now()}`,
|
|
183
|
+
timestamp: new Date().toISOString(),
|
|
184
|
+
tasksCompleted: tasks.map(t => t.id),
|
|
185
|
+
note: `Auto-checkpoint: ${tasks.length} tasks completed`
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
feature.checkpoints = feature.checkpoints || [];
|
|
189
|
+
feature.checkpoints.push(checkpoint);
|
|
190
|
+
|
|
191
|
+
console.log(chalk.magenta(`\n🎉 Auto-checkpoint reached! ${tasks.length} tasks completed`));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get recently completed tasks (last N)
|
|
196
|
+
*/
|
|
197
|
+
getRecentCompleted(tasks, count) {
|
|
198
|
+
const completed = tasks
|
|
199
|
+
.filter(t => t.status === 'completed' && t.completedAt)
|
|
200
|
+
.sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
|
|
201
|
+
|
|
202
|
+
return completed.slice(0, count);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get next pending task (based on dependencies)
|
|
207
|
+
*/
|
|
208
|
+
getNextTask(tasks) {
|
|
209
|
+
const pending = tasks.filter(t => t.status === 'pending');
|
|
210
|
+
|
|
211
|
+
// Find first task with all dependencies completed
|
|
212
|
+
for (const task of pending) {
|
|
213
|
+
const missingDeps = this.checkDependencies(task, tasks);
|
|
214
|
+
if (missingDeps.length === 0) {
|
|
215
|
+
return task;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Display progress
|
|
224
|
+
*/
|
|
225
|
+
displayProgress(feature) {
|
|
226
|
+
const { total, completed, inProgress, pending, percentage } = feature.progress;
|
|
227
|
+
|
|
228
|
+
console.log(chalk.bold(`\n📊 Progress: ${percentage}% (${completed}/${total})`));
|
|
229
|
+
console.log(chalk.gray(` Completed: ${completed} | In Progress: ${inProgress} | Pending: ${pending}`));
|
|
230
|
+
|
|
231
|
+
// Progress bar
|
|
232
|
+
const barLength = 30;
|
|
233
|
+
const filledLength = Math.round((percentage / 100) * barLength);
|
|
234
|
+
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
|
|
235
|
+
console.log(chalk.cyan(` [${bar}] ${percentage}%`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Start a task (mark as in_progress)
|
|
240
|
+
*/
|
|
241
|
+
async startTask(featureName, taskId) {
|
|
242
|
+
const state = await this.loadState();
|
|
243
|
+
const feature = state.features[featureName];
|
|
244
|
+
|
|
245
|
+
if (!feature) {
|
|
246
|
+
throw new Error(`Feature '${featureName}' not found`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const task = feature.tasks.find(t => t.id === taskId);
|
|
250
|
+
|
|
251
|
+
if (!task) {
|
|
252
|
+
throw new Error(`Task ${taskId} not found`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (task.status === 'completed') {
|
|
256
|
+
console.log(chalk.yellow(`⚠️ Task ${taskId} already completed`));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Validate dependencies
|
|
261
|
+
const missingDeps = this.checkDependencies(task, feature.tasks);
|
|
262
|
+
if (missingDeps.length > 0) {
|
|
263
|
+
throw new Error(`Cannot start ${taskId}: missing dependencies: ${missingDeps.join(', ')}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
task.status = 'in_progress';
|
|
267
|
+
task.startedAt = new Date().toISOString();
|
|
268
|
+
|
|
269
|
+
await this.saveState(state);
|
|
270
|
+
|
|
271
|
+
console.log(chalk.blue(`▶️ Task ${taskId} started: ${task.title}`));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get next task suggestion
|
|
276
|
+
*/
|
|
277
|
+
async getNext(featureName) {
|
|
278
|
+
const state = await this.loadState();
|
|
279
|
+
const feature = state.features[featureName];
|
|
280
|
+
|
|
281
|
+
if (!feature) {
|
|
282
|
+
throw new Error(`Feature '${featureName}' not found`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const nextTask = this.getNextTask(feature.tasks);
|
|
286
|
+
|
|
287
|
+
if (nextTask) {
|
|
288
|
+
console.log(chalk.cyan(`\n⏭️ Next task: ${nextTask.id} - ${nextTask.title}`));
|
|
289
|
+
if (nextTask.dependencies && nextTask.dependencies.length > 0) {
|
|
290
|
+
console.log(chalk.gray(` Dependencies: ${nextTask.dependencies.join(', ')}`));
|
|
291
|
+
}
|
|
292
|
+
if (nextTask.files && nextTask.files.length > 0) {
|
|
293
|
+
console.log(chalk.gray(` Files:`));
|
|
294
|
+
nextTask.files.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
console.log(chalk.green('🎉 All tasks completed!'));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// CLI
|
|
303
|
+
async function main() {
|
|
304
|
+
const args = process.argv.slice(2);
|
|
305
|
+
const command = args[0];
|
|
306
|
+
|
|
307
|
+
const manager = new TaskManager();
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
switch (command) {
|
|
311
|
+
case 'done':
|
|
312
|
+
case 'complete': {
|
|
313
|
+
const featureName = args[1];
|
|
314
|
+
const taskIds = args.slice(2);
|
|
315
|
+
|
|
316
|
+
if (!featureName || taskIds.length === 0) {
|
|
317
|
+
console.error(chalk.red('Usage: npx morph-spec task done <feature> <task-id> [task-id...]'));
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await manager.completeTasks(featureName, taskIds);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
case 'start': {
|
|
326
|
+
const featureName = args[1];
|
|
327
|
+
const taskId = args[2];
|
|
328
|
+
|
|
329
|
+
if (!featureName || !taskId) {
|
|
330
|
+
console.error(chalk.red('Usage: npx morph-spec task start <feature> <task-id>'));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
await manager.startTask(featureName, taskId);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
case 'next': {
|
|
339
|
+
const featureName = args[1];
|
|
340
|
+
|
|
341
|
+
if (!featureName) {
|
|
342
|
+
console.error(chalk.red('Usage: npx morph-spec task next <feature>'));
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await manager.getNext(featureName);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
default:
|
|
351
|
+
console.error(chalk.red(`Unknown command: ${command}`));
|
|
352
|
+
console.log(chalk.gray('\nAvailable commands:'));
|
|
353
|
+
console.log(chalk.gray(' done <feature> <task-id...> - Mark tasks as completed'));
|
|
354
|
+
console.log(chalk.gray(' start <feature> <task-id> - Start a task (mark as in_progress)'));
|
|
355
|
+
console.log(chalk.gray(' next <feature> - Show next suggested task'));
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}`));
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (require.main === module) {
|
|
365
|
+
main();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = TaskManager;
|