@polymorphism-tech/morph-spec 4.8.11 → 4.8.14

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.
Files changed (29) hide show
  1. package/README.md +379 -379
  2. package/bin/{task-manager.cjs → task-manager.js} +47 -158
  3. package/claude-plugin.json +14 -14
  4. package/docs/CHEATSHEET.md +203 -203
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/agents.json +111 -24
  7. package/framework/hooks/README.md +202 -202
  8. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +6 -0
  9. package/framework/hooks/claude-code/session-start/inject-morph-context.js +7 -0
  10. package/framework/hooks/claude-code/statusline.py +6 -0
  11. package/framework/hooks/claude-code/stop/validate-completion.js +21 -2
  12. package/framework/hooks/dev/guard-version-numbers.js +1 -1
  13. package/framework/hooks/shared/phase-utils.js +3 -0
  14. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +55 -0
  15. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
  16. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
  17. package/framework/skills/level-1-workflows/phase-design/SKILL.md +57 -1
  18. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +23 -1
  19. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
  20. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +25 -2
  21. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
  22. package/package.json +87 -87
  23. package/src/commands/project/update.js +12 -2
  24. package/src/commands/state/advance-phase.js +32 -13
  25. package/src/commands/tasks/task.js +2 -2
  26. package/src/core/paths/output-schema.js +1 -0
  27. package/src/lib/detectors/design-system-detector.js +5 -4
  28. package/src/lib/tasks/task-parser.js +94 -0
  29. package/src/lib/validators/content/content-validator.js +34 -106
@@ -7,84 +7,21 @@
7
7
  * Part of MORPH-SPEC 3.0 - Event-driven state management.
8
8
  */
9
9
 
10
- const fs = require('fs').promises;
11
- const fsSync = require('fs');
12
- const path = require('path');
13
-
14
- // Simple ANSI color helpers (chalk v5 is ESM-only, can't require in CJS)
15
- const chalk = {
16
- red: (s) => `\x1b[31m${s}\x1b[0m`,
17
- green: (s) => `\x1b[32m${s}\x1b[0m`,
18
- yellow: (s) => `\x1b[33m${s}\x1b[0m`,
19
- blue: (s) => `\x1b[34m${s}\x1b[0m`,
20
- magenta: (s) => `\x1b[35m${s}\x1b[0m`,
21
- cyan: (s) => `\x1b[36m${s}\x1b[0m`,
22
- gray: (s) => `\x1b[90m${s}\x1b[0m`,
23
- bold: (s) => `\x1b[1m${s}\x1b[0m`,
24
- };
10
+ import { fileURLToPath } from 'url';
11
+ import { join, dirname } from 'path';
12
+ import { access } from 'fs/promises';
13
+ import { execSync } from 'child_process';
14
+ import chalk from 'chalk';
25
15
 
26
- // ============================================================================
27
- // v3 Schema Helpers
28
- // ============================================================================
29
-
30
- /**
31
- * Parse tasks.md to extract task stubs for v3 state format.
32
- * Looks for headings like: ### T001 — Task title
33
- */
34
- async function parseTasksMd(featureName) {
35
- const tasksPath = path.join(process.cwd(), `.morph/features/${featureName}/3-tasks/tasks.md`);
36
- let content = '';
37
- try {
38
- content = await fs.readFile(tasksPath, 'utf-8');
39
- } catch {
40
- return [];
41
- }
42
-
43
- const tasks = [];
44
- const headingRe = /^###\s+(T\d+)\s+[—–-]\s+(.+)$/gm;
45
- let match;
46
- while ((match = headingRe.exec(content)) !== null) {
47
- tasks.push({
48
- id: match[1],
49
- title: match[2].trim(),
50
- status: 'pending',
51
- dependencies: [],
52
- files: [],
53
- checkpoint: null
54
- });
55
- }
56
- return tasks;
57
- }
16
+ import { loadState, saveState } from '../src/core/state/state-manager.js';
17
+ import { parseTasksMd, ensureTaskList, syncCounters } from '../src/lib/tasks/task-parser.js';
58
18
 
59
- /**
60
- * Ensure feature.taskList exists (array of individual task objects).
61
- * In v3 state, feature.tasks is a counter object {total, completed, ...}.
62
- * Individual task objects live in feature.taskList.
63
- */
64
- async function ensureTaskList(feature, featureName) {
65
- if (Array.isArray(feature.tasks)) {
66
- // v2 format: tasks IS the array
67
- return feature.tasks;
68
- }
69
- // v3 format: use taskList or build from tasks.md
70
- if (!feature.taskList || feature.taskList.length === 0) {
71
- feature.taskList = await parseTasksMd(featureName);
72
- }
73
- return feature.taskList;
74
- }
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
75
21
 
76
- /**
77
- * After modifying taskList, sync counts back to feature.tasks counter.
78
- * tasks.total is derived from the actual taskList length (Problem 3 fix).
79
- */
80
- function syncCounters(feature) {
81
- if (Array.isArray(feature.tasks)) return; // v2, nothing to sync
82
- const list = feature.taskList || [];
83
- feature.tasks.total = list.length; // Auto-sync total from parsed taskList
84
- feature.tasks.completed = list.filter(t => t.status === 'completed').length;
85
- feature.tasks.inProgress = list.filter(t => t.status === 'in_progress').length;
86
- feature.tasks.pending = list.filter(t => t.status === 'pending').length;
87
- }
22
+ // ============================================================================
23
+ // Breaking-Change Detection
24
+ // ============================================================================
88
25
 
89
26
  /**
90
27
  * Detect potentially broken consumers of recently removed exports.
@@ -102,7 +39,6 @@ function syncCounters(feature) {
102
39
  */
103
40
  async function detectBreakingChanges() {
104
41
  try {
105
- const { execSync } = require('child_process');
106
42
  const execOpts = { cwd: process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 5 * 1024 * 1024 };
107
43
 
108
44
  // Collect diff from staged + unstaged changes in source files
@@ -133,7 +69,6 @@ async function detectBreakingChanges() {
133
69
  const warnings = [];
134
70
  for (const sym of removedExports) {
135
71
  try {
136
- // git grep returns exit code 1 when nothing found — that's handled by catch
137
72
  const result = execSync(`git grep -l "${sym}"`, execOpts).toString().trim();
138
73
  if (result) {
139
74
  const consumers = result.split('\n').filter(Boolean);
@@ -150,39 +85,11 @@ async function detectBreakingChanges() {
150
85
  }
151
86
  }
152
87
 
153
- class TaskManager {
154
- constructor(statePath = '.morph/state.json') {
155
- this.statePath = statePath;
156
- }
157
-
158
- /**
159
- * Load state.json
160
- */
161
- async loadState() {
162
- try {
163
- const content = await fs.readFile(this.statePath, 'utf-8');
164
- return JSON.parse(content);
165
- } catch (error) {
166
- if (error.code === 'ENOENT') {
167
- throw new Error(`State file not found: ${this.statePath}. Run 'npx morph-spec state init' first.`);
168
- }
169
- throw error;
170
- }
171
- }
172
-
173
- /**
174
- * Save state.json
175
- */
176
- async saveState(state) {
177
- // v3 state uses metadata.lastUpdated, v2 used project.updatedAt
178
- if (state.metadata) {
179
- state.metadata.lastUpdated = new Date().toISOString();
180
- } else if (state.project) {
181
- state.project.updatedAt = new Date().toISOString();
182
- }
183
- await fs.writeFile(this.statePath, JSON.stringify(state, null, 2), 'utf-8');
184
- }
88
+ // ============================================================================
89
+ // TaskManager
90
+ // ============================================================================
185
91
 
92
+ class TaskManager {
186
93
  /**
187
94
  * Complete one or more tasks (runs validation first)
188
95
  * @param {string} featureName - Feature name
@@ -191,7 +98,7 @@ class TaskManager {
191
98
  * @param {boolean} options.skipValidation - Skip code validation
192
99
  */
193
100
  async completeTasks(featureName, taskIds, options = {}) {
194
- const state = await this.loadState();
101
+ const state = loadState();
195
102
  const feature = state.features[featureName];
196
103
 
197
104
  if (!feature) {
@@ -201,12 +108,12 @@ class TaskManager {
201
108
  const taskList = await ensureTaskList(feature, featureName);
202
109
 
203
110
  if (taskList.length === 0) {
204
- const tasksPath = path.join(process.cwd(), `.morph/features/${featureName}/3-tasks/tasks.md`);
205
- const tasksExist = await fs.access(tasksPath).then(() => true).catch(() => false);
111
+ const tasksPath = join(process.cwd(), `.morph/features/${featureName}/3-tasks/tasks.md`);
112
+ const tasksExist = await access(tasksPath).then(() => true).catch(() => false);
206
113
  if (!tasksExist) {
207
114
  throw new Error(`No tasks found for '${featureName}' — tasks.md not generated yet.\n Complete the tasks phase first: run /phase-tasks`);
208
115
  }
209
- throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title`);
116
+ throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title or ### T001: Title`);
210
117
  }
211
118
 
212
119
  const results = [];
@@ -296,8 +203,8 @@ class TaskManager {
296
203
  }
297
204
  }
298
205
 
299
- // Save state
300
- await this.saveState(state);
206
+ // Persist state (atomic write via state-manager)
207
+ saveState(state);
301
208
 
302
209
  // Run TaskCompleted agent-teams hook for each completed task (non-blocking)
303
210
  for (const task of results) {
@@ -351,7 +258,7 @@ class TaskManager {
351
258
  } catch (error) {
352
259
  // If validation runner fails to load, warn but don't block task completion.
353
260
  // This is fail-open by design: a broken validator shouldn't block commits.
354
- // Common cause: ESM import failure on Windows or missing optional deps.
261
+ // Common cause: missing optional deps.
355
262
  console.log(chalk.yellow(`\n⚠️ Validation skipped (${error.message})`));
356
263
  console.log(chalk.gray(' Run manually: npx morph-spec validate --verbose'));
357
264
  return true;
@@ -382,13 +289,7 @@ class TaskManager {
382
289
  const pending = tasks.filter(t => t.status === 'pending').length;
383
290
  const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
384
291
 
385
- return {
386
- total,
387
- completed,
388
- inProgress,
389
- pending,
390
- percentage
391
- };
292
+ return { total, completed, inProgress, pending, percentage };
392
293
  }
393
294
 
394
295
  /**
@@ -420,7 +321,6 @@ class TaskManager {
420
321
  let checkpointResult = null;
421
322
  let validationNote = '';
422
323
 
423
- // Run checkpoint hooks (new enhanced validation system)
424
324
  try {
425
325
  const { runCheckpointHooks, shouldRunCheckpoint } = await import('../src/lib/checkpoints/checkpoint-hooks.js');
426
326
 
@@ -432,7 +332,6 @@ class TaskManager {
432
332
  } else {
433
333
  validationNote = ` | ✗ ${checkpointResult.summary.errors} errors, ${checkpointResult.summary.warnings} warnings`;
434
334
 
435
- // Check if we should block progress
436
335
  const config = await this.loadCheckpointConfig();
437
336
  if (config.checkpoints?.onFailure?.blockProgress && checkpointResult.summary.errors > 0) {
438
337
  console.log(chalk.red('\n❌ Checkpoint FAILED - Fix violations before proceeding'));
@@ -442,7 +341,7 @@ class TaskManager {
442
341
  }
443
342
  } catch (error) {
444
343
  if (error.message === 'Checkpoint validation failed') {
445
- throw error; // Re-throw to block task completion
344
+ throw error;
446
345
  }
447
346
  // Fallback to old validation if checkpoint-hooks not available
448
347
  console.log(chalk.yellow('⚠️ Checkpoint hooks not available, using legacy validation'));
@@ -457,7 +356,6 @@ class TaskManager {
457
356
  }
458
357
  }
459
358
 
460
- // Create checkpoint record
461
359
  const checkpoint = {
462
360
  id: `CHECKPOINT_AUTO_${Date.now()}`,
463
361
  timestamp: new Date().toISOString(),
@@ -483,18 +381,16 @@ class TaskManager {
483
381
  */
484
382
  async loadCheckpointConfig() {
485
383
  try {
486
- const configPath = path.join(process.cwd(), '.morph/config/llm-interaction.json');
487
- const content = await fs.readFile(configPath, 'utf-8');
384
+ const { readFile } = await import('fs/promises');
385
+ const configPath = join(process.cwd(), '.morph/config/llm-interaction.json');
386
+ const content = await readFile(configPath, 'utf-8');
488
387
  return JSON.parse(content);
489
388
  } catch {
490
- // Return defaults if config doesn't exist
491
389
  return {
492
390
  checkpoints: {
493
391
  frequency: 3,
494
392
  autoValidate: true,
495
- onFailure: {
496
- blockProgress: false
497
- }
393
+ onFailure: { blockProgress: false }
498
394
  }
499
395
  };
500
396
  }
@@ -507,7 +403,6 @@ class TaskManager {
507
403
  try {
508
404
  const config = await this.loadCheckpointConfig();
509
405
 
510
- // Check if auto-generation is enabled
511
406
  if (!config.metadata?.autoGenerate) {
512
407
  return;
513
408
  }
@@ -515,21 +410,14 @@ class TaskManager {
515
410
  const { extractFeatureMetadata } = await import('../src/lib/generators/metadata-extractor.js');
516
411
  const metadata = extractFeatureMetadata(feature);
517
412
 
518
- const outputPath = path.join(
519
- process.cwd(),
520
- `.morph/features/${featureName}/metadata.json`
521
- );
522
-
523
- // Ensure directory exists
524
- const outputDir = path.dirname(outputPath);
525
- await fs.mkdir(outputDir, { recursive: true });
413
+ const outputPath = join(process.cwd(), `.morph/features/${featureName}/metadata.json`);
526
414
 
527
- // Write metadata
528
- await fs.writeFile(outputPath, JSON.stringify(metadata, null, 2), 'utf-8');
415
+ const { mkdir, writeFile } = await import('fs/promises');
416
+ await mkdir(dirname(outputPath), { recursive: true });
417
+ await writeFile(outputPath, JSON.stringify(metadata, null, 2), 'utf-8');
529
418
 
530
- console.log(chalk.gray(` 📊 Metadata updated: ${path.relative(process.cwd(), outputPath)}`));
419
+ console.log(chalk.gray(` 📊 Metadata updated: .morph/features/${featureName}/metadata.json`));
531
420
  } catch (error) {
532
- // Don't block task completion if metadata generation fails
533
421
  console.log(chalk.yellow(` ⚠️ Metadata generation failed: ${error.message}`));
534
422
  }
535
423
  }
@@ -551,7 +439,6 @@ class TaskManager {
551
439
  getNextTask(tasks) {
552
440
  const pending = tasks.filter(t => t.status === 'pending');
553
441
 
554
- // Find first task with all dependencies completed
555
442
  for (const task of pending) {
556
443
  const missingDeps = this.checkDependencies(task, tasks);
557
444
  if (missingDeps.length === 0) {
@@ -571,7 +458,6 @@ class TaskManager {
571
458
  console.log(chalk.bold(`\n📊 Progress: ${percentage}% (${completed}/${total})`));
572
459
  console.log(chalk.gray(` Completed: ${completed} | In Progress: ${inProgress} | Pending: ${pending}`));
573
460
 
574
- // Progress bar
575
461
  const barLength = 30;
576
462
  const filledLength = Math.round((percentage / 100) * barLength);
577
463
  const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
@@ -582,7 +468,7 @@ class TaskManager {
582
468
  * Start a task (mark as in_progress)
583
469
  */
584
470
  async startTask(featureName, taskId) {
585
- const state = await this.loadState();
471
+ const state = loadState();
586
472
  const feature = state.features[featureName];
587
473
 
588
474
  if (!feature) {
@@ -592,12 +478,12 @@ class TaskManager {
592
478
  const taskList = await ensureTaskList(feature, featureName);
593
479
 
594
480
  if (taskList.length === 0) {
595
- const tasksPath = path.join(process.cwd(), `.morph/features/${featureName}/3-tasks/tasks.md`);
596
- const tasksExist = await fs.access(tasksPath).then(() => true).catch(() => false);
481
+ const tasksPath = join(process.cwd(), `.morph/features/${featureName}/3-tasks/tasks.md`);
482
+ const tasksExist = await access(tasksPath).then(() => true).catch(() => false);
597
483
  if (!tasksExist) {
598
484
  throw new Error(`No tasks found for '${featureName}' — tasks.md not generated yet.\n Complete the tasks phase first: run /phase-tasks`);
599
485
  }
600
- throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title`);
486
+ throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title or ### T001: Title`);
601
487
  }
602
488
 
603
489
  const task = taskList.find(t => t.id === taskId);
@@ -611,7 +497,6 @@ class TaskManager {
611
497
  return;
612
498
  }
613
499
 
614
- // Validate dependencies
615
500
  const missingDeps = this.checkDependencies(task, taskList);
616
501
  if (missingDeps.length > 0) {
617
502
  throw new Error(`Cannot start ${taskId}: missing dependencies: ${missingDeps.join(', ')}`);
@@ -621,7 +506,7 @@ class TaskManager {
621
506
  task.startedAt = new Date().toISOString();
622
507
  syncCounters(feature);
623
508
 
624
- await this.saveState(state);
509
+ saveState(state);
625
510
 
626
511
  console.log(chalk.blue(`▶️ Task ${taskId} started: ${task.title}`));
627
512
  }
@@ -630,7 +515,7 @@ class TaskManager {
630
515
  * Get next task suggestion
631
516
  */
632
517
  async getNext(featureName) {
633
- const state = await this.loadState();
518
+ const state = loadState();
634
519
  const feature = state.features[featureName];
635
520
 
636
521
  if (!feature) {
@@ -655,7 +540,10 @@ class TaskManager {
655
540
  }
656
541
  }
657
542
 
658
- // CLI
543
+ // ============================================================================
544
+ // CLI entry point
545
+ // ============================================================================
546
+
659
547
  async function main() {
660
548
  const args = process.argv.slice(2);
661
549
  const command = args[0];
@@ -719,8 +607,9 @@ async function main() {
719
607
  }
720
608
  }
721
609
 
722
- if (require.main === module) {
610
+ // ESM equivalent of `if (require.main === module)`
611
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
723
612
  main();
724
613
  }
725
614
 
726
- module.exports = TaskManager;
615
+ export default TaskManager;
@@ -1,14 +1,14 @@
1
- {
2
- "name": "morph-spec",
3
- "version": "4.8.11",
4
- "displayName": "MORPH-SPEC Framework",
5
- "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
- "publisher": "polymorphism-tech",
7
- "skills": {
8
- "directory": "framework/skills",
9
- "namespace": "morph-spec"
10
- },
11
- "hooks": {
12
- "directory": "framework/hooks/claude-code"
13
- }
14
- }
1
+ {
2
+ "name": "morph-spec",
3
+ "version": "4.8.14",
4
+ "displayName": "MORPH-SPEC Framework",
5
+ "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
+ "publisher": "polymorphism-tech",
7
+ "skills": {
8
+ "directory": "framework/skills",
9
+ "namespace": "morph-spec"
10
+ },
11
+ "hooks": {
12
+ "directory": "framework/hooks/claude-code"
13
+ }
14
+ }