@polymorphism-tech/morph-spec 1.0.4 → 2.1.0

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 (152) hide show
  1. package/CLAUDE.md +1381 -0
  2. package/LICENSE +72 -0
  3. package/README.md +89 -6
  4. package/bin/detect-agents.js +225 -0
  5. package/bin/morph-spec.js +120 -0
  6. package/bin/render-template.js +302 -0
  7. package/bin/semantic-detect-agents.js +246 -0
  8. package/bin/validate-agents-skills.js +239 -0
  9. package/bin/validate-agents.js +69 -0
  10. package/bin/validate-phase.js +263 -0
  11. package/content/.azure/README.md +293 -0
  12. package/content/.azure/docs/azure-devops-setup.md +454 -0
  13. package/content/.azure/docs/branch-strategy.md +398 -0
  14. package/content/.azure/docs/local-development.md +515 -0
  15. package/content/.azure/pipelines/pipeline-variables.yml +34 -0
  16. package/content/.azure/pipelines/prod-pipeline.yml +319 -0
  17. package/content/.azure/pipelines/staging-pipeline.yml +234 -0
  18. package/content/.azure/pipelines/templates/build-dotnet.yml +75 -0
  19. package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -0
  20. package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -0
  21. package/content/.azure/pipelines/templates/infra-deploy.yml +90 -0
  22. package/content/.claude/commands/morph-apply.md +118 -26
  23. package/content/.claude/commands/morph-archive.md +9 -9
  24. package/content/.claude/commands/morph-clarify.md +184 -0
  25. package/content/.claude/commands/morph-design.md +275 -0
  26. package/content/.claude/commands/morph-proposal.md +56 -15
  27. package/content/.claude/commands/morph-setup.md +100 -0
  28. package/content/.claude/commands/morph-status.md +47 -32
  29. package/content/.claude/commands/morph-tasks.md +319 -0
  30. package/content/.claude/commands/morph-uiux.md +211 -0
  31. package/content/.claude/skills/specialists/ai-system-architect.md +604 -0
  32. package/content/.claude/skills/specialists/ms-agent-expert.md +143 -89
  33. package/content/.claude/skills/specialists/ui-ux-designer.md +744 -9
  34. package/content/.claude/skills/stacks/dotnet-blazor.md +244 -8
  35. package/content/.claude/skills/stacks/dotnet-nextjs.md +2 -2
  36. package/content/.morph/.morphversion +5 -0
  37. package/content/.morph/config/agents.json +101 -8
  38. package/content/.morph/config/azure-pricing.json +70 -0
  39. package/content/.morph/config/azure-pricing.schema.json +50 -0
  40. package/content/.morph/config/config.template.json +15 -3
  41. package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -0
  42. package/content/.morph/hooks/README.md +239 -0
  43. package/content/.morph/hooks/pre-commit-agents.sh +24 -0
  44. package/content/.morph/hooks/pre-commit-all.sh +48 -0
  45. package/content/.morph/hooks/pre-commit-costs.sh +91 -0
  46. package/content/.morph/hooks/pre-commit-specs.sh +49 -0
  47. package/content/.morph/hooks/pre-commit-tests.sh +60 -0
  48. package/content/.morph/project.md +5 -4
  49. package/content/.morph/schemas/agent.schema.json +296 -0
  50. package/content/.morph/standards/agent-framework-setup.md +453 -0
  51. package/content/.morph/standards/architecture.md +142 -7
  52. package/content/.morph/standards/azure.md +218 -23
  53. package/content/.morph/standards/coding.md +47 -12
  54. package/content/.morph/standards/dotnet10-migration.md +494 -0
  55. package/content/.morph/standards/fluent-ui-setup.md +590 -0
  56. package/content/.morph/standards/migration-guide.md +514 -0
  57. package/content/.morph/standards/passkeys-auth.md +423 -0
  58. package/content/.morph/standards/vector-search-rag.md +536 -0
  59. package/content/.morph/state.json +18 -0
  60. package/content/.morph/templates/FluentDesignTheme.cs +149 -0
  61. package/content/.morph/templates/MudTheme.cs +281 -0
  62. package/content/.morph/templates/contracts.cs +55 -55
  63. package/content/.morph/templates/decisions.md +4 -4
  64. package/content/.morph/templates/design-system.css +226 -0
  65. package/content/.morph/templates/infra/.dockerignore.example +89 -0
  66. package/content/.morph/templates/infra/Dockerfile.example +82 -0
  67. package/content/.morph/templates/infra/README.md +286 -0
  68. package/content/.morph/templates/infra/app-service.bicep +164 -0
  69. package/content/.morph/templates/infra/deploy.ps1 +229 -0
  70. package/content/.morph/templates/infra/deploy.sh +208 -0
  71. package/content/.morph/templates/infra/main.bicep +41 -7
  72. package/content/.morph/templates/infra/parameters.dev.json +6 -0
  73. package/content/.morph/templates/infra/parameters.prod.json +6 -0
  74. package/content/.morph/templates/infra/parameters.staging.json +29 -0
  75. package/content/.morph/templates/proposal.md +3 -3
  76. package/content/.morph/templates/recap.md +3 -3
  77. package/content/.morph/templates/spec.md +9 -8
  78. package/content/.morph/templates/sprint-status.yaml +68 -0
  79. package/content/.morph/templates/state.template.json +222 -0
  80. package/content/.morph/templates/story.md +143 -0
  81. package/content/.morph/templates/tasks.md +1 -1
  82. package/content/.morph/templates/ui-components.md +276 -0
  83. package/content/.morph/templates/ui-design-system.md +286 -0
  84. package/content/.morph/templates/ui-flows.md +336 -0
  85. package/content/.morph/templates/ui-mockups.md +133 -0
  86. package/content/.morph/test-infra/example.bicep +59 -0
  87. package/content/CLAUDE.md +124 -0
  88. package/content/README.md +79 -0
  89. package/detectors/config-detector.js +223 -0
  90. package/detectors/conversation-analyzer.js +163 -0
  91. package/detectors/index.js +84 -0
  92. package/detectors/standards-generator.js +275 -0
  93. package/detectors/structure-detector.js +221 -0
  94. package/docs/README.md +149 -0
  95. package/docs/api/cost-calculator.js.html +513 -0
  96. package/docs/api/design-system-generator.js.html +382 -0
  97. package/docs/api/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  98. package/docs/api/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  99. package/docs/api/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  100. package/docs/api/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  101. package/docs/api/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  102. package/docs/api/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  103. package/docs/api/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  104. package/docs/api/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  105. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  106. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  107. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  108. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  109. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  110. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  111. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  112. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  113. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  114. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  115. package/docs/api/global.html +5263 -0
  116. package/docs/api/index.html +96 -0
  117. package/docs/api/scripts/collapse.js +39 -0
  118. package/docs/api/scripts/commonNav.js +28 -0
  119. package/docs/api/scripts/linenumber.js +25 -0
  120. package/docs/api/scripts/nav.js +12 -0
  121. package/docs/api/scripts/polyfill.js +4 -0
  122. package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
  123. package/docs/api/scripts/prettify/lang-css.js +2 -0
  124. package/docs/api/scripts/prettify/prettify.js +28 -0
  125. package/docs/api/scripts/search.js +99 -0
  126. package/docs/api/state-manager.js.html +423 -0
  127. package/docs/api/styles/jsdoc.css +776 -0
  128. package/docs/api/styles/prettify.css +80 -0
  129. package/docs/examples.md +328 -0
  130. package/docs/getting-started.md +302 -0
  131. package/docs/installation.md +361 -0
  132. package/docs/templates.md +418 -0
  133. package/docs/validation-checklist.md +266 -0
  134. package/package.json +39 -12
  135. package/src/commands/cost.js +181 -0
  136. package/src/commands/create-story.js +283 -0
  137. package/src/commands/detect.js +104 -0
  138. package/src/commands/doctor.js +67 -0
  139. package/src/commands/generate.js +149 -0
  140. package/src/commands/init.js +69 -45
  141. package/src/commands/shard-spec.js +224 -0
  142. package/src/commands/sprint-status.js +250 -0
  143. package/src/commands/state.js +333 -0
  144. package/src/commands/sync.js +167 -0
  145. package/src/commands/update-pricing.js +206 -0
  146. package/src/commands/update.js +88 -13
  147. package/src/lib/complexity-analyzer.js +292 -0
  148. package/src/lib/cost-calculator.js +429 -0
  149. package/src/lib/design-system-generator.js +298 -0
  150. package/src/lib/state-manager.js +340 -0
  151. package/src/utils/file-copier.js +59 -0
  152. package/src/utils/version-checker.js +175 -0
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MORPH-SPEC Template Renderer
5
+ *
6
+ * Renders templates by replacing {{PLACEHOLDER}} with actual values.
7
+ *
8
+ * Usage:
9
+ * node bin/render-template.js <template-path> <output-path> <variables-json>
10
+ *
11
+ * Example:
12
+ * node bin/render-template.js \
13
+ * content/.morph/templates/spec.md \
14
+ * .morph/project/outputs/my-feature/spec.md \
15
+ * '{"FEATURE_NAME":"my-feature","STACK":"Blazor","DATE":"2024-01-15"}'
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import { fileURLToPath } from 'url';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+
25
+ // ANSI color codes
26
+ const colors = {
27
+ green: '\x1b[32m',
28
+ red: '\x1b[31m',
29
+ yellow: '\x1b[33m',
30
+ cyan: '\x1b[36m',
31
+ reset: '\x1b[0m'
32
+ };
33
+
34
+ /**
35
+ * Load config to get default values
36
+ */
37
+ function loadConfig() {
38
+ const configPaths = [
39
+ path.join(process.cwd(), '.morph/config/config.json'),
40
+ path.join(process.cwd(), 'content/.morph/config/config.json'),
41
+ path.join(__dirname, '../content/.morph/config/config.template.json')
42
+ ];
43
+
44
+ for (const configPath of configPaths) {
45
+ if (fs.existsSync(configPath)) {
46
+ try {
47
+ const content = fs.readFileSync(configPath, 'utf-8');
48
+ return JSON.parse(content);
49
+ } catch (error) {
50
+ // Continue to next path
51
+ }
52
+ }
53
+ }
54
+
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Get default variables from config and environment
60
+ */
61
+ function getDefaultVariables() {
62
+ const config = loadConfig();
63
+ const now = new Date();
64
+
65
+ return {
66
+ DATE: now.toISOString().split('T')[0],
67
+ YEAR: now.getFullYear().toString(),
68
+ AUTHOR: config?.project?.author || 'MORPH-SPEC',
69
+ PROJECT_NAME: config?.project?.name || 'Project',
70
+ STACK: config?.project?.stack || 'Blazor',
71
+ NAMESPACE: config?.project?.namespace || 'Project'
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Convert string to various case formats
77
+ */
78
+ function transformCase(str) {
79
+ return {
80
+ // kebab-case → PascalCase
81
+ pascal: str
82
+ .split('-')
83
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
84
+ .join(''),
85
+
86
+ // kebab-case → camelCase
87
+ camel: str
88
+ .split('-')
89
+ .map((word, index) =>
90
+ index === 0
91
+ ? word.toLowerCase()
92
+ : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
93
+ )
94
+ .join(''),
95
+
96
+ // kebab-case → Title Case
97
+ title: str
98
+ .split('-')
99
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
100
+ .join(' '),
101
+
102
+ // kebab-case → UPPER_SNAKE_CASE
103
+ upperSnake: str.toUpperCase().replace(/-/g, '_'),
104
+
105
+ // kebab-case → lower_snake_case
106
+ lowerSnake: str.toLowerCase().replace(/-/g, '_')
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Render template by replacing placeholders
112
+ */
113
+ function renderTemplate(templatePath, variables) {
114
+ if (!fs.existsSync(templatePath)) {
115
+ throw new Error(`Template file not found: ${templatePath}`);
116
+ }
117
+
118
+ let content = fs.readFileSync(templatePath, 'utf-8');
119
+
120
+ // Get default variables
121
+ const defaults = getDefaultVariables();
122
+
123
+ // Merge with provided variables (provided takes precedence)
124
+ const allVariables = { ...defaults, ...variables };
125
+
126
+ // Auto-generate case transformations for FEATURE_NAME if provided
127
+ if (allVariables.FEATURE_NAME) {
128
+ const featureName = allVariables.FEATURE_NAME;
129
+ const transformations = transformCase(featureName);
130
+
131
+ allVariables.FEATURE_NAME_PASCAL = transformations.pascal;
132
+ allVariables.FEATURE_NAME_CAMEL = transformations.camel;
133
+ allVariables.FEATURE_NAME_TITLE = transformations.title;
134
+ allVariables.FEATURE_NAME_UPPER_SNAKE = transformations.upperSnake;
135
+ allVariables.FEATURE_NAME_LOWER_SNAKE = transformations.lowerSnake;
136
+
137
+ // Backward compatibility: FEATURE_TITLE maps to FEATURE_NAME_TITLE
138
+ if (!allVariables.FEATURE_TITLE) {
139
+ allVariables.FEATURE_TITLE = transformations.title;
140
+ }
141
+ }
142
+
143
+ // Track which placeholders were replaced
144
+ const replacedPlaceholders = new Set();
145
+ const unreplacedPlaceholders = new Set();
146
+
147
+ // Replace all placeholders
148
+ for (const [key, value] of Object.entries(allVariables)) {
149
+ const placeholder = `{{${key}}}`;
150
+ if (content.includes(placeholder)) {
151
+ content = content.replaceAll(placeholder, value);
152
+ replacedPlaceholders.add(key);
153
+ }
154
+ }
155
+
156
+ // Find unreplaced placeholders
157
+ const placeholderRegex = /\{\{([A-Z_]+)\}\}/g;
158
+ let match;
159
+ while ((match = placeholderRegex.exec(content)) !== null) {
160
+ unreplacedPlaceholders.add(match[1]);
161
+ }
162
+
163
+ return {
164
+ content,
165
+ replacedPlaceholders: Array.from(replacedPlaceholders),
166
+ unreplacedPlaceholders: Array.from(unreplacedPlaceholders)
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Print help message
172
+ */
173
+ function printHelp() {
174
+ console.log(`
175
+ ${colors.cyan}MORPH-SPEC Template Renderer${colors.reset}
176
+
177
+ ${colors.green}Usage:${colors.reset}
178
+ node bin/render-template.js <template-path> <output-path> <variables-json>
179
+
180
+ ${colors.green}Arguments:${colors.reset}
181
+ template-path Path to the template file (required)
182
+ output-path Path where rendered output will be written (required)
183
+ variables-json JSON object with variables to replace (required)
184
+
185
+ ${colors.green}Standard Placeholders:${colors.reset}
186
+ {{FEATURE_NAME}} - kebab-case feature name (e.g., "scheduled-reports")
187
+ {{FEATURE_NAME_PASCAL}} - PascalCase (e.g., "ScheduledReports")
188
+ {{FEATURE_NAME_CAMEL}} - camelCase (e.g., "scheduledReports")
189
+ {{FEATURE_NAME_TITLE}} - Title Case (e.g., "Scheduled Reports")
190
+ {{FEATURE_NAME_UPPER_SNAKE}} - UPPER_SNAKE_CASE (e.g., "SCHEDULED_REPORTS")
191
+ {{FEATURE_NAME_LOWER_SNAKE}} - lower_snake_case (e.g., "scheduled_reports")
192
+ {{FEATURE_TITLE}} - Alias for FEATURE_NAME_TITLE
193
+ {{STACK}} - Project stack (e.g., "Blazor", "Next.js")
194
+ {{DATE}} - Current date (YYYY-MM-DD)
195
+ {{YEAR}} - Current year
196
+ {{AUTHOR}} - Author name (from config.json)
197
+ {{PROJECT_NAME}} - Project name (from config.json)
198
+ {{NAMESPACE}} - C# namespace (from config.json)
199
+
200
+ ${colors.green}Example:${colors.reset}
201
+ node bin/render-template.js \\
202
+ content/.morph/templates/spec.md \\
203
+ .morph/project/outputs/scheduled-reports/spec.md \\
204
+ '{"FEATURE_NAME":"scheduled-reports","STACK":"Blazor"}'
205
+
206
+ ${colors.green}Flags:${colors.reset}
207
+ --help, -h Show this help message
208
+ --verbose, -v Show detailed replacement information
209
+ --dry-run, -d Preview output without writing file
210
+
211
+ ${colors.yellow}Note:${colors.reset} DATE, YEAR, AUTHOR, PROJECT_NAME, STACK, NAMESPACE are auto-populated from config.json
212
+ `);
213
+ }
214
+
215
+ /**
216
+ * Main CLI logic
217
+ */
218
+ function main() {
219
+ const args = process.argv.slice(2);
220
+
221
+ // Check for help flag
222
+ if (args.includes('--help') || args.includes('-h')) {
223
+ printHelp();
224
+ process.exit(0);
225
+ }
226
+
227
+ // Parse flags
228
+ const verbose = args.includes('--verbose') || args.includes('-v');
229
+ const dryRun = args.includes('--dry-run') || args.includes('-d');
230
+
231
+ // Filter out flags
232
+ const flags = ['--verbose', '-v', '--dry-run', '-d', '--help', '-h'];
233
+ const cleanArgs = args.filter(arg => !flags.includes(arg));
234
+
235
+ // Validate arguments
236
+ if (cleanArgs.length < 3) {
237
+ console.error(`${colors.red}❌ Error: Missing required arguments${colors.reset}\n`);
238
+ printHelp();
239
+ process.exit(1);
240
+ }
241
+
242
+ const [templatePath, outputPath, variablesJson] = cleanArgs;
243
+
244
+ // Parse variables JSON
245
+ let variables;
246
+ try {
247
+ variables = JSON.parse(variablesJson);
248
+ } catch (error) {
249
+ console.error(`${colors.red}❌ Error: Invalid JSON in variables argument${colors.reset}`);
250
+ console.error(` ${error.message}\n`);
251
+ process.exit(1);
252
+ }
253
+
254
+ try {
255
+ // Render template
256
+ const { content, replacedPlaceholders, unreplacedPlaceholders } = renderTemplate(
257
+ templatePath,
258
+ variables
259
+ );
260
+
261
+ // Dry run: just preview
262
+ if (dryRun) {
263
+ console.log(`${colors.cyan}📋 DRY RUN - Preview (file will NOT be written)${colors.reset}\n`);
264
+ console.log(content);
265
+ console.log(`\n${colors.cyan}═══════════════════════════════════════════${colors.reset}`);
266
+ } else {
267
+ // Create output directory if it doesn't exist
268
+ const outputDir = path.dirname(outputPath);
269
+ if (!fs.existsSync(outputDir)) {
270
+ fs.mkdirSync(outputDir, { recursive: true });
271
+ }
272
+
273
+ // Write rendered content
274
+ fs.writeFileSync(outputPath, content, 'utf-8');
275
+ console.log(`${colors.green}✅ Template rendered successfully${colors.reset}`);
276
+ console.log(` Output: ${outputPath}`);
277
+ }
278
+
279
+ // Verbose output
280
+ if (verbose || unreplacedPlaceholders.length > 0) {
281
+ console.log(`\n${colors.cyan}📊 Replacement Summary:${colors.reset}`);
282
+ console.log(` ${colors.green}Replaced:${colors.reset} ${replacedPlaceholders.length} placeholders`);
283
+
284
+ if (replacedPlaceholders.length > 0) {
285
+ console.log(` ${replacedPlaceholders.map(p => `{{${p}}}`).join(', ')}`);
286
+ }
287
+
288
+ if (unreplacedPlaceholders.length > 0) {
289
+ console.log(`\n ${colors.yellow}⚠️ Unreplaced:${colors.reset} ${unreplacedPlaceholders.length} placeholders`);
290
+ console.log(` ${unreplacedPlaceholders.map(p => `{{${p}}}`).join(', ')}`);
291
+ }
292
+ }
293
+
294
+ process.exit(0);
295
+ } catch (error) {
296
+ console.error(`${colors.red}❌ Error: ${error.message}${colors.reset}`);
297
+ process.exit(1);
298
+ }
299
+ }
300
+
301
+ // Run CLI
302
+ main();
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MORPH-SPEC Semantic Agent Detection (LLM-based)
5
+ *
6
+ * Uses LLM analysis to detect which agents should be activated based on user request.
7
+ * More accurate than keyword matching - understands context and implicit needs.
8
+ *
9
+ * Usage:
10
+ * export ANTHROPIC_API_KEY=sk-...
11
+ * node bin/semantic-detect-agents.js "user request here"
12
+ *
13
+ * Falls back to keyword-based detection if API key not available.
14
+ */
15
+
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import { fileURLToPath } from 'url';
19
+ import { execSync } from 'child_process';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+
24
+ // ANSI colors
25
+ const colors = {
26
+ green: '\x1b[32m',
27
+ red: '\x1b[31m',
28
+ yellow: '\x1b[33m',
29
+ cyan: '\x1b[36m',
30
+ gray: '\x1b[90m',
31
+ reset: '\x1b[0m'
32
+ };
33
+
34
+ /**
35
+ * Call Anthropic Claude API for semantic analysis
36
+ */
37
+ async function callClaudeAPI(userRequest, agentDescriptions) {
38
+ const apiKey = process.env.ANTHROPIC_API_KEY;
39
+
40
+ if (!apiKey) {
41
+ return null; // Will fall back to keyword detection
42
+ }
43
+
44
+ const prompt = `You are an expert at analyzing software development requests and determining which specialist agents are needed.
45
+
46
+ Given this user request:
47
+ "${userRequest}"
48
+
49
+ And these available specialist agents:
50
+ ${agentDescriptions}
51
+
52
+ Analyze the request and determine which specialist agents should be activated. Consider:
53
+ - Explicit mentions (e.g., "create a dashboard" → UI/UX Designer)
54
+ - Implicit needs (e.g., "show data to users" → UI/UX Designer)
55
+ - Technical requirements (e.g., "scheduled tasks" → Hangfire Orchestrator)
56
+ - Architecture patterns (e.g., "RAG pipeline" → AI System Architect)
57
+
58
+ Respond with ONLY a JSON array of agent IDs, like:
59
+ ["uiux-designer", "hangfire-orchestrator"]
60
+
61
+ If no specialists are needed, respond with: []
62
+ Do not include core agents (they are always active).`;
63
+
64
+ try {
65
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ 'x-api-key': apiKey,
70
+ 'anthropic-version': '2023-06-01'
71
+ },
72
+ body: JSON.stringify({
73
+ model: 'claude-3-haiku-20240307',
74
+ max_tokens: 200,
75
+ messages: [{
76
+ role: 'user',
77
+ content: prompt
78
+ }]
79
+ })
80
+ });
81
+
82
+ if (!response.ok) {
83
+ const error = await response.text();
84
+ console.error(`${colors.yellow}⚠ Claude API error: ${error}${colors.reset}`);
85
+ return null;
86
+ }
87
+
88
+ const data = await response.json();
89
+ const content = data.content[0].text.trim();
90
+
91
+ // Parse JSON response
92
+ const agentIds = JSON.parse(content);
93
+ return agentIds;
94
+ } catch (error) {
95
+ console.error(`${colors.yellow}⚠ Claude API failed: ${error.message}${colors.reset}`);
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Fallback: keyword-based detection (existing behavior)
102
+ */
103
+ function keywordBasedDetection(userRequest) {
104
+ console.log(`${colors.yellow}ℹ Using keyword-based detection (fallback)${colors.reset}`);
105
+
106
+ try {
107
+ const result = execSync(`node bin/detect-agents.js "${userRequest}" --json`, {
108
+ encoding: 'utf-8',
109
+ cwd: process.cwd()
110
+ });
111
+
112
+ const parsed = JSON.parse(result);
113
+ return parsed.specialists || [];
114
+ } catch (error) {
115
+ console.error(`${colors.red}✗ Fallback detection failed: ${error.message}${colors.reset}`);
116
+ return [];
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Load agent descriptions for LLM context
122
+ */
123
+ function loadAgentDescriptions() {
124
+ const agentsPath = path.join(process.cwd(), 'content/.morph/config/agents.json');
125
+
126
+ if (!fs.existsSync(agentsPath)) {
127
+ throw new Error('agents.json not found');
128
+ }
129
+
130
+ const agents = JSON.parse(fs.readFileSync(agentsPath, 'utf-8'));
131
+ const specialists = agents.agents.specialists || [];
132
+
133
+ return specialists.map(agent =>
134
+ `- ${agent.id}: ${agent.description}`
135
+ ).join('\n');
136
+ }
137
+
138
+ /**
139
+ * Print detection results
140
+ */
141
+ function printResults(agentIds, method, userRequest) {
142
+ console.log(`\n${colors.cyan}╔════════════════════════════════════════════════╗${colors.reset}`);
143
+ console.log(`${colors.cyan}║ MORPH-SPEC SEMANTIC AGENT DETECTION ║${colors.reset}`);
144
+ console.log(`${colors.cyan}╚════════════════════════════════════════════════╝${colors.reset}\n`);
145
+
146
+ console.log(`${colors.gray}Request:${colors.reset} "${userRequest}"`);
147
+ console.log(`${colors.gray}Method:${colors.reset} ${method}`);
148
+ console.log(`${colors.gray}Agents Detected:${colors.reset} ${agentIds.length}`);
149
+
150
+ if (agentIds.length > 0) {
151
+ console.log('');
152
+ agentIds.forEach(id => {
153
+ console.log(` ${colors.green}✓${colors.reset} ${id}`);
154
+ });
155
+ } else {
156
+ console.log(`\n ${colors.gray}(No specialist agents needed)${colors.reset}`);
157
+ }
158
+
159
+ console.log('');
160
+ }
161
+
162
+ /**
163
+ * Print help
164
+ */
165
+ function printHelp() {
166
+ console.log(`
167
+ ${colors.cyan}MORPH-SPEC Semantic Agent Detection${colors.reset}
168
+
169
+ Uses LLM analysis to detect which agents should be activated.
170
+ More accurate than keyword matching - understands context.
171
+
172
+ ${colors.green}Setup:${colors.reset}
173
+ export ANTHROPIC_API_KEY=sk-ant-...
174
+
175
+ ${colors.green}Usage:${colors.reset}
176
+ node bin/semantic-detect-agents.js "user request here"
177
+
178
+ ${colors.green}Examples:${colors.reset}
179
+ # Detects UI/UX Designer (implicit need)
180
+ node bin/semantic-detect-agents.js "sistema que mostra dados ao usuário"
181
+
182
+ # Detects Hangfire Orchestrator
183
+ node bin/semantic-detect-agents.js "processar arquivos todo dia às 3h"
184
+
185
+ # Detects AI System Architect
186
+ node bin/semantic-detect-agents.js "pipeline de RAG com chunking"
187
+
188
+ ${colors.green}Fallback:${colors.reset}
189
+ If ANTHROPIC_API_KEY is not set, falls back to keyword-based detection.
190
+
191
+ ${colors.green}Benefits:${colors.reset}
192
+ ✓ Understands context ("show data to users" → UI)
193
+ ✓ Works in any language (not just keywords)
194
+ ✓ Eliminates false positives/negatives
195
+ ✓ Detects implicit needs
196
+
197
+ ${colors.green}Output:${colors.reset}
198
+ Prints detected agent IDs, one per line, for use in scripts.
199
+ `);
200
+ }
201
+
202
+ /**
203
+ * Main function
204
+ */
205
+ async function main() {
206
+ const args = process.argv.slice(2);
207
+
208
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
209
+ printHelp();
210
+ process.exit(args.length === 0 ? 1 : 0);
211
+ }
212
+
213
+ const userRequest = args.join(' ');
214
+ const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
215
+
216
+ let agentIds;
217
+ let method;
218
+
219
+ if (hasApiKey) {
220
+ method = `${colors.green}LLM-based (Claude Haiku)${colors.reset}`;
221
+ const agentDescriptions = loadAgentDescriptions();
222
+ agentIds = await callClaudeAPI(userRequest, agentDescriptions);
223
+
224
+ if (!agentIds) {
225
+ // API failed, fall back
226
+ method = `${colors.yellow}Keyword-based (API fallback)${colors.reset}`;
227
+ agentIds = keywordBasedDetection(userRequest);
228
+ }
229
+ } else {
230
+ method = `${colors.yellow}Keyword-based (no API key)${colors.reset}`;
231
+ agentIds = keywordBasedDetection(userRequest);
232
+ }
233
+
234
+ printResults(agentIds, method, userRequest);
235
+
236
+ // Output for scripting (one ID per line)
237
+ if (args.includes('--quiet') || args.includes('-q')) {
238
+ console.clear();
239
+ agentIds.forEach(id => console.log(id));
240
+ }
241
+ }
242
+
243
+ main().catch(error => {
244
+ console.error(`${colors.red}✗ Error: ${error.message}${colors.reset}`);
245
+ process.exit(1);
246
+ });