Code-aware, calibrated scoring done in-session (the scoring agent)¶
Date: 2026-06-19
Status: Draft PRD — authored from a dogfooding session + 4 research streams
Plugin: anvil
Tracks: scoring quality (SL-adjacent); pairs with 2026-06-19-ergonomics-unattended.md (routing consumes these scores)
Breaking: YES (schema v6→v7: the six score dimensions change). Migration is additive-then-backfill; see §9.
1. Goal¶
Make anvil's task scoring code-aware and calibrated, and do the reasoning with the agent already in the session instead of a separate LLM API call.
Two failures this session motivated it: a refactor scored complexity=2 that was
really ~4 (a hidden cross-file dependency), and a brand-new isolated parser scored
blast_radius=5 (it imports nothing and nothing imports it). Both trace to one
root cause — scoring the predicted likely_files text, not the real code graph.
The fix is not "call an LLM on the task description" (that is today's --use-llm,
and the research shows text-only scoring underperforms historical actuals). It is:
let the in-session agent read the code, the git history, and past outcomes, and
propose adjustments to a deterministic rule-based floor.
2. Decisions locked (from the session Q&A)¶
- The in-session host skill does the scoring reasoning. Not MCP sampling — Claude Code does not support it (anthropics/claude-code#1785, open since 2025-06). Not a separate API call when an agent is already present.
- Adopt the 6-axis rubric as the Score dimensions (replacing the current six). Schema migration v6→v7.
- Keep both scores, surface the diff. A deterministic rule-based value AND the
agent-proposed value per axis; flag tasks where
|agent − rule| ≥ 2. - The agent grounds judgment in: the codebase (read/grep), git history/churn, calibration from past tasks, and the dependency/conflict graph.
3. The three tiers (one new, two existing, refined)¶
| Tier | Who scores | When | Determinism |
|---|---|---|---|
| 0 — Rules | score_task() heuristic (planning/scoring.py) |
always; the floor | deterministic |
| 1 — In-session host | the /anvil:plan skill reasons inline (no API call) |
an agent host is present | non-deterministic, clamped by Tier 0 |
| 2 — Direct API | --use-llm → resolve_planner_provider() |
headless / CI / non-Claude harness | model-dependent |
| 1b — MCP sampling | ctx.sample() in the score_tasks MCP tool |
future, behind a capability probe | guarded seam only |
The numbers stay rule-based as a floor; the agent proposes, deterministic rails
clamp. This extends anvil's existing invariant ("the LLM never overwrites the
numeric scores, only the explanation") rather than discarding it. Tier 1b is wired
but dormant: probe ctx for the sampling capability and light up only if a host
ever implements it.
Detection — explicit, not sniffed¶
Add --scoring-mode {rules|host|api} (and Config.scoring_mode), default rules.
The /anvil:plan skill — which knows it is in-session — passes host. CI/headless
keeps the default or sets api. Do not sniff CLAUDECODE/TTY heuristics
(brittle, unportable).
4. The six axes (the new Score dimensions)¶
Replaces complexity / parallelizability / context_load / blast_radius /
review_risk / agent_suitability. Each scored 1–5 with a one-line justification.
The bold label is the human-readable name; the snake_case token in parens is
the schema field / identifier used everywhere else in this doc and in code.
- Structural entanglement (
structural_entanglement) — files touched, import fan-out, cross-module spread, coupling; cyclomatic/cognitive complexity and test coverage of the affected code. - Conceptual difficulty (
conceptual_difficulty) — concurrency/distributed state, algorithmic depth, novelty (net-new vs known pattern), specialized domain knowledge. - Uncertainty (
uncertainty) — spec clarity, presence of acceptance criteria, likelihood of requirement change, number of open unknowns needing investigation. - Verifiability (
verifiability) — can correctness be checked cheaply and deterministically? Low verifiability RAISES effective difficulty (no feedback signal → no self-correction). - Coordination (
coordination) — people/teams/approvals; sole-owner areas. - Risk & reversibility (
risk_reversibility) — prod/data blast radius, security/privacy/compliance, migration/backfill, rollback difficulty. One-way doors.
These axes are consumed by score-driven routing — see the companion ergonomics PRD. (That is why "uncertainty" and "verifiability" are first-class: they decide whether the agent acts, asks, or writes a test first.)
5. What the agent pulls from (ranked — the value-add)¶
Ranked by research (efferent coupling > churn > coverage as fault predictors):
- Real dependency fan-out — resolve imports/call-sites of
likely_files. A file nothing imports is low-entanglement regardless of size (fixes the "isolated parser = 5" error). Efferent coupling beats keyword guessing. - Co-change / evolutionary coupling from git —
git logfiles that historically commit together surface the hidden dependency a refactor drags in (fixes the "complexity-2 was really 4" error). - Relative code churn — high-churn touched files predict defects; normalize by size (absolute churn misleads).
- Test surface on the diff — low coverage of changed lines raises the verifiability axis (a modifier, not a predictor — coverage ≠ assertion quality).
- Cognitive-complexity hotspots of touched functions (better than cyclomatic).
- Concurrent-branch/author churn → coordination/merge-conflict signal (advisory; predictors ~60%).
The agent needs Read, Grep, Bash (for git log), and the dependency graph.
6. Calibration (the biggest lever)¶
The agile/ML literature is blunt: historical actuals beat model sophistication
(UCL replication, arXiv 2201.05401). And it is nearly free here — Evidence already
stores files_changed, commit_sha, review verdict, and reopened status.
- New
task_outcomestable instate.db: on completion store(axis, predicted, actual_proxy, files_touched, review_caught, reopened). - Per-axis correction factor, shrunk toward 1.0 by sample size:
f_adj = (n·f + k·1) / (n + k), withf = mean(actual/predicted)andk ≈ 5. Reference-class forecasting + local calibration in one query — no retraining. - Agent path: retrieve the 3–5 nearest past tasks (similarity-selected, not random) as few-shot anchors.
7. Evaluation — no parameter change ships unmeasured¶
Changing how tasks are scored without measuring the effect is flying blind. No scoring-parameter or dims change (especially the v6→v7 migration in §9) ships until it beats a frozen baseline on a scoring-eval harness. This gates §9.
Reuse the SL-2 pattern (the critic false-pass harness): corpus + baseline + metric, wired into CI.
- Corpus — tasks with revealed outcomes. The
task_outcomestable (§6) is the source. Seed it with completed anvil tasks (this session produced ~12 with real outcomes) plus a hand-labeled set that must include the known mis-scores as regression cases: the isolated parser scoredblast_radius=5(actual: low), and the refactor scoredcomplexity=2(actual: ~4, hidden dep). - Ground-truth proxy per axis — what actually happened: reopened? diff size vs
predicted? count of review findings? needed expansion? merge conflicts? — exactly
the fields
Evidencealready captures. - Metric — per-axis prediction error (MAE) + rank correlation (Spearman) vs the
actual-proxy, plus a directional check ("did it flag the genuinely high-risk /
high-uncertainty ones?"). Weight
risk_reversibilityanduncertaintyhighest — routing keys off them, so an error there costs more than one on, say, coordination. - Baseline — the current rule scorer's error on the corpus, frozen as the bar.
- A/B — run rule vs in-session-agent vs
--use-llmon the same held-out corpus; report deltas. Ship a new scorer only if it beats baseline on the weighted metric by a margin that clears the noise (report confidence intervals; require a minimum corpus size — scoring is small-sample, and over-claiming on thin data is itself a failure, per §8's small-sample pitfall). - Regression guard — wire the harness into CI like SL-2 so a future change can't silently regress scoring quality.
This closes the loop with calibration: the same task_outcomes rows that train the
correction factor (§6) are what the harness scores against — the system that
improves scoring and the system that measures it share one source of truth.
8. Pitfalls to design against¶
- Trusting
likely_filesblindly — both failures came from this. The agent must verify the files exist and resolve their real graph; an empty/wronglikely_filestriggers discovery, not a default score. - Small-sample overfitting — a thin category gives an unstable factor. Require a min-n, cap the multiplier, shrink toward the global prior (already in §6).
- Goodhart gaming — if scores feed routing/eval they get padded. Keep the calibration signal separate from any performance judgement; keep the rule-based score as an auditable floor (agent proposes, rails clamp).
9. Schema migration (v6 → v7)¶
The six dims change names/meaning, so this is breaking. Sequence:
- Add the six new axis columns + keep the old ones for one release (additive ALTER).
- Re-express
score_task()(the rule floor) in the six axes. - Backfill historical rows with explicit formulas on the 1–5 scale (no author-by-author guessing), and mark backfilled rows:
risk_reversibility := blast_radius(direct).structural_entanglement := context_load(direct).conceptual_difficulty := complexity(direct).verifiability := 6 − review_risk(inverse: high review risk ⇒ low verifiability).uncertainty := 3andcoordination := 3(no clean historical source — default to the neutral midpoint and flag the row for agent re-score).agent_suitabilityis dropped as a backfill source, not mapped: it was itself derived (6 − complexity), so it carries no independent signal; its behavioural role moves to routing (companion PRD).- Remap dependent thresholds:
auto_expand(wascomplexity≥4) → "several axes ≥4 or conceptual_difficulty≥4"; the oldagent_suitability≤2flag → routing (companion PRD). Add atask_outcomestable (§6). - Drop the old columns in the following release. Three-file version bump per
CLAUDE.md. Because
verifiabilityis inverse-mapped, literal numeric replay-equivalence is impossible by design — so the migration's equivalence test asserts routing equivalence: every historical task lands in the same routing branch (act / ask / test-first / decompose / halt) before and after, not identical numbers. Numeric replay-equivalence still applies to the direct-mapped axes.
10. Open questions¶
- For the two axes with no historical source (
uncertainty,coordination), is "default 3 + flag for agent re-score" (§9.3) right, or should backfilled rows stay null until a re-score runs? - Does the rule floor produce all six axes, or only the ones it can compute deterministically (leaving the rest agent-only with a "rule: n/a" marker)?
- Does the diff-flag (
|agent − rule| ≥ 2) block promotion or just annotate? - Where does the agent's per-axis justification live —
Score.explanation(today) or a structured per-axis field?
11. References¶
bin/src/anvil/planning/scoring.py(rule scorer +_augment_explanationseam),cli/plan.py(--use-llm~L91-119,score~L686),state/models.py(ScoreL249,EvidenceL420 — outcome fields already present),mcp_server.py:2232(score_tasks, rule-only, unusedctx).- MCP sampling spec: modelcontextprotocol.io/specification/2025-06-18/client/sampling; Claude Code gap: anthropics/claude-code#1785.
- Estimation research: arXiv 2201.05401 (text models underperform actuals); Zimmermann TSE 2005 (evolutionary coupling); Nagappan & Ball (relative churn); arXiv 2403.08430 (similarity-selected few-shot).