ci: absent prerequisites SKIP with a named reason, and skipping is bounded - #3
Conversation
…unded
The first CI run of the public repo was red: 21 pytest failures, 8 module
selftests and 2 capability gates, against 330 green locally. None was a defect
in this tree. Every one needed something that exists only on the machine the
system runs on, and a GitHub runner has none of it.
Four prerequisite classes, established by reproducing the runner exactly
(committed files only via `git archive`, empty HOME, no agent CLIs, no sibling
checkouts, unauthenticated gh, fresh state) — 305 passed / 21 failed / 4 skipped
/ same 8 selftests / same 2 gates, byte-for-byte with CI:
* the populated capability LEDGER (8 tests, 5 selftests, both gates). The
ledger is machine-local state; a fresh bootstrap holds the 14 rows the code
declares, this instance's holds 40. The other 26 are registration history and
are not reconstructible from source.
* `~/.codex/skills/.../audit_code_root.sh` (8 tests) — the skill compiler is
deliberately exercised against a real installed skill, not a fixture.
* the version-capable Codex binary for exact profiles (2 tests, 2 selftests) —
default path is inside a macOS app bundle.
* an installed agent CLI / credential file for a seat (2 tests).
Each of those now SKIPS with a reason NAMING the missing thing, via detectors in
the new `env_prereq.py`. Detection is of the prerequisite, never of `$CI`, so the
same code is right on a runner, here, and on a second instance. Assertions are
untouched: a check that runs asserts exactly what it asserted before. Selftests
gate the smallest SECTION that needs the missing thing rather than the whole
function, so CI keeps everything else.
One failure was NOT a prerequisite. `test_experiment_arm_identity` and
`exp_abcd`'s selftest replace `subprocess.Popen` wholesale, and model resolution
spawns a catalog probe when the advertised-model cache is cold — which lands in
the stub and dies on `stdout.write` with an int. Warm cache here, cold on a fresh
machine. Fixed by isolation with adapters' own `ORCH_MODEL_PROBE` kill-switch, so
CI now runs MORE, not less.
THE SKIP CEILING is the reason skipping was allowed at all. verify.py now:
* bounds skipped tests / selftests / gates against `.verify-floor.json`
(24/7/2 — exactly what a machine with none of the local prerequisites skips).
One more than agreed is a RED naming the key to raise.
* counts the floor as `passed + skipped`, so a check may move between passing
and consciously-skipped but the two together may never shrink. `--update-floor`
records `passed + skipped` and never re-measures the ceilings, so the floor
cannot ratchet down behind a leak.
* treats a selftest or gate that exits 0 having skipped as SKIPPED, not ok —
the silent-zero-exit rule's twin, and the reason `ok` can be trusted again.
* prints every skip and its reason, so green always states what did not run.
* prints EVERY pytest failure, not the last 12 lines. The first CI run reported
21 failures and named 7, which cost a whole round trip to diagnose.
Deliberate break -> revert, all three reverted byte-identical (`cmp`):
* one extra skipped file: 33 > 24 fails, naming `skipped_max`. With the ceiling
removed the IDENTICAL break exits 0 and reads "VERIFIED" — the ceiling is
load-bearing, not decoration.
* blinding `run_selftests` to the skip mark: claims "81 of 81 modules ran" while
7 ran nothing; `verify.py --selftest` catches it.
* floor back to bare `passed`: CI goes red for being honest.
Also fixed, both found by the reproduction:
* `capability_activation_audit._fleet_label_index` crashed the whole audit on a
missing `gh` binary, while a FAILED gh call was already handled as "unknown".
* ci.yml claimed state-independence "verified with ORCH_STATE_DIR pointed at an
empty directory". That moved the wrong knob: the ledger and Brain live behind
ORCH_LOCAL_RUNTIME, so the experiment left the populated 40-row ledger in
place and proved nothing. Both are set now.
* `capability_admission`'s commitment-detector probe depended on the real audit
ledger (gitignored) and on which records it happened to contain. It now runs
against a synthetic empty record set — deterministic everywhere.
DEDUP FINDING (CLAUDE.md 0), recorded before writing code and kept in
env_prereq.py's docstring: grepped for the concept, not the name. `pytest.skip`
appears in exactly one file (test_model_tier_resolution.py, twice, both
`shutil.which`-gated — the idiom this generalises); no conftest.py, no pytest.ini,
no pyproject.toml, no shared applicability helper. Nearest relatives are
single-call-site degradations, not reusable machinery: capability_admission.py:335
("cannot judge without the ledger") and _fleet_label_index skipping a failed repo.
Nothing to wire, activate or un-gate. env_prereq.py is test-applicability
infrastructure, not an orchestrator capability — no dispatch path, no outcome, no
ledger row — so the admission gate does not bind on it.
Verified: 330 passed / 0 failed / 0 of 24 skipped / 81 of 81 selftests / 5 of 5
gates locally AND re-run from the mirror layout (cmp-clean is not agreement).
In the runner reproduction: 306 passed / 0 failed / 24 of 24 skipped / 74 of 81
selftests / 3 of 5 gates, exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 22 minutes Limit details: You’ve used the included review currently available. Your 64 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?Wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (24)
Comment |
…wanted, plus .json (#110) Maint Coverage Guard failed the moment it was unheld, on both runs (#3 scheduled, #4 manual), with: error: Coverage verification could not find required coverage artifacts on a successful Gate run. warn: Unable to locate a recent successful Gate workflow run with required coverage artifacts: gate-coverage-trend, gate-coverage-trend-history, gate-coverage. maint-coverage-guard.yml downloads exactly three artifact NAMES. The Gate produced the first two under those names and the third as "gate-coverage.json", so the guard probed ten successful runs, found two of three every time, and reported no usable run at all. Measured rather than inferred: Gate run #241 (main, success) uploaded gate-coverage-summary.md, gate-coverage.json, gate-summary.md, gate-coverage-3.13-1, gate-coverage-trend-history, gate-coverage-trend, gate-coverage-summary, gate-coverage-3.12-1 -- trend and trend-history present, "gate-coverage" absent. An artifact NAME and the FILE inside it are different things, and this step conflated them. Renaming the artifact is safe and was checked, not assumed: "gate-coverage.json" as an artifact name appears nowhere else in this repo, and the only download-artifact steps in any workflow here are the guard's three. The file inside is still gate-coverage.json, so anything reading contents is unaffected. A paired literal in two files that disagreed -- this repo's signature defect -- and it stayed invisible for as long as the guard was held. That is the second thing the holds were concealing, after PR #90's six F821s. pr-00-gate.yml is create_only in the Workflows sync manifest with Orchestrator absent from overwrite_repos, so this local fix persists. The same mismatch likely affects every consumer whose Gate emits coverage, which is worth raising upstream separately. 448 collected, ruff and black clean, test_ci_gate_config 12 passed. Co-authored-by: Tim Stranske <tim@stranskemo.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The first CI run of the public repo came back red: 21 pytest failures, 8 module selftests, 2 capability gates — against 330 green locally. Not one was a defect in this tree.
What was actually wrong
Nothing, in the code. Every failing check needed something that exists only on the machine the system runs on. I reproduced the runner exactly — committed files only (
git archive), emptyHOME, no agent CLIs, no sibling checkouts, unauthenticatedgh, fresh state — and got 305 passed / 21 failed / 4 skipped / the same 8 selftests / the same 2 gates, byte-for-byte with CI. That made the diagnosis measurement rather than guesswork:~/.codex/skills/.../audit_code_root.shThe ledger is the big one and worth stating plainly: it is machine-local state by design. A fresh bootstrap holds the 14 rows the code declares; this instance's holds 40. The other 26 are accumulated registration history and cannot be rebuilt from source.
The fix
Each genuinely-absent prerequisite now skips with a reason naming the missing thing, via detectors in the new
env_prereq.py. Detection is of the prerequisite — never of$CI— so the same code is right on a runner, on the owner's box, and on a second instance with a differentORCH_STATE_DIR.Assertions are untouched. A check that runs asserts exactly what it asserted before; only its applicability gate is new. Selftests gate the smallest section that needs the missing thing rather than the whole function, so CI keeps everything else —
feedback.py's selftest still runs its several hundred other assertions.One failure was not a prerequisite and is fixed by isolation, not a skip.
test_experiment_arm_identityandexp_abcd's selftest replacesubprocess.Popenwholesale, and model resolution spawns a catalog probe whenever the advertised-model cache is cold — which lands in the stub and dies onstdout.writewith an int. Warm cache locally, cold on a fresh machine. Using adapters' ownORCH_MODEL_PROBEkill-switch makes both hermetic, so CI now runs more, not less.The skip ceiling — the reason skipping was allowed at all
A suite that looks green while running less is the failure this repo exists to prevent, so
verify.pynow bounds skipping instead of permitting it:.verify-floor.json(24 / 7 / 2— exactly what a machine with none of the local prerequisites skips). One more than agreed is a red that names the key to raise.passed + skipped, so a check may move between passing and consciously-skipped but the two together may never shrink.--update-floorrecordspassed + skippedand never re-measures the ceilings, so the floor cannot ratchet down behind a leak.ok— the silent-zero-exit rule's twin, and the reason the74 of 81number can be trusted.Deliberate break → revert (all three reverted byte-identical,
cmp-confirmed)33 > 24fails, namingskipped_max. With the ceiling removed, the identical break exits 0 and prints "VERIFIED". The ceiling is load-bearing, not decoration.run_selfteststo the skip mark → claims "81 of 81 modules ran" while 7 ran nothing;verify.py --selftestcatches it.passed→ CI goes red for being honest.Two real bugs the reproduction surfaced
capability_activation_audit._fleet_label_indextook the whole audit down on a missingghbinary, while a failedghcall was already handled as "unknown for this repo". Same information, opposite outcome.ci.ymlclaimed the suite was state-independent, "verified by running it withORCH_STATE_DIRpointed at an empty directory — 330 passed". That moved the wrong knob: the ledger and Brain live behindORCH_LOCAL_RUNTIME. The experiment left the populated 40-row ledger in place and proved nothing about a fresh machine. Both are set now, andCLAUDE.mdrecords the distinction.Verification
Also re-run from the mirror layout via
orch-sync-mirror.sh—cmp-clean is not agreement — same verdict. The local run is not weakened: it still executes all 330 and skips nothing.🤖 Generated with Claude Code