@arvorco/relentless 0.3.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/relentless.constitution.md +1 -1
- package/.claude/commands/relentless.convert.md +25 -0
- package/.claude/commands/relentless.specify.md +1 -1
- package/.claude/skills/analyze/SKILL.md +113 -40
- package/.claude/skills/analyze/templates/analysis-report.md +138 -0
- package/.claude/skills/checklist/SKILL.md +143 -51
- package/.claude/skills/checklist/templates/checklist.md +43 -11
- package/.claude/skills/clarify/SKILL.md +70 -11
- package/.claude/skills/constitution/SKILL.md +61 -3
- package/.claude/skills/constitution/templates/constitution.md +241 -160
- package/.claude/skills/constitution/templates/prompt.md +150 -20
- package/.claude/skills/convert/SKILL.md +248 -0
- package/.claude/skills/implement/SKILL.md +82 -34
- package/.claude/skills/plan/SKILL.md +136 -27
- package/.claude/skills/plan/templates/plan.md +92 -9
- package/.claude/skills/specify/SKILL.md +110 -19
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +2 -2
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +1 -1
- package/.claude/skills/specify/templates/spec.md +40 -5
- package/.claude/skills/tasks/SKILL.md +75 -1
- package/.claude/skills/tasks/templates/tasks.md +5 -4
- package/CHANGELOG.md +63 -1
- package/MANUAL.md +40 -0
- package/README.md +263 -11
- package/bin/relentless.ts +292 -5
- package/package.json +2 -2
- package/relentless/config.json +46 -2
- package/relentless/constitution.md +2 -2
- package/relentless/prompt.md +97 -18
- package/src/agents/amp.ts +53 -13
- package/src/agents/claude.ts +70 -15
- package/src/agents/codex.ts +73 -14
- package/src/agents/droid.ts +68 -14
- package/src/agents/exec.ts +96 -0
- package/src/agents/gemini.ts +59 -16
- package/src/agents/opencode.ts +188 -9
- package/src/cli/fallback-order.ts +210 -0
- package/src/cli/index.ts +63 -0
- package/src/cli/mode-flag.ts +198 -0
- package/src/cli/review-flags.ts +192 -0
- package/src/config/loader.ts +16 -1
- package/src/config/schema.ts +157 -2
- package/src/execution/runner.ts +144 -21
- package/src/init/scaffolder.ts +285 -25
- package/src/prd/parser.ts +92 -1
- package/src/prd/types.ts +136 -0
- package/src/review/index.ts +92 -0
- package/src/review/prompt.ts +293 -0
- package/src/review/runner.ts +337 -0
- package/src/review/tasks/docs.ts +529 -0
- package/src/review/tasks/index.ts +80 -0
- package/src/review/tasks/lint.ts +436 -0
- package/src/review/tasks/quality.ts +760 -0
- package/src/review/tasks/security.ts +452 -0
- package/src/review/tasks/test.ts +456 -0
- package/src/review/tasks/typecheck.ts +323 -0
- package/src/review/types.ts +139 -0
- package/src/routing/cascade.ts +310 -0
- package/src/routing/classifier.ts +338 -0
- package/src/routing/estimate.ts +270 -0
- package/src/routing/fallback.ts +512 -0
- package/src/routing/index.ts +124 -0
- package/src/routing/registry.ts +501 -0
- package/src/routing/report.ts +570 -0
- package/src/routing/router.ts +287 -0
- package/src/tui/App.tsx +2 -0
- package/src/tui/TUIRunner.tsx +103 -8
- package/src/tui/components/CurrentStory.tsx +23 -1
- package/src/tui/hooks/useTUI.ts +1 -0
- package/src/tui/types.ts +9 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +0 -799
package/src/agents/amp.ts
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Amp Agent Adapter
|
|
3
3
|
*
|
|
4
|
-
* Adapter for Sourcegraph's Amp CLI
|
|
4
|
+
* Adapter for Sourcegraph's Amp CLI with model/mode selection support.
|
|
5
5
|
* https://ampcode.com
|
|
6
|
+
*
|
|
7
|
+
* ## Supported Modes
|
|
8
|
+
* - `free` - Free tier mode ($10/day grant, may have rate limits)
|
|
9
|
+
* - `smart` - Smart mode (uses Amp's intelligent model selection)
|
|
10
|
+
*
|
|
11
|
+
* ## Model Selection Method
|
|
12
|
+
* Amp uses the `-m`/`--mode` CLI flag for mode selection.
|
|
13
|
+
* Amp uses `-x`/`--execute` to run a single prompt non-interactively.
|
|
14
|
+
*
|
|
15
|
+
* ## Usage Example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const result = await ampAdapter.invoke("Fix the bug", {
|
|
18
|
+
* model: "free", // Uses -m free
|
|
19
|
+
* workingDirectory: "/path/to/project"
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module agents/amp
|
|
6
24
|
*/
|
|
7
25
|
|
|
8
26
|
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
|
|
27
|
+
import { getModelById } from "../routing/registry";
|
|
28
|
+
import { runCommand } from "./exec";
|
|
9
29
|
|
|
10
30
|
export const ampAdapter: AgentAdapter = {
|
|
11
31
|
name: "amp",
|
|
@@ -37,31 +57,41 @@ export const ampAdapter: AgentAdapter = {
|
|
|
37
57
|
},
|
|
38
58
|
|
|
39
59
|
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
40
|
-
const startTime = Date.now();
|
|
41
60
|
const args: string[] = [];
|
|
42
61
|
|
|
43
62
|
if (options?.dangerouslyAllowAll) {
|
|
44
63
|
args.push("--dangerously-allow-all");
|
|
45
64
|
}
|
|
46
65
|
|
|
47
|
-
|
|
66
|
+
// Add mode flag if model is provided
|
|
67
|
+
if (options?.model) {
|
|
68
|
+
const modelProfile = getModelById(options.model);
|
|
69
|
+
const modeValue =
|
|
70
|
+
modelProfile?.harness === "amp" ? modelProfile.cliValue : options.model;
|
|
71
|
+
const modeFlag =
|
|
72
|
+
modelProfile?.harness === "amp" ? modelProfile.cliFlag : "-m";
|
|
73
|
+
args.push(modeFlag, modeValue);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Execute single prompt in non-interactive mode
|
|
77
|
+
args.push("-x");
|
|
78
|
+
|
|
79
|
+
const result = await runCommand(["amp", ...args], {
|
|
48
80
|
cwd: options?.workingDirectory,
|
|
49
81
|
stdin: new Blob([prompt]),
|
|
50
|
-
|
|
51
|
-
stderr: "pipe",
|
|
82
|
+
timeoutMs: options?.timeout,
|
|
52
83
|
});
|
|
53
84
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const duration = Date.now() - startTime;
|
|
85
|
+
const timeoutNote =
|
|
86
|
+
result.timedOut && options?.timeout
|
|
87
|
+
? `\n[Relentless] Idle timeout after ${options.timeout}ms.`
|
|
88
|
+
: "";
|
|
89
|
+
const output = result.stdout + (result.stderr ? `\n${result.stderr}` : "") + timeoutNote;
|
|
90
|
+
const duration = result.duration;
|
|
61
91
|
|
|
62
92
|
return {
|
|
63
93
|
output,
|
|
64
|
-
exitCode,
|
|
94
|
+
exitCode: result.exitCode,
|
|
65
95
|
isComplete: this.detectCompletion(output),
|
|
66
96
|
duration,
|
|
67
97
|
};
|
|
@@ -72,12 +102,22 @@ export const ampAdapter: AgentAdapter = {
|
|
|
72
102
|
},
|
|
73
103
|
|
|
74
104
|
detectRateLimit(output: string): RateLimitInfo {
|
|
105
|
+
if (output.includes("[Relentless] Idle timeout")) {
|
|
106
|
+
return {
|
|
107
|
+
limited: true,
|
|
108
|
+
message: "Amp idle timeout",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
75
112
|
// Amp rate limit patterns
|
|
76
113
|
const patterns = [
|
|
77
114
|
/quota exceeded/i,
|
|
78
115
|
/limit reached/i,
|
|
79
116
|
/rate limit/i,
|
|
80
117
|
/too many requests/i,
|
|
118
|
+
/execute mode is not permitted/i,
|
|
119
|
+
/unexpected error inside amp cli/i,
|
|
120
|
+
/amp threads share --support/i,
|
|
81
121
|
];
|
|
82
122
|
|
|
83
123
|
for (const pattern of patterns) {
|
package/src/agents/claude.ts
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude Code Agent Adapter
|
|
3
3
|
*
|
|
4
|
-
* Adapter for Anthropic's Claude Code CLI
|
|
4
|
+
* Adapter for Anthropic's Claude Code CLI.
|
|
5
5
|
* https://docs.anthropic.com/claude-code
|
|
6
|
+
*
|
|
7
|
+
* ## Model Selection
|
|
8
|
+
*
|
|
9
|
+
* Claude Code supports model selection via the `--model` flag.
|
|
10
|
+
* Pass the model name in the `options.model` parameter.
|
|
11
|
+
*
|
|
12
|
+
* **Supported models:**
|
|
13
|
+
* - `opus-4.5` (claude-opus-4-5-20251101) - SOTA, best for code review and architecture
|
|
14
|
+
* - `sonnet-4.5` (claude-sonnet-4-5-20251020) - Balanced, good for daily coding
|
|
15
|
+
* - `haiku-4.5` (claude-haiku-4-5-20251001) - Fast and cheap, good for simple tasks
|
|
16
|
+
*
|
|
17
|
+
* These IDs map to the full Claude model identifiers in the CLI.
|
|
18
|
+
*
|
|
19
|
+
* **CLI command format:**
|
|
20
|
+
* ```
|
|
21
|
+
* claude --model <model> -p <prompt>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const result = await claudeAdapter.invoke("Fix the bug", {
|
|
27
|
+
* model: "opus-4.5",
|
|
28
|
+
* workingDirectory: "/path/to/project"
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
6
31
|
*/
|
|
7
32
|
|
|
8
33
|
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types.js";
|
|
34
|
+
import { getModelById } from "../routing/registry";
|
|
35
|
+
import { runCommand } from "./exec";
|
|
9
36
|
|
|
10
37
|
export const claudeAdapter: AgentAdapter = {
|
|
11
38
|
name: "claude",
|
|
@@ -37,7 +64,6 @@ export const claudeAdapter: AgentAdapter = {
|
|
|
37
64
|
},
|
|
38
65
|
|
|
39
66
|
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
40
|
-
const startTime = Date.now();
|
|
41
67
|
const args = ["-p"];
|
|
42
68
|
|
|
43
69
|
if (options?.dangerouslyAllowAll) {
|
|
@@ -45,27 +71,30 @@ export const claudeAdapter: AgentAdapter = {
|
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
if (options?.model) {
|
|
48
|
-
|
|
74
|
+
const modelProfile = getModelById(options.model);
|
|
75
|
+
if (modelProfile?.harness === "claude") {
|
|
76
|
+
args.push("--model", modelProfile.cliValue);
|
|
77
|
+
} else {
|
|
78
|
+
args.push("--model", options.model);
|
|
79
|
+
}
|
|
49
80
|
}
|
|
50
81
|
|
|
51
|
-
const
|
|
82
|
+
const result = await runCommand(["claude", ...args], {
|
|
52
83
|
cwd: options?.workingDirectory,
|
|
53
84
|
stdin: new Blob([prompt]),
|
|
54
|
-
|
|
55
|
-
stderr: "pipe",
|
|
85
|
+
timeoutMs: options?.timeout,
|
|
56
86
|
});
|
|
57
87
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const duration = Date.now() - startTime;
|
|
88
|
+
const timeoutNote =
|
|
89
|
+
result.timedOut && options?.timeout
|
|
90
|
+
? `\n[Relentless] Idle timeout after ${options.timeout}ms.`
|
|
91
|
+
: "";
|
|
92
|
+
const output = result.stdout + (result.stderr ? `\n${result.stderr}` : "") + timeoutNote;
|
|
93
|
+
const duration = result.duration;
|
|
65
94
|
|
|
66
95
|
return {
|
|
67
96
|
output,
|
|
68
|
-
exitCode,
|
|
97
|
+
exitCode: result.exitCode,
|
|
69
98
|
isComplete: this.detectCompletion(output),
|
|
70
99
|
duration,
|
|
71
100
|
};
|
|
@@ -83,7 +112,12 @@ export const claudeAdapter: AgentAdapter = {
|
|
|
83
112
|
}
|
|
84
113
|
|
|
85
114
|
if (options?.model) {
|
|
86
|
-
|
|
115
|
+
const modelProfile = getModelById(options.model);
|
|
116
|
+
if (modelProfile?.harness === "claude") {
|
|
117
|
+
args.push("--model", modelProfile.cliValue);
|
|
118
|
+
} else {
|
|
119
|
+
args.push("--model", options.model);
|
|
120
|
+
}
|
|
87
121
|
}
|
|
88
122
|
|
|
89
123
|
const proc = Bun.spawn(["claude", ...args], {
|
|
@@ -130,6 +164,27 @@ export const claudeAdapter: AgentAdapter = {
|
|
|
130
164
|
},
|
|
131
165
|
|
|
132
166
|
detectRateLimit(output: string): RateLimitInfo {
|
|
167
|
+
if (output.includes("[Relentless] Idle timeout")) {
|
|
168
|
+
return {
|
|
169
|
+
limited: true,
|
|
170
|
+
message: "Claude idle timeout",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (/(?:operation not permitted|permission denied|\beperm\b).*(?:\/\.claude|\.claude)/i.test(output)) {
|
|
175
|
+
return {
|
|
176
|
+
limited: true,
|
|
177
|
+
message: "Claude unavailable due to permission error",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (/not_found_error/i.test(output) && /model/i.test(output)) {
|
|
182
|
+
return {
|
|
183
|
+
limited: true,
|
|
184
|
+
message: "Claude model not found",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
133
188
|
// Pattern: "You've hit your limit · resets 12am (America/Sao_Paulo)"
|
|
134
189
|
if (output.includes("You've hit your limit") || output.includes("you've hit your limit")) {
|
|
135
190
|
const resetMatch = output.match(/resets\s+(\d{1,2})(am|pm)/i);
|
package/src/agents/codex.ts
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codex Agent Adapter
|
|
3
3
|
*
|
|
4
|
-
* Adapter for OpenAI's Codex CLI
|
|
4
|
+
* Adapter for OpenAI's Codex CLI.
|
|
5
5
|
* https://developers.openai.com/codex/cli/
|
|
6
|
+
*
|
|
7
|
+
* ## Model Selection
|
|
8
|
+
*
|
|
9
|
+
* Codex supports model selection via the `--model` flag.
|
|
10
|
+
* Pass the model name in the `options.model` parameter.
|
|
11
|
+
*
|
|
12
|
+
* **Supported models (GPT-5.2 reasoning tiers):**
|
|
13
|
+
* - `gpt-5.2-xhigh` - reasoning-effort xhigh (~$1.75/$14 per MTok)
|
|
14
|
+
* - `gpt-5.2-high` - reasoning-effort high (~$1.75/$14 per MTok)
|
|
15
|
+
* - `gpt-5.2-medium` - reasoning-effort medium (~$1.25/$10 per MTok)
|
|
16
|
+
* - `gpt-5.2-low` - reasoning-effort low (~$0.75/$6 per MTok)
|
|
17
|
+
*
|
|
18
|
+
* These IDs map to `--model gpt-5.2 -c reasoning_effort="<tier>"` in the CLI.
|
|
19
|
+
*
|
|
20
|
+
* **CLI command format:**
|
|
21
|
+
* ```
|
|
22
|
+
* codex exec --model gpt-5.2 -c reasoning_effort="<low|medium|high|xhigh>" -
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const result = await codexAdapter.invoke("Fix the bug", {
|
|
28
|
+
* model: "gpt-5.2-high",
|
|
29
|
+
* workingDirectory: "/path/to/project"
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
6
32
|
*/
|
|
7
33
|
|
|
8
34
|
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
|
|
35
|
+
import { getModelById } from "../routing/registry";
|
|
36
|
+
import { runCommand } from "./exec";
|
|
9
37
|
|
|
10
38
|
export const codexAdapter: AgentAdapter = {
|
|
11
39
|
name: "codex",
|
|
@@ -37,27 +65,40 @@ export const codexAdapter: AgentAdapter = {
|
|
|
37
65
|
},
|
|
38
66
|
|
|
39
67
|
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
40
|
-
const
|
|
68
|
+
const args = ["exec"];
|
|
69
|
+
|
|
70
|
+
// Add model selection if specified
|
|
71
|
+
if (options?.model) {
|
|
72
|
+
const modelProfile = getModelById(options.model);
|
|
73
|
+
if (modelProfile?.harness === "codex") {
|
|
74
|
+
args.push(modelProfile.cliFlag, modelProfile.cliValue);
|
|
75
|
+
if (modelProfile.cliArgs) {
|
|
76
|
+
args.push(...modelProfile.cliArgs);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
args.push("--model", options.model);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Codex uses `-` to read from stdin
|
|
84
|
+
args.push("-");
|
|
41
85
|
|
|
42
|
-
|
|
43
|
-
const proc = Bun.spawn(["codex", "exec", "-"], {
|
|
86
|
+
const result = await runCommand(["codex", ...args], {
|
|
44
87
|
cwd: options?.workingDirectory,
|
|
45
88
|
stdin: new Blob([prompt]),
|
|
46
|
-
|
|
47
|
-
stderr: "pipe",
|
|
89
|
+
timeoutMs: options?.timeout,
|
|
48
90
|
});
|
|
49
91
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const duration = Date.now() - startTime;
|
|
92
|
+
const timeoutNote =
|
|
93
|
+
result.timedOut && options?.timeout
|
|
94
|
+
? `\n[Relentless] Idle timeout after ${options.timeout}ms.`
|
|
95
|
+
: "";
|
|
96
|
+
const output = result.stdout + (result.stderr ? `\n${result.stderr}` : "") + timeoutNote;
|
|
97
|
+
const duration = result.duration;
|
|
57
98
|
|
|
58
99
|
return {
|
|
59
100
|
output,
|
|
60
|
-
exitCode,
|
|
101
|
+
exitCode: result.exitCode,
|
|
61
102
|
isComplete: this.detectCompletion(output),
|
|
62
103
|
duration,
|
|
63
104
|
};
|
|
@@ -68,6 +109,24 @@ export const codexAdapter: AgentAdapter = {
|
|
|
68
109
|
},
|
|
69
110
|
|
|
70
111
|
detectRateLimit(output: string): RateLimitInfo {
|
|
112
|
+
if (output.includes("[Relentless] Idle timeout")) {
|
|
113
|
+
return {
|
|
114
|
+
limited: true,
|
|
115
|
+
message: "Codex idle timeout",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
/cannot access session files/i.test(output) ||
|
|
121
|
+
/failed to create session/i.test(output) ||
|
|
122
|
+
/(permission denied|operation not permitted).*\/\.codex\/sessions/i.test(output)
|
|
123
|
+
) {
|
|
124
|
+
return {
|
|
125
|
+
limited: true,
|
|
126
|
+
message: "Codex unavailable due to session permissions",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
71
130
|
// Codex/OpenAI rate limit patterns
|
|
72
131
|
const patterns = [
|
|
73
132
|
/rate limit exceeded/i,
|
package/src/agents/droid.ts
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Droid Agent Adapter
|
|
3
3
|
*
|
|
4
|
-
* Adapter for Factory's Droid CLI
|
|
4
|
+
* Adapter for Factory's Droid CLI.
|
|
5
5
|
* https://factory.ai
|
|
6
|
+
*
|
|
7
|
+
* ## Model Selection
|
|
8
|
+
*
|
|
9
|
+
* Droid supports model selection via the `-m` flag (short form).
|
|
10
|
+
* Pass the model name in the `options.model` parameter.
|
|
11
|
+
*
|
|
12
|
+
* **Supported models:**
|
|
13
|
+
* - `claude-opus-4-5-20251101` - Claude Opus 4.5
|
|
14
|
+
* - `claude-sonnet-4-5-20250929` - Claude Sonnet 4.5
|
|
15
|
+
* - `claude-haiku-4-5-20251001` - Claude Haiku 4.5
|
|
16
|
+
* - `gpt-5.2` - GPT-5.2
|
|
17
|
+
* - `gpt-5.1` - GPT-5.1
|
|
18
|
+
* - `gpt-5.1-codex` - GPT-5.1 Codex
|
|
19
|
+
* - `gpt-5.1-codex-max` - GPT-5.1 Codex Max
|
|
20
|
+
* - `gemini-3-pro-preview` - Gemini 3 Pro Preview
|
|
21
|
+
*
|
|
22
|
+
* **CLI command format:**
|
|
23
|
+
* ```
|
|
24
|
+
* droid exec -m <model> --reasoning-effort <level> --auto high
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const result = await droidAdapter.invoke("Fix the bug", {
|
|
30
|
+
* model: "gpt-5.2",
|
|
31
|
+
* workingDirectory: "/path/to/project"
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
6
34
|
*/
|
|
7
35
|
|
|
8
36
|
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
|
|
37
|
+
import { getModelById } from "../routing/registry";
|
|
38
|
+
import { runCommand } from "./exec";
|
|
9
39
|
|
|
10
40
|
export const droidAdapter: AgentAdapter = {
|
|
11
41
|
name: "droid",
|
|
@@ -37,28 +67,38 @@ export const droidAdapter: AgentAdapter = {
|
|
|
37
67
|
},
|
|
38
68
|
|
|
39
69
|
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
40
|
-
|
|
70
|
+
// Build command args: droid exec [-m <model>] --auto high
|
|
71
|
+
const args = ["exec"];
|
|
72
|
+
|
|
73
|
+
if (options?.model) {
|
|
74
|
+
const modelProfile = getModelById(options.model);
|
|
75
|
+
const modelValue =
|
|
76
|
+
modelProfile?.harness === "droid" ? modelProfile.cliValue : options.model;
|
|
77
|
+
args.push("-m", modelValue);
|
|
78
|
+
if (modelProfile?.harness === "droid" && modelProfile.cliArgs) {
|
|
79
|
+
args.push(...modelProfile.cliArgs);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
41
82
|
|
|
42
|
-
// Droid reads from stdin when piped or redirected.
|
|
43
83
|
// Use --auto high for high risk tolerance by default
|
|
44
|
-
|
|
84
|
+
args.push("--auto", "high");
|
|
85
|
+
|
|
86
|
+
const result = await runCommand(["droid", ...args], {
|
|
45
87
|
cwd: options?.workingDirectory,
|
|
46
88
|
stdin: new Blob([prompt]),
|
|
47
|
-
|
|
48
|
-
stderr: "pipe",
|
|
89
|
+
timeoutMs: options?.timeout,
|
|
49
90
|
});
|
|
50
91
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const duration = Date.now() - startTime;
|
|
92
|
+
const timeoutNote =
|
|
93
|
+
result.timedOut && options?.timeout
|
|
94
|
+
? `\n[Relentless] Idle timeout after ${options.timeout}ms.`
|
|
95
|
+
: "";
|
|
96
|
+
const output = result.stdout + (result.stderr ? `\n${result.stderr}` : "") + timeoutNote;
|
|
97
|
+
const duration = result.duration;
|
|
58
98
|
|
|
59
99
|
return {
|
|
60
100
|
output,
|
|
61
|
-
exitCode,
|
|
101
|
+
exitCode: result.exitCode,
|
|
62
102
|
isComplete: this.detectCompletion(output),
|
|
63
103
|
duration,
|
|
64
104
|
};
|
|
@@ -69,6 +109,20 @@ export const droidAdapter: AgentAdapter = {
|
|
|
69
109
|
},
|
|
70
110
|
|
|
71
111
|
detectRateLimit(output: string): RateLimitInfo {
|
|
112
|
+
if (output.includes("[Relentless] Idle timeout")) {
|
|
113
|
+
return {
|
|
114
|
+
limited: true,
|
|
115
|
+
message: "Droid idle timeout",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (/mcp start failed/i.test(output) || /error reloading mcp servers/i.test(output)) {
|
|
120
|
+
return {
|
|
121
|
+
limited: true,
|
|
122
|
+
message: "Droid unavailable due to MCP initialization failure",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
72
126
|
// Droid rate limit patterns
|
|
73
127
|
const patterns = [
|
|
74
128
|
/rate limit/i,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared command runner with idle timeout support for agent adapters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface RunCommandOptions {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
stdin?: Blob;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RunCommandResult {
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
exitCode: number;
|
|
15
|
+
duration: number;
|
|
16
|
+
timedOut: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function readStream(
|
|
20
|
+
stream: ReadableStream<Uint8Array>,
|
|
21
|
+
onChunk: () => void
|
|
22
|
+
): Promise<string> {
|
|
23
|
+
const reader = stream.getReader();
|
|
24
|
+
const decoder = new TextDecoder();
|
|
25
|
+
let output = "";
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
while (true) {
|
|
29
|
+
const { done, value } = await reader.read();
|
|
30
|
+
if (done) break;
|
|
31
|
+
if (value) {
|
|
32
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
33
|
+
if (chunk) {
|
|
34
|
+
output += chunk;
|
|
35
|
+
onChunk();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
reader.releaseLock();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runCommand(
|
|
47
|
+
command: string[],
|
|
48
|
+
options: RunCommandOptions = {}
|
|
49
|
+
): Promise<RunCommandResult> {
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
const proc = Bun.spawn(command, {
|
|
52
|
+
cwd: options.cwd,
|
|
53
|
+
stdin: options.stdin,
|
|
54
|
+
stdout: "pipe",
|
|
55
|
+
stderr: "pipe",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let lastOutput = Date.now();
|
|
59
|
+
let timedOut = false;
|
|
60
|
+
let idleTimer: ReturnType<typeof setInterval> | undefined;
|
|
61
|
+
|
|
62
|
+
const onChunk = () => {
|
|
63
|
+
lastOutput = Date.now();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
67
|
+
idleTimer = setInterval(() => {
|
|
68
|
+
if (Date.now() - lastOutput > options.timeoutMs!) {
|
|
69
|
+
timedOut = true;
|
|
70
|
+
try {
|
|
71
|
+
proc.kill();
|
|
72
|
+
} catch {
|
|
73
|
+
// Best-effort kill on timeout.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, 500);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const [stdout, stderr] = await Promise.all([
|
|
80
|
+
readStream(proc.stdout, onChunk),
|
|
81
|
+
readStream(proc.stderr, onChunk),
|
|
82
|
+
]);
|
|
83
|
+
const exitCode = await proc.exited;
|
|
84
|
+
|
|
85
|
+
if (idleTimer) {
|
|
86
|
+
clearInterval(idleTimer);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
stdout,
|
|
91
|
+
stderr,
|
|
92
|
+
exitCode,
|
|
93
|
+
duration: Date.now() - startTime,
|
|
94
|
+
timedOut,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/agents/gemini.ts
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gemini Agent Adapter
|
|
3
3
|
*
|
|
4
|
-
* Adapter for Google's Gemini CLI
|
|
4
|
+
* Adapter for Google's Gemini CLI with model selection support.
|
|
5
5
|
* https://github.com/google-gemini/gemini-cli
|
|
6
|
+
*
|
|
7
|
+
* ## Supported Models
|
|
8
|
+
* - `gemini-3-pro` - Gemini 3 Pro (best for complex reasoning and coding tasks)
|
|
9
|
+
* - `gemini-3-flash` - Gemini 3 Flash (faster, more cost-effective for simpler tasks)
|
|
10
|
+
*
|
|
11
|
+
* ## CLI Command Format
|
|
12
|
+
* With model: `gemini --model <model> --prompt "<prompt>"`
|
|
13
|
+
* Without model: `gemini --prompt "<prompt>"`
|
|
14
|
+
* With dangerous mode: `gemini --yolo --model <model> --prompt "<prompt>"`
|
|
15
|
+
*
|
|
16
|
+
* ## Usage Example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const result = await geminiAdapter.invoke("Fix the bug", {
|
|
19
|
+
* model: "gemini-3-pro",
|
|
20
|
+
* dangerouslyAllowAll: true, // Sets --yolo flag
|
|
21
|
+
* workingDirectory: "/path/to/project"
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @module agents/gemini
|
|
6
26
|
*/
|
|
7
27
|
|
|
8
28
|
import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
|
|
29
|
+
import { getModelById } from "../routing/registry";
|
|
30
|
+
import { runCommand } from "./exec";
|
|
9
31
|
|
|
10
32
|
export const geminiAdapter: AgentAdapter = {
|
|
11
33
|
name: "gemini",
|
|
@@ -37,7 +59,6 @@ export const geminiAdapter: AgentAdapter = {
|
|
|
37
59
|
},
|
|
38
60
|
|
|
39
61
|
async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
|
|
40
|
-
const startTime = Date.now();
|
|
41
62
|
const args: string[] = [];
|
|
42
63
|
|
|
43
64
|
if (options?.dangerouslyAllowAll) {
|
|
@@ -45,29 +66,35 @@ export const geminiAdapter: AgentAdapter = {
|
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
if (options?.model) {
|
|
48
|
-
|
|
69
|
+
const modelProfile = getModelById(options.model);
|
|
70
|
+
if (modelProfile?.harness === "gemini") {
|
|
71
|
+
args.push("--model", modelProfile.cliValue);
|
|
72
|
+
if (modelProfile.cliArgs) {
|
|
73
|
+
args.push(...modelProfile.cliArgs);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
args.push("--model", options.model);
|
|
77
|
+
}
|
|
49
78
|
}
|
|
50
79
|
|
|
51
|
-
// Gemini CLI
|
|
52
|
-
args.push(prompt);
|
|
80
|
+
// Gemini CLI uses -p/--prompt for non-interactive prompts
|
|
81
|
+
args.push("--prompt", prompt);
|
|
53
82
|
|
|
54
|
-
const
|
|
83
|
+
const result = await runCommand(["gemini", ...args], {
|
|
55
84
|
cwd: options?.workingDirectory,
|
|
56
|
-
|
|
57
|
-
stderr: "pipe",
|
|
85
|
+
timeoutMs: options?.timeout,
|
|
58
86
|
});
|
|
59
87
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const duration = Date.now() - startTime;
|
|
88
|
+
const timeoutNote =
|
|
89
|
+
result.timedOut && options?.timeout
|
|
90
|
+
? `\n[Relentless] Idle timeout after ${options.timeout}ms.`
|
|
91
|
+
: "";
|
|
92
|
+
const output = result.stdout + (result.stderr ? `\n${result.stderr}` : "") + timeoutNote;
|
|
93
|
+
const duration = result.duration;
|
|
67
94
|
|
|
68
95
|
return {
|
|
69
96
|
output,
|
|
70
|
-
exitCode,
|
|
97
|
+
exitCode: result.exitCode,
|
|
71
98
|
isComplete: this.detectCompletion(output),
|
|
72
99
|
duration,
|
|
73
100
|
};
|
|
@@ -78,6 +105,13 @@ export const geminiAdapter: AgentAdapter = {
|
|
|
78
105
|
},
|
|
79
106
|
|
|
80
107
|
detectRateLimit(output: string): RateLimitInfo {
|
|
108
|
+
if (output.includes("[Relentless] Idle timeout")) {
|
|
109
|
+
return {
|
|
110
|
+
limited: true,
|
|
111
|
+
message: "Gemini idle timeout",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
81
115
|
// Gemini rate limit patterns
|
|
82
116
|
const patterns = [
|
|
83
117
|
/quota exceeded/i,
|
|
@@ -85,6 +119,15 @@ export const geminiAdapter: AgentAdapter = {
|
|
|
85
119
|
/rate limit/i,
|
|
86
120
|
/\b429\b/,
|
|
87
121
|
/too many requests/i,
|
|
122
|
+
/operation not permitted/i,
|
|
123
|
+
/\beperm\b/i,
|
|
124
|
+
/error initializing chat recording service/i,
|
|
125
|
+
/err_module_not_found/i,
|
|
126
|
+
/cannot find package/i,
|
|
127
|
+
/data collection is disabled/i,
|
|
128
|
+
/gemini_api_key/i,
|
|
129
|
+
/google_genai_use_vertexai/i,
|
|
130
|
+
/google_genai_use_gca/i,
|
|
88
131
|
];
|
|
89
132
|
|
|
90
133
|
for (const pattern of patterns) {
|