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
Conversation
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).
… into feat/issue-143-cross-host-parity
…/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.
| 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] |
|
🏷️ Automatic Labeling I've analyzed this pull request and added the following labels:
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. |
🔬 EvidenceEvery command and its literal output. Worktree at 1. A real
|
CI status on this branch — both reds are inherited from the dependenciesThe job this PR adds passes: Two checks are red. Both reproduce on the dependency branches that are merged into this one, and neither is touched by my commit:
|
🔍 Review — PR #245Verdict: 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 Two findings below are latent rather than present-tense breakage, so neither blocks. Nothing here is a regression introduced by this commit. BlockingNone. Non-blocking1. The fallback parser has no sequence handling at all: a line ending in 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 The comment at 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 2. The 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/skillsIt 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 command -v python3 >/dev/null || { echo "cap-evolve: python3 not found — --host resolution needs it; pass --dest" >&2; ...; }3. 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 Nits
Can the smoke job pass on a broken install?No — I could not break it. Seven probes, all in
One honest gap, and I do not think it changes the verdict. The run sets Single-source verification
All 20 aliases across 12 hosts resolve identically between
The chicken-and-egg risk is handled. Stdlib fallbackThe blocker is load-bearing. I neutered
Are the two red CI checks really inherited?Yes, both. Verified independently, not taken on trust.
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. exe = Path(doctor.sys.prefix) / "bin" / "python"
if exe.is_symlink():
assert not str(exe.resolve()).startswith(doctor.sys.prefix), "precondition"The
Neither red hides a regression from this PR. Merge-order noteMerge #193 → #202 → #245. Once both land, #245's own commit ( I reproduced the merge to build the baseline: Two follow-ups worth filing rather than blocking: enforce the flow-style Is exactly one ✅ the right call?Yes. 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 OKSmoke 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=0Both 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.0Broken-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 exercisedAll 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.23sStdlib 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
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/skillsPre- $ 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=0All 20 aliases, 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 OKEvery documented 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 failRepo left clean throughout ( |
…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.
🔧 Review fixesThanks — 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 1. Block-style
|
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:
install.shwas broken for every stock install. Its copy loop iterates"$SRC/$comp"/*/with a[[ -d ]]guard — directories only — soskills/optimizers/registry.yaml(a plain file) was never installed andrun-optimizer/scripts/run.pyhard-raisesFileNotFoundError. Fixed in Addcap-evolve doctorinstall/health diagnostic #193. Repo-checkout developers were unaffected only becauserun.py's parent-walk finds it in the source tree, which is why nobody noticed.install.sh; every job sets$CAPEVOLVE_SKILLS_DIRto the repo tree, which takes install.sh's first precedence branch and bypasses the--hostmapping entirely. So "cross-host parity" had never been tested on any host.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.yamlis now the single source: per host, the--hostaliases, the install destination, the grade, the artifact justifying that grade, and thedisplay/description/invoketriple a host UI needs (the metadata #143 asked for).That list previously lived in five places and had drifted:
install.sh--hostcase, 12 armspython3 -m cap_evolve.hosts --destdoctor._VERIFIED_HOST_DIRS_known_host_dirs(), deriveddocs/HOST_SUPPORT.mddestination tabledocs/HOST_SUPPORT.mdoptimizer tableThe guard —
core/tests/test_host_parity.py(27 tests) fails the build when any consumer disagrees withhosts.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_DIRShas not come back.test_install_sh_derives_its_mapping_from_hosts_yaml— assertscase "$HOST" inis gone and no row's dest is hardcoded in the--hostbranch.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_exists— averifiedrow 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.pyruns the real install path in a subprocess whosesys.meta_pathraisesImportErrorfor every module not insys.stdlib_module_names— not by inspection. Covered: host metadata + the--destCLI,read_yamlon the realoptimizers/registry.yaml,build_manifest.py, andcap-evolve version/check. Plustest_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.shcallspython3 -m cap_evolve.hostsbeforepip install ./corehas happened, so that module must work with nothing installed.3. #208's smoke job — implemented
ci/install_smoke.sh+ theinstall-smokejob inci.yml. Every detail is deliberate:./install.sh --host claudewith a temp$HOME, so the mapping actually places the files;$CAPEVOLVE_SKILLS_DIR, or the mapping is never consulted;run.py's parent-walk silently rescues you and the job proves nothing;test_reward 1.0, not exit 0 — a failing optimizer is silent: the run completes, keeps the seed, reports0.0.Counter-proof that it is load-bearing: with #193's
registry.yamlcopy reverted, the job fails (both at the file assert and, with that bypassed, attest_reward 0.0). Output in the Evidence comment.4. Host grades — what changed and why
claude/claude-code→$HOME/.claude/skillsci/install_smoke.sh, run by theinstall-smokeCI job andcore/tests/test_install_smoke.py. Installs through the--hostmapping and completes a real zero-API optimization from outside the repo.codex,gemini-cli,opencode,ibm-bobcursor,droid,copilot,kimi,pi,antigravity,openclawThe destination table now has exactly one ✅, and it is the only row with an executing artifact.
Also fixed on the way:
install.shcopies_registry/hosts.yaml(same bug class as #193 — a plain file underskills/the copy loop skips), and warns on stderr instead of silently guessing when--hosthas no row.Expected merge order
cap-evolve doctorinstall/health diagnostic #193 (feat/issue-121-doctor) — dependency. Itsregistry.yamlfix is what makes the smoke job pass; without it the new job red-linesmain.fix/issue-126-onboarding-polish) — dependency. Addsdocs/HOST_SUPPORT.md, which this PR's guard reads.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.
A real
./install.shinstall completes a run from a cwd outside the repo:Counter-proof — revert #193's
registry.yamlcopy and the job fails:and with that file check bypassed, the run-level assert catches it too —
"best_id": "seed","test_reward": 0.0,EXIT=1. That silent0.0is precisely the failure mode #193 describes.Stdlib-only fallback under blocked non-stdlib imports (all 6 pass):
Metadata and HOST_SUPPORT.md cannot diverge — broke each of the three consumers in turn, the guard fired each time, restored:
Full commands and untruncated output in the
## 🔬 Evidencecomment below.