Repository navigation
feat(rules): add a command rule kind for dynamic checks - #60
Conversation
Some rules cannot be expressed as a static banned shape. Add
RuleKind::Command: an exit-code predicate that runs a configured `run`
template — 0 passes, non-zero is a violation. It never parses the
command's output for meaning, which would make it a judge (CLOUD-93).
Invocation contract (the hk idiom: glob-as-gate decoupled from
glob-as-argv):
* The glob gates first — no match skips the rule without spawning (§4).
* A bare {{files}} argument expands in place to the matched paths; a
template omitting it runs once and self-discovers.
* Matched paths are batched under a documented argv bound so a large
match set cannot overflow; batching preserves order, keeping findings
byte-stable.
* The template is split on whitespace and executed directly, never
through a shell, so what runs is exactly what a reviewer reads (§9).
* A command that cannot run (missing binary) is a config error, exit 2 —
never a silent pass.
Consumes CLOUD-170's decision rather than re-litigating it:
spawns_processes() is true, so the kind runs only under `enforce` and
`check` refuses it.
Resolves the schema tension #54 left: per-kind fields are Options on the
flat struct, with kind/field agreement validated explicitly, because a
`#[serde(flatten)]` enum would defeat deny_unknown_fields. A field
belonging to another kind is an error, never ignored.
Findings gain an optional line: a command's exit code condemns a batch,
not a line, so it reports the rule's glob without inventing one.
Dogfood: batten.toml gains a conflict-marker rule, so Batten now gates
its own repository with its own engine.
Refs CLOUD-89.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4v4gg2DRyfPAR2fXCQ3ZZ
CLOUD-89 Add a `command` rule kind for dynamic checks
Why Scope Acceptance
Refinement — Ready (invocation contract pinned, hk idiom) Decision: a File-passing follows jdx's hk idiom (see this repo's
Acceptance (adds to the above):
Note: output-string promotion (tool exits Re-scoped against the landed engine (PR #54, CLOUD-12). The engine, Prerequisite decision (owned elsewhere): the effect-model soundness question — a Tension #54 hands to this issue:
|
|
/fast-forward |
What
Resolves CLOUD-89: adds
RuleKind::Command, the sanctioned escape hatch forrules no static shape can express — an exit-code predicate (
0passes,non-zero is a violation) that never parses the command's output for meaning,
which would make it a judge (CLOUD-93).
Also dogfoods the engine:
batten.tomlgains a real rule, so Batten now gatesits own repository (consumer #1 in practice, not just in principle).
Invocation contract
Follows the hk idiom — glob-as-gate decoupled from glob-as-argv:
"cheap when irrelevant").
{{files}}argument expands in place to the matched paths; atemplate omitting it runs once and self-discovers.
MAX_FILES_BYTES,sized under Windows' ~32 KiB command line), so a large match set cannot
overflow. Batching preserves order, which keeps findings byte-stable (§6).
shell, so what runs is exactly what a reviewer reads (§9: rules "name a
command already on the operator's PATH").
2,never a silent pass.
Consuming CLOUD-170
spawns_processes()istruefor this kind, so it is routed automatically:enforceruns it,checkrefuses it (exit2, namingbatten enforce).The gate written in #58 was vacuous then — it binds now, with no edit to it.
The schema tension #54 left
Per-kind fields are
Options on the flat struct, with kind/field agreementvalidated explicitly — a
#[serde(flatten)]enum would silently defeatdeny_unknown_fields. A field belonging to another kind is an error, neverignored, so a rule can't half-apply.
Finding::linebecomes optional: a command's exit code condemns a batch, not aline, so it reports the rule's glob rather than inventing a line number.
Tests
7 new unit gates (exit-code mapping, no-match-never-spawns, missing binary,
{{files}}substitution, self-discovering form runs once, batching bound +order, kind/field agreement) and 5 end-to-end over the compiled binary
(
checkrefuses and points atenforce;enforcemaps 0/non-zero; missingbinary → 2; unmatched glob → no spawn). 46 unit + 17 e2e green;
mise run verifygreen, rebased on latestmain.Deferred (unchanged scope)
Surfacing the command's stdout needs the bounded, pointer-only drain that
CLOUD-82 owns, so streams are discarded here rather than emitted unbudgeted.
The
fixside stays with CLOUD-90; output-string promotion with CLOUD-117.Refs CLOUD-89. Builds on CLOUD-12 (#54) and CLOUD-170 (#58).