Skip to content

plan-review: ship campaign-selected dual-reviewer engine as a .claude/skills/ skill (workflow-eval step 3) - #725

Merged
igerber merged 12 commits into
mainfrom
workflow-eval-plan-review-skill
Jul 24, 2026
Merged

plan-review: ship campaign-selected dual-reviewer engine as a .claude/skills/ skill (workflow-eval step 3)#725
igerber merged 12 commits into
mainfrom
workflow-eval-plan-review-skill

Conversation

@igerber

@igerber igerber commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Retires /review-plan + /revise-plan for a first-class .claude/skills/plan-review/ skill that runs the dual review engine (two blind reviewers — Claude @ Opus 4.8 + codex gpt-5.6-sol — then a merge/verify pass), degrading loudly to a single-Claude review when codex is unavailable. This is workflow-eval step 3 (the engine swap); the content-hash ExitPlanMode gate from step 1 is untouched.
  • The dual engine is the configuration Campaign 1 selected on sensitivity: dual reliably caught 7/9 must-catch plan defects vs 1/9 for every single-reviewer arm. Campaign 1 is EXPLORATORY / NON-GATING — under its own pre-registration the gates did not apply (base_sha-contaminated s3 negative controls, a trop-silent-drop candidate-criteria regression, and an Opus-vs-registered-claude-fable-5 model deviation). Every "validated"/"data-backed" claim was recalibrated to that honest framing across the verdict, DECISION_RULE.md, the eval README, SKILL.md, CLAUDE.md, and CHANGELOG; a clean re-validation is tracked.
  • Bundled artifacts: a tested render.py (strict __TOKEN__ renderer, byte-equivalent to the eval harness), codex_review.py (campaign-pinned codex model/effort; the Claude side uses the opus runtime family alias — the Task tool takes aliases, not exact IDs — documented as such, not an immutable pin), and merge_verify.md (production-adapted to name the two reviewers). The detection prompts criteria.md + reviewer_prompt.md are byte-identical to what the campaign graded.
  • Robustness: per-invocation scratch scoping (concurrent same-worktree reviews cannot cross-wire and stamp one plan's report under another's hash), a deterministic P-count→assessment rule (malformed reviewer output aborts rather than persisting a guess), a machine-readable engine-provenance marker the revise phase reads, and self-contained ingress (no reliance on shell-variable persistence across tool calls or Write-tool expansion).
  • plan_snapshot.py snapshot/persist/check + the hash-gate contract are unchanged; abort is now strict by default with an explicit --allow-missing no-op for post-persist cleanup.

Methodology references

  • N/A — internal workflow tooling. No estimator, identification, weighting, variance/SE, or docs/methodology/REGISTRY.md changes.

Validation

  • Tests added/updated: tests/test_plan_review_skill.py (prompt byte-match, render equivalence, behavioral codex_review tests, lifecycle contracts), tests/test_plan_snapshot.py (strict/--allow-missing abort lifecycle), tests/test_command_contract.py (migrated the retired-command contracts), tests/test_plan_review_eval.py (config pin). 179 tests pass across the skill/gate/helper/contract/eval suites; black + ruff clean.
  • Live end-to-end smoke exercised the dual engine on a throwaway plan (complementary reviewers, consensus/named attribution, per-finding verification, and the gate allow→deny-after-edit→allow cycle).

Known / tracked

  • The codex reviewer runs --sandbox read-only, which blocks writes but does not confine READS to the repository — the same accepted surface /ai-review-local --backend codex already ships with. It is disclosed in SKILL.md and tracked for OS-level isolation in TODO.md; plans are authored in the user's own session.
  • Tracked follow-ups: clean-negative precision re-run, trop-silent-drop criteria re-validation, dual-mode peer cancellation, and the (descoped) --updated/--pr modes.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

igerber added 9 commits July 24, 2026 07:01
Records the outcome of the first plan-review measurement campaign and pins
the arm config to the production reviewer.

- config/configs.json: A/B/C/E Claude side re-pinned claude-fable-5 ->
  claude-opus-4-8 (production default moved to Opus 4.8); cli_version bumped
  to the campaign build 2.1.217. D stays claude-sonnet-5 (the cheaper-Claude
  probe). All single-field contrasts preserved (A-vs-B variant, B-vs-C mode,
  B-vs-D model, C-vs-E codex model).
