Skip to content

ci: check in the main-branch protection ruleset - #1

Merged
wenzowski merged 1 commit into
mainfrom
chore/branch-protection
Aug 5, 2026
Merged

wenzowski merged 1 commit into
mainfrom
chore/branch-protection

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Documents and version-controls the trunk-based protection now active on main.

Protection applied (GitHub ruleset protect-main, id 20481001):

  • Pull request required — no direct pushes to main
  • Required status checks, strict/up-to-date: test (ubuntu-latest), test (macos-latest), cargo-deny
  • Linear history (merge commits disabled repo-wide; squash/rebase only)
  • Force-push and branch deletion blocked
  • Conversation resolution required before merge

Merge settings: auto-merge + delete-branch-on-merge enabled to keep branches short-lived, per trunk-based development.

This PR also exercises the flow: it is the first change that had to go through a PR because a direct push to main is now rejected.

Documents the trunk-based protection applied to main: PR required, CI must be
green and up to date, linear history, no force-push or deletion.
@wenzowski
wenzowski enabled auto-merge (squash) August 5, 2026 22:57
@wenzowski
wenzowski merged commit 9da8195 into main Aug 5, 2026
3 checks passed
@wenzowski
wenzowski deleted the chore/branch-protection branch August 5, 2026 22:57
wenzowski pushed a commit that referenced this pull request Aug 7, 2026
Every release so far shipped zero assets. `mise run release` is
release-plz only — tag plus a release entry, never a build — and
cross-check runs `cargo check` (codegen to metadata, no linking), so a
green `cross` proved compilation without ever producing a runnable
binary. There was nothing to install, which blocks dogfooding Batten as
consumer #1 and left house-style §12 prose with no mechanism.

mise-tasks/dist builds --profile dist (the profile Cargo.toml already
declared and nothing used) for one target and stages a named archive.
The release workflow calls that same task per matrix leg, so the archive
CI uploads is the one a maintainer reproduces locally.

The archive name is a contract, not a convenience: CLOUD-65's binstall
path resolves assets by name, so naming and per-platform format are
pinned in tests/dist.bats rather than left to whatever the last release
emitted.

Provenance binds to the binary, not the archive, so repackaging cannot
launder it. mise-action runs with cache: false — a poisoned cache entry
would land inside a signed artifact and be faithfully attested.

Refs CLOUD-173.
wenzowski pushed a commit that referenced this pull request Aug 7, 2026
Onboarding never ran: it is skipped when memories exist, and the curated
memory set pre-dated Serena's first successful connection, so the
existence check has always answered "already performed" for content that
was authored by hand rather than by onboarding. Run now in a fresh session
under an additive-only contract: the curated memories are authoritative
and untouched; only genuinely missing onboarding content is written.

The audit found one real gap. Workflow, GitHub etiquette, toolchain, and
Rust style are all owned by AGENTS.md, the rule files, or an existing
memory — onboarding's tech-stack/commands/conventions templates were
skipped as drift-prone restatement. Nothing, however, mapped the crate's
internals: rust.md's layout tree is the scaffold-era skeleton naming 5 of
12 source files.

`core` is that map — one entry per src module stating what it owns and
pointing at the file's own rationale doc comment rather than restating it
(verified against the sources: the CLOUD refs match each file's own doc
comment), plus where behavioral tests live and the consumer-#1 setup.
`core` is also the discovery root of Serena's memory graph, per its
memory-maintenance model. rust.md's skeleton now points at it instead of
growing a second, drifting copy.

memory_maintenance is Serena's own operating contract for how its tools
write and reference memories (the mem: graph model); it is committed so
that behavior is pinned and reviewable like everything else in
.serena/memories rather than regenerating unversioned per machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ga5K9K38p6qt8shSTPKY9t
wenzowski pushed a commit that referenced this pull request Aug 7, 2026
…sitory

Refs: CLOUD-71

Three committed files asserted that the checked-in batten.toml is what
`batten check` runs against this repository. Nothing did: no hk step, no
mise task, no workflow invoked the binary, so the engine's one configured
rule had never executed anywhere. The assertion becomes a mechanism:

  mise.toml   batten-check — `cargo run --quiet -p batten -- check`, so
              the gate judges the working tree's engine and config as the
              pair that ships
  hk.pkl      a batten-check step in the shared gate, chained after `test`
              so one cargo build holds the target-dir lock at a time; no
              glob, because any file can carry a violation
  tests/cli.rs the committed batten.toml pinned behaviourally: it loads,
              and its rule fires on the shape it names, pointer-only and
              byte-stable

The bash gates this repo runs are not migrated here: none of them is a
content predicate the current rule table can express (path existence,
reference resolution, exemption lists) — each such gap is capability-issue
material on the board, not a reason to widen this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VS5eTnMTruCWytpAk2gW7k
wenzowski pushed a commit that referenced this pull request Aug 8, 2026
…CLOUD-32)

Policy changes need to be attributable after the fact. The epoch is a
deterministic hash of the files that GOVERN a run, so two records
carrying the same epoch were produced under provably the same rules, and
one carrying a different epoch was not.

The tracked set is config, never code. Which files govern a repository is
that repository's business — an agent settings file, a contributor guide,
a hook config, each meaningful in one repository and meaningless in the
next. So the set is `[epoch] tracked` in batten.toml, and the core
carries only the default: batten.toml itself, the one file that governs
every consumer by definition. Batten's own list lives in Batten's own
config, as consumer #1, which is where a worked example belongs.

That keeps non-negotiable rule 1 true as a GREP, not merely in spirit.
The first draft named this repo's files in doc comments and in a test
fixture — prose and fixtures, not behaviour, but the rule states its
predicate as "a grep returns zero hits", and a rule stated that way is
one a gate can be written on. Both are now generic; `git grep` over
crates/batten for any of those names returns nothing.

An unreadable tracked path is exit 1, NAMING THE PATH, never a skip. The
tracked set IS config — it defaults to batten.toml itself — so an
unreadable tracked path is unreadable config, which §7 routes to 1; 3
stays for an I/O failure not attributable to the config. Same shape as
config_trust's unreadable-ref case, which also names what it could not
read: a refusal the reader cannot act on is barely better than a silent
skip, and a skip would compute a STABLE epoch over a changed surface,
which looks exactly like a valid answer.

