From 7bab3dc9bf174dcb8b13c9f368596135929ed537 Mon Sep 17 00:00:00 2001 From: bowlerjim Date: Sun, 20 Sep 2026 23:30:06 -0400 Subject: [PATCH] fix(Evals): run static_analysis commands through a shell and gate on exit code (#2184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `await $`cd ${workingDir} && ${command}`` escapes the interpolation into a single argv token, so a command like `bun --version` was executed as one program of that literal name. The shell answered `command not found`, which contains no `error`/`failed` keyword, so countIssues() returned 0 and `passed = errors === 0` was true. Every static_analysis grader scored 1.0 regardless of what it was asked to run — a lint or typecheck suite that could not go red. Spawn `/bin/sh -c ` (`cmd /c` on win32) and fail on a non-zero exit as well as on counted errors, so a tool that reports only through its exit status is graded correctly. Verified: commands `bun --version` -> 1/1 passed, `echo hello && exit 1` -> 0/1 passed, `echo all good` -> 1/1 passed. Before the change all three scored 1.0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PuS77R8a6Lhp7jK6dnwdTh --- .../Evals/Graders/CodeBased/StaticAnalysis.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/LifeOS/install/skills/Evals/Graders/CodeBased/StaticAnalysis.ts b/LifeOS/install/skills/Evals/Graders/CodeBased/StaticAnalysis.ts index 2ef1f59858..6fec77a99f 100755 --- a/LifeOS/install/skills/Evals/Graders/CodeBased/StaticAnalysis.ts +++ b/LifeOS/install/skills/Evals/Graders/CodeBased/StaticAnalysis.ts @@ -5,7 +5,9 @@ import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts'; import type { GraderConfig, GraderResult, StaticAnalysisParams } from '../../Types/index.ts'; -import { $ } from 'bun'; + +// Windows has no /bin/sh; cmd takes /c where POSIX shells take -c. +const SHELL_PREFIX = process.platform === 'win32' ? ['cmd', '/c'] : ['/bin/sh', '-c']; export class StaticAnalysisGrader extends BaseGrader { type = 'static_analysis' as const; @@ -26,14 +28,28 @@ export class StaticAnalysisGrader extends BaseGrader { for (const command of params.commands) { try { - const result = await $`cd ${workingDir} && ${command}`.quiet().nothrow(); + // Run through a shell: the command is a user-authored string ("bun test", + // "eslint . --max-warnings 0"), and a tagged-template interpolation would + // escape the whole string into a single argv token, so nothing would run. + const proc = Bun.spawn([...SHELL_PREFIX, command], { + cwd: workingDir, + stdout: 'pipe', + stderr: 'pipe', + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); - const output = result.stdout.toString() + result.stderr.toString(); + const output = stdout + stderr; const warnings = this.countIssues(output, 'warning'); const errors = this.countIssues(output, 'error'); - // Pass if no errors (and no warnings if fail_on_warning is set) - const passed = errors === 0 && (!params.fail_on_warning || warnings === 0); + // A non-zero exit is a failure on its own: a tool that reports its verdict + // only through its exit status (tsc, most test runners) prints nothing the + // keyword scan below would ever count. + const passed = exitCode === 0 && errors === 0 && (!params.fail_on_warning || warnings === 0); results.push({ command,