@polymorphism-tech/morph-spec 4.22.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 (211) 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 -1198
  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 -31
  33. package/framework/hooks/shared/state-reader.js +4 -5
  34. package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -243
  35. package/framework/phases.json +41 -115
  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/templates/CLAUDE.md.core.guide.md +29 -0
  47. package/framework/templates/CLAUDE.md.core.template +22 -0
  48. package/framework/templates/REGISTRY.json +4 -4
  49. package/framework/workflows/configs/fast-track.json +5 -10
  50. package/framework/workflows/configs/full.json +4 -8
  51. package/framework/workflows/configs/quick.json +32 -0
  52. package/framework/workflows/configs/standard.json +57 -64
  53. package/package.json +7 -6
  54. package/src/commands/mcp/mcp-setup.js +58 -31
  55. package/src/commands/project/doctor.js +69 -70
  56. package/src/commands/project/init.js +26 -5
  57. package/src/commands/project/status.js +18 -19
  58. package/src/commands/state/approve.js +1 -1
  59. package/src/commands/validation/validate-feature.js +18 -112
  60. package/src/core/paths/output-schema.js +133 -152
  61. package/src/core/state/phase-state-machine.js +10 -24
  62. package/src/core/state/state-manager.js +17 -17
  63. package/src/core/workflows/workflow-detector.js +214 -31
  64. package/src/core/workflows/workflow-router.js +204 -0
  65. package/src/dashboard/app/pages/AgentMap.jsx +32 -33
  66. package/src/dashboard/app/pages/LiveState.jsx +1 -1
  67. package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
  68. package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
  69. package/src/dashboard/app/pages/Workflows.jsx +2 -2
  70. package/src/dashboard/scanner/agents-scanner.js +30 -35
  71. package/src/lib/agents/metrics-aggregator.js +1 -1
  72. package/src/lib/agents/output-collector.js +7 -9
  73. package/src/lib/context/context-builder.js +289 -0
  74. package/src/lib/detectors/claude-config-detector.js +19 -3
  75. package/src/lib/generators/context-generator.js +20 -42
  76. package/src/lib/generators/index.js +1 -0
  77. package/src/lib/generators/mandate-generator.js +512 -0
  78. package/src/lib/generators/metadata-extractor.js +1 -1
  79. package/src/lib/generators/recap-generator.js +3 -4
  80. package/src/lib/installers/mcp-installer.js +300 -92
  81. package/src/lib/phase-chain/phase-validator.js +36 -40
  82. package/src/lib/stack/stack-profile.js +74 -91
  83. package/src/lib/tasks/task-parser.js +1 -1
  84. package/src/lib/tasks/wave-resolver.js +353 -0
  85. package/src/lib/validators/checkpoint-persistence.js +2 -2
  86. package/src/lib/validators/content/cross-phase-validator.js +281 -0
  87. package/src/lib/validators/content/index.js +1 -0
  88. package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
  89. package/src/scripts/global-install.js +2 -27
  90. package/src/utils/agents-installer.js +224 -447
  91. package/src/utils/claude-settings-manager.js +57 -15
  92. package/src/utils/file-copier.js +1 -0
  93. package/src/utils/hooks-installer.js +15 -117
  94. package/src/utils/skills-installer.js +12 -18
  95. package/bin/detect-agents.js +0 -226
  96. package/bin/task-manager.js +0 -1036
  97. package/framework/agents/README.md +0 -13
  98. package/framework/agents/ai-agents/ai-system-architect.md +0 -194
  99. package/framework/agents/ai-agents/vector-search-expert.md +0 -247
  100. package/framework/agents/architecture/architecture-expert.md +0 -98
  101. package/framework/agents/architecture/morph-spec-architect.md +0 -66
  102. package/framework/agents/architecture/packages-validator.md +0 -89
  103. package/framework/agents/architecture/security-expert.md +0 -79
  104. package/framework/agents/architecture/seo-growth-hacker.md +0 -168
  105. package/framework/agents/architecture/standards-architect.md +0 -153
  106. package/framework/agents/backend/dotnet-senior.md +0 -163
  107. package/framework/agents/backend/ef-modeler.md +0 -119
  108. package/framework/agents/backend/hangfire-orchestrator.md +0 -133
  109. package/framework/agents/backend/ms-agent-expert.md +0 -55
  110. package/framework/agents/backend/neon-expert.md +0 -379
  111. package/framework/agents/frontend/css-specialist.md +0 -89
  112. package/framework/agents/frontend/design-system-validator.md +0 -100
  113. package/framework/agents/frontend/gsap-animator.md +0 -131
  114. package/framework/agents/frontend/nextjs-component-validator.md +0 -97
  115. package/framework/agents/frontend/nextjs-expert.md +0 -201
  116. package/framework/agents/frontend/ui-designer.md +0 -259
  117. package/framework/agents/infrastructure/infra-architect.md +0 -215
  118. package/framework/agents/integrations/asaas-financial.md +0 -138
  119. package/framework/agents/integrations/neon-auth.md +0 -151
  120. package/framework/agents/integrations/resend-email.md +0 -127
  121. package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
  122. package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
  123. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
  124. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
  125. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
  126. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
  127. package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
  128. package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
  129. package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
  130. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
  131. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
  132. package/framework/hooks/shared/compact-restore.js +0 -123
  133. package/framework/hooks/shared/dispatch-helpers.js +0 -147
  134. package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
  135. package/framework/hooks/shared/stale-task-reset.js +0 -57
  136. package/framework/hooks/shared/worktree-helpers.js +0 -75
  137. package/framework/skills/level-0-meta/README.md +0 -7
  138. package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
  139. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
  140. package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
  141. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
  142. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
  143. package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
  144. package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
  145. package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
  146. package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
  147. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
  148. package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
  149. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
  150. package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
  151. package/framework/skills/level-0-meta/morph-gemini-image/SKILL.md +0 -290
  152. package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
  153. package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
  154. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -401
  155. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
  156. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
  157. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
  158. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
  159. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
  160. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
  161. package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
  162. package/framework/skills/level-1-workflows/README.md +0 -7
  163. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
  164. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
  165. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
  166. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
  167. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
  168. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  169. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -634
  170. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
  171. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -54
  172. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
  173. package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
  174. package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
  175. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
  176. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  177. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
  178. package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
  179. package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
  180. package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
  181. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -995
  182. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
  183. package/framework/squad-templates/backend-only.json +0 -47
  184. package/framework/squad-templates/frontend-only.json +0 -56
  185. package/framework/squad-templates/full-stack.json +0 -74
  186. package/src/commands/agents/agent-metrics.js +0 -174
  187. package/src/commands/agents/audit-dispatch.js +0 -82
  188. package/src/commands/agents/detect-agents.js +0 -275
  189. package/src/commands/agents/dispatch-agents.js +0 -461
  190. package/src/commands/agents/index.js +0 -13
  191. package/src/commands/agents/manage-agents.js +0 -352
  192. package/src/commands/agents/project-metrics.js +0 -74
  193. package/src/commands/phase/phase-reset.js +0 -74
  194. package/src/commands/scope/escalate.js +0 -217
  195. package/src/commands/state/advance-phase.js +0 -600
  196. package/src/commands/state/phase-runner.js +0 -215
  197. package/src/commands/state/state.js +0 -444
  198. package/src/commands/task/expand.js +0 -100
  199. package/src/commands/tasks/index.js +0 -4
  200. package/src/commands/tasks/task.js +0 -105
  201. package/src/commands/templates/index.js +0 -5
  202. package/src/commands/templates/template-render.js +0 -93
  203. package/src/commands/trust/trust.js +0 -118
  204. package/src/lib/agents/agents-manager.js +0 -349
  205. package/src/lib/build/build-runner.js +0 -99
  206. package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
  207. package/src/lib/contracts/sprint-contract-generator.js +0 -137
  208. package/src/lib/scope/impact-analyzer.js +0 -106
  209. package/src/lib/trust/trust-manager.js +0 -157
  210. package/src/utils/tier-helpers.js +0 -24
  211. /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' },