Skip to content

Add plan-review eval harness + content-hash plan-approval gate (workflow-eval step 1b)#716

Merged
igerber merged 7 commits into
mainfrom
plan-review-eval
Jul 21, 2026
Merged

Add plan-review eval harness + content-hash plan-approval gate (workflow-eval step 1b)#716
igerber merged 7 commits into
mainfrom
plan-review-eval

Conversation

@igerber

@igerber igerber commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • tools/plan-review-eval/ (new): five-arm k=2 harness measuring plan-review engine candidates against the pinned production workflow before anything ships — control criteria sourced via git show at a pinned full SHA; candidate engine (P0–P3 criteria, dual Claude+codex blind review with merge+verify, extraction prompts) as lab artifacts in candidates/; pre-registered DECISION_RULE.md (regression/FP/improvement gates, equal-exposure UNDETERMINED rule, neutral severity vocabulary, corpus floor); extraction-based blinded grading whose Bundle ID is the hash of the exact grader-visible bytes; mechanical verdict.py with strict grading-table validation (complete grids, negative-assessment cells, registered contrasts only, realized-grid vs declared-schedule check). Real plan corpus stays local-only (gitignored); a fabricated fixture case is committed for CI/smoke/dress-rehearsal. Live-smoke-verified: the control arm reviewed the fixture plan end-to-end (headless claude -p --safe-mode, detached worktree at base_sha) and caught both seeded defects.
  • tools/eval_core/ evolutions (generic, additive; reviewer-eval unchanged and green): Config gains variant/mode arm dimensions, build_bundle takes a header override, blinding patterns cover Claude-family names, known-FP topic ids render in bundles. The worktree-cleanup containment fix (guards before any git command; symlinked leaves unlinked, never followed; strict trusted-root containment) is applied to both harnesses' adapters.
  • Plan-approval mechanics: .claude/hooks/check-plan-review.py replaces the shell hook — a plan_sha256 content-hash gate replaces the .last-reviewed sentinel + mtime design, structurally killing the concurrent-worktree sentinel race, the ls -t fallback, and the touch-after-revision hack; fails closed on payload drift and malformed payloads (payload contract verified against the deployed CLI binary's own ExitPlanMode input schema). A new tested helper .claude/scripts/plan_snapshot.py owns the snapshot/persist protocol (invocation-unique immutable snapshots keyed by a state token; persistence certifies the recorded snapshot digest only after re-verifying the live plan; canonical review keys matching the hook; hostile-meta-proof frontmatter); review-plan.md/revise-plan.md/CLAUDE.md are thin wrappers around it. Revisions now require a fresh review (or an honestly-labeled Skipped marker) — no re-stamp path.
  • Threat model recorded (DEFERRED.md decision record + hook docstring): the gate prevents accidents — stale approvals, cross-worktree confusion, mid-review edits — not malicious local processes, which no userland hook can stop (nothing verifies review authorship). Repo-wide codex read-isolation is tracked in TODO.md (the same surface production reviewer tooling already accepts).

Methodology references (required if estimator / math changes)

  • Method name(s): N/A — no estimator/library changes. The methodology in this PR is the evaluation protocol itself: please scrutinize tools/plan-review-eval/DECISION_RULE.md (gate soundness, equal-exposure rule, corpus floor) and tools/plan-review-eval/candidates/ (criteria, merge+verify, extraction prompts) as first-class review surfaces — the step-2 campaign may not run until they have survived review (campaign-readiness gate).
  • Paper / source link(s): N/A
  • Any intentional deviations from the source (and why): The accident-vs-adversary threat model for the approval gate is an explicit recorded decision (DEFERRED.md "Decision record", 2026-07-20) rather than an oversight.

Validation

  • Tests added/updated: tests/test_plan_review_hook.py (26-case allow/deny matrix incl. the two-concurrent-plans race, payload drift/type discipline, symlink+tilde normalization; every deny asserts the PreToolUse protocol), tests/test_plan_snapshot.py (12 behavioral: invocation-unique snapshots, A→B→A identity certification, rewritten-snapshot rejection, cross-wiring impossibility, hostile paths/meta), tests/test_plan_review_eval.py (69: loader contracts, hermetic pinned-SHA sourcing, arm-matrix decomposition, verdict gate boundaries on synthetic k=2 tables, UNDETERMINED on unequal exposure, bundle-id-as-artifact-hash, fixture exclusion, argv confinement), tests/test_command_contract.py (helper-invocation + path-validation contracts), tests/test_evals_*.py (cleanup-signature update only). 250 pass across the seven suites in one process; verify-corpus passes for both harnesses (1/1 and 11/11).
  • Backtest / simulation / notebook evidence (if applicable): two live smokes of the control arm on the committed fixture case (before/after the permission-model and --safe-mode changes) — both caught the two seeded defects with exact repo citations.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes. Real plan corpus and run artifacts are gitignored; the README's data-handling section documents provider transmission and artifact retention explicitly.

@github-actions

Copy link
Copy Markdown

Overall Assessment

⚠️ Needs changes — three unmitigated P1 findings.

Executive Summary

  • No estimator, variance, weighting, inference, or identification logic changed.
  • Campaign verdicts are not bound to the pre-registered protocol/configuration.
  • /review-plan contradicts its own required write constraints.
  • Valid plan paths containing spaces or Unicode are rejected.
  • Known Codex isolation and cancellation limitations are tracked and non-blocking.

Methodology

P1 — Campaign verdict lacks immutable protocol provenance

Location: tools/plan-review-eval/run_eval.py:L235-L246, L556-L563, L576-L589, L657-L716

Impact: The manifest does not record hashes of DECISION_RULE.md, configs.json, candidate artifacts, extraction identity, or registered contrasts. verdict reloads the current config to derive gating pairs. A post-run edit to arm roles, extraction settings, or protocol artifacts can therefore produce gating: true under a different protocol than the pre-registered campaign. The bundle hash protects grader-visible bytes, but not their methodological provenance.

Concrete fix: Snapshot/hash the decision rule, full config, candidate artifacts, extraction prompt/model, and registered contrasts into the run manifest. Store that identity in blinding.json, and make verdict fail or mark the result non-gating on any mismatch. Derive contrasts from the manifest snapshot, not live config.

Code Quality

P1 — /review-plan forbids files it requires itself to write

Location: .claude/commands/review-plan.md:L42-L47, L60-L82, L466-L499

Impact: The constraints say the review output is the only written file and that Write/mkdir may only target ~/.claude/plans/. The workflow then requires <git-path>/plan-review/plan-path.txt plus snapshot state, metadata, and body files. An agent following the hard constraints can refuse or abort the mandatory protocol.

Concrete fix: Explicitly allow the ingress file and invocation-scoped temporary files, while retaining the prohibition on project source/configuration writes. Document cleanup behavior and distinguish temporary files from the sole persistent output.

Performance

P3 — Dual-review failure waits for the surviving peer

Impact: An early peer failure can delay INFRA_ERROR for up to the other reviewer’s timeout.

Concrete fix: No approval action required; this is tracked in TODO.md.

Maintainability

P1 — Safe data paths are rejected as shell-hostile

Location: .claude/scripts/plan_snapshot.py:L67-L98, .claude/commands/revise-plan.md:L134-L146

Impact: Absolute paths containing spaces, Unicode, or other valid filesystem characters cannot be reviewed. This includes users whose home directory itself contains a space. The helper already receives paths through a file and does not need a shell-safety whitelist.

Concrete fix: Accept arbitrary absolute paths in plan_snapshot.py. Replace revise-plan’s interpolated shasum/grep commands with an argv-safe helper or file-ingress subcommand.

Tech Debt

No untracked blocking debt found. The two newly recorded TODO.md limitations are correctly classified as P3 informational.

Security

P3 — Codex read isolation is not repository-confined

Impact: Dual-arm Codex processes can read outside the worktree.

Concrete fix: No approval action required; the limitation is explicitly documented and tracked in TODO.md.

Documentation / Tests

P2 — Tests encode the path compatibility regression

Location: tests/test_plan_snapshot.py:L200-L205, tests/test_command_contract.py:L246-L289

Impact: Spaces are classified as hostile, so the suite prevents support for valid filesystem paths.

Concrete fix: Retain rejection tests for shell interpolation, but add successful round trips for paths and $HOME values containing spaces and Unicode. Add a contract test covering the review command’s temporary-write allowance.

Static parsing succeeded for 14 changed Python files and four JSON files.

Path to Approval

  1. Bind every campaign verdict to immutable hashes of the registered protocol, config, candidate artifacts, extraction identity, and contrasts.
  2. Reconcile /review-plan’s constraints with its required temporary files.
  3. Support valid absolute paths through data-safe helpers and add space/Unicode regression tests.

@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 7d560cd7c2a3c740b3a9613f4bef0713fd37dfd9


Overall Assessment

Blocker — one P0 security vulnerability and three unmitigated P1 findings.

Executive Summary

  • No estimator, weighting, variance, inference, or identification code changed; the Methodology Registry is unaffected.
  • Prior temporary-write constraints are fixed.
  • Shell-metacharacter plan names can execute commands during persistence.
  • Protocol provenance is recorded after execution, so it may describe different artifacts.
  • Campaign plans can change between arms, silently confounding comparisons.
  • Same-basename plans still overwrite each other’s approvals.

Methodology

P1 — Protocol provenance does not identify the executed protocol

Location: tools/plan-review-eval/run_eval.py:L163-L181, L246-L274, L730-L756

Impact: Configs and reviewer artifacts are loaded before the long-running matrix, but _protocol_identity() is captured only after it finishes. If protocol files change during the campaign, the manifest records the new hashes while results came from the old artifacts; the later live comparison passes and may report gating: true.

Concrete fix: Snapshot protocol/config/artifact bytes before constructing configs or reviewers, build execution from that snapshot, record it immediately, and abort or mark non-gating if an end-of-run hash differs. Add a mid-run mutation regression test.

P1 — [Newly identified] Arms can review different plan bytes

Location: tools/plan-review-eval/plan_adapters/plan_reviewer.py:L178-L225, L331-L335; tools/eval_core/runner.py:L197-L209, L255-L274

Impact: case_tag() hashes the plan once, but every job later re-reads the live corpus file. Editing a plan during a campaign can expose later arms/repeats to different content under the original run key and case snapshot, silently confounding the verdict.

Concrete fix: Capture each plan’s bytes once before scheduling, include its digest in the case snapshot, and render every arm from those immutable bytes. Reject any end-of-run corpus drift.

No library methodology or paper-parity issue was found.

Code Quality

The prior contradiction between required temporary writes and /review-plan constraints is resolved.

Performance

P3 — Tracked peer-cancellation delay

Impact: A surviving dual-review subprocess may continue after its peer fails.

Concrete fix: No approval action required; tracked in TODO.md.

Maintainability

P1 — [Newly identified] Same-basename plans overwrite approvals

Location: .claude/scripts/plan_snapshot.py:L114-L117; .claude/hooks/check-plan-review.py:L197-L200

Impact: Review keys use only the canonical basename. Two plans such as /repo-a/plan.md and /repo-b/plan.md share one review file; reviewing either overwrites the other, causing concurrent approval failures.

Concrete fix: Include a canonical-path digest in the review filename in the helper, hook, and check path. Add a two-path/same-basename concurrency test.

P2 — Aborted reviews leave “temporary” snapshots behind

Location: .claude/commands/review-plan.md:L42-L44; .claude/scripts/plan_snapshot.py:L184-L196

Impact: Cleanup occurs only during persist; cancellation or reviewer failure before persistence leaves plan copies and state files indefinitely.

Concrete fix: Add an argv-safe abort operation and invoke it on every pre-persist failure/cancellation path.

Tech Debt

P3 — Recorded limitations

Codex read isolation is tracked in TODO.md, and the non-adversarial hash-gate threat model is recorded in DEFERRED.md; neither blocks approval.

Security

P0 — Untrusted plan basename reaches Bash command substitution

Location: .claude/scripts/plan_snapshot.py:L148-L156; .claude/commands/review-plan.md:L488-L492; .claude/commands/revise-plan.md:L82-L87; CLAUDE.md:L249-L252

Impact: The generated state_path contains the raw plan stem, then prose instructs inserting it into:

--state-file "<state-path>"

Double quotes do not suppress $() or backticks. A valid plan named weird$(touch marker).md therefore produces a state path that executes touch marker when the persistence command runs.

Concrete fix: Never interpolate the returned path into shell source. Pass it through a file-ingress argument or return a safe opaque token that the helper resolves internally. Generated filenames should also contain only a strict safe charset.

Documentation / Tests

P2 — Tests bypass the vulnerable workflow boundary

Location: tests/test_plan_snapshot.py:L207-L218; tests/test_plan_review_eval.py:L1307-L1331

Impact: The shellish-name test invokes Python with an argv list, so it cannot detect the prose-level shell injection. The provenance test checks only dictionary inequality, not mutation during _run_matrix.

Concrete fix: Add an isolated end-to-end command test using $()/backticks in the plan name, plus a test that mutates protocol and plan inputs while the matrix stub is running.

Static parsing passed for 14 changed Python files and four JSON files. Pytest was unavailable in the review environment.

Path to Approval

  1. Replace literal state_path shell interpolation with a fully data-safe handoff and add an execution regression test.
  2. Freeze and record protocol bytes before execution; detect all mid-run drift.
  3. Freeze each case’s plan bytes for the entire matrix.
  4. Make review-file keys collision-free across canonical plan paths.

@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: fb4b5c2cc78814e79bd26d0c68f254b44e77de3a


Overall Assessment

⚠️ Needs changes — three unmitigated P1 findings remain.

Executive Summary

  • No estimator, variance, weighting, or identification code changed; the Methodology Registry is unaffected.
  • Prior shell injection, cross-plan collision, and differing-plan-byte findings are resolved.
  • Protocol provenance can still misidentify transiently changed execution artifacts.
  • Conflicting severities can double-count one false positive and flip the verdict.
  • Re-review workflows still search the obsolete unhashed review filename.

Methodology

P1 — Prior protocol-provenance finding remains unresolved

Location: tools/plan-review-eval/run_eval.py:L163-L180, L260-L299; tools/plan-review-eval/plan_adapters/criteria_source.py:L138-L145

Impact: _protocol_identity() hashes live files, then configs and candidate artifacts are independently re-read for execution. If an artifact changes A→B→A between those operations, the campaign executes B while recording A, and the end comparison reports no drift. A gating verdict can therefore be attributed to the wrong protocol.

Concrete fix: Read every protocol/config/candidate artifact once into an immutable snapshot, derive the manifest identity from those exact bytes, and construct configs/reviewers exclusively from that snapshot.

P1 — [Newly identified] Severity disagreements bypass FP deduplication

Location: tools/plan-review-eval/verdict.py:L149-L169

Impact: Duplicate identity is (severity, summary). The same defect listed as both blocker and major passes validation and can count twice. With allow_severities: [], this mechanically changed a 1-vs-1 comparison into candidate=2, producing a false NO-GO.

Concrete fix: Deduplicate by normalized finding identity independent of severity—at minimum normalized summary—or require a stable finding ID. Reject conflicting severities for the same finding.

Code Quality

No additional findings.

Performance

P3 — Tracked peer-cancellation delay

Impact: A surviving dual-review subprocess can run until timeout after its peer fails.

Concrete fix: No approval action required; tracked in TODO.md.

Maintainability

P1 — [Newly identified] Hashed review-path contract was not propagated

Location: .claude/scripts/plan_snapshot.py:L119-L127; .claude/commands/revise-plan.md:L49-L55; .claude/commands/review-plan.md:L89-L102; CLAUDE.md:L229-L231

Impact: The helper now writes stem.<path-digest>.review.md, but /revise-plan, --updated, and documentation still look for stem.review.md. Existing reviews appear missing, causing unnecessary re-reviews/skips and false “delta unavailable” results.

Concrete fix: Consume the helper’s returned review_path everywhere, remove basename-derived lookup, and update all filename documentation.

Tech Debt

No untracked blocking debt found.

Security

P3 — Tracked Codex read-isolation limitation

Impact: Read-only sandboxing does not confine Codex reads to the worktree.

Concrete fix: No approval action required; tracked in TODO.md. The prior shell-injection finding is fixed.

Documentation / Tests

P2 — Remediation boundaries lack regression tests

Impact: Current tests cover helper/hook key agreement but not command-level discovery, transient A→B→A protocol mutation, or same-summary/different-severity FP duplication.

Concrete fix: Add focused regressions for those three cases.

Static parsing passed for 14 changed Python files and four JSON files.

Path to Approval

  1. Build execution and provenance from one immutable protocol snapshot.
  2. Reject duplicate FP findings regardless of severity.
  3. Replace every basename-derived review lookup with helper-returned review_path and add workflow-level tests.

@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 175ad74b4a22ed35838b664c0b37c255b2caeceb


Overall Assessment

⚠️ Needs changes — two P1 findings remain.

Executive Summary

  • No estimator, variance, weighting, or identification code changed; Methodology Registry cross-checking is not applicable.
  • Prior FP deduplication and hashed review-path findings are resolved.
  • Protocol provenance still omits the control arm’s actual prompt.
  • /revise-plan can fail on first use because its scratch directory is not created.

Methodology

P1 — Prior protocol-provenance finding remains partially unresolved

Location: tools/plan-review-eval/run_eval.py:L169-L203, L231-L244; tools/plan-review-eval/plan_adapters/criteria_source.py:L91-L111, L165-L169

Impact: The immutable protocol snapshot covers configs and candidate files, but not _CONTROL_PROMPT. The control prompt is loaded later from live Python state and is absent from the recorded protocol identity. Changing it can alter arm A while the campaign still produces a gating verdict attributed to the unchanged protocol identity.

Concrete fix: Include the control prompt’s exact bytes in _protocol_snapshot(), hash them into the identity, and pass that snapshotted text into load_artifacts() instead of reading _CONTROL_PROMPT independently.

Code Quality

No additional findings.

Performance

P3 — Tracked peer-cancellation delay

Impact: A surviving dual-review subprocess may run until timeout after its peer fails.

Concrete fix: No approval action required; tracked in TODO.md.

Maintainability

P1 — [Newly identified] /revise-plan does not create its scratch directory

Location: .claude/commands/revise-plan.md:L49-L56

Impact: On a fresh worktree, <git-path>/plan-review/ need not exist. The initial Write to plan-path.txt therefore fails before the command can check for a review or offer the review/skip choices.

Concrete fix: Run mkdir -p "$SCRATCH" before the first Write, matching /review-plan, and reuse that initialized directory for later checks.

Tech Debt

No untracked blocking debt found.

Security

P3 — Tracked read-isolation limitation

Impact: Codex read-only sandboxing does not confine reads to the worktree.

Concrete fix: No approval action required; tracked in TODO.md. The local-adversary limitation is also explicitly recorded in DEFERRED.md.

Documentation / Tests

P2 — Missing regressions for the remaining P1s

Impact: Current tests verify candidate snapshotting and helper lookup presence, but not control-prompt provenance or first-use scratch initialization.

Concrete fix: Add tests proving the control prompt participates in protocol identity/execution and that /revise-plan initializes an absent scratch directory.

Static AST parsing passed for 17 Python files and JSON parsing passed for four JSON files. pytest was unavailable in the review environment.

Path to Approval

  1. Snapshot and identify the exact control prompt used by arm A.
  2. Create /revise-plan’s scratch directory before its first Write and add the two focused regressions above.

@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 7e246dd38ec72c96a009d4ac3aacf05855e10e88


Overall Assessment

Looks good — both prior P1 findings are resolved. No unmitigated P0/P1 findings remain.

Executive Summary

  • Control-prompt provenance now covers both identity and execution.
  • /revise-plan initializes its scratch directory before use.
  • No estimator, variance, weighting, SE, or identification code changed.
  • One low-priority path-encoding edge case remains.
  • Tracked performance/security limitations do not block approval.

Methodology

P3 — Documented evaluation-protocol divergences

Impact: Control criteria are inlined, and dual verification lacks production conversation context. Both are explicitly disclosed in tools/plan-review-eval/README.md:L101-L109; no hidden methodology mismatch exists.

Concrete fix: None required.

The mechanical regression, FP, equal-exposure, registered-contrast, and corpus-floor gates match DECISION_RULE.md. The Methodology Registry has no affected estimator method.

Code Quality

P2 — Arbitrary-path claim exceeds the frontmatter contract

Location: .claude/scripts/plan_snapshot.py:L87-L103, L273-L283; .claude/hooks/check-plan-review.py:L89-L113

Impact: Ingress .strip() silently changes valid paths ending in whitespace, while embedded newlines produce a plan: field the line-based hook cannot recover. Such unusual filesystem-valid paths can target the wrong plan or fail approval.

Concrete fix: Preserve paths through structured ingress, or explicitly reject CR/LF and trailing-whitespace paths with a clear error; add regressions.

Performance

P3 — Tracked peer-cancellation delay

Impact: A surviving dual-review subprocess can run until timeout after its peer fails.

Concrete fix: No approval action required; tracked in TODO.md:L56.

Maintainability

No findings.

Tech Debt

No untracked blocking debt found.

Security

P3 — Tracked Codex read-isolation limitation

Impact: Read-only sandboxing does not confine Codex reads to the worktree.

Concrete fix: No approval action required; tracked in TODO.md:L55. The local-adversary threat model is recorded in DEFERRED.md:L128.

Documentation / Tests

No blocking findings. Focused regressions cover both prior P1 fixes.

Static validation passed for 24 Python files and four JSON files, plus direct protocol/verdict checks. pytest was unavailable, and filesystem-writing tests could not run in the read-only review environment.

@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: c6c742fc868979d50ae6f86d564089f02c2c0c5a


Overall Assessment

⚠️ Needs changes — three newly identified P1 issues remain.

Executive Summary

  • No estimator, weighting, variance, SE, or identification code changed.
  • Prior control-provenance, scratch-directory, and path-encoding findings are resolved.
  • Campaign protocol identity is not enforced across extraction, comparison, and verdict stages.
  • Blinded arm attribution can silently drift because the stored mapping is trusted.
  • Missing tool_input silently disables the approval gate.

Methodology

P1 — [Newly identified] Protocol identity is not end-to-end

Location: tools/plan-review-eval/run_eval.py:L169-L207, L510-L538, L582-L658, L808-L835

Impact: A campaign run under protocol A can be extracted and bundled under modified protocol B, then have files restored to A before verdict. The live-vs-recorded check then passes and the B-derived bundle can receive a gating verdict. Evaluator code such as verdict.py, _GRADING_HEADER, and eval_core/compare.py is also absent from the recorded identity. This contradicts DECISION_RULE.md:L3-L6 and L94-L110.

Concrete fix: Require the manifest’s protocol identity to match before extract and compare; record the extraction/bundle identity in blinding.json; hash the executable scoring/bundling sources or require an unchanged clean commit through verdict. Add an A→B→A cross-stage regression.

P1 — [Newly identified] Blinded arm mapping is not verified

Location: tools/plan-review-eval/run_eval.py:L638-L657, L770-L802

Impact: blinding.json["mapping"] is trusted directly. Swapping two arm assignments silently attributes grades to the wrong engines while the run-key and bundle-ID checks still pass, potentially reversing GO/NO-GO.

Concrete fix: Recompute the deterministic mapping from manifest run keys and rendered arm set during verdict, require exact equality, and add a swapped-mapping rejection test.

P3 — Documented fidelity differences

Location: tools/plan-review-eval/README.md:L101-L109

Impact: Inlined control criteria, context-free merge verification, and detached read-only worktrees differ from production but are explicitly documented.

Concrete fix: None required.

No library method or Methodology Registry entry is affected.

Code Quality

P1 — [Newly identified] Missing outer tool_input fails open

Location: .claude/hooks/check-plan-review.py:L133-L160

Impact: payload.get("tool_input", {}) treats a missing field as a valid empty input and allows approval. A payload-shape change could therefore disable the gate silently. Directly invoking the hook with {} returned an empty successful allow response.

Concrete fix: Deny when "tool_input" is absent, while retaining the intended allow behavior for {"tool_input": {}}; add both regression cases.

Performance

P3 — Tracked peer-cancellation delay

Impact: A surviving dual-review process may run until timeout after its peer fails.

Concrete fix: None required; tracked in TODO.md:L56.

Maintainability

No additional findings.

Tech Debt

No untracked deferrable work found.

Security

P3 — Tracked Codex read isolation

Impact: Codex read-only sandboxing does not confine reads to the worktree.

Concrete fix: None required; tracked in TODO.md:L55. The local-adversary boundary is documented in DEFERRED.md:L128.

No apparent secrets were found.

Documentation / Tests

The prior findings are resolved:

  • Control prompt provenance participates in execution and protocol identity.
  • /revise-plan initializes its scratch directory at .claude/commands/revise-plan.md:L49-L60.
  • CR/LF and edge-whitespace paths are explicitly rejected in .claude/scripts/plan_snapshot.py:L87-L121.

Static Python compilation and JSON parsing passed. pytest is unavailable in the review environment.

Path to Approval

  1. Bind extraction, comparison, and scoring to the manifest’s recorded protocol, including executable source identity and an A→B→A regression.
  2. Recompute and verify the blinded arm mapping before unblinding.
  3. Fail closed when the hook payload omits tool_input, with tests distinguishing missing from empty input.

@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 8e67cf615c7b6bb03456192f0968863326147a54


Overall Assessment

⚠️ Needs changes — three P1 issues remain.

Executive Summary

  • No estimator, weighting, variance, SE, or identification code changed; the Methodology Registry is unaffected.
  • Prior blind-mapping and missing-tool_input findings are resolved.
  • Protocol identity still excludes executable extraction behavior.
  • Corpus verification accepts schema-invalid values that can alter verdicts.
  • Concurrent /revise-plan sessions can consume another session’s plan path.

Methodology

P1 — Prior protocol-identity finding remains partially unresolved

Location: tools/plan-review-eval/run_eval.py:L169-L226, L524-L566, L610-L669; tools/plan-review-eval/plan_adapters/plan_reviewer.py:L413-L456

Impact: evaluator_sha hashes run_eval.py, verdict.py, and compare.py, but not the implementation that performs extraction (PlanReviewer.extract, _claude_argv, or criteria_source.render). Editing those sources after run passes the protocol check, while extraction metadata still records only prompt, model, and review hashes. Additionally, extract and compare validate protocol identity and then reread inputs separately, permitting an A→B→A edit during a stage to produce a gating artifact under protocol A.

Concrete fix: Include all extraction/execution sources in protocol identity, derive every post-run input from one immutable protocol snapshot, record that identity in extraction metadata and blinding.json, and recheck it after each stage. Add actual extract/compare A→B→A regressions and an extractor-source-drift test.

P1 — [Newly identified] Schema-invalid must_catch values silently change gates

Location: tools/plan-review-eval/corpus/manifest.schema.json:L30-L46; tools/plan-review-eval/plan_adapters/corpus_loader.py:L93-L170; tools/plan-review-eval/verdict.py:L311-L317

Impact: The committed schema requires a boolean, but the loader never applies it. For example, "must_catch": "false" passes corpus verification and becomes True through bool("false"), potentially turning an optional defect into a false NO-GO. Other invalid truthy/falsy values can likewise change campaign decisions.

Concrete fix: Validate each case.json against the schema before constructing dataclasses, or add equivalent strict type validation. Test that strings and numeric booleans are rejected and that genuine false remains non-mandatory.

P3 — Documented fidelity differences

Location: tools/plan-review-eval/README.md:L101-L109

Impact: Inlined control criteria, context-free merge verification, and detached read-only worktrees differ from production.

Concrete fix: None required; these differences are explicitly documented.

Code Quality

P1 — [Newly identified] Shared revise-plan ingress can select the wrong review

Location: .claude/commands/revise-plan.md:L51-L63, L146-L158

Impact: Both check calls use the deterministic <scratch>/plan-path.txt, but unlike snapshot calls, they do not require confirming the returned plan_path. Concurrent sessions in the same worktree can overwrite the ingress file, causing one session to read another plan’s review or freshness result and revise from unrelated feedback. This is an in-scope accidental concurrency failure under DEFERRED.md:L128.

Concrete fix: Use helper-generated invocation-unique ingress files for every check and snapshot, and add an interleaved two-session regression. At minimum, require matching the returned canonical plan_path after every check.

The prior missing-tool_input fail-open is resolved at .claude/hooks/check-plan-review.py:L133-L140.

Performance

P3 — Tracked peer-cancellation delay

Impact: A surviving dual-review process may run until its timeout after its peer fails.

Concrete fix: No action required for this PR; tracked in TODO.md:L56.

Maintainability

No additional findings.

Tech Debt

No untracked deferrable work found.

Security

P3 — Tracked Codex read isolation

Impact: Read-only sandboxing does not confine Codex reads to the worktree.

Concrete fix: No action required here; tracked in TODO.md:L55. No apparent secrets were found.

Documentation / Tests

The blind-mapping and hook-payload regressions are covered. Static Python parsing and JSON validation passed. pytest is not installed in the review environment, so the suites could not be executed.

Path to Approval

  1. Bind extraction and comparison execution to one complete, immutable protocol identity, including extractor sources, with source-drift and A→B→A tests.
  2. Enforce the corpus schema’s boolean/type contracts and add invalid-must_catch regressions.
  3. Replace shared plan-path.txt ingress with invocation-unique files and test concurrent /revise-plan checks.

…val step 1)

Builds the measurement infrastructure for the plan-review workflow family
(the first target of the eval-per-workflow-surface program) and lands the
purely mechanical gate fixes. The live review engine does not change.

tools/plan-review-eval/ — five-arm harness over eval_core (verify-corpus /
smoke / run / extract / compare / verdict): pinned-git-SHA control arm,
candidate single+dual arms, Sonnet-5 and gpt-5.6-terra probe arms, all k=2.
Pre-registered DECISION_RULE.md; neutral-severity extraction with blinded
bundles whose bundle_id is the hash of the grader-visible bytes; mechanical
verdict gates (GO / NO-GO / PARITY / UNDETERMINED) with protocol-violation
labeling (NON-GATING) for rehearsal or subset runs.

Campaign provenance is end-to-end: one immutable protocol snapshot per
stage feeds both the recorded identity and execution; the identity covers
the decision rule, configs, every candidate artifact, the control prompt,
and EVERY Python source in the plan-review-eval and eval_core trees (walked,
not hand-listed); stages gate at entry against the snapshot and re-check at
exit with a fresh read; the protocol sha is stamped into extraction metadata
and blinding.json and re-verified downstream; the blind mapping is
recomputed at verdict, never trusted. The corpus loader enforces the
manifest.schema.json type contract before dataclass construction, so a
schema-invalid value (e.g. "must_catch": "false") can never flip a campaign
gate through truthiness. Real plan corpora stay local-only (gitignored);
one fabricated fixture case is committed for CI, smoke, and the dress
rehearsal.

