@polymorphism-tech/morph-spec 4.21.0 → 5.0.1

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 (213) hide show
  1. package/CLAUDE.md +57 -71
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +224 -491
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +55 -38
  8. package/framework/MORPH.md +244 -0
  9. package/framework/agents.json +127 -1183
  10. package/framework/commands/morph-apply.md +56 -240
  11. package/framework/commands/morph-archive.md +48 -85
  12. package/framework/commands/morph-preflight.md +7 -7
  13. package/framework/commands/morph-proposal.md +35 -191
  14. package/framework/commands/morph-status.md +19 -50
  15. package/framework/commands/morph-troubleshoot.md +2 -2
  16. package/framework/evals/rubrics/README.md +44 -0
  17. package/framework/evals/rubrics/architecture.md +46 -0
  18. package/framework/evals/rubrics/code-quality.md +50 -0
  19. package/framework/evals/rubrics/contracts.md +47 -0
  20. package/framework/evals/rubrics/test-coverage.md +50 -0
  21. package/framework/evals/scenarios/dotnet-crud.yaml +42 -0
  22. package/framework/evals/scenarios/nextjs-auth.yaml +38 -0
  23. package/framework/evals/suites/standard.yaml +5 -0
  24. package/framework/evals/trace-schema.md +67 -0
  25. package/framework/hooks/claude-code/core/README.md +3 -0
  26. package/framework/hooks/claude-code/core/denylist.json +14 -0
  27. package/framework/hooks/claude-code/{pre-tool-use → core}/enforce-phase-writes.js +5 -5
  28. package/framework/hooks/claude-code/core/post-edit-typecheck.js +69 -0
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +29 -0
  30. package/framework/hooks/claude-code/{pre-tool-use → core}/protect-spec-files.js +4 -6
  31. package/framework/hooks/claude-code/{stop → core}/validate-completion.js +13 -82
  32. package/framework/hooks/shared/phase-utils.js +20 -28
  33. package/framework/hooks/shared/state-reader.js +4 -5
  34. package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -210
  35. package/framework/phases.json +40 -114
  36. package/framework/rubrics/eval-rubric.json +66 -0
  37. package/framework/skills/README.md +39 -64
  38. package/framework/skills/core/README.md +3 -0
  39. package/framework/skills/core/morph-brainstorming/SKILL.md +102 -0
  40. package/framework/skills/core/morph-eval/SKILL.md +57 -0
  41. package/framework/skills/core/morph-implement/SKILL.md +83 -0
  42. package/framework/skills/core/morph-plan/SKILL.md +64 -0
  43. package/framework/skills/core/morph-review/SKILL.md +62 -0
  44. package/framework/skills/core/morph-stack-scan/SKILL.md +109 -0
  45. package/framework/skills/core/morph-standards/SKILL.md +49 -0
  46. package/framework/standards/STANDARDS.json +32 -0
  47. package/framework/standards/frontend/design-system/ai-image-generation.md +244 -0
  48. package/framework/standards/integration/mcp/mcp-tools.md +48 -0
  49. package/framework/templates/CLAUDE.md.core.guide.md +29 -0
  50. package/framework/templates/CLAUDE.md.core.template +22 -0
  51. package/framework/templates/REGISTRY.json +4 -4
  52. package/framework/workflows/configs/fast-track.json +5 -10
  53. package/framework/workflows/configs/full.json +4 -8
  54. package/framework/workflows/configs/quick.json +32 -0
  55. package/framework/workflows/configs/standard.json +57 -64
  56. package/package.json +7 -6
  57. package/src/commands/mcp/mcp-setup.js +58 -31
  58. package/src/commands/project/doctor.js +69 -70
  59. package/src/commands/project/init.js +26 -5
  60. package/src/commands/project/status.js +18 -19
  61. package/src/commands/state/approve.js +1 -1
  62. package/src/commands/validation/validate-feature.js +18 -112
  63. package/src/core/paths/output-schema.js +133 -151
  64. package/src/core/state/phase-state-machine.js +10 -24
  65. package/src/core/state/state-manager.js +17 -17
  66. package/src/core/workflows/workflow-detector.js +214 -31
  67. package/src/core/workflows/workflow-router.js +204 -0
  68. package/src/dashboard/app/pages/AgentMap.jsx +32 -33
  69. package/src/dashboard/app/pages/LiveState.jsx +1 -1
  70. package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
  71. package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
  72. package/src/dashboard/app/pages/Workflows.jsx +2 -2
  73. package/src/dashboard/scanner/agents-scanner.js +30 -35
  74. package/src/lib/agents/metrics-aggregator.js +1 -1
  75. package/src/lib/agents/output-collector.js +7 -8
  76. package/src/lib/context/context-builder.js +289 -0
  77. package/src/lib/detectors/claude-config-detector.js +19 -3
  78. package/src/lib/generators/context-generator.js +20 -42
  79. package/src/lib/generators/index.js +1 -0
  80. package/src/lib/generators/mandate-generator.js +512 -0
  81. package/src/lib/generators/metadata-extractor.js +1 -1
  82. package/src/lib/generators/recap-generator.js +3 -4
  83. package/src/lib/installers/mcp-installer.js +300 -92
  84. package/src/lib/phase-chain/phase-validator.js +36 -40
  85. package/src/lib/stack/stack-profile.js +74 -91
  86. package/src/lib/tasks/task-parser.js +1 -1
  87. package/src/lib/tasks/wave-resolver.js +353 -0
  88. package/src/lib/validators/checkpoint-persistence.js +2 -2
  89. package/src/lib/validators/content/cross-phase-validator.js +281 -0
  90. package/src/lib/validators/content/index.js +1 -0
  91. package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
  92. package/src/scripts/global-install.js +2 -27
  93. package/src/utils/agents-installer.js +224 -447
  94. package/src/utils/claude-settings-manager.js +57 -15
  95. package/src/utils/file-copier.js +1 -0
  96. package/src/utils/hooks-installer.js +15 -117
  97. package/src/utils/skills-installer.js +12 -18
  98. package/bin/detect-agents.js +0 -226
  99. package/bin/task-manager.js +0 -1036
  100. package/framework/agents/README.md +0 -13
  101. package/framework/agents/ai-agents/ai-system-architect.md +0 -194
  102. package/framework/agents/ai-agents/vector-search-expert.md +0 -247
  103. package/framework/agents/architecture/architecture-expert.md +0 -98
  104. package/framework/agents/architecture/morph-spec-architect.md +0 -66
  105. package/framework/agents/architecture/packages-validator.md +0 -89
  106. package/framework/agents/architecture/security-expert.md +0 -79
  107. package/framework/agents/architecture/seo-growth-hacker.md +0 -168
  108. package/framework/agents/architecture/standards-architect.md +0 -153
  109. package/framework/agents/backend/dotnet-senior.md +0 -163
  110. package/framework/agents/backend/ef-modeler.md +0 -119
  111. package/framework/agents/backend/hangfire-orchestrator.md +0 -133
  112. package/framework/agents/backend/ms-agent-expert.md +0 -55
  113. package/framework/agents/backend/neon-expert.md +0 -379
  114. package/framework/agents/frontend/css-specialist.md +0 -89
  115. package/framework/agents/frontend/design-system-validator.md +0 -100
  116. package/framework/agents/frontend/gsap-animator.md +0 -131
  117. package/framework/agents/frontend/nextjs-component-validator.md +0 -97
  118. package/framework/agents/frontend/nextjs-expert.md +0 -201
  119. package/framework/agents/frontend/ui-designer.md +0 -259
  120. package/framework/agents/infrastructure/infra-architect.md +0 -215
  121. package/framework/agents/integrations/asaas-financial.md +0 -138
  122. package/framework/agents/integrations/neon-auth.md +0 -151
  123. package/framework/agents/integrations/resend-email.md +0 -127
  124. package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
  125. package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
  126. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
  127. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
  128. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
  129. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
  130. package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
  131. package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
  132. package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
  133. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
  134. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
  135. package/framework/hooks/shared/compact-restore.js +0 -123
  136. package/framework/hooks/shared/dispatch-helpers.js +0 -147
  137. package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
  138. package/framework/hooks/shared/stale-task-reset.js +0 -57
  139. package/framework/hooks/shared/worktree-helpers.js +0 -75
  140. package/framework/skills/level-0-meta/README.md +0 -7
  141. package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
  142. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
  143. package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
  144. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
  145. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
  146. package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
  147. package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
  148. package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
  149. package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
  150. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
  151. package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
  152. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
  153. package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
  154. package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
  155. package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
  156. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -382
  157. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
  158. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
  159. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
  160. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
  161. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
  162. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
  163. package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
  164. package/framework/skills/level-1-workflows/README.md +0 -7
  165. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
  166. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
  167. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
  168. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
  169. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
  170. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  171. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -493
  172. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
  173. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -46
  174. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
  175. package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
  176. package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
  177. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
  178. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  179. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
  180. package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
  181. package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
  182. package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
  183. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -788
  184. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
  185. package/framework/squad-templates/backend-only.json +0 -47
  186. package/framework/squad-templates/frontend-only.json +0 -56
  187. package/framework/squad-templates/full-stack.json +0 -74
  188. package/src/commands/agents/agent-metrics.js +0 -174
  189. package/src/commands/agents/audit-dispatch.js +0 -82
  190. package/src/commands/agents/detect-agents.js +0 -275
  191. package/src/commands/agents/dispatch-agents.js +0 -461
  192. package/src/commands/agents/index.js +0 -13
  193. package/src/commands/agents/manage-agents.js +0 -352
  194. package/src/commands/agents/project-metrics.js +0 -74
  195. package/src/commands/phase/phase-reset.js +0 -74
  196. package/src/commands/scope/escalate.js +0 -217
  197. package/src/commands/state/advance-phase.js +0 -600
  198. package/src/commands/state/phase-runner.js +0 -215
  199. package/src/commands/state/state.js +0 -444
  200. package/src/commands/task/expand.js +0 -100
  201. package/src/commands/tasks/index.js +0 -4
  202. package/src/commands/tasks/task.js +0 -105
  203. package/src/commands/templates/index.js +0 -5
  204. package/src/commands/templates/template-render.js +0 -93
  205. package/src/commands/trust/trust.js +0 -118
  206. package/src/lib/agents/agents-manager.js +0 -349
  207. package/src/lib/build/build-runner.js +0 -99
  208. package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
  209. package/src/lib/contracts/sprint-contract-generator.js +0 -137
  210. package/src/lib/scope/impact-analyzer.js +0 -106
  211. package/src/lib/trust/trust-manager.js +0 -157
  212. package/src/utils/tier-helpers.js +0 -24
  213. /package/framework/hooks/claude-code/{post-tool-use → core}/loop-detect.js +0 -0
@@ -1,447 +1,224 @@
1
- /**
2
- * Agents Installer
3
- *
4
- * Converts MORPH-SPEC tier-1 and tier-2 agents from agents.json into
5
- * native Claude Code subagent markdown files in .claude/agents/.
6
- *
7
- * Each file has YAML frontmatter (name, description, model, tools,
8
- * maxTurns, skills, memory) followed by the agent's spawn prompt as the body.
9
- */
10
-
11
- import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
12
- import { join } from 'path';
13
- import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
14
-
15
- /**
16
- * Installs tier-1 and tier-2 morph agents as native Claude Code subagents
17
- * in .claude/agents/ with YAML frontmatter.
18
- *
19
- * @param {string} projectDir - Target project directory
20
- * @param {string} frameworkDir - Path to morph-spec framework/ directory
21
- * @param {object} options - Optional settings
22
- * @param {string|null} options.projectStack - Detected project stack (e.g. 'nextjs', 'blazor')
23
- */
24
- export async function installAgents(projectDir, frameworkDir = 'framework', options = {}) {
25
- const { projectTags = [] } = options;
26
-
27
- const agentsJsonPath = join(frameworkDir, 'agents.json');
28
- const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
29
-
30
- // Support both object map and array formats
31
- const agentsRaw = agentsJson.agents ?? agentsJson;
32
- const agents = Array.isArray(agentsRaw)
33
- ? agentsRaw
34
- : Object.entries(agentsRaw)
35
- .filter(([key]) => !key.startsWith('_comment'))
36
- .map(([id, data]) => ({ id, ...data }));
37
-
38
- const targetDir = join(projectDir, '.claude', 'agents');
39
- mkdirSync(targetDir, { recursive: true });
40
-
41
- const eligible = agents.filter(a => {
42
- if (a.tier < 1 || a.tier > 4) return false;
43
- // Must have a teammate with spawn_prompt (except tier-1 orchestrators which always have one)
44
- if (a.tier >= 3 && !a.teammate?.spawn_prompt) return false;
45
- // Filter by stacks if the agent declares them
46
- const stacks = a.stacks ?? [];
47
- if (!shouldInstall(stacks, projectTags)) return false;
48
- return true;
49
- });
50
-
51
- let tier1 = 0;
52
- let tier2 = 0;
53
- let tier3 = 0;
54
- let tier4 = 0;
55
- const installed = new Set();
56
-
57
- // Build the full set of possible filenames for orphan cleanup
58
- const allPossible = new Set();
59
- for (const a of agents) {
60
- if (a.tier < 1 || a.tier > 4) continue;
61
- const slug = a.id ?? a.name?.toLowerCase().replace(/\s+/g, '-');
62
- const prefix = a.tier === 4 ? 'morph-validator-' : 'morph-';
63
- allPossible.add(`${prefix}${slug}.md`);
64
- }
65
-
66
- for (const agent of eligible) {
67
- const slug = agent.id ?? agent.name?.toLowerCase().replace(/\s+/g, '-');
68
- const prefix = agent.tier === 4 ? 'morph-validator-' : 'morph-';
69
- const filename = `${prefix}${slug}.md`;
70
- const targetPath = join(targetDir, filename);
71
-
72
- const description = buildDescription(agent);
73
- const body = buildBody(agent);
74
-
75
- const frontmatter = buildFrontmatter(agent, description);
76
- const content = `---\n${frontmatter}---\n\n${body}\n`;
77
- writeFileSync(targetPath, content, 'utf-8');
78
- installed.add(filename);
79
-
80
- if (agent.tier === 1) tier1++;
81
- else if (agent.tier === 2) tier2++;
82
- else if (agent.tier === 3) tier3++;
83
- else if (agent.tier === 4) tier4++;
84
- }
85
-
86
- // Cleanup orphan morph-* / morph-validator-* files that were filtered out
87
- for (const file of readdirSync(targetDir)) {
88
- if (allPossible.has(file) && !installed.has(file)) {
89
- unlinkSync(join(targetDir, file));
90
- }
91
- // Remove morph-domain-{name}.md when morph-{name}.md was installed from agents.json
92
- if (file.startsWith('morph-domain-')) {
93
- const suffix = file.slice('morph-domain-'.length);
94
- if (installed.has(`morph-${suffix}`)) {
95
- unlinkSync(join(targetDir, file));
96
- }
97
- }
98
- }
99
-
100
- return { tier1, tier2, tier3, tier4 };
101
- }
102
-
103
- /**
104
- * Tier-based defaults for Claude Code subagent frontmatter fields.
105
- *
106
- * Tier 1 (Orchestrators): opus model, can spawn subagents via Agent tool, 30 turns, project memory, high effort.
107
- * Tier 2 (Domain Leaders): sonnet model, coordinate specialists, 20 turns, local memory, medium effort.
108
- * Tier 3 (Specialists): sonnet model, implementation focus, 15 turns, local memory, medium effort.
109
- * Tier 4 (Validators): haiku model, read-only scanning, 10 turns, background execution, low effort.
110
- */
111
- const TIER_DEFAULTS = {
112
- 1: {
113
- model: 'opus',
114
- tools: 'Read, Grep, Glob, Bash, Agent, Write, Edit',
115
- maxTurns: 30,
116
- skills: ['morph-checklist'],
117
- memory: 'project',
118
- effort: 'high',
119
- permissionMode: 'acceptEdits',
120
- background: false,
121
- },
122
- 2: {
123
- model: 'sonnet',
124
- tools: 'Read, Write, Edit, Bash, Glob, Grep',
125
- maxTurns: 20,
126
- skills: ['morph-checklist'],
127
- memory: 'local',
128
- effort: 'medium',
129
- permissionMode: 'acceptEdits',
130
- background: false,
131
- },
132
- 3: {
133
- model: 'sonnet',
134
- tools: 'Read, Write, Edit, Bash, Glob, Grep',
135
- maxTurns: 15,
136
- skills: [],
137
- memory: 'local',
138
- effort: 'medium',
139
- permissionMode: 'default',
140
- background: false,
141
- },
142
- 4: {
143
- model: 'haiku',
144
- tools: 'Read, Glob, Grep',
145
- maxTurns: 10,
146
- skills: [],
147
- memory: 'project',
148
- effort: 'low',
149
- permissionMode: 'plan',
150
- background: true,
151
- },
152
- };
153
-
154
- /**
155
- * Builds the YAML frontmatter content (without --- delimiters) for an agent file.
156
- * Returns a string ending with a blank line.
157
- *
158
- * @param {object} agent - Agent definition from agents.json
159
- * @param {string} description - Pre-built description string
160
- * @returns {string}
161
- */
162
- function buildFrontmatter(agent, description) {
163
- const defaults = TIER_DEFAULTS[agent.tier] ?? TIER_DEFAULTS[2];
164
- const name = agent.title ?? agent.name;
165
-
166
- // Allow per-agent overrides from teammate config
167
- const tools = agent.teammate?.tools ?? defaults.tools;
168
- const maxTurns = agent.teammate?.maxTurns ?? defaults.maxTurns;
169
- const effort = agent.teammate?.effort ?? defaults.effort;
170
- const permissionMode = agent.teammate?.permissionMode ?? defaults.permissionMode;
171
- const background = agent.teammate?.background ?? defaults.background;
172
- // color is a top-level field on the agent (not inside teammate)
173
- const color = agent.color;
174
-
175
- const lines = [
176
- `name: ${name}`,
177
- `description: ${description}`,
178
- `model: ${defaults.model}`,
179
- `tools: ${tools}`,
180
- `maxTurns: ${maxTurns}`,
181
- ];
182
-
183
- if (color) lines.push(`color: ${color}`);
184
- if (effort) lines.push(`effort: ${effort}`);
185
- if (permissionMode && permissionMode !== 'default') lines.push(`permissionMode: ${permissionMode}`);
186
- if (background) lines.push(`background: true`);
187
-
188
- if (defaults.skills.length > 0) {
189
- const skillsList = defaults.skills.map(s => ` - ${s}`).join('\n');
190
- lines.push(`skills:`, skillsList);
191
- }
192
-
193
- lines.push(`memory: ${defaults.memory}`, '');
194
- return lines.join('\n');
195
- }
196
-
197
- /** Maps agent group to task categories for routing hints in description */
198
- const GROUP_TO_CATEGORIES = {
199
- 'backend': 'Domain, Infrastructure, Application',
200
- 'frontend': 'Presentation',
201
- 'domain-analysis': 'Architecture, Cross-cutting',
202
- 'testing': 'Tests',
203
- 'infra': 'Infra, DevOps',
204
- 'ai': 'AI, Automation',
205
- };
206
-
207
- function buildDescription(agent) {
208
- const role = agent.teammate?.role ?? `Handles ${(agent.domains ?? []).join(', ')}`;
209
- const triggers = agent.teammate?.triggers;
210
- const categories = GROUP_TO_CATEGORIES[agent.group];
211
-
212
- let desc = role;
213
- if (triggers) {
214
- desc += `. Use proactively when: ${triggers}`;
215
- } else if (categories) {
216
- desc += ` | Tasks: ${categories}`;
217
- }
218
-
219
- // Truncate to 200 chars for settings display
220
- return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
221
- }
222
-
223
- /**
224
- * Serialize an AgentStandard to a readable markdown line.
225
- *
226
- * @param {string|{id: string, scope?: string, priority?: string}} standard
227
- * @returns {string} e.g., "- core-coding (all, required)" or "- core-coding"
228
- */
229
- export function formatStandardRef(standard) {
230
- if (typeof standard === 'string') return `- ${standard}`;
231
- const id = standard.id || String(standard);
232
- const parts = [standard.scope, standard.priority].filter(Boolean);
233
- return parts.length > 0 ? `- ${id} (${parts.join(', ')})` : `- ${id}`;
234
- }
235
-
236
- function buildBody(agent) {
237
- const lines = [];
238
- if (agent.teammate?.spawn_prompt) {
239
- lines.push(agent.teammate.spawn_prompt);
240
- } else {
241
- lines.push(`You are the ${agent.title} agent in the MORPH-SPEC framework.`);
242
- if (agent.domains?.length) {
243
- lines.push(`\n## Domains\n${agent.domains.map(d => `- ${d}`).join('\n')}`);
244
- }
245
- }
246
-
247
- // Task routing — tells the agent which task categories it handles
248
- const categories = GROUP_TO_CATEGORIES[agent.group];
249
- if (categories) {
250
- lines.push(`\n## Task Routing\nHandle tasks categorized as: ${categories}`);
251
- }
252
-
253
- // Key standards — @ references for upfront awareness
254
- if (agent.standards?.length) {
255
- lines.push(`\n## Key Standards\n${agent.standards.map(formatStandardRef).join('\n')}`);
256
- }
257
- return lines.join('\n');
258
- }
259
-
260
- /**
261
- * Parses YAML frontmatter from a skill markdown file.
262
- * Handles both inline values and YAML block scalars (>).
263
- *
264
- * @param {string} rawContent - Raw file content
265
- * @returns {{ name: string|null, description: string|null, allowedTools: string, color: string|null, effort: string|null, permissionMode: string|null, background: boolean, stacks: string[], body: string }}
266
- */
267
- export function parseSkillFile(rawContent) {
268
- // Normalize CRLF to LF for consistent parsing on Windows
269
- const content = rawContent.replace(/\r\n/g, '\n');
270
- const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
271
- if (!match) return { name: null, description: null, allowedTools: 'Read, Write, Edit, Bash, Glob, Grep', color: null, effort: null, permissionMode: null, background: false, body: rawContent };
272
-
273
- const fm = match[1];
274
- const body = match[2];
275
-
276
- const name = (fm.match(/^name:\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
277
- const allowedTools = (fm.match(/^allowed-tools:\s*(.+)$/m) ?? [])[1]?.trim() ?? 'Read, Write, Edit, Bash, Glob, Grep';
278
- const color = (fm.match(/^color:\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
279
- const effort = (fm.match(/^effort:\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
280
- const permissionMode = (fm.match(/^permissionMode:\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
281
- const background = (fm.match(/^background:\s*(.+)$/m) ?? [])[1]?.trim() === 'true';
282
-
283
- let description = null;
284
- const blockMatch = fm.match(/^description:\s*>\s*\n((?:[ \t]+.*\n?)*)/m);
285
- if (blockMatch) {
286
- description = blockMatch[1].trim().replace(/\s+/g, ' ');
287
- } else {
288
- const inlineMatch = fm.match(/^description:\s*(.+)$/m);
289
- if (inlineMatch) description = inlineMatch[1].trim();
290
- }
291
-
292
- const stacks = parseStacks(rawContent);
293
- return { name, description, allowedTools, color, effort, permissionMode, background, stacks, body };
294
- }
295
-
296
- /**
297
- * Recursively collects all .md skill files from a directory, excluding README.md.
298
- *
299
- * @param {string} dir - Directory to walk
300
- * @returns {string[]} Absolute paths to skill .md files
301
- */
302
- export function collectSkillFiles(dir) {
303
- const results = [];
304
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
305
- const fullPath = join(dir, entry.name);
306
- if (entry.isDirectory()) {
307
- results.push(...collectSkillFiles(fullPath));
308
- } else if (entry.name.endsWith('.md') && entry.name !== 'README.md') {
309
- results.push(fullPath);
310
- }
311
- }
312
- return results;
313
- }
314
-
315
- /**
316
- * Installs domain agent files as native Claude Code subagents
317
- * in .claude/agents/ with memory:project, model:sonnet, maxTurns:20.
318
- * Supports optional CC-native fields (color, effort, permissionMode, background)
319
- * declared in each domain file's YAML frontmatter.
320
- *
321
- * @param {string} projectDir - Target project directory
322
- * @param {string} frameworkDir - Path to morph-spec framework/ directory
323
- */
324
- export async function installDomainAgents(projectDir, frameworkDir = 'framework', { projectTags = [] } = {}) {
325
- const domainsDir = join(frameworkDir, 'agents');
326
- if (!existsSync(domainsDir)) return { specialists: 0 };
327
-
328
- const targetDir = join(projectDir, '.claude', 'agents');
329
- mkdirSync(targetDir, { recursive: true });
330
-
331
- let specialists = 0;
332
- const installed = new Set();
333
- for (const filePath of collectSkillFiles(domainsDir)) {
334
- const raw = readFileSync(filePath, 'utf-8');
335
- const { name, description, allowedTools, color, effort, permissionMode, background, stacks, body } = parseSkillFile(raw);
336
- if (!name) continue;
337
- if (!shouldInstall(stacks, projectTags)) continue;
338
-
339
- const desc = description ?? `MORPH-SPEC domain agent: ${name}`;
340
- const lines = [
341
- `name: ${name}`,
342
- `description: ${desc}`,
343
- `model: sonnet`,
344
- `tools: ${allowedTools}`,
345
- `maxTurns: 20`,
346
- ];
347
- if (color) lines.push(`color: ${color}`);
348
- if (effort) lines.push(`effort: ${effort}`);
349
- if (permissionMode && permissionMode !== 'default') lines.push(`permissionMode: ${permissionMode}`);
350
- if (background) lines.push(`background: true`);
351
- lines.push(`memory: project`, '');
352
- const frontmatter = lines.join('\n');
353
-
354
- // If agents.json already generated morph-{name}.md, append domain content there
355
- const agentsJsonFile = `morph-${name}.md`;
356
- const agentsJsonPath = join(targetDir, agentsJsonFile);
357
- if (existsSync(agentsJsonPath)) {
358
- appendFileSync(agentsJsonPath, `\n\n---\n\n## Domain Knowledge\n\n${body.trimStart()}`, 'utf-8');
359
- specialists++;
360
- } else {
361
- const filename = `morph-domain-${name}.md`;
362
- const targetPath = join(targetDir, filename);
363
- writeFileSync(targetPath, `---\n${frontmatter}---\n\n${body.trimStart()}`, 'utf-8');
364
- installed.add(filename);
365
- specialists++;
366
- }
367
- }
368
-
369
- // Cleanup orphan morph-domain-* files
370
- for (const file of readdirSync(targetDir)) {
371
- if (file.startsWith('morph-domain-') && !installed.has(file)) {
372
- unlinkSync(join(targetDir, file));
373
- }
374
- }
375
-
376
- return { specialists };
377
- }
378
-
379
- /**
380
- * Check which agents SHOULD be installed but are NOT.
381
- * Returns a list of missing agent names with their source (agents.json or domain).
382
- *
383
- * @param {string} projectDir - Target project directory
384
- * @param {string} frameworkDir - Path to morph-spec framework/ directory
385
- * @param {string[]} projectTags - Stack + integration tags
386
- * @returns {{ missing: Array<{ name: string, stacks: string[], source: string }>, installed: number, expected: number }}
387
- */
388
- export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
389
- const targetDir = join(projectDir, '.claude', 'agents');
390
- const installedFiles = new Set();
391
- if (existsSync(targetDir)) {
392
- for (const f of readdirSync(targetDir)) installedFiles.add(f);
393
- }
394
-
395
- const missing = [];
396
- let expected = 0;
397
-
398
- // Check tier 1/2/4 from agents.json
399
- const agentsJsonPath = join(frameworkDir, 'agents.json');
400
- if (existsSync(agentsJsonPath)) {
401
- const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
402
- const agentsRaw = agentsJson.agents ?? agentsJson;
403
- const agents = Array.isArray(agentsRaw)
404
- ? agentsRaw
405
- : Object.entries(agentsRaw)
406
- .filter(([key]) => !key.startsWith('_comment'))
407
- .map(([id, data]) => ({ id, ...data }));
408
-
409
- for (const a of agents) {
410
- if (a.tier < 1 || a.tier > 4) continue;
411
- if (a.tier >= 3 && !a.teammate?.spawn_prompt) continue;
412
- const stacks = a.stacks ?? [];
413
- if (!shouldInstall(stacks, projectTags)) continue;
414
-
415
- expected++;
416
- const slug = a.id ?? a.name?.toLowerCase().replace(/\s+/g, '-');
417
- const prefix = a.tier === 4 ? 'morph-validator-' : 'morph-';
418
- const filename = `${prefix}${slug}.md`;
419
- if (!installedFiles.has(filename)) {
420
- missing.push({ name: a.title ?? slug, stacks, source: `tier-${a.tier}` });
421
- }
422
- }
423
- }
424
-
425
- // Check domain agents (skip if merged into agents.json entry)
426
- const domainsDir = join(frameworkDir, 'agents');
427
- if (existsSync(domainsDir)) {
428
- for (const filePath of collectSkillFiles(domainsDir)) {
429
- const raw = readFileSync(filePath, 'utf-8');
430
- const { name, stacks } = parseSkillFile(raw);
431
- if (!name) continue;
432
- if (!shouldInstall(stacks, projectTags)) continue;
433
-
434
- // If agents.json has morph-{name}.md, domain content is merged there
435
- const agentsJsonFile = `morph-${name}.md`;
436
- if (installedFiles.has(agentsJsonFile)) continue;
437
-
438
- expected++;
439
- const filename = `morph-domain-${name}.md`;
440
- if (!installedFiles.has(filename)) {
441
- missing.push({ name, stacks, source: 'domain' });
442
- }
443
- }
444
- }
445
-
446
- return { missing, installed: installedFiles.size, expected };
447
- }
1
+ /**
2
+ * Agents Installer (v5)
3
+ *
4
+ * Installs morph-spec domain personas as native Claude Code subagents
5
+ * in .claude/agents/morph-{id}.md.
6
+ *
7
+ * In v5 the harness has 7 domain personas (no tiers) — each persona is a
8
+ * subagent the Specialist (Claude in implementer role) can dispatch when
9
+ * working in that domain. The 3 roles (Orchestrator/Specialist/Evaluator)
10
+ * are assumed by Claude itself per MORPH.md, not as separate agents.
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'node:fs';
14
+ import { join } from 'path';
15
+ import { shouldInstall } from '../lib/stack-filter.js';
16
+
17
+ /**
18
+ * Install morph-spec domain personas as native Claude Code subagents.
19
+ *
20
+ * @param {string} projectDir - Target project directory
21
+ * @param {string} frameworkDir - Path to morph-spec framework/ directory
22
+ * @param {object} options
23
+ * @param {string[]} [options.projectTags=[]] - Stack tags from config (e.g. ['dotnet', 'nextjs'])
24
+ * @returns {Promise<{ installed: number }>}
25
+ */
26
+ export async function installAgents(projectDir, frameworkDir = 'framework', options = {}) {
27
+ const { projectTags = [] } = options;
28
+
29
+ const agentsJsonPath = join(frameworkDir, 'agents.json');
30
+ const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
31
+
32
+ const agentsRaw = agentsJson.agents ?? {};
33
+ const agents = Object.entries(agentsRaw)
34
+ .filter(([key]) => !key.startsWith('_comment'))
35
+ .map(([id, data]) => ({ id, ...data }));
36
+
37
+ const targetDir = join(projectDir, '.claude', 'agents');
38
+ mkdirSync(targetDir, { recursive: true });
39
+
40
+ // Filter by stack
41
+ const eligible = agents.filter(a => {
42
+ const stack = a.stack ?? ['*'];
43
+ return shouldInstall(stack, projectTags);
44
+ });
45
+
46
+ // Build set of all possible filenames for orphan cleanup
47
+ const allPossible = new Set(agents.map(a => `morph-${a.id}.md`));
48
+
49
+ let installed = 0;
50
+ const installedFilenames = new Set();
51
+
52
+ for (const agent of eligible) {
53
+ const filename = `morph-${agent.id}.md`;
54
+ const targetPath = join(targetDir, filename);
55
+
56
+ const description = buildDescription(agent);
57
+ const body = buildBody(agent);
58
+ const frontmatter = buildFrontmatter(agent, description);
59
+
60
+ writeFileSync(targetPath, `---\n${frontmatter}---\n\n${body}\n`, 'utf-8');
61
+ installedFilenames.add(filename);
62
+ installed++;
63
+ }
64
+
65
+ // Cleanup orphan morph-* files filtered out by stack
66
+ for (const file of readdirSync(targetDir)) {
67
+ if (allPossible.has(file) && !installedFilenames.has(file)) {
68
+ unlinkSync(join(targetDir, file));
69
+ }
70
+ }
71
+
72
+ return { installed };
73
+ }
74
+
75
+ function buildFrontmatter(agent, description) {
76
+ const lines = [
77
+ `name: ${agent.id}`,
78
+ `description: ${description}`,
79
+ `model: sonnet`,
80
+ `tools: Read, Write, Edit, Bash, Glob, Grep`,
81
+ `maxTurns: 20`,
82
+ ];
83
+ if (agent.color) lines.push(`color: ${agent.color}`);
84
+ lines.push(`memory: project`, '');
85
+ return lines.join('\n');
86
+ }
87
+
88
+ function buildDescription(agent) {
89
+ const role = agent.title ?? agent.id;
90
+ const domains = (agent.domains ?? []).join(', ');
91
+ const desc = domains ? `${role} — domains: ${domains}` : role;
92
+ return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
93
+ }
94
+
95
+ function buildBody(agent) {
96
+ const lines = [
97
+ `# ${agent.title ?? agent.id}`,
98
+ '',
99
+ agent.persona ?? `You are the ${agent.title} persona for morph-spec.`,
100
+ '',
101
+ ];
102
+
103
+ if (agent.domains?.length) {
104
+ lines.push('## Domains', ...agent.domains.map(d => `- ${d}`), '');
105
+ }
106
+
107
+ if (agent.standards?.length) {
108
+ lines.push('## Standards to consult on demand');
109
+ for (const s of agent.standards) lines.push(`- ${s}`);
110
+ lines.push('');
111
+ }
112
+
113
+ lines.push(
114
+ '## How you operate',
115
+ '',
116
+ 'You are a domain persona dispatched per task. Read the spec/mandate/tasks/standards passed to you, do the work, and return a concise summary plus an auto-score 0-10. Refer to MORPH.md section 6 for the dispatch contract.',
117
+ );
118
+
119
+ return lines.join('\n');
120
+ }
121
+
122
+ /**
123
+ * Compatibility shim for older callers that expected installDomainAgents.
124
+ * In v5 there is no separate "domain" install step — installAgents handles
125
+ * everything from agents.json.
126
+ */
127
+ export async function installDomainAgents(projectDir, frameworkDir, options) {
128
+ // No-op: domain content is now defined inline in agents.json (persona field).
129
+ return { specialists: 0 };
130
+ }
131
+
132
+ /**
133
+ * Check which agents SHOULD be installed but are NOT.
134
+ *
135
+ * @param {string} projectDir
136
+ * @param {string} frameworkDir
137
+ * @param {string[]} projectTags
138
+ * @returns {{ missing: Array<{ name: string, stacks: string[] }>, installed: number, expected: number }}
139
+ */
140
+ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
141
+ const targetDir = join(projectDir, '.claude', 'agents');
142
+ const installedFiles = new Set();
143
+ if (existsSync(targetDir)) {
144
+ for (const f of readdirSync(targetDir)) installedFiles.add(f);
145
+ }
146
+
147
+ const agentsJsonPath = join(frameworkDir, 'agents.json');
148
+ if (!existsSync(agentsJsonPath)) {
149
+ return { missing: [], installed: installedFiles.size, expected: 0 };
150
+ }
151
+
152
+ const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
153
+ const agentsRaw = agentsJson.agents ?? {};
154
+ const agents = Object.entries(agentsRaw)
155
+ .filter(([key]) => !key.startsWith('_comment'))
156
+ .map(([id, data]) => ({ id, ...data }));
157
+
158
+ const missing = [];
159
+ let expected = 0;
160
+
161
+ for (const a of agents) {
162
+ const stack = a.stack ?? ['*'];
163
+ if (!shouldInstall(stack, projectTags)) continue;
164
+ expected++;
165
+ const filename = `morph-${a.id}.md`;
166
+ if (!installedFiles.has(filename)) {
167
+ missing.push({ name: a.title ?? a.id, stacks: stack });
168
+ }
169
+ }
170
+
171
+ return { missing, installed: installedFiles.size, expected };
172
+ }
173
+
174
+ /**
175
+ * Compatibility export — kept so existing tests/CLI code that imports
176
+ * formatStandardRef or parseSkillFile / collectSkillFiles still work,
177
+ * but they are no-ops (will be removed in v5.1).
178
+ */
179
+ export function formatStandardRef(standard) {
180
+ if (typeof standard === 'string') return `- ${standard}`;
181
+ const id = standard.id || String(standard);
182
+ const parts = [standard.scope, standard.priority].filter(Boolean);
183
+ return parts.length > 0 ? `- ${id} (${parts.join(', ')})` : `- ${id}`;
184
+ }
185
+
186
+ export function parseSkillFile(rawContent) {
187
+ const content = rawContent.replace(/\r\n/g, '\n');
188
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
189
+ if (!match) {
190
+ return {
191
+ name: null, description: null, allowedTools: 'Read, Write, Edit, Bash, Glob, Grep',
192
+ color: null, effort: null, permissionMode: null, background: false, stacks: [],
193
+ body: rawContent,
194
+ };
195
+ }
196
+ const fm = match[1];
197
+ const body = match[2];
198
+ const name = (fm.match(/^name:\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
199
+ const allowedTools = (fm.match(/^allowed-tools:\s*(.+)$/m) ?? [])[1]?.trim() ?? 'Read, Write, Edit, Bash, Glob, Grep';
200
+ const color = (fm.match(/^color:\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
201
+ let description = null;
202
+ const blockMatch = fm.match(/^description:\s*>\s*\n((?:[ \t]+.*\n?)*)/m);
203
+ if (blockMatch) {
204
+ description = blockMatch[1].trim().replace(/\s+/g, ' ');
205
+ } else {
206
+ const inlineMatch = fm.match(/^description:\s*(.+)$/m);
207
+ if (inlineMatch) description = inlineMatch[1].trim();
208
+ }
209
+ return { name, description, allowedTools, color, effort: null, permissionMode: null, background: false, stacks: [], body };
210
+ }
211
+
212
+ export function collectSkillFiles(dir) {
213
+ const results = [];
214
+ if (!existsSync(dir)) return results;
215
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
216
+ const fullPath = join(dir, entry.name);
217
+ if (entry.isDirectory()) {
218
+ results.push(...collectSkillFiles(fullPath));
219
+ } else if (entry.name.endsWith('.md') && entry.name !== 'README.md') {
220
+ results.push(fullPath);
221
+ }
222
+ }
223
+ return results;
224
+ }