@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.
- package/.claude/commands/relentless.convert.md +25 -0
- package/.claude/skills/analyze/SKILL.md +113 -40
- package/.claude/skills/analyze/templates/analysis-report.md +138 -0
- package/.claude/skills/checklist/SKILL.md +144 -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 +139 -27
- package/.claude/skills/plan/templates/plan.md +92 -9
- package/.claude/skills/specify/SKILL.md +112 -20
- package/.claude/skills/specify/templates/spec.md +40 -5
- package/.claude/skills/tasks/SKILL.md +76 -1
- package/.claude/skills/tasks/templates/tasks.md +5 -4
- package/CHANGELOG.md +84 -1
- package/MANUAL.md +40 -0
- package/README.md +268 -13
- package/bin/relentless.ts +292 -5
- package/package.json +2 -2
- package/relentless/config.json +45 -1
- package/relentless/constitution.md +41 -19
- package/relentless/prompt.md +142 -72
- 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 +111 -6
- 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
|
@@ -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
|
+
}
|