Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
8e7ae71
revert(hook): a declared row was deleted against a world that rewrite…
wenzowski Sep 2, 2026
ce169d3
feat(facts)!: a vendored prompt's dispatch becomes a fact the tree ca…
wenzowski Sep 2, 2026
1aebd1e
fix(review): the dispatch sent a prompt and no subject
wenzowski Sep 2, 2026
cc2e91b
feat(policy): the declared review becomes a gate, and an absent runne…
wenzowski Sep 2, 2026
08fd1b3
feat(ready): a refinement block owes a dispatched pressure test
wenzowski Sep 2, 2026
9195fc2
fix(review): the prompt went down a channel a real runner ignores
wenzowski Sep 2, 2026
2c2c2c1
test(review): the untouched-subject case could not see the narrowing …
wenzowski Sep 2, 2026
a636554
docs(tools): the validator row stops advertising a gap that was closed
wenzowski Sep 2, 2026
f82f7ba
fix(facts): the census count and the module's placement, both missed …
wenzowski Sep 2, 2026
98c522d
fix(policy): withdraw an override route for a case that never refuses
wenzowski Sep 2, 2026
61ab5ac
feat(policy): a backgrounded wait that polls a local process is refused
wenzowski Sep 2, 2026
b125d9f
fix(policy): two harness-wiring mutations reached nothing, for two re…
wenzowski Sep 2, 2026
ff0b021
fix(policy): the stale guard's case took the arm its mutation does no…
wenzowski Sep 2, 2026
c71c84f
fix(prune): the basis count went eleven past its tolerance under a re…
wenzowski Sep 2, 2026
98e1071
fix(test): a Windows path is not a TOML escape, and a stub that canno…
wenzowski Sep 2, 2026
6d68d32
style(config): taplo-normalise the conflict resolution
wenzowski Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .claude/rules/policy-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,20 @@ by content, carries no clock, and would answer a question about a mutable field
from whichever read sorts first in digest order (CLOUD-1310); and
`input.tree.captured` is a declared REDUCTION
over the capture store — `present`, `count` or a bounded token, never a payload
(CLOUD-1188).
(CLOUD-1188); and `input.tree.review` is whether a VENDORED agent prompt was
dispatched over a declared subject, keyed by (prompt digest, subject digest) so
editing the subject leaves the record under a name nothing looks up (CLOUD-472).

**`review` is the one key whose ARM a module must get right rather than merely
its spelling, so it is worth the extra sentence.** A declared id ABSENT from the
map was never dispatched, and that absence is the ONLY thing a predicate over it
may refuse on. Its `findings` are pointers — `{path, line, clause}`, with no
field an agent's prose could occupy — and a module refusing on what the agent
CONCLUDED would be a model verdict wearing an exit code, which non-negotiable
rule 3 forbids. `forge-verdict-required` refuses the opposite arm for a reason
that does not carry: the forge is a third party that may legitimately not have
judged yet, where a review this branch was supposed to dispatch and did not is
the branch's own conduct.

A **mediated-call** module (`scope = "mediated_call"`, run by `batten hook`)
reads `input.call.command`, `input.call.segments`, `input.call.programs`,
Expand Down
29 changes: 29 additions & 0 deletions .serena/memories/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,35 @@ transcript CONTENT needs 1029 first, and nothing landed authorises one.
row would override four correct per-verb answers with one weaker sentence.
Per-path beats per-verb only where the path fact dominates. It makes a message
specific; it does not make the named surface reachable (CLOUD-663).
- `review.rs` — the vendored-prompt dispatch tier (CLOUD-472): the SECOND
occupant of `Cost::Effect` and the third adopter of `secrets.rs`'
delegated-analyser shape, after `symbols.rs`. It answers one narrow question —
**did a particular vendored prompt run over these exact bytes** — and that
narrowness is the mechanism rather than a limitation: a gate over it refuses
ABSENCE, which is a comparison of two digests, so no model verdict reaches an
exit code (rule 3). The findings reach a module as `{path, line, clause}`
pointers with no field an agent's prose could occupy, so rule 4 holds
structurally rather than by the parser remembering to strip.
**It is what the cheaper tiers cannot be.** `ready-lint` gates the SHAPE of a
refinement block, and shape is what an author optimises against once the gate
exists — the measured failure that opened CLOUD-472, where every clause was
present and none had been pressure-tested. `obligations-bound` binds a §7 entry
to a killer mutation, but only at implementation time; at refinement there is
no code, no case file and no `#MUTANT` row to reach. A hash comparison is what
better-shaped prose cannot satisfy, because the prose is the input to the hash.
**Spawn on miss, read on hit**, keyed by (prompt digest, subject digest) —
`step-receipt`'s pattern, so the agent runs once per unique subject rather than
once per landing lap, and editing the ticket body or pushing a commit leaves
the record under a name nothing looks up. The keying is the anti-staleness
property, not an optimisation.
**The dispatch is the ENGINE's**, which is the whole difference from
`tool-verdict`'s producer-writes-outside store — identical read shape, measured
dead, because somebody has to remember to run the tool and pipe its output
(CLOUD-1265). The prompt is compiled in the way `policy/presets/**` are, so its
digest is a constant of the build and a consumer cannot satisfy the gate by
pointing it at an easier prompt. Every failure path — runner missing, non-zero
exit, unparseable stream — leaves NO record, so a broken agent and one that
never ran are indistinguishable and both refuse.
- `refusal.rs` — the refusal contract (CLOUD-122): ONE `Refusal` value —
`{rule, reason, fix}` — constructed at every deny site and projected onto
whatever channel a host reads, so the shape is never re-typed per harness.
Expand Down
194 changes: 187 additions & 7 deletions batten.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5064,6 +5064,62 @@ key = "CLOUD-1188"
node = "project"
reduce = "present"

