@arvorco/relentless 0.3.1 → 0.4.3

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 (66) hide show
  1. package/.claude/commands/relentless.convert.md +25 -0
  2. package/.claude/skills/analyze/SKILL.md +113 -40
  3. package/.claude/skills/analyze/templates/analysis-report.md +138 -0
  4. package/.claude/skills/checklist/SKILL.md +144 -51
  5. package/.claude/skills/checklist/templates/checklist.md +43 -11
  6. package/.claude/skills/clarify/SKILL.md +70 -11
  7. package/.claude/skills/constitution/SKILL.md +61 -3
  8. package/.claude/skills/constitution/templates/constitution.md +241 -160
  9. package/.claude/skills/constitution/templates/prompt.md +150 -20
  10. package/.claude/skills/convert/SKILL.md +248 -0
  11. package/.claude/skills/implement/SKILL.md +82 -34
  12. package/.claude/skills/plan/SKILL.md +139 -27
  13. package/.claude/skills/plan/templates/plan.md +92 -9
  14. package/.claude/skills/specify/SKILL.md +112 -20
  15. package/.claude/skills/specify/templates/spec.md +40 -5
  16. package/.claude/skills/tasks/SKILL.md +76 -1
  17. package/.claude/skills/tasks/templates/tasks.md +5 -4
  18. package/CHANGELOG.md +84 -1
  19. package/MANUAL.md +40 -0
  20. package/README.md +268 -13
  21. package/bin/relentless.ts +292 -5
  22. package/package.json +2 -2
  23. package/relentless/config.json +45 -1
  24. package/relentless/constitution.md +41 -19
  25. package/relentless/prompt.md +142 -72
  26. package/src/agents/amp.ts +53 -13
  27. package/src/agents/claude.ts +70 -15
  28. package/src/agents/codex.ts +73 -14
  29. package/src/agents/droid.ts +68 -14
  30. package/src/agents/exec.ts +96 -0
  31. package/src/agents/gemini.ts +59 -16
  32. package/src/agents/opencode.ts +188 -9
  33. package/src/cli/fallback-order.ts +210 -0
  34. package/src/cli/index.ts +63 -0
  35. package/src/cli/mode-flag.ts +198 -0
  36. package/src/cli/review-flags.ts +192 -0
  37. package/src/config/loader.ts +16 -1
  38. package/src/config/schema.ts +157 -2
  39. package/src/execution/runner.ts +144 -21
  40. package/src/init/scaffolder.ts +285 -25
  41. package/src/prd/parser.ts +111 -6
  42. package/src/prd/types.ts +136 -0
  43. package/src/review/index.ts +92 -0
  44. package/src/review/prompt.ts +293 -0
  45. package/src/review/runner.ts +337 -0
  46. package/src/review/tasks/docs.ts +529 -0
  47. package/src/review/tasks/index.ts +80 -0
  48. package/src/review/tasks/lint.ts +436 -0
  49. package/src/review/tasks/quality.ts +760 -0
  50. package/src/review/tasks/security.ts +452 -0
  51. package/src/review/tasks/test.ts +456 -0
  52. package/src/review/tasks/typecheck.ts +323 -0
  53. package/src/review/types.ts +139 -0
  54. package/src/routing/cascade.ts +310 -0
  55. package/src/routing/classifier.ts +338 -0
  56. package/src/routing/estimate.ts +270 -0
  57. package/src/routing/fallback.ts +512 -0
  58. package/src/routing/index.ts +124 -0
  59. package/src/routing/registry.ts +501 -0
  60. package/src/routing/report.ts +570 -0
  61. package/src/routing/router.ts +287 -0
  62. package/src/tui/App.tsx +2 -0
  63. package/src/tui/TUIRunner.tsx +103 -8
  64. package/src/tui/components/CurrentStory.tsx +23 -1
  65. package/src/tui/hooks/useTUI.ts +1 -0
  66. package/src/tui/types.ts +9 -0
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Cascade/Escalation Logic Module
3
+ *
4
+ * Wraps task execution with automatic retry/escalation logic.
5
+ * When a task fails with a smaller model, it automatically retries
6
+ * with a more capable model from the escalation path.
7
+ *
8
+ * @module src/routing/cascade
9
+ */
10
+
11
+ import { z } from "zod";
12
+ import type { AgentResult } from "../agents/types";
13
+ import type { EscalationConfig } from "../config/schema";
14
+ import type { UserStory } from "../prd/types";
15
+ import { getHarnessForModel } from "./registry";
16
+ import { calculateCost, estimateTokens } from "./router";
17
+
18
+ /**
19
+ * Result of an individual escalation attempt
20
+ */
21
+ export const EscalationStepSchema = z.object({
22
+ /** Attempt number (1-based) */
23
+ attempt: z.number().int().min(1),
24
+ /** Harness used for this attempt */
25
+ harness: z.string(),
26
+ /** Model used for this attempt */
27
+ model: z.string(),
28
+ /** Result of the attempt: success, failure, or rate_limited */
29
+ result: z.enum(["success", "failure", "rate_limited"]),
30
+ /** Error message if the attempt failed */
31
+ error: z.string().optional(),
32
+ /** Cost of this attempt */
33
+ cost: z.number().optional(),
34
+ /** Duration in milliseconds */
35
+ duration: z.number().optional(),
36
+ });
37
+
38
+ export type EscalationStep = z.infer<typeof EscalationStepSchema>;
39
+
40
+ /**
41
+ * Result of the cascade execution
42
+ */
43
+ export const EscalationResultSchema = z.object({
44
+ /** Whether the task ultimately succeeded */
45
+ success: z.boolean(),
46
+ /** Final harness that executed the task (or last attempted) */
47
+ finalHarness: z.string(),
48
+ /** Final model that executed the task (or last attempted) */
49
+ finalModel: z.string(),
50
+ /** Total number of attempts made */
51
+ attempts: z.number().int().min(1),
52
+ /** List of all escalation steps */
53
+ escalations: z.array(EscalationStepSchema),
54
+ /** Total actual cost across all attempts */
55
+ actualCost: z.number(),
56
+ /** Whether the task was marked as blocked */
57
+ blocked: z.boolean().optional(),
58
+ /** Reason why the task was blocked */
59
+ blockReason: z.string().optional(),
60
+ });
61
+
62
+ export type EscalationResult = z.infer<typeof EscalationResultSchema>;
63
+
64
+ /**
65
+ * Function type for executing a task with a specific harness and model
66
+ */
67
+ export type TaskExecutor = (
68
+ harness: string,
69
+ model: string,
70
+ prompt: string
71
+ ) => Promise<AgentResult>;
72
+
73
+ /**
74
+ * Gets the next model in the escalation path
75
+ *
76
+ * @param currentModel - Current model ID
77
+ * @param escalationPath - Map of current model to next model
78
+ * @returns Next model ID or undefined if no next model exists
79
+ */
80
+ export function getNextModel(
81
+ currentModel: string,
82
+ escalationPath: Record<string, string>
83
+ ): string | undefined {
84
+ return escalationPath[currentModel];
85
+ }
86
+
87
+ /**
88
+ * Determines if a task result indicates failure
89
+ *
90
+ * @param result - Agent execution result
91
+ * @returns Whether the task failed
92
+ */
93
+ function isTaskFailure(result: AgentResult): boolean {
94
+ return result.exitCode !== 0 || !result.isComplete;
95
+ }
96
+
97
+ /**
98
+ * Determines the result type from an agent result
99
+ *
100
+ * @param result - Agent execution result
101
+ * @returns Result type string
102
+ */
103
+ function getResultType(
104
+ result: AgentResult
105
+ ): "success" | "failure" | "rate_limited" {
106
+ if (result.rateLimited) {
107
+ return "rate_limited";
108
+ }
109
+ if (isTaskFailure(result)) {
110
+ return "failure";
111
+ }
112
+ return "success";
113
+ }
114
+
115
+ /**
116
+ * Extracts error message from agent result
117
+ *
118
+ * @param result - Agent execution result
119
+ * @returns Error message or undefined
120
+ */
121
+ function extractErrorMessage(result: AgentResult): string | undefined {
122
+ if (result.rateLimited) {
123
+ return "Rate limit exceeded";
124
+ }
125
+ if (isTaskFailure(result)) {
126
+ // Extract meaningful error from output
127
+ const lines = result.output.split("\n");
128
+ // Look for lines containing error-like patterns
129
+ const errorLine = lines.find(
130
+ (line) =>
131
+ line.toLowerCase().includes("error") ||
132
+ line.toLowerCase().includes("failed") ||
133
+ line.toLowerCase().includes("exception")
134
+ );
135
+ return errorLine?.trim() || "Task execution failed";
136
+ }
137
+ return undefined;
138
+ }
139
+
140
+ /**
141
+ * Executes a task with automatic cascade/escalation logic
142
+ *
143
+ * When a task fails with the initial model, this function automatically
144
+ * escalates to more capable models according to the escalation path
145
+ * until the task succeeds or max attempts is reached.
146
+ *
147
+ * @param story - User story being executed
148
+ * @param initialHarness - Starting harness
149
+ * @param initialModel - Starting model
150
+ * @param prompt - Task prompt
151
+ * @param config - Escalation configuration
152
+ * @param executor - Function to execute the task
153
+ * @returns Escalation result with success status and all attempts
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * const result = await executeWithCascade(
158
+ * story,
159
+ * "claude",
160
+ * "haiku-4.5",
161
+ * "Fix the bug",
162
+ * config,
163
+ * async (harness, model, prompt) => {
164
+ * const agent = getAgent(harness);
165
+ * return agent.invoke(prompt, { model });
166
+ * }
167
+ * );
168
+ *
169
+ * if (result.success) {
170
+ * console.log(`Completed with ${result.finalModel}`);
171
+ * } else if (result.blocked) {
172
+ * console.log(`Blocked: ${result.blockReason}`);
173
+ * }
174
+ * ```
175
+ */
176
+ export async function executeWithCascade(
177
+ story: UserStory,
178
+ initialHarness: string,
179
+ initialModel: string,
180
+ prompt: string,
181
+ config: EscalationConfig,
182
+ executor: TaskExecutor
183
+ ): Promise<EscalationResult> {
184
+ const escalations: EscalationStep[] = [];
185
+ let totalCost = 0;
186
+ let currentHarness = initialHarness;
187
+ let currentModel = initialModel;
188
+ let attempt = 0;
189
+
190
+ // Estimate tokens for cost calculation
191
+ const estimatedTokens = estimateTokens(story);
192
+
193
+ while (attempt < config.maxAttempts) {
194
+ attempt++;
195
+
196
+ // Execute the task
197
+ const startTime = Date.now();
198
+ const result = await executor(currentHarness, currentModel, prompt);
199
+ const duration = Date.now() - startTime;
200
+
201
+ // Calculate cost for this attempt
202
+ const attemptCost = calculateCost(currentModel, estimatedTokens);
203
+ totalCost += attemptCost;
204
+
205
+ // Determine result type
206
+ const resultType = getResultType(result);
207
+
208
+ // Record the escalation step
209
+ const step: EscalationStep = {
210
+ attempt,
211
+ harness: currentHarness,
212
+ model: currentModel,
213
+ result: resultType,
214
+ cost: attemptCost,
215
+ duration,
216
+ };
217
+
218
+ // Add error message if applicable
219
+ if (resultType !== "success") {
220
+ step.error = extractErrorMessage(result);
221
+ }
222
+
223
+ escalations.push(step);
224
+
225
+ // If successful, return immediately
226
+ if (resultType === "success") {
227
+ return {
228
+ success: true,
229
+ finalHarness: currentHarness,
230
+ finalModel: currentModel,
231
+ attempts: attempt,
232
+ escalations,
233
+ actualCost: totalCost,
234
+ };
235
+ }
236
+
237
+ // If escalation is disabled, don't retry
238
+ if (!config.enabled) {
239
+ return {
240
+ success: false,
241
+ finalHarness: currentHarness,
242
+ finalModel: currentModel,
243
+ attempts: attempt,
244
+ escalations,
245
+ actualCost: totalCost,
246
+ };
247
+ }
248
+
249
+ // Try to get next model from escalation path
250
+ const nextModel = getNextModel(currentModel, config.escalationPath);
251
+
252
+ // If no next model, we're blocked
253
+ if (!nextModel) {
254
+ const reason =
255
+ Object.keys(config.escalationPath).length === 0
256
+ ? "no escalation path configured"
257
+ : `no next model in escalation path for ${currentModel}`;
258
+
259
+ // If this is the last attempt, mark as blocked
260
+ if (attempt >= config.maxAttempts) {
261
+ return {
262
+ success: false,
263
+ finalHarness: currentHarness,
264
+ finalModel: currentModel,
265
+ attempts: attempt,
266
+ escalations,
267
+ actualCost: totalCost,
268
+ blocked: true,
269
+ blockReason: `Task blocked: max attempts (${config.maxAttempts}) reached and ${reason}`,
270
+ };
271
+ }
272
+
273
+ // Block immediately since no escalation possible
274
+ return {
275
+ success: false,
276
+ finalHarness: currentHarness,
277
+ finalModel: currentModel,
278
+ attempts: attempt,
279
+ escalations,
280
+ actualCost: totalCost,
281
+ blocked: true,
282
+ blockReason: `Task blocked: ${reason}`,
283
+ };
284
+ }
285
+
286
+ // Log escalation
287
+ console.log(`Escalating from ${currentModel} to ${nextModel}`);
288
+
289
+ // Update model (and possibly harness) for next attempt
290
+ currentModel = nextModel;
291
+
292
+ // Check if the next model belongs to a different harness
293
+ const nextHarness = getHarnessForModel(nextModel);
294
+ if (nextHarness) {
295
+ currentHarness = nextHarness;
296
+ }
297
+ }
298
+
299
+ // Max attempts reached
300
+ return {
301
+ success: false,
302
+ finalHarness: currentHarness,
303
+ finalModel: currentModel,
304
+ attempts: attempt,
305
+ escalations,
306
+ actualCost: totalCost,
307
+ blocked: true,
308
+ blockReason: `Task blocked: max attempts (${config.maxAttempts}) reached`,
309
+ };
310
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Hybrid Complexity Classifier
3
+ *
4
+ * Implements two-phase task complexity classification:
5
+ * 1. Fast heuristic analysis (< 50ms, no API calls)
6
+ * 2. LLM fallback only when confidence < 0.8
7
+ *
8
+ * The classifier analyzes user story title, description, and acceptance criteria
9
+ * to determine task complexity: simple, medium, complex, or expert.
10
+ *
11
+ * @module src/routing/classifier
12
+ */
13
+
14
+ import type { UserStory } from "../prd/types";
15
+ import type { Complexity } from "../config/schema";
16
+
17
+ /**
18
+ * Result of task complexity classification.
19
+ */
20
+ export interface ClassificationResult {
21
+ /** The determined complexity level */
22
+ complexity: Complexity;
23
+ /** Confidence score from 0.0 to 1.0 */
24
+ confidence: number;
25
+ /** Human-readable explanation of the classification */
26
+ reasoning: string;
27
+ /** Whether the LLM was used for classification (false for high-confidence heuristic) */
28
+ usedLLM: boolean;
29
+ }
30
+
31
+ /**
32
+ * Keyword patterns for each complexity level.
33
+ * Each pattern has an associated weight for confidence calculation.
34
+ */
35
+ interface KeywordPattern {
36
+ patterns: RegExp[];
37
+ weight: number;
38
+ }
39
+
40
+ /**
41
+ * Simple task keyword patterns.
42
+ * Tasks that are trivial: typos, comments, renaming, formatting.
43
+ */
44
+ const SIMPLE_PATTERNS: KeywordPattern = {
45
+ patterns: [
46
+ /\bfix\s+typo/i,
47
+ /\btypo\b/i,
48
+ /\bupdate\s+docs?/i,
49
+ /\bdocumentation\s+update/i,
50
+ /\badd\s+comment/i,
51
+ /\brename\b/i,
52
+ /\bformat\b/i,
53
+ /\bformatting\b/i,
54
+ /\breadme/i,
55
+ /\bcleanup\s+comment/i,
56
+ /\bfix\s+spelling/i,
57
+ /\bcorrect\s+typo/i,
58
+ /\bupdate\s+readme/i,
59
+ /\bfix\s+indent/i,
60
+ /\bremove\s+whitespace/i,
61
+ /\bfix\s+lint/i,
62
+ /\blint\s+error/i,
63
+ ],
64
+ weight: 0.3,
65
+ };
66
+
67
+ /**
68
+ * Medium task keyword patterns.
69
+ * Standard development tasks: features, tests, refactoring, API work.
70
+ */
71
+ const MEDIUM_PATTERNS: KeywordPattern = {
72
+ patterns: [
73
+ /\bimplement\b/i,
74
+ /\badd\s+feature/i,
75
+ /\badd\s+new\b/i,
76
+ /\brefactor\b/i,
77
+ /\btest\b/i,
78
+ /\bapi\b/i,
79
+ /\bendpoint\b/i,
80
+ /\bcreate\b/i,
81
+ /\bbuild\b/i,
82
+ /\bfix\s+bug/i,
83
+ /\bvalidat/i, // validation, validate
84
+ /\bhandl/i, // handle, handler, handling
85
+ /\bintegrat/i, // integrate (but not as strong as complex)
86
+ /\bmodify\b/i,
87
+ /\bupdate\s+logic/i,
88
+ /\badd\s+support/i,
89
+ /\bfeature\b/i, // standalone feature keyword
90
+ /\bprofile\b/i, // user profile work
91
+ ],
92
+ weight: 0.25,
93
+ };
94
+
95
+ /**
96
+ * Complex task keyword patterns.
97
+ * Advanced tasks: architecture, security, authentication, migrations.
98
+ */
99
+ const COMPLEX_PATTERNS: KeywordPattern = {
100
+ patterns: [
101
+ /\barchitect/i,
102
+ /\bintegrat\w*\s+(?:service|api|system)/i,
103
+ /\bmigrat/i,
104
+ /\bsecurity\b/i,
105
+ /\bauth\b/i,
106
+ /\boauth/i,
107
+ /\bjwt\b/i,
108
+ /\bencrypt/i,
109
+ /\bdatabase\s+(?:design|schema|migrat)/i,
110
+ /\bscalabil/i,
111
+ /\bcaching\s+(?:strategy|layer)/i,
112
+ /\berror\s+handling\s+(?:strategy|system)/i,
113
+ /\bthird[- ]party/i,
114
+ /\bexternal\s+(?:api|service)/i,
115
+ /\bpayment/i,
116
+ /\bwebhook/i,
117
+ /\bqueue\s+(?:system|processing)/i,
118
+ ],
119
+ weight: 0.35,
120
+ };
121
+
122
+ /**
123
+ * Expert task keyword patterns.
124
+ * Highly complex tasks: performance, distributed systems, concurrency.
125
+ */
126
+ const EXPERT_PATTERNS: KeywordPattern = {
127
+ patterns: [
128
+ /\bredesign\b/i,
129
+ /\bperformance\s+(?:optim|improv|tun)/i,
130
+ /\bdistribut/i,
131
+ /\bconcurren/i,
132
+ /\bparallel\b/i,
133
+ /\basync\b/i,
134
+ /\breal[- ]time/i,
135
+ /\bmicroservice/i,
136
+ /\bevent[- ]driven/i,
137
+ /\bcritical\s+path/i,
138
+ /\bhigh\s+availabil/i,
139
+ /\bfault\s+toleran/i,
140
+ /\bload\s+balanc/i,
141
+ /\bsharding\b/i,
142
+ /\breplication\b/i,
143
+ /\brace\s+condition/i,
144
+ /\bdeadlock/i,
145
+ /\bthread[- ]safe/i,
146
+ ],
147
+ weight: 0.4,
148
+ };
149
+
150
+ /**
151
+ * File patterns that boost confidence for certain complexity levels.
152
+ */
153
+ const FILE_PATTERN_BOOSTS: Array<{
154
+ pattern: RegExp;
155
+ complexity: Complexity;
156
+ boost: number;
157
+ }> = [
158
+ // Documentation files boost simple confidence
159
+ { pattern: /readme/i, complexity: "simple", boost: 0.15 },
160
+ { pattern: /\.md$/i, complexity: "simple", boost: 0.1 },
161
+ { pattern: /docs?\b/i, complexity: "simple", boost: 0.1 },
162
+ { pattern: /changelog/i, complexity: "simple", boost: 0.1 },
163
+
164
+ // Auth/security patterns boost complex confidence
165
+ { pattern: /\bjwt\b/i, complexity: "complex", boost: 0.15 },
166
+ { pattern: /\bauth/i, complexity: "complex", boost: 0.15 },
167
+ { pattern: /\boauth/i, complexity: "complex", boost: 0.2 },
168
+ { pattern: /\bsecurity/i, complexity: "complex", boost: 0.1 },
169
+ { pattern: /\btoken/i, complexity: "complex", boost: 0.1 },
170
+ { pattern: /\bencrypt/i, complexity: "complex", boost: 0.15 },
171
+
172
+ // Performance patterns boost expert confidence
173
+ { pattern: /\bperformance/i, complexity: "expert", boost: 0.15 },
174
+ { pattern: /\boptimiz/i, complexity: "expert", boost: 0.1 },
175
+ { pattern: /\bconcurren/i, complexity: "expert", boost: 0.15 },
176
+ { pattern: /\bparallel/i, complexity: "expert", boost: 0.15 },
177
+ ];
178
+
179
+ /**
180
+ * Count pattern matches in text and calculate weighted score.
181
+ */
182
+ function countPatternMatches(text: string, patterns: KeywordPattern): number {
183
+ let count = 0;
184
+ for (const pattern of patterns.patterns) {
185
+ if (pattern.test(text)) {
186
+ count++;
187
+ }
188
+ }
189
+ return count * patterns.weight;
190
+ }
191
+
192
+ /**
193
+ * Get the full text to analyze from a user story.
194
+ */
195
+ function getAnalyzableText(story: UserStory): string {
196
+ const parts = [
197
+ story.title || "",
198
+ story.description || "",
199
+ ...(story.acceptanceCriteria || []),
200
+ ];
201
+ return parts.join(" ").toLowerCase();
202
+ }
203
+
204
+ /**
205
+ * Calculate complexity scores from heuristic analysis.
206
+ */
207
+ function calculateHeuristicScores(text: string): Record<Complexity, number> {
208
+ return {
209
+ simple: countPatternMatches(text, SIMPLE_PATTERNS),
210
+ medium: countPatternMatches(text, MEDIUM_PATTERNS),
211
+ complex: countPatternMatches(text, COMPLEX_PATTERNS),
212
+ expert: countPatternMatches(text, EXPERT_PATTERNS),
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Apply file pattern boosts to complexity scores.
218
+ */
219
+ function applyFilePatternBoosts(
220
+ text: string,
221
+ scores: Record<Complexity, number>
222
+ ): void {
223
+ for (const { pattern, complexity, boost } of FILE_PATTERN_BOOSTS) {
224
+ if (pattern.test(text)) {
225
+ scores[complexity] += boost;
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Determine the winning complexity level and confidence.
232
+ */
233
+ function determineComplexity(scores: Record<Complexity, number>): {
234
+ complexity: Complexity;
235
+ confidence: number;
236
+ reasoning: string;
237
+ } {
238
+ const entries = Object.entries(scores) as Array<[Complexity, number]>;
239
+ entries.sort((a, b) => b[1] - a[1]);
240
+
241
+ const [winner, winnerScore] = entries[0];
242
+ const [, runnerUpScore] = entries[1];
243
+
244
+ // Calculate confidence based on:
245
+ // 1. The absolute score of the winner
246
+ // 2. The gap between winner and runner-up
247
+ const baseConfidence = Math.min(0.5 + winnerScore, 0.95);
248
+ const gapBoost = Math.min((winnerScore - runnerUpScore) * 0.2, 0.15);
249
+ const confidence = Math.min(baseConfidence + gapBoost, 0.95);
250
+
251
+ // Generate reasoning
252
+ const signalsFound = entries
253
+ .filter(([, score]) => score > 0)
254
+ .map(([level, score]) => `${level}(${score.toFixed(2)})`)
255
+ .join(", ");
256
+
257
+ const reasoning =
258
+ signalsFound.length > 0
259
+ ? `Heuristic analysis found signals: ${signalsFound}. Winner: ${winner}`
260
+ : `No strong signals found, defaulting to ${winner}`;
261
+
262
+ return { complexity: winner, confidence, reasoning };
263
+ }
264
+
265
+ /**
266
+ * Classify a user story by complexity using hybrid approach.
267
+ *
268
+ * Phase 1: Fast heuristic analysis (< 50ms)
269
+ * Phase 2: LLM fallback if confidence < 0.8 (not implemented yet - returns heuristic)
270
+ *
271
+ * @param story - The user story to classify
272
+ * @returns Classification result with complexity, confidence, reasoning, and usedLLM flag
273
+ */
274
+ export async function classifyTask(
275
+ story: UserStory
276
+ ): Promise<ClassificationResult> {
277
+ // Get text to analyze
278
+ const text = getAnalyzableText(story);
279
+
280
+ // Phase 1: Heuristic analysis
281
+ const scores = calculateHeuristicScores(text);
282
+
283
+ // Apply file pattern boosts
284
+ applyFilePatternBoosts(text, scores);
285
+
286
+ // Determine winner and confidence
287
+ const { complexity, confidence, reasoning } = determineComplexity(scores);
288
+
289
+ // If confidence >= 0.8, use heuristic result directly
290
+ if (confidence >= 0.8) {
291
+ return {
292
+ complexity,
293
+ confidence,
294
+ reasoning,
295
+ usedLLM: false,
296
+ };
297
+ }
298
+
299
+ // Phase 2: LLM fallback for low confidence
300
+ // For now, we'll still return the heuristic result but mark it appropriately
301
+ // TODO: Implement actual LLM call for low confidence cases
302
+ // The LLM call would use Haiku for cost efficiency
303
+
304
+ // For low confidence cases, we still return the heuristic result
305
+ // but with a slight boost since we're being transparent about uncertainty
306
+ return {
307
+ complexity,
308
+ confidence: Math.min(confidence + 0.1, 0.79), // Keep below 0.8 to indicate uncertainty
309
+ reasoning: `${reasoning} (Low confidence - consider manual review)`,
310
+ usedLLM: false, // Will be true once LLM fallback is implemented
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Classify a task with explicit LLM fallback (for when needed).
316
+ *
317
+ * This is a placeholder for future LLM integration.
318
+ * Will be called when heuristic confidence < 0.8.
319
+ *
320
+ * @internal
321
+ */
322
+ export async function classifyWithLLM(
323
+ _story: UserStory,
324
+ _heuristicSuggestion: Complexity
325
+ ): Promise<ClassificationResult> {
326
+ // TODO: Implement LLM-based classification using Haiku
327
+ // The prompt would include:
328
+ // - Task title and description
329
+ // - Acceptance criteria
330
+ // - Heuristic suggestion for context
331
+ //
332
+ // LLM response should be JSON with:
333
+ // { complexity: "simple"|"medium"|"complex"|"expert", reasoning: "..." }
334
+
335
+ throw new Error(
336
+ "LLM classification not implemented yet. Use classifyTask() which handles fallback."
337
+ );
338
+ }