- verdicts/campaign-1.md (new): committed aggregate result — sensitivity
  scorecard (dual+codex-sol 7/9 must-catch vs 1/9 single), precision
  (hallucination rate 3.4% dual / 0% single), the codex-sol-vs-terra and
  criteria-rewrite contrasts, and the negative-control base_sha-contamination
  lesson. Numbers and lessons only; the real-plan corpus stays gitignored.
- DECISION_RULE.md: "Campaign 1 outcome" note — the pre-registered
  dual-vs-single and default-model questions are now data-backed; records that
  the FP gate was not applied (contaminated negatives) and precision was
  re-measured as true-vs-false hallucination rate.
- test_plan_review_eval.py: the provenance-snapshot test reads arm A's model
  from the config instead of hardcoding it, so it survives the pin change.
Promotes the Campaign 1 winner (candidate criteria + dual review: Claude @
Opus 4.8 + codex gpt-5.6-sol, merge/verify) into a first-class Agent Skill and
retires the single-reviewer commands. The content-hash ExitPlanMode gate and
snapshot helper are engine-agnostic and untouched.

- .claude/skills/plan-review/: SKILL.md owns the review->revise cycle;
  render.py (strict __TOKEN__ renderer mirroring criteria_source.render, byte-
  equivalent — never free-text substitution) with a CLI so prompts render in
  tested Python; codex_review.py runs reviewer 2 with the validated pins
  (gpt-5.6-sol/xhigh/600s) and loud-fallback exit codes; criteria.md,
  reviewer_prompt.md, merge_verify.md bundled as-validated (byte-identical to
  candidates/). Dual: Opus reviewer subagent + codex-sol, blind, then a merge/
  verify subagent; codex absent/timeout/error -> loud single-Claude fallback.
- CLAUDE.md "Plan Review Before Approval": the gate now invokes the skill
  instead of spawning a single review agent over review-plan.md.
- Retire .claude/commands/review-plan.md + revise-plan.md; their plan-review
  contract tests migrate to tests/test_plan_review_skill.py (targeting
  SKILL.md + CLAUDE.md), and review-plan/revise-plan drop out of
  test_command_contract.py's _HARDENED set. The harness control arm still
  sources the old criteria via `git show <sha>:` from history, so the deletion
  breaks nothing.
- tests/test_plan_review_skill.py: artifact byte-match to the graded
  candidates, render byte-equivalence + raise-on-unfilled-token, the codex/
  Claude reviewer pins, snapshot routing, and the migrated command contracts.
- CHANGELOG: one internal-tooling entry.
…de, reviewer names

Two locked pre-campaign decisions that the Step-3 plan under-implemented, now
restored (both verified against the decision ledger):

- Adaptive three-way recommendation: the ExitPlanMode gate ALWAYS offers Dual /
  Single / Skip, with the (Recommended) tag chosen adaptively by plan
  complexity (substantive/high-risk -> Dual; localized/mechanical -> Single;
  trivial -> Skip), not a fixed default. SKILL.md gains a deliberate
  single-reviewer mode (Opus alone), distinct from the codex-unavailable
  fallback.
- Names in the persisted file: production merge_verify names the reviewer on
  single-reviewer findings ([single reviewer: claude] / [single reviewer:
  codex]) so a human can weigh the complementary strengths the campaign found
  (codex on methodology/math, Claude on codebase-structure facts). criteria.md
  + reviewer_prompt.md stay byte-locked to the graded copies; only
  merge_verify's OUTPUT LABELING is un-blinded (the campaign blinded it for
  grading) — detection/verify logic unchanged, no re-validation.

Two carried-open design questions resolved to no-change: the initial review
does not re-dump the plan (already in context from plan mode; revise phase
shows both), and --pr feedback-coverage stays an explicit capability passed by
read-feedback-revise, not auto-detected in the gate.

Tests updated: adaptive-offer + single-mode assertions, merge_verify
production-adaptation, and the detection-prompt byte-lock split from
merge_verify.
Resolve the findings from the local codex review of the plan-review skill.

