@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.
- package/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/hooks/dev/guard-version-numbers.js +1 -1
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
- package/package.json +4 -4
- package/.morph/analytics/threads-log.jsonl +0 -54
- package/.morph/state.json +0 -198
- package/docs/ARCHITECTURE.md +0 -328
- package/docs/COMMAND-FLOWS.md +0 -398
- package/docs/plans/2026-02-22-claude-docs-morph-alignment-analysis.md +0 -514
- package/docs/plans/2026-02-22-claude-settings.md +0 -517
- package/docs/plans/2026-02-22-morph-cc-alignment-impl.md +0 -730
- package/docs/plans/2026-02-22-morph-spec-next.md +0 -480
- package/docs/plans/2026-02-22-native-alignment-design.md +0 -201
- package/docs/plans/2026-02-22-native-alignment-impl.md +0 -927
- package/docs/plans/2026-02-22-native-enrichment-design.md +0 -246
- package/docs/plans/2026-02-22-native-enrichment.md +0 -737
- package/docs/plans/2026-02-23-ddd-architecture-refactor.md +0 -1155
- package/docs/plans/2026-02-23-ddd-nextsteps.md +0 -684
- package/docs/plans/2026-02-23-infra-architect-refactor.md +0 -439
- package/docs/plans/2026-02-23-nextjs-code-review-design.md +0 -157
- package/docs/plans/2026-02-23-nextjs-code-review-impl.md +0 -1256
- package/docs/plans/2026-02-23-nextjs-standards-design.md +0 -150
- package/docs/plans/2026-02-23-nextjs-standards-impl.md +0 -1848
- package/docs/plans/2026-02-24-cli-radical-simplification.md +0 -592
- package/docs/plans/2026-02-24-framework-failure-points.md +0 -125
- package/docs/plans/2026-02-24-morph-init-design.md +0 -337
- package/docs/plans/2026-02-24-morph-init-impl.md +0 -1269
- package/docs/plans/2026-02-24-tutorial-command-design.md +0 -71
- package/docs/plans/2026-02-24-tutorial-command.md +0 -298
- package/scripts/bump-version.js +0 -248
- package/scripts/generate-refs.js +0 -336
- package/scripts/generate-standards-registry.js +0 -44
- package/scripts/install-dev-hooks.js +0 -138
- package/scripts/scan-nextjs.mjs +0 -169
- 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`*
|