.claude/hooks/check-plan-review.py replaces the sentinel-file gate: the
ExitPlanMode hook now validates a plan_sha256 recorded in the review file's
frontmatter against the CURRENT plan bytes — no sentinel, no mtime, no
touch hack, no cross-worktree races. It fails closed on payload drift,
missing tool_input, unreadable plans, malformed frontmatter, and path
mismatches (realpath-normalized both sides). .claude/scripts/plan_snapshot.py
provides the tested snapshot/persist/check/abort protocol the commands use:
invocation-unique digest+nonce state tokens, recorded-digest certification
(persist re-verifies the live plan against the reviewed snapshot),
canonical-path-digest review filenames, and strict path ingress (absolute,
no CR/LF, no whitespace edges). /review-plan and /revise-plan are thin
wrappers over the helper; every check/snapshot call site requires
confirming the helper's echoed plan_path (shared per-worktree ingress).
The recorded threat model (DEFERRED.md): the gate prevents accidents, not
local adversaries.

Tests: hook decision table, snapshot protocol (incl. hostile-stem bash
end-to-end), harness units (corpus contracts, hermetic pinned-SHA sourcing,
arm schema, synthetic k=2 verdict tables, protocol-identity regressions),
and command-contract scans.
@igerber
igerber force-pushed the plan-review-eval branch from 8e67cf6 to 89cc78a Compare July 20, 2026 19:02
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 89cc78afcafe619af1ace1bbcddef3d1533931b3