The epoch follows --config-from. Under a base ref both the tracked list
and the bytes come from the ref, never the working tree: an epoch
attributing a run to a config that did not govern it would be worse than
none.

The construction is identity::surface_fingerprint, not a second hash of
the same bytes. tagged_fingerprint and write_field are lifted out of
fingerprint_of, so findings and the epoch share ONE length-prefixed
SHA-256 framing rather than two that can drift. Without the length prefix
("ab","c") and ("a","bc") hash identically, so a rename could be hidden
by choosing names — asserted directly. Text is NFC/LF-canonicalized so a
CRLF checkout attributes identically; non-UTF-8 content is hashed
verbatim, since there is nothing to normalize and refusing it would make
the tracked set silently text-only. Paths are canonicalized, sorted and
deduplicated, and the path is hashed as well as the bytes, so adding an
empty file to the tracked set still moves the epoch — the SET is part of
what governs.

batten.toml raises min_batten_version off 0.0.0 now that it declares
`[epoch]`: a new key must be REFUSED by an older binary, never ignored.
deny_unknown_fields is what forces that; the floor states the intent so
the refusal names the version rather than the key.

House style §2 gains `epoch` on its `config` row in the same move — §11
makes the binary the emitter of the spec, not a warrant to outrun the
doc, and CLOUD-19 settled §2 as authoritative for the surface.

Surfaced two ways that must agree: `batten config epoch` and
doctor --json's config_epoch, with a test asserting the join CLOUD-133
will key guard records on. doctor OMITS the field when the surface cannot
be read rather than emitting a placeholder — a placeholder would be a
stable value over an unknown surface — and still exits 1 rather than 3,
so it stays the config-or-usage-only verb §7 needs it to be.

Scope: the value only. Stamping onto guard records is CLOUD-133's, which
defines the record; §8's cache and etag-style revalidation are
CLOUD-232's. What lands here is recompute-per-invocation, the reference
implementation that cannot go stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119C7XbeQLWR5TTaW2ca6Gp
wenzowski pushed a commit that referenced this pull request Aug 8, 2026
…CLOUD-32)

Policy changes need to be attributable after the fact. The epoch is a
deterministic hash of the files that GOVERN a run, so two records
carrying the same epoch were produced under provably the same rules, and
one carrying a different epoch was not.

The tracked set is config, never code. Which files govern a repository is
that repository's business — an agent settings file, a contributor guide,
a hook config, each meaningful in one repository and meaningless in the
next. So the set is `[epoch] tracked` in batten.toml, and the core
carries only the default: batten.toml itself, the one file that governs
every consumer by definition. Batten's own list lives in Batten's own
config, as consumer #1, which is where a worked example belongs.

That keeps non-negotiable rule 1 true as a GREP, not merely in spirit.
The first draft named this repo's files in doc comments and in a test
fixture — prose and fixtures, not behaviour, but the rule states its
predicate as "a grep returns zero hits", and a rule stated that way is
one a gate can be written on. Both are now generic; `git grep` over
crates/batten for any of those names returns nothing.

An unreadable tracked path is exit 1, NAMING THE PATH, never a skip. The
tracked set IS config — it defaults to batten.toml itself — so an
unreadable tracked path is unreadable config, which §7 routes to 1; 3
stays for an I/O failure not attributable to the config. Same shape as
config_trust's unreadable-ref case, which also names what it could not
read: a refusal the reader cannot act on is barely better than a silent
skip, and a skip would compute a STABLE epoch over a changed surface,
which looks exactly like a valid answer.

The epoch follows --config-from. Under a base ref both the tracked list
and the bytes come from the ref, never the working tree: an epoch
attributing a run to a config that did not govern it would be worse than
none.

The construction is identity::surface_fingerprint, not a second hash of
the same bytes. tagged_fingerprint and write_field are lifted out of
fingerprint_of, so findings and the epoch share ONE length-prefixed
SHA-256 framing rather than two that can drift. Without the length prefix
("ab","c") and ("a","bc") hash identically, so a rename could be hidden
by choosing names — asserted directly. Text is NFC/LF-canonicalized so a
CRLF checkout attributes identically; non-UTF-8 content is hashed
verbatim, since there is nothing to normalize and refusing it would make
the tracked set silently text-only. Paths are canonicalized, sorted and
deduplicated, and the path is hashed as well as the bytes, so adding an
empty file to the tracked set still moves the epoch — the SET is part of
what governs.

batten.toml raises min_batten_version off 0.0.0 now that it declares
`[epoch]`: a new key must be REFUSED by an older binary, never ignored.
deny_unknown_fields is what forces that; the floor states the intent so
the refusal names the version rather than the key.

House style §2 gains `epoch` on its `config` row in the same move — §11
makes the binary the emitter of the spec, not a warrant to outrun the
doc, and CLOUD-19 settled §2 as authoritative for the surface.

Surfaced two ways that must agree: `batten config epoch` and
doctor --json's config_epoch, with a test asserting the join CLOUD-133
will key guard records on. doctor OMITS the field when the surface cannot
be read rather than emitting a placeholder — a placeholder would be a
stable value over an unknown surface — and still exits 1 rather than 3,
so it stays the config-or-usage-only verb §7 needs it to be.

Scope: the value only. Stamping onto guard records is CLOUD-133's, which
defines the record; §8's cache and etag-style revalidation are
CLOUD-232's. What lands here is recompute-per-invocation, the reference
implementation that cannot go stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119C7XbeQLWR5TTaW2ca6Gp
wenzowski pushed a commit that referenced this pull request Aug 10, 2026
CLOUD-86. Two `[[rule]]` rows in this repository's own batten.toml, as
consumer #1 — no engine change. A tool built here is a tool nobody
attested: the build runs on whatever toolchain the runner had and
produces a binary no checksum covers, while every tool this repo uses is
pinned in mise.toml as a prebuilt artifact with a locked url and a
per-platform checksum. CLOUD-281 is the instance where the one tool not
pinned that way was the one installed from an unverified download.

Two rows because a `forbid` pattern is a literal substring and the two
files spell the same mistake differently: `"cargo:` in mise.toml (the
opening quote is load-bearing — TOML bare keys cannot contain `:`, so a
backend key is always quoted, which keeps the row off prose that merely
says "cargo"), and `cargo install` in a workflow.