Decisions (user-directed):
- CQ1 descope: remove the advertised-but-unimplemented `--updated`/`--pr`
  modes from SKILL.md Modes; they are named only as a tracked TODO follow-up.
- M2 timeout: widen the codex ceiling 600s -> 1200s and loosen the claim -
  the timeout is an interactive-gate choice, NOT a campaign-validated pin (the
  campaign ran unattended at 3600s); only model/effort are campaign pins.

Mechanical fixes:
- CQ2: Step 8 is now advisory (surface findings only). Plan edits move to the
  Revise phase, which re-reviews and re-stamps - editing after persist would
  invalidate plan_sha256 and re-deny ExitPlanMode.
- MT1: the Revise re-review re-runs in the SAME engine mode the existing review
  used (dual vs deliberate-single), read from the review body - no silent
  upgrade to dual.
- MT2: Revise honors the helper's `fresh` flag - a stale review (plan already
  changed) triggers a fresh Review pass before revising.
- TD1: a snapshot-lifecycle callout requires `plan_snapshot.py abort` on ANY
  post-snapshot failure/stop, not only persist exit 3.
- S1: codex_review.py runs openai_review's sensitive-file scan + notice before
  call_codex (the direct entry bypassed main() where the /ai-review-local codex
  path prints it).
- Perf: reviewer 1 (Opus subagent) and reviewer 2 (codex) launch concurrently.
- M1: campaign-1 verdict precision table adds the `unverif.` column + a Σ row
  so every row reconciles (findings = true + false + unverifiable; 213 total).

Tests: bump the timeout pin assertion to 1200; add a descope contract test and
behavioral tests for codex_review.main (pins forwarded, sensitive-notice fires,
exit-code contract 0/2/3). TODO.md gains the descope, clean-negative-rerun, and
trop-silent-drop follow-up rows.
…sessment, engine provenance

Address the round-2 local codex review (⛔) of the plan-review skill.

P0 — concurrency integrity: the four intermediate prompt/review files used
fixed $SCRATCH names, so two concurrent same-worktree reviews could cross-wire
and persist one plan's merged report under another plan's certified hash,
silently defeating the gate. Each invocation now gets a private `<work_dir>`
(mktemp) that holds all intermediates; it is removed on persist and on every
abort/failure path. (The helper's own snapshot/state/body/meta paths were
already nonce-unique.)

P0 — codex read surface (user decision: keep parity + tracking): document the
codex `--sandbox read-only` cross-repo read surface in-skill, the same surface
/ai-review-local already documents and accepts, cross-linking the existing
TODO.md OS-isolation follow-up. No feature gate.

P2 fixes:
- Assessment is now derived by a deterministic P-count -> label rule (P0/P1 ->
  Significant; P2 -> Minor; P3/none -> Ready); malformed reviewer output aborts
  instead of persisting a guessed assessment.
- Engine provenance is a machine-readable `<!-- plan-review-engine: ... -->`
  body marker (dual / single / single-fallback); Revise reads the marker rather
  than inferring the mode from fragile prose.

Tests: contract tests for invocation-scoping, the deterministic assessment
rule + malformed-abort, the provenance marker + revise wiring, and the codex
read-surface documentation. TODO.md gains the dual-mode peer-cancellation
follow-up (the remaining P2, deferred). 96 tests pass across the skill/gate/
helper/contract suites.
…und-3 fixes

Address the round-3 local codex review (⚠️) of the plan-review skill.

Campaign claims (user decision: recalibrate + ship). The campaign supports
"dual >> single" on sensitivity (7/9 vs 1/9, 0 regressions B->C), but it is
NON-GATING under its own pre-registration: the s3 negative controls were
base_sha-contaminated (FP gate invalid, corpus floor unmet), the candidate
criteria carried a must-catch regression (trop-silent-drop, a NO-GO by gate 1),
and the run used claude-opus-4-8 where the arms register claude-fable-5. The
shipped engine also differs from the graded one (un-blinded merge prompt, opus
alias). So "validated / data-backed / as-validated" overclaimed. Recalibrated
that language to an honest "exploratory / campaign-selected, NON-GATING" framing
that names the deviations and points to the tracked clean re-validation, across
DECISION_RULE.md, verdicts/campaign-1.md, the eval README, SKILL.md, CLAUDE.md,
and CHANGELOG. Dual stays the default (the sensitivity signal is robust and
independent of the unmet FP gate); we ship arm C as run (candidate criteria +
dual), carrying trop-silent-drop as a tracked follow-up. The bundled-prompt
description now distinguishes the byte-identical detection prompts from the
production-adapted (un-blinded) merge_verify.md.

