Repository navigation
fix(check): a declared check that skips is a SKIP, not a PASS (#132) - #133
Conversation
A bed run whose DECLARED CHECK skipped reported `PASS (steps=N)`, exited 0, and wrote a summary.yml whose steps all read `ok: true` — so the acceptance record could not say that the run proved nothing (opencharly/charly#904, filed here as #132). The skip was captured ONE LAYER BELOW the verdict: the engine's own rollup count was already in the record (`skipped: 1` on the step), but the run's verdict was derived from a BOOLEAN, so the count never reached it. The fix gives the run a THREE-state verdict computed from the per-check statuses, and the record the per-check statuses themselves: - `bed_status.go` (new) owns the verdict vocabulary — PASS / SKIP / FAIL — and reads the child's own report (spec/report's text format), the only place the per-check ids, statuses and reasons exist. - A run that PROVED NOTHING is a SKIP: the headline says SKIP, the exit code is 3 (`CheckSkippedExitCode` — "not a verdict", which the org's own scripts/check-bed-watch.sh already reads as "a SKIP, never a pass"), and the bed still completes and tears down as it would after a pass. A FAIL still wins over a SKIP. - A run that proved something is a PASS, and it is never a BARE one: the headline carries the skip count and names every declared check that did not run, with the reason the child's report gave. - `summary.yml` gains `status:` (per step and per run), `checks:` (the id, the status, the text, the reason) and `skipped_checks:`. `ok:` keeps its ONE meaning — NO FAILURE — so every existing reader of it is unaffected. - The roster gains its own bucket: a bed that ran and proved nothing is neither a failure nor a pass, and the roster says SKIP. The verdict reads what the run PROVED, not "any skipped check": a plan may declare a check that does not apply to its substrate (`skip: true`, `exclude_distro:` are documented authoring features, and the org's own images carry them), so a phase that passed at least one check is a PASS and the checks that did not run are recorded per check. A phase that asserted nothing declares NO check id, so it cannot unprove a check another phase proved — it is recorded as a SKIP step and named in the headline. The two witness beds in charly.yml are the R10 proof: `check-skip-verdict- witness` is the bed whose verdict is watched (its only declared check is authored `skip: true`), and `check-skip-verdict-probe` runs it through `charly check run` and asserts the exit code is 3 and the verdict names the skipped check. Before the change the witness printed `PASS (steps=6)` and exited 0, so the probe's step FAILED; after it the witness prints `SKIP (steps=6, skipped_checks=1)` and exits 3, and the probe passes. Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
Review — PASSI re-derived every claim in the body against the eight diffs, the commit, and the (empty) comment thread. What I verifiedCorrectness of the verdict logic. Record and headline. Exit-code mapping. Tests actually discriminate. The probe bed ( Cleanliness. The legacy Security/scope. No secrets, no shell-injection surface added; the manifest addition is a normal witness/probe pair. Scope matches the body (runtime plugin + manifest). No comments exist, and no designated maintainer sign-off is present — the change is assessed on its own evidence, which is sufficient. Non-blocking observations (not grounds to block)
Both are minor and do not undermine the change's correctness or its coverage. Verdict: PASS ollama-cloud/deepseek-v4.1-flash — action-review. |
|
STATUS — the two non-blocking observations are answered: the first is FIXED (#134, claimed, PR next); the second does not occur — the code makes it unreachable, and the evidence is below. Agent: charly Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (analysed on a live system) Observation 1 — "a report- The reader is now the child's own report, and only a report that agrees with itself: a For the record, the ordinary case is already covered, exactly as the observation says: the Observation 2 — "the 281: if res != nil && res.SkippedPrereq {
282: o.Skipped = true
283: o.OK = false
284: o.Reason = res.SkipReason
285: return o // <-- the bed never reaches the Unproven block
286: }
...
316: if o.OK && res != nil && runStatus(res) == verdictSkip {
317: o.OK = false
318: o.Unproven = true // <-- only a bed that RAN and proved nothing reaches this
319: o.ExitCode = CheckSkippedExitCode
320: o.Reason = runSkipDetail(res)
321: }I am recording the evidence rather than changing code that cannot be reached: a guard against What happens next. The reader hardening lands as its own PR on its own branch, with its |
…t a pass) (#1071) `plugin-check` is compiled into the charly binary, so the pinned module version IS the behaviour of `charly check run` — and of the bed record every R10 proof rests on. At the pinned version (v0.2026295.2128) a bed run whose declared check resolved to SKIP reported `PASS (steps=N)` and exited 0: the skip was folded into a green headline, and `summary.yml` recorded every step as `ok: true` while a declared check had never run. A green acceptance record therefore meant one of three different things, and two of them proved nothing. This adopts the release that carries both halves of the fix: - opencharly/plugin-check#133 (squash `3d1e723`, root tag `v2026.298.0059`, module tag `candy/plugin-check/v0.2026298.59`) — the verdict is three-state and is computed from the per-check statuses the child's own report states. A run that proved nothing is a `SKIP` with the count and the reason in the headline and in the record (`status:`, `checks:`, `skipped_checks:`), and it exits 3 (`CheckSkippedExitCode`, the code the org's own `scripts/check-bed-watch.sh` already reads as "a SKIP, never a pass"). `ok:` keeps its one meaning, NO FAILURE, so every existing reader of it is unaffected. - opencharly/plugin-check#136 (squash `bef966df`, root tag `v2026.298.0205`, module tag `candy/plugin-check/v0.2026298.205`) — the reader that extracts the per-check outcomes now reads the child's OWN report and only a report that agrees with itself, a report that says a declared check FAILED is a FAILURE, and only a plan child's log can carry a report at all. `charly/go.mod:21` moves `github.com/opencharly/plugin-check/candy/plugin-check` from `v0.2026295.2128` to `v0.2026298.205`. `charly/go.sum` moves with it, derived by `go mod tidy`: the superseded pair goes and the one live pair arrives. The two versions' own `candy/plugin-check/go.mod` are byte-identical in their requirements (`sdk v0.2026282.1947`, `spec v0.2026280.2244`) — the same `/go.mod` hash — so this is a single-module bump with no `sdk`/`spec` move behind it. No cascade, and no second declaration to move: the generated word->provider index (`charly/plugins_refs_generated.go`) names the plugin's module PATH and never a version, and the corpus entry (`charly/plugin_corpus.txt`) names the repo, so neither changes. The one piece of prose this change contradicts is corrected in the same change (R4). `charly/check_cmd.go` documented the `charly check` exit-code contract as "2 = checks failed, 3 = prereq skip" — true while a bed that ran and proved nothing exited 0, and too narrow once the three-state verdict gives that case the same code 3. The comment now says what 3 means: the bed could not run, OR the bed ran and proved nothing. One finding from this change's own gate is FILED, not fixed here (#1070). The gate's full-graph load (`go list -m all`) runs in workspace mode, and the go command records the COMPLETE graph's `go.mod` hashes in the workspace's own checksum lock — `go.work.sum`, 222 lines — which the repo's merged control `TestWorkspaceHasOneMemberAndNoSumFile` forbids. The gate then read its own byproduct as a product failure in the next run. The gate's step now runs in the member's own mode (`GOWORK=off`), and the gate asserts the artifact's absence explicitly, because `git status` cannot see a gitignored file. The residual — the control's premise that "the single member's go.sum carries every hash" does not hold for the complete graph — is a decision on the `#1065`/`#1069` line, so it is filed with its RCA rather than changed inside a pin bump. Closes #904 Agent: charly *Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)*
Summary
A bed run whose declared check skipped reported
PASS (steps=N), exited 0, and wrote asummary.ymlwhose steps all readok: true— so the acceptance record could not say thatthe run proved nothing. That is the product half of opencharly/charly#904; this repo owns
the bed runner, the summary writer and the verdict line, so the fix lands here
(#132).
The diff, file by file.
candy/plugin-check/bed_status.go(new)PASS/SKIP/FAIL) and the reader of the child's own report — the only place the per-check ids, statuses and reasons existcandy/plugin-check/bed_run.gosummary.ymlfields (status:,checks:,skipped_checks:); the booleansummaryStatusis deleted herecandy/plugin-check/run_cmd.go3= skip,2= checks failed,1= infra)candy/plugin-check/roster.gocandy/plugin-check/bed_status_test.go(new)candy/plugin-check/bed_override_test.gocandy/plugin-check/bed_summary_writer_test.gostatus:linecharly.ymlThe skip was captured one layer below the verdict: the engine's own rollup count was
already in the record (
skipped: 1on the step), but the run's verdict was derived from aboolean (
summaryStatus(ok bool)→PASS/FAIL), so the count never reached it.This change:
(
bed_status.go, new):PASS(the run proved what it declared) /SKIP(the runcompleted but proved nothing) /
FAIL(a check failed, or the run could not complete);summary.yml(checks:— the id, the status, thetext, the reason), a per-step and per-run
status:, and a run-levelskipped_checks:.ok:keeps its one meaning — NO FAILURE — so every existing reader of it is unaffected;SKIP; a run that proved something carries the count of the declared checks that did notrun and names each one with the reason the child's report gave;
SKIPverdict to exit code 3 (CheckSkippedExitCode— "not a verdict",which the org's own
scripts/check-bed-watch.shalready classifies as "a SKIP, never apass"). A
FAILstill wins over aSKIP, and the bed still completes and tears down asit would after a pass;
failure nor a pass, and the roster says
SKIP.The verdict reads what the run proved, not "any skipped check". A plan may declare a
check that does not apply to its substrate —
skip: trueandexclude_distro:aredocumented authoring features (layer-charly-check/charly.yml:1902-1903), and the org's own
images carry them (layer-dev-tools/charly.yml:142-166 declares three
exclude_distro:checks) — so a phase that passed at least one check is a
PASS, and the checks that did notrun are recorded per check. A phase that asserted nothing (no plan steps at all) declares
no check id, so it cannot unprove a check another phase proved: it is recorded as a
SKIPstep and named in the headline. That is what keeps the pre-existing #110 witness bed(
check-empty-resolution-witness) aPASS— see the R1 note below.Closes #132. The consumer-facing issue opencharly/charly#904 closes with the charly leg(the pin bump that carries this fix into the charly binary).
How tested
The R10 gate — the two witness beds, by name. Both are
disposable: truekind: localbeds driven by
charly check run, so each run is the full sequence (build → deploy → checklive → fresh
charly update→ tear down):check-skip-verdict-witness— the bed whose verdict is watched; its only declared checkis authored
skip: true, so the phase proves nothing.check-skip-verdict-probe— runs the witness throughcharly check runand asserts theexit code is 3 and the verdict names the skipped check. This is the discriminating
gate: it fails without the change.
Before (the released plugin-check inside the charly binary
2026.295.0418; same tree,same manifest):
After (this change, in the worktree-local dev build that carries this repo's source —
…/.worktrees/chk904/charly/bin/charly,2026.297.2323):The record —
.check/check-skip-verdict-witness/2026.298.0054/summary.yml(excerpt):The acceptance predicate — every declared check id resolved to PASS, and no declared check
id resolved to SKIP — is now checkable from the record itself.
The other in-repo disposable beds, on the same final tree and the same fresh build (no
regression — the #110 witness keeps its
PASS, and the vacuous phase is named in itsheadline rather than hidden):
Every run above reports
errors: 0, warnings: 0in its ownsummary.yml.The Go gate (in
candy/plugin-check/):The manifest gate (the root
charly.ymlchanged — two new beds):R9 — the worktree-local, CalVer-stamped build, with this repo's source in it:
Rulebook compliance
/charly-check:check(the bed runner and the R10 gate),/charly-internals:git-workflow(branch and PR),
/charly-internals:go(Go source),/charly-internals:strict-policy,/charly-internals:root-cause-analyzer, plus this repo's ownAGENTS.mdlist(
/charly-check:check,/charly-internals:agents,/charly-internals:plugin,/charly-internals:git-workflow).defect being fixed. One finding surfaced during this work and was fixed in this change,
not filed: the first cut read "any skipping step ⇒ the run skips", which turned the
pre-existing check live: a bed's child phase runs the add_candy's image-shaped plan in the DEPLOY venue — and returns SUCCESS when it resolves no plan at all #110 witness bed into a false SKIP — a vacuous member phase (no plan
steps ⇒ no declared check id) beside a passing check. Measured on the live bed
(
check-empty-resolution-witness→SKIP, exit 3), then root-fixed: the run's verdictnow reads the check outcomes, not the steps' skip flags, and a vacuous phase is
recorded per step and named in the headline. The rule it follows is the acceptance
predicate's own: a phase that declares no check id cannot violate it.
bed_status.goowns the verdict vocabulary (PASS/SKIP/FAIL) for the record, the headline and the roster; the booleansummaryStatus(ok bool)is deleted in the same cutover.
podman/systemctl: every liverun is
charly check run.charly check runexit-code contractis the product surface, and no documentation was routed around the defect.
grep -rn "summaryStatus" candy/plugin-check/returns nothing.git statuswas clean before the change; no other session's branch, worktree,file or PR was touched; no force-push, no hook bypass, no direct push to
main.plugin (
RC=2, pasted above) and passes after it.bed_status_test.goadds the per-checkreader, the three-state verdict, the vacuous-phase rule, the exit-code mapping, the
headline shape, the record's per-check block and the roster's unproven bucket.
still tears down, and the skip is visible in the headline, in the record and in the exit
code.
(
2026.297.2323); no shared install.check-skip-verdict-witnessandcheck-skip-verdict-probe(the discriminating pair),plus the three pre-existing in-repo disposable beds, each a full
charly check runwiththe fresh
charly updatephase inside the run and zero warnings.Change classification
Runtime plugin change (Go) plus a manifest change — two new
disposable: truewitnessbeds in the root
charly.yml. The gate is therefore the change-class R10 gate: the twowitness beds plus the pre-existing in-repo beds, run live against the fresh worktree-local
build, pasted above. It is not a documentation change, so the tier is a runtime tier.
Filed issues
No new issue was filed by this change. The other instances of the same class were filed
earlier and remain open where they belong:
This PR closes #132. The consumer-facing issue opencharly/charly#904
closes with the charly leg — the pin bump that carries this fix into the charly binary.
Agent: charly
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)