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,711 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Cascade Fallback System
5
+ *
6
+ * Tracks model failures and determines when to escalate to alternate models.
7
+ * Auto-escalates after repeated failures on the same error category.
8
+ *
9
+ * Part of Phase 3: Intelligent Routing
10
+ *
11
+ * Usage:
12
+ * flow cascade status Show current cascade state
13
+ * flow cascade reset [model] Reset failure counts
14
+ * flow cascade config Show cascade configuration
15
+ * flow cascade simulate Simulate failures for testing
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const {
21
+ PROJECT_ROOT,
22
+ parseFlags,
23
+ outputJson,
24
+ color,
25
+ info,
26
+ warn,
27
+ error,
28
+ getConfig,
29
+ fileExists,
30
+ printHeader,
31
+ printSection
32
+ } = require('./flow-utils');
33
+
34
+ // ============================================================
35
+ // Constants
36
+ // ============================================================
37
+
38
+ /**
39
+ * Standardized failure categories for consistent error classification.
40
+ * Used across the system for stats tracking and cascade decisions.
41
+ */
42
+ const FAILURE_CATEGORIES = {
43
+ PARSE_ERROR: 'parse_error',
44
+ IMPORT_ERROR: 'import_error',
45
+ TYPE_ERROR: 'type_error',
46
+ SYNTAX_ERROR: 'syntax_error',
47
+ RUNTIME_ERROR: 'runtime_error',
48
+ RATE_LIMIT: 'rate_limit',
49
+ CONTEXT_OVERFLOW: 'context_overflow',
50
+ CAPABILITY_MISMATCH: 'capability_mismatch',
51
+ TIMEOUT: 'timeout',
52
+ UNKNOWN: 'unknown'
53
+ };
54
+
55
+ /**
56
+ * Configuration constants for error tracking.
57
+ */
58
+ const MAX_ERROR_MESSAGE_LENGTH = 200;
59
+ const MAX_ERRORS_TO_STORE = 10;
60
+
61
+ /**
62
+ * Default cascade configuration.
63
+ * Can be overridden in .workflow/config.json under "cascade" key.
64
+ */
65
+ const DEFAULT_CASCADE_CONFIG = {
66
+ enabled: true,
67
+ maxFailuresBeforeEscalate: 3,
68
+ escalateOnCategories: ['capability_mismatch', 'context_overflow', 'rate_limit'],
69
+ resetAfterMinutes: 30,
70
+ persistState: false
71
+ };
72
+
73
+ /**
74
+ * Patterns for detecting failure categories from error messages.
75
+ * Order matters - first match wins.
76
+ */
77
+ const CATEGORY_PATTERNS = [
78
+ { pattern: /rate.?limit|too.?many.?requests|429/i, category: FAILURE_CATEGORIES.RATE_LIMIT },
79
+ { pattern: /context.*(?:length|overflow|exceeded)|token.*limit/i, category: FAILURE_CATEGORIES.CONTEXT_OVERFLOW },
80
+ { pattern: /capability|unsupported|not.?available/i, category: FAILURE_CATEGORIES.CAPABILITY_MISMATCH },
81
+ { pattern: /timeout|timed?.?out/i, category: FAILURE_CATEGORIES.TIMEOUT },
82
+ { pattern: /cannot.?find.?module|import.*error|module.?not.?found/i, category: FAILURE_CATEGORIES.IMPORT_ERROR },
83
+ { pattern: /type.?error|is.?not.?assignable|property.*does.?not.?exist/i, category: FAILURE_CATEGORIES.TYPE_ERROR },
84
+ { pattern: /syntax.?error|unexpected.?token/i, category: FAILURE_CATEGORIES.SYNTAX_ERROR },
85
+ { pattern: /parse.?error|json.*parse|invalid.?json/i, category: FAILURE_CATEGORIES.PARSE_ERROR },
86
+ { pattern: /runtime.?error|reference.?error/i, category: FAILURE_CATEGORIES.RUNTIME_ERROR }
87
+ ];
88
+
89
+ // ============================================================
90
+ // State Management
91
+ // ============================================================
92
+
93
+ const STATE_PATH = path.join(PROJECT_ROOT, '.workflow', 'state', 'cascade-state.json');
94
+
95
+ /**
96
+ * In-memory failure tracker.
97
+ * Key format: "modelId:taskType:category"
98
+ * Value: { count, firstFailure, lastFailure, errors[] }
99
+ */
100
+ let failureTracker = {};
101
+
102
+ /**
103
+ * Load cascade state from file (if persistence enabled).
104
+ * @returns {Object} Loaded state or empty tracker
105
+ */
106
+ function loadState() {
107
+ const config = getCascadeConfig();
108
+
109
+ if (!config.persistState || !fileExists(STATE_PATH)) {
110
+ return {};
111
+ }
112
+
113
+ try {
114
+ const content = fs.readFileSync(STATE_PATH, 'utf-8');
115
+ const state = JSON.parse(content);
116
+
117
+ // Clean up expired entries
118
+ const now = Date.now();
119
+ const resetMs = config.resetAfterMinutes * 60 * 1000;
120
+
121
+ for (const key of Object.keys(state)) {
122
+ if (now - new Date(state[key].lastFailure).getTime() > resetMs) {
123
+ delete state[key];
124
+ }
125
+ }
126
+
127
+ return state;
128
+ } catch (err) {
129
+ warn(`Could not load cascade state: ${err.message}`);
130
+ return {};
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Save cascade state to file (if persistence enabled).
136
+ */
137
+ function saveState() {
138
+ const config = getCascadeConfig();
139
+
140
+ if (!config.persistState) {
141
+ return;
142
+ }
143
+
144
+ try {
145
+ const dir = path.dirname(STATE_PATH);
146
+ if (!fs.existsSync(dir)) {
147
+ fs.mkdirSync(dir, { recursive: true });
148
+ }
149
+ fs.writeFileSync(STATE_PATH, JSON.stringify(failureTracker, null, 2));
150
+ } catch (err) {
151
+ warn(`Could not save cascade state: ${err.message}`);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Initialize failure tracker from persisted state.
157
+ */
158
+ function initializeTracker() {
159
+ failureTracker = loadState();
160
+ }
161
+
162
+ // Initialize on module load
163
+ initializeTracker();
164
+
165
+ // ============================================================
166
+ // Configuration
167
+ // ============================================================
168
+
169
+ /**
170
+ * Get cascade configuration from config.json with defaults.
171
+ * @returns {Object} Cascade configuration
172
+ */
173
+ function getCascadeConfig() {
174
+ const config = getConfig();
175
+ return {
176
+ ...DEFAULT_CASCADE_CONFIG,
177
+ ...(config?.cascade || {})
178
+ };
179
+ }
180
+
181
+ // ============================================================
182
+ // Failure Detection
183
+ // ============================================================
184
+
185
+ /**
186
+ * Detect failure category from error message.
187
+ * @param {string|Error} errorOrMessage - Error object or message string
188
+ * @returns {string} Detected failure category
189
+ */
190
+ function detectCategory(errorOrMessage) {
191
+ const message = errorOrMessage instanceof Error
192
+ ? errorOrMessage.message
193
+ : String(errorOrMessage);
194
+
195
+ for (const { pattern, category } of CATEGORY_PATTERNS) {
196
+ if (pattern.test(message)) {
197
+ return category;
198
+ }
199
+ }
200
+
201
+ return FAILURE_CATEGORIES.UNKNOWN;
202
+ }
203
+
204
+ /**
205
+ * Generate tracker key from components.
206
+ * @param {string} modelId - Model identifier
207
+ * @param {string} taskType - Task type
208
+ * @param {string} category - Failure category
209
+ * @returns {string} Tracker key
210
+ */
211
+ function getTrackerKey(modelId, taskType, category) {
212
+ return `${modelId}:${taskType}:${category}`;
213
+ }
214
+
215
+ // ============================================================
216
+ // Core Functions
217
+ // ============================================================
218
+
219
+ /**
220
+ * Record a failure for cascade tracking.
221
+ * @param {Object} params - Failure parameters
222
+ * @param {string} params.modelId - Model that failed
223
+ * @param {string} params.taskType - Type of task that failed
224
+ * @param {string|Error} params.error - Error message or object
225
+ * @param {string} [params.category] - Override detected category
226
+ * @returns {Object} Updated failure info and escalation recommendation
227
+ */
228
+ function recordFailure(params) {
229
+ const { modelId, taskType, error: errorInput, category: explicitCategory } = params;
230
+ const config = getCascadeConfig();
231
+
232
+ if (!config.enabled) {
233
+ return { recorded: false, reason: 'cascade disabled' };
234
+ }
235
+
236
+ const category = explicitCategory || detectCategory(errorInput);
237
+ const key = getTrackerKey(modelId, taskType, category);
238
+ const now = new Date().toISOString();
239
+ const errorMessage = errorInput instanceof Error ? errorInput.message : String(errorInput);
240
+
241
+ // Check if we need to reset due to time expiry
242
+ if (failureTracker[key]) {
243
+ const lastFailure = new Date(failureTracker[key].lastFailure).getTime();
244
+ const resetMs = config.resetAfterMinutes * 60 * 1000;
245
+
246
+ if (Date.now() - lastFailure > resetMs) {
247
+ delete failureTracker[key];
248
+ }
249
+ }
250
+
251
+ // Initialize or update tracker entry
252
+ if (!failureTracker[key]) {
253
+ failureTracker[key] = {
254
+ modelId,
255
+ taskType,
256
+ category,
257
+ count: 0,
258
+ firstFailure: now,
259
+ lastFailure: now,
260
+ errors: []
261
+ };
262
+ }
263
+
264
+ failureTracker[key].count++;
265
+ failureTracker[key].lastFailure = now;
266
+ failureTracker[key].errors.push({
267
+ timestamp: now,
268
+ message: errorMessage.slice(0, MAX_ERROR_MESSAGE_LENGTH)
269
+ });
270
+
271
+ // Keep only most recent errors
272
+ if (failureTracker[key].errors.length > MAX_ERRORS_TO_STORE) {
273
+ failureTracker[key].errors = failureTracker[key].errors.slice(-MAX_ERRORS_TO_STORE);
274
+ }
275
+
276
+ saveState();
277
+
278
+ // Check if escalation is recommended
279
+ const shouldEscalateNow = shouldEscalate(modelId, taskType, category);
280
+
281
+ return {
282
+ recorded: true,
283
+ key,
284
+ category,
285
+ count: failureTracker[key].count,
286
+ threshold: config.maxFailuresBeforeEscalate,
287
+ shouldEscalate: shouldEscalateNow,
288
+ escalateReason: shouldEscalateNow
289
+ ? `${failureTracker[key].count} consecutive ${category} failures`
290
+ : null
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Record a success, resetting failure count for that model/task combination.
296
+ * @param {Object} params - Success parameters
297
+ * @param {string} params.modelId - Model that succeeded
298
+ * @param {string} params.taskType - Type of task
299
+ */
300
+ function recordSuccess(params) {
301
+ const { modelId, taskType } = params;
302
+
303
+ // Reset all failure categories for this model/task
304
+ const prefix = `${modelId}:${taskType}:`;
305
+ for (const key of Object.keys(failureTracker)) {
306
+ if (key.startsWith(prefix)) {
307
+ delete failureTracker[key];
308
+ }
309
+ }
310
+
311
+ saveState();
312
+ }
313
+
314
+ /**
315
+ * Check if escalation is recommended for a model/task combination.
316
+ * @param {string} modelId - Model identifier
317
+ * @param {string} taskType - Task type
318
+ * @param {string} [category] - Specific category to check
319
+ * @returns {boolean} Whether escalation is recommended
320
+ */
321
+ function shouldEscalate(modelId, taskType, category = null) {
322
+ const config = getCascadeConfig();
323
+
324
+ if (!config.enabled) {
325
+ return false;
326
+ }
327
+
328
+ // If category specified, check only that
329
+ if (category) {
330
+ const key = getTrackerKey(modelId, taskType, category);
331
+ const entry = failureTracker[key];
332
+
333
+ if (!entry) return false;
334
+
335
+ // Check if this category triggers escalation
336
+ if (!config.escalateOnCategories.includes(category)) {
337
+ return false;
338
+ }
339
+
340
+ return entry.count >= config.maxFailuresBeforeEscalate;
341
+ }
342
+
343
+ // Check all escalation-triggering categories
344
+ for (const cat of config.escalateOnCategories) {
345
+ const key = getTrackerKey(modelId, taskType, cat);
346
+ const entry = failureTracker[key];
347
+
348
+ if (entry && entry.count >= config.maxFailuresBeforeEscalate) {
349
+ return true;
350
+ }
351
+ }
352
+
353
+ return false;
354
+ }
355
+
356
+ /**
357
+ * Get escalation recommendation with target model.
358
+ * @param {string} currentModelId - Currently failing model
359
+ * @param {Object} routing - Routing decision with fallback/escalation info
360
+ * @returns {Object} Escalation recommendation
361
+ */
362
+ function getEscalationTarget(currentModelId, routing) {
363
+ const config = getCascadeConfig();
364
+
365
+ if (!config.enabled) {
366
+ return { shouldEscalate: false, reason: 'cascade disabled' };
367
+ }
368
+
369
+ // Get failure info for current model
370
+ const failures = getModelFailures(currentModelId);
371
+
372
+ if (failures.length === 0) {
373
+ return { shouldEscalate: false, reason: 'no failures recorded' };
374
+ }
375
+
376
+ // Check if any category hit threshold
377
+ const triggeredCategory = failures.find(f =>
378
+ config.escalateOnCategories.includes(f.category) &&
379
+ f.count >= config.maxFailuresBeforeEscalate
380
+ );
381
+
382
+ if (!triggeredCategory) {
383
+ return {
384
+ shouldEscalate: false,
385
+ reason: 'threshold not reached',
386
+ failures
387
+ };
388
+ }
389
+
390
+ // Determine target model
391
+ let targetModel = null;
392
+ let targetReason = '';
393
+
394
+ // Try fallback first
395
+ if (routing?.fallback && routing.fallback.modelId !== currentModelId) {
396
+ targetModel = routing.fallback.modelId;
397
+ targetReason = 'fallback model';
398
+ }
399
+ // Then escalation
400
+ else if (routing?.escalation && routing.escalation.modelId !== currentModelId) {
401
+ targetModel = routing.escalation.modelId;
402
+ targetReason = 'escalation model (higher tier)';
403
+ }
404
+
405
+ if (!targetModel) {
406
+ return {
407
+ shouldEscalate: true,
408
+ reason: `${triggeredCategory.count}x ${triggeredCategory.category}`,
409
+ noAlternative: true,
410
+ message: 'No alternative model available'
411
+ };
412
+ }
413
+
414
+ return {
415
+ shouldEscalate: true,
416
+ reason: `${triggeredCategory.count}x ${triggeredCategory.category}`,
417
+ targetModel,
418
+ targetReason,
419
+ triggeredCategory: triggeredCategory.category
420
+ };
421
+ }
422
+
423
+ /**
424
+ * Get all failures for a specific model.
425
+ * @param {string} modelId - Model identifier
426
+ * @returns {Object[]} Array of failure entries
427
+ */
428
+ function getModelFailures(modelId) {
429
+ const prefix = `${modelId}:`;
430
+ return Object.entries(failureTracker)
431
+ .filter(([key]) => key.startsWith(prefix))
432
+ .map(([key, value]) => ({
433
+ ...value,
434
+ key
435
+ }));
436
+ }
437
+
438
+ /**
439
+ * Reset failures for a model or all models.
440
+ * @param {string} [modelId] - Specific model to reset, or null for all
441
+ * @param {string} [taskType] - Specific task type to reset
442
+ */
443
+ function resetFailures(modelId = null, taskType = null) {
444
+ if (!modelId) {
445
+ failureTracker = {};
446
+ } else if (!taskType) {
447
+ const prefix = `${modelId}:`;
448
+ for (const key of Object.keys(failureTracker)) {
449
+ if (key.startsWith(prefix)) {
450
+ delete failureTracker[key];
451
+ }
452
+ }
453
+ } else {
454
+ const prefix = `${modelId}:${taskType}:`;
455
+ for (const key of Object.keys(failureTracker)) {
456
+ if (key.startsWith(prefix)) {
457
+ delete failureTracker[key];
458
+ }
459
+ }
460
+ }
461
+
462
+ saveState();
463
+ }
464
+
465
+ /**
466
+ * Get current cascade status.
467
+ * @returns {Object} Status summary
468
+ */
469
+ function getCascadeStatus() {
470
+ const config = getCascadeConfig();
471
+ const now = Date.now();
472
+ const resetMs = config.resetAfterMinutes * 60 * 1000;
473
+
474
+ const entries = Object.entries(failureTracker).map(([key, value]) => {
475
+ const timeSinceLastMs = now - new Date(value.lastFailure).getTime();
476
+ const expiresInMs = resetMs - timeSinceLastMs;
477
+
478
+ return {
479
+ ...value,
480
+ key,
481
+ expiresIn: expiresInMs > 0 ? Math.ceil(expiresInMs / 60000) : 0,
482
+ atThreshold: value.count >= config.maxFailuresBeforeEscalate,
483
+ willTriggerEscalation: value.count >= config.maxFailuresBeforeEscalate &&
484
+ config.escalateOnCategories.includes(value.category)
485
+ };
486
+ });
487
+
488
+ // Group by model
489
+ const byModel = {};
490
+ for (const entry of entries) {
491
+ if (!byModel[entry.modelId]) {
492
+ byModel[entry.modelId] = [];
493
+ }
494
+ byModel[entry.modelId].push(entry);
495
+ }
496
+
497
+ return {
498
+ enabled: config.enabled,
499
+ config: {
500
+ maxFailuresBeforeEscalate: config.maxFailuresBeforeEscalate,
501
+ escalateOnCategories: config.escalateOnCategories,
502
+ resetAfterMinutes: config.resetAfterMinutes
503
+ },
504
+ totalTracked: entries.length,
505
+ atThreshold: entries.filter(e => e.atThreshold).length,
506
+ willTriggerEscalation: entries.filter(e => e.willTriggerEscalation).length,
507
+ byModel,
508
+ entries
509
+ };
510
+ }
511
+
512
+ // ============================================================
513
+ // CLI Output
514
+ // ============================================================
515
+
516
+ /**
517
+ * Print cascade status.
518
+ */
519
+ function printStatus() {
520
+ const status = getCascadeStatus();
521
+
522
+ printHeader('CASCADE FALLBACK STATUS');
523
+
524
+ printSection('Configuration');
525
+ console.log(` ${color('dim', 'Enabled:')} ${status.enabled ? color('green', 'Yes') : color('red', 'No')}`);
526
+ console.log(` ${color('dim', 'Threshold:')} ${status.config.maxFailuresBeforeEscalate} failures`);
527
+ console.log(` ${color('dim', 'Reset after:')} ${status.config.resetAfterMinutes} minutes`);
528
+ console.log(` ${color('dim', 'Escalate on:')} ${status.config.escalateOnCategories.join(', ')}`);
529
+
530
+ printSection('Current State');
531
+ console.log(` ${color('dim', 'Tracked entries:')} ${status.totalTracked}`);
532
+ console.log(` ${color('dim', 'At threshold:')} ${status.atThreshold}`);
533
+ console.log(` ${color('dim', 'Will escalate:')} ${status.willTriggerEscalation}`);
534
+
535
+ if (status.totalTracked > 0) {
536
+ printSection('By Model');
537
+ for (const [modelId, entries] of Object.entries(status.byModel)) {
538
+ const atThreshold = entries.filter(e => e.atThreshold).length;
539
+ const icon = atThreshold > 0 ? color('red', '!') : color('green', '-');
540
+ console.log(`\n ${icon} ${color('cyan', modelId)}`);
541
+
542
+ for (const entry of entries) {
543
+ const countColor = entry.atThreshold ? 'red' : 'yellow';
544
+ const escalateIcon = entry.willTriggerEscalation ? ' [ESCALATE]' : '';
545
+ console.log(` ${entry.taskType}/${entry.category}: ${color(countColor, entry.count)}/${status.config.maxFailuresBeforeEscalate}${escalateIcon}`);
546
+ if (entry.expiresIn > 0) {
547
+ console.log(` ${color('dim', `expires in ${entry.expiresIn}m`)}`);
548
+ }
549
+ }
550
+ }
551
+ }
552
+
553
+ console.log('');
554
+ }
555
+
556
+ /**
557
+ * Print cascade configuration.
558
+ */
559
+ function printConfig() {
560
+ const config = getCascadeConfig();
561
+
562
+ printHeader('CASCADE CONFIGURATION');
563
+
564
+ console.log(`\nCurrent settings (from .workflow/config.json):\n`);
565
+ console.log(JSON.stringify({ cascade: config }, null, 2));
566
+
567
+ console.log(`\nFailure categories:`);
568
+ for (const [name, value] of Object.entries(FAILURE_CATEGORIES)) {
569
+ const triggers = config.escalateOnCategories.includes(value)
570
+ ? color('yellow', ' [triggers escalation]')
571
+ : '';
572
+ console.log(` ${color('cyan', name)}: ${value}${triggers}`);
573
+ }
574
+
575
+ console.log('');
576
+ }
577
+
578
+ // ============================================================
579
+ // CLI Entry Point
580
+ // ============================================================
581
+
582
+ function showHelp() {
583
+ console.log(`
584
+ Wogi Flow - Cascade Fallback System
585
+
586
+ Track model failures and auto-escalate to alternate models.
587
+
588
+ Usage:
589
+ flow cascade status Show current cascade state
590
+ flow cascade reset [model] Reset failure counts
591
+ flow cascade config Show cascade configuration
592
+ flow cascade simulate Simulate failures for testing
593
+
594
+ Options:
595
+ --model <id> Target model for operation
596
+ --task-type <type> Target task type
597
+ --category <cat> Failure category
598
+ --json Output as JSON
599
+ --help, -h Show this help
600
+
601
+ Examples:
602
+ flow cascade status # Show status
603
+ flow cascade reset # Reset all
604
+ flow cascade reset --model claude-sonnet-4 # Reset specific model
605
+ flow cascade simulate --model claude-sonnet-4 --category context_overflow
606
+ `);
607
+ }
608
+
609
+ async function main() {
610
+ const args = process.argv.slice(2);
611
+ const { flags } = parseFlags(args);
612
+ const command = args.find(a => !a.startsWith('--')) || 'status';
613
+
614
+ if (flags.help || flags.h) {
615
+ showHelp();
616
+ process.exit(0);
617
+ }
618
+
619
+ switch (command) {
620
+ case 'status':
621
+ if (flags.json) {
622
+ outputJson(getCascadeStatus());
623
+ } else {
624
+ printStatus();
625
+ }
626
+ break;
627
+
628
+ case 'config':
629
+ if (flags.json) {
630
+ outputJson({
631
+ config: getCascadeConfig(),
632
+ categories: FAILURE_CATEGORIES
633
+ });
634
+ } else {
635
+ printConfig();
636
+ }
637
+ break;
638
+
639
+ case 'reset':
640
+ const modelToReset = flags.model || null;
641
+ const taskTypeToReset = flags['task-type'] || null;
642
+ resetFailures(modelToReset, taskTypeToReset);
643
+
644
+ if (modelToReset) {
645
+ info(`Reset failures for model: ${modelToReset}`);
646
+ } else {
647
+ info('Reset all failure tracking');
648
+ }
649
+ break;
650
+
651
+ case 'simulate':
652
+ // For testing - simulate failures
653
+ if (!flags.model) {
654
+ error('--model is required for simulate');
655
+ process.exit(1);
656
+ }
657
+
658
+ const result = recordFailure({
659
+ modelId: flags.model,
660
+ taskType: flags['task-type'] || 'feature',
661
+ error: flags.error || 'Simulated failure',
662
+ category: flags.category
663
+ });
664
+
665
+ if (flags.json) {
666
+ outputJson(result);
667
+ } else {
668
+ info(`Recorded failure: ${result.category}`);
669
+ console.log(` Count: ${result.count}/${result.threshold}`);
670
+ if (result.shouldEscalate) {
671
+ warn(`Escalation recommended: ${result.escalateReason}`);
672
+ }
673
+ }
674
+ break;
675
+
676
+ default:
677
+ error(`Unknown command: ${command}`);
678
+ showHelp();
679
+ process.exit(1);
680
+ }
681
+ }
682
+
683
+ // ============================================================
684
+ // Exports
685
+ // ============================================================
686
+
687
+ module.exports = {
688
+ // Constants
689
+ FAILURE_CATEGORIES,
690
+ DEFAULT_CASCADE_CONFIG,
691
+
692
+ // Core functions
693
+ recordFailure,
694
+ recordSuccess,
695
+ shouldEscalate,
696
+ getEscalationTarget,
697
+ getModelFailures,
698
+ resetFailures,
699
+ getCascadeStatus,
700
+ getCascadeConfig,
701
+
702
+ // Utilities
703
+ detectCategory
704
+ };
705
+
706
+ if (require.main === module) {
707
+ main().catch(err => {
708
+ error(err.message);
709
+ process.exit(1);
710
+ });
711
+ }