Skip to content

docs(evals): scoring rubric for /recce-verify v1 - #28

Merged
even-wei merged 12 commits into
mainfrom
feature/drc-3403-rubric
May 14, 2026
Merged

even-wei merged 12 commits into
mainfrom
feature/drc-3403-rubric

Conversation

@even-wei

Copy link
Copy Markdown

Summary

  • Add evals/agent-blind-spots/ — qualitative case-study eval contract for the /recce-verify v1 skill, gating v1 backend additions on a ≤5-entry gap report.
  • RUBRIC.md defines three case-study lenses (binary catch, primary evidence tier, counterfactual delta vs frozen baseline) and resists aggregation: explicitly not a leaderboard, not a regression suite, not statistical evidence.
  • Templates for per-fixture Tier-0 baseline (frozen at commit) and gap report (target ≤5 with overflow-with-rationale path); README sets up the run procedure and the Super / 205DataLab framing.

Why this shape

N=6 is too small for statistics. The eval is built around named-case narratives so it survives presentation to Super / 205DataLab without the credibility trap of implied false rigor. Two review passes shaped this: Codex via /codex:rescue (11 findings — leaderboard-slip language, tier ambiguity, delta-vs-absolute conflict, gap-report cap) and a self-review via /recce-dev:claude-code-review (3 ISSUEs + 3 NOTEs — delta taxonomy non-exhaustive, frozen-baseline wording inconsistent, commit-instruction collision with workspace rule).

Tier 1 capability subsets

/recce-verify v1 targets Tier 1 (single dev environment). The rubric splits this into recordable subsets so attribution is unambiguous: 1a column-level lineage, 1b AST / SQL semantic analysis, 1c structured queries against the current dev env. Tier 2 (base environment) is explicitly out of v1; a fixture reaching it is a v2 signal for the gap report, not a v1 capability claim.

Out of scope

  • Fixtures themselves — built separately in DRC-3402 (parallel issue).
  • Skill implementation — DRC-3404.
  • Eval run — DRC-3405; uses this rubric as input.

Test plan

  • Codex /codex:rescue adversarial review — findings addressed in b672c9c
  • /recce-dev:claude-code-review self-review on branch — findings addressed in 7206e10
  • First real eval run consumes this rubric (gated on DRC-3402 fixtures + DRC-3404 skill)

Resolves DRC-3403.

even-wei added 3 commits May 12, 2026 09:46
Add evals/agent-blind-spots/ with:

- RUBRIC.md: 3-dimension qualitative scoring (binary catch, confidence
  tier, counterfactual delta vs frozen Tier-0 baseline). Tier 1 is the
  /recce-verify v1 target (single dev environment); Tier 2 (base
  environment available) is recorded only if a fixture genuinely needs
  it.
- templates/tier-0-baseline.md: per-fixture frozen agent-only verdict;
  must be committed before scoring with Recce so the eval measures
  delta, not absolute correctness.
- templates/gap-report.md: ranked shortlist, capped at 5 entries, with
  a "Not promoted" receipts section so the 33-item Notion capability
  list is deliberately pruned rather than silently dropped.
- README.md: layout, run procedure, and the "qualitative not
  quantitative" guard rail aimed at Super / 205DataLab framing.

Fixtures will be populated under fixtures/ via DRC-3402.

Linear: DRC-3403
Signed-off-by: even-wei <evenwei@infuseai.io>
BLOCKER
- gap-report.md: replace hard "Maximum 5 entries" with "Target ≤5" plus
  an exceed-with-rationale overflow path, so 6 genuinely independent
  blockers (one per fixture class) aren't artificially forced out.

MAJOR — leaderboard-slip guardrails
- README.md and RUBRIC.md: reframe "3 dimensions" as "3 case-study
  lenses — observation axes, not metrics; do not aggregate".
- Replace "ranked" with "prioritized for action" / "action-prioritized"
  in README and RUBRIC gap-report sections.
- gap-report.md: replace the implicit "blocked × cheap × decisive"
  formula with explicit qualitative criteria, plus a "if you find
  yourself ranking by numeric product, stop — that's the leaderboard
  trap" guard rail.

MAJOR — tier coherence
- RUBRIC.md: split Tier 1 into capability subsets (1a CLL, 1b AST,
  1c structured single-env queries) so attribution is unambiguous.
- Reframe Tier 2 as "Beyond v1 — base environment needed". A fixture
  reaching Tier 2 is a v2 signal for the gap report, not a v1
  capability claim. Resolves the "tier and out-of-scope" tension.

MAJOR — delta vs absolute reconciled
- RUBRIC.md lens 3: state explicitly that binary catch is recorded for
  BOTH the Tier-0 baseline and the with-Recce run; the delta is between
  those two binary-catch values. Enumerate the three delta shapes
  (miss→catch, catch→catch, catch→miss) so the relationship is concrete.

MAJOR — minimum-tier ambiguity
- RUBRIC.md lens 2: "minimum tier" → "primary evidence tier — which
  capability set produced the decisive piece of evidence the agent
  cited". Secondary citations go under Notes.

MAJOR — gap-report receipts scope
- gap-report.md: drop the "33-item Notion list completeness"
  requirement. The Not-promoted section now covers candidates we
  actually considered for the run, not the entire backlog.

NIT
- tier-0-baseline.md: drop the speculative model-id example; ask for
  what the agent runtime actually reports.
- README.md: rephrase "commit before proceeding" so the baseline-freeze
  step is compatible with the workspace user-confirmation rule for
  commits.

Linear: DRC-3403
Signed-off-by: even-wei <evenwei@infuseai.io>
ISSUEs from /recce-dev:claude-code-review self-review on the branch:

- RUBRIC.md lens 3: catch values are {catch, partial, miss} → 9 possible
  deltas, but only 3 were enumerated. Collapse to three ordered buckets
  (Improvement / Same / Regression) covering all 9 cases, with examples
  and an interpretation per bucket. Regression bucket flagged as a
  v1-release-blocker signal.
- Unify "frozen baseline" wording across README, RUBRIC, and the
  baseline template: the baseline is "frozen at commit" — once it
  lands on the branch, the with-Recce run can begin and the baseline
  must not be edited even if later evidence suggests revision.
