docs(evals): scoring rubric for /recce-verify v1 - #28
Conversation
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>
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>
Update (HEAD:
|
| # | Finding | Status |
|---|---|---|
| ISSUE 1 | Template-vs-rubric drift on Tier-0 inputs | Resolved — templates/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 |
Resolved — RUBRIC.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 |
Resolved — RUBRIC.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 | Resolved — templates/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 open — RUBRIC.md:41 unchanged. Non-blocking; can soften in a follow-up. |
| NOTE 4 | Lens 2 singular-vs-plural inconsistency | Still open — RUBRIC.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:55writes the per-fixture source path asevals/agent-blind-spots/.tmp/sources/<id>/(repo-root-relative), whileRUBRIC.md:57andtemplates/tier-0-baseline.md:16write.tmp/sources/<id>/(relative to the eval directory). Both resolve to the same place given the build script'sSCRIPT_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.shand the on-disk directories use${slug}(pr1-fix-clv, etc.). The PR docs(evals): build six PR fixture set for /recce-verify v1 eval #29fixtures/README.mdpresumably reconciles these, but a one-line glossary line inRUBRIC.md("<id>= fixture slug, e.g.pr1-fix-clv") would close the loop. Non-blocking. - NOTE C — Intermediate-commit artifacts unmentioned.
build_fixtures.shproduces an extraartifacts/intermediate-commit-<sha>/directory forpr44-promotion-flags, and that fixture also ships adiff-from-base-to-intermediate.patch.RUBRIC.md:49–55enumerates 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 --statconfirms 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.shfrom PR docs(evals): build six PR fixture set for /recce-verify v1 eval #29767ca540directly; confirmedgit worktree add --detach "${SOURCES_DIR}/${slug}" "${head_sha}"lines exist and run insidebuild_fixture()after each pair ofbuild_at_shacalls. - 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 #28 — docs(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
-
Template-vs-rubric drift on Tier-0 inputs —
evals/agent-blind-spots/templates/tier-0-baseline.md:16says 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 commitca837edwas 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 referenceRUBRIC.md § Tier-0 agent runtime contractas the authoritative input list. -
Working-tree contract not met by
build_fixtures.sh—evals/agent-blind-spots/RUBRIC.md:55promises "read access to the dbt project source at the head SHA (the.tmp/jaffle_shop_golden/checkout left by the build script)." Butbuild_fixtures.shin PR docs(evals): build six PR fixture set for /recce-verify v1 eval #29 runsbuild_at_shasix (sometimes seven) times in a loop, each call doinggit reset --hard && git clean -fdx && git checkout --detach <sha>. After the script finishes, the checkout is atpr46-net-clv-segments's head SHA — not the current fixture's. An evaluator running, say,pr1-fix-clvwho 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.
- (a) Drop the working-tree dependency from the contract. Source at head SHA is already encoded in
Notes
-
dbt parse/dbt compileare ambiguous at Tier 0 —RUBRIC.md:57–59listsdbt run,dbt test, "live SQL execution" as forbidden, but notdbt parse,dbt compile, ordbt docs generate. These don't necessarily hit the warehouse (with--empty-catalog), but they regeneratemanifest.jsonagainst the source repo's Snowflake-only profile (no credentials → may fail; may also succeed differently than the frozenmanifest-after.jsonbecause seeds/ and dbt_packages/ aren't staged asbuild_fixtures.shdoes). 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 forbiddbt parse|compile|docs generateat Tier 0 (preferred — preserves the "frozen artifacts" premise), or explicitly permit them with a one-line caveat about reproducibility. -
Gap-report cap is gameable via fix-label splitting —
templates/gap-report.md:5–6collapses 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." -
"Regression = v1-release-blocker" is too strong given N=6 framing —
RUBRIC.md:41says 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." -
Lens 2 singular-vs-plural inconsistency —
RUBRIC.md:23says 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 likepr1-fix-clvan agent can legitimately cite 1a + 1b + 1c (CLL shows the dependency, AST shows the newWHERE, 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." -
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 < 2doesn'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
- I could not run the eval end-to-end. The cross-reference to
build_fixtures.shis based on reading the script in PR docs(evals): build six PR fixture set for /recce-verify v1 eval #29; the working-tree behavior could differ if a future commit on PR docs(evals): build six PR fixture set for /recce-verify v1 eval #29 changes the checkout discipline. - I did not check whether the Linear issue DRC-3403 description requires anything not in this PR.
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 → catchall correctly land in Improvement;catch → partial,catch → miss,partial → missall correctly land in Regression. Orderingcatch > partial > missis consistent across the document.
Independent review pass — no sibling reviews consulted. Posted as COMMENT (workspace pattern: don't REQUEST CHANGES on own PR).
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
left a comment
There was a problem hiding this comment.
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-parseagainst non-ancestor SHAs,git fetch. Brittle (relies on agent compliance with a ban list it can trivially violate). - Build-side (preferred): have
build_fixtures.shproduce a freshgit clone --depth 1per 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>
|
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 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):
This wires to the #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 #3 — Tier-1 primary/supporting wording: folded in Reworded lens 2. Old phrasing "record the highest decisive tier" collapsed into one rule something that has two regimes; split that out:
Kept the cross-tier ordering rule explicit so runners don't second-guess the |
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>
|
Re: point #2 — Tier-0 runtime contract leak via shared object DB. Folded into PR #29 as commit What changed — replaced the 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/headNo Built-in sanity check — the script now exits non-zero if
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 ( 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). |
docs(evals): build six PR fixture set for /recce-verify v1 eval
Signed-off-by: even-wei <evenwei@infuseai.io> # Conflicts: # .gitignore
Fixture / test-workflow integration check (HEAD:
|
| 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_idinmanifest-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.mdInputs 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 0section ("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 readfixtures/<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
left a comment
There was a problem hiding this comment.
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.
…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>
Summary
evals/agent-blind-spots/— qualitative case-study eval contract for the/recce-verifyv1 skill, gating v1 backend additions on a ≤5-entry gap report.RUBRIC.mddefines 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.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-verifyv1 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
Test plan
/codex:rescueadversarial review — findings addressed inb672c9c/recce-dev:claude-code-reviewself-review on branch — findings addressed in7206e10Resolves DRC-3403.