The exemption half is what this waited on CLOUD-208 for: an exemption is
a `[[waiver]]` carrying a reason and an expiry, never
`severity = "allow"`, which records no reason and lapses never. The
suite covers the whole shape — a waived entry passes, a lapsed one is red
again with nobody having acted, an exemption with no reason is exit 1
rather than applied, and a second violation still blocks.

release-artifacts.yml's CLOUD-259 comment narrated install-action's
fallback chain ending in the literal command, so it was the one hit on a
clean tree. Reworded, because a gate that fires on its own explanation is
a gate people delete. Narrowing the pattern to `run: cargo install` was
the alternative and would have missed the block form — coverage that
looks total and isn't.

Not gated here: whether a PREBUILT artifact is attested. That is
CLOUD-90's manifest and CLOUD-281's `github:`-vs-`ubi:` distinction; a
row firing on `ubi:` would be answering a different question.
wenzowski pushed a commit that referenced this pull request Aug 11, 2026
Caught by running the gate against its own PR body, which it failed. The match
was a markdown table cell — `| judgement call | keep |` — documenting the shape,
not deferring anything. Being consumer #1 is what surfaced it, and a gate that
fires on its own documentation would teach exactly the wrong lesson.

The fix is the house pattern rather than a new idea: `gh-guard` and `issue-guard`
neutralise QUOTED spans before judging a command, for the same reason a commit
message mentioning `gh pr merge` is not that command. Backticked spans get the
same treatment here.

Discrimination intact after the change, re-measured: the corpus still fires on
exactly [243], the one genuinely untracked deferral. A regression test pins the
table row.

Two linter traps recorded in the source, both hit while writing this:

  * a literal backtick inside single quotes reads as a missed expansion
    (SC2016), so it is a named constant rather than a suppression
  * the comment explaining that then began with the linter's own name, which
    parses as a DIRECTIVE (SC1073)

The bats header also described `.claude/rules/toolchain.md` as it was FOUND
rather than as this branch corrects it — two artifacts disagreeing in miniature,
which is the defect this whole change is about.

Refs: CLOUD-323
wenzowski pushed a commit that referenced this pull request Aug 11, 2026
Caught by running the gate against its own PR body, which it failed. The match
was a markdown table cell — `| judgement call | keep |` — documenting the shape,
not deferring anything. Being consumer #1 is what surfaced it, and a gate that
fires on its own documentation would teach exactly the wrong lesson.

The fix is the house pattern rather than a new idea: `gh-guard` and `issue-guard`
neutralise QUOTED spans before judging a command, for the same reason a commit
message mentioning `gh pr merge` is not that command. Backticked spans get the
same treatment here.

Discrimination intact after the change, re-measured: the corpus still fires on
exactly [243], the one genuinely untracked deferral. A regression test pins the
table row.

Two linter traps recorded in the source, both hit while writing this:

  * a literal backtick inside single quotes reads as a missed expansion
    (SC2016), so it is a named constant rather than a suppression
  * the comment explaining that then began with the linter's own name, which
    parses as a DIRECTIVE (SC1073)

The bats header also described `.claude/rules/toolchain.md` as it was FOUND
rather than as this branch corrects it — two artifacts disagreeing in miniature,
which is the defect this whole change is about.

Refs: CLOUD-323
wenzowski added a commit that referenced this pull request Aug 21, 2026
…y name

Batten shipped no default policy at all — the tree carried one `.rego` file and
it was a test fixture. That is the anomaly rather than the discipline: Conftest
ships OCI bundles, Semgrep `p/default`, ESLint `eslint:recommended`, Clippy its
lint groups. And the non-negotiable that looks like it forbids this argues for
it — a preset is prior art shipped as DATA, which is the opposite of expanding
the core.

This is not the OCI distribution CLOUD-129 rejected, and that verdict is intact.
Its ground was that remote policy fetch is a supply-chain surface: there is no
network here, no registry and no trust-on-first-use. `include_str!` at build
time, so the bytes ship inside the binary the operator already trusts, under the
same checksum as everything else in it. Its other ground — one committed
authority per repo — does not reach a preset either, because a preset is not an
authority; it is content the authority enables.

`preset` is the third of three mutually-exclusive sources on a policy row, and
the valid names are derived from the embedded set rather than hand-maintained.
`surface::SURFACE`'s discipline: the loader, the published schema's enum and the
tests all read one list, so a name that parses in an editor cannot be one the
loader then refuses. An unknown name is a config error naming what IS available,
because a consumer who typed `trunk-basd` should be told rather than quietly
gated by nothing.

Two presets, both true of a PRACTICE and naming no path, task, tracker key or
entity. `trunk-based/no-force-push` deliberately does not match
`--force-with-lease`: that is the sanctioned form — it refuses when the remote
moved — and a preset banning both would push its consumers toward the bypass
rather than toward the safer flag. `commit-hygiene/no-empty-commit` refuses the
commit that records only that somebody wanted a new SHA.

The id-collision refusal reaches across the vendored/in-repo boundary for free,
because a preset is just another bundle with a declared id set. Bundles are
isolated as ENGINES and still visible to each other for id claims, and that
difference is the design: a preset cannot supply a helper to a consumer's module
and cannot silently shadow one of its predicate ids either.

## Rule 1 over presets: the mechanism already existed

§7(c) asked for a gate refusing a preset source that names a consumer-specific
identifier. This ships the assertion rather than a second scanner, and the
reasoning is the finding. `batten.toml`'s rule-1 `forbid` rows glob `crates/**`,
and the embedded preset sources are under it — they are already scanned on every
gate invocation. A second scanner over the same paths with the same patterns is
the two-authorities-that-drift shape `mise-tasks/rules-drift` exists to warn
about, and the patterns cannot be restated in this crate anyway, because naming
them here is the violation. So `presets_are_inside_the_rule_one_glob` asserts the
COVERAGE — the sources are inside `crates/`, and the authority still carries a
`crates/**` row — which turns "rule 1 reaches presets" from an assumption into a
fact and fails if either half moves.

## Consumer #1 eats it, and declines the other one on the merits

This repo enables `trunk-based`: AGENTS.md's landing contract IS trunk-based
development, `main` is the one long-lived branch, and a force push there is
exactly what the practice refuses.