Mechanical fixes:
- Skip branch (CLAUDE.md) now `mkdir -p "$SCRATCH"` before writing plan-path.txt
  — on a fresh worktree .git/plan-review does not exist, so the Write failed.
- plan_snapshot.py `abort` is now idempotent when the state file is absent:
  persist self-cleans on exit 3 / altered-snapshot, so a follow-up abort finds
  nothing and must succeed (not error). SKILL.md step 7 now aborts uniformly on
  any non-zero persist + rm -rf work_dir.
- Eval README status updated from "campaign has not run" to the exploratory /
  NON-GATING outcome.

Tests: CLAUDE.md Skip-branch scratch-init contract; idempotent-abort lifecycle
test. 98 tests pass across the skill/gate/helper/contract suites; 106 eval-harness
tests pass; black + ruff clean.
Address the round-4 local codex review (⚠️) of the plan-review skill.

P1 (executability) — the SKILL.md and CLAUDE.md ingress relied on `$SCRATCH`/
`$SKILL` shell variables surviving across separate Bash tool calls and the Write
tool expanding `$SCRATCH/plan-path.txt`; neither holds (the codebase's own
submit-pr/pr_prepare convention). Fixed every ingress: the skill dir is now the
literal `.claude/skills/plan-review/` (no `$SKILL`), the scratch dir is derived +
`echo`'d so its printed literal is what the Write tool receives, and every
snapshot/check Bash block re-derives the scratch path inline via `$(git
rev-parse --git-path plan-review)`. Covers the dual, single, revise, and skip
paths.

P2 fixes:
- Both Skip paths (SKILL.md revise-Skipped, CLAUDE.md Skip) now `abort` on a
  pre-persist failure so the helper-managed snapshot is not retained.
- Model provenance: `model: "opus"` is documented as a runtime family alias (the
  Task tool takes aliases, not exact IDs), NOT an immutable pin — matching the
  recalibrated campaign claims.
- Test rigor: `test_reviewer_invocations_are_pinned` now asserts BOTH the
  reviewer and merge subagent sections select opus (not one match anywhere); new
  contract tests assert ingress self-containment and Skip-path abort-on-failure.

P3: CHANGELOG now says the hash-gate + snapshot/persist contract are unchanged
while `abort` is idempotent (was inaccurately "unchanged").

100 tests pass across the skill/gate/helper/contract suites; black + ruff clean.
… note

Address the genuine (mechanical) findings of the round-5 local codex review.
The round-5 ⛔ is driven by the codex read-surface P0, which the user already
adjudicated (round 2: keep parity + tracking — the same surface /ai-review-local
ships with, tracked at TODO.md); no code change there.

P2 — `abort` over-permissive: round 3 asked for idempotent abort; round 5 noted
idempotent-by-default silently accepts a wrong/typo'd/cross-wired state token,
masking a leaked snapshot. Resolved both with an explicit flag: `abort` now
FAILS (exit 2) on a nonexistent state by default, and only `--allow-missing`
downgrades that to a no-op. The skill's post-snapshot cleanup calls pass
`--allow-missing` (persist self-cleans on exit 3 / altered-snapshot), so it can
clean up uniformly without masking a wrong token. Strict default protects the
eval harness and any direct/future caller.

P2 — configs.json protocol drift: arms A/B/C/E declare claude-opus-4-8 while
DECISION_RULE.md registers claude-fable-5. Added a `_deviation_note` making this
explicit: the file is the AS-RUN exploratory Campaign 1 config (Opus override);
the immutable pre-registration stays fable-5; a gating re-validation would add a
fresh versioned config. Consistent with the recalibrated NON-GATING framing.

Also clarified the codex-surface test: it verifies the accepted-surface
DISCLOSURE (per the parity+tracking decision), not a filesystem boundary.