Overall Assessment

⚠️ Needs changes — three unmitigated P1 methodology issues remain.

Executive Summary

  • No estimator, weighting, variance, SE, identification, or library-default code changed; no Methodology Registry method is affected.
  • The shared revise-plan ingress race and strict must_catch boolean handling are fixed.
  • Protocol provenance still permits cached outcomes to be attributed to a later protocol.
  • Partial schema enforcement and over-broad model redaction can silently alter campaign verdicts.
  • Documented fidelity/security limitations remain non-blocking P3 items.

Methodology

Prior protocol-identity finding remains partially unresolved

Severity: P1
Location: tools/plan-review-eval/run_eval.py:L164-L250, L323-L380; tools/plan-review-eval/plan_adapters/plan_reviewer.py:L90-L176; tools/eval_core/runner.py:L179-L248

Impact: The identity excludes the external .claude/scripts/openai_review.py implementation executed by dual arms. More importantly, resume keys do not include the recorded protocol identity: editing DECISION_RULE.md or evaluator/verdict code and rerunning the same subdirectory can reuse all prior reviews, then overwrite the manifest with the new protocol. This retroactively associates already-observed outcomes with a changed pre-registration. PlanReviewer is also imported after the source snapshot, leaving the prior A→B→A execution window open.

Concrete fix: Include every executable dependency in an immutable source snapshot, bind its protocol hash into every run key/result, and refuse cache reuse when an existing or partial campaign’s protocol differs. Add cached-resume, external-wrapper-drift, and late-import A→B→A regressions.

Corpus schema enforcement remains incomplete

Severity: P1
Location: tools/plan-review-eval/plan_adapters/corpus_loader.py:L29-L74, L118-L157; tools/plan-review-eval/run_eval.py:L763-L778

Impact: Only the type of stratum is checked. An unknown stratum filed under a matching directory verifies successfully and counts toward the eight-case corpus floor. Other schema-invalid values are silently transformed; for example, a string class_keywords becomes a character list. Both can change grader-visible evidence or allow a schema-invalid campaign to gate.

Concrete fix: Apply the complete JSON schema or equivalent strict validation, including enum, fixture, string-list, and known-topic ID contracts. Add tests proving unknown strata cannot satisfy the corpus floor and string class_keywords is rejected.

[Newly identified] Claude redaction destroys repository paths

Severity: P1
Location: tools/eval_core/compare.py:L176-L220, L224-L252

Impact: The pattern claude[\s._-]?[a-z0-9.-]* treats the repository’s .claude/ directory as a model name. For example, .claude/hooks/check-plan-review.py becomes .[model-redacted]/hooks/check-plan-review.py in the blinded artifact. Ground-truth paths remain unchanged, so genuine catches can be graded as partial or missed.

Concrete fix: Restrict redaction to configured model IDs and genuinely model-shaped Claude tokens. Add regressions preserving .claude/... paths and ordinary “Claude workflow” prose while still redacting claude-fable-5.

Documented fidelity differences

Severity: P3
Location: tools/plan-review-eval/README.md:L101-L110

Impact: Inlined control criteria, context-free merge verification, and detached worktrees differ from production.

Concrete fix: None required; these are explicitly documented.

Code Quality

No additional findings. The prior concurrent revise-plan ingress issue is resolved by checking returned plan_path before consuming helper output at .claude/commands/revise-plan.md:L51-L63 and L152-L162.

Performance

Tracked peer-cancellation delay

Severity: P3
Impact: A surviving dual-review process may run until timeout after its peer fails.

Concrete fix: No action required for this PR; tracked in TODO.md:L56.

Maintainability

No additional findings.

Tech Debt

No untracked deferrable work found.

Security

Tracked Codex read isolation

Severity: P3
Impact: Read-only sandboxing does not confine Codex reads to the worktree.

Concrete fix: No action required here; tracked in TODO.md:L55. The non-adversarial hash-gate threat model is recorded in DEFERRED.md:L129. No apparent secrets were found.

Documentation / Tests

No additional documentation finding. The new tests cover the exact prior boolean and ingress cases, but not the three issues above. Pytest was unavailable in the review environment; static parsing succeeded for 20 affected Python files.

Path to Approval

  1. Bind cached and newly executed runs to the complete immutable protocol identity, including openai_review.py, with resume/source-drift regressions.
  2. Enforce the full corpus schema and reject unknown strata and malformed string-list fields.
  3. Narrow Claude-model redaction and test that .claude/... evidence remains intact.

…on, full corpus schema

Pre-registration integrity gets its invariant-level closure. A subdirectory
IS a campaign: its protocol identity is registered write-once, BEFORE any
reviewer call (a crash mid-matrix leaves a registered campaign with empty
run_keys, never an orphaned cache), and any invocation under a different
protocol refuses to touch it — so the store's cached outcomes can never be
re-attributed to a later protocol by editing DECISION_RULE.md and re-running
the same subdir. Smoke keeps re-registering its fixture subdir freely (never
gating, never graded). The external codex wrapper
(.claude/scripts/openai_review.py, importlib-executed by dual arms) joins
the evaluator identity, and the snapshot now proves imported code ==
hashed bytes via a read->import->re-read stability bracket: the executing
modules are imported inside the bracket and the snapshot aborts if any
source changed across it (closing the snapshot-then-import window).

