feat(packaging): ship a runnable pip install cap-evolve release path (#125) - #250
feat(packaging): ship a runnable pip install cap-evolve release path (#125)#250OsherElhadad wants to merge 2 commits into
pip install cap-evolve release path (#125)#250Conversation
#125) Rename the distribution `cap-evolve-core` -> `cap-evolve` (the old name was never published, so there is nothing to migrate) and make the built artifact actually runnable, then add the workflow that would publish it. The core is pure `.py`, but a run also needs data that lives outside it: `skills/optimizers/registry.yaml`, `skills/_registry/manifest.json`, every skill's `scripts/run.py`, and `templates/project/`. A wheel built from the old pyproject shipped none of it — #193's `install.sh` hole (a stock install that cannot run an optimizer) in pip shape. Verified as a negative control: such a wheel dies with `skills dir not found`. - `cap_evolve/_bundled/{skills,templates}` are SYMLINKS to the repo trees, declared as package-data. setuptools follows them into both wheel and sdist, so there is one copy and the package can never drift from the repo. - New `cap_evolve.resources.resource_root()` returns the repo root or the packaged bundle, whichever is real. It replaces six `parents[2]` repo-layout assumptions in `harness.py` and the CLI's `_find_skills_dir()` parent-walk, which worked only from a checkout. - Version single-sourced in `cap_evolve.__version__`; pyproject reads it via `[tool.setuptools.dynamic]`, so there is no second literal to forget. - `CITATION.cff` date-released 2026-06-14 -> 2026-07-27, the v0.1.0 GitHub release date (#186). Machine-read metadata that was six weeks stale. - `MANIFEST.in` + exclude-package-data keep a local `__pycache__` out of the artifacts, so a dev's build matches CI's. `.github/workflows/release.yml` builds sdist+wheel, `twine check`s them, asserts the required data files are inside the artifacts, then installs EACH artifact into a clean venv and runs a real zero-API optimization to a sealed test number from a temp dir outside the checkout with `HOME` redirected — inside the repo the skills-dir fallbacks rescue a broken package and prove nothing. Publish is PyPI Trusted Publishing (OIDC, no stored token) on a `v*` tag, plus a GitHub Release whose notes are sliced from the CHANGELOG section for that version. NOTHING IS PUBLISHED by this change. No tag is pushed, no PyPI/TestPyPI credentials exist in the repo, and the publish jobs only fire on a `v*` tag or an explicit manual dispatch. `core/tests/test_packaging.py` guards the version single-sourcing, the CITATION agreement, the data-file declarations, the symlink invariant, and that no `parents[2]` repo-walk creeps back into the core. Closes #125
🔬 EvidenceEvery command and its literal output. Python: 0. Baseline test suite on
|
🔍 Review — PR #250CHANGES REQUESTED The artifact itself is good: I built both, installed both into clean venvs, and both completed a real run from outside the checkout at The release path is not. Two defects, both on the irreversible side of the tag push. Blocking1. The job graph: The Consequence: someone follows the runbook, pushes Fix: make the notes slice a precondition, not a postcondition. Move the step into 2.
Note the metadata is already baked into the built wheel, so this is not fixable post-publish either: Fix, pick one: (a) drop the Non-blocking3. The symlink becomes a 15-byte text file, setuptools globs nothing, and you get a wheel that installs and answers 4. 5. But 6. Nits7. 8. Does the artifact work?Yes — both, independently reproduced. Neither run had Both wrote a standalone Negative control — fails exactly as claimed. I rebuilt the pre-PR packaging shape from Same command, same env, same cwd shape — only the packaging differs. The positive result means something. Symlinked package-data auditReal bytes in both artifacts, zero symlinks survive into either. setuptools dereferences on the way in, for wheel and sdist: And byte-compared, not name-compared — all 213 data files against the repo, in both artifacts: (The sdist's 309 vs 213 is tar directory members; the extras list is entirely directories.) Post-install trees, both venvs: (The Case-insensitive filesystem: no risk found — no two paths under Windows / no-symlink checkout: real but bounded — see finding 3. The link degrades to a text file and the wheel ships zero data while passing CompletenessDerived from what a run actually opens: the 7
Reverse direction — nothing shipped that shouldn't be: Name change
Every reference updated: Remaining occurrences are all deliberate or historical: The one broken documented install line is finding 2 — and it is a new name, not a stale one. Release-path safetyThe tag-mismatch gate fires. Simulated both directions with the workflow's own code: The dynamic version also resolves in the built artifact, not just in-tree: And the guard against a second literal actually bites — I added Can host state fake a green build? For the artifact check, no — and this is the part of the PR I'd defend hardest. The gate that does not protect anything is the CHANGELOG one — finding 1. Host state can't fake a green build; job ordering can let a red build publish anyway. That is the same class of hole, one layer up.
Merge-order note#189 → #184 → #250. Not the order the PR assumes.
Verification I re-ranFull suite, 187 (179 + 8), Repo-checkout mode is unaffected by the And I ran the toy example from an identical harness against this branch and against PR verification claims: all reproduced. 247/383 artifact entries, 0 Test quality of the 8 new tests: they assert declarations, not artifact contents — Release runbook ( |
|
❌ 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. |
… the unresolvable [dashboard] extra Review fixes for #250. BLOCKING 1 — the workflow published, then failed its own gate. The CHANGELOG slice lived in `github-release` (`needs: publish-pypi`), so pushing `v0.1.0` today would upload to PyPI (irreversible) and only then fail, because this branch has no `[0.1.0]` section. All gates now run in `build`/`verify`: tag/version match, a `_bundled` symlink precondition, the CHANGELOG slice, `twine check`, the artifact-contents assertion, and the install-and-run. `publish-pypi` holds nothing but the upload; `github-release` validates nothing and consumes the notes artifact `build` proved non-empty. This also removes the dependency on #184 landing first — a premature tag fails in `build`, uploading nothing. Artifact `dist` -> `dist-core` so a second distribution cannot collide, and `CAPEVOLVE_RESOURCE_ROOT` joins the `env -u` list for symmetry. BLOCKING 2 — the PyPI long description documented an install that cannot resolve. `dashboard = ["capevolve-dashboard"]` named a distribution that is 404 on PyPI and that no workflow publishes, and `core/README.md` (the rendered PyPI page) told users to run `pip install 'cap-evolve[dashboard]'`. Extras bake into the wheel METADATA, so this was unfixable post-publish. Dropped the extra rather than take on a second distribution: every run already writes a standalone `dashboard.html` with nothing extra installed, so this is a docs/metadata gap, not a feature gap. README now points at the from-clone install. New `test_documented_pip_installs_can_resolve` asserts every documented `pip install` on the two published surfaces names a declared extra, and `test_pypi_page_counts_match_the_repo` guards the counts on the one page where a stale count is published rather than merely wrong. Windows-symlink silent pass: on a `core.symlinks=false` checkout the `_bundled` symlinks degrade to 15-byte text files, the package-data globs match nothing, and `python -m build` emits a data-free wheel that `twine check` PASSES. Kept the symlinks (they are what stops the packaged trees drifting from the repo) and made the failure loud instead: a pre-build symlink assertion, an artifact-assertion failure message that names this cause, and the constraint documented in pyproject.toml, resources.py, and docs/TROUBLESHOOTING.md. Also documents `CAPEVOLVE_RESOURCE_ROOT` in docs/TROUBLESHOOTING.md, including the #208 stale-`~/.claude/skills` trap it is the escape hatch for.
🔧 Review fixesCommit Gate orderingJob order on a
Simulation, not reading
1. Missing 2. Tag mismatch: 3. Artifact missing its data files — induced the real cause (a And the artifact assertion itself, run on that data-free wheel (which 4. Control — CHANGELOG section present (post-#184 shape): all runnable gates pass, and only then is the upload reached. #184 dependency removed. The CHANGELOG gate is in
|
Closes #125
pip install cap-evolvenow produces an install that can actually run. The distributionis renamed
cap-evolve-core→cap-evolve, the built artifact carries the runtimedata a run cannot start without, and a release workflow builds + verifies + (on a tag)
publishes it.
No upload happened and none can happen from this branch. I have no authorization to
publish, and publishing is irreversible (a PyPI version can be yanked but never
replaced). Concretely:
if: startsWith(github.ref, 'refs/tags/v');gh secret listshows no PyPI or TestPyPI credentials in the repo (onlyANTHROPIC_AUTH_TOKEN,ANTHROPIC_BASE_URL,BOBSHELL_API_KEY,SKILLBERRY_BOT_TOKEN), and the environments arecopilot+ one unrelated one. TheTestPyPI dry-run was therefore not run, per the instruction not to create or request
credentials. The
publish-testpypijob exists and is manual-dispatch only.Everything verifiable without write intent was verified: build,
twine check, artifactcontents, and a real end-to-end run from a fresh venv installed from the artifact.
The bug this fixes (the reason the verify job is not optional)
The core is pure
.py, but a run also needs data that lives outside the package:skills/optimizers/registry.yaml,skills/_registry/manifest.json, every skill'sscripts/run.py, andtemplates/project/. A wheel built from the oldpyproject.tomlshipped none of it. That is #193's
install.shhole — a stock install that looks fineand cannot run an optimizer — in pip shape.
I built that broken wheel deliberately as a negative control:
Worth stating why the run has to happen outside the checkout with
HOMEredirected(#208's lesson):
_find_skills_dir()falls back to./.claude/skills,~/.claude/skills,then a parent-walk to the repo's own
skills/. My first attempt ran inside my own$HOMEand silently picked up a stale
~/.claude/skills— it failed withhill-climb: error: unrecognized arguments: --capabilitiesand told me nothing about thewheel. A green run in the repo tree proves nothing.
What data files were missing, and why they are in now
skills/_registry/manifest.jsoncap-evolve runresolves every phase/algorithm skill through itskills/optimizers/registry.yamloptimizer_skillto a backend (#193's exact file)skills/**/scripts/{run,check,abstract}.pyskills/**/SKILL.md,meta.yaml,references/**templates/project/**optimizer/INSTRUCTIONS.md, whichharness.pyrenders per iterationThey ship as
cap_evolve/_bundled/{skills,templates}— symlinks to the repo trees,declared as package-data. setuptools follows them into both wheel and sdist, so there is
exactly one copy on disk and the packaged data can never drift from what the repo tests.
core/tests/test_packaging.pyasserts they stay symlinks rather than becoming a second copy.dashboard/frontend/dist/(#188) is deliberately not included: it belongs tocapevolve-dashboard, whoseresolve_static_dir()looks for it atdashboard/frontend/distrelative to its own package — putting it in this wheel wouldduplicate ~950 KB that nothing would read. The dashboard is the
[dashboard]extra, andevery run still writes a self-contained static
dashboard.htmlwith no backend at all.How the code changed
resources.resource_root()returns the repo root or the packaged bundle, whichever isreal (
$CAPEVOLVE_RESOURCE_ROOToverrides both). It replaces the sixPath(__file__).resolve().parents[2]repo-layout assumptions inharness.pyand theCLI's
_find_skills_dir()parent-walk — root cause, one place, rather than a special caseper call site. A test fails if
parents[2]reappears in the core.Version single-source
cap_evolve.__version__is authoritative.pyproject.tomlreads it via[tool.setuptools.dynamic], so there is no second literal to forget; the test rejects ahardcoded
version =in pyproject.CITATION.cff'sversion:must match it (asserted),and the release workflow refuses to build if the git tag disagrees with
__version__.Does #186 block the release?
No — it is fixed here, because it is release metadata and this is the release PR.
CITATION.cffsaiddate-released: 2026-06-14whilev0.1.0's GitHub release published2026-07-27(tag commit2026-07-26). Both dates verified viagh release view v0.1.0(publishedAt: 2026-07-27T10:51:37Z) andgit log -1 v0.1.0.Now
2026-07-27, matching #101's Keep-a-Changelog reasoning (date the release, not thetag commit), and guarded by a test. #186 can be closed by this PR.
Expected merge order
## [0.1.0]section.I verified my slice against docs(changelog): correct stale 0.46→0.80, add real [0.1.0] section, move features to Added #184's actual CHANGELOG (see Verification): 119 lines,
zero link definitions. Merge docs(changelog): correct stale 0.46→0.80, add real [0.1.0] section, move features to Added #184 before pushing any tag. Merging this PR first is
fine; the workflow only reads the CHANGELOG on a tag, and it fails loudly on an
empty slice rather than shipping blank notes.
core/README.md(the page PyPI renders) states20 skills · 5 algorithms (3 run-executable + 2 agent-mode) · 14 optimizer backends;
I recomputed all four from the filesystem on this branch by docs: unify skill count at 20 and algorithm count at 5 across all surfaces (closes #102) #189's own counting rule
and they match, so the two PRs agree in either order.
What a human must do to release
Nothing here has been done. Steps 1–2 are one-time.
1. Configure PyPI Trusted Publishing (one time, no token to store).
On https://pypi.org/manage/account/publishing/ add a pending publisher:
PyPI project
cap-evolve· ownerskillberry-ai· repocap-evolve·workflow
release.yml· environmentpypi.(The name
cap-evolveis currently unregistered —GET /pypi/cap-evolve/json→ 404 —so the first publish claims it.
cap-evolve-coreis also unpublished.)2. Create the
pypiGitHub environment (andtestpypiif you want the dry-run):gh api -X PUT repos/skillberry-ai/cap-evolve/environments/pypi gh api -X PUT repos/skillberry-ai/cap-evolve/environments/testpypi # optional3. Dry run — build + verify only, uploads nothing:
Both
verify (wheel)andverify (sdist)must printsealed test_reward 1.0.4. Optional TestPyPI rehearsal (needs step 2's
testpypienv + a TestPyPI pendingpublisher; this DOES upload, to TestPyPI only):
gh workflow run release.yml --repo skillberry-ai/cap-evolve -f target=testpypi pip install --index-url https://test.pypi.org/simple/ cap-evolve && cap-evolve version5. Confirm the CHANGELOG has a section for the version (PR #184 must be merged):
6. THE IRREVERSIBLE STEP — push the tag. A PyPI version can be yanked but never
replaced. Bump
__version__first if0.1.0is not the version you mean to publish(
v0.1.0already exists as a tag, so a re-release needs a new version):That builds, verifies, publishes to PyPI, and cuts the GitHub Release with the
CHANGELOG-sliced notes.
7. Verify the published package on a clean machine:
8. Flip the docs.
docs/INSTALL.mdcarries a "Not published yet" callout on the PyPIpath — delete it once step 7 passes, and update
README.md/site/getting-started.htmlquickstarts to lead with
pip install cap-evolve(the./corefrom-source path stays,for dev and for
examples/).Verification
Full pasted output is in the 🔬 Evidence comment. Summary:
Test suite — 187 passed, 0 failed (baseline 179 + 8 new packaging tests):
Build +
twine check+ artifact contents:Fresh venv from the WHEEL, run from
/tmp/finw-7TvtwithHOMEredirected:Fresh venv from the SDIST (sdists commonly miss data files — this one does not),
run from
/tmp/fin-Vion: identicaltest_reward 1.0,baseline_val 0.0, 3 iterations.CHANGELOG release-notes slice, against PR #184's actual CHANGELOG:
compileallovercore/cap_evolve+core/tests: clean, exit 0.Files touched
Packaging:
core/pyproject.toml,core/MANIFEST.in(new),core/cap_evolve/_bundled/{skills,templates}(new symlinks).Code:
core/cap_evolve/resources.py(new),core/cap_evolve/harness.py,core/cap_evolve/cli.py,core/cap_evolve/_bootstrap.py.Tests:
core/tests/test_packaging.py(new).Release:
.github/workflows/release.yml(new),.github/workflows/ci.yml(comments).Metadata/docs:
CITATION.cff,CHANGELOG.md,core/README.md(the page PyPI renders),README.md,docs/INSTALL.md,docs/GETTING_STARTED.md,docs/TROUBLESHOOTING.md,site/getting-started.html,dashboard/backend/pyproject.toml,dashboard/backend/README.md,dashboard/backend/capevolve_dashboard/_bootstrap.py.