Universal CI/CD Diff Sentinel &
AI Coding Agent Gatekeeper
Traverses AST diffs via tree-sitter over merge-base git graphs. Rejects assertion weakening, vacuous and mock-only tests, stub bodies, swallowed errors, test suppression, loosened toolchain configuration, widened agent and container sandboxes, and text aimed at the next agent, across pull requests and merge requests. And it guards its own trust boundary: a change cannot switch its run to advisory or excuse itself without review.
Autonomous AI agents in an iterate-until-green loop optimize for passing tests, not preserving invariants. Discipline halts erosion at the gate.
Tree-Sitter AST Diff Inspection
Unlike fragile regex checks that trigger on comments or log messages, Discipline parses the complete AST of changed functions against the merge base. It counts executable assertions and distinguishes real invariants from dead code: an assertion under if false or after a return counts for nothing, and a property-test budget inside a string or comment is not a budget. Fourteen packs: Rust, Python, JavaScript / TypeScript, Java, Go, PHP, C, C++, C#, Ruby, Kotlin, Swift, Scala and Objective-C, plus a section parser for PHPT test files.
AI Coding Agent Guardrails
Flags silent test erosion: assertion count reductions (e.g. assert_eq! → assert!), test suppression and retries (#[ignore], @pytest.mark.flaky, it.skip), tautological or mock-only tests (assert!(true), toHaveBeenCalled alone), stealth test deletions, stub bodies even when padded with a log line (todo!(), raise NotImplementedError), swallowed errors (except: pass, a handler that only logs, let _ = fallible(), rescue nil, PHP @call()), a snapshot added for a test that already existed, a repointed lockfile entry, hard-coded sleeps and not-null-only assertions, and injection text in comments, PR descriptions or agent instruction files.
Fail-Closed Engineering
If the base ref or the merge base cannot be resolved (a clone too shallow to reach it, a branch never fetched), or a configuration file is malformed, Discipline exits with code 2. It never yields a false-positive pass on broken pipelines or empty diff outputs. The trust boundary is guarded too: a change cannot switch its own run to advisory, disable config-integrity, loosen tsconfig.json / ruff.toml / [lints] / coverage floors, mask the CI step, or add a postinstall hook or a .npmrc registry that runs on every install, without a scoped directive.
Native MR & PR Reporting
Discipline emits industry-standard reports without external plugins: GitLab Code Quality diffs, JUnit test widgets, GitHub PR comments, GitHub Actions Annotations, and SARIF security tabs.
Every waiver, loosened option, grandfathered finding and protected-path edit your merged history carried, ranked by what it could hide, with a link to the exact line on your forge.
Ranked, not raw
Signals such as a loosened guard gate, a loosening pushed without a pull request, or one never tightened back lead the page, each with the next thing to check.
Honest about gaps
With --forge it reads owner ratifications, reviews and pull-request descriptions; with --replay, which waivers lifted anything. Whatever it cannot tell, such as which agent made a change, is marked not checked, never shown as clean.
One page, linked to the source
discipline audit --format html writes a self-contained page that loads nothing. Each row opens the file and line, the diff or the commit on GitHub, GitLab, Gitea or Forgejo.
First-class support across all enterprise CI/CD systems, container runtimes, and local developer hooks.
Composite GitHub Action writing workflow annotations, a step summary and step outputs. comment: true adds one pull-request comment (needs pull-requests: write); DISCIPLINE_REPORT_SARIF plus an upload step adds code scanning (guide).
The action is pinned by commit SHA because the default ci-integrity gate rejects a newly added action that is not, @v0 included. A SHA ref runs the binary of the release that commit names. The step fails on errors only; add fail_on_warnings: true under with: once the warnings are fixed or baselined.
Native GitLab CI/CD integration. Populates GitLab Merge Request Code Quality diffs and JUnit test reports.
Runs under forgejo-runner from .forgejo/workflows/, with the base ref and pull-request event detected automatically. edited re-runs the check when the pull-request description (and a directive in it) changes.
Runs under self-hosted Gitea act_runner. The action itself is shell-only; the runner image needs git, curl (or wget) and Node.js for actions/checkout. Reference the action by its full URL, since the runner resolves a bare owner/repo against your own instance.
Kubernetes-native pre-merge gating for GitOps and continuous deployment pipelines.
Non-root, minimal static Alpine image for local containers or custom CI runners.
Git hook configuration preventing non-compliant commits before they reach remote branches.
Direct command-line execution for local developers, script automation, and self-checks.
Tell the agent while it edits: after an edit that weakens a test, the hook hands the agent the finding and its repair, judged by the default branch's configuration so the change cannot switch its own check off. Where the agent supports it, a pre-tool entry refuses an edit into another worktree before it runs, and a session-start entry takes this worktree's lease.
Every agent's contract, the MCP registration for each client, and what a change cannot do to its check: Agent Hooks and MCP Server.
Azure Pipelines, Bitbucket Pipelines, CircleCI and Jenkins templates, and the full wiring for each platform: CI/CD Platform Integration Guide.
Every gate operates with fail-closed semantics, truthful examination counts, and explicit remediation instructions.
| Gate ID | Category | Default | Languages | Rule Description |
|---|---|---|---|---|
| agents-md | Agent Guard | Warning | Any | AGENTS.md exists; CLAUDE.md / GEMINI.md do not fork it |
| assertion-reduction | Agent Guard | Error | 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 | Error | 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 | Error | 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 | Error | Rust | unsafe blocks / impls carry a // SAFETY: comment |
| deletion-rationale | Agent Guard | Error | Any | deleted files and removed tests need a scoped removes: rationale |
| time-estimates | Hygiene | Warning | Any | no calendar / duration estimates in markdown or the PR body |
| pii | Hygiene | Error | 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 | Error | Any | agent scratch state is never tracked |
| shell-secrets | Hygiene | Error | 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 | Off | Any | PR title or description links a tracking issue (#123, Fixes #123) |
| review-threads | Hygiene | Off | Any | the pull request has no unresolved review thread |
| ratified-paths | Agent Guard | Off | Any | edits to protected paths carry an owner's ratification on an issue the pull request closes |
| citation-metadata | Hygiene | Error | Any | CITATION.cff and .zenodo.json are valid, agree with each other, and cite the concept DOI |
| commit-provenance | Hygiene | Off | Any | commits carry the required trailers; an agent-produced commit carries a review by someone else |
| config-integrity | Integrity | Error | Any | a change cannot weaken its own discipline.toml without a token |
| stub-bodies | Agent Guard | Error | 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 | Error | Go, Python, JS/TS, Rust | a file the test runner loads does not make a failing run pass |
| error-swallowing | Agent Guard | Error | 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 | Error | 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 | Error | 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 | Error | 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 | Error | 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 | Off | Any | changes stay inside authorized paths |
| suppression-delta | Agent Guard | Warning | per pack | newly added linter / compiler suppression annotations |
| provenance-tags | Hygiene | Off | Any | published numerics carry (measured|target|projected) |
| ci-integrity | Integrity | Error | Any | workflow weakening: continue-on-error, || true, unpinned actions |
| ci-skip-set | Integrity | Error | Any | rollup skip set matches each job's `if:` under the observed filter outputs |
| test-floor | Integrity | Error | Any | test-count ratchet read from the base ref |
| golden-output | Integrity | Error | Any | prevents stealth edits to committed golden/test output files without explicit override |
| dependency-delta | Integrity | Error | Any | manifest diff inspection: zero wildcards, source/license allowlists, and deny.toml verification |
| test-budget | Integrity | Error | Rust, Python, JS/TS, Go, any | property-test and fuzz effort ratchet (cases, shrink iters, fuzztime, seed corpus) |
| pr-checklist | Hygiene | Off | Any | ticked PR checkboxes and declared touched surfaces are reconciled against the diff |
| command | Verification | Error | Any | fail-closed wrapper for any tool: zero-tests guard, canary, count ratchet |
| sanitizers | Verification | Off | Rust, C/C++ | ASan / TSan preset with audited suppressions and a race canary |
| msrv | Quality | Off | Rust | cargo check under the pinned MSRV |
| miri | Verification | Off | Rust | Miri tiers with zero-tests guard |
| unsafe-budget | Verification | Off | Rust | unsafe count ratchet |
| bench-regression | Performance | Warning | Rust, Go, Python, C/C++ | benchmark drift via harness adapters (deterministic counts or BCa intervals) |
| archive-contents | Integrity | Off | Any | distribution archive must contain required paths and zero forbidden developer artifacts |
| manifest-sync | Integrity | Off | Any | reconcile git-tracked files against packaging manifest declarations |
| version-lockstep | Integrity | Off | Any | version declarations across headers, manifests, and files must remain in lockstep |
| gate-command-lint | Agent Guard | Off | markdown, shell | reject vacuous, masked, or inverted verification commands in plan files |
| mechanism-sections | Hygiene | Off | markdown | require (inferred) or (verified: <target>) evidence tags in root cause sections |
| citation-anchors | Hygiene | Off | markdown, any | verify path:line@sha citations against quoted text and git history, and relative Markdown links against the tree |
| claim-labels | Hygiene | Off | markdown, any | require RUN or READ labels on verification claims in PR bodies, commits, and Markdown |
Audit-Trailed Override Directives
Legitimate deletions or changes are authorized through explicit, scoped directives in the PR description or commit message; on a push to the default branch, the merged pull request's body is read through the forge (merged-pr-body), because a squash or rebase merge drops it from the commit message. Every applied override is recorded in the report; directives.max_overrides caps them per change, and directives.require_approval makes them count only once the forge shows an approving review of the head commit by a listed reviewer other than the author. policy_from: base judges a change by the base branch's discipline.toml, so a policy edit takes effect once it has merged.
Composite Action Inputs
Complete input parameters for GitHub Actions, Gitea Actions, and Forgejo Actions:
| Input | Default | Description |
|---|---|---|
| config | discipline.toml |
Path to discipline.toml. When the default discipline.toml is absent, built-in defaults apply (each gate at its built-in default enablement and severity); any other path that does not exist is an error (exit 2). |
| suite | all |
Suite to run: all, agent-guard, hygiene, integrity, quality, verification, bench |
| base_ref | (none) | Branch or commit the change is measured against. Default: PR base branch, else the pushed-from commit, else the default branch. |
| enable | (none) | Gate ids to force on (comma or newline separated). See `discipline gates`. |
| disable | (none) | Gate ids to force off (comma or newline separated), e.g. "time-estimates, pii". |
| config_override | (none) | Inline TOML merged over discipline.toml: tables merge, lists append, scalars replace. |
| hostname_denylist | (none) | Hostnames the pii gate must reject (comma or newline separated). Pass a secret; matches are never echoed. |
| fail_on_warnings | false |
Treat warnings as failures. |
| fail_on_overrides | false |
Treat the overrides a change writes as failures (requires human sign-off); inline markers already in the tree are not counted. |
| advisory | false |
Advisory mode: run all checks and emit reports, but exit code 0 even if violations occur. |
| comment | false |
Post the report as one pull-request comment, edited on every run. Needs `pull-requests: write` (GitHub) or a token that can comment; a fork's read-only token is reported, not failed. |
| token | ${{ github.token }} |
Forge token handed to discipline: read by the gates that ask the forge (issue-link verify_references, ratified-paths, review-threads, require_approval; GitHub GraphQL needs a token), and used to post the comment when `comment` is true. |
| policy_from | head |
Which side's discipline.toml judges the change: 'head' (the change's own copy) or 'base' (the base ref's, so a policy edit takes effect once merged; config-integrity still reports it). |
| actor | ${{ github.event.pull_request.user.login || github.actor }} |
Login judged against allowed_override_actors. Default: the pull request author (github.event.pull_request.user.login), which the server sets; otherwise github.actor or the forge equivalent. The triggering login is not used on a pull request, since whoever edits the description must not be able to authorize their own override. |
| directive_sources | (none) | Comma-separated list of allowed directive sources (pr-body, commits, merged-pr-body). merged-pr-body reads, on a push event, the body of the merged pull request each pushed commit arrived through. |
| pr_body | ${{ github.event.pull_request.body }} |
PR description: carries override directives and is itself scanned by hygiene gates. |
| pr_title | ${{ github.event.pull_request.title }} |
PR title: checked by hygiene gates (e.g. issue-link). |
| working_directory | . |
Directory of the repository to check. |
| version | (none) | Release to download (e.g. v0.1.0). Default: the tag this action was referenced by (`@vX.Y.Z`); for a major tag, a commit SHA or a branch, the release in the action''s own Cargo.toml; the latest release only when neither is available. |
| binary_path | (none) | Use this discipline binary instead of downloading one (air-gapped Gitea/Forgejo runners, self-tests). |
| download_url | https://github.com/orieg/discipline/releases |
Base URL of the release store, for mirrors: https://, file://, or http:// to a loopback address. Set DISCIPLINE_INSTALL_ALLOW_INSECURE_HTTP=1 in the environment to accept another cleartext mirror. |
| require_provenance | false |
Fail the install, before the binary runs, when its build provenance cannot be verified (provenance output unverified). A binary_path binary is accepted. Set it on a mirror install once the mirror serves discipline.intoto.jsonl and the runner has gh. |
| trusted_root | (none) | Path on the runner to a Sigstore trusted root (the output of gh attestation trusted-root) for the bundle check of a mirror install, so it needs no network. Never take it from the mirror itself. Default: Sigstore TUF repository, online. |
| install_only | false |
Install discipline and put it on PATH for the steps that follow, without running a check. For `.github/workflows/copilot-setup-steps.yml`, so the hooks of Copilot cloud agent find the binary. |
| baseline_file | (none) | Path to grandfathering baseline file (defaults to discipline-baseline.toml if present). |
| no_baseline | false |
Ignore grandfathering baseline even if present. |
| ci_context | (none) | Rollup job only: `toJson(needs)` of the rollup job (inline JSON, or a path to a file holding it) for the ci-skip-set gate. Empty: the gate reports "not evaluated". |
| test_report | (none) | Path to test report XML file relative to repo root (JUnit XML) for test-floor identity ratcheting. |
| test_base_report | (none) | In-job base test report XML file for test-floor identity ratcheting in dual-report mode. |
| test_head_report | (none) | In-job head test report XML file for test-floor identity ratcheting in dual-report mode. |
Composite Action Outputs
Step outputs emitted by the composite action:
| Output | Description |
|---|---|
| status | pass, fail, or error |
| errors | Number of blocking violations |
| warnings | Number of non-blocking violations |
| failed_gates | Comma separated ids of the gates that reported a violation |
| overrides | Number of applied override directives |
| baselined | Number of grandfathered findings matching baseline (not blocking) |
| overridden_gates | Comma separated ids of the gates that had an override applied |
| passed_gates | Number of passing gates |
| examined_items | Total number of items examined across all enabled gates |
| report | Path of the JSON report |
| provenance | How the binary was checked: verified (the build provenance attestation of the discipline release workflow, from the GitHub API or from the Sigstore bundle discipline.intoto.jsonl of a mirror), unverified (SHA256SUMS of the release store only: no gh, or a mirror without the bundle), or local (binary_path) |
| install_error | Why installing the binary failed (empty on success) |
| bin | Path to the installed discipline executable |
Zero-dependency, standalone binary. With the default gates, a pull-request run reads git in memory and makes no network requests; the forge-reading gates (issue-link verification, review-threads, ratified-paths) and --comment are opt-in.
Available as a one-line installer, native APT/RPM packages, Homebrew formula, pre-compiled static binary, container image, or from source via Cargo.