Claude redaction is narrowed to MODEL-shaped tokens ("claude" + version or
family tail, e.g. claude-fable-5, Claude 3.5, claude_opus; standalone
family words unchanged). The previous pattern treated the repository's own
.claude/ tree as a model name — `.claude/hooks/check-plan-review.py` became
`.[model-redacted]/hooks/...` in blinded bundles, mangling exactly the
evidence quotes graders score against ground truth and downgrading genuine
catches to misses. Bare "claude" carries no arm signal (every arm runs the
claude CLI); regressions pin both directions, including the composed
evidence-quote case. Shared-engine change: reviewer-eval inherits the
narrowed pattern (its blinding tests stay green).

The corpus loader now enforces the complete manifest.schema.json contract:
stratum must be a known enum value (an unknown stratum directory previously
loaded, verified, and counted toward the pre-registered >=8-case floor),
class_keywords/expected_files/allow_severities must be string lists (a
string class_keywords silently became a character list in grader-visible
evidence), fixture and provenance must be objects with string fields, and
known_fp_topics entries must be objects.

Regressions: external-wrapper identity membership; mid-import source-drift
abort; write-once-per-protocol refusal (resume under the recorded protocol
stays allowed); registration-before-observation on a mid-matrix crash;
unknown-stratum and string-class_keywords rejection; .claude-path/CLAUDE.md
preservation alongside model-id redaction.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: d32753348b97b31383d91639e4eb6109e1da7aff


Overall Assessment

⚠️ Needs changes — three unmitigated P1 evaluation-methodology issues remain.

Executive Summary

  • No estimator, inference, weighting, variance, or library-default behavior changed; the Methodology Registry is unaffected.
  • The prior Claude redaction defect is resolved.
  • Protocol identity still does not bind the dynamically imported Codex wrapper to executed bytes.
  • Corpus verification still ignores the schema’s fixture discriminator.
  • The new write-once registration permits changing a campaign’s corpus or schedule after observing results.

Methodology

Protocol identity remains incomplete

Severity: P1
Location: tools/plan-review-eval/run_eval.py:L164-L255, L321-L335; tools/plan-review-eval/plan_adapters/plan_reviewer.py:L50-L60, L84-L100

Impact: openai_review.py is hashed during _protocol_snapshot(), but PlanReviewer imports it afterward from the live filesystem. An A→B→A edit can therefore execute wrapper B while the manifest and final drift check record wrapper A, allowing a gating verdict to be attributed to the wrong protocol.

Concrete fix: Import the wrapper inside the read/import/re-read bracket and pass that exact module to PlanReviewer, or execute from an immutable copied source tree. Add an end-to-end late-import A→B→A regression.

Corpus schema discriminator is not enforced

Severity: P1
Location: tools/plan-review-eval/plan_adapters/corpus_loader.py:L34-L109, L208-L295; tools/plan-review-eval/plan_adapters/worktree.py:L58-L85; tools/plan-review-eval/corpus/manifest.schema.json:L13-L18

Impact: The schema requires fixture.kind == "plan_at_sha", but the loader only checks that it is a string and the worktree adapter ignores it. A copied kind: "git_range" case can verify and count toward the corpus floor while silently being materialized as plan_at_sha, potentially reviewing the wrong repository state.

Concrete fix: Enforce all required fields and constants from manifest.schema.json, particularly fixture.kind, before dataclass construction. Test unknown fixture kinds and missing required ground-truth fields.

Campaign registration does not freeze the campaign

Severity: P1 — newly identified in the new registration logic
Location: tools/plan-review-eval/run_eval.py:L366-L380, L399-L457

Impact: _require_subdir_protocol() compares only protocol identity. The same subdirectory can be rerun with different cases, edited case contents, configurations, or repeat schedules; the manifest is then overwritten. This permits outcome-dependent corpus changes after results have been observed while still producing a gating verdict.

Concrete fix: Register and enforce an immutable campaign fingerprint containing protocol identity, case IDs and content tags, resolved base SHAs, config IDs, k, and overrides. Resume only when it matches exactly.

Documented fidelity differences

Severity: P3
Location: tools/plan-review-eval/README.md:L101-L110

Impact: Inlined control criteria, context-free merge verification, and detached worktrees differ from production.

Concrete fix: None required; explicitly documented.

Code Quality

No additional findings.

Performance

Tracked dual-peer cancellation delay

Severity: P3
Impact: A surviving reviewer may run until timeout after its peer fails.

Concrete fix: No action required here; tracked in TODO.md:L56.

Maintainability

No additional findings.

Tech Debt

No untracked deferrable work found.

Security

Tracked Codex read isolation

Severity: P3
Impact: Codex read-only sandboxing does not confine reads to the worktree.

Concrete fix: No action required here; tracked in TODO.md:L55. The non-adversarial hash-gate threat model is recorded in DEFERRED.md. No apparent secrets were found.

Documentation / Tests

The redaction regression is adequately covered. Tests are still needed for the three P1 paths above. Pytest was unavailable in the review environment; static parsing succeeded for 14 affected Python files.

Path to Approval

  1. Execute the Codex wrapper from the exact immutable source represented by the protocol identity.
  2. Enforce the complete corpus schema, including fixture.kind.
  3. Bind each campaign subdirectory to an immutable corpus and execution schedule.

…nator, frozen sample plan

The codex wrapper now executes provably-hashed bytes: _import_executing_
modules() loads .claude/scripts/openai_review.py INSIDE the snapshot's
read->import->re-read stability bracket and the snapshot hands that module
object to PlanReviewer (openai_mod parameter), so a disk edit after the
snapshot can never reach dual-arm execution — the last late-import window
in the provenance chain.

The corpus schema's discriminator is enforced end-to-end: fixture.kind must
be the schema constant "plan_at_sha" at load (a copied "git_range" case
previously verified and counted toward the corpus floor while being
silently materialized as a plan_at_sha checkout — reviewing the wrong
repository state), base_sha is schema-required, and materialize() refuses
unknown kinds as defense in depth.

Campaign registration now freezes the SAMPLE PLAN, not just the protocol:
the registered manifest carries a campaign fingerprint (arms, k, overrides,
and per-case declared base_sha + frozen plan bytes), and an invocation
differing in protocol OR fingerprint refuses the subdirectory — an
outcome-dependent change to cases, plan content, or repeat schedule after
results exist is a new campaign, never a rewrite of this one.

README gains "Provenance guarantees and limits": what the machinery
guarantees (whole-tree identity incl. the external wrapper, bracket-proven
execution, write-once registration-before-observation, stage gates, bound
extraction/blinding artifacts, recomputed mapping, byte-hash bundle ids)
and the documented out-of-scope floor (sub-bracket TOCTOU detected-not-
prevented; hostile local writers per the DEFERRED.md threat model; smoke's
never-graded fixture subdir).

Regressions: bracketed-wrapper wiring (reviewer._mod IS the snapshot's
module), fingerprint freeze across six mutation axes (k, arms, overrides,
plan bytes, base sha, case set), registration carries the fingerprint,
unknown fixture kind rejected at load AND at materialize().
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: bc2fe30edee9f7195ad7da6609e228a1bfe23c8b


Overall Assessment

