cc-os/openspec/changes/archive/2026-07-03-add-os-adr-eval-.../design.md

128 lines
8.6 KiB
Markdown
Raw Normal View History

## Context
`os-adr` ships with `docs/adr-system/04-plugin-requirements.md` requirements 45: the plugin
must be effective at *unprompted* write-trigger recognition and *unprompted, correct* retrieval.
Eval A (`plugins/os-adr/eval/`) only measures skill-*following* once explicitly invoked — its
runner-prompt literally tells the model under test "the user has invoked `/os-adr:{{SKILL}}`".
That architecture cannot answer requirements 45 by construction: the thing being measured
(does the model notice on its own) is exactly what the runner prompt would be giving away.
Eval A's driver used in-session Agent-tool subagents pinned to a model tier, and treated the
missing SessionStart hook context as an acceptable fidelity gap ("irrelevant — these are
explicit invocations"). For Eval B that gap is not acceptable: the SessionStart hook's existence
note (naming `/os-adr:create` and `/os-adr:find`) is one of the two legitimate discovery paths
being measured (the other being the model reasoning its own way there without any hook hint,
e.g. via `docs/adr/` conventionally existing). If the hook never fires, we can't tell "the model
didn't notice" from "the model was never given the hint a real session would have."
Scenario shapes are frozen and out of scope here (`docs/adr-system/06-eval-scenarios.md`, 7
scenarios: W1W3 write-trigger, R1R4 retrieval) — this design is about the execution harness
only.
## Goals / Non-Goals
**Goals:**
- A runner that executes the model-under-test in an environment where the real SessionStart
hook fires, real skills/CLIs are reachable, and no part of the prompt names the ADR system.
- Fixtures realistic enough to support all 7 scenarios, including R2's distractor ADRs (23
near-miss, one Superseded) and R4's one-hop graph reachability (needs a real `graphify-out/`).
- A checker that scores both axes (unprompted-consultation, correctness) with as much
determinism as possible, reserving LLM judgment only for genuinely ambiguous cases (e.g. did
the model's prose count as "proposing" an ADR).
- Sandbox isolation so headless runs never mutate the canonical fixture or the real cc-os repo.
- Output format (TSV or similar) compatible with a later `autoresearch` Classic-mode loop.
**Non-Goals:**
- Running the grid, scoring a real pass rate, or iterating on retrieval/trigger heuristics —
that is the deferred next stage, not this change.
- Adding, removing, or reshaping scenario shapes in `06-eval-scenarios.md`.
- Multi-machine or CI execution — local headless runs only, matching Eval A's scope.
## Decisions
**1. Headless (`claude -p`) is the sole runner mode; no subagent fallback.**
Eval A offers both an in-session subagent path (cheaper, preferred) and a headless fallback
(`bin/run-headless`) for full fidelity. Eval B inverts that: only headless mode is valid, because
Agent-tool subagents share the parent session's already-loaded plugin/hook state and cwd
semantics in ways that don't reproduce a fresh SessionStart firing against the sandbox directory.
`claude -p --output-format stream-json --verbose --model <tier> --cwd <sandbox>` gives a fresh
process, a real SessionStart event, and a full transcript to check against.
*Alternative considered*: reuse Eval A's subagent shortcut for cost — rejected, since it would
silently reintroduce the exact fidelity gap this eval exists to close.
**2. Transcript-based mechanical check for axis (a) (unprompted consultation).**
`--output-format stream-json --verbose` emits every tool_use block. The checker greps the JSONL
for: a Skill/slash invocation matching `os-adr:*`, a `Bash` call matching `bin/adr-*`, or a
`Read`/`Glob` touching `docs/adr/`. This is fully mechanical — no model judgment needed to
detect "did it touch the ADR system at all."
*Alternative considered*: ask an LLM judge to read the transcript and decide — rejected for axis
(a) specifically, since tool-call presence is unambiguous and cheaper to check by grep.
**3. Axis (b) (correctness) is deterministic where possible, LLM-judged only at the edges.**
For R1R4 (retrieval), the fixture encodes the single correct ADR ID; the checker mechanically
confirms the transcript's retrieved/cited ADR ID matches (or, for R2, that it is NOT one of the
seeded distractor IDs). For W1W3 (write-trigger), "proposes... or asks whether to" is prose-
shaped and not fully mechanical — a new `docs/adr/NNNN-*.md` file appearing in the sandbox is a
mechanical PASS; absent that, a small rubric-bound LLM judge reads only the final transcript
message (not the full transcript) and answers a yes/no: "did the agent explicitly propose
recording this decision as an ADR, or ask whether to?" Kept as thin as possible to limit judgment
surface, mirroring the `os-doc-hygiene`/`os-adr` classify-only-the-ambiguous-part pattern already
used elsewhere in this repo.
*Alternative considered*: fully mechanical W1W3 checking via keyword grep on the final message
(e.g. "ADR") — rejected as too brittle/gameable; a judge with a narrow, fixed rubric is more
robust and still cheap since it reads one short message, not a transcript.
**4. Dedicated Eval-B fixture project(s), not reuse of Eval A's fixtures or the real llf-schema
pilot.**
Eval A's fixtures are too small (4 ADRs, no graph) and R2/R4 need specific engineered content
(a Superseded pair plus 23 near-misses; a real one-hop-only conflict via Graphify). Building
`plugins/os-adr/eval-b/fixture/` fresh, with `docs/adr/` populated per-scenario and a genuinely
built `graphify-out/` (via the real `graphify` binary against the fixture's own small codebase,
not a hand-authored stub) keeps the graph-layer degradation check (R4) honest.
*Alternative considered*: reuse the llf-schema pilot project directly — rejected; its exclude
list and ADR set are project-specific and not shaped for the distractor/one-hop scenarios,
and would tie the eval's stability to project state a migration exercise might later change.
**5. Per-cell repeat count is a runner flag, default left as an open question for the eval-
running stage, not fixed by this design.**
Unprompted behavior is more stochastic than the explicit-invocation behavior Eval A measures.
The harness supports `--reps N` on the sandbox/runner tooling, but this change does not decide
whether the eventual grid uses N=1 or N=3 — that's a call for whoever runs the eval, informed by
observed variance in a pilot run.
## Risks / Trade-offs
- **[Risk] Headless runs perform real, uncontained tool use (file edits, possibly destructive
commands) against the sandbox.** → Mitigation: every run is a fresh git-initialized sandbox
copy (mirrors Eval A's `bin/sandbox` pattern) under a scratch/tmp root, never the canonical
fixture or the cc-os repo; sandbox is disposable and diffed/discarded after checking.
- **[Risk] Cost — headless full-session runs are pricier than Eval A's subagent shortcut.** →
Mitigation: accepted deliberately (see Decision 1); this is the whole reason the fidelity gap
matters here. Reps default is left open (Decision 5) specifically to let the eval-running
stage control cost/variance trade-off with real data instead of a guess baked in now.
- **[Risk] LLM-judged axis (b) for W1W3 could itself be inconsistent or gameable over
`autoresearch` iterations.** → Mitigation: judge rubric and prompt are frozen alongside the
checker/fixtures/scenarios during any future `autoresearch` loop (same discipline Eval A's
README already imposes on its own checker), and Decision 3 keeps the judge's input surface
(one final message, not a transcript) as narrow as possible.
- **[Risk] A genuinely-built `graphify-out/` for the fixture could drift if `graphify`'s output
format changes.** → Mitigation: the fixture's `graphify-out/` is rebuilt by an explicit fixture
setup step (documented in the harness README), not committed as a frozen binary blob, so it's
always regenerated against whatever `graphify` version is installed.
## Migration Plan
Not applicable — this is a new, additive test-harness directory with no effect on shipped plugin
behavior. No rollback beyond deleting `plugins/os-adr/eval-b/` if abandoned.
## Open Questions
- Default `--reps` per cell for the eventual grid run (left to the eval-running stage; see
Decision 5).
- Whether the R4 degradation check (retrieval must fail without `graphify-out/`) needs its own
sandbox variant with the graph directory deliberately removed, or whether one fixture toggling
a flag suffices — resolve during tasks/implementation, not architecturally significant enough
to block this design.
- Exact rubric wording for the W1W3 LLM judge — drafted during implementation (tasks.md), not
fixed here.