Skip to content

chore(eval): eval through the Claude Code CLI, scored by two judges - #105

Merged
0xMMA merged 4 commits into
mainfrom
chore/eval-via-claude-code
Oct 11, 2026
Merged

0xMMA merged 4 commits into
mainfrom
chore/eval-via-claude-code

Conversation

@0xMMA

@0xMMA 0xMMA commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Depends on #127 (fix(llm): isolate Claude Code CLI calls from the user's sessions). This branch is rebased on top of it, so its first commit edad910 belongs to #127. Merge #127 first; after that this PR shows only its own commits.

What it does

Both eval suites (Pyramidize and Fix) run through the installed Claude Code CLI by default, and every sample is scored by two judges, claude-haiku-5-5 and claude-sonnet-5-5, each reported on its own. This is E3 phase 1, step "Instrument" (decisions of 2026-10-11).

CLI eval (the original #105, rebased):

  • Pipeline and judges run as claude-code, so no API key is read.
  • --provider claude and EVAL_JUDGE_PROVIDER=claude switch to the API. .env keys load only when an API provider is in the run.
  • Resolved IDs: recorded with main's ModelRecorder (model holds the ID, plus modelRequested). Each judge records its own resolvedModel.
  • CLI version: claudeCodeVersion is read at the start and end of a run and reported next to a verdict. It is not keyed (decision 2026-10-11), because the CLI updates itself.

Two judges:

  • Records: summary.json has judges[], avgJudges and judgeCounts.
  • Aggregate: eval-aggregate.sh and baseline.json hold one interval per judge, at run level and per sample, plus per-judge coverage. Nothing averages across judges.
    • Judge columns are keyed by the ID that answered, as the key is. Two spellings of the same judge (haiku and claude-haiku-5-5) land in one column.
  • configKey: the judge provider, resolved model and temperature fields hold one +-joined entry per judge, ordered by ID.
    • A single-judge key is unchanged, so every recorded baseline reads "not comparable" (exit 2) against a two-judge run. A test runs every baseline in test-data/eval-baselines/ through this.
  • --compare: applies the interval rule to each metric (deterministic, then each judge).
    • Any metric clearly worse is a regression. An improvement needs at least one metric clearly better and none worse.
    • regressedMetrics / improvedMetrics name what flagged the verdict, so a regression carried by one judge alone reads as that. The fields are filled only for a comparison that was actually made.
    • A judge with no interval on one side gives "not comparable", never "overlap".
  • Overrides: EVAL_JUDGES=a,b,c chooses the set. Bridge run: EVAL_JUDGES=claude-haiku-5-5,claude-sonnet-5-5,claude-sonnet-4-5-20250929. EVAL_JUDGE_MODEL is a set of one. Both variables at once, an empty entry or a duplicate are refused.
  • Temperature (llm.SupportsTemperature(provider, model)):
    • CLI: no judge is pinned (null plus a note).
    • API: Claude models from the 4.7 generation on take no temperature, so the 5.5 judges record null with a note instead of sending 0 and getting a 400. The Sonnet 4.5 bridge judge stays at 0.
  • Judge IDs: Haiku 5.5 and Sonnet 5.5 have no dated snapshot, so the full ID is the pin. In the smoke run (CLI 2.1.296), modelUsage reported exactly those IDs.
  • eval.sh:
    • Shows the judge set.
    • go test -timeout is 10800 s.
    • --dry-run asks the CLI nothing; --preflight adds the sign-in check.
    • Its env -u list is tested against blockedCLIEnv.

Review follow-ups (Opus review)

  • Columns keyed by the requested name produced a silent wrong verdict (0.95 → 0.40 read "inconclusive", exit 0). Fixed and covered by a test.
  • The API judge route sent temperature 0 to models that reject it. Now model-aware.
  • API-era baselines: the docs no longer promise a route back to them. They are not comparable with this instrument on any route.
  • 2026-10-04 CLI baselines: marked in both quality-status files as possibly affected by the auto-memory leak (fixed in fix(llm): isolate Claude Code CLI calls from the user's sessions #127) and superseded. The readings built on them ("the CLI route corrects less", the judge-spread explanation) are flagged as unconfirmed. Nothing was re-recorded.

Conflicts resolved against main

Verification

  • go vet -tags gtk3 ./... and go vet -tags eval,gtk3 ./internal/...: clean.
  • go test -tags gtk3 ./internal/...: pass. Every new behaviour test was seen failing first: the resolved-ID columns, missing interval, metric naming, CLI version not keyed, model-aware temperature, env-list sync and dry-run.
  • go build -tags gtk3 -o bin/KeyLint .: ok.
  • cd frontend && npm test: 37 files, 496 tests pass.
  • wire gen / bindings: not needed.

Smoke run (made before the review fixes; nothing re-run since)

One sample per suite, filtered with go test -run. 6 model calls, plus 5 non-model CLI invocations (--version ×4, auth status ×1).

Suite Sample Pipeline → resolved Haiku 5.5 Sonnet 5.5
Fix en-grammar-basics haiku → claude-haiku-5-5 0.97 0.96
Pyramidize email-meeting-request sonnet → claude-sonnet-5-5 0.92 0.82

These are single samples, not measurements. The Fix smoke sample came from the holdout split; future smoke runs should use a tune sample, e.g. EVAL_SPLIT=tune go test -tags eval ./internal/features/enhance/ -run 'TestEvalFix/^interpunktion$' -v.

Not done

  • No two-judge baseline and no bridge run.
  • Per-form deterministic checks.
  • The API judge route is still uncalled (no key).
  • Never merged.

0xMMA added a commit that referenced this pull request Oct 11, 2026
PR #105 rebased onto main as one commit (the original eleven are on
backup/eval-via-claude-code-pre-rebase). Pipeline and judge run through
the installed Claude Code CLI by default; the judge records temperature
null plus a note; claudeCodeVersion is recorded; .env keys load only
when an API provider is in the run. Claude Code CLI calls run in an
empty KeyLint-owned directory with auto-memory off, and inherited
thinking, output-cap and alias-remapping variables are stripped.

Resolved against main:
- Resolved model IDs: main's Response.Model, answeringModel and
  llm.ModelRecorder replace the PR's ResolvedModels; summary.json keeps
  main's shape (model = resolved ID, modelRequested). The judge records
  its own resolvedModel through a second recorder. UsesAPIKey and
  SupportsTemperature move to internal/llm/traits.go.
- Thinking: main's Request.NoThinking replaces the PR's DisableThinking.
  The CLI keeps ignoring it, as on main, so EVAL_THINKING / --thinking
  are gone; a Fix run records what the service does (fixThinkingLabel).
- Judge: main's noEffort and the PR's nil temperature and deadline.
- Effort, model families, typed errors and the rest of main unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
@0xMMA
0xMMA force-pushed the chore/eval-via-claude-code branch from dfbea5d to 78ad7bb Compare October 11, 2026 17:44
@0xMMA 0xMMA changed the title chore(eval): run both eval suites through the Claude Code CLI chore(eval): eval through the Claude Code CLI, scored by two judges Oct 11, 2026
0xMMA added a commit that referenced this pull request Oct 11, 2026
PR #105 rebased onto main as one commit (the original eleven are on
backup/eval-via-claude-code-pre-rebase). Pipeline and judge run through
the installed Claude Code CLI by default; the judge records temperature
null plus a note; claudeCodeVersion is recorded; .env keys load only
when an API provider is in the run. Claude Code CLI calls run in an
empty KeyLint-owned directory with auto-memory off, and inherited
thinking, output-cap and alias-remapping variables are stripped.

Resolved against main:
- Resolved model IDs: main's Response.Model, answeringModel and
  llm.ModelRecorder replace the PR's ResolvedModels; summary.json keeps
  main's shape (model = resolved ID, modelRequested). The judge records
  its own resolvedModel through a second recorder. UsesAPIKey and
  SupportsTemperature move to internal/llm/traits.go.
- Thinking: main's Request.NoThinking replaces the PR's DisableThinking.
  The CLI keeps ignoring it, as on main, so EVAL_THINKING / --thinking
  are gone; a Fix run records what the service does (fixThinkingLabel).
- Judge: main's noEffort and the PR's nil temperature and deadline.
- Effort, model families, typed errors and the rest of main unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
@0xMMA
0xMMA force-pushed the chore/eval-via-claude-code branch from ee2132f to fecbfaf Compare October 11, 2026 18:21
0xMMA added a commit that referenced this pull request Oct 11, 2026
--setting-sources "" does not stop Claude Code's auto-memory: with
KeyLint's working directory inherited, the memory file of the matching
~/.claude/projects/<slug> was injected into every Fix and Pyramidize
call (measured 2026-10-04, while building the CLI eval in #105).

- Every spawn, the status probes included, runs in an empty
  KeyLint-owned directory under the user cache dir. Without a cache dir
  it uses a private os.MkdirTemp directory for the process (random
  name, 0700) instead of a shared fixed path; if that fails too the call
  fails with a clear error rather than inheriting the cwd.
- CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 is set; an inherited value cannot
  turn it back on.
- Inherited alias-remapping (ANTHROPIC_DEFAULT_*_MODEL,
  ANTHROPIC_SMALL_FAST_MODEL), thinking-budget and output-cap variables
  are stripped like the credential ones.
- Tests point the cache dir at a temp dir on every OS.

Co-authored-by: Michael <0xMMA@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
0xMMA and others added 4 commits October 11, 2026 20:57
PR #105 rebased onto main as one commit (the original eleven are on
backup/eval-via-claude-code-pre-rebase). Pipeline and judge run through
the installed Claude Code CLI by default; the judge records temperature
null plus a note; claudeCodeVersion is recorded; .env keys load only
when an API provider is in the run. Claude Code CLI calls run in an
empty KeyLint-owned directory with auto-memory off, and inherited
thinking, output-cap and alias-remapping variables are stripped.

Resolved against main:
- Resolved model IDs: main's Response.Model, answeringModel and
  llm.ModelRecorder replace the PR's ResolvedModels; summary.json keeps
  main's shape (model = resolved ID, modelRequested). The judge records
  its own resolvedModel through a second recorder. UsesAPIKey and
  SupportsTemperature move to internal/llm/traits.go.
- Thinking: main's Request.NoThinking replaces the PR's DisableThinking.
  The CLI keeps ignoring it, as on main, so EVAL_THINKING / --thinking
  are gone; a Fix run records what the service does (fixThinkingLabel).
- Judge: main's noEffort and the PR's nil temperature and deadline.
- Effort, model families, typed errors and the rest of main unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Haiku 5.5 and Sonnet 5.5 judge each sample in both suites, both through
the Claude Code CLI by default (decision 2026-10-11, E3 phase 1). Full
model IDs: neither has a dated snapshot, and each judge records the ID
that answered.

- summary.json: judges[] (with resolvedModel), avgJudges, judgeCounts;
  results.jsonl: judges / judgeErrors keyed by judge model.
- eval-aggregate.sh: one interval per judge (run and sample level),
  per-judge coverage; nothing averages across judges. Runs from before
  the set (judge / avgJudge) read as a set of one.
- configKey: the judge provider, resolved model and temperature fields
  hold one "+"-joined entry per judge, ordered by ID. A single-judge key
  is unchanged, so every recorded baseline reads "not comparable"
  against a two-judge run instead of crashing.
- --compare: interval rule per metric (deterministic, each judge); any
  metric clearly worse is a regression, an improvement needs one clearly
  better and none worse.
- EVAL_JUDGES=a,b,c chooses the set (bridge run with the old Sonnet 4.5
  judge); EVAL_JUDGE_MODEL is a set of one; both, a hole or a duplicate
  are refused. eval.sh shows the set; go test timeout 10800 s.

Co-Authored-By: Claude <noreply@anthropic.com>
Instrument note only, no new numbers: the single-judge baselines read
"not comparable" against a two-judge run until new ones are recorded.

Co-Authored-By: Claude <noreply@anthropic.com>
Review follow-ups on the two-judge instrument:

- Per-judge columns are keyed by the ID that answered, as the configKey
  is. Keyed by the requested name, a baseline judged as "haiku" and a run
  judged as "claude-haiku-5-5" shared a key but no column, and a drop
  from 0.95 to 0.40 read "inconclusive", exit 0. A judge with no
  interval on one side is now "not comparable", never "overlap".
- SupportsTemperature is model-aware: on the API, Claude models from the
  4.7 generation on take no temperature, so the 5.5 judges record null
  with a note instead of sending 0 (a 400). The Sonnet 4.5 bridge judge
  keeps temperature 0.
- claudeCodeVersion leaves the configKey (the CLI updates itself); the
  baseline lists every version seen and --compare reports a change. The
  output names the metrics that flagged a verdict (regressedMetrics,
  improvedMetrics), and only for a comparison actually made.
- eval.sh: its env -u list is tested against blockedCLIEnv; --dry-run
  asks the CLI nothing, --preflight adds the sign-in check; stale
  comments fixed.
- Docs: API-era baselines are not comparable with this instrument on
  any route; the 2026-10-04 CLI baselines are marked as possibly
  affected by the auto-memory leak (#127) and superseded, and the
  readings built on them are flagged unconfirmed.

Co-Authored-By: Claude <noreply@anthropic.com>
@0xMMA
0xMMA force-pushed the chore/eval-via-claude-code branch from fecbfaf to 3862a51 Compare October 11, 2026 18:57
@0xMMA
0xMMA merged commit c10706a into main Oct 11, 2026
1 check passed
@0xMMA
0xMMA deleted the chore/eval-via-claude-code branch October 11, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant