Skip to content

About

Universal CI/CD gatekeeper and AI coding agent diff sentinel

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

 

History

807 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

discipline

CI OpenSSF Best Practices Documentation License: MIT OR Apache-2.0 Rust 1.90+ Release Crates.io DOI Marketplace pre-commit

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.

Why 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) or assert!(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 from unsafe blocks.
  • Shipping the signature and leaving todo!(), raise NotImplementedError or return null as 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 a Result.
  • Keeping the assertion but moving it under if false or after a return, or padding a todo!() 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 they extends — instead of fixing the cause.
  • Repointing a lockfile entry at another host or dropping its integrity hash, or trading npm ci / --frozen-lockfile for 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.json postinstall, a build.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.

Quickstart

1. Local Developer & Agent Inner Loop

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-reduction

After 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.

2. Adopting on an Existing Repository (Brownfield)

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!

3. Pre-Merge CI Sentinel (GitHub Actions)

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.0

The 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. Specifying types: [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 on push events (base github.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 on main that follows. Gate on pull_request as above; if you also run on push, give the run a token that can read pull requests (the merged-pr-body source 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.

4. Pre-Merge CI Sentinel (Gitea & Forgejo Actions)

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.0

The 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/discipline

A 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 network

This 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):

Coding-agent integration:

Adoption and editor tooling:

5. First Week: From Advisory to Enforcing

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.

  1. discipline init writes a starter discipline.toml. Commit it. (discipline init --profile observe writes it with step 4's advisory line already set, and --profile agent-guard the review controls for coding agents: Init Profiles.)
  2. discipline replay --last 50 shows 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.html writes the same as one self-contained page, each blocked change grouped by gate and linked to the forge.
  3. discipline baseline --write records the findings already in the tree. Commit discipline-baseline.toml with allow-gate-weakening: baseline <reason> (Brownfield).
  4. Add the workflow from part 3 or 4, and uncomment mode = "advisory" under [meta] in discipline.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 carries allow-gate-weakening: meta <reason>, and it applies once that pull request has merged; this holds for the change that adds the first discipline.toml too, which is compared with the built-in defaults. (advisory: true under the action's with: does the same for one workflow; it is a ci-integrity finding, ci-integrity/discipline-action-advisory, lifted by allow-gate-weakening: ci-integrity <reason>.)
  5. 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.
  6. Fix the warnings (write the AGENTS.md, for one) or record them with discipline baseline --write --fail-on-warnings, then add fail_on_warnings: true under with:. A baseline that records more findings carries allow-gate-weakening: baseline <reason> again.

Gates

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

Non-Goals

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.

Commands

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)

Auditing What Got Through

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
discipline audit report of this repository's last 200 merged changes: the headline, a strip of checks marked found, checked or not checked, and a ranked Needs a decision list

Results

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?

Discipline 0.32.0 blocked 40 of 232 merged coding-agent pull requests (17%); a simple rule that blocks any change to a test, CI or configuration file blocked 130 (56%).

Were those blocks right?

84% of discipline's blocks are correct: 103 of 122, likely 76–90%, or 59–95% per pull request. The simple rule: 3 of 150 (2%). By check: CI workflow changes 66 of 66, swallowed errors 10 of 11, changed expected outputs 11 of 13, new dependencies 9 of 20.

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?

Each agent edit is checked in 7.9 seconds on this repository and 4.3 seconds on ripgrep when the hook can block, and in under 0.1 second in observe mode; a full CI check takes 19.5 and 6.3 seconds.

Does it catch test gaming? Not yet, mostly (#838).

On a benchmark of impossible tests, discipline 0.26.0 caught 11 of 138 agent edits that made a test pass by changing it (8%, likely 4.5–14%), short of the 80% bar set in advance. Simple rules that block any changed test line, or any test, CI or configuration file, caught 82% and 91%. Honest solutions wrongly blocked: discipline 11 of 644 (1.7%), the simple rules 1 and 2.

The same 138 cases on later builds: 0.31.0 caught 12, the 0.32.0 code before its last three fixes 14, and 0.32.0 21 (15%, likely 10–23%). Those fixes were written from misses on these cases. Honest solutions wrongly blocked stay at 11 of 644.

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.

Installation

Discipline is distributed as a standalone static binary, native operating system packages (APT, RPM, Homebrew, MacPorts), cargo toolchain binary, or container image:

Quick Install (Linux & macOS)

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.sh

Alternatively, install via one-liner (the installer script internally enforces SHA-256 checksum verification):

curl -fsSL https://orieg.github.io/discipline/install.sh | bash

Custom destination directory or pinned release tag:

bash install.sh --to ~/.local/bin --version v0.32.0

Debian / Ubuntu (APT)

Add 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 discipline

Direct .deb package downloads and repository metadata: Discipline APT Repository.

Enterprise Linux / Fedora (RPM)

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 discipline

Direct .rpm package downloads and repodata manifests: Discipline RPM Repository.

macOS (Homebrew & MacPorts)

  • 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

Rust Toolchain

  • cargo install (crates.io, requires rustc 1.90+):
    cargo install discipline
  • From source via git:
    cargo install --git https://github.com/orieg/discipline

Shell Completions

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.fish

PowerShell 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.

Binary Verification & Provenance

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.json

An 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.0

install.sh --version v0.32.0 --verify-provenance [--trusted-root trusted_root.jsonl] runs the same check before it installs.

Documentation

  • 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 (in man/, 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

Development

cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test

Every 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.

Citing

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

License

Dual-licensed under MIT or Apache-2.0, at your option.

About

Universal CI/CD gatekeeper and AI coding agent diff sentinel

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages