@polymorphism-tech/morph-spec 4.21.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CLAUDE.md +57 -71
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +224 -491
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +55 -38
  8. package/framework/MORPH.md +244 -0
  9. package/framework/agents.json +127 -1183
  10. package/framework/commands/morph-apply.md +56 -240
  11. package/framework/commands/morph-archive.md +48 -85
  12. package/framework/commands/morph-preflight.md +7 -7
  13. package/framework/commands/morph-proposal.md +35 -191
  14. package/framework/commands/morph-status.md +19 -50
  15. package/framework/commands/morph-troubleshoot.md +2 -2
  16. package/framework/evals/rubrics/README.md +44 -0
  17. package/framework/evals/rubrics/architecture.md +46 -0
  18. package/framework/evals/rubrics/code-quality.md +50 -0
  19. package/framework/evals/rubrics/contracts.md +47 -0
  20. package/framework/evals/rubrics/test-coverage.md +50 -0
  21. package/framework/evals/scenarios/dotnet-crud.yaml +42 -0
  22. package/framework/evals/scenarios/nextjs-auth.yaml +38 -0
  23. package/framework/evals/suites/standard.yaml +5 -0
  24. package/framework/evals/trace-schema.md +67 -0
  25. package/framework/hooks/claude-code/core/README.md +3 -0
  26. package/framework/hooks/claude-code/core/denylist.json +14 -0
  27. package/framework/hooks/claude-code/{pre-tool-use → core}/enforce-phase-writes.js +5 -5
  28. package/framework/hooks/claude-code/core/post-edit-typecheck.js +69 -0
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +29 -0
  30. package/framework/hooks/claude-code/{pre-tool-use → core}/protect-spec-files.js +4 -6
  31. package/framework/hooks/claude-code/{stop → core}/validate-completion.js +13 -82
  32. package/framework/hooks/shared/phase-utils.js +20 -28
  33. package/framework/hooks/shared/state-reader.js +4 -5
  34. package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -210
  35. package/framework/phases.json +40 -114
  36. package/framework/rubrics/eval-rubric.json +66 -0
  37. package/framework/skills/README.md +39 -64
  38. package/framework/skills/core/README.md +3 -0
  39. package/framework/skills/core/morph-brainstorming/SKILL.md +102 -0
  40. package/framework/skills/core/morph-eval/SKILL.md +57 -0
  41. package/framework/skills/core/morph-implement/SKILL.md +83 -0
  42. package/framework/skills/core/morph-plan/SKILL.md +64 -0
  43. package/framework/skills/core/morph-review/SKILL.md +62 -0
  44. package/framework/skills/core/morph-stack-scan/SKILL.md +109 -0
  45. package/framework/skills/core/morph-standards/SKILL.md +49 -0
  46. package/framework/standards/STANDARDS.json +32 -0
  47. package/framework/standards/frontend/design-system/ai-image-generation.md +244 -0
  48. package/framework/standards/integration/mcp/mcp-tools.md +48 -0
  49. package/framework/templates/CLAUDE.md.core.guide.md +29 -0
  50. package/framework/templates/CLAUDE.md.core.template +22 -0
  51. package/framework/templates/REGISTRY.json +4 -4
  52. package/framework/workflows/configs/fast-track.json +5 -10
  53. package/framework/workflows/configs/full.json +4 -8
  54. package/framework/workflows/configs/quick.json +32 -0
  55. package/framework/workflows/configs/standard.json +57 -64
  56. package/package.json +7 -6
  57. package/src/commands/mcp/mcp-setup.js +58 -31
  58. package/src/commands/project/doctor.js +69 -70
  59. package/src/commands/project/init.js +26 -5
  60. package/src/commands/project/status.js +18 -19
  61. package/src/commands/state/approve.js +1 -1
  62. package/src/commands/validation/validate-feature.js +18 -112
  63. package/src/core/paths/output-schema.js +133 -151
  64. package/src/core/state/phase-state-machine.js +10 -24
  65. package/src/core/state/state-manager.js +17 -17
  66. package/src/core/workflows/workflow-detector.js +214 -31
  67. package/src/core/workflows/workflow-router.js +204 -0
  68. package/src/dashboard/app/pages/AgentMap.jsx +32 -33
  69. package/src/dashboard/app/pages/LiveState.jsx +1 -1
  70. package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
  71. package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
  72. package/src/dashboard/app/pages/Workflows.jsx +2 -2
  73. package/src/dashboard/scanner/agents-scanner.js +30 -35
  74. package/src/lib/agents/metrics-aggregator.js +1 -1
  75. package/src/lib/agents/output-collector.js +7 -8
  76. package/src/lib/context/context-builder.js +289 -0
  77. package/src/lib/detectors/claude-config-detector.js +19 -3
  78. package/src/lib/generators/context-generator.js +20 -42
  79. package/src/lib/generators/index.js +1 -0
  80. package/src/lib/generators/mandate-generator.js +512 -0
  81. package/src/lib/generators/metadata-extractor.js +1 -1
  82. package/src/lib/generators/recap-generator.js +3 -4
  83. package/src/lib/installers/mcp-installer.js +300 -92
  84. package/src/lib/phase-chain/phase-validator.js +36 -40
  85. package/src/lib/stack/stack-profile.js +74 -91
  86. package/src/lib/tasks/task-parser.js +1 -1
  87. package/src/lib/tasks/wave-resolver.js +353 -0
  88. package/src/lib/validators/checkpoint-persistence.js +2 -2
  89. package/src/lib/validators/content/cross-phase-validator.js +281 -0
  90. package/src/lib/validators/content/index.js +1 -0
  91. package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
  92. package/src/scripts/global-install.js +2 -27
  93. package/src/utils/agents-installer.js +224 -447
  94. package/src/utils/claude-settings-manager.js +57 -15
  95. package/src/utils/file-copier.js +1 -0
  96. package/src/utils/hooks-installer.js +15 -117
  97. package/src/utils/skills-installer.js +12 -18
  98. package/bin/detect-agents.js +0 -226
  99. package/bin/task-manager.js +0 -1036
  100. package/framework/agents/README.md +0 -13
  101. package/framework/agents/ai-agents/ai-system-architect.md +0 -194
  102. package/framework/agents/ai-agents/vector-search-expert.md +0 -247
  103. package/framework/agents/architecture/architecture-expert.md +0 -98
  104. package/framework/agents/architecture/morph-spec-architect.md +0 -66
  105. package/framework/agents/architecture/packages-validator.md +0 -89
  106. package/framework/agents/architecture/security-expert.md +0 -79
  107. package/framework/agents/architecture/seo-growth-hacker.md +0 -168
  108. package/framework/agents/architecture/standards-architect.md +0 -153
  109. package/framework/agents/backend/dotnet-senior.md +0 -163
  110. package/framework/agents/backend/ef-modeler.md +0 -119
  111. package/framework/agents/backend/hangfire-orchestrator.md +0 -133
  112. package/framework/agents/backend/ms-agent-expert.md +0 -55
  113. package/framework/agents/backend/neon-expert.md +0 -379
  114. package/framework/agents/frontend/css-specialist.md +0 -89
  115. package/framework/agents/frontend/design-system-validator.md +0 -100
  116. package/framework/agents/frontend/gsap-animator.md +0 -131
  117. package/framework/agents/frontend/nextjs-component-validator.md +0 -97
  118. package/framework/agents/frontend/nextjs-expert.md +0 -201
  119. package/framework/agents/frontend/ui-designer.md +0 -259
  120. package/framework/agents/infrastructure/infra-architect.md +0 -215
  121. package/framework/agents/integrations/asaas-financial.md +0 -138
  122. package/framework/agents/integrations/neon-auth.md +0 -151
  123. package/framework/agents/integrations/resend-email.md +0 -127
  124. package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
  125. package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
  126. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
  127. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
  128. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
  129. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
  130. package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
  131. package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
  132. package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
  133. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
  134. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
  135. package/framework/hooks/shared/compact-restore.js +0 -123
  136. package/framework/hooks/shared/dispatch-helpers.js +0 -147
  137. package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
  138. package/framework/hooks/shared/stale-task-reset.js +0 -57
  139. package/framework/hooks/shared/worktree-helpers.js +0 -75
  140. package/framework/skills/level-0-meta/README.md +0 -7
  141. package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
  142. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
  143. package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
  144. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
  145. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
  146. package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
  147. package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
  148. package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
  149. package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
  150. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
  151. package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
  152. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
  153. package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
  154. package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
  155. package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
  156. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -382
  157. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
  158. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
  159. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
  160. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
  161. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
  162. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
  163. package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
  164. package/framework/skills/level-1-workflows/README.md +0 -7
  165. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
  166. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
  167. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
  168. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
  169. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
  170. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  171. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -493
  172. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
  173. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -46
  174. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
  175. package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
  176. package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
  177. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
  178. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  179. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
  180. package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
  181. package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
  182. package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
  183. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -788
  184. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
  185. package/framework/squad-templates/backend-only.json +0 -47
  186. package/framework/squad-templates/frontend-only.json +0 -56
  187. package/framework/squad-templates/full-stack.json +0 -74
  188. package/src/commands/agents/agent-metrics.js +0 -174
  189. package/src/commands/agents/audit-dispatch.js +0 -82
  190. package/src/commands/agents/detect-agents.js +0 -275
  191. package/src/commands/agents/dispatch-agents.js +0 -461
  192. package/src/commands/agents/index.js +0 -13
  193. package/src/commands/agents/manage-agents.js +0 -352
  194. package/src/commands/agents/project-metrics.js +0 -74
  195. package/src/commands/phase/phase-reset.js +0 -74
  196. package/src/commands/scope/escalate.js +0 -217
  197. package/src/commands/state/advance-phase.js +0 -600
  198. package/src/commands/state/phase-runner.js +0 -215
  199. package/src/commands/state/state.js +0 -444
  200. package/src/commands/task/expand.js +0 -100
  201. package/src/commands/tasks/index.js +0 -4
  202. package/src/commands/tasks/task.js +0 -105
  203. package/src/commands/templates/index.js +0 -5
  204. package/src/commands/templates/template-render.js +0 -93
  205. package/src/commands/trust/trust.js +0 -118
  206. package/src/lib/agents/agents-manager.js +0 -349
  207. package/src/lib/build/build-runner.js +0 -99
  208. package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
  209. package/src/lib/contracts/sprint-contract-generator.js +0 -137
  210. package/src/lib/scope/impact-analyzer.js +0 -106
  211. package/src/lib/trust/trust-manager.js +0 -157
  212. package/src/utils/tier-helpers.js +0 -24
  213. /package/framework/hooks/claude-code/{post-tool-use → core}/loop-detect.js +0 -0
@@ -9,8 +9,8 @@
9
9
  * @module workflow-detector
10
10
  */
11
11
 
12
- import { readFileSync, existsSync, readdirSync } from 'fs';
13
- import { join, dirname } from 'path';
12
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
13
+ import { join, dirname, basename } from 'path';
14
14
  import { fileURLToPath } from 'url';
15
15
  import { getPhaseSequence } from '../state/phase-state-machine.js';
16
16
 
@@ -221,6 +221,153 @@ function matchComplexity(complexity, criteria) {
221
221
  return { matches: meetsAll, score };
222
222
  }
223
223
 
224
+ // Directories to skip during codebase scanning
225
+ const SCAN_SKIP_DIRS = new Set([
226
+ 'node_modules', '.git', '.morph', 'dist', 'out', '.next',
227
+ 'obj', 'bin', '.worktrees', '.vs', '.vscode', 'coverage'
228
+ ]);
229
+
230
+ /**
231
+ * Quick codebase scan to estimate change complexity from actual files.
232
+ * Reads only filenames (not content). Capped at 2000 files.
233
+ *
234
+ * @param {string} projectPath - Project root
235
+ * @param {string[]} keywords - Keywords extracted from user request
236
+ * @returns {{ affectedFiles: number, hasSharedInfra: boolean, hasTests: boolean, score: number }}
237
+ */
238
+ export function quickCodebaseScan(projectPath, keywords) {
239
+ if (!projectPath || projectPath === '.' || !existsSync(projectPath)) {
240
+ return { affectedFiles: 0, hasSharedInfra: false, hasTests: false, score: 0.5 };
241
+ }
242
+
243
+ const infraPatterns = ['dockerfile', 'docker-compose', '.bicep', '.tf', 'appsettings.json', 'terraform'];
244
+ const testPatterns = ['.test.', '.spec.', '__tests__'];
245
+ const keywordsLower = keywords.map(k => k.toLowerCase());
246
+
247
+ let filesExamined = 0;
248
+ let affectedFiles = 0;
249
+ let hasSharedInfra = false;
250
+ let hasTests = false;
251
+ const MAX_FILES = 2000;
252
+
253
+ function walk(dir) {
254
+ if (filesExamined >= MAX_FILES) return;
255
+ let entries;
256
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
257
+
258
+ for (const entry of entries) {
259
+ if (filesExamined >= MAX_FILES) return;
260
+ if (entry.isDirectory()) {
261
+ if (!SCAN_SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
262
+ continue;
263
+ }
264
+ filesExamined++;
265
+ const nameLower = entry.name.toLowerCase();
266
+
267
+ if (infraPatterns.some(p => nameLower.includes(p))) hasSharedInfra = true;
268
+ if (testPatterns.some(p => nameLower.includes(p))) hasTests = true;
269
+ if (keywordsLower.some(k => k.length >= 3 && nameLower.includes(k))) affectedFiles++;
270
+ }
271
+ }
272
+
273
+ walk(projectPath);
274
+
275
+ // Score: more affected files = higher complexity
276
+ let score = 0.5;
277
+ if (affectedFiles === 0) score = 0.3;
278
+ else if (affectedFiles <= 3) score = 0.4;
279
+ else if (affectedFiles <= 8) score = 0.6;
280
+ else if (affectedFiles <= 15) score = 0.75;
281
+ else score = 0.9;
282
+
283
+ if (hasSharedInfra) score = Math.min(1, score + 0.1);
284
+
285
+ return { affectedFiles, hasSharedInfra, hasTests, score };
286
+ }
287
+
288
+ /**
289
+ * Find completed features similar to the current description.
290
+ * Uses Jaccard similarity on tokenized keywords.
291
+ *
292
+ * @param {string} userRequest - Current feature description
293
+ * @param {string} projectPath - Project root (for loading state)
294
+ * @returns {{ similarFeatures: Array<{name: string, workflow: string, similarity: number}>, suggestedWorkflow: string|null, score: number }}
295
+ */
296
+ export function findSimilarFeatures(userRequest, projectPath) {
297
+ const neutral = { similarFeatures: [], suggestedWorkflow: null, score: 0.5 };
298
+
299
+ let state;
300
+ try {
301
+ const statePath = join(projectPath, '.morph', 'state.json');
302
+ if (!existsSync(statePath)) return neutral;
303
+ state = JSON.parse(readFileSync(statePath, 'utf8'));
304
+ } catch { return neutral; }
305
+
306
+ if (!state?.features) return neutral;
307
+
308
+ const requestTokens = new Set(
309
+ userRequest.toLowerCase().split(/[\s\-_.,;:!?]+/).filter(t => t.length >= 3)
310
+ );
311
+ if (requestTokens.size === 0) return neutral;
312
+
313
+ const candidates = [];
314
+
315
+ for (const [name, feature] of Object.entries(state.features)) {
316
+ if (feature.status !== 'completed') continue;
317
+ if (!feature.workflow || feature.workflow === 'auto') continue;
318
+
319
+ // Build token set from feature name + matched keywords
320
+ const featureTokens = new Set(
321
+ name.toLowerCase().split(/[\s\-_.,;:!?]+/).filter(t => t.length >= 3)
322
+ );
323
+ if (feature.workflowDetection?.matchedKeywords) {
324
+ for (const kw of feature.workflowDetection.matchedKeywords) {
325
+ kw.toLowerCase().split(/[\s\-_]+/).filter(t => t.length >= 3).forEach(t => featureTokens.add(t));
326
+ }
327
+ }
328
+
329
+ // Jaccard similarity
330
+ const intersection = [...requestTokens].filter(t => featureTokens.has(t)).length;
331
+ const union = new Set([...requestTokens, ...featureTokens]).size;
332
+ const similarity = union > 0 ? intersection / union : 0;
333
+
334
+ if (similarity > 0.3) {
335
+ candidates.push({ name, workflow: feature.workflow, similarity });
336
+ }
337
+ }
338
+
339
+ if (candidates.length === 0) return neutral;
340
+
341
+ candidates.sort((a, b) => b.similarity - a.similarity);
342
+ const top = candidates.slice(0, 3);
343
+
344
+ // Suggest the workflow used by the most similar feature
345
+ const suggestedWorkflow = top[0].workflow;
346
+
347
+ // Score: higher similarity = more confidence in the suggestion
348
+ const score = Math.min(1, top[0].similarity + 0.2);
349
+
350
+ return { similarFeatures: top, suggestedWorkflow, score };
351
+ }
352
+
353
+ /**
354
+ * Score based on description length and structure.
355
+ * Short descriptions suggest simple tasks, long ones suggest complex.
356
+ *
357
+ * @param {string} userRequest
358
+ * @returns {number} 0-1 score where higher = more complex
359
+ */
360
+ export function scoreDescriptionLength(userRequest) {
361
+ const len = userRequest.trim().length;
362
+ const sentences = userRequest.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
363
+
364
+ if (len < 30) return 0.2;
365
+ if (len < 80) return 0.35;
366
+ if (len < 150) return 0.5;
367
+ if (len < 300 && sentences <= 3) return 0.65;
368
+ return 0.8;
369
+ }
370
+
224
371
  /**
225
372
  * Detect workflow for a feature request
226
373
  * @param {Object} options - Detection options
@@ -254,6 +401,14 @@ export async function detectWorkflow(options) {
254
401
  // Estimate complexity
255
402
  const complexity = estimateComplexity(userRequest);
256
403
 
404
+ // New signals: codebase scan + feature history + description length
405
+ const codebaseScan = quickCodebaseScan(projectPath, complexity.hasInfra
406
+ ? [...userRequest.toLowerCase().split(/\s+/).filter(w => w.length >= 3)]
407
+ : userRequest.toLowerCase().split(/\s+/).filter(w => w.length >= 3)
408
+ );
409
+ const featureHistory = findSimilarFeatures(userRequest, projectPath);
410
+ const descriptionScore = scoreDescriptionLength(userRequest);
411
+
257
412
  // Score each workflow
258
413
  const scores = [];
259
414
 
@@ -261,30 +416,53 @@ export async function detectWorkflow(options) {
261
416
  const keywordMatch = matchKeywords(userRequest, workflow.keywords || []);
262
417
  const complexityMatch = matchComplexity(complexity, workflow.criteria || {});
263
418
 
264
- // Weighted scoring
265
- const keywordWeight = 0.4;
266
- const complexityWeight = 0.3;
267
- const priorityWeight = 0.2;
268
- const contextWeight = 0.1;
419
+ // Enhanced weighted scoring (6 signals)
420
+ const weights = {
421
+ keywords: 0.25,
422
+ complexity: 0.20,
423
+ codebaseAnalysis: 0.20,
424
+ featureHistory: 0.15,
425
+ priority: 0.10,
426
+ descriptionLength: 0.10,
427
+ };
269
428
 
270
429
  // Priority score (lower number = higher priority = higher score)
271
430
  const priorityScore = workflow.priority ? (4 - workflow.priority) / 3 : 0.5;
272
431
 
273
- // Context score (checks for special conditions)
274
- let contextScore = 0.5;
275
- if (workflow.criteria?.hasPrototype && (userRequest.toLowerCase().includes('prototype') || userRequest.toLowerCase().includes('mockup'))) {
276
- contextScore = 1.0;
432
+ // Codebase analysis score: align scan results with workflow expectations
433
+ let codebaseScore = codebaseScan.score;
434
+ if (workflow.criteria?.noInfrastructure && codebaseScan.hasSharedInfra) {
435
+ codebaseScore *= 0.5; // Penalize simple workflows when infra detected
436
+ }
437
+ if (workflow.criteria?.minFiles && codebaseScan.affectedFiles >= workflow.criteria.minFiles) {
438
+ codebaseScore = Math.min(1, codebaseScore + 0.2);
439
+ }
440
+
441
+ // Feature history score: boost if similar features used this workflow
442
+ let historyScore = featureHistory.score;
443
+ if (featureHistory.suggestedWorkflow === workflow.id) {
444
+ historyScore = Math.min(1, historyScore + 0.3);
445
+ } else if (featureHistory.suggestedWorkflow && featureHistory.suggestedWorkflow !== workflow.id) {
446
+ historyScore *= 0.6;
447
+ }
448
+
449
+ // Description length score: align with workflow complexity expectations
450
+ let descScore = descriptionScore;
451
+ if (workflow.priority === 1 && descriptionScore > 0.6) {
452
+ descScore *= 0.7; // Long descriptions less likely to be fast-track
277
453
  }
278
- if (workflow.criteria?.hasExistingPages && userRequest.toLowerCase().includes('existing')) {
279
- contextScore = 1.0;
454
+ if (workflow.priority === 3 && descriptionScore < 0.4) {
455
+ descScore *= 0.7; // Short descriptions less likely to be full
280
456
  }
281
457
 
282
458
  // Calculate total score
283
459
  let totalScore =
284
- (keywordMatch.score * keywordWeight) +
285
- (complexityMatch.score * complexityWeight) +
286
- (priorityScore * priorityWeight) +
287
- (contextScore * contextWeight);
460
+ (keywordMatch.score * weights.keywords) +
461
+ (complexityMatch.score * weights.complexity) +
462
+ (codebaseScore * weights.codebaseAnalysis) +
463
+ (historyScore * weights.featureHistory) +
464
+ (priorityScore * weights.priority) +
465
+ (descScore * weights.descriptionLength);
288
466
 
289
467
  // Penalty for workflows with no keyword matches
290
468
  if (keywordMatch.matched.length === 0) {
@@ -335,13 +513,26 @@ export async function detectWorkflow(options) {
335
513
  reason: `${w.workflowName} (${Math.round(w.score * 100)}% match)`
336
514
  }));
337
515
 
516
+ // Warnings for low confidence or ambiguous results
517
+ const warnings = [];
518
+ if (confidence < 0.4) {
519
+ warnings.push('Low confidence — consider specifying workflow explicitly with --workflow');
520
+ }
521
+ if (alternatives.length > 0 && alternatives[0].confidence > confidence * 0.8) {
522
+ warnings.push(`Close alternative: ${alternatives[0].workflowId} (${Math.round(alternatives[0].confidence * 100)}%) — verify recommendation`);
523
+ }
524
+ if (featureHistory.similarFeatures.length > 0 && featureHistory.suggestedWorkflow !== topResult.workflowId) {
525
+ warnings.push(`Similar features used ${featureHistory.suggestedWorkflow} workflow`);
526
+ }
527
+
338
528
  return {
339
529
  workflowId: topResult.workflowId,
340
530
  confidence,
341
531
  matchedKeywords: topResult.keywordMatch,
342
532
  estimatedComplexity: complexity,
343
533
  reasoning: reasoning.join(' + '),
344
- alternativeWorkflows: alternatives
534
+ alternativeWorkflows: alternatives,
535
+ warnings
345
536
  };
346
537
  }
347
538
 
@@ -387,17 +578,9 @@ export function listWorkflows(projectPath = '.') {
387
578
  * @param {string} feature
388
579
  * @returns {Promise<boolean>}
389
580
  */
390
- export async function checkZeroTouchEligibility(feature) {
391
- try {
392
- const { getTrust } = await import('../../lib/trust/trust-manager.js').catch(() => ({
393
- getTrust: null
394
- }));
395
-
396
- if (!getTrust) return false;
397
-
398
- const trust = getTrust(feature);
399
- return trust.level === 'auto';
400
- } catch {
401
- return false;
402
- }
581
+ export async function checkZeroTouchEligibility(/* feature */) {
582
+ // v5: trust manager removed. The autonomy contract (MORPH.md) replaces it —
583
+ // Claude advances autonomously between gates. Returning false keeps any
584
+ // legacy callers happy without granting unintended privileges.
585
+ return false;
403
586
  }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @fileoverview Natural Language Workflow Router
3
+ *
4
+ * Classifies free-text input into morph-spec intents and routes to the
5
+ * appropriate command. Uses pattern matching for intent detection and
6
+ * delegates to detectWorkflow() for proposal classification.
7
+ *
8
+ * @module workflow-router
9
+ */
10
+
11
+ import { detectWorkflow } from './workflow-detector.js';
12
+ import { readFileSync, existsSync } from 'fs';
13
+ import { join } from 'path';
14
+
15
+ /**
16
+ * @typedef {Object} RoutedIntent
17
+ * @property {string} intent - 'proposal' | 'status' | 'approve' | 'fast-track'
18
+ * @property {string} command - The morph-spec command to execute
19
+ * @property {string[]} args - Arguments for the command
20
+ * @property {number} confidence - 0-1
21
+ * @property {string} explanation - Human-readable reason for routing decision
22
+ * @property {Object} [workflowDetection] - If intent is proposal/fast-track, includes detection result
23
+ */
24
+
25
+ const INTENT_PATTERNS = [
26
+ // Status queries (highest priority)
27
+ { pattern: /^(status|progress|where\s+am\s+i)\b/i, intent: 'status', confidence: 0.9 },
28
+ { pattern: /what('s|\s+is)\s+(next|done|left|remaining)/i, intent: 'status', confidence: 0.85 },
29
+ { pattern: /what\s+tasks/i, intent: 'status', confidence: 0.85 },
30
+ { pattern: /tasks?\s+(left|remaining|pending|todo)/i, intent: 'status', confidence: 0.85 },
31
+ { pattern: /^show\s+(me\s+)?status/i, intent: 'status', confidence: 0.9 },
32
+
33
+ // Approval actions
34
+ { pattern: /^(approve|lgtm|looks?\s+good|ship\s+it)\b/i, intent: 'approve', confidence: 0.85 },
35
+
36
+ // Explicit simple/fix intents → fast-track proposal
37
+ { pattern: /^(fix|typo|rename|toggle|bump|config)\b/i, intent: 'fast-track', confidence: 0.75 },
38
+ { pattern: /\b(typo|spelling|label|icon|badge|tooltip|css\s+fix)\b/i, intent: 'fast-track', confidence: 0.7 },
39
+
40
+ // Everything else → proposal (further classified by detectWorkflow)
41
+ { pattern: /.+/, intent: 'proposal', confidence: 0.5 },
42
+ ];
43
+
44
+ /**
45
+ * Route natural language to the best morph-spec command.
46
+ *
47
+ * @param {string} input - User's natural language description
48
+ * @param {Object} [options]
49
+ * @param {string} [options.projectPath='.'] - Project path for codebase analysis
50
+ * @param {string} [options.activeFeature] - Currently active feature name (for status/approve)
51
+ * @returns {Promise<RoutedIntent>}
52
+ */
53
+ export async function routeIntent(input, options = {}) {
54
+ const { projectPath = '.' } = options;
55
+ const trimmed = (input || '').trim();
56
+
57
+ if (!trimmed) {
58
+ return {
59
+ intent: 'proposal',
60
+ command: 'morph-spec',
61
+ args: ['--help'],
62
+ confidence: 0,
63
+ explanation: 'Empty input — showing help'
64
+ };
65
+ }
66
+
67
+ // Match against patterns in order
68
+ let matchedIntent = null;
69
+ for (const { pattern, intent, confidence } of INTENT_PATTERNS) {
70
+ if (pattern.test(trimmed)) {
71
+ matchedIntent = { intent, confidence };
72
+ break;
73
+ }
74
+ }
75
+
76
+ const activeFeature = options.activeFeature || findActiveFeature(projectPath);
77
+
78
+ // Route based on intent
79
+ switch (matchedIntent.intent) {
80
+ case 'status': {
81
+ const feature = activeFeature || '<feature>';
82
+ return {
83
+ intent: 'status',
84
+ command: 'morph-spec',
85
+ args: ['status', feature],
86
+ confidence: activeFeature ? matchedIntent.confidence : matchedIntent.confidence * 0.6,
87
+ explanation: activeFeature
88
+ ? `Status query for active feature "${feature}"`
89
+ : 'Status query — no active feature found'
90
+ };
91
+ }
92
+
93
+ case 'approve': {
94
+ const feature = activeFeature || '<feature>';
95
+ return {
96
+ intent: 'approve',
97
+ command: 'morph-spec',
98
+ args: ['approve', feature],
99
+ confidence: activeFeature ? matchedIntent.confidence : matchedIntent.confidence * 0.5,
100
+ explanation: activeFeature
101
+ ? `Approve active feature "${feature}"`
102
+ : 'Approve — no active feature found'
103
+ };
104
+ }
105
+
106
+ case 'fast-track': {
107
+ // Validate with detectWorkflow — regex might be too eager
108
+ const detection = await detectWorkflow({
109
+ userRequest: trimmed,
110
+ projectPath,
111
+ });
112
+
113
+ // If detectWorkflow disagrees, defer to it
114
+ if (detection.workflowId !== 'fast-track' && detection.confidence > matchedIntent.confidence) {
115
+ const slug = slugify(trimmed);
116
+ return {
117
+ intent: 'proposal',
118
+ command: 'morph-spec',
119
+ args: ['state', 'init', slug, '--user-request', trimmed],
120
+ confidence: detection.confidence,
121
+ explanation: `Detected as ${detection.workflowId} (${detection.reasoning})`,
122
+ workflowDetection: detection
123
+ };
124
+ }
125
+
126
+ const slug = slugify(trimmed);
127
+ return {
128
+ intent: 'fast-track',
129
+ command: 'morph-spec',
130
+ args: ['state', 'init', slug, '--workflow', 'fast-track', '--user-request', trimmed],
131
+ confidence: Math.max(matchedIntent.confidence, detection.confidence),
132
+ explanation: `Simple task → fast-track workflow (${detection.reasoning})`,
133
+ workflowDetection: detection
134
+ };
135
+ }
136
+
137
+ case 'proposal':
138
+ default: {
139
+ const detection = await detectWorkflow({
140
+ userRequest: trimmed,
141
+ projectPath,
142
+ });
143
+
144
+ const slug = slugify(trimmed);
145
+ const args = ['state', 'init', slug, '--user-request', trimmed];
146
+ if (detection.workflowId !== 'auto') {
147
+ args.push('--workflow', detection.workflowId);
148
+ }
149
+
150
+ return {
151
+ intent: 'proposal',
152
+ command: 'morph-spec',
153
+ args,
154
+ confidence: detection.confidence,
155
+ explanation: `New feature → ${detection.workflowId} workflow (${detection.reasoning})`,
156
+ workflowDetection: detection
157
+ };
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Find the currently active feature from state.json.
164
+ * Returns the most recently updated feature.
165
+ *
166
+ * @param {string} [projectPath='.']
167
+ * @returns {string|null} Feature name or null
168
+ */
169
+ export function findActiveFeature(projectPath = '.') {
170
+ try {
171
+ const statePath = join(projectPath, '.morph', 'state.json');
172
+ if (!existsSync(statePath)) return null;
173
+
174
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
175
+ if (!state?.features) return null;
176
+
177
+ const entries = Object.entries(state.features)
178
+ .sort((a, b) => {
179
+ const dateA = a[1].updatedAt || a[1].createdAt || '';
180
+ const dateB = b[1].updatedAt || b[1].createdAt || '';
181
+ return dateB.localeCompare(dateA);
182
+ });
183
+
184
+ return entries.length > 0 ? entries[0][0] : null;
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Convert a description into a feature slug.
192
+ * @param {string} text
193
+ * @returns {string}
194
+ */
195
+ function slugify(text) {
196
+ return text
197
+ .toLowerCase()
198
+ .replace(/[^a-z0-9\s-]/g, '')
199
+ .trim()
200
+ .replace(/\s+/g, '-')
201
+ .replace(/-+/g, '-')
202
+ .slice(0, 50)
203
+ .replace(/-$/, '');
204
+ }
@@ -7,18 +7,22 @@ import DetailPanel from '../components/DetailPanel.jsx';
7
7
  import SearchFilter from '../components/SearchFilter.jsx';
8
8
  import { useDetailPanel } from '../hooks/useDetailPanel.js';
9
9
 
10
- // Tier color palette: bg color, border color
11
- const TIER_COLORS = {
12
- user: { bg: '#78350f', border: '#f59e0b', label: 'User' },
13
- 1: { bg: '#991b1b', border: '#ef4444', label: 'T1' },
14
- 2: { bg: '#9a3412', border: '#f97316', label: 'T2' },
15
- 3: { bg: '#1e40af', border: '#3b82f6', label: 'T3' },
16
- 4: { bg: '#6d28d9', border: '#8b5cf6', label: 'T4' },
10
+ // Domain persona color palette (v5 — flat, no tiers)
11
+ const PERSONA_COLORS = {
12
+ user: { bg: '#78350f', border: '#f59e0b', label: 'User' },
13
+ 'dotnet-senior':{ bg: '#991b1b', border: '#ef4444', label: 'Backend' },
14
+ 'ef-modeler': { bg: '#7c2d12', border: '#fb923c', label: 'Data' },
15
+ 'maf-expert': { bg: '#1e3a5f', border: '#38bdf8', label: 'Agents' },
16
+ 'nextjs-expert':{ bg: '#064e3b', border: '#34d399', label: 'Frontend' },
17
+ 'ui-designer': { bg: '#4a1d96', border: '#a78bfa', label: 'Design' },
18
+ 'infra-engineer':{ bg: '#1e3a5f', border: '#60a5fa', label: 'Infra' },
19
+ 'evaluator': { bg: '#374151', border: '#9ca3af', label: 'Eval' },
17
20
  };
21
+ const FALLBACK_PERSONA = { bg: '#1e40af', border: '#3b82f6', label: 'Persona' };
18
22
 
19
23
  // Custom AgentNode component
20
24
  function AgentNode({ data }) {
21
- const colors = data.isUser ? TIER_COLORS.user : (TIER_COLORS[data.tier] || TIER_COLORS[4]);
25
+ const colors = data.isUser ? PERSONA_COLORS.user : (PERSONA_COLORS[data.personaId] || FALLBACK_PERSONA);
22
26
  return (
23
27
  <div
24
28
  className="rounded-lg px-3 py-2 min-w-[130px] max-w-[160px] cursor-pointer select-none"
@@ -29,7 +33,7 @@ function AgentNode({ data }) {
29
33
  {data.label}
30
34
  </div>
31
35
  <div className="text-gray-300 text-[10px] mt-0.5 truncate">
32
- {data.isUser ? 'Principal' : `${colors.label} · ${data.group || ''}`}
36
+ {data.isUser ? 'Principal' : `${colors.label} · ${data.domains?.[0] || ''}`}
33
37
  </div>
34
38
  <Handle type="source" position={Position.Bottom} style={{ background: colors.border, width: 8, height: 8 }} />
35
39
  </div>
@@ -58,22 +62,19 @@ function buildLayout(nodes, edges) {
58
62
  // AgentDetail shown inside the panel
59
63
  function AgentDetail({ agent }) {
60
64
  const [showPrompt, setShowPrompt] = useState(false);
61
- const tierColors = TIER_COLORS[agent.tier] || TIER_COLORS[4];
65
+ const personaColors = PERSONA_COLORS[agent.id] || FALLBACK_PERSONA;
62
66
 
63
67
  return (
64
68
  <div className="text-sm text-gray-200 space-y-4">
65
69
  {/* Header */}
66
70
  <div>
67
71
  <div className="flex items-center gap-2 mb-1">
68
- <span className="px-2 py-0.5 rounded text-xs font-bold" style={{ background: tierColors.bg, color: tierColors.border, border: `1px solid ${tierColors.border}` }}>
69
- Tier {agent.tier}
72
+ <span className="px-2 py-0.5 rounded text-xs font-bold" style={{ background: personaColors.bg, color: personaColors.border, border: `1px solid ${personaColors.border}` }}>
73
+ {personaColors.label}
70
74
  </span>
71
- <span className="text-gray-400 text-xs">{agent.group}</span>
75
+ <span className="text-gray-400 text-xs">{agent.domains?.[0] || ''}</span>
72
76
  </div>
73
77
  <p className="text-gray-400 text-xs">ID: <span className="text-gray-300 font-mono">{agent.id}</span></p>
74
- {agent.reportsTo && (
75
- <p className="text-gray-400 text-xs mt-0.5">Reports to: <span className="text-blue-300 font-mono">{agent.reportsTo}</span></p>
76
- )}
77
78
  </div>
78
79
 
79
80
  {/* Active Phases */}
@@ -173,15 +174,15 @@ function AgentDetail({ agent }) {
173
174
  export default function AgentMap() {
174
175
  const { agents } = morphData;
175
176
  const [search, setSearch] = useState('');
176
- const [tierFilter, setTierFilter] = useState('');
177
+ const [domainFilter, setDomainFilter] = useState('');
177
178
  const [groupFilter, setGroupFilter] = useState('');
178
179
  const [phaseFilter, setPhaseFilter] = useState('');
179
180
  const [stackFilter, setStackFilter] = useState('');
180
181
  const { panel, openPanel, closePanel } = useDetailPanel();
181
182
 
182
183
  // Derive unique groups, tiers, phases, and stacks for filters
183
- const allGroups = useMemo(() => [...new Set(agents.list.map((a) => a.group).filter(Boolean))].sort(), [agents.list]);
184
- const allTiers = useMemo(() => [...new Set(agents.list.map((a) => String(a.tier)))].sort(), [agents.list]);
184
+ const allGroups = useMemo(() => [...new Set(agents.list.map((a) => a.domains?.[0]).filter(Boolean))].sort(), [agents.list]);
185
+ const allDomains = useMemo(() => [...new Set(agents.list.flatMap((a) => a.domains || []))].sort(), [agents.list]);
185
186
  const allPhases = useMemo(() => {
186
187
  const phases = new Set();
187
188
  for (const a of agents.list) {
@@ -208,17 +209,17 @@ export default function AgentMap() {
208
209
  agents.list
209
210
  .filter((a) => {
210
211
  const matchesSearch = !q || a.title.toLowerCase().includes(q) || a.id.toLowerCase().includes(q) || a.keywords?.some((k) => k.toLowerCase().includes(q));
211
- const matchesTier = !tierFilter || String(a.tier) === tierFilter;
212
- const matchesGroup = !groupFilter || a.group === groupFilter;
212
+ const matchesDomain = !domainFilter || (a.domains || []).includes(domainFilter);
213
+ const matchesGroup = !groupFilter || (a.domains || []).includes(groupFilter);
213
214
  const matchesPhase = !phaseFilter || (a.activePhases || []).includes('*') || (a.activePhases || []).includes(phaseFilter);
214
215
  const matchesStack = !stackFilter || (a.stacks || []).includes(stackFilter);
215
- return matchesSearch && matchesTier && matchesGroup && matchesPhase && matchesStack;
216
+ return matchesSearch && matchesDomain && matchesGroup && matchesPhase && matchesStack;
216
217
  })
217
218
  .map((a) => a.id)
218
219
  );
219
220
  }, [agents.list, search, tierFilter, groupFilter, phaseFilter, stackFilter]);
220
221
 
221
- const hasFilter = search || tierFilter || groupFilter || phaseFilter || stackFilter;
222
+ const hasFilter = search || domainFilter || groupFilter || phaseFilter || stackFilter;
222
223
 
223
224
  // Build React Flow nodes and edges
224
225
  const { nodes: layoutNodes, edges } = useMemo(() => {
@@ -236,15 +237,15 @@ export default function AgentMap() {
236
237
  // Agent nodes
237
238
  for (const agent of agents.list) {
238
239
  const dimmed = hasFilter && !matchedIds.has(agent.id);
239
- const tierColors = TIER_COLORS[agent.tier] || TIER_COLORS[4];
240
+ const personaColors = PERSONA_COLORS[agent.id] || FALLBACK_PERSONA;
240
241
  rawNodes.push({
241
242
  id: agent.id,
242
243
  type: 'agentNode',
243
244
  data: {
244
245
  label: agent.title,
245
- tier: agent.tier,
246
- group: agent.group,
247
- color: tierColors.border,
246
+ personaId: agent.id,
247
+ domains: agent.domains,
248
+ color: personaColors.border,
248
249
  agentRef: agent,
249
250
  },
250
251
  style: dimmed ? { opacity: 0.2 } : {},
@@ -252,17 +253,15 @@ export default function AgentMap() {
252
253
  });
253
254
  }
254
255
 
255
- // reports_to edges (solid, tier-colored)
256
+ // user → agent edges (flat — no hierarchy in v5)
256
257
  for (const agent of agents.list) {
257
- const source = agent.reportsTo === 'user' ? '__user__' : agent.reportsTo;
258
- if (!source) continue;
259
- const tierColors = TIER_COLORS[agent.tier] || TIER_COLORS[4];
258
+ const personaColors = PERSONA_COLORS[agent.id] || FALLBACK_PERSONA;
260
259
  rawEdges.push({
261
260
  id: `rt-${agent.id}`,
262
- source,
261
+ source: '__user__',
263
262
  target: agent.id,
264
263
  type: 'smoothstep',
265
- style: { stroke: tierColors.border, strokeWidth: 1.5 },
264
+ style: { stroke: personaColors.border, strokeWidth: 1.5 },
266
265
  animated: false,
267
266
  });
268
267
  }
@@ -2,7 +2,7 @@ import morphData from 'virtual:morph-data';
2
2
 
3
3
  // ─── Constants ────────────────────────────────────────────────────────────────
4
4
 
5
- const PHASE_ORDER = ['proposal', 'uiux', 'design', 'plan', 'tasks', 'implement', 'sync'];
5
+ const PHASE_ORDER = ['proposal', 'uiux', 'plan', 'implement', 'review'];
6
6
 
7
7
  const STATUS_BADGE = {
8
8
  in_progress: { label: 'In Progress', bg: 'bg-blue-900/60', border: 'border-blue-600', text: 'text-blue-300' },