Skip to content

Cross-host parity: single-source per-host metadata + a stdlib-only fallback that is proven (and the install.sh smoke job that makes any host ✅ honest) - #245

Open
OsherElhadad wants to merge 8 commits into
mainfrom
feat/issue-143-cross-host-parity
Open

Cross-host parity: single-source per-host metadata + a stdlib-only fallback that is proven (and the install.sh smoke job that makes any host ✅ honest)#245
OsherElhadad wants to merge 8 commits into
mainfrom
feat/issue-143-cross-host-parity

Conversation

@OsherElhadad

Copy link
Copy Markdown
Collaborator

Closes #143.

Issue #143 asked for per-host metadata, a stdlib-only fallback, and turning "best-guess" host dirs into verified ones. Reading the ground truth turned up two things that change what that issue means:

So this PR is not "add metadata claiming parity" — it makes parity checkable, then states only what is actually verified.

1. Single source of per-host metadata + its guard

skills/_registry/hosts.yaml is now the single source: per host, the --host aliases, the install destination, the grade, the artifact justifying that grade, and the display / description / invoke triple a host UI needs (the metadata #143 asked for).

That list previously lived in five places and had drifted:

Consumer Before Now
install.sh --host hardcoded case, 12 arms shells python3 -m cap_evolve.hosts --dest
doctor._VERIFIED_HOST_DIRS hand-maintained tuple — knew 6 of 12 destinations, so six correct host dirs were reported "best-guess" _known_host_dirs(), derived
docs/HOST_SUPPORT.md destination table independent copy a rendering, guarded
docs/HOST_SUPPORT.md optimizer table independent copy unchanged (different axis: backends, not destinations)

The guardcore/tests/test_host_parity.py (27 tests) fails the build when any consumer disagrees with hosts.yaml, in all three directions:

  • test_host_support_md_destination_table_matches_hosts_yaml — parses the docs table and compares aliases/dests/badges set-wise.
  • test_doctor_known_host_dirs_come_from_hosts_yaml — also asserts _VERIFIED_HOST_DIRS has not come back.
  • test_install_sh_derives_its_mapping_from_hosts_yaml — asserts case "$HOST" in is gone and no row's dest is hardcoded in the --host branch.
  • test_every_alias_resolves_through_the_cli_install_sh_calls — parametrized over all 20 aliases, running the literal command install.sh shells.
  • test_verified_rows_cite_an_executing_artifact_that_existsa verified row must cite a file that exists. This is the exact defect Onboarding honesty: real toy_calc screenshot (+ fix the dashboard charts that never rendered) + verified/best-guess host labels #202's review caught (a ✅ with no proving artifact).

Nothing in the tests hardcodes a destination, so the tests cannot become a sixth copy.

2. What the stdlib-only fallback covers, and how it is proven

core/tests/test_stdlib_only.py runs the real install path in a subprocess whose sys.meta_path raises ImportError for every module not in sys.stdlib_module_names — not by inspection. Covered: host metadata + the --dest CLI, read_yaml on the real optimizers/registry.yaml, build_manifest.py, and cap-evolve version/check. Plus test_the_blocker_actually_blocks, so the hook can't silently become a no-op and make the rest vacuous.

This is load-bearing, not decorative: install.sh calls python3 -m cap_evolve.hosts before pip install ./core has happened, so that module must work with nothing installed.

3. #208's smoke job — implemented

ci/install_smoke.sh + the install-smoke job in ci.yml. Every detail is deliberate:

  • runs ./install.sh --host claude with a temp $HOME, so the mapping actually places the files;
  • unsets $CAPEVOLVE_SKILLS_DIR, or the mapping is never consulted;
  • runs from a cwd outside the repo — inside it, run.py's parent-walk silently rescues you and the job proves nothing;
  • asserts test_reward 1.0, not exit 0 — a failing optimizer is silent: the run completes, keeps the seed, reports 0.0.

Counter-proof that it is load-bearing: with #193's registry.yaml copy reverted, the job fails (both at the file assert and, with that bypassed, at test_reward 0.0). Output in the Evidence comment.

4. Host grades — what changed and why

Host destination Before After Artifact
claude / claude-code$HOME/.claude/skills 🟡 ✅ verified ci/install_smoke.sh, run by the install-smoke CI job and core/tests/test_install_smoke.py. Installs through the --host mapping and completes a real zero-API optimization from outside the repo.
codex, gemini-cli, opencode, ibm-bob 🟡 🟡 unchanged Vendor reference docs only. The smoke job installs to one destination; grading these would be the unproven ✅ this epic keeps finding.
cursor, droid, copilot, kimi, pi, antigravity, openclaw ➖ unchanged Dotdir convention only.

The destination table now has exactly one ✅, and it is the only row with an executing artifact.

Also fixed on the way: install.sh copies _registry/hosts.yaml (same bug class as #193 — a plain file under skills/ the copy loop skips), and warns on stderr instead of silently guessing when --host has no row.

Expected merge order

  1. Add cap-evolve doctor install/health diagnostic #193 (feat/issue-121-doctor) — dependency. Its registry.yaml fix is what makes the smoke job pass; without it the new job red-lines main.
  2. Onboarding honesty: real toy_calc screenshot (+ fix the dashboard charts that never rendered) + verified/best-guess host labels #202 (fix/issue-126-onboarding-polish) — dependency. Adds docs/HOST_SUPPORT.md, which this PR's guard reads.
  3. This PR. Both are merged into this branch, so the diff is additive once they land.

Zero new runtime deps in core.

Verification

Tests: 179 (main) → 225 (with #193 + #202 merged) → 259 here. 0 failed. +34 mine: 27 parity, 6 stdlib-only, 1 install smoke.

$ PYTHONPATH=$PWD/core python -m pytest core/tests -q
259 passed in 71.11s (0:01:11)

$ bash -n install.sh && bash -n ci/install_smoke.sh && echo OK
OK

$ python -m compileall -q core/cap_evolve core/tests skills && echo OK
OK

A real ./install.sh install completes a run from a cwd outside the repo:

== install via ./install.sh --host claude (HOME=/…/capevolve-install-smoke.…/home) ==
  to:   /…/capevolve-install-smoke.…/home/.claude/skills
OK: 22 skill dirs + registry + hosts.yaml
== zero-API toy_calc run from OUTSIDE the repo, against the INSTALLED skills ==
{
  "best_id": "cand_0001",
  "baseline_val": 0.0,
  "test_reward": 1.0,
  "test_delta": 1.0,
  …
}
PASS: ./install.sh --host claude produces an install that optimizes from outside the repo

Counter-proof — revert #193's registry.yaml copy and the job fails:

::error::installed tree is missing optimizers/registry.yaml
EXIT=1

and with that file check bypassed, the run-level assert catches it too — "best_id": "seed", "test_reward": 0.0, EXIT=1. That silent 0.0 is precisely the failure mode #193 describes.

Stdlib-only fallback under blocked non-stdlib imports (all 6 pass):

test_the_blocker_actually_blocks PASSED
test_hosts_metadata_resolves_with_no_third_party_modules PASSED
test_hosts_cli_dest_works_with_no_third_party_modules PASSED
test_optimizer_registry_parses_with_no_third_party_modules PASSED
test_manifest_build_works_with_no_third_party_modules PASSED
test_cli_version_and_check_work_with_no_third_party_modules PASSED

Metadata and HOST_SUPPORT.md cannot diverge — broke each of the three consumers in turn, the guard fired each time, restored:

# docs badge drift  (codex 🟡 -> ✅ in the docs only)
AssertionError: $HOME/.agents/skills: docs say verified, hosts.yaml says docs-checked

# dest drift  (hosts.yaml .kimi -> .kimi2)
AssertionError: only in docs: ['$HOME/.kimi/skills']  only in hosts.yaml: ['$HOME/.kimi2/skills']

# doctor regressed to a hand-maintained tuple
AssertionError: doctor does not know $HOME/.agents/skills

Full commands and untruncated output in the ## 🔬 Evidence comment below.

Osher Elhadad added 7 commits July 30, 2026 00:37
One stdlib-only module (`core/cap_evolve/doctor.py`) plus CLI wiring. Nine checks,
each targeting a failure `docs/TROUBLESHOOTING.md` already documents as a real
support case: Python version, core importability + venv trap, `cap-evolve` on PATH
+ shadowing second install, git (the default version store), skills dir + manifest
consistency (flagging install.sh's "best-guess" host dirs), optimizer CLI
availability, provider credentials, run-dir writability, and — inside a project —
`cap-evolve check` (reused, not reimplemented).

Credentials are reported PRESENCE-only (`NAME: set (hidden)`); values are never
read into any reported field, presence/absence is kept in structured
`present`/`absent` name lists, and the whole report passes through
`dashboard.redact` as defense in depth. `test_secret_value_never_printed` plants a
canary token in every known credential var and asserts neither it nor any 8+ char
prefix reaches the human output, the `--json` output, or the raw report.

Exits non-zero on any hard failure so CI can gate on it; warnings stay advisory.

Closes #121
…st labels

Closes #126.

Two onboarding-credibility papercuts from the docs/site review, both honesty-flavored.

1. The getting-started screenshot showed something the user won't see. Its own
   caption admitted it: "Shown here from a real benchmark run, not the toy."

   Root cause of *why* nobody had ever screenshotted the toy: the dashboard's SVG
   charts have never rendered. `ParentNode.append()` returns undefined, so every
   `el.append(svg(...)).textContent = x` threw a TypeError that killed all charts
   on the page — while leaving the panel <h2> titles intact, so the existing test
   passed. Fixed in the shared svg() helper via a `text:` pseudo-attribute (one
   place, all seven call sites) rather than patching each caller, and the
   regression test now asserts on the chaining shape that throws.

   With charts rendering, site/assets/toy-calc-dashboard.png is a real headless
   screenshot of the dashboard.html written by `bash examples/toy_calc/run.sh` —
   $0.0000, 0 tokens, 2 tasks, 4 candidates, because that is what the toy is.
   docs/GETTING_STARTED.md now also pastes the literal unedited run output,
   including the "dashboard": "skipped" line and pass^2 = 0.0, and explains both.

2. Host support advertised ~14 backends as first-class while install.sh privately
   called several skill-dir mappings "best-guess". docs/HOST_SUPPORT.md is now the
   single source of truth, grading every host verified (CI-executed, artifact
   cited per row) / docs-checked (vendor docs read, never run here) / best-guess
   (dotdir guess, needs --dest). Only claude-code and mock clear the verified bar.
   README, RUN.md, docs/INSTALL.md, install.sh and the site link to it instead of
   restating the list, so the claim can only drift in one file. The table is also
   the intended base for the cross-host skill parity work in #143.

The README hero shot stays — it is a genuine tau2 run and was already labelled as
such; it now points at Getting started for what the quickstart really produces.
…ble-install FAIL

Review on PR #193 requested changes on 5 blocking findings.

Credential leak (the CONFIRMED one, `project` check). Two compounding causes, both
fixed:
- `dashboard.redact` was shape-based only, so a watsonx key, a UUID token or a
  GitHub PAT passed through. Adds `ghp_`/`github_pat_`/UUID shape rules and — the
  part that actually generalizes — a shape-INDEPENDENT pass that scrubs the literal
  values of this process's secret-looking env vars.
- More importantly, arbitrary third-party exception text is no longer routed to
  stdout verbatim. `_summarize_untrusted` redacts first, then bounds the message to
  a short excerpt; the full text goes to `.capevolve/project/doctor-check.log` for
  local inspection. Redact-before-truncate matters: cutting first can slice a secret
  into a prefix that no longer matches any rule. `check.py` also names the exception
  TYPE, which is always safe and is the part a reader acts on.
- The canary test was `sk-`-prefixed, i.e. only the shape redact already handled.
  Replaced with 5 shapes (bare high-entropy, UUID, `ghp_` PAT, watsonx-style,
  `sk-`) x 3 surfaces (human report, --json, to_dict), plus a case where a user
  adapter RAISES with the credential in its message — the actual proven leak path —
  asserting neither the value nor a 6-char prefix appears.

install.sh was the real bug behind the "exits 0 on an install that cannot run"
finding. The copy loop only walked component DIRECTORIES, so the plain file
`skills/optimizers/registry.yaml` was never installed, and
`run-optimizer/scripts/run.py` raises FileNotFoundError the moment a run starts
without it — every stock install was in that state. install.sh now copies it to
`$DEST/optimizers/`, exactly where run.py's parent-walk looks. Doctor reports a
missing registry as FAIL (was WARN whose fix was "run ./install.sh", the command
that produced the state) with a remediation that works, and splits "on PATH" from
"local/zero-API" so the FAIL/WARN branches are reachable instead of mock alone
always passing.

Wrong-venv false positive: compared `resolve(sys.executable)` against $VIRTUAL_ENV,
which follows bin/python's symlink out to the base interpreter, so every correctly
activated venv warned. Compares $VIRTUAL_ENV to sys.prefix now, with a regression
test for the PASS case.

Remediation audit: `store: none` does not exist (store.py accepts git|copy|command)
-> `store: copy`. Audited every other remediation string against the code; the
build_manifest hint now prints a path that is valid from a flat install.

Crash on malformed input: `s.get("entry", "")` returns None on an explicit null and
`Path / None` is a TypeError, so doctor died on exactly the malformed manifest it
exists to explain. All manifest reads are None-hardened; a null entry is treated as
missing, which it is.

Non-blocking: credentials warns on a partially-set group that needs all its vars
(RITS/watsonx/the ANTHROPIC_BASE_URL+AUTH_TOKEN pair); reads NAMES only from a
repo-root .env as INSTALL.md mandates; an adapterless scaffold is a FAIL instead of
"not a project"; relative skills dirs resolve before the suffix test;
/.openclaw/workspace/skills added to the verified list; CHANGELOG entry added.

Tests: 224 passed, 0 failed. compileall clean.
…, add a dead-panel test

- GETTING_STARTED: pass^k is per-task over TRIALS, not tasks. The real cause of
  `"2": 0.0` is num_trials: 1 (k > n). Notes that PR #187 omits the key entirely.
- GETTING_STARTED: stop calling the transcript "literal, unedited" unconditionally —
  it was the degraded `"dashboard": "skipped"` branch. Metric lines are byte-exact;
  the three environment-dependent lines are marked and the installed-backend variant
  is spelled out.
- llms.txt / OPTIMIZE_YOUR_OWN / run-optimizer SKILL.md: the LLM-facing surfaces
  carried the unqualified 14-backend list. All now say only claude-code and mock are
  CI-executed and link docs/HOST_SUPPORT.md; llms.txt gains it in the doc index.
- HOST_SUPPORT: the install-DESTINATION table cannot meet this page's own ✅ bar —
  nothing in CI runs install.sh (CI sets $CAPEVOLVE_SKILLS_DIR, bypassing the --host
  mapping). Downgraded claude/claude-code to 🟡 with a note stating what the badges
  in that table do and do not mean. Cross-referenced the gemini extension-vs-workdir
  path divergence for #143.
- test_dashboard: the source-level regex only ever caught one shape; its comment now
  says so. New test_dashboard_js_renders_all_panels EXECUTES the generated inline
  script under a minimal DOM shim (append() returns undefined, as in the browser) and
  asserts section/h2/svg/text counts — so a throw inside any panel fails CI instead
  of passing on source-string titles, which is how this bug survived six weeks.
- CHANGELOG: entry for the shipped dashboard regression (8 of 13 panels absent since
  4c87ed1).
…/issue-143-cross-host-parity

# Conflicts:
#	CHANGELOG.md
… that is proven

Closes #143.

Issue #143 asked for per-host metadata, a stdlib-only fallback, and "verified vs
best-guess" host placements. Two findings while reading the ground truth changed
what that means, so this lands the checkable version rather than the claimed one.

1) skills/_registry/hosts.yaml is now the SINGLE source of per-host metadata:
   aliases, install destination, grade + the artifact justifying it, and the
   display/description/invoke triple a host UI needs. That list previously lived in
   five places and had drifted — install.sh's `case`, doctor._VERIFIED_HOST_DIRS
   (which knew 6 of the 12 real destinations, so six correct host dirs were reported
   "best-guess"), and two docs/HOST_SUPPORT.md tables. install.sh now resolves
   --host by shelling `python3 -m cap_evolve.hosts --dest`, doctor derives its list,
   HOST_SUPPORT.md is a rendering, and core/tests/test_host_parity.py fails the
   build when any of them disagree — including that a `verified` row must cite an
   artifact that exists, which is the exact defect PR #202's review caught.

2) ci/install_smoke.sh + the install-smoke CI job are the first thing anywhere that
   executes ./install.sh (#208). Nothing did: every job sets $CAPEVOLVE_SKILLS_DIR
   to the repo tree, taking install.sh's first precedence branch and bypassing the
   --host mapping — which is how #193's "no stock install can run an optimizer"
   survived green CI. The job installs through the mapping into a temp $HOME, unsets
   $CAPEVOLVE_SKILLS_DIR, and completes a zero-API toy_calc run from a cwd OUTSIDE
   the repo (inside it, run-optimizer's parent-walk finds the source tree and the
   job proves nothing), asserting test_reward 1.0 rather than exit 0 — a broken
   optimizer silently keeps the seed and reports 0.0. Verified load-bearing by
   reverting #193's registry.yaml copy: the job fails.

   That artifact promotes the `claude` destination row 🟡 -> ✅. Every other row
   stays 🟡/➖: the job exercises one destination, and grading the rest would be the
   unproven ✅ this epic keeps finding.

3) The stdlib-only fallback is proven, not asserted. core/tests/test_stdlib_only.py
   runs the whole install path (host metadata + the --dest CLI, the real
   optimizers/registry.yaml, the manifest build, cap-evolve version/check) in a
   subprocess whose sys.meta_path raises ImportError for every non-stdlib module,
   plus a guard-the-guard test so the hook cannot silently become a no-op.
   install.sh depends on this literally: it calls cap_evolve.hosts BEFORE
   `pip install ./core` has happened.

install.sh also copies _registry/hosts.yaml, the same class of file (a plain file
under skills/, not a skill dir) that the copy loop skipped in #193, and now warns on
stderr instead of silently guessing when a --host has no row.

Zero new runtime deps. Tests: 179 (main) -> 225 (with #193 + #202) -> 259 here.
Copilot AI review requested due to automatic review settings July 30, 2026 21:19

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.

text = dashboard.write_dashboard(rd).read_text(encoding="utf-8")

run_data = re.search(r'id="run-data">(.*?)</script>', text, re.S).group(1)
script = re.findall(r"<script>(.*?)</script>", text, re.S)[-1]
@skillberry-bot skillberry-bot added honesty Honesty/consistency of reported results (brand-critical) dx Developer/onboarding experience enhancement New feature or request documentation Improvements or additions to documentation observability Live run visibility, logging, tracing site GitHub Pages website labels Jul 30, 2026
@skillberry-bot

Copy link
Copy Markdown
Contributor

🏷️ Automatic Labeling

I've analyzed this pull request and added the following labels:

  • honesty - dx - enhancement - documentation - observability - honesty - dx - enhancement - documentation - observability - site

These labels were selected based on the PR title, description, and changed files. If you believe any labels are incorrect or missing, feel free to adjust them manually.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔬 Evidence

Every command and its literal output. Worktree at feat/issue-143-cross-host-parity (both dependency branches merged in), Python from /tmp/ce-venv.

1. A real ./install.sh install completes a run from OUTSIDE the repo

=== 1. install smoke, full output ===
$ PATH="/tmp/ce-venv/bin:$PATH" bash ci/install_smoke.sh
== install via ./install.sh --host claude (HOME=/var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/capevolve-install-smoke.XXXXXX.5mhQrtfWPo/home) ==
cap-evolve: installing skills
  from: /tmp/wt-143/skills
  to:   /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/capevolve-install-smoke.XXXXXX.5mhQrtfWPo/home/.claude/skills
  + orchestrate/orchestrate
  + orchestrate/using-cap-evolve
  + phases/baseline
  + phases/diagnose
  + phases/evaluate
  + phases/finalize
  + phases/gate
  + phases/implement-and-check
  + phases/intake
  + phases/report
  + capabilities/mcp-tool
  + capabilities/skill-package
  + capabilities/system-prompt
  + capabilities/tools
  + algorithms/agent-optimize
  + algorithms/evograph
  + algorithms/gepa
  + algorithms/hill-climb
  + algorithms/skillopt
  + optimizers/run-optimizer
  + optimizers/registry.yaml
  + _registry/hosts.yaml
wrote /tmp/wt-143/skills/_registry/manifest.json (20 skill(s))
  algorithm: agent-optimize, evograph, gepa, hill-climb, skillopt
  capability: mcp-tool, skill-package, system-prompt, tools
  optimizer: run-optimizer
  orchestrate: orchestrate, using-cap-evolve
  phase: baseline, diagnose, evaluate, finalize, gate, implement-and-check, intake, report
wrote /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/capevolve-install-smoke.XXXXXX.5mhQrtfWPo/home/.claude/skills/_registry/manifest.json (20 skill(s))
  algorithm: agent-optimize, evograph, gepa, hill-climb, skillopt
  capability: mcp-tool, skill-package, system-prompt, tools
  optimizer: run-optimizer
  orchestrate: orchestrate, using-cap-evolve
  phase: baseline, diagnose, evaluate, finalize, gate, implement-and-check, intake, report

Done. Skills installed to: /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/capevolve-install-smoke.XXXXXX.5mhQrtfWPo/home/.claude/skills
Next:
  1) pip install /tmp/wt-143/core        # the honest-eval substrate (or set CAPEVOLVE_CORE=/tmp/wt-143/core)
  2) point your agent at /tmp/wt-143/RUN.md   — or run: cap-evolve run --spec .capevolve/project/capevolve.yaml
OK: 22 skill dirs + registry + hosts.yaml
== zero-API toy_calc run from OUTSIDE the repo, against the INSTALLED skills ==
{
  "run_dir": ".capevolve/run_smoke",
  "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_smoke/dashboard.html"
}
PASS: ./install.sh --host claude produces an install that optimizes from outside the repo
EXIT=0

--host claude is resolved through hosts.yaml into a temp $HOME; $CAPEVOLVE_SKILLS_DIR is unset so the mapping is what places the files; the run cwd is outside the repo so run.py's parent-walk cannot rescue it; and the assert is test_reward 1.0, not exit 0.

2. Counter-proof — revert #193's registry.yaml copy and the smoke FAILS

Layer 1, the installed-file assert:

=== 3a. counter-proof: #193 reverted, file assert ===
== install via ./install.sh --host claude (HOME=/var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/capevolve-install-smoke.XXXXXX.RD7PLPOdDg/home) ==
cap-evolve: installing skills
  from: /tmp/wt-143/skills
  to:   /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/capevolve-install-smoke.XXXXXX.RD7PLPOdDg/home/.claude/skills

Done. Skills installed to: /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/capevolve-install-smoke.XXXXXX.RD7PLPOdDg/home/.claude/skills
Next:
  1) pip install /tmp/wt-143/core        # the honest-eval substrate (or set CAPEVOLVE_CORE=/tmp/wt-143/core)
  2) point your agent at /tmp/wt-143/RUN.md   — or run: cap-evolve run --spec .capevolve/project/capevolve.yaml
::error::installed tree is missing optimizers/registry.yaml
EXIT=
raw exit=1

Layer 2 — with that file check temporarily bypassed, the run-level assert still catches it. This is the important one: the run succeeds, keeps the seed, and reports 0.0. A job asserting only exit 0 would have gone green on a totally broken install, which is exactly how #193 hid:

=== 3b. same, with the file assert bypassed: the RUN assert catches it ===
== zero-API toy_calc run from OUTSIDE the repo, against the INSTALLED skills ==
{
  "run_dir": ".capevolve/run_smoke",
  "best_id": "seed",
  "baseline_val": 0.0,
  "test_reward": 0.0,
  "test_baseline_reward": 0.0,
  "test_delta": 0.0,
  "test_pass_k": {
    "1": 0.0,
    "2": 0.0
  },
  "iterations": 2,
  "dashboard": ".capevolve/run_smoke/dashboard.html"
}
::error::installed tree did not reach baseline_val 0.0 -> test_reward 1.0
EXIT=1

Both reverts restored; git diff against the commit is empty.

3. Stdlib-only fallback under blocked non-stdlib imports

Each test runs the real code in a subprocess whose sys.meta_path raises ImportError for every module not in sys.stdlib_module_names. test_the_blocker_actually_blocks guards the guard — without it, a broken hook would make the other five vacuously pass.

$ PYTHONPATH=$PWD/core python -m pytest core/tests/test_stdlib_only.py -v
============================= test session starts ==============================
core/tests/test_stdlib_only.py::test_the_blocker_actually_blocks PASSED  [ 16%]
core/tests/test_stdlib_only.py::test_hosts_metadata_resolves_with_no_third_party_modules PASSED [ 33%]
core/tests/test_stdlib_only.py::test_hosts_cli_dest_works_with_no_third_party_modules PASSED [ 50%]
core/tests/test_stdlib_only.py::test_optimizer_registry_parses_with_no_third_party_modules PASSED [ 66%]
core/tests/test_stdlib_only.py::test_manifest_build_works_with_no_third_party_modules PASSED [ 83%]
core/tests/test_stdlib_only.py::test_cli_version_and_check_work_with_no_third_party_modules PASSED [100%]
============================== 6 passed in 0.65s ===============================

4. Metadata and HOST_SUPPORT.md cannot diverge — broke all three consumers, guard fired each time

=== 4a. break the DOCS badge (codex 🟡 -> ✅ in HOST_SUPPORT.md only) ===
E           AssertionError: $HOME/.agents/skills: docs say verified, hosts.yaml says docs-checked
core/tests/test_host_parity.py:136: AssertionError
1 failed, 26 passed in 2.17s

=== 4b. break a DEST in hosts.yaml (.kimi -> .kimi2) ===
            f"  only in docs:      {sorted(set(doc) - set(yml))}\n"
            f"  only in hosts.yaml:{sorted(set(yml) - set(doc))}")
E       AssertionError: HOST_SUPPORT.md destination table and hosts.yaml disagree on destinations.
E           only in docs:      ['$HOME/.kimi/skills']
E           only in hosts.yaml:['$HOME/.kimi2/skills']
core/tests/test_host_parity.py:130: AssertionError
1 failed, 26 passed in 2.35s

=== 4c. regress DOCTOR back to a hand-maintained tuple ===
E           AssertionError: doctor does not know $HOME/.agents/skills
core/tests/test_host_parity.py:148: AssertionError
1 failed, 26 passed in 2.13s

=== all restored: guard green again ===
...........................                                              [100%]
27 passed in 2.24s

5. All 34 new tests

=== 2. new tests, verbose ===
============================= test session starts ==============================
core/tests/test_host_parity.py::test_hosts_yaml_parses_and_rows_are_well_formed PASSED [  2%]
core/tests/test_host_parity.py::test_verified_rows_cite_an_executing_artifact_that_exists PASSED [  5%]
core/tests/test_host_parity.py::test_install_sh_derives_its_mapping_from_hosts_yaml PASSED [  8%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[agy] PASSED [ 11%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[antigravity] PASSED [ 14%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[bob] PASSED [ 17%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[claude] PASSED [ 20%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[claude-code] PASSED [ 23%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[codex] PASSED [ 26%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[copilot] PASSED [ 29%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[cursor] PASSED [ 32%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[droid] PASSED [ 35%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[factory] PASSED [ 38%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[factory-droid] PASSED [ 41%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[gemini] PASSED [ 44%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[gemini-cli] PASSED [ 47%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[github-copilot] PASSED [ 50%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[ibm-bob] PASSED [ 52%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[kimi] PASSED [ 55%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[kimi-code] PASSED [ 58%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[openclaw] PASSED [ 61%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[opencode] PASSED [ 64%]
core/tests/test_host_parity.py::test_every_alias_resolves_through_the_cli_install_sh_calls[pi] PASSED [ 67%]
core/tests/test_host_parity.py::test_host_support_md_destination_table_matches_hosts_yaml PASSED [ 70%]
core/tests/test_host_parity.py::test_doctor_known_host_dirs_come_from_hosts_yaml PASSED [ 73%]
core/tests/test_host_parity.py::test_installer_copies_hosts_yaml_so_an_installed_tree_has_the_metadata PASSED [ 76%]
core/tests/test_host_parity.py::test_cli_reports_dest_and_json PASSED    [ 79%]
core/tests/test_stdlib_only.py::test_the_blocker_actually_blocks PASSED  [ 82%]
core/tests/test_stdlib_only.py::test_hosts_metadata_resolves_with_no_third_party_modules PASSED [ 85%]
core/tests/test_stdlib_only.py::test_hosts_cli_dest_works_with_no_third_party_modules PASSED [ 88%]
core/tests/test_stdlib_only.py::test_optimizer_registry_parses_with_no_third_party_modules PASSED [ 91%]
core/tests/test_stdlib_only.py::test_manifest_build_works_with_no_third_party_modules PASSED [ 94%]
core/tests/test_stdlib_only.py::test_cli_version_and_check_work_with_no_third_party_modules PASSED [ 97%]
core/tests/test_install_smoke.py::test_install_sh_produces_a_runnable_install PASSED [100%]
============================== 34 passed in 5.34s ==============================

6. Full suite, compileall, bash -n, CI YAML parse

$ PYTHONPATH=$PWD/core python -m pytest core/tests -q
...........................................                              [100%]
259 passed in 70.41s (0:01:10)

# attribution: main=179, +#193/#202=225, +this PR=259

$ python -m compileall -q core/cap_evolve core/tests skills; echo "exit=$?"
exit=0

$ bash -n install.sh && bash -n ci/install_smoke.sh && echo "syntax OK"
syntax OK

$ python -c "import yaml; d=yaml.safe_load(open(\".github/workflows/ci.yml\")); print(list(d[\"jobs\"]))"
['test-python', 'build-dashboard', 'toy-example', 'install-smoke', 'docs-links', 'spellcheck']

$ python -m codespell_lib ... README.md docs; echo "exit=$?"
exit=0

7. The derivation, end to end

$ python -c "from cap_evolve import doctor; print(doctor._known_host_dirs())"
('/.claude/skills', '/.agents/skills', '/.gemini/extensions/cap-evolve/skills', '/.config/opencode/skills', '/.bob/skills', '/.openclaw/workspace/skills', '/.cursor/skills', '/.factory/skills', '/.copilot/skills', '/.kimi/skills', '/.pi/skills', '/.antigravity/skills', '/.capevolve/skills')

# 13 dirs, derived from hosts.yaml + the no-host default. The tuple it replaced had 6.

$ # install.sh's own detect_dest(), sourced, with a temp $HOME
codex        -> /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/tmp.5VlXVxVghi/.agents/skills
cap-evolve: no hosts.yaml row for --host 'weirdthing' (or cap_evolve.hosts could not run) — falling back to the dotdir convention. Pass --dest to be sure.
weirdthing   -> /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/tmp.5VlXVxVghi/.config/weirdthing/skills
claude       -> /var/folders/zh/srgnbq_97qvb6002zsr40tgc0000gn/T/tmp.5VlXVxVghi/.claude/skills
cursor       -> /private/tmp/wt-143/.cursor/skills

$ python -m cap_evolve.hosts --json | head -12
{
  "antigravity": {
    "aliases": [
      "antigravity",
      "agy"
    ],
    "description": "Honest capability optimization with a sealed test split.",
    "dest": "$HOME/.antigravity/skills",
    "display": "cap-evolve",
    "evidence": "Dotdir convention only; references/antigravity.md marks the headless invocation best-guess. Pass --dest.",
    "invoke": "Read RUN.md, then: cap-evolve run --spec .capevolve/project/capevolve.yaml",
    "status": "best-guess"

Note the unknown host (weirdthing) warns on stderr rather than silently guessing, and cursor correctly resolves against $PWD rather than $HOME.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

CI status on this branch — both reds are inherited from the dependencies

The job this PR adds passes: Install smoke (./install.sh --host, zero-API)pass (run). So the ✅ on the claude destination row is backed by a job that really executes in CI, not just locally.

Two checks are red. Both reproduce on the dependency branches that are merged into this one, and neither is touched by my commit:

Check This branch #193 feat/issue-121-doctor #202 fix/issue-126-onboarding-polish Verdict
Test Python (core) fail fail pass inherited from #193
CodeQL fail pass fail inherited from #202

Test Python (core)#193's own test, not one of mine

FAILED core/tests/test_doctor.py::test_core_passes_on_a_correctly_activated_venv - AssertionError: precondition
1 failed, 258 passed in 12.34s

The failing line is #193's own "precondition" guard:

exe = Path(doctor.sys.prefix) / "bin" / "python"
if exe.is_symlink():
    assert not str(exe.resolve()).startswith(doctor.sys.prefix), "precondition"

It assumes bin/python resolves outside sys.prefix (true on macOS/Homebrew, which is where it was written). On ubuntu-latest's hosted toolcache it resolves inside it — /opt/hostedtoolcache/Python/3.11.15/x64/bin/python…/x64/bin/python3.11 — so the precondition is false and the test errors. It is testing the environment, not doctor.

#193's own branch fails identically (1 failed, 223 passed, run) — same test, same assert, 36 fewer tests because mine aren't there yet. My only change to test_doctor.py in this PR is a one-line docstring (the install.sh:38-40 line reference is stale now that the list lives in hosts.yaml):

 core/tests/test_doctor.py | 2 +-
-    """install.sh:38-40 admits several host dirs are guesses — flag them explicitly."""
+    """A dir matching no skills/_registry/hosts.yaml row is a guess — flag it."""

Locally (macOS, where the precondition holds) the full suite is 259 passed, 0 failed. Deliberately leaving this for #193 to fix rather than patching another PR's test from here — that fix belongs on its own branch, and the cleanest form is probably to drop the assert and just skip when the symlink resolves inside the prefix.

CodeQL — reproduces on #202

CodeQL fails on #202's branch and passes on #193's and on main. The underlying Analyze (python), Analyze (javascript-typescript), and both call-codeql / Analyze (…) jobs all pass here; it is the aggregating CodeQL check that reports failure, and its run page 404s from the API, which is consistent with a config/permission-level failure rather than a found alert.

Both should clear once the dependencies land and this branch is rebased on main — see the merge order in the PR description (#193#202 → this).

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔍 Review — PR #245

Verdict: APPROVE WITH NITS

I re-ran every claim in the evidence comment. All of them reproduced, including both counter-proof layers, all three parity-guard breakages, the verified-artifact guard, and the stdlib blocker probe. 259 passed locally; true baseline (#193+#202 merged) is 225, so +34 is exact. The smoke job is real: it passes in actual CI (Install smoke (./install.sh --host, zero-API) → pass), and I could not make it green on a broken install through any probe I tried.

Two findings below are latent rather than present-tense breakage, so neither blocks. Nothing here is a regression introduced by this commit.

Blocking

None.

Non-blocking

1. core/cap_evolve/specfile.py:78-95read_yaml's fallback silently returns {} for block sequences, and hosts.yaml's flow-style aliases is the only thing hiding it.

The fallback parser has no sequence handling at all: a line ending in : unconditionally creates a dict. hosts.yaml happens to write aliases: [claude, claude-code] in flow style, which _coerce handles, so today the fallback and PyYAML agree byte for byte (I verified FULL EQUALITY: True across all 12 rows). But the moment a contributor adds a row with the ordinary block spelling — which is what most people write, and what nothing in the repo forbids — aliases parses as {}, dest_for() returns None, and install.sh falls through to $HOME/.config/<host>/skills.

The consequence is specific and bad: all 35 parity/stdlib guards stay green while the new host is broken on exactly the bare machine the stdlib fallback exists to serve. I reproduced the full sequence — added a newhost row with block aliases, updated HOST_SUPPORT.md to match, and the guard suite went green at 35 passed, while install.sh --host nh under a PyYAML-less resolver printed no hosts.yaml row for --host 'nh' and installed to .config/nh/skills. This is the same defect class #197's fixer found, on a tree that does not have #197.

The comment at skills/_registry/hosts.yaml:20-22 ("all scalars / flat lists, so the stdlib YAML reader parses it") is load-bearing documentation for a constraint that nothing enforces. Fix, one line, in core/tests/test_stdlib_only.py::test_hosts_metadata_resolves_with_no_third_party_modules — it already loads every row under blocked imports, so assert the invariant it is one line from proving:

assert all(isinstance(r["aliases"], list) and r["aliases"] for r in rows.values())

That converts "we wrote it in flow style on purpose" from a comment into a build failure, and it fires under the import blocker where it matters. (Fixing read_yaml to parse block sequences is the real fix but belongs to #197, not here.)

2. install.sh:44-53 — the fallback warning misdiagnoses the failure it most needs to diagnose.

The if mapped=...; then guard collapses two very different outcomes into one message: "no row for this host" and "the resolver could not run". With no python3 on PATH, --host claude — the one ✅ row — prints no hosts.yaml row for --host 'claude' and installs to ~/.config/claude/skills:

cap-evolve: no hosts.yaml row for --host 'claude' (or cap_evolve.hosts could
not run) — falling back to the dotdir convention. Pass --dest to be sure.
  to:   .../home/.config/claude/skills

It does warn rather than silently guess, and the parenthetical is technically honest, so this is a nit not a bug. But a user reading "no hosts.yaml row for claude" will go edit hosts.yaml, which is the wrong action — the actual problem is a missing interpreter. install.sh never checks for python3 even though it needs it three more times (two build_manifest.py calls plus this one), and those are || true-guarded so the install "succeeds" with no manifest. Fix: split the branch —

command -v python3 >/dev/null || { echo "cap-evolve: python3 not found — --host resolution needs it; pass --dest" >&2; ...; }

3. CHANGELOG.md / core/cap_evolve/doctor.py:81 — "6 of the 12 real destinations" is 5, not 6.

The claim is directionally right and the fix is right, but the count is off by one. #193's tuple was:

_VERIFIED_HOST_DIRS = ("/.claude/skills", "/.agents/skills", "/.config/opencode/skills",
                       "/.capevolve/skills", "/.gemini/extensions/cap-evolve/skills",
                       "/.openclaw/workspace/skills")

Six entries, but /.capevolve/skills is the no-host default, not a host destination — the new docstring at doctor.py:82-84 says so itself. So it knew 5 of 12 host dests; seven correct host dirs were reported "best-guess", not six. The derived list is 13 (12 rows + the default), which I confirmed. Fix: s/6 of the 12/5 of the 12/ and s/six correct host dirs/seven/ in both places.

Nits

  • ci/install_smoke.sh:44 echoes the skill-dir count but never asserts it. Every real failure mode I could construct was caught downstream by the manifest, so this is cosmetic — but [[ $(find ... | wc -l) -ge 20 ]] would make the line mean something.
  • core/tests/test_host_parity.py:100 — the assert name in row["aliases"] or name.replace("-","") in "".join(row["aliases"]) second clause is loose enough to pass on unrelated substrings (droid's key is in its aliases, but a key like bo would match ibm-bob). Only the first clause is load-bearing; drop the second and rename the droid key to match, or leave it.

Can the smoke job pass on a broken install?

No — I could not break it. Seven probes, all in /tmp against a real ./install.sh --host claude install:

Probe Result
Counter-proof layer 1 — delete installed optimizers/registry.yaml (revert #193) ::error::installed tree is missing optimizers/registry.yaml. Fires.
Counter-proof layer 2 — same, file assert bypassed, run-level assert only Run succeeds, best_id: seed, test_reward 0.0::error::installed tree did not reach baseline_val 0.0 -> test_reward 1.0. This is the important one and it works: a job asserting exit 0 would have gone green here.
Skills missing but the three asserted files present (installer copies plain files, drops every skill dir; manifest rebuilt so it is valid for an empty tree) KeyError: "skill 'run-optimizer' not in manifest" → FAIL. The manifest is the real gate; the three file asserts are belt-and-braces.
Stale $HOME/.claude/skills from a previous run Impossible. FAKE_HOME lives inside a per-invocation mktemp -d with trap 'rm -rf "$TMP"' EXIT (ci/install_smoke.sh:27-30). No path outside that temp dir is ever written.
Temp $HOME inheriting the developer's real one No. HOME="$FAKE_HOME" is passed on both env invocations (lines 36, 54) and the real $HOME is never read. mktemp -d resolves to $TMPDIR, outside the repo.
run.py's parent-walk rescuing a broken install despite the cd Does not happen, and the cd is not actually what prevents it. I forced WORK to be a genuine descendant of $REPO (/tmp/rv-245/probe_inside/deep/work), broke the install, and it still reported best_id: seed / test_reward 0.0. The protection is the installed manifest, which is stronger than #208 asked for. Keeping the cd is still correct as defence in depth.
Installed skill code not actually exercised — corrupt only the installed hill-climb/scripts/run.py, repo untouched {"step": "algorithm", "returncode": 1, "error": "CORRUPTED INSTALLED SKILL"} → FAIL. Confirms the run executes the installed tree, not the repo's.

One honest gap, and I do not think it changes the verdict. The run sets PYTHONPATH="$REPO/core" and CAPEVOLVE_CORE="$REPO/core" (ci/install_smoke.sh:55-56), so it exercises the source core — step 1 of the documented install, pip install ./core, is not covered by this job. That is a deliberate and reasonable scope choice: the job's subject is install.sh's skill placement, and pip install ./core is a plain pip install already covered by every other job. CAPEVOLVE_TOY_DATA / CAPEVOLVE_MOCK_SCRIPT also point into the repo, but those are the fixture (tasks + mock transcript), not library code — legitimate. Worth one sentence in the script header so nobody later reads the ✅ as covering the pip step too.

Single-source verification

case "$HOST" in is gone from install.sh — I diffed it, all 13 arms removed and replaced by the python3 -m cap_evolve.hosts --dest shell-out at line 45. No parallel table survives.

All 20 aliases across 12 hosts resolve identically between hosts.yaml and the path install.sh echoes — agy antigravity bob claude claude-code codex copilot cursor droid factory factory-droid gemini gemini-cli github-copilot ibm-bob kimi kimi-code openclaw opencode pi, all OK. cursor initially looked like a mismatch; it was my two resolvers running from different cwds, and $PWD/.cursor/skills matched exactly once I fixed that. cursor correctly expands against $PWD, not $HOME.

Consumer Derives from hosts.yaml? Guard fires on drift?
install.sh --host Yes — shells python3 -m cap_evolve.hosts --dest (install.sh:45-48) Yes — test_install_sh_derives_its_mapping_from_hosts_yaml asserts case "$HOST" in absent; per-alias parametrized test asserts the exact subprocess output
docs/HOST_SUPPORT.md destination table Yes — a rendering Yes. Badge drift (codex 🟡→✅): AssertionError: $HOME/.agents/skills: docs say verified, hosts.yaml says docs-checked. Dest drift (.kimi.kimi2): only in docs: ['$HOME/.kimi/skills'] / only in hosts.yaml:['$HOME/.kimi2/skills']
cap_evolve.doctor Yes — _known_host_dirs() (doctor.py:76-92), 13 dirs Yes. Regressed it to a hardcoded 6-tuple: AssertionError: the hand-maintained tuple is back — derive from cap_evolve.hosts instead. The hasattr check catches the reintroduction by name, which is stronger than value comparison
verified grade ↔ artifact Yes Yes, both ways. Bad path: claude-code: verified, but cited artifact ci/install_smoke_NOPE.sh does not exist. No path at all (evidence: "trust me it works"): claude-code: verified but evidence cites no artifact path

The chicken-and-egg risk is handled. python3 -m cap_evolve.hosts with no cap_evolve installed and no PYTHONPATH fails as expected (ModuleNotFoundError) — but install.sh never invokes it that way. It sets PYTHONPATH="$REPO_DIR/core" from BASH_SOURCE, which is cwd-independent. Verified from an unrelated cwd against system /usr/bin/python3 (3.9.6, no cap-evolve, no PyYAML in scope): printed /Users/.../.claude/skills, exit 0. The real user's first run works.

Stdlib fallback

The blocker is load-bearing. I neutered find_spec to return None and re-ran: test_the_blocker_actually_blocks failed with AssertionError: the import blocker did not fire: NOT BLOCKED, while the other five passed — exactly the vacuous-pass state the meta-test exists to prevent. Unweakened, the hook really blocks PyYAML on the live path: I installed it in-process and got pyyaml blocked: blocked: yaml, then loaded 12 rows and resolved dest_for('claude') and dest_for('agy') through the fallback parser. Not vacuous, unlike #213's block-scalar parser and #189's manifest read.

aliases does parse without #197 — but only because of flow style, which nothing enforces. See non-blocking #1. Today: fallback and PyYAML produce identical dicts for all 12 rows. With block-sequence aliases: fallback gives {'aliases': {}} vs PyYAML's ['newhost','nh'], and no guard notices. optimizers/registry.yaml parses fine (rows >= 12, _mock_apply.py present).

Are the two red CI checks really inherited?

Yes, both. Verified independently, not taken on trust.

Test Python (core) — inherited from #193. Same test, same file, same line, same message on both branches:

259 collected, matching my local run, so this PR's 34 tests all pass in CI. #202 passes this check, confirming the attribution.

This is a genuine finding against #193, worth reporting there. core/tests/test_doctor.py:119-121:

exe = Path(doctor.sys.prefix) / "bin" / "python"
if exe.is_symlink():
    assert not str(exe.resolve()).startswith(doctor.sys.prefix), "precondition"

The if guards on being a symlink, but the assert requires it to resolve outside sys.prefix. On macOS/Homebrew venvs that holds; on ubuntu-latest the setup-python toolcache symlink resolves inside sys.prefix, so the precondition is false and the test fails. It is testing the environment, not the code. Fix belongs in #193: if exe.is_symlink() and not str(exe.resolve()).startswith(doctor.sys.prefix): and drop the assert, or skip when the precondition does not hold.

CodeQL — inherited from #202. One alert, py/bad-tag-filter high, at core/tests/test_dashboard.py:243 (re.findall(r"<script>(.*?)</script>", ...) — "does not match upper case <SCRIPT> tags"). I traced it to commit 22ad618f, which is the tip of fix/issue-126-onboarding-polish and not an ancestor of main; #202's own CodeQL is red and #193's is green. This PR's commit 3d8dd206 does not touch test_dashboard.py at all. It is a false positive on test-only regex over self-generated HTML, and it belongs to #202.

Neither red hides a regression from this PR.

Merge-order note

Merge #193#202#245. Once both land, #245's own commit (3d8dd206) is fully additive: 13 files, +793/-54, and the only edits to existing files are install.sh (the case → shell-out swap), doctor.py (tuple → derived), docs/HOST_SUPPORT.md/INSTALL.md, one test_doctor.py docstring, and the CI/CHANGELOG appends. Nothing depends on merge order between #193 and #202 specifically — they touch disjoint code — but both must precede #245, since it deletes _VERIFIED_HOST_DIRS (#193's) and rewrites HOST_SUPPORT.md (#202's).

I reproduced the merge to build the baseline: #193 + #202 conflicts on CHANGELOG.md only (UU CHANGELOG.md); install.sh and docs/INSTALL.md auto-merge cleanly. Expect one trivial changelog resolution, nothing structural.

Two follow-ups worth filing rather than blocking: enforce the flow-style aliases invariant (non-blocking #1) and fix read_yaml's block-sequence blindness under #197.

Is exactly one ✅ the right call?

Yes. ci/install_smoke.sh installs to exactly one destination and completes exactly one run there, so claude/claude-code is the only row with an executing artifact behind it. Grading any other row ✅ would be precisely the unproven ✅ this epic keeps catching — and the test_verified_rows_cite_an_executing_artifact_that_exists guard now makes that mechanically impossible, in both directions (missing file, and no cited path at all). 1×✅ / 4×🟡 / 7×➖ matches hosts.yaml exactly, and the docs prose at docs/HOST_SUPPORT.md:69-78 states the one-destination limitation plainly instead of implying broader coverage. Restraint is correct here.

Verification I re-ran

$ PYTHONPATH=/tmp/rv-245/core python -m pytest core/tests -q
259 passed in 66.88s (0:01:06)

# true baseline: #193 + #202 merged, this PR's commit absent
$ git merge origin/fix/issue-126-onboarding-polish   # onto feat/issue-121-doctor
Automatic merge failed; fix conflicts   ->  UU CHANGELOG.md  (install.sh auto-merged)
$ PYTHONPATH=/tmp/rv-base/core python -m pytest core/tests -q
225 passed in 63.61s (0:01:03)
# 259 - 225 = 34 new tests. Attribution confirmed.

$ python -m compileall -q core/cap_evolve core/tests skills; echo exit=$?
exit=0
$ bash -n install.sh && bash -n ci/install_smoke.sh && echo "bash -n OK"
bash -n OK

Smoke job, end to end:

$ PATH="/tmp/ce-venv/bin:$PATH" bash ci/install_smoke.sh
OK: 22 skill dirs + registry + hosts.yaml
== zero-API toy_calc run from OUTSIDE the repo, against the INSTALLED skills ==
{ "best_id": "cand_0001", "baseline_val": 0.0, "test_reward": 1.0, ... }
PASS: ./install.sh --host claude produces an install that optimizes from outside the repo
RAWEXIT=0

Both counter-proof layers:

=== LAYER 1: revert #193 (delete installed optimizers/registry.yaml) ===
::error::installed tree is missing optimizers/registry.yaml

=== LAYER 2: file assert bypassed — does the RUN assert catch the silent 0.0? ===
{ "best_id": "seed", "baseline_val": 0.0, "test_reward": 0.0, "test_delta": 0.0, ... }
::error::installed tree did not reach baseline_val 0.0 -> test_reward 1.0

Broken-install probes:

=== skills dropped, three asserted files present, manifest valid-for-empty ===
wrote .../_registry/manifest.json (0 skill(s))
OK: 2 skill dirs + registry + hosts.yaml      <-- echoed, never asserted
KeyError: "skill 'run-optimizer' not in manifest"
>>> SMOKE VERDICT: FAIL

=== cwd genuinely INSIDE the repo (/tmp/rv-245/probe_inside/deep/work), install broken ===
  "best_id": "seed",
  "test_reward": 0.0,
>>> still FAILS even inside the repo -> the installed manifest is the gate, not the cd

=== corrupt only the INSTALLED hill-climb/scripts/run.py ===
{"step": "algorithm", "returncode": 1, "error": "CORRUPTED INSTALLED SKILL\n"}
>>> SMOKE VERDICT: FAIL  <-- installed skill code IS exercised

All three parity guards + the verified-artifact guard:

=== 4a. docs badge drift (codex 🟡 -> ✅ in the DESTINATIONS table) ===
E  AssertionError: $HOME/.agents/skills: docs say verified, hosts.yaml says docs-checked
1 failed, 26 passed

=== 4b. dest drift in hosts.yaml (.kimi -> .kimi2) ===
E  AssertionError: HOST_SUPPORT.md destination table and hosts.yaml disagree on destinations.
E      only in docs:      ['$HOME/.kimi/skills']
E      only in hosts.yaml:['$HOME/.kimi2/skills']

=== 4c. doctor regressed to a hand-maintained tuple ===
E  AssertionError: the hand-maintained tuple is back — derive from cap_evolve.hosts instead

=== verified row cites a nonexistent artifact ===
E  AssertionError: claude-code: verified, but cited artifact ci/install_smoke_NOPE.sh does not exist
=== verified row cites no artifact path at all ===
E  AssertionError: claude-code: verified but evidence cites no artifact path

=== all restored ===
27 passed in 2.23s

Stdlib blocker, weakened and unweakened:

=== find_spec neutered to `return None` ===
E  AssertionError: the import blocker did not fire: NOT BLOCKED
1 failed, 5 passed          <-- the other five would have passed vacuously

=== blocker intact, real path ===
pyyaml blocked: blocked: yaml
rows under blocked imports: 12
aliases claude-code: ['claude', 'claude-code']
dest_for(claude): /Users/.../.claude/skills
dest_for(agy): /Users/.../.antigravity/skills

read_yaml fallback vs PyYAML on hosts.yaml, on a tree without #197:

fallback rows: 12   pyyaml rows: 12
aliases equal for all rows: True
FULL EQUALITY: True

# ...but the natural block spelling:
fallback parse of BLOCK-SEQUENCE aliases: {'newhost': {'aliases': {}, ...}}
pyyaml parse                            : {'newhost': {'aliases': ['newhost','nh'], ...}}
# and with docs updated to match, every guard is green:
35 passed in 3.45s
# while a PyYAML-less install.sh --host nh says:
cap-evolve: no hosts.yaml row for --host 'nh' ... -> .../home/.config/nh/skills

Pre-pip install resolution, unrelated cwd, system python 3.9.6:

$ cd /tmp && env -u PYTHONPATH /usr/bin/python3 -m cap_evolve.hosts --dest claude
ModuleNotFoundError: No module named 'cap_evolve'        # not how install.sh calls it
$ cd /tmp && PYTHONPATH=/tmp/rv-245/core /usr/bin/python3 -m cap_evolve.hosts --dest claude
/Users/.../.claude/skills
exit=0

All 20 aliases, hosts.yaml vs the path install.sh echoes (same cwd, temp $HOME):

agy antigravity bob claude claude-code codex copilot cursor droid factory
factory-droid gemini gemini-cli github-copilot ibm-bob kimi kimi-code
openclaw opencode pi                                  -> 20/20 OK

Every documented install.sh invocation:

1) no args        -> ~/.capevolve/skills            (empty HOME, no ./.claude/skills)
2) --dest DIR     -> DIR
3) --link         -> 20 symlinks
4) --host claude  -> ~/.claude/skills
   --host codex   -> ~/.agents/skills
   --host weirdhost -> warns on stderr, ~/.config/weirdhost/skills
5) --help         -> usage, exit 0
6) --bogus        -> "unknown arg: --bogus", exit 2
7) no python3 on PATH, --host claude -> warns, but misdiagnoses (nit #2)

CI attribution:

$ gh pr checks 245 | grep -E "Install smoke|Test Python|^CodeQL"
Install smoke (./install.sh --host, zero-API)   pass
Test Python (core)                             fail
CodeQL                                         fail

#193: Test Python (core) fail  / CodeQL pass
#202: Test Python (core) pass  / CodeQL fail

Repo left clean throughout (git status --porcelain empty after every mutation probe); nothing pushed.

…stall.sh's fallback causes

Review fixes for PR #245 (issue #143). No behaviour change to the resolver
itself; all five findings were latent or cosmetic.

1. test_stdlib_only.py now asserts every row's `aliases` is a non-empty list *as
   the stdlib YAML reader parses it*. read_yaml's fallback has no block-sequence
   handling, so a block-spelled row parses as {} on the no-PyYAML path only:
   dest_for() returns None, install.sh dotdir-guesses, and all 35 guards stayed
   green — on exactly the bare host that fallback exists to serve. hosts.yaml's
   header now states that flow style is REQUIRED, not stylistic. (Fixing
   read_yaml to parse block sequences belongs to #197.)

2. install.sh's --host fallback warning split into three distinct messages: no
   python3 on PATH, resolver could not run (missing/unreadable hosts.yaml or a
   broken core), and no row for this host. It previously reported all three as
   "no hosts.yaml row for '<host>'", sending a user with a missing interpreter
   to edit a file that was correct.

3. Corrected the off-by-one in our own claim: #193's tuple had six entries but
   only 5 of the 12 host destinations — its /.capevolve/skills entry is the
   no-host default — so SEVEN correct host dirs were misreported as best-guess,
   not six. Fixed in doctor.py's docstring and CHANGELOG.md.

4. ci/install_smoke.sh asserts its skill-dir count (>= 20) instead of only
   echoing it, and its header now states the scope gap: the run uses
   PYTHONPATH=$REPO/core, so `pip install ./core` is deliberately NOT covered by
   the claude-code ✅.

5. test_host_parity.py: dropped the loose `name.replace("-","") in
   "".join(aliases)` escape hatch, which would pass on unrelated substrings (a
   key `bo` "matching" ibm-bob). Every row key is literally in its own aliases,
   so strict membership is enough.

Test Python (core) red on ubuntu is #193's test_doctor.py:121, tracked as #247.
Copilot AI review requested due to automatic review settings July 30, 2026 22:11

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

Thanks — that was an unusually thorough review, and I took the "do not churn" instruction seriously: nothing in the resolver, the smoke job's mechanics, the badge grades, or the case deletion changed. New commit cce107e9, not an amend. Every claim below is literal output.

1. Block-style aliases now FAILS the stdlib test — fail → revert → pass

This is the finding that mattered, so here is the whole cycle. Added a probehost row with the ordinary block spelling:

probehost:
  aliases:
    - probehost
    - ph
  dest: "$HOME/.probehost/skills"
$ PYTHONPATH=core python -m pytest core/tests/test_stdlib_only.py -q
E       AssertionError: Traceback (most recent call last):
E           File "<string>", line 23, in <module>
E             assert not bad, ('aliases empty under the stdlib YAML reader for %r — ...
E         AssertionError: aliases empty under the stdlib YAML reader for ['probehost'] —
E         hosts.yaml must spell aliases in FLOW style, aliases: [a, b], not as a block sequence
FAILED core/tests/test_stdlib_only.py::test_hosts_metadata_resolves_with_no_third_party_modules
1 failed, 5 passed in 0.78s

Row reverted:

$ PYTHONPATH=core python -m pytest core/tests/test_stdlib_only.py core/tests/test_host_parity.py -q
33 passed in 2.72s

The assertion runs inside _run_blocked, so it fires under the import blocker — the only place the defect is observable. skills/_registry/hosts.yaml's header now leads with "FLOW STYLE FOR LISTS IS REQUIRED, NOT STYLISTIC" and shows the wrong spelling explicitly, so the constraint is documented and enforced. Fixing read_yaml to parse block sequences is left to #197, as you said.

2. The install.sh warning distinguishes its causes — three, not two

You asked for two; the probe found a third worth separating (resolver present but hosts.yaml unreadable), so the branch splits three ways:

########## A: no python3 on PATH, --host claude (a REAL row) ##########
cap-evolve: python3 not found on PATH — --host resolution needs it to read
skills/_registry/hosts.yaml. hosts.yaml is fine; the interpreter is missing.
Install python3 or pass --dest DIR. Falling back to the dotdir convention.
  to:   .../h1/.config/claude/skills

########## B: python3 present, --host weirdhost (NO row) ##########
cap-evolve: no hosts.yaml row for --host 'weirdhost' — falling back to the dotdir
convention. Add a row to skills/_registry/hosts.yaml, or pass --dest to be sure.
  to:   .../h2/.config/weirdhost/skills

########## C: python3 present, hosts.yaml MISSING (resolver cannot run) ##########
cap-evolve: could not run cap_evolve.hosts (missing or unreadable
skills/_registry/hosts.yaml, or a broken /tmp/fx-245/core) — this is NOT a problem
with your --host 'claude'. Pass --dest DIR. Falling back to the dotdir convention.
  to:   .../h3/.config/claude/skills

Only case B now names hosts.yaml as the thing to edit. Note on the probe: --json exits 0 printing {} when the file is absent, so ! ... --json alone put case C in the wrong branch on the first attempt — the check is [[ "$table" != *'"dest"'* ]], i.e. "did we get a real table", not the exit code.

3. The count is 5 of 12, and seven dirs were misreported

hosts.yaml rows          : 12
derived dirs (12 + dflt) : 13
old tuple entries        : 6
old tuple HOST dests it knew (5 of 12):
    /.claude/skills
    /.agents/skills
    /.config/opencode/skills
    /.gemini/extensions/cap-evolve/skills
    /.openclaw/workspace/skills
host dirs misreported best-guess (7):
    /.bob/skills
    /.cursor/skills
    /.factory/skills
    /.copilot/skills
    /.kimi/skills
    /.pi/skills
    /.antigravity/skills
6th old entry /.capevolve/skills is a host dest? False

Corrected in both core/cap_evolve/doctor.py:81 and CHANGELOG.md.

4. Nit — ci/install_smoke.sh skill-dir count is now asserted

[[ "$ndirs" -ge 20 ]], and it catches the exact tree you constructed (skills dropped, three asserted files present) at the install step rather than downstream at the manifest:

::error::only 2 skill dirs installed (expected >= 20)

5. Nit — the loose parity clause is gone

Dropped or name.replace("-","") in "".join(row["aliases"]). All 12 keys are verbatim members of their own aliases, so nothing needed the escape hatch and it can no longer pass on an unrelated substring. Kept the droid key as-is — renaming it would churn the docs table for no gain.

The pip install scope sentence

Added to ci/install_smoke.sh's header:

SCOPE — what this job does NOT cover: the run below sets PYTHONPATH="$REPO/core" and CAPEVOLVE_CORE="$REPO/core", so it exercises the SOURCE core. Step 1 of the documented install, pip install ./core, is deliberately out of scope here — this job's subject is install.sh's skill placement, and the pip step is a plain install already covered by every other CI job. Do not read the ✅ on claude-code in hosts.yaml as covering it. ($CAPEVOLVE_TOY_DATA / $CAPEVOLVE_MOCK_SCRIPT also point into the repo, but those are the fixture — tasks and a mock transcript — not library code.)

Re-verification

Nothing you validated regressed.

$ PYTHONPATH=core python -m pytest core/tests -q
259 passed in 66.33s          # 0 failed locally; no new tests, the fix is one assertion
                              # inside an existing test
$ python -m compileall -q core/cap_evolve core/tests skills; echo exit=$?
exit=0
$ bash -n install.sh && bash -n ci/install_smoke.sh && echo "bash -n OK"
bash -n OK

# all 20 aliases, hosts.yaml vs what install.sh resolves
aliases OK: 20  mismatched: 0

# smoke job end to end
OK: 22 skill dirs + registry + hosts.yaml
  "best_id": "cand_0001",
  "test_reward": 1.0,
PASS: ./install.sh --host claude produces an install that optimizes from outside the repo
smoke exit=0

# counter-proof, both layers, with #193 reverted (installer skips optimizers/registry.yaml)
--- layer 1 (file assert) ---
::error::installed tree is missing optimizers/registry.yaml
--- layer 2: file assert bypassed — the RUN assert still catches it ---
  "best_id": "seed",
  "test_reward": 0.0,
::error::installed tree did not reach baseline_val 0.0 -> test_reward 1.0

# all four guards still fire
docs badge drift    : AssertionError: $HOME/.agents/skills: docs say verified, hosts.yaml says docs-checked
dest drift          : only in docs: ['$HOME/.kimi/skills'] / only in hosts.yaml:['$HOME/.kimi2/skills']
doctor regression   : AssertionError: the hand-maintained tuple is back — derive from cap_evolve.hosts instead
verified w/o artifact: AssertionError: claude-code: verified, but cited artifact ci/install_smoke_NOPE.sh does not exist
all restored        : 27 passed in 2.24s

# the stdlib blocker is still load-bearing
find_spec -> return None : AssertionError: the import blocker did not fire: NOT BLOCKED
                           1 failed, 5 passed
restored                 : 6 passed

Repo clean after every mutation probe.

On the red checks

Left alone, as instructed. Test Python (core) is #193's core/tests/test_doctor.py:121 — the if exe.is_symlink() guard asserting the symlink resolves outside sys.prefix, true on macOS and false on ubuntu-latest. Not mine to fix; filed and tracked as #247 with a portable replacement. CodeQL is #202's py/bad-tag-filter at test_dashboard.py:243, a false positive on test-only regex over self-generated HTML. cce107e9 touches neither file. So the honest read of the numbers is 259 passed / 0 failed locally, with the two CI reds inherited from #193 (→ #247) and #202 respectively.

Files touched: install.sh, ci/install_smoke.sh, skills/_registry/hosts.yaml, core/cap_evolve/doctor.py, core/tests/test_stdlib_only.py, core/tests/test_host_parity.py, CHANGELOG.md.

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

Labels

documentation Improvements or additions to documentation dx Developer/onboarding experience enhancement New feature or request honesty Honesty/consistency of reported results (brand-critical) observability Live run visibility, logging, tracing site GitHub Pages website

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cross-host skill parity: per-host metadata + stdlib-only fallback

4 participants