179 tests pass across the skill/gate/helper/contract/eval suites; black + ruff
clean; configs.json valid.
@github-actions

Copy link
Copy Markdown

Overall Assessment

⚠️ Needs changes — No causal estimator or statistical methodology is affected. One unmitigated P1 remains in the new workflow’s shell-path handling.

Executive Summary

  • No estimator, weighting, variance, inference, assumption, or default behavior changed.
  • P1: generated paths are reinserted into shell source, allowing $() or backticks in worktree paths to execute.
  • Snapshot cleanup can silently fail because production paths always use --allow-missing.
  • Campaign limitations, Codex isolation, peer cancellation, and retired flags are properly tracked.

Methodology

No findings. No causal methods or Registry sections are affected; paper cross-checking is not applicable.

Code Quality

P2 — Snapshot cleanup can be silently skipped

Impact: On an ingress race, the skill reruns without aborting the first snapshot. Additionally, every cleanup uses --allow-missing, which necessarily masks a mistyped state token despite the prose claiming otherwise. Full plan text can remain orphaned under .snapshots/.

Location: .claude/skills/plan-review/SKILL.md:L82-L114, .claude/skills/plan-review/SKILL.md:L242-L257

Concrete fix: Strictly abort the mismatched snapshot before rerunning. Use unflagged abort before persist; reserve --allow-missing only for a persist invocation known to have self-cleaned.

P2 — Assessment validation trusts the reviewer’s table

Impact: A report containing a P0/P1 finding but an inconsistent zero-count table can be persisted as Ready to implement.

Location: .claude/skills/plan-review/SKILL.md:L234-L249

Concrete fix: Add a Python parser that counts finding lines, validates the summary table against them, and derives metadata; reject any mismatch.

Performance

P3 — Dual-review peer cancellation is deferred

Impact: A failed reviewer may leave its peer running for up to 1200 seconds.

Concrete fix: None required for this PR; tracked in TODO.md:L58.

Maintainability

No additional findings.

Tech Debt

P3 — Tracked limitations

The retired --updated/--pr modes, clean-negative rerun, and trop-silent-drop criteria regression are explicitly tracked in TODO.md:L57-L60. No action required for approval.

Security

P1 — Printed paths are reparsed as shell syntax

Impact: <work_dir>, <snapshot_path>, and <state_path> are inserted into later Bash commands inside double quotes. Shell command substitution still executes inside double quotes, so a linked-worktree/common-directory path containing $() or backticks can run unintended commands. This is an accidental path-handling issue, not the deferred hostile-local-actor gate model.

Location: .claude/skills/plan-review/SKILL.md:L94-L125, .claude/skills/plan-review/SKILL.md:L161-L181, .claude/skills/plan-review/SKILL.md:L251-L258

Concrete fix: Keep full paths out of generated shell source. Prefer a Python orchestrator using argv arrays, or pass only validated safe nonce tokens and derive paths inside Python. Add regression coverage using a git/worktree path containing literal $() and backticks.

P3 — Codex read isolation

The broader filesystem read surface is disclosed and tracked in TODO.md:L55; no action required here.

Documentation / Tests

P2 — Tests miss production cleanup and path interpolation

Impact: Tests verify strict abort only without --allow-missing, while actual workflow call sites always pass it. They also reject placeholder assignments but not direct placeholder arguments.

Location: tests/test_plan_snapshot.py:L181-L201, tests/test_plan_review_skill.py:L195-L203

Concrete fix: Add behavioral tests for wrong production state tokens, ingress-mismatch cleanup, and metacharacter-bearing worktree paths.

P3 — Changelog overstates abort idempotence

Impact: abort is strict by default; only abort --allow-missing is idempotent.

Location: CHANGELOG.md:L23-L26

Concrete fix: Name the flag explicitly.

Path to Approval

  1. Eliminate reinsertion of generated full paths into Bash source and add a regression test using $()/backticks in the worktree path.

…nt + assessment hardening

Address the authoritative CI codex review (⚠️). Note: the recalibration worked —
the codex read-surface finding is now correctly P3/tracked, not the local
reviewer's insisted P0.

P1 (security) — printed paths reparsed as shell. The round-4 ingress rework had
the agent paste a `mktemp`-under-`$(git rev-parse --git-path plan-review)` work
dir into later Bash commands; command substitution executes inside double
quotes, so a repo/worktree checked out at a path containing `$()`/backticks
would execute it. Fixed at the root: `plan_snapshot.py` now EMITS the per-
invocation work dir under its own snapshots dir (same strict [a-f0-9.] leaf,
HOME-prefixed), and `_cleanup_invocation` removes it (persist + abort). The skill
uses the helper-emitted `work_dir` — no repo/worktree path is composed into a
shell command anywhere. This also drops the manual `rm -rf` and the mktemp step.

P2 — `--allow-missing` over-applied. Reserved it for the ONE post-persist abort
(where persist may have self-cleaned); pre-persist aborts are plain, so a
mistyped/cross-wired state token fails loudly instead of silently no-op'ing.

P2 — assessment trusted the reviewer's table. Now the label is derived from the
actual finding LINES (counted by severity tag), cross-checked against the
summary table; a mismatch or malformed report aborts rather than persisting a
guessed assessment.

P2 (minor) — an ingress-race rerun now aborts the first (orphaned) snapshot
before re-running.

CHANGELOG corrected to describe the helper accurately (emits/cleans a work dir;
strict `abort` + `--allow-missing`). Tests: work_dir emission + persist/abort
cleanup, the abort-placement contract, and updated scoping/assessment contracts.
210 tests pass across the skill/gate/helper/contract/eval suites; black + ruff
clean.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 5bf026f66825c4f55c8825f16f695a509821164f


Overall Assessment

⚠️ Needs changes — No causal methodology is affected, but the prior shell-path P1 remains partially unresolved.

Executive Summary

  • No estimator, weighting, variance, inference, assumptions, or defaults changed.
  • The worktree-path injection was fixed, but helper paths still inherit an unsafe $HOME prefix.
  • Assessment validation now fails closed and no longer trusts only the reviewer table.
  • Cleanup gaps remain P2; campaign and isolation limitations are tracked.

Methodology

No findings. The PR changes internal workflow tooling only; docs/methodology/REGISTRY.md and causal estimators are unaffected.

Code Quality

P2 — Post-persist cleanup can still mask a wrong token

Impact: After any failed persist, the workflow runs abort --allow-missing. If the original state token was mistyped, both operations target the wrong path and cleanup reports success while the real snapshot remains.

Location: .claude/skills/plan-review/SKILL.md:L245-L251, CLAUDE.md:L279-L284, .claude/scripts/plan_snapshot.py:L318-L342

Concrete fix: Use --allow-missing only for exit codes known to self-clean, or make persist always clean a successfully loaded invocation before returning.

P2 — Work-directory creation is not transactional

Impact: The snapshot is written before work_dir is created. If directory or state creation fails, no state token is returned and the snapshot/work directory cannot be aborted normally.

Location: .claude/scripts/plan_snapshot.py:L181-L200

Concrete fix: Wrap snapshot, work-directory, and state creation in rollback logic that removes every partially created artifact.

Performance

P3 — Dual-review peer cancellation is tracked

Impact: A failed reviewer can leave its peer running for up to 1200 seconds.

Concrete fix: None required here; tracked in TODO.md:L58.

Maintainability

No findings. The prior assessment-table issue is addressed by line counting, table cross-checking, and fail-closed handling.

Tech Debt

P3 — Tracked limitations

Impact: Retired flags, clean-negative validation, and the trop-silent-drop criteria regression remain deferred.

Concrete fix: None required for approval; tracked in TODO.md:L57-L60.

Security

P1 — Prior shell-path injection remains through $HOME

Impact: Generated leaves are safe, but their parent comes from os.path.expanduser("~"). The resulting full snapshot_path, work_dir, and state_path are pasted into double-quoted Bash source. A home path containing literal $() or backticks executes command substitution. Thus the prior “keep full paths out of generated shell source” finding is only partially resolved.

Location: .claude/scripts/plan_snapshot.py:L129-L132, .claude/scripts/plan_snapshot.py:L169-L210, .claude/skills/plan-review/SKILL.md:L85-L117, .claude/skills/plan-review/SKILL.md:L153-L173, .claude/skills/plan-review/SKILL.md:L245-L251

Concrete fix: Pass only a validated opaque invocation ID through Bash and derive absolute paths inside Python, or use a Python orchestrator that consumes paths strictly as data.

P3 — Codex read isolation is tracked

Impact: Codex can read outside the repository despite the read-only write sandbox.

Concrete fix: None required here; disclosed and tracked in TODO.md:L55.

Documentation / Tests

P2 — Path regression test checks only safe leaves

Impact: The test uses a normal temporary $HOME, so it passes while the remaining parent-path injection is present.

Location: tests/test_plan_snapshot.py:L164-L190, tests/test_plan_review_skill.py:L368-L383

Concrete fix: Add behavioral coverage with $HOME containing literal $() and backticks across render, Codex, persist, and abort invocations.

Validation note: Python AST and render smoke checks passed. pytest was unavailable in the review environment.

Path to Approval

  1. Stop reinserting helper-emitted absolute paths into Bash; pass a safe invocation token or use Python argv orchestration.
  2. Add a regression test using a $HOME path containing literal $() and backticks, asserting no command executes.

…ctional snapshot

Address the authoritative CI codex re-review (⚠️). It confirmed the git-path
injection was fixed and accepted the assessment hardening; the remaining P1 was
the same class one prefix deeper (the helper's HOME-derived path prefix), plus
two P2s. Per the chosen approach (fail-closed validation gate):

P1 (security) — HOME-prefix path injection. Every path plan_snapshot.py emits is
pasted into the caller's shell as a quoted literal; the digest+nonce leaf is safe
by construction but the HOME-derived prefix was not (a `$HOME` containing
`$()`/backticks executes under command substitution). `_plans_home()` now
validates the resolved plans dir and FAILS CLOSED if it carries a shell
metacharacter ($ ` " \\ or newline), so no unsafe path is ever emitted. All
snapshot/persist/check/abort paths inherit the guard.

P2 — post-persist cleanup could mask a wrong token. Made `persist` self-clean the
ENTIRE invocation (snapshot + work_dir + sidecars) on every post-load failure,
so the skill no longer runs any abort AFTER persist. `--allow-missing` is removed
entirely and `abort` is strict (a missing state token is an error), ending the
round-3→5→CI oscillation on that flag.

P2 — non-transactional snapshot. Snapshot + work dir + state are now created as a
unit; any failure rolls back every partial artifact rather than orphan a snapshot
with no state token to abort.

SKILL.md / CLAUDE.md updated (plain pre-persist abort only; no post-persist abort;
no `--allow-missing`). CHANGELOG corrected. Tests: fail-closed validation gate,
persist-self-cleans-on-failure, transactional/work-dir cleanup, strict abort.
211 tests pass across the skill/gate/helper/contract/eval suites; black + ruff
clean.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 5c99ecaf19596b2506a9cd04928156d2ed5b655a


Overall Assessment

Looks good — Prior P1 path-injection and cleanup findings are resolved. Only P2/P3 documentation and test gaps remain.

Executive Summary

  • No causal estimator, methodology, weighting, variance, inference, or default behavior changed.
  • Unsafe $HOME paths now fail closed before helper-generated paths reach shell commands.
  • Snapshot creation is transactional; persist self-cleans; abort rejects missing tokens.
  • Campaign limitations and Codex isolation are properly tracked.
  • Two minor documentation/test inconsistencies remain.

Methodology

No findings. No methods are affected, and docs/methodology/REGISTRY.md is unchanged.

Code Quality

No findings. The prior cleanup issues are addressed in .claude/scripts/plan_snapshot.py:L224-L238 and :L289-L369.

Performance

P3 — Peer cancellation remains tracked

Impact: A surviving reviewer can continue until its timeout after its peer fails.

Concrete fix: None required for approval; tracked in TODO.md:L58.

Maintainability

P3 — Helper is incorrectly labeled “UNCHANGED”

Impact: The skill says the snapshot helper is unchanged even though this PR adds work_dir, transactional creation, strict abort, and self-cleaning persistence.

Location: .claude/skills/plan-review/SKILL.md:L14-L16, :L62

Concrete fix: Say the hash-certification contract is unchanged while the helper lifecycle is extended.

Tech Debt

P3 — Deferred workflow limitations are tracked

Impact: Retired flags, clean-negative validation, and the known criteria regression remain pending.

Concrete fix: None required; tracked in TODO.md:L57-L60.

Security

P3 — Codex read isolation is tracked

Impact: Read-only mode does not prevent Codex from reading outside the repository.

Concrete fix: None required here; disclosed and tracked in TODO.md:L55.

The prior shell-path P1 is resolved by .claude/scripts/plan_snapshot.py:L138-L159.

Documentation / Tests

P2 — Non-gating campaign is still called “validated”

Impact: Maintainers could mistake exploratory, non-gating results for successful validation.

Location: .claude/skills/plan-review/codex_review.py:L3-L5, :L24-L25; .claude/skills/plan-review/render.py:L3-L9; tests/test_plan_review_skill.py:L3-L5

Concrete fix: Replace “campaign-validated” with “campaign-graded,” “as-run,” or “selected from exploratory sensitivity results.”

P2 — Unsafe-home regression covers $() but not backticks

Impact: Removing backtick detection from the validation regex would not fail this regression test.

Location: tests/test_plan_snapshot.py:L232-L246

Concrete fix: Parameterize the test with both $() and literal-backtick home paths.

Validation: git diff --check and Python AST parsing passed. pytest was unavailable in the review environment.

…e helper, backtick test

Final P2/P3 items from the ✅ CI review (no P0/P1 outstanding):
- Replace the last 'campaign-validated' remnants in codex_review.py, render.py,
  and the skill-test docstring/assert with 'campaign-graded' / 'as-run' /
  'unmeasured' — consistent with the exploratory/NON-GATING framing.
- SKILL.md no longer calls the snapshot helper 'UNCHANGED': its hash-certification
  contract is unchanged, but the lifecycle is extended (work dir, transactional
  creation, self-cleaning persist, strict abort).
- The fail-closed-home regression test now covers backticks as well as $().

49 targeted tests pass; black + ruff clean.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: ed0c1baa024ca0a5ba51b9a3db77d37d705a178f


Overall Assessment

Looks good — No unmitigated P0/P1 findings. Prior review findings are resolved; remaining limitations are explicitly tracked.

Executive Summary

  • No estimator, weighting, variance, inference, assumptions, or defaults changed.
  • Campaign language now consistently identifies Campaign 1 as exploratory/non-gating.
  • Helper lifecycle wording and backtick-path regression coverage are corrected.
  • Remaining isolation, cancellation, and validation work is tracked in TODO.md.
  • Python AST validation passed; pytest was unavailable in the review environment.

Methodology

No findings. docs/methodology/REGISTRY.md and causal methods are unaffected.

Code Quality

No findings. Transactional creation and self-cleaning persistence are consistently implemented in .claude/scripts/plan_snapshot.py:L189-L238 and :L277-L370.

Performance

P3 — Peer cancellation is tracked

Impact: A surviving reviewer may run until the 1200-second timeout after its peer fails.

Concrete fix: Surface the Codex process handle and cancel the peer on failure, as tracked in TODO.md:L58.

Maintainability

No findings. The prior inaccurate “unchanged helper” wording is corrected in .claude/skills/plan-review/SKILL.md:L14-L19.

Tech Debt

P3 — Deferred workflow and validation work is tracked

Impact: Delta/PR modes, clean-negative precision measurement, and the known criteria blind spot remain deferred.

Concrete fix: Follow the concrete entries in TODO.md:L57-L60; no approval action required.

Security

P3 — Codex read isolation remains tracked

Impact: Read-only mode prevents writes but does not restrict reads to the repository.

Concrete fix: Add OS-level isolation as tracked in TODO.md:L55. The limitation is disclosed at .claude/skills/plan-review/SKILL.md:L146-L154.

Documentation / Tests

No findings. The prior campaign-language issue is corrected in codex_review.py:L3-L5, render.py:L3-L9, and the tests. Both $() and backtick unsafe-home cases are covered in tests/test_plan_snapshot.py:L232-L248.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 24, 2026
@igerber
igerber merged commit 5ced8b4 into main Jul 24, 2026
29 of 30 checks passed
@igerber
igerber deleted the workflow-eval-plan-review-skill branch August 15, 2026 16:49
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