* Add writing-lean-proofs plugin Structured Lean 4 proof writing and library design following Mathlib conventions: sorry-skeleton workflows, API-first definitions, lemma extraction, and an anti-pattern catalog mapped to the linters that enforce each rule. Includes a review-flow eval suite (evals/): five Lean fixtures derived from a real formal-verification project with known planted flaws and known non-flaws, natural review prompts, per-case rubrics, and a runner comparing a baseline arm against a skill arm with an LLM judge plus a deterministic no-rewrite check. The grader ships a self-test that asserts a known-bad review fails every criterion. Smoke-tested: the baseline arm reproduces the folk-advice mistake the skill corrects (calling redundant `show` lines noise); the skill arm passes 5/5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Claude review findings - Validate the grader's "id" field before writing grades.json, so a malformed grader response fails with a clear error instead of a KeyError in the summary. - Restore title-case "When to Use"/"When NOT to Use" headings: the validator's required-section check matches them case-sensitively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address github-actions review findings Eval harness: - checksum_tree fails when it finds zero .lean files, so the no-rewrite check can no longer pass vacuously - both arms run with --setting-sources project, so a user-level install of this skill cannot contaminate the baseline arm - a failed case is recorded and the run continues; the runner exits non-zero after printing the summary - critical steps in run_case return explicitly (errexit is suppressed when the function runs in an if-context) - self-test prints its artifact dir; the on-failure leak is intentional - case 04: dropped the trailing omega so the squeezed simp only in carry_le_one is genuinely terminal (the direction test was inverted) - case 05: overall-verdict retyped from must-flag to overall, with the grader prompt told to judge such criteria solely by pass-when text - fixture overlap removed: Felt.lean's unscored unfold replaced by a bound-free lemma; Bounds.lean's unscored Fact instance replaced by the NeZero instance ZMod.val_lt actually needs Skill content (claims verified against Mathlib docs and the ImProver paper): - lake build alone does not catch sorries (they are warnings); step 4 now includes an explicit grep gate - style linters are enabled in Mathlib's own build but off by default downstream; the opt-in is now spelled out. The show, nameCheck, and setOption linters do exist, so those attributions stand. - ImProver's 100% is on the paper's accuracy metric and holds by construction (fallback to unchanged input); now stated as such - isCompact_union does not exist in Mathlib; example is isCompact_iUnion - library-design.md now distinguishes rfl-proved API lemmas (correct) from downstream proofs needing rfl (the smell) - plugin README Contents paths fixed to be relative to the skill dir Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address second-round github-actions review findings Eval harness: - case 05 gains an engagement criterion: the review must name specific declarations from the file, so an empty "looks fine" can no longer score 5/5 by omission - the grader self-test is now two-sided: the canned bad review must fail every criterion (catches a permissive judge) and a new canned good review must pass every criterion (catches an over-strict judge) - case 01 scores the native_decide in the Fact instance; it was the most severe flaw in the fixture and previously unscored - checksum_tree excludes .claude/ so both arms fingerprint the same file set - runtime check on the isolation guard: a baseline transcript that mentions writing-lean-proofs fails the case as contaminated - results stamp includes the PID so same-second runs cannot collide - case 04: mod_add_carry_mul uses non-terminal simp [carry] (a bare simp risked "no progress"/goal-closing errors that would invalidate the criterion); rubric wording updated Skill content: - the sorry check had inverted exit status for CI use (grep exits 1 on no match); now ! grep, with the comment/docstring false-positive caveat and #print axioms alternative spelled out - anti-patterns.md documents native_decide (trust-base widening, caught by #print axioms, kernel/certificate alternatives) - llm-techniques.md sibling links demoted to plain-text mentions: AGENTS.md prohibits reference chains (file1 -> file2), which the earlier link conversion had introduced Verified: two-sided self-test passes (bad 0/7, good 7/7); live case 05 run scores baseline 2/6 vs skill 6/6 with no-rewrite passing in both arms and no contamination false-positive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add linting, performance, and tactics references to Lean skill * Make the eval harness's own checks able to fail Five review findings, all cases of a check that reports rather than gates. - no-rewrite gates the exit status. It was written, printed, and otherwise ignored: CASE_FAILURES only counted reviewer/grader *errors*, so a reviewer that started applying its own fixes under --permission-mode acceptEdits would rewrite every fixture, print no-rewrite:fail on every line, and still exit 0 — CI would read the run as green. - Add the skill arm's mirror of the baseline contamination backstop. cp -R succeeding proves the skill is on disk, not that the CLI discovered it. If project-skill discovery under --setting-sources project ever changes, the skill arm would run bare and both arms would score alike — which is indistinguishable from the true negative "the skill provides no uplift", the one conclusion this suite exists to measure. One canary call per run asks the CLI what it can see. - Guarantee the scratch tree is removed. run_case's body moved into run_case_body so the single rm -rf covers every early return; six paths (fixture copy, skill copy, both checksums, the reviewer call, the contamination check) previously leaked a fixture copy — plus a full skill copy on the skill arm — per case. - Run the offline half of --self-test before the CLI preflight, so the schema, isolation-fixture and rubric checks work with no `claude` on PATH and no authentication — which is where you would want them, e.g. a CI shell-test suite. - Count rubric criteria with the same pattern the Python capture uses. The shell count accepted `- id: foo bar`, which the capture drops, so a malformed line surfaced later as a confusing id-mismatch diff. Also anchor linting.md's warnings-as-errors CI snippet against vacuity: Lake caches per-module artifacts and linter warnings are emitted only on recompile, so on a restored cache build.log is empty, the grep matches nothing, and the gate reports clean over live warnings and sorries — the exact failure the same file preaches against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: kz-tob <kara.zaffarano@trailofbits.com> Co-authored-by: Marc Ilunga <marc.ilunga@trailofbits.com>
7.7 KiB
Evals: writing-lean-proofs (review flow)
This suite tests one flow of the writing-lean-proofs skill: reviewing an
existing Lean file ("review this Lean file", "how could these proofs be
improved"). It does not test proof writing, library design from scratch, or
refactoring.
What a case is
Each case under cases/<name>/ is:
input/*.lean— a small, self-contained fixture with known planted flaws and known non-flaws (correct code that folk advice wrongly flags). Fixtures are close derivatives of a real Trail of Bits formal-verification project; the flaws are real patterns observed there, not inventions.prompt.md— the user prompt, phrased the way a user actually asks ("Please review the Lean file X.lean").rubric.md— grading criteria.must-flag: the review names the specific declaration, explains why it's a problem, and proposes a fix.must-not-flag: the review does not assert a known non-issue as a problem.
Every case also gets a deterministic no-rewrite check: the reviewer was asked for a review, so the fixture files must be byte-identical afterwards. A rewritten fixture fails the run — the check gates the exit status, it is not only reported in the summary.
The cases
| Case | Planted flaws | Non-flaws (must not flag) |
|---|---|---|
| 01-definitions-review | global Fact instance; native_decide in library code; Prop/Bool dual spelling of IsU32; copy-pasted doc comment |
scoped set_option ... in; junk-value toU32 |
| 02-missing-api | downstream unfold GOLDILOCKS_PRIME while u32_lt_prime sits unused above |
@[simp] ... := rfl projection lemmas (they ARE the API) |
| 03-normal-form | statements in > form; gt_iff_lt tax visible in a proof |
unsqueezed terminal simp |
| 04-structural | unscoped set_options; dishonest show; unfocused goals; squeezed terminal simp and bare non-terminal simp (direction test) |
— |
| 05-clean-restraint | none — the file is deliberately good (an engagement criterion requires the review to name specific declarations, so an empty "looks fine" cannot score by omission) | terminal simp, redundant show lines, := rfl def lemma; no invented rules |
Cases 03/05 and the non-flaw columns carry the uplift signal: the skill
contradicts popular folk advice there (squeeze everything, delete redundant
shows, split proofs over N lines), so a baseline run tends to fail them
while a skill run should not.
Running
Requires the claude CLI and python3. Fixtures are not compiled —
no Lean toolchain is needed; the flaws are stylistic/structural and
reviewable from source.
./run.sh # all cases, baseline + skill arms
./run.sh --arm skill # skill arm only
./run.sh --arm baseline 03-normal-form 05-clean-restraint
EVAL_MODEL=<model-id-or-alias> ./run.sh # pin a model accepted by your CLI
- The skill arm copies
skills/writing-lean-proofsinto the work dir's.claude/skills/, so the reviewer discovers it the way a plugin user would. The baseline arm runs bare. Comparing arms measures uplift. - Both arms run with
--setting-sources project. Claude Code's skill-location documentation puts personal skills in theusersource and project skills in theprojectsource, so the flag excludes personal skills while retaining the copied skill in the skill arm. Before any model call, the runner also rejects a personal or enabled-plugin copy ofwriting-lean-proofs; this converts a future isolation regression into a preflight failure. A baseline transcript that names the skill is a final runtime backstop. - The skill arm has the mirror of that backstop: one canary call per run asks
the CLI to list the skills it can see under
--setting-sources projectand fails the run if the copied skill is not among them. Copying the skill in does not prove the CLI discovered it; without this, a change to project- skill discovery would silently make the skill arm run bare, and the suite would report the two arms as tied — indistinguishable from a true "no uplift" result. - Each reviewer runs headless (
claude -p) in a throwaway copy of the fixture with--permission-mode acceptEdits: edits are possible, so the no-rewrite check is meaningful. - Grading is an LLM judge (
claude -p) in an empty working directory with project-only settings, applyingrubric.mdto the review transcript viagrade-prompt.md. The runner requires exactly one JSON array whose ids are unique and exactly match the rubric in order, and rejects missing evidence, extra keys, an empty transcript, a zero-criterion rubric, or a fixture with zero Lean files. - Results:
results/<timestamp>/<arm>/<case>/{transcript.md,grades.json,no-rewrite.txt,...}plus a printed summary. Results are gitignored.
Grader self-test
./run.sh --self-test
Two-sided, both against case 01's rubric:
-
Before calling the judge, deterministic malformed-output fixtures prove that the validator rejects duplicate, reordered, and unknown criterion ids, and an enabled-plugin fixture proves that the isolation preflight detects a colliding copy of the skill. This half makes no model call and runs before the CLI preflight, so it works on a machine with no
claudeonPATHand no authentication — which is where you would want to run it, e.g. from a CI shell-test suite. -
selftest/bad-review.md— misses every planted flaw and commits every forbidden move (turns one tactic per line into an exceptionless rule, invents a 20-line threshold, flags the scopedset_option, demands squeezing terminal simp, and wantsOptioninstead of junk values) — must fail every criterion. This catches a grader that has become too permissive. -
selftest/good-review.md— flags all four planted flaws with location, rationale, and fix, and commits none of the forbidden moves — must pass every criterion. This catches a grader that has become too strict (which would otherwise read as "the skill provides no uplift").
Run this after editing rubric 01, grade-prompt.md, or when changing
EVAL_MODEL. Model aliases change over time, so prefer the CLI's stable
family alias or a full model id reported by the installed claude version
instead of copying an id from this README. Rubrics 02–05 have no canned
coverage; after editing those, spot-check a live run's grades.json evidence
fields instead.
Interpreting results
- A
must-flagfail on the skill arm means the skill didn't surface its own rule — look at whether the SKILL.md wording is reachable from a plain "review this file" prompt. - A
must-not-flagfail on the skill arm is worse: the skill's anti-folk-advice guidance lost to the model's prior. Those rules may need to be more prominent (they live in the "Rationalizations to reject" and anti-patterns tables). - Baseline-arm failures are expected and are the point: cases the baseline already aces provide no signal about the skill.
Provenance and caveats
- Fixtures derive from an internal Trail of Bits Lean project with identifiers and structure simplified; used with permission.
- Fixtures were written to be plausible Lean 4 + Mathlib but are not compiled in CI. If a fixture contains an accidental error a reviewer fixates on, treat that as fixture debt and fix the fixture, not the rubric.
- The LLM judge makes borderline calls on hedged reviews ("you might
consider squeezing...").
grade-prompt.mdinstructs it to fail only actual recommendations; spot-checkgrades.jsonevidence fields when a number looks surprising.