@polymorphism-tech/morph-spec 4.8.7 → 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.
- package/README.md +2 -2
- package/bin/morph-spec.js +22 -1
- package/bin/task-manager.cjs +120 -16
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +1854 -1815
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +141 -23
- package/framework/hooks/claude-code/statusline.py +0 -12
- package/framework/hooks/claude-code/statusline.sh +6 -2
- package/framework/hooks/claude-code/stop/validate-completion.js +70 -23
- package/framework/hooks/dev/guard-version-numbers.js +1 -1
- package/framework/skills/level-0-meta/morph-init/SKILL.md +44 -6
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +67 -16
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +77 -7
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +114 -50
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +139 -1
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +29 -6
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +4 -3
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +944 -933
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +429 -0
- package/framework/templates/REGISTRY.json +1909 -1888
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +282 -0
- package/package.json +1 -1
- package/src/commands/agents/dispatch-agents.js +430 -0
- package/src/commands/agents/index.js +2 -1
- package/src/commands/project/doctor.js +137 -2
- package/src/commands/state/state.js +20 -4
- package/src/commands/templates/generate-contracts.js +445 -0
- package/src/commands/templates/index.js +1 -0
- 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
|
+
**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.
|
|
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();
|
package/bin/task-manager.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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') {
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED