Batten is an agent-era completion gate: repo-state conformance checks — is
this ref on main? did the required checks conclude green for this exact SHA? —
enforced at the agent's tool call, and re-checked in CI and at pre-commit so the
verdict cannot be bypassed.
Your agent says "done." The repository knows otherwise: the branch never landed, the checks never ran on that SHA, the tests were edited until they passed. Batten is the deterministic check at the moment of the claim — the throughput stays, the false "done" dies.
"Done" here is the minimum falsifiable completion predicate: landed on
main by fast-forward, with the required checks green for that exact SHA. It
kills false success. It does not certify correctness, and review still gates
release.
Status: early scaffold. The command surface is being filled in against the project plan; see Roadmap. The crate is not yet published to a registry, so install from a release archive — distribution to a registry is a recorded, deferred decision on the project board.
Binary first: a release archive holds a single static executable, and every
package manager below is a convenience over the same asset. One line, every
environment — curl and tar are the only requirements, so no Rust toolchain,
no package manager, no clone, and nothing harness-specific:
curl -fsSL https://raw.githubusercontent.com/button-inc/batten/main/install.sh | shIt detects your platform, downloads that target's archive, verifies it against
the SHA-256 digest the release reports and refuses to install on a mismatch,
and puts batten in ${XDG_BIN_HOME:-$HOME/.local/bin}. BATTEN_VERSION
selects a tag other than the latest, BATTEN_INSTALL_DIR a different
destination, and BATTEN_TARGET overrides platform detection — Linux resolves to
the statically linked musl build, which runs whatever the host's glibc version.
The script comes from main and the binary comes from the latest release, so
what installs is a tested artifact rather than a branch tip.
Three behaviours matter wherever that line runs unattended — a CI runner, a container's setup step, an agent sandbox:
- A token is read if one is set, from
BATTEN_GITHUB_TOKEN,GH_TOKEN,GITHUB_TOKENorGITHUB_PERSONAL_ACCESS_TOKEN, in that order. A public release needs none of them. This repository is private today, so a fetch needs a token with release-read scope until that changes; a host carrying several tokens that are not equivalent names the working one throughBATTEN_GITHUB_TOKEN, which wins. - A proxy that re-terminates TLS is handled by honouring the CA bundle the
environment already declares —
CURL_CA_BUNDLE, elseSSL_CERT_FILE. Nothing is disabled and an unproxied host is untouched. Set one of those rather than reaching forNO_PROXY. - Requests retry with backoff (
BATTEN_RETRIES, default 3) and carry connect and total timeouts, so a transient failure is retried and a hung connection fails rather than hanging the caller.
Installing somewhere not on PATH is a refusal, not a warning: every hook
registration names batten bare, so a binary the shell cannot resolve is
indistinguishable from no binary. Point BATTEN_INSTALL_DIR at a directory on
PATH, or set BATTEN_ALLOW_OFF_PATH=1 when a staging destination is deliberate.
cargo binstall reads the same assets through [package.metadata.binstall]:
cargo binstall --git https://github.com/button-inc/batten battenThe plain cargo binstall batten form needs the crate on a registry, which the
distribution decision defers.
Binaries are never committed to this repository; they come from a release, and
mise run install-check is the gate that keeps every reader of an asset name
agreeing with the one that writes it.
Repo-config-driven conformance gates that can judge an agent's tool call
before execution are new, and earlier tooling was built for humans at commit
time rather than agents operating mid-trajectory. The hook layer itself is
deliberately boring: the major harnesses have converged on one wire shape — a
JSON payload on stdin, a block returned as exit code 2, a JSON verdict on
stdout — so Batten's normalized envelope and thin per-host shims are cheap
insurance against divergence, not the product. Batten's own layer is the one
behind the hook: one engine rendering the same verdict from the same committed
config at the agent's tool call — and again in CI and at pre-commit, so the
verdict cannot be bypassed — with completion predicates (landed, verified,
CI-green) as first-class rules.
The hook is the binding surface because it fires on events the agent cannot decline, and because it reads committed, out-of-band config that the model's context cannot influence. Any surface the model must choose to consult loses to the primitive it already trusts.
A gate an agent routes around is a gate that does not run, and what agents route around is expense. Three pains compound in an agent's context, and a tool that answers with a dump makes every one of them worse:
- Tail-calling. The output did not fit, so the agent runs the command again to see a different slice — paying twice for one answer, often for the wrong slice.
- Lost-in-the-middle. A two-thousand-line dump buries the one line that mattered exactly where retrieval is weakest.
- Context rot. Every avoidable byte crowds out the working state the agent needs to finish the task it was actually doing.
Batten's output contract answers all three at once. A finding is a pointer, not
a payload — a count and a path:line, never the matched content — so a wrapped
tool's two thousand lines become one. Output is byte-stable, so an unchanged
repository renders identical bytes and the agent's prefix cache stays warm instead
of being invalidated by a reordered map or a timestamp.
And a refusal points at the fix — the class name IS the pointer. A mediated
deny emits one line: a declared three-word class and the pointers it applies to,
as in shell edit refused mise-tasks/land.sh:845. The reason, the routes out
(the escape hatch and the override alike) and the class's full definition are
one hop away, at batten policy explain "shell edit refused".
That is a decision rather than an omission, and it is the same argument as the
three pains above turned on the tool's own output. The reason and the remedy do
not change between firings, and a mediated refusal fires hundreds of times in a
long session, so inlining them means paying per firing for text that was
declared once. The name carries the class because the names are a declared
vocabulary rather than free text — three positional words, each glossed — which
is what makes one hop cheap and the elision honest rather than merely shorter.
The pointer, which DOES change per firing, stays inline: this shortens the
prose, never the operand a reader acts on. The ceiling on that line is declared
in batten.toml and gated, so "one line" is a property of the data and not of
an author's restraint.
Magnitude belongs to the benchmark, not to this page. A benchmark is the proof, measured per capability against a named workload with a stated baseline and run count. No figure is published here until it has been measured that way; a capability with no defensible number reports "not measured" rather than borrowing one.
- Agent-neutral operation rather than a bespoke interposition layer.
- Deterministic verification with byte-stable machine-readable output — the same input produces identical bytes, keeping agent caches warm.
- Rules ship with their mechanism. A prose rule without a gate is feedforward-only; a defect log without a gate is sensor-only. Both are half a harness.
- The CLI is data. A single usage spec is the source of truth for
completions, man pages, and markdown, and effects are annotated once and
reused (the agent read-only allowlist is derived from those annotations and
emitted by
batten specasread_only_allowlist, so a consumer reads it rather than re-deriving it). - Narrow configuration. A two-layer TOML model — a repo file plus env and
flag overrides — with no upward walk and no
conf.dmerge surface. - Opt-in scaffolding.
batten initwrites a starterbatten.tomland refuses to overwrite an existing one;batten doctor --jsonchecks what it wrote.checkon built-in defaults is still planned (CLOUD-70) — today it requires abatten.tomlin the working directory, which is what makesinitthe first command a new repository runs. - Gates decide, they do not estimate. A predicate that only approximates its own question — a model judgement, or a match over open-ended content — may advise; it never blocks.
- Consumer #1 is Batten itself — its own checked-in
batten.tomlruns against its own repository.
Batten evaluates narrow content predicates and wraps linters, scanners, and hook runners as evidence sources — a rule kind exists to gate on a tool's verdict, never to replace the tool, so the boundary holds even as rule kinds grow. Its threat model is honest agent or human error: acting on the wrong entity, at the wrong time, or with the wrong completion signal — where "wrong entity" means a call's argument values, judged against committed, out-of-band config. It cannot reliably catch attacker- or error-chosen parameters of otherwise-permitted calls, harmful composition of individually legal steps, cross-session poisoning, or errors in its own spec. And a green check certifies exactly its predicates, nothing more: over-trusting it is the misuse cost of any gate that works, which is why review still gates release.
mise install # the pinned toolchain — see CONTRIBUTING.md for one-time setup
mise run ci # the same gate CI runsEverything goes through mise tasks so local runs, git hooks, and CI execute byte-identical commands; CONTRIBUTING.md has the per-clone setup and the task tour.
Batten runs as a PreToolUse hook, so its cost is paid on every mediated
tool call rather than once per commit. These are measured numbers, not targets —
mise run perf reproduces them, and mise run perf-assert fails when a
measured p95 leaves its budget.
wired is the number an agent actually waits on: the entry point
.claude/settings.json invokes, launcher included, derived from that file at
measure time rather than hardcoded — so the published figure describes what is
installed rather than a binary nothing calls. hook stays measured beside it so
the launcher's own share is attributable.
| path | what it does | p50 | p95 | budget |
|---|---|---|---|---|
noop |
process start, command tree, render | 2.1 ms | 2.4 ms | ≤ 100 ms |
check |
+ config load, trust resolution, one-rule tree | 2.3 ms | 2.7 ms | ≤ 100 ms |
hook |
+ envelope decode, adjudication, decision write | 2.8 ms | 3.0 ms | ≤ 100 ms |
passthrough |
a call no rule selects — decode, allow, no config load | — | — | ≤ 100 ms |
posttool |
a PostToolUse call — decode, capture the response | — | — | ≤ 100 ms |
wired |
the hook as .claude/settings.json invokes it |
8.0 ms | 8.4 ms | ≤ 100 ms |
posttool is the same envelope decode plus the response capture, and its cells
are — for passthrough's reason: the series above comes from a quieter
machine, and a figure from a noisier one published in that column reads as a
step change rather than as a different room. Measured against its own
contemporaneous hook on one container, 100 runs each, the capture costs
1.25x at p50 and 1.12x at p95 — 16.3 ms and 21.1 ms against that machine's
own 13.0 ms and 18.9 ms for hook, so about 5x inside the 100 ms budget on
a noisy container and further inside it on the machine the table above reports.
perf-compare reads a different pairing and gets a different number, and both
are honest: it runs this arm against the MERGE BASE's binary, which performs no
capture at all, so its 1.806x prices the feature rather than a drift in the cost
of the same work. That arm carries an exemption in perf-compare until main
holds a capturing binary and the comparison is like with like; the absolute
budget is what gates it in the meantime.
100 ms is the Command Line Interface Guidelines' floor for a response
that reads as instant. It is an absolute ceiling rather than a tight band around
the measured value: a shared runner's p95 moves by more than a percentage band
between two runs of identical bytes, so a tighter gate would fire on noise
instead of on regressions. check is measured and deliberately not budgeted —
its cost is bounded by the repository it is pointed at, not by Batten, and no
ceiling here could tell a large tree apart from a regression.
Measured 2026-08-12 on a 4-core x86_64 Linux container: release build, 10 warmup runs discarded, 100 timed runs per path, p95 from the sorted run times. Your machine will differ; the budget is what the gate holds, and the schedule in
.github/workflows/perf.ymlis what keeps holding it.
A correction, kept because the mistake is the instructive part. The previous revision of this table published
hookat 16.6 ms and blamed a concurrently-loaded container. That explanation was wrong. The cost was a real regression — onereceiptrow made every mediated call resolve receipts, four git subprocesses' worth, including calls no receipt rule could ever match (CLOUD-460). Measured on the command that exposed it, the fix is 3.44× ± 0.30 faster: 9.4 ms → 2.7 ms. It sat inside the ≤ 100 ms budget the whole time, so no gate went red — which is exactly why a wrong explanation in a performance note is worse than none: it tells the next reader the number is environmental and not to look.
One table, total, with no per-verb exception. batten --help prints the same
table, and each meaning below is asserted against the binary's own rendering, so
this section cannot drift from the codes the binary returns.
| Code | Meaning |
|---|---|
0 |
clean — nothing to report; a mediated call is allowed |
1 |
config or usage error — fail loud, do not block |
2 |
policy verdict — a violation, or a mediated call denied |
3 |
internal error — fail loud, do not block |
The numbering is chosen so the mediation channel needs no translation: hosts
with a pre-tool hook read 0 as allow, 2 as deny with stderr as the reason,
and anything else as "the hook itself failed, let the call through". A deny and
a violation share a code because they are the same kind of answer, and
failing open is structural — the only codes a Batten failure can produce
are ones every harness already treats as non-blocking.
The channel varies by harness even though the number does not: a host whose
only decision channel is process status is denied by exit 2, while a host
that reads an in-band decision document is denied by that document with exit
0.
batten doctor is the post-install self-check: it reports whether Batten can
run in this repository, with --json for a byte-stable machine reading. It is a
diagnostic, so it never returns 2 — every failure it can report is the
config-or-usage class, and a harness must never read "this checkout is
misconfigured" as a policy denial.
Any predicate you can express as a command plus an exit code is expressible in Batten — and most of them need no command at all. The failure mode is picking the wrong kind, so the boundary matters more than the mechanics.
Reach for the narrowest kind that fits. A command rule spawns a process,
which can read any file and reach the network; every other kind is decided from
facts the boundary already resolved. That is why a command rule runs only under
batten enforce while the rest are admitted to the read-only check surface.
| What you are gating on | Reach for | Where it is configured |
|---|---|---|
| A literal string banned from matched files | forbid |
[[rule]] with kind="forbid" |
| A file's contents, judged by a program you supply | command |
[[rule]] with kind="command" |
| A command line an agent is about to run | shape |
[[rule]] with kind="shape" |
| A count that must not grow — a budget you are paying down | ratchet |
[[rule]] with kind="ratchet" |
| Whether a verification receipt exists and still answers | receipt |
[[rule]] with kind="receipt" |
| The shape of a pipeline — how a call is composed | pipeline |
[[rule]] with kind="pipeline" |
| A judgement a model makes, recorded with its own no-fix reason | judge |
[[rule]] with kind="judge" |
| Credentials reaching a file, via a pinned scanner | secrets |
[[rule]] with kind="secrets" |
| A document's own structure | document |
[[rule]] with kind="document" |
| A relationship between facts no single row can express | policy |
[[rule]] with kind="policy" |
Two surfaces are not rule kinds and are configured on their own:
| What you are gating on | Reach for | Where it is configured |
|---|---|---|
| A command's output, when the tool lies about exit 0 | exec output predicates |
[[exec_pattern]] |
| An existing warn finding, to make it block | fail_on_warning |
a top-level key |
The other kinds are each 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 at all — the engine's own rule loop
is flat, and no row can consume another's verdict.
[[rule]]
id = "no-orphan-workflow"
kind = "policy"
scope = "tree"
sources = [".github/workflows/*.yml"]
module = "policy/no-orphan-workflow.rego"
severity = "deny"A module is deny-only by construction — there is no allow spelling — so
enabling one can never weaken policy, which is what preserves the raise-only
invariant above. A refusal it raises is {rule, verdict, subjects}, and the
verdict is a declared class rather than free prose, so
batten policy explain <class> reaches the remedy from any refusal.
Batten ships policy modules for common practices, compiled into the binary. They are the reason a new repository does not have to author every predicate from scratch — the same shape Conftest, Semgrep, ESLint and Clippy all take.
[[rule]]
id = "trunk-based"
kind = "policy"
scope = "mediated_call"
preset = "trunk-based"
severity = "deny"batten config show lists the presets this binary ships. Each declares the
scope its modules decide, and enabling one at the other scope is refused at
load rather than quietly deciding nothing.
There is no network and no registry: a preset's bytes ship inside the binary you already trust, under the same checksum as the rest of it.
Everything a consumer adds is raise-only (§8): a git-ignored
batten.local.toml may add a rule or a pattern, never redefine or remove one the
committed authority declares. A weakening is refused with exit 1, not applied.
glob selects the files, check names the command, and {{files}} is
substituted with the matched paths. Exit 0 passes; any non-zero exit is a
violation.
[[rule]]
id = "single-entrypoint"
kind = "command"
glob = "src/**/*.rs"
check = "./scripts/one-entrypoint {{files}}"
severity = "deny"The key is check rather than run because the kind carries a check/fix
duality (§9): check is the inspection-only gate, and an optional fix names
the mutating command that repairs what it condemned. Enforcement is always the
check side. fix is reserved, not yet executed — serialised fix execution is
not a capability this engine has, so batten enforce refuses a rule declaring
one with a usage error rather than accepting a repair that would silently never
run.
A command rule runs under batten enforce only. batten check refuses it
with a usage error rather than running it, which is what keeps check's
read-only effect honest — the read-only surface never reaches user-supplied code.
Don't reach for this when you want to gate a command's output: the child's streams are discarded here, deliberately. The exit code is the whole predicate.
For a tool that 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.
[[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"$ batten exec -- cargo deny check
stdout:14 no-unfailed-duplicate
exec: 1 output match(es)
no-unfailed-duplicate: set the tool's own severity to deny; …A match always fails. There is no severity field on a pattern and no
dependence on fail_on_warning, because the only surface an agent acts on is the
exit code: a warn-but-pass match would be invisible to it, which is the exact
false green the predicate exists to kill.
Batten only ever adds failure — a child that already exited non-zero passes its code through untouched.
Don't reach for this when the tool has its own severity model. Configure that instead; re-implementing a tool's severity as output-scraping is the thing this surface should not become.
A warn-severity rule reports and does not fail the run. fail_on_warning
promotes it, and it is the only promotion knob: no verb carries its own.
fail_on_warning = truebatten exec is deliberately not a consumer — an exec output match already
fails unconditionally, so there is nothing for a promotion to promote.
| Not promoted | Promoted | |
|---|---|---|
a warn finding from check/enforce |
exit 0 |
exit 2 |
an exec output match |
— | exit 1 |
Read that table as two questions rather than one question with two answers. A rule
finding is a policy verdict about the repository, which is exit 2 on every
surface that renders one. An exec output match is a statement about the
invocation — the wrapped command reported success while its own output betrays
that it is not done — which is what exit 1 means in the table above. Neither is
an exception to §7; they are §7 applied to two different claims.
The codes are not interchangeable here, and 1 is not a placeholder for a 2 that
would be tidier. exec is a transparent passthrough, so every other code on that
channel is the child's, and the one thing that makes such a channel readable is
that Batten never mints a 2 on it: a 2 out of exec came from the wrapped
command, and nothing can mistake it for a verdict. Renumbering an output match to
2 would spend that guarantee for a symmetry the table does not ask for. Decided
on CLOUD-292, with the three rejected alternatives recorded there; crates/batten/src/exit.rs
carries the reasoning and crates/batten/tests/it/extension_surfaces.rs gates it.
This is a project-wide law, not a quirk of one adapter. Every check Batten runs
reports a count, a path:line, or a boolean — never the bytes it read. A
forbid finding names the line number and not the line; an exec match names
stream:line <id> and not the matched text; a command rule's child has both its
streams discarded, so there is nothing to leak in the first place.
The reason is that a policy engine reads the places secrets live. Findings travel
into CI logs, PR comments and model context, and a check that echoed what it
matched would publish the thing it was gating. So the law binds your checks
too: a command rule you write should exit non-zero and say where, not print what
it found.
$ batten check
src/config.rs:41 no-hardcoded-token # the pointer
# never the tokencrates/batten/tests/it/pointer_only.rs decides this rather than asserting it. It
seeds a corpus in which every byte a check can read is a unique canary, runs
every leaf verb of the command surface over it, and fails if a canary reaches
either channel. A verb added to the surface must declare which side of the law it
sits on before the suite will pass, so the guarantee stays total as the surface
grows.
crates/batten/tests/it/extension_surfaces.rs runs each command in this section
against the compiled binary and asserts the exit code it claims. A drifted example
fails CI, so this documentation cannot rot into fiction.
The shipped Action works with an empty with: block — that is the bar it is
held to, not a convenience it happens to offer:
- uses: actions/checkout@v5
- uses: button-inc/batten@v0.0.61That runs batten check in the workspace against the batten.toml committed
there, and fails the step on a policy verdict. Every input has a default, and the
defaults are the useful configuration.
The version is not a fourth thing to keep in sync: with version unset the Action
reads its own crate version from the Cargo.toml beside it, so the ref you
pin selects the binary. @v0.0.61 runs Batten 0.0.61. There is no latest
resolution to let the two disagree, and a tag whose release published no asset
fails loudly rather than substituting another version.
| input | default | meaning |
|---|---|---|
command |
check |
the verb to run |
args |
"" |
extra arguments, split on whitespace |
working-directory |
. |
where the verb runs, and therefore which batten.toml governs |
stdin |
"" |
text piped to the verb; hook reads its payload there |
version |
"" |
empty means this action ref's own version |
github-token |
${{ github.token }} |
reads the release asset |
cache |
true |
restore and save the downloaded binary |
fail |
true |
fail the step on a non-zero code; false reports it as an output |
| output | meaning |
|---|---|
exit-code |
Batten's code, under the one contract in the table above |
version |
the version that ran |
binary |
absolute path to the binary, for a later step to invoke |
fail: false is how a caller asserts an exact code rather than merely "the
step went red" — the run continues and the code arrives on exit-code:
- uses: button-inc/batten@v0.0.61
id: batten
with:
fail: false
- run: test "${{ steps.batten.outputs.exit-code }}" = "2"Which matters most where 2 sits: a violation and a denied mediated call are the
same code because they are the same kind of answer, so a caller that wants to
tell "policy said no" from "Batten could not run" is reading 1 and 3, not the
absence of 0. The Action's own self-test asserts all four exactly
(.github/workflows/test.yml), including the 3 that a file Batten cannot read
produces — a check that could not look is not a check that found nothing.
hook adjudicates one command rather than walking a tree, and it reads the
harness payload on stdin — which is what the stdin input is for:
- uses: button-inc/batten@v0.0.61
id: guard
with:
command: hook
args: --harness exit-code
stdin: '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"gh pr merge 42"}}'
fail: false
- run: test "${{ steps.guard.outputs.exit-code }}" = "2"With stdin empty the Action closes the descriptor rather than leaving it open,
so a verb that reads standard input ends at EOF instead of blocking the job.
The Action does not prepend its install directory to PATH. A $GITHUB_PATH
write changes how every later step in the job resolves a command, which is a
hazard disproportionate to the convenience; use the binary output instead.
The downloaded binary is cached under
key: batten-<version>-<target>
path: ~/.cache/batten/<version>/<target>
where <target> is the Rust target triple the runner maps to — x86_64/aarch64
-unknown-linux-musl on Linux (the statically linked build, so it runs on any
image regardless of glibc), -apple-darwin on macOS, x86_64-pc-windows-gnu on
Windows.
Both the version and the target are in the key and there are no restore keys:
a near-miss would restore a different version's binary under this one's name,
which is precisely the confusion a policy engine must not create. An entry is
therefore valid for exactly as long as its version is. Set cache: false to skip
restore and save entirely; the download is a little over a megabyte.
Nothing above requires the Action. The same thing by hand, with no third-party action in the path:
- name: Install Batten
env:
GH_TOKEN: ${{ github.token }}
VERSION: 0.0.61
TARGET: x86_64-unknown-linux-musl
run: |
set -euo pipefail
asset="batten-v$VERSION-$TARGET.tar.gz"
id=$(gh api "repos/button-inc/batten/releases/tags/v$VERSION" \
--jq ".assets[] | select(.name == \"$asset\") | .id")
gh api "repos/button-inc/batten/releases/assets/$id" \
-H "Accept: application/octet-stream" > "$asset"
tar -xzf "$asset" -C /usr/local/bin batten
- run: batten checkThe asset name is a contract, not a convenience — mise-tasks/dist.sh builds it and
the release workflow uploads it under exactly that name — so this stays correct
independently of the Action.
github-token defaults to ${{ github.token }}, which needs contents: read.
Inside this repository that is enough to read a release asset. From another
repository it is not: the job token is scoped to the repository running the
workflow, so a consumer reading releases on button-inc/batten from elsewhere
passes a token of their own.
The note worth carrying past the install step: events created with
GITHUB_TOKEN do not trigger further workflow runs. GitHub suppresses them
deliberately, to stop a workflow recursing into itself. So if you wire Batten's
result into something that pushes a commit, opens a pull request, or files an
issue, that downstream event will start no workflow of its own while the default
token is in play. Reaching for a PAT is the documented way around it, and it is a
deliberate choice with the recursion the suppression exists to prevent — not a
configuration detail.
.github/workflows/test.yml checks this repository out into a subdirectory,
materializes a fixture repository from crates/batten/tests/fixtures/repos/ at
the workspace root, and invokes the Action with no with: key at all — so the
empty-with: claim above is executed on a real runner rather than asserted here.
Work is tracked on the project board across phases:
- Pre-implementation blockers and decisions
- Foundation — scaffold, config, core extraction, fixtures
- Contracts and checks — CLI contract and rule/check engine
- Enforcement and capabilities — hook layer, advisory subsystem
- Packaging and distribution
- Consumer #1 migration
Licensed under the Apache License, Version 2.0. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.