# CLOUD-472's consumer, and the row that makes `Fact::Review` a gate rather than
# a store.
#
# `[[rule.review]]` is what pays for it, on `[[rule.tools]]`'s shape and for
# `symbols = true`'s reason: this is the second `Cost::Effect` fact, so a MISS
# dispatches an agent — minutes and tokens, where clippy is seconds. The digest
# key makes that once per unique subject rather than once per landing lap, but a
# run that paid it unasked would still be the shape that gets a gate switched off.
#
# `scope = "tree"` is forced rather than chosen: `Fact::Review` is not
# `Surface::Hook`, and `validate()` refuses a mediated row declaring a
# tree-surface fact. A round trip to an agent on every tool call would be
# disqualifying against the invocation budget besides.
#
# THE PROMPT ID NAMES A VENDORED PROMPT, never a path. Its text is compiled into
# the binary, so its digest is a constant of the build — which is what makes "a
# particular prompt ran" a checkable claim rather than an intention, and what
# stops a consumer satisfying the gate by pointing it at an easier one.
[[rule]]
id = "review-dispatched"
kind = "policy"
scope = "tree"
base = "origin/main"
delta_sources = ["AGENTS.md"]
module = "policy/review-dispatched.rego"
severity = "deny"

[[rule.review]]
id = "ready-pressure-test"
prompt = "ready-pressure-test"
runner = "batten-review-runner"
version = "0"
subject = "document"
path = "AGENTS.md"

# THE GROOMING SUBJECT, and the reason it carries no `path` (CLOUD-472).
#
# A refinement block lives on the TRACKER, so there is nothing in the tree to
# digest: the subject is the description the tracker RETURNED, which reaches the
# engine on `ready lint`'s payload. That is the same forgery control the
# recorder's `verdict` authority column earned — a digest over a payload the
# caller assembled would certify whatever the caller typed, measured green three
# times against text in a local file, once under an id no row carried.
#
# THIS ROW IS READ BY `ready lint`, NOT BY `batten check`. The tree surface skips
# a review with no path rather than guessing one, and `lib.rs` filters this kind
# out for the grammar. Two subjects, one declaration, and neither gate can answer
# for the other: `review-dispatched` refuses at landing, `ready lint` refuses at
# the ready queue, and only the second can see a body that never touches disk.
[[rule.review]]
id = "ready-pressure-test-body"
prompt = "ready-pressure-test"
runner = "batten-review-runner"
version = "0"
subject = "tracker-body"

# A third-party validator's verdict for a file it read (CLOUD-1171, the
# adjudication half of `pkl-check`, `renovate-config-validator` and
# `hook-profile-check`).
Expand All @@ -5086,10 +5142,19 @@ reduce = "present"
# in `crates/batten` (non-negotiable rule 1). The engine composes a key and reads
# a file; which file and which tool are the consumer's.
#
# SILENT UNTIL A PRODUCER WRITES. No record under this key means nothing has
# validated these bytes at this version — absent from the map, not a verdict — so
# this row is inert on a checkout nobody has recorded against, which is every
# checkout today.
# THE PRODUCER IS `mise run record-verdicts` (CLOUD-1265), and naming it here is
# the point rather than a courtesy: this block said the opposite — "SILENT UNTIL
# A PRODUCER WRITES … inert on a checkout nobody has recorded against, which is
# every checkout today" — for as long as the producer existed, and that sentence
# read as a live gap and bought a duplicate writer for it (7aade8f8, reverted).
# `[tasks.verify]` calls the task BEFORE the gates, so the record is present by
# the time this row is adjudicated on any tree `verify` has touched.
#
# ABSENT IS STILL NOT A REFUSAL, and that is the module's decision rather than
# this comment's. No record under this key means nothing has validated these
# bytes at this version — absent from the map, not a verdict — which is what a
# checkout gets if the producer was skipped or died. The row that wants a verdict
# to be REQUIRED is `forge-verdict-required`'s shape and is not this one.
[[rule]]
id = "validator-verdict-clean"
kind = "policy"
Expand Down Expand Up @@ -6079,7 +6144,28 @@ measured = "2026-09-02"
# the gate is what the number is compared against — a basis refreshed from a
# second reading of the tree would red again on the next lap while looking correct
# in review. Which file the two readers disagree about is unresolved and is not
# this bundle's; it is a pointer for whoever takes CLOUD-1158's floor re-derivation.
# this bundle's; it is a pointer for whoever takes CLOUD-1158's floor re-derivation.#
# THE DISAGREEING READER IS THE GLOB IMPLEMENTATION, NOT A MISSING FILE, and the
# entry above leaves it open. Measured 2026-09-02 over one tree:
# `git ls-files 'crates/batten/tests/**/*.rs'` and the gate's own walk differ by
# exactly `crates/batten/tests/policy_modules.rs` — the one `.rs` sitting directly
# under `tests/` with no intervening directory. Git pathspec requires `**/` to
# match at least one path component; a globset-style `**/` matches zero. Same glob
# string, two grammars, one file.
#
# So the entry above is right to write down the GATE's reading, and the reason is
# sharper than "the gate is what the number is compared against": a `git ls-files`
# reading is not a worse measurement of the same set, it is a measurement of a
# DIFFERENT set. Whoever takes CLOUD-1158's floor re-derivation should read the
# floors against the gate's walk for the same reason.
#
# AND THE CALLER MISREPORTS THIS REFUSAL, which cost an hour on the branch that
# found it. `verify` says "not enough disk to run the gate, and pruning did not
# recover it — the refusal above names free space and the floor". It does not: the
# refusal names a STEM COUNT, and free space was 3GB clear of the floor on the lap
# that tripped it. `target prune`'s stale-basis exit is not its below-floor exit,
# and the caller collapses them — so an operator who reads the caller rather than
# the callee deletes files and gets nowhere.

[prune.warm.basis]
glob = "crates/batten/tests/**/*.rs"
Expand Down Expand Up @@ -7105,6 +7191,10 @@ gloss = "an executable"
word = "plan"
gloss = "the work a branch declared it would do"

[[vocabulary.subject]]
word = "prompt"
gloss = "a vendored agent prompt"

[[vocabulary.subject]]
word = "prose"
gloss = "authored text"
Expand Down Expand Up @@ -9267,7 +9357,7 @@ when the condition holds — that is a background wait and is allowed.
[[verdict.route]]
id = "task run first"
kind = "command"
target = "until <test>; do sleep 1; done"
target = "until <test>; do sleep 1; done — where <test> reads something the harness does NOT report, never a process table"

[[verdict.route]]
id = "task run other"
Expand All @@ -9294,13 +9384,55 @@ is allowed.
[[verdict.route]]
id = "task run first"
kind = "command"
target = "until <test>; do sleep 1; done"
target = "until <test>; do sleep 1; done — where <test> reads something the harness does NOT report, never a process table"

[[verdict.route]]
id = "task run other"
kind = "command"
target = "mise run alive"

# THE OTHER HALF OF THE ROW ABOVE, and it exists because that row's own exemption
# was the escape (CLOUD-1337). `timer run refused` allows a backgrounded `sleep`
# wrapped in `until`/`while`, on sound reasoning: a loop testing a condition exits
# on the condition rather than on the clock. But the exemption asks WHETHER there
# is a condition and never WHAT it is about — so a loop polling a LOCAL PROCESS
# passed, and that is the one condition the harness already reports.
#
# MEASURED 2026-09-02: eleven of these on one container, the oldest 9h35m, while
# exactly one real job existed. AGENTS.md has carried the rule since CLOUD-821
# with its measurement and the claim that the shape is "refused by
# `run-shape-guard`" — a claim that was false for as long as it stood. This class
# is what makes it true.
[[verdict]]
id = "task watch duplicate"
gloss = "a backgrounded wait polls a process whose exit is already notified"
class = """
The harness re-invokes the caller when a backgrounded task exits, measured 523 of \
524 in one session including every failure, so a loop reading the process table \
asks a question that is already answered and cannot be answered sooner. What it \
costs is not the CPU of a `sleep`: it is a watcher that outlives the thing it \
watches, because nothing retires it when the answer arrives. \
A CONDITION THE HARNESS DOES NOT REPORT IS NOT THIS CLASS — a forge status, a \
remote queue, a file another machine writes are all still allowed, and the \
`until` form stays the recommended shape for them. \
The narrower reading is refused deliberately too: these eleven were ALSO broken, \
since `pgrep -f` reads full command lines and a mediated call runs as \
`bash -c` over the whole text, so the pattern was a substring of the polling \
shell's own command line and the probe matched itself forever. Refusing only that \
is satisfied by bracketing the pattern, which buys eleven working watchers \
instead of eleven broken ones and no less waste. The wait is the defect.
"""

