@polymorphism-tech/morph-spec 4.8.6 → 4.8.8

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 (33) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +22 -1
  3. package/bin/task-manager.cjs +120 -16
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/agents.json +1854 -1815
  8. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +141 -23
  9. package/framework/hooks/claude-code/statusline.py +304 -280
  10. package/framework/hooks/claude-code/statusline.sh +6 -2
  11. package/framework/hooks/claude-code/stop/validate-completion.js +70 -23
  12. package/framework/hooks/dev/guard-version-numbers.js +1 -1
  13. package/framework/skills/level-0-meta/morph-init/SKILL.md +44 -6
  14. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +67 -16
  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 +77 -7
  17. package/framework/skills/level-1-workflows/phase-design/SKILL.md +114 -50
  18. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +139 -1
  19. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +29 -6
  20. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +4 -3
  21. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
  22. package/framework/standards/STANDARDS.json +944 -933
  23. package/framework/standards/architecture/vertical-slice/vertical-slice.md +429 -0
  24. package/framework/templates/REGISTRY.json +1909 -1888
  25. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +282 -0
  26. package/package.json +1 -1
  27. package/src/commands/agents/dispatch-agents.js +430 -0
  28. package/src/commands/agents/index.js +2 -1
  29. package/src/commands/project/doctor.js +137 -2
  30. package/src/commands/state/state.js +20 -4
  31. package/src/commands/templates/generate-contracts.js +445 -0
  32. package/src/commands/templates/index.js +1 -0
  33. package/src/lib/validators/validation-runner.js +19 -7
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
4
4
 
5
5
  **Package:** `@polymorphism-tech/morph-spec`
6
- **Version:** 4.8.6
6
+ **Version:** 4.8.8
7
7
  **Requires:** Node.js 18+, Claude Code
8
8
 
9
9
  ---
@@ -376,4 +376,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
376
376
 
377
377
  ---
378
378
 
379
- *morph-spec v4.8.6 by [Polymorphism Tech](https://polymorphism.tech)*
379
+ *morph-spec v4.8.8 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { program } from 'commander';
4
4
  import chalk from 'chalk';
@@ -23,6 +23,9 @@ import { validatePhaseCommand } from '../src/commands/state/validate-phase.js';
23
23
  import { advancePhaseCommand } from '../src/commands/state/advance-phase.js';
24
24
  import { approveCommand, approvalStatusCommand, unapproveCommand } from '../src/commands/state/approve.js';
25
25
 
26
+ // Agent commands
27
+ import { dispatchAgentsCommand } from '../src/commands/agents/dispatch-agents.js';
28
+
26
29
  // Task commands
27
30
  import { taskDoneCommand, taskStartCommand, taskNextCommand } from '../src/commands/tasks/task.js';
28
31
 
@@ -32,6 +35,7 @@ import { validateFeatureCommand } from '../src/commands/validation/validate-feat
32
35
 
33
36
  // Template commands
34
37
  import { templateRenderCommand } from '../src/commands/templates/template-render.js';
38
+ import { generateContractsCommand } from '../src/commands/templates/generate-contracts.js';
35
39
 
36
40
  // MCP commands
37
41
  import { mcpSetupCommand } from '../src/commands/mcp/mcp-setup.js';
@@ -90,6 +94,7 @@ program
90
94
  .command('doctor')
91
95
  .description('Check MORPH installation health')
92
96
  .option('--full', 'Run full health check (lib files, commands, HOPs, standards, agents, state)')
97
+ .option('--mcp', 'Check MCP server configuration (binary + env vars)')
93
98
  .option('--reset', 'Remove morph-managed entries from .claude/settings.local.json')
94
99
  .action(doctorCommand);
95
100
 
@@ -150,6 +155,14 @@ generateCommand
150
155
  await generateRecap('.', feature, options);
151
156
  });
152
157
 
158
+ generateCommand
159
+ .command('contracts <feature>')
160
+ .description('Generate contracts.cs from schema-analysis.md using real field names and types')
161
+ .option('--dry-run', 'Preview output without writing file')
162
+ .option('--output <path>', 'Override output path for contracts.cs')
163
+ .option('-v, --verbose', 'Show stack trace on error')
164
+ .action((feature, options) => generateContractsCommand(feature, options));
165
+
153
166
  // Validation commands (Sprint 4: Continuous Validation)
154
167
  program
155
168
  .command('validate [validator]')
@@ -276,4 +289,12 @@ mcpCommand
276
289
  .option('--auto', 'Auto-install credential-free MCPs without prompts')
277
290
  .action((name, options) => mcpSetupCommand(name, options));
278
291
 
292
+ // Agent orchestration commands
293
+ program
294
+ .command('dispatch-agents <feature> <phase>')
295
+ .description('Build dispatch config for parallel agent orchestration (design | tasks | implement)')
296
+ .option('--table', 'Human-readable table output instead of JSON')
297
+ .option('-v, --verbose', 'Show stack trace on error')
298
+ .action((feature, phase, options) => dispatchAgentsCommand(feature, phase, options));
299
+
279
300
  program.parse();
