Skip to content

Ship a filled, runnable worked project reference (the scaffold #108 describes is gitignored, not blank) - #244

Open
OsherElhadad wants to merge 2 commits into
mainfrom
docs/issue-108-worked-project
Open

Ship a filled, runnable worked project reference (the scaffold #108 describes is gitignored, not blank)#244
OsherElhadad wants to merge 2 commits into
mainfrom
docs/issue-108-worked-project

Conversation

@OsherElhadad

@OsherElhadad OsherElhadad commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Closes #108

What #108 asked for, and the correction

The issue says the checked-in .capevolve/project/ scaffold is an untouched blank
template, and offers Option A (fill it) or Option B (delete it).

Neither is available: the path does not exist. .capevolve/ is gitignored
(.gitignore: # cap-evolve run state (per-run, not source)), and no path under it has
ever been tracked:

$ git log --all --oneline -- '.capevolve'
(no output)
$ git log --all --oneline --name-only | grep -c "capevolve/project"
0

So there is nothing to delete (Option B is already the status quo), and Option A as
literally written would mean committing an ignored run dir.

The real gap survives the correction. The repo ships templates/project/ — a blank
form — and zero filled PROJECT.md or commented capevolve.yaml anywhere:

$ git ls-files | grep -c PROJECT.md
1                       # templates/project/PROJECT.md — the blank template itself

examples/toy_calc/ did not even have a spec: run.sh copied
templates/project/capevolve.yaml. So the artifact intake produces first had no worked
counterpart to read anywhere in the repo. That is the gap this fills, in the example dir
the docs already point at.

Judgement on #233's benchmark zoo — does it make this redundant?

Partly, and I scoped around the overlap rather than duplicating it.

The zoo genuinely supersedes the hand-written adapter teaching angle for benchmarks
that fit its manifest: it cuts 78 → 36 hand-authored lines, and benchmark verify is a
far stronger correctness story than anything a doc can assert. If the question is "how do
I add a benchmark", the zoo is now the answer and this PR should not compete with it.

But the zoo's benchmarks/toy_calc/project/capevolve.yaml is GENERATED from benchmark.yaml and says so in its first line: "edit the manifest and re-run
cap-evolve benchmark add --refresh, not this file."
A generated file is the opposite of
a worked reference — it deliberately explains nothing about why a value was chosen, and it
carries no PROJECT.md at all. The zoo also covers only the declarative path; a project
whose runner does not fit the manifest still writes a CapabilityAdapter by hand and still
has nothing filled to read.

So I scoped this to the gap the zoo does not close: the hand-written path, and the
commentary and honesty framing a generated file structurally cannot carry.
No new
directory, no third copy of toy_calc — the two files land in the example that already
exists, and examples/toy_calc/README.md cross-links the zoo entry and says outright
"Prefer the zoo when the manifest fits your benchmark." If a reviewer disagrees and
wants this closed as zoo-covered, the only thing lost is the filled PROJECT.md, which
exists nowhere else.

What the reference demonstrates

Two genuinely filled files, plus the honesty framing:

  • examples/toy_calc/PROJECT.md — the filled templates/project/PROJECT.md: what is
    optimized, how the runner works, how the scorer works and what its feedback says, the
    data and splits, the optimizer and algorithm, the budget, and which RECOMMENDED inputs
    were skipped and why.
  • examples/toy_calc/capevolve.yaml — the run spec with a note on why for every
    decision, rather than a copy of the template's option list.
  • run.sh now uses that spec instead of the blank template, and runs
    cap-evolve check first, so the documented hard gate is exercised, not described.

Both docs lead with what it does NOT prove

This is the part worth more than the parts that work, because it is what readers over-read:

  1. The significance gate never does real work here. gate_mode: paired,
    gate_k_se: 1.0 are set, but both val tasks move 0 → 1 together, so the spread of
    the per-task deltas is zero, SE(Δ) = 0, and the gate logs a gate_warning and takes
    its documented STRICT fallback. Verbatim from the run:
    paired Δ̄=+1.0000 > 0 (SE=0 → STRICT fallback, warned; n=2).
  2. k_se = 1.0 is stricter than it looks. Improving exactly ONE of n val tasks
    gives mean(Δ) = SE(Δ) exactly, so the strict > rejects it — at every n,
    and however large that single gain is. Improve ≥ 2 tasks at k_se = 1.0, or bank
    nothing.
  3. val = 2 is the harness floor (MIN_VAL_TASKS), not a recommendation — and still
    under the low-confidence threshold.
  4. check passing proves less than it looks. It never calls run_target, so a
    non-deterministic runner passes it; a raising materialize() can still yield
    {"ok": true}.
  5. The 0.0 → 1.0 improvement is engineered by construction.

Correct against the current (unmerged) contracts

How this is prevented from rotting

Prose asserting a contract is exactly what this epic keeps finding stale (#203's
cap-evolve finalize, maybe_cached_score, "4 methods"). So the claims are tested, not
written down:

core/tests/test_e2e_slice.py gains two tests that drive the committed files through
the real CLI:

  • test_worked_reference_check_and_spec_are_honest — runs the real run_check, then
    asserts the spec rules: no declared protected_paths (post-Protected-paths tamper guard: verify the optimizer never edited scoring/eval/task files #197), val ≥ 2
    (post-Guard tiny/empty val splits and add a Student-t small-sample correction to the paired gate #195), no pre-rebrand name, and no template placeholder token still present. The
    placeholder list is derived from templates/project/PROJECT.md at test time, so it
    cannot go stale against the template it mirrors — and it fails loudly if the template
    stops having placeholders at all.
  • test_worked_reference_runs_end_to_end — a real zero-API cap-evolve run subprocess
    through to the sealed test number, and asserts the gate actually logs the
    gate_warning and STRICT fallback reason the docs claim. If the gate stops falling
    back, the doc claim is stale and this test fails rather than the doc lying.

Expected merge order

Both are one-line-adjacent, not blocking, but land them first for a clean history:

  1. fix: finish the Acapo -> cap-evolve rebrand (#105) #183 (fix/issue-105-rebrand) — owns templates/project/PROJECT.md's H1. Verified
    clean auto-merge with this branch.
  2. Protected-paths tamper guard: verify the optimizer never edited scoring/eval/task files #197 (feat/issue-142-protected-paths) — makes protected_paths: [] a hard error;
    this reference is already correct for it (omits the key). No conflict either way.

Neither is required for the tests here to pass on main today.

Verification

Every number below is pasted literal output. Full commands + transcripts in the
🔬 Evidence comment.

cap-evolve check on the reference:

$ python3 -m cap_evolve.cli check /tmp/ref/.capevolve/project
{
  "ok": true,
  "stubs": [],
  "problems": [],
  "notes": [
    "tasks('val') -> 8 task(s)",
    "scorer deterministic (probe reward=0.0000)",
    "materialize() callable (dry-run into temp copy; host untouched)"
  ]
}

A real zero-API cap-evolve run with the mock optimizer, to a sealed test number:

$ python3 -m cap_evolve.cli run --spec .capevolve/project/capevolve.yaml \
    --project .capevolve/project --run-ts ref
{"dashboard": "http://127.0.0.1:7898"}
{
  "run_dir": ".capevolve/run_ref",
  "best_id": "cand_0001",
  "baseline_val": 0.0,
  "test_reward": 1.0,
  "test_baseline_reward": 0.0,
  "test_delta": 1.0,
  "test_pass_k": {
    "1": 1.0,
    "2": 0.0
  },
  "iterations": 3,
  "dashboard": ".capevolve/run_ref/dashboard.html",
  "dashboard_server": "http://127.0.0.1:7899"
}

Sealed test = 1.0 from baseline_val 0.0. iterations: 3 — the win lands on
iteration 1, then stall: 2 ends the run, exactly as capevolve.yaml documents.

The gate claim, from that same run's events (proving the honesty caveat is real):

splits       :: {"train": 4, "val": 2, "test": 2, "seed": 0}
gate_warning :: combined/paired SE is 0 (likely n_trials=1 or identical trials) — the
                significance gate cannot distinguish noise from signal and is falling
                back to STRICT (accept any Δ>0). ...
step         :: paired Δ̄=+1.0000 > 0 (SE=0 → STRICT fallback, warned; n=2)
step         :: paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)
step         :: paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)
finalize     :: {"test_reward": 1.0, "test_baseline_reward": 0.0, "test_delta": 1.0,
                 "best_id": "cand_0001"}

Post-#197 protected_paths correctness (run on feat/issue-142-protected-paths):

$ python3 -m cap_evolve.cli check /tmp/ref197/.capevolve/project
{"ok": true, "stubs": [], "problems": [], ...}

$ protect.resolve_protected(project)        # the key is OMITTED -> real defaults
['adapters/adapter.py', 'capevolve.yaml']

# control — what the reference deliberately avoids:
$ (append "protected_paths: []"); protect.protected_globs(project)
TamperError: cap-evolve: `protected_paths` in .../capevolve.yaml is an EMPTY list.
That would protect nothing (the scorer / eval harness / task data would all be optimi...

Post-#195 split floor (run on fix/issue-113-small-samples):

"test_reward": 1.0,
"honest_gate": true,
"warnings": [
  "val split (at baseline) has only 2 tasks (< 5) — acceptance decisions are LOW
   CONFIDENCE. The gate applies a Student-t small-sample correction (df=1), which
   widens the bar, but the honest fix is more val tasks.",
  ...
]

Clears the MIN_VAL_TASKS = 2 floor, and the warning is exactly what PROJECT.md
documents as an honest limit.

run.sh end to end (the CI gate path):

{"ok": true, "stubs": [], "problems": [], ...}
"baseline_val": 0.0,
"test_reward": 1.0,

CI's assertion greps "baseline_val": 0.0 + "test_reward": 1.0 — both still present.

Full test suite:

$ PYTHONPATH=core python -m pytest core/tests -q      # THIS BRANCH
181 passed in 68.87s

$ PYTHONPATH=core python -m pytest core/tests -q      # BASELINE (origin/main, clean worktree)
179 passed in 57.19s

179 baseline → 181: exactly +2 (my two new tests), 0 failures on either side.
Earlier in the session the same command hit
test_dashboard_launch.py::test_maybe_launch_spawns_when_available — the known port-7878
flake tracked in #200 — on the branch and on the unmodified baseline
(1 failed, 180 passed vs 1 failed, 178 passed: the same +2 with the same pre-existing
failure). It is timing-dependent and unrelated to this change.

compileall:

$ python -m compileall -q core examples
compileall: clean

Links: all 15 relative links in the two new/edited docs resolve (checked
programmatically; full output in the Evidence comment).

Files touched

File Change
examples/toy_calc/PROJECT.md new — the filled PROJECT.md, incl. Honest limits
examples/toy_calc/capevolve.yaml new — the filled spec, a note per decision
examples/toy_calc/run.sh use the filled spec; run cap-evolve check first
examples/toy_calc/README.md reframed as the worked reference; zoo cross-link; #181 wording
core/tests/test_e2e_slice.py +2 anti-rot tests over the committed files
templates/project/PROJECT.md +1 line pointing at the filled example (H1 untouched)
CHANGELOG.md Unreleased / Added entry

Issue #108 says the checked-in `.capevolve/project/` scaffold is a blank template.
It is not — it does not exist. `.capevolve/` is gitignored and no path under it has
ever been tracked in this repo's history. The scaffold the issue describes is a
LOCAL artifact intake writes; the repo cannot ship a filled version of a path it
does not ship at all, and committing one would be committing an ignored run dir.

The real gap the issue is pointing at survives that correction: the repo ships
`templates/project/` (a blank form) and ZERO filled `PROJECT.md` or commented
`capevolve.yaml` anywhere. `examples/toy_calc/` did not even have a spec — `run.sh`
copied the blank template. So the first concrete artifact a user meets after intake
had no worked counterpart to read. That is what this fills, in the example dir the
docs already point at, rather than by resurrecting an ignored path.

Added, both genuinely filled:

- `examples/toy_calc/PROJECT.md` — what is optimized, how the runner works, how the
  scorer works and what its feedback says, the data and splits, the optimizer and
  algorithm, the budget, and which RECOMMENDED inputs were skipped and why.
- `examples/toy_calc/capevolve.yaml` — the run spec with a note on WHY for every
  decision, not a copy of the template's option list.

`run.sh` now uses that spec instead of `templates/project/capevolve.yaml`, and runs
`cap-evolve check` before the pipeline, so the documented hard gate is exercised
rather than described.

Both docs LEAD with what the example does not prove, because that is what gets
over-read:

- `gate_mode: paired` / `gate_k_se: 1.0` are set but never tested here. Both val
  tasks move 0 -> 1 together, so the spread of the per-task deltas is zero,
  SE(delta) = 0, and the gate logs a `gate_warning` and takes its documented STRICT
  fallback. The run says so literally:
  `paired delta=+1.0000 > 0 (SE=0 -> STRICT fallback, warned; n=2)`.
- At `k_se = 1.0`, improving exactly ONE of n val tasks gives mean(delta) = SE(delta)
  exactly, so the strict `>` rejects it — at every n, and however large that one gain
  is. Improve >= 2 tasks or bank nothing.
- `val = 2` equals the harness floor (MIN_VAL_TASKS), not a recommendation.
- `cap-evolve check` passing proves less than it looks: it never calls `run_target`,
  so a non-deterministic runner passes, and a raising `materialize()` can still yield
  `{"ok": true}`.
- The 0.0 -> 1.0 improvement is engineered by construction.

`protected_paths` is deliberately OMITTED, with a comment saying why: an empty list
is a HARD ERROR, never a "use defaults" shorthand. The adapter description matches
the corrected contract — three required abstract methods (`tasks`, `run_target`,
`score`) plus an override of the optional `apply` hook, not "4 methods".

Anti-rot: two tests in `core/tests/test_e2e_slice.py` drive the COMMITTED files
through the real CLI — `cap-evolve check` -> a full zero-API `cap-evolve run` -> the
sealed test number — and assert the spec rules (no declared `protected_paths`, val
>= 2, no template placeholder token still present, no pre-rebrand name) and that the
gate really does log the `gate_warning` the docs claim. The placeholder check derives
its token list from the template at test time, so it cannot go stale against it. A
contract change now breaks a test instead of quietly rotting the prose.

`templates/project/PROJECT.md` gains one line pointing at the filled example. Its H1
is left untouched so the rebrand PR owns that line with no conflict.
Copilot AI review requested due to automatic review settings July 30, 2026 20:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔬 Evidence

Every command, verbatim, with full output. Env: macOS, Python 3.14, /tmp/ce-venv.

0. The correction — .capevolve/project/ is gitignored and never existed

$ git log --all --oneline -- ".capevolve"
(no output — never tracked)

$ git ls-files | grep PROJECT.md          # BEFORE this PR (on origin/main)
templates/project/PROJECT.md             # ...the blank template, and nothing else

$ git ls-files | grep PROJECT.md          # AFTER
examples/toy_calc/PROJECT.md             # <- the filled reference this PR adds
templates/project/PROJECT.md

$ grep -n -A1 "cap-evolve run state" .gitignore
16:# cap-evolve run state (per-run, not source)
17-.capevolve/

1. cap-evolve check on the reference

$ rm -rf /tmp/ref && mkdir -p /tmp/ref/.capevolve/project/adapters
$ cp examples/toy_calc/adapter.py /tmp/ref/.capevolve/project/adapters/
$ cp examples/toy_calc/capevolve.yaml examples/toy_calc/PROJECT.md /tmp/ref/.capevolve/project/
$ cp -R examples/toy_calc/capability /tmp/ref/seed_capability
$ export CAPEVOLVE_TOY_DATA=$PWD/examples/toy_calc PYTHONPATH=$PWD/core
$ python -m cap_evolve.cli check /tmp/ref/.capevolve/project
{
  "ok": true,
  "stubs": [],
  "problems": [],
  "notes": [
    "tasks('val') -> 8 task(s)",
    "scorer deterministic (probe reward=0.0000)",
    "materialize() callable (dry-run into temp copy; host untouched)"
  ]
}

2. A real zero-API cap-evolve run to the sealed test number

$ cd /tmp/refE && python -m cap_evolve.cli run --spec .capevolve/project/capevolve.yaml --project .capevolve/project --run-ts ref
{"dashboard": "http://127.0.0.1:7878"}
{
  "run_dir": ".capevolve/run_ref",
  "best_id": "cand_0001",
  "baseline_val": 0.0,
  "test_reward": 1.0,
  "test_baseline_reward": 0.0,
  "test_delta": 1.0,
  "test_pass_k": {
    "1": 1.0,
    "2": 0.0
  },
  "iterations": 3,
  "dashboard": ".capevolve/run_ref/dashboard.html",
  "dashboard_server": "http://127.0.0.1:7878"
}

3. Frozen splits (val = 2 = MIN_VAL_TASKS) and every gate decision

$ cat .capevolve/run_ref/splits.json
{
  "train": [
    "a5",
    "a2",
    "a6",
    "a3"
  ],
  "val": [
    "a1",
    "a4"
  ],
  "test": [
    "a8",
    "a7"
  ],
  "seed": 0,
  "test_used": true
}
$ # all splits/gate/step/finalize events from that run
splits :: {"train": 4, "val": 2, "test": 2, "seed": 0}
gate_warning :: combined/paired SE is 0 (likely n_trials=1 or identical trials) — the significance gate cannot distinguish noise from signal and is falling back to STRICT (accept any Δ>0). Increase n_trials and ensure the runner forwards the per-trial seed to get real variance.
step :: paired Δ̄=+1.0000 > 0 (SE=0 → STRICT fallback, warned; n=2)
gate_warning :: combined/paired SE is 0 (likely n_trials=1 or identical trials) — the significance gate cannot distinguish noise from signal and is falling back to STRICT (accept any Δ>0). Increase n_trials and ensure the runner forwards the per-trial seed to get real variance.
step :: paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)
gate_warning :: combined/paired SE is 0 (likely n_trials=1 or identical trials) — the significance gate cannot distinguish noise from signal and is falling back to STRICT (accept any Δ>0). Increase n_trials and ensure the runner forwards the per-trial seed to get real variance.
step :: paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)
finalize :: {"test_reward": 1.0, "test_baseline_reward": 0.0, "test_delta": 1.0, "best_id": "cand_0001"}

4. Post-#197 protected_paths — correct, and the control that proves the rule

Run on origin/feat/issue-142-protected-paths (the branch that introduces the rule).

$ # reference spec under #197
$ python -m cap_evolve.cli check /tmp/ref197/.capevolve/project
{
  "ok": true,
  "stubs": [],
  "problems": [],
  "notes": [
    "tasks('val') -> 8 task(s)",
    "scorer deterministic (probe reward=0.0000)",
    "materialize() callable (dry-run into temp copy; host untouched)"
  ]
}

$ # the key is OMITTED, so resolve_protected() yields the LAYOUT DEFAULTS:
['adapters/adapter.py', 'capevolve.yaml']

$ # CONTROL — what the reference deliberately avoids (append protected_paths: [])
TamperError: cap-evolve: `protected_paths` in /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/tmppank9ic5/project/capevolve.yaml is an EMPTY list. That would protect nothing (the scorer / eval harness / task data would all be optimizer-writable). Remove the key to use the defaults, or declare the paths.

5. Post-#195 split floor + k_se cap

Run on origin/fix/issue-113-small-samples.

$ python -m cap_evolve.cli run --spec ... --run-ts r195   (tail)
{"dashboard": "http://127.0.0.1:7878"}
{
  "run_dir": ".capevolve/run_r195b",
  "best_id": "cand_0001",
  "baseline_val": 0.0,
  "test_reward": 1.0,
  "test_baseline_reward": 0.0,
  "test_delta": 1.0,
  "test_pass_k": {
    "1": 1.0,
    "2": 0.0
  },
  "iterations": 3,
  "honest_gate": true,
  "warnings": [
    "val split (at baseline) has only 2 tasks (< 5) \u2014 acceptance decisions are LOW CONFIDENCE. The gate applies a Student-t small-sample correction (df=1), which widens the bar, but the honest fix is more val tasks.",
    "val split (at split freeze) has only 2 tasks (< 5) \u2014 acceptance decisions are LOW CONFIDENCE. The gate applies a Student-t small-sample correction (df=1), which widens the bar, but the honest fix is more val tasks."
  ],
  "dashboard": ".capevolve/run_r195b/dashboard.html",
  "dashboard_server": "http://127.0.0.1:7878"
}

Clears the floor (honest_gate: true), and the LOW CONFIDENCE warning is exactly
what PROJECT.md documents as honest limit #3. gate_k_se: 1.0 is far under the 26.5 cap.

6. #181 adapter contract, side by side

docs/ADAPTER_CONTRACT.md on origin/fix/issue-103-adapter-methods:

## The three required methods

These three are `@abstractmethod` — `cap-evolve check` refuses to run until all
three are real (no `IMPLEMENT ME` stub):

```python
class Adapter(CapabilityAdapter):
    def tasks(self, split: str) -> list[Task]: ...
    def run_target(self, task: Task, ctx, *, seed: int = 0) -> Rollout: ...
    def score(self, task: Task, rollout: Rollout) -> Score: ...
  • tasks(split) — where evaluation data comes from, for split in
    ...

You only override these when the default behavior doesn't fit:

def materialize(self, candidate_dir, edits=None) -> None   # PURE write of {component: text} edits into candidate_dir
def live(self, candidate_dir)                              # @contextmanager: make the candidate live for ONE eval, yield ctx
def apply(self, candidate_dir, edits=None) -> None         # back-compat inject hook (env var / config patch / copy)
def trajectories(self, split, ctx=None) -> Path | None     # the runner's NATIVE trace dir for the last eval (default: None)
def runner_model(self) -> str | None                       # the CONSUMING model id, for check's mismatch note (default: None)

Three more optional fast paths are not on the base class — the harness probes for them
with hasattr (core/cap_evolve/harness.py) and uses them when present:

def run_batch(self, tasks, ctx, *, seed=0) -> ...                                  # drive a benchmark's OWN batch runner INSTEAD of run_target (as tau2 does)
def run_trials(self, tasks, ctx, *, n_trials, base_seed) -> {id: [Rollout, ...]}   # batched fast path: ALL trials in ONE run
def score_batch(self, tasks, rollouts) -> {id: Score}                             # batched fast path: score a WHOLE trial in ONE call (e.g. one Docker harness invocation, as swebench does)

My reference (`examples/toy_calc/README.md`):

20:- adapter.py — a CapabilityAdapter: the 3 required methods
21: (tasks, run_target, score) plus an override of the optional apply hook.
68: adapter implements: 3 required @abstractmethods, defaulted hooks, hasattr-probed


My reference (`examples/toy_calc/PROJECT.md` — describing the same adapter):

18: adapter.py::Adapter.run_target and computes the arithmetic correctly only when
21:- How run_target invokes it: reads ctx/prompt.txt (where ctx is the candidate dir
30:- Feedback signal: on failure, score() reports what was expected vs produced **plus
45:- tasks(split) deliberately returns all tasks and lets the harness filter by the
84: stable, score() is deterministic, and materialize() is callable. It does not run
85: run_target, so a non-deterministic runner passes check. Notably a materialize()


And the adapter itself implements exactly that set:

$ grep -n " def " examples/toy_calc/adapter.py
34: def tasks(self, split: str) -> list[Task]:
44: def run_target(self, task: Task, ctx, *, seed: int = 0) -> Rollout:
60: def score(self, task: Task, rollout: Rollout) -> Score:
70: def apply(self, candidate_dir: Path, edits: dict | None = None) -> None:

Three required abstract methods + one optional-hook override. No "4 methods" anywhere:

$ grep -rn "4 method|four method" examples/toy_calc/
(no matches)


### 7. #198 gate rule — no unqualified "banks 1-task gains"

$ grep -rn "banks|k_se = 1.0|k_se = 1.0|>= 2 tasks|≥ 2 tasks" examples/toy_calc/*.md examples/toy_calc/capevolve.yaml
examples/toy_calc/PROJECT.md:75: gives mean(Δ) = SE(Δ) exactly, so the strict > rejects it — at every n,
examples/toy_calc/PROJECT.md:76: and no matter how large that single gain is. Improve ≥ 2 tasks at k_se = 1.0, or
examples/toy_calc/PROJECT.md:77: bank nothing.
Set gate_k_se: 0.2 (as the other examples do) to bank a 1-of-n
examples/toy_calc/capevolve.yaml:60:# 1. At k_se = 1.0, improving exactly ONE of n val tasks gives mean(Δ) = SE(Δ)
examples/toy_calc/capevolve.yaml:62:# gain is. You need >= 2 improved tasks to bank anything at 1.0. (The examples


### 8. #183 rebrand merges cleanly (H1 untouched)

$ git merge --no-commit --no-ff origin/fix/issue-105-rebrand
Auto-merging templates/project/PROJECT.md
Automatic merge went well; stopped before committing as requested

$ git diff origin/main -- templates/project/PROJECT.md
diff --git a/templates/project/PROJECT.md b/templates/project/PROJECT.md
index 32d1c447..80a6b351 100644
--- a/templates/project/PROJECT.md
+++ b/templates/project/PROJECT.md
@@ -3,6 +3,9 @@
Filled by the intake skill. Records the decisions behind this run so anyone
(human or agent) can understand and reproduce it.

+> For a filled worked example of this file (and of capevolve.yaml), see
+> examples/toy_calc/ — a runnable, zero-API reference project.
+

What we're optimizing

  • Capability: <skill-package | tools | mcp-tool | system-prompt | ...>
  • Artifact: <path>
#183 edits line 1 (the H1); mine adds at line 3. Non-adjacent, hence the clean auto-merge.

### 9. `run.sh` end to end (the CI gate path)

$ bash examples/toy_calc/run.sh
Working directory: /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/toy_calc.XXXXXX.QxcJlLN1lH
{
"ok": true,
"stubs": [],
"problems": [],
"notes": [
"tasks('val') -> 8 task(s)",
"scorer deterministic (probe reward=0.0000)",
"materialize() callable (dry-run into temp copy; host untouched)"
]
}
{"dashboard": "skipped", "reason": "capevolve-dashboard not installed (pip install -e dashboard/backend)"}
{
"run_dir": ".capevolve/run_demo",
"best_id": "cand_0001",
"baseline_val": 0.0,
"test_reward": 1.0,
"test_baseline_reward": 0.0,
"test_delta": 1.0,
"test_pass_k": {
"1": 1.0,
"2": 0.0
},
"iterations": 3,
"dashboard": ".capevolve/run_demo/dashboard.html",
"dashboard_server": "skipped"
}

CI (`.github/workflows/ci.yml`) greps `"baseline_val": 0.0` and `"test_reward": 1.0` — both present.

### 10. Full test suite — this branch

$ PYTHONPATH=core python -m pytest core/tests -q
........................................................................ [ 79%]
..................................... [100%]
181 passed in 68.87s (0:01:08)


### 10b. Full test suite — the unmodified baseline (`origin/main`), same command

$ PYTHONPATH=core python -m pytest core/tests -q
........................................................................ [ 80%]
................................... [100%]
179 passed in 57.19s


**179 baseline -> 181 on this branch: exactly +2, my two new tests, 0 failures either
side.** Both runs are green here; earlier in this session the same command hit
`test_dashboard_launch.py::test_maybe_launch_spawns_when_available` (the known port-7878
flake, **#200**) on the branch *and* on the unmodified baseline — `1 failed, 180 passed`
vs `1 failed, 178 passed`, the same +2 with the same pre-existing failure. It is timing-
dependent and unrelated to anything here.

### 11. Just the new tests (no `CAPEVOLVE_*` env preset — they build the project themselves)

$ PYTHONPATH=core python -m pytest core/tests/test_e2e_slice.py -v
core/tests/test_e2e_slice.py::test_full_slice PASSED [ 20%]
core/tests/test_e2e_slice.py::test_cyclic_variant_also_improves PASSED [ 40%]
core/tests/test_e2e_slice.py::test_baseline_better_than_nothing_is_gated PASSED [ 60%]
core/tests/test_e2e_slice.py::test_worked_reference_check_and_spec_are_honest PASSED [ 80%]
core/tests/test_e2e_slice.py::test_worked_reference_runs_end_to_end PASSED [100%]
============================== 5 passed in 6.61s ===============================


### 12. `compileall`

$ python -m compileall -q core examples && echo clean
clean


### 13. All relative links resolve

OK README.md -> ../../templates/project
OK README.md -> PROJECT.md
OK README.md -> capevolve.yaml
OK README.md -> adapter.py
OK README.md -> PROJECT.md
OK README.md -> ../tau2_airline
OK README.md -> ../skillsbench
OK README.md -> ../../docs/ADAPTER_CONTRACT.md
OK README.md -> ../../templates/project
OK PROJECT.md -> adapter.py
OK PROJECT.md -> capevolve.yaml
OK PROJECT.md -> capability/prompt.txt
OK PROJECT.md -> tasks.jsonl
OK PROJECT.md -> mock_script.json
OK PROJECT.md -> run.sh

broken: none


### 14. Anti-rot: the placeholder check derives its tokens from the template at test time
So it cannot go stale against the file it mirrors, and it fails loudly if the template
stops carrying placeholders at all:
```python
    template = (REPO / "templates" / "project" / "PROJECT.md").read_text(encoding="utf-8")
    tokens = set(re.findall(r"<[^<>\n]{2,40}>", template))
    assert tokens, "template has no placeholders to compare against — did its shape change?"
    for name in ("capevolve.yaml", "PROJECT.md"):
        body = (proj / name).read_text(encoding="utf-8")
        assert "Acapo" not in body, f"{name} carries the pre-rebrand name"
        left = sorted(t for t in tokens if t in body)
        assert not left, f"{name} still carries unfilled template placeholders: {left}"
    project_md = (proj / "PROJECT.md").read_text(encoding="utf-8")

15. Files touched

$ git show --stat --oneline HEAD
7d862c29 Ship a filled, runnable worked project reference (examples/toy_calc)
 CHANGELOG.md                     |  24 +++++++++
 core/tests/test_e2e_slice.py     | 108 +++++++++++++++++++++++++++++++++++++++
 examples/toy_calc/PROJECT.md     | 100 ++++++++++++++++++++++++++++++++++++
 examples/toy_calc/README.md      |  66 +++++++++++++++++++-----
 examples/toy_calc/capevolve.yaml |  79 ++++++++++++++++++++++++++++
 examples/toy_calc/run.sh         |   7 ++-
 templates/project/PROJECT.md     |   3 ++
 7 files changed, 373 insertions(+), 14 deletions(-)

16. Commit authorship — no Claude attribution

$ git log -1 --format="%an <%ae> | committer: %cn <%ce>"
Osher Elhadad <Osher.Elhadad@ibm.com> | committer: Osher Elhadad <Osher.Elhadad@ibm.com>
$ git log -1 --format=%B | grep -ci "co-authored\|generated with claude"
0
(0 = none)

@skillberry-bot

Copy link
Copy Markdown
Contributor

Automatic Labeling Failed

An error occurred while trying to automatically label this pull request. Please check the workflow logs for details and add labels manually.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔍 Review — PR #244

APPROVE WITH NITS. The premise rejection is correct and evidenced. The contract claims all match their sibling branches' ground truth — I checked each one against the unmerged branch, not against main. The two honesty disclosures reproduce verbatim. One anti-rot assertion is weaker than it reads (non-blocking, below), and the check-never-runs-run_target claim is true by default but has a documented opt-in escape the reference doesn't mention.

Nothing blocking.

Blocking

None.

Non-blocking

  1. core/tests/test_e2e_slice.py:195 — the Honest limits guard passes on a gutted section. The assertion is "Honest limits" in project_md, but examples/toy_calc/PROJECT.md:26 contains the incidental prose — see *Honest limits*. I deleted the entire section body (lines 64–90, all five caveats) and the test still passed:

    --- PROBE 5: delete the ENTIRE Honest limits section ---
    1                       # remaining "Honest limits" occurrences (the line-26 cross-ref)
    ..                                                                       [100%]
    2 passed, 3 deselected in 3.29s
    

    Consequence: the one property this PR's whole value rests on — that the caveats stay in the file — is the one property the anti-rot test does not actually protect. Every other asserted property failed when broken (see below); this is the single hole.

    Fix: anchor on the heading and require the caveats survive, e.g.

    assert "## Honest limits" in project_md, "the reference must keep its honest-limits section"
    body = project_md.split("## Honest limits", 1)[1].split("\n## ", 1)[0]
    for claim in ("STRICT fallback", "MIN_VAL_TASKS", "run_target"):
        assert claim in body, f"Honest limits lost its {claim} caveat"
  2. examples/toy_calc/PROJECT.md:85 — "It does not run run_target" is true by default but not unconditionally. core/cap_evolve/check.py:178-182 does call run_target twice, gated behind CAPEVOLVE_N_TRIALS>1 and CAPEVOLVE_CHECK_TRIAL_PROBE=1 (the degenerate-trials probe). Consequence: a reader who sets both env vars sees check fire real rollouts, contradicting the reference's flat claim — mild, since the probe only ever appends a WARNING note and never fails the check. Fix: "It does not run run_target (except an opt-in degenerate-trials probe behind CAPEVOLVE_CHECK_TRIAL_PROBE=1, which only warns), so a non-deterministic runner passes check."

  3. No forward link to the benchmark zoo (Benchmark zoo + cap-evolve benchmark add/verify + declarative manifest #233). Benchmark zoo + cap-evolve benchmark add/verify + declarative manifest #233's benchmarks/toy_calc/README.md links back here ("examples/toy_calc/ keeps the hand-written adapter form... the reference for what a custom CapabilityAdapter looks like when the manifest does not fit"), but nothing in this PR points at benchmarks/toy_calc/. I grepped examples/toy_calc/ and CHANGELOG.md for benchmark|zoo — only incidental prose hits. Consequence: the cross-link is one-directional, so a reader landing here doesn't learn the declarative path exists. Fix: one line in examples/toy_calc/README.md's Related section once Benchmark zoo + cap-evolve benchmark add/verify + declarative manifest #233 lands. Ordering makes this unavoidable in this PR; worth a follow-up note rather than a rebase.

Nits

  1. examples/toy_calc/PROJECT.md:72 and capevolve.yaml:67 quote the transcript as paired Δ̄=+1.0000 > 0 (SE=0 → STRICT fallback, warned; n=2). That is byte-exact on main and on Protected-paths tamper guard: verify the optimizer never edited scoring/eval/task files #197/docs: document the real gate modes (paired is the default; add simplicity_tiebreak) #198, but Guard tiny/empty val splits and add a Student-t small-sample correction to the paired gate #195 appends ; no t-correction on this path:

    #195 branch:  "paired Δ̄=+1.0000 > 0 (SE=0 → STRICT fallback, warned; n=2; no t-correction on this path)"
    this branch:  "paired Δ̄=+1.0000 > 0 (SE=0 → STRICT fallback, warned; n=2)"
    

    Not a defect today and the test uses a substring ("STRICT fallback" in reason), so it survives the merge — but the two quoted strings go subtly stale when Guard tiny/empty val splits and add a Student-t small-sample correction to the paired gate #195 lands. Prefer quoting the stable prefix (SE=0 → STRICT fallback, warned; n=2 or dropping the closing paren.

  2. examples/toy_calc/PROJECT.md:81 says val=2 "is still under the low-confidence threshold" without naming it. LOW_CONFIDENCE_VAL_TASKS = 5 (core/cap_evolve/splits.py:27 on Guard tiny/empty val splits and add a Student-t small-sample correction to the paired gate #195). Naming the constant makes the caveat checkable.

Is the premise rejection right?

Yes, on all three facts, and I verified each independently:

$ grep -n "capevolve" .gitignore
17:.capevolve/
$ git log --all --oneline -- .capevolve        # (no output — never tracked on any ref)
$ git ls-files | grep -i "PROJECT.md"
templates/project/PROJECT.md                   # exactly one, the template

So the issue's factual claim — "the scaffolded example project that ships in the repo, .capevolve/project/" — is wrong. Nothing ships there. Option A ("replace .capevolve/project/ with a filled worked example") would mean committing a path that .gitignore:17 excludes, and Option B ("delete the checked-in scaffold") is a no-op because there is nothing to delete. The issue was written against a developer's local run-state dir, not the repo.

Is filling examples/toy_calc/ the right substitute? Yes, and this is the correct call rather than a dodge. Read past the wrong path, #108's actual complaint is "a newcomer has no filled reference; the template teaches nothing" — and its own "How to test" is no <placeholder> tokens left plus cap-evolve check passes, both of which this PR satisfies at the new location. I also confirmed the gap was real: on origin/main, git ls-tree origin/main examples/toy_calc/ has no capevolve.yaml and no PROJECT.md, and run.sh:19 copied templates/project/capevolve.yaml — the blank template. So the repo genuinely had zero filled spec and zero filled PROJECT.md anywhere. Claim verified.

Does the epic instead want a .gitignore change? I don't think so, and the PR is right to flag rather than assume. Committing a scaffold at .capevolve/project/ would mean un-ignoring a live run-state directory — every subsequent cap-evolve run writes .capevolve/run_*/ siblings, so you'd need a negative-pattern dance (!.capevolve/project/) and users' real runs would then fight the tracked reference on every git status. That is a genuinely different decision with a maintenance cost, and it should not be made silently inside a docs PR. The templates/project/PROJECT.md:6-8 pointer this PR adds is the cheap version of the same discovery win: a reader who opens the blank template is told where the filled one lives. If the epic owner does want the checked-in path, that's a separate issue about .gitignore policy — not this one.

Is this additive over #233's zoo, or should it be closed?

Additive — keep it. But the margin is thinner than the PR's framing implies, and the honest reason to keep it is not the one the PR leads with.

I read benchmarks/toy_calc/ on #233's branch. The PR's three factual claims about it hold:

  • benchmarks/toy_calc/project/capevolve.yaml:1-2 is headed # GENERATED from benchmark.yaml by 'cap-evolve benchmark add' — edit the manifest and re-run ... not this file. So it is structurally disqualified as a "worked reference you read to learn the spec" — it tells you not to read it as authored.
  • No PROJECT.md anywhere under benchmarks/toy_calc/ (find returns 9 files; none is a PROJECT.md).
  • The zoo's code is project/target.py — "one function, run(task, ctx, *, seed=0)" per its own README — i.e. the declarative path only. There is no hand-written CapabilityAdapter subclass there.

The decisive point, though, is that #233's own README asks for this PR to exist: "examples/toy_calc/ keeps the hand-written adapter form of the same benchmark — it is the 'before' side of the boilerplate measurement, and the reference for what a custom CapabilityAdapter looks like when the manifest does not fit." #233 depends on examples/toy_calc/ remaining the hand-written reference. Closing this PR as zoo-covered would leave #233's cross-link pointing at a directory with no filled spec and no PROJECT.md — a worse outcome than either branch alone.

Now the honest deduction. The PR says "the only thing lost would be the filled PROJECT.md." That undersells its own case slightly and oversells it in another way:

  • Genuinely unique and worth the PR: the filled PROJECT.md, the per-decision commentary in capevolve.yaml (the zoo's is machine-generated and says don't read it), the five Honest limits caveats, and the two anti-rot tests. None of these exist anywhere in the repo today, on either branch.
  • The commentary is the real payload, not the PROJECT.md. capevolve.yaml:39-43 (why val=2 is the floor), :49-53 (why protected_paths is omitted rather than []), and :59-69 (the two distinct gate caveats) encode three contract rules that a reader otherwise learns by breaking them. I reproduced all three against their sibling branches. That is teaching content, and it is not derivable from a generated file.

Verdict: additive, keep open. ~370 lines for the repo's only filled reference plus two tests that pin four contract rules is a fair trade, and the alternative leaves a dangling promise in #233. This is not manufactured work.

Contract accuracy

Every claim checked against the sibling branch, not main.

Claim in this PR Sibling branch ground truth Match?
"the 3 required methods (tasks, run_target, score)" — README.md:20-21 #181 core/cap_evolve/adapter.py: exactly 3 @abstractmethod at lines 77/86/99 → tasks, run_target, score. docs/ADAPTER_CONTRACT.md:8 "## The three required methods"
"plus an override of the optional apply hook" — README.md:21, PROJECT.md:95 #181 docs/ADAPTER_CONTRACT.md:41 "## Optional hooks (working defaults provided)", line 48 lists apply apply correctly labelled optional
Any "4 methods" claim grep for four methods|4 methods|4 required across all new files → no hits ✅ none present
"3 required @abstractmethods, defaulted hooks, hasattr-probed optional fast paths" — README.md:68-69 #181 doc lines 41-58: defaulted hooks + 3 hasattr-probed paths (run_batch, run_trials, score_batch)
"at k_se = 1.0, improving exactly ONE of n gives mean(Δ) = SE(Δ) exactly, so strict > rejects it — at every n, and however large that one gain is" — capevolve.yaml:60-63, PROJECT.md:74-78 #198 docs/ADAPTER_CONTRACT.md:113: "(mean(Δ) = SE(Δ) exactly, at any n and any magnitude) — ≥2 improved tasks, or gate_k_se < 1.0" ✅ exact, incl. both quantifiers
"gate_mode: paired … the default in every real run" — capevolve.yaml:56 #198 doc:99 "paired — the default."
Unqualified "banks 1-task gains" anywhere grep → no such claim; both docs state the opposite ✅ absent
"protected_paths DELIBERATELY OMITTED … an empty list is a HARD ERROR" — capevolve.yaml:49-53 #197 reproduced both halves on its branch — see Verification
"val = 2 … equals the harness's MIN_VAL_TASKS = 2; a smaller val split is refused" — PROJECT.md:79-81 #195 core/cap_evolve/splits.py:22 MIN_VAL_TASKS = 2; gate.py:130-144 raises TinyValSplitError below it
"2 is still under the low-confidence threshold" — PROJECT.md:81 #195 splits.py:27 LOW_CONFIDENCE_VAL_TASKS = 5; run emits honest_gate: true + the LOW CONFIDENCE warning ✅ (nit 5: name the 5)
"materialize() that raises can still yield {"ok": true}" — PROJECT.md:86 Reproduced directly — see Verification
"check … does not run run_target" — PROJECT.md:85 core/cap_evolve/check.py:178-182 runs it behind CAPEVOLVE_N_TRIALS>1 and CAPEVOLVE_CHECK_TRIAL_PROBE=1 ⚠️ true by default only — non-blocking #2
Transcript paired Δ̄=+1.0000 > 0 (SE=0 → STRICT fallback, warned; n=2) byte-exact here and on #197/#198; #195 appends ; no t-correction on this path ⚠️ nit 4

Anti-rot tests

Real, with one hole. I broke each asserted property and re-ran. 7 of 8 failed as they must:

# Property broken Result
1 appended protected_paths: [] to the spec FAILED at :171
2 split_val: 0.250.10 (val floor) FAILED at :183
3 reinserted a <path> placeholder into PROJECT.md FAILED at :193
4 ## Honest limits## Caveats (heading only) ⚠️ passed — matched the line-26 cross-ref
5 deleted the whole Honest limits section (all 5 caveats) ⚠️ passed — non-blocking #1
6 patched gate.py to stop logging gate_warning FAILED at :234
7 patched the STRICT fallback reason string out of gate.py FAILED at :237
8 stripped every <...> placeholder from templates/project/PROJECT.md FAILED at :188

The placeholder list is genuinely derived at test time — verified, not taken on trust. Probe 8 is the proof: test_e2e_slice.py:186-188 reads the template and re.findall(r"<[^<>\n]{2,40}>", ...), then asserts tokens is non-empty. Removing the template's placeholders makes the token set empty and the test fails with "template has no placeholders to compare against — did its shape change?" — so the list cannot silently become vacuous. Probe 7 (adding a new placeholder to the template that isn't in the reference) correctly passed, which is the right behavior.

The subprocess test is honest work: :208-212 shells out to the real CLI with the committed files, :215-224 parses the last top-level JSON object, and :232-237 reads events.jsonl rather than trusting stdout.

Merge-order note

Trial-merged this branch against all six siblings:

MERGE origin/fix/issue-105-rebrand           -> exit=0 conflicts=[none]
MERGE origin/docs/issue-104-gate-modes       -> exit=0 conflicts=[none]
MERGE origin/fix/issue-113-small-samples     -> exit=0 conflicts=[none]
MERGE origin/fix/issue-103-adapter-methods   -> exit=1 conflicts=[examples/toy_calc/README.md]
MERGE origin/feat/issue-142-protected-paths  -> exit=1 conflicts=[CHANGELOG.md]
MERGE origin/feat/issue-141-benchmark-zoo    -> exit=1 conflicts=[CHANGELOG.md]

#183 auto-merges clean, as the PR claims. Verified the geometry: #183's only hunk is templates/project/PROJECT.md:1 (-# Acapo project / +# cap-evolve project); this PR's hunk starts at line 6 (@@ -3,6 +3,9 @@). Non-adjacent, so git takes both.

One conflict the PR does not mention. #181 also edits examples/toy_calc/README.md — the same "## Files" bullet list this PR rewrites:

<<<<<<< HEAD
- [`adapter.py`](adapter.py) — a `CapabilityAdapter`: the **3 required** methods
  (`tasks`, `run_target`, `score`) plus an override of the optional `apply` hook.
=======
- `adapter.py` — a `CapabilityAdapter`: the 3 required methods (deterministic agent +
  exact-match scorer) plus an `apply` hook override.
>>>>>>> origin/fix/issue-103-adapter-methods

Trivial to resolve — take HEAD (this PR's version is the more specific and equally correct one; it names the three methods and labels apply optional). Worth calling out so it isn't a surprise. The two CHANGELOG.md conflicts are the ordinary "everyone appends under ### Added" kind.

Recommended order: #183 → #181 → #198 → #195 → #197 → #244 → #233. Rationale: the doc/contract branches land first so this PR's cross-references (docs/ADAPTER_CONTRACT.md, the gate rules) resolve against merged truth rather than a branch tip; #244 lands after them (resolving the one-line #181 README conflict and, if #195 is in, relaxing the quoted transcript per nit 4); #233 lands last so its examples/toy_calc/ back-link resolves and its author can add the missing forward link (non-blocking #3) in the same pass.

Verification I re-ran

Tests — exactly +2, as claimed:

origin/main:  179 passed in 65.90s
this branch:  181 passed in 65.78s

No flakes; test_dashboard_launch.py (#200) passed on both runs.

compileall clean; all 15 relative links resolve:

relative links checked=15 broken=0
compileall OK

No third copy of toy_calcfind -name tasks.jsonlexamples/toy_calc/ and templates/adapters/jsonl_litellm/ (an unrelated template). Only benchmarks/toy_calc/ (#233) + examples/toy_calc/ exist as toy_calc dirs.

bash examples/toy_calc/run.shcheck green, and the claimed sealed number reached:

{
  "ok": true,
  "stubs": [],
  "problems": [],
  "notes": [
    "tasks('val') -> 8 task(s)",
    "scorer deterministic (probe reward=0.0000)",
    "materialize() callable (dry-run into temp copy; host untouched)"
  ]
}
{
  "run_dir": ".capevolve/run_demo", "best_id": "cand_0001",
  "baseline_val": 0.0,
  "test_reward": 1.0,
  "test_baseline_reward": 0.0, "test_delta": 1.0,
  "test_pass_k": {"1": 1.0, "2": 0.0},
  "iterations": 3
}

baseline_val 0.0 → test_reward 1.0, iterations: 3 — matches PROJECT.md:52-54 and capevolve.yaml:72-74 (win on iteration 1, then stall: 2). CI's greps still match: .github/workflows/ci.yml:86-87 greps '"baseline_val": 0.0' and '"test_reward": 1.0' — both present verbatim in the output above.

The gate really does degrade to STRICT — reproduced from events.jsonl, not stdout:

Counter({'evaluate': 6, 'gate_warning': 3, 'step': 3, 'splits': 1, 'baseline': 1, 'finalize': 1})

GATE_WARNING: {"kind": "gate_warning", "mode": "paired", "context": "n=2",
  "reason": "combined/paired SE is 0 (likely n_trials=1 or identical trials) — the
   significance gate cannot distinguish noise from signal and is falling back to
   STRICT (accept any Δ>0). ..."}

STEP accept=True  | paired Δ̄=+1.0000 >  0 (SE=0 → STRICT fallback, warned; n=2)
STEP accept=False | paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)
STEP accept=False | paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)

SPLIT: {"kind": "splits", "train": 4, "val": 2, "test": 2, "seed": 0}

Three gate_warnings, all three steps on the STRICT path, and the accepted step's reason is byte-identical to the string quoted at PROJECT.md:72 and capevolve.yaml:67. Splits are 4/2/2 as documented.

Is the degradation a teaching hazard or a strength here? A strength. The disclosure is adequate and then some — it is the lead item of Honest limits (PROJECT.md:68-72), repeated in the spec at the point of use (capevolve.yaml:59-69, directly under the gate_k_se: 1.0 line a copier would copy), in README.md:60-62, and in the CHANGELOG. Critically, the reference does not overstate: README.md:57-58 claims only "the gate wiring", and PROJECT.md:89 says 0.0 → 1.0 "proves the loop plumbing, gate wiring, and seal — not that optimization works." A reader cannot come away believing the significance test was exercised. And test_e2e_slice.py:232-237 asserts the warning still fires, so the disclosure can't rot into a lie.

#197 control — both halves reproduce on origin/feat/issue-142-protected-paths:

### A) reference AS-IS (protected_paths omitted) ###
  "test_reward": 1.0, "test_delta": 1.0, "iterations": 3        # runs clean, defaults apply

### B) CONTROL: same spec + "protected_paths: []" ###
cap_evolve.protect.TamperError: cap-evolve: `protected_paths` in
.capevolve/project/capevolve.yaml is an EMPTY list. That would protect nothing
(the scorer / eval harness / task data would all be optimizer-writable).
Remove the key to use the defaults, or declare the paths.
  raised at core/cap_evolve/protect.py:152 via harness.baseline

So capevolve.yaml:49-53's comment is exactly right, and the omission is load-bearing rather than an oversight.

#195 control on origin/fix/issue-113-small-sampleshonest_gate: true and the LOW CONFIDENCE warning both appear:

"baseline_val": 0.0,
"honest_gate": true,
"warnings": [
  "val split (at baseline) has only 2 tasks (< 5) — acceptance decisions are LOW
   CONFIDENCE. The gate applies a Student-t small-sample correction (df=1), which
   widens the bar, but the honest fix is more val tasks.",
  "val split (at split freeze) has only 2 tasks (< 5) — ..."
]
split_warning | {"val": 2, "min_recommended": 5, ...}
STEP accept=true | paired Δ̄=+1.0000 > 0 (SE=0 → STRICT fallback, warned; n=2;
                                          no t-correction on this path)

val=2 == MIN_VAL_TASKS, honest_gate: true, warning present. Note the reason-string suffix (nit 4).

Raising materialize() still yields ok: true — reproduced:

{
  "ok": true, "stubs": [], "problems": [],
  "notes": [ ..., "materialize() probe raised (may be expected): BOOM" ]
}

PROJECT.md:86 states this, and PROJECT.md:82-86 is careful that check "is not proof the adapter is correct." The reference does not imply check proves more than it does.

Net: every verification claim in the PR description reproduced. The only corrections are the run_target opt-in probe (non-blocking #2), the weak Honest limits assertion (non-blocking #1), and the unmentioned #181 README conflict (merge note).

…s-ref

The anti-rot assertion was `"Honest limits" in project_md`, which matched an
incidental cross-reference earlier in PROJECT.md — so deleting the whole section
(all 5 caveats) or renaming the heading both left the test green. It now anchors
on `## Honest limits`, slices the section body, asserts a distinctive phrase per
caveat, and pins the count at 5.

Also:
- PROJECT.md: qualify "check does not run run_target" — check.py:178-182 does,
  behind CAPEVOLVE_CHECK_TRIAL_PROBE=1 and CAPEVOLVE_N_TRIALS>1 (warn-only).
- PROJECT.md: name LOW_CONFIDENCE_VAL_TASKS = 5 instead of "the threshold".
- PROJECT.md / capevolve.yaml: quote the stable prefix of the gate transcript,
  since #195 appends a suffix after `n=2`.
- README.md: forward link to benchmarks/toy_calc/ (the declarative form), making
  the #233 cross-reference bidirectional.
Copilot AI review requested due to automatic review settings July 30, 2026 21:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔧 Review fixes

Commit 617bc37a (new commit, not an amend). Every claim below is literal output.

Finding 1 — the Honest limits guard now asserts the content [FIXED]

You were right and the hole was exactly where you said. "Honest limits" in project_md
matched the cross-reference at PROJECT.md:26, so the section could be gutted or
renamed under it. The guard now anchors the heading, slices the section body, asserts a
distinctive phrase per caveat, and pins the count:

assert "\n## Honest limits" in project_md, "the reference lost its `## Honest limits` heading"
limits = project_md.split("\n## Honest limits", 1)[1].split("\n## ", 1)[0]
for claim in ("STRICT fallback", "bank nothing", "MIN_VAL_TASKS", "run_target",
              "engineered"):
    assert claim in limits, f"Honest limits lost its `{claim}` caveat"
caveats = re.findall(r"^\d+\. \*\*", limits, flags=re.M)
assert len(caveats) == 5, f"Honest limits should list 5 caveats, found {len(caveats)}"

The three break-probes you asked for — all three now FAIL.

=== BASELINE (unbroken) ===
.                                                                        [100%]
1 passed, 4 deselected in 0.07s

=== PROBE A: delete the ENTIRE 'Honest limits' section (all 5 caveats) ===
remaining "Honest limits" occurrences: 1
E       assert '\n## Honest limits' in '# cap-evolve project — toy_calc arithmetic stand-in\n\nA **filled** version of `templates/project/PROJECT.md`. The te...cript.json) | the deterministic edit `mock` applies |\n| [`run.sh`](run.sh) | the whole thing end to end, zero API |\n'

core/tests/test_e2e_slice.py:198: AssertionError
=========================== short test summary info ============================
FAILED core/tests/test_e2e_slice.py::test_worked_reference_check_and_spec_are_honest
1 failed, 4 deselected in 0.53s

=== PROBE B: rename the heading only (## Honest limits -> ## Caveats) ===
remaining "Honest limits" occurrences: 1
E       assert '\n## Honest limits' in '# cap-evolve project — toy_calc arithmetic stand-in\n\nA **filled** version of `templates/project/PROJECT.md`. The te...cript.json) | the deterministic edit `mock` applies |\n| [`run.sh`](run.sh) | the whole thing end to end, zero API |\n'

core/tests/test_e2e_slice.py:198: AssertionError
=========================== short test summary info ============================
FAILED core/tests/test_e2e_slice.py::test_worked_reference_check_and_spec_are_honest
1 failed, 4 deselected in 1.04s

=== PROBE C: delete ONE caveat (#3, the val=2 floor) ===
caveats remaining: 4
E           assert 'MIN_VAL_TASKS' in ' — what this example does NOT prove\n\nWorth more than the parts that work, because these are the claims people over-...that optimization works on a real benchmark.\n   For that, see `examples/tau2_airline/` and `examples/skillsbench/`.\n'

core/tests/test_e2e_slice.py:202: AssertionError
=========================== short test summary info ============================
FAILED core/tests/test_e2e_slice.py::test_worked_reference_check_and_spec_are_honest
1 failed, 4 deselected in 0.06s

=== RESTORED ===
.                                                                        [100%]
1 passed, 4 deselected in 0.04s

Note probe B: one Honest limits occurrence survives the rename (the line-26
cross-ref) — the exact string that used to keep the old assertion green — and the test
now fails anyway. And the count guard is not decorative; adding a 6th caveat while
leaving all five phrases intact also fails, so the guard cannot silently drift out of
sync with the section:

=== PROBE C2: count guard is live (append a 6th caveat, all 5 phrases intact) ===
core/tests/test_e2e_slice.py:204: AssertionError
=========================== short test summary info ============================
FAILED core/tests/test_e2e_slice.py::test_worked_reference_check_and_spec_are_honest
1 failed, 4 deselected in 0.05s

Point taken on this being the seventh instance in the epic of a check matching the wrong
thing. The general shape of that mistake is asserting on a substring of a whole file
when the property is scoped to a section; slicing the section first is what makes the
assertion mean what it says.

The other 7 anti-rot properties — re-run, none weakened

=== P1: append protected_paths: [] to the spec ===
FAILED core/tests/test_e2e_slice.py::test_worked_reference_check_and_spec_are_honest
1 failed, 1 passed, 3 deselected in 3.42s

=== P2: split_val 0.25 -> 0.10 (val floor) ===
37:split_val: 0.10
FAILED core/tests/test_e2e_slice.py::test_worked_reference_check_and_spec_are_honest
1 failed, 1 passed, 3 deselected in 3.41s

=== P3: reinsert a <path> placeholder into PROJECT.md ===
FAILED core/tests/test_e2e_slice.py::test_worked_reference_check_and_spec_are_honest
1 failed, 1 passed, 3 deselected in 3.37s

=== P8: strip every <...> placeholder from templates/project/PROJECT.md ===
FAILED core/tests/test_e2e_slice.py::test_worked_reference_check_and_spec_are_honest
1 failed, 1 passed, 3 deselected in 3.38s

=== P6: patch gate.py to stop logging gate_warning ===
FAILED core/tests/test_e2e_slice.py::test_worked_reference_runs_end_to_end
1 failed, 1 passed, 3 deselected in 3.46s

=== P7: patch the 'STRICT fallback' reason string out of gate.py ===
FAILED core/tests/test_e2e_slice.py::test_worked_reference_runs_end_to_end
1 failed, 1 passed, 3 deselected in 3.56s

=== P7b (control): add a NEW placeholder to the template, absent from the reference — must PASS ===
..                                                                       [100%]
2 passed, 3 deselected in 3.33s

=== ALL RESTORED ===
..                                                                       [100%]
2 passed, 3 deselected in 3.53s

So it is now 10 of 10 broken properties detected (your 7, plus heading rename,
section deletion, and single-caveat deletion), with the false-positive control still
correctly passing.

Finding 2 — the run_target claim, qualified against the code [FIXED]

examples/toy_calc/PROJECT.md:84-89 now reads:

84:4. **`cap-evolve check` passing is not proof the adapter is correct.** It proves the
85-   three abstract methods are implemented and non-stubbed, `tasks()` is non-empty and
86-   stable, `score()` is deterministic, and `materialize()` is callable. It does not run
87-   `run_target` (except an opt-in degenerate-trials probe behind
88-   `CAPEVOLVE_CHECK_TRIAL_PROBE=1` *and* `CAPEVOLVE_N_TRIALS>1`, which only ever appends

Matched against the code it describes:

177:    import os as _os
178:    if (int(_os.environ.get("CAPEVOLVE_N_TRIALS", "1")) > 1
179:            and _os.environ.get("CAPEVOLVE_CHECK_TRIAL_PROBE") == "1"):
180:        try:
181:            r1 = adapter.run_target(probe_task, Path(project_dir), seed=0)
182:            r2 = adapter.run_target(probe_task, Path(project_dir), seed=1)
183:            d1 = json.dumps(r1.to_dict(), default=str, sort_keys=True)

Both env vars, the and conjunction, and the warn-only outcome (rep.notes.append, no
rep.problems) are all now stated. Your framing is the right one: an honesty doc making
an unqualified claim the code contradicts is precisely the failure this reference exists
to prevent.

Finding 3 — cross-link is now bidirectional [FIXED]

Added to examples/toy_calc/README.md's Related section, and made explicit that the
two directories are a deliberate pair rather than duplicates:

- `benchmarks/toy_calc/` — the **declarative** form of this same benchmark: a
  `benchmark.yaml` manifest plus a one-function `project/target.py`, with the spec
  *generated* by `cap-evolve benchmark add`. Take that path when the manifest fits; this
  directory is the reference for the hand-written `CapabilityAdapter` when it doesn't.
  The two are deliberately paired — the zoo entry links back here for the adapter form,
  and this is the "before" side of its boilerplate measurement. *(Lands with #233; the
  path is intentionally unlinked until then.)*

Deliberately not a markdown link, since benchmarks/toy_calc/ doesn't exist until
#233 merges and the link-resolution check (15/15 below) would otherwise fail on this
branch. It becomes linkable in #233's pass.

Nit 4 — quoted transcript now uses the stable prefix [FIXED]

PROJECT.md:72 and capevolve.yaml:67 dropped the closing paren so #195's
; no t-correction on this path suffix can't make them stale:

  • PROJECT.md: …warned; n=2`… (the suffix varies by branch; the prefix is the stable part).
  • capevolve.yaml: …warned; n=2` (the stable prefix; the suffix after `n=2` varies by branch).

Nit 5 — constant named [FIXED]

PROJECT.md:81 now: "2 is still under LOW_CONFIDENCE_VAL_TASKS = 5, the threshold
that flags acceptance decisions as LOW CONFIDENCE."
Matches core/cap_evolve/splits.py:27
on #195 (LOW_CONFIDENCE_VAL_TASKS = 5).

Verification

cap-evolve check green and the sealed number reproduces — bash examples/toy_calc/run.sh:

{
  "ok": true,
  "stubs": [],
  "problems": [],
  "notes": [
    "tasks('val') -> 8 task(s)",
    "scorer deterministic (probe reward=0.0000)",
    "materialize() callable (dry-run into temp copy; host untouched)"
  ]
}
{"dashboard": "skipped", "reason": "capevolve-dashboard not installed (pip install -e dashboard/backend)"}
{
  "run_dir": ".capevolve/run_demo",
  "best_id": "cand_0001",
  "baseline_val": 0.0,
  "test_reward": 1.0,
  "test_baseline_reward": 0.0,
  "test_delta": 1.0,
  "test_pass_k": {"1": 1.0, "2": 0.0},
  "iterations": 3
}

Full suite — still exactly 181, 0 failed (no new tests; the existing guard got teeth):

$ PYTHONPATH=core python -m pytest core/tests -q
........................................................................ [ 39%]
........................................................................ [ 79%]
.....................................                                    [100%]
181 passed in 69.63s (0:01:09)

test_dashboard_launch.py (#200) passed this run.

compileall clean; all 15 relative links still resolve (the new zoo pointer is
deliberately unlinked, so the count is unchanged):

compileall clean
relative links checked=15 broken=0

Authorship:

Osher Elhadad <Osher.Elhadad@ibm.com> | Osher Elhadad <Osher.Elhadad@ibm.com>

Merge order — corrected, with the conflict I missed

Adopting your order:

#183 → #181 → #198 → #195 → #197 → #244 → #233

Three conflicts to expect, all trivial:

  1. #181 also edits examples/toy_calc/README.md — the same ## Files bullet list
    this PR rewrites. I did not mention this and should have. Resolution: take HEAD
    (this PR's version, which names the three required methods and labels apply optional).
  2. #197CHANGELOG.md — ordinary "everyone appends under ### Added".
  3. #233CHANGELOG.md — same.

#183 auto-merges clean (its only hunk is templates/project/PROJECT.md:1, the H1;
this PR's hunk starts at line 6 — non-adjacent).

Two order-dependent follow-ups: once #195 is in, the quoted transcript prefix above
is already suffix-tolerant, so nothing further is needed; once #233 is in, its author
(or a one-line follow-up) turns the benchmarks/toy_calc/ pointer into a real link.

Thanks for the review — particularly for breaking the assertion rather than reading it,
and for the point that I undersold the PR's own case. The per-decision commentary being
the real payload (not the PROJECT.md) is the better framing, and it is what #233's
back-link depends on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ship a filled, runnable worked .capevolve/project reference (current scaffold is a blank template)

3 participants