@polymorphism-tech/morph-spec 8.29.0 → 8.32.0

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 (183) hide show
  1. package/CLAUDE.md +11 -1
  2. package/README.md +13 -2
  3. package/bin/morph-spec.js +67 -1
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +4 -1
  9. package/framework/MORPH.md +4 -3
  10. package/framework/agents.json +6 -6
  11. package/framework/commands/morph-status.md +1 -1
  12. package/framework/docs/CLI.md +68 -45
  13. package/framework/evals/rubrics/code-quality.md +17 -0
  14. package/framework/evals/rubrics/condensed.md +3 -1
  15. package/framework/hooks/claude-code/statusline.sh +11 -11
  16. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  17. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  18. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  19. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  20. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  21. package/framework/hooks/shared/phase-utils.js +2 -0
  22. package/framework/hooks/shared/stdin-reader.js +26 -26
  23. package/framework/mcp/registry.json +39 -4
  24. package/framework/rubrics/backend.json +2 -2
  25. package/framework/rubrics/default.json +2 -2
  26. package/framework/rubrics/frontend.json +2 -2
  27. package/framework/schemas/tasks.schema.json +25 -0
  28. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  29. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  30. package/framework/skills/core/morph-apply/SKILL.md +14 -4
  31. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  32. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  33. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  34. package/framework/skills/core/morph-implement/SKILL.md +12 -2
  35. package/framework/skills/core/morph-plan/SKILL.md +45 -3
  36. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  37. package/framework/skills/core/morph-review/SKILL.md +12 -4
  38. package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
  39. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  40. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  41. package/framework/standards/ai-agents/agent-session.md +160 -160
  42. package/framework/standards/ai-agents/batching.md +193 -193
  43. package/framework/standards/ai-agents/context-providers.md +188 -188
  44. package/framework/standards/ai-agents/mcp-server.md +250 -250
  45. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  46. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  47. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  48. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  49. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  50. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  51. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  52. package/framework/standards/ai-agents/production.md +231 -231
  53. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  54. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  55. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  56. package/framework/standards/ai-agents/structured-output.md +249 -249
  57. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  58. package/framework/templates/.idea/morph-templates.xml +92 -0
  59. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  60. package/framework/templates/IDE-SNIPPETS.md +262 -262
  61. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  62. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  63. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  64. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  65. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  66. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  67. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  68. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  69. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  70. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  71. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  72. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  73. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  74. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  75. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  76. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  77. package/framework/templates/code/typescript/contracts.ts +167 -167
  78. package/framework/templates/context/ARCHITECTURE.md +20 -0
  79. package/framework/templates/docs/clarifications.md +253 -253
  80. package/framework/templates/docs/schema-analysis.md +119 -119
  81. package/framework/templates/docs/ui-components.md +52 -52
  82. package/framework/templates/docs/ui-design-system.md +76 -76
  83. package/framework/templates/docs/ui-flows.md +167 -167
  84. package/framework/templates/docs/ui-mockups.md +98 -98
  85. package/framework/templates/feature/decisions.md +122 -122
  86. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  87. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  88. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  89. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  90. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  91. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  92. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  93. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  94. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  95. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  96. package/framework/templates/infrastructure/github/README.md +56 -56
  97. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  98. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  99. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  100. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  101. package/framework/templates/integrations/asaas-client.cs +387 -387
  102. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  103. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  104. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  105. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  106. package/framework/templates/project/validate.js +120 -120
  107. package/framework/templates/state.template.json +17 -17
  108. package/framework/templates/ui/design-system.css +226 -226
  109. package/package.json +1 -1
  110. package/src/commands/dashboard/dashboard.js +53 -53
  111. package/src/commands/graph/affected.js +89 -0
  112. package/src/commands/graph/build.js +94 -0
  113. package/src/commands/graph/explain.js +96 -0
  114. package/src/commands/graph/status.js +93 -0
  115. package/src/commands/index.js +13 -13
  116. package/src/commands/project/branch.js +170 -0
  117. package/src/commands/project/checkpoint.js +209 -209
  118. package/src/commands/project/doctor.js +148 -9
  119. package/src/commands/project/fleet.js +10 -3
  120. package/src/commands/project/worktree.js +73 -50
  121. package/src/commands/state/finish.js +41 -26
  122. package/src/commands/state/list.js +79 -6
  123. package/src/core/paths/output-schema.js +7 -0
  124. package/src/core/state/lifecycle-audit.js +127 -0
  125. package/src/core/state/phase-state-machine.js +197 -197
  126. package/src/core/templates/template-renderer.js +477 -477
  127. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  128. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  129. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  130. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  131. package/src/dashboard/app/main.jsx +13 -13
  132. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  133. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  134. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  135. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  136. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  137. package/src/dashboard/app/pages/Standards.jsx +332 -332
  138. package/src/dashboard/app/styles/index.css +47 -47
  139. package/src/dashboard/index.html +12 -12
  140. package/src/dashboard/package.json +24 -24
  141. package/src/dashboard/scanner/phases-scanner.js +15 -15
  142. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  143. package/src/dashboard/scanner/rules-scanner.js +37 -37
  144. package/src/dashboard/scanner/skills-scanner.js +43 -43
  145. package/src/dashboard/scanner/squads-scanner.js +13 -13
  146. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  147. package/src/dashboard/vite.config.js +24 -24
  148. package/src/lib/detectors/design-system-detector.js +188 -188
  149. package/src/lib/detectors/index.js +217 -217
  150. package/src/lib/generators/architecture-scanner.js +45 -4
  151. package/src/lib/generators/design-system-generator.js +508 -508
  152. package/src/lib/generators/settings-generator.js +209 -209
  153. package/src/lib/git/branch-classifier.js +177 -0
  154. package/src/lib/git/gh.js +30 -0
  155. package/src/lib/git/pending-prs.js +89 -0
  156. package/src/lib/git/pr-index.js +70 -0
  157. package/src/lib/graph/god-nodes.js +75 -0
  158. package/src/lib/graph/graph-health.js +61 -0
  159. package/src/lib/graph/graph-loader.js +86 -0
  160. package/src/lib/graph/graphify-runtime.js +100 -0
  161. package/src/lib/graph/graphifyignore.js +86 -0
  162. package/src/lib/graph/sanitize.js +280 -0
  163. package/src/lib/hooks/hook-executor.js +167 -167
  164. package/src/lib/installers/mcp-installer.js +719 -719
  165. package/src/lib/tasks/task-classifier.js +30 -30
  166. package/src/lib/tasks/task-parser.js +209 -209
  167. package/src/lib/tasks/tasks-json-parser.js +26 -0
  168. package/src/lib/validators/checkpoint-persistence.js +39 -39
  169. package/src/lib/validators/css/css-validator.js +340 -340
  170. package/src/lib/validators/reuse/index.js +10 -0
  171. package/src/lib/validators/reuse/reuse-validator.js +324 -0
  172. package/src/lib/validators/reuse/symbol-scanner.js +144 -0
  173. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  174. package/src/lib/validators/shared/issue-counter.js +14 -14
  175. package/src/lib/validators/validation-runner.js +40 -1
  176. package/src/lib/worktree/fleet-scanner.js +51 -21
  177. package/src/lib/worktree/ignored-triage.js +197 -0
  178. package/src/lib/worktree/worktree-audit.js +200 -35
  179. package/src/scripts/setup-infra.js +60 -1
  180. package/src/utils/agent-id-mapper.js +11 -11
  181. package/src/utils/claude-settings-manager.js +300 -300
  182. package/src/utils/file-copier.js +40 -0
  183. package/src/utils/skills-installer.js +187 -187
@@ -1,187 +1,187 @@
1
- /**
2
- * Skills Installer
3
- *
4
- * Copies framework skills to .claude/skills/ during morph-spec init.
5
- * Installs only the `core/` skill set — the canonical NLH skills referenced by
6
- * MORPH.md (morph-brainstorming, morph-stack-scan, morph-plan, morph-implement,
7
- * morph-eval, morph-review, morph-standards). The 3-tier system was collapsed
8
- * into a single source of truth in framework/skills/core/.
9
- *
10
- * Source skills are directory-based: framework/skills/core/{skill-name}/SKILL.md
11
- * (plus optional scripts/, references/, assets/ subdirectories).
12
- *
13
- * Directory-based skills have their entire directory copied so Claude Code can
14
- * execute scripts and load references on demand.
15
- */
16
-
17
- import { mkdirSync, copyFileSync, existsSync, readdirSync, statSync, readFileSync, rmSync } from 'fs';
18
- import { join } from 'path';
19
- import { fileURLToPath } from 'url';
20
- import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
21
-
22
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
23
- const FRAMEWORK_SKILLS_DIR = join(__dirname, '..', '..', 'framework', 'skills');
24
-
25
- /**
26
- * Skill levels to install in .claude/skills/.
27
- * Only `core/` survives — single source of truth aligned with MORPH.md.
28
- */
29
- const SKILL_LEVELS_TO_INSTALL = ['core'];
30
-
31
- /**
32
- * Check if a skill should be installed based on its stacks: frontmatter.
33
- * @param {string} skillContent - Raw SKILL.md content
34
- * @param {string[]} projectTags - Stack tags from config
35
- * @returns {boolean}
36
- */
37
- export function shouldInstallSkill(skillContent, projectTags) {
38
- return shouldInstall(parseStacks(skillContent), projectTags);
39
- }
40
-
41
- /**
42
- * Recursively copy a directory tree from src to dest.
43
- * Creates dest if it doesn't exist. Skips README.md files.
44
- *
45
- * @param {string} src - Source directory path
46
- * @param {string} dest - Destination directory path
47
- */
48
- function copyDirectory(src, dest) {
49
- mkdirSync(dest, { recursive: true });
50
- for (const entry of readdirSync(src)) {
51
- if (entry === 'README.md') continue;
52
- const srcPath = join(src, entry);
53
- const destPath = join(dest, entry);
54
- if (statSync(srcPath).isDirectory()) {
55
- copyDirectory(srcPath, destPath);
56
- } else {
57
- copyFileSync(srcPath, destPath);
58
- }
59
- }
60
- }
61
-
62
- /**
63
- * Walk srcDir and install skills into destDir.
64
- *
65
- * Handles two source formats:
66
- * 1. Directory-based skill: srcDir/{skill-name}/SKILL.md
67
- * → copies entire directory (SKILL.md + scripts/ + references/ + assets/)
68
- * → destDir/{skill-name}/
69
- *
70
- * 2. Flat .md skill (legacy): srcDir/{skill-name}.md
71
- * → destDir/{skill-name}/SKILL.md
72
- *
73
- * The `core/` level directory is traversed recursively. A directory is
74
- * recognized as a skill directory when it contains a SKILL.md file.
75
- *
76
- * @param {string} srcDir - Source directory to walk
77
- * @param {string} destDir - Destination base directory (.claude/skills/)
78
- * @param {string[]} projectTags - Stack tags from config
79
- * @param {Set<string>} installedNow - Accumulator: skill names this call
80
- * actually (re)installed. NOT derived by re-scanning destDir afterwards —
81
- * a disk scan can't distinguish "installed this run" from "leftover from a
82
- * previous run with different projectTags", which is exactly the stale
83
- * entry a stack change needs cleaned up.
84
- */
85
- function installSkillsFromDir(srcDir, destDir, projectTags, installedNow) {
86
- const entries = readdirSync(srcDir);
87
- for (const entry of entries) {
88
- const srcPath = join(srcDir, entry);
89
- const stat = statSync(srcPath);
90
-
91
- if (stat.isDirectory()) {
92
- const skillMdPath = join(srcPath, 'SKILL.md');
93
- if (existsSync(skillMdPath)) {
94
- // Directory-based skill — check stack filter before copying
95
- const content = readFileSync(skillMdPath, 'utf-8');
96
- if (!shouldInstallSkill(content, projectTags)) continue;
97
- const skillDestDir = join(destDir, entry);
98
- copyDirectory(srcPath, skillDestDir);
99
- installedNow.add(entry);
100
- } else {
101
- // Level/category directory — recurse
102
- installSkillsFromDir(srcPath, destDir, projectTags, installedNow);
103
- }
104
- } else if (entry.endsWith('.md') && entry !== 'README.md') {
105
- // Legacy flat .md skill — check stack filter before copying
106
- const content = readFileSync(srcPath, 'utf-8');
107
- if (!shouldInstallSkill(content, projectTags)) continue;
108
- const skillName = entry.slice(0, -3);
109
- const skillDir = join(destDir, skillName);
110
- mkdirSync(skillDir, { recursive: true });
111
- copyFileSync(srcPath, join(skillDir, 'SKILL.md'));
112
- installedNow.add(skillName);
113
- }
114
- }
115
- }
116
-
117
- function collectSourceSkillNames() {
118
- const names = new Set();
119
- for (const level of SKILL_LEVELS_TO_INSTALL) {
120
- const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
121
- if (!existsSync(levelDir)) continue;
122
- collectNamesFromDir(levelDir, names);
123
- }
124
- return names;
125
- }
126
-
127
- function collectNamesFromDir(dir, names) {
128
- for (const entry of readdirSync(dir)) {
129
- const fullPath = join(dir, entry);
130
- if (!statSync(fullPath).isDirectory()) continue;
131
- if (existsSync(join(fullPath, 'SKILL.md'))) {
132
- names.add(entry);
133
- } else {
134
- collectNamesFromDir(fullPath, names);
135
- }
136
- }
137
- }
138
-
139
- /**
140
- * Install morph framework skills to .claude/skills/ in the target project.
141
- * Skills are copied into subdirectories as SKILL.md files so Claude Code can
142
- * discover them natively via /<skill-name> syntax (e.g., /phase-design,
143
- * /nextjs-expert). Directory-based skills also get their scripts/,
144
- * references/, and assets/ subdirectories copied for on-demand loading.
145
- *
146
- * Stack filtering: if projectTags is provided, only skills whose `stacks:`
147
- * frontmatter matches the project tags are installed. Skills without a
148
- * `stacks:` field are always installed (universal).
149
- *
150
- * @param {string} projectDir - Target project root directory
151
- * @param {Object} [options]
152
- * @param {string[]} [options.projectTags=[]] - Stack tags from config
153
- */
154
- export async function installSkills(projectDir, { projectTags = [] } = {}) {
155
- const claudeSkillsDir = join(projectDir, '.claude', 'skills');
156
- mkdirSync(claudeSkillsDir, { recursive: true });
157
-
158
- // Skill names this call actually (re)installs, given projectTags. Tracked
159
- // as we go (NOT by re-scanning claudeSkillsDir afterwards) — a disk scan
160
- // can't tell "installed this run" apart from "leftover from a previous
161
- // run with different projectTags", which is exactly the stale entry a
162
- // stack change (or a re-run that narrows the stack) needs cleaned up.
163
- const installedNow = new Set();
164
- for (const level of SKILL_LEVELS_TO_INSTALL) {
165
- const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
166
- if (!existsSync(levelDir)) continue;
167
- installSkillsFromDir(levelDir, claudeSkillsDir, projectTags, installedNow);
168
- }
169
-
170
- // Clean-slate: remove any skill dir that is a KNOWN framework skill (still
171
- // present in framework/skills/core/) but did NOT get (re)installed this
172
- // run — i.e. it was filtered out by a `stacks:` mismatch (project.stack
173
- // changed, or a re-run narrowed the tags). Previously scoped to `morph-`
174
- // prefixed names only, which silently exempted every non-`morph-` skill
175
- // (the Umbraco pair) from ever being cleaned up: install once with
176
- // `umbraco` in the stack, drop the stack later, and they'd survive every
177
- // reinstall/update forever. Names outside allSourceSkills (user-added,
178
- // non-framework skills) are never touched.
179
- const allSourceSkills = collectSourceSkillNames();
180
- for (const entry of readdirSync(claudeSkillsDir)) {
181
- if (!installedNow.has(entry) && allSourceSkills.has(entry)) {
182
- rmSync(join(claudeSkillsDir, entry), { recursive: true, force: true });
183
- }
184
- }
185
-
186
- return { installed: installedNow.size };
187
- }
1
+ /**
2
+ * Skills Installer
3
+ *
4
+ * Copies framework skills to .claude/skills/ during morph-spec init.
5
+ * Installs only the `core/` skill set — the canonical NLH skills referenced by
6
+ * MORPH.md (morph-brainstorming, morph-stack-scan, morph-plan, morph-implement,
7
+ * morph-eval, morph-review, morph-standards). The 3-tier system was collapsed
8
+ * into a single source of truth in framework/skills/core/.
9
+ *
10
+ * Source skills are directory-based: framework/skills/core/{skill-name}/SKILL.md
11
+ * (plus optional scripts/, references/, assets/ subdirectories).
12
+ *
13
+ * Directory-based skills have their entire directory copied so Claude Code can
14
+ * execute scripts and load references on demand.
15
+ */
16
+
17
+ import { mkdirSync, copyFileSync, existsSync, readdirSync, statSync, readFileSync, rmSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { fileURLToPath } from 'url';
20
+ import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
21
+
22
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
23
+ const FRAMEWORK_SKILLS_DIR = join(__dirname, '..', '..', 'framework', 'skills');
24
+
25
+ /**
26
+ * Skill levels to install in .claude/skills/.
27
+ * Only `core/` survives — single source of truth aligned with MORPH.md.
28
+ */
29
+ const SKILL_LEVELS_TO_INSTALL = ['core'];
30
+
31
+ /**
32
+ * Check if a skill should be installed based on its stacks: frontmatter.
33
+ * @param {string} skillContent - Raw SKILL.md content
34
+ * @param {string[]} projectTags - Stack tags from config
35
+ * @returns {boolean}
36
+ */
37
+ export function shouldInstallSkill(skillContent, projectTags) {
38
+ return shouldInstall(parseStacks(skillContent), projectTags);
39
+ }
40
+
41
+ /**
42
+ * Recursively copy a directory tree from src to dest.
43
+ * Creates dest if it doesn't exist. Skips README.md files.
44
+ *
45
+ * @param {string} src - Source directory path
46
+ * @param {string} dest - Destination directory path
47
+ */
48
+ function copyDirectory(src, dest) {
49
+ mkdirSync(dest, { recursive: true });
50
+ for (const entry of readdirSync(src)) {
51
+ if (entry === 'README.md') continue;
52
+ const srcPath = join(src, entry);
53
+ const destPath = join(dest, entry);
54
+ if (statSync(srcPath).isDirectory()) {
55
+ copyDirectory(srcPath, destPath);
56
+ } else {
57
+ copyFileSync(srcPath, destPath);
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Walk srcDir and install skills into destDir.
64
+ *
65
+ * Handles two source formats:
66
+ * 1. Directory-based skill: srcDir/{skill-name}/SKILL.md
67
+ * → copies entire directory (SKILL.md + scripts/ + references/ + assets/)
68
+ * → destDir/{skill-name}/
69
+ *
70
+ * 2. Flat .md skill (legacy): srcDir/{skill-name}.md
71
+ * → destDir/{skill-name}/SKILL.md
72
+ *
73
+ * The `core/` level directory is traversed recursively. A directory is
74
+ * recognized as a skill directory when it contains a SKILL.md file.
75
+ *
76
+ * @param {string} srcDir - Source directory to walk
77
+ * @param {string} destDir - Destination base directory (.claude/skills/)
78
+ * @param {string[]} projectTags - Stack tags from config
79
+ * @param {Set<string>} installedNow - Accumulator: skill names this call
80
+ * actually (re)installed. NOT derived by re-scanning destDir afterwards —
81
+ * a disk scan can't distinguish "installed this run" from "leftover from a
82
+ * previous run with different projectTags", which is exactly the stale
83
+ * entry a stack change needs cleaned up.
84
+ */
85
+ function installSkillsFromDir(srcDir, destDir, projectTags, installedNow) {
86
+ const entries = readdirSync(srcDir);
87
+ for (const entry of entries) {
88
+ const srcPath = join(srcDir, entry);
89
+ const stat = statSync(srcPath);
90
+
91
+ if (stat.isDirectory()) {
92
+ const skillMdPath = join(srcPath, 'SKILL.md');
93
+ if (existsSync(skillMdPath)) {
94
+ // Directory-based skill — check stack filter before copying
95
+ const content = readFileSync(skillMdPath, 'utf-8');
96
+ if (!shouldInstallSkill(content, projectTags)) continue;
97
+ const skillDestDir = join(destDir, entry);
98
+ copyDirectory(srcPath, skillDestDir);
99
+ installedNow.add(entry);
100
+ } else {
101
+ // Level/category directory — recurse
102
+ installSkillsFromDir(srcPath, destDir, projectTags, installedNow);
103
+ }
104
+ } else if (entry.endsWith('.md') && entry !== 'README.md') {
105
+ // Legacy flat .md skill — check stack filter before copying
106
+ const content = readFileSync(srcPath, 'utf-8');
107
+ if (!shouldInstallSkill(content, projectTags)) continue;
108
+ const skillName = entry.slice(0, -3);
109
+ const skillDir = join(destDir, skillName);
110
+ mkdirSync(skillDir, { recursive: true });
111
+ copyFileSync(srcPath, join(skillDir, 'SKILL.md'));
112
+ installedNow.add(skillName);
113
+ }
114
+ }
115
+ }
116
+
117
+ function collectSourceSkillNames() {
118
+ const names = new Set();
119
+ for (const level of SKILL_LEVELS_TO_INSTALL) {
120
+ const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
121
+ if (!existsSync(levelDir)) continue;
122
+ collectNamesFromDir(levelDir, names);
123
+ }
124
+ return names;
125
+ }
126
+
127
+ function collectNamesFromDir(dir, names) {
128
+ for (const entry of readdirSync(dir)) {
129
+ const fullPath = join(dir, entry);
130
+ if (!statSync(fullPath).isDirectory()) continue;
131
+ if (existsSync(join(fullPath, 'SKILL.md'))) {
132
+ names.add(entry);
133
+ } else {
134
+ collectNamesFromDir(fullPath, names);
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Install morph framework skills to .claude/skills/ in the target project.
141
+ * Skills are copied into subdirectories as SKILL.md files so Claude Code can
142
+ * discover them natively via /<skill-name> syntax (e.g., /phase-design,
143
+ * /nextjs-expert). Directory-based skills also get their scripts/,
144
+ * references/, and assets/ subdirectories copied for on-demand loading.
145
+ *
146
+ * Stack filtering: if projectTags is provided, only skills whose `stacks:`
147
+ * frontmatter matches the project tags are installed. Skills without a
148
+ * `stacks:` field are always installed (universal).
149
+ *
150
+ * @param {string} projectDir - Target project root directory
151
+ * @param {Object} [options]
152
+ * @param {string[]} [options.projectTags=[]] - Stack tags from config
153
+ */
154
+ export async function installSkills(projectDir, { projectTags = [] } = {}) {
155
+ const claudeSkillsDir = join(projectDir, '.claude', 'skills');
156
+ mkdirSync(claudeSkillsDir, { recursive: true });
157
+
158
+ // Skill names this call actually (re)installs, given projectTags. Tracked
159
+ // as we go (NOT by re-scanning claudeSkillsDir afterwards) — a disk scan
160
+ // can't tell "installed this run" apart from "leftover from a previous
161
+ // run with different projectTags", which is exactly the stale entry a
162
+ // stack change (or a re-run that narrows the stack) needs cleaned up.
163
+ const installedNow = new Set();
164
+ for (const level of SKILL_LEVELS_TO_INSTALL) {
165
+ const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
166
+ if (!existsSync(levelDir)) continue;
167
+ installSkillsFromDir(levelDir, claudeSkillsDir, projectTags, installedNow);
168
+ }
169
+
170
+ // Clean-slate: remove any skill dir that is a KNOWN framework skill (still
171
+ // present in framework/skills/core/) but did NOT get (re)installed this
172
+ // run — i.e. it was filtered out by a `stacks:` mismatch (project.stack
173
+ // changed, or a re-run narrowed the tags). Previously scoped to `morph-`
174
+ // prefixed names only, which silently exempted every non-`morph-` skill
175
+ // (the Umbraco pair) from ever being cleaned up: install once with
176
+ // `umbraco` in the stack, drop the stack later, and they'd survive every
177
+ // reinstall/update forever. Names outside allSourceSkills (user-added,
178
+ // non-framework skills) are never touched.
179
+ const allSourceSkills = collectSourceSkillNames();
180
+ for (const entry of readdirSync(claudeSkillsDir)) {
181
+ if (!installedNow.has(entry) && allSourceSkills.has(entry)) {
182
+ rmSync(join(claudeSkillsDir, entry), { recursive: true, force: true });
183
+ }
184
+ }
185
+
186
+ return { installed: installedNow.size };
187
+ }