@arvorco/relentless 0.3.0 → 0.4.2
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/.claude/commands/relentless.constitution.md +1 -1
- package/.claude/commands/relentless.convert.md +25 -0
- package/.claude/commands/relentless.specify.md +1 -1
- package/.claude/skills/analyze/SKILL.md +113 -40
- package/.claude/skills/analyze/templates/analysis-report.md +138 -0
- package/.claude/skills/checklist/SKILL.md +143 -51
- package/.claude/skills/checklist/templates/checklist.md +43 -11
- package/.claude/skills/clarify/SKILL.md +70 -11
- package/.claude/skills/constitution/SKILL.md +61 -3
- package/.claude/skills/constitution/templates/constitution.md +241 -160
- package/.claude/skills/constitution/templates/prompt.md +150 -20
- package/.claude/skills/convert/SKILL.md +248 -0
- package/.claude/skills/implement/SKILL.md +82 -34
- package/.claude/skills/plan/SKILL.md +136 -27
- package/.claude/skills/plan/templates/plan.md +92 -9
- package/.claude/skills/specify/SKILL.md +110 -19
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +2 -2
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +1 -1
- package/.claude/skills/specify/templates/spec.md +40 -5
- package/.claude/skills/tasks/SKILL.md +75 -1
- package/.claude/skills/tasks/templates/tasks.md +5 -4
- package/CHANGELOG.md +63 -1
- package/MANUAL.md +40 -0
- package/README.md +263 -11
- package/bin/relentless.ts +292 -5
- package/package.json +2 -2
- package/relentless/config.json +46 -2
- package/relentless/constitution.md +2 -2
- package/relentless/prompt.md +97 -18
- package/src/agents/amp.ts +53 -13
- package/src/agents/claude.ts +70 -15
- package/src/agents/codex.ts +73 -14
- package/src/agents/droid.ts +68 -14
- package/src/agents/exec.ts +96 -0
- package/src/agents/gemini.ts +59 -16
- package/src/agents/opencode.ts +188 -9
- package/src/cli/fallback-order.ts +210 -0
- package/src/cli/index.ts +63 -0
- package/src/cli/mode-flag.ts +198 -0
- package/src/cli/review-flags.ts +192 -0
- package/src/config/loader.ts +16 -1
- package/src/config/schema.ts +157 -2
- package/src/execution/runner.ts +144 -21
- package/src/init/scaffolder.ts +285 -25
- package/src/prd/parser.ts +92 -1
- package/src/prd/types.ts +136 -0
- package/src/review/index.ts +92 -0
- package/src/review/prompt.ts +293 -0
- package/src/review/runner.ts +337 -0
- package/src/review/tasks/docs.ts +529 -0
- package/src/review/tasks/index.ts +80 -0
- package/src/review/tasks/lint.ts +436 -0
- package/src/review/tasks/quality.ts +760 -0
- package/src/review/tasks/security.ts +452 -0
- package/src/review/tasks/test.ts +456 -0
- package/src/review/tasks/typecheck.ts +323 -0
- package/src/review/types.ts +139 -0
- package/src/routing/cascade.ts +310 -0
- package/src/routing/classifier.ts +338 -0
- package/src/routing/estimate.ts +270 -0
- package/src/routing/fallback.ts +512 -0
- package/src/routing/index.ts +124 -0
- package/src/routing/registry.ts +501 -0
- package/src/routing/report.ts +570 -0
- package/src/routing/router.ts +287 -0
- package/src/tui/App.tsx +2 -0
- package/src/tui/TUIRunner.tsx +103 -8
- package/src/tui/components/CurrentStory.tsx +23 -1
- package/src/tui/hooks/useTUI.ts +1 -0
- package/src/tui/types.ts +9 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +0 -799
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness Fallback Chain Module
|
|
3
|
+
*
|
|
4
|
+
* Provides automatic harness fallback when the current harness is unavailable
|
|
5
|
+
* due to rate limits, missing installation, or missing API keys.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Checks harness availability (installed, API key present, not rate-limited)
|
|
9
|
+
* - Manages cooldown state for rate-limited harnesses
|
|
10
|
+
* - Supports free mode constraints (only harnesses with free models)
|
|
11
|
+
* - Logs fallback events with reasons
|
|
12
|
+
*
|
|
13
|
+
* @module routing/fallback
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import type { HarnessName, AutoModeConfig, Mode, Complexity } from "../config/schema";
|
|
18
|
+
import { DEFAULT_CONFIG } from "../config/schema";
|
|
19
|
+
import { getModelById, getModelsByHarness } from "./registry";
|
|
20
|
+
import { MODE_MODEL_MATRIX } from "./router";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default cooldown period in milliseconds (60 seconds)
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_COOLDOWN_MS = 60000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* In-memory cooldown state for rate-limited harnesses
|
|
29
|
+
* Maps harness name to cooldown end time
|
|
30
|
+
*/
|
|
31
|
+
const cooldownState: Map<HarnessName, Date> = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* In-memory installation state for testing
|
|
35
|
+
* Maps harness name to installation status
|
|
36
|
+
* Only used when testing - real checks use agent registry
|
|
37
|
+
*/
|
|
38
|
+
const testInstallationState: Map<HarnessName, boolean> = new Map();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether we're in test mode (using mock installation state)
|
|
42
|
+
*/
|
|
43
|
+
let testMode = false;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Schema for harness availability result
|
|
47
|
+
*/
|
|
48
|
+
export const HarnessAvailabilitySchema = z.object({
|
|
49
|
+
available: z.boolean(),
|
|
50
|
+
harness: z.string().optional(),
|
|
51
|
+
reason: z.string().optional(),
|
|
52
|
+
cooldownUntil: z.date().optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type HarnessAvailability = z.infer<typeof HarnessAvailabilitySchema>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Schema for fallback result
|
|
59
|
+
*/
|
|
60
|
+
export const FallbackResultSchema = z.object({
|
|
61
|
+
harness: z.string(),
|
|
62
|
+
model: z.string(),
|
|
63
|
+
fallbacksUsed: z.array(z.string()),
|
|
64
|
+
allUnavailable: z.boolean(),
|
|
65
|
+
reason: z.string().optional(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export type FallbackResult = z.infer<typeof FallbackResultSchema>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Schema for fallback event (recorded in escalation steps)
|
|
72
|
+
*/
|
|
73
|
+
export const FallbackEventSchema = z.object({
|
|
74
|
+
harness: z.string(),
|
|
75
|
+
result: z.enum(["rate_limited", "unavailable", "no_api_key", "not_installed"]),
|
|
76
|
+
error: z.string().optional(),
|
|
77
|
+
nextHarness: z.string().optional(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export type FallbackEvent = z.infer<typeof FallbackEventSchema>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Maps harness names to their required environment variables
|
|
84
|
+
* Note: Some harnesses (opencode, droid, amp) use free models and don't require API keys
|
|
85
|
+
*/
|
|
86
|
+
const HARNESS_ENV_VARS: Partial<Record<HarnessName, string>> = {
|
|
87
|
+
claude: "ANTHROPIC_API_KEY",
|
|
88
|
+
codex: "OPENAI_API_KEY",
|
|
89
|
+
droid: "FACTORY_API_KEY",
|
|
90
|
+
gemini: "GOOGLE_API_KEY",
|
|
91
|
+
// amp can work without API key in free mode
|
|
92
|
+
// opencode uses free models, no API key required
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Harnesses that have free tier models available
|
|
97
|
+
*/
|
|
98
|
+
const FREE_TIER_HARNESSES: Set<HarnessName> = new Set([
|
|
99
|
+
"opencode", // glm-4.7, grok-code-fast-1, minimax-m2.1
|
|
100
|
+
// gemini requires API key and has paid tiers
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Checks if an error message indicates a rate limit
|
|
105
|
+
*
|
|
106
|
+
* @param errorMessage - The error message to check
|
|
107
|
+
* @returns true if the error indicates a rate limit
|
|
108
|
+
*/
|
|
109
|
+
export function isRateLimitError(errorMessage: string): boolean {
|
|
110
|
+
const lowerMessage = errorMessage.toLowerCase();
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
lowerMessage.includes("429") ||
|
|
114
|
+
lowerMessage.includes("rate limit") ||
|
|
115
|
+
(lowerMessage.includes("quota") && lowerMessage.includes("exhausted")) ||
|
|
116
|
+
lowerMessage.includes("too many requests")
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Marks a harness as rate-limited and sets a cooldown period
|
|
122
|
+
*
|
|
123
|
+
* @param harness - The harness name to mark as rate-limited
|
|
124
|
+
* @param cooldownMs - Cooldown period in milliseconds (default: 60 seconds)
|
|
125
|
+
*/
|
|
126
|
+
export function markHarnessRateLimited(
|
|
127
|
+
harness: HarnessName,
|
|
128
|
+
cooldownMs: number = DEFAULT_COOLDOWN_MS
|
|
129
|
+
): void {
|
|
130
|
+
const cooldownEnd = new Date(Date.now() + cooldownMs);
|
|
131
|
+
cooldownState.set(harness, cooldownEnd);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Checks if a harness is currently on cooldown
|
|
136
|
+
*
|
|
137
|
+
* @param harness - The harness name to check
|
|
138
|
+
* @returns true if the harness is on cooldown
|
|
139
|
+
*/
|
|
140
|
+
export function isHarnessOnCooldown(harness: HarnessName): boolean {
|
|
141
|
+
const cooldownEnd = cooldownState.get(harness);
|
|
142
|
+
if (!cooldownEnd) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if cooldown has expired
|
|
147
|
+
if (cooldownEnd <= new Date()) {
|
|
148
|
+
cooldownState.delete(harness);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Gets the cooldown end time for a harness
|
|
157
|
+
*
|
|
158
|
+
* @param harness - The harness name
|
|
159
|
+
* @returns The cooldown end time, or undefined if not on cooldown
|
|
160
|
+
*/
|
|
161
|
+
export function getCooldownEnd(harness: HarnessName): Date | undefined {
|
|
162
|
+
return cooldownState.get(harness);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Sets the cooldown end time for a harness (for testing)
|
|
167
|
+
*
|
|
168
|
+
* @param harness - The harness name
|
|
169
|
+
* @param endTime - The cooldown end time
|
|
170
|
+
*/
|
|
171
|
+
export function setCooldownEnd(harness: HarnessName, endTime: Date): void {
|
|
172
|
+
cooldownState.set(harness, endTime);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resets all cooldown state (for testing)
|
|
177
|
+
*/
|
|
178
|
+
export function resetCooldowns(): void {
|
|
179
|
+
cooldownState.clear();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Sets the installation state for a harness (for testing)
|
|
184
|
+
*
|
|
185
|
+
* @param harness - The harness name
|
|
186
|
+
* @param installed - Whether the harness is installed
|
|
187
|
+
*/
|
|
188
|
+
export function setHarnessInstalled(harness: HarnessName, installed: boolean): void {
|
|
189
|
+
testMode = true;
|
|
190
|
+
testInstallationState.set(harness, installed);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resets test installation state
|
|
195
|
+
*/
|
|
196
|
+
export function resetTestInstallationState(): void {
|
|
197
|
+
testInstallationState.clear();
|
|
198
|
+
testMode = false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Gets the required environment variable for a harness
|
|
203
|
+
*
|
|
204
|
+
* @param harness - The harness name
|
|
205
|
+
* @returns The required environment variable name, or undefined if none required
|
|
206
|
+
*/
|
|
207
|
+
export function getRequiredEnvVar(harness: HarnessName): string | undefined {
|
|
208
|
+
return HARNESS_ENV_VARS[harness];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Checks if the required API key is set for a harness
|
|
213
|
+
*
|
|
214
|
+
* @param harness - The harness name
|
|
215
|
+
* @returns true if the API key is set or not required
|
|
216
|
+
*/
|
|
217
|
+
export function hasRequiredApiKey(harness: HarnessName): boolean {
|
|
218
|
+
const envVar = getRequiredEnvVar(harness);
|
|
219
|
+
if (!envVar) {
|
|
220
|
+
return true; // No API key required
|
|
221
|
+
}
|
|
222
|
+
return !!process.env[envVar];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Checks if a harness has free tier models available
|
|
227
|
+
*
|
|
228
|
+
* @param harness - The harness name
|
|
229
|
+
* @returns true if the harness has free models
|
|
230
|
+
*/
|
|
231
|
+
export function hasFreeTierModel(harness: HarnessName): boolean {
|
|
232
|
+
return FREE_TIER_HARNESSES.has(harness);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Filters harnesses to only those with free tier models
|
|
237
|
+
*
|
|
238
|
+
* @param harnesses - Array of harness names
|
|
239
|
+
* @returns Array of harnesses with free models
|
|
240
|
+
*/
|
|
241
|
+
export function getFreeModeHarnesses(harnesses: HarnessName[]): HarnessName[] {
|
|
242
|
+
return harnesses.filter((h) => hasFreeTierModel(h));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Formats an unavailability message for logging
|
|
247
|
+
*
|
|
248
|
+
* @param harness - The unavailable harness
|
|
249
|
+
* @param reason - The reason for unavailability
|
|
250
|
+
* @param nextHarness - The next harness to try
|
|
251
|
+
* @returns Formatted log message
|
|
252
|
+
*/
|
|
253
|
+
export function formatUnavailableMessage(
|
|
254
|
+
harness: HarnessName,
|
|
255
|
+
reason: string,
|
|
256
|
+
nextHarness?: HarnessName
|
|
257
|
+
): string {
|
|
258
|
+
const next = nextHarness ? `, falling back to ${nextHarness}` : "";
|
|
259
|
+
return `Harness ${harness} unavailable (${reason})${next}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Creates a fallback event for recording
|
|
264
|
+
*
|
|
265
|
+
* @param harness - The harness that was unavailable
|
|
266
|
+
* @param result - The type of unavailability
|
|
267
|
+
* @param nextHarness - The next harness to try
|
|
268
|
+
* @returns FallbackEvent object
|
|
269
|
+
*/
|
|
270
|
+
export function createFallbackEvent(
|
|
271
|
+
harness: HarnessName,
|
|
272
|
+
result: FallbackEvent["result"],
|
|
273
|
+
nextHarness?: HarnessName
|
|
274
|
+
): FallbackEvent {
|
|
275
|
+
return {
|
|
276
|
+
harness,
|
|
277
|
+
result,
|
|
278
|
+
error: formatUnavailableMessage(harness, result, nextHarness),
|
|
279
|
+
nextHarness,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Options for getAvailableHarness
|
|
285
|
+
*/
|
|
286
|
+
interface GetAvailableHarnessOptions {
|
|
287
|
+
freeMode?: boolean;
|
|
288
|
+
skipApiKeyCheck?: boolean;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Checks if a harness is installed
|
|
293
|
+
* Uses test state if in test mode, otherwise checks actual installation
|
|
294
|
+
*
|
|
295
|
+
* @param harness - The harness name
|
|
296
|
+
* @returns Promise<boolean> whether the harness is installed
|
|
297
|
+
*/
|
|
298
|
+
async function isHarnessInstalled(harness: HarnessName): Promise<boolean> {
|
|
299
|
+
if (testMode) {
|
|
300
|
+
return testInstallationState.get(harness) ?? false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// In production, use the agent registry
|
|
304
|
+
try {
|
|
305
|
+
const { getAgent } = await import("../agents/registry");
|
|
306
|
+
const agent = getAgent(harness);
|
|
307
|
+
if (!agent) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return await agent.isInstalled();
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Gets the first available harness from the fallback order
|
|
318
|
+
*
|
|
319
|
+
* @param fallbackOrder - Array of harness names in priority order
|
|
320
|
+
* @param options - Options for filtering (freeMode, skipApiKeyCheck)
|
|
321
|
+
* @returns HarnessAvailability with the first available harness or unavailable status
|
|
322
|
+
*/
|
|
323
|
+
export async function getAvailableHarness(
|
|
324
|
+
fallbackOrder: HarnessName[],
|
|
325
|
+
options: GetAvailableHarnessOptions = {}
|
|
326
|
+
): Promise<HarnessAvailability & { harness?: HarnessName }> {
|
|
327
|
+
const { freeMode = false, skipApiKeyCheck = false } = options;
|
|
328
|
+
|
|
329
|
+
// Filter to free harnesses if in free mode
|
|
330
|
+
const harnesses = freeMode ? getFreeModeHarnesses(fallbackOrder) : fallbackOrder;
|
|
331
|
+
|
|
332
|
+
const unavailableReasons: string[] = [];
|
|
333
|
+
|
|
334
|
+
for (const harness of harnesses) {
|
|
335
|
+
// Check if on cooldown (rate limited)
|
|
336
|
+
if (isHarnessOnCooldown(harness)) {
|
|
337
|
+
const cooldownEnd = getCooldownEnd(harness);
|
|
338
|
+
unavailableReasons.push(`${harness}: rate_limited until ${cooldownEnd?.toISOString()}`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check if installed
|
|
343
|
+
const installed = await isHarnessInstalled(harness);
|
|
344
|
+
if (!installed) {
|
|
345
|
+
unavailableReasons.push(`${harness}: not installed`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Check API key (unless skipped or harness has free tier)
|
|
350
|
+
if (!skipApiKeyCheck && !hasFreeTierModel(harness) && !hasRequiredApiKey(harness)) {
|
|
351
|
+
unavailableReasons.push(`${harness}: missing API key (${getRequiredEnvVar(harness)})`);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Harness is available
|
|
356
|
+
return {
|
|
357
|
+
available: true,
|
|
358
|
+
harness,
|
|
359
|
+
reason:
|
|
360
|
+
unavailableReasons.length > 0 ? unavailableReasons.join("; ") : undefined,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// No harness available
|
|
365
|
+
return {
|
|
366
|
+
available: false,
|
|
367
|
+
reason:
|
|
368
|
+
unavailableReasons.length > 0
|
|
369
|
+
? `All harnesses unavailable: ${unavailableReasons.join("; ")}`
|
|
370
|
+
: "No harnesses in fallback order",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Selects a harness considering fallback chain and all constraints
|
|
376
|
+
*
|
|
377
|
+
* @param config - AutoModeConfig with fallback order and settings
|
|
378
|
+
* @param options - Options for selection
|
|
379
|
+
* @returns FallbackResult with selected harness and model
|
|
380
|
+
*/
|
|
381
|
+
export async function selectHarnessWithFallback(
|
|
382
|
+
config: AutoModeConfig,
|
|
383
|
+
options: { mode?: Mode; complexity?: Complexity } = {}
|
|
384
|
+
): Promise<FallbackResult> {
|
|
385
|
+
const { mode = config.defaultMode, complexity = "medium" } = options;
|
|
386
|
+
const freeMode = mode === "free";
|
|
387
|
+
|
|
388
|
+
const fallbacksUsed: string[] = [];
|
|
389
|
+
const harnesses = freeMode
|
|
390
|
+
? getFreeModeHarnesses(config.fallbackOrder)
|
|
391
|
+
: config.fallbackOrder;
|
|
392
|
+
|
|
393
|
+
for (const harness of harnesses) {
|
|
394
|
+
// Check availability
|
|
395
|
+
const isOnCooldown = isHarnessOnCooldown(harness);
|
|
396
|
+
const installed = await isHarnessInstalled(harness);
|
|
397
|
+
const hasApiKey = hasFreeTierModel(harness) || hasRequiredApiKey(harness);
|
|
398
|
+
|
|
399
|
+
if (isOnCooldown) {
|
|
400
|
+
fallbacksUsed.push(harness);
|
|
401
|
+
console.log(formatUnavailableMessage(harness, "rate_limited"));
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!installed) {
|
|
406
|
+
fallbacksUsed.push(harness);
|
|
407
|
+
console.log(formatUnavailableMessage(harness, "not_installed"));
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!hasApiKey) {
|
|
412
|
+
fallbacksUsed.push(harness);
|
|
413
|
+
console.log(formatUnavailableMessage(harness, "no_api_key"));
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Harness available - get model
|
|
418
|
+
const model = getModelForHarnessAndMode(harness, mode, complexity, config);
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
harness,
|
|
422
|
+
model,
|
|
423
|
+
fallbacksUsed,
|
|
424
|
+
allUnavailable: false,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// All harnesses unavailable
|
|
429
|
+
return {
|
|
430
|
+
harness: harnesses[0] ?? "claude",
|
|
431
|
+
model: "unknown",
|
|
432
|
+
fallbacksUsed,
|
|
433
|
+
allUnavailable: true,
|
|
434
|
+
reason: `All harnesses unavailable: ${fallbacksUsed.join(", ")}`,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Gets the appropriate model for a harness, mode, and complexity combination
|
|
440
|
+
*
|
|
441
|
+
* @param harness - The harness name
|
|
442
|
+
* @param mode - The cost optimization mode
|
|
443
|
+
* @param complexity - The task complexity
|
|
444
|
+
* @returns The model identifier
|
|
445
|
+
*/
|
|
446
|
+
export function getModelForHarnessAndMode(
|
|
447
|
+
harness: HarnessName,
|
|
448
|
+
mode: Mode,
|
|
449
|
+
complexity: Complexity,
|
|
450
|
+
config?: AutoModeConfig
|
|
451
|
+
): string {
|
|
452
|
+
// Use MODE_MODEL_MATRIX to get the default routing
|
|
453
|
+
const rule = MODE_MODEL_MATRIX[mode][complexity];
|
|
454
|
+
|
|
455
|
+
if (config && hasCustomModeModels(config)) {
|
|
456
|
+
const overrideModel = config.modeModels[complexity];
|
|
457
|
+
const overrideProfile = getModelById(overrideModel);
|
|
458
|
+
if (overrideProfile && overrideProfile.harness === harness) {
|
|
459
|
+
if (mode !== "free" || overrideProfile.tier === "free") {
|
|
460
|
+
return overrideModel;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// If the matrix specifies this harness, use its model
|
|
466
|
+
if (rule.harness === harness) {
|
|
467
|
+
return rule.model;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Otherwise, select an appropriate model for the harness based on mode
|
|
471
|
+
const models = getModelsByHarness(harness);
|
|
472
|
+
if (models.length === 0) {
|
|
473
|
+
return "unknown";
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// For free mode, prefer free tier models
|
|
477
|
+
if (mode === "free") {
|
|
478
|
+
const freeModel = models.find((m) => m.tier === "free");
|
|
479
|
+
if (freeModel) {
|
|
480
|
+
return freeModel.id;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// For cheap mode, prefer cheaper models
|
|
485
|
+
if (mode === "cheap") {
|
|
486
|
+
const cheapModel = models.find((m) => m.tier === "cheap" || m.tier === "standard");
|
|
487
|
+
if (cheapModel) {
|
|
488
|
+
return cheapModel.id;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// For good/genius modes, prefer premium/sota models
|
|
493
|
+
if (mode === "good" || mode === "genius") {
|
|
494
|
+
const premiumModel = models.find((m) => m.tier === "sota" || m.tier === "premium");
|
|
495
|
+
if (premiumModel) {
|
|
496
|
+
return premiumModel.id;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Default to first available model
|
|
501
|
+
return models[0].id;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function hasCustomModeModels(config: AutoModeConfig): boolean {
|
|
505
|
+
const defaults = DEFAULT_CONFIG.autoMode.modeModels;
|
|
506
|
+
return (
|
|
507
|
+
config.modeModels.simple !== defaults.simple ||
|
|
508
|
+
config.modeModels.medium !== defaults.medium ||
|
|
509
|
+
config.modeModels.complex !== defaults.complex ||
|
|
510
|
+
config.modeModels.expert !== defaults.expert
|
|
511
|
+
);
|
|
512
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing Module
|
|
3
|
+
*
|
|
4
|
+
* Exports for smart model routing including model registry,
|
|
5
|
+
* complexity classification, and routing logic.
|
|
6
|
+
*
|
|
7
|
+
* @module src/routing
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Re-export registry types and functions
|
|
11
|
+
export {
|
|
12
|
+
ModelTierSchema,
|
|
13
|
+
ModelProfileSchema,
|
|
14
|
+
HarnessProfileSchema,
|
|
15
|
+
MODEL_REGISTRY,
|
|
16
|
+
HARNESS_PROFILES,
|
|
17
|
+
getModelById,
|
|
18
|
+
getModelsByHarness,
|
|
19
|
+
getModelsByTier,
|
|
20
|
+
getDefaultModelForHarness,
|
|
21
|
+
getHarnessForModel,
|
|
22
|
+
type ModelTier,
|
|
23
|
+
type ModelProfile,
|
|
24
|
+
type HarnessProfile,
|
|
25
|
+
} from "./registry";
|
|
26
|
+
|
|
27
|
+
// Re-export classifier types and functions
|
|
28
|
+
export { classifyTask, type ClassificationResult } from "./classifier";
|
|
29
|
+
|
|
30
|
+
// Re-export router types and functions
|
|
31
|
+
export {
|
|
32
|
+
MODE_MODEL_MATRIX,
|
|
33
|
+
RoutingRuleSchema,
|
|
34
|
+
RoutingDecisionSchema,
|
|
35
|
+
routeTask,
|
|
36
|
+
estimateTokens,
|
|
37
|
+
calculateCost,
|
|
38
|
+
type RoutingRule,
|
|
39
|
+
type RoutingDecision,
|
|
40
|
+
} from "./router";
|
|
41
|
+
|
|
42
|
+
// Re-export cascade types and functions
|
|
43
|
+
export {
|
|
44
|
+
EscalationStepSchema,
|
|
45
|
+
EscalationResultSchema,
|
|
46
|
+
executeWithCascade,
|
|
47
|
+
getNextModel,
|
|
48
|
+
type EscalationStep,
|
|
49
|
+
type EscalationResult,
|
|
50
|
+
type TaskExecutor,
|
|
51
|
+
} from "./cascade";
|
|
52
|
+
|
|
53
|
+
// Re-export fallback types and functions
|
|
54
|
+
export {
|
|
55
|
+
DEFAULT_COOLDOWN_MS,
|
|
56
|
+
HarnessAvailabilitySchema,
|
|
57
|
+
FallbackResultSchema,
|
|
58
|
+
FallbackEventSchema,
|
|
59
|
+
isRateLimitError,
|
|
60
|
+
markHarnessRateLimited,
|
|
61
|
+
isHarnessOnCooldown,
|
|
62
|
+
getCooldownEnd,
|
|
63
|
+
setCooldownEnd,
|
|
64
|
+
resetCooldowns,
|
|
65
|
+
setHarnessInstalled,
|
|
66
|
+
resetTestInstallationState,
|
|
67
|
+
getRequiredEnvVar,
|
|
68
|
+
hasRequiredApiKey,
|
|
69
|
+
hasFreeTierModel,
|
|
70
|
+
getFreeModeHarnesses,
|
|
71
|
+
formatUnavailableMessage,
|
|
72
|
+
createFallbackEvent,
|
|
73
|
+
getAvailableHarness,
|
|
74
|
+
selectHarnessWithFallback,
|
|
75
|
+
getModelForHarnessAndMode,
|
|
76
|
+
type HarnessAvailability,
|
|
77
|
+
type FallbackResult,
|
|
78
|
+
type FallbackEvent,
|
|
79
|
+
} from "./fallback";
|
|
80
|
+
|
|
81
|
+
// Re-export estimate types and functions
|
|
82
|
+
export {
|
|
83
|
+
ESCALATION_BUFFER_PERCENT,
|
|
84
|
+
StoryEstimateSchema,
|
|
85
|
+
FeatureCostEstimateSchema,
|
|
86
|
+
ModeComparisonSchema,
|
|
87
|
+
estimateStoryCost,
|
|
88
|
+
estimateFeatureCost,
|
|
89
|
+
formatCostEstimate,
|
|
90
|
+
formatCostBreakdown,
|
|
91
|
+
compareModes,
|
|
92
|
+
formatModeComparison,
|
|
93
|
+
type StoryEstimate,
|
|
94
|
+
type FeatureCostEstimate,
|
|
95
|
+
type ModeComparison,
|
|
96
|
+
} from "./estimate";
|
|
97
|
+
|
|
98
|
+
// Re-export report types and functions
|
|
99
|
+
export {
|
|
100
|
+
EscalationEventSchema,
|
|
101
|
+
StoryExecutionSchema,
|
|
102
|
+
ModelUtilizationSchema,
|
|
103
|
+
CostComparisonSchema,
|
|
104
|
+
FeatureCostReportSchema,
|
|
105
|
+
createStoryExecution,
|
|
106
|
+
getBaselineCost,
|
|
107
|
+
calculateModelUtilization,
|
|
108
|
+
calculateEscalationOverhead,
|
|
109
|
+
generateCostReport,
|
|
110
|
+
formatStoryLine,
|
|
111
|
+
formatEscalationLine,
|
|
112
|
+
formatComparisonLine,
|
|
113
|
+
formatUtilizationStats,
|
|
114
|
+
formatCostReport,
|
|
115
|
+
saveCostReport,
|
|
116
|
+
loadHistoricalCosts,
|
|
117
|
+
type EscalationEvent,
|
|
118
|
+
type StoryExecution,
|
|
119
|
+
type ModelUtilization,
|
|
120
|
+
type CostComparison,
|
|
121
|
+
type FeatureCostReport,
|
|
122
|
+
type HistoricalCostEntry,
|
|
123
|
+
type FileSystemInterface,
|
|
124
|
+
} from "./report";
|