Repository navigation
feat: add multi-client agent evaluation for Claude Code and Gemini CLI (issue #1932) #8930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,8 @@ import { retainOwnedCommandCleanup } from "../../dist/providers/OwnedCommandClea | |
| /** Keep command ownership through failure and join cleanup before removing a run. */ | ||
| export function createAgentEvaluationCommands({ | ||
| codex, | ||
| claude = process.env.REA_AGENT_EVAL_CLAUDE_COMMAND ?? "claude", | ||
| gemini = process.env.REA_AGENT_EVAL_GEMINI_COMMAND ?? "gemini", | ||
| evaluationRoot, | ||
| evaluationEnvironment, | ||
| optionalModel, | ||
|
|
@@ -32,6 +34,10 @@ export function createAgentEvaluationCommands({ | |
| run: (work) => resources.run(work), | ||
| }; | ||
| const diagnosticBytes = 32_768; | ||
| const launchGemini = (arguments_) => | ||
| /\.(?:c|m)?js$/iu.test(gemini) | ||
| ? [process.execPath, [gemini, ...arguments_]] | ||
| : [gemini, arguments_]; | ||
|
Comment on lines
+37
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A relative JavaScript path in Resolve the selected entry point once before changing the working directory, as Prompt To Fix With AIThis is a comment left during a code review.
Path: scripts/lib/agent-evaluation-commands.mjs
Line: 37-40
Comment:
**Relative Gemini paths break**
A relative JavaScript path in `REA_AGENT_EVAL_GEMINI_COMMAND` resolves from two different folders. `readAgentVersion` runs from `repositoryRoot`, but `runGemini` runs from `evaluationRoot`. A path such as `node_modules/@google/gemini-cli/bundle/gemini.js` can pass the version check and then fail with a missing-module error for every scenario.
Resolve the selected entry point once before changing the working directory, as `verify-gemini-client.mjs` already does.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
|
|
||
| async function execute( | ||
| command, | ||
|
|
@@ -73,6 +79,64 @@ export function createAgentEvaluationCommands({ | |
| }); | ||
| } | ||
|
|
||
| async function executeJsonLines( | ||
| command, | ||
| arguments_, | ||
| cwd, | ||
| env, | ||
| timeoutMessage, | ||
| ) { | ||
| const events = []; | ||
| const nonJson = []; | ||
| let nonJsonBytes = 0; | ||
| const stdout = new PassThrough(); | ||
| const lines = createInterface({ input: stdout }); | ||
| const linesClosed = new Promise((resolve) => | ||
| lines.once("close", resolve), | ||
| ); | ||
| let lineError; | ||
| lines.on("error", (cause) => { | ||
| lineError = cause; | ||
| lines.close(); | ||
| }); | ||
| lines.on("line", (line) => { | ||
| try { | ||
| events.push(JSON.parse(line)); | ||
| } catch { | ||
| if (nonJsonBytes >= diagnosticBytes) return; | ||
| const bytes = Buffer.from( | ||
| `non-json stdout: ${line.slice(0, diagnosticBytes - nonJsonBytes)}`, | ||
| ); | ||
| const retained = bytes.subarray(0, diagnosticBytes - nonJsonBytes); | ||
| if (retained.length > 0) nonJson.push(retained); | ||
| nonJsonBytes += retained.length; | ||
| } | ||
| }); | ||
| try { | ||
| const result = await execute( | ||
| command, | ||
| arguments_, | ||
| cwd, | ||
| env, | ||
| { timeoutMs, stdout }, | ||
| timeoutMessage, | ||
| ); | ||
| await linesClosed; | ||
| if (lineError !== undefined) throw lineError; | ||
| return { | ||
| events, | ||
| exitCode: result.exitCode ?? 1, | ||
| stderr: Buffer.concat([...nonJson, Buffer.from(result.stderr.text)]) | ||
| .subarray(0, diagnosticBytes) | ||
| .toString("utf8"), | ||
| }; | ||
| } finally { | ||
| stdout.destroy(); | ||
| lines.close(); | ||
| await linesClosed; | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| async runCodex(prompt, approvedTools) { | ||
| const arguments_ = [ | ||
|
|
@@ -96,57 +160,61 @@ export function createAgentEvaluationCommands({ | |
| ...(optionalModel === undefined ? [] : ["--model", optionalModel]), | ||
| prompt, | ||
| ]; | ||
| const events = []; | ||
| const nonJson = []; | ||
| let nonJsonBytes = 0; | ||
| const stdout = new PassThrough(); | ||
| const lines = createInterface({ input: stdout }); | ||
| const linesClosed = new Promise((resolve) => | ||
| lines.once("close", resolve), | ||
| return executeJsonLines( | ||
| codex, | ||
| arguments_, | ||
| evaluationRoot, | ||
| evaluationEnvironment, | ||
| `Codex evaluation timed out after ${String(timeoutMs)}ms`, | ||
| ); | ||
| }, | ||
|
|
||
| async runClaude(prompt) { | ||
| return executeJsonLines( | ||
| claude, | ||
| [ | ||
| "--print", | ||
| "--output-format", | ||
| "stream-json", | ||
| "--verbose", | ||
| "--no-session-persistence", | ||
| "--permission-mode", | ||
| "dontAsk", | ||
| "--settings", | ||
| JSON.stringify({ | ||
| permissions: { | ||
| allow: ["Skill", "WaitForMcpServers", "mcp__rea__*"], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Use one shared safe-tool policy with explicit scenario exceptions, and restrict Gemini's shell and write tools.
Prompt To Fix With AIThis is a comment left during a code review.
Path: scripts/lib/agent-evaluation-commands.mjs
Line: 186
Comment:
**Evaluations can change host files**
`runClaude` and `runGemini` ignore `scenario.approvedTools` and approve every REA tool. This includes `capture_process_scenario`, which runs a chosen command with the user's permissions. If an agent tries to execute a fixture or repair a failed analysis, it can change files outside `evaluationRoot` without asking. Changing `HOME` does not prevent this.
Use one shared safe-tool policy with explicit scenario exceptions, and restrict Gemini's shell and write tools.
> **How this was verified:** Both launchers approve the full REA catalog, and `capture_process_scenario` forwards the chosen command to the host without restricting it to the evaluation folder.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| }, | ||
| }), | ||
| ...(optionalModel === undefined ? [] : ["--model", optionalModel]), | ||
| "--", | ||
| prompt, | ||
| ], | ||
| evaluationRoot, | ||
| evaluationEnvironment, | ||
| `Claude Code evaluation timed out after ${String(timeoutMs)}ms`, | ||
| ); | ||
| }, | ||
|
|
||
| async runGemini(prompt) { | ||
| const [command, arguments_] = launchGemini([ | ||
| "--approval-mode", | ||
| "yolo", | ||
| "--allowed-mcp-server-names", | ||
| "rea", | ||
| "--output-format", | ||
| "stream-json", | ||
| ...(optionalModel === undefined ? [] : ["--model", optionalModel]), | ||
| "-p", | ||
| prompt, | ||
| ]); | ||
| return executeJsonLines( | ||
| command, | ||
| arguments_, | ||
| evaluationRoot, | ||
| evaluationEnvironment, | ||
| `Gemini CLI evaluation timed out after ${String(timeoutMs)}ms`, | ||
| ); | ||
| let lineError; | ||
| lines.on("error", (cause) => { | ||
| // The owned pipeline reports stream failure or cancellation. Join this | ||
| // consumer too, without turning its forwarded error into a process crash. | ||
| lineError = cause; | ||
| lines.close(); | ||
| }); | ||
| lines.on("line", (line) => { | ||
| try { | ||
| events.push(JSON.parse(line)); | ||
| } catch { | ||
| if (nonJsonBytes >= diagnosticBytes) return; | ||
| const bytes = Buffer.from( | ||
| `non-json stdout: ${line.slice(0, diagnosticBytes - nonJsonBytes)}`, | ||
| ); | ||
| const retained = bytes.subarray(0, diagnosticBytes - nonJsonBytes); | ||
| if (retained.length > 0) nonJson.push(retained); | ||
| nonJsonBytes += retained.length; | ||
| } | ||
| }); | ||
| try { | ||
| const result = await execute( | ||
| codex, | ||
| arguments_, | ||
| evaluationRoot, | ||
| evaluationEnvironment, | ||
| { timeoutMs, stdout }, | ||
| `Codex evaluation timed out after ${String(timeoutMs)}ms`, | ||
| ); | ||
| await linesClosed; | ||
| if (lineError !== undefined) throw lineError; | ||
| return { | ||
| events, | ||
| exitCode: result.exitCode ?? 1, | ||
| stderr: Buffer.concat([...nonJson, Buffer.from(result.stderr.text)]) | ||
| .subarray(0, diagnosticBytes) | ||
| .toString("utf8"), | ||
| }; | ||
| } finally { | ||
| stdout.destroy(); | ||
| lines.close(); | ||
| await linesClosed; | ||
| } | ||
| }, | ||
|
|
||
| async runProcess(command, arguments_, cwd, timeout, env = process.env) { | ||
|
|
@@ -167,12 +235,12 @@ export function createAgentEvaluationCommands({ | |
| ); | ||
| }, | ||
|
|
||
| async readCommandVersion(command) { | ||
| async readCommandVersion(command, env = process.env) { | ||
| const result = await execute( | ||
| command, | ||
| ["--version"], | ||
| repositoryRoot, | ||
| process.env, | ||
| env, | ||
| { timeoutMs: 10_000, diagnosticBytes: 8192 }, | ||
| `${command} --version timed out`, | ||
| ); | ||
|
|
@@ -186,6 +254,29 @@ export function createAgentEvaluationCommands({ | |
| return version; | ||
| }, | ||
|
|
||
| async readAgentVersion(client, env = evaluationEnvironment) { | ||
| const [command, arguments_] = | ||
| client === "gemini-cli" | ||
| ? launchGemini(["--version"]) | ||
| : [client === "claude-code" ? claude : codex, ["--version"]]; | ||
| const result = await execute( | ||
| command, | ||
| arguments_, | ||
| repositoryRoot, | ||
| env, | ||
| { timeoutMs: 10_000, diagnosticBytes: 8192 }, | ||
| `${client} --version timed out`, | ||
|
Comment on lines
+257
to
+268
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Context Used: AGENTS.md (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: scripts/lib/agent-evaluation-commands.mjs
Line: 257-268
Comment:
**Version checks have two owners**
`readAgentVersion` duplicates the execution, timeout, exit-code, and empty-output checks in `readCommandVersion`. The runner now uses the new function, while the existing boundary tests still exercise only the old one. Fixes can drift between the two copies without those tests catching it.
`AGENTS.md` requires removing superseded representations after migrating their consumers. Before merging, keep one version-reading implementation and move the existing checks onto the path the runner actually uses.
**Context Used:** AGENTS.md ([source](https://github.com/morluto/rea/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| ); | ||
| if (result.exitCode !== 0) | ||
| throw new Error( | ||
| `${client} --version failed with ${String(result.exitCode)}: ${result.stderr.text}`, | ||
| ); | ||
| const version = result.stdout.text.trim(); | ||
| if (version.length === 0) | ||
| throw new Error(`${client} --version returned empty output`); | ||
| return version; | ||
| }, | ||
|
|
||
| close: () => resources.close(), | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This removes every language link from the main README, although the translated READMEs still exist. Readers arriving at the repository can no longer choose their language from its landing page. Restore the language switcher; this removal is unrelated to adding evaluation clients.
Prompt To Fix With AI