@polymorphism-tech/morph-spec 4.8.12 → 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.
- package/README.md +379 -379
- package/bin/{task-manager.cjs → task-manager.js} +47 -158
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +203 -203
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +111 -24
- package/framework/hooks/README.md +202 -202
- package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +6 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +7 -0
- package/framework/hooks/claude-code/statusline.py +6 -0
- package/framework/hooks/claude-code/stop/validate-completion.js +21 -2
- package/framework/hooks/shared/phase-utils.js +3 -0
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +55 -0
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +57 -1
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +23 -1
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +25 -2
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
- package/package.json +87 -87
- package/src/commands/state/advance-phase.js +32 -13
- package/src/commands/tasks/task.js +2 -2
- package/src/core/paths/output-schema.js +1 -0
- package/src/lib/detectors/design-system-detector.js +5 -4
- package/src/lib/tasks/task-parser.js +94 -0
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 =
|
|
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 =
|
|
205
|
-
const tasksExist = await
|
|
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
|
-
//
|
|
300
|
-
|
|
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:
|
|
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;
|
|
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
|
|
487
|
-
const
|
|
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 =
|
|
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
|
-
|
|
528
|
-
await
|
|
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:
|
|
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 =
|
|
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 =
|
|
596
|
-
const tasksExist = await
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
615
|
+
export default TaskManager;
|
package/claude-plugin.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "morph-spec",
|
|
3
|
-
"version": "4.8.
|
|
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
|
+
}
|