- README.md commit instruction: hand commit responsibility to the
  commander explicitly (workspace's normal change-control flow),
  rather than asking the runner to commit autonomously. Resolves the
  collision with the "NEVER push or commit without user confirmation"
  workspace rule.

NOTEs picked up while in the files:

- README "schema expansion" → "schema-expansion" so it matches every
  other class name and is greppable as a single token.
- tier-0-baseline.md reasoning section: drop "or close paraphrase".
  Reasoning is exactly the thing that gets rewritten by accident —
  require verbatim quoting (link to transcript if long).
- gap-report.md entries: add a Receipts field pointing at the
  per-fixture scoring entries that surfaced the gap, so the audit
  trail from "gap promoted" → "evidence" is explicit, not implied.

NOTEs deferred (not landing this pass to keep churn bounded):

- RUBRIC.md "highest decisive tier" ordering across 1a/1b/1c — fair
  point, but the subsets are orthogonal by design and the rubric
  already asks for the subset to be recorded explicitly. Revisit if
  a real scoring run exposes ambiguity.
- Worked example for Tier 2 "provably requires" — bloat risk;
  defer until first fixture hits Tier 2.
- gap-report overflow "second-pair-of-eyes" guard and
  partial-run escape hatch in README step 5 — both add process,
  not clarity. Revisit after first eval run.

Linear: DRC-3403
Signed-off-by: even-wei <evenwei@infuseai.io>
even-wei added 2 commits May 12, 2026 11:35
Replaces the previous artifact-committed approach (+377k LOC) with a
build-script + gitignore approach. The eval runner runs `build_fixtures.sh`
once before each eval run to (re)generate the per-fixture artifacts/
directories from pinned dbt versions against an empty DuckDB.

Keep in tree (human-authored, small):
- evals/agent-blind-spots/build_fixtures.sh
- evals/agent-blind-spots/fixtures/README.md (caveats rolled up)
- evals/agent-blind-spots/fixtures/<slug>/README.md
- evals/agent-blind-spots/fixtures/<slug>/tier-0-baseline.md
- evals/agent-blind-spots/fixtures/<slug>/commits.txt
- evals/agent-blind-spots/fixtures/<slug>/diff.patch
- evals/agent-blind-spots/fixtures/pr44-promotion-flags/diff-from-base-to-intermediate.patch

Gitignored (regenerable):
- evals/agent-blind-spots/.tmp/
- evals/agent-blind-spots/fixtures/*/artifacts/

Fixtures, classes:
- pr1-fix-clv (semantic)
- pr42-is-closed-filter (row-grain; older base SHA)
- pr2-refactor-cte-to-models (refactor; negative control)
- pr3-amount-double-to-decimal (type; PR has merge head on top of substantive 6ffc23f)
- pr44-promotion-flags (schema-expansion + intermediate row-filter at 23b96ca)
- pr46-net-clv-segments (multi-model semantic; stress-test fixture)

Build pins (per upstream packages.yml + DRC-3402 capture):
- Python 3.11.11, dbt-core 1.11.9, dbt-duckdb 1.10.1, duckdb 1.5.2
- dbt packages: dbt_profiler@0.8.1, dbt_utils@0.9.6, audit_helper@0.11.0

Manifests/catalogs are scrubbed of user_id, invocation_id, root_path.

Resolves DRC-3402

Signed-off-by: even-wei <evenwei@infuseai.io>
The original rubric defined Tier-0 *inputs* but not the agent runtime,
leaving the eval run (DRC-3405) to invent it. That risk: different runs
pick different Tier-0 capability slices, so the with-Recce delta becomes
unreproducible.

Add a "Tier-0 agent runtime contract" section pinning:

- Inputs: the artifacts the build script produces plus read access to
  the head-SHA project source. Strongest plausible Tier-0 baseline,
  so Recce signal isn't overstated by an artificially weak comparison.
- Generic tools allowed (file read, grep, jq, git read commands).
- Explicit prohibitions (no Recce CLI / MCP / skills, no warehouse,
  no dbt run / dbt test, no base-env comparison beyond the artifacts).
- Prompt shape — eval runners may adapt wording; the capability list
  is the contract.
- Agent-specific scaffolding is allowed; deviation goes in Tier-0
  baseline Notes so the delta stays interpretable.

Surfaces from "what does a Tier-0 fixture actually look like in
practice?" — the rubric had this implicit; making it explicit before
DRC-3405 starts.

Linear: DRC-3403
Signed-off-by: even-wei <evenwei@infuseai.io>
@even-wei

even-wei commented May 12, 2026

Copy link
Copy Markdown
Author

Update (HEAD: c0cb2a5) — Verdict: GO

Re-reviewed after commit c0cb2a5 ("docs(evals): tighten Tier-0 contract and gap-report overflow") and verified the cross-PR fix at PR #29 commit 767ca54. Both prior ISSUEs are resolved; 2 of 5 NOTEs are resolved; the 3 remaining NOTEs are non-blocking observations that can be addressed in a follow-up.

Prior findings — resolution

# Finding Status
ISSUE 1 Template-vs-rubric drift on Tier-0 inputs Resolvedtemplates/tier-0-baseline.md:16 now references the full Tier-0 runtime contract and enumerates all six artifact files + the per-fixture head-SHA source tree at .tmp/sources/<fixture-id>/. Template no longer undercounts.
ISSUE 2 build_fixtures.sh working-tree contract breakage ResolvedRUBRIC.md:55 retargets read access to .tmp/sources/<id>/ (per-fixture worktree) and explicitly warns against reading from the shared .tmp/jaffle_shop_golden/ clone. PR #29's build_fixtures.sh at 767ca54 calls git worktree add --detach "${source_dir}" "${head_sha}" after each fixture is built, materializing six distinct head SHAs. Verified the build script end-to-end against the spec.
NOTE 1 dbt parse/compile/docs generate ambiguous ResolvedRUBRIC.md:59 adds these three commands to the explicit-NOT-allowed list with the right rationale ("regenerating … lets the agent reach beyond the captured snapshot"). Template echoes the constraint at line 16.
NOTE 2 Gap-report cap gameable via fix-label splitting Resolvedtemplates/gap-report.md:5–7 makes "Cheapest fix" the explicit dedup key, gives a worked example (six symptoms → one entry under "no base-environment comparison"), and the overflow line instructs reviewers to skim the Cheapest-fix column for duplicates.
NOTE 3 "Regression = v1-release-blocker" too strong given N=6 Still openRUBRIC.md:41 unchanged. Non-blocking; can soften in a follow-up.
NOTE 4 Lens 2 singular-vs-plural inconsistency Still openRUBRIC.md:23 ("the decisive piece"), :28 ("subset(s)"), and :82 (<0 | 1a | 1b | 1c | 2> pipe-or) remain inconsistent. Non-blocking.
NOTE 5 1a / 1b / 1c not cleanly distinguishable Still open — no worked example added. Non-blocking.

New findings introduced by c0cb2a5

None at ISSUE or BLOCKER severity. Minor observations:

  • NOTE A — Path style inconsistency (cosmetic). RUBRIC.md:55 writes the per-fixture source path as evals/agent-blind-spots/.tmp/sources/<id>/ (repo-root-relative), while RUBRIC.md:57 and templates/tier-0-baseline.md:16 write .tmp/sources/<id>/ (relative to the eval directory). Both resolve to the same place given the build script's SCRIPT_DIR, but the two forms next to each other invite reader confusion. Pick one and use it consistently.
  • NOTE B — <id> vs <slug> terminology drift. RUBRIC and template use <id> / <fixture-id>; build_fixtures.sh and the on-disk directories use ${slug} (pr1-fix-clv, etc.). The PR docs(evals): build six PR fixture set for /recce-verify v1 eval #29 fixtures/README.md presumably reconciles these, but a one-line glossary line in RUBRIC.md ("<id> = fixture slug, e.g. pr1-fix-clv") would close the loop. Non-blocking.
  • NOTE C — Intermediate-commit artifacts unmentioned. build_fixtures.sh produces an extra artifacts/intermediate-commit-<sha>/ directory for pr44-promotion-flags, and that fixture also ships a diff-from-base-to-intermediate.patch. RUBRIC.md:49–55 enumerates the standard six artifacts but doesn't acknowledge this PR-specific extra. Either add a one-liner ("Some fixtures ship additional intermediate snapshots — see the fixture's README.md") or accept this as documented at the fixture level. Non-blocking.

Verification

  • git diff ca837ed..c0cb2a5 --stat confirms the fix commit touches only the three intended files (RUBRIC.md +11/-6, gap-report.md +3/-1, tier-0-baseline.md +1/-1).
  • Cross-PR claim verified: fetched build_fixtures.sh from PR docs(evals): build six PR fixture set for /recce-verify v1 eval #29 767ca540 directly; confirmed git worktree add --detach "${SOURCES_DIR}/${slug}" "${head_sha}" lines exist and run inside build_fixture() after each pair of build_at_sha calls.
  • No stale references to the old "checkout left by the build script" promise remain in either file.

Recommendation

Ready to merge. The two ISSUEs that blocked the prior review are cleanly resolved with concrete, traceable changes. Remaining NOTEs (3 carried over, 3 new) are quality observations that don't gate v1 — fold them into a follow-up doc pass if desired.


Original review (HEAD reviewed: ca837ed) — NO-GO, kept for audit history

Code Review: PR #28docs(evals): scoring rubric for /recce-verify v1

Files reviewed: 4 (README.md, RUBRIC.md, templates/tier-0-baseline.md, templates/gap-report.md)
Categories: docs-only
Passes run: A (correctness/coherence), C (cross-doc consistency), F (diff-specific drift across 4 commits)
HEAD reviewed: ca837ed
Stance: Independent adversarial pass — no sibling reviews consulted.

Validation Results

Pass A — Coherence of the three lenses — PASS (with one NOTE)

The three lenses (binary catch, primary evidence tier, counterfactual delta) compose without contradiction. A with-Recce run whose Primary evidence tier scores 0 maps cleanly to a Same delta, which is internally consistent — though a reader could briefly conflate "this run had Tier-0 access only" with "this with-Recce run cited Tier-0 evidence as decisive." See NOTE 4.

Pass C — Cross-doc consistency (README ↔ RUBRIC ↔ templates ↔ Tier-0 contract) — FAIL

The Tier-0 runtime contract added in ca837ed is not reflected in templates/tier-0-baseline.md. See ISSUE 1.

Pass F — Diff-specific cross-reference with PR #29 (build_fixtures.sh) — FAIL

Rubric line 55 promises "read access to the dbt project source at the head SHA (the .tmp/jaffle_shop_golden/ checkout left by the build script)." The implementation in PR #29 leaves the checkout at the LAST fixture built (pr46-net-clv-segments), not the current fixture. See ISSUE 2.

Verification Results

Docs-only change; no lint/test suite applies. CI guard bundle-freshness.yml is unaffected.

Verdict: NO-GO

Issues

  1. Template-vs-rubric drift on Tier-0 inputsevals/agent-blind-spots/templates/tier-0-baseline.md:16 says inputs are "dbt manifest, compiled SQL pre/post, git diff" (singular manifest, no catalog, no source checkout). RUBRIC.md:49–55 specifies six artifact files (manifest-before.json, manifest-after.json, compiled-before/, compiled-after/, catalog-before.json, catalog-after.json) PLUS read access to .tmp/jaffle_shop_golden/. An evaluator filling out the template alone will run Tier-0 with a smaller input set than the rubric specifies, understating the agent's solo capability and overstating Recce's delta — the exact failure mode commit ca837ed was added to prevent. Fix: either expand the template's "Inputs available to agent" line to enumerate all six artifacts + source checkout (preferred — keeps the template self-contained), or have the template explicitly reference RUBRIC.md § Tier-0 agent runtime contract as the authoritative input list.

  2. Working-tree contract not met by build_fixtures.shevals/agent-blind-spots/RUBRIC.md:55 promises "read access to the dbt project source at the head SHA (the .tmp/jaffle_shop_golden/ checkout left by the build script)." But build_fixtures.sh in PR docs(evals): build six PR fixture set for /recce-verify v1 eval #29 runs build_at_sha six (sometimes seven) times in a loop, each call doing git reset --hard && git clean -fdx && git checkout --detach <sha>. After the script finishes, the checkout is at pr46-net-clv-segments's head SHA — not the current fixture's. An evaluator running, say, pr1-fix-clv who reads source from .tmp/jaffle_shop_golden/ will be reading PR #46's models, not PR refactor(plugin): rename recce-dev to recce, add recce-dev for internal tooling #13's. Two options:

    • (a) Drop the working-tree dependency from the contract. Source at head SHA is already encoded in compiled-after/ + diff.patch; the agent doesn't need a live checkout.
    • (b) Add a "before-fixture" step to the rubric: git -C .tmp/jaffle_shop_golden checkout <head-sha-of-this-fixture> so the working tree matches the fixture under review. Document that the checkout is shared mutable state across fixture runs.

Notes

  1. dbt parse/dbt compile are ambiguous at Tier 0RUBRIC.md:57–59 lists dbt run, dbt test, "live SQL execution" as forbidden, but not dbt parse, dbt compile, or dbt docs generate. These don't necessarily hit the warehouse (with --empty-catalog), but they regenerate manifest.json against the source repo's Snowflake-only profile (no credentials → may fail; may also succeed differently than the frozen manifest-after.json because seeds/ and dbt_packages/ aren't staged as build_fixtures.sh does). The phrase "Anything the agent could plausibly run on a developer's laptop without Recce installed" leaves an agent reasonably permitted to run them. Either explicitly forbid dbt parse|compile|docs generate at Tier 0 (preferred — preserves the "frozen artifacts" premise), or explicitly permit them with a one-line caveat about reproducibility.

  2. Gap-report cap is gameable via fix-label splittingtemplates/gap-report.md:5–6 collapses entries that "share a fix," but a motivated runner can split one gap into two by choosing different "Cheapest fix" labels (e.g., "skill-side prompt change" for entry A, "MCP tool addition" for entry B) even when one fix would close both. No reviewer gate exists. Suggest tightening: "Two entries that share a fix collapse to one — even if you could rephrase the fix label. If the same sentence change in the skill prompt would close two entries, they are one entry."

  3. "Regression = v1-release-blocker" is too strong given N=6 framingRUBRIC.md:41 says any regression bucket case is "a v1-release-blocker signal." Given the rubric's own "N=6 means stories, not averages" stance, a single regression could be model variance rather than a real Recce-misleads-agent failure mode. Suggest softening to "warrants a release-readiness discussion before shipping; do not dismiss without investigating root cause."

  4. Lens 2 singular-vs-plural inconsistencyRUBRIC.md:23 says record "the decisive piece of evidence" (singular). Line 28 says "always record the subset(s)" (plural). Line 77's example field shows <0 | 1a | 1b | 1c | 2> (pipe-or, singular). For a fixture like pr1-fix-clv an agent can legitimately cite 1a + 1b + 1c (CLL shows the dependency, AST shows the new WHERE, query confirms row drop). Add an explicit tie-breaker within Tier 1 — e.g., "If multiple Tier-1 subsets contribute, record the one that produced the citation closest to the verdict statement as primary, list the rest under Notes."

  5. 1a / 1b / 1c are not cleanly distinguishable — CLL (1a) is computed via AST analysis (1b). Row-grain effects are detected by AST and confirmed by 1c. The rubric handles overlap via "highest decisive tier," but 0 < 1 < 2 doesn't help within Tier 1. The note plural-subset recording mitigates this, but the lens table would be sharper if it gave a concrete example of "this is a 1a citation; this is a 1b citation; this is a 1c citation" for the same PR (e.g., pr1-fix-clv).

What I could not verify

What I looked for and did not find

  • Quantitative-framing slips ("X% accuracy", "Y% improvement"). RUBRIC.md and README.md both explicitly reject these and there are no accidental slips.
  • A "Recce vs no-Recce" leaderboard framing. Absent.
  • Per-tier scoring aggregation (e.g., "Tier 1 mean catch rate"). Absent.
  • A "this run vs prior run" delta that conflates model variance with Recce signal. The frozen-baseline rule (RUBRIC.md:43) explicitly prevents this.
  • Tier 2 promoted as a v1 capability claim. RUBRIC.md:29 correctly flags Tier 2 as v2 signal only.
  • Internal contradictions in delta-bucket examples. miss → catch, miss → partial, partial → catch all correctly land in Improvement; catch → partial, catch → miss, partial → miss all correctly land in Regression. Ordering catch > partial > miss is consistent across the document.

Independent review pass — no sibling reviews consulted. Posted as COMMENT (workspace pattern: don't REQUEST CHANGES on own PR).

even-wei added 3 commits May 12, 2026 12:55
Independent /codex:rescue and /recce-dev:claude-code-review passes
converged on three issues, plus two Codex-only MAJORs:

CONVERGENT — Tier-0 input list drift (3 reviews caught)
- templates/tier-0-baseline.md:16 listed "dbt manifest, compiled SQL,
  git diff" but RUBRIC.md's runtime contract (commit ca837ed) added
  catalogs and per-fixture source-tree read access. Eval runners
  filling the template alone would run an undercount Tier-0, biasing
  the delta the eval exists to measure. Replace with full contract
  reference.

CONVERGENT — `dbt parse / compile / docs generate` ambiguity (Codex + Claude)
- RUBRIC.md's "explicitly not allowed" list named `dbt run` and
  `dbt test` but not the parse/compile/docs-generate trio. Those don't
  hit a warehouse but they DO regenerate the manifest/compiled/catalog
  artifacts — which lets the agent reach beyond the frozen snapshot
  (later upstream-package changes, etc.) and breaks reproducibility.
  Add them to the ban list with the rationale inline.

CROSS-PR — Working-tree contract not met (Claude /recce-dev on PR #28)
- RUBRIC.md promised "the .tmp/jaffle_shop_golden/ checkout left by
  the build script", but build_fixtures.sh in PR #29 loops over six
  fixtures with `git reset --hard && git checkout --detach`, leaving
  the working tree at the LAST fixture's SHA. An evaluator reading
  source for pr1-fix-clv would actually see pr46's models.
- Fix in the rubric: point at per-fixture `.tmp/sources/<id>/` instead
  (which the build script will materialize via git worktree in the
  PR #29 follow-up). Mark `.tmp/jaffle_shop_golden/` as build scratch
  not to be read directly.

MAJOR (Codex) — Prompt-engineering loophole
- "Eval runners may adapt wording" was too loose. A runner can prime
  the agent toward humility or aggression and shift the verdict
  without changing capabilities. Tighten with MUST/MUST-NOT rules:
  no input paraphrase, neutral framing, prompt recorded verbatim,
  agent scaffolding goes in Notes.

MAJOR (Codex) — Gap-report overflow gameable by symptom-splitting
- A single "no base-env comparison" gap could be split into six
  entries by symptom (row count / dupkey / nulls / CLL / lineage /
  semantic delta) to dodge the ≤5 target. Require each entry to
  name its "Cheapest fix" explicitly; entries naming the same fix
  collapse. The fix field becomes the dedup key, and reviewers can
  audit by skimming the column vertically.

Deferred:
- The NIT on catch>partial>miss ordering masking action/confidence
  mismatch — real concern, but adding a structured action-vs-verdict
  matrix to every baseline form is more rubric than the v1 eval can
  absorb. Revisit after first scoring run produces real data.

Linear: DRC-3403
Signed-off-by: even-wei <evenwei@infuseai.io>
From the four-way review pass (Codex + Claude on PR #28 and PR #29).
Three findings converged across reviewers; two were PR-29-specific.

CONVERGENT — Tier-0 baseline template inputs out of sync with rubric
(3 reviews caught this — PR #28 Codex, PR #28 Claude, PR #29 Claude)
- All six per-fixture `tier-0-baseline.md` files said "Inputs available
  to agent: dbt manifest, compiled SQL pre/post, git diff" but the
  rubric's Tier-0 runtime contract (PR #28 commit ca837ed) added
  catalogs and per-fixture source-tree access. Eval runners filling
  the baseline form alone would run an undercount Tier-0, biasing
  the delta the eval exists to measure. Replace with the full contract
  reference plus per-fixture path.

CROSS-PR — build_fixtures.sh did not deliver the promised source tree
(Claude /recce-dev on PR #28)
- RUBRIC.md promised "the .tmp/jaffle_shop_golden/ checkout left by
  the build script", but the build loop ran `git reset --hard &&
  git checkout --detach` six times over the same JSG_DIR, leaving
  the working tree at the LAST fixture's SHA. An evaluator reading
  source for pr1-fix-clv would see pr46's models.
- Fix: after building each fixture's head artifacts, materialize a
  per-fixture worktree at `${SOURCES_DIR}/${slug}/` via `git worktree
  add --detach`. Six independent worktrees off the shared clone —
  each fixture gets its own head-SHA source tree, the shared clone
  remains build-script scratch.
- Idempotent: existing worktrees are removed before re-creation.
- The matching rubric change is in PR #28 (commit c0cb2a5) which
  retargets readers to `.tmp/sources/<id>/` and explicitly marks
  the shared JSG_DIR as not-for-reading.

PR-29 ONLY — Private repo + auth not documented (Claude /recce-dev on PR #29)
- `DataRecce/jaffle_shop_golden` is private. `fixtures/README.md`
  claimed only `uv` and `git` were required. First-time runs on a
  fresh machine hit a confusing "Repository not found" error.
- Document: run `gh auth setup-git` once, or have a credential
  helper configured for the repo. Only matters on first clone;
  subsequent runs only fetch.

PR-29 ONLY — Verification class drift on pr44 baseline (Claude /recce-dev on PR #29)
- `pr44-promotion-flags/README.md` says class is
  `schema-expansion + (intermediate row-filter accident)`. Baseline
  file said just `schema-expansion`, dropping the most interesting
  half of the fixture. The intermediate-commit row-filter accident
  is the eval signal the fixture exists to test. Align baseline
  to README.

Linear: DRC-3402
Signed-off-by: even-wei <evenwei@infuseai.io>
Two NOTE-level findings from the re-review pass; both 1–2 line fixes.

- Orphan worktree registrations: if a user `rm -rf`s
  .tmp/sources/<slug>/ without `git worktree remove`, the next run
  fails with "missing but already registered worktree" because git's
  registration outlives the directory. Sweep with `git worktree prune`
  once after the fetch step — idempotent and cheap (no-op on a clean
  state). Verified by removing .tmp/sources/pr1-fix-clv/ between runs
  and re-running successfully.

- Pre-flight auth check: jaffle_shop_golden is private, so a fresh
  clone without configured creds errors with the unhelpful
  "Repository not found". Run `git ls-remote` before clone; on
  failure, print the actual remedy (`gh auth setup-git` or another
  credential helper) and exit. Only runs on first clone (when
  JSG_DIR/.git is absent), so warm reruns pay no extra latency.

Linear: DRC-3402
Signed-off-by: even-wei <evenwei@infuseai.io>

@wcchang1115 wcchang1115 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three points from cross-reading RUBRIC.md against the templates and the paired PR #29. All three are docs-level, no scope creep. Order is roughly severity × cost-to-fix-now.

1. Pin same-model contract between baseline and with-Recce run

templates/tier-0-baseline.md records Agent / Model for the frozen baseline, and RUBRIC.md's per-fixture artifact section records Agent / model for the with-Recce run — but nothing requires them to match.

If the baseline is captured on Opus 4.6 and the with-Recce run lands weeks later on 4.7, the lens-3 delta confounds Recce signal with model drift. Since DRC-3405 isn't gated on a calendar, this is a real risk.

Suggested edit — one line, somewhere in lens 3 or under the Tier-0 runtime contract:

The with-Recce run MUST use the same agent + model as the frozen baseline. If a model upgrade lands mid-eval, either re-capture the baseline (and re-commit it) or record the mismatch under Notes and treat that fixture's delta as confounded.

2. Tier-0 runtime contract leaks "future" commits via shared object DB

Confirmed against PR #29's build_fixtures.sh: .tmp/sources/<slug>/ is a git worktree (the build script does git worktree prune and explicitly sweeps orphan worktree registrations), and the parent clone at .tmp/jaffle_shop_golden/ fetches all PR heads via +refs/pull/*/head:refs/remotes/origin/pr/*. Since worktrees share the parent .git object database, an agent inside .tmp/sources/pr1-fix-clv/ can run git log --all, git show <other-sha>, or git rev-parse origin/pr/<n> and immediately see (a) later commits on the same branch that contain the actual fix, and (b) every other fixture's head SHA.

That defeats the Tier-0 frozen-input contract.

Two paths:

  • Rubric-side: add to "explicitly NOT allowed" — git log --all, git rev-list --all, git show / git rev-parse against non-ancestor SHAs, git fetch. Brittle (relies on agent compliance with a ban list it can trivially violate).
  • Build-side (preferred): have build_fixtures.sh produce a fresh git clone --depth 1 per fixture instead of a worktree. The leak then becomes structurally impossible — there is no other object to reach.

The second is more robust because the contract is enforced by the filesystem, not by the agent's good behavior.

3. "Highest decisive tier" is ambiguous within Tier 1

RUBRIC.md lens 2: "record the highest decisive tier and list secondary citations under Notes." Tiers 0 < 1 < 2 have an order, but 1a / 1b / 1c are not ordered — they're orthogonal capability surfaces (and 1a is computed via 1b, so they're entangled too). Two runners scoring the same fixture could legitimately disagree on which subset is "highest."

I see this is already noted as deferred (NOTE 5 in 7206e10, and called out as a D2 risk). Reasonable to defer, but the fix is genuinely one line and removes the ambiguity before any scoring run hits it:

Suggested edit to lens 2:

If the agent cited evidence from multiple Tier-1 subsets, mark the subset whose evidence appeared in the verdict sentence as primary, the rest as supporting. Tier ordering (0 < 1 < 2) applies across tiers; within Tier 1, primary/supporting replaces "highest."

If you'd rather defer the wording until the first scoring run produces real evidence of the ambiguity, that's also defensible — just wanted to surface that the cheap fix exists now.


Not blocking the PR. #1 is the only one I'd consider a pre-merge ask, and even that is "land in this PR or a follow-up before DRC-3405 starts."

Folds two items from Andy's review on PR #28:

- Lens 3 / frozen-baseline section: add Same-model contract. The
  with-Recce run MUST use the same agent + model as the frozen
  Tier-0 baseline; otherwise the lens-3 delta conflates Recce signal
  with model drift. Provides explicit fallback (re-capture or mark
  the fixture's delta as confounded under Notes).
- Lens 2 wording: clarify that tier ordering (0 < 1 < 2) applies
  across tiers, but within Tier 1 the subsets 1a/1b/1c are
  orthogonal — primary/supporting replaces "highest" inside Tier 1.

Item #2 from the same review (Tier-0 cross-fixture leakage via
shared git object DB) is being folded structurally in PR #29's
build_fixtures.sh — switching from worktree to git clone --depth 1
per fixture — and is intentionally not addressed here.

Refs: DRC-3403
Signed-off-by: even-wei <evenwei@infuseai.io>
@even-wei

Copy link
Copy Markdown
Author

Thanks for the cross-read — folding two of three in this PR; the third is being addressed structurally in PR #29.

#1 — Same-model contract: folded in 0fb378a.

Added a "Same-model contract" paragraph to the lens-3 frozen-baseline section (next to the existing model-variance note, since that's the constraint it extends):

The with-Recce run MUST use the same agent + model as the frozen Tier-0 baseline (the Agent and Model fields in templates/tier-0-baseline.md). If a model upgrade lands mid-eval, either re-capture the baseline (and re-commit it) or record the mismatch in the per-fixture artifact's Notes section and treat that fixture's delta as confounded. Without this constraint, the lens-3 delta conflates Recce signal with model drift.

This wires to the Agent / Model fields already in templates/tier-0-baseline.md, and the "record under Notes / treat as confounded" path lets DRC-3405 continue mid-stream without invalidating earlier fixtures.

#2 — Tier-0 cross-fixture leakage via shared object DB: being folded in PR #29 structurally, not here.

Agree the build-side fix is the right one — a ban list the agent can git log --all past is not a contract. PR #29's build_fixtures.sh is switching from git worktree add to git clone --depth 1 per fixture, so each .tmp/sources/<id>/ ends up with its own object database containing only the head SHA's history. After that lands, git log --all / git rev-parse origin/pr/<n> / git show <other-sha> are structurally impossible from inside a fixture, regardless of what the rubric says. Not adding the ban list to RUBRIC.md in this PR — the filesystem will enforce it, and a brittle ban list would just rot.

#3 — Tier-1 primary/supporting wording: folded in 0fb378a.

Reworded lens 2. Old phrasing "record the highest decisive tier" collapsed into one rule something that has two regimes; split that out:

If the agent cited evidence from multiple tiers, record the higher tier as decisive (tier ordering 0 < 1 < 2 applies across tiers) and list secondary citations under Notes. Within Tier 1, the subsets 1a / 1b / 1c are not ordered — they are orthogonal capability surfaces (and 1a is computed via 1b, so they're entangled). If the agent cited evidence from multiple Tier-1 subsets, mark the subset whose evidence appeared in the verdict sentence as primary, the rest as supporting; primary/supporting replaces "highest" within Tier 1.

Kept the cross-tier ordering rule explicit so runners don't second-guess the 0 < 1 < 2 direction.

Per Andy's review of PR #28 (item #2): the previous build flow materialised
each fixture's source tree via `git worktree add` off the parent
`.tmp/jaffle_shop_golden/` clone, which fetched every PR head into
`refs/remotes/origin/pr/*`. Because worktrees share the parent's object
database, an agent reading `.tmp/sources/<slug>/` could run `git log --all`,
`git rev-parse origin/pr/<n>`, or `git show <sibling-sha>` and immediately
see (a) later commits on the same branch (the actual fix) and (b) every
other fixture's head SHA. That defeated the Tier-0 frozen-input contract
in RUBRIC.md.

Replace the per-fixture `git worktree add` with a fresh, standalone repo:

  - `git init` inside `.tmp/sources/<slug>/`
  - `git fetch --depth 1 --no-tags <local-cache> +<full-sha>:refs/fixture/head`
  - `git checkout --detach refs/fixture/head`

No `git remote add` runs, so a stray `git fetch` inside a fixture has no
remote to consult; with `--depth 1` only that single commit lands in
`.git/objects/`, so sibling SHAs cannot be resolved and `git log --all`
returns just the fixture's own commit. The cache repo gets
`uploadpack.allowAnySHA1InWant=true` so the SHA-targeted fetch is allowed.
Abbreviated SHAs from `commits.txt` are expanded via the cache before the
fetch, since `fetch <local> <sha>` requires the full 40-char form.

Added an inline sanity check: `git rev-list --all HEAD --count` inside the
fixture's `.git` must equal `1`. Anything larger indicates a ref or pack
leaked through; the script then exits non-zero with a FAIL line.

The intermediate-commit replay for PR #20 is unaffected — that path only
ever wrote artifacts under `<artifacts>/intermediate-commit-23b96ca/` via
the shared cache (build-script scratch, not agent-readable); it never
populated `.tmp/sources/<slug>/`. Verified end-to-end: cold-start build
finishes in 2:48 (matches the prior ~2:40 baseline), the rebuild run
finishes in 2:39, and the leak check returns `1` for all six fixtures
(probed pr1-fix-clv adversarially: `git show 297eb54`, `git rev-parse
origin/pr/20`, `git show 23b96ca` all fail; `git log --all` shows only
`2abf877 PR1`).

The orphan-worktree pruning (`git worktree prune`) is removed — no
worktrees are created anymore, so there are no orphans to sweep. Auth
pre-flight is preserved.

Refs: DRC-3402
Reviewed-by: Andy (PR #28 review, item #2, 2026-05-14 01:22Z)
Signed-off-by: even-wei <evenwei@infuseai.io>
@even-wei

Copy link
Copy Markdown
Author

Re: point #2 — Tier-0 runtime contract leak via shared object DB.

Folded into PR #29 as commit ae28f37. Replying here since this thread is where the discussion lives; will keep #29's PR description focused on the original DRC-3402 scope.

What changed — replaced the per-fixture git worktree add off the shared .tmp/jaffle_shop_golden/ cache with a fresh standalone repo per fixture:

git -C "${source_dir}" init
git -C "${source_dir}" fetch --quiet --depth 1 --no-tags \
    "${JSG_DIR}" "+${full_head}:refs/fixture/head"
git -C "${source_dir}" checkout --quiet --detach refs/fixture/head

No git remote add, no refspec other than the one head SHA — the resulting .git carries one commit, zero remotes, and no other refs. Cache repo gets uploadpack.allowAnySHA1InWant=true once so the SHA-targeted fetch is allowed.

Built-in sanity check — the script now exits non-zero if git rev-list --all HEAD --count inside .tmp/sources/<slug>/ returns anything other than 1. Re-runs cleanly on all six fixtures; adversarial probe of pr1-fix-clv confirms the leak surface is sealed:

Probe Before (worktree) After (standalone clone)
git log --all shows full branch + all PR heads shows only 2abf877 PR1
git show 297eb54 (sibling PR head) succeeds fatal: ambiguous argument '297eb54'
git rev-parse origin/pr/20 resolves fatal: ambiguous argument 'origin/pr/20'
git show 23b96ca (PR #20 intermediate) succeeds fatal: ambiguous argument '23b96ca'
git fetch pulls from origin no-op (no remote configured)

Cold-start build: 2:48 (was ~2:40). Idempotent: rebuild run 2:39. Auth pre-flight preserved. Orphan-worktree pruning removed (no worktrees anymore).

PR #20 intermediate-commit replay (23b96ca) still works — that path only ever wrote artifacts to <artifacts>/intermediate-commit-23b96ca/ via the shared cache (build-script scratch, not agent-readable); it never populated .tmp/sources/<slug>/.

Re: points #1 and #3 — being folded separately (#1 into the RUBRIC same-model contract by another teammate on PR #28; #3 will follow once we see how it lands).

Even Wei and others added 2 commits May 14, 2026 14:11
docs(evals): build six PR fixture set for /recce-verify v1 eval
Signed-off-by: even-wei <evenwei@infuseai.io>

# Conflicts:
#	.gitignore
@wcchang1115

Copy link
Copy Markdown
Contributor

Fixture / test-workflow integration check (HEAD: d60295d) — Verdict: GO

Targeted re-read at your request: does build_fixtures.sh produce what RUBRIC.md + the per-fixture templates expect? Yes. Verified the contract end-to-end by running the build locally and probing the produced artifacts. One pre-existing NOTE worth folding into the rubric before DRC-3405 starts; not blocking.

Build produces the contract RUBRIC.md describes

Ran ./build_fixtures.sh cold-start on this branch (d60295d). All six fixtures OK. For each fixture under evals/agent-blind-spots/fixtures/<slug>/:

Contract item (RUBRIC.md:53–59) Produced? How
diff.patch committed
artifacts/manifest-{before,after}.json build_at_sha, lines 168, scrubbed
artifacts/compiled-{before,after}/ build_at_sha, lines 171–175
artifacts/catalog-{before,after}.json build_at_sha, line 177
evals/agent-blind-spots/.tmp/sources/<slug>/ head-SHA tree build_fixture per-fixture git init + +<sha>:refs/fixture/head, line 234–248

Spot-checks:

  • metadata.user_id / metadata.invocation_id in manifest-after.json"redacted" (scrubber works).
  • compiled-after/jaffle_shop/models/... tree shape matches expectation (model SQL + schema.yml-derived test SQL).
  • pr44-promotion-flags/artifacts/intermediate-commit-23b96ca/ present, as required by that fixture's two-scenarios design.
  • All six per-fixture tier-0-baseline.md Inputs lines correctly resolve to ../../.tmp/sources/<actual-slug>/ after the build runs.

Tier-0 leak surface is structurally sealed

The hardest part of the contract (RUBRIC.md:59 — "do NOT read from .tmp/jaffle_shop_golden/ because the build loop leaves it at the last fixture's SHA") is enforced by the build, not by the agent's good behavior. The fresh-clone approach in build_fixtures.sh:240–248 (git init + single-SHA git fetch --depth 1 --no-tags, no git remote add) plus the runtime assertion at build_fixtures.sh:253–258 (git rev-list --all HEAD --count == 1) caught every fixture cleanly. Adversarial probe of pr1-fix-clv's .tmp/sources/pr1-fix-clv/:

Probe Result
git log --all --oneline 2abf877 PR1 (1 commit) ✓
git show 297eb54 (pr46 head) fatal: ambiguous argument '297eb54'
git show 23b96ca (pr44 intermediate) fatal: ambiguous argument '23b96ca'
git rev-parse origin/pr/20 fatal: ambiguous argument
git remote -v empty ✓

Worth calling out as a positive — the rev-list --all HEAD --count invariant in build_fixtures.sh:254 makes the contract structurally testable at build time. This is the kind of runtime assertion that makes the workflow trustworthy.

NOTE — Contract integrity gap on agent file-access

This is the one rough edge worth surfacing now rather than discovering during DRC-3405.

The Tier-0 runtime contract (RUBRIC.md:53–63) enumerates what inputs the agent gets, but it does not constrain what the agent can also see in the working tree. An eval runner who invokes Claude Code (or any agent with default workspace read access) from the repo root will give the agent read access to:

  • evals/agent-blind-spots/fixtures/<slug>/README.md — which contains an explicit ## Expected agent verdict — Tier 0 section ("Likely catch quality: partial", etc.). A diligent agent that greps for context will find these and bias toward them.
  • evals/agent-blind-spots/RUBRIC.md — which describes the rubric the agent is being scored against.
  • Sibling fixtures' files — for cross-comparison.

RUBRIC.md:70 acknowledges that file access varies ("Agent-specific scaffolding (file-access mode, tool whitelisting, system prompt) is allowed but must be recorded in the baseline's Notes section") — but it doesn't require the runner to sandbox the agent away from spoilers. A naive runner produces silently-contaminated baselines that look identical to clean ones.

Suggested addition to the Tier-0 runtime contract, near line 60 ("Generic tools allowed at Tier 0"):

Agent view restriction (eval-runner contract). The eval runner MUST restrict the agent's file-read access to: fixtures/<slug>/diff.patch, fixtures/<slug>/artifacts/, and .tmp/sources/<slug>/. The agent MUST NOT be able to read fixtures/<slug>/README.md (contains expected-verdict spoilers), RUBRIC.md, other fixtures' files, or anywhere else in this repository. How this is enforced is runner-specific (chroot, tool-whitelist, separate working directory, sandbox policy); record the mechanism in the baseline's Notes section.

The fix is a paragraph in RUBRIC.md, not a code change. Folding it before DRC-3405 starts costs less than retroactively quarantining a baseline run.

Verification summary

Check Result
Build runs cleanly cold-start ✓ (~3 min)
All six fixtures OK
Contract paths match build output
Tier-0 leak check (rev-list --all HEAD --count == 1) ✓ for all six
Adversarial probe against sibling SHAs from inside .tmp/sources/pr1-fix-clv/ ✓ all sealed
Manifest scrubbing applied
Per-fixture template Inputs paths resolve correctly ✓ all six

Recommendation

Ready to merge. The fixture build and rubric/templates are contract-compatible and the Tier-0 leak surface is structurally sealed. The agent-view restriction NOTE is a follow-up that should land before the first DRC-3405 run — either in this PR or a quick follow-up to RUBRIC.md.

@wcchang1115 wcchang1115 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixture build + rubric/templates contract verified end-to-end on d60295d — see follow-up comment. One non-blocking NOTE on agent-view restriction worth folding into RUBRIC before DRC-3405.

@even-wei
even-wei merged commit 6750c83 into main May 14, 2026
@even-wei
even-wei deleted the feature/drc-3403-rubric branch May 14, 2026 13:10
even-wei pushed a commit that referenced this pull request Jun 3, 2026
…3584, DRC-3430) (#36)

* feat(evals): Tier-0/Tier-1 sandbox profiles + Recce-aware strip (DRC-3584, DRC-3430)

Make the /recce-verify v1 eval's Tier-0 / Tier-1 split enforceable
rather than aspirational, so the lens-3 counterfactual delta (Tier-0
verdict → Tier-1 verdict) actually isolates Recce's contribution.

DRC-3584 — sandbox profile templates under runner-configs/:
- Claude Code: per-tier .claude/{settings.json, hooks/deny-tier-N.sh}.
  permissions.deny + a PreToolUse hook (belt-and-suspenders per open
  Claude Code issue #6699). Tier 0 denies Recce CLI, Recce MCP namespaces
  (mcp__recce__*, mcp__plugin_recce_*), /recce-* skills, dbt subcommands
  that regenerate frozen Tier-0 inputs, and direct SQL clients. Tier 1
  allows Recce CLI + MCP but keeps dbt regen and raw SQL clients denied.
- Codex: per-tier README + config.toml. Process sandbox flag
  (--sandbox=read-only / workspace-write) + an empty (Tier 0) or
  templated (Tier 1) mcp_servers table + a PATH scrub recipe to drop
  recce/dbt binaries.
- ENFORCEMENT.md: per-agent recipes, the agent-view-restriction folded
  from PR #28 follow-up, what's mechanically enforced vs. contract-only,
  and the recording-in-baseline requirement.
- RUBRIC.md: adds the agent-view-restriction paragraph pointing at
  ENFORCEMENT.md.
- templates/tier-0-baseline.md: adds the required "Sandbox profile
  used" block in Notes; missing block disqualifies the baseline.
- runs/2026-05-28/sandbox-verification.md: synthetic hook unit tests
  (Tier-0 6 cells, Tier-1 4 cells) all pass; one end-to-end claude
  --print × Tier-0 confirms the PreToolUse hook fires on a real agent
  attempt at `recce check`. CC × Tier-1, Codex × Tier-0, Codex × Tier-1
  cells deferred to the operator with recipes.

DRC-3430 (bundled because the DRC-3584 verify step is contaminated
without it) — build_fixtures.sh:
- Strip .github/prompts/, .github/workflows/recce-*.yml,
  .github/workflows/claude.yml, recce.yml from each per-fixture source
  tree right after the head-SHA checkout.
- Belt-and-suspenders post-strip grep for
  mcp__recce__|recce.yml|RECCE_API_TOKEN with --exclude-dir=.git.
- Initial run surfaced claude.yml (a "Claude Code + Recce MCP" reviewer
  workflow that wasn't on the original strip list); now stripped. All
  six fixtures rebuild cleanly with empty post-strip grep.
- fixtures/README.md documents the strip list and rationale.

gitignore exception added so the runner-configs/**/.claude/ templates
can be committed under the existing repo-wide .claude/ ignore.

Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): address PR #36 review — Python hooks + expanded strip (DRC-3584, DRC-3430)

Review found 6 BLOCKERs + 4 ISSUEs + 3 NOTEs against the v1 sandbox
profiles. Every blocker was an exit-0 from the case-glob hook on an
input the rubric said must be exit-2. This commit collapses them.

Hook rewrite — Python with shlex tokenization (B1+B2+B3+B4+B5+I10):
- New `deny-tier-{0,1}.py` (replacing `.sh`). Splits commands on shell
  metacharacters (`;`, `&`, `|`, `()`, newline) plus `$()` and backtick
  substitutions, shlex-parses each segment, basenames the executable
  (so `/usr/local/bin/recce` is caught), and recurses into `sh -c` /
  `bash -lc` / `zsh -c` arguments. Skill match is now case-insensitive.
  MCP namespace regex broadened to `^mcp__(plugin_)?recce(_|-|$)` so
  `mcp__recce_dev__*` and future variants are covered.
- Tier-0 flipped to a positive allowlist (git/grep/jq/file-read +
  POSIX text utilities). The legitimate Tier-0 surface is tiny per
  RUBRIC.md; allowlist is safer than chasing bypass shapes.
- Tier-1 stays a denylist but the matching is principled. dbt's
  banned subcommands are detected anywhere after `dbt`, so
  `dbt --debug parse` and `dbt --profiles-dir /tmp parse` are caught.
- `runs/2026-05-28/sandbox-verification.md` grew a Bypass attempts
  table with every reviewer shape; all rows pass exit-2 against v2.

ENFORCEMENT.md fix (B6):
- Corrected the false claim that the hook blocks
  `Bash(cat ../../<host-path>)`. The hook does not gate Read/Grep/Glob
  and `cat` is in the Tier-0 allowlist. Spoiler-path protection comes
  from cwd separation alone — the recipe's `cd "${FIXTURE_DIR}"` step
  is therefore not optional. Document says so now.
- Reframed the `permissions.deny` + hook relationship: the hook is the
  load-bearing layer, not a backup. `permissions.deny` is
  documentation that mirrors the documented surface, not the full
  bypass coverage.

Gitignore restructure (I8):
- Renamed `runner-configs/claude-code/tier-{0,1}/.claude/` →
  `claude-overlay/`. Per gitignore docs, you can't re-include a file
  under an excluded directory, so the v1 broad un-ignore would have
  let arbitrary files under `.claude/` slip through. Renaming
  sidesteps the `.claude/` ignore rule entirely; the eval runner now
  copies `claude-overlay/` → `.claude/` (one extra arg to `cp -r`).
  Gitignore exception removed.

Strip + leak-grep expansion (I9+N11):
- Strip list grew with `.devcontainer/`, `.github/mcp_config.json`,
  `.github/workflows/{recce_*.yml, recce_*.yaml, dbt_base.yml,
  dbt-build-pr.yml, dbt-build-base.yml}` — surfaced by the broadened
  leak grep on the first re-build.
- Leak grep broadened from `mcp__recce__|recce\.yml|RECCE_API_TOKEN`
  to case-insensitive `mcp__recce|recce\.yml|RECCE_API_TOKEN|recce`
  with `profiles.yml` whitelisted (`role: RECCE` is the Snowflake
  role name, not Recce-the-tool priming, and dbt needs the file).
- `printf '  %s\n' ${leak_hits}` (unquoted) replaced with
  `sed 's/^/  /' <<< "${leak_hits}"` — robust against whitespace in
  filenames.

Other doc fixes (N12, N13):
- Tier-1 hook header notes Recce MCP is intentionally not gated.
- `runner-configs/README.md` calls out that `permissions.deny` and
  the hook now intentionally cover different surfaces.
- Maintenance section updated for the v2 layout.

Build verified end-to-end after every change. The Bypass attempts
table in `runs/2026-05-28/sandbox-verification.md` covers all 16
hook unit tests plus 1 end-to-end claude --print cell — every row
green.

Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): close cycle-review bypasses (Tier-1 dbt flag-with-value + exec wrappers)

The pr-cycle reviewer found 2 new BLOCKERs in v2 that the prior review
missed. Both are static hook-logic bugs; v3 fixes them with two small
changes plus a regex tweak.

BLOCKER 1 — Tier-1 dbt-flag-with-value bypass (`deny-tier-1.py`):
  `has_denied_dbt_subcommand` skipped `-`-prefixed tokens but never
  consumed the value following a value-bearing flag, so the value
  became the "first positional" and the real subcommand was never
  inspected. Eight verified shapes:
    dbt --target dev parse        → was exit 0, now exit 2
    dbt --profiles-dir /tmp parse → was exit 0, now exit 2
    dbt --project-dir /x run      → was exit 0, now exit 2
    dbt --vars "x: 1" parse       → was exit 0, now exit 2
    dbt --log-format json parse   → was exit 0, now exit 2
    dbt --log-level debug parse   → was exit 0, now exit 2
    dbt --printer-width 80 parse  → was exit 0, now exit 2
    dbt --profile myprof parse    → was exit 0, now exit 2

  Fix: scan EVERY token after `dbt` for a banned subcommand, not just
  the "first positional". False positives require the agent to pass a
  literal banned-subcommand name as a flag value (e.g. `--target parse`),
  which is perverse and would deserve denial anyway since Tier-1
  warehouse access is supposed to be mediated through Recce MCP.

  Also expanded DBT_DENIED_SUBCOMMANDS with `run-operation`, `debug`,
  `source` — all hit the warehouse and were missing from the prior list.

BLOCKER 2 — exec-wrapper bypass (both hooks):
  `xargs recce` / `find -exec recce` / `time recce` / `nohup recce`
  spawned a denied binary as a child process; the hook only inspected
  the wrapper's head token and let the call through. Tier-0's positive
  allowlist let `xargs` and `find` pass; Tier-1's denylist saw a
  non-DENIED_BINS head and stopped looking.

  Fix: new EXEC_WRAPPERS set covers
    xargs, time, nice, nohup, setsid, parallel, exec, timeout, watch,
    ionice, chrt, stdbuf
  plus `find` (which needs special handling because its wrapped command
  lives after `-exec`/`-execdir`). For each wrapper at the head of a
  segment, walk the args for the first executable-shaped token and
  recurse-check from there. Heuristic `looks_like_executable` skips
  numeric positionals (timeout duration, chrt priority), flags, and
  placeholders (`{}`, `[]`).

Regex fix (split_segments):
  `(?<!\\)[;&|()\n]+` — negative lookbehind on backslash so escaped
  separators (`find -exec ... \;`) stay attached to the same segment.
  Without this, `find -exec dbt parse \;` would split at the `;` and
  shlex would error on the trailing backslash; the malformed-payload
  path would silently allow.

Verification:
  All v3 cells exit-2 in red-team testing. Bypass attempts table in
  `runs/2026-05-28/sandbox-verification.md` grew from 16 to 38 rows
  covering every reviewer-named bypass (B1-B6, I7, cycle B1, cycle B2)
  plus counter-claim regressions (Recce/MCP/dbt-list allowed at T1,
  benign xargs/find at T0). Build re-verified across all 6 fixtures —
  the broader leak grep + expanded strip still pass clean.

Closes the cycle review's BLOCKER 1 and BLOCKER 2. Addresses ISSUE 3
(per-tier disambiguation now in the table) and ISSUE 4 (collapsed by
BLOCKER 1's fix).

Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): close iter-2 bypasses (eval / \$() smuggling / dbt clone+retry)

The pr-cycle iter-2 review surfaced three more bypass classes at
Tier-1 — all real, none caught by v3. v4 closes them.

1. `eval` shell-builtin smuggling
   `eval dbt run` / `eval "dbt run"` were exit 0 because `eval` was
   not in SHELL_WRAPPERS. Fix: added `eval` to Tier-0's SHELL_WRAPPERS
   (banned outright since the positive allowlist has no legit reason
   for a shell) and added a dedicated `eval ARGS...` branch in
   Tier-1's check_tokens that joins the args and recurses into the
   resulting command — same threat surface as `sh -c`, different
   syntactic shape.

2. `$()` / backtick substitution at command-head position
   `$(echo dbt) run`, `` `echo dbt` run ``, `$(printf %s dbt) parse`
   evaluate to `dbt run` / `dbt parse` at bash runtime; the
   substitution provides the head binary, which the static
   check_tokens path can't see because the substitution's content
   has been stripped from the outer segment. Fix: new
   `check_substitution_at_head` in Tier-1 pre-walks each segment for
   `$(...)` / backtick at head and, if the substitution payload
   contains a denied binary name, denies the outer command. For dbt,
   it also combines payload tail + outer args to decide whether a
   denied subcommand is present (so `$(echo dbt)` alone is still
   allowed — bare dbt is discovery-only).

   Segmentation for this pre-check uses `(?<!\\)[;&|\n]+` (no
   parens) so `$(...)` stays intact. The main split_segments still
   includes parens because it pre-extracts substitutions before
   splitting.

3. Missing dbt subcommands `clone` and `retry`
   `dbt clone` (dbt-core ≥1.6) materialises cloned models in the
   warehouse. `dbt retry` re-executes the prior failed command, so
   it inherits any deny semantics of that prior command. Both added
   to DBT_DENIED_SUBCOMMANDS.

False-positive sanity (all exit 0):
  git log --grep=\$(echo psql)      — $() at non-head position
  echo \$(date)                     — benign substitution
  \$(which python) script.py        — non-denied binary
  \$(echo grep) -rn foo .           — non-denied binary
  \$(echo dbt)                      — dbt alone (allowed)
  echo \$(echo dbt) is good         — dbt in non-head $()

All v3 / v2 regressions still pass. Bypass attempts table in
`runs/2026-05-28/sandbox-verification.md` grew with the iter-2 cells
plus the new false-positive counter-claims. Build re-verified across
all 6 fixtures.

Signed-off-by: even-wei <evenwei@infuseai.io>

* refactor(evals): hooks → bashlex AST parser (DRC-3584 v5, closes iter-3 bypasses)

The pr-cycle iter-3 review surfaced 9 more bypass classes that the
shlex+regex hooks couldn't model — all real, all rooted in Bash
semantics shlex doesn't represent.

Iter-3 BLOCKERs:
  1. Nested $() — `$(sh -c "$(echo dbt) run")`, `$($(echo dbt) run)`,
     `` `$(echo dbt) run` `` (single-level regex misses nesting)
  2. `eval $(...)` — substitution stripped before eval branch ran
  3. `xargs $(echo dbt)` — wrapper-at-head + substitution-as-arg;
     find_wrapped_command saw no executable-shaped token
  4. `dbt $(echo run)` — substitution supplied the subcommand;
     has_denied_dbt_subcommand only scanned outer literal tokens
  5. Bash ANSI-C $'dbt' / parameter expansion ${a:-dbt} — shlex
     passed through as opaque basenames that didn't equal 'dbt'
  6. `coproc dbt run` — Bash keyword not in any wrapper set
  7. `command dbt run` / `builtin eval dbt run` — Bash command
     modifiers not handled
  8. `! dbt run` — Bash logical-not prefix not matched
  9. `xargs -I {} sh -c "{} parse" dbt` — xargs substitutes dbt into
     {} at runtime; hook saw only the literal {}

Rather than patch each class with another regex, this commit
**replaces the regex/shlex approach with `bashlex` (a real Bash AST
parser)**. Substitutions are walked as a tree; ANSI-C / parameter
expansion are extracted from their AST nodes; Bash keywords (!,
command, builtin) are handled as transparent prefixes; `coproc` is
denied outright (bashlex can't represent it).

New dependency: `bashlex` (pure Python, MIT, well-maintained). Both
hooks fail closed if bashlex isn't installed — better to deny a
legit call than silently allow a bypass. ENFORCEMENT.md + runner-
configs README document the install step (`python3 -m pip install
bashlex`).

Architecture:
  Two passes per command (in walk()):
    1. WALK INTO every $()/backtick substitution as if it were a
       top-level command. Catches `$(sh -c "dbt run")` because the
       inner sh -c "dbt run" is itself denied.
    2. RESOLVE the head word of each command to candidate output
       strings (literal text, ANSI-C decoded via raw-position lookup,
       parameter defaults, substitution payload words). At Tier 0,
       ALL candidates must be in the allowlist — `$(echo dbt) run`
       resolves to ['echo','dbt'] and 'dbt' isn't allowlisted, so
       deny. At Tier 1, any candidate matching a denied binary
       (with denied subcommand visible anywhere in args, including
       substitution payloads) denies.

  Exec wrappers (xargs/find -exec/time/nohup/etc.) now scan EVERY
  wrapped-command word's candidates for a denied basename — exec
  wrappers supply args from stdin/find-output, so "bare dbt is
  allowed" reasoning doesn't apply under a wrapper.

  Shell wrappers (sh -c, bash -lc, ...) reparse the LITERAL word
  text of the -c arg (not its substitution candidates) so the
  inner command structure stays intact. Catches nested $(sh -c
  "$(echo dbt) run").

Verification: 51 cells across both tiers. Every iter-1/iter-2/iter-3
BLOCKER closed. False-positive sanity for benign $() at non-head,
$(echo grep), $(which python), echo $(date), command grep, find
without -exec, xargs grep, etc. — all allow correctly.

Build re-verified across all 6 fixtures.

Closes the iter-3 9 BLOCKER classes. The reviewer's recommended
follow-up (pytest-parameterized over the bypass matrix) is filed as
an out-of-PR improvement.

Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): close iter-4 BLOCKERs (process sub / CompoundNode / brace / xargs-chain / \$0)

Iter-4 cycle review found 7 BLOCKERs + 5 ISSUEs against v5's bashlex
hooks — all rooted in AST-walker gaps inside the bashlex code (not
in bashlex itself). v6 closes them.

BLOCKERs:

1+7. **Process substitution `<(...)` / `>(...)`** — `_walk_substitutions`
   only handled `commandsubstitution` kind. Now also handles
   `processsubstitution` (same `.command` attribute). Catches
   `diff <(dbt run) /etc/hosts` (head-position) AND
   `echo x 2> >(dbt parse)` (process sub inside a RedirectNode —
   `walk()` now also iterates `kind == 'redirect'` parts and walks
   their `.output` word's substitutions).

2. **CompoundNode.list** — bashlex's subshell `(...)` and group
   `{ ...; }` store children in `.list`, not `.parts`. The walker's
   descent path now tries `.list` first then `.parts`, so
   `(dbt parse)` and `{ dbt parse; }` are no longer silently
   allowed.

3+4. **Brace expansion** — bashlex returns `{dbt,bash}` and
   `{parse,run}` as literal `word` text with no `parts`. New
   `_expand_brace_literal()` regex decodes `prefix{a,b,c}suffix`
   into candidate list (`['prefix.a.suffix', 'prefix.b.suffix',
   ...]`). Used by `resolve_word` for both head and args.

   Multi-candidate head fix: `{dbt,bash} run` resolves to
   `{'dbt','bash'}`. Before v6, the shell-wrapper branch fired on
   `bash` and returned without checking the dbt arm. Now an
   eager-deny up front checks dbt+args and direct DENIED_BINS
   BEFORE the shell-wrapper / eval / exec-wrapper branches —
   any candidate matching a denied shape denies regardless of
   what other candidates are present.

5. **xargs → sh -c chain** — `echo dbt | xargs -I {} sh -c "{} parse"`
   exec-wrapper found `sh` as wrapped, but `sh` isn't in DENIED_BINS
   so the wrapper-scan exited without escalating. v6 adds: if the
   wrapped binary IS a shell wrapper, find the `-c` arg; if the
   xargs `-I` placeholder appears inside the shell body,
   conservatively deny (placeholder substitution from stdin is
   dynamic — can't be statically resolved, and the pattern has very
   narrow legit use). Otherwise re-parse the literal -c text.

6. **`$0` head** — bashlex parses `$0` as ParameterNode(value='0').
   `resolve_word`'s parameter branch only handled `:-`/`:=` defaults.
   Now also handles `0`, `BASH`, `SHELL`, `BASH_SOURCE` — these
   resolve at runtime to a shell name; we surface them as the
   candidate `"sh"` so the shell-wrapper branch fires conservatively.

ISSUEs:

8. **coproc regex inside string literals** — pre-parse `\bcoproc\b`
   match was firing on `echo coproc`, `echo 'no coproc here'`, etc.
   Removed the pre-parse check; rely on bashlex's NotImplementedError
   raised when the actual `coproc` keyword is present. Error message
   is inspected for `'coproc'` to differentiate from other
   unsupported constructs.

9. **NotImplementedError fail-closed contradicted docstring** — bashlex
   raises NotImplementedError on legit `$((arith))`, `select`, etc.
   v5 denied all such cases. v6 only denies if the error message
   mentions `coproc`; other unsupported constructs fall through
   (the rest of the sandbox catches denied binaries).

10. **`_looks_like_executable`** — added `[[`, `]]`, `;;`, `&&`, `||`
    to the skip set.

11. **ANSI-C decoder** — switched from `re.sub(r"\\(.)", r"\1", inner)`
    to `codecs.decode(inner, 'unicode_escape')` for proper bash
    escape semantics. Falls back to the naive regex on decode error.

12. **Reparse fallback** — already scans every token, no change
    needed.

Verification: 49 cells across both tiers + iter-1/2/3/4 regressions
+ false-positive sanity. All green. Build re-verified across all 6
fixtures.

Closes the iter-4 cycle's 7 BLOCKERs and 5 ISSUEs.

Signed-off-by: even-wei <evenwei@infuseai.io>

* docs(evals): add non-adversarial threat-model section to ENFORCEMENT.md

Document the v1 enforcement contract explicitly: hooks are calibrated
for a non-adversarial code agent (RUBRIC.md's stated subject), not a
red-teamer actively evading the sandbox. Catalogue the seven classes
of static-Bash-analysis-unreachable bypass (bashlex NIE, heredoc-body,
pipeline string flow, stdin-supplied subcommands, trap evaluation,
interpreter shell-out, variable-flow loops/assignments) as accepted
out-of-scope for v1, with rationale and the follow-up trigger
(non-adversarial agent reaching one in practice).

Reframes the iter-3/4/5 cycle-reviewer BLOCKER inflation as adversarial
review against a non-adversarial artifact; closes that loop on the
documentation layer rather than the code layer.

Signed-off-by: even-wei <evenwei@infuseai.io>

* feat(recce): opt-in PostHog telemetry scaffolding for /recce-verify (DRC-3597) (#38)

Foundation for the L3 funnel signal proposed in the 2026-05-29 project
rethink. Tells the project whether real-world agent users reach for
/recce-verify, complete it, and convert downstream -- the production
complement to the L1 offline eval (DRC-3405) and L2 in-driver trace
metrics (DRC-3586).

What ships:

- plugins/recce/hooks/scripts/telemetry.sh
  Event emitter. Fires recce_verify.* events to PostHog via curl
  fire-and-forget. Off by default; opt in via RECCE_TELEMETRY_OPT_IN=1
  or ~/.recce/config.yml `telemetry_opt_in: true`. Anonymous stable
  installation ID at ~/.recce/installation-id (UUID4). Failure modes
  are silent and non-blocking by construction.

- plugins/recce/hooks/scripts/test-telemetry.sh
  Audit script that exercises every opt-in / opt-out short-circuit
  without firing network traffic. Currently 4/4 pass on default-off,
  opt-in + no key, opt-in + DISABLED bypass, and missing-event-name
  edge case.

- plugins/recce/hooks/scripts/README-telemetry.md
  Documents the event schema (skill_invoked, tier_degraded, tool_call,
  verdict_emitted, session_completed), opt-in mechanism, per-event
  wiring recommendations, plugin-maintainer responsibilities (PostHog
  key), and audit script usage.

What's NOT in this PR (deferred to follow-up):

- Auto-wiring in hooks.json (which PostToolUse / Stop hooks fire which
  events; needs design decision on session-scoping)
- Inline emit calls in plugins/recce/skills/recce-verify/SKILL.md
- The actual PostHog project key (plugin maintainer fills in at release)
- Recce Cloud signup join key (coordinate with Andy when Cloud picks up
  the parameter)

This is independent of the eval chain (DRC-3585 / 3586 / 3587 / 3405)
and can land on its own without disturbing the in-flight eval work.

Signed-off-by: even-wei <evenwei@infuseai.io>

* feat(evals): Karpathy spike driver for /recce-verify v1 eval (DRC-3586) (#37)

* feat(evals): Karpathy spike driver for /recce-verify v1 eval (DRC-3586)

Single-file Python driver at evals/agent-blind-spots/spike-driver/. Dispatches
up to 6 fixtures x 2 agents x 2 tiers = 24 cells, captures each agent's
transcript, runs a Claude-as-judge pass per transcript to produce a three-axis
verdict (catch / tier / delta), and writes a CSV + Markdown summary under
runs/<date>/spike-driver/.

Stability checks (both optional, both produce judge-quality signal):
- --judge-stability: double-judges each transcript, reports per-axis
  self-consistency (catch / tier / delta). Floor 80%.
- --baseline-dir <path>: compares judge verdicts against DRC-3585 manual
  baseline once it lands (judge-vs-human catch agreement on Tier-0 cells).

Graceful degradation:
- Skips an agent's cells if its CLI is not on PATH; codex commonly absent
  in lighter dev envs (recipe in runner-configs/codex/tier-{0,1}/README.md).
- --no-run mode re-judges existing transcripts without re-running agents.

Sandbox profile integration (from DRC-3584):
- Claude Code cells stamp tier-N claude-overlay/ into the fixture worktree
  before invoking `claude --print`, with a neutered CLAUDE_CONFIG_DIR per
  cell and scrubbed warehouse env.
- Codex cells use `codex exec --sandbox=<read-only|workspace-write>
  --ask-for-approval=never --config <tier-N/config.toml>` plus PATH scrub.

Non-goals for this spike (deferred):
- Durable harness with resume / parallel dispatch -> DRC-3587 Inspect AI port.
- Gap-report generator across 6 fixtures -> DRC-3405.
- Auto-iteration on prompt / skill changes (closed-loop optimization overfits
  at N=6 -- explicit non-goal of the spike).

Stacks on PR #36 (DRC-3584 + DRC-3430). When #36 merges to main, this PR
should be rebased onto main; GitHub auto-retargets the base branch.

Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): stage frozen Tier-0 inputs into cwd before agent run

The agent's cwd is .tmp/sources/<id>/ (the per-fixture standalone repo),
but the frozen Tier-0 inputs (diff.patch, artifacts/{manifest, compiled,
catalog}*) live at fixtures/<id>/, outside cwd. Without staging:

- Claude Code reaches them via absolute paths (Read tool isn't cwd-anchored),
  but the prompt doesn't tell the agent where to look.
- Codex Tier-0 read-only sandbox blocks reads outside cwd entirely, so the
  agent literally cannot reach the artifacts.

Driver now symlinks fixtures/<id>/{diff.patch, artifacts} into a
_eval_inputs/ subdir under cwd before each cell. Prompt updated to point
at _eval_inputs/. Works under both agent sandboxes.

Verified locally:
  >>> stage_inputs(SOURCES_DIR / 'pr1-fix-clv', 'pr1-fix-clv')
  >>> sorted(p.name for p in (SOURCES_DIR / 'pr1-fix-clv' / '_eval_inputs').iterdir())
  ['artifacts', 'diff.patch']

Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): inherit CLAUDE_CONFIG_DIR for auth in unattended runs

ENFORCEMENT.md recipe step 3 sets CLAUDE_CONFIG_DIR to a fresh mktemp dir
to neuter user-level ~/.claude/settings.json. That step also strips the
Claude Code auth state, so a child `claude --print` invoked by the spike
driver fails with "Not logged in" and no transcript ever lands.

For unattended runs, the load-bearing enforcement is the project-level
.claude/settings.json overlay (stamped per cell) plus the PreToolUse hook
(deny-tier-N.py); a user-level `permissions.allow` cannot bypass an
exit-2 hook. Skip the CLAUDE_CONFIG_DIR override by default.

For paranoid mode (e.g. when running the eval on a machine with risky
~/.claude/ contents), set RECCE_EVAL_STRICT_CONFIG=1 to enable the
override. The operator is responsible for preseeding auth under the
per-cell _claude_cfg dir.

Verified end-to-end smoke run after this fix:
  uv run driver.py --smoke --agents claude --tiers 0
    [run]   pr1-fix-clv · claude · tier-0
    [judge] pr1-fix-clv · claude · tier-0
  Agent VERDICT: catch · request-changes
  Judge verdict: {catch, tier-0, same}  (delta=same expected with no T1 paired)

Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): address pr-cycle review of #37 — 3 ISSUEs

1. Drop dead VERDICT_TAIL_RE regex. Defined in driver.py:99 but never
   used; agent verdict parsing is fully handled by judge_cell()
   downstream. Removed alongside the regex line.

2. Add surgical .gitignore entries for spike-driver runtime outputs:
     evals/agent-blind-spots/runs/*/spike-driver/transcripts/
     evals/agent-blind-spots/runs/*/spike-driver/_claude_cfg/
   These are the genuinely-transient subdirs. verdicts.csv,
   summary.md, cells.json under runs/<date>/spike-driver/ are
   intentionally NOT ignored — they may be committed manually as
   run baselines for cross-iteration tracking.

3. stage_inputs() now raises FileNotFoundError when no Tier-0
   inputs (neither diff.patch nor artifacts/) could be staged under
   the fixture's source dir. run_cell() catches and records into
   cell.error so the cell shows up as failed in the matrix instead
   of silently feeding the agent an empty _eval_inputs/ and letting
   it review with no inputs.

Verified:
  python3 -m py_compile spike-driver/driver.py  -> ok
  stage_inputs(.../pr1-fix-clv, 'pr1-fix-clv')  -> happy path
  stage_inputs(/tmp/dne, 'does-not-exist')      -> raises FileNotFoundError

NOTE-level findings 4-7 from the review are accepted as-is per the
spike framing (judge inherits parent env intentionally for auth;
same-day run dirs are an operator concern; broken-symlink in
overlay copy is rare and surfaced via run_cell's general except;
no tests is consistent with the Karpathy-spike scope and the
DRC-3587 follow-up which adds Inspect AI test harness).

Signed-off-by: even-wei <evenwei@infuseai.io>

---------

Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): close iter-6 BLOCKER (git history leak) + ISSUE (Tier-1 dbt policy)

Two findings from the pr-cycle iter-6 review of #36, both re-confirming
wcchang1115 (Andy) review comments that remained open.

## BLOCKER: per-fixture git history leaks stripped content

build_fixtures.sh fetched the upstream head SHA into refs/fixture/head
and `rm -rf`'d Recce-aware paths from the working tree. The original
commit (and its tree) was untouched, so a Tier-0 agent allowlisted to
run `git` could recover everything via `git show HEAD:recce.yml`,
`git cat-file -p HEAD^{tree}`, `git log -p`, etc. The working-tree
strip is bypassable by anyone reading git objects.

Fix: after strip + leak-grep, rewrite the per-fixture repo's history
into a single fresh commit whose tree IS the stripped working tree.
Delete .git, `git init`, `git add -A`, `git commit`. No ancestor
references the original head's tree; no other ref / reflog entry
keeps it reachable.

Verified end-to-end on a copy of pr1-fix-clv's source:

  Before:
    git show HEAD:recce.yml -> dumps recce.yml content (leak)
    git ls-tree -r HEAD | grep recce.yml -> recce.yml is listed

  After rewrite:
    git cat-file -e HEAD:recce.yml -> exit 128 (unreachable)
    git cat-file -e HEAD:.github/prompts/system-prompt.md -> unreachable
    git cat-file -e HEAD:.github/workflows/claude.yml -> unreachable
    git cat-file -e HEAD:.github/workflows/recce_ci.yml -> unreachable
    git cat-file -e HEAD:.devcontainer/post-create.sh -> unreachable
    rev-list --all HEAD --count -> 1 (single commit invariant preserved)

Post-rewrite path-leak regex tightened to anchor at path components
(`(^|/)recce\.yml$` etc.) so the generic .devcontainer.json VS Code
config (no Recce content) isn't a false positive.

## ISSUE: Tier-1 dbt subcommand policy divergence

Codex Tier-1 PATH-scrubs the `dbt` binary entirely (no dbt at all);
Claude Code Tier-1 hook denylisted only {run,test,parse,compile,...}
and allowed `dbt list/show/ls/deps/--help/--version`. The lens-3
cross-runner delta would partially measure "Claude Code can call
read-only dbt subcommands but Codex can't" instead of the
Recce-equipped-vs-not-equipped signal.

Fix: at Tier-1, deny ALL dbt invocations regardless of subcommand.
The recce-verify SKILL.md uses `git diff --name-only` + Recce MCP
for model discovery, not `dbt list` — so denying read-only dbt
subcommands costs nothing in capability. The DENIED_BINS check at
line 340 still has the `- {"dbt"}` exclusion so the dbt branch
gives its own specific message; the message is updated to reflect
the broader policy ("not reachable at Tier-1").

Synthetic hook tests (Tier-1):
  dbt list       -> BLOCKED ✓
  dbt show       -> BLOCKED ✓
  dbt --version  -> BLOCKED ✓
  dbt run        -> BLOCKED ✓ (already was)
  dbt parse      -> BLOCKED ✓ (already was)
  git status     -> allowed ✓
  recce list     -> allowed ✓

Signed-off-by: even-wei <evenwei@infuseai.io>

* chore(evals): address iter-7 NOTEs — remove dead Tier-1 dbt code + doc drift

Both findings from pr-cycle iter-7 review of #36 (explicit non-blockers,
author already acknowledged the doc-drift one inline).

NOTE 1: dead code in deny-tier-1.py after the iter-6 ISSUE fix.
The unconditional `if "dbt" in head_names: deny(...)` at line ~340
makes the entire subcommand-discrimination path unreachable. Removed:
  - DBT_DENIED_SUBCOMMANDS frozenset (was lines 64-68)
  - _dbt_args_have_denied() function (was lines 519-527)
  - The `# --- dbt ---` block in walk() (was lines 458-465)

deny-tier-0.py still has its own DBT_DENIED_SUBCOMMANDS — Tier-0
denies the same set + bare `dbt`. The two hook files are kept
separate intentionally; cross-file dedup would require restructuring
the per-tier overlay layout. Comment at line 65-68 documents this.

NOTE 2: doc drift at fixtures/README.md:32 — the prior paragraph
mentioned only the simple `grep -E '...'` belt-and-suspenders and
made no reference to the history rewrite. Updated to describe:
  - Both leak-grep layers (tight identifier + loose `[Rr]ecce`)
  - profiles.yml whitelist rationale
  - The git history rewrite step + post-rewrite path-leak check
  - Why the rewrite matters (Tier-0-allowlisted `git` recoverability)

Verified: py_compile ok, no functional orphan refs (only one comment
mentions the deleted DBT_DENIED_SUBCOMMANDS name as cross-reference
to tier-0). 10/10 synthetic hook tests still pass:
  - dbt list/show/run/parse/--version/deps -> all BLOCKED
  - git status, recce list -> allowed
  - sh -c "dbt parse" -> BLOCKED (shell wrapper)
  - {dbt,bash} run -> BLOCKED (brace expansion)

Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): close iter-8 BLOCKERs + ISSUEs on spike driver + overlays

Address @wcchang1115's re-review at ff46b98 (PR #36).

BLOCKER 1 — overlay-as-spoiler at the agent's cwd. The Tier-0 enforcement
overlay copied to `<fixture>/.claude/` contains Recce-shaped strings
(`mcp__recce__*`, `recce`, `RECCE_API_TOKEN`); `cat`/`ls`/`grep` over
`.claude/` would let a Tier-0 agent read its own enforcement file and
defeat the "agent cannot reach Recce-shaped signals" contract.

  * Tier-0 + Tier-1 `settings.json`: deny `Read/Glob/Grep(.claude/**)`.
    (Project-level deny rules apply even with
    `--dangerously-skip-permissions`; that flag bypasses the interactive
    prompt, not the rule.)
  * Tier-0 hook (`deny-tier-0.py`): new Pass 0 over command words and
    redirect targets. Any argument matching `(^|[/=])\.claude(/|$)` exits
    2 with a "references the Tier-0 enforcement overlay" message.
    Covers `cat .claude/...`, `cat ./.claude/...`, `cat < .claude/...`,
    `ls .claude/`, `find .claude`, `grep -r recce .claude/`, and
    `cat $(echo .claude/...)` — i.e., the non-adversarial shapes.
    Interpreter-shell-out (awk/python `-c`) is intentionally out of
    scope per ENFORCEMENT.md § "Threat model — non-adversarial".

BLOCKER 2 — Codex Tier-0 PATH-regex scrub was a no-op for normal installs.
`grep -v -E '/recce(/|$)|/dbt(/|$)|\.recce'` strips dirs whose path
contains literal `/recce/` or `/dbt/` — NOT dirs containing the binary
named `recce`/`dbt`. So `/opt/homebrew/bin`, `.venv/bin`, `~/.local/bin`
all survived and Codex Tier-0 could still run `recce list` / `dbt list`.

  * New `runner-configs/codex/tier-0/stub-bin/{recce,dbt}` — exit-127
    stubs prepended to PATH so the real binaries are masked regardless
    of which bin dir they live in.
  * `driver.py::codex_tier0_path()` builds the PATH (stub-bin first,
    then the legacy regex scrub as belt-and-suspenders).
  * `driver.py::assert_codex_tier0_masked()` runs `command -v recce|dbt`
    under the final env before the agent launches; if either resolves
    outside `stub-bin/`, the cell errors out instead of contaminating
    the Tier-0 baseline. Verified: regression detected when stub-bin is
    skipped.
  * `codex/tier-0/README.md` updated with the new recipe + pre-flight
    assertion shell snippet.

ISSUE 1 — Tier-0 read/write asymmetry. Codex Tier-0 is OS read-only;
Claude Code Tier-0 had no Write/Edit deny + matcher didn't fire on those
tool kinds + `--dangerously-skip-permissions` was set.

  * Tier-0 `settings.json`: deny `Write`, `Edit`, `MultiEdit`,
    `NotebookEdit`. Deny rules are enforced separately from
    `--dangerously-skip-permissions`.

ISSUE 2 — cells don't reset state between runs.
`fixture_dir = SOURCES_DIR / cell.fixture` was reused across all four
cells per fixture with no `git reset`, so a write from cell N persisted
into cell N+1.

  * New `driver.py::reset_fixture_dir()` runs `git reset --hard HEAD`
    + `git clean -fdx` at the top of `run_cell()`, before
    `stage_inputs()` (whose `_eval_inputs/` symlinks would otherwise be
    cleaned away) and before the overlay copy. Each cell now starts
    from the same committed base.

ISSUE 3 — no model pinning (ENFORCEMENT.md:157).

  * `driver.py`: new `--model` flag (default `claude-opus-4-5`),
    `--no-model` opt-out. The pin is passed to `claude --model` and
    `codex --model` for every agent invocation and to the judge call.
    Cell metadata (`cells.json`, transcript headers) records the
    pinned model so cross-cell comparability is auditable.

Deferred (Andy's Notes, non-blocking for this PR):
  - N1: tier-1 dbt deny-list / docs drift.
  - N2: `_eval_inputs/` symlinks could be written through at Tier-1.
  - N3: `codex/tier-1/config.toml` `command = ""` unvalidated.

Verification:
  - `py_compile` clean on deny-tier-{0,1}.py + driver.py.
  - JSON valid on both `settings.json` files.
  - Synthetic Bash-hook tests: 12/12 `.claude/` access shapes denied;
    8/8 allowlisted commands pass.
  - `codex_tier0_path()` + `assert_codex_tier0_masked()` pre-flight
    works under the new stub-bin recipe and catches regressions if
    stub-bin is dropped.
  - `driver.py --help` shows `--model` / `--no-model`.
Signed-off-by: even-wei <evenwei@infuseai.io>

* fix(evals): close iter-8 BLOCKER (Tier-0 overlay leak via cwd) structurally

Andy's iter-8 review (PR #36, on 80d5ea3) flagged that the Pass 0
overlay-leak guard in deny-tier-0.py only matches the literal `.claude`
substring, so a non-adversarial Tier-0 agent dumps the overlay's Recce
vocabulary via ordinary recursive reads the Bash AST hook can't see
ahead of glob expansion:

    cat .*/settings.json      (glob expands AFTER the hook reads literal)
    grep -r recce .           (recurse descends into .claude/)
    find . -type f -exec cat {} \;
    cat $(find . -type f)

Token-matching is whack-a-mole; the root cause is structural — the
overlay (mcp__recce__*, Bash(recce *), RECCE_API_TOKEN, recce literals
in the hook source) lives in the agent's cwd as <fixture>/.claude/.

Andy's recommended fix (verified on CC v2.1.160): load settings via
`claude --settings <abs-path>` from outside cwd, with the hook command
pointing to an absolute path also outside cwd. The agent's cwd then
becomes a pristine dbt project with no Recce-shaped files — leak is
structurally impossible.

Driver change (spike-driver/driver.py):
* Add render_claude_settings() — reads the template settings.json,
  substitutes ${RUNNER_HOOK_PATH} with the absolute path to the
  tier-N hook script (under runner-configs/), writes the rendered
  file to run_dir/_settings/<fixture>_t<tier>.json.
* run_claude() drops shutil.copytree(overlay_src, fixture/.claude),
  calls render_claude_settings(), and passes
  --settings <rendered-abs-path> to `claude --print`.
* Codex path (run_codex, assert_codex_tier0_masked) unchanged —
  Codex uses --config <toml> + PATH stub-bin masking, separate
  enforcement shape.

Template change (tier-{0,1}/claude-overlay/settings.json):
* `hooks.PreToolUse[].hooks[].command` now uses ${RUNNER_HOOK_PATH}
  placeholder; the runner substitutes at render time.
* Hook matcher unchanged: Bash|Skill|mcp__(plugin_)?recce(_|-).*
* permissions.deny rules unchanged (belt-and-suspenders, even though
  the overlay is no longer in cwd).

Doc updates:
* ENFORCEMENT.md Claude Code recipe: bash example now renders the
  template to a temp path and passes `claude --settings <abs-path>`
  instead of `cp -r claude-overlay/ .claude/`. Same shape for Tier-1.
* runner-configs/README.md: directory map and Quick start updated;
  added the "why not copy" explainer pointing at the leak.

Verification:
* py_compile passes for driver.py, deny-tier-{0,1}.py.
* JSON parse passes for tier-{0,1} settings templates.
* Filesystem check: after reset_fixture_dir() + stage_inputs(),
  find <fixture> -maxdepth 2 -name "*.claude*" returns empty.
  Fixture cwd contains dbt project files + _eval_inputs/ symlink
  only.
* Rendered settings live at run_dir/_settings/, outside fixture cwd.
* Rendered hook command field carries an absolute path that resolves
  to an existing deny-tier-N.py with no ${RUNNER_HOOK_PATH} leakage.
* Andy's 4 bypass shapes (cat .*/settings.json, grep -r Bash(recce .,
  find . -type f -exec grep -l deny-tier {} \;,
  cat $(find . -name deny-tier-0.py)) all return empty / no-such-file
  — overlay simply isn't there.
* Standalone hook invocation (Bash dbt parse → exit 2; Bash ls → exit 0)
  still works when the hook script is executed from outside cwd.
* Codex Tier-0 PATH masking (assert_codex_tier0_masked) is untouched
  and still aborts when recce/dbt resolve outside stub-bin.

Out of scope (per the iter-8 review):
* Codex --ignore-rules / --dangerously-bypass-hook-trust (codex-only,
  separate code path).
* B1 (git history leak) and I2 (Tier-1 dbt policy) — already closed
  in 6f20289.
* 7 adversarial-bypass classes documented in ENFORCEMENT.md §
  "Threat model — non-adversarial code agent".
* The 3 NOTEs Andy explicitly deferred.

Refs: DRC-3584
Signed-off-by: even-wei <evenwei@infuseai.io>

---------

Signed-off-by: even-wei <evenwei@infuseai.io>
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.

2 participants