@polymorphism-tech/morph-spec 2.1.2 → 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/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;
@@ -118,11 +118,15 @@ function validateAgentsSkills(config, verbose = false) {
118
118
  const skillPath = `.claude/skills/${skillFile.replace('.claude/skills/', '')}`;
119
119
 
120
120
  if (!agentSkillPaths.has(skillPath)) {
121
- warnings.push({
122
- type: 'orphan-skill',
123
- skillPath: skillPath,
124
- message: `Skill '${skillPath}' has no corresponding agent in agents.json`
125
- });
121
+ // Orphan skills são OK - podem ser usados manualmente
122
+ // Apenas reportar em verbose mode
123
+ if (verbose) {
124
+ info.push({
125
+ type: 'manual-skill',
126
+ skillPath: skillPath,
127
+ message: `ℹ️ Manual skill: ${skillPath} (no auto-activation)`
128
+ });
129
+ }
126
130
  }
127
131
  }
128
132
 
@@ -194,6 +198,14 @@ function displayResults(results, verbose) {
194
198
  log('║ ✅ All agents and skills are properly mapped! ║', 'green');
195
199
  }
196
200
 
201
+ // Nota sobre manual skills
202
+ const manualSkills = info.filter(i => i.type === 'manual-skill').length;
203
+ if (manualSkills > 0 && verbose) {
204
+ log('║ ║', 'cyan');
205
+ log(`║ ℹ️ ${manualSkills} manual-only skills found (use --verbose to see) ║`, 'gray');
206
+ log('║ These are extra skills without auto-activation. ║', 'gray');
207
+ }
208
+
197
209
  log('╚════════════════════════════════════════════════════════════════╝\n', 'cyan');
198
210
  }
199
211