wogiflow 1.0.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 (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,884 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Model Router
5
+ *
6
+ * Selects optimal model based on task analysis and routing strategy.
7
+ * Supports quality-first, cost-optimized, and learned routing.
8
+ * Includes task-type preferences, language routing, and cascade fallback.
9
+ *
10
+ * Part of Phase 2: Multi-Model Core
11
+ * Enhanced in Phase 3: Intelligent Routing
12
+ *
13
+ * Usage:
14
+ * flow model-route "<task>" [--strategy quality-first]
15
+ * flow model-route --analysis <json> --strategy cost-optimized
16
+ * flow route "<task>" --constraints '{"maxCostTier":"standard"}'
17
+ */
18
+
19
+ const path = require('path');
20
+ const {
21
+ PROJECT_ROOT,
22
+ parseFlags,
23
+ outputJson,
24
+ color,
25
+ info,
26
+ warn,
27
+ error,
28
+ safeJsonParse,
29
+ getConfig,
30
+ printHeader,
31
+ printSection
32
+ } = require('./flow-utils');
33
+
34
+ const { analyzeTask } = require('./flow-task-analyzer');
35
+ const { loadRegistry, loadStats } = require('./flow-models');
36
+
37
+ // Phase 3: Import cascade fallback (cached singleton)
38
+ let cascadeModule = null;
39
+ try {
40
+ cascadeModule = require('./flow-cascade');
41
+ } catch (err) {
42
+ // Cascade module not available - log only if not a "cannot find module" error
43
+ if (!err.code || err.code !== 'MODULE_NOT_FOUND') {
44
+ console.error('[flow-model-router] Cascade module error:', err.message);
45
+ }
46
+ }
47
+
48
+ // ============================================================
49
+ // Constants
50
+ // ============================================================
51
+
52
+ const CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow', 'config.json');
53
+
54
+ const ROUTING_STRATEGIES = {
55
+ 'quality-first': 'Select highest-capability model matching requirements',
56
+ 'cost-optimized': 'Select cheapest model with required capabilities',
57
+ 'learned': 'Use historical success rates to optimize selection'
58
+ };
59
+
60
+ const COST_TIER_ORDER = {
61
+ economy: 1,
62
+ standard: 2,
63
+ premium: 3
64
+ };
65
+
66
+ /**
67
+ * Task-type specific routing preferences (Phase 3).
68
+ * Maps task types to preferred model tiers and required capabilities.
69
+ */
70
+ const TASK_TYPE_ROUTING = {
71
+ architecture: {
72
+ preferTier: 'premium',
73
+ capabilities: ['reasoning', 'analysis'],
74
+ description: 'Complex architectural decisions benefit from premium models'
75
+ },
76
+ planning: {
77
+ preferTier: 'premium',
78
+ capabilities: ['reasoning', 'analysis'],
79
+ description: 'Planning tasks need strong reasoning capabilities'
80
+ },
81
+ feature: {
82
+ preferTier: 'standard',
83
+ capabilities: ['code-gen', 'reasoning'],
84
+ description: 'Feature development balances quality and cost'
85
+ },
86
+ bugfix: {
87
+ preferTier: 'standard',
88
+ capabilities: ['code-gen', 'analysis'],
89
+ description: 'Bug fixes need good analysis capabilities'
90
+ },
91
+ refactor: {
92
+ preferTier: 'standard',
93
+ capabilities: ['code-gen', 'analysis'],
94
+ description: 'Refactoring needs code understanding'
95
+ },
96
+ boilerplate: {
97
+ preferTier: 'economy',
98
+ capabilities: ['code-gen'],
99
+ description: 'Simple boilerplate can use cheaper models'
100
+ },
101
+ docs: {
102
+ preferTier: 'economy',
103
+ capabilities: ['code-gen'],
104
+ description: 'Documentation tasks are straightforward'
105
+ },
106
+ test: {
107
+ preferTier: 'standard',
108
+ capabilities: ['code-gen'],
109
+ description: 'Test writing needs good code generation'
110
+ }
111
+ };
112
+
113
+ /**
114
+ * Language-specific routing preferences (Phase 3).
115
+ * Maps languages to minimum proficiency requirements.
116
+ */
117
+ const LANGUAGE_ROUTING = {
118
+ typescript: { minProficiency: 8, description: 'TypeScript needs strong type support' },
119
+ javascript: { minProficiency: 7, description: 'JavaScript is well-supported' },
120
+ python: { minProficiency: 7, description: 'Python is well-supported' },
121
+ rust: { minProficiency: 6, description: 'Rust has specialized syntax' },
122
+ go: { minProficiency: 6, description: 'Go is straightforward' },
123
+ java: { minProficiency: 7, description: 'Java needs good OOP support' },
124
+ csharp: { minProficiency: 7, description: 'C# needs good OOP support' },
125
+ cpp: { minProficiency: 6, description: 'C++ has complex features' },
126
+ ruby: { minProficiency: 6, description: 'Ruby is less common' },
127
+ php: { minProficiency: 6, description: 'PHP is well-documented' }
128
+ };
129
+
130
+ const DEFAULT_CONFIG = {
131
+ routingStrategy: 'quality-first',
132
+ fallbackEnabled: true,
133
+ maxEscalations: 2,
134
+ // Phase 3 additions
135
+ constraints: {
136
+ maxCostTier: 'premium',
137
+ requiredCapabilities: []
138
+ },
139
+ taskTypeOverrides: {},
140
+ cascadeEnabled: true
141
+ };
142
+
143
+ /**
144
+ * Load multi-model config
145
+ * @returns {Object} Config with defaults
146
+ */
147
+ function loadMultiModelConfig() {
148
+ const config = safeJsonParse(CONFIG_PATH);
149
+ return {
150
+ ...DEFAULT_CONFIG,
151
+ ...(config?.multiModel || {})
152
+ };
153
+ }
154
+
155
+ // ============================================================
156
+ // Model Scoring
157
+ // ============================================================
158
+
159
+ /**
160
+ * Score a model for a given task analysis
161
+ * @param {Object} model - Model data from registry
162
+ * @param {Object} analysis - Task analysis
163
+ * @param {string} strategy - Routing strategy
164
+ * @param {Object} stats - Model stats (optional). Only used for 'learned' strategy
165
+ * to incorporate historical success rates. Passed to all
166
+ * strategies for consistent function signature across routing.
167
+ * @returns {Object} Scoring result
168
+ */
169
+ function scoreModel(model, analysis, strategy, stats = {}) {
170
+ const scores = {
171
+ capability: 0,
172
+ language: 0,
173
+ cost: 0,
174
+ history: 0,
175
+ total: 0
176
+ };
177
+ const reasons = [];
178
+
179
+ // 1. Capability matching (0-40 points)
180
+ const requiredCaps = new Set(analysis.capabilities);
181
+ const modelCaps = new Set(model.capabilities || []);
182
+ let capMatches = 0;
183
+ let capMisses = [];
184
+
185
+ for (const cap of requiredCaps) {
186
+ if (modelCaps.has(cap)) {
187
+ capMatches++;
188
+ } else {
189
+ capMisses.push(cap);
190
+ }
191
+ }
192
+
193
+ if (requiredCaps.size > 0) {
194
+ scores.capability = (capMatches / requiredCaps.size) * 40;
195
+ if (capMatches === requiredCaps.size) {
196
+ reasons.push('All required capabilities matched');
197
+ } else if (capMisses.length > 0) {
198
+ reasons.push(`Missing capabilities: ${capMisses.join(', ')}`);
199
+ }
200
+ } else {
201
+ scores.capability = 30; // Default if no specific requirements
202
+ }
203
+
204
+ // 2. Language proficiency (0-30 points)
205
+ const primaryLang = analysis.languages.primary;
206
+ // Type guard: ensure model.languages is an object before accessing
207
+ const langScore = (model.languages && typeof model.languages === 'object')
208
+ ? (model.languages[primaryLang] || 5)
209
+ : 5;
210
+ scores.language = (langScore / 10) * 30;
211
+
212
+ if (langScore >= 9) {
213
+ reasons.push(`Excellent ${primaryLang} support (${langScore}/10)`);
214
+ } else if (langScore >= 7) {
215
+ reasons.push(`Good ${primaryLang} support (${langScore}/10)`);
216
+ } else {
217
+ reasons.push(`Limited ${primaryLang} support (${langScore}/10)`);
218
+ }
219
+
220
+ // 3. Cost scoring (0-20 points, depends on strategy)
221
+ const tierScore = {
222
+ economy: 20,
223
+ standard: 10,
224
+ premium: 5
225
+ };
226
+
227
+ if (strategy === 'cost-optimized') {
228
+ // Higher score for cheaper models
229
+ scores.cost = tierScore[model.costTier] || 10;
230
+ reasons.push(`Cost tier: ${model.costTier}`);
231
+ } else if (strategy === 'quality-first') {
232
+ // Higher score for premium models
233
+ scores.cost = 20 - (tierScore[model.costTier] || 10);
234
+ } else {
235
+ // Balanced
236
+ scores.cost = 10;
237
+ }
238
+
239
+ // 4. Historical performance (0-10 points, for learned routing)
240
+ if (strategy === 'learned' && stats[model.id]) {
241
+ const modelStats = stats[model.id];
242
+ const successRate = modelStats.successRate || 0.5;
243
+ scores.history = successRate * 10;
244
+
245
+ // Check task-type specific stats
246
+ const taskType = analysis.taskType;
247
+ if (modelStats.byTaskType?.[taskType]) {
248
+ const typeStats = modelStats.byTaskType[taskType];
249
+ const total = (typeStats.success || 0) + (typeStats.fail || 0);
250
+ if (total >= 5 && total > 0) {
251
+ const typeRate = (typeStats.success || 0) / total;
252
+ scores.history = typeRate * 10;
253
+ reasons.push(`${(typeRate * 100).toFixed(0)}% success rate on ${taskType} tasks`);
254
+ }
255
+ }
256
+ }
257
+
258
+ // Calculate total
259
+ scores.total = scores.capability + scores.language + scores.cost + scores.history;
260
+
261
+ return {
262
+ modelId: model.id || model.modelId,
263
+ displayName: model.displayName,
264
+ provider: model.provider,
265
+ costTier: model.costTier,
266
+ scores,
267
+ reasons,
268
+ meetsRequirements: capMisses.length === 0
269
+ };
270
+ }
271
+
272
+ // ============================================================
273
+ // Routing Strategies
274
+ // ============================================================
275
+
276
+ /**
277
+ * Route using quality-first strategy
278
+ * @param {Object[]} models - Available models
279
+ * @param {Object} analysis - Task analysis
280
+ * @param {Object} stats - Model stats
281
+ * @returns {Object} Routing decision
282
+ */
283
+ function routeQualityFirst(models, analysis, stats) {
284
+ const scored = models
285
+ .map(m => scoreModel(m, analysis, 'quality-first', stats))
286
+ .filter(s => s.meetsRequirements)
287
+ .sort((a, b) => b.scores.total - a.scores.total);
288
+
289
+ if (scored.length === 0) {
290
+ // Fall back to highest capability model even if not perfect match
291
+ const allScored = models
292
+ .map(m => scoreModel(m, analysis, 'quality-first', stats))
293
+ .sort((a, b) => b.scores.total - a.scores.total);
294
+
295
+ return {
296
+ strategy: 'quality-first',
297
+ primary: allScored[0],
298
+ fallback: allScored[1] || null,
299
+ escalation: null,
300
+ warning: 'No model fully meets requirements, using best available'
301
+ };
302
+ }
303
+
304
+ return {
305
+ strategy: 'quality-first',
306
+ primary: scored[0],
307
+ fallback: scored[1] || null,
308
+ escalation: null // Already using best
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Route using cost-optimized strategy
314
+ * @param {Object[]} models - Available models
315
+ * @param {Object} analysis - Task analysis
316
+ * @param {Object} stats - Model stats
317
+ * @returns {Object} Routing decision
318
+ */
319
+ function routeCostOptimized(models, analysis, stats) {
320
+ const scored = models
321
+ .map(m => scoreModel(m, analysis, 'cost-optimized', stats))
322
+ .filter(s => s.meetsRequirements);
323
+
324
+ // Sort by cost tier first, then by capability within tier
325
+ scored.sort((a, b) => {
326
+ const tierDiff = COST_TIER_ORDER[a.costTier] - COST_TIER_ORDER[b.costTier];
327
+ if (tierDiff !== 0) return tierDiff;
328
+ return b.scores.capability - a.scores.capability;
329
+ });
330
+
331
+ if (scored.length === 0) {
332
+ // Fall back to cheapest model
333
+ const allScored = models
334
+ .map(m => scoreModel(m, analysis, 'cost-optimized', stats))
335
+ .sort((a, b) => COST_TIER_ORDER[a.costTier] - COST_TIER_ORDER[b.costTier]);
336
+
337
+ return {
338
+ strategy: 'cost-optimized',
339
+ primary: allScored[0],
340
+ fallback: null,
341
+ escalation: allScored.find(m => COST_TIER_ORDER[m.costTier] > COST_TIER_ORDER[allScored[0].costTier]),
342
+ warning: 'No model fully meets requirements, using cheapest available'
343
+ };
344
+ }
345
+
346
+ // Find escalation option (higher tier)
347
+ const primaryTier = COST_TIER_ORDER[scored[0].costTier];
348
+ const escalation = scored.find(m => COST_TIER_ORDER[m.costTier] > primaryTier);
349
+
350
+ return {
351
+ strategy: 'cost-optimized',
352
+ primary: scored[0],
353
+ fallback: scored.find(m => m.modelId !== scored[0].modelId && COST_TIER_ORDER[m.costTier] === primaryTier),
354
+ escalation
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Route using learned strategy (historical performance)
360
+ * @param {Object[]} models - Available models
361
+ * @param {Object} analysis - Task analysis
362
+ * @param {Object} stats - Model stats
363
+ * @returns {Object} Routing decision
364
+ */
365
+ function routeLearned(models, analysis, stats) {
366
+ const scored = models
367
+ .map(m => scoreModel(m, analysis, 'learned', stats))
368
+ .filter(s => s.meetsRequirements)
369
+ .sort((a, b) => b.scores.total - a.scores.total);
370
+
371
+ if (scored.length === 0) {
372
+ // Fall back to quality-first if no learned data
373
+ return routeQualityFirst(models, analysis, stats);
374
+ }
375
+
376
+ // Check if we have enough data for learned routing
377
+ const hasEnoughData = Object.values(stats).some(s => (s.totalRuns || 0) >= 10);
378
+
379
+ if (!hasEnoughData) {
380
+ const result = routeQualityFirst(models, analysis, stats);
381
+ result.warning = 'Insufficient historical data, falling back to quality-first';
382
+ return result;
383
+ }
384
+
385
+ return {
386
+ strategy: 'learned',
387
+ primary: scored[0],
388
+ fallback: scored[1] || null,
389
+ escalation: scored.find(m => COST_TIER_ORDER[m.costTier] > COST_TIER_ORDER[scored[0].costTier])
390
+ };
391
+ }
392
+
393
+ // ============================================================
394
+ // Phase 3: Enhanced Routing
395
+ // ============================================================
396
+
397
+ /**
398
+ * Apply constraints to filter models.
399
+ * @param {Object[]} models - Available models
400
+ * @param {Object} constraints - Constraint configuration
401
+ * @returns {Object[]} Filtered models
402
+ */
403
+ function applyConstraints(models, constraints = {}) {
404
+ let filtered = [...models];
405
+
406
+ // Filter by max cost tier
407
+ if (constraints.maxCostTier) {
408
+ const maxTier = COST_TIER_ORDER[constraints.maxCostTier] || 3;
409
+ filtered = filtered.filter(m => (COST_TIER_ORDER[m.costTier] || 2) <= maxTier);
410
+ }
411
+
412
+ // Filter by required capabilities
413
+ if (constraints.requiredCapabilities && constraints.requiredCapabilities.length > 0) {
414
+ const required = new Set(constraints.requiredCapabilities);
415
+ filtered = filtered.filter(m => {
416
+ const caps = new Set(m.capabilities || []);
417
+ return [...required].every(c => caps.has(c));
418
+ });
419
+ }
420
+
421
+ return filtered;
422
+ }
423
+
424
+ /**
425
+ * Apply task-type preferences to scoring.
426
+ * @param {Object} scored - Scored model result
427
+ * @param {string} taskType - Task type
428
+ * @param {Object} overrides - User overrides from config
429
+ * @returns {Object} Adjusted scored model
430
+ */
431
+ function applyTaskTypePreferences(scored, taskType, overrides = {}) {
432
+ const prefs = overrides[taskType] || TASK_TYPE_ROUTING[taskType];
433
+ if (!prefs) return scored;
434
+
435
+ const adjustedScores = { ...scored.scores };
436
+ const reasons = [...scored.reasons];
437
+
438
+ // Bonus for matching preferred tier
439
+ if (prefs.preferTier === scored.costTier) {
440
+ adjustedScores.taskTypeBonus = 5;
441
+ reasons.push(`Matches preferred tier for ${taskType} tasks`);
442
+ } else {
443
+ adjustedScores.taskTypeBonus = 0;
444
+ }
445
+
446
+ adjustedScores.total = adjustedScores.capability + adjustedScores.language +
447
+ adjustedScores.cost + adjustedScores.history + adjustedScores.taskTypeBonus;
448
+
449
+ return {
450
+ ...scored,
451
+ scores: adjustedScores,
452
+ reasons,
453
+ taskTypeMatch: prefs.preferTier === scored.costTier
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Apply language proficiency requirements.
459
+ * @param {Object} model - Model data
460
+ * @param {string} language - Primary language
461
+ * @returns {Object} Language check result
462
+ */
463
+ function checkLanguageProficiency(model, language) {
464
+ const langReq = LANGUAGE_ROUTING[language];
465
+ if (!langReq) return { meets: true, reason: 'No specific requirements' };
466
+
467
+ const proficiency = (model.languages && typeof model.languages === 'object')
468
+ ? (model.languages[language] || 5)
469
+ : 5;
470
+
471
+ const meets = proficiency >= langReq.minProficiency;
472
+
473
+ return {
474
+ meets,
475
+ proficiency,
476
+ required: langReq.minProficiency,
477
+ reason: meets
478
+ ? `${language} proficiency ${proficiency}/10 meets requirement`
479
+ : `${language} proficiency ${proficiency}/10 below required ${langReq.minProficiency}`
480
+ };
481
+ }
482
+
483
+ /**
484
+ * Check cascade fallback status for a model.
485
+ * @param {string} modelId - Model identifier
486
+ * @param {string} taskType - Task type
487
+ * @param {Object} routing - Current routing decision
488
+ * @returns {Object|null} Cascade recommendation if applicable
489
+ */
490
+ function checkCascadeFallback(modelId, taskType, routing) {
491
+ if (!cascadeModule) return null;
492
+
493
+ const escalation = cascadeModule.getEscalationTarget(modelId, routing);
494
+ return escalation;
495
+ }
496
+
497
+ /**
498
+ * Enhanced routing with constraints, task-type preferences, and cascade.
499
+ * @param {Object} params - Routing parameters
500
+ * @returns {Object} Enhanced routing decision
501
+ */
502
+ function routeTaskEnhanced(params) {
503
+ const {
504
+ analysis,
505
+ strategy = 'quality-first',
506
+ constraints = {},
507
+ checkCascade = true
508
+ } = params;
509
+
510
+ // Load registry and stats
511
+ const registry = loadRegistry();
512
+ if (!registry) {
513
+ return { success: false, error: 'Model registry not found' };
514
+ }
515
+
516
+ const stats = loadStats();
517
+ const config = loadMultiModelConfig();
518
+
519
+ // Merge constraints
520
+ const effectiveConstraints = {
521
+ ...config.constraints,
522
+ ...constraints
523
+ };
524
+
525
+ // Convert registry models to array with IDs
526
+ let models = Object.entries(registry.models || {}).map(([id, data]) => ({
527
+ id,
528
+ ...data
529
+ }));
530
+
531
+ if (models.length === 0) {
532
+ return { success: false, error: 'No models in registry' };
533
+ }
534
+
535
+ // Phase 3: Apply constraints
536
+ const constrainedModels = applyConstraints(models, effectiveConstraints);
537
+ const constraintsApplied = constrainedModels.length < models.length;
538
+
539
+ if (constrainedModels.length === 0) {
540
+ return {
541
+ success: false,
542
+ error: 'No models meet constraints',
543
+ constraints: effectiveConstraints,
544
+ originalCount: models.length
545
+ };
546
+ }
547
+
548
+ models = constrainedModels;
549
+
550
+ // Check language proficiency for all models
551
+ const primaryLang = analysis.languages?.primary;
552
+ if (primaryLang) {
553
+ models = models.map(m => ({
554
+ ...m,
555
+ languageCheck: checkLanguageProficiency(m, primaryLang)
556
+ }));
557
+ }
558
+
559
+ // Run base routing
560
+ const effectiveStrategy = strategy || config.routingStrategy;
561
+ let decision;
562
+
563
+ switch (effectiveStrategy) {
564
+ case 'quality-first':
565
+ decision = routeQualityFirst(models, analysis, stats);
566
+ break;
567
+ case 'cost-optimized':
568
+ decision = routeCostOptimized(models, analysis, stats);
569
+ break;
570
+ case 'learned':
571
+ decision = routeLearned(models, analysis, stats);
572
+ break;
573
+ default:
574
+ decision = routeQualityFirst(models, analysis, stats);
575
+ }
576
+
577
+ // Phase 3: Apply task-type preferences
578
+ const taskType = analysis.taskType || analysis.type || 'feature';
579
+ if (decision.primary) {
580
+ decision.primary = applyTaskTypePreferences(
581
+ decision.primary,
582
+ taskType,
583
+ config.taskTypeOverrides
584
+ );
585
+ }
586
+
587
+ // Phase 3: Check cascade fallback
588
+ let cascadeInfo = null;
589
+ if (checkCascade && config.cascadeEnabled && cascadeModule && decision.primary) {
590
+ cascadeInfo = checkCascadeFallback(
591
+ decision.primary.modelId,
592
+ taskType,
593
+ decision
594
+ );
595
+
596
+ if (cascadeInfo?.shouldEscalate) {
597
+ decision.cascadeTriggered = true;
598
+ decision.cascadeInfo = cascadeInfo;
599
+
600
+ // If we have a target model, use it as primary
601
+ if (cascadeInfo.targetModel) {
602
+ const targetModelData = models.find(m => m.id === cascadeInfo.targetModel);
603
+ if (targetModelData) {
604
+ decision.originalPrimary = decision.primary;
605
+ decision.primary = scoreModel(targetModelData, analysis, effectiveStrategy, stats);
606
+ decision.primary.cascadeEscalated = true;
607
+ }
608
+ }
609
+ }
610
+ }
611
+
612
+ // Add enhanced metadata
613
+ decision.success = true;
614
+ decision.config = config;
615
+ decision.routedAt = new Date().toISOString();
616
+ decision.analysis = {
617
+ complexity: analysis.complexity?.level || 'medium',
618
+ domains: analysis.domains?.primary || 'general',
619
+ languages: analysis.languages?.primary || 'javascript',
620
+ taskType,
621
+ capabilities: analysis.capabilities || []
622
+ };
623
+ decision.enhanced = true;
624
+ decision.constraintsApplied = constraintsApplied;
625
+ decision.constraints = effectiveConstraints;
626
+
627
+ // Add task-type routing info
628
+ const taskTypeInfo = TASK_TYPE_ROUTING[taskType];
629
+ if (taskTypeInfo) {
630
+ decision.taskTypeRouting = {
631
+ taskType,
632
+ preferredTier: taskTypeInfo.preferTier,
633
+ requiredCapabilities: taskTypeInfo.capabilities,
634
+ description: taskTypeInfo.description
635
+ };
636
+ }
637
+
638
+ return decision;
639
+ }
640
+
641
+ /**
642
+ * Get routing configuration (for CLI display).
643
+ * @returns {Object} Routing configuration
644
+ */
645
+ function getRoutingConfig() {
646
+ const config = loadMultiModelConfig();
647
+ return {
648
+ strategy: config.routingStrategy,
649
+ constraints: config.constraints,
650
+ taskTypeOverrides: config.taskTypeOverrides,
651
+ cascadeEnabled: config.cascadeEnabled,
652
+ taskTypeRouting: TASK_TYPE_ROUTING,
653
+ languageRouting: LANGUAGE_ROUTING
654
+ };
655
+ }
656
+
657
+ // ============================================================
658
+ // Main Router
659
+ // ============================================================
660
+
661
+ /**
662
+ * Route task to optimal model
663
+ * @param {Object} params - Routing parameters
664
+ * @returns {Object} Routing decision
665
+ */
666
+ function routeTask(params) {
667
+ const { analysis, strategy = 'quality-first' } = params;
668
+
669
+ // Load registry and stats
670
+ const registry = loadRegistry();
671
+ if (!registry) {
672
+ return {
673
+ success: false,
674
+ error: 'Model registry not found'
675
+ };
676
+ }
677
+
678
+ const stats = loadStats();
679
+ const config = loadMultiModelConfig();
680
+
681
+ // Convert registry models to array with IDs
682
+ const models = Object.entries(registry.models || {}).map(([id, data]) => ({
683
+ id,
684
+ ...data
685
+ }));
686
+
687
+ if (models.length === 0) {
688
+ return {
689
+ success: false,
690
+ error: 'No models in registry'
691
+ };
692
+ }
693
+
694
+ // Select routing strategy
695
+ const effectiveStrategy = strategy || config.routingStrategy;
696
+ let decision;
697
+
698
+ switch (effectiveStrategy) {
699
+ case 'quality-first':
700
+ decision = routeQualityFirst(models, analysis, stats);
701
+ break;
702
+ case 'cost-optimized':
703
+ decision = routeCostOptimized(models, analysis, stats);
704
+ break;
705
+ case 'learned':
706
+ decision = routeLearned(models, analysis, stats);
707
+ break;
708
+ default:
709
+ decision = routeQualityFirst(models, analysis, stats);
710
+ }
711
+
712
+ // Add metadata
713
+ decision.success = true;
714
+ decision.config = config;
715
+ decision.routedAt = new Date().toISOString();
716
+ decision.analysis = {
717
+ complexity: analysis.complexity.level,
718
+ domains: analysis.domains.primary,
719
+ languages: analysis.languages.primary,
720
+ capabilities: analysis.capabilities
721
+ };
722
+
723
+ return decision;
724
+ }
725
+
726
+ // ============================================================
727
+ // CLI Output
728
+ // ============================================================
729
+
730
+ /**
731
+ * Print routing decision
732
+ * @param {Object} decision - Routing decision
733
+ */
734
+ function printDecision(decision) {
735
+ printHeader('MODEL ROUTING DECISION');
736
+
737
+ if (!decision.success) {
738
+ error(decision.error);
739
+ return;
740
+ }
741
+
742
+ // Strategy
743
+ printSection('Strategy');
744
+ console.log(` ${color('cyan', decision.strategy)}`);
745
+ console.log(` ${ROUTING_STRATEGIES[decision.strategy]}`);
746
+
747
+ // Task Analysis Summary
748
+ printSection('Task Analysis');
749
+ console.log(` Complexity: ${decision.analysis.complexity}`);
750
+ console.log(` Domain: ${decision.analysis.domains}`);
751
+ console.log(` Language: ${decision.analysis.languages}`);
752
+ console.log(` Capabilities: ${decision.analysis.capabilities.join(', ')}`);
753
+
754
+ // Primary Model
755
+ printSection('Primary Model');
756
+ const primary = decision.primary;
757
+ console.log(` ${color('green', primary.displayName)} (${primary.provider})`);
758
+ console.log(` Cost tier: ${primary.costTier}`);
759
+ console.log(` Score: ${primary.scores.total.toFixed(1)}/100`);
760
+ for (const reason of primary.reasons.slice(0, 3)) {
761
+ console.log(` - ${reason}`);
762
+ }
763
+
764
+ // Fallback
765
+ if (decision.fallback) {
766
+ printSection('Fallback Model');
767
+ console.log(` ${color('yellow', decision.fallback.displayName)} (${decision.fallback.provider})`);
768
+ console.log(` Score: ${decision.fallback.scores.total.toFixed(1)}/100`);
769
+ }
770
+
771
+ // Escalation
772
+ if (decision.escalation) {
773
+ printSection('Escalation Model');
774
+ console.log(` ${color('cyan', decision.escalation.displayName)} (${decision.escalation.provider})`);
775
+ console.log(` Cost tier: ${decision.escalation.costTier}`);
776
+ }
777
+
778
+ // Warning
779
+ if (decision.warning) {
780
+ console.log('');
781
+ warn(decision.warning);
782
+ }
783
+
784
+ console.log('');
785
+ }
786
+
787
+ // ============================================================
788
+ // Main
789
+ // ============================================================
790
+
791
+ async function main() {
792
+ const { positional, flags } = parseFlags(process.argv.slice(2));
793
+
794
+ let analysis;
795
+
796
+ // Get analysis from flag or run analyzer
797
+ if (flags.analysis) {
798
+ // flags.analysis is a JSON string from CLI, not a file path
799
+ // Security: Check for prototype pollution attempts
800
+ if (flags.analysis.includes('__proto__') ||
801
+ flags.analysis.includes('constructor') ||
802
+ flags.analysis.includes('prototype')) {
803
+ error('Invalid --analysis JSON: contains restricted keys');
804
+ process.exit(1);
805
+ }
806
+ try {
807
+ analysis = JSON.parse(flags.analysis);
808
+ if (!analysis || typeof analysis !== 'object' || Array.isArray(analysis)) {
809
+ error('Invalid --analysis JSON: must be a non-array object');
810
+ process.exit(1);
811
+ }
812
+ // Validate expected structure
813
+ const validKeys = ['taskType', 'languages', 'capabilities', 'complexity', 'domains', 'patterns'];
814
+ const analysisKeys = Object.keys(analysis);
815
+ const invalidKeys = analysisKeys.filter(k => !validKeys.includes(k));
816
+ if (invalidKeys.length > 0) {
817
+ error(`Invalid --analysis JSON: unexpected keys: ${invalidKeys.join(', ')}`);
818
+ process.exit(1);
819
+ }
820
+ } catch (err) {
821
+ error(`Invalid --analysis JSON: ${err.message}`);
822
+ process.exit(1);
823
+ }
824
+ } else if (positional.length > 0) {
825
+ const taskDescription = positional.join(' ');
826
+ analysis = analyzeTask({
827
+ title: taskDescription,
828
+ type: flags.type || 'feature'
829
+ });
830
+ } else {
831
+ error('Usage: flow model-route "<task description>" [--strategy quality-first]');
832
+ error(' flow model-route --analysis <json>');
833
+ process.exit(1);
834
+ }
835
+
836
+ // Route task
837
+ const strategy = flags.strategy || 'quality-first';
838
+ const decision = routeTask({ analysis, strategy });
839
+
840
+ // Output
841
+ if (flags.json) {
842
+ outputJson(decision);
843
+ } else {
844
+ printDecision(decision);
845
+ }
846
+ }
847
+
848
+ // Export for use by other scripts
849
+ module.exports = {
850
+ // Core routing
851
+ routeTask,
852
+ routeTaskEnhanced,
853
+ scoreModel,
854
+
855
+ // Strategy functions
856
+ routeQualityFirst,
857
+ routeCostOptimized,
858
+ routeLearned,
859
+
860
+ // Phase 3: Enhanced routing helpers
861
+ applyConstraints,
862
+ applyTaskTypePreferences,
863
+ checkLanguageProficiency,
864
+ checkCascadeFallback,
865
+ getRoutingConfig,
866
+
867
+ // Registry/stats access
868
+ loadRegistry,
869
+ loadStats,
870
+ loadMultiModelConfig,
871
+
872
+ // Constants
873
+ ROUTING_STRATEGIES,
874
+ COST_TIER_ORDER,
875
+ TASK_TYPE_ROUTING,
876
+ LANGUAGE_ROUTING
877
+ };
878
+
879
+ if (require.main === module) {
880
+ main().catch(err => {
881
+ error(err.message);
882
+ process.exit(1);
883
+ });
884
+ }