⚠️ Needs changes — two unmitigated P1 evaluation-methodology issues remain.

Executive Summary

  • No estimator, inference, weighting, variance, or library default changed; the Methodology Registry is unaffected.
  • The prior wrapper-identity and fixture-discriminator defects are resolved.
  • Campaign registration still permits changing scoring metadata after observing results.
  • Corpus validation still does not enforce the schema-required ground-truth rationale.

Methodology

Campaign fingerprint omits scoring metadata

Severity: P1 — prior finding partially unresolved
Location: tools/plan-review-eval/run_eval.py:L378-L421; tools/eval_core/runner.py:L63-L82

Impact: Registration hashes only case ID, base SHA, and plan bytes. The same campaign can be rerun after changing its stratum, ground truth, must_catch, FP allowances, known-FP topics, or grading notes. A reproduction confirmed radically different positive/negative cases produce identical fingerprints, enabling outcome-dependent redefinition under the same campaign subdirectory.

Concrete fix: Fingerprint the canonical complete case snapshot—including every verdict/grader-visible field, fixture metadata, resolved base SHA, and frozen plan bytes. Add regressions mutating each scoring field.

Required corpus field remains optional

Severity: P1 — prior schema finding partially unresolved
Location: tools/plan-review-eval/corpus/manifest.schema.json:L30-L46; tools/plan-review-eval/plan_adapters/corpus_loader.py:L86-L135

Impact: The schema requires ground_truth[].rationale, but the loader silently defaults a missing value to "". Such a case verifies without the explanation graders need to match findings against ground truth.

Concrete fix: Reject ground-truth entries lacking rationale; add a missing-field regression. If rationales must be nonempty, add minLength: 1 to the schema and enforce it.

Documented fidelity differences

Severity: P3
Location: tools/plan-review-eval/README.md:L101-L110

Impact: Inlined control criteria, context-free merge verification, and detached worktrees differ from production.

Concrete fix: None required; documented.

Code Quality

No additional findings.

Performance

Severity: P3 — tracked in TODO.md:L56
Impact: A surviving dual-review subprocess may run until timeout after its peer fails.
Concrete fix: None required for this PR.

Maintainability

No additional findings.

Tech Debt

No untracked deferrable work found.

Security

Severity: P3 — tracked in TODO.md:L55
Impact: Codex read-only sandboxing does not confine reads to the worktree. The non-adversarial hash-gate threat model is recorded in DEFERRED.md:L129.
Concrete fix: None required here. No apparent secrets found.

Documentation / Tests

The two P1 paths above need regressions. Pytest was unavailable in the review environment; static parsing succeeded for 20 affected Python files.

Path to Approval

  1. Bind campaign registration to the complete case/scoring definition, not only plan bytes and IDs.
  2. Enforce the schema-required ground-truth fields, including rationale, with a missing-field test.

…re rationale

The campaign fingerprint now covers HOW observations are scored, not just
which are made: the loader stamps every case with the canonical hash of its
complete definition (ground truth, must_catch, FP allowances, known-FP
topics, weight, notes — sorted-keys JSON of the raw case.json), and that
case_sha joins the registered fingerprint beside base_sha and the frozen
plan bytes. Editing any scoring field after results exist now refuses the
subdirectory — outcome-dependent redefinition of ground truth under a
registered campaign is closed.

ground_truth[].rationale is schema-required and now enforced: the loader
rejects a missing, empty, or whitespace rationale (graders match findings
against it), and the schema pins minLength 1 so the documented contract and
the executable enforcement agree.

Regressions: missing/empty/whitespace rationale rejected; two cases
identical in plan bytes and base_sha but differing in a scoring field
produce different case_shas and different campaign fingerprints, and the
fingerprint echoes the loader's stamped hash.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 080c937ec0db49423c98c408fc672232b5a98d9b


Overall Assessment

Looks good — both prior P1 findings are resolved; no unmitigated P0/P1 issues remain.

Executive Summary

  • No estimator, weighting, variance, inference, or library default changed.
  • Campaign registration now binds the complete case definition and scoring metadata.
  • Missing, empty, and whitespace-only ground-truth rationales are rejected.
  • Documented fidelity, performance, and security limitations remain P3 informational.
  • Pytest was unavailable; 20 affected Python files parsed successfully and focused executable probes passed.

Methodology

Both prior findings are resolved:

  • Campaign fingerprints include the canonical whole-case hash, frozen plan hash, base SHA, arms, and repeat schedule (corpus_loader.py:L211-L218, run_eval.py:L378-L424). Real cases also require immutable full commit SHAs (corpus_loader.py:L300-L312).
  • Ground-truth rationale is required by both schema and loader, including nonblank enforcement (manifest.schema.json:L30-L46, corpus_loader.py:L104-L111). Regression coverage is at tests/test_plan_review_eval.py:L1883-L1947.
  • P3 — informational: Documented control/production fidelity differences remain (README.md:L101-L110).
    Impact: Known experimental limitations only.
    Concrete fix: None required.

The Methodology Registry is unaffected because no causal estimator behavior changed.

Code Quality

No findings.

Performance

  • P3 — tracked in TODO.md:L56
    Impact: A dual-review peer can survive until timeout after its counterpart fails.
    Concrete fix: None required for this PR.

Maintainability

No findings.

Tech Debt

No untracked deferrable work found.

Security

  • P3 — tracked/documented (TODO.md:L55, DEFERRED.md:L129)
    Impact: Codex read isolation and the non-adversarial hash-gate threat model are explicit limitations.
    Concrete fix: None required for this PR. No apparent secrets found.

Documentation / Tests

No findings. The new regressions cover both previously reported defects. Full pytest execution was unavailable because pytest is not installed in the review environment.

@igerber igerber added ready-for-ci Triggers CI test workflows and removed ready-for-ci Triggers CI test workflows labels Jul 20, 2026
… tests

The label-gated CI run failed only in the single-process Pure Python
Fallback leg: test_evals_runtime.py's 28 bare `import run_eval` sites are
ambiguous now that TWO harnesses ship a run_eval.py, and which module wins
depends on sys.path insert order at collection time. In CI's alphabetical
collection the plan-review-eval suite's insert displaced reviewer-eval's,
so the reviewer-eval tests executed against the WRONG harness (27
failures: missing CorpusLoader/_resolve_configs, foreign treatment
fields). Local runs always listed test_plan_review_eval.py first, which
masked the order dependence; the parallelized matrix legs split the suites
across workers and never saw it.

Fix mirrors test_plan_review_eval.py's existing loader: reviewer-eval
tests load their harness CLI by explicit file path under the unique module
name reviewer_eval_run_eval (lru-cached), eliminating the ambiguous bare
import entirely. With run_eval de-ambiguated, no shared bare module name
remains between the harnesses (adapters vs plan_adapters are distinct;
verdict.py exists only in plan-review-eval; eval_core is a single shared
copy). Verified in both collection orders: the CI-order repro goes 27
failures -> 0, and the original order stays green (299 passed).
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: b1944639758a8fbefc808433bb0dbe9f051a34b0


Overall Assessment

⚠️ Needs changes — one unmitigated P1 methodology issue remains.

Executive Summary

  • Both prior P1 findings are resolved: full case definitions are fingerprinted, and rationales are mandatory/nonblank.
  • No estimator, weighting, variance, inference, or library default changed; the Methodology Registry is unaffected.
  • A format-specific extraction rule can systematically disadvantage the control arm and produce a false GO verdict.
  • Documented fidelity, performance, and security limitations remain P3 informational.
  • Pytest was unavailable; 21 affected Python files parsed successfully and direct import/verdict probes passed.

