@polymorphism-tech/morph-spec 4.8.1 → 4.8.4

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 (44) 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/hooks/dev/guard-version-numbers.js +1 -1
  6. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
  7. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
  8. package/framework/skills/level-1-workflows/phase-design/SKILL.md +1 -1
  9. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +1 -1
  10. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
  11. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +1 -1
  12. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
  13. package/package.json +4 -4
  14. package/.morph/analytics/threads-log.jsonl +0 -54
  15. package/.morph/state.json +0 -198
  16. package/docs/ARCHITECTURE.md +0 -328
  17. package/docs/COMMAND-FLOWS.md +0 -398
  18. package/docs/plans/2026-02-22-claude-docs-morph-alignment-analysis.md +0 -514
  19. package/docs/plans/2026-02-22-claude-settings.md +0 -517
  20. package/docs/plans/2026-02-22-morph-cc-alignment-impl.md +0 -730
  21. package/docs/plans/2026-02-22-morph-spec-next.md +0 -480
  22. package/docs/plans/2026-02-22-native-alignment-design.md +0 -201
  23. package/docs/plans/2026-02-22-native-alignment-impl.md +0 -927
  24. package/docs/plans/2026-02-22-native-enrichment-design.md +0 -246
  25. package/docs/plans/2026-02-22-native-enrichment.md +0 -737
  26. package/docs/plans/2026-02-23-ddd-architecture-refactor.md +0 -1155
  27. package/docs/plans/2026-02-23-ddd-nextsteps.md +0 -684
  28. package/docs/plans/2026-02-23-infra-architect-refactor.md +0 -439
  29. package/docs/plans/2026-02-23-nextjs-code-review-design.md +0 -157
  30. package/docs/plans/2026-02-23-nextjs-code-review-impl.md +0 -1256
  31. package/docs/plans/2026-02-23-nextjs-standards-design.md +0 -150
  32. package/docs/plans/2026-02-23-nextjs-standards-impl.md +0 -1848
  33. package/docs/plans/2026-02-24-cli-radical-simplification.md +0 -592
  34. package/docs/plans/2026-02-24-framework-failure-points.md +0 -125
  35. package/docs/plans/2026-02-24-morph-init-design.md +0 -337
  36. package/docs/plans/2026-02-24-morph-init-impl.md +0 -1269
  37. package/docs/plans/2026-02-24-tutorial-command-design.md +0 -71
  38. package/docs/plans/2026-02-24-tutorial-command.md +0 -298
  39. package/scripts/bump-version.js +0 -248
  40. package/scripts/generate-refs.js +0 -336
  41. package/scripts/generate-standards-registry.js +0 -44
  42. package/scripts/install-dev-hooks.js +0 -138
  43. package/scripts/scan-nextjs.mjs +0 -169
  44. package/scripts/validate-real.mjs +0 -255
