@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.
Files changed (149) hide show
  1. package/CLAUDE.md +3 -0
  2. package/LICENSE +71 -71
  3. package/README.md +379 -379
  4. package/bin/morph-spec.js +89 -0
  5. package/bin/task-manager.js +59 -6
  6. package/claude-plugin.json +14 -14
  7. package/docs/CHEATSHEET.md +1 -1
  8. package/docs/QUICKSTART.md +206 -206
  9. package/framework/agents/README.md +14 -14
  10. package/framework/agents/architecture/standards-architect.md +159 -159
  11. package/framework/agents/frontend/blazor-builder.md +13 -0
  12. package/framework/agents/frontend/css-specialist.md +87 -0
  13. package/framework/agents/frontend/nextjs-expert.md +13 -0
  14. package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
  15. package/framework/agents/frontend/ui-ux-designer.md +80 -10
  16. package/framework/agents/infrastructure/azure-architect.md +149 -149
  17. package/framework/agents/infrastructure/infra-architect.md +48 -48
  18. package/framework/agents.json +2268 -2567
  19. package/framework/hooks/README.md +204 -204
  20. package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
  21. package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
  22. package/framework/hooks/shared/phase-utils.js +139 -139
  23. package/framework/hooks/shared/worktree-helpers.js +30 -8
  24. package/framework/rules/csharp-standards.md +13 -13
  25. package/framework/rules/frontend-standards.md +13 -13
  26. package/framework/rules/infrastructure-standards.md +16 -16
  27. package/framework/rules/testing-standards.md +14 -14
  28. package/framework/skills/README.md +0 -1
  29. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
  30. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
  31. package/framework/skills/level-0-meta/morph-init/SKILL.md +88 -3
  32. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
  33. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
  34. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
  35. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -471
  36. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
  37. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -258
  38. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
  39. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +424 -323
  40. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  41. package/framework/standards/STANDARDS.json +2040 -2040
  42. package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
  43. package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
  44. package/framework/templates/README.md +814 -814
  45. package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
  46. package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
  47. package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
  48. package/framework/templates/code/dotnet/contracts/README.md +74 -74
  49. package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
  50. package/framework/templates/code/dotnet/frontend/component.razor +239 -239
  51. package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
  52. package/framework/templates/code/dotnet/jobs/job.cs +171 -171
  53. package/framework/templates/code/dotnet/test.cs +239 -239
  54. package/framework/templates/code/sql/rls-policy.sql +57 -57
  55. package/framework/templates/code/sql/supabase-migration.sql +100 -100
  56. package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
  57. package/framework/templates/code/typescript/contracts.ts +168 -168
  58. package/framework/templates/context/CONTEXT.md +181 -181
  59. package/framework/templates/context/design-system.md +211 -0
  60. package/framework/templates/docs/proposal.md +182 -182
  61. package/framework/templates/docs/user-stories.md +34 -34
  62. package/framework/templates/examples/design-system-examples.md +357 -357
  63. package/framework/templates/examples/spec-examples.md +90 -90
  64. package/framework/templates/feature/decisions.md +187 -187
  65. package/framework/templates/feature/recap.md +146 -146
  66. package/framework/templates/feature/tasks.md +199 -199
  67. package/framework/templates/infrastructure/azure/README.md +286 -286
  68. package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
  69. package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
  70. package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
  71. package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
  72. package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
  73. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  74. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  75. package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
  76. package/package.json +82 -82
  77. package/src/commands/agents/agent-metrics.js +174 -0
  78. package/src/commands/agents/audit-dispatch.js +82 -0
  79. package/src/commands/agents/detect-agents.js +275 -0
  80. package/src/commands/agents/dispatch-agents.js +41 -135
  81. package/src/commands/agents/index.js +13 -4
  82. package/src/commands/agents/manage-agents.js +352 -0
  83. package/src/commands/project/doctor.js +59 -0
  84. package/src/commands/project/install-plugin-cmd.js +28 -28
  85. package/src/commands/project/setup-infra-cmd.js +12 -12
  86. package/src/commands/project/update.js +1 -17
  87. package/src/commands/project/worktree.js +58 -2
  88. package/src/commands/state/advance-phase.js +11 -3
  89. package/src/commands/tasks/index.js +4 -4
  90. package/src/commands/templates/index.js +5 -5
  91. package/src/commands/utils/index.js +3 -3
  92. package/src/commands/validation/index.js +4 -4
  93. package/src/core/state/phase-state-machine.js +217 -217
  94. package/src/core/state/state-manager.js +81 -0
  95. package/src/lib/agents/agents-manager.js +349 -0
  96. package/src/lib/agents/metrics-calculator.js +125 -0
  97. package/src/lib/agents/output-collector.js +75 -0
  98. package/src/lib/generators/context-generator.js +8 -7
  99. package/src/lib/generators/design-system-generator.js +298 -298
  100. package/src/lib/generators/index.js +10 -10
  101. package/src/lib/generators/metadata-extractor.js +387 -387
  102. package/src/lib/stack-filter.js +65 -65
  103. package/src/lib/standards/index.js +7 -7
  104. package/src/lib/standards/standards-context-injector.js +302 -302
  105. package/src/lib/tasks/task-parser.js +94 -3
  106. package/src/lib/validators/architecture/architecture-validator.js +387 -387
  107. package/src/lib/validators/architecture/index.js +7 -7
  108. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
  109. package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
  110. package/src/lib/validators/blazor/blazor-validator.js +369 -369
  111. package/src/lib/validators/blazor/index.js +9 -9
  112. package/src/lib/validators/checkpoint-persistence.js +39 -0
  113. package/src/lib/validators/content/content-validator.js +287 -279
  114. package/src/lib/validators/content/index.js +7 -7
  115. package/src/lib/validators/contracts/index.js +7 -7
  116. package/src/lib/validators/css/css-validator.js +352 -352
  117. package/src/lib/validators/css/index.js +7 -7
  118. package/src/lib/validators/design-system/design-system-validator.js +231 -231
  119. package/src/lib/validators/design-system/index.js +7 -7
  120. package/src/lib/validators/nextjs/index.js +6 -6
  121. package/src/lib/validators/nextjs/next-component-validator.js +183 -183
  122. package/src/lib/validators/packages/index.js +7 -7
  123. package/src/lib/validators/packages/package-validator.js +360 -360
  124. package/src/lib/validators/shared/index.js +12 -12
  125. package/src/lib/validators/shared/issue-counter.js +18 -18
  126. package/src/lib/validators/shared/result-formatter.js +124 -124
  127. package/src/lib/validators/spec-validator.js +2 -2
  128. package/src/lib/validators/ui/index.js +7 -7
  129. package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
  130. package/src/lib/validators/validation-runner.js +20 -0
  131. package/src/scripts/global-install.js +58 -58
  132. package/src/scripts/install-plugin.js +126 -126
  133. package/src/scripts/setup-infra.js +1 -17
  134. package/src/utils/agent-id-mapper.js +11 -0
  135. package/src/utils/agents-installer.js +40 -8
  136. package/src/utils/claude-md-injector.js +90 -90
  137. package/src/utils/claude-settings-manager.js +1 -1
  138. package/src/utils/file-copier.js +200 -196
  139. package/src/utils/hooks-installer.js +556 -739
  140. package/src/utils/logger.js +32 -32
  141. package/src/utils/skills-installer.js +178 -178
  142. package/src/utils/tier-helpers.js +24 -0
  143. package/src/utils/version-checker.js +175 -175
  144. package/framework/agents/architecture/po-pm-advisor.md +0 -203
  145. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
  146. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  147. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  148. package/framework/templates/.idea/morph-templates.xml +0 -92
  149. 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
- * Priority: explicit override → domain mapping → tier fallback.
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
- if (AGENT_GROUP_OVERRIDES[agentId]) return AGENT_GROUP_OVERRIDES[agentId];
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 (in priority order):
201
- * 1. Tier-4 validators → never dispatch (no task work) [overridden by validate mode]
202
- * 2. Agent-level `active_phases` array → use that list
203
- * 3. Relationships-level `active_phases` array → use that list
204
- * 4. `always_active: true` → dispatchable for all DISPATCHABLE_PHASES
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
- // Explicit active_phases on agent root
217
- if (Array.isArray(agentData.active_phases)) {
218
- return agentData.active_phases.includes(phase);
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 always_active agents that might not have been explicitly detected
235
+ // Add always-active agents (active_phases includes '*')
356
236
  ...Object.entries(allAgents)
357
- .filter(([id, data]) => !id.startsWith('_comment') && data.always_active)
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
+ }