@polymorphism-tech/morph-spec 8.16.0 → 8.18.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 (33) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +11 -0
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +45 -38
  7. package/framework/MORPH.md +119 -167
  8. package/framework/docs/CLI.md +40 -0
  9. package/framework/evals/rubrics/condensed.md +49 -0
  10. package/framework/hooks/claude-code/core/protect-spec-files.js +9 -0
  11. package/framework/rules/morph-task-tracking.md +4 -2
  12. package/framework/schemas/tasks.schema.json +10 -0
  13. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -1
  14. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -1
  15. package/framework/skills/core/morph-apply/SKILL.md +42 -34
  16. package/framework/skills/core/morph-brainstorming/SKILL.md +2 -12
  17. package/framework/skills/core/morph-eval/SKILL.md +16 -13
  18. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  19. package/framework/skills/core/morph-implement/SKILL.md +26 -25
  20. package/framework/skills/core/morph-plan/SKILL.md +15 -10
  21. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  22. package/framework/skills/core/morph-review/SKILL.md +1 -1
  23. package/framework/skills/core/morph-stack-scan/SKILL.md +2 -14
  24. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  25. package/package.json +2 -2
  26. package/src/commands/project/doctor.js +11 -4
  27. package/src/commands/project/init.js +1 -1
  28. package/src/commands/project/standards.js +220 -0
  29. package/src/commands/project/update.js +91 -7
  30. package/src/lib/stack-filter.js +7 -2
  31. package/src/lib/tasks/tasks-json-parser.js +26 -0
  32. package/src/scripts/setup-infra.js +27 -8
  33. package/src/utils/skills-installer.js +28 -15
@@ -75,8 +75,14 @@ function copyDirectory(src, dest) {
75
75
  *
76
76
  * @param {string} srcDir - Source directory to walk
77
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.
78
84
  */
79
- function installSkillsFromDir(srcDir, destDir, projectTags) {
85
+ function installSkillsFromDir(srcDir, destDir, projectTags, installedNow) {
80
86
  const entries = readdirSync(srcDir);
81
87
  for (const entry of entries) {
82
88
  const srcPath = join(srcDir, entry);
@@ -90,9 +96,10 @@ function installSkillsFromDir(srcDir, destDir, projectTags) {
90
96
  if (!shouldInstallSkill(content, projectTags)) continue;
91
97
  const skillDestDir = join(destDir, entry);
92
98
  copyDirectory(srcPath, skillDestDir);
99
+ installedNow.add(entry);
93
100
  } else {
94
101
  // Level/category directory — recurse
95
- installSkillsFromDir(srcPath, destDir, projectTags);
102
+ installSkillsFromDir(srcPath, destDir, projectTags, installedNow);
96
103
  }
97
104
  } else if (entry.endsWith('.md') && entry !== 'README.md') {
98
105
  // Legacy flat .md skill — check stack filter before copying
@@ -102,6 +109,7 @@ function installSkillsFromDir(srcDir, destDir, projectTags) {
102
109
  const skillDir = join(destDir, skillName);
103
110
  mkdirSync(skillDir, { recursive: true });
104
111
  copyFileSync(srcPath, join(skillDir, 'SKILL.md'));
112
+ installedNow.add(skillName);
105
113
  }
106
114
  }
107
115
  }
@@ -147,28 +155,33 @@ export async function installSkills(projectDir, { projectTags = [] } = {}) {
147
155
  const claudeSkillsDir = join(projectDir, '.claude', 'skills');
148
156
  mkdirSync(claudeSkillsDir, { recursive: true });
149
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();
150
164
  for (const level of SKILL_LEVELS_TO_INSTALL) {
151
165
  const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
152
166
  if (!existsSync(levelDir)) continue;
153
- installSkillsFromDir(levelDir, claudeSkillsDir, projectTags);
154
- }
155
-
156
- // Collect installed morph-* skill names
157
- const installed = new Set();
158
- for (const entry of readdirSync(claudeSkillsDir)) {
159
- if (existsSync(join(claudeSkillsDir, entry, 'SKILL.md'))) {
160
- installed.add(entry);
161
- }
167
+ installSkillsFromDir(levelDir, claudeSkillsDir, projectTags, installedNow);
162
168
  }
163
169
 
164
- // Cleanup orphan morph-* skill dirs that exist in source but were filtered out
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.
165
179
  const allSourceSkills = collectSourceSkillNames();
166
180
  for (const entry of readdirSync(claudeSkillsDir)) {
167
- if (!entry.startsWith('morph-')) continue;
168
- if (!installed.has(entry) && allSourceSkills.has(entry)) {
181
+ if (!installedNow.has(entry) && allSourceSkills.has(entry)) {
169
182
  rmSync(join(claudeSkillsDir, entry), { recursive: true, force: true });
170
183
  }
171
184
  }
172
185
 
173
- return { installed: installed.size };
186
+ return { installed: installedNow.size };
174
187
  }