@polymorphism-tech/morph-spec 4.10.6 → 4.10.8
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/CLAUDE.md +3 -0
- package/LICENSE +71 -71
- package/README.md +379 -379
- package/bin/morph-spec.js +89 -0
- package/bin/task-manager.js +59 -6
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +206 -206
- package/framework/agents/README.md +14 -14
- package/framework/agents/architecture/standards-architect.md +159 -159
- package/framework/agents/frontend/blazor-builder.md +13 -0
- package/framework/agents/frontend/css-specialist.md +87 -0
- package/framework/agents/frontend/nextjs-expert.md +13 -0
- package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
- package/framework/agents/frontend/ui-ux-designer.md +80 -10
- package/framework/agents/infrastructure/azure-architect.md +149 -149
- package/framework/agents/infrastructure/infra-architect.md +48 -48
- package/framework/agents.json +2268 -2567
- package/framework/hooks/README.md +204 -204
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
- package/framework/hooks/shared/phase-utils.js +139 -139
- package/framework/hooks/shared/worktree-helpers.js +30 -8
- package/framework/rules/csharp-standards.md +13 -13
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +16 -16
- package/framework/rules/testing-standards.md +14 -14
- package/framework/skills/README.md +0 -1
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
- package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
- package/framework/skills/level-0-meta/morph-init/SKILL.md +88 -3
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -471
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -258
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +424 -323
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +2040 -2040
- package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
- package/framework/templates/README.md +814 -814
- package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
- package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
- package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
- package/framework/templates/code/dotnet/contracts/README.md +74 -74
- package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
- package/framework/templates/code/dotnet/frontend/component.razor +239 -239
- package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
- package/framework/templates/code/dotnet/jobs/job.cs +171 -171
- package/framework/templates/code/dotnet/test.cs +239 -239
- package/framework/templates/code/sql/rls-policy.sql +57 -57
- package/framework/templates/code/sql/supabase-migration.sql +100 -100
- package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
- package/framework/templates/code/typescript/contracts.ts +168 -168
- package/framework/templates/context/CONTEXT.md +181 -181
- package/framework/templates/context/design-system.md +211 -0
- package/framework/templates/docs/proposal.md +182 -182
- package/framework/templates/docs/user-stories.md +34 -34
- package/framework/templates/examples/design-system-examples.md +357 -357
- package/framework/templates/examples/spec-examples.md +90 -90
- package/framework/templates/feature/decisions.md +187 -187
- package/framework/templates/feature/recap.md +146 -146
- package/framework/templates/feature/tasks.md +199 -199
- package/framework/templates/infrastructure/azure/README.md +286 -286
- package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
- package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
- package/package.json +82 -82
- package/src/commands/agents/agent-metrics.js +174 -0
- package/src/commands/agents/audit-dispatch.js +82 -0
- package/src/commands/agents/detect-agents.js +275 -0
- package/src/commands/agents/dispatch-agents.js +41 -135
- package/src/commands/agents/index.js +13 -4
- package/src/commands/agents/manage-agents.js +352 -0
- package/src/commands/project/doctor.js +59 -0
- package/src/commands/project/install-plugin-cmd.js +28 -28
- package/src/commands/project/setup-infra-cmd.js +12 -12
- package/src/commands/project/update.js +1 -17
- package/src/commands/project/worktree.js +58 -2
- package/src/commands/state/advance-phase.js +11 -3
- package/src/commands/tasks/index.js +4 -4
- package/src/commands/templates/index.js +5 -5
- package/src/commands/utils/index.js +3 -3
- package/src/commands/validation/index.js +4 -4
- package/src/core/state/phase-state-machine.js +217 -217
- package/src/core/state/state-manager.js +81 -0
- package/src/lib/agents/agents-manager.js +349 -0
- package/src/lib/agents/metrics-calculator.js +125 -0
- package/src/lib/agents/output-collector.js +75 -0
- package/src/lib/generators/context-generator.js +8 -7
- package/src/lib/generators/design-system-generator.js +298 -298
- package/src/lib/generators/index.js +10 -10
- package/src/lib/generators/metadata-extractor.js +387 -387
- package/src/lib/stack-filter.js +65 -65
- package/src/lib/standards/index.js +7 -7
- package/src/lib/standards/standards-context-injector.js +302 -302
- package/src/lib/tasks/task-parser.js +94 -3
- package/src/lib/validators/architecture/architecture-validator.js +387 -387
- package/src/lib/validators/architecture/index.js +7 -7
- package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
- package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
- package/src/lib/validators/blazor/blazor-validator.js +369 -369
- package/src/lib/validators/blazor/index.js +9 -9
- package/src/lib/validators/checkpoint-persistence.js +39 -0
- package/src/lib/validators/content/content-validator.js +287 -279
- package/src/lib/validators/content/index.js +7 -7
- package/src/lib/validators/contracts/index.js +7 -7
- package/src/lib/validators/css/css-validator.js +352 -352
- package/src/lib/validators/css/index.js +7 -7
- package/src/lib/validators/design-system/design-system-validator.js +231 -231
- package/src/lib/validators/design-system/index.js +7 -7
- package/src/lib/validators/nextjs/index.js +6 -6
- package/src/lib/validators/nextjs/next-component-validator.js +183 -183
- package/src/lib/validators/packages/index.js +7 -7
- package/src/lib/validators/packages/package-validator.js +360 -360
- package/src/lib/validators/shared/index.js +12 -12
- package/src/lib/validators/shared/issue-counter.js +18 -18
- package/src/lib/validators/shared/result-formatter.js +124 -124
- package/src/lib/validators/spec-validator.js +2 -2
- package/src/lib/validators/ui/index.js +7 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
- package/src/lib/validators/validation-runner.js +20 -0
- package/src/scripts/global-install.js +58 -58
- package/src/scripts/install-plugin.js +126 -126
- package/src/scripts/setup-infra.js +1 -17
- package/src/utils/agent-id-mapper.js +11 -0
- package/src/utils/agents-installer.js +40 -8
- package/src/utils/claude-md-injector.js +90 -90
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +200 -196
- package/src/utils/hooks-installer.js +556 -739
- package/src/utils/logger.js +32 -32
- package/src/utils/skills-installer.js +178 -178
- package/src/utils/tier-helpers.js +24 -0
- package/src/utils/version-checker.js +175 -175
- package/framework/agents/architecture/po-pm-advisor.md +0 -203
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
|
@@ -21,6 +21,9 @@ import { join, dirname } from 'path';
|
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
22
|
import { loadState, stateExists } from '../../core/state/state-manager.js';
|
|
23
23
|
import { buildAgentBriefing, buildValidatorPrompt } from '../../lib/standards/digest-builder.js';
|
|
24
|
+
import { agentIdToSubagentType } from '../../utils/agent-id-mapper.js';
|
|
25
|
+
import { parseAndGroupTasks } from '../../lib/tasks/task-parser.js';
|
|
26
|
+
export { parseAndGroupTasks } from '../../lib/tasks/task-parser.js';
|
|
24
27
|
import chalk from 'chalk';
|
|
25
28
|
|
|
26
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -34,62 +37,6 @@ const PHASES_JSON_PATH = join(__dirname, '../../../framework/phases.json');
|
|
|
34
37
|
/** Phases where agent dispatch is meaningful */
|
|
35
38
|
const DISPATCHABLE_PHASES = ['design', 'tasks', 'implement'];
|
|
36
39
|
|
|
37
|
-
/**
|
|
38
|
-
* Domain → parallel group mapping.
|
|
39
|
-
* Agents inherit group from their first matching domain.
|
|
40
|
-
*/
|
|
41
|
-
const DOMAIN_TO_GROUP = {
|
|
42
|
-
'backend': 'backend',
|
|
43
|
-
'backend-data': 'backend',
|
|
44
|
-
'backend-jobs': 'backend',
|
|
45
|
-
'backend-events': 'backend',
|
|
46
|
-
'frontend': 'frontend',
|
|
47
|
-
'frontend-blazor': 'frontend',
|
|
48
|
-
'frontend-nextjs': 'frontend',
|
|
49
|
-
'frontend-css': 'frontend',
|
|
50
|
-
'frontend-design': 'frontend',
|
|
51
|
-
'infrastructure': 'infra',
|
|
52
|
-
'cloud-azure': 'infra',
|
|
53
|
-
'devops': 'infra',
|
|
54
|
-
'domain': 'domain-analysis',
|
|
55
|
-
'domain-modeling': 'domain-analysis',
|
|
56
|
-
'architecture': 'domain-analysis',
|
|
57
|
-
'ai-orchestration': 'domain-analysis',
|
|
58
|
-
'testing': 'tests',
|
|
59
|
-
'qa': 'tests',
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/** Agent-level group overrides (higher priority than domain mapping) */
|
|
63
|
-
const AGENT_GROUP_OVERRIDES = {
|
|
64
|
-
'domain-architect': 'domain-analysis',
|
|
65
|
-
'standards-architect': 'domain-analysis',
|
|
66
|
-
'ai-system-architect': 'domain-analysis',
|
|
67
|
-
'popm-advisor': 'domain-analysis',
|
|
68
|
-
'dotnet-senior': 'backend',
|
|
69
|
-
'infra-architect': 'infra',
|
|
70
|
-
'ui-designer': 'frontend',
|
|
71
|
-
'testing-specialist': 'tests',
|
|
72
|
-
'code-analyzer': 'tests',
|
|
73
|
-
'load-testing-expert': 'tests',
|
|
74
|
-
'documentation-specialist': 'docs',
|
|
75
|
-
'troubleshooting-expert': 'support',
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Task category → parallel group mapping for implement-phase task splitting.
|
|
80
|
-
* Matches task categories defined in phase-tasks SKILL.md.
|
|
81
|
-
*/
|
|
82
|
-
const TASK_CATEGORY_TO_GROUP = {
|
|
83
|
-
'domain': 'backend',
|
|
84
|
-
'domain-bc': 'backend',
|
|
85
|
-
'infrastructure': 'backend',
|
|
86
|
-
'application': 'backend',
|
|
87
|
-
'presentation': 'frontend',
|
|
88
|
-
'tests': 'tests',
|
|
89
|
-
'infra': 'infra',
|
|
90
|
-
'docs': 'docs',
|
|
91
|
-
};
|
|
92
|
-
|
|
93
40
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
41
|
// Required Skills Injection
|
|
95
42
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -175,34 +122,24 @@ export function buildMcpBlock(phase, phasesData, opts = {}) {
|
|
|
175
122
|
|
|
176
123
|
/**
|
|
177
124
|
* Determine the parallel execution group for an agent.
|
|
178
|
-
*
|
|
125
|
+
* Reads from the `group` field in agents.json v4; falls back to 'backend'.
|
|
179
126
|
*
|
|
180
127
|
* @param {string} agentId
|
|
181
128
|
* @param {Object} agentData
|
|
182
129
|
* @returns {string} group name
|
|
183
130
|
*/
|
|
184
131
|
function getAgentGroup(agentId, agentData) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const domains = agentData.domains || [];
|
|
188
|
-
for (const domain of domains) {
|
|
189
|
-
if (DOMAIN_TO_GROUP[domain]) return DOMAIN_TO_GROUP[domain];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Tier-2 domain leaders default to backend if no domain matched
|
|
193
|
-
if (agentData.tier === 2) return 'backend';
|
|
194
|
-
return 'backend';
|
|
132
|
+
return agentData.group || 'backend';
|
|
195
133
|
}
|
|
196
134
|
|
|
197
135
|
/**
|
|
198
136
|
* Check whether an agent should be dispatched for the given phase.
|
|
199
137
|
*
|
|
200
|
-
* Rules
|
|
201
|
-
* 1. Tier-4 validators → never dispatch (
|
|
202
|
-
* 2.
|
|
203
|
-
* 3.
|
|
204
|
-
* 4.
|
|
205
|
-
* 5. Default: specialists without active_phases → implement only
|
|
138
|
+
* Rules:
|
|
139
|
+
* 1. Tier-4 validators → never dispatch (validate mode bypasses this)
|
|
140
|
+
* 2. `active_phases` includes '*' → all dispatchable phases
|
|
141
|
+
* 3. `active_phases` includes the phase → dispatch
|
|
142
|
+
* 4. Default (no active_phases): implement only
|
|
206
143
|
*
|
|
207
144
|
* @param {string} agentId
|
|
208
145
|
* @param {Object} agentData
|
|
@@ -210,72 +147,16 @@ function getAgentGroup(agentId, agentData) {
|
|
|
210
147
|
* @returns {boolean}
|
|
211
148
|
*/
|
|
212
149
|
function isRelevantForPhase(agentId, agentData, phase) {
|
|
213
|
-
// Tier-4 validators have no implementation tasks (validate mode bypasses this)
|
|
214
150
|
if (agentData.tier === 4) return false;
|
|
215
151
|
|
|
216
|
-
|
|
217
|
-
if (Array.isArray(
|
|
218
|
-
return
|
|
152
|
+
const phases = agentData.active_phases;
|
|
153
|
+
if (Array.isArray(phases)) {
|
|
154
|
+
return phases.includes('*') || phases.includes(phase);
|
|
219
155
|
}
|
|
220
156
|
|
|
221
|
-
// active_phases nested in relationships
|
|
222
|
-
const relPhases = agentData.relationships?.active_phases;
|
|
223
|
-
if (Array.isArray(relPhases)) {
|
|
224
|
-
return relPhases.includes(phase);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// always_active agents: dispatch for all recognised phases
|
|
228
|
-
if (agentData.always_active && DISPATCHABLE_PHASES.includes(phase)) return true;
|
|
229
|
-
|
|
230
|
-
// Specialists without active_phases: implement phase only
|
|
231
157
|
return phase === 'implement';
|
|
232
158
|
}
|
|
233
159
|
|
|
234
|
-
/**
|
|
235
|
-
* Parse tasks.md and return a map of { parallelGroup → taskId[] }.
|
|
236
|
-
* Gracefully returns {} if the file doesn't exist or is unparseable.
|
|
237
|
-
*
|
|
238
|
-
* @param {string} tasksPath - Absolute path to tasks.md
|
|
239
|
-
* @returns {Object}
|
|
240
|
-
*/
|
|
241
|
-
export function parseAndGroupTasks(tasksPath) {
|
|
242
|
-
if (!existsSync(tasksPath)) return {};
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
const content = readFileSync(tasksPath, 'utf8');
|
|
246
|
-
const groups = {};
|
|
247
|
-
|
|
248
|
-
// Match task headings: ### T001 — Title or ### T001: Title
|
|
249
|
-
const taskHeadingRe = /^###\s+(T\d{3,})\s*[—–:\-]\s*(.+)$/gm;
|
|
250
|
-
// Match category line within a task block: **category**: `domain` or - category: application
|
|
251
|
-
const categoryRe = /(?:\*\*category\*\*\s*:\s*`?([a-z-]+)`?|category:\s*([a-z-]+))/i;
|
|
252
|
-
|
|
253
|
-
let headingMatch;
|
|
254
|
-
const taskPositions = [];
|
|
255
|
-
|
|
256
|
-
while ((headingMatch = taskHeadingRe.exec(content)) !== null) {
|
|
257
|
-
taskPositions.push({ id: headingMatch[1], pos: headingMatch.index });
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
for (let i = 0; i < taskPositions.length; i++) {
|
|
261
|
-
const { id, pos } = taskPositions[i];
|
|
262
|
-
const end = i + 1 < taskPositions.length ? taskPositions[i + 1].pos : content.length;
|
|
263
|
-
const block = content.slice(pos, end);
|
|
264
|
-
|
|
265
|
-
const catMatch = categoryRe.exec(block);
|
|
266
|
-
const category = (catMatch?.[1] || catMatch?.[2] || 'application').toLowerCase();
|
|
267
|
-
const group = TASK_CATEGORY_TO_GROUP[category] || 'backend';
|
|
268
|
-
|
|
269
|
-
if (!groups[group]) groups[group] = [];
|
|
270
|
-
groups[group].push(id);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return groups;
|
|
274
|
-
} catch {
|
|
275
|
-
return {};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
160
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
280
161
|
// Core Logic
|
|
281
162
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -345,16 +226,15 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
345
226
|
const activeAgentIds = new Set(feature.activeAgents || []);
|
|
346
227
|
|
|
347
228
|
// Collect dispatchable agents
|
|
348
|
-
// Process all agents: active ones from state + always_active ones not yet in state
|
|
349
229
|
const isValidateMode = opts.mode === 'validate';
|
|
350
230
|
const processed = new Set();
|
|
351
231
|
const dispatchableAgents = [];
|
|
352
232
|
|
|
353
233
|
const candidateIds = [
|
|
354
234
|
...activeAgentIds,
|
|
355
|
-
// Add
|
|
235
|
+
// Add always-active agents (active_phases includes '*')
|
|
356
236
|
...Object.entries(allAgents)
|
|
357
|
-
.filter(([id, data]) =>
|
|
237
|
+
.filter(([id, data]) => Array.isArray(data.active_phases) && data.active_phases.includes('*'))
|
|
358
238
|
.map(([id]) => id),
|
|
359
239
|
// In validate mode, also include tier-4 validators
|
|
360
240
|
...(isValidateMode
|
|
@@ -402,6 +282,7 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
402
282
|
|
|
403
283
|
dispatchableAgents.push({
|
|
404
284
|
id: agentId,
|
|
285
|
+
subagentType: agentIdToSubagentType(agentId, agentData.tier),
|
|
405
286
|
title: agentData.title,
|
|
406
287
|
tier: agentData.tier,
|
|
407
288
|
icon: agentData.teammate?.icon || '🤖',
|
|
@@ -452,6 +333,7 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
452
333
|
const groupAgent = dispatchableAgents.find(a => a.parallelGroup === group);
|
|
453
334
|
taskGroups[group] = {
|
|
454
335
|
agentId: groupAgent?.id ?? null,
|
|
336
|
+
subagentType: groupAgent?.subagentType ?? null,
|
|
455
337
|
description: `${group} implementation tasks for feature '${featureName}'`,
|
|
456
338
|
tasks: taskIds,
|
|
457
339
|
canRunParallel: true,
|
|
@@ -467,6 +349,7 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
467
349
|
|
|
468
350
|
taskGroups[agent.parallelGroup] = {
|
|
469
351
|
agentId: agent.id,
|
|
352
|
+
subagentType: agent.subagentType,
|
|
470
353
|
description: agent.role,
|
|
471
354
|
tasks: [],
|
|
472
355
|
canRunParallel: true,
|
|
@@ -475,6 +358,19 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
475
358
|
}
|
|
476
359
|
}
|
|
477
360
|
|
|
361
|
+
// ── Pre-flight: check agent files exist ──────────────────────────────────
|
|
362
|
+
const warnings = [];
|
|
363
|
+
for (const agent of dispatchableAgents) {
|
|
364
|
+
const agentFile = join(projectPath, '.claude', 'agents', `${agent.subagentType}.md`);
|
|
365
|
+
if (!existsSync(agentFile)) {
|
|
366
|
+
warnings.push({
|
|
367
|
+
agentId: agent.id,
|
|
368
|
+
subagentType: agent.subagentType,
|
|
369
|
+
message: `File .claude/agents/${agent.subagentType}.md not found`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
478
374
|
return {
|
|
479
375
|
feature: featureName,
|
|
480
376
|
phase,
|
|
@@ -487,6 +383,7 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
487
383
|
: `No dispatchable agents found for phase '${phase}'`,
|
|
488
384
|
agents: dispatchableAgents,
|
|
489
385
|
taskGroups,
|
|
386
|
+
warnings,
|
|
490
387
|
};
|
|
491
388
|
}
|
|
492
389
|
|
|
@@ -563,6 +460,15 @@ export async function dispatchAgentsCommand(featureName, phase, options = {}) {
|
|
|
563
460
|
// JSON (default — consumed by phase skills / LLM)
|
|
564
461
|
console.log(JSON.stringify(config, null, 2));
|
|
565
462
|
}
|
|
463
|
+
|
|
464
|
+
// Pre-flight mode: exit with code 1 if warnings exist
|
|
465
|
+
if (options.preflight && config.warnings?.length > 0) {
|
|
466
|
+
console.error(chalk.yellow(`\n ⚠ Pre-flight: ${config.warnings.length} missing agent file(s):`));
|
|
467
|
+
for (const w of config.warnings) {
|
|
468
|
+
console.error(chalk.yellow(` - ${w.agentId}: ${w.message}`));
|
|
469
|
+
}
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
566
472
|
} catch (err) {
|
|
567
473
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
568
474
|
if (options.verbose) console.error(err.stack);
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Commands
|
|
3
|
-
*/
|
|
4
|
-
export { dispatchAgentsCommand, buildDispatchConfig } from './dispatch-agents.js';
|
|
1
|
+
/**
|
|
2
|
+
* Agent Commands
|
|
3
|
+
*/
|
|
4
|
+
export { dispatchAgentsCommand, buildDispatchConfig } from './dispatch-agents.js';
|
|
5
|
+
export { auditDispatchCommand } from './audit-dispatch.js';
|
|
6
|
+
export { detectAgentsCommand, detectAgents } from './detect-agents.js';
|
|
7
|
+
export {
|
|
8
|
+
agentListCommand,
|
|
9
|
+
agentShowCommand,
|
|
10
|
+
agentAddCommand,
|
|
11
|
+
agentEditCommand,
|
|
12
|
+
agentRemoveCommand,
|
|
13
|
+
} from './manage-agents.js';
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* manage-agents.js — CLI command handlers for agent management
|
|
3
|
+
*
|
|
4
|
+
* Thin wrappers around agents-manager.js core functions.
|
|
5
|
+
* Each handler: parses args -> calls core -> formats output via logger.
|
|
6
|
+
*
|
|
7
|
+
* @module manage-agents
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join, dirname } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { logger } from '../../utils/logger.js';
|
|
16
|
+
import {
|
|
17
|
+
loadAgentsFile,
|
|
18
|
+
saveAgentsFile,
|
|
19
|
+
listAgents,
|
|
20
|
+
showAgent,
|
|
21
|
+
addAgent,
|
|
22
|
+
editAgent,
|
|
23
|
+
removeAgent,
|
|
24
|
+
TIER_LABELS,
|
|
25
|
+
KNOWN_PHASES,
|
|
26
|
+
} from '../../lib/agents/agents-manager.js';
|
|
27
|
+
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const AGENTS_PATH = join(__dirname, '../../../framework/agents.json');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the agents.json path.
|
|
33
|
+
* Uses the canonical framework/agents.json (v4 schema).
|
|
34
|
+
* If running inside a client project (not the framework repo), falls back
|
|
35
|
+
* to .morph/framework/agents.json.
|
|
36
|
+
*/
|
|
37
|
+
function getAgentsPath() {
|
|
38
|
+
// Prefer the framework source file (always v4 schema)
|
|
39
|
+
if (existsSync(AGENTS_PATH)) return AGENTS_PATH;
|
|
40
|
+
// Fallback for client projects
|
|
41
|
+
const clientPath = join(process.cwd(), '.morph', 'framework', 'agents.json');
|
|
42
|
+
if (existsSync(clientPath)) return clientPath;
|
|
43
|
+
return AGENTS_PATH;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── List ────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export async function agentListCommand(options) {
|
|
49
|
+
const agentsData = loadAgentsFile(getAgentsPath());
|
|
50
|
+
const filters = {};
|
|
51
|
+
if (options.group) filters.group = options.group;
|
|
52
|
+
if (options.tier) filters.tier = parseInt(options.tier, 10);
|
|
53
|
+
|
|
54
|
+
const result = listAgents(agentsData, filters);
|
|
55
|
+
|
|
56
|
+
if (options.json) {
|
|
57
|
+
console.log(JSON.stringify(result.agents, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (result.total === 0) {
|
|
62
|
+
logger.info('No agents found matching filters.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logger.header(`Agents (${result.total} total)`);
|
|
67
|
+
|
|
68
|
+
// Table header
|
|
69
|
+
const header = ` ${'ID'.padEnd(28)} ${'Tier'.padEnd(6)} ${'Group'.padEnd(18)} ${'Phases'.padEnd(16)} Keywords`;
|
|
70
|
+
console.log(chalk.dim(header));
|
|
71
|
+
console.log(chalk.dim(' ' + '\u2500'.repeat(90)));
|
|
72
|
+
|
|
73
|
+
for (const a of result.agents) {
|
|
74
|
+
const tierLabel = `T${a.tier}`;
|
|
75
|
+
const group = (a.group || '\u2014').padEnd(18);
|
|
76
|
+
const phases = (a.phases || '*').padEnd(16);
|
|
77
|
+
console.log(` ${a.id.padEnd(28)} ${tierLabel.padEnd(6)} ${group} ${phases} ${a.keywords || '\u2014'}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.blank();
|
|
81
|
+
const groups = Object.entries(result.groupCounts).map(([g, c]) => `${g}(${c})`).join(' ');
|
|
82
|
+
logger.dim(`Groups: ${groups}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Show ────────────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export async function agentShowCommand(id, options) {
|
|
88
|
+
const agentsData = loadAgentsFile(getAgentsPath());
|
|
89
|
+
const agent = showAgent(agentsData, id);
|
|
90
|
+
|
|
91
|
+
if (!agent) {
|
|
92
|
+
const ids = Object.keys(agentsData.agents || {});
|
|
93
|
+
const similar = ids.filter(aid => levenshtein(aid, id) <= 3);
|
|
94
|
+
logger.error(`Agent '${id}' not found.`);
|
|
95
|
+
if (similar.length > 0) {
|
|
96
|
+
logger.dim(`Did you mean: ${similar.join(', ')}?`);
|
|
97
|
+
}
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (options.json) {
|
|
103
|
+
console.log(JSON.stringify({ [id]: agent }, null, 2));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.header(`Agent: ${id}`);
|
|
108
|
+
logger.info(`Title: ${agent.title}`);
|
|
109
|
+
logger.info(`Tier: ${agent.tier} (${TIER_LABELS[agent.tier] || '?'})`);
|
|
110
|
+
logger.info(`Group: ${agent.group}`);
|
|
111
|
+
logger.info(`Phases: ${(agent.active_phases || []).join(', ')}`);
|
|
112
|
+
logger.info(`Reports to: ${agent.reports_to}`);
|
|
113
|
+
logger.info(`Domains: ${(agent.domains || []).join(', ') || '\u2014'}`);
|
|
114
|
+
logger.info(`Keywords: ${(agent.keywords || []).join(', ') || '\u2014'}`);
|
|
115
|
+
logger.info(`Validators: ${(agent.validators || []).join(', ') || '\u2014'}`);
|
|
116
|
+
|
|
117
|
+
if (agent.stacks?.length > 0) {
|
|
118
|
+
logger.info(`Stacks: ${agent.stacks.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (agent.standards?.length > 0) {
|
|
122
|
+
logger.blank();
|
|
123
|
+
logger.info('Standards:');
|
|
124
|
+
for (const s of agent.standards) {
|
|
125
|
+
logger.dim(` ${s.id} (scope: ${s.scope}, priority: ${s.priority})`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (agent.teammate) {
|
|
130
|
+
logger.blank();
|
|
131
|
+
logger.info('Teammate:');
|
|
132
|
+
logger.dim(` Role: ${agent.teammate.role}`);
|
|
133
|
+
logger.dim(` Icon: ${agent.teammate.icon}`);
|
|
134
|
+
const prompt = agent.teammate.spawn_prompt || '';
|
|
135
|
+
const truncated = options.full ? prompt : (prompt.length > 200 ? prompt.slice(0, 200) + '...' : prompt);
|
|
136
|
+
logger.dim(` Prompt: ${truncated}`);
|
|
137
|
+
} else {
|
|
138
|
+
logger.blank();
|
|
139
|
+
logger.dim('No teammate configured.');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Add ─────────────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export async function agentAddCommand(id, options) {
|
|
146
|
+
const agentsData = loadAgentsFile(getAgentsPath());
|
|
147
|
+
|
|
148
|
+
let fields;
|
|
149
|
+
if (options.nonInteractive) {
|
|
150
|
+
fields = parseNonInteractiveFlags(options);
|
|
151
|
+
} else {
|
|
152
|
+
fields = await runAddWizard(id, agentsData);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const result = addAgent(agentsData, id, fields);
|
|
156
|
+
|
|
157
|
+
if (!result.success) {
|
|
158
|
+
for (const e of result.errors) logger.error(e);
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (result.warnings?.length > 0) {
|
|
164
|
+
for (const w of result.warnings) logger.warn(w);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
saveAgentsFile(getAgentsPath(), result.data);
|
|
168
|
+
logger.success(`Created agent '${id}' in agents.json`);
|
|
169
|
+
logger.dim(`Run 'morph-spec setup-infra' to install the .claude/agents/ file`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Edit ────────────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
export async function agentEditCommand(id, options) {
|
|
175
|
+
const agentsData = loadAgentsFile(getAgentsPath());
|
|
176
|
+
const changes = {};
|
|
177
|
+
|
|
178
|
+
if (options.tier) changes.tier = parseInt(options.tier, 10);
|
|
179
|
+
if (options.title) changes.title = options.title;
|
|
180
|
+
if (options.group) changes.group = options.group;
|
|
181
|
+
if (options.activePhases) changes.active_phases = options.activePhases.split(',').map(s => s.trim());
|
|
182
|
+
if (options.reportsTo) changes.reports_to = options.reportsTo;
|
|
183
|
+
if (options.keywords) changes.keywords = options.keywords.split(',').map(s => s.trim());
|
|
184
|
+
if (options.domains) changes.domains = options.domains.split(',').map(s => s.trim());
|
|
185
|
+
if (options.spawnPrompt) changes.spawn_prompt = options.spawnPrompt;
|
|
186
|
+
|
|
187
|
+
if (Object.keys(changes).length === 0) {
|
|
188
|
+
logger.error('No changes specified. Use flags like --title, --tier, --keywords, etc.');
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result = editAgent(agentsData, id, changes);
|
|
194
|
+
|
|
195
|
+
if (!result.success) {
|
|
196
|
+
for (const e of result.errors) logger.error(e);
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
saveAgentsFile(getAgentsPath(), result.data);
|
|
202
|
+
logger.success(`Updated agent '${id}'`);
|
|
203
|
+
for (const c of result.changes) logger.dim(` ${c}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Remove ──────────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
export async function agentRemoveCommand(id, options) {
|
|
209
|
+
const agentsData = loadAgentsFile(getAgentsPath());
|
|
210
|
+
const agent = showAgent(agentsData, id);
|
|
211
|
+
|
|
212
|
+
if (!agent) {
|
|
213
|
+
logger.error(`Agent '${id}' not found.`);
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Show summary
|
|
219
|
+
logger.info(`Agent: ${id} (T${agent.tier} ${agent.group})`);
|
|
220
|
+
|
|
221
|
+
// Check for dependents
|
|
222
|
+
const dependents = Object.entries(agentsData.agents)
|
|
223
|
+
.filter(([aid, a]) => aid !== id && a.reports_to === id)
|
|
224
|
+
.map(([aid]) => aid);
|
|
225
|
+
|
|
226
|
+
if (dependents.length > 0) {
|
|
227
|
+
logger.warn(`These agents report to '${id}': ${dependents.join(', ')}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check state.json for active references (advisory)
|
|
231
|
+
checkActiveFeatures(id);
|
|
232
|
+
|
|
233
|
+
if (!options.force) {
|
|
234
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
235
|
+
const answer = await new Promise(resolve => {
|
|
236
|
+
rl.question(chalk.yellow(' Remove this agent? (y/N) '), resolve);
|
|
237
|
+
});
|
|
238
|
+
rl.close();
|
|
239
|
+
if (answer.toLowerCase() !== 'y') {
|
|
240
|
+
logger.info('Cancelled.');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const result = removeAgent(agentsData, id);
|
|
246
|
+
saveAgentsFile(getAgentsPath(), result.data);
|
|
247
|
+
logger.success(`Removed agent '${id}' from agents.json`);
|
|
248
|
+
logger.dim(`Run 'morph-spec setup-infra' to clean up .claude/agents/ files`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Levenshtein distance between two strings.
|
|
255
|
+
* Used for "did you mean?" suggestions on typos.
|
|
256
|
+
*/
|
|
257
|
+
export function levenshtein(a, b) {
|
|
258
|
+
const m = a.length;
|
|
259
|
+
const n = b.length;
|
|
260
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array(n + 1).fill(0));
|
|
261
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
262
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
263
|
+
for (let i = 1; i <= m; i++) {
|
|
264
|
+
for (let j = 1; j <= n; j++) {
|
|
265
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
266
|
+
? dp[i - 1][j - 1]
|
|
267
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return dp[m][n];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Interactive wizard for adding an agent.
|
|
275
|
+
*/
|
|
276
|
+
async function runAddWizard(id, agentsData) {
|
|
277
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
278
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
279
|
+
|
|
280
|
+
console.log(chalk.bold(`\n Adding agent: ${id}\n`));
|
|
281
|
+
|
|
282
|
+
const knownGroups = [...new Set(Object.values(agentsData.agents || {}).map(a => a.group))];
|
|
283
|
+
|
|
284
|
+
const tier = parseInt(await ask(` ? Tier (1=${TIER_LABELS[1]}, 2=${TIER_LABELS[2]}, 3=${TIER_LABELS[3]}, 4=${TIER_LABELS[4]}): `), 10);
|
|
285
|
+
const title = await ask(' ? Title: ');
|
|
286
|
+
console.log(chalk.dim(` Known groups: ${knownGroups.join(', ')}`));
|
|
287
|
+
const group = await ask(' ? Group: ');
|
|
288
|
+
const phasesRaw = await ask(' ? Active phases (comma-sep, or * for all): ');
|
|
289
|
+
const active_phases = phasesRaw.trim() === '*' ? ['*'] : phasesRaw.split(',').map(s => s.trim());
|
|
290
|
+
|
|
291
|
+
const existingIds = Object.keys(agentsData.agents || {});
|
|
292
|
+
console.log(chalk.dim(` Valid: ${existingIds.slice(0, 5).join(', ')}... or "user"`));
|
|
293
|
+
const reports_to = await ask(' ? Reports to: ');
|
|
294
|
+
|
|
295
|
+
const domainsRaw = await ask(' ? Domains (comma-sep): ');
|
|
296
|
+
const domains = domainsRaw.split(',').map(s => s.trim()).filter(Boolean);
|
|
297
|
+
const keywordsRaw = await ask(' ? Keywords (comma-sep): ');
|
|
298
|
+
const keywords = keywordsRaw.split(',').map(s => s.trim()).filter(Boolean);
|
|
299
|
+
|
|
300
|
+
let teammate = null;
|
|
301
|
+
if (tier >= 3) {
|
|
302
|
+
const role = await ask(' ? Teammate role: ');
|
|
303
|
+
const icon = await ask(' ? Teammate icon (emoji): ');
|
|
304
|
+
const spawn_prompt = await ask(' ? Spawn prompt: ');
|
|
305
|
+
teammate = { role, icon, spawn_prompt };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
rl.close();
|
|
309
|
+
|
|
310
|
+
return { tier, title, group, active_phases, reports_to, domains, keywords, teammate };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Parse --non-interactive flags into fields object.
|
|
315
|
+
*/
|
|
316
|
+
function parseNonInteractiveFlags(options) {
|
|
317
|
+
return {
|
|
318
|
+
tier: options.tier ? parseInt(options.tier, 10) : undefined,
|
|
319
|
+
title: options.title,
|
|
320
|
+
group: options.group,
|
|
321
|
+
active_phases: options.activePhases ? options.activePhases.split(',').map(s => s.trim()) : ['implement'],
|
|
322
|
+
reports_to: options.reportsTo || 'user',
|
|
323
|
+
domains: options.domains ? options.domains.split(',').map(s => s.trim()) : [],
|
|
324
|
+
keywords: options.keywords ? options.keywords.split(',').map(s => s.trim()) : [],
|
|
325
|
+
teammate: options.spawnPrompt ? {
|
|
326
|
+
role: options.role || 'specialist',
|
|
327
|
+
icon: options.icon || '\u{1F527}',
|
|
328
|
+
spawn_prompt: options.spawnPrompt,
|
|
329
|
+
} : null,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Advisory check: is this agent referenced in active features?
|
|
335
|
+
* Fail-open — any errors are silently ignored.
|
|
336
|
+
*/
|
|
337
|
+
function checkActiveFeatures(agentId) {
|
|
338
|
+
try {
|
|
339
|
+
const statePath = join(process.cwd(), '.morph', 'state.json');
|
|
340
|
+
if (!existsSync(statePath)) return;
|
|
341
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
342
|
+
const activeIn = [];
|
|
343
|
+
for (const [fname, fdata] of Object.entries(state.features || {})) {
|
|
344
|
+
if (fdata.activeAgents?.includes(agentId)) activeIn.push(fname);
|
|
345
|
+
}
|
|
346
|
+
if (activeIn.length > 0) {
|
|
347
|
+
logger.warn(`Agent '${agentId}' is active in features: ${activeIn.join(', ')}. Removing from agents.json does not affect state.`);
|
|
348
|
+
}
|
|
349
|
+
} catch {
|
|
350
|
+
/* ignore — state check is advisory */
|
|
351
|
+
}
|
|
352
|
+
}
|