@arvorco/relentless 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/relentless.analyze.md +20 -0
- package/.claude/commands/relentless.checklist.md +15 -0
- package/.claude/commands/relentless.clarify.md +19 -0
- package/.claude/commands/relentless.constitution.md +78 -0
- package/.claude/commands/relentless.implement.md +15 -0
- package/.claude/commands/relentless.plan.md +22 -0
- package/.claude/commands/relentless.plan.old.md +89 -0
- package/.claude/commands/relentless.specify.md +254 -0
- package/.claude/commands/relentless.tasks.md +25 -0
- package/.claude/commands/relentless.taskstoissues.md +15 -0
- package/.claude/settings.local.json +23 -0
- package/.claude/skills/analyze/SKILL.md +149 -0
- package/.claude/skills/checklist/SKILL.md +173 -0
- package/.claude/skills/checklist/templates/checklist-template.md +40 -0
- package/.claude/skills/clarify/SKILL.md +174 -0
- package/.claude/skills/constitution/SKILL.md +150 -0
- package/.claude/skills/constitution/templates/constitution-template.md +228 -0
- package/.claude/skills/implement/SKILL.md +141 -0
- package/.claude/skills/plan/SKILL.md +179 -0
- package/.claude/skills/plan/templates/plan-template.md +104 -0
- package/.claude/skills/prd/SKILL.md +242 -0
- package/.claude/skills/relentless/SKILL.md +265 -0
- package/.claude/skills/specify/SKILL.md +220 -0
- package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.claude/skills/specify/scripts/bash/common.sh +156 -0
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
- package/.claude/skills/specify/templates/spec-template.md +115 -0
- package/.claude/skills/tasks/SKILL.md +202 -0
- package/.claude/skills/tasks/templates/tasks-template.md +251 -0
- package/.claude/skills/taskstoissues/SKILL.md +97 -0
- package/.specify/memory/constitution.md +50 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +104 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/CHANGES_SUMMARY.md +255 -0
- package/CLAUDE.md +92 -0
- package/GEMINI_SETUP.md +256 -0
- package/LICENSE +21 -0
- package/README.md +1171 -0
- package/REFACTOR_SUMMARY.md +267 -0
- package/bin/relentless.ts +536 -0
- package/bun.lock +352 -0
- package/eslint.config.js +37 -0
- package/package.json +61 -0
- package/prd.json.example +64 -0
- package/prompt.md +108 -0
- package/ralph.sh +80 -0
- package/relentless/config.json +38 -0
- package/relentless/features/.gitkeep +0 -0
- package/relentless/features/ghsk-ideas/prd.json +229 -0
- package/relentless/features/ghsk-ideas/prd.md +191 -0
- package/relentless/features/ghsk-ideas/progress.txt +408 -0
- package/relentless/prompt.md +79 -0
- package/skills/checklist/SKILL.md +349 -0
- package/skills/clarify/SKILL.md +476 -0
- package/skills/prd/SKILL.md +242 -0
- package/skills/relentless/SKILL.md +268 -0
- package/skills/tasks/SKILL.md +577 -0
- package/src/agents/amp.ts +115 -0
- package/src/agents/claude.ts +185 -0
- package/src/agents/codex.ts +89 -0
- package/src/agents/droid.ts +90 -0
- package/src/agents/gemini.ts +109 -0
- package/src/agents/index.ts +16 -0
- package/src/agents/opencode.ts +88 -0
- package/src/agents/registry.ts +95 -0
- package/src/agents/types.ts +101 -0
- package/src/config/index.ts +8 -0
- package/src/config/loader.ts +237 -0
- package/src/config/schema.ts +115 -0
- package/src/execution/index.ts +8 -0
- package/src/execution/router.ts +49 -0
- package/src/execution/runner.ts +512 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +7 -0
- package/src/init/scaffolder.ts +377 -0
- package/src/prd/analyzer.ts +512 -0
- package/src/prd/index.ts +11 -0
- package/src/prd/issues.ts +249 -0
- package/src/prd/parser.ts +281 -0
- package/src/prd/progress.ts +198 -0
- package/src/prd/types.ts +170 -0
- package/src/tui/App.tsx +85 -0
- package/src/tui/TUIRunner.tsx +400 -0
- package/src/tui/components/AgentOutput.tsx +45 -0
- package/src/tui/components/AgentStatus.tsx +64 -0
- package/src/tui/components/CurrentStory.tsx +66 -0
- package/src/tui/components/Header.tsx +49 -0
- package/src/tui/components/ProgressBar.tsx +39 -0
- package/src/tui/components/StoryGrid.tsx +86 -0
- package/src/tui/hooks/useTUI.ts +147 -0
- package/src/tui/hooks/useTimer.ts +51 -0
- package/src/tui/index.tsx +17 -0
- package/src/tui/theme.ts +41 -0
- package/src/tui/types.ts +77 -0
- package/templates/constitution.md +228 -0
- package/templates/plan.md +273 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relentless Agent Adapter Types
|
|
3
|
+
*
|
|
4
|
+
* Defines the interface that all AI coding agents must implement
|
|
5
|
+
* to work with the Relentless orchestrator.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type AgentName = "claude" | "amp" | "opencode" | "codex" | "droid" | "gemini";
|
|
9
|
+
|
|
10
|
+
export interface InvokeOptions {
|
|
11
|
+
/** Working directory for the agent */
|
|
12
|
+
workingDirectory?: string;
|
|
13
|
+
/** Timeout in milliseconds */
|
|
14
|
+
timeout?: number;
|
|
15
|
+
/** Model to use (agent-specific) */
|
|
16
|
+
model?: string;
|
|
17
|
+
/** Skip all permission prompts */
|
|
18
|
+
dangerouslyAllowAll?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rate limit detection result
|
|
23
|
+
*/
|
|
24
|
+
export interface RateLimitInfo {
|
|
25
|
+
/** Whether the agent is rate limited */
|
|
26
|
+
limited: boolean;
|
|
27
|
+
/** When the rate limit resets (if known) */
|
|
28
|
+
resetTime?: Date;
|
|
29
|
+
/** Raw error message */
|
|
30
|
+
message?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AgentResult {
|
|
34
|
+
/** Raw output from the agent */
|
|
35
|
+
output: string;
|
|
36
|
+
/** Exit code of the process */
|
|
37
|
+
exitCode: number;
|
|
38
|
+
/** Whether the agent signaled completion */
|
|
39
|
+
isComplete: boolean;
|
|
40
|
+
/** Duration in milliseconds */
|
|
41
|
+
duration: number;
|
|
42
|
+
/** Whether the agent hit rate limits */
|
|
43
|
+
rateLimited?: boolean;
|
|
44
|
+
/** When the rate limit resets */
|
|
45
|
+
resetTime?: Date;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AgentAdapter {
|
|
49
|
+
/** Agent identifier */
|
|
50
|
+
name: AgentName;
|
|
51
|
+
|
|
52
|
+
/** Human-readable display name */
|
|
53
|
+
displayName: string;
|
|
54
|
+
|
|
55
|
+
/** Check if the agent is installed on this system */
|
|
56
|
+
isInstalled(): Promise<boolean>;
|
|
57
|
+
|
|
58
|
+
/** Get the path to the agent executable */
|
|
59
|
+
getExecutablePath(): Promise<string | null>;
|
|
60
|
+
|
|
61
|
+
/** Invoke the agent with a prompt */
|
|
62
|
+
invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult>;
|
|
63
|
+
|
|
64
|
+
/** Invoke the agent with streaming output */
|
|
65
|
+
invokeStream?(
|
|
66
|
+
prompt: string,
|
|
67
|
+
options?: InvokeOptions
|
|
68
|
+
): AsyncGenerator<string, AgentResult, unknown>;
|
|
69
|
+
|
|
70
|
+
/** Detect if the output indicates completion */
|
|
71
|
+
detectCompletion(output: string): boolean;
|
|
72
|
+
|
|
73
|
+
/** Detect if the output indicates rate limiting */
|
|
74
|
+
detectRateLimit(output: string): RateLimitInfo;
|
|
75
|
+
|
|
76
|
+
/** Whether this agent supports skills/plugins */
|
|
77
|
+
hasSkillSupport: boolean;
|
|
78
|
+
|
|
79
|
+
/** Command to install skills for this agent (if supported) */
|
|
80
|
+
skillInstallCommand?: string;
|
|
81
|
+
|
|
82
|
+
/** Install skills to the agent (if supported) */
|
|
83
|
+
installSkills?(projectPath: string): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Story types for smart routing
|
|
88
|
+
*/
|
|
89
|
+
export type StoryType = "database" | "ui" | "api" | "test" | "refactor" | "docs" | "general";
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Agent specializations for smart routing
|
|
93
|
+
*/
|
|
94
|
+
export const AGENT_SPECIALIZATIONS: Record<AgentName, StoryType[]> = {
|
|
95
|
+
claude: ["database", "refactor", "api", "general"],
|
|
96
|
+
amp: ["ui", "test", "general"],
|
|
97
|
+
opencode: ["api", "general"],
|
|
98
|
+
codex: ["api", "database", "general"],
|
|
99
|
+
droid: ["refactor", "test", "general"],
|
|
100
|
+
gemini: ["docs", "general"],
|
|
101
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads and validates relentless/config.json and constitution.md
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { RelentlessConfigSchema, DEFAULT_CONFIG, type RelentlessConfig } from "./schema";
|
|
10
|
+
|
|
11
|
+
const CONFIG_FILENAME = "config.json";
|
|
12
|
+
const CONSTITUTION_FILENAME = "constitution.md";
|
|
13
|
+
const RELENTLESS_DIR = "relentless";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find the relentless directory in the current or parent directories
|
|
17
|
+
*/
|
|
18
|
+
export function findRelentlessDir(startDir: string = process.cwd()): string | null {
|
|
19
|
+
let dir = startDir;
|
|
20
|
+
|
|
21
|
+
while (dir !== "/") {
|
|
22
|
+
const relentlessPath = join(dir, RELENTLESS_DIR);
|
|
23
|
+
if (existsSync(relentlessPath)) {
|
|
24
|
+
return relentlessPath;
|
|
25
|
+
}
|
|
26
|
+
dir = join(dir, "..");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find the config file in the relentless directory
|
|
34
|
+
*/
|
|
35
|
+
export function findConfigFile(startDir: string = process.cwd()): string | null {
|
|
36
|
+
const relentlessDir = findRelentlessDir(startDir);
|
|
37
|
+
if (!relentlessDir) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const configPath = join(relentlessDir, CONFIG_FILENAME);
|
|
42
|
+
if (existsSync(configPath)) {
|
|
43
|
+
return configPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load configuration from file
|
|
51
|
+
*/
|
|
52
|
+
export async function loadConfig(configPath?: string): Promise<RelentlessConfig> {
|
|
53
|
+
const path = configPath ?? findConfigFile();
|
|
54
|
+
|
|
55
|
+
if (!path) {
|
|
56
|
+
console.warn(`No relentless/${CONFIG_FILENAME} found, using defaults`);
|
|
57
|
+
return DEFAULT_CONFIG;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const content = await Bun.file(path).text();
|
|
62
|
+
const json = JSON.parse(content);
|
|
63
|
+
const validated = RelentlessConfigSchema.parse(json);
|
|
64
|
+
return validated;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error instanceof SyntaxError) {
|
|
67
|
+
throw new Error(`Invalid JSON in ${path}: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Save configuration to file
|
|
75
|
+
*/
|
|
76
|
+
export async function saveConfig(
|
|
77
|
+
config: RelentlessConfig,
|
|
78
|
+
path: string = join(process.cwd(), RELENTLESS_DIR, CONFIG_FILENAME)
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
const validated = RelentlessConfigSchema.parse(config);
|
|
81
|
+
const content = JSON.stringify(validated, null, 2);
|
|
82
|
+
await Bun.write(path, content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a default configuration file
|
|
87
|
+
*/
|
|
88
|
+
export async function createDefaultConfig(
|
|
89
|
+
dir: string = process.cwd()
|
|
90
|
+
): Promise<string> {
|
|
91
|
+
const path = join(dir, RELENTLESS_DIR, CONFIG_FILENAME);
|
|
92
|
+
await saveConfig(DEFAULT_CONFIG, path);
|
|
93
|
+
return path;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Find the constitution file in the relentless directory
|
|
98
|
+
*/
|
|
99
|
+
export function findConstitutionFile(startDir: string = process.cwd()): string | null {
|
|
100
|
+
const relentlessDir = findRelentlessDir(startDir);
|
|
101
|
+
if (!relentlessDir) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const constitutionPath = join(relentlessDir, CONSTITUTION_FILENAME);
|
|
106
|
+
if (existsSync(constitutionPath)) {
|
|
107
|
+
return constitutionPath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Constitution principle level
|
|
115
|
+
*/
|
|
116
|
+
export type PrincipleLevel = "MUST" | "SHOULD";
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parsed constitution principle
|
|
120
|
+
*/
|
|
121
|
+
export interface Principle {
|
|
122
|
+
level: PrincipleLevel;
|
|
123
|
+
text: string;
|
|
124
|
+
section: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parsed constitution
|
|
129
|
+
*/
|
|
130
|
+
export interface Constitution {
|
|
131
|
+
raw: string;
|
|
132
|
+
principles: Principle[];
|
|
133
|
+
sections: string[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse constitution markdown to extract MUST/SHOULD principles
|
|
138
|
+
*/
|
|
139
|
+
export function parseConstitution(content: string): Constitution {
|
|
140
|
+
const principles: Principle[] = [];
|
|
141
|
+
const sections = new Set<string>();
|
|
142
|
+
|
|
143
|
+
let currentSection = "";
|
|
144
|
+
const lines = content.split("\n");
|
|
145
|
+
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
// Track current section
|
|
148
|
+
if (line.startsWith("## ")) {
|
|
149
|
+
currentSection = line.replace("## ", "").trim();
|
|
150
|
+
sections.add(currentSection);
|
|
151
|
+
} else if (line.startsWith("### ")) {
|
|
152
|
+
currentSection = line.replace("### ", "").trim();
|
|
153
|
+
sections.add(currentSection);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Extract MUST principles
|
|
157
|
+
const mustMatch = line.match(/\*\*MUST\*\*\s+(.+)/);
|
|
158
|
+
if (mustMatch) {
|
|
159
|
+
principles.push({
|
|
160
|
+
level: "MUST",
|
|
161
|
+
text: mustMatch[1].trim(),
|
|
162
|
+
section: currentSection,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Extract SHOULD principles
|
|
167
|
+
const shouldMatch = line.match(/\*\*SHOULD\*\*\s+(.+)/);
|
|
168
|
+
if (shouldMatch) {
|
|
169
|
+
principles.push({
|
|
170
|
+
level: "SHOULD",
|
|
171
|
+
text: shouldMatch[1].trim(),
|
|
172
|
+
section: currentSection,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
raw: content,
|
|
179
|
+
principles,
|
|
180
|
+
sections: Array.from(sections),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Load constitution from file
|
|
186
|
+
*/
|
|
187
|
+
export async function loadConstitution(constitutionPath?: string): Promise<Constitution | null> {
|
|
188
|
+
const path = constitutionPath ?? findConstitutionFile();
|
|
189
|
+
|
|
190
|
+
if (!path || !existsSync(path)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const content = await Bun.file(path).text();
|
|
196
|
+
return parseConstitution(content);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.warn(`Failed to load constitution: ${error}`);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Validate constitution format
|
|
205
|
+
*/
|
|
206
|
+
export function validateConstitution(constitution: Constitution): { valid: boolean; errors: string[] } {
|
|
207
|
+
const errors: string[] = [];
|
|
208
|
+
|
|
209
|
+
// Check that we have at least some principles
|
|
210
|
+
if (constitution.principles.length === 0) {
|
|
211
|
+
errors.push("Constitution has no MUST or SHOULD principles defined");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check that we have both MUST and SHOULD principles
|
|
215
|
+
const hasMust = constitution.principles.some(p => p.level === "MUST");
|
|
216
|
+
const hasShould = constitution.principles.some(p => p.level === "SHOULD");
|
|
217
|
+
|
|
218
|
+
if (!hasMust) {
|
|
219
|
+
errors.push("Constitution has no MUST principles (required directives)");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!hasShould) {
|
|
223
|
+
errors.push("Constitution has no SHOULD principles (recommended guidelines)");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check for empty principle text
|
|
227
|
+
for (const principle of constitution.principles) {
|
|
228
|
+
if (!principle.text || principle.text.trim().length === 0) {
|
|
229
|
+
errors.push(`Empty ${principle.level} principle in section: ${principle.section}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
valid: errors.length === 0,
|
|
235
|
+
errors,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relentless Configuration Schema
|
|
3
|
+
*
|
|
4
|
+
* Defines the structure and validation for relentless.config.json
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
// Agent and story types defined inline in Zod schemas below
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Agent-specific configuration
|
|
12
|
+
*/
|
|
13
|
+
export const AgentConfigSchema = z.object({
|
|
14
|
+
model: z.string().optional(),
|
|
15
|
+
dangerouslyAllowAll: z.boolean().default(true),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type AgentConfig = z.infer<typeof AgentConfigSchema>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Routing rule for smart agent selection
|
|
22
|
+
*/
|
|
23
|
+
export const RoutingRuleSchema = z.object({
|
|
24
|
+
storyType: z.enum(["database", "ui", "api", "test", "refactor", "docs", "general"]),
|
|
25
|
+
agent: z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type RoutingRule = z.infer<typeof RoutingRuleSchema>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fallback configuration for automatic agent switching
|
|
32
|
+
*/
|
|
33
|
+
export const FallbackConfigSchema = z.object({
|
|
34
|
+
/** Enable automatic fallback when rate limited */
|
|
35
|
+
enabled: z.boolean().default(true),
|
|
36
|
+
/** Priority order for agents (first = preferred) */
|
|
37
|
+
priority: z.array(z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]))
|
|
38
|
+
.default(["claude", "codex", "amp", "opencode", "gemini"]),
|
|
39
|
+
/** Automatically switch back when limits reset */
|
|
40
|
+
autoRecovery: z.boolean().default(true),
|
|
41
|
+
/** Delay (ms) before trying fallback agent */
|
|
42
|
+
retryDelay: z.number().int().nonnegative().default(2000),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type FallbackConfig = z.infer<typeof FallbackConfigSchema>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Execution configuration
|
|
49
|
+
*/
|
|
50
|
+
export const ExecutionConfigSchema = z.object({
|
|
51
|
+
maxIterations: z.number().int().positive().default(20),
|
|
52
|
+
iterationDelay: z.number().int().nonnegative().default(2000),
|
|
53
|
+
timeout: z.number().int().positive().default(600000), // 10 minutes
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type ExecutionConfig = z.infer<typeof ExecutionConfigSchema>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Routing configuration
|
|
60
|
+
*/
|
|
61
|
+
export const RoutingConfigSchema = z.object({
|
|
62
|
+
rules: z.array(RoutingRuleSchema).default([]),
|
|
63
|
+
default: z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]).default("claude"),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type RoutingConfig = z.infer<typeof RoutingConfigSchema>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Complete Relentless configuration
|
|
70
|
+
*/
|
|
71
|
+
export const RelentlessConfigSchema = z.object({
|
|
72
|
+
defaultAgent: z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]).default("claude"),
|
|
73
|
+
agents: z.record(
|
|
74
|
+
z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]),
|
|
75
|
+
AgentConfigSchema
|
|
76
|
+
).default({}),
|
|
77
|
+
routing: RoutingConfigSchema.default({}),
|
|
78
|
+
fallback: FallbackConfigSchema.default({}),
|
|
79
|
+
execution: ExecutionConfigSchema.default({}),
|
|
80
|
+
prompt: z.object({
|
|
81
|
+
path: z.string().default("prompt.md"),
|
|
82
|
+
}).default({}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export type RelentlessConfig = z.infer<typeof RelentlessConfigSchema>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Default configuration
|
|
89
|
+
*/
|
|
90
|
+
export const DEFAULT_CONFIG: RelentlessConfig = {
|
|
91
|
+
defaultAgent: "claude",
|
|
92
|
+
agents: {
|
|
93
|
+
claude: { dangerouslyAllowAll: true },
|
|
94
|
+
amp: { dangerouslyAllowAll: true },
|
|
95
|
+
gemini: { dangerouslyAllowAll: true },
|
|
96
|
+
},
|
|
97
|
+
routing: {
|
|
98
|
+
rules: [],
|
|
99
|
+
default: "claude",
|
|
100
|
+
},
|
|
101
|
+
fallback: {
|
|
102
|
+
enabled: true,
|
|
103
|
+
priority: ["claude", "codex", "amp", "opencode", "gemini"],
|
|
104
|
+
autoRecovery: true,
|
|
105
|
+
retryDelay: 2000,
|
|
106
|
+
},
|
|
107
|
+
execution: {
|
|
108
|
+
maxIterations: 20,
|
|
109
|
+
iterationDelay: 2000,
|
|
110
|
+
timeout: 600000,
|
|
111
|
+
},
|
|
112
|
+
prompt: {
|
|
113
|
+
path: "prompt.md",
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Agent Router
|
|
3
|
+
*
|
|
4
|
+
* Routes stories to the best-suited agent based on story type
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentName, StoryType } from "../agents/types";
|
|
8
|
+
import { AGENT_SPECIALIZATIONS } from "../agents/types";
|
|
9
|
+
import type { RoutingConfig } from "../config/schema";
|
|
10
|
+
import { inferStoryType, type UserStory } from "../prd/types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Route a story to the best agent
|
|
14
|
+
*/
|
|
15
|
+
export function routeStory(story: UserStory, routing: RoutingConfig): AgentName {
|
|
16
|
+
const storyType = inferStoryType(story) as StoryType;
|
|
17
|
+
|
|
18
|
+
// Check explicit routing rules first
|
|
19
|
+
const rule = routing.rules.find((r) => r.storyType === storyType);
|
|
20
|
+
if (rule) {
|
|
21
|
+
return rule.agent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Find best agent based on specializations
|
|
25
|
+
const scores = Object.entries(AGENT_SPECIALIZATIONS).map(([agent, specializations]) => {
|
|
26
|
+
const score = specializations.includes(storyType) ? 2 : 0;
|
|
27
|
+
const generalScore = specializations.includes("general") ? 1 : 0;
|
|
28
|
+
return { agent: agent as AgentName, score: score + generalScore };
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Sort by score descending
|
|
32
|
+
scores.sort((a, b) => b.score - a.score);
|
|
33
|
+
|
|
34
|
+
// If no good match, use default
|
|
35
|
+
if (scores[0].score === 0) {
|
|
36
|
+
return routing.default;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return scores[0].agent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get recommended agent for a story type
|
|
44
|
+
*/
|
|
45
|
+
export function getRecommendedAgent(storyType: StoryType): AgentName[] {
|
|
46
|
+
return Object.entries(AGENT_SPECIALIZATIONS)
|
|
47
|
+
.filter(([, specializations]) => specializations.includes(storyType))
|
|
48
|
+
.map(([agent]) => agent as AgentName);
|
|
49
|
+
}
|