[[verdict.route]]
id = "turn watch first"
kind = "command"
target = "delete the loop and end the turn — the backgrounded task wakes you on exit, and the turn in between is the designed state"

[[verdict.route]]
id = "task watch other"
kind = "command"
target = "mise run alive"

[[verdict]]
id = "workflow parse broken"
gloss = "a workflow could not be parsed, so its lanes were never judged"
Expand Down Expand Up @@ -9672,6 +9804,54 @@ target = "mise.toml"
# counts one collection plus a page guard, so the second condition is
# `review read absent`'s below. Leaving it named here would send a reader looking for
# threads on a head whose only problem is that nobody looked.
# CLOUD-472. A declared review that never ran over these bytes.
#
# THE SUBJECT IS `prompt`, NOT `review`, and the distinction is load-bearing
# rather than fussy: `review answer missing` and `review read absent` below are
# about the FORGE's review of a pull request, and a reader meeting a third
# `review …` class would have to look up which kind it meant. This one is about a
# vendored prompt this repository dispatches itself.
[[verdict]]
id = "prompt run never"
gloss = "a review this branch declared it would run has not run over these bytes"
class = """
Refuses ABSENCE and nothing else. A declared id missing from the review store \
means the vendored prompt has not been shown to run over this subject, which is \
a comparison of two digests — a record keyed to another prompt, or to bytes that \
have since changed, lives under a different name and does not answer. What the \
agent CONCLUDED is never read: refusing on that would be a model verdict wearing \
an exit code, which non-negotiable rule 3 forbids, and the fact is shaped so it \
cannot be written. A review that ran and pointed at something is as clean as one \
that pointed at nothing, deliberately — pricing findings is how you buy an agent \
that reports none. This is the opposite arm from `forge check red`, which reads \
absence as could-not-look because the forge may legitimately not have judged \
yet; a review this branch was supposed to dispatch and did not is the branch's \
own conduct.
"""

[[verdict.route]]
id = "task run first"
kind = "command"
target = "mise run verify"

[[verdict.route]]
id = "task run other"
kind = "command"
target = "check the declared runner is installed and on PATH — a dispatch that cannot spawn leaves no record, exactly as one that never ran"

# NO OVERRIDE ROUTE, AND ITS ABSENCE IS THE DECISION (CLOUD-472). One was drafted
# — precondition "the review cannot be dispatched in this environment at all — no
# runner exists here" — and withdrawn before landing, because that state is not a
# refusal to begin with. `policy/review-dispatched.rego` reads an absent runner as
# could-not-look; `a_missing_runner_is_could_not_look_and_never_a_refusal` asserts
# it over the compiled binary and `could-not-look-refused` is the declared mutation
# that pins it. So no case matching the precondition ever produces this verdict,
# and the route was a hatch for a door that is never shut.
#
# What remains has command routes and needs no hatch: the runner is here and was
# asked, and left no record. That is the branch's own conduct, and the two routes
# above name what to do about it.

[[verdict]]
id = "review answer missing"
gloss = "readying would buy a CI matrix on a head carrying unresolved review threads"
Expand Down
16 changes: 16 additions & 0 deletions crates/batten/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,21 @@ pub struct Ready {
/// creation time, so this assumes nothing about how a consumer spells a key.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prose_dialect_required_from: Option<String>,
/// The instant from which a row owes a dispatched PRESSURE TEST (CLOUD-472).
///
/// A sibling of the field above rather than a reuse of it, and the
/// separation is the point: these two ratchets convert different things and
/// a consumer must be able to move one without moving the other. Folding
/// them into one instant would mean the day you demand a structured claims
/// object is also the day you demand an agent dispatch, and a repository
/// that wants the first and cannot yet afford the second would have no way
/// to say so.
///
/// Read exactly as its sibling is — fixed-width ISO-8601 UTC compared
/// lexically, absent is could-not-look, and moving it LATER is the only
/// direction that tightens.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pressure_test_required_from: Option<String>,
}

/// The `[trust]` table: what `--config-from` may do when the ref is unreachable.
Expand Down Expand Up @@ -1718,6 +1733,7 @@ fn default_rules() -> Vec<Rule> {
extract: Vec::new(),
landing: Vec::new(),
symbols: false,
review: Vec::new(),
delta_sources: Vec::new(),
run: None,
verbatim: None,
Expand Down
Loading
Loading