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.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- 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
|
+
}
|