Repository navigation
[Phase 1] Build the deterministic eval scorer + offline tests (triage) #583
Copy link
Copy link
Closed
Labels
dev-leadFor dev-lead agent pickupFor dev-lead agent pickupinitiativeEpic / initiative tracking issueEpic / initiative tracking issue
Description
Story
As a skill maintainer,
I want a scorer that runs a skill against its held-out cases and emits a reproducible pass/score report, with offline tests,
so that any candidate version of a skill can be scored deterministically against the fixed held-out set.
Acceptance Criteria
- skills/evals/run-eval.sh loads that skill's cases.jsonl, invokes the skill prompt per case, compares output to the expected fields, and prints a per-case result plus an aggregate score as JSON.
- For triage, scoring is deterministic: parse the model's JSON output and compare
escalate(andrisk) to expected; a mismatch or unparseable output counts as a failure. - The scorer invokes the skill through the existing engine abstraction (scripts/engine.sh run_triage) rather than reimplementing model invocation, and accepts an env override for the engine command so tests run without a real provider.
- Offline bats tests drive run-eval.sh against a stubbed engine + fixture cases with no network (mirroring the DRY_RUN-offline pattern of tests/test_initiative_planner.bats) and are added to the lint.yml bats list.
- run-eval.sh passes
shellcheck --severity=warning -x; any python helper follows the repo's inline-pip convention.
Tasks / Subtasks
- Write skills/evals/run-eval.sh that sources scripts/engine.sh and iterates a skill's cases (AC: test issue from agent #1, Add @claude delegation, auto-merge, and rebase handling #3)
- Pipe each case's PR context into run_triage with prompts/triage.md
- Allow REVIEW_ENGINE / engine-command override for offline runs
- Implement the deterministic triage comparator (escalate + risk equality) (AC: test issue from agent #1, Go-live improvements for PR review agent #2)
- Add a stub engine + fixtures and tests/test_skill_evals.bats (AC: Optimize review: small-PR and incremental fast paths #4)
- Assert correct outputs score pass
- Assert a wrong output scores fail
- Register the new bats file in .github/workflows/lint.yml and confirm shellcheck-clean (AC: Optimize review: small-PR and incremental fast paths #4, feat: add Copilot engine support via REVIEW_ENGINE toggle #5)
Dev Notes
- scripts/engine.sh already exposes run_triage (no-tool tier, stdout capture) and honors REVIEW_ENGINE/per-tier overrides — source it, do not reinvent model invocation. Triage runs on a Haiku-tier model per set_engine_config.
- Make the engine command injectable (env override) exactly as engine.sh allows REVIEW_ENGINE to be overridden, so the bats tests run fully offline with a stub — copy the offline discipline of tests/test_initiative_planner.bats (no network in CI).
- Emit machine-readable output (aggregate score JSON); reuse the
::error::-style failure messaging convention from validate-plan.py where a hard failure is warranted. - Keep cases read-only here: the scorer reads skills/evals//cases.jsonl but never writes it. Optional cost capture via TOKEN_LOG_FILE (scripts/lib/token-metrics.sh) — leave unset for zero overhead.
- Follow AGENTS.md bash standards: #!/usr/bin/env bash, set -euo pipefail, shellcheck source= annotations for engine.sh.
Project Structure Notes
New scorer lives beside its data under skills/evals/; tests added under tests/ alongside existing bats suites and registered in lint.yml exactly like test_initiative_planner.bats.
References
- scripts/engine.sh#run_triage
- scripts/engine.sh#set_engine_config
- scripts/initiative-planner/validate-plan.py
- tests/test_initiative_planner.bats
- .github/workflows/lint.yml#bats
- scripts/lib/token-metrics.sh
- AGENTS.md
Likely target surface
skills/evals/run-eval.shtests/test_skill_evals.bats.github/workflows/lint.yml
Story prepared by the BMAD Scrum Master (Bob) for epic #581. Status: ready-for-dev.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
dev-leadFor dev-lead agent pickupFor dev-lead agent pickupinitiativeEpic / initiative tracking issueEpic / initiative tracking issue
Activity
don-petry commentedon Jun 14, 2026
Dev-Lead Implementation Plan
Issue: #583 — [Phase 1] Build the deterministic eval scorer + offline tests (triage)
Scope
Build a deterministic eval scorer that runs the
triageskill against its held-outcases.jsonlvia the existingscripts/engine.sh run_triageabstraction and emits a reproducible per-case + aggregate score as JSON, plus fully-offline bats tests driven by a stub engine.Placement decision (deviation from the literal issue text)
The issue names
skills/evals/run-eval.sh, but epic #581 decision A1 (landed in the #582 commit) relocated the held-out cases to repo-rootevals/, and.github/CODEOWNERS+evals/README.mdrequire the scorer to stay separate from the CODEOWNER-gated held-out set (reward-hacking guard). Putting a scorer inside/evals/would break that separation. The scorer therefore lives atscripts/evals/run-eval.sh— underscripts/(org "shell orchestration" home), automatically covered by thescripts/**/*.shshellcheck gate, and readingevals/<skill>/cases.jsonlread-only.Implementation Checklist
scripts/evals/run-eval.sh—run-eval.sh <skill>(defaulttriage); sourcesscripts/engine.sh(init output to stderr), iteratesevals/<skill>/cases.jsonl, builds prompt =prompts/<skill>.md+## Pre-fetched PR context+ case input, invokes the triage engine command, parses model JSON, comparesescalate+risk, emits aggregate JSON with acases[]array.EVAL_ENGINE_CMD, defaultrun_triage) so tests run offline with a stub — mirrors how engine.sh honorsREVIEW_ENGINE.EVALS_DIRoverride (default repo-rootevals) so tests point at fixture cases; cases are read-only.::error::-style hard-failure messaging (bad usage / missing prompt / missing cases) per validate-plan.py convention; tiered exit (0 all-pass, 1 some-fail, 2 hard-error)..github/workflows/lint.yml— addtests/test_skill_evals.batsto the bats list.Tests to Write (
tests/test_skill_evals.bats, offline, no network)score==1,cases[].pass==true).escalate/riskmismatch) scores fail.::error::.Test Command
bats tests/test_skill_evals.bats(full suite: the bats list in.github/workflows/lint.yml); lint:bash .dev-lead/scripts/dev-lead-lint.shdon-petry commentedon Jun 14, 2026
Dev-Lead: Implementation Complete
Plan Execution
scripts/evals/run-eval.sh— sourcesscripts/engine.sh, iteratesevals/<skill>/cases.jsonl(read-only), inlines each case input under## Pre-fetched PR context, invokes the engine command, and emits a single aggregate JSON object with a per-casecases[]array.escalateandrisk; a mismatch OR unparseable/non-JSON output OR a non-zero engine exit is a case failure.run_triage(AC Add @claude delegation, auto-merge, and rebase handling #3) with anEVAL_ENGINE_CMDenv override (defaultrun_triage) so tests run offline with a stub — no model invocation reimplemented.tests/test_skill_evals.bats— stub engine + fixture cases, fully offline (mirrors the DRY_RUN discipline oftest_initiative_planner.bats).tests/test_skill_evals.batsin.github/workflows/lint.ymlbats list.shellcheck --severity=warning -xclean;bash .dev-lead/scripts/dev-lead-lint.shpasses.Placement note (deviation from the literal issue text)
The issue named
skills/evals/run-eval.sh, but epic #581 decision A1 (landed in the #582 commit) relocated the held-out cases to repo-rootevals/, and.github/CODEOWNERS+evals/README.mdrequire the scorer to stay separate from the CODEOWNER-gated held-out set (reward-hacking guard). Placing a scorer inside/evals/would break that separation, so the scorer lives atscripts/evals/run-eval.sh— underscripts/(org shell-orchestration home, auto-covered by thescripts/**/*.shshellcheck gate) and readsevals/<skill>/cases.jsonlread-only.Test Results
Files Changed
scripts/evals/run-eval.sh(new): deterministic eval scorer.tests/test_skill_evals.bats(new): 9 offline tests (stub engine + fixtures)..github/workflows/lint.yml: addedtests/test_skill_evals.batsto the bats list.Notes
0all cases pass,1≥1 case failed (gateable),2hard error (bad usage / missing prompt or cases file / missingjq). Hard failures use the::error::convention fromvalidate-plan.py.total=8, scorer parses and scores every line without writing the set.bats_require_minimum_version 1.5.0forrun --separate-stderr(keeps the scorer's stderr diagnostics out of the asserted JSON). CI's aptbatson ubuntu-latest is 1.10.0.TOKEN_LOG_FILEcost capture is honored transparently byengine.sh; left unset here for zero overhead.triageexists today; the scorer is keyed on<skill>so additional skills slot in once theirevals/<skill>/cases.jsonl+prompts/<skill>.mdland.feat: implement issue #583 — [Phase 1] Build the deterministic eval s…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
fix(evals): tolerate fenced/wrapped JSON in the deterministic scorer …
13 remaining items
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…
feat: implement issue #583 — deterministic eval scorer (triage) over #…