Agent-diff integrity gate built in Rust: it holds a change to its tests, its safety justifications, its policy and its verification harness. It reads the structure of a change and the policy around it, not what the code does at run time; Non-Goals lists what that leaves out. One static binary, identical in GitHub Actions, GitLab CI/CD, Forgejo Actions, Gitea Actions, Argo Workflows, pre-commit hooks, and an agent's local inner loop. Interactive guides and documentation: orieg.github.io/discipline.
Autonomous coding agents operating in iterate-until-green loops optimize for passing tests, not preserving invariants:
- Weakening assertions (
assert_eq!(a, b)→assert!(a == b)orassert!(true)). - Adding vacuous "ghost tests" that compile and run but assert nothing.
- Marking failing or flaky tests
#[ignore]/@pytest.mark.skip/it.skip. - Stealthily deleting tests, fixtures, or benchmarks that stand in the way of a green build.
- Dropping
// SAFETY:justifications fromunsafeblocks. - Shipping the signature and leaving
todo!(),raise NotImplementedErrororreturn nullas the body. - Wrapping a failure in an empty
catch {}/except: pass, logging it and moving on,@-silencing a PHP call,rescue nil,runCatching { }.getOrNull(), or discarding aResult. - Keeping the assertion but moving it under
if falseor after areturn, or padding atodo!()with a log line so the body no longer looks empty. - Swapping the real dependency for a mock and asserting only that the mock was called.
- Regenerating snapshots, adding a snapshot for a test that already existed, adding retries, or loosening
tsconfig.json,ruff.toml,clippy.toml,[lints]and coverage floors — or swapping what theyextends— instead of fixing the cause. - Repointing a lockfile entry at another host or dropping its integrity hash, or trading
npm ci/--frozen-lockfilefor an install that rewrites the lock. - Editing the gate configuration (
discipline.toml), the CI workflow, or the agent's own instruction files (AGENTS.md,.cursorrules) to disable failing checks. - Carrying text aimed at the next agent: an injection in a comment, a PR description or a commit message, or a bidirectional override that hides what a parser reads.
- Widening its own sandbox: an allowed command,
bypassPermissions, the sandbox or a hook switched off, a new MCP server, or a devcontainer or Compose service made privileged or put on the host network. - Running a command on every install from where CI checks do not look: a
package.jsonpostinstall, abuild.rs, or a repointed registry in.npmrc/pip.conf. - Introducing unverified calendar estimates or leaking developer workstation paths and LAN IPs.
Discipline reads the diff against the merge base: tree-sitter syntax trees for code, structured parsers for configuration, and fixed literal patterns for text, failing closed when it cannot read one. It judges structure and policy, not runtime semantics or data flow. It rejects erosion patterns before they reach review, and it guards its own trust boundary: a change cannot switch its run to advisory, disable the gate that judges its configuration, narrow the CI step that runs the gate without a recorded reason, or (with directives.require_approval) excuse itself without a review by someone else. On a push to the default branch, the waivers reviewed on the merged pull request still count: merged-pr-body reads that pull request's body through the forge, because a squash or rebase merge drops it from the commit message.
Install the binary and inspect your working tree before pushing:
# Install static binary
curl -fsSL https://orieg.github.io/discipline/install.sh | bash
# Write a starter discipline.toml (gates run at their defaults without one)
discipline init
# Inspect uncommitted changes against HEAD across all gates
discipline diff
# Or inspect current branch diff against origin/main
discipline check
# Tell your coding agent while it edits (claude-code, codex, cursor, aider, copilot, agy, qwen, opencode)
discipline hook install --agent claude-code
# What would discipline have blocked in your last 50 merged changes?
# (each change is rebuilt and checked once; a second run reads the verdicts back from the replay cache)
discipline replay --last 50
# Which waivers, loosenings and grandfathered findings did they carry?
discipline audit --last 50
# What a gate checks and how its finding is lifted
discipline explain assertion-reductionAfter an edit that weakens a test, the agent hook hands the agent the finding and its repair (and, for most agents, refuses an edit into another worktree before it runs); the tutorial walks through it in a sandbox, and Agent Hooks covers every agent and the MCP server.
Adopting Discipline on an existing repository never requires resolving all historical debt upfront. Discipline provides cryptographic grandfathering baselines that pin existing findings so current code passes:
# Step 1: Baseline existing findings so current repository state passes
discipline baseline --write
# Step 2: Commit baseline file and enable Discipline
git add discipline-baseline.toml
git commit -m "chore: baseline existing discipline debt" \
-m "allow-gate-weakening: baseline initial grandfathered baseline"
# (a new baseline is a config-integrity weakening: the directive line records why)
# All future diffs and pull requests are now strictly guarded against new regressions!Add Discipline as a required check on pull requests:
name: CI Sentinel
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
jobs:
discipline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # merge base must be reachable
- uses: orieg/discipline@f3d6d4a0bc4f1f60f63fa101aa27e1a17d0d5276 # v0.32.0The action is pinned by commit SHA because the default ci-integrity gate reports any tag ref (@v0, @v0.32.0) newly added to a workflow as an unpinned action, so a tag would fail the pull request that adds it. A SHA ref runs the binary of the release that commit's Cargo.toml names; version: under with: picks another release, and Renovate or Dependabot move the pin. A repository that turns ci-integrity off can use the floating @v0 ref, which tracks the latest v0.x.y release and moves only after that release has passed its smoke tests. Pin @v0.32.0 or a commit SHA when a verdict must be reproducible from the workflow file alone. Before 1.0, a minor release can change gate behaviour; each one lists those changes under "Upgrading" in its release notes and in the compatibility ledger.
The step fails on errors only. A warning, such as AGENTS.md Missing in a repository without one, shows in the report and the step summary without failing it. Add fail_on_warnings: true under with: once the warnings are fixed or baselined; First Week gives the order.
Note on
edited: GitHub Actions does not trigger workflows on PR description edits by default. Specifyingtypes: [opened, synchronize, reopened, edited]ensures that updating the PR body (such as adding an authorized override directive or resolving a PR-body hygiene finding) immediately re-runs the gate without requiring an empty commit.Note on
push: the action also runs onpushevents (basegithub.event.before), but a push run reads directives from the pushed commits' messages only: a squash or rebase merge drops the PR body, so a waiver that passed on the pull request fails the push run onmainthat follows. Gate onpull_requestas above; if you also run onpush, give the run a token that can read pull requests (themerged-pr-bodysource then reads the merged pull request's body and names it in the notes) or put directives in commit messages too. Details: Override Directives.
A check only blocks a merge when the branch requires it. See Repository Protection for the settings (required check, up-to-date branch, no bypass, CODEOWNERS on gate configuration) and a ruleset example.
The same composite action runs unchanged under Gitea's act_runner and under forgejo-runner. Reference it by its full URL, since the runner resolves a bare owner/repo against your own instance. Create .gitea/workflows/discipline.yml (or .forgejo/workflows/discipline.yml):
name: CI Sentinel
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
discipline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # merge base must be reachable
persist-credentials: false # the job's token can write: keep it out of .git/config
- uses: https://github.com/orieg/discipline@f3d6d4a0bc4f1f60f63fa101aa27e1a17d0d5276 # v0.32.0The commit SHA and fail_on_warnings work as in the GitHub example above.
The action fetches the matching release archive and its SHA256SUMS from GitHub and verifies the checksum before running, so the runner needs outbound HTTPS to github.com. On an air-gapped runner, point binary_path at a discipline binary already on the runner image (see Installation), or set download_url to an internal mirror of the release store:
- uses: https://github.com/orieg/discipline@v0
with:
binary_path: /opt/discipline/disciplineA mirror can replace an archive and its SHA256SUMS together, so a mirror install is checked further only when the mirror also serves the release's Sigstore bundle, discipline.intoto.jsonl, and the runner has gh: the bundle must be signed by this repository's release workflow for the requested tag, which a mirror cannot forge. require_provenance: true fails the install, before the binary runs, when that check cannot be made; trusted_root (a file from gh attestation trusted-root) lets it run without network:
- uses: https://github.com/orieg/discipline@<full commit SHA> # v0.32.0
with:
version: v0.32.0
download_url: https://mirror.example.com/discipline/releases
require_provenance: true
trusted_root: /opt/sigstore/trusted_root.jsonl # optional, for no networkThis protects only when action.yml itself comes from a trusted source pinned by commit SHA, not from the same mirror: whoever serves the action serves the check.
The runs-on label must map to a runner image with git, curl (or wget) and Node.js (actions/checkout is a JavaScript action; the default act_runner images qualify). The action itself is shell-only. Gitea and Forgejo name the resulting status check CI Sentinel / discipline (pull_request); use that name when making it a required check on the default branch. discipline doctor reads a GITEA_TOKEN or FORGEJO_TOKEN to confirm the protection is in place. See the Forgejo & Gitea Actions guide for the full walkthrough.
For other CI platforms and orchestrators (copy-paste pipelines for GitLab, Argo, Azure Pipelines, Bitbucket, CircleCI and Jenkins are in templates/, indexed in the CI guide):
- GitLab CI/CD Component & Job Guide
- Forgejo & Gitea Actions Guide
- Argo Workflows GitOps Template
- pre-commit & Local Git Hooks
- Pull-Request Comments (
--comment) - Docker Container Run
- CLI Reference & Local Inner Loop
Coding-agent integration:
- Agent Hooks (Claude Code, Codex, Cursor, Aider, Copilot CLI, agy, Qwen Code, OpenCode)
- MCP Server (
discipline mcp)
Adoption and editor tooling:
- Previewing Adoption (
discipline replay) - Auditing Escape Hatches (
discipline audit) - Explaining a Gate (
discipline explain) - VS Code Problems Panel
- Keeping Pins Current with Renovate
- Keeping Pins Current with Dependabot
- Agent Hook Files in a Bot's Update
An order of adoption in which every change passes its own check. A step that changes a file is a pull request, and a directive it needs goes on its own line in that pull request's body.
discipline initwrites a starterdiscipline.toml. Commit it. (discipline init --profile observewrites it with step 4's advisory line already set, and--profile agent-guardthe review controls for coding agents: Init Profiles.)discipline replay --last 50shows what the gates would have blocked in the last 50 merged changes: the findings to expect, and the configuration to tune before the check runs.--format html -o replay.htmlwrites the same as one self-contained page, each blocked change grouped by gate and linked to the forge.discipline baseline --writerecords the findings already in the tree. Commitdiscipline-baseline.tomlwithallow-gate-weakening: baseline <reason>(Brownfield).- Add the workflow from part 3 or 4, and uncomment
mode = "advisory"under[meta]indiscipline.toml(the starter carries the line): the check reports what it finds and never fails. Advisory mode loosens every gate, so the pull request that sets it carriesallow-gate-weakening: meta <reason>, and it applies once that pull request has merged; this holds for the change that adds the firstdiscipline.tomltoo, which is compared with the built-in defaults. (advisory: trueunder the action'swith:does the same for one workflow; it is aci-integrityfinding,ci-integrity/discipline-action-advisory, lifted byallow-gate-weakening: ci-integrity <reason>.) - Once a run of pull requests has shown findings the team acts on, delete the
mode = "advisory"line and make the check required (Repository Protection). Deleting it loosens nothing, so it needs no directive. - Fix the warnings (write the
AGENTS.md, for one) or record them withdiscipline baseline --write --fail-on-warnings, then addfail_on_warnings: trueunderwith:. A baseline that records more findings carriesallow-gate-weakening: baseline <reason>again.
Fourteen tree-sitter language packs (Rust, Python, JavaScript / TypeScript, Java, Go, PHP, C, C++, C#, Ruby, Kotlin, Swift, Scala, Objective-C) and a section parser for PHPT test files supply the AST facts; the language table in docs/GATES.md lists what each pack reads. discipline gates lists every gate with its suite, effective state and severity. Detailed rules, detection boundaries, and what gates do not catch are documented in docs/GATES.md. Gates ship on unless their rule is a repository policy (issue-link, review-threads, ratified-paths, commit-provenance, provenance-tags, scope-confinement, pr-checklist, msrv, archive-contents, manifest-sync, version-lockstep, and the sanitizers, miri and unsafe-budget presets); docs/ROADMAP.md records every default change per release.
| Gate | Suite | Languages | Rule Description |
|---|---|---|---|
agents-md |
agent-guard | any | AGENTS.md exists; CLAUDE.md / GEMINI.md do not fork it |
assertion-reduction |
agent-guard | Rust, PHPT, Python, JS/TS, Java, Go, PHP, C/C++, C#, Ruby, Kotlin, Swift, Scala, Objective-C | assertion count / strength must not drop in an existing test |
vacuous-tests |
agent-guard | Rust, PHPT, Python, JS/TS, Java, Go, PHP, C/C++, C#, Ruby, Kotlin, Swift, Scala, Objective-C | new tests must carry a non-tautological assertion |
ignored-tests |
agent-guard | Rust, PHPT, Python, JS/TS, Java, Go, PHP, C/C++, C#, Ruby, Kotlin, Swift, Scala, Objective-C | tests must not be newly #[ignore]d or skipped without directive |
unsafe-safety-comment |
agent-guard | Rust | unsafe blocks / impls carry a // SAFETY: comment |
deletion-rationale |
agent-guard | any | deleted files and removed tests need a scoped removes: rationale |
time-estimates |
hygiene | any | no calendar / duration estimates in markdown or the PR body |
pii |
hygiene | any | no home paths, LAN IPs, denylisted hostnames, or fixed-format token literals (private-key headers; AWS, GitHub, Slack, OpenAI and Anthropic token formats; literal bearer headers) in tracked text: literal format matching, not credential scanning |
agent-scratch |
hygiene | any | agent scratch state is never tracked |
shell-secrets |
hygiene | shell, docker, workflows | no secrets on command lines, fixed-format token literals, or unverified piped scripts in shell, docker, or CI files: literal pattern matching, not secret scanning |
issue-link |
hygiene | any | PR title or description links a tracking issue (#123, Fixes #123) |
review-threads |
hygiene | any | the pull request has no unresolved review thread |
ratified-paths |
agent-guard | any | edits to protected paths carry an owner's ratification on an issue the pull request closes |
citation-metadata |
hygiene | any | CITATION.cff and .zenodo.json are valid, agree with each other, and cite the concept DOI |
commit-provenance |
hygiene | any | commits carry the required trailers; an agent-produced commit carries a review by someone else |
config-integrity |
integrity | any | a change cannot weaken its own discipline.toml without a token |
stub-bodies |
agent-guard | Rust, Python, JS/TS, Java, Go, PHP, C/C++, C#, Ruby, Kotlin, Swift, Scala, Objective-C | added functions are not stubs; existing bodies are not replaced by todo!() / NotImplementedError / return null |
harness-tampering |
agent-guard | Go, Python, JS/TS, Rust | a file the test runner loads does not make a failing run pass |
error-swallowing |
agent-guard | Rust, Python, JS/TS, Java, Go, PHP, C/C++, C#, Ruby, Kotlin, Swift, Scala, Objective-C | no new empty error handler or discarded Result outside tests |
instruction-smuggling |
agent-guard | any (invisible characters, instruction files); Rust, Python, JS/TS, Java, Go, PHP, C/C++, C#, Ruby, Kotlin, Swift, Scala, Objective-C and prose files (phrases) | no invisible Unicode, unreviewed agent-instruction edits, or instruction-like text in comments and prose |
build-hooks |
integrity | package.json, build.rs, setup.py, .npmrc, .pypirc, pip.conf, .cargo/config.toml, .env* | install and build hooks that gain network or shell access, and package-manager configuration edits, need a token |
toolchain-config |
integrity | tsconfig, ruff, mypy, pytest (addopts flags and -k / -m selection), coverage, flake8, Cargo lints, rustflags, nextest, eslintrc, golangci, jest and vitest (JSON or a literal object), package.json test scripts, codecov, phpstan, phpunit, and compiler warning flags in Makefile, CMake, setup.py and build.rs | compiler, linter, type-checker, test-runner and coverage configuration cannot be loosened without a token |
sandbox-config |
integrity | Claude Code, Codex, Gemini CLI, Qwen Code, OpenCode, Cursor and Copilot CLI settings, MCP server lists, devcontainer.json, Docker Compose, CI job and service containers | a change cannot widen an agent's permissions or sandbox, or a container's isolation, without a token |
scope-confinement |
agent-guard | any | changes stay inside authorized paths |
suppression-delta |
agent-guard | per pack | newly added linter / compiler suppression annotations |
provenance-tags |
hygiene | any | published numerics carry (measured |
ci-integrity |
integrity | any | workflow weakening: continue-on-error, |
ci-skip-set |
integrity | any | rollup skip set matches each job's if: under the observed filter outputs |
test-floor |
integrity | any | test-count ratchet read from the base ref |
golden-output |
integrity | any | prevents stealth edits to committed golden/test output files without explicit override |
dependency-delta |
integrity | any | manifest diff inspection: zero wildcards, source/license allowlists, and deny.toml verification |
test-budget |
integrity | Rust, Python, JS/TS, Go, any | property-test and fuzz effort ratchet (cases, shrink iters, fuzztime, seed corpus) |
pr-checklist |
hygiene | any | ticked PR checkboxes and declared touched surfaces are reconciled against the diff |
command |
verification | any | fail-closed wrapper for any tool: zero-tests guard, canary, count ratchet |
sanitizers |
verification | Rust, C/C++ | ASan / TSan preset with audited suppressions and a race canary |
msrv |
quality | Rust | cargo check under the pinned MSRV |
miri |
verification | Rust | Miri tiers with zero-tests guard |
unsafe-budget |
verification | Rust | unsafe count ratchet |
bench-regression |
bench | Rust, Go, Python, C/C++ | benchmark drift via harness adapters (deterministic counts or BCa intervals) |
archive-contents |
integrity | any | distribution archive must contain required paths and zero forbidden developer artifacts |
manifest-sync |
integrity | any | reconcile git-tracked files against packaging manifest declarations |
version-lockstep |
integrity | any | version declarations across headers, manifests, and files must remain in lockstep |
gate-command-lint |
agent-guard | markdown, shell | reject vacuous, masked, or inverted verification commands in plan files |
mechanism-sections |
hygiene | markdown | require (inferred) or (verified: ) evidence tags in root cause sections |
citation-anchors |
hygiene | markdown, any | verify path:line@sha citations against quoted text and git history, and relative Markdown links against the tree |
claim-labels |
hygiene | markdown, any | require RUN or READ labels on verification claims in PR bodies, commits, and Markdown |
companion-edit |
integrity | any | a change to one path set must also change its companion path set |
changed-line-coverage |
integrity | any (LCOV, Cobertura) | a line the change adds that the coverage report lists as never run |
visual-evidence |
hygiene | any | a change to UI paths needs before and after images in the pull request body |
invented-names |
hygiene | markdown, any | a flag or configuration key a document gives a tool that the tool's declared surface does not list |
Discipline's lane is the integrity of an agent's diff: tests, safety justifications, policy and the verification harness. It reads syntax, configuration and fixed text patterns; it does not run the code, trace data flow, or decide whether a secret works. The classes below lie outside every gate. Each is a known_miss case in the evasion corpus, so tests/test_evasion_corpus.rs fails the day a gate starts catching one, and the case moves to caught. The concrete shapes live in the corpus, not here.
| Class | Corpus case | Stated under | Tracking |
|---|---|---|---|
| value/dataflow gating | out-of-lane-value-dataflow-gating |
assertion-reduction |
#941 |
| dataflow to external sinks | out-of-lane-dataflow-to-external-sinks |
error-swallowing |
#941 |
| runtime payload semantics | out-of-lane-runtime-payload-semantics |
instruction-smuggling |
#941 |
| credential validity | out-of-lane-credential-validity |
pii |
#941 |
Entries in each gate's "What it does NOT catch" list:
agents-md 1 · assertion-reduction 24 · vacuous-tests 8 · ignored-tests 11 · unsafe-safety-comment 3 · deletion-rationale 5 · time-estimates 7 · pii 9 · agent-scratch 1 · shell-secrets 5 · issue-link 3 · review-threads 4 · ratified-paths 4 · citation-metadata 4 · commit-provenance 8 · config-integrity 6 · stub-bodies 6 · harness-tampering 9 · error-swallowing 10 · instruction-smuggling 11 · build-hooks 3 · toolchain-config 9 · sandbox-config 7 · scope-confinement 4 · suppression-delta 5 · provenance-tags 4 · ci-integrity 7 · ci-skip-set 3 · test-floor 8 · golden-output 1 · dependency-delta 5 · test-budget 5 · pr-checklist 11 · command 5 · sanitizers 3 · msrv 3 · miri 3 · unsafe-budget 3 · bench-regression 2 · archive-contents 2 · manifest-sync 2 · version-lockstep 2 · gate-command-lint 6 · mechanism-sections 4 · citation-anchors 7 · claim-labels 3 · companion-edit 6 · changed-line-coverage 4 · visual-evidence 5 · invented-names 4
Generated by discipline docs --write from each gate's "What it does NOT catch" list in docs/GATES.md; discipline docs --check fails when this section and those lists disagree.
Approvals prove which login agreed, not which person. A ratified-paths ratification and a require_approval review are checked against the logins you list. When your agents push and comment with your own login, an approval records that this login agreed. Anything holding that login could have written it, so it is a record, not a second key. To make it one, give the agents logins of their own and list them in gates.ratified-paths.agent_logins. Then set gates.ratified-paths.refuse_author_ratification = true, so a pull request's author cannot ratify it. The threat model states this as assumption 3.
Every subcommand of the binary. discipline <command> --help and man discipline list each one's options; the CLI reference has them as tables.
| Subcommand | Description |
|---|---|
check |
Run the configured gates. Exit 0 = pass, 1 = violations, 2 = could not check |
diff |
Shorthand for checking uncommitted or working tree changes against HEAD |
baseline |
Record or manage grandfathered finding baselines |
init |
Write a minimal discipline.toml: gates run at their built-in defaults; commented examples show what to change |
gates |
List every gate: id, suite, availability, and effective state |
schema |
Print the JSON Schema for discipline.toml |
self-test |
Run the embedded negative / positive controls against this binary |
completions |
Generate shell completion script to stdout (bash, zsh, fish, powershell, elvish) |
install-hooks |
Install pre-commit hook in the local git repository |
hook |
Run the gates inside a coding agent's edit loop (Claude Code, Codex, Cursor, Aider, Copilot CLI, agy, Qwen Code, OpenCode) |
explain |
Explain a gate: what it checks, its state here, and the directive that lifts a finding |
replay |
Replay the last N merged changes through a configuration: what it would have blocked |
audit |
List the escape hatches the last N merged changes carried: directives, configuration loosenings, baseline growth, inline markers |
mcp |
Serve the gates to an MCP client over stdio (read-only tools: check_diff, list_gates, explain_finding) |
bench |
Benchmark tooling for the bench-regression gate |
doctor |
Check that the repository and its platform enforce discipline: workflows, CODEOWNERS, branch protection, and the local agent settings. Exit 0 = healthy, 1 = a failing check, 2 = could not check |
lease |
Claim this worktree's branches for one agent session, so other worktrees' sessions do not move them (kept in the common git directory, never committed) |
discipline audit reads a branch's merged history and lists every escape hatch it carried: directive waivers, loosened discipline.toml options, grandfathered findings, inline markers and edits to protected paths. It ranks them by what they could hide and says what it could not check. --forge adds what lives on the forge (owner ratifications, reviews, pull-request descriptions, the issues waivers cite), and --replay a discipline replay --json report, to say which waivers lifted a finding. --format html writes one self-contained page with each row linked to the file and line, diff or commit on your forge. Open the report for this repository.
discipline audit --last 200 --format html -o audit.html
In short: discipline 0.32.0 blocked 17% of merged agent pull requests; a simple baseline rule that blocks any change to a test, CI or configuration file blocked 56%. 84% of discipline's blocks are correct (likely 76–90%), against 2% for that rule. Each agent edit is checked in a few seconds. It does not yet catch most test gaming: on a public benchmark it blocked 8% of agents' edits that made impossible tests pass, short of the 80% set in advance.
Every number comes from public pull requests or public benchmarks. Each chart states what was measured, and how each study was run, with its raw data, is in docs/benchmarks/.
How often does it block?
Were those blocks right?
AI models graded every finding, not people. Two models from different vendors agreed on 266 of 276; a third, from one of those vendors, settled the other 10. One pull request holds 52 of the 122 findings, so the per-pull-request range is wider.
How long does each check take?
Does it catch test gaming? Not yet, mostly (#838).
The verdict is fixed by the release chosen before the study, 0.26.0: it fails its bar. 75 of the 138 cases are tests the benchmark itself broke (they do not parse, or the changed line sits outside any test), so a diff check has nothing to see; on the other 63, discipline caught 8. The later builds catch more, but they were fixed using these same cases, so only fresh cases can show whether that holds. The simple rules catch more because they block every test edit; on real pull requests the file rule blocked 56%.
Judge it on your own repository: discipline replay --last 50 shows what discipline would have blocked in your last 50 merged changes, without blocking anything; --format html -o replay.html writes it as one page to share. The charts are generated from the committed results by scripts/benchmark_charts.py.
Discipline is distributed as a standalone static binary, native operating system packages (APT, RPM, Homebrew, MacPorts), cargo toolchain binary, or container image:
Install the latest pre-compiled static binary verified with cryptographic SHA-256 checksums:
# Recommended (download, inspect, and run):
curl -fsSL -o install.sh https://orieg.github.io/discipline/install.sh
bash install.shAlternatively, install via one-liner (the installer script internally enforces SHA-256 checksum verification):
curl -fsSL https://orieg.github.io/discipline/install.sh | bashCustom destination directory or pinned release tag:
bash install.sh --to ~/.local/bin --version v0.32.0Add the official APT repository and install via apt:
# 1. Install the repository signing key and add the source
curl -fsSL https://orieg.github.io/discipline/apt/discipline-archive-keyring.gpg | sudo tee /usr/share/keyrings/discipline-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/discipline-archive-keyring.gpg] https://orieg.github.io/discipline/apt/ stable main" | sudo tee /etc/apt/sources.list.d/discipline.list
# 2. Update package cache and install
sudo apt update
sudo apt install -y disciplineDirect .deb package downloads and repository metadata: Discipline APT Repository.
Add the official RPM repository and install via dnf or yum:
# 1. Add repository configuration
sudo dnf config-manager --add-repo https://orieg.github.io/discipline/rpm/discipline.repo
# 2. Install discipline binary
sudo dnf install -y disciplineDirect .rpm package downloads and repodata manifests: Discipline RPM Repository.
- Homebrew:
brew install orieg/tap/discipline
- MacPorts: each release attaches a generated
Portfile. Until the port is in the MacPorts tree, install it from a local ports tree:mkdir -p ~/ports/devel/discipline curl -fsSL -o ~/ports/devel/discipline/Portfile https://github.com/orieg/discipline/releases/latest/download/Portfile # Once: add `file:///Users/<you>/ports` above the rsync line in /opt/local/etc/macports/sources.conf (cd ~/ports && portindex) && sudo port install discipline
cargo install(crates.io, requiresrustc1.90+):cargo install discipline
- From source via git:
cargo install --git https://github.com/orieg/discipline
The APT, RPM, Homebrew and MacPorts packages install completions for bash, zsh and fish; nothing to do. A release tarball carries the same scripts under completions/, and any binary can regenerate them with discipline completions <shell>. For a tarball, install.sh or cargo install setup, put the script where the shell loads it:
# bash (bash-completion loads this directory)
discipline completions bash > ~/.local/share/bash-completion/completions/discipline# zsh: a directory on $fpath, before `compinit` runs (add `fpath+=~/.zfunc` to ~/.zshrc if it is new)
mkdir -p ~/.zfunc && discipline completions zsh > ~/.zfunc/_discipline# fish
discipline completions fish > ~/.config/fish/completions/discipline.fishPowerShell and Elvish are generated the same way (discipline completions powershell, discipline completions elvish). The script describes the subcommands and flags of the binary that produced it: regenerate it after every upgrade, and clear zsh's cache (rm -f ~/.zcompdump*) when a new subcommand does not complete.
Each release publishes pre-compiled binaries with SHA-256 checksums, SLSA Build Level 2 build-provenance attestations, and a CycloneDX Software Bill of Materials (discipline.cdx.json) with an SBOM attestation:
# Verify the build provenance attestation for a release archive
gh attestation verify discipline-x86_64-unknown-linux-musl.tar.gz --repo orieg/discipline
# Verify the CycloneDX SBOM attestation for a release archive
gh attestation verify discipline-x86_64-unknown-linux-musl.tar.gz --repo orieg/discipline --predicate-type https://cyclonedx.org/bom
# Inspect the CycloneDX SBOM directly
jq . discipline.cdx.jsonAn archive from a mirror, or on a machine with no network, is checked against the release's Sigstore bundle instead of GitHub's API. Fetch the trusted root once from a trusted machine, never from the mirror: whoever supplies the root chooses the certificate authority. Refresh it when you upgrade, since Sigstore rotates its keys.
# On a machine with network access
gh attestation trusted-root > trusted_root.jsonl
# Offline: the archive and discipline.intoto.jsonl of the same release, with its tag
gh attestation verify discipline-x86_64-unknown-linux-musl.tar.gz \
--bundle discipline.intoto.jsonl --custom-trusted-root trusted_root.jsonl \
--repo orieg/discipline --signer-workflow orieg/discipline/.github/workflows/release.yml \
--source-ref refs/tags/v0.32.0install.sh --version v0.32.0 --verify-provenance [--trusted-root trusted_root.jsonl] runs the same check before it installs.
- Interactive Documentation Site
- Tutorial: Sandbox Quickstart — trigger and fix an assertion weakening, then hand it to a coding agent's hook
- How-to: CI Platform Guide — GitHub, GitLab, Forgejo / Gitea, Argo and the other templates, rollup and benchmark wiring
- Reference: Gate Specifications — rules, before/after diffs, and what each gate does not catch
- Reference: Configuration & CLI — configuration layers, schema, every CLI option, action inputs and outputs, integrations
- Reference: man pages —
man discipline,man discipline.toml(inman/, installed by the packages) - Explanation: Engine Architecture — fail-closed contracts (F1–F12), AST diff engine, CI pipelines
- Explanation: Roadmap & Compatibility Ledger — delivery phases and every behaviour change per release
- How-to: Reproducing the Benchmarks — re-run the precision, recall and latency studies, and what each one measures
cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo testEvery gate requires a unit test, an end-to-end test through the binary, a mutation test that kills the mutant, and a self-test case; see AGENTS.md §3.4.
Citation metadata is in CITATION.cff; GitHub renders it under Cite this repository. .zenodo.json holds the same record for Zenodo's archive.
Discipline is archived on Zenodo from v0.15.0 on. Cite the concept DOI for the project as a whole (it always resolves to the latest release), or a version DOI to pin the exact release you used:
| Scope | DOI |
|---|---|
| Concept (all versions) | 10.5281/zenodo.23046102 |
| v0.15.0 | 10.5281/zenodo.23046103 |
Dual-licensed under MIT or Apache-2.0, at your option.