@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.
Files changed (71) hide show
  1. package/.claude/commands/relentless.constitution.md +1 -1
  2. package/.claude/commands/relentless.convert.md +25 -0
  3. package/.claude/commands/relentless.specify.md +1 -1
  4. package/.claude/skills/analyze/SKILL.md +113 -40
  5. package/.claude/skills/analyze/templates/analysis-report.md +138 -0
  6. package/.claude/skills/checklist/SKILL.md +143 -51
  7. package/.claude/skills/checklist/templates/checklist.md +43 -11
  8. package/.claude/skills/clarify/SKILL.md +70 -11
  9. package/.claude/skills/constitution/SKILL.md +61 -3
  10. package/.claude/skills/constitution/templates/constitution.md +241 -160
  11. package/.claude/skills/constitution/templates/prompt.md +150 -20
  12. package/.claude/skills/convert/SKILL.md +248 -0
  13. package/.claude/skills/implement/SKILL.md +82 -34
  14. package/.claude/skills/plan/SKILL.md +136 -27
  15. package/.claude/skills/plan/templates/plan.md +92 -9
  16. package/.claude/skills/specify/SKILL.md +110 -19
  17. package/.claude/skills/specify/scripts/bash/create-new-feature.sh +2 -2
  18. package/.claude/skills/specify/scripts/bash/setup-plan.sh +1 -1
  19. package/.claude/skills/specify/templates/spec.md +40 -5
  20. package/.claude/skills/tasks/SKILL.md +75 -1
  21. package/.claude/skills/tasks/templates/tasks.md +5 -4
  22. package/CHANGELOG.md +63 -1
  23. package/MANUAL.md +40 -0
  24. package/README.md +263 -11
  25. package/bin/relentless.ts +292 -5
  26. package/package.json +2 -2
  27. package/relentless/config.json +46 -2
  28. package/relentless/constitution.md +2 -2
  29. package/relentless/prompt.md +97 -18
  30. package/src/agents/amp.ts +53 -13
  31. package/src/agents/claude.ts +70 -15
  32. package/src/agents/codex.ts +73 -14
  33. package/src/agents/droid.ts +68 -14
  34. package/src/agents/exec.ts +96 -0
  35. package/src/agents/gemini.ts +59 -16
  36. package/src/agents/opencode.ts +188 -9
  37. package/src/cli/fallback-order.ts +210 -0
  38. package/src/cli/index.ts +63 -0
  39. package/src/cli/mode-flag.ts +198 -0
  40. package/src/cli/review-flags.ts +192 -0
  41. package/src/config/loader.ts +16 -1
  42. package/src/config/schema.ts +157 -2
  43. package/src/execution/runner.ts +144 -21
  44. package/src/init/scaffolder.ts +285 -25
  45. package/src/prd/parser.ts +92 -1
  46. package/src/prd/types.ts +136 -0
  47. package/src/review/index.ts +92 -0
  48. package/src/review/prompt.ts +293 -0
  49. package/src/review/runner.ts +337 -0
  50. package/src/review/tasks/docs.ts +529 -0
  51. package/src/review/tasks/index.ts +80 -0
  52. package/src/review/tasks/lint.ts +436 -0
  53. package/src/review/tasks/quality.ts +760 -0
  54. package/src/review/tasks/security.ts +452 -0
  55. package/src/review/tasks/test.ts +456 -0
  56. package/src/review/tasks/typecheck.ts +323 -0
  57. package/src/review/types.ts +139 -0
  58. package/src/routing/cascade.ts +310 -0
  59. package/src/routing/classifier.ts +338 -0
  60. package/src/routing/estimate.ts +270 -0
  61. package/src/routing/fallback.ts +512 -0
  62. package/src/routing/index.ts +124 -0
  63. package/src/routing/registry.ts +501 -0
  64. package/src/routing/report.ts +570 -0
  65. package/src/routing/router.ts +287 -0
  66. package/src/tui/App.tsx +2 -0
  67. package/src/tui/TUIRunner.tsx +103 -8
  68. package/src/tui/components/CurrentStory.tsx +23 -1
  69. package/src/tui/hooks/useTUI.ts +1 -0
  70. package/src/tui/types.ts +9 -0
  71. 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";