@polymorphism-tech/morph-spec 4.10.2 → 4.10.3
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/agents.json +4 -0
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +21 -1
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -0
- package/package.json +1 -1
- package/src/commands/project/doctor.js +26 -3
- package/src/commands/project/init.js +32 -2
- package/src/lib/detectors/index.js +52 -22
- package/src/lib/stack-filter.js +11 -4
- package/src/scripts/setup-infra.js +1 -1
- package/src/utils/agents-installer.js +90 -6
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
|
|
4
4
|
|
|
5
5
|
**Package:** `@polymorphism-tech/morph-spec`
|
|
6
|
-
**Version:** 4.10.
|
|
6
|
+
**Version:** 4.10.3
|
|
7
7
|
**Requires:** Node.js 18+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -376,4 +376,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
376
376
|
|
|
377
377
|
---
|
|
378
378
|
|
|
379
|
-
*morph-spec v4.10.
|
|
379
|
+
*morph-spec v4.10.3 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/agents.json
CHANGED
|
@@ -178,6 +178,7 @@
|
|
|
178
178
|
"tier": 2,
|
|
179
179
|
"role": "domain-leader",
|
|
180
180
|
"title": "Backend Squad Leader",
|
|
181
|
+
"stacks": ["dotnet", "blazor"],
|
|
181
182
|
"domains": [
|
|
182
183
|
"backend"
|
|
183
184
|
],
|
|
@@ -249,6 +250,7 @@
|
|
|
249
250
|
"tier": 2,
|
|
250
251
|
"role": "domain-leader",
|
|
251
252
|
"title": "Infrastructure Squad Leader",
|
|
253
|
+
"stacks": ["azure", "docker"],
|
|
252
254
|
"domains": [
|
|
253
255
|
"infrastructure"
|
|
254
256
|
],
|
|
@@ -2322,6 +2324,7 @@
|
|
|
2322
2324
|
"tier": 4,
|
|
2323
2325
|
"role": "validator",
|
|
2324
2326
|
"title": "Blazor Concurrency Validator",
|
|
2327
|
+
"stacks": ["blazor"],
|
|
2325
2328
|
"domains": [
|
|
2326
2329
|
"blazor-concurrency"
|
|
2327
2330
|
],
|
|
@@ -2428,6 +2431,7 @@
|
|
|
2428
2431
|
},
|
|
2429
2432
|
"nextjs-component-validator": {
|
|
2430
2433
|
"tier": 4,
|
|
2434
|
+
"stacks": ["nextjs"],
|
|
2431
2435
|
"title": "Next.js Component Validator",
|
|
2432
2436
|
"description": "Tier-4 validator for Next.js Server/Client component patterns",
|
|
2433
2437
|
"keywords": [
|
|
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 3 (Clarify). Iterative clarification loop driven b
|
|
|
4
4
|
argument-hint: "[feature-name]"
|
|
5
5
|
user-invocable: false
|
|
6
6
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
|
7
|
-
cliVersion: "4.10.
|
|
7
|
+
cliVersion: "4.10.3"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MORPH Clarify — Phase 3
|
|
@@ -3,7 +3,7 @@ name: morph:phase-codebase-analysis
|
|
|
3
3
|
description: MORPH-SPEC Design sub-phase that analyzes existing codebase and database schema, producing schema-analysis.md with real column names, types, relationships, and field mismatches. Use at the start of Design phase before generating contracts.cs to prevent incorrect field names or types.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
|
6
|
-
cliVersion: "4.10.
|
|
6
|
+
cliVersion: "4.10.3"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# MORPH Codebase Analysis - Design Sub-phase
|
|
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 2 (Design). Schema-first interactive design: reads
|
|
|
4
4
|
argument-hint: "[feature-name]"
|
|
5
5
|
user-invocable: false
|
|
6
6
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent
|
|
7
|
-
cliVersion: "4.10.
|
|
7
|
+
cliVersion: "4.10.3"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MORPH Design — Phase 2
|
|
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 1 (Setup). Reads project context, detects tech sta
|
|
|
4
4
|
argument-hint: "[feature-name]"
|
|
5
5
|
user-invocable: false
|
|
6
6
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
|
7
|
-
cliVersion: "4.10.
|
|
7
|
+
cliVersion: "4.10.3"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MORPH Setup — Phase 1
|
|
@@ -151,6 +151,26 @@ MCP Readiness:
|
|
|
151
151
|
|
|
152
152
|
---
|
|
153
153
|
|
|
154
|
+
### Step 2.9: Check for Missing Agents
|
|
155
|
+
|
|
156
|
+
Run a quick check to verify all expected agents are installed for this project's stack:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npx morph-spec doctor 2>&1 | grep -A 1 "agents completeness"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
If the output shows **missing agents**, inform the user:
|
|
163
|
+
|
|
164
|
+
> ⚠ Some agents for your stack are not installed. Run `npx morph-spec init --force` to install them.
|
|
165
|
+
|
|
166
|
+
Use `AskUserQuestion` with header `"Agents"` and options:
|
|
167
|
+
- **Install now** — run `npx morph-spec init --force` to reinstall with current stack tags
|
|
168
|
+
- **Continue without** — proceed with available agents (may lack specialized support)
|
|
169
|
+
|
|
170
|
+
If "Install now" is selected, run the command and continue.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
154
174
|
### Step 3: Confirm Stack
|
|
155
175
|
|
|
156
176
|
Based on the proposal and context, confirm:
|
|
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 1.5 (UI/UX). Creates design-system.md, mockups.md,
|
|
|
4
4
|
argument-hint: "[feature-name]"
|
|
5
5
|
user-invocable: false
|
|
6
6
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
|
7
|
-
cliVersion: "4.10.
|
|
7
|
+
cliVersion: "4.10.3"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MORPH UI/UX Design - Phase 1.5
|
package/package.json
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
getInstalledCLIVersion
|
|
13
13
|
} from '../../utils/version-checker.js';
|
|
14
14
|
import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
|
|
15
|
+
import { checkMissingAgents } from '../../utils/agents-installer.js';
|
|
16
|
+
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
15
17
|
|
|
16
18
|
const isWindows = platform() === 'win32';
|
|
17
19
|
/**
|
|
@@ -687,11 +689,32 @@ export async function doctorCommand(options = {}) {
|
|
|
687
689
|
}
|
|
688
690
|
|
|
689
691
|
// Check .claude/agents/
|
|
690
|
-
const
|
|
691
|
-
if (await pathExists(
|
|
692
|
-
const agentFiles = await fs.readdir(
|
|
692
|
+
const agentsPathClaude = join(claudePath, 'agents');
|
|
693
|
+
if (await pathExists(agentsPathClaude)) {
|
|
694
|
+
const agentFiles = await fs.readdir(agentsPathClaude);
|
|
693
695
|
const morphAgents = agentFiles.filter(f => f.startsWith('morph-') && f.endsWith('.md'));
|
|
694
696
|
checks.push({ name: '.claude/agents/', status: 'ok', msg: `${morphAgents.length} agents` });
|
|
697
|
+
|
|
698
|
+
// Check for missing agents based on stack tags
|
|
699
|
+
try {
|
|
700
|
+
const config = await readJson(configPath);
|
|
701
|
+
const projectTags = getProjectTags(config);
|
|
702
|
+
if (projectTags.length > 0) {
|
|
703
|
+
const frameworkDir = join(import.meta.dirname, '..', '..', '..', 'framework');
|
|
704
|
+
const { missing } = checkMissingAgents(targetPath, frameworkDir, projectTags);
|
|
705
|
+
if (missing.length > 0) {
|
|
706
|
+
const names = missing.map(m => m.name).join(', ');
|
|
707
|
+
checks.push({
|
|
708
|
+
name: 'agents completeness',
|
|
709
|
+
status: 'warn',
|
|
710
|
+
msg: `${missing.length} agent(s) missing for [${projectTags.join(', ')}]: ${names} — run morph-spec init --force`
|
|
711
|
+
});
|
|
712
|
+
hasWarnings = true;
|
|
713
|
+
} else {
|
|
714
|
+
checks.push({ name: 'agents completeness', status: 'ok', msg: `all agents for [${projectTags.join(', ')}]` });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch { /* config not readable — skip completeness check */ }
|
|
695
718
|
} else {
|
|
696
719
|
checks.push({ name: '.claude/agents/', status: 'warn', msg: 'missing — run morph-spec update' });
|
|
697
720
|
hasWarnings = true;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { join } from 'path';
|
|
1
|
+
import { join, dirname } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
2
3
|
import { homedir } from 'os';
|
|
3
4
|
import fs from 'fs-extra';
|
|
4
5
|
import ora from 'ora';
|
|
@@ -22,7 +23,10 @@ import {
|
|
|
22
23
|
generateSetupInstructions,
|
|
23
24
|
formatMcpStatusTable
|
|
24
25
|
} from '../../lib/installers/mcp-installer.js';
|
|
25
|
-
import { setupInfra } from '../../scripts/setup-infra.js';
|
|
26
|
+
import { setupInfra, installRulesForStack } from '../../scripts/setup-infra.js';
|
|
27
|
+
import { installSkills } from '../../utils/skills-installer.js';
|
|
28
|
+
import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
|
|
29
|
+
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
26
30
|
|
|
27
31
|
export async function initCommand(options) {
|
|
28
32
|
const targetPath = options.path || process.cwd();
|
|
@@ -147,6 +151,9 @@ export async function initCommand(options) {
|
|
|
147
151
|
if (structure?.uiLibrary) {
|
|
148
152
|
config.project.uiLibrary = structure.uiLibrary;
|
|
149
153
|
}
|
|
154
|
+
if (structure?.integrations?.length > 0) {
|
|
155
|
+
config.project.integrations = structure.integrations;
|
|
156
|
+
}
|
|
150
157
|
await writeJson(configPath, config);
|
|
151
158
|
|
|
152
159
|
spinner.stop();
|
|
@@ -155,6 +162,7 @@ export async function initCommand(options) {
|
|
|
155
162
|
logger.info(`Stack detected: ${structure.stack}`);
|
|
156
163
|
if (structure?.architecture) logger.dim(`Architecture: ${structure.architecture}`);
|
|
157
164
|
if (structure?.uiLibrary) logger.dim(`UI Library: ${structure.uiLibrary}`);
|
|
165
|
+
if (structure?.integrations?.length > 0) logger.dim(`Integrations: ${structure.integrations.join(', ')}`);
|
|
158
166
|
|
|
159
167
|
const { standardsChoice } = await inquirer.prompt([{
|
|
160
168
|
type: 'list',
|
|
@@ -175,6 +183,28 @@ export async function initCommand(options) {
|
|
|
175
183
|
logger.dim('Review and edit .morph/context/standards.md as needed.');
|
|
176
184
|
}
|
|
177
185
|
|
|
186
|
+
// Re-install stack-filtered assets now that we know the stack
|
|
187
|
+
const stackTags = getProjectTags(config);
|
|
188
|
+
if (stackTags.length > 0) {
|
|
189
|
+
spinner.start('Re-installing stack-filtered assets...');
|
|
190
|
+
const frameworkDir = join(targetPath, '.morph', 'framework');
|
|
191
|
+
const fwSrcDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'framework');
|
|
192
|
+
|
|
193
|
+
// Rules
|
|
194
|
+
const rulesSrc = join(fwSrcDir, 'rules');
|
|
195
|
+
const rulesDest = join(targetPath, '.claude', 'rules');
|
|
196
|
+
installRulesForStack(rulesSrc, rulesDest, stackTags);
|
|
197
|
+
|
|
198
|
+
// Skills
|
|
199
|
+
await installSkills(targetPath, { projectTags: stackTags });
|
|
200
|
+
|
|
201
|
+
// Agents (tier 1/2/4 from agents.json + domain agents)
|
|
202
|
+
await installAgents(targetPath, fwSrcDir, { projectTags: stackTags });
|
|
203
|
+
await installDomainAgents(targetPath, fwSrcDir, { projectTags: stackTags });
|
|
204
|
+
|
|
205
|
+
spinner.succeed(`Stack-filtered assets reinstalled for [${stackTags.join(', ')}]`);
|
|
206
|
+
}
|
|
207
|
+
|
|
178
208
|
spinner.start('Continuing...');
|
|
179
209
|
}
|
|
180
210
|
} catch (error) {
|
|
@@ -86,29 +86,39 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// ── .csproj detection ──────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} catch { /* ignore */ }
|
|
89
|
+
let hasDotnet = false;
|
|
90
|
+
let hasBlazer = false;
|
|
91
|
+
const csprojLocations = ['.', 'src', 'src/Api', 'src/Web', 'src/backend'];
|
|
92
|
+
for (const loc of csprojLocations) {
|
|
93
|
+
try {
|
|
94
|
+
const dir = join(targetPath, loc);
|
|
95
|
+
if (existsSync(dir)) {
|
|
96
|
+
const entries = readdirSyncSafe(dir);
|
|
97
|
+
for (const entry of entries.filter(e => e.endsWith('.csproj'))) {
|
|
98
|
+
hasDotnet = true;
|
|
99
|
+
try {
|
|
100
|
+
const content = readFileSync(join(dir, entry), 'utf8');
|
|
101
|
+
if (content.includes('Microsoft.AspNetCore.Components') || content.includes('Blazor')) {
|
|
102
|
+
hasBlazer = true;
|
|
103
|
+
}
|
|
104
|
+
if (content.includes('Hangfire')) {
|
|
105
|
+
if (!structure.integrations.includes('hangfire')) structure.integrations.push('hangfire');
|
|
107
106
|
}
|
|
108
|
-
|
|
109
|
-
}
|
|
107
|
+
} catch { /* ignore */ }
|
|
110
108
|
}
|
|
111
|
-
}
|
|
109
|
+
}
|
|
110
|
+
} catch { /* ignore */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Determine stack: monorepo (nextjs+dotnet), blazor, dotnet, or keep frontend stack
|
|
114
|
+
if (hasDotnet) {
|
|
115
|
+
if (structure.stack === 'nextjs' || structure.stack === 'react' || structure.stack === 'vue') {
|
|
116
|
+
// Monorepo: frontend + .NET backend
|
|
117
|
+
structure.stack = `${structure.stack}+dotnet`;
|
|
118
|
+
} else if (hasBlazer) {
|
|
119
|
+
structure.stack = 'blazor';
|
|
120
|
+
} else if (structure.stack === 'unknown') {
|
|
121
|
+
structure.stack = 'dotnet';
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
|
|
@@ -137,7 +147,27 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
137
147
|
if (existsSync(join(targetPath, 'docker-compose.yml')) ||
|
|
138
148
|
existsSync(join(targetPath, 'docker-compose.yaml')) ||
|
|
139
149
|
existsSync(join(targetPath, 'Dockerfile'))) {
|
|
140
|
-
structure.integrations.push('docker');
|
|
150
|
+
if (!structure.integrations.includes('docker')) structure.integrations.push('docker');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Azure detection ────────────────────────────────────────────────────
|
|
154
|
+
const azureSignals = [
|
|
155
|
+
'.azure',
|
|
156
|
+
'azure-pipelines.yml',
|
|
157
|
+
'infra/main.bicep',
|
|
158
|
+
'infra',
|
|
159
|
+
];
|
|
160
|
+
const hasAzure = azureSignals.some(signal => {
|
|
161
|
+
const p = join(targetPath, signal);
|
|
162
|
+
if (!existsSync(p)) return false;
|
|
163
|
+
if (signal === 'infra') {
|
|
164
|
+
// Check for .bicep files inside infra/
|
|
165
|
+
return readdirSyncSafe(p).some(f => f.endsWith('.bicep'));
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
});
|
|
169
|
+
if (hasAzure && !structure.integrations.includes('azure')) {
|
|
170
|
+
structure.integrations.push('azure');
|
|
141
171
|
}
|
|
142
172
|
} catch {
|
|
143
173
|
// Fail-safe — return defaults
|
package/src/lib/stack-filter.js
CHANGED
|
@@ -37,13 +37,20 @@ export function parseStacks(content) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Extract project
|
|
41
|
-
*
|
|
40
|
+
* Extract project tags from config object.
|
|
41
|
+
* Combines stack tags (from `project.stack` split by '-' or '+')
|
|
42
|
+
* with integration tags (from `project.integrations` array).
|
|
43
|
+
* Deduplicates the result.
|
|
42
44
|
*/
|
|
43
45
|
export function getProjectTags(config) {
|
|
44
46
|
const stack = config?.project?.stack;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
const stackTags = (stack && typeof stack === 'string')
|
|
48
|
+
? stack.split(/[-+]/).filter(Boolean)
|
|
49
|
+
: [];
|
|
50
|
+
const integrations = Array.isArray(config?.project?.integrations)
|
|
51
|
+
? config.project.integrations
|
|
52
|
+
: [];
|
|
53
|
+
return [...new Set([...stackTags, ...integrations])];
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
/**
|
|
@@ -212,7 +212,7 @@ export async function setupInfra(targetPath, { _exec = execSync } = {}) {
|
|
|
212
212
|
|
|
213
213
|
// --- 12. installAgents ---
|
|
214
214
|
log('Step 12: Installing agents...');
|
|
215
|
-
const agentCounts = await installAgents(targetPath, FRAMEWORK_DIR, {
|
|
215
|
+
const agentCounts = await installAgents(targetPath, FRAMEWORK_DIR, { projectTags });
|
|
216
216
|
|
|
217
217
|
// --- 13. installDomainAgents (filtered by stack) ---
|
|
218
218
|
log('Step 13: Installing domain agents...');
|
|
@@ -22,7 +22,7 @@ import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
|
|
|
22
22
|
* @param {string|null} options.projectStack - Detected project stack (e.g. 'nextjs', 'blazor')
|
|
23
23
|
*/
|
|
24
24
|
export async function installAgents(projectDir, frameworkDir = 'framework', options = {}) {
|
|
25
|
-
const {
|
|
25
|
+
const { projectTags = [] } = options;
|
|
26
26
|
|
|
27
27
|
const agentsJsonPath = join(frameworkDir, 'agents.json');
|
|
28
28
|
const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
|
|
@@ -38,21 +38,30 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
38
38
|
const targetDir = join(projectDir, '.claude', 'agents');
|
|
39
39
|
mkdirSync(targetDir, { recursive: true });
|
|
40
40
|
|
|
41
|
-
const DOTNET_STACKS = ['dotnet', 'blazor', 'dotnet-api', 'fullstack'];
|
|
42
41
|
const eligible = agents.filter(a => {
|
|
43
42
|
if (a.tier !== 1 && a.tier !== 2 && a.tier !== 4) return false;
|
|
44
43
|
// Tier 4 validators must have a teammate with spawn_prompt
|
|
45
44
|
if (a.tier === 4 && !a.teammate?.spawn_prompt) return false;
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
45
|
+
// Filter by stacks if the agent declares them
|
|
46
|
+
const stacks = a.stacks ?? [];
|
|
47
|
+
if (!shouldInstall(stacks, projectTags)) return false;
|
|
50
48
|
return true;
|
|
51
49
|
});
|
|
52
50
|
|
|
53
51
|
let tier1 = 0;
|
|
54
52
|
let tier2 = 0;
|
|
55
53
|
let tier4 = 0;
|
|
54
|
+
const installed = new Set();
|
|
55
|
+
|
|
56
|
+
// Build the full set of possible filenames for orphan cleanup
|
|
57
|
+
const allPossible = new Set();
|
|
58
|
+
for (const a of agents) {
|
|
59
|
+
if (a.tier !== 1 && a.tier !== 2 && a.tier !== 4) continue;
|
|
60
|
+
const slug = a.id ?? a.name?.toLowerCase().replace(/\s+/g, '-');
|
|
61
|
+
const prefix = a.tier === 4 ? 'morph-validator-' : 'morph-';
|
|
62
|
+
allPossible.add(`${prefix}${slug}.md`);
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
for (const agent of eligible) {
|
|
57
66
|
const slug = agent.id ?? agent.name?.toLowerCase().replace(/\s+/g, '-');
|
|
58
67
|
const prefix = agent.tier === 4 ? 'morph-validator-' : 'morph-';
|
|
@@ -65,11 +74,20 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
65
74
|
const frontmatter = buildFrontmatter(agent, description);
|
|
66
75
|
const content = `---\n${frontmatter}---\n\n${body}\n`;
|
|
67
76
|
writeFileSync(targetPath, content, 'utf-8');
|
|
77
|
+
installed.add(filename);
|
|
68
78
|
|
|
69
79
|
if (agent.tier === 1) tier1++;
|
|
70
80
|
else if (agent.tier === 2) tier2++;
|
|
71
81
|
else if (agent.tier === 4) tier4++;
|
|
72
82
|
}
|
|
83
|
+
|
|
84
|
+
// Cleanup orphan morph-* / morph-validator-* files that were filtered out
|
|
85
|
+
for (const file of readdirSync(targetDir)) {
|
|
86
|
+
if (allPossible.has(file) && !installed.has(file)) {
|
|
87
|
+
unlinkSync(join(targetDir, file));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
return { tier1, tier2, tier4 };
|
|
74
92
|
}
|
|
75
93
|
|
|
@@ -259,3 +277,69 @@ export async function installDomainAgents(projectDir, frameworkDir = 'framework'
|
|
|
259
277
|
|
|
260
278
|
return { specialists };
|
|
261
279
|
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check which agents SHOULD be installed but are NOT.
|
|
283
|
+
* Returns a list of missing agent names with their source (agents.json or domain).
|
|
284
|
+
*
|
|
285
|
+
* @param {string} projectDir - Target project directory
|
|
286
|
+
* @param {string} frameworkDir - Path to morph-spec framework/ directory
|
|
287
|
+
* @param {string[]} projectTags - Stack + integration tags
|
|
288
|
+
* @returns {{ missing: Array<{ name: string, stacks: string[], source: string }>, installed: number, expected: number }}
|
|
289
|
+
*/
|
|
290
|
+
export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
291
|
+
const targetDir = join(projectDir, '.claude', 'agents');
|
|
292
|
+
const installedFiles = new Set();
|
|
293
|
+
if (existsSync(targetDir)) {
|
|
294
|
+
for (const f of readdirSync(targetDir)) installedFiles.add(f);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const missing = [];
|
|
298
|
+
let expected = 0;
|
|
299
|
+
|
|
300
|
+
// Check tier 1/2/4 from agents.json
|
|
301
|
+
const agentsJsonPath = join(frameworkDir, 'agents.json');
|
|
302
|
+
if (existsSync(agentsJsonPath)) {
|
|
303
|
+
const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
|
|
304
|
+
const agentsRaw = agentsJson.agents ?? agentsJson;
|
|
305
|
+
const agents = Array.isArray(agentsRaw)
|
|
306
|
+
? agentsRaw
|
|
307
|
+
: Object.entries(agentsRaw)
|
|
308
|
+
.filter(([key]) => !key.startsWith('_comment'))
|
|
309
|
+
.map(([id, data]) => ({ id, ...data }));
|
|
310
|
+
|
|
311
|
+
for (const a of agents) {
|
|
312
|
+
if (a.tier !== 1 && a.tier !== 2 && a.tier !== 4) continue;
|
|
313
|
+
if (a.tier === 4 && !a.teammate?.spawn_prompt) continue;
|
|
314
|
+
const stacks = a.stacks ?? [];
|
|
315
|
+
if (!shouldInstall(stacks, projectTags)) continue;
|
|
316
|
+
|
|
317
|
+
expected++;
|
|
318
|
+
const slug = a.id ?? a.name?.toLowerCase().replace(/\s+/g, '-');
|
|
319
|
+
const prefix = a.tier === 4 ? 'morph-validator-' : 'morph-';
|
|
320
|
+
const filename = `${prefix}${slug}.md`;
|
|
321
|
+
if (!installedFiles.has(filename)) {
|
|
322
|
+
missing.push({ name: a.title ?? slug, stacks, source: `tier-${a.tier}` });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check domain agents
|
|
328
|
+
const domainsDir = join(frameworkDir, 'agents');
|
|
329
|
+
if (existsSync(domainsDir)) {
|
|
330
|
+
for (const filePath of collectSkillFiles(domainsDir)) {
|
|
331
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
332
|
+
const { name, stacks } = parseSkillFile(raw);
|
|
333
|
+
if (!name) continue;
|
|
334
|
+
if (!shouldInstall(stacks, projectTags)) continue;
|
|
335
|
+
|
|
336
|
+
expected++;
|
|
337
|
+
const filename = `morph-domain-${name}.md`;
|
|
338
|
+
if (!installedFiles.has(filename)) {
|
|
339
|
+
missing.push({ name, stacks, source: 'domain' });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { missing, installed: installedFiles.size, expected };
|
|
345
|
+
}
|