`commit-hygiene` is deliberately not enabled, which is a decision rather than an
oversight. Its predicate overlaps AGENTS.md's own ban on empty commits to kick
CI, which the landing loop already enforces where it can act on it; a second gate
over one practice doubles a refusal without doubling coverage, and which row
fired would become a question of which one a reader hit first.

Refs: CLOUD-836
wenzowski added a commit that referenced this pull request Aug 21, 2026
…y name

Batten shipped no default policy at all — the tree carried one `.rego` file and
it was a test fixture. That is the anomaly rather than the discipline: Conftest
ships OCI bundles, Semgrep `p/default`, ESLint `eslint:recommended`, Clippy its
lint groups. And the non-negotiable that looks like it forbids this argues for
it — a preset is prior art shipped as DATA, which is the opposite of expanding
the core.

This is not the OCI distribution CLOUD-129 rejected, and that verdict is intact.
Its ground was that remote policy fetch is a supply-chain surface: there is no
network here, no registry and no trust-on-first-use. `include_str!` at build
time, so the bytes ship inside the binary the operator already trusts, under the
same checksum as everything else in it. Its other ground — one committed
authority per repo — does not reach a preset either, because a preset is not an
authority; it is content the authority enables.

`preset` is the third of three mutually-exclusive sources on a policy row, and
the valid names are derived from the embedded set rather than hand-maintained.
`surface::SURFACE`'s discipline: the loader, the published schema's enum and the
tests all read one list, so a name that parses in an editor cannot be one the
loader then refuses. An unknown name is a config error naming what IS available,
because a consumer who typed `trunk-basd` should be told rather than quietly
gated by nothing.

Two presets, both true of a PRACTICE and naming no path, task, tracker key or
entity. `trunk-based/no-force-push` deliberately does not match
`--force-with-lease`: that is the sanctioned form — it refuses when the remote
moved — and a preset banning both would push its consumers toward the bypass
rather than toward the safer flag. `commit-hygiene/no-empty-commit` refuses the
commit that records only that somebody wanted a new SHA.

The id-collision refusal reaches across the vendored/in-repo boundary for free,
because a preset is just another bundle with a declared id set. Bundles are
isolated as ENGINES and still visible to each other for id claims, and that
difference is the design: a preset cannot supply a helper to a consumer's module
and cannot silently shadow one of its predicate ids either.

## Rule 1 over presets: the mechanism already existed

§7(c) asked for a gate refusing a preset source that names a consumer-specific
identifier. This ships the assertion rather than a second scanner, and the
reasoning is the finding. `batten.toml`'s rule-1 `forbid` rows glob `crates/**`,
and the embedded preset sources are under it — they are already scanned on every
gate invocation. A second scanner over the same paths with the same patterns is
the two-authorities-that-drift shape `mise-tasks/rules-drift` exists to warn
about, and the patterns cannot be restated in this crate anyway, because naming
them here is the violation. So `presets_are_inside_the_rule_one_glob` asserts the
COVERAGE — the sources are inside `crates/`, and the authority still carries a
`crates/**` row — which turns "rule 1 reaches presets" from an assumption into a
fact and fails if either half moves.

## Consumer #1 eats it, and declines the other one on the merits

This repo enables `trunk-based`: AGENTS.md's landing contract IS trunk-based
development, `main` is the one long-lived branch, and a force push there is
exactly what the practice refuses.

`commit-hygiene` is deliberately not enabled, which is a decision rather than an
oversight. Its predicate overlaps AGENTS.md's own ban on empty commits to kick
CI, which the landing loop already enforces where it can act on it; a second gate
over one practice doubles a refusal without doubling coverage, and which row
fired would become a question of which one a reader hit first.

Refs: CLOUD-836
wenzowski added a commit that referenced this pull request Aug 21, 2026
…y name

Batten shipped no default policy at all — the tree carried one `.rego` file and
it was a test fixture. That is the anomaly rather than the discipline: Conftest
ships OCI bundles, Semgrep `p/default`, ESLint `eslint:recommended`, Clippy its
lint groups. And the non-negotiable that looks like it forbids this argues for
it — a preset is prior art shipped as DATA, which is the opposite of expanding
the core.

This is not the OCI distribution CLOUD-129 rejected, and that verdict is intact.
Its ground was that remote policy fetch is a supply-chain surface: there is no
network here, no registry and no trust-on-first-use. `include_str!` at build
time, so the bytes ship inside the binary the operator already trusts, under the
same checksum as everything else in it. Its other ground — one committed
authority per repo — does not reach a preset either, because a preset is not an
authority; it is content the authority enables.

`preset` is the third of three mutually-exclusive sources on a policy row, and
the valid names are derived from the embedded set rather than hand-maintained.
`surface::SURFACE`'s discipline: the loader, the published schema's enum and the
tests all read one list, so a name that parses in an editor cannot be one the
loader then refuses. An unknown name is a config error naming what IS available,
because a consumer who typed `trunk-basd` should be told rather than quietly
gated by nothing.

Two presets, both true of a PRACTICE and naming no path, task, tracker key or
entity. `trunk-based/no-force-push` deliberately does not match
`--force-with-lease`: that is the sanctioned form — it refuses when the remote
moved — and a preset banning both would push its consumers toward the bypass
rather than toward the safer flag. `commit-hygiene/no-empty-commit` refuses the
commit that records only that somebody wanted a new SHA.

The id-collision refusal reaches across the vendored/in-repo boundary for free,
because a preset is just another bundle with a declared id set. Bundles are
isolated as ENGINES and still visible to each other for id claims, and that
difference is the design: a preset cannot supply a helper to a consumer's module
and cannot silently shadow one of its predicate ids either.

## Rule 1 over presets: the mechanism already existed

§7(c) asked for a gate refusing a preset source that names a consumer-specific
identifier. This ships the assertion rather than a second scanner, and the
reasoning is the finding. `batten.toml`'s rule-1 `forbid` rows glob `crates/**`,
and the embedded preset sources are under it — they are already scanned on every
gate invocation. A second scanner over the same paths with the same patterns is
the two-authorities-that-drift shape `mise-tasks/rules-drift` exists to warn
about, and the patterns cannot be restated in this crate anyway, because naming
them here is the violation. So `presets_are_inside_the_rule_one_glob` asserts the
COVERAGE — the sources are inside `crates/`, and the authority still carries a
`crates/**` row — which turns "rule 1 reaches presets" from an assumption into a
fact and fails if either half moves.

## Consumer #1 eats it, and declines the other one on the merits

This repo enables `trunk-based`: AGENTS.md's landing contract IS trunk-based
development, `main` is the one long-lived branch, and a force push there is
exactly what the practice refuses.

`commit-hygiene` is deliberately not enabled, which is a decision rather than an
oversight. Its predicate overlaps AGENTS.md's own ban on empty commits to kick
CI, which the landing loop already enforces where it can act on it; a second gate
over one practice doubles a refusal without doubling coverage, and which row
fired would become a question of which one a reader hit first.

Refs: CLOUD-836
wenzowski added a commit that referenced this pull request Aug 22, 2026
…orrect one

THE LEVER IS COST, NOT PROHIBITION, and the earlier attempt on this branch got
that wrong twice. `NOT_REGULAR` was a denylist of subjects; it covered one
program, said nothing about the failure that actually recurs, and blessed
everything it did not name. Reverted already. This is the mechanism that
replaces it.

A `regex.*` builtin may not reach a string literal. Expressions are `[[pattern]]`
rows in the one committed authority, projected into the evaluator's `data`
document, referenced by id:

    regex.match(`CLOUD-[0-9]+`, w)                            refused at load
    regex.match(data.batten.patterns["tracker-key"], w)       loads and decides

The gradient is the point. A one-off pattern costs a config row and an id; a
shared one is free after the first; and asking the same question of an
already-parsed document costs a field access with no config edit at all. Effort
now orders the same way correctness does, which is the only version of this that
survives a translator that does not reason carefully — and 86 bash gates written
in `grep -E` are queued to be translated.

WHY THIS IS NOT FRICTION BOLTED ON. `Config::verbs` already carries the argument
verbatim for the mutating-verb table: "consumer-specific by nature, so it lives
here and never in the crate (non-negotiable rule 1)". A tracker-key expression is
a consumer identifier by exactly that test. Two more things fall out rather than
being designed in: duplication becomes unwritable instead of detectable — one
declaration, one home — and the pattern inventory becomes reviewable data (§11),
readable out of one file.

Measured on the tree this engine gates, comments stripped: 82 of 140 shell
programs use `grep -E`/`sed -E`/`awk`/`=~` over 338 sites, and ONE concept — a
tracker key — carries 19 distinct spellings across 17 programs. `CLOUD-[0-9]+`
alone sits at 15 sites in 9 of them, with a `CLOUD-*` glob spelling beside the
regexes and one program holding two variants of its own pattern 52 lines apart.
Correct regexes over a regular thing, duplicated until they drifted.

CLOSED AT BOTH ENDS, and the second half is not optional. Refusing the inline
form alone leaves the identical hole one step later: `data.batten.patterns["typo"]`
resolves to UNDEFINED, Rego reads undefined as "this rule body does not hold", so
the module loads clean, evaluates clean and gates nothing. So a reference no row
declares is refused too — the same shape `check_tree_paths_are_emittable` uses
against a `tree` key the engine never emits.

The same failure arriving by deletion is `WeakeningKind::PatternRemoved`: a local
override dropping a row silences every module referencing it, and one row can
silence several predicates at once, because a declared pattern is shared by
design. Reported with the id, never the expression.

TWO BUGS ONLY RUNNING IT COULD HAVE FOUND, both from an AST probe rather than a
reading:

  - a Rego backtick literal serialises as `RawString`, not `String`. Reading only
    `String` let every realistic inline pattern through, since backticks are how
    a regex carrying backslashes is written.
  - a REFERENCE contains a literal: `data.batten.patterns["x"]` is a `RefBrack`
    whose index is the string `"x"`, so a naive sweep read the sanctioned form as
    the refused one and no module could load at all.

CONSUMER #1 PROVES IT. `policy/run-shape.rego`'s flag-cluster shape is declared
in `batten.toml` and referenced by name; its `#MUTANT` directive now corrupts the
REFERENCE, so the mutation exercises the silent-disarm path directly — the
allow cases go red when the id stops resolving.

Four completeness gates in this repo refused the new table until it was
classified, and the third found a defect this change would otherwise have
shipped: validation call site (`config.rs`), resolve provenance, trust verdict —
which is where the silent-disarm consequence surfaced — and every-kind-exercised.

Refs: CLOUD-885
wenzowski added a commit that referenced this pull request Aug 22, 2026
…correct one

THE LEVER IS COST, NOT PROHIBITION, and the earlier attempt on this branch got
that wrong twice. `NOT_REGULAR` was a denylist of subjects; it covered one
program, said nothing about the failure that actually recurs, and blessed
everything it did not name. Reverted already. This is the mechanism that
replaces it.

A `regex.*` builtin may not reach a string literal. Expressions are `[[pattern]]`
rows in the one committed authority, projected into the evaluator's `data`
document, referenced by id:

    regex.match(`CLOUD-[0-9]+`, w)                            refused at load
    regex.match(data.batten.patterns["tracker-key"], w)       loads and decides

The gradient is the point. A one-off pattern costs a config row and an id; a
shared one is free after the first; and asking the same question of an
already-parsed document costs a field access with no config edit at all. Effort
now orders the same way correctness does, which is the only version of this that
survives a translator that does not reason carefully — and 86 bash gates written
in `grep -E` are queued to be translated.

WHY THIS IS NOT FRICTION BOLTED ON. `Config::verbs` already carries the argument
verbatim for the mutating-verb table: "consumer-specific by nature, so it lives
here and never in the crate (non-negotiable rule 1)". A tracker-key expression is
a consumer identifier by exactly that test. Two more things fall out rather than
being designed in: duplication becomes unwritable instead of detectable — one
declaration, one home — and the pattern inventory becomes reviewable data (§11),
readable out of one file.

Measured on the tree this engine gates, comments stripped: 82 of 140 shell
programs use `grep -E`/`sed -E`/`awk`/`=~` over 338 sites, and ONE concept — a
tracker key — carries 19 distinct spellings across 17 programs. `CLOUD-[0-9]+`
alone sits at 15 sites in 9 of them, with a `CLOUD-*` glob spelling beside the
regexes and one program holding two variants of its own pattern 52 lines apart.
Correct regexes over a regular thing, duplicated until they drifted.

CLOSED AT BOTH ENDS, and the second half is not optional. Refusing the inline
form alone leaves the identical hole one step later: `data.batten.patterns["typo"]`
resolves to UNDEFINED, Rego reads undefined as "this rule body does not hold", so
the module loads clean, evaluates clean and gates nothing. So a reference no row
declares is refused too — the same shape `check_tree_paths_are_emittable` uses
against a `tree` key the engine never emits.

The same failure arriving by deletion is `WeakeningKind::PatternRemoved`: a local
override dropping a row silences every module referencing it, and one row can
silence several predicates at once, because a declared pattern is shared by
design. Reported with the id, never the expression.

TWO BUGS ONLY RUNNING IT COULD HAVE FOUND, both from an AST probe rather than a
reading:

  - a Rego backtick literal serialises as `RawString`, not `String`. Reading only
    `String` let every realistic inline pattern through, since backticks are how
    a regex carrying backslashes is written.
  - a REFERENCE contains a literal: `data.batten.patterns["x"]` is a `RefBrack`
    whose index is the string `"x"`, so a naive sweep read the sanctioned form as
    the refused one and no module could load at all.

CONSUMER #1 PROVES IT. `policy/run-shape.rego`'s flag-cluster shape is declared
in `batten.toml` and referenced by name; its `#MUTANT` directive now corrupts the
REFERENCE, so the mutation exercises the silent-disarm path directly — the
allow cases go red when the id stops resolving.

Four completeness gates in this repo refused the new table until it was
classified, and the third found a defect this change would otherwise have
shipped: validation call site (`config.rs`), resolve provenance, trust verdict —
which is where the silent-disarm consequence surfaced — and every-kind-exercised.

Refs: CLOUD-885

BREAKING CHANGE: the pattern table has to reach the evaluator, so it is threaded
beside `provisions` the way that table already is. `Config` gains `patterns`,
`trust::WeakeningKind` gains `PatternRemoved`, and `rules::run_static`,
`rules::run_recorded`, `rules::run_all`, `policy::load` and `policy::compile`
each take one more argument. Caught by `mise run verify`'s semver gate, which
named all three lint classes rather than letting a patch-compatible claim ship
over an API that moved.
wenzowski added a commit that referenced this pull request Aug 22, 2026
…correct one

THE LEVER IS COST, NOT PROHIBITION, and the earlier attempt on this branch got
that wrong twice. `NOT_REGULAR` was a denylist of subjects; it covered one
program, said nothing about the failure that actually recurs, and blessed
everything it did not name. Reverted already. This is the mechanism that
replaces it.

A `regex.*` builtin may not reach a string literal. Expressions are `[[pattern]]`
rows in the one committed authority, projected into the evaluator's `data`
document, referenced by id:

    regex.match(`CLOUD-[0-9]+`, w)                            refused at load
    regex.match(data.batten.patterns["tracker-key"], w)       loads and decides

The gradient is the point. A one-off pattern costs a config row and an id; a
shared one is free after the first; and asking the same question of an
already-parsed document costs a field access with no config edit at all. Effort
now orders the same way correctness does, which is the only version of this that
survives a translator that does not reason carefully — and 86 bash gates written
in `grep -E` are queued to be translated.

WHY THIS IS NOT FRICTION BOLTED ON. `Config::verbs` already carries the argument
verbatim for the mutating-verb table: "consumer-specific by nature, so it lives
here and never in the crate (non-negotiable rule 1)". A tracker-key expression is
a consumer identifier by exactly that test. Two more things fall out rather than
being designed in: duplication becomes unwritable instead of detectable — one
declaration, one home — and the pattern inventory becomes reviewable data (§11),
readable out of one file.

Measured on the tree this engine gates, comments stripped: 82 of 140 shell
programs use `grep -E`/`sed -E`/`awk`/`=~` over 338 sites, and ONE concept — a
tracker key — carries 19 distinct spellings across 17 programs. `CLOUD-[0-9]+`
alone sits at 15 sites in 9 of them, with a `CLOUD-*` glob spelling beside the
regexes and one program holding two variants of its own pattern 52 lines apart.
Correct regexes over a regular thing, duplicated until they drifted.

CLOSED AT BOTH ENDS, and the second half is not optional. Refusing the inline
form alone leaves the identical hole one step later: `data.batten.patterns["typo"]`
resolves to UNDEFINED, Rego reads undefined as "this rule body does not hold", so
the module loads clean, evaluates clean and gates nothing. So a reference no row
declares is refused too — the same shape `check_tree_paths_are_emittable` uses
against a `tree` key the engine never emits.

The same failure arriving by deletion is `WeakeningKind::PatternRemoved`: a local
override dropping a row silences every module referencing it, and one row can
silence several predicates at once, because a declared pattern is shared by
design. Reported with the id, never the expression.

TWO BUGS ONLY RUNNING IT COULD HAVE FOUND, both from an AST probe rather than a
reading:

  - a Rego backtick literal serialises as `RawString`, not `String`. Reading only
    `String` let every realistic inline pattern through, since backticks are how
    a regex carrying backslashes is written.
  - a REFERENCE contains a literal: `data.batten.patterns["x"]` is a `RefBrack`
    whose index is the string `"x"`, so a naive sweep read the sanctioned form as
    the refused one and no module could load at all.

CONSUMER #1 PROVES IT. `policy/run-shape.rego`'s flag-cluster shape is declared
in `batten.toml` and referenced by name; its `#MUTANT` directive now corrupts the
REFERENCE, so the mutation exercises the silent-disarm path directly — the
allow cases go red when the id stops resolving.

Four completeness gates in this repo refused the new table until it was
classified, and the third found a defect this change would otherwise have
shipped: validation call site (`config.rs`), resolve provenance, trust verdict —
which is where the silent-disarm consequence surfaced — and every-kind-exercised.

Refs: CLOUD-885

BREAKING CHANGE: the pattern table has to reach the evaluator, so it is threaded
beside `provisions` the way that table already is. `Config` gains `patterns`,
`trust::WeakeningKind` gains `PatternRemoved`, and `rules::run_static`,
`rules::run_recorded`, `rules::run_all`, `policy::load` and `policy::compile`
each take one more argument. Caught by `mise run verify`'s semver gate, which
named all three lint classes rather than letting a patch-compatible claim ship
over an API that moved.
wenzowski added a commit that referenced this pull request Aug 22, 2026
…correct one

THE LEVER IS COST, NOT PROHIBITION, and the earlier attempt on this branch got
that wrong twice. `NOT_REGULAR` was a denylist of subjects; it covered one
program, said nothing about the failure that actually recurs, and blessed
everything it did not name. Reverted already. This is the mechanism that
replaces it.

A `regex.*` builtin may not reach a string literal. Expressions are `[[pattern]]`
rows in the one committed authority, projected into the evaluator's `data`
document, referenced by id:

    regex.match(`CLOUD-[0-9]+`, w)                            refused at load
    regex.match(data.batten.patterns["tracker-key"], w)       loads and decides

The gradient is the point. A one-off pattern costs a config row and an id; a
shared one is free after the first; and asking the same question of an
already-parsed document costs a field access with no config edit at all. Effort
now orders the same way correctness does, which is the only version of this that
survives a translator that does not reason carefully — and 86 bash gates written
in `grep -E` are queued to be translated.

WHY THIS IS NOT FRICTION BOLTED ON. `Config::verbs` already carries the argument
verbatim for the mutating-verb table: "consumer-specific by nature, so it lives
here and never in the crate (non-negotiable rule 1)". A tracker-key expression is
a consumer identifier by exactly that test. Two more things fall out rather than
being designed in: duplication becomes unwritable instead of detectable — one
declaration, one home — and the pattern inventory becomes reviewable data (§11),
readable out of one file.

Measured on the tree this engine gates, comments stripped: 82 of 140 shell
programs use `grep -E`/`sed -E`/`awk`/`=~` over 338 sites, and ONE concept — a
tracker key — carries 19 distinct spellings across 17 programs. `CLOUD-[0-9]+`
alone sits at 15 sites in 9 of them, with a `CLOUD-*` glob spelling beside the
regexes and one program holding two variants of its own pattern 52 lines apart.
Correct regexes over a regular thing, duplicated until they drifted.

CLOSED AT BOTH ENDS, and the second half is not optional. Refusing the inline
form alone leaves the identical hole one step later: `data.batten.patterns["typo"]`
resolves to UNDEFINED, Rego reads undefined as "this rule body does not hold", so
the module loads clean, evaluates clean and gates nothing. So a reference no row
declares is refused too — the same shape `check_tree_paths_are_emittable` uses
against a `tree` key the engine never emits.

The same failure arriving by deletion is `WeakeningKind::PatternRemoved`: a local
override dropping a row silences every module referencing it, and one row can
silence several predicates at once, because a declared pattern is shared by
design. Reported with the id, never the expression.

TWO BUGS ONLY RUNNING IT COULD HAVE FOUND, both from an AST probe rather than a
reading:

  - a Rego backtick literal serialises as `RawString`, not `String`. Reading only
    `String` let every realistic inline pattern through, since backticks are how
    a regex carrying backslashes is written.
  - a REFERENCE contains a literal: `data.batten.patterns["x"]` is a `RefBrack`
    whose index is the string `"x"`, so a naive sweep read the sanctioned form as
    the refused one and no module could load at all.

CONSUMER #1 PROVES IT. `policy/run-shape.rego`'s flag-cluster shape is declared
in `batten.toml` and referenced by name; its `#MUTANT` directive now corrupts the
REFERENCE, so the mutation exercises the silent-disarm path directly — the
allow cases go red when the id stops resolving.

Four completeness gates in this repo refused the new table until it was
classified, and the third found a defect this change would otherwise have
shipped: validation call site (`config.rs`), resolve provenance, trust verdict —
which is where the silent-disarm consequence surfaced — and every-kind-exercised.

Refs: CLOUD-885

BREAKING CHANGE: the pattern table has to reach the evaluator, so it is threaded
beside `provisions` the way that table already is. `Config` gains `patterns`,
`trust::WeakeningKind` gains `PatternRemoved`, and `rules::run_static`,
`rules::run_recorded`, `rules::run_all`, `policy::load` and `policy::compile`
each take one more argument. Caught by `mise run verify`'s semver gate, which
named all three lint classes rather than letting a patch-compatible claim ship
over an API that moved.
wenzowski added a commit that referenced this pull request Aug 22, 2026
…correct one

THE LEVER IS COST, NOT PROHIBITION, and the earlier attempt on this branch got
that wrong twice. `NOT_REGULAR` was a denylist of subjects; it covered one
program, said nothing about the failure that actually recurs, and blessed
everything it did not name. Reverted already. This is the mechanism that
replaces it.

A `regex.*` builtin may not reach a string literal. Expressions are `[[pattern]]`
rows in the one committed authority, projected into the evaluator's `data`
document, referenced by id:

    regex.match(`CLOUD-[0-9]+`, w)                            refused at load
    regex.match(data.batten.patterns["tracker-key"], w)       loads and decides

The gradient is the point. A one-off pattern costs a config row and an id; a
shared one is free after the first; and asking the same question of an
already-parsed document costs a field access with no config edit at all. Effort
now orders the same way correctness does, which is the only version of this that
survives a translator that does not reason carefully — and 86 bash gates written
in `grep -E` are queued to be translated.

WHY THIS IS NOT FRICTION BOLTED ON. `Config::verbs` already carries the argument
verbatim for the mutating-verb table: "consumer-specific by nature, so it lives
here and never in the crate (non-negotiable rule 1)". A tracker-key expression is
a consumer identifier by exactly that test. Two more things fall out rather than
being designed in: duplication becomes unwritable instead of detectable — one
declaration, one home — and the pattern inventory becomes reviewable data (§11),
readable out of one file.

Measured on the tree this engine gates, comments stripped: 82 of 140 shell
programs use `grep -E`/`sed -E`/`awk`/`=~` over 338 sites, and ONE concept — a
tracker key — carries 19 distinct spellings across 17 programs. `CLOUD-[0-9]+`
alone sits at 15 sites in 9 of them, with a `CLOUD-*` glob spelling beside the
regexes and one program holding two variants of its own pattern 52 lines apart.
Correct regexes over a regular thing, duplicated until they drifted.

CLOSED AT BOTH ENDS, and the second half is not optional. Refusing the inline
form alone leaves the identical hole one step later: `data.batten.patterns["typo"]`
resolves to UNDEFINED, Rego reads undefined as "this rule body does not hold", so
the module loads clean, evaluates clean and gates nothing. So a reference no row
declares is refused too — the same shape `check_tree_paths_are_emittable` uses
against a `tree` key the engine never emits.

The same failure arriving by deletion is `WeakeningKind::PatternRemoved`: a local
override dropping a row silences every module referencing it, and one row can
silence several predicates at once, because a declared pattern is shared by
design. Reported with the id, never the expression.

TWO BUGS ONLY RUNNING IT COULD HAVE FOUND, both from an AST probe rather than a
reading:

  - a Rego backtick literal serialises as `RawString`, not `String`. Reading only
    `String` let every realistic inline pattern through, since backticks are how
    a regex carrying backslashes is written.
  - a REFERENCE contains a literal: `data.batten.patterns["x"]` is a `RefBrack`
    whose index is the string `"x"`, so a naive sweep read the sanctioned form as
    the refused one and no module could load at all.

CONSUMER #1 PROVES IT. `policy/run-shape.rego`'s flag-cluster shape is declared
in `batten.toml` and referenced by name; its `#MUTANT` directive now corrupts the
REFERENCE, so the mutation exercises the silent-disarm path directly — the
allow cases go red when the id stops resolving.

Four completeness gates in this repo refused the new table until it was
classified, and the third found a defect this change would otherwise have
shipped: validation call site (`config.rs`), resolve provenance, trust verdict —
which is where the silent-disarm consequence surfaced — and every-kind-exercised.

Refs: CLOUD-885

BREAKING CHANGE: the pattern table has to reach the evaluator, so it is threaded
beside `provisions` the way that table already is. `Config` gains `patterns`,
`trust::WeakeningKind` gains `PatternRemoved`, and `rules::run_static`,
`rules::run_recorded`, `rules::run_all`, `policy::load` and `policy::compile`
each take one more argument. Caught by `mise run verify`'s semver gate, which
named all three lint classes rather than letting a patch-compatible claim ship
over an API that moved.
wenzowski added a commit that referenced this pull request Aug 22, 2026
…correct one

THE LEVER IS COST, NOT PROHIBITION, and the earlier attempt on this branch got
that wrong twice. `NOT_REGULAR` was a denylist of subjects; it covered one
program, said nothing about the failure that actually recurs, and blessed
everything it did not name. Reverted already. This is the mechanism that
replaces it.

A `regex.*` builtin may not reach a string literal. Expressions are `[[pattern]]`
rows in the one committed authority, projected into the evaluator's `data`
document, referenced by id:

    regex.match(`CLOUD-[0-9]+`, w)                            refused at load
    regex.match(data.batten.patterns["tracker-key"], w)       loads and decides

The gradient is the point. A one-off pattern costs a config row and an id; a
shared one is free after the first; and asking the same question of an
already-parsed document costs a field access with no config edit at all. Effort
now orders the same way correctness does, which is the only version of this that
survives a translator that does not reason carefully — and 86 bash gates written
in `grep -E` are queued to be translated.

WHY THIS IS NOT FRICTION BOLTED ON. `Config::verbs` already carries the argument
verbatim for the mutating-verb table: "consumer-specific by nature, so it lives
here and never in the crate (non-negotiable rule 1)". A tracker-key expression is
a consumer identifier by exactly that test. Two more things fall out rather than
being designed in: duplication becomes unwritable instead of detectable — one
declaration, one home — and the pattern inventory becomes reviewable data (§11),
readable out of one file.

Measured on the tree this engine gates, comments stripped: 82 of 140 shell
programs use `grep -E`/`sed -E`/`awk`/`=~` over 338 sites, and ONE concept — a
tracker key — carries 19 distinct spellings across 17 programs. `CLOUD-[0-9]+`
alone sits at 15 sites in 9 of them, with a `CLOUD-*` glob spelling beside the
regexes and one program holding two variants of its own pattern 52 lines apart.
Correct regexes over a regular thing, duplicated until they drifted.

CLOSED AT BOTH ENDS, and the second half is not optional. Refusing the inline
form alone leaves the identical hole one step later: `data.batten.patterns["typo"]`
resolves to UNDEFINED, Rego reads undefined as "this rule body does not hold", so
the module loads clean, evaluates clean and gates nothing. So a reference no row
declares is refused too — the same shape `check_tree_paths_are_emittable` uses
against a `tree` key the engine never emits.

The same failure arriving by deletion is `WeakeningKind::PatternRemoved`: a local
override dropping a row silences every module referencing it, and one row can
silence several predicates at once, because a declared pattern is shared by
design. Reported with the id, never the expression.

TWO BUGS ONLY RUNNING IT COULD HAVE FOUND, both from an AST probe rather than a
reading:

  - a Rego backtick literal serialises as `RawString`, not `String`. Reading only
    `String` let every realistic inline pattern through, since backticks are how
    a regex carrying backslashes is written.
  - a REFERENCE contains a literal: `data.batten.patterns["x"]` is a `RefBrack`
    whose index is the string `"x"`, so a naive sweep read the sanctioned form as
    the refused one and no module could load at all.

CONSUMER #1 PROVES IT. `policy/run-shape.rego`'s flag-cluster shape is declared
in `batten.toml` and referenced by name; its `#MUTANT` directive now corrupts the
REFERENCE, so the mutation exercises the silent-disarm path directly — the
allow cases go red when the id stops resolving.

Four completeness gates in this repo refused the new table until it was
classified, and the third found a defect this change would otherwise have
shipped: validation call site (`config.rs`), resolve provenance, trust verdict —
which is where the silent-disarm consequence surfaced — and every-kind-exercised.

Refs: CLOUD-885

BREAKING CHANGE: the pattern table has to reach the evaluator, so it is threaded
beside `provisions` the way that table already is. `Config` gains `patterns`,
`trust::WeakeningKind` gains `PatternRemoved`, and `rules::run_static`,
`rules::run_recorded`, `rules::run_all`, `policy::load` and `policy::compile`
each take one more argument. Caught by `mise run verify`'s semver gate, which
named all three lint classes rather than letting a patch-compatible claim ship
over an API that moved.
@wenzowski wenzowski mentioned this pull request Sep 2, 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

Development

Successfully merging this pull request may close these issues.

1 participant