@@ -32,7 +32,7 @@ const chalk = {
32
32
  * Looks for headings like: ### T001 — Task title
33
33
  */
34
34
  async function parseTasksMd(featureName) {
35
- const tasksPath = path.join(process.cwd(), `.morph/features/${featureName}/tasks.md`);
35
+ const tasksPath = path.join(process.cwd(), `.morph/features/${featureName}/3-tasks/tasks.md`);
36
36
  let content = '';
37
37
  try {
38
38
  content = await fs.readFile(tasksPath, 'utf-8');
@@ -75,14 +75,79 @@ async function ensureTaskList(feature, featureName) {
75
75
 
76
76
  /**
77
77
  * After modifying taskList, sync counts back to feature.tasks counter.
78
+ * tasks.total is derived from the actual taskList length (Problem 3 fix).
78
79
  */
79
80
  function syncCounters(feature) {
80
81
  if (Array.isArray(feature.tasks)) return; // v2, nothing to sync
81
82
  const list = feature.taskList || [];
83
+ feature.tasks.total = list.length; // Auto-sync total from parsed taskList
82
84
  feature.tasks.completed = list.filter(t => t.status === 'completed').length;
83
85
  feature.tasks.inProgress = list.filter(t => t.status === 'in_progress').length;
84
86
  feature.tasks.pending = list.filter(t => t.status === 'pending').length;
85
- // Don't touch feature.tasks.total — it is the authoritative count
87
+ }
88
+
89
+ /**
90
+ * Detect potentially broken consumers of recently removed exports.
91
+ *
92
+ * Strategy:
93
+ * 1. Parse `git diff HEAD` (staged + unstaged) for removed export declarations.
94
+ * 2. Use `git grep` to find files that still reference each removed symbol.
95
+ * 3. Return warnings (non-blocking) so Claude Code can review before marking done.
96
+ *
97
+ * Returns null when git is unavailable, not in a repo, or no removed exports found.
98
+ * Returns [] when removed exports exist but no consumers found.
99
+ * Returns [{export, consumers[]}] when consumers are found.
100
+ *
101
+ * @returns {Promise<Array<{export: string, consumers: string[]}>|null>}
102
+ */
103
+ async function detectBreakingChanges() {
104
+ try {
105
+ const { execSync } = require('child_process');
106
+ const execOpts = { cwd: process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 5 * 1024 * 1024 };
107
+
108
+ // Collect diff from staged + unstaged changes in source files
109
+ let diff = '';
110
+ const diffPatterns = ['*.ts', '*.tsx', '*.js', '*.jsx', '*.cs'];
111
+ for (const flag of ['--cached', '']) {
112
+ try {
113
+ const args = ['git', 'diff', '--unified=0', flag, '--', ...diffPatterns].filter(Boolean);
114
+ diff += execSync(args.join(' '), execOpts).toString();
115
+ } catch {
116
+ // git not available or no changes — continue
117
+ }
118
+ }
119
+
120
+ if (!diff.trim()) return null;
121
+
122
+ // Extract removed export symbol names (lines starting with -)
123
+ const removedExports = new Set();
124
+ const exportRe = /^-\s*export\s+(?:(?:default\s+)?(?:async\s+)?function|const|let|class|interface|type|enum)\s+(\w+)/gm;
125
+ let match;
126
+ while ((match = exportRe.exec(diff)) !== null) {
127
+ removedExports.add(match[1]);
128
+ }
129
+
130
+ if (removedExports.size === 0) return null;
131
+
132
+ // For each removed symbol, check if any tracked file still imports/uses it
133
+ const warnings = [];
134
+ for (const sym of removedExports) {
135
+ try {
136
+ // git grep returns exit code 1 when nothing found — that's handled by catch
137
+ const result = execSync(`git grep -l "${sym}"`, execOpts).toString().trim();
138
+ if (result) {
139
+ const consumers = result.split('\n').filter(Boolean);
140
+ warnings.push({ export: sym, consumers });
141
+ }
142
+ } catch {
143
+ // No match found — safe
144
+ }
145
+ }
146
+
147
+ return warnings;
148
+ } catch {
149
+ return null; // Non-blocking: any unexpected error → skip detection
150
+ }
86
151
  }
87
152
 
88
153
  class TaskManager {
@@ -134,6 +199,16 @@ class TaskManager {
134
199
  }
135
200
 
136
201
  const taskList = await ensureTaskList(feature, featureName);
202
+
203
+ 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);
206
+ if (!tasksExist) {
207
+ throw new Error(`No tasks found for '${featureName}' — tasks.md not generated yet.\n Complete the tasks phase first: run /phase-tasks`);
208
+ }
209
+ throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title`);
210
+ }
211
+
137
212
  const results = [];
138
213
  const tasksToComplete = [];
139
214
 
@@ -141,7 +216,7 @@ class TaskManager {
141
216
  const task = taskList.find(t => t.id === taskId);
142
217
 
143
218
  if (!task) {
144
- console.error(chalk.red(`❌ Task ${taskId} not found`));
219
+ console.error(chalk.red(`❌ Task ${taskId} not found (available: ${taskList.map(t => t.id).join(', ')})`));
145
220
  continue;
146
221
  }
147
222
 
@@ -171,6 +246,23 @@ class TaskManager {
171
246
  }
172
247
  }
173
248
 
249
+ // Breaking change detection (non-blocking warning)
250
+ if (tasksToComplete.length > 0) {
251
+ const breakingChanges = await detectBreakingChanges();
252
+ if (breakingChanges && breakingChanges.length > 0) {
253
+ console.log(chalk.yellow('\n⚠️ BREAKING CHANGE DETECTION:'));
254
+ console.log(chalk.yellow(' Removed exports with active consumers found — review before completing:\n'));
255
+ for (const { export: sym, consumers } of breakingChanges) {
256
+ console.log(chalk.yellow(` • "${sym}" removed — used by:`));
257
+ consumers.slice(0, 5).forEach(f => console.log(chalk.gray(` - ${f}`)));
258
+ if (consumers.length > 5) {
259
+ console.log(chalk.gray(` … and ${consumers.length - 5} more files`));
260
+ }
261
+ }
262
+ console.log(chalk.yellow('\n Fix broken imports before the smoke test, or use --skip-validation to bypass.\n'));
263
+ }
264
+ }
265
+
174
266
  // Mark tasks as completed (only after validation passes)
175
267
  for (const task of tasksToComplete) {
176
268
  task.status = 'completed';
@@ -191,17 +283,16 @@ class TaskManager {
191
283
  syncCounters(feature);
192
284
  feature.progress = this.calculateProgress(taskList);
193
285
 
194
- // Auto-checkpoint every 3 tasks
195
- const recentCompleted = this.getRecentCompleted(taskList, 3);
196
- if (recentCompleted.length === 3) {
197
- const lastCheckpoint = feature.checkpoints[feature.checkpoints.length - 1];
198
- const shouldAutoCheckpoint = !lastCheckpoint ||
199
- !recentCompleted.every(t =>
200
- lastCheckpoint.tasksCompleted.includes(t.id)
201
- );
202
-
203
- if (shouldAutoCheckpoint) {
286
+ // Auto-checkpoint every 3 tasks (at exact multiples of 3, not after every task).
287
+ // Uses feature._lastAutoCheckpointNum to track which threshold already fired.
288
+ const completedCount = taskList.filter(t => t.status === 'completed').length;
289
+ if (completedCount > 0 && completedCount % 3 === 0) {
290
+ const checkpointNum = Math.floor(completedCount / 3);
291
+ const lastAutoNum = feature._lastAutoCheckpointNum || 0;
292
+ if (checkpointNum > lastAutoNum) {
293
+ const recentCompleted = this.getRecentCompleted(taskList, 3);
204
294
  await this.autoCheckpoint(feature, recentCompleted, featureName);
295
+ feature._lastAutoCheckpointNum = checkpointNum;
205
296
  }
206
297
  }
207
298
 
@@ -258,8 +349,11 @@ class TaskManager {
258
349
  formatValidationResults(result);
259
350
  return result.passed;
260
351
  } catch (error) {
261
- // If validation runner fails to load, warn but don't block
262
- console.log(chalk.yellow(`\n⚠️ Validation skipped: ${error.message}`));
352
+ // If validation runner fails to load, warn but don't block task completion.
353
+ // 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.
355
+ console.log(chalk.yellow(`\n⚠️ Validation skipped (${error.message})`));
356
+ console.log(chalk.gray(' Run manually: npx morph-spec validate --verbose'));
263
357
  return true;
264
358
  }
265
359
  }
@@ -496,10 +590,20 @@ class TaskManager {
496
590
  }
497
591
 
498
592
  const taskList = await ensureTaskList(feature, featureName);
593
+
594
+ 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);
597
+ if (!tasksExist) {
598
+ throw new Error(`No tasks found for '${featureName}' — tasks.md not generated yet.\n Complete the tasks phase first: run /phase-tasks`);
599
+ }
600
+ throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title`);
601
+ }
602
+
499
603
  const task = taskList.find(t => t.id === taskId);
500
604
 
501
605
  if (!task) {
502
- throw new Error(`Task ${taskId} not found`);
606
+ throw new Error(`Task ${taskId} not found in '${featureName}' (${taskList.length} tasks available: ${taskList.map(t => t.id).join(', ')})`);
503
607
  }
504
608
 
505
609
  if (task.status === 'completed') {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "4.8.6",
3
+ "version": "4.8.8",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -200,4 +200,4 @@ These files are never edited directly. Use CLI commands or `morph-spec update` i
200
200
 
201
201
  ---
202
202
 
203
- *morph-spec v4.8.6 by Polymorphism Tech*
203
+ *morph-spec v4.8.8 by Polymorphism Tech*
@@ -203,4 +203,4 @@ morph-spec doctor
203
203
 
204
204
  ---
205
205
 
206
- *morph-spec v4.8.6 by Polymorphism Tech*
206
+ *morph-spec v4.8.8 by Polymorphism Tech*