@polymorphism-tech/morph-spec 4.10.0 → 4.10.2
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/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +5 -69
- package/framework/agents/backend/api-designer.md +3 -0
- package/framework/agents/backend/dotnet-senior.md +3 -0
- package/framework/agents/backend/ef-modeler.md +2 -0
- package/framework/agents/backend/hangfire-orchestrator.md +2 -0
- package/framework/agents/backend/ms-agent-expert.md +2 -0
- package/framework/agents/frontend/blazor-builder.md +2 -0
- package/framework/agents/frontend/nextjs-expert.md +2 -0
- package/framework/agents/infrastructure/azure-architect.md +2 -0
- package/framework/agents/infrastructure/azure-deploy-specialist.md +2 -0
- package/framework/agents/infrastructure/bicep-architect.md +2 -0
- package/framework/agents/infrastructure/container-specialist.md +2 -0
- package/framework/agents/infrastructure/devops-engineer.md +3 -0
- package/framework/agents/infrastructure/infra-architect.md +3 -0
- package/framework/agents/integrations/asaas-financial.md +2 -0
- package/framework/agents/integrations/azure-identity.md +2 -0
- package/framework/agents/integrations/clerk-auth.md +3 -0
- package/framework/agents/integrations/hangfire-integration.md +2 -0
- package/framework/agents/integrations/resend-email.md +2 -0
- package/framework/commands/morph-apply.md +151 -161
- package/framework/commands/morph-archive.md +28 -28
- package/framework/commands/morph-infra.md +79 -79
- package/framework/commands/morph-preflight.md +92 -56
- package/framework/commands/morph-proposal.md +94 -70
- package/framework/commands/morph-status.md +31 -31
- package/framework/commands/morph-troubleshoot.md +63 -60
- package/framework/rules/csharp-standards.md +3 -0
- package/framework/rules/frontend-standards.md +2 -0
- package/framework/rules/infrastructure-standards.md +3 -0
- package/framework/rules/morph-workflow.md +57 -2
- package/framework/rules/nextjs-standards.md +2 -0
- package/framework/rules/testing-standards.md +3 -0
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +54 -49
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +42 -19
- package/framework/skills/level-0-meta/morph-code-review/SKILL.md +8 -5
- package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +7 -5
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +139 -136
- package/framework/skills/level-0-meta/morph-init/SKILL.md +42 -13
- package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +130 -130
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +95 -87
- package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +24 -0
- package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +42 -41
- package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +22 -11
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +123 -114
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +120 -102
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +206 -214
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +38 -0
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +241 -360
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +107 -115
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +135 -135
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +14 -0
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +143 -139
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +168 -165
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +57 -8
- package/package.json +3 -3
- package/src/commands/project/doctor.js +7 -2
- package/src/commands/project/update.js +4 -4
- package/src/lib/stack-filter.js +58 -0
- package/src/scripts/setup-infra.js +53 -18
- package/src/utils/agents-installer.js +19 -5
- package/src/utils/claude-md-injector.js +90 -0
- package/src/utils/hooks-installer.js +1 -4
- package/src/utils/skills-installer.js +67 -7
- package/CLAUDE.md +0 -98
- package/framework/memory/patterns-learned.md +0 -766
- package/framework/skills/level-0-meta/morph-terminal-title/SKILL.md +0 -61
- package/framework/skills/level-0-meta/morph-terminal-title/scripts/set_title.sh +0 -65
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { installClaudeHooks, installGlobalStatusline, installVSCodeTerminalSettings, installShellIntegration } from '../../utils/claude-settings-manager.js';
|
|
29
29
|
import { installSkills } from '../../utils/skills-installer.js';
|
|
30
30
|
import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
|
|
31
|
+
import { injectMorphImport } from '../../utils/claude-md-injector.js';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Backup user's config.json before cleaning
|
|
@@ -321,11 +322,10 @@ export async function updateCommand(options) {
|
|
|
321
322
|
logger.dim(' ⚠ Could not configure VS Code terminal settings (non-critical)');
|
|
322
323
|
}
|
|
323
324
|
|
|
324
|
-
//
|
|
325
|
-
updateSpinner.text = '
|
|
326
|
-
const claudeMdSrc = join(frameworkDir, 'CLAUDE.md');
|
|
325
|
+
// Ensure root CLAUDE.md has morph-spec @import (preserves user content)
|
|
326
|
+
updateSpinner.text = 'Checking CLAUDE.md import...';
|
|
327
327
|
const claudeMdDest = join(targetPath, 'CLAUDE.md');
|
|
328
|
-
await
|
|
328
|
+
await injectMorphImport(claudeMdDest);
|
|
329
329
|
|
|
330
330
|
// Restore user config after framework reinstallation
|
|
331
331
|
updateSpinner.text = 'Restoring user configuration...';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `stacks:` from YAML frontmatter content.
|
|
3
|
+
* Returns ['*'] if no stacks field found (backwards compatible — always install).
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* stacks:\n - dotnet\n - blazor (YAML list)
|
|
7
|
+
* stacks: [dotnet, blazor] (inline array)
|
|
8
|
+
*
|
|
9
|
+
* @param {string} content - Raw file content with YAML frontmatter
|
|
10
|
+
* @returns {string[]}
|
|
11
|
+
*/
|
|
12
|
+
export function parseStacks(content) {
|
|
13
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
14
|
+
const fmMatch = normalized.match(/^---\n([\s\S]*?)\n---/);
|
|
15
|
+
if (!fmMatch) return ['*'];
|
|
16
|
+
|
|
17
|
+
const fm = fmMatch[1];
|
|
18
|
+
|
|
19
|
+
// Try inline format: stacks: [dotnet, blazor]
|
|
20
|
+
const inlineMatch = fm.match(/^stacks:\s*\[([^\]]*)\]/m);
|
|
21
|
+
if (inlineMatch) {
|
|
22
|
+
const items = inlineMatch[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
23
|
+
return items.length > 0 ? items : ['*'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Try YAML list format: stacks:\n - dotnet\n - blazor
|
|
27
|
+
const listMatch = fm.match(/^stacks:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
28
|
+
if (listMatch) {
|
|
29
|
+
const items = listMatch[1]
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map(line => line.replace(/^\s+-\s+/, '').replace(/["']/g, '').trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
return items.length > 0 ? items : ['*'];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return ['*'];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract project stack tags from config object.
|
|
41
|
+
* Splits `project.stack` by '-' or '+' to produce tag array.
|
|
42
|
+
*/
|
|
43
|
+
export function getProjectTags(config) {
|
|
44
|
+
const stack = config?.project?.stack;
|
|
45
|
+
if (!stack || typeof stack !== 'string') return [];
|
|
46
|
+
return stack.split(/[-+]/).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Determine if an asset should be installed for the given project tags.
|
|
51
|
+
* - stacks includes '*' → always install
|
|
52
|
+
* - stacks is empty → treat as '*' (backwards compatible)
|
|
53
|
+
* - Otherwise → install if intersection with projectTags is non-empty
|
|
54
|
+
*/
|
|
55
|
+
export function shouldInstall(assetStacks, projectTags) {
|
|
56
|
+
if (assetStacks.length === 0 || assetStacks.includes('*')) return true;
|
|
57
|
+
return assetStacks.some(s => projectTags.includes(s));
|
|
58
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { join, dirname } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { execSync } from 'child_process';
|
|
17
|
+
import { readFileSync, readdirSync, copyFileSync, unlinkSync } from 'fs';
|
|
17
18
|
import {
|
|
18
19
|
copyDirectory,
|
|
19
20
|
copyFile,
|
|
@@ -23,6 +24,8 @@ import {
|
|
|
23
24
|
writeFile,
|
|
24
25
|
updateGitignore
|
|
25
26
|
} from '../utils/file-copier.js';
|
|
27
|
+
import { parseStacks, getProjectTags, shouldInstall } from '../lib/stack-filter.js';
|
|
28
|
+
import { injectMorphImport } from '../utils/claude-md-injector.js';
|
|
26
29
|
import { saveProjectMorphVersion, getInstalledCLIVersion } from '../utils/version-checker.js';
|
|
27
30
|
import { installClaudeHooks, installGlobalStatusline, installVSCodeTerminalSettings, installShellIntegration } from '../utils/claude-settings-manager.js';
|
|
28
31
|
import { installSkills } from '../utils/skills-installer.js';
|
|
@@ -47,6 +50,35 @@ async function installRequiredPlugins(log, exec) {
|
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Install only stack-matching rules from rulesSrc to rulesDest.
|
|
55
|
+
* Also removes orphan rules in rulesDest that are no longer in the install set.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} rulesSrc - Source rules directory (framework/rules/)
|
|
58
|
+
* @param {string} rulesDest - Destination rules directory (.claude/rules/)
|
|
59
|
+
* @param {string[]} projectTags - Stack tags from config (e.g., ['dotnet', 'blazor'])
|
|
60
|
+
*/
|
|
61
|
+
export function installRulesForStack(rulesSrc, rulesDest, projectTags) {
|
|
62
|
+
const sourceFiles = readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
|
|
63
|
+
const installed = new Set();
|
|
64
|
+
|
|
65
|
+
for (const file of sourceFiles) {
|
|
66
|
+
const content = readFileSync(join(rulesSrc, file), 'utf-8');
|
|
67
|
+
const stacks = parseStacks(content);
|
|
68
|
+
if (shouldInstall(stacks, projectTags)) {
|
|
69
|
+
copyFileSync(join(rulesSrc, file), join(rulesDest, file));
|
|
70
|
+
installed.add(file);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Cleanup orphans
|
|
75
|
+
for (const file of readdirSync(rulesDest).filter(f => f.endsWith('.md'))) {
|
|
76
|
+
if (!installed.has(file)) {
|
|
77
|
+
unlinkSync(join(rulesDest, file));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
/**
|
|
51
83
|
* Installs MORPH-SPEC infrastructure into the target project directory.
|
|
52
84
|
* Headless — no prompts, no spinner (suppressed when MORPH_QUIET=1), no stack detection.
|
|
@@ -63,19 +95,12 @@ export async function setupInfra(targetPath, { _exec = execSync } = {}) {
|
|
|
63
95
|
// --- 0. Install required Claude Code plugins ---
|
|
64
96
|
await installRequiredPlugins(log, _exec);
|
|
65
97
|
|
|
66
|
-
// --- 1.
|
|
67
|
-
log('Step 1:
|
|
98
|
+
// --- 1. Inject @import into root CLAUDE.md (preserves user content) ---
|
|
99
|
+
log('Step 1: Setting up CLAUDE.md...');
|
|
68
100
|
const claudeMdSrc = join(FRAMEWORK_DIR, 'CLAUDE.md');
|
|
69
101
|
const claudeMdDest = join(targetPath, 'CLAUDE.md');
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const { readFile } = await import('../utils/file-copier.js');
|
|
73
|
-
const existingContent = await readFile(claudeMdDest);
|
|
74
|
-
if (!existingContent.includes('MORPH-SPEC')) {
|
|
75
|
-
await copyFile(claudeMdDest, `${claudeMdDest}.backup`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
await copyFile(claudeMdSrc, claudeMdDest);
|
|
102
|
+
const importResult = await injectMorphImport(claudeMdDest);
|
|
103
|
+
log(` ✓ Root CLAUDE.md: ${importResult}`);
|
|
79
104
|
|
|
80
105
|
// --- 2. Create .morph directory structure ---
|
|
81
106
|
log('Step 2: Creating .morph structure...');
|
|
@@ -163,27 +188,37 @@ export async function setupInfra(targetPath, { _exec = execSync } = {}) {
|
|
|
163
188
|
await copyDirectory(commandsSrc, commandsDest);
|
|
164
189
|
}
|
|
165
190
|
|
|
166
|
-
// ---
|
|
191
|
+
// --- Compute project tags for stack-aware installation ---
|
|
192
|
+
let projectTags = [];
|
|
193
|
+
if (await pathExists(configPath)) {
|
|
194
|
+
try {
|
|
195
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
196
|
+
projectTags = getProjectTags(config);
|
|
197
|
+
} catch { /* ignore malformed config */ }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- 10. Copy framework/rules → .claude/rules (filtered by stack) ---
|
|
167
201
|
log('Step 10: Installing path-scoped rules...');
|
|
168
202
|
const rulesSrc = join(FRAMEWORK_DIR, 'rules');
|
|
169
203
|
const rulesDest = join(claudeDest, 'rules');
|
|
170
204
|
if (await pathExists(rulesSrc)) {
|
|
171
|
-
await
|
|
205
|
+
await ensureDir(rulesDest);
|
|
206
|
+
installRulesForStack(rulesSrc, rulesDest, projectTags);
|
|
172
207
|
}
|
|
173
208
|
|
|
174
|
-
// --- 11. installSkills ---
|
|
209
|
+
// --- 11. installSkills (filtered by stack) ---
|
|
175
210
|
log('Step 11: Installing skills...');
|
|
176
|
-
await installSkills(targetPath);
|
|
211
|
+
await installSkills(targetPath, { projectTags });
|
|
177
212
|
|
|
178
213
|
// --- 12. installAgents ---
|
|
179
214
|
log('Step 12: Installing agents...');
|
|
180
215
|
const agentCounts = await installAgents(targetPath, FRAMEWORK_DIR, { projectStack: null });
|
|
181
216
|
|
|
182
|
-
// --- 13. installDomainAgents ---
|
|
217
|
+
// --- 13. installDomainAgents (filtered by stack) ---
|
|
183
218
|
log('Step 13: Installing domain agents...');
|
|
184
|
-
const domainCounts = await installDomainAgents(targetPath, FRAMEWORK_DIR);
|
|
219
|
+
const domainCounts = await installDomainAgents(targetPath, FRAMEWORK_DIR, { projectTags });
|
|
185
220
|
|
|
186
|
-
// --- 14. Copy framework/CLAUDE.md → .claude/CLAUDE.md ---
|
|
221
|
+
// --- 14. Copy framework/CLAUDE.md → .claude/CLAUDE.md (morph-owned, gitignored) ---
|
|
187
222
|
log('Step 14: Installing .claude/CLAUDE.md...');
|
|
188
223
|
const runtimeClaudeMdDest = join(claudeDest, 'CLAUDE.md');
|
|
189
224
|
if (await pathExists(claudeMdSrc)) {
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* maxTurns, skills, memory) followed by the agent's spawn prompt as the body.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
+
import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Installs tier-1 and tier-2 morph agents as native Claude Code subagents
|
|
@@ -186,7 +187,8 @@ export function parseSkillFile(rawContent) {
|
|
|
186
187
|
if (inlineMatch) description = inlineMatch[1].trim();
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
|
|
190
|
+
const stacks = parseStacks(rawContent);
|
|
191
|
+
return { name, description, allowedTools, stacks, body };
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
/**
|
|
@@ -215,7 +217,7 @@ export function collectSkillFiles(dir) {
|
|
|
215
217
|
* @param {string} projectDir - Target project directory
|
|
216
218
|
* @param {string} frameworkDir - Path to morph-spec framework/ directory
|
|
217
219
|
*/
|
|
218
|
-
export async function installDomainAgents(projectDir, frameworkDir = 'framework') {
|
|
220
|
+
export async function installDomainAgents(projectDir, frameworkDir = 'framework', { projectTags = [] } = {}) {
|
|
219
221
|
const domainsDir = join(frameworkDir, 'agents');
|
|
220
222
|
if (!existsSync(domainsDir)) return { specialists: 0 };
|
|
221
223
|
|
|
@@ -223,10 +225,12 @@ export async function installDomainAgents(projectDir, frameworkDir = 'framework'
|
|
|
223
225
|
mkdirSync(targetDir, { recursive: true });
|
|
224
226
|
|
|
225
227
|
let specialists = 0;
|
|
228
|
+
const installed = new Set();
|
|
226
229
|
for (const filePath of collectSkillFiles(domainsDir)) {
|
|
227
230
|
const raw = readFileSync(filePath, 'utf-8');
|
|
228
|
-
const { name, description, allowedTools, body } = parseSkillFile(raw);
|
|
231
|
+
const { name, description, allowedTools, stacks, body } = parseSkillFile(raw);
|
|
229
232
|
if (!name) continue;
|
|
233
|
+
if (!shouldInstall(stacks, projectTags)) continue;
|
|
230
234
|
|
|
231
235
|
const desc = description ?? `MORPH-SPEC domain agent: ${name}`;
|
|
232
236
|
const frontmatter = [
|
|
@@ -239,9 +243,19 @@ export async function installDomainAgents(projectDir, frameworkDir = 'framework'
|
|
|
239
243
|
'',
|
|
240
244
|
].join('\n');
|
|
241
245
|
|
|
242
|
-
const
|
|
246
|
+
const filename = `morph-domain-${name}.md`;
|
|
247
|
+
const targetPath = join(targetDir, filename);
|
|
243
248
|
writeFileSync(targetPath, `---\n${frontmatter}---\n\n${body.trimStart()}`, 'utf-8');
|
|
249
|
+
installed.add(filename);
|
|
244
250
|
specialists++;
|
|
245
251
|
}
|
|
252
|
+
|
|
253
|
+
// Cleanup orphan morph-domain-* files
|
|
254
|
+
for (const file of readdirSync(targetDir)) {
|
|
255
|
+
if (file.startsWith('morph-domain-') && !installed.has(file)) {
|
|
256
|
+
unlinkSync(join(targetDir, file));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
246
260
|
return { specialists };
|
|
247
261
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-md-injector.js
|
|
3
|
+
*
|
|
4
|
+
* Manages the root CLAUDE.md file in client projects.
|
|
5
|
+
* Instead of overwriting the user's CLAUDE.md, injects an @import line
|
|
6
|
+
* that references the morph-spec managed .claude/CLAUDE.md.
|
|
7
|
+
*
|
|
8
|
+
* The root CLAUDE.md belongs to the USER — morph-spec only adds its import.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { pathExists, readFile, writeFile } from './file-copier.js';
|
|
12
|
+
|
|
13
|
+
const MORPH_IMPORT = '@.claude/CLAUDE.md';
|
|
14
|
+
const MORPH_MARKER = 'MORPH-SPEC';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Injects the morph-spec @import into the root CLAUDE.md.
|
|
18
|
+
*
|
|
19
|
+
* Three scenarios:
|
|
20
|
+
* 1. No CLAUDE.md exists → create minimal file with the import
|
|
21
|
+
* 2. Existing morph-only CLAUDE.md (old overwrite) → replace with minimal + import
|
|
22
|
+
* 3. Existing user CLAUDE.md → append import section at the end
|
|
23
|
+
*
|
|
24
|
+
* Idempotent — does nothing if the import already exists.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} claudeMdPath - Absolute path to root CLAUDE.md
|
|
27
|
+
* @returns {Promise<'created'|'migrated'|'injected'|'already-present'>}
|
|
28
|
+
*/
|
|
29
|
+
export async function injectMorphImport(claudeMdPath) {
|
|
30
|
+
if (!await pathExists(claudeMdPath)) {
|
|
31
|
+
await writeFile(claudeMdPath, `# Project\n\n${MORPH_IMPORT}\n`);
|
|
32
|
+
return 'created';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const content = await readFile(claudeMdPath);
|
|
36
|
+
|
|
37
|
+
if (content.includes(MORPH_IMPORT)) {
|
|
38
|
+
return 'already-present';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isMorphOnly(content)) {
|
|
42
|
+
await writeFile(claudeMdPath, `# Project\n\n${MORPH_IMPORT}\n`);
|
|
43
|
+
return 'migrated';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// User has their own content — append the import section
|
|
47
|
+
const separator = content.endsWith('\n') ? '\n' : '\n\n';
|
|
48
|
+
await writeFile(claudeMdPath, `${content}${separator}## MORPH-SPEC\n\n${MORPH_IMPORT}\n`);
|
|
49
|
+
return 'injected';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checks if CLAUDE.md contains ONLY morph-spec content (old overwrite pattern).
|
|
54
|
+
* Heuristic: contains MORPH-SPEC marker AND no substantial user content.
|
|
55
|
+
*/
|
|
56
|
+
function isMorphOnly(content) {
|
|
57
|
+
if (!content.includes(MORPH_MARKER)) return false;
|
|
58
|
+
|
|
59
|
+
// Strip morph-spec boilerplate markers and check if anything meaningful remains
|
|
60
|
+
const stripped = content
|
|
61
|
+
.replace(/# MORPH-SPEC Runtime Instructions/g, '')
|
|
62
|
+
.replace(/by Polymorphism Tech[^\n]*/g, '')
|
|
63
|
+
.replace(/\*MORPH-SPEC by Polymorphism Tech\*/g, '')
|
|
64
|
+
.replace(/@\.morph\/context\/README\.md/g, '')
|
|
65
|
+
.replace(/@\.claude\/CLAUDE\.md/g, '')
|
|
66
|
+
.replace(/## Critical Rules[\s\S]*?(?=##|$)/g, '')
|
|
67
|
+
.replace(/## Quick Reference[\s\S]*?(?=##|$)/g, '')
|
|
68
|
+
.replace(/## State & Outputs[\s\S]*?(?=##|$)/g, '')
|
|
69
|
+
.replace(/## Phase Sequence[\s\S]*?(?=##|$)/g, '')
|
|
70
|
+
.replace(/## Agents[\s\S]*?(?=##|$)/g, '')
|
|
71
|
+
.replace(/## Context Window Tip[\s\S]*?(?=##|$)/g, '')
|
|
72
|
+
.replace(/## Project Context[\s\S]*?(?=##|$)/g, '')
|
|
73
|
+
.replace(/[#\-|>*`\s]/g, '');
|
|
74
|
+
|
|
75
|
+
return stripped.length < 50;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks if the root CLAUDE.md has the morph-spec import.
|
|
80
|
+
* Used by doctor.js for health checks.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} claudeMdPath - Absolute path to root CLAUDE.md
|
|
83
|
+
* @returns {Promise<'ok'|'missing'|'missing-import'>}
|
|
84
|
+
*/
|
|
85
|
+
export async function checkClaudeMdImport(claudeMdPath) {
|
|
86
|
+
if (!await pathExists(claudeMdPath)) return 'missing';
|
|
87
|
+
const content = await readFile(claudeMdPath);
|
|
88
|
+
if (content.includes(MORPH_IMPORT)) return 'ok';
|
|
89
|
+
return 'missing-import';
|
|
90
|
+
}
|
|
@@ -31,10 +31,7 @@ const MORPH_PERMISSIONS = [
|
|
|
31
31
|
'Edit(.morph/state.json)',
|
|
32
32
|
'Write(.morph/framework/**)',
|
|
33
33
|
'Edit(.morph/framework/**)',
|
|
34
|
-
//
|
|
35
|
-
'Write(CLAUDE.md)',
|
|
36
|
-
'Edit(CLAUDE.md)',
|
|
37
|
-
// .claude/CLAUDE.md (managed copy — source is framework/CLAUDE_runtime.md)
|
|
34
|
+
// .claude/CLAUDE.md (managed copy — source is framework/CLAUDE.md)
|
|
38
35
|
'Write(.claude/CLAUDE.md)',
|
|
39
36
|
'Edit(.claude/CLAUDE.md)',
|
|
40
37
|
// .claude/rules/ (copied from framework/rules/)
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
* assets/) so Claude Code can execute scripts and load references on demand.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { mkdirSync, copyFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
19
|
+
import { mkdirSync, copyFileSync, existsSync, readdirSync, statSync, readFileSync, rmSync } from 'fs';
|
|
20
20
|
import { join } from 'path';
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
|
+
import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
|
|
22
23
|
|
|
23
24
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
24
25
|
const FRAMEWORK_SKILLS_DIR = join(__dirname, '..', '..', 'framework', 'skills');
|
|
@@ -32,6 +33,16 @@ const FRAMEWORK_SKILLS_DIR = join(__dirname, '..', '..', 'framework', 'skills');
|
|
|
32
33
|
*/
|
|
33
34
|
const SKILL_LEVELS_TO_INSTALL = ['level-0-meta', 'level-1-workflows'];
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Check if a skill should be installed based on its stacks: frontmatter.
|
|
38
|
+
* @param {string} skillContent - Raw SKILL.md content
|
|
39
|
+
* @param {string[]} projectTags - Stack tags from config
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function shouldInstallSkill(skillContent, projectTags) {
|
|
43
|
+
return shouldInstall(parseStacks(skillContent), projectTags);
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
/**
|
|
36
47
|
* Recursively copy a directory tree from src to dest.
|
|
37
48
|
* Creates dest if it doesn't exist. Skips README.md files.
|
|
@@ -71,7 +82,7 @@ function copyDirectory(src, dest) {
|
|
|
71
82
|
* @param {string} srcDir - Source directory to walk
|
|
72
83
|
* @param {string} destDir - Destination base directory (.claude/skills/)
|
|
73
84
|
*/
|
|
74
|
-
function installSkillsFromDir(srcDir, destDir) {
|
|
85
|
+
function installSkillsFromDir(srcDir, destDir, projectTags) {
|
|
75
86
|
const entries = readdirSync(srcDir);
|
|
76
87
|
for (const entry of entries) {
|
|
77
88
|
const srcPath = join(srcDir, entry);
|
|
@@ -80,15 +91,19 @@ function installSkillsFromDir(srcDir, destDir) {
|
|
|
80
91
|
if (stat.isDirectory()) {
|
|
81
92
|
const skillMdPath = join(srcPath, 'SKILL.md');
|
|
82
93
|
if (existsSync(skillMdPath)) {
|
|
83
|
-
// Directory-based skill —
|
|
94
|
+
// Directory-based skill — check stack filter before copying
|
|
95
|
+
const content = readFileSync(skillMdPath, 'utf-8');
|
|
96
|
+
if (!shouldInstallSkill(content, projectTags)) continue;
|
|
84
97
|
const skillDestDir = join(destDir, entry);
|
|
85
98
|
copyDirectory(srcPath, skillDestDir);
|
|
86
99
|
} else {
|
|
87
100
|
// Level/category directory — recurse
|
|
88
|
-
installSkillsFromDir(srcPath, destDir);
|
|
101
|
+
installSkillsFromDir(srcPath, destDir, projectTags);
|
|
89
102
|
}
|
|
90
103
|
} else if (entry.endsWith('.md') && entry !== 'README.md') {
|
|
91
|
-
// Legacy flat .md skill —
|
|
104
|
+
// Legacy flat .md skill — check stack filter before copying
|
|
105
|
+
const content = readFileSync(srcPath, 'utf-8');
|
|
106
|
+
if (!shouldInstallSkill(content, projectTags)) continue;
|
|
92
107
|
const skillName = entry.slice(0, -3);
|
|
93
108
|
const skillDir = join(destDir, skillName);
|
|
94
109
|
mkdirSync(skillDir, { recursive: true });
|
|
@@ -97,6 +112,28 @@ function installSkillsFromDir(srcDir, destDir) {
|
|
|
97
112
|
}
|
|
98
113
|
}
|
|
99
114
|
|
|
115
|
+
function collectSourceSkillNames() {
|
|
116
|
+
const names = new Set();
|
|
117
|
+
for (const level of SKILL_LEVELS_TO_INSTALL) {
|
|
118
|
+
const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
|
|
119
|
+
if (!existsSync(levelDir)) continue;
|
|
120
|
+
collectNamesFromDir(levelDir, names);
|
|
121
|
+
}
|
|
122
|
+
return names;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectNamesFromDir(dir, names) {
|
|
126
|
+
for (const entry of readdirSync(dir)) {
|
|
127
|
+
const fullPath = join(dir, entry);
|
|
128
|
+
if (!statSync(fullPath).isDirectory()) continue;
|
|
129
|
+
if (existsSync(join(fullPath, 'SKILL.md'))) {
|
|
130
|
+
names.add(entry);
|
|
131
|
+
} else {
|
|
132
|
+
collectNamesFromDir(fullPath, names);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
100
137
|
/**
|
|
101
138
|
* Install morph framework skills to .claude/skills/ in the target project.
|
|
102
139
|
* Skills are copied into subdirectories as SKILL.md files so Claude Code can
|
|
@@ -104,15 +141,38 @@ function installSkillsFromDir(srcDir, destDir) {
|
|
|
104
141
|
* /blazor-builder). Directory-based skills also get their scripts/,
|
|
105
142
|
* references/, and assets/ subdirectories copied for on-demand loading.
|
|
106
143
|
*
|
|
144
|
+
* Stack filtering: if projectTags is provided, only skills whose `stacks:`
|
|
145
|
+
* frontmatter matches the project tags are installed. Skills without a
|
|
146
|
+
* `stacks:` field are always installed (universal).
|
|
147
|
+
*
|
|
107
148
|
* @param {string} projectDir - Target project root directory
|
|
149
|
+
* @param {Object} [options]
|
|
150
|
+
* @param {string[]} [options.projectTags=[]] - Stack tags from config
|
|
108
151
|
*/
|
|
109
|
-
export async function installSkills(projectDir) {
|
|
152
|
+
export async function installSkills(projectDir, { projectTags = [] } = {}) {
|
|
110
153
|
const claudeSkillsDir = join(projectDir, '.claude', 'skills');
|
|
111
154
|
mkdirSync(claudeSkillsDir, { recursive: true });
|
|
112
155
|
|
|
113
156
|
for (const level of SKILL_LEVELS_TO_INSTALL) {
|
|
114
157
|
const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
|
|
115
158
|
if (!existsSync(levelDir)) continue;
|
|
116
|
-
installSkillsFromDir(levelDir, claudeSkillsDir);
|
|
159
|
+
installSkillsFromDir(levelDir, claudeSkillsDir, projectTags);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Collect installed morph-* skill names
|
|
163
|
+
const installed = new Set();
|
|
164
|
+
for (const entry of readdirSync(claudeSkillsDir)) {
|
|
165
|
+
if (existsSync(join(claudeSkillsDir, entry, 'SKILL.md'))) {
|
|
166
|
+
installed.add(entry);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Cleanup orphan morph-* skill dirs that exist in source but were filtered out
|
|
171
|
+
const allSourceSkills = collectSourceSkillNames();
|
|
172
|
+
for (const entry of readdirSync(claudeSkillsDir)) {
|
|
173
|
+
if (!entry.startsWith('morph-')) continue;
|
|
174
|
+
if (!installed.has(entry) && allSourceSkills.has(entry)) {
|
|
175
|
+
rmSync(join(claudeSkillsDir, entry), { recursive: true, force: true });
|
|
176
|
+
}
|
|
117
177
|
}
|
|
118
178
|
}
|
package/CLAUDE.md
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# MORPH-SPEC Runtime Instructions
|
|
2
|
-
|
|
3
|
-
> by Polymorphism Tech — Spec-driven development for .NET/Blazor/Next.js/Azure
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Project Context
|
|
8
|
-
|
|
9
|
-
@.morph/context/README.md
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Critical Rules
|
|
14
|
-
|
|
15
|
-
**NEVER:**
|
|
16
|
-
- Skip to code without a specification
|
|
17
|
-
- Implement without design approval
|
|
18
|
-
- Ignore standards in `.morph/framework/standards/`
|
|
19
|
-
- Create infrastructure manually
|
|
20
|
-
- Generate code without defined contracts
|
|
21
|
-
- List questions as plain text — always use the `AskUserQuestion` tool
|
|
22
|
-
|
|
23
|
-
**ALWAYS:**
|
|
24
|
-
- Follow the mandatory phases
|
|
25
|
-
- Generate outputs in `.morph/features/{feature}/`
|
|
26
|
-
- Document decisions in `decisions.md`
|
|
27
|
-
- Checkpoint every 3 implemented tasks
|
|
28
|
-
- Use Infrastructure as Code
|
|
29
|
-
- Use `AskUserQuestion` tool whenever asking the user anything (1–4 questions per call; split into sequential calls if more)
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Quick Reference
|
|
34
|
-
|
|
35
|
-
| Command | Purpose |
|
|
36
|
-
|---------|---------|
|
|
37
|
-
| `/morph-proposal {feature}` | Full spec pipeline (phases 1–4, pauses for approval) |
|
|
38
|
-
| `/morph-apply {feature}` | Implement feature (phase 5) |
|
|
39
|
-
| `/morph-status` | Feature status dashboard |
|
|
40
|
-
| `/morph-preflight` | Pre-implementation validation |
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## State & Outputs
|
|
45
|
-
|
|
46
|
-
| Path | Notes |
|
|
47
|
-
|------|-------|
|
|
48
|
-
| `.morph/state.json` | **READ-ONLY** — use `morph-spec` CLI to update |
|
|
49
|
-
| `.morph/features/{feature}/{phase}/` | Feature outputs organized by phase |
|
|
50
|
-
| `.morph/framework/` | **READ-ONLY** — framework files managed by morph-spec |
|
|
51
|
-
| `.morph/config/config.json` | Project configuration (editable) |
|
|
52
|
-
|
|
53
|
-
### mark-output types
|
|
54
|
-
|
|
55
|
-
Use `morph-spec state mark-output <feature> <type>` with one of these exact type names:
|
|
56
|
-
|
|
57
|
-
| Type | Phase | kebab alias |
|
|
58
|
-
|------|-------|-------------|
|
|
59
|
-
| `proposal` | proposal | — |
|
|
60
|
-
| `schemaAnalysis` | design | `schema-analysis` |
|
|
61
|
-
| `spec` | design | — |
|
|
62
|
-
| `contracts` | design | — |
|
|
63
|
-
| `contractsVsa` | design | `contracts-vsa` |
|
|
64
|
-
| `decisions` | design | — |
|
|
65
|
-
| `clarifications` | clarify | — |
|
|
66
|
-
| `tasks` | tasks | — |
|
|
67
|
-
| `uiDesignSystem` | uiux | `ui-design-system` |
|
|
68
|
-
| `uiMockups` | uiux | `ui-mockups` |
|
|
69
|
-
| `uiComponents` | uiux | `ui-components` |
|
|
70
|
-
| `uiFlows` | uiux | `ui-flows` |
|
|
71
|
-
| `recap` | implement | — |
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## Phase Sequence
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
proposal → setup → [uiux] → design → clarify → tasks → implement → [sync]
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Use `morph-spec status {feature}` to see current phase and pending approval gates.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## Agents
|
|
85
|
-
|
|
86
|
-
Tier-1 and tier-2 MORPH agents are available as native subagents in `.claude/agents/`.
|
|
87
|
-
They can be invoked directly by Claude Code during multi-agent workflows.
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Context Window Tip
|
|
92
|
-
|
|
93
|
-
When using 3+ MCPs, add `"experimental": { "mcpCliMode": true }` to `.claude/settings.json`.
|
|
94
|
-
MCP tools load on-demand instead of all at startup — keeps context clean for actual work.
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
*MORPH-SPEC by Polymorphism Tech*
|