@@ -1,737 +0,0 @@
1
- # Claude Code Native Enrichment Implementation Plan
2
-
3
- **Status:** COMPLETE
4
-
5
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
6
-
7
- **Goal:** Enrich morph-spec's use of Claude Code native primitives — richer agent frontmatter, Stop hook upgrade, level-2 skill installation, and SubagentStart telemetry.
8
-
9
- **Architecture:** Four independent improvements to `agents-installer.js`, `skills-installer.js`, `hooks-installer.js`, and a new `log-agent-start.js` hook. Each follows TDD with tests first. All are self-contained and can be committed independently.
10
-
11
- **Tech Stack:** Node.js ESM, node:test, fs/promises, Claude Code settings.json schema
12
-
13
- **Design doc:** `docs/plans/2026-02-22-native-enrichment-design.md`
14
-
15
- ---
16
-
17
- ## Task 1: Richer agent frontmatter
18
-
19
- **Context:** Currently generated `.claude/agents/morph-*.md` files have only `name` and `description` in YAML. Claude Code supports `model`, `tools`, `maxTurns`, `skills`, and `memory`. This task adds tier-based defaults to the installer.
20
-
21
- **Files:**
22
- - Modify: `src/utils/agents-installer.js`
23
- - Modify: `test/utils/agents-installer.test.js`
24
-
25
- **Tier defaults:**
26
-
27
- | Tier | model | tools | maxTurns | skills | memory |
28
- |------|-------|-------|----------|--------|--------|
29
- | 1 (Orchestrators) | `inherit` | `Read, Grep, Glob, Bash, Task` | `30` | `morph-checklist` | `project` |
30
- | 2 (Domain Leaders) | `inherit` | `Read, Grep, Glob, Bash` | `20` | `morph-checklist` | `local` |
31
-
32
- `morph-checklist` refers to `.claude/skills/morph-checklist.md` (installed from `framework/skills/level-0-meta/morph-checklist.md`).
33
-
34
- ---
35
-
36
- ### Step 1: Write failing tests
37
-
38
- Add to the bottom of `test/utils/agents-installer.test.js`:
39
-
40
- ```js
41
- describe('installAgents — richer frontmatter', () => {
42
- let tmpDir;
43
-
44
- before(async () => {
45
- tmpDir = await mkdtemp(join(tmpdir(), 'morph-agents-rich-'));
46
- await installAgents(tmpDir, FRAMEWORK_DIR);
47
- });
48
-
49
- after(async () => {
50
- await rm(tmpDir, { recursive: true, force: true });
51
- });
52
-
53
- test('tier-1 agent has model field', async () => {
54
- const files = await readdir(join(tmpDir, '.claude', 'agents'));
55
- const tier1File = files.find(f => f.includes('standards-architect') || f.includes('ai-system'));
56
- assert.ok(tier1File, 'should have a tier-1 agent file');
57
- const content = await readFile(join(tmpDir, '.claude', 'agents', tier1File), 'utf-8');
58
- assert.ok(content.includes('model:'), `${tier1File} should have model field`);
59
- });
60
-
61
- test('tier-1 agent has tools field including Task', async () => {
62
- const files = await readdir(join(tmpDir, '.claude', 'agents'));
63
- const tier1File = files.find(f => f.includes('standards-architect'));
64
- assert.ok(tier1File);
65
- const content = await readFile(join(tmpDir, '.claude', 'agents', tier1File), 'utf-8');
66
- assert.ok(content.includes('tools:'), `${tier1File} should have tools field`);
67
- assert.ok(content.includes('Task'), `${tier1File} tier-1 should have Task tool`);
68
- });
69
-
70
- test('tier-2 agent has tools field without Task', async () => {
71
- const files = await readdir(join(tmpDir, '.claude', 'agents'));
72
- // tier-2 agents don't have Task in their tools
73
- const tier2File = files.find(f => f.includes('thread-orchestrator'));
74
- if (tier2File) {
75
- const content = await readFile(join(tmpDir, '.claude', 'agents', tier2File), 'utf-8');
76
- assert.ok(content.includes('tools:'), `${tier2File} should have tools field`);
77
- }
78
- // At minimum, all files should have a tools: field
79
- for (const file of files) {
80
- const content = await readFile(join(tmpDir, '.claude', 'agents', file), 'utf-8');
81
- assert.ok(content.includes('tools:'), `${file} should have tools field`);
82
- }
83
- });
84
-
85
- test('all agents have maxTurns field', async () => {
86
- const files = await readdir(join(tmpDir, '.claude', 'agents'));
87
- for (const file of files) {
88
- const content = await readFile(join(tmpDir, '.claude', 'agents', file), 'utf-8');
89
- assert.ok(content.includes('maxTurns:'), `${file} should have maxTurns field`);
90
- }
91
- });
92
-
93
- test('all agents have memory field', async () => {
94
- const files = await readdir(join(tmpDir, '.claude', 'agents'));
95
- for (const file of files) {
96
- const content = await readFile(join(tmpDir, '.claude', 'agents', file), 'utf-8');
97
- assert.ok(content.includes('memory:'), `${file} should have memory field`);
98
- }
99
- });
100
-
101
- test('all agents have skills field with morph-checklist', async () => {
102
- const files = await readdir(join(tmpDir, '.claude', 'agents'));
103
- for (const file of files) {
104
- const content = await readFile(join(tmpDir, '.claude', 'agents', file), 'utf-8');
105
- assert.ok(content.includes('skills:'), `${file} should have skills field`);
106
- assert.ok(content.includes('morph-checklist'), `${file} should reference morph-checklist skill`);
107
- }
108
- });
109
- });
110
- ```
111
-
112
- ### Step 2: Run test to verify it fails
113
-
114
- ```bash
115
- npm test -- test/utils/agents-installer.test.js
116
- ```
117
-
118
- Expected: FAIL — `model:` / `tools:` / `maxTurns:` / `memory:` / `skills:` not found in generated files.
119
-
120
- ### Step 3: Implement the changes in `agents-installer.js`
121
-
122
- In `buildDescription()` — no change.
123
-
124
- Replace the content generation line in `installAgents()`:
125
-
126
- ```js
127
- // BEFORE (line 46):
128
- const content = `---\nname: ${agent.title ?? agent.name}\ndescription: ${description}\n---\n\n${body}\n`;
129
-
130
- // AFTER:
131
- const frontmatter = buildFrontmatter(agent, description);
132
- const content = `---\n${frontmatter}---\n\n${body}\n`;
133
- ```
134
-
135
- Add `buildFrontmatter()` function after `buildDescription()`:
136
-
137
- ```js
138
- function buildFrontmatter(agent, description) {
139
- const isOrchestrator = agent.tier === 1;
140
- const tools = isOrchestrator
141
- ? 'Read, Grep, Glob, Bash, Task'
142
- : 'Read, Grep, Glob, Bash';
143
- const maxTurns = isOrchestrator ? 30 : 20;
144
- const memory = isOrchestrator ? 'project' : 'local';
145
- const name = agent.title ?? agent.name;
146
-
147
- return [
148
- `name: ${name}`,
149
- `description: ${description}`,
150
- `model: inherit`,
151
- `tools: ${tools}`,
152
- `maxTurns: ${maxTurns}`,
153
- `skills:`,
154
- ` - morph-checklist`,
155
- `memory: ${memory}`,
156
- ``
157
- ].join('\n');
158
- }
159
- ```
160
-
161
- ### Step 4: Run tests to verify they pass
162
-
163
- ```bash
164
- npm test -- test/utils/agents-installer.test.js
165
- ```
166
-
167
- Expected: All 11 tests pass (5 original + 6 new).
168
-
169
- ### Step 5: Run full suite to verify no regressions
170
-
171
- ```bash
172
- npm test
173
- ```
174
-
175
- Expected: All existing tests pass, 6 new tests pass.
176
-
177
- ### Step 6: Commit
178
-
179
- ```bash
180
- git add src/utils/agents-installer.js test/utils/agents-installer.test.js
181
- git commit -m "feat(agents): add tier-based model/tools/maxTurns/skills/memory to agent frontmatter"
182
- ```
183
-
184
- ---
185
-
186
- ## Task 2: Upgrade Stop hook to `type: "agent"`
187
-
188
- **Context:** The current Stop hook runs `validate-completion.js`, a Node.js script that reads `state.json` and injects advisory context. The docs support `type: "agent"` — a subagent with tool access that makes a `{"ok": true/false}` decision. This replaces the Node.js script with an inline LLM-based check.
189
-
190
- **Key difference:** The current hook is *advisory* (injects context, always exits 0). The new hook is *blocking* (returns `ok: false` to continue Claude working). This changes behavior — Claude will be asked to continue if work is incomplete, rather than just warned.
191
-
192
- **Marker problem:** `removeMorphHooks()` identifies morph hooks by path pattern in `command`. Agent hooks have no `command`. We must add `_morph: true` to all morph hook entries so the cleanup logic can identify them regardless of type.
193
-
194
- **Files:**
195
- - Modify: `src/utils/hooks-installer.js`
196
- - Test: `test/hooks/hooks-installer.test.js`
197
-
198
- ---
199
-
200
- ### Step 1: Write failing tests
201
-
202
- Add to `test/hooks/hooks-installer.test.js` inside the `describe('installClaudeHooks', ...)` block:
203
-
204
- ```js
205
- test('Stop hook uses type "agent" not type "command"', async () => {
206
- await installClaudeHooks(tempDir);
207
- const settings = readSettings();
208
-
209
- const stopHooks = settings.hooks.Stop;
210
- assert.ok(stopHooks, 'Stop event should exist');
211
- assert.ok(stopHooks.length > 0);
212
- const stopHook = stopHooks[0];
213
- assert.ok(stopHook.hooks?.length > 0);
214
- assert.strictEqual(stopHook.hooks[0].type, 'agent', 'Stop hook should use type: agent');
215
- });
216
-
217
- test('Stop hook has prompt and timeout fields', async () => {
218
- await installClaudeHooks(tempDir);
219
- const settings = readSettings();
220
-
221
- const stopHook = settings.hooks.Stop[0].hooks[0];
222
- assert.ok(typeof stopHook.prompt === 'string', 'agent hook should have prompt');
223
- assert.ok(stopHook.prompt.includes('.morph/state.json'), 'prompt should reference state.json');
224
- assert.ok(typeof stopHook.timeout === 'number', 'agent hook should have timeout');
225
- });
226
-
227
- test('SubagentStart hook is installed with morph-.* matcher', async () => {
228
- await installClaudeHooks(tempDir);
229
- const settings = readSettings();
230
-
231
- assert.ok(settings.hooks.SubagentStart, 'SubagentStart event should exist');
232
- const entry = settings.hooks.SubagentStart[0];
233
- assert.strictEqual(entry.matcher, 'morph-.*');
234
- });
235
-
236
- test('morph hook entries have _morph marker for cleanup identification', async () => {
237
- await installClaudeHooks(tempDir);
238
- const settings = readSettings();
239
-
240
- // Check a known morph hook event
241
- const preToolUse = settings.hooks.PreToolUse;
242
- const morphEntry = preToolUse.find(e => e._morph === true);
243
- assert.ok(morphEntry, 'at least one PreToolUse entry should have _morph: true');
244
- });
245
- ```
246
-
247
- ### Step 2: Run tests to verify they fail
248
-
249
- ```bash
250
- npm test -- test/hooks/hooks-installer.test.js
251
- ```
252
-
253
- Expected: FAIL — Stop hook still uses `type: "command"`, no `SubagentStart`, no `_morph` marker.
254
-
255
- ### Step 3: Update `MORPH_HOOKS` in `hooks-installer.js`
256
-
257
- **3a.** Add `_morph: true` to each hook entry in `MORPH_HOOKS`. Change every `hooks: [...]` array item wrapper to include the marker. Example — the SessionStart entry becomes:
258
-
259
- ```js
260
- {
261
- event: 'SessionStart',
262
- matcher: 'startup|resume|compact',
263
- _morph: true,
264
- hooks: [{ type: 'command', command: 'node framework/hooks/claude-code/session-start/inject-morph-context.js' }]
265
- },
266
- ```
267
-
268
- Do this for ALL entries in `MORPH_HOOKS` (9 existing entries).
269
-
270
- **3b.** Replace the Stop entry:
271
-
272
- ```js
273
- // BEFORE:
274
- {
275
- event: 'Stop',
276
- matcher: null,
277
- hooks: [{
278
- type: 'command',
279
- command: 'node framework/hooks/claude-code/stop/validate-completion.js'
280
- }]
281
- },
282
-
283
- // AFTER:
284
- {
285
- event: 'Stop',
286
- matcher: null,
287
- _morph: true,
288
- hooks: [{
289
- type: 'agent',
290
- prompt: `Check if the active morph-spec feature phase outputs are complete.
291
- 1. Read the file .morph/state.json to find features with status "in_progress".
292
- 2. For each in_progress feature, check if required output files for the current phase exist and are non-empty.
293
- - proposal phase: .morph/features/{feature}/0-proposal/proposal.md
294
- - design phase: .morph/features/{feature}/1-design/spec.md
295
- - tasks phase: .morph/features/{feature}/3-tasks/tasks.md
296
- - implement phase: check tasks.completed vs tasks.total from state.json
297
- 3. If all required outputs exist and tasks are complete, return {"ok": true}.
298
- 4. If any required output is missing or empty, return {"ok": false, "reason": "Missing output: <path>"}.
299
- 5. If state.json does not exist or no feature is in_progress, return {"ok": true}.
300
- Do NOT modify any files. Read only.`,
301
- timeout: 60
302
- }]
303
- },
304
- ```
305
-
306
- **3c.** Add SubagentStart entry (after the Notification entry):
307
-
308
- ```js
309
- // === SubagentStart ===
310
- {
311
- event: 'SubagentStart',
312
- matcher: 'morph-.*',
313
- _morph: true,
314
- hooks: [{
315
- type: 'command',
316
- command: 'node framework/hooks/claude-code/subagent/log-agent-start.js'
317
- }]
318
- },
319
- ```
320
-
321
- **3d.** Update `removeMorphHooks()` to detect `_morph: true` marker (not just command path):
322
-
323
- ```js
324
- function removeMorphHooks(settings) {
325
- const morphPathPattern = /framework\/hooks\/claude-code\//;
326
-
327
- for (const [event, entries] of Object.entries(settings.hooks)) {
328
- if (!Array.isArray(entries)) continue;
329
-
330
- settings.hooks[event] = entries.filter(entry => {
331
- // Remove if marked with _morph flag
332
- if (entry._morph === true) return false;
333
-
334
- if (!Array.isArray(entry.hooks)) return true;
335
- // Legacy detection: remove if all hooks are morph-managed command hooks
336
- const nonMorphHooks = entry.hooks.filter(h =>
337
- typeof h.command !== 'string' || !morphPathPattern.test(h.command)
338
- );
339
- return nonMorphHooks.length > 0;
340
- });
341
-
342
- if (settings.hooks[event].length === 0) {
343
- delete settings.hooks[event];
344
- }
345
- }
346
- }
347
- ```
348
-
349
- **3e.** Update the install loop to propagate `_morph: true` to the installed entry:
350
-
351
- ```js
352
- // In the for loop inside installClaudeHooks():
353
- const entry = { _morph: true }; // ← add this flag
354
- if (matcher) {
355
- entry.matcher = matcher;
356
- }
357
- entry.hooks = hooks.map(h => {
358
- // For agent/prompt type hooks, pass through as-is (no command path transform)
359
- if (h.type === 'agent' || h.type === 'prompt') {
360
- return { type: h.type, prompt: h.prompt, timeout: h.timeout };
361
- }
362
- return {
363
- type: h.type,
364
- command: `node "$CLAUDE_PROJECT_DIR/framework/hooks/claude-code/${getHookSubpath(h.command)}"`,
365
- ...(h.timeout ? { timeout: h.timeout } : {})
366
- };
367
- });
368
- ```
369
-
370
- ### Step 4: Run tests to verify they pass
371
-
372
- ```bash
373
- npm test -- test/hooks/hooks-installer.test.js
374
- ```
375
-
376
- Expected: All tests pass, including the 4 new ones.
377
-
378
- ### Step 5: Run full suite
379
-
380
- ```bash
381
- npm test
382
- ```
383
-
384
- Expected: All existing tests pass. Note: the `installs all hook events` test checks `settings.hooks.Stop` — this still passes since Stop still exists, just uses type:agent now.
385
-
386
- ### Step 6: Commit
387
-
388
- ```bash
389
- git add src/utils/hooks-installer.js test/hooks/hooks-installer.test.js
390
- git commit -m "feat(hooks): upgrade Stop hook to type:agent; add SubagentStart hook; add _morph marker"
391
- ```
392
-
393
- ---
394
-
395
- ## Task 3: Create `framework/hooks/claude-code/subagent/log-agent-start.js`
396
-
397
- **Context:** The SubagentStart hook added in Task 2 references this script. It needs to exist and be a simple, fail-open logger.
398
-
399
- **Files:**
400
- - Create: `framework/hooks/claude-code/subagent/log-agent-start.js`
401
-
402
- ---
403
-
404
- ### Step 1: Write the hook script
405
-
406
- ```js
407
- #!/usr/bin/env node
408
-
409
- /**
410
- * SubagentStart Hook: Log Morph Agent Activation
411
- *
412
- * Event: SubagentStart (matcher: morph-.*)
413
- *
414
- * Appends a JSON line to .morph/logs/agents.log whenever a morph
415
- * native subagent is activated. Fail-open: exits 0 on any error.
416
- */
417
-
418
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
419
- import { join } from 'path';
420
-
421
- try {
422
- const input = readFileSync('/dev/stdin', 'utf-8');
423
- const data = JSON.parse(input);
424
-
425
- const logDir = join(process.cwd(), '.morph', 'logs');
426
- if (!existsSync(logDir)) {
427
- mkdirSync(logDir, { recursive: true });
428
- }
429
-
430
- const logPath = join(logDir, 'agents.log');
431
- const entry = JSON.stringify({
432
- timestamp: new Date().toISOString(),
433
- agentType: data.agent_type ?? data.agentType ?? 'unknown',
434
- sessionId: data.session_id ?? null
435
- }) + '\n';
436
-
437
- writeFileSync(logPath, entry, { flag: 'a', encoding: 'utf-8' });
438
- } catch {
439
- // Fail-open: never block agent activation
440
- }
441
-
442
- process.exit(0);
443
- ```
444
-
445
- **Windows note:** `/dev/stdin` does not exist on Windows. Use `process.stdin` with a streaming approach. For cross-platform support, replace the stdin read with:
446
-
447
- ```js
448
- import { readFileSync } from 'node:fs';
449
-
450
- function readStdin() {
451
- try {
452
- // Unix
453
- return readFileSync('/dev/stdin', 'utf-8');
454
- } catch {
455
- // Windows fallback: read from fd 0
456
- return readFileSync(0, 'utf-8');
457
- }
458
- }
459
- ```
460
-
461
- Actually, use the pattern already used by the other hooks. Check `framework/hooks/shared/stdin-reader.js` for the correct stdin reading utility.
462
-
463
- ### Step 2: Check the existing stdin pattern
464
-
465
- ```bash
466
- cat framework/hooks/shared/stdin-reader.js
467
- ```
468
-
469
- Use whatever pattern it exports (likely `readStdin()` or similar). Import and use it.
470
-
471
- ### Step 3: Run full test suite (no new unit tests for this hook — it's a simple I/O script)
472
-
473
- ```bash
474
- npm test
475
- ```
476
-
477
- Expected: All tests still pass. No regressions.
478
-
479
- ### Step 4: Commit
480
-
481
- ```bash
482
- git add framework/hooks/claude-code/subagent/log-agent-start.js
483
- git commit -m "feat(hooks): add SubagentStart log-agent-start.js hook script"
484
- ```
485
-
486
- ---
487
-
488
- ## Task 4: Install level-2 skills in `skills-installer.js`
489
-
490
- **Context:** Level-2 domain skills (`level-2-domains/`) contain 17 `.md` files across 7 subdirectories. The skills-installer currently only handles flat directories (level-0 and level-1). Level-2 requires recursive walk. Level-3 and level-4 only have README.md — no content to install yet.
491
-
492
- **Duplicate file warning:** `hangfire-orchestrator.md` exists in both `backend/` and `integrations/` subdirectories. The flat install will overwrite with whichever comes last. This is acceptable — they likely have different content, but users get one version. Document this in comments.
493
-
494
- **Files:**
495
- - Modify: `src/utils/skills-installer.js`
496
- - Create: `test/utils/skills-installer.test.js`
497
-
498
- ---
499
-
500
- ### Step 1: Write failing tests
501
-
502
- Create `test/utils/skills-installer.test.js`:
503
-
504
- ```js
505
- /**
506
- * Tests for src/utils/skills-installer.js
507
- */
508
-
509
- import { test, describe, before, after } from 'node:test';
510
- import assert from 'node:assert/strict';
511
- import { mkdtemp, rm, readdir } from 'node:fs/promises';
512
- import { join } from 'path';
513
- import { tmpdir } from 'os';
514
- import { fileURLToPath } from 'url';
515
- import { dirname } from 'path';
516
- import { installSkills } from '../../src/utils/skills-installer.js';
517
-
518
- const __dirname = dirname(fileURLToPath(import.meta.url));
519
- // skills-installer uses FRAMEWORK_SKILLS_DIR hardcoded from __dirname
520
- // So we can only test with the real framework directory
521
-
522
- describe('installSkills', () => {
523
- let tmpDir;
524
-
525
- before(async () => {
526
- tmpDir = await mkdtemp(join(tmpdir(), 'morph-skills-'));
527
- });
528
-
529
- after(async () => {
530
- await rm(tmpDir, { recursive: true, force: true });
531
- });
532
-
533
- test('creates .claude/skills/ directory', async () => {
534
- await installSkills(tmpDir);
535
- const files = await readdir(join(tmpDir, '.claude', 'skills'));
536
- assert.ok(files.length > 0, 'should have skill files installed');
537
- });
538
-
539
- test('installs level-0-meta skills', async () => {
540
- await installSkills(tmpDir);
541
- const files = await readdir(join(tmpDir, '.claude', 'skills'));
542
- // morph-checklist comes from level-0-meta
543
- assert.ok(files.some(f => f === 'morph-checklist.md'), 'morph-checklist.md should be installed');
544
- assert.ok(files.some(f => f === 'code-review.md'), 'code-review.md should be installed');
545
- });
546
-
547
- test('installs level-1-workflows skills', async () => {
548
- await installSkills(tmpDir);
549
- const files = await readdir(join(tmpDir, '.claude', 'skills'));
550
- assert.ok(files.some(f => f === 'phase-design.md'), 'phase-design.md should be installed');
551
- assert.ok(files.some(f => f === 'phase-implement.md'), 'phase-implement.md should be installed');
552
- });
553
-
554
- test('installs level-2-domains skills from subdirectories', async () => {
555
- await installSkills(tmpDir);
556
- const files = await readdir(join(tmpDir, '.claude', 'skills'));
557
- // These come from level-2-domains subdirectories
558
- assert.ok(files.some(f => f === 'blazor-builder.md'), 'blazor-builder.md should be installed');
559
- assert.ok(files.some(f => f === 'dotnet-senior.md'), 'dotnet-senior.md should be installed');
560
- assert.ok(files.some(f => f === 'azure-architect.md'), 'azure-architect.md should be installed');
561
- assert.ok(files.some(f => f === 'testing-specialist.md'), 'testing-specialist.md should be installed');
562
- });
563
-
564
- test('does not install README.md files', async () => {
565
- await installSkills(tmpDir);
566
- const files = await readdir(join(tmpDir, '.claude', 'skills'));
567
- assert.ok(!files.includes('README.md'), 'README.md should not be installed');
568
- });
569
-
570
- test('installs more than 20 skills total (level-0 + level-1 + level-2)', async () => {
571
- await installSkills(tmpDir);
572
- const files = await readdir(join(tmpDir, '.claude', 'skills'));
573
- // level-0: 6, level-1: 8, level-2: ~17 = ~31 total
574
- assert.ok(files.length > 20, `should install more than 20 skills, got ${files.length}`);
575
- });
576
-
577
- test('is idempotent — running twice produces same result', async () => {
578
- await installSkills(tmpDir);
579
- const firstCount = (await readdir(join(tmpDir, '.claude', 'skills'))).length;
580
- await installSkills(tmpDir);
581
- const secondCount = (await readdir(join(tmpDir, '.claude', 'skills'))).length;
582
- assert.strictEqual(firstCount, secondCount, 'file count should not change on second run');
583
- });
584
- });
585
- ```
586
-
587
- ### Step 2: Run tests to verify they fail
588
-
589
- ```bash
590
- npm test -- test/utils/skills-installer.test.js
591
- ```
592
-
593
- Expected: FAIL — `blazor-builder.md`, `dotnet-senior.md`, `azure-architect.md`, `testing-specialist.md` not found (level-2 not installed). Also fails on `> 20 skills` count.
594
-
595
- ### Step 3: Update `skills-installer.js`
596
-
597
- Replace the `SKILL_LEVELS_TO_INSTALL` constant and add a recursive walk helper:
598
-
599
- ```js
600
- /**
601
- * Skill levels to install as flat files in .claude/skills/.
602
- * Level-0 (meta), Level-1 (workflows), Level-2 (domain specialists).
603
- * Level-3 and Level-4 only contain README.md — no content to install.
604
- * For Level-2, subdirectories are walked recursively and files are installed flat.
605
- * Note: if two subdirs have a file with the same name, last-write wins.
606
- */
607
- const SKILL_LEVELS_TO_INSTALL = ['level-0-meta', 'level-1-workflows', 'level-2-domains'];
608
- ```
609
-
610
- Replace the install loop to handle subdirectories:
611
-
612
- ```js
613
- export async function installSkills(projectDir) {
614
- const claudeSkillsDir = join(projectDir, '.claude', 'skills');
615
- mkdirSync(claudeSkillsDir, { recursive: true });
616
-
617
- for (const level of SKILL_LEVELS_TO_INSTALL) {
618
- const levelDir = join(FRAMEWORK_SKILLS_DIR, level);
619
- if (!existsSync(levelDir)) continue;
620
-
621
- installSkillsFromDir(levelDir, claudeSkillsDir);
622
- }
623
- }
624
-
625
- /**
626
- * Recursively copy all .md skill files (excluding README.md) from srcDir
627
- * to destDir as flat files.
628
- * @param {string} srcDir
629
- * @param {string} destDir
630
- */
631
- function installSkillsFromDir(srcDir, destDir) {
632
- const entries = readdirSync(srcDir);
633
- for (const entry of entries) {
634
- const srcPath = join(srcDir, entry);
635
- const stat = statSync(srcPath);
636
-
637
- if (stat.isDirectory()) {
638
- // Recurse into subdirectory
639
- installSkillsFromDir(srcPath, destDir);
640
- } else if (entry.endsWith('.md') && entry !== 'README.md') {
641
- const destPath = join(destDir, basename(entry));
642
- copyFileSync(srcPath, destPath);
643
- }
644
- }
645
- }
646
- ```
647
-
648
- ### Step 4: Run tests to verify they pass
649
-
650
- ```bash
651
- npm test -- test/utils/skills-installer.test.js
652
- ```
653
-
654
- Expected: All 7 tests pass.
655
-
656
- ### Step 5: Run full suite
657
-
658
- ```bash
659
- npm test
660
- ```
661
-
662
- Expected: All existing tests pass, 7 new tests pass.
663
-
664
- ### Step 6: Commit
665
-
666
- ```bash
667
- git add src/utils/skills-installer.js test/utils/skills-installer.test.js
668
- git commit -m "feat(skills): install level-2 domain skills to .claude/skills/ with recursive walk"
669
- ```
670
-
671
- ---
672
-
673
- ## Task 5: Final verification
674
-
675
- ### Step 1: Confirm test counts
676
-
677
- ```bash
678
- npm run test:coverage:summary
679
- ```
680
-
681
- Expected: More tests than before Task 1 (was ~588). Verify no regressions.
682
-
683
- ### Step 2: Smoke test with `morph-spec init`
684
-
685
- ```bash
686
- node bin/morph-spec.js init --force --skip-mcp --skip-detection
687
- ```
688
-
689
- Verify:
690
- ```bash
691
- # Check agents have new frontmatter
692
- head -15 .claude/agents/morph-standards-architect.md
693
- # Expected: model:, tools:, maxTurns:, skills:, memory: all present
694
-
695
- # Check Stop hook type
696
- node -e "const s=JSON.parse(require('fs').readFileSync('.claude/settings.local.json')); console.log('Stop type:', s.hooks.Stop[0].hooks[0].type)"
697
- # Expected: Stop type: agent
698
-
699
- # Check SubagentStart hook exists
700
- node -e "const s=JSON.parse(require('fs').readFileSync('.claude/settings.local.json')); console.log('SubagentStart:', !!s.hooks.SubagentStart)"
701
- # Expected: SubagentStart: true
702
-
703
- # Check level-2 skills installed
704
- ls .claude/skills/ | grep -E "blazor|dotnet|azure" | head
705
- # Expected: blazor-builder.md, dotnet-senior.md, azure-architect.md etc.
706
-
707
- # Check total skill count
708
- ls .claude/skills/*.md | wc -l
709
- # Expected: > 20
710
- ```
711
-
712
- ### Step 3: Commit any final cleanup
713
-
714
- If any cleanup needed:
715
- ```bash
716
- git add <changed files>
717
- git commit -m "chore: native enrichment cleanup after smoke test"
718
- ```
719
-
720
- ---
721
-
722
- ## Summary of Files Changed
723
-
724
- | File | Change |
725
- |------|--------|
726
- | `src/utils/agents-installer.js` | Add `buildFrontmatter()` with tier-based model/tools/maxTurns/skills/memory |
727
- | `test/utils/agents-installer.test.js` | 6 new tests for richer frontmatter |
728
- | `src/utils/hooks-installer.js` | Stop→type:agent, SubagentStart hook, `_morph` marker, updated `removeMorphHooks()` |
729
- | `test/hooks/hooks-installer.test.js` | 4 new tests for Stop type, SubagentStart, `_morph` marker |
730
- | `framework/hooks/claude-code/subagent/log-agent-start.js` | **NEW** — fail-open agent activation logger |
731
- | `src/utils/skills-installer.js` | Add level-2 to install list, add recursive `installSkillsFromDir()` |
732
- | `test/utils/skills-installer.test.js` | **NEW** — 7 tests for skills installation |
733
-
734
- ---
735
-
736
- *Plan saved: 2026-02-22*
737
- *Design doc: `docs/plans/2026-02-22-native-enrichment-design.md`*