Methodology

  • Severity: P1 [Newly identified] — Control-native questions are discarded by extraction.
    Impact: The control format requires a Questions for the Author section for implementation-blocking ambiguities (.claude/commands/review-plan.md:L426-L428), while the candidate emits only flat findings. The extractor categorically excludes “questions” (tools/plan-review-eval/candidates/extraction_prompt.md:L22-L27). Thus, the same defect can be dropped for A but retained for B, creating a spurious candidate improvement or GO. Raw-review reconciliation does not help when both graders agree on the incomplete extraction (tools/plan-review-eval/DECISION_RULE.md:L114-L116).
    Concrete fix: Extract substantive questions that identify missing decisions, contradictions, or execution blockers; exclude only non-defect clarification questions. Add a regression case where the sole control finding appears under Questions for the Author and verify it survives extraction equivalently to a candidate bullet.

Prior findings are resolved:

  • Whole-case scoring metadata is hashed and included in campaign registration (corpus_loader.py:L211-L218, run_eval.py:L378-L424).

  • Missing or blank rationales are rejected (corpus_loader.py:L104-L111).

  • Severity: P3 — documented fidelity differences (tools/plan-review-eval/README.md:L101-L110)
    Impact: Known experimental limitations only.
    Concrete fix: None required.

Code Quality

No findings.

Performance

  • Severity: P3 — tracked in TODO.md:L56
    Impact: A surviving dual-review subprocess may run until timeout after its peer fails.
    Concrete fix: Tracked cancellation work; no action required for this PR.

Maintainability

No findings.

Tech Debt

No untracked deferrable work found.

Security

  • Severity: P3 — tracked/documented (TODO.md:L55, DEFERRED.md:L129)
    Impact: Codex read isolation and the non-adversarial hash-gate threat model are explicit limitations. No apparent secrets were found.
    Concrete fix: None required for this PR.

Documentation / Tests

No additional findings. Pytest is not installed in the review environment; static parsing and focused executable probes passed.

Path to Approval

  1. Update extraction_prompt.md to preserve substantive defect-bearing questions.
  2. Add the control-question versus candidate-finding regression described above and rerun the dress rehearsal under the changed protocol.

…ings

The extraction stage no longer categorically excludes questions. The control
engine's native format raises implementation-blocking ambiguities under
"Questions for the Author", so a blanket question exclusion dropped those
findings for arm A while the candidate's flat-finding phrasing survived — a
systematic bias toward the candidate arms capable of manufacturing a false
GO. The rule now extracts a question as a finding when it identifies a
missing decision, contradiction, unresolved dependency, or execution
blocker (at least `major`; `blocker` when marked blocking), and excludes
only non-defect clarifications that assert no gap.

The dress rehearsal gains a format-parity extraction probe
(DECISION_RULE.md): a committed control-format sample review
(corpus/fixture/fx-mini-plan/control_format_review.md) whose ONLY mention
of the seeded nonexistent-symbol defect sits under the questions section
must survive extraction as a finding, or the rehearsal fails. A
deterministic contract test pins the prompt rules (no categorical
exclusion, substantive-question inclusion, qualified-only exclusion) and
the sample's sole-mention structure.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 1294333f5a41fce6f1fa272d997f4fe99e0837db


Overall Assessment

Looks good — no unmitigated P0 or P1 findings.

Executive Summary

  • The prior P1 is resolved: substantive defect-bearing questions are now extracted, while non-defect clarifications remain excluded (extraction_prompt.md:L26-L36).
  • The control-format fixture and mandatory rehearsal probe cover the reported bias (DECISION_RULE.md:L151-L159).
  • No estimator, inference, variance, weighting, assumptions, or library defaults changed; the Methodology Registry is unaffected.
  • Focused static and whitespace checks passed. pytest is unavailable in this environment.

Methodology

The affected methodology is the evaluation protocol only; no causal estimator or cited paper applies.

  • Severity: P3 — Documented fidelity differences (README.md:L101-L110)
    Impact: Known experimental limitations only.
    Concrete fix: None required.

The previous extraction-bias P1 is resolved.

Code Quality

No findings.

Performance

  • Severity: P3 — Tracked in TODO.md:L56
    Impact: A surviving dual-review subprocess may run until timeout after its peer fails.
    Concrete fix: No PR action required.

Maintainability

No findings.

Tech Debt

No untracked deferrable work found.

Security

  • Severity: P3 — Tracked/documented (TODO.md:L55, DEFERRED.md:L129)
    Impact: Codex read isolation and the non-adversarial hash-gate threat model remain explicit limitations.
    Concrete fix: None required.

Documentation / Tests

  • Severity: P3 — Live format-parity check is rehearsal-gated (DECISION_RULE.md:L151-L159, tests/test_plan_review_eval.py:L1904-L1937)
    Impact: CI checks the prompt and fixture structure; actual model extraction must pass the mandatory dress rehearsal before campaign execution.
    Concrete fix: None required for approval.

@igerber igerber added ready-for-ci Triggers CI test workflows and removed ready-for-ci Triggers CI test workflows labels Jul 20, 2026
The label-gated CI matrix is otherwise green (9768 passed); the one failure
was test_control_prompt_participates_in_protocol_identity, whose
load_artifacts call materializes the control criteria from the REAL pinned
SHA — absent in CI's depth-1 checkout. The step-1 plan's own rule (hermetic
tmp-repo tests cover the git-show mechanism; real-pin tests skip on shallow
clones) was applied to test_configured_pin_resolves_in_full_clone but
missed on this round-4 regression test.

The probe is factored into _skip_unless_pin_available(), shared by both
real-pin tests; the control-prompt test still asserts the snapshot's
control_prompt_sha plumbing unconditionally and skips only the
load_artifacts materialization when the pin is unreachable. Verified in a
local depth-1 clone (the exact CI condition): both tests skip, the rest of
the suite passes; full clone still runs both.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 341e4f4a598fa94489d24fef66fa189d2d61a631


Overall Assessment

Looks good — no unmitigated P0 or P1 findings.

Executive Summary

  • The prior extraction-bias finding remains resolved (extraction_prompt.md:L26-L36).
  • No estimator, weighting, variance, inference, assumptions, or defaults changed.
  • Evaluation gates match the pre-registered decision rule.
  • Python sources parse successfully; pytest is unavailable in this environment.

Methodology

  • Severity: P3 — Documented fidelity differences
    Impact: Evaluation-only differences from production are disclosed (README.md:L101-L110).
    Concrete fix: None required.

Code Quality

No findings.

Performance

  • Severity: P3 — Tracked limitation
    Impact: A surviving dual-review subprocess may wait until timeout after its peer fails (TODO.md:L56).
    Concrete fix: None required for this PR.

Maintainability

No findings.

Tech Debt

No untracked findings.

Security

  • Severity: P3 — Tracked/documented limitations
    Impact: Codex read isolation and the non-adversarial hash-gate threat model remain explicit (TODO.md:L55, DEFERRED.md:L129).
    Concrete fix: None required.

Documentation / Tests

  • Severity: P3 — Rehearsal-gated live extraction check
    Impact: Static tests cover the prior bias; actual model extraction must pass the required format-parity rehearsal (DECISION_RULE.md:L151-L159, test_plan_review_eval.py:L1915-L1948).
    Concrete fix: Run the prescribed rehearsal before campaign execution.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 21, 2026
@igerber
igerber merged commit 56443ae into main Jul 21, 2026
29 of 30 checks passed
@igerber
igerber deleted the plan-review-eval branch July 21, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant