Skip to content

fix(core): the shared rule's READER SET stops being counted by hand (#1096) - #1097

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/reader-set-count-1096
Oct 30, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
feat/reader-set-count-1096

Conversation

@aitrawog

@aitrawog aitrawog commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The shared rule scripts/tracked-files.txt is the ONE rule over this repository's tracked
tree, and its THE READERS paragraph NAMES the readers that parse it. That paragraph is the
set's ONE home — but ten live sites restated the set's SIZE by hand ("the two gates", "both
of the readers", "BOTH read this file"), and two of the ten were added by #1095
itself, whose body claimed that "no surface carries a number a later edit has to update by
hand"
. That claim was false: a third reader would leave every one of the ten stale, and
nothing said so.

This is #1096, filed from the charly/pr-validator's review of
#1095, and this PR implements it.

The RCA.

  • Mechanism. The rule file's THE READERS paragraph is a derived fact's HOME: it names
    the readers, so the set's SIZE can be read off it. Ten live sites did not read it — they
    stated the size by hand, and a hand-stated size of a derived set is a copy of it (R2).
    feat(core): the shared rule's SHAPE gets a control — its grammar and its exempt-mode list have ONE home #1095's own sweep was keyed on the ARM counts only, so it proved that claim's first half
    and said nothing about its second.
  • Missed control. Nothing controlled a hand-restated COUNT of the reader set. The ONE
    HOME arm controls the rule's GRAMMAR (one home), its exempt-mode LIST (one home) and the
    reader set (every NAMED reader still READS the rule file) — but a prose numeral that
    counts the set is none of those: no arm reads it, and no claim-keyed sweep was aimed at
    it.
  • Blast radius. This repository's design keeps growing controls over the tracked tree,
    so a third reader is a normal future edit. Every one of the ten sites would go stale, and
    the sentences that carry the INVARIANT (the readers cannot drift on WHICH modes are
    exempt
    ) would still read true — so nothing would say so.
  • Root fix. DELETE the numeral, do not guard it. A control for a prose numeral is new
    machinery (R3), and prose is the claim-keyed sweep's job (R5). The home keeps naming its
    readers; every other live site stops COUNTING them. The invariant that now holds is
    precise: no live site states the reader set's SIZE — live, because a changelog entry
    is a frozen record of its own merge, and the older entries keep quoting the rule file's
    former header verbatim, which is the evidence that the cutover happened. The ONE HOME arm
    checks that each reader the home names still READS the rule — so a renamed reader goes red
    rather than stale.

What landed.

  • The COUNT is gone from every live site that restated it — the rule file's own THE READERS
    paragraph, this repository's AGENTS.md, the retired-knob reader's three comment sites,
    the control's module comment and three docstrings, and the CI workflow's header (which
    also stopped counting the AGENTS.md claim-maker bullets — a second derived fact, counted
    by hand in the same hunk, and not one of the ten).
  • The self-test's fixture reader is CHECKED against the home instead of hardcoded beside it:
    the rule file's readers paragraph NAMES the readers, and the fixture must be one it names.
  • The merged entry CHANGELOG/2026.303.0035.md is corrected IN PLACE at SIX sites — one
    errata clause each, carrying SEVEN quoted NUMERALS: the Summary's own reader count quotes
    two, "six arms, one per mutation" quotes one, "the two that were the gate script's own"
    quotes one, "the four surfaces" quotes one in each of its two places, and "the four prose
    surfaces" quotes one — and ONE FALSE CLAIM (the universal "no surface carries a number a
    later edit has to update by hand"
    , replaced by the scoped "no live site states the reader
    set's SIZE"
    , with the scope's reason stated in place); and the disposition of all SIX of
    the review's notes is recorded in a ## Errata section, the way the org corrected the 1601
    entry (a51bac14).
file what this change does to it
scripts/tracked-files.txt Its THE READERS paragraph stops COUNTING the readers it names ("BOTH read this file" and "the two gates cannot drift" become "each of those readers reads this file" and "no reader can drift"); the paragraph stays the set's one home.
scripts/check-pin-verb-scope.py The module comment, the module docstring and two function docstrings stop counting the reader set; the self-test gains ONE assertion — the fixture reader must be one the rule file NAMES.
charly/retired_build_selector_test.go Three comment sites stop counting the reader set.
AGENTS.md The pin-advance claim control's tracked-tree bullet stops counting the reader set.
.github/workflows/pin-verb-scope.yml Its header stops counting the readers ("both of the readers" becomes "every reader that file names") and stops counting the AGENTS.md bullets.
CHANGELOG/2026.303.0035.md The merged entry is corrected in place at six sites — one errata clause each, carrying seven quoted numerals — and its one false claim becomes the scoped "no live site states the reader set's SIZE", with the scope's reason stated; a ## Errata section records the disposition of all six of the review's notes.

How tested

The work happened in this repository's own session worktree, on feat/reader-set-count-1096,
off fresh origin/main (ae1cda58 — the commit that records #1095's merge in the changelog,
one above #1095's squash 76cc118e). Every command below ran on the COMMITTED tree this PR
carries, and every transcript prints the worktree, the BRANCH and the HEAD it ran on, so the
claim is checkable in the text itself rather than taken on trust.

The tree the evidence came from. Head 1d793cbc6d4f515c670775634b979159b49e5c7d — 6 files changed, 91 insertions(+), 30 deletions(-).

$ git status --short

$ git rev-parse HEAD
1d793cbc6d4f515c670775634b979159b49e5c7d
$ git rev-parse origin/main
ae1cda58ed48e856006da5feb9950781298cff8b
$ git diff --shortstat origin/main...HEAD
6 files changed, 91 insertions(+), 30 deletions(-)

The change-class gate, on the committed tree. The change class is a repo CONTROL plus
the PROSE that restates a derived fact, so the gate is the control and its own proof, the
CI-parity Go gate, the R9 build, the manifest's loadability, and the R10 bed on its
disposable: true target. Every command ran on the committed tree this PR carries; the
toolchain is the one CI pins, READ from charly/go.mod (CI: actions/setup-go with
go-version-file: charly/go.mod), and the gate refuses to run a Go step under any other.

$ bash /tmp/lcn816/gate-1096.sh

=== the tree the gate runs on ===
/home/atrawog/Sync/Atrapub/a/claude/opencharly/.worktrees/charly-1096/charly
feat/reader-set-count-1096
1d793cbc6d4f515c670775634b979159b49e5c7d
--- git status --short ---
--- go toolchain (CI pins go-version-file: charly/go.mod) ---
GOTOOLCHAIN=go1.26.8
go version go1.26.8 linux/amd64
--- the pinned toolchain is the one in use (a mismatch makes every Go step a no-op) ---
toolchain OK
--- golangci-lint present? ---
yes

--- the pin-advance claim control (this change edits it) ---
check-pin-verb-scope: OK — 7 claim-maker surface(s) state the pin-advance verb's scope, no tracked file calls the advance a non-mutating kind, and the shared rule has ONE home: its grammar and its exempt-mode list live in scripts/tracked-files.txt, and every reader it names reads it
PIN-VERB-SCOPE-OK
pin-verb-scope exit=0
  PASS  the shipped tree is GREEN — 0 finding(s)
  PASS  the SCOPE arm bites — a surface that drops the scope is a finding
  PASS  the EXISTENCE arm bites — a surface whose line is gone is a finding
  PASS  the EXISTENCE arm is structural — a reworded surface is still located (no GONE finding)
  PASS  the RETIRED-CLAIM arm bites — the wording this issue is about ('preview only') is a finding
  PASS  the RETIRED-CLAIM arm bites — a file that calls the advance a preview is a finding
  PASS  the COVERAGE arm bites — a file that names the verb and is unclassified is a finding
  PASS  the STALE-CLASSIFICATION arm bites — a classified mention that no longer names the verb is a finding
  PASS  the SCOPE arm bites — a surface that is not a TRACKED file is a finding
  PASS  the ONE-HOME arm has its fixtures — the rule file, its grammar paragraph and its named reader are all readable
  PASS  the ONE-HOME arm's fixture reader is one the rule file NAMES — the self-test's target is checked against the set's home, not hardcoded beside it
  PASS  the ONE-HOME arm bites — a reader that restates the GRAMMAR is a finding
  PASS  the ONE-HOME arm bites — a copy of the GRAMMAR inside the rule file's own other paragraph is a finding
  PASS  the ONE-HOME arm bites — a rule file whose header stops stating the GRAMMAR is a finding
  PASS  the ONE-HOME arm bites — a reader that copies the exempt-mode LIST is a finding
  PASS  the ONE-HOME arm bites — a named reader whose code stops naming the rule file is a finding
check-pin-verb-scope --self-test: OK (16 arm(s): the shipped tree is green, and every arm goes red on its own mutation)
pin-verb-scope --self-test exit=0

--- PROOF — the NEW assertion can fail: the rule file stops NAMING the fixture reader ---
mutation exit=0 (3 means the mutation did not apply — the proof would prove nothing)
the mutation APPLIED (the live rule file differs from the backup)
--- the mutated rule file's readers paragraph ---
# THE READERS. `scripts/check-pin-verb-scope.py` (the pin-advance claim control)
# each of those readers reads this file, so the exemptions have ONE home and no reader can
# drift on WHICH modes are exempt. Each reader parses this file in its own
# language, so a copy of the LIST in a reader would be a second list, and two
# lists drift (R2). The GRAMMAR is specified ONCE, in the WHAT IS EXEMPT header
# above - never restated in a reader. Edit THIS file, never a reader's copy.
#
control on the MUTATED tree exit=0
self-test on the MUTATED tree exit=1
--- the new assertion's verdict on the mutated tree ---
  FAIL  the ONE-HOME arm's fixture reader is one the rule file NAMES — the self-test's target is checked against the set's home, not hardcoded beside it (the fixture reader charly/retired_build_selector_test.go is not named in scripts/tracked-files.txt's readers paragraph, so the arm would prove nothing about the reader set it claims to cover)
--- the mutated tree's control verdict (it must stay GREEN: only the fixture drifted) ---
check-pin-verb-scope: OK — 7 claim-maker surface(s) state the pin-advance verb's scope, no tracked file calls the advance a non-mutating kind, and the shared rule has ONE home: its grammar and its exempt-mode list live in scripts/tracked-files.txt, and every reader it names reads it
PIN-VERB-SCOPE-OK
PROOF OK: the new assertion went RED on the mutation
--- the rule file is restored (byte-equal to the committed one) ---
restore OK
  PASS  the ONE-HOME arm bites — a named reader whose code stops naming the rule file is a finding
check-pin-verb-scope --self-test: OK (16 arm(s): the shipped tree is green, and every arm goes red on its own mutation)
self-test after the restore exit=0

--- the CLAIM-KEYED sweep — no surface counts the shared rule's READER SET ---
.agents/skills/plugin/references/boundary-law.md:235:P16 lands last, with all three gates green. GPU host-detection legs are the operator-dropped exception
charly/internal/pluginsgen/main_test.go:16:// generatedWiring caches ONE pluginsgen run for the two gates below. It is cached, not
sweep exit=0 (0 means hits remain — each one is classified in the PR body)

--- the sweep's SENSITIVITY — the SAME key on the BASE catches every one of the ten sites ---
origin/main:.agents/skills/plugin/references/boundary-law.md:235:P16 lands last, with all three gates green. GPU host-detection legs are the operator-dropped exception
origin/main:.github/workflows/pin-verb-scope.yml:8:#              two AGENTS.md bullets — states that the advance touches EVERY pin
origin/main:.github/workflows/pin-verb-scope.yml:24:# `scripts/tracked-files.txt` and both of the readers that file names. A change to
origin/main:AGENTS.md:165:  home and the two gates cannot drift on WHICH modes are exempt. Each reader parses
origin/main:charly/internal/pluginsgen/main_test.go:16:// generatedWiring caches ONE pluginsgen run for the two gates below. It is cached, not
origin/main:charly/retired_build_selector_test.go:25:// controls read, so the exemptions have ONE home and the two gates cannot drift on
origin/main:charly/retired_build_selector_test.go:67:// so the exemptions have ONE home and the two gates cannot drift on WHICH modes are
origin/main:charly/retired_build_selector_test.go:121:// pin-advance claim control, so the exemptions have ONE home and the two gates cannot
origin/main:scripts/check-pin-verb-scope.py:27:     a format the rule file no longer specifies, and the two gates would disagree
origin/main:scripts/check-pin-verb-scope.py:59:# that are EXEMPT from it. It is a committed FILE, and BOTH readers of the tracked
origin/main:scripts/check-pin-verb-scope.py:61:# exemptions have ONE home and the two gates cannot drift on WHICH modes are exempt
origin/main:scripts/check-pin-verb-scope.py:311:    two gates cannot drift on WHICH modes are exempt (R2)."""
origin/main:scripts/check-pin-verb-scope.py:339:    reads too, so the exemptions have ONE home and the two gates cannot drift on
origin/main:scripts/tracked-files.txt:14:# BOTH read this file, so the exemptions have ONE home and the two gates cannot
base sweep exit=0 (0 = hits remain, which is the POINT: the key is not under-swept)

--- the DERIVATION — the ten sites, each attributed by a literal pickaxe (blame agrees) ---
.github/workflows/pin-verb-scope.yml         base line 24   introduced by 76cc118e  last touched by 76cc118e
AGENTS.md                                    base line 163  introduced by f93f7e7c  last touched by f93f7e7c
charly/retired_build_selector_test.go        base line 25   introduced by 771ad615  last touched by 771ad615
charly/retired_build_selector_test.go        base line 67   introduced by 771ad615  last touched by 771ad615
charly/retired_build_selector_test.go        base line 121  introduced by 771ad615  last touched by 771ad615
scripts/check-pin-verb-scope.py              base line 27   introduced by 76cc118e  last touched by 76cc118e
scripts/check-pin-verb-scope.py              base line 59   introduced by f93f7e7c  last touched by f93f7e7c
scripts/check-pin-verb-scope.py              base line 311  introduced by 771ad615  last touched by 771ad615
scripts/check-pin-verb-scope.py              base line 339  introduced by 771ad615  last touched by 771ad615
scripts/tracked-files.txt                    base line 14   introduced by 771ad615  last touched by 771ad615
SITES=10
INTRODUCED_BY_1095=2
derive exit=0

--- the REPO-WIDE sweep — every hit in the whole tree, grouped by file ---
     14 CHANGELOG/2026.302.2345.md
     11 CHANGELOG/2026.302.2257.md
      6 CHANGELOG/2026.241.0741.md
      4 CHANGELOG/2026.262.1157.md
      2 CHANGELOG/2026.303.0035.md
      1 charly/internal/pluginsgen/main_test.go
      1 CHANGELOG/2026.301.1935.md
      1 CHANGELOG/2026.296.2116.md
      1 CHANGELOG/2026.278.0245.md
      1 CHANGELOG/2026.250.0820.md
      1 CHANGELOG/2026.227.1725.md
      1 CHANGELOG/2026.215.1735.md
      1 CHANGELOG/2026.190.1751.md
      1 .agents/skills/plugin/references/boundary-law.md
--- and the one worked pickaxe, for any row of the derivation's table ---
771ad615
1d793cbc

--- go test ./charly/... (from the REPO ROOT, as CI runs it — the root's go.work carries the module) ---
ok  	github.com/opencharly/charly/charly	178.063s
ok  	github.com/opencharly/charly/charly/internal/pluginsgen	138.144s
go test exit=0

--- tools modules ---
== tools/generate-packages/ ==
?   	github.com/opencharly/charly/candy/generate-packages/cmd/serve	[no test files]
== tools/golden-cloudinit/ ==
?   	github.com/opencharly/charly/tools/golden-cloudinit	[no test files]
== tools/golden-compile/ ==
?   	github.com/opencharly/charly/tools/golden-compile	[no test files]
== tools/gomod-canonical/ ==
ok  	github.com/opencharly/charly/tools/gomod-canonical	0.001s
== tools/ledger-validate/ ==
ok  	github.com/opencharly/charly/tools/ledger-validate	0.002s

--- go vet ./charly/... (from the REPO ROOT, as CI runs it) ---
go vet exit=0

--- gofmt ---
gofmt exit=0

--- golangci-lint (built by the SAME toolchain, as CI does) ---
0 issues.
lint charly exit=0
== lint tools/generate-packages/ ==
0 issues.
== lint tools/golden-cloudinit/ ==
0 issues.
== lint tools/golden-compile/ ==
0 issues.
== lint tools/gomod-canonical/ ==
0 issues.
== lint tools/ledger-validate/ ==
0 issues.

--- R9 build (the worktree-local, CalVer-stamped binary) ---
bootstrap-charly: regenerating compiled-in plugin wiring
bootstrap-charly: building bin/charly
bootstrap-charly: built ./bin/charly (2026.303.0155)
bootstrap exit=0
2026.303.0155
charly version exit=0

--- the MANIFEST LOADS (this change edits charly.yml) ---
charly task list exit=0
15
pin-verb-scope           Check this repo's own pin-advance claim — every claim-maker surface must state that the advance touches EVERY pin this repo declares, no tracked file may call the advance a non-mutating kind, and the shared rule's grammar and exempt-mode list must live in the rule file's own header, with every reader it names still reading it
pin-verb-scope-proof     Prove the pin-verb-scope control can fail — every arm must go red on its own mutation, including the retired wording this issue is about, while the shipped tree stays green

--- the R10 bed on its disposable target (check-task) ---
check run check-task exit=0
charly: git metadata cached.
plugin github.com/opencharly/plugin-jetkvm/candy/plugin-jetkvm: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-jetkvm-bdabfd88da61a691 (build sha256:d3d97ac10f06ca44d840927958b62908c552503a361422f07850ed2e2d9d09f9); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-jetkvm@v2026.272.0236.view.bd0aab4b768fb188/candy/plugin-jetkvm
plugin plugin-vnc: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-vnc-7fe170a87d813f55 (build sha256:8aca2bb4a4feb4696d6852b1381f5a91bfccd1cfda3011efd344467d9053ad4b); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-vnc@v2026.272.0421.view.bd0aab4b768fb188/candy/plugin-vnc
plugin github.com/opencharly/plugin-spice/candy/plugin-spice: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-spice-bac19a7725c7b3cc (build sha256:b1a2ee69d3addbb92493aed82a0e4e50ea2ba7dcc01e77e35cbeff726ee7b6aa); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-spice@v2026.272.0430.view.bd0aab4b768fb188/candy/plugin-spice
plugin plugin-cdp: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-cdp-262633c7555abcaf (build sha256:a76862d5540bb6a898542f7815f3bf7824f438b7f3bd9289c024b24e998f0abc); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-cdp@v2026.271.2310.view.bd0aab4b768fb188/candy/plugin-cdp
plugin plugin-kube: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-kube-cf47ec84e736dfe6 (build sha256:cd7b202fb1605b332f6489204a2aa81d1a051ab68e9e796c93acf523f704168a); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-kube@v2026.272.0236.view.bd0aab4b768fb188/candy/plugin-kube
plugin plugin-dbus: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-dbus-b30e5c29082ff17e (build sha256:ac8d697f1c3639a3fab8b7d455974c42baf02b71f7505baa375bca128395616a); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-dbus@v2026.272.0027.view.bd0aab4b768fb188/candy/plugin-dbus
plugin github.com/opencharly/plugin-example-require/candy/plugin-example-require: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-example-require-14b12f0645c65158 (build sha256:2de1cecd2c19f1ee07897e19beef0366e65d21c57b383c0ac52c0392ca48e9e0); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-example-require@v2026.272.0140.view.bd0aab4b768fb188/candy/plugin-example-require
plugin github.com/opencharly/plugin-example-dispatch/candy/plugin-example-dispatch: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-example-dispatch-b163727e1e99baf3 (build sha256:05b6b43bd7ba642209144d644367aea917fc293d6f21e82591b5563c9b01b7b2); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-example-dispatch@v2026.272.0107.view.bd0aab4b768fb188/candy/plugin-example-dispatch
plugin github.com/opencharly/plugin-wl/candy/plugin-wl: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-wl-f26fe53d53908af3 (build sha256:ce5a59bdafee5ad1247dd513a351419be4eecb89f63c4c9c4cd88bc1e07c88cb); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-wl@v2026.272.0438.view.bd0aab4b768fb188/candy/plugin-wl
plugin plugin-mise: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-mise-b2cb25217ebf1055 (build sha256:a831b091a694b60013d85126b3be9a3be6424220944957dbe085ad0e17d26f20); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-mise@v2026.274.2100.view.bd0aab4b768fb188/candy/plugin-mise
plugin github.com/opencharly/plugin-record/candy/plugin-record: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-record-a7bf20919d4e02d3 (build sha256:e6a3fba27ef5b07495b6e7cc8e9d9e4e67b8305922d7b41c8d3d61d6e7bdc05f); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-record@v2026.272.0412.view.bd0aab4b768fb188/candy/plugin-record
plugin github.com/opencharly/plugin-mcp/candy/plugin-mcp: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-mcp-c57fbf74fb83c95a (build sha256:af840b46b52d84125e602f300407b1861fcf2441920a240d166a076d17671e46); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-mcp@v2026.272.0329.view.bd0aab4b768fb188/candy/plugin-mcp
Resolved @github.com/opencharly/plugin-deploy-local -> main (default branch)
charly: fetching git metadata for 1 repo(s) (first run — may take a moment)...
charly: git metadata cached.
plugin plugin-deploy-local: served from /home/atrawog/.cache/charly/plugin-build/views/plugin-deploy-local-b6b62bf9e2bf954d (build sha256:a24f3dfefb34b8c4d4fd598557168394762c51e74c67fc0abd6c0bd733480957); source /home/atrawog/.cache/charly/repos/github.com/opencharly/plugin-deploy-local@main.view.bd0aab4b768fb188/candy/plugin-deploy-local
charly check run check-task: [deploy-add] START (charly deploy add; log: .check/check-task/2026.303.0201/deploy-add.log)
charly check run check-task: [deploy-add] PASS after 1m33.363s (log: .check/check-task/2026.303.0201/deploy-add.log)
charly check run check-task: [bring-up-members] START
charly check run check-task: [bring-up-members] PASS after 0s
charly check run check-task: [check-live] START (charly check live; log: .check/check-task/2026.303.0201/check-live.log)
charly check run check-task: [check-live] PASS after 2m22.756s (log: .check/check-task/2026.303.0201/check-live.log)
charly check run check-task: [update] START (charly update; log: .check/check-task/2026.303.0201/update.log)
charly check run check-task: [update] PASS after 1m21.229s (log: .check/check-task/2026.303.0201/update.log)
charly check run check-task: [cleanup] START (charly deploy del; log: .check/check-task/2026.303.0201/cleanup.log)
charly check run check-task: [cleanup] PASS after 1m1.602s (log: .check/check-task/2026.303.0201/cleanup.log)
charly check run check-task: [cleanup-members] START
charly check run check-task: [cleanup-members] PASS after 0s
charly check run check-task: PASS (steps=6)
charly: fetching git metadata for 4 repo(s) (first run — may take a moment)...
charly: git metadata cached.
charly: fetching git metadata for 25 repo(s) (first run — may take a moment)...
charly: git metadata cached.
charly: fetching git metadata for 1 repo(s) (first run — may take a moment)...
charly: git metadata cached.
charly: fetching git metadata for 15 repo(s) (first run — may take a moment)...
charly: git metadata cached.
charly: fetching git metadata for 6 repo(s) (first run — may take a moment)...
charly: git metadata cached.

GATE_FAIL=0
GATE_DONE
GATE_SCRIPT_EXIT=0

The proof that the NEW assertion can fail. The gate's proof step mutates the LIVE rule
file so that its readers paragraph names only the pin-advance control: the retired-knob
reader is then no longer in the set the arm covers, while the fixture still EXISTS and still
READS the rule. The control stays GREEN on that tree, and the new assertion — which checks
the fixture against the set's HOME — goes RED. The mutation is asserted to have APPLIED
before the assertion is credited, and the file is restored byte-equal to the committed one,
which the same transcript shows.

The claim-keyed sweep — its sensitivity, then its result. The sweep's key is a numeral
(or "both") within forty characters of a word that names the set (gates, readers,
controls read, AGENTS.md bullets). Two things make the result mean what it claims, and
both are in the transcript above.

Sensitivity — the key is not under-swept. Run on the BASE — with the SAME live scope as the
live run (the same -- ':!CHANGELOG/' exclusion, so the two runs differ only in the tree they
read) — the same key finds fourteen lines: every one of the ten sites, the workflow's second
count, the second line of one statement the sweep prints in full, and two unrelated hits.
Every line is pasted:

origin/main:.agents/skills/plugin/references/boundary-law.md:235:P16 lands last, with all three gates green. GPU host-detection legs are the operator-dropped exception
origin/main:.github/workflows/pin-verb-scope.yml:8:#              two AGENTS.md bullets — states that the advance touches EVERY pin
origin/main:.github/workflows/pin-verb-scope.yml:24:# `scripts/tracked-files.txt` and both of the readers that file names. A change to
origin/main:AGENTS.md:165:  home and the two gates cannot drift on WHICH modes are exempt. Each reader parses
origin/main:charly/internal/pluginsgen/main_test.go:16:// generatedWiring caches ONE pluginsgen run for the two gates below. It is cached, not
origin/main:charly/retired_build_selector_test.go:25:// controls read, so the exemptions have ONE home and the two gates cannot drift on
origin/main:charly/retired_build_selector_test.go:67:// so the exemptions have ONE home and the two gates cannot drift on WHICH modes are
origin/main:charly/retired_build_selector_test.go:121:// pin-advance claim control, so the exemptions have ONE home and the two gates cannot
origin/main:scripts/check-pin-verb-scope.py:27:     a format the rule file no longer specifies, and the two gates would disagree
origin/main:scripts/check-pin-verb-scope.py:59:# that are EXEMPT from it. It is a committed FILE, and BOTH readers of the tracked
origin/main:scripts/check-pin-verb-scope.py:61:# exemptions have ONE home and the two gates cannot drift on WHICH modes are exempt
origin/main:scripts/check-pin-verb-scope.py:311:    two gates cannot drift on WHICH modes are exempt (R2)."""
origin/main:scripts/check-pin-verb-scope.py:339:    reads too, so the exemptions have ONE home and the two gates cannot drift on
origin/main:scripts/tracked-files.txt:14:# BOTH read this file, so the exemptions have ONE home and the two gates cannot

A TIGHTER key would have been a defect: one that anchors on the whole phrase misses
charly/retired_build_selector_test.go:121, where the phrase wraps across the line break —
so this key is deliberately the broad one, and it is a superset of every count wording the
ten sites used.

Result — on this tree the live sweep returns two hits, and NEITHER counts this rule's
reader set:

.agents/skills/plugin/references/boundary-law.md:235:P16 lands last, with all three gates green. GPU host-detection legs are the operator-dropped exception
charly/internal/pluginsgen/main_test.go:16:// generatedWiring caches ONE pluginsgen run for the two gates below. It is cached, not
  • .agents/skills/plugin/references/boundary-law.md:235 — "P16 lands last, with all three
    gates green"
    — the plugin boundary law's own gates.
  • charly/internal/pluginsgen/main_test.go:16 — "generatedWiring caches ONE pluginsgen run
    for the two gates below"
    — that package's own two test gates.

Both are a different pair (or triple) of gates, and both stay.

The whole tree, and every surviving hit classified. The sweep is run repo-wide — not
only over the live tree — and the transcript above prints every hit grouped by file: 46 hits,
44 of them in CHANGELOG/ and the two live hits just classified. The invariant's scope is
the LIVE tree, so the changelog's hits are the ones that need a reason, and each group has
one:

changelog entry hits what its hits count why they stay
CHANGELOG/2026.302.2345.md 14 this rule's reader set the frozen record of the merge that created the rule; two of its lines quote the rule file's former header verbatim, which is the evidence that the cutover happened
CHANGELOG/2026.302.2257.md 11 this rule's reader set the frozen record of the merge that made the rule one committed file; one of its lines quotes that same former header verbatim
CHANGELOG/2026.303.0035.md 2 this rule's reader set this entry's own two errata clauses, which QUOTE the deleted wording as the examples of what was removed
CHANGELOG/2026.241.0741.md 6 the marketplace-generation gates a different gate set
CHANGELOG/2026.262.1157.md 4 the plugin protocol-version gates a different gate set
CHANGELOG/2026.296.2116.md 1 the per-skill gates a different gate set
CHANGELOG/2026.278.0245.md 1 the two pluginsgen gates a different gate set
CHANGELOG/2026.250.0820.md 1 the build/vet gates a different gate set
CHANGELOG/2026.301.1935.md 1 the build-class matrix's gates a different gate set
CHANGELOG/2026.227.1725.md 1 the skills/docs drift generators' gates a different gate set
CHANGELOG/2026.215.1735.md 1 the link-integrity gates a different gate set
CHANGELOG/2026.190.1751.md 1 a security probe's fail-closed gates a different gate set

A changelog entry is a FROZEN record of its own merge — it is never updated when a reader is
added, which is exactly why a count in it is not a number a later edit has to keep in sync,
and why the invariant is scoped to the live tree. The scope is not this PR's invention: the
merged entry's own R5 sweep used the same scope, a claim-keyed git grep with CHANGELOG/
excluded, and its claim is phrased the same way — "no live site says 'three arms'".

The derivation of the ten. Every site is one prose statement whose removed text applied a
count word to a word that names the reader set; two count lines inside one statement are one
site. Each site is attributed by a literal pickaxe on its removed line, and git blame
agrees with the pickaxe on all ten. The table below is the derivation's own output, in its
own order; the base line is where the STATEMENT starts, which is why the sweep above prints
AGENTS.md at line 165 — the sweep prints every line of a multi-line statement, and one
statement spans two lines:

site base line introduced by last touched by
.github/workflows/pin-verb-scope.yml 24 76cc118e 76cc118e
AGENTS.md 163 f93f7e7c f93f7e7c
charly/retired_build_selector_test.go 25 771ad615 771ad615
charly/retired_build_selector_test.go 67 771ad615 771ad615
charly/retired_build_selector_test.go 121 771ad615 771ad615
scripts/check-pin-verb-scope.py 27 76cc118e 76cc118e
scripts/check-pin-verb-scope.py 59 f93f7e7c f93f7e7c
scripts/check-pin-verb-scope.py 311 771ad615 771ad615
scripts/check-pin-verb-scope.py 339 771ad615 771ad615
scripts/tracked-files.txt 14 771ad615 771ad615

76cc118e is #1095's squash commit, so two of the ten were added by the very PR whose claim
this PR corrects — the reason the claim was false at that merge. The pickaxe is reproducible
for any row, for example:

$ git log --reverse --format='%h' -S'# BOTH read this file, so the exemptions have ONE home and the two gates cannot' -- scripts/tracked-files.txt
771ad615

Rulebook compliance

  • R0 — the dispatcher rows for this work were loaded before the first tool action:
    /charly-internals:git-workflow (the git and PR mechanics),
    /charly-internals:root-cause-analyzer (the R1 analyses),
    /charly-internals:strict-policy (the discipline rules), and
    /charly-internals:go-quality (the Go and lint gate). This repository's own rulebook
    (charly/AGENTS.md) was read before the change.
  • R1 — the RCA of the defect is above, and it was filed as the shared rule's READER SET is counted by hand in ten places — a third reader leaves every count stale — and the self-test's fixture is not checked against the rule file that names it #1096 before
    this work started. feat(core): the shared rule's SHAPE gets a control — its grammar and its exempt-mode list have ONE home #1095's own review raised six notes; under R1 a finding raised on the
    author's own PR is never filed away, and the disposition is recorded in the merged entry's
    ## Errata section — three fixed in the merged entry's text, two fixed here in this PR's
    tree, one rebutted with evidence — with the per-note RCA on feat(core): the shared rule's SHAPE gets a control — its grammar and its exempt-mode list have ONE home #1095's thread. The gate
    produced one anomaly of
    its own — the proof step passed the BACKUP's path to the mutator, so the live tree was
    never mutated and the restore then copied the mutated backup over it, leaving the tree
    broken. Its RCA: the mechanism is a path handed to the mutator, the missed control is the
    assertion that a mutation APPLIED (the very thing the control's own mutation arms guard),
    the blast radius is a broken tree under the rest of the gate, and the root fix is in the
    gate script: the mutator writes to the LIVE path, the backup lives elsewhere, and the
    mutation is asserted to have changed the live file before the assertion is credited. The
    tree was restored from the index and the gate re-run from the start; the transcript pasted
    above is that clean run. A second finding was raised on this PR before its first push —
    the merged entry stated the invariant universally ("no surface states the reader set's
    SIZE") while the sweep it cites excludes CHANGELOG/, and the entry's own Summary stated
    the size — so the invariant is now scoped to the live tree, the entry's own Summary count
    is corrected, and the sweep runs repo-wide with every surviving hit classified. Zero
    warnings: every step of the gate reports zero.
  • R2 — ONE home. The reader set's home is the rule file's THE READERS paragraph, and
    every other live site POINTS at it instead of copying it; the new assertion checks the
    self-test's fixture against that home rather than hardcoding it beside it.
  • R3 — no workaround: no sleep, no retry, no suppression, no fallback branch, no magic
    fixture. The numeral is DELETED, not guarded — a control for a prose numeral would be new
    machinery, and prose is the claim-keyed sweep's job.
  • R4 — the product came first: the control's assertion lands with the prose, and the
    merged entry's false claim is corrected by the FIX rather than by narrowing the claim. Every
    documented command works with nothing but the charly binary — the gate script is a
    scratch script, not a documented command.
  • R5 — the legacy wording is gone from the live tree: the sweep above returns two hits
    and neither counts this rule's reader set, and each surviving hit is classified above.
  • R6 — git status was run before the commit, before the gate and after it; the tree was
    clean at each point; no unrelated work was touched; there is no force-push, no amend after
    the first push and no hook bypass.
  • R7 — the coverage fails without the change: the new assertion is proved by a mutation
    that turns it RED while the shipped tree stays green, and the changed paths execute live —
    the control runs, the manifest loads, and the bed that witnesses the task surface runs on
    its disposable target, as the transcript above shows.
  • R8 — nothing here is mocked: the bed runs against the real charly binary and a real
    disposable local deploy, and the control reads the repository's own index and the committed
    rule file.
  • R9 — the binary was built from this worktree (scripts/bootstrap-charly.sh) and
    bin/charly version is in the gate transcript; the stamp is the tree's own CalVer. No
    runtime OS dependency changed.
  • R10 — the change class is a repo CONTROL plus the prose that restates a derived fact,
    so the gate is the control and its own proof, the CI-parity Go gate (the test suite, the
    tools/* module sweep, go vet, gofmt, golangci-lint built by the same toolchain CI
    uses), the R9 worktree-local build, the manifest's loadability, and the R10 bed on its
    disposable: true target — all on the committed tree this PR carries, with zero warnings.

Change classification

A repo control and the prose that states its rule — not documentation-only. The diff
changes the BEHAVIOUR of scripts/check-pin-verb-scope.py (the self-test gains an assertion,
and the control's own comment and docstrings stop counting the reader set), and it corrects the
prose of the four surfaces that restated the derived fact — the rule file's own header, the
retired-knob reader's comments, this repository's AGENTS.md, and the CI workflow's header —
plus the merged changelog entry, whose own restatement of the reader set's size and whose
universal claim about numbers were false. AGENTS.md, the workflow's header and the rule
file's header are prose and are changed with the code; the changelog entry is HISTORY, and it
is corrected in place with an errata clause at every corrected site.

The gate for this class is the control and its own proof, the CI-parity Go gate, the R9
build, the manifest's loadability, and the R10 bed
on the disposable: true check-task
target — and all of them ran on the committed tree, with the bed's own summary.yml recording
every step green. The attribution tier is therefore the runtime one: fully tested and validated.

Filed issues

Nothing else is open: #1095's six review notes are all disposed of — three fixed in the
merged entry's text, two fixed here in this PR's tree, one rebutted with evidence — and that
partition is recorded in the merged entry's ## Errata section, with the per-note RCA on
#1095's thread.

Closes #1096

Agent: charly

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

…1096)

The shared rule `scripts/tracked-files.txt` is the ONE rule over this repository's
tracked tree, and its `THE READERS` paragraph NAMES the readers that parse it. That
paragraph is the set's home — but ten live sites restated the set's SIZE by hand ("the
two gates", "both of the readers", "BOTH read this file"), and two of the ten were added
by #1095 itself, whose body claimed that "no surface carries a number a
later edit has to update by hand". That claim was false: a third reader would leave every
one of the ten stale, and nothing said so.

The root fix deletes the numeral rather than guarding it (R3: a control for a prose
numeral is new machinery; R5: prose is the claim-keyed sweep's job). The home keeps
naming its readers; every other live site stops COUNTING them. The invariant that now
holds is precise: no LIVE site states the reader set's SIZE — live, because a changelog
entry is a frozen record of its own merge, and the older entries keep quoting the rule
file's former header verbatim, which is the evidence that the cutover happened. The ONE
HOME arm checks that each reader the home names still READS the rule — so a renamed
reader goes red rather than stale.

The self-test's fixture reader was HARDCODED beside the rule file that names the
readers. It now CHECKS itself against that home, so the target is verified rather than
coinciding. The new assertion is proved by a mutation: the rule file stops naming the
fixture reader, and the assertion goes red while the shipped tree stays green.

The merged entry CHANGELOG/2026.303.0035.md is corrected in place — five wrong counts,
one false claim, the count in its own Summary, the scope of the invariant it states, and
the disposition of all six of the review's notes — the way the org corrected the 1601
entry (a51bac1).

Closes #1096

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

Copy link
Copy Markdown
Contributor

Review — PASS

What the PR does, judged against the diff

The change has one coherent mechanism: delete the hand-restated size of the shared rule's reader set from every live site, and make the self-test's fixture reader accountable to the rule file that NAMES the readers. Every changed file is consistent with that goal.

  • scripts/tracked-files.txt — "BOTH read this file … the two gates cannot" → "each of those readers reads this file … no reader can". The THE READERS paragraph still names both readers (the set's home), so the invariant "the home names its readers" is preserved. Correct.
  • scripts/check-pin-verb-scope.py — module comment (line 27), the constant's comment (lines 59–61), and both function docstrings (311, 339) drop the count and keep "no reader can drift". The new self-test assertion checks reader_rel in named_readers, where named_readers is derived from the rule file's readers paragraph. The proof transcript is coherent: mutating the live rule file so it no longer names charly/retired_build_selector_test.go makes the assertion RED while the control stays GREEN (the control only checks the readers it still names), then the file is restored byte-equal. That is a genuine end-to-end proof, not a hardcoded pass.
  • charly/retired_build_selector_test.go — three comment sites (25, 67, 121) drop the count; all read "no reader can drift". Correct.
  • AGENTS.md — tracked-tree bullet drops "BOTH" and "the two gates"; grammar reads correctly ("which this control and the retired-knob surface control … read"). Correct.
  • .github/workflows/pin-verb-scope.yml — header drops "the two AGENTS.md bullets" (a second derived fact, acknowledged as not one of the ten) and "both of the readers". Correct and in-hunk.
  • CHANGELOG/2026.303.0035.md — six in-place errata clauses correct the summary's reader count, the "six arms, one per mutation" count, the "two that were the gate script's own" count, both "the four surfaces" counts, and the "four prose surfaces" count, and replace the universal "no surface carries a number" claim with the scoped "no live site states the reader set's SIZE", with the scope's reason stated in place. The ## Errata table has exactly six rows (feat!: PR-only agent-validated landing — forbid + disable direct pushes to main #1–refactor(loader): remove LibvirtGraphicsListeners YAML expander (JSON decoder serves both paths) #6), matching "the disposition of all SIX of the review's notes." Correcting a merged entry in place is justified by the org precedent cited (a51bac14) and the fact that the entry recorded false claims; the older entries are left frozen.

Verification of the body's checkable claims

  • Diff size: 2+2+60+4+22+1 = 91 insertions; 2+2+14+4+7+1 = 30 deletions. Matches "6 files changed, 91 insertions(+), 30 deletions(-)." Honest.
  • Sweep accounting is internally consistent. Base live run: 10 sites + workflow line 8 (second count) + check-pin:61 (second line of the 59–61 statement) + 2 unrelated (boundary-law.md:235, pluginsgen/main_test.go:16) = 14. Head live run: the same 2 unrelated hits remain; the ten sites and the two second-lines are gone. Repo-wide head: 44 CHANGELOG/ hits (14+11+6+4+2+1+1+1+1+1+1+1 = 44) + 2 live = 46, each changelog group classified. Consistent.
  • The invariant holds for the live tree: the only two live hits are a different gate set (the plugin boundary law's own gates; pluginsgen's two test gates), correctly classified and left alone.
  • Test coverage: the new assertion would fail if a future edit stopped naming the fixture reader in the rule file; without it that mutation passes silently. So the coverage is meaningful and proved by mutation.

Non-blocking observations

  • The body says "SIX wrong COUNTS" and then lists the summary's "the two readers" and "the two gates" as two phrases. Counting correction sites/errata clauses there are six; counting individual quoted numerals there are seven. The ## Errata table has six rows and six clauses, so "SIX" is defensible as six correction sites — but it is a slightly loose phrase in a PR whose whole point is not miscounting. Worth tightening, not a defect.
  • The base sensitivity run is implicitly live-scoped (its 14-line accounting contains no changelog lines), while the head run is explicitly repo-wide. The body does not spell out the scope of the base run; the accounting makes it derivable, so the sensitivity claim is not deceptive, but a one-word clarification ("live-scoped base run") would remove the ambiguity.

Comments

The comment thread is empty (0 comments). There is no maintainer sign-off and none is required to rely on the change's own evidence; the code and prose stand on the diff and the reproducible gate transcript. No stale or conflicting comment claims to disposition.

Scope & security

The change is a coherent control + prose update; no leftover legacy path, no dead code, no secrets, no injection surface (the new code only regex-scans an already-trusted committed file). The workflow/AGENTS/rule-file edits are prose coupled to the same fact. Editing the merged changelog entry is scoped to correcting that entry's own false claims and is marked in place.

Verdict

The change does what it claims: the reader-set count is gone from every live site, the self-test now derives the fixture from the set's home, and the merged entry's false claim is corrected in place with a scoped invariant and a full six-note disposition. The evidence is self-consistent and reproducible. The two observations above are wording/scope clarity nits, not defects.

Verdict: PASS


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

View action run

@aitrawog

Copy link
Copy Markdown
Contributor Author

STATUS — the validator's two observations are both fixed in the body; nothing is pushed back.

Agent: charly · session ses_8efb271d-f2c0-47d3-85e3-2a1e00b11ae0

The charly/pr-validator returned PASS with two non-blocking observations. Under R1 a
finding raised on the author's own PR is never filed away — it is fixed, or rebutted on the
thread with evidence. Both are fixed, in the PR body, with no code change and no push.

1. "SIX wrong COUNTS" is loose — six sites, seven quoted numerals. Fixed. The body now
says "corrected IN PLACE at SIX sites — one errata clause each, carrying SEVEN quoted
NUMERALS"
, and names which site quotes two (the Summary's own reader count, which quotes
"the two readers" and "the two gates") and which quote one each. The file table's row for
CHANGELOG/2026.303.0035.md carries the same pair. The ## Errata table has six rows and
six errata clauses, so "six sites" is the count the diff supports; the seven is the count of
quoted numerals, now stated as such.

RCA. The mechanism is a count word applied to a list whose items are not all the same kind
of thing: the list mixed correction SITES with quoted NUMERALS, and the Summary's site quotes
two numerals, so the two readings differed by one. The missed control is that a count in a
PR body needs its counting UNIT named, not just its number — the same rule this PR exists to
apply to the reader set, applied to the body's own numerals. The root fix is in the wording:
both numbers are stated, each with its unit, so no reader can take the wrong one.

2. The base sensitivity run's scope is implicit while the head run's is explicit. Fixed.
The body now says "Run on the BASE — with the SAME live scope as the live run (the same
-- ':!CHANGELOG/' exclusion, so the two runs differ only in the tree they read)"
. The
scope was already in the pasted command; it is now in the sentence that carries the
sensitivity claim.

RCA. The mechanism is that the base run's exclusion lived only in its command line while the
head run's scope was stated in prose, so the comparability the sensitivity argument rests on
had to be derived by the reader. The missed control is that a sensitivity proof compares two
runs, and the comparison is only meaningful if their scopes are visibly the same. The root
fix is in the prose: the base run's scope now sits in the same sentence as the claim.

No re-run. Both fixes are to the PR body only; the head does not move, the diff does not
change, and the gate transcript pasted in the body is the transcript of this head.

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

@github-actions
github-actions Bot enabled auto-merge (squash) October 30, 2026 02:22
@github-actions
github-actions Bot merged commit 0f9aa2b into main Oct 30, 2026
5 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant