@polymorphism-tech/morph-spec 7.2.0 → 7.4.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 (74) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +4 -2
  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 +2 -2
  8. package/framework/MORPH.md +29 -12
  9. package/framework/agents.json +132 -132
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-proposal.md +1 -1
  12. package/framework/evals/rubrics/README.md +9 -8
  13. package/framework/hooks/README.md +3 -1
  14. package/framework/hooks/claude-code/core/gate-guard.js +160 -0
  15. package/framework/hooks/claude-code/core/state-sync.js +15 -3
  16. package/framework/hooks/claude-code/core/validate-completion.js +241 -136
  17. package/framework/hooks/shared/state-reader.js +236 -221
  18. package/framework/phases.json +1 -1
  19. package/framework/skills/README.md +2 -2
  20. package/framework/skills/core/morph-apply/SKILL.md +63 -21
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +5 -1
  22. package/framework/skills/core/morph-eval/SKILL.md +4 -5
  23. package/framework/skills/core/morph-implement/SKILL.md +47 -19
  24. package/framework/skills/core/morph-plan/SKILL.md +8 -2
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +13 -6
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/standards/ai-agents/setup.md +258 -244
  30. package/framework/templates/README.md +708 -709
  31. package/framework/templates/REGISTRY.json +1 -20
  32. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  33. package/framework/templates/docs/proposal.md +171 -171
  34. package/framework/templates/feature/recap.md +145 -145
  35. package/package.json +1 -1
  36. package/src/commands/project/doctor.js +941 -946
  37. package/src/commands/project/init.js +517 -515
  38. package/src/commands/project/status.js +373 -376
  39. package/src/commands/state/approve.js +251 -213
  40. package/src/commands/validation/validate-feature.js +356 -287
  41. package/src/core/state/state-manager.js +910 -934
  42. package/src/dashboard/app/App.jsx +29 -31
  43. package/src/dashboard/app/components/Sidebar.jsx +44 -45
  44. package/src/dashboard/scanner/index.js +20 -21
  45. package/src/lib/context/context-builder.js +3 -3
  46. package/src/lib/generators/context-generator.js +2 -3
  47. package/src/lib/generators/mandate-generator.js +48 -92
  48. package/src/lib/generators/metadata-extractor.js +385 -386
  49. package/src/lib/generators/recap-generator.js +285 -285
  50. package/src/lib/risk-detector.js +2 -2
  51. package/src/lib/validators/content/content-validator.js +89 -286
  52. package/src/lib/validators/content/index.js +0 -1
  53. package/src/utils/hooks-installer.js +527 -516
  54. package/framework/templates/feature/tasks.md +0 -199
  55. package/framework/workflows/README.md +0 -1041
  56. package/framework/workflows/configs/fast-track.json +0 -46
  57. package/framework/workflows/configs/full.json +0 -64
  58. package/framework/workflows/configs/quick.json +0 -32
  59. package/framework/workflows/configs/standard.json +0 -57
  60. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
  61. package/framework/workflows/docs/design-impl.md +0 -37
  62. package/framework/workflows/docs/enforcement-pipeline.md +0 -668
  63. package/framework/workflows/docs/fast-track.md +0 -29
  64. package/framework/workflows/docs/full-morph.md +0 -69
  65. package/framework/workflows/docs/standard.md +0 -49
  66. package/framework/workflows/docs/ui-refresh.md +0 -39
  67. package/src/commands/state/index.js +0 -7
  68. package/src/core/workflows/workflow-detector.js +0 -586
  69. package/src/core/workflows/workflow-router.js +0 -204
  70. package/src/dashboard/app/pages/Workflows.jsx +0 -346
  71. package/src/dashboard/scanner/workflows-scanner.js +0 -18
  72. package/src/lib/phase-chain/phase-validator.js +0 -359
  73. package/src/lib/validators/content/cross-phase-validator.js +0 -281
  74. package/src/lib/validators/spec-validator.js +0 -312
@@ -1,586 +0,0 @@
1
- /**
2
- * @fileoverview Workflow Detection Engine
3
- *
4
- * Analyzes user requests to automatically select the appropriate workflow based on:
5
- * - Keyword matching
6
- * - Complexity estimation
7
- * - Workflow criteria
8
- *
9
- * @module workflow-detector
10
- */
11
-
12
- import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
13
- import { join, dirname, basename } from 'path';
14
- import { fileURLToPath } from 'url';
15
- import { getPhaseSequence } from '../state/phase-state-machine.js';
16
-
17
- /**
18
- * Load the canonical phase id set from framework/phases.json.
19
- * Falls back to getPhaseSequence() if the file is unavailable.
20
- * @returns {Set<string>}
21
- */
22
- function getValidPhaseIds() {
23
- try {
24
- const phasesPath = join(__dirname, '..', '..', '..', 'framework', 'phases.json');
25
- const { phases } = JSON.parse(readFileSync(phasesPath, 'utf8'));
26
- return new Set(Object.keys(phases));
27
- } catch {
28
- // Graceful fallback to state machine list
29
- return new Set(getPhaseSequence());
30
- }
31
- }
32
-
33
- const __dirname = dirname(fileURLToPath(import.meta.url));
34
-
35
- /**
36
- * Get framework root directory
37
- * @param {string} projectPath - Project path
38
- * @returns {string} Framework root path
39
- */
40
- function getFrameworkRoot(projectPath) {
41
- // Try npm-installed package first (only if it has the workflow configs)
42
- const npmPath = join(projectPath, 'node_modules/@polymorphism-tech/morph-spec');
43
- if (existsSync(npmPath) && existsSync(join(npmPath, 'framework/workflows/configs'))) {
44
- return npmPath;
45
- }
46
-
47
- // Fallback to local development path (from src/core/workflows/ → root)
48
- return join(__dirname, '..', '..', '..');
49
- }
50
-
51
- /**
52
- * Load all workflow configurations
53
- * @param {string} projectPath - Project path
54
- * @returns {Array<Object>} Array of workflow configs
55
- */
56
- function loadWorkflowConfigs(projectPath) {
57
- const frameworkRoot = getFrameworkRoot(projectPath);
58
- const configsDir = join(frameworkRoot, 'framework/workflows/configs');
59
-
60
- if (!existsSync(configsDir)) {
61
- return [];
62
- }
63
-
64
- const configFiles = readdirSync(configsDir).filter(f => f.endsWith('.json'));
65
- const configs = [];
66
-
67
- for (const file of configFiles) {
68
- try {
69
- const configPath = join(configsDir, file);
70
- const content = readFileSync(configPath, 'utf8');
71
- configs.push(JSON.parse(content));
72
- } catch (err) {
73
- // Skip invalid configs
74
- continue;
75
- }
76
- }
77
-
78
- return configs;
79
- }
80
-
81
- /**
82
- * Match keywords in user request
83
- * @param {string} userRequest - User request text
84
- * @param {Array<string>} keywords - Workflow keywords
85
- * @returns {{matched: Array<string>, score: number}}
86
- */
87
- function matchKeywords(userRequest, keywords) {
88
- const requestLower = userRequest.toLowerCase();
89
- const matched = [];
90
-
91
- for (const keyword of keywords) {
92
- const keywordLower = keyword.toLowerCase();
93
-
94
- // Check for partial matches (e.g., "paginação" matches "pagination")
95
- if (requestLower.includes(keywordLower)) {
96
- matched.push(keyword);
97
- }
98
- }
99
-
100
- // Score: percentage of keywords matched
101
- const score = matched.length > 0 ? matched.length / keywords.length : 0;
102
-
103
- return { matched, score };
104
- }
105
-
106
- /**
107
- * Estimate complexity from user request
108
- * @param {string} userRequest - User request text
109
- * @returns {{files: number, lines: number, components: number, hasInfra: boolean}}
110
- */
111
- function estimateComplexity(userRequest) {
112
- const requestLower = userRequest.toLowerCase();
113
-
114
- // Scope indicators
115
- const scopeIndicators = {
116
- trivial: ['button', 'cor', 'color', 'css', 'fix', 'typo', 'edge case', 'one'],
117
- small: ['add', 'create', 'single', 'simple', 'quick'],
118
- medium: ['feature', 'pagination', 'filter', 'export', 'refactor', 'optimize'],
119
- large: ['system', 'complete', 'all', 'multiple', 'integration', 'architecture'],
120
- huge: ['platform', 'infrastructure', 'subsystem', 'migrate', 'rebuild']
121
- };
122
-
123
- // Infrastructure indicators
124
- const infraKeywords = ['bicep', 'azure', 'deploy', 'docker', 'kubernetes', 'infrastructure', 'terraform'];
125
- const hasInfra = infraKeywords.some(kw => requestLower.includes(kw));
126
-
127
- // Component indicators
128
- const componentKeywords = ['component', 'page', 'screen', 'form', 'table', 'chart', 'modal'];
129
- const componentMatches = componentKeywords.filter(kw => requestLower.includes(kw)).length;
130
-
131
- // Entity indicators
132
- const entityKeywords = ['user', 'order', 'product', 'customer', 'invoice', 'report'];
133
- const entityMatches = entityKeywords.filter(kw => requestLower.includes(kw)).length;
134
-
135
- // Determine scope
136
- let scope = 'medium';
137
- for (const [level, indicators] of Object.entries(scopeIndicators)) {
138
- if (indicators.some(indicator => requestLower.includes(indicator))) {
139
- scope = level;
140
- break;
141
- }
142
- }
143
-
144
- // Estimate based on scope
145
- const estimates = {
146
- trivial: { files: 1, lines: 30, components: 0 },
147
- small: { files: 2, lines: 100, components: 1 },
148
- medium: { files: 5, lines: 200, components: 1 },
149
- large: { files: 12, lines: 800, components: 3 },
150
- huge: { files: 25, lines: 2000, components: 5 }
151
- };
152
-
153
- const base = estimates[scope] || estimates.medium;
154
-
155
- // Adjust based on entities and components mentioned
156
- const files = base.files + entityMatches + componentMatches;
157
- const lines = base.lines + (entityMatches * 50) + (componentMatches * 100);
158
- const components = base.components + componentMatches;
159
-
160
- return {
161
- files,
162
- lines,
163
- components,
164
- hasInfra
165
- };
166
- }
167
-
168
- /**
169
- * Check if complexity matches workflow criteria
170
- * @param {Object} complexity - Estimated complexity
171
- * @param {Object} criteria - Workflow criteria
172
- * @returns {{matches: boolean, score: number}}
173
- */
174
- function matchComplexity(complexity, criteria) {
175
- let matches = 0;
176
- let total = 0;
177
-
178
- // File count
179
- if (criteria.maxFiles !== undefined) {
180
- total++;
181
- if (complexity.files <= criteria.maxFiles) matches++;
182
- }
183
- if (criteria.minFiles !== undefined) {
184
- total++;
185
- if (complexity.files >= criteria.minFiles) matches++;
186
- }
187
-
188
- // Line count
189
- if (criteria.maxLines !== undefined) {
190
- total++;
191
- if (complexity.lines <= criteria.maxLines) matches++;
192
- }
193
- if (criteria.minLines !== undefined) {
194
- total++;
195
- if (complexity.lines >= criteria.minLines) matches++;
196
- }
197
-
198
- // Component count
199
- if (criteria.maxComponents !== undefined) {
200
- total++;
201
- if (complexity.components <= criteria.maxComponents) matches++;
202
- }
203
- if (criteria.minComponents !== undefined) {
204
- total++;
205
- if (complexity.components >= criteria.minComponents) matches++;
206
- }
207
-
208
- // Infrastructure
209
- if (criteria.noInfrastructure !== undefined) {
210
- total++;
211
- if (criteria.noInfrastructure === !complexity.hasInfra) matches++;
212
- }
213
- if (criteria.infrastructure !== undefined) {
214
- total++;
215
- if (criteria.infrastructure === complexity.hasInfra) matches++;
216
- }
217
-
218
- const score = total > 0 ? matches / total : 0.5;
219
- const meetsAll = total > 0 && matches === total;
220
-
221
- return { matches: meetsAll, score };
222
- }
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
-
371
- /**
372
- * Detect workflow for a feature request
373
- * @param {Object} options - Detection options
374
- * @param {string} options.userRequest - User's feature request
375
- * @param {string} [options.projectPath='.'] - Project path
376
- * @param {string} [options.featureName] - Feature name (optional)
377
- * @returns {Object} Detection result
378
- */
379
- export async function detectWorkflow(options) {
380
- const { userRequest, projectPath = '.', featureName } = options;
381
-
382
- if (!userRequest || userRequest.trim().length === 0) {
383
- throw new Error('userRequest is required');
384
- }
385
-
386
- // Load all workflow configs
387
- const workflows = loadWorkflowConfigs(projectPath);
388
-
389
- if (workflows.length === 0) {
390
- // Fallback if no configs found
391
- return {
392
- workflowId: 'standard',
393
- confidence: 0.5,
394
- matchedKeywords: [],
395
- estimatedComplexity: { files: 0, lines: 0, components: 0, hasInfra: false },
396
- reasoning: 'No workflow configs found - using standard as fallback',
397
- alternativeWorkflows: []
398
- };
399
- }
400
-
401
- // Estimate complexity
402
- const complexity = estimateComplexity(userRequest);
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
-
412
- // Score each workflow
413
- const scores = [];
414
-
415
- for (const workflow of workflows) {
416
- const keywordMatch = matchKeywords(userRequest, workflow.keywords || []);
417
- const complexityMatch = matchComplexity(complexity, workflow.criteria || {});
418
-
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
- };
428
-
429
- // Priority score (lower number = higher priority = higher score)
430
- const priorityScore = workflow.priority ? (4 - workflow.priority) / 3 : 0.5;
431
-
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
453
- }
454
- if (workflow.priority === 3 && descriptionScore < 0.4) {
455
- descScore *= 0.7; // Short descriptions less likely to be full
456
- }
457
-
458
- // Calculate total score
459
- let totalScore =
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);
466
-
467
- // Penalty for workflows with no keyword matches
468
- if (keywordMatch.matched.length === 0) {
469
- totalScore *= 0.5; // 50% penalty for no keyword match
470
- }
471
-
472
- // Boost for strong keyword matches (3+ keywords matched)
473
- if (keywordMatch.matched.length >= 3) {
474
- totalScore *= 1.2; // 20% boost
475
- }
476
-
477
- scores.push({
478
- workflowId: workflow.id,
479
- workflowName: workflow.name,
480
- score: totalScore,
481
- keywordMatch: keywordMatch.matched,
482
- complexityMatch: complexityMatch.matches,
483
- reasoning: []
484
- });
485
- }
486
-
487
- // Sort by score (highest first)
488
- scores.sort((a, b) => b.score - a.score);
489
-
490
- // Get top result
491
- const topResult = scores[0];
492
- const confidence = topResult.score;
493
-
494
- // Build reasoning
495
- const reasoning = [];
496
- if (topResult.keywordMatch.length > 0) {
497
- reasoning.push(`Matched keyword${topResult.keywordMatch.length > 1 ? 's' : ''}: ${topResult.keywordMatch.join(', ')}`);
498
- }
499
- reasoning.push(`Estimated ${complexity.files} files, ~${complexity.lines} lines`);
500
- if (complexity.hasInfra) {
501
- reasoning.push('Infrastructure keywords detected');
502
- }
503
- if (topResult.complexityMatch) {
504
- reasoning.push('Meets all complexity criteria');
505
- }
506
-
507
- // Get alternatives (next 2 workflows with score > 0.2)
508
- const alternatives = scores.slice(1, 3)
509
- .filter(w => w.score > 0.2)
510
- .map(w => ({
511
- workflowId: w.workflowId,
512
- confidence: w.score,
513
- reason: `${w.workflowName} (${Math.round(w.score * 100)}% match)`
514
- }));
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
-
528
- return {
529
- workflowId: topResult.workflowId,
530
- confidence,
531
- matchedKeywords: topResult.keywordMatch,
532
- estimatedComplexity: complexity,
533
- reasoning: reasoning.join(' + '),
534
- alternativeWorkflows: alternatives,
535
- warnings
536
- };
537
- }
538
-
539
- /**
540
- * Get workflow configuration by ID
541
- * @param {string} workflowId - Workflow ID
542
- * @param {string} [projectPath='.'] - Project path
543
- * @returns {Object|null} Workflow config or null
544
- */
545
- export function getWorkflowConfig(workflowId, projectPath = '.') {
546
- const workflows = loadWorkflowConfigs(projectPath);
547
- const config = workflows.find(w => w.id === workflowId) || null;
548
-
549
- if (config) {
550
- const validPhases = getValidPhaseIds();
551
- const declaredPhases = config.phases?.run || [];
552
- for (const phase of declaredPhases) {
553
- if (!validPhases.has(phase)) {
554
- console.warn(`[workflow] Warning: workflow "${workflowId}" declares unknown phase "${phase}" — ignored`);
555
- }
556
- }
557
- }
558
-
559
- return config;
560
- }
561
-
562
- /**
563
- * List all available workflows
564
- * @param {string} [projectPath='.'] - Project path
565
- * @returns {Array<{id: string, name: string, description: string}>}
566
- */
567
- export function listWorkflows(projectPath = '.') {
568
- const workflows = loadWorkflowConfigs(projectPath);
569
- return workflows.map(w => ({
570
- id: w.id,
571
- name: w.name,
572
- description: w.description
573
- }));
574
- }
575
-
576
- /**
577
- * Check zero-touch eligibility for a feature (requires trust-manager)
578
- * @param {string} feature
579
- * @returns {Promise<boolean>}
580
- */
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;
586
- }