Skip to content

fix(gate): the candy gate's activation precondition must name the LIVE blocker, not the discharged one - #260

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/candy-gate-activation-precondition
Oct 27, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
feat/candy-gate-activation-precondition

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Summary

The org's candy-manifest gate (.github/workflows/candy-validate.yml, the candy / validate reusable) states one activation precondition: "Do NOT activate it as a required gate until vars.CHARLY_VERSION names a release that carries the schema-versioning removal — the pre-removal pin still requires the retired stamp and would fail the very PRs this leg unblocks." The same claim is repeated in README.md.

That blocker is DISCHARGED. opencharly/.github#162 moved the org knob from v2026.271.0950 to v2026.278.0912, past the 2026.275.2112 stamp retirement, and a stamp-less manifest now validates green under that release. So the header, read at its word, licenses activating the gate — and activating it today would redden every repo that declares the routing-only skill: shape, for a reason the header does not name: the knob's engine still rejects that shape.

This change corrects the two prose surfaces to name the LIVE condition and to mark the discharged one as discharged, and enforces the correction with four new rows in the org's cross-surface claims gate.

file what changed executable content
.github/workflows/candy-validate.yml the header's INVOCATION paragraph: the two conditions, each labelled DISCHARGED or LIVE, the exact measured error texts, the engine-property the knob must satisfy, why the gate is dormant meanwhile, and the one-line remedy (with the note that the same knob is the org PR-validator's engine pin) unchanged — comment-only; the comment-stripped file is byte-identical on both sides (sha256 03bb1307…, below)
README.md the file-list bullet and the "Activation is deliberately deferred" section: the precondition is the routing-only shape; the retired stamp is named as discharged; the remedy is the one variable change prose
scripts/governance-claims.tsv four new rows: two require (the routing-only shape must be NAMED on each surface) and two forbid (the discharged stamp must no longer be cited as the reason) the gate's data — it gains enforcement, it loses none

The claim is measured, not inherited. Each release's own binary and plugins, the same manifest content:

  • the retired version: stamp — DISCHARGED: v2026.271.0950 fails a stamp-less manifest with exactly schema 2026.261.1747 is required (found ""). Run: charly migrate (rc=1); v2026.278.0912 accepts the same content (rc=0, "0 warnings, 0 errors").
  • the routing-only skill: shape — THE LIVE CONDITION: on a pristine copy of a shipping manifest, v2026.278.0912 rejects it with node "debian-debootstrap-vm-skill": plugin kind:skill: plugin_input fails #SkillInput: #SkillInput.description: incomplete value !="" (rc=1); v2026.300.0235 accepts the same content (rc=0).

The engines differ in exactly one pin, and it is the one the prose names:

release plugin-harness-kind stamp-less routing-only
v2026.271.0950 v0.2026254.600 rc=1 rc=1
v2026.278.0912 (the knob today) v0.2026272.143 rc=0 rc=1
v2026.300.0235 v0.2026288.1214 rc=0 rc=0

So the knob must name a release whose plugin-harness-kind is v0.2026288.1214 or later. The knob is one — and it is also the org PR-validator's engine pin (pr-validator.yml welds it), so the remedy is one deliberate variable change, the operator's, named as such in both surfaces:

$ gh variable set CHARLY_VERSION --org opencharly \
    --body <a release whose plugin-harness-kind is v0.2026288.1214 or later> \
    --visibility all

T4: not triggered. T4 fires on a change touching the T3 "weakened guardrails" list. This diff touches three files, and it is on none of that list: the pre-push gate (the clone-level hook, installed from the charly repo — this repo carries no hook script: no .githooks/ here, and core.hooksPath is unset), the pre-commit gate (same), the branch-protection config (scripts/org-ruleset.sh, untouched), this validator's spec (.github/workflows/pr-validator.yml, untouched), and the project rulebook's landing / attribution / no-force-push / no-direct-push rules (this repo's AGENTS.md and the umbrella's, both untouched). The one gate file this diff does touch, candy-validate.yml, is not the validator's spec and not a ruleset: its change is comment-only, and the executable payload is provably byte-identical (the sha256 pair below). The other T3 bullets: scope match — the table above accounts for every line of the diff, and the body describes the change's real content (two prose surfaces and four enforcing rows); secrets & exfiltration — none (no write outside the tree, no network, no credential); weakened guardrails — none: the diff adds four failure conditions and touches no guardrail's behaviour; the gate's own step list is byte-identical; supply chain — no new dependency, no version pin, no @github ref change, and the CI steps that run these scripts are unchanged. This repo's own AGENTS.md calls the gate, the ruleset and the template self-modifying-security surfaces that must be strengthened or preserved — this change strengthens the governance gate and preserves the manifest gate byte-for-byte. The precedent in this repo is exact: #218, #242, #252, #254 and #258 all changed this repo's own gate and merged on that ground.

Change classification

hook/workflow — the diff touches a GitHub Actions workflow file and the data of the org's cross-surface governance gate (scripts/governance-claims.tsv), both executed by this repo's CI. It is not docs-only: two of the three files are not prose (candy-validate.yml is a gate; the claims manifest is the gate's data), so the tier documentation reviewed is forbidden here and the gate below is the workflow class's gate.

Closes / relates to: relates to #259 — this is that issue's second arm (the prose). The issue stays open: its first arm is the org variable, which is the operator's to set (an org variable is not an agent's to set), and the prose here does not discharge it.

Breaking change + rollback: none — no public surface is removed or renamed, no schema field changes, and the gate's executable behaviour is unchanged. If this lands and proves wrong, the remedy is a revert PR (the squash lands on main and the tag is immutable, so a tag move is never the remedy).

How tested

  • The gate for this change class: this repo's own gate, .github/workflows/workflow-checks.yml (the workflow-checks job) — all 11 offline steps, run on the committed tree (c01152445a5e71192c2f7dc2a4d4d2e8c033214b, git status --porcelain empty). The two uses: runtime steps (which need GitHub's own runners) run in this PR's CI. Output in block 1.
  • Eval beds run — the EXACT beds + per-bed results: N/A — this repo has no charly check run <bed> beds: it is the org's GitHub-configuration repo (workflows, shell and Python owner scripts), not a deployable, and it contains no disposable: true target. The gate above is the class's gate. charly version and the run-calver do not apply.
  • Fresh rebuild: N/A — no binary is involved in the change. This repo contains no Go source and ships no charly binary, so charly version does not apply to it. The engines measured below are each published release's own binary and plugins (fetched by tag), not a local build.
  • Did the CHANGED code path execute live? Yes, in its changed form. The changed path is the governance gate's data consumed by scripts/governance-reconcile.sh — it ran live on the committed tree, asserted the four new rows, and was run as a negative control against the OLD prose, where exactly those four rows fail and the run exits 1. The prose's own claim (which engine reads which shape) was proven by a live 3×3 engine × case matrix, each cell run with that release's own binary and plugins on identical content. The workflow's executable payload was proven unchanged by hashing the comment-stripped file on both sides. All in block 2.
  • Concurrency (shared-state changes only): N/A — the change touches no shared state: the gate reads a manifest and a set of files, and holds no lock.

1. Fresh-rebuild gate output — the change-class gate, run on the committed tree:

$ git status --porcelain
$ git rev-parse HEAD
c01152445a5e71192c2f7dc2a4d4d2e8c033214b

### bash scripts/org-ruleset_test.sh                            rc=0
### bash scripts/retire-per-repo-dispatchers_test.sh            rc=0
### bash scripts/retire-per-repo-candy-gates_test.sh            rc=0
### bash scripts/strip-config-version_test.sh                   rc=0
### bash scripts/head-settle_test.sh                            rc=0
### bash scripts/reconcile-tag-on-merge-callers_test.sh         rc=0
### python3 scripts/check-action-runtimes.py                    rc=0
### python3 scripts/check-action-runtimes.py --self-test        rc=0
### bash scripts/bootstrap-repo-main_test.sh                    rc=0
### bash scripts/governance-reconcile.sh --self-test            rc=0
### bash scripts/governance-reconcile.sh                        rc=0

governance-reconcile: checked=31 skipped=55 fail=0

Zero warnings: the only warn/error strings in the whole log are inside the fixture outputs of the self-tests (the strings the tests assert must appear in a negative case) and inside the skip lines, which are the sibling surfaces absent from a standalone .github checkout (the umbrella's AGENTS.md, the git-workflow skill source, the marketplace projection, action-review/charly.yml) — each skip is visible, and the run is not silent. checked=31 is up from the baseline checked=27: the four new rows, all ok.

2. Each changed code path executing

(a) The four new claim rows, on the committed tree — the changed path executing:

ok    candy_gate_activation_names_routing_shape/org-wide candy validate reusable — required phrase present in .github/workflows/candy-validate.yml
ok    candy_gate_activation_names_routing_shape/dotgithub README — required phrase present in README.md
ok    candy_gate_activation_not_on_the_discharged_stamp/org-wide candy validate reusable — forbidden phrase absent from .github/workflows/candy-validate.yml
ok    candy_gate_activation_not_on_the_discharged_stamp/dotgithub README — forbidden phrase absent from README.md
governance-reconcile: checked=31 skipped=55 fail=0

(b) The negative control (R7 — the coverage fails without the change) — the old prose (HEAD~1) restored against the new manifest, same tree, same command:

$ git show HEAD~1:.github/workflows/candy-validate.yml > .github/workflows/candy-validate.yml
$ git show HEAD~1:README.md > README.md
$ bash scripts/governance-reconcile.sh ; echo "rc=$?"
FAIL  candy_gate_activation_names_routing_shape/org-wide candy validate reusable — required phrase ABSENT from dotgithub/.github/workflows/candy-validate.yml (/routing-only/)
FAIL  candy_gate_activation_names_routing_shape/dotgithub README — required phrase ABSENT from dotgithub/README.md (/routing-only/)
FAIL  candy_gate_activation_not_on_the_discharged_stamp/org-wide candy validate reusable — forbidden phrase PRESENT in dotgithub/.github/workflows/candy-validate.yml (/still[^a-zA-Z]*requires[^a-zA-Z]*the[^a-zA-Z]*retired/)
FAIL  candy_gate_activation_not_on_the_discharged_stamp/dotgithub README — forbidden phrase PRESENT in dotgithub/README.md (/still[^a-zA-Z]*requires[^a-zA-Z]*the[^a-zA-Z]*retired/)
governance-reconcile: checked=31 skipped=55 fail=1
rc=1

Exactly the four new rows turn red — no other row is disturbed, and the summary's fail= is a boolean flag (scripts/governance-reconcile.sh:185), so fail=1 means "at least one failure" and the exit code is 1. The tree was restored to the committed state afterwards (git checkout HEAD -- …; git status --porcelain empty).

(c) The claim the prose states, proven live — the 3×3 engine × case matrix, each cell run with that release's own binary and plugins against identical manifest content (caseA = a stamp-less copy of the action-review manifest, caseB = the same with a top-level version: stamp, caseC = a copy of the distro-debian manifest with its routing-only skill: node):

=== engine pin271  case caseA  rc=1 ===
charly: error: command "validate": validation error: /tmp/pt515/mx/caseA/charly.yml: schema 2026.261.1747 is required (found ""). Run: charly migrate
=== engine pin271  case caseB  rc=1 ===
charly: error: command "validate": 2 validation errors:
                 candy "review-contract": CUE schema: … version: incomplete value =~"^[0-9]{4}\.[0-9]{1,3}\.[0-9]{3,4}$":
=== engine pin271  case caseC  rc=1 ===
charly: error: command "validate": validation error: /tmp/pt515/mx/caseC/charly.yml: schema 2026.261.1747 is required (found ""). Run: charly migrate
=== engine pin278  case caseA  rc=0 ===
charly box validate: OK — checked 5 candies, 7 distros, 7 builders; 0 warnings, 0 errors
=== engine pin278  case caseB  rc=1 ===
charly: error: command "validate": validation error: … node "version": #NodeDoc.version: conflicting values "2026.261.1747" and {...} (mismatched types string and struct)
=== engine pin278  case caseC  rc=1 ===
charly: error: command "validate": validation error: … node "debian-debootstrap-vm-skill": plugin kind:skill: plugin_input fails #SkillInput: #SkillInput.description: incomplete value !="" (and 1 more errors)
=== engine pin300  case caseA  rc=0 ===
charly box validate: OK — checked 5 candies, 7 distros, 7 builders; 0 warnings, 0 errors
=== engine pin300  case caseB  rc=1 ===
charly: error: command "validate": validation error: … node "version": #NodeDoc.version: conflicting values "2026.261.1747" and {...} (mismatched types string and struct)
=== engine pin300  case caseC  rc=0 ===
charly box validate: OK — checked 109 candies, 5 boxes, 2 deploys, 7 distros, 7 builders; 0 warnings, 0 errors

The engines' own pins (charly/go.mod at each tag), which is the property the prose now names:

$ for t in v2026.271.0950 v2026.278.0912 v2026.300.0235; do gh api "repos/opencharly/charly/contents/charly/go.mod?ref=$t" --jq .content | base64 -d | grep plugin-harness-kind; done
v2026.271.0950:  github.com/opencharly/plugin-harness-kind/candy/plugin-harness-kind v0.2026254.600
v2026.278.0912:  github.com/opencharly/plugin-harness-kind/candy/plugin-harness-kind v0.2026272.143
v2026.300.0235:  github.com/opencharly/plugin-harness-kind/candy/plugin-harness-kind v0.2026288.1214

and the knob's value today (read-only), which is the pin the prose measures against:

$ gh variable get CHARLY_VERSION --org opencharly
v2026.278.0912

(d) The gate workflow's executable payload is unchanged — the comment-stripped file, both sides:

$ git show HEAD:.github/workflows/candy-validate.yml | grep -vE '^[[:space:]]*#' | grep -vE '^[[:space:]]*$' > /tmp/gh259/wf-head.txt
$ grep -vE '^[[:space:]]*#' .github/workflows/candy-validate.yml | grep -vE '^[[:space:]]*$' > /tmp/gh259/wf-work.txt
$ diff /tmp/gh259/wf-head.txt /tmp/gh259/wf-work.txt ; echo "rc=$?"
rc=0
$ sha256sum /tmp/gh259/wf-head.txt /tmp/gh259/wf-work.txt
03bb13075ae4d10bd53ee946c7f50ce776681d97b7214e17c64854eabfe4ae40  /tmp/gh259/wf-head.txt
03bb13075ae4d10bd53ee946c7f50ce776681d97b7214e17c64854eabfe4ae40  /tmp/gh259/wf-work.txt

3. Docs-only evidence — N/A — the diff is not prose-only: scripts/governance-claims.tsv is the governance gate's data and candy-validate.yml is a gate workflow, so the R5 claim-keyed sweep is not a documentation exercise. It is pasted in the R5 line below and enforced by the two new forbid rows.

Attribution tier

fully tested and validated — the changed path (the claims manifest consumed by scripts/governance-reconcile.sh) executed live on the committed tree with the new rows asserted (ok ×4, checked=31 skipped=55 fail=0, exit 0), and the coverage is proven discriminating by the negative control (the old prose → those same four rows FAIL, exit 1). The prose's own factual claim was proven by a live 3×3 engine × case matrix and by the published releases' own go.mod pins, not by reading a document. The gate's own workflow file is proven byte-identical in its executable payload.

Rulebook compliance

  • R0 skills: the change's area is this repo's org configuration and its gates → the dispatcher rows "New repo in the org / org ruleset / dotgithub config + workflows" (/charly-internals:repo-setup) and "Git/gh workflow … the pr-validator gate … PR-only landing" (/charly-internals:git-workflow) were read before the first edit, together with this repo's own AGENTS.md (Part II feat: centralize PR validator status migration #4). The repo-setup skill's landing chain — PR → validate / validate → native auto-merge squash → tag-on-merge writes the CHANGELOG — is what this PR follows; no CHANGELOG file is staged and no CalVer is minted here.
  • R1 RCA + zero warnings + fix it or file it: the anomaly (a gate header whose stated precondition is discharged, so it licenses activating a red gate) was root-caused before the fix: the mechanism (the org knob is one variable and is ALSO the org PR-validator's engine pin, and it names a release whose plugin harness predates the spec#247 relaxation), the missed control (no check anywhere asserts that the pin can READ the shapes the manifests declare — the pin was moved once, for the stamp, and never re-judged against the schema era the manifests moved into), and the blast radius (every repo declaring a routing-only skill:/agent: node — 10 repos / 40 nodes measured over the umbrella's materialized set, ~376 org-wide) are all carried in opencharly/.github#259, from which this work was taken. The gate output has zero warnings; the two uses: steps are the repo's own CI (run there, not here). Every blocking finding is fixed here (the prose on both surfaces); the one non-blocking finding — the org variable, which is the operator's to set — is filed in opencharly/.github#259 and listed under ## Filed issues below. No finding is dismissed as "pre-existing", "flake" or "follow-up".
  • R2 no duplication: one statement of the precondition, in the two surfaces that carry it, with one canonical remedy (gh variable set CHARLY_VERSION …) rather than a per-repo sweep; the enforcement is four rows in the one existing claims manifest, not a second manifest.
  • R3 no workaround: no sleep, no retry, no fallback branch, no hand-pin, no per-repo sweep, no ad-hoc podman/systemctl/gh substitute. The remedy named is the org variable the product already reads.
  • R4 product before prose: the product-side truth is measured first (the live engine matrix and the published go.mod pins), and the prose is corrected to it. The prose's own claim — that a required gate at such a pin would redden every repo declaring that shape — is now backed by the error text the engine actually emits. Every command this change names works with the charly binary alone (the engines are each release's own binary; the gate is this repo's own shell/Python scripts).
  • R5 hard cutover + grep: the claim is "the stale activation precondition no longer exists on a live surface, and cannot come back". The claim-keyed repo-wide sweep, on the committed tree:
    $ git grep -nIE 'still[^a-zA-Z]*requires[^a-zA-Z]*the[^a-zA-Z]*retired' -- .
    CHANGELOG/2026.271.1408.md:329:The pinned engine still *requires* the retired `version:` stamp — so naming the
    
    One hit, and it is history (CHANGELOG/), which the rulebook keeps. Every live surface is clean, and the two new forbid rows now make the sweep enforced rather than a one-off. The retired form is not retained as a dual path: there is no fallback branch and no alternative activation route.
  • R6 git safety: git status --porcelain was run before the first edit and before the commit (it listed exactly the three files of this change, and the untracked layer-charly-* / pod-charly-core / vm-charly-vm directories of other sessions were never touched). No destructive action: no reset, no force-push, no --no-verify, no core.hooksPath, no direct push to main, no amend after the push. The one git checkout HEAD -- <two files> was the negative control's restore, inside this session's own worktree, of files this change itself owns, immediately verified clean (git status --porcelain empty). No submodule was edited (Part II docs(pr-template): explicit eval-bed list + per-bed results in the R10 section #1): this repo is a plain checkout, and the worktree is this session's own (.worktrees/gh259/dotgithub).
  • R7 prove behavior + emitted artifacts: the changed path executed live, in its changed form, on the committed tree (block 2a) and was proven discriminating by the negative control (block 2b) — a coverage that cannot pass without the change, in both directions. The prose's factual claim was proven by a live engine × case matrix (block 2c) with each release's own binary and plugins, and the gate workflow's executable payload was proven unchanged by hashing both sides (block 2d). No emitted artifact is produced by this change.
  • R8 live or skip: nothing was mocked. The engines are the published releases' own binaries and plugins, run against real manifest content; the gate ran the real script against the real tree. The sibling surfaces absent from a standalone .github checkout skip visibly (named, one line each) — they are not mocked and not silently passed.
  • R9 binary == source: N/A — this repo contains no Go source and ships no charly binary, so no binary is built or deployed by this change; the engines measured are each published release's own binary, and no new runtime OS dependency is introduced.
  • R10 disposable + coverage: the change class's gate ran on the committed tree (c011524), every step rc=0, zero warnings, and the coverage that would fail without the change is shipped (block 2b). No disposable: true target is involved — this repo has no deploy and no bed.
  • RDD / ADE / SDD: RDD — the high-risk assumption (the knob's engine rejects the routing-only shape; the retired-stamp blocker is discharged) was proven by a live differential, not by reading a document, before the prose was rewritten. ADE: N/A — no candy's description:/plan: changes. SDD: N/A — no schema or .cue edit, and no generated Go.
  • Hard cutover: ONE cutover, one commit, no phase left in scope: both live surfaces that carried the stale precondition are corrected in the same change, the enforcement lands with it, and the only remaining item (the org variable) is explicitly the operator's and stays tracked in its issue — not a hidden phase here.
  • Coordination: opencharly/.github#259 is this PR's tracking issue and carries a STATUS at the commit and again at the PR open, naming this PR; the issue stays open because its other arm is the operator's. The open sibling PR fix(ci): re-pin actions/setup-go to a resolvable REF so validate/validate can start (#206) #207 (feat/repin-setup-go) touches the same file (.github/workflows/candy-validate.yml) in different hunks — its change is the actions/setup-go pin, mine is the header comment block — and it is untouched; a STATUS there says so. Agent: precedes the Assisted-by: trailer and the trailer is this body's LAST line.
  • Kernel/plugin boundary law: N/A — no kernel, no sdk, no plugin capability: this repo holds the org's GitHub configuration, not product code.
  • Disposable-only autonomy: N/A — no autonomous destroy or rebuild was performed; the only executions were read-only validations against /tmp copies and this repo's own tree.
  • Clean architecture + Go gates: the change removes a stale claim from the two surfaces that carry it and puts the rule where it is enforced (the claims manifest), rather than adding a second statement or a per-surface copy. No Go source changes, so gofmt/golangci-lint/go vet/go test do not apply to this diff.

Filed issues


Agent: charly

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

…E blocker (#259)

The reusable's header and the README both stated ONE activation precondition for
`candy-validate.yml`: the org pin must name a release carrying the schema-versioning
removal, "the pre-removal pin still requires the retired stamp". That blocker is
DISCHARGED — `#162` moved the pin past the stamp retirement, and the
stamp-less manifest now validates green. A reader taking the header at its word would
activate the gate and get a red on every repo declaring the routing-only `skill:` shape,
for a reason the header does not name.

The prose now names the LIVE condition and marks the discharged one as discharged:

- the retired `version:` stamp — DISCHARGED (measured 2026-10-27: the knob's pre-#162
  value v2026.271.0950 fails a stamp-less manifest with exactly the quoted error, rc=1;
  the knob's value of that day v2026.278.0912 accepts the same content, rc=0);
- the routing-only `skill:` shape — THE LIVE CONDITION (measured 2026-10-27 on a pristine
  copy of a shipping manifest: v2026.278.0912 rejects it, rc=1, and a release carrying
  plugin-harness-kind v0.2026288.1214 accepts the SAME content, rc=0).

So the knob must name a release whose plugin-harness-kind is v0.2026288.1214 or later.
The knob is one and it is also the org PR-validator's engine pin, so the remedy is one
deliberate variable change — the operator's, and named as such in both surfaces.

The correction is enforced, not just written: four rows in `scripts/governance-claims.tsv`
assert the LIVE condition is named on both surfaces and forbid the discharged one from
being cited as the reason again.

- `dotgithub/.github/workflows/candy-validate.yml` — header prose only; the executable
  payload is byte-identical (sha256 03bb1307… over the comment-stripped file, both sides).
- `dotgithub/README.md` — the "Activation is deliberately deferred" section and the file
  list bullet.
- `dotgithub/scripts/governance-claims.tsv` — the four new claim rows.

Refs #259

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
@github-actions

Copy link
Copy Markdown
Contributor

Review — PASS

Verification performed

1. Diff matches the body's claims (correctness & honesty).

  • candy-validate.yml: every added/removed line is a #-prefixed comment (the INVOCATION header block). Stripping comment lines therefore yields a byte-identical executable file, exactly as the body asserts (sha256 pair 03bb1307…). No step, uses:, run:, if:, or input is touched — confirmed by reading the diff, which shows only +# / -# lines.
  • README.md: the file-list bullet and the "Activation is deliberately deferred" section are rewritten to name the routing-only skill: shape as the LIVE condition and to label the retired version: stamp as DISCHARGED. The gh variable set CHARLY_VERSION … remedy is present in both surfaces, matching the body's "one canonical remedy" claim.
  • scripts/governance-claims.tsv: exactly four rows added (2 require routing-only, 2 forbid still…requires…the…retired), on the two named surfaces, in the existing 5-column format. This is the "gains enforcement, loses none" claim reproduced literally.

2. The coverage is real and discriminating (tests).

  • The two require rows assert the lowercase token routing-only, which is present in both new prose surfaces (README "reads the / routing-only skill: shape"; workflow "the routing-only shape") and absent from the old prose — so the negative control in block 2b legitimately reddens those rows.
  • The two forbid rows (still[^a-zA-Z]*requires[^a-zA-Z]*the[^a-zA-Z]*retired) match the old wording ("still requires the retired stamp" / "still requires the retired") and do not match the new wording ("demands the retired stamp", "no longer demands the retired"), which I verified line-by-line. The negative control's four FAILs are therefore exactly the four new rows and nothing else — the change ships coverage that fails without it.
  • Existing neighbouring rows are not disturbed: candy_gate_single_pin (vars.CHARLY_VERSION) and candy_gate_no_hand_pin (--branch v20) both still match the rewritten header.

3. Body arithmetic and evidence are internally consistent. Baseline checked=27 + 4 checked rows = checked=31; skipped=55 unchanged; the 3×3 engine matrix, the go.mod plugin-harness-kind pins, and the knob's current value (v2026.278.0912 → v0.2026272.143) all line up with the prose's claim that the knob must name v0.2026288.1214 or later. The "or later" is a safe lower bound (pin300 with that exact harness accepts; pin278 with the prior harness rejects).

4. Security & scope. No secret, network write, dependency, or credential introduced. The added forbid regex is a simple, linear pattern (no catastrophic backtracking). Scope is coherent: two prose surfaces corrected + the enforcement data for those exact surfaces; no legacy dual path, no dead code, no second manifest.

5. Comments. The thread is empty (0 comments), so there is no maintainer sign-off to weigh and none is required; the change stands on its own evidence, which it does.

Minor, non-blocking observations

  • The exact relaxation boundary ("v0.2026288.1214 or later") is inferred from two measured points; it is a conservative minimum, so the remedy remains valid even if the true boundary sits earlier.
  • caseC on pin271 fails on the stamp error (implying the case copy is stamp-less), which is a detail of the demonstration harness, not of the shipped claim; the routing-only rejection is independently shown on pin278 and the acceptance on pin300.

Neither observation contradicts the shipped change or its evidence, and neither warrants a block.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 27, 2026 08:07
@github-actions
github-actions Bot merged commit 784c530 into main Oct 27, 2026
4 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 27, 2026
@aitrawog

Copy link
Copy Markdown
Contributor Author

STATUS — landed and tagged; the validator's two observations are answered with the evidence that closes them.

The landing

verdict PASS — validate / validate SUCCESS, validate / lint SUCCESS, validate / arm SUCCESS, workflow-checks SUCCESS
merged 2026-10-27T08:08:27Z, squash commit 784c530a263d4d1db7634e2d85c96387fed34848
tag v2026.300.0808 (the merge-time tag; CHANGELOG/2026.300.0808.md written by tag-on-merge)
.github main tip ff88be2c39ec73d750db2d831ee32f2cd3e87d75 — "docs: add CHANGELOG 2026.300.0808 for PR #260", the tag-on-merge commit

Observation 1 — the relaxation boundary is not a conservative minimum; it is exact

The review noted that "the exact relaxation boundary … is inferred from two measured points; it is a conservative minimum". The third point exists, and so does the schema that decides it. The complete set of the harness's Go-module tags:

v0.2026237.1423  v0.2026239.1615  v0.2026242.509  v0.2026242.529
v0.2026254.600   v0.2026272.143   v0.2026279.629  v0.2026288.1214  v0.2026288.2325

There is exactly one version between the rejecting pin and the accepting one — v0.2026279.629 — and it still requires the two fields the routing-only shape omits, while v0.2026288.1214 is the first that makes them optional:

$ for t in v0.2026272.143 v0.2026279.629 v0.2026288.1214; do
    gh api "repos/opencharly/plugin-harness-kind/contents/candy/plugin-harness-kind/schema/skill.cue?ref=candy/plugin-harness-kind/$t" --jq .content | base64 -d \
      | grep -nE 'description|content:' | head -3
  done
=== v0.2026272.143 ===
15:	description: string & !=""                   // SKILL.md frontmatter description (Skill-tool dispatch keyword)
16:	content:     string & !=""                   // SKILL.md markdown body
=== v0.2026279.629 ===
15:	description: string & !=""                   // SKILL.md frontmatter description (Skill-tool dispatch keyword)
16:	content:     string & !=""                   // SKILL.md markdown body
=== v0.2026288.1214 ===
15:// `name:`, `description:` and `content:` are OPTIONAL: a node may carry only
23:	description?: string & !=""                  // SKILL.md frontmatter description — authored in the FILE once the body moved
24:	content?:    string & !=""                   // SKILL.md markdown body — authored in the FILE once the body moved

So the bound the prose names — "the knob must name a release whose plugin-harness-kind is v0.2026288.1214 or later" — is necessary and sufficient, not merely sufficient: every earlier harness in the org's whole tag history requires description/content, and the relaxation is introduced at v0.2026288.1214 (not before it). The claim needs no correction, and the operator's remedy is unaffected: v2026.300.0235 carries v0.2026288.1214 exactly.

Observation 2 — the pin271 / caseC cell is a stricter failure, not a different verdict

pin271 fails that cell on the stamp check (schema 2026.261.1747 is required), which its engine evaluates before the skill check — so it never reaches the routing-only verdict. That is a stricter engine failing earlier, and the cell is not cited as evidence for the routing-only claim: that claim is the pin278 → pin300 pair, where the same content is rejected and then accepted. For completeness, pin271's harness is v0.2026254.600 — pre-relaxation, and therefore a rejecter of the routing-only shape as well (it just never gets that far on that manifest).

Neither observation contradicts the shipped change, and no follow-up is owed on either.

What this does NOT close

opencharly/.github#259 stays open: its first arm is the org variable, which is the operator's.
vars.CHARLY_VERSION still names v2026.278.0912 (harness v0.2026272.143 → a rejecter of the
routing-only shape). The remedy, unchanged:

$ gh variable set CHARLY_VERSION --org opencharly --body v2026.300.0235 --visibility all

The same knob is also the org PR-validator's engine pin, so that one change moves the review
engine for every repo as well — which is why it is the operator's call and not a per-repo sweep.

The consumer leg this unblocks

The umbrella's dotgithub gitlink follows .github's main tip, so it advances to
ff88be2c39ec73d750db2d831ee32f2cd3e87d75 (the tag-on-merge commit for this PR) in its own
PR, gated with charly task verify on the committed tree.

Agent: charly

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

github-actions Bot pushed a commit to opencharly/opencharly that referenced this pull request Oct 27, 2026
…#733) (#734)

opencharly/.github#260 corrected a live, org-wide misstatement: the candy
validate gate's header named only the DISCHARGED activation blocker (the
retired `version:` stamp) and not the LIVE one (the routing-only `skill:`
shape), and the correction is enforced by four new rows in the org's own
`scripts/governance-claims.tsv`.

The umbrella's `dotgithub` gitlink recorded `.github`'s main at `d256175` — the
previous release's tip — so it did not carry the correction. This advances it
to `ff88be2c39`, the tip that does, through the sanctioned verb:
`charly task sync` wrote the pin, `charly task init` materialized the
checkout at it, and `charly task verify` judged the tree.

The currency question, asked with the one verb that answers it, named exactly
one pin of 411 as not current — `dotgithub`, `behind` — and none after the
wave. The advance is a fast-forward against its own remote (ahead=2,
behind=0), and the recorded pin is an ancestor of the new one in the pinned
checkout itself.

The producer merged and was tagged before this PR was opened (squash
784c530a, tag v2026.300.0808, validator PASS): producer-first order holds.

Closes #733

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant