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

128 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 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.