@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.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/CLAUDE.md +5 -69
  6. package/framework/agents/backend/api-designer.md +3 -0
  7. package/framework/agents/backend/dotnet-senior.md +3 -0
  8. package/framework/agents/backend/ef-modeler.md +2 -0
  9. package/framework/agents/backend/hangfire-orchestrator.md +2 -0
  10. package/framework/agents/backend/ms-agent-expert.md +2 -0
  11. package/framework/agents/frontend/blazor-builder.md +2 -0
  12. package/framework/agents/frontend/nextjs-expert.md +2 -0
  13. package/framework/agents/infrastructure/azure-architect.md +2 -0
  14. package/framework/agents/infrastructure/azure-deploy-specialist.md +2 -0
  15. package/framework/agents/infrastructure/bicep-architect.md +2 -0
  16. package/framework/agents/infrastructure/container-specialist.md +2 -0
  17. package/framework/agents/infrastructure/devops-engineer.md +3 -0
  18. package/framework/agents/infrastructure/infra-architect.md +3 -0
  19. package/framework/agents/integrations/asaas-financial.md +2 -0
  20. package/framework/agents/integrations/azure-identity.md +2 -0
  21. package/framework/agents/integrations/clerk-auth.md +3 -0
  22. package/framework/agents/integrations/hangfire-integration.md +2 -0
  23. package/framework/agents/integrations/resend-email.md +2 -0
  24. package/framework/commands/morph-apply.md +151 -161
  25. package/framework/commands/morph-archive.md +28 -28
  26. package/framework/commands/morph-infra.md +79 -79
  27. package/framework/commands/morph-preflight.md +92 -56
  28. package/framework/commands/morph-proposal.md +94 -70
  29. package/framework/commands/morph-status.md +31 -31
  30. package/framework/commands/morph-troubleshoot.md +63 -60
  31. package/framework/rules/csharp-standards.md +3 -0
  32. package/framework/rules/frontend-standards.md +2 -0
  33. package/framework/rules/infrastructure-standards.md +3 -0
  34. package/framework/rules/morph-workflow.md +57 -2
  35. package/framework/rules/nextjs-standards.md +2 -0
  36. package/framework/rules/testing-standards.md +3 -0
  37. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +54 -49
  38. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +42 -19
  39. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +8 -5
  40. package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +7 -5
  41. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +139 -136
  42. package/framework/skills/level-0-meta/morph-init/SKILL.md +42 -13
  43. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +130 -130
  44. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +95 -87
  45. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +24 -0
  46. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +42 -41
  47. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +22 -11
  48. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +123 -114
  49. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +120 -102
  50. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +206 -214
  51. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +38 -0
  52. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +241 -360
  53. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +107 -115
  54. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +135 -135
  55. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +14 -0
  56. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +143 -139
  57. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +168 -165
  58. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +57 -8
  59. package/package.json +3 -3
  60. package/src/commands/project/doctor.js +7 -2
  61. package/src/commands/project/update.js +4 -4
  62. package/src/lib/stack-filter.js +58 -0
  63. package/src/scripts/setup-infra.js +53 -18
  64. package/src/utils/agents-installer.js +19 -5
  65. package/src/utils/claude-md-injector.js +90 -0
  66. package/src/utils/hooks-installer.js +1 -4
  67. package/src/utils/skills-installer.js +67 -7
  68. package/CLAUDE.md +0 -98
  69. package/framework/memory/patterns-learned.md +0 -766
  70. package/framework/skills/level-0-meta/morph-terminal-title/SKILL.md +0 -61
  71. 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
- // Update CLAUDE.md
325
- updateSpinner.text = 'Updating CLAUDE.md...';
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 copyFile(claudeMdSrc, claudeMdDest);
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. Copy CLAUDE.md (backup existing non-MORPH CLAUDE.md) ---
67
- log('Step 1: Copying CLAUDE.md...');
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
- if (await pathExists(claudeMdDest)) {
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
- // --- 10. Copy framework/rules .claude/rules ---
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 copyDirectory(rulesSrc, rulesDest);
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
- return { name, description, allowedTools, body };
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 targetPath = join(targetDir, `morph-domain-${name}.md`);
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
- // Root CLAUDE.md (managed copy — source is framework/CLAUDE.md)
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 — copy entire directory including scripts/, references/, assets/
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 — wrap in subdirectory as SKILL.md
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*