-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbatten.example.toml
More file actions
529 lines (510 loc) · 27.8 KB
/
Copy pathbatten.example.toml
File metadata and controls
529 lines (510 loc) · 27.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
# Example batten.toml — copy to your repository root and adjust.
#
# Batten reads exactly one committed authority (this file) plus raise-only
# overrides (env vars, flags, and a git-ignored batten.local.toml that may only
# tighten policy, never weaken a gate). There is no upward directory walk and no
# conf.d merge surface. See the Batten house style, §8.
# The config schema version. Required; this build supports version 1. A file
# declaring any other version is refused rather than half-understood.
version = 1
# The minimum Batten version permitted to read this file (semver). A binary older
# than this refuses to run rather than reporting green over rules it does not
# understand — exit 1, the config-error code, because "this binary is too old" is
# a statement about the invocation and never a policy verdict (exit 2). Equal or
# newer runs, and 0.0.0 accepts every build.
#
# This file is validated against the JSON Schema Batten ships
# (schema/batten.schema.json, published as a release asset), so a mistyped key is
# caught in your editor rather than first at `batten check` time. Bind it with a
# .taplo.toml rule -- see this repository's own for the shape.
min_batten_version = "0.0.0"
# How strictly gates apply: "permissive", "standard" (the default), or "strict".
# Policy-bearing, so the §8 raise-only rule governs it: an override may only
# raise it. Overrides, highest precedence first:
#
# --strictness <level> the flag
# BATTEN_STRICTNESS=<level> the env var
# batten.local.toml a git-ignored local file (may also *add* rules,
# but never redefine one declared here)
#
# Anything that would lower strictness — or redefine a committed rule — is
# refused with exit 1 rather than applied. The value committed here is also the
# floor: an override naming a level below it (or below the "standard" default
# when this key is absent) is refused, so "permissive" only takes effect when
# this file itself declares it. Run `batten config show` to see the effective
# config and which layer set each key.
strictness = "standard"
# Whether a "warn"-severity finding is promoted to a violation. Absent means
# false: a warn finding is reported and the run still exits 0. Set it and any
# warn finding from `batten check` / `batten enforce` exits 2, the same policy
# verdict a "deny" finding returns — a promotion changes which findings block,
# never which code a blocking run reports.
#
# One setting, resolved once, exposed three ways. Overrides, highest precedence
# first:
#
# --fail-on-warning the flag (no negative form: it can only turn
# the setting on, never off)
# BATTEN_FAIL_ON_WARNING=true the env var; "true"/"false" only, and an
# empty value means unset, not invalid
# batten.local.toml the git-ignored local file
#
# Policy-bearing, so §8's raise-only rule governs it: once this file commits
# `true`, no lower-precedence source can turn it back off — that is refused with
# exit 1 rather than applied. Every consumer reads the resolved value; no verb
# carries a promotion knob of its own, and `batten exec` is deliberately not a
# consumer at all (an exec output match always fails, because a warn-but-pass
# match would be invisible to an agent reading only the exit code).
fail_on_warning = false
# The three path sets policy is defined over. They overlap in practice but are
# three independent evaluators: a path's membership in each is computed from that
# key's list alone, and no membership is ever derived from another set's.
#
# scope the paths policy applies to, as an ordered include/exclude list.
# A "!" prefix marks an exclude (gitignore's spelling) and an
# exclude beats an overlapping include, whatever the order.
# protected paths whose modification is guarded. A plain include set.
# unlanded paths whose work is not yet landed. A plain include set.
#
# A "!" entry in protected or unlanded is refused with exit 1 rather than read as
# a literal glob — only scope carries exclude semantics. An absent key is the
# *empty* set, never "everything": defaulting to universal membership would widen
# policy silently, and widening is the one direction that must never be implicit.
scope = ["**", "!target/**"]
protected = ["batten.toml", ".github/workflows/**"]
unlanded = []
# `protected` earns a second job when you also declare mutating verbs: crossing
# the two is the protected-path gate (CLOUD-96), which `batten hook` applies to
# one mediated command at a time. It is an INTERSECTION, not a rule — declaring
# it as `[[rule]]` rows would need one row per verb × path pair.
#
# Each verb names the sanctioned mutation to point at when it is refused, because
# a deny that says only "no" is a refusal a caller cannot act on (CLOUD-122).
#
# `>` and `>>` are not programs: a truncating redirect has no mutating program to
# classify, since in `cat x > p` the program is `cat`. The engine surfaces the
# operator as a pseudo-program so you can gate truncation like any other verb.
# Those two tokens are a contract with the engine — `verb = "redirect"` would
# simply never match.
#
# Bounds worth knowing before you rely on this: it is a pre-execution *textual*
# gate, so a path arriving through `$VAR`, a glob, or `$(command)` is invisible to
# it, and an absolute path is compared as written rather than resolved against the
# repo root. Every such miss under-denies, which is the safe direction.
[[verb]]
verb = "rm"
effect = "destructive"
redirect = "restore it with `git checkout --`, or change it through the surface that owns it"
[[verb]]
verb = ">"
effect = "destructive"
redirect = "append, or write through the surface that owns the file"
# Rules are the output-producing half of the config: glob-selected, kind-typed
# predicates over the repository. This one ships live so a copied config detects
# something real on day one: a conflict marker that survived a botched merge.
#
# It is a `command` rule, and that is the lesson worth copying. Batten does not
# re-implement a check some tool already owns; it selects the paths, delegates,
# and decides what a non-zero exit means downstream. Write a `forbid` rule when
# no existing tool answers your question, and a `command` rule when one does.
#
# check the inspection-only command — the gate, and the only side
# enforcement ever runs. `{{files}}` expands in place to the matched
# paths, batched under the argv limit; omit it and the command
# self-discovers its own inputs. Split on whitespace and executed
# directly, never through a shell, so what runs is what you read.
# fix the optional mutating half of the same duality. Reserved: this
# build executes no fix, and refuses a rule declaring one rather
# than accepting a repair that would silently never happen.
#
# THE GLOB IS A GATE BEFORE IT IS AN ARGV SOURCE. No match means no process is
# spawned at all — so keep it to the files you actually want handed to the
# delegate, and remember that the walk currently has no gitignore: a `**` in a
# repository with a build directory hands that directory over too.
#
# The predicate is the exit code ALONE. The command's stdout and stderr are
# discarded and never parsed for meaning, so a finding reports this rule's own
# pointer — the glob, with no line number, because a command condemns a batch
# rather than a span. A `forbid` rule reports `path:line rule-id` instead.
#
# A `command` rule spawns a process, so it runs under `batten enforce` only;
# `batten check` refuses it with a usage error rather than skipping it, which is
# what keeps `check`'s read-only effect a promise rather than a hope.
#
# Two per-rule keys are deliberately separate axes (cargo-deny's model):
#
# severity what a match does — "deny" fails the run (exit 2), "warn" reports
# the finding without failing it (unless fail_on_warning above
# promotes it), "allow" switches the rule off.
# Required on every rule: the default is stated explicitly here,
# with no implicit fallback — omitting it is refused with exit 1.
# scope where the rule looks — "tree" is the whole working tree (the
# default). Scope is never severity: a severity value in the scope
# key (or the reverse) is refused with exit 1, not reinterpreted.
[[rule]]
id = "source carry broken"
kind = "command"
glob = "**/*.rs"
check = "hk util check-merge-conflict --assume-in-merge {{files}}"
severity = "deny"
scope = "tree"
# Identity keys (CLOUD-164). A finding carries a fingerprint over its normalized
# span, so a formatter reflow does not re-mint it and an inserted neighbour does
# not move it. Two optional keys tune that, and neither is needed by an ordinary
# rule.
#
# `verbatim` hashes the matched span exactly, instead of collapsing whitespace.
# For a rule whose subject IS literal content, where a reformat genuinely changes
# the thing being flagged. The default (collapse) follows `git patch-id`.
#
# `identity_key` splits one identity into several. It is split-only BY
# CONSTRUCTION: the default identity is hashed as a field of the override, so a
# discriminator can fragment a group and is unable to merge two. Changing it is a
# deliberate re-mint, not a rename.
[[rule]]
id = "no-hardcoded-banner"
kind = "forbid"
glob = "**/*.txt"
pattern = "ACME CORP"
severity = "warn"
scope = "tree"
verbatim = true
identity_key = "banner"
# A shape rule refuses a banned COMMAND rather than a banned file, so it is
# adjudicated by `batten hook` against one mediated tool call at a time and is
# never evaluated by `check`. That is what `scope` selects: "tree" is the working
# tree, "mediated_call" is the single command being mediated.
#
# `pattern` is the command line as you would say it aloud. The first word is the
# program, matched *after* wrapper look-through — so this row also refuses
# `mise exec -- terraform apply` and `env TF_LOG=debug terraform apply`, which is
# the whole reason the look-through exists. The remaining words are the adjacent
# non-flag words that must follow, so `terraform apply -auto-approve` matches and
# `terraform plan apply-notes` does not.
#
# `reason` is REQUIRED here, unlike on a file rule. A file finding is a
# `path:line` a reader can open; a mediated deny is all the caller gets back, so
# a refusal that named only its id would leave nothing to act on.
[[rule]]
id = "no-unreviewed-apply"
kind = "shape"
scope = "mediated_call"
severity = "deny"
pattern = "terraform apply"
reason = "apply runs from the pipeline, not a workstation — open a change request"
# Optional. Appended to the refusal so a caller can read the policy itself.
policy_url = "https://example.invalid/handbook/infrastructure"
# A judge rule (CLOUD-56) is the one sanctioned surface for a predicate no static
# shape can express — "this test asserts behaviour, not a tautology". It hands the
# row's `criteria` and the admitted classes of its matched files to the command in
# `[judge].run`, and reads that command's EXIT CODE ONLY: 0 clean, 2 raised,
# anything else unresolved (a judge that failed to answer is never a pass).
#
# THE VERDICT CANNOT BLOCK, and not by convention. The evidence base behind this
# decision measured model judges at AUROC <=0.65 on false-success detection, so a
# judge verdict may inform and never gate. A judge outcome is not a finding: it
# goes to the findings store as an advisory, so `--fail-on-warning` has nothing to
# promote and `batten enforce` exits 0 no matter what the judge says. Deterministic
# rules stay the default; a predicate no kind can express is an engine issue, never
# something quietly routed to a model.
#
# `severity` is REFUSED on this kind — that column decides the exit contract, and a
# judge must not reach it by any path. Declare `tier` instead: "advisory" (the
# default), "caution", or "warning", which is a required RESPONSE LATENCY, not a
# verdict. `no_fix_reason` is required, because a stored finding is one something
# later has to close and a model's opinion has no mechanical fix.
#
# Because it runs a configured command, `batten check` (a read-effect verb) refuses
# a config carrying one and names `batten enforce`.
#
# Commented rather than live, unlike every other rule in this file, and the
# reason is a property of the kind rather than an oversight: `batten check`
# refuses a config carrying a judge row outright, so a live one here would make
# the copied template exit 1 on `check` until you install a judge command.
# Uncomment it together with a `[judge].run` you actually have.
#
# [[rule]]
# id = "tests-assert-behaviour"
# kind = "judge"
# glob = "tests/**/*.rs"
# criteria = "Does each test assert an observable behaviour rather than restating the implementation?"
# tier = "advisory"
# no_fix_reason = "a judge finding is answered by a person, not a command"
# Output predicates over a wrapped command's streams (CLOUD-117): literals that,
# found in `batten exec`'s output, promote a lying exit `0` to a failure.
#
# The case this exists for: a tool exits 0 while its own output says the work is
# not really done, and has no severity knob of its own to make it fail. The only
# surface an agent acts on is the exit code, so a warning that leaves the code at
# 0 is operationally invisible.
#
# A match ALWAYS fails. There is deliberately no severity field and no dependence
# on `fail_on_warning` above: a "warn-but-pass" match would reproduce the very
# false green this predicate exists to kill. You declare a pattern here precisely
# because the string means not-actually-done.
#
# `pattern` is a case-sensitive literal substring — the same predicate a `forbid`
# rule uses, and a shape a reviewer can read without evaluating it. `stream` is
# `stdout`, `stderr`, or `both` (the default, and the widest reading: a tool that
# moves a warning between streams must not silently stop being gated).
#
# `reason` is REQUIRED. A promoted exit code is the whole of what a caller gets
# back, so a refusal naming only its id would be un-actionable — and the report is
# pointer-only (`stream:line <id>`), never the line that matched, because a
# wrapped command's output is the likeliest place for a secret to appear.
#
# REACH FOR SOMETHING ELSE WHEN: the tool has its own severity model (configure
# that — do not rebuild it as output-scraping); you want to gate a file's contents
# (a `command` rule over a glob); or you want to promote an existing Batten
# finding (`fail_on_warning`, which does not apply here).
#
# A git-ignored batten.local.toml may ADD a pattern — one more way to catch a
# lying command is tightening — but may not redefine one declared here.
[[exec_pattern]]
id = "no-unfailed-duplicate"
pattern = "warning[duplicate]"
stream = "both"
reason = "set the tool's own severity to deny; do not let a warning ride an exit 0"
# The designed escape hatch: a per-rule waiver that lapses on its own.
#
# A gate with no override gets removed, and a gate with an unaudited override
# becomes decoration — so this is a feature rather than a concession. A waiver
# names the `rule` it suppresses, carries a REQUIRED `reason`, and carries a
# REQUIRED `expires` date (`YYYY-MM-DD`, the last day it applies). Every finding
# it suppresses writes an audit line to stderr; stdout is untouched, so a `-J`
# document never mentions a waiver.
#
# EXPIRY IS THE POINT, not paperwork. A justification and a dead-waiver lint both
# leave "the waiver persists" as what happens when nobody looks. An expiry inverts
# that: past its date the waiver simply stops applying, the rule fires again, and
# the run exits 2. Nobody has to notice for the suppression to lapse.
#
# `path` is optional and narrows the waiver to some of the rule's findings, using
# the same glob vocabulary a rule's own `glob` uses. Absent means the whole rule —
# the widest a waiver can be, which is why it has to be reached deliberately.
#
# `batten config lint` names three dead shapes: `waiver-names-no-rule` (no
# [[rule]] declares that id), `waiver-expired`, and `waiver-unreachable-kind` —
# the rule exists and its `kind` produces no finding to suppress, which is every
# `shape` and `receipt` row (those are adjudicated per mediated call) and every
# `judge` row (advisory, so it never becomes a finding at all).
# And `--config-from <ref>` reports an
# added waiver as `waiver-added`: it is the one config entity whose PRESENCE
# lowers the bar, so a branch that adds one is naming a weakening.
#
# A waiver is not a severity. `severity = "allow"` switches a rule off with no
# reason and no end date; that is the undesigned hatch this table replaces.
#
# A git-ignored batten.local.toml may NOT waive a rule declared here — a waiver
# lowers the bar, so the durable tier is the committed authority alone (§8).
[[waiver]]
rule = "source carry broken"
reason = "the vendored tree is being replaced in CLOUD-123; gating it churns the diff"
expires = "2026-12-31"
path = "vendor/**"
# The governing config surface: the files whose bytes `batten config epoch`
# hashes into one attributable value, so two records carrying the same epoch
# were produced under provably the same rules.
#
# List the files that actually govern YOUR repository — an agent settings file,
# a contributor guide, a hook config. Order does not matter (the set is sorted
# before hashing) and a duplicate is ignored. Absent, or empty, means the
# default: this file alone, which governs every consumer by definition.
#
# Every listed path must be readable. An unreadable one exits 1 — the config-error
# code, because this list is config, so a tracked path that cannot be read is
# unreadable config rather than a fault of Batten's own (which would be 3). It is
# refused rather than skipped: a skip would compute a stable epoch over a surface
# that changed, which looks exactly like a valid answer.
[epoch]
tracked = ["batten.toml"]
# ---------------------------------------------------------------------------
# Side effects attached to hook events (CLOUD-91) — house style §9's extension
# surface, so repo-specific cleanup or keepalive is reconstructed here instead of
# being carried by the engine.
#
# Each row names an event and a command already on your PATH. `run` is argv, not
# a shell string: there is no quoting layer between what you write and what runs,
# so no word splitting, no glob expansion, no $(...).
#
# `on` takes a NORMALIZED event token, never a host's own spelling — declare it
# once and it fires on every host that offers the moment:
#
# post-tool after a tool ran
# stop end of turn
# session-start start of a session
# task-completed a task was marked complete (Claude-only; nothing fires on
# hosts that do not emit it)
# config-change a settings file is being edited (Claude-only)
#
# `pre-tool` is deliberately NOT declarable. It is the event policy adjudicates,
# so a side effect there would run before a deny that may be about to refuse the
# very call — and it is the hottest path in the binary, which stays config-free
# when there is nothing to judge.
#
# Three properties worth relying on:
#
# * An action can never change the answer. The decision and exit code follow
# the hook contract whatever the command does; a failure is reported as
# `hook.action <id>: exit N` on stderr and the command's own output is
# discarded, never forwarded.
# * A host that does not emit the event fires nothing. There is no degrading
# to a stand-in event: observing a moment approximately is reasonable,
# *doing something* at a moment you did not name is not.
# * These rows may only be declared in this committed file. A batten.local.toml
# that could add one would be able to run anything under your agent's hook.
#
# argv words may carry {event}, {tool}, {path} and {session}; a fact the event
# does not carry expands to nothing. An unknown placeholder is left as written
# rather than emptied, so a typo fails loudly instead of silently shortening argv.
#
# The worked example: report at-risk work when a task is marked complete — the
# completion signal is exactly where "done" and "landed" drift apart, which is
# what this engine exists to catch.
[[hook.action]]
id = "worktree-hygiene"
on = "task-completed"
run = ["batten", "worktree", "status"]
# ---------------------------------------------------------------------------
# Pinned tools this repository provisions (CLOUD-90).
#
# One entry per tool: a pinned version, where to get it, what it must hash to,
# how to unpack it, and the binary it yields. House style §9's rule is that
# consumer-specific behaviour is reconstructed through config rather than baked
# into the core, and this is that surface for binaries.
#
# TWO VERBS, one pair. `batten provision status` is the freshness gate and is
# read-effect; `batten provision apply` is the write that fixes it. The gate is
# `read` for a structural reason rather than a behavioural one: the whole
# equality test is a checksum over cached bytes, so THE PROVISIONED BINARY IS
# NEVER EXECUTED. A freshness check that ran `--version` would be a read verb
# executing an artifact fetched from the internet.
#
# `apply` fetches into memory, verifies against `sha256`, and only then writes.
# A mismatched artifact never reaches the cache, so there is no partial install
# and no window where a bad binary sits under a good name. The mismatch is exit
# 2 — a verdict about the pin — while an unreachable URL is exit 3, which is a
# different claim: the apply could not complete, rather than one it did make.
#
# `version` is a cache PATH SEGMENT, so two pins coexist and a version bump is a
# cache miss rather than an overwrite. The cache lives outside the repository
# (the OS data directory), so nothing provisioned is ever committed — a property
# of where the path resolves, not a promise to be careful.
#
# `url` takes `https://` or `file://`. Plain `http://` is absent deliberately: a
# pinned checksum makes tampering detectable, not impossible to attempt, and
# there is no reason to fetch a pinned artifact over a rewritable channel.
# `file://` is what makes a hermetic fixture possible.
#
# The https fetch runs the host's `curl`, so host CA configuration keeps working
# behind a TLS re-terminating proxy — it IS the host's default TLS stack rather
# than a re-implementation of one. That is also §9's posture: name a command
# already on the operator's PATH, never a downloaded, executed binary.
#
# `unpack` is "none" (the artifact is the binary) or "tar_gz". A tarball entry is
# matched on its FILE NAME, so a release that nests its binary under a versioned
# directory needs no second statement of the version here.
# A real, verifiable entry rather than an illustrative one: this URL and this
# digest are the actual jq 1.7.1 linux-amd64 release binary, measured. A worked
# example carrying a made-up checksum teaches the shape and mis-teaches the
# discipline — the pin is the whole security property, so a fake one is the one
# field that must not be faked.
[[provision]]
name = "jq"
version = "1.7.1"
url = "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64"
sha256 = "5942c9b0934e510ee61eb3e30273f1b3fe2590df93933a93d7c58b81d19c8ff5"
# The artifact IS the binary — no archive to open.
unpack = "none"
binary = "jq"
# --- design-evidence audit ---------------------------------------------------
#
# `batten design audit` reads a JSONL stream of typed claim records on stdin and
# nothing else — no path key here, because stdin subsumes one: a corpus that is a
# file in your repository reaches the gate as `batten design audit < corpus.jsonl`,
# with no config and no credential. That keeps the gate offline, byte-stable, and
# testable with no live data.
#
# The gates are exact comparisons over the record's typed fields, never a reading
# of claim prose: a duplicate id, a claim recorded `verified` whose `polarity` is
# `absence` (no capture can attest that nothing does X), bytes that do not hash to
# the recorded digest, a status past `claimed` with no verifier named, and a
# declared `byte_count` that disagrees with the bytes carried. Four more are
# advisories: no claimant recorded, a verifier who is the claimant, a `verified`
# claim whose digest binding cannot be decided, and a capture over the ceiling
# below. Advisories promote to violations under `--strictness strict`.
#
# The one key is that ceiling, in bytes, per capture. Absent means 16384. Lower it
# to make the gate stricter; a local file may lower it further and may not raise
# it, since for a budget "may not weaken" means "may not raise".
[design]
max_capture_bytes = 16384
# --- the judge's boundary and its command ------------------------------------
#
# What may cross into a model call (CLOUD-135), and what to call. The table is
# optional; without it a `judge` rule cannot resolve a command and is refused at
# load, which is the intended direction — a judge you did not configure never runs.
#
# Every field defaults to the REFUSING reading, because the judge's verdict is
# advisory and cannot block: content that crosses this boundary buys a signal
# that is structurally unable to gate, which is an argument for a refusing
# default rather than a careful one.
#
# `raw` names the content classes admitted as bytes: "span_text", "file_text",
# both, or (the default) neither, which sends pointers and hashes only. There is
# deliberately no "all" spelling, so the diff that widens the boundary shows what
# it widened.
#
# NOT configurable, deliberately: what happens over the `protected` set. One
# protected file among a row's matches refuses the WHOLE invocation — not just
# that file — because silently sending a subset means the verdict is about
# content the row never described. An opt-in key here is a recorded rejected
# alternative, not an omission.
#
# `max_payload_bytes` is the ceiling on assembled payload bytes; absent means
# 16384. Over it the invocation is refused whole rather than truncated, since a
# truncated payload judges a prefix while the record says it judged the row. A
# local file may lower it and may not raise it.
#
# `run` is the command, as a template. Its first whitespace-separated token is a
# program on your PATH, executed DIRECTLY and never through a shell, so this is
# data a reviewer can read rather than a script. The payload crosses on the
# command's stdin — never argv, which is world-readable process state.
#
# `model` is opaque to Batten: never parsed, never validated against a list. It is
# substituted wherever `{{model}}` appears in `run`, and naming the placeholder
# without setting this key is refused rather than passed through literally.
[judge]
raw = ["span_text"]
max_payload_bytes = 16384
run = "my-judge --model {{model}}"
model = "some-model-id"
# THE BATTERIES: policy presets, compiled into the binary.
#
# Every rule above is one predicate over one object. A `policy` rule is a Rego
# module deciding over the whole resolved fact set, which is what makes a
# predicate over the RELATIONSHIP between facts expressible — the engine's rule
# loop is flat, so no row can consume another's verdict.
#
# You do not have to write one to get value from them. Batten ships modules for
# common practices, and enabling one is a rule row naming it. `batten config show`
# lists what this binary carries; today that is `ci-hygiene`, `commit-hygiene`,
# `landing-loop`, `pinned-toolchain`, `shell-hygiene` and `trunk-based`.
#
# SCOPE IS THE FIELD TO GET RIGHT. A preset's modules read one surface's facts:
# `tree` for the working tree, `mediated_call` for the single command a hook is
# adjudicating. Enabling one at the wrong scope is refused at LOAD, naming both —
# it does not quietly evaluate and decide nothing.
#
# A preset is deny-only, like every module, so switching one on can only ADD
# refusals. There is no network and no registry: the bytes ship inside the binary
# you already trust, under the same checksum as the rest of it.
#
# Uncomment to enable one:
#
# [[rule]]
# id = "trunk-based"
# kind = "policy"
# scope = "mediated_call"
# preset = "trunk-based"
# severity = "deny"