Gate Specifications & Enforcement Rules

This document establishes the normative enforcement rules, detection capabilities, limits, and configuration keys for all gates in discipline.

Superseded when: A gate rule is amended, a new gate ships, or language-pack detection boundaries expand. Update in place; do not fork.


Gate Catalog

| Gate id | Suite | Status | Languages | Rule Description | |—|—|—|—|—| | agents-md | agent-guard | shipped | any | AGENTS.md exists; CLAUDE.md / GEMINI.md do not fork it | | assertion-reduction | agent-guard | shipped | 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 | shipped | 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 | shipped | 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 | shipped | Rust | unsafe blocks / impls carry a // SAFETY: comment | | deletion-rationale | agent-guard | shipped | any | deleted files and removed tests need a scoped removes: rationale | | time-estimates | hygiene | shipped | any | no calendar / duration estimates in markdown or the PR body | | pii | hygiene | shipped | 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 | shipped | any | agent scratch state is never tracked | | shell-secrets | hygiene | shipped | 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 | shipped | any | PR title or description links a tracking issue (#123, Fixes #123) | | review-threads | hygiene | shipped | any | the pull request has no unresolved review thread | | ratified-paths | agent-guard | shipped | any | edits to protected paths carry an owner’s ratification on an issue the pull request closes | | citation-metadata | hygiene | shipped | any | CITATION.cff and .zenodo.json are valid, agree with each other, and cite the concept DOI | | commit-provenance | hygiene | shipped | any | commits carry the required trailers; an agent-produced commit carries a review by someone else | | config-integrity | integrity | shipped | any | a change cannot weaken its own discipline.toml without a token | | stub-bodies | agent-guard | shipped | 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 | shipped | Go, Python, JS/TS, Rust | a file the test runner loads does not make a failing run pass | | error-swallowing | agent-guard | shipped | 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 | shipped | 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 | shipped | 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 | shipped | 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 | shipped | 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 | shipped | any | changes stay inside authorized paths | | suppression-delta | agent-guard | shipped | per pack | newly added linter / compiler suppression annotations | | provenance-tags | hygiene | shipped | any | published numerics carry (measured|target|projected) | | ci-integrity | integrity | shipped | any | workflow weakening: continue-on-error, || true, unpinned actions | | ci-skip-set | integrity | shipped | any | rollup skip set matches each job’s if: under the observed filter outputs | | test-floor | integrity | shipped | any | test-count ratchet read from the base ref | | golden-output | integrity | shipped | any | prevents stealth edits to committed golden/test output files without explicit override | | dependency-delta | integrity | shipped | any | manifest diff inspection: zero wildcards, source/license allowlists, and deny.toml verification | | test-budget | integrity | shipped | Rust, Python, JS/TS, Go, any | property-test and fuzz effort ratchet (cases, shrink iters, fuzztime, seed corpus) | | pr-checklist | hygiene | shipped | any | ticked PR checkboxes and declared touched surfaces are reconciled against the diff | | command | verification | shipped | any | fail-closed wrapper for any tool: zero-tests guard, canary, count ratchet | | sanitizers | verification | shipped | Rust, C/C++ | ASan / TSan preset with audited suppressions and a race canary | | msrv | quality | shipped | Rust | cargo check under the pinned MSRV | | miri | verification | shipped | Rust | Miri tiers with zero-tests guard | | unsafe-budget | verification | shipped | Rust | unsafe count ratchet | | bench-regression | bench | shipped | Rust, Go, Python, C/C++ | benchmark drift via harness adapters (deterministic counts or BCa intervals) | | archive-contents | integrity | shipped | any | distribution archive must contain required paths and zero forbidden developer artifacts | | manifest-sync | integrity | shipped | any | reconcile git-tracked files against packaging manifest declarations | | version-lockstep | integrity | shipped | any | version declarations across headers, manifests, and files must remain in lockstep | | gate-command-lint | agent-guard | shipped | markdown, shell | reject vacuous, masked, or inverted verification commands in plan files | | mechanism-sections | hygiene | shipped | markdown | require (inferred) or (verified: ) evidence tags in root cause sections | | [`citation-anchors`](#citation-anchors) | hygiene | **shipped** | markdown, any | verify path:line@sha citations against quoted text and git history, and relative Markdown links against the tree | | [`claim-labels`](#claim-labels) | hygiene | **shipped** | markdown, any | require RUN or READ labels on verification claims in PR bodies, commits, and Markdown |

Scope on an Agent Edit

An agent’s edit hook (discipline hook run after an edit) runs the gates about the changed files (file) and the change as a whole (change set). A gate about the whole repository or the finished change (repository), or about the pull request and its commits (provenance), runs on an edit only when the change touches one of its inputs. Otherwise it is reported as not evaluated, with the note not evaluated on an edit, and counts neither as passed nor as failed. The agent prompt names those gates, and the hook log lists them as not_evaluated. The end of the turn, pre-commit and CI run every gate. A run in CI is never treated as an edit, even when the hook’s variable is set. The scope is the whole change set, never the one file the agent edited: deletion-rationale, scope-confinement and ratified-paths judge the whole diff. The trigger paths are the default locations of a gate’s inputs. A gate configured to read files elsewhere (ci-integrity workflows) is still checked at the end of the turn. instruction-smuggling keeps its own hook branch: an unchanged generated hook file is a note in any hook’s check, edit or stop alike, because no hook passes the pull-request body that records it.

| Gate id | Scope | On an agent edit | |—|—|—| | agent-scratch | file | runs | | agents-md | repository | runs when the change touches AGENTS.md, CLAUDE.md, GEMINI.md; otherwise end of turn and CI | | archive-contents | repository | not evaluated; end of turn and CI | | assertion-reduction | file | runs | | bench-regression | repository | not evaluated; end of turn and CI | | build-hooks | file | runs | | ci-integrity | repository | runs when the change touches .github/workflows/**, .gitea/workflows/**, .forgejo/workflows/**, .gitlab-ci.yml, .gitlab-ci.yaml, **/action.yml, **/action.yaml; otherwise end of turn and CI | | ci-skip-set | repository | runs when the change touches .github/workflows/**, .gitea/workflows/**, .forgejo/workflows/**, .gitlab-ci.yml, .gitlab-ci.yaml, **/action.yml, **/action.yaml; otherwise end of turn and CI | | citation-anchors | file | runs | | citation-metadata | file | runs | | claim-labels | change set | runs | | command | repository | not evaluated; end of turn and CI | | commit-provenance | provenance | not evaluated; end of turn and CI | | config-integrity | file | runs | | deletion-rationale | change set | runs | | dependency-delta | repository | not evaluated; end of turn and CI | | error-swallowing | file | runs | | gate-command-lint | file | runs | | golden-output | repository | not evaluated; end of turn and CI | | harness-tampering | file | runs | | ignored-tests | file | runs | | instruction-smuggling | file | runs | | issue-link | provenance | not evaluated; end of turn and CI | | manifest-sync | repository | not evaluated; end of turn and CI | | mechanism-sections | file | runs | | miri | repository | not evaluated; end of turn and CI | | msrv | repository | not evaluated; end of turn and CI | | pii | file | runs | | pr-checklist | provenance | not evaluated; end of turn and CI | | provenance-tags | file | runs | | ratified-paths | change set | runs | | review-threads | provenance | not evaluated; end of turn and CI | | sandbox-config | file | runs | | sanitizers | repository | not evaluated; end of turn and CI | | scope-confinement | change set | runs | | shell-secrets | file | runs | | stub-bodies | file | runs | | suppression-delta | file | runs | | test-budget | repository | not evaluated; end of turn and CI | | test-floor | repository | not evaluated; end of turn and CI | | time-estimates | file | runs | | toolchain-config | file | runs | | unsafe-budget | repository | not evaluated; end of turn and CI | | unsafe-safety-comment | file | runs | | vacuous-tests | file | runs | | version-lockstep | repository | not evaluated; end of turn and CI |

Finding Codes

Every finding carries a code, gate/code (ci-integrity/unpinned-action, vacuous-tests/vacuous-test-added): the code field of the JSON report and the [gate/code] heading of the agent-prompt report that hooks and discipline mcp hand to agents. discipline explain <gate> lists a gate’s codes. The code names what the finding protects, not how it is detected: a gate that learns to catch more cases of the same problem keeps the code. Codes are registered in src/findings.rs; the title beside a code is display text. A rule id that starts with policy/ in a SARIF, JUnit or GitLab report is not a finding code: it is a policy refusal (an override budget, a required approval, a hidden directive that was not read), which no gate reports and no directive lifts (docs/CONFIGURATION.md, “Policy Refusals”).

Titles. A title is display text and may be reworded in any release; nothing keys on it. Titles follow one grammar, which src/findings.rs tests: Title Case with every word capitalized, ASCII only, no data (paths, counts, versions and names go in the message), unique within a gate. A title names either something present at head (Hardcoded Secret (GitHub Token)) or a change (Verification Job Removed); a trailing parenthesis holds one value from a closed set: the mechanism in its literal spelling ((continue-on-error), (--locked)) or a class ((tests)). Verbs keep one meaning each: Added a new entry; Removed an entry gone, Deleted a file gone; Missing required but absent; Changed any edit; Weakened a construct kept but made weaker; Loosened a threshold relaxed; Widened scope or authority grown; Narrowed verification coverage shrunk; Increased / Decreased a count or size; Masked a failure hidden; Regressed a measurement worsened; Detected found at head.

All finding codes | Code | Title | |---|---| | `assertion-reduction/source-parsed-with-errors-preprocessor` | Source File Parsed With Errors (preprocessor) | | `assertion-reduction/source-parsed-with-errors` | Source File Parsed With Errors | | `assertion-reduction/nul-byte-added` | NUL Byte Added To Source File | | `assertion-reduction/assertion-failure-caught` | Assertion Failure Caught Inside Test | | `assertion-reduction/assertion-bound-loosened` | Assertion Bound Loosened | | `assertion-reduction/weakened-bound` | Assertion Comparison Weakened | | `assertion-reduction/expected-value-changed` | Expected Value Changed In Existing Test | | `assertion-reduction/expected-exception-widened` | Expected Exception Or Panic Widened | | `assertion-reduction/mocking-increased-without-stronger-assertions` | Mocking Increased Without Stronger Assertions | | `assertion-reduction/fatal-assertions-weakened` | Fatal Assertions Weakened To Non-Fatal | | `assertion-reduction/assertions-reduced` | Assertion Count Decreased In Existing Test | | `assertion-reduction/test-cases-reduced` | Test Cases Reduced In Parametrized Test | | `assertion-reduction/test-helper-weakened` | Test Helper Function Weakened | | `assertion-reduction/self-comparison-assertion-introduced` | Self-Comparison Assertion Introduced In Existing Test | | `vacuous-tests/asserts-only-on-mocks` | Test Asserts Only On Mocks | | `vacuous-tests/asserts-only-trivial-properties` | Test Asserts Only Trivial Properties | | `vacuous-tests/vacuous-test-added` | Vacuous Test Added | | `vacuous-tests/assertion-density-below-floor` | Insufficient Assertion Density | | `vacuous-tests/self-comparison-assertion-added` | Self-Comparison Assertion In New Test | | `ignored-tests/skip-justification-insufficient` | Skip Justification Insufficient | | `ignored-tests/ignored-test-added` | Ignored Test Added | | `ignored-tests/existing-test-skipped` | Existing Test Skipped | | `ignored-tests/test-sleep-added` | Test Sleep Added | | `ignored-tests/test-retry-added` | Test Retry Added | | `ignored-tests/test-conditionally-skipped` | Test Conditionally Skipped | | `unsafe-safety-comment/safety-comment-missing` | Unsafe Without SAFETY Comment | | `deletion-rationale/file-deleted-without-rationale` | File Deleted Without Rationale | | `deletion-rationale/test-removed-without-rationale` | Test Removed Without Rationale | | `deletion-rationale/test-shadowed-by-redefinition` | Test Shadowed By A Redefinition | | `agents-md/agents-md-missing` | AGENTS.md Missing | | `agents-md/agent-guide-forked` | Forked Agent Guide | | `time-estimates/time-estimate` | Time Estimate | | `pii/host-or-pii-leak` | Host / PII Leak | | `agent-scratch/agent-scratch-tracked` | Tracked Agent Scratch State | | `shell-secrets/argv-env` | Unsafe Shell Pattern (ARGV-ENV) | | `shell-secrets/argv-docker` | Unsafe Shell Pattern (ARGV-DOCKER) | | `shell-secrets/argv-inline` | Unsafe Shell Pattern (ARGV-INLINE) | | `shell-secrets/inject-xargs` | Unsafe Shell Pattern (INJECT-XARGS) | | `shell-secrets/inject-pipe` | Unsafe Shell Pattern (INJECT-PIPE) | | `shell-secrets/github-token` | Hardcoded Secret (GitHub Token) | | `shell-secrets/aws-access-key` | Hardcoded Secret (AWS Access Key) | | `shell-secrets/slack-token` | Hardcoded Secret (Slack Token) | | `shell-secrets/llm-api-token` | Hardcoded Secret (OpenAI / Anthropic Token) | | `shell-secrets/private-key-block` | Hardcoded Secret (Private Key Block) | | `shell-secrets/authorization-bearer-token` | Hardcoded Secret (Authorization Bearer Token) | | `shell-secrets/password-flag` | Hardcoded Secret (Command-Line Password Flag) | | `shell-secrets/credential-assignment` | Hardcoded Secret (Literal Credential Assignment) | | `issue-link/directive-in-subject-line` | Directive In Subject Line | | `issue-link/issue-link-missing-in-commit-message` | Tracking Issue Link Missing In Commit Message | | `issue-link/issue-link-missing-in-commits` | Tracking Issue Link Missing In Commits | | `issue-link/issue-link-missing` | Tracking Issue Link Missing | | `issue-link/issue-reference-not-found` | Tracking Issue Reference Not Found | | `issue-link/issue-reference-closed` | Tracking Issue Reference Closed | | `review-threads/unresolved-review-thread` | Unresolved Review Thread | | `ratified-paths/protected-path-unratified` | Protected Path Edited Without Ratification | | `ratified-paths/never-ratifiable-path-changed` | Never-Ratifiable Path Edited | | `ratified-paths/ratification-entry-malformed` | Ratification Entry Refused | | `ratified-paths/ratification-names-never-ratifiable-path` | Ratification Names A Never-Ratifiable Path | | `ratified-paths/ratification-author-not-accepted` | Ratification Author Not Accepted | | `ratified-paths/ratification-comment-edited` | Ratification Comment Not Accepted | | `ratified-paths/ratification-outside-window` | Ratification Outside Its Window | | `ratified-paths/ratification-by-pull-author` | Ratification By The Pull Request's Author | | `citation-metadata/cff-invalid` | CITATION.cff Is Not Valid | | `citation-metadata/zenodo-invalid` | Zenodo Metadata Is Not Valid | | `citation-metadata/doi-malformed` | Malformed DOI In Citation Record | | `citation-metadata/doi-not-concept` | Citation DOI Is Not The Concept DOI | | `citation-metadata/records-disagree` | Citation Records Disagree | | `commit-provenance/commit-trailer-missing` | Commit Trailer Missing | | `commit-provenance/agent-commit-without-review` | Agent Commit Without Review | | `commit-provenance/agent-commit-reviewed-by-author` | Agent Commit Reviewed By Its Author | | `config-integrity/gate-weakened` | Gate Weakened By This Change | | `config-integrity/base-configuration-unreadable` | Base Configuration Unreadable | | `config-integrity/baseline-new-findings` | Baseline Contains New Findings | | `config-integrity/baseline-increased` | Baseline Increased | | `config-integrity/baseline-migration-not-alone` | Baseline Migration Mixed With Other Changes | | `golden-output/snapshot-added-for-existing-test` | Snapshot Added For Existing Test | | `golden-output/golden-output-regenerated-without-source-change` | Golden Output Regenerated Without Source Change | | `golden-output/golden-output-changed-without-directive` | Golden Output Changed | | `stub-bodies/stub-body-added` | Stub Body Added | | `stub-bodies/body-replaced-by-stub` | Function Body Replaced By Stub | | `stub-bodies/equality-made-constant` | Equality Method Made Constant | | `stub-bodies/test-runner-probe-added` | Test Runner Probe Added To Production Code | | `harness-tampering/test-main-result-discarded` | Go TestMain Discards The Test Result | | `harness-tampering/pytest-hook-masks-results` | Pytest Hook Masks Test Results | | `harness-tampering/unittest-result-method-replaced` | Unittest Result Method Replaced | | `harness-tampering/harness-exits-zero` | Test Harness Exits With Status Zero | | `error-swallowing/result-discarded` | Result Discarded | | `error-swallowing/value-discarded` | Value Discarded | | `error-swallowing/empty-error-handler-added` | Empty Error Handler Added | | `error-swallowing/error-logged-and-dropped` | Error Logged And Dropped | | `error-swallowing/unparseable-input-skipped` | Unparseable Input Skipped | | `error-swallowing/error-silenced` | Error Silenced | | `error-swallowing/error-replaced-by-constant` | Error Replaced By Constant | | `error-swallowing/test-path-reclassification` | File Renamed Into Test Scope | | `instruction-smuggling/agent-instructions-changed` | Agent Instructions Changed | | `instruction-smuggling/invisible-characters-added` | Invisible Characters Added | | `instruction-smuggling/instruction-like-text-added` | Instruction-Like Text Added | | `instruction-smuggling/invisible-characters-in-description` | Invisible Characters In Change Description | | `instruction-smuggling/instruction-like-text-in-description` | Instruction-Like Text In Change Description | | `build-hooks/install-hook-added` | Install Hook Added | | `build-hooks/install-hook-network-or-shell` | Install Hook Runs Network Or Shell | | `build-hooks/build-script-added` | Build Script Added | | `build-hooks/build-script-network-or-shell` | Build Script Gains Network Or Shell Access | | `build-hooks/package-manager-config-changed` | Package Manager Configuration Changed | | `toolchain-config/toolchain-config-change-not-analysed` | Toolchain Configuration Change Not Analysed | | `toolchain-config/toolchain-config-deleted` | Toolchain Configuration Deleted | | `toolchain-config/toolchain-config-unreadable` | Toolchain Configuration Unreadable | | `toolchain-config/toolchain-config-weakened` | Toolchain Configuration Weakened | | `sandbox-config/sandbox-config-widened` | Sandbox Configuration Widened | | `sandbox-config/sandbox-change-not-analysed` | Sandbox Configuration Change Not Analysed | | `sandbox-config/sandbox-config-unreadable` | Sandbox Configuration Unreadable | | `scope-confinement/file-in-forbidden-scope` | File In Forbidden Scope | | `scope-confinement/file-outside-authorized-scope` | File Outside Authorized Scope | | `scope-confinement/planned-file-in-forbidden-scope` | Planned File In Forbidden Scope | | `scope-confinement/planned-file-outside-authorized-scope` | Planned File Outside Authorized Scope | | `scope-confinement/plan-without-declared-outputs` | Plan Without Declared Outputs | | `gate-command-lint/pipeline-exit-masked` | Pipeline Exit Masked | | `gate-command-lint/pre-existing-path-test` | Pre Existing Path Test | | `gate-command-lint/prose-negation-detected` | Prose Negation Detected | | `gate-command-lint/unfailable-command` | Unfailable Command | | `gate-command-lint/no-gates-declared` | No Gates Declared | | `mechanism-sections/untagged-mechanism-claim` | Untagged Mechanism Claim | | `mechanism-sections/evidence-target-not-found` | Evidence Target Not Found | | `mechanism-sections/empty-designated-section` | Empty Designated Section | | `citation-anchors/commit-unresolvable` | Commit Unresolvable | | `citation-anchors/file-not-found` | File Not Found | | `citation-anchors/line-out-of-bounds` | Line Out Of Bounds | | `citation-anchors/text-mismatch` | Text Mismatch | | `citation-anchors/unanchored-citation` | Unanchored Citation | | `citation-anchors/tracker-title-mismatch` | Tracker Title Mismatch | | `citation-anchors/quote-too-short` | Quoted Text Too Short | | `citation-anchors/link-target-missing` | Link Target Missing | | `suppression-delta/suppression-added` | Suppression Added | | `provenance-tags/table-numerics-unprovenanced` | Unprovenanced Table Numerics | | `provenance-tags/mechanism-claim-without-evidence` | Mechanism Claim Without Evidence | | `provenance-tags/wall-clock-ratio-without-interval` | Bare Wall-Clock Ratio Without Interval | | `provenance-tags/paired-figures-without-workload-tag` | Paired Figures Without Workload Tag | | `provenance-tags/cross-metric-figures-without-workload-tag` | Cross-Metric Figures Without Workload Tag | | `provenance-tags/pending-measurement-without-open-issue` | Pending Measurement Without Open Issue | | `provenance-tags/superseded-figure-republished` | Superseded Figure Republished | | `provenance-tags/placeholder-provenance-tag` | Measured Tag Names No Host Or Commit | | `provenance-tags/unresolvable-measured-commit` | Measured Tag Commit Does Not Resolve | | `provenance-tags/unresolvable-record-commit` | Result Record Commit Does Not Resolve | | `provenance-tags/figure-disagrees-with-artifact` | Tagged Figure Not In Cited Artifact | | `ci-integrity/verification-workflow-deleted` | Verification Workflow Deleted | | `ci-integrity/rollup-needs-removed` | Rollup Job Needs Entry Removed | | `ci-integrity/rollup-needs-incomplete` | Rollup Job Needs Incomplete | | `ci-integrity/job-count-file-missing` | Documented Job Count File Missing | | `ci-integrity/job-count-mismatch` | Documented Job Count Mismatch | | `ci-integrity/pull-request-target-trigger` | Dangerous Trigger (pull_request_target) | | `ci-integrity/workflow-permissions-widened` | Workflow Permissions Widened | | `ci-integrity/workflow-timeout-removed` | Workflow Timeout Removed (timeout-minutes) | | `ci-integrity/verification-job-removed` | Verification Job Removed | | `ci-integrity/job-timeout-removed` | Job Timeout Removed (timeout-minutes) | | `ci-integrity/verification-step-removed` | Verification Step Removed | | `ci-integrity/job-failure-masked-continue-on-error` | Verification Job Failure Masked (continue-on-error) | | `ci-integrity/step-failure-masked-continue-on-error` | Verification Step Failure Masked (continue-on-error) | | `ci-integrity/verification-job-masked-by-condition` | Verification Job Masked By Condition | | `ci-integrity/unpinned-action` | Unpinned Third-Party Action | | `ci-integrity/unpinned-container-image` | Unpinned Container Image | | `ci-integrity/banned-action` | Banned Action Referenced | | `ci-integrity/template-injection` | Untrusted Expression Interpolated Into A Run Script | | `ci-integrity/secrets-inherit` | Reusable Workflow Inherits Every Secret | | `ci-integrity/checkout-persists-credentials` | Checkout Persists Credentials In A Job That Can Write | | `ci-integrity/secrets-with-third-party-action` | Job Reading Secrets Runs A Third-Party Action | | `ci-integrity/schedule-trigger-with-secrets` | Scheduled Workflow Reads Secrets | | `ci-integrity/discipline-action-policy-from-weakened` | Discipline Action Weakened (policy_from) | | `ci-integrity/discipline-version-changed` | Discipline Version Chosen By The Change | | `ci-integrity/discipline-action-disable-input` | Discipline Action Weakened (disable input) | | `ci-integrity/discipline-action-advisory` | Discipline Action Weakened (advisory: true) | | `ci-integrity/discipline-action-fail-on-warnings-off` | Discipline Action Weakened (fail_on_warnings: false) | | `ci-integrity/discipline-action-config-override-invalid` | Discipline Action Input Invalid (config_override) | | `ci-integrity/discipline-action-suite-changed` | Discipline Action Suite Changed | | `ci-integrity/discipline-action-directive-sources-widened` | Discipline Action Directive Sources Widened | | `ci-integrity/discipline-run-advisory` | Discipline Run Weakened (--advisory) | | `ci-integrity/compiler-deny-warnings-removed` | Compiler Flag Removed (-D warnings) | | `ci-integrity/cargo-locked-removed` | Cargo Flag Removed (--locked) | | `ci-integrity/frozen-install-flag-removed` | Frozen Install Flag Removed | | `ci-integrity/install-command-weakened` | Install Command Weakened | | `ci-integrity/clippy-all-targets-removed` | Clippy Flag Removed (--all-targets) | | `ci-integrity/verification-step-masked-by-condition` | Verification Step Masked By Condition | | `ci-integrity/verification-step-narrowed` | Verification Step Narrowed | | `ci-integrity/exit-code-masked` | Command Exit Code Masked | | `ci-integrity/pipeline-file-unreadable` | Pipeline File Unreadable | | `ci-integrity/job-failure-masked-allow-failure` | Verification Job Failure Masked (allow_failure) | | `ci-integrity/verification-job-made-manual` | Verification Job Made Manual | | `ci-integrity/verification-job-narrowed` | Verification Job Narrowed | | `ci-skip-set/change-job-missing-from-needs` | Change-Detection Job Missing From Needs | | `ci-skip-set/change-job-did-not-succeed` | Change-Detection Job Did Not Succeed | | `ci-skip-set/unconditional-job-missing-from-needs` | Unconditional Job Missing From Needs | | `ci-skip-set/unconditional-job-skipped` | Unconditional Job Skipped | | `ci-skip-set/needs-names-undefined-job` | Undefined Job In Needs | | `ci-skip-set/skip-decision-unverifiable` | Skip Decision Unverifiable | | `ci-skip-set/job-skipped-while-condition-true` | Job Skipped While Condition True | | `ci-skip-set/job-ran-while-condition-false` | Job Ran While Condition False | | `test-floor/floor-constant-missing-in-base` | Floor Constant Missing In Base Ref | | `test-floor/floor-constant-file-missing-in-base` | Floor Constant File Missing In Base Ref | | `test-floor/floor-constant-decreased` | Floor Constant Decreased | | `test-floor/configured-floor-decreased` | Configured Test Floor Decreased | | `test-floor/required-suite-missing` | Required Test Suite Missing | | `test-floor/test-count-below-floor` | Test Count Below Floor | | `test-floor/test-dropped-from-suite` | Test Dropped From Suite | | `test-floor/untrusted-test-command` | Untrusted Test Command | | `test-floor/tests-moved-out-of-default-run` | Tests Moved Out Of Default Run | | `test-floor/test-density-regression` | Test Density Regression | | `dependency-delta/lockfile-deleted` | Lockfile Deleted | | `dependency-delta/lockfile-entry-from-new-source` | Lockfile Entry From New Source | | `dependency-delta/lockfile-integrity-hash-removed` | Lockfile Integrity Hash Removed | | `dependency-delta/manifest-changed-without-lockfile` | Manifest Changed Without Lockfile | | `dependency-delta/direct-dependency-added` | Direct Dependency Added | | `dependency-delta/dependency-constraint-loosened` | Dependency Constraint Loosened | | `dependency-delta/dependency-source-changed` | Dependency Source Changed | | `dependency-delta/wildcard-dependency-version` | Wildcard Dependency Version | | `dependency-delta/unpinned-git-dependency` | Unpinned Git Dependency | | `dependency-delta/banned-dependency` | Banned Dependency | | `dependency-delta/dependency-outside-allowlist` | Dependency Outside Allowlist (allow_dependencies) | | `dependency-delta/dependency-outside-deny-allowlist` | Dependency Outside Allowlist (deny.toml) | | `dependency-delta/unauthorized-git-source` | Unauthorized Git Repository Source | | `test-budget/fuzz-target-removed` | Fuzz Target Removed | | `test-budget/fuzz-target-deleted` | Fuzz Target Deleted | | `test-budget/test-budget-decreased` | Test Budget Decreased | | `test-budget/seed-corpus-decreased` | Seed Corpus Size Decreased | | `pr-checklist/checklist-claims-tests` | Checklist Claim Unsupported (tests) | | `pr-checklist/checklist-claims-docs` | Checklist Claim Unsupported (docs) | | `pr-checklist/checklist-claims-benchmarks` | Checklist Claim Unsupported (benchmarks) | | `pr-checklist/checklist-claim-unsupported` | Checklist Claim Unsupported | | `pr-checklist/prose-claims-tests` | Prose Claims Tests The Change Does Not Contain | | `pr-checklist/undeclared-boundary-delta` | Boundary Change Not Declared | | `command/untrusted-command-modification` | Untrusted Command Modification | | `command/policy-file-deleted` | Policy File Deleted | | `command/canary-diagnostic-missing` | Canary Diagnostic Missing | | `command/canary-command-succeeded` | Canary Command Succeeded | | `command/command-failed` | Command Failed | | `command/forbidden-output` | Forbidden Output Detected | | `command/zero-items-executed` | Zero Items Selected Or Executed | | `command/count-below-ratchet` | Command Count Below Ratchet Floor | | `command/count-pattern-unmatched` | Count Pattern Unmatched | | `command/base-test-failed` | Base Test Failed Against Head Code | | `command/base-tests-no-cases` | Base Tests Read No Test Cases | | `command/snapshot-mismatch` | Command Output Differs From Snapshot | | `sanitizers/canary-diagnostic-missing` | Canary Diagnostic Missing | | `sanitizers/violation-detected` | Sanitizer Violation Detected | | `sanitizers/untrusted-command-modification` | Untrusted Command Modification | | `msrv/msrv-declaration-missing` | MSRV Declaration Missing | | `msrv/msrv-command-failed` | MSRV Command Failed | | `msrv/untrusted-command-modification` | Untrusted Command Modification | | `miri/zero-tests-executed` | Zero Tests Executed | | `miri/undefined-behavior-detected` | Undefined Behavior Detected | | `miri/untrusted-command-modification` | Untrusted Command Modification | | `unsafe-budget/budget-exceeded` | Unsafe Budget Exceeded | | `unsafe-budget/unsafe-added-without-authorization` | Unsafe Code Added | | `unsafe-budget/unsafe-count-increased` | Unsafe Count Increased | | `bench-regression/benchmark-artifact-deleted` | Benchmark Artifact Deleted | | `bench-regression/new-artifact-baseline-missing` | Benchmark Baseline Missing For New Artifact | | `bench-regression/benchmark-provenance-mismatch` | Benchmark Provenance Mismatch | | `bench-regression/cross-host-comparison` | Cross-Host Benchmark Comparison Mismatch | | `bench-regression/benchmark-baseline-missing` | Benchmark Baseline Missing | | `bench-regression/override-void-citation-does-not-measure` | Regression Override Void (Citation Does Not Measure This Code) | | `bench-regression/override-unverified-citation-undecidable` | Regression Override Unverified (Citation Undecidable) | | `bench-regression/counter-regressed` | Deterministic Counter Regressed | | `bench-regression/benchmark-removed` | Benchmark Removed | | `bench-regression/new-or-renamed-arm-baseline-missing` | Benchmark Baseline Missing For New Or Renamed Arm | | `bench-regression/performance-regressed` | Benchmark Performance Regressed | | `bench-regression/override-void-no-resolvable-citation` | Regression Override Void (No Resolvable Citation) | | `bench-regression/override-void-names-no-regressed-arm` | Regression Override Void (Names No Regressed Arm) | | `bench-regression/counter-regressed-unapproved-arm` | Deterministic Counter Regressed (Unapproved Arm) | | `bench-regression/stale-arm-exemption` | Stale Benchmark Arm Exemption | | `bench-regression/paired-ratio-not-comparable` | Paired Ratio Not Comparable | | `bench-regression/paired-ratio-cell-missing` | Paired Ratio Cell Missing | | `bench-regression/paired-ratio-inconsistent-with-rounds` | Paired Ratio Inconsistent With Rounds | | `bench-regression/paired-ratio-regressed` | Paired Ratio Regressed | | `bench-regression/ratio-baseline-loosened` | Ratio Baseline Loosened | | `bench-regression/ratio-baseline-changed-with-source` | Ratio Baseline Changed With Source | | `bench-regression/paired-ratio-run-missing` | Paired Ratio Run Missing | | `archive-contents/required-path-missing` | Required Archive Path Missing | | `archive-contents/forbidden-entry` | Forbidden Entry In Archive | | `archive-contents/source-leaked` | Source Leaked In Archive | | `archive-contents/source-map-shipped` | Source Map Shipped | | `manifest-sync/manifest-drift` | Manifest Synchronization Drift | | `version-lockstep/version-mismatch` | Version Declaration Lockstep Mismatch | | `claim-labels/claim-without-label` | Verification Claim Without Label | | `claim-labels/label-placeholder` | Verification Label Placeholder | | `claim-labels/read-label-unresolvable` | Unresolvable Read Label | | `engine/could-not-run` | Check Could Not Run |

Directive Policy

Each gate has at most one canonical directive (with at most one documented deprecated spelling); allow-test-shrink serves both test-floor and test-budget. A few gates (unsafe-safety-comment, agents-md, pii, agent-scratch, ci-skip-set, time-estimates) have no directive: they are lifted by fixing the finding, an inline marker where the gate documents one, or exempt_paths. Directives must be scoped to their natural subject (file path, test name, action ref, workflow job, dependency name, or rule identifier). Blanket waivers without subjects are rejected. The subject is the first thing after the colon and the rest is the reason: allow-ignore: test_sync flaky on the shared runner lifts a finding on test_sync, and allow-ignore: test_other skipped the way test_sync was does not, because a word of the reason never names a subject. One directive line names one subject: there is no list form, so two subjects take two lines (Syntax & Grammar).


Language Scope & Detection Boundaries

Most gates are language-independent and inspect text, git diffs, configuration, workflows, or repository metadata; the Languages column of the catalog above is the authority for each. The AST gates (assertion-reduction, vacuous-tests, ignored-tests, unsafe-safety-comment, error-swallowing, stub-bodies, harness-tampering, suppression-delta, and the phrase tier of instruction-smuggling) operate through tree-sitter AST extraction.

What no gate here catches. Every gate reads a change. Code that is built to the tests its author can see, without editing them (overfitting to the visible suite), passes all of them: nothing in the diff is weaker than before. The control is held-out tests the author of the change cannot see, which the repository owns (docs/ARCHITECTURE.md §1.4; see SpecBench, arXiv:2605.21384).

When a change touches source files in a language without an active pack, each AST gate names the unanalysed files in its report notes (F7) rather than rendering a silent zero.

Language Packs

Language Test function patterns Assertion vocabulary (strong = equality / pattern) Skip markers (ignored-tests) Suppression markers (suppression-delta) Status
Rust #[test], #[tokio::test], #[async_std::test], #[rstest] assert*!, debug_assert*!, prop_assert*!; strong: _eq, _ne, matches #[ignore], #[cfg_attr(..., ignore)]; conditional: #[cfg_attr(<predicate>, ignore)], a #[cfg(..)] on a Cargo feature #[allow(...)], #[expect(...)] (the // SAFETY: rule is unsafe-safety-comment) shipped
Python pytest / unittest collection rules: test* functions, test* methods of Test* classes and TestCase subclasses (self_test() is not a test unless [tests] functions declares it); a name defined twice in one module or class body is one test, the last def (plain, async or decorated), since the later definition rebinds the name. A def inside an if, try or other compound statement is a conditional definition and is not judged as rebinding: each such def is read as a test. A rebinding by assignment (test_f = other) is not read assert statements, self.assert*, pytest.raises, pytest.approx, a called mock assertion method (m.assert_called_once_with(1), also assert_called, assert_called_once, assert_called_with, assert_awaited*, assert_not_called, assert_any_call, assert_has_calls; never strong; the name read without a call is not one) (#828); strong: ==, !=, in, not in, is, is not (against a non-None operand), assertEqual family, assertIsNone; weak: is not None, isinstance, issubclass, assertIsNotNone, assertIsInstance, assertNotIsInstance, assertTrue, assertFalse, < / <= / > / >=; tautology: assert True, assert x == x, self.assertTrue(True), an assert of a non-empty tuple (assert (x == 1, "msg")), and assert m.called_once_with(1) (also called_with, has_calls, any_call, called_once, not_called) on a mock the test function makes without a spec (#836), and an equality whose one side is a name the function binds only to the same call as the other side (expected = f(2) then assert f(2) == expected or self.assertEqual(f(2), expected); see assertion-reduction) (#899), and an assertion whose every call is to a target the test patches (with mock.patch('src.calc.f', return_value=9): then assert calc.f(1) == 9; see vacuous-tests) (#899); parenthesised forms are read as the expression inside, for strength, tautology, expected values and bounds (assert (f(3) == 6), "msg" is the equality; a parenthesised tuple stays a tuple) (#859) @pytest.mark.skip / xfail, @unittest.skip; a module-scope pytest.importorskip("m") or pytest.skip(.., allow_module_level=True) in an except ImportError handler, for every test of the file, when m is a module of the repository; a call of a module-level test function that the file’s if __name__ == "__main__": entry point no longer makes (#843); conditional: @pytest.mark.skipif, @pytest.mark.xfail(<condition>), @unittest.skipIf / skipUnless, a name the file binds to one of these marks, pytest.skip() under an if, the module-scope import skips of another project’s module # type: ignore / # type:ignore, # noqa (any letter-case, also after other comment text), # pylint: disable= / disable-next=, # pragma: no cover shipped
JavaScript / TypeScript test( / it( callbacks (Jest, Vitest, Mocha, node:test) expect(...).matcher, assert.*; strong: toBe, toEqual, toStrictEqual, assert.strictEqual, and assert(a === b) / assert.ok(a === b) whose first argument is one === / == / !== / != comparison (#862); weak: toBeTruthy, toBeDefined, assert(x) / assert.ok(x) with any other argument; a matcher that returns a promise (.resolves, .rejects, chai-as-promised .eventually, assert.rejects, assert.doesNotReject, Deno assertRejects) counts only when the test waits on it (#829) .skip, .todo, xit, xdescribe, Mocha this.skip(); conditional: .skipIf(..), .runIf(..) @ts-ignore, @ts-expect-error, @ts-nocheck, eslint-disable*, istanbul ignore / c8 ignore shipped
Golden (PHPT) Standard PHPT sections (--TEST--, --FILE--, --EXPECT--) Exact expectation sections (--EXPECT--, --EXPECTF--, --EXPECTREGEX--) --SKIPIF--, --XFAIL-- — shipped
Java @Test, @ParameterizedTest, @RepeatedTest (JUnit 4/5, TestNG) JUnit assert*, AssertJ assertThat(...); strong: assertEquals, assertThrows, isEqualTo @Disabled, @Ignore, @Test(enabled = false); conditional: JUnit 5 @DisabledIf* / @EnabledIf* / @DisabledOn* / @EnabledOn*, assumeTrue / assumeFalse / assumingThat / assumeThat / assumeNotNull, also under an if @SuppressWarnings (qualified or not), // NOSONAR, //noinspection <id> shipped
Kotlin JUnit 4 / 5 and TestNG @Test, @ParameterizedTest, @RepeatedTest, @TestFactory; Kotest StringSpec / FunSpec / DescribeSpec / ShouldSpec / ExpectSpec / FeatureSpec / BehaviorSpec bodies; test* functions in a test path kotlin.test and JUnit assert*, assert(...), AssertJ / Truth assertThat, Kotest shouldBe and the other should* matchers (infix or call), assertThrows<E> { } / shouldThrow<E> { }; strong: assertEquals, shouldBe, assertThat; tautology: assertTrue(true), assertEquals(x, x), x shouldBe x @Disabled, @Ignore, @Test(enabled = false), class-level @Disabled, Kotest "!name", xtest / xit / xdescribe, .config(enabled = false); conditional: the JUnit 5 annotations and assumptions of the Java row @Suppress, @SuppressWarnings, @SuppressLint (qualified or not, @file: included), // NOSONAR shipped
Swift XCTest func test*() in an XCTestCase subclass (any type in a test path); Swift Testing @Test XCTAssert*, XCTFail, XCTUnwrap, #expect, #require; strong: XCTAssertEqual, #expect(a == b), #require throw XCTSkip(..), the .disabled(..) trait; conditional: XCTSkip under if / guard, XCTSkipIf(..), XCTSkipUnless(..), .disabled(if: ..), .enabled(if: ..) // swiftlint:disable shipped
Scala ScalaTest test("x") { }, "x" should "y" in { }, WordSpec / FunSpec nesting; MUnit test("x") { }; specs2 "x" >> { }; JUnit @Test assert, assertEquals, assertResult, intercept[E], assertThrows[E], fail, should / must matchers; strong: assert(a == b), assertEquals, shouldBe x ignore("x"), ... ignore { }, test("x".ignore), pending, cancel(...); conditional: cancel(..) / pending under an if, assume(..) @nowarn, @SuppressWarnings (qualified or not), // scalastyle:off, // scalafix:off / ok shipped
Objective-C XCTest - (void)test* with no parameters in an XCTestCase subclass (a *Tests class, or any class in a test path) XCTAssert*, XCTFail; strong: XCTAssertEqual, XCTAssertEqualObjects XCTSkip(..); conditional: XCTSkip under an if, XCTSkipIf(..), XCTSkipUnless(..) #pragma clang diagnostic ignored, // NOLINT shipped
C / C++ GoogleTest TEST*, Catch2 TEST_CASE, doctest, C ABI smoke (main, test_*) EXPECT_* / ASSERT_*, REQUIRE / CHECK, assert(...); strong: _EQ, _STREQ, comparison operators DISABLED_ prefix, GTEST_SKIP(), Catch2 SKIP() / [.], Unity TEST_IGNORE() / TEST_IGNORE_MESSAGE(..); conditional: the skip calls under an if // NOLINT shipped
Go func Test*(t *testing.T), subtests t.Error* / t.Fatal*, testify assert.* / require.*; strong: Equal, DeepEqual t.Skip*; conditional: t.Skip* under an if //nolint, //lint:ignore, revive:disable shipped
PHP PHPUnit test* methods, @test docblock / attribute (a top-level test* function only under a test path or a [tests] paths glob), Pest test( / it( PHPUnit assert*, Pest matchers (->toBe, ->toEqual); strong: assertEquals, assertSame, assertCount $this->markTestSkipped(), $this->markTestIncomplete(), ->skip(); conditional: markTestSkipped() / markTestIncomplete() under an if, #[Requires*] attributes // @psalm-suppress, // @phpstan-ignore, // phpcs:ignore shipped
C# [Fact], [Theory] (xUnit), [Test] (NUnit), [TestMethod] (MSTest) Assert.*, StringAssert.*, CollectionAssert.*; strong: Equal, Same, AreEqual / AreSame (NUnit, MSTest), Assert.That(a, Is.EqualTo(b)) / Is.SameAs, Throws [Ignore], [Fact(Skip = "...")], Assert.Ignore(..), Assert.Skip(..); conditional: those calls under an if, Assume.That(..), Skip.If(..) / Skip.IfNot(..), Assert.SkipWhen(..) / Assert.SkipUnless(..), [Fact(Skip = "..", SkipWhen = nameof(X))] / SkipUnless #pragma warning disable, [SuppressMessage] shipped
Ruby def test_* (Minitest, Test::Unit), it / specify (RSpec) assert_*, refute_*, RSpec expect(...).to eq(...); strong: assert_equal, eq xit, xdescribe, :skip, skip, omit, pending; conditional: those calls under if / unless (statement or modifier), a ternary or && / ||, omit_if(..), omit_unless(..) # rubocop:disable / todo, spaced or not (#rubocop:disable) shipped

Confidence & Stakes Severity Hierarchy

Discipline organizes violation severity into a 3-tier hierarchy based on detection confidence and operational stakes:

  1. error (Blocking, Exit 1): High-confidence violations representing clear security compromises, assertion drops, or intentional test erosions. Fails CI by default.
  2. warning (Non-blocking, Exit 0 unless --fail-on-warnings): Heuristic or prose scans where context matters, or performance/benchmark gates that may fluctuate across hardware. Visible in all reports; promoted to blocking with --fail-on-warnings.
  3. note (Informational, Exit 0): Advisory annotations (e.g. conditional skips on target platforms, stale baseline entries, unanalysed languages). Mapped to note in SARIF, info in GitLab Code Quality, and GitHub Actions notice annotations.

Default Severity by Gate

Default enablement and default severity are part of the compatibility contract (docs/ARCHITECTURE.md §3.1): within a major version a default may only become stricter unless the change is recorded in the Default Changes ledger. tests/test_config.rs::default_enablement_and_severity_match_snapshot pins every row below, so a default cannot change without a deliberate edit that shows up in review.

A default is chosen from two inputs: detection confidence (how often a finding on an arbitrary repository is a real defect) and the cost of being wrong (a false block stops an unrelated merge; a missed finding lets erosion through). A gate whose finding population in an unknown repository is high because the flagged construct is also a routine, reviewed practice defaults to warning: it stays visible in every report and blocks under --fail-on-warnings or an explicit severity = "error".

Default-on gates:

Gate Default Detection confidence Cost of being wrong Rationale
assertion-reduction on, error High: AST count and strength comparison of the same test on base and head. False block: a legitimate test refactor needs a scoped allow-assertion-drop: directive. Miss: silent loss of coverage. The construct flagged (a test losing assertions) is rare in reviewed changes and is the core erosion this tool exists to stop. self-comparison-assertion-introduced is a warning whatever the gate’s severity: it matches an exact form, and a deliberate reflexivity or not-NaN check has that form.
vacuous-tests on, error High: AST detection of a new test with no non-tautological assertion. False block: an assertion helper not yet listed in assert_helper_fns. Miss: a test that can never fail. Only new tests are judged, so the population is bounded by the change itself. self-comparison-assertion-added is a warning whatever the gate’s severity, for the reason given for assertion-reduction; a test of nothing but self-comparisons is the vacuous test, at the gate’s severity.
ignored-tests on, error (conditional skips: note; CI-conditional skips: error, set by ci_skip_severity) High: AST skip markers on tests added or changed in the diff. False block: an intentional skip needs allow-ignore:. Miss: a disabled test counted as passing. Platform-predicated skips are already downgraded to note; unconditional skips are rare and deliberate.
unsafe-safety-comment on, error High: AST unsafe block or impl without a preceding // SAFETY: comment. False block: a missing comment on a sound block, fixed by writing it. Miss: an undocumented soundness invariant. Only diff-touched unsafe sites are checked; writing the comment is the fix.
deletion-rationale on, error High: git records the deletion exactly. False block: a planned removal needs one removes: line. Miss: a stealth deletion of tests or benchmarks. Deletions are infrequent and the fix is a single scoped line.
pii on, error High for home paths, LAN IPs, denylisted hosts and fixed-format tokens; diff_only = false, so the whole tracked tree is swept. False block: a pre-existing documentation path. Miss: a leaked workstation path or internal host in a public repository. A leak is not reversible once published. Brownfield adopters use diff_only = true, allowed_users, or the grandfathering baseline.
agent-scratch on, error High: a tracked path matching agent state directories. False block: a deliberately committed directory of the same name, exempted by path. Miss: private agent state in history. The path set is narrow and the committed state is not reversible once pushed.
shell-secrets on, error (token rules) / warning (heuristic rules) High for structured tokens; heuristic for argv and pipe patterns. False block: a token-shaped test fixture, exempted by path. Miss: a live credential in history. The split already downgrades the heuristic rules at finding level.
config-integrity on, error High: base and head configuration are diffed structurally. False block: an intended loosening needs allow-gate-weakening:. Miss: a change lowering its own bar (F9). The gate protects every other gate; it cannot be advisory.
instruction-smuggling on, error (invisible characters, instruction files) / warning (phrases, encoded blobs) High for a bidi override or a zero-width character: the code point is either there or not. High for an instruction-file edit: the path is the fact. Low for a phrase: a paraphrase defeats it. False block: a localisation table with directional marks needs exempt_paths or a directive; every AGENTS.md edit needs a directive. Miss: any injection that avoids the phrase list. What a reviewer sees must be what the parser reads; what the next agent reads must be reviewed as code. The phrase tier is a tripwire and is reported as one.
error-swallowing on, error High: the handler body is read from the syntax tree; base and head are compared per file as a multiset, so a moved handler is not new. False block: a deliberate best-effort handler needs allow-swallow: or an inline marker. Miss: a discarded result the language does not mark (_); a Rust let _ = <call> whose fallible callee is off the known-fallible name list is reported at warning, not blocked. An empty catch or a discarded Result is how a failure an agent cannot fix stops surfacing.
stub-bodies on, error High: the body is read from the syntax tree and is the whole body; base and head are compared per function. False block: a deliberate placeholder needs allow-stub:. Miss: a stub that carries one extra statement, or a body that special-cases the inputs its tests use. An added todo!() or a body replaced by return null is the change no other gate sees.
harness-tampering on, error (harness-exits-zero: warning at most) High: each form is an exact shape of the syntax tree of a file the test runner loads, and only the forms a change adds are reported, so an existing tree does not fail on upgrade. False block: a harness file that rewrites a status on purpose needs a rewrite or allow-harness-tampering:; two such idioms are reported by design (see the gate’s entry). Miss: a harness written round the exact forms. A harness file that makes a failing run pass is seen by no test-facing gate. The three exact forms (a Go TestMain that drops the result, a pytest hook that rewrites outcomes or removes items, a replaced unittest result method) report at error: each one makes failing tests report success, and severity follows that, not how often the form appears. On by default since #833: the agent-PR precision study v3 ran with the gate enabled and it reported nothing on 232 agent pull requests (measured, results-v3/summary.json), which bounds false blocks at about 0.016 per pull request (Wilson 95% upper bound); that is a bound on false blocks, not a precision estimate. An exit with status zero in a harness file, or at the top level of a Python module a test may import (#843), never reports above warning: the same call ends a healthy tear-down in working repositories, and no measured false-positive rate on a stated corpus supports blocking on it. Raising it is a change to a severity that consumers key on, and goes through a deprecation, not a silent change.
build-hooks on, error High for a lifecycle script or a manager-config path: the JSON key or the path is the fact. Medium for a build-script line: token match on added lines. False block: a prepare: husky hook or a private-registry .npmrc needs allow-build-hook:. Miss: a hook that shells out through a script file the token list does not see. Code that runs on every install is the workflow an agent can still reach after ci-integrity closes the workflow files.
toolchain-config on, error High for a data file: base and head are diffed structurally against a per-tool rule table. A configuration written as code is reported at warning as changed, not analysed, except the keys of a literal Jest or Vitest object, which are judged like a data file. False block: an intended loosening needs allow-toolchain-weakening:. Miss: a lint or type bar lowered in the same change that would have failed it. Same stakes as ci-integrity dropping -D warnings, one file over.
sandbox-config on, error High for a data file: base and head are diffed structurally against a per-agent rule table, and a mode is judged against the value an absent key takes. A firewall script is reported at warning as changed, not analysed. False block: an intended widening (a new allowed command, an MCP server) needs allow-sandbox-widening:. Miss: a key or a value the table does not model. A widened sandbox removes the boundary every other control assumes.
ci-integrity on, error High for continue-on-error, \|\| true and unpinned actions in modified workflows (diff_only = true). False block: an intended pattern needs allow-ci-weakening:. Miss: a rollup that reports green while a job is skipped. Only modified workflows are scanned by default, so pre-existing patterns do not block adoption.
ci-skip-set on, error High: each needs result is compared to its job’s if: evaluated over the observed filter outputs; an unmodelled term is a finding, not a guess. Inert (a named “not evaluated” note) unless the rollup job supplies DISCIPLINE_CI_CONTEXT. False block: a rollup whose workflow uses an if: form outside the modelled subset. Miss: a skip set the evaluator cannot distinguish from a legitimate one (all filters false on a change that touches no filtered path), reported as a note. Supplying the context is the opt-in, so enabling it by default costs an ordinary diff check nothing.
test-floor on, error High: the base-ref test count is the floor unless one is configured. False block: a test consolidation needs allow-test-shrink:. Miss: silent test-count erosion. The ratchet is relative to the base ref, so it never fails a repository for its existing state.
golden-output on, error High: an edit to a committed golden or snapshot file. False block: a legitimate snapshot update needs allow-golden-update:. Miss: an expectation rewritten to match a regression. Snapshot edits are exactly the change a reviewer must see called out.
dependency-delta on, error High: manifest diffs are parsed; only changed dependencies are judged. False block: a new dependency outside the allow-list needs allow-dependency: or an allow-list entry. Miss: a wildcard or unpinned git dependency. The judged population is the dependencies the change adds.
test-budget on, error High: property-test and fuzz budgets compared base to head. False block: an intended budget cut needs allow-test-shrink:. Miss: a fuzz or proptest budget quietly reduced. Budgets are rarely edited and a cut is a deliberate decision.
command on, error Inert until a command, preset, or DISCIPLINE_COMMAND is configured; then the configured tool’s own verdict. False block: only a failure of the tool the repository chose to run. Miss: none while inert. Enabling it by default costs nothing; configuring it is the opt-in.
suppression-delta on, warning Syntactically high, semantically low: #[allow(...)] is the reviewed escape hatch from clippy -D warnings, and # noqa / // nolint are routine. False block at error: 78 findings (measured) across one consumer’s last 100 merged pull requests, nearly all reviewed and intended. Miss at warning: none; the finding is still reported. The flagged construct is a normal reviewed practice, so blocking by default fails ordinary changes. Repositories that treat every new suppression as a defect set severity = "error" (this repository does).
time-estimates on, warning Heuristic prose match over the whole tree (diff_only = false). False block at error: at v0.4.2, 51 findings (measured) in one consumer repository and 26 (measured) in another, nearly all pre-existing documentation. Prose context decides whether a duration is an estimate.
bench-regression on, warning Depends on the adapter: deterministic counts are high, wall-clock intervals are hardware-sensitive. False block: runner jitter on wall-clock benchmarks. Miss: a real regression, still reported. Projects with deterministic counters set severity = "error".
citation-metadata on, error High: both files are parsed and their shape is checked against the Citation File Format 1.2.0 and Zenodo’s deposit vocabulary; ORCID check digits are computed. The concept-DOI rule relies on how identifiers describes each DOI. False block: none known; a CFF 1.2.0 feature outside the checked subset is ignored, not rejected. Miss: a DOI that is well formed but does not resolve, or names another work. A repository with neither file examines nothing, and only a change that edits one is judged, so enabling it costs nothing elsewhere.
agents-md on, warning High, but the finding is documentation hygiene, not a code defect. False block: a repository without an agent guide fails every change. Missing or forked guidance does not make a change unsafe.

Default-off gates (issue-link, review-threads, ratified-paths, commit-provenance, provenance-tags, pr-checklist, scope-confinement, archive-contents, manifest-sync, version-lockstep, sanitizers, miri, unsafe-budget, msrv) need repository-specific input (a tracker convention, a forge token, protected paths and their owners, archive path, manifest rules, version sources, toolchain) or encode a policy most repositories do not hold. They default to error so that enabling one is a single enabled = true line that blocks.

Finding-Level Severity Overrides

Certain gates distinguish high-confidence rules from heuristic indicators within the same gate:

Severity Lifecycle

How a gate’s default severity is allowed to move. This section is doctrine for changes to the built-in defaults; it changes no behaviour of the binary.


Shipped Gates

Pillar 1: Agent Conformance and Diff Guard (agent-guard)

assertion-reduction

vacuous-tests

ignored-tests

error-swallowing

instruction-smuggling

stub-bodies

harness-tampering

unsafe-safety-comment

deletion-rationale

agents-md

scope-confinement

suppression-delta

gate-command-lint


Pillar 2: Hygiene (hygiene)

time-estimates

pii

agent-scratch

shell-secrets

commit-provenance

citation-metadata

review-threads

ratified-paths

provenance-tags

pr-checklist

mechanism-sections

citation-anchors

claim-labels


Pillar 3: Integrity (integrity)

config-integrity

build-hooks

toolchain-config

sandbox-config

golden-output

dependency-delta

test-budget

ci-integrity

ci-skip-set

test-floor

Counting basis: static or runtime

test-floor can count tests two ways, and the two numbers for the same repository routinely differ. A floor is only meaningful against the basis it was measured on.

  Static count (default) Runtime count (test_command)
What is counted Every test function the language packs find in tracked files (#[test], def test_*, @Test, it(...) and so on), read from source without building anything, except in a file a parsed runner configuration excludes (see What the static count cannot see). The lines ending in : test that the command prints (the cargo test -- --list format), or a single integer if the command prints only that.
Sees Every test function in the source, including tests behind a #[cfg(...)] or cargo feature that is off and tests in files no target compiles. Only what compiled and was enumerated in that build: the enabled features, the current platform, the targets the command selects. It also sees what the source scan does not: doc-tests, and tests generated by macros or parametrisation.
Base ref Counted the same way, so the zero-config ratchet (no floor configured) compares HEAD against the base ref on one basis. Not available: the base ref would have to be checked out and built. A floor must therefore be configured (min_tests, or constant_file + constant_name); test_command without one exits 2 rather than compare a runtime count against a static one.
Cost Parses tracked files; no toolchain needed. Whatever the command costs: for cargo test -- --list, a full test build.

Why they differ. Suppose the static count is 1046 and cargo test -- --list reports 300. Neither is wrong. The static scan counts every test function it can see, whether or not a build would include it; a workspace with large feature-gated or platform-gated suites, or test files outside any target, counts far higher statically. The runtime list counts one concrete build, so it drops everything that build excludes and adds doc-tests and generated cases the scan never sees. The gap is a property of the repository, not an error, and it is stable only as long as the build configuration is.

Which to use.

Worked example: reproducing a cargo test -- --list floor. A legacy script ran cargo test --workspace --all-features -- --list, counted the lines ending in : test, and failed below 300. The equivalent configuration:

[gates.test-floor]
enabled = true
# Same command and flags the floor was measured with. Runs from the
# repository root, without a shell: no pipes, no `| wc -l`.
test_command = "cargo test --workspace --all-features -- --list"
# The ported floor, on the same (runtime) basis.
min_tests = 300

The gate counts the lines ending in : test (: benchmark lines and the N tests, M benchmarks summaries are not counted), and that count is what min_tests is compared against: the gate reports it as the outcome’s examined value and quotes it in a Test Count Below Floor finding. A failing command (non-zero exit, or a binary not on PATH) is a gate error with exit 2, never a count of zero. tests/test_adoption.rs pins this: a repository with two static test functions and a five-test listing passes a floor of 5 and fails a floor of 6 with the listing’s count in the message.

A change cannot supply the command. test_command is executed, so under the default policy side, where the gate reads the change’s own discipline.toml, it is compared with the merge base copy before anything runs. A test_command the change adds or alters (including one in a configuration the base does not have, or whose base copy does not load) is not run: the gate reports test-floor/untrusted-test-command, takes no count (examined is 0) and skips the floor comparison. No directive lifts it. The runner’s environment authorises the change with DISCIPLINE_ALLOW_COMMAND_CHANGE, the switch of the command gate, so one switch covers both gates. DISCIPLINE_COMMAND supplies that gate’s table command and authorises nothing here. In a CI run with no base at all (--staged before the first commit) a configured test_command is refused the same way. Removing test_command runs nothing and is config-integrity’s to report. Under --policy-from base the base copy is the one in force: its test_command is what runs and nothing is reported.

The command runs under bounds. test_command goes through the same runner as the command gate: it is given 600 seconds (the gate has no timeout_seconds; this is the longest limit a built-in gate gives a command), and its stdout is captured up to 25 MiB. A command that times out, is not on PATH, or exits non-zero stops the run (exit 2), and so does a listing cut off at the capture limit: a partial listing is not a count, and neither is zero.

To port the other way, adopting the static basis instead, run discipline check once with the gate enabled and no floor; the outcome’s examined value is the static count to set as min_tests.

Test identity ratcheting (JUnit XML)

When test reports are present, test-floor enforces identity-based ratcheting in addition to test counts. It parses JUnit XML test reports and asserts:

Identity Invariant: Every test ID that passed on the base ref must pass on the head ref.

A test that was passing on base and becomes missing, skipped, or failed on head is reported as Test Dropped From Suite (test-floor/test-dropped-from-suite), preventing count-preserving test removal or deselection evasion (e.g. dropping a complex test and adding a trivial passing test).

Configuring test reports:

Lifting an intentional test removal: An intentional test removal, rename, or drop must be excused with an explicit directive naming the test:

allow-test-shrink: test_logout merged into unified session test

or

removes: test_logout deprecated legacy endpoint test
What the static count cannot see

Several kinds of test erosion are visible only at run time and escape static AST counts. What the static count does see:

Still visible only at run time:

Configuring test_report closes these gaps by ratcheting the set of executed test identities across base and head (discipline doctor reports an informational finding when a runner is detected without test_report). The change that adds test_report, head_report or test_command to a discipline.toml the base ref already has moves the count to another basis, so config-integrity reports it (`test_report` changed from unset to ...): put allow-gate-weakening: test-floor <reason> on its own line in that change’s PR body or a commit message. A repository’s first discipline.toml has no base copy to compare with and needs none.

Tests moved out of the default run

One rule holds for the count and one for the finding, and neither changes the other.

ignored-tests reads skips inside a test (t.Skip), not a file’s build constraint. With test_report (and base_report) configured, a test that stops running also leaves the head report and is reported as Test Dropped From Suite.

archive-contents

manifest-sync

version-lockstep

Writing groups: a worked multi-ecosystem example

Each source is a file and a regex. The gate reads the first match of the regex in the file and takes capture group 1 as the version (the whole match when the regex has no group). So the regex must be anchored tightly enough that its first match is the project’s own version and not a dependency’s, a parent’s or a toolchain floor. Every source in a group must declare the same version; a group needs at least two sources.

The example below keeps one library, example-lib, in lockstep across six ecosystems. It is not illustrative only: tests/test_adoption.rs reads this exact block from this file, builds a repository containing all six files (with decoy versions next to each real one), and checks that the gate passes when they agree and names the drifted file when any one of them is bumped alone.

[gates.version-lockstep]
enabled = true

[[gates.version-lockstep.groups]]
name = "example-release"

# Rust. `[package]` then the first line-leading `version =` after it, so a
# dependency's `version = "..."` inline table or `rust-version` never matches.
[[gates.version-lockstep.groups.sources]]
path = "Cargo.toml"
regex = '''(?ms)^\[package\].*?^version\s*=\s*"([^"]+)"'''

# npm. The first "version" key, which is the top-level one: dependency
# entries are keyed by package name, not by "version".
[[gates.version-lockstep.groups.sources]]
path = "package.json"
regex = '''"version"\s*:\s*"([^"]+)"'''

# NuGet. The <Version> element; a <PackageReference Version="..."> is an
# attribute, so it cannot match.
[[gates.version-lockstep.groups.sources]]
path = "dotnet/ExampleLib.csproj"
regex = '''<Version>([^<]+)</Version>'''

# Maven. The <version> that follows the project's own <artifactId>, not the
# <parent> block's version or a dependency's.
[[gates.version-lockstep.groups.sources]]
path = "java/pom.xml"
regex = '''<artifactId>example-lib</artifactId>\s*<version>([^<]+)</version>'''

# RubyGems. `spec.version = "..."`; `required_ruby_version` does not match
# because the regex needs `.version` directly.
[[gates.version-lockstep.groups.sources]]
path = "ruby/example-lib.gemspec"
regex = '''\.version\s*=\s*(?:"|')([^"']+)(?:"|')'''

# C header. The string macro, not EXAMPLE_VERSION_MAJOR and friends: the
# regex requires whitespace and a quote right after the macro name.
[[gates.version-lockstep.groups.sources]]
path = "include/example_lib.h"
regex = '''#define\s+EXAMPLE_VERSION\s+"([^"]+)"'''

The files it reads look like this (the version is 1.4.2 everywhere):

File The line the regex captures from
Cargo.toml version = "1.4.2" under [package]
package.json "version": "1.4.2",
dotnet/ExampleLib.csproj <Version>1.4.2</Version>
java/pom.xml <artifactId>example-lib</artifactId> then <version>1.4.2</version>
ruby/example-lib.gemspec spec.version = "1.4.2"
include/example_lib.h #define EXAMPLE_VERSION "1.4.2"

Notes for adapting it:


Pillar 4: Verification Suite (verification)

command

sanitizers

miri

unsafe-budget


Pillar 5: Quality & Compiler Toolchain (quality)

msrv


Pillar 6: Benchmark Drift (bench)

bench-regression


Preset Profile Configurations

The following curated configuration profiles provide turn-key setups tailored for specific engineering environments. Copy the desired profile directly into discipline.toml at the repository root and replace <project>. Whether warnings block is a property of the run, not of the file: pass --fail-on-warnings (or set DISCIPLINE_FAIL_ON_WARNINGS=1) where a profile says so.

Profile 1: Research & High-Assurance Algorithm Labs

Tailored for scientific computing, cryptographic libraries, and high-assurance algorithmic cores. Enforces zero-tolerance benchmark drift with statistical variance guards, strict hygiene (zero time estimates, PII redaction), property-test ratchets, and immutability of golden outputs.

# Run with --fail-on-warnings.
[meta]
version = 1
name = "<project>"

[directives]
sources = ["pr-body", "commits"]
allow_hidden = false
fail_on_overrides = false

[gates.assertion-reduction]
enabled = true
severity = "error"

[gates.vacuous-tests]
enabled = true
severity = "error"
min_assertions_per_test = 1

[gates.ignored-tests]
enabled = true
severity = "error"

[gates.unsafe-safety-comment]
enabled = true
severity = "error"

[gates.deletion-rationale]
enabled = true
severity = "error"

[gates.time-estimates]
enabled = true
severity = "error"

[gates.pii]
enabled = true
severity = "error"
lan_ips = true
redact_lan_ips = true

[gates.agent-scratch]
enabled = true
severity = "error"

[gates.config-integrity]
enabled = true
severity = "error"

[gates.golden-output]
enabled = true
severity = "error"

[gates.dependency-delta]
enabled = true
severity = "error"
allow_wildcards = false
require_git_pins = true
deny_file = "deny.toml"

[gates.test-budget]
enabled = true
severity = "error"

[gates.bench-regression]
enabled = true
severity = "error"
tolerance_pct = 3.0
max_noise_cv = 0.15
noise_margin_pct = 2.0
allow_cross_host = false

Profile 2: Enterprise Backend & Systems Services

Tailored for production web services, distributed systems, and enterprise microservices. Focuses on multi-language test coverage preservation, supply chain audit verification, PII redaction, and preventing silent test suppression.

[meta]
version = 1
name = "<project>"

[directives]
sources = ["pr-body"]
allow_hidden = false
fail_on_overrides = false

[gates.assertion-reduction]
enabled = true
severity = "error"
assert_helper_fns = ["check_response_ok", "assert_valid_record"]

[gates.vacuous-tests]
enabled = true
severity = "error"

[gates.ignored-tests]
enabled = true
severity = "error"

[gates.deletion-rationale]
enabled = true
severity = "error"
paths = ["tests/**", "src/**", "migrations/**"]

[gates.time-estimates]
enabled = true
severity = "warning"

[gates.pii]
enabled = true
severity = "error"

[gates.agent-scratch]
enabled = true
severity = "error"

[gates.config-integrity]
enabled = true
severity = "error"

[gates.dependency-delta]
enabled = true
severity = "error"
allow_wildcards = false

[gates.command]
enabled = true
severity = "error"

[[gates.command.commands]]
name = "cargo-deny"
preset = "cargo-deny"

Profile 3: AI Coding Agent Diff Sentinel

Designed specifically for automated agent workflows (Claude Code, Antigravity, Copilot, Cursor). Restricts agent drift, prevents deletion or weakening of test suites, blocks ghost/vacuous tests with assertion density requirements, rejects placeholder justifications (e.g. todo, fix later), and bans ephemeral agent scratch directories from entering git history.

# Run with --fail-on-warnings.
[meta]
version = 1
name = "<project>"

[directives]
sources = ["pr-body"]
allow_hidden = false
fail_on_overrides = false

[gates.agents-md]
enabled = true
severity = "error"

[gates.assertion-reduction]
enabled = true
severity = "error"

[gates.vacuous-tests]
enabled = true
severity = "error"
min_assertions_per_test = 1

[gates.ignored-tests]
enabled = true
severity = "error"

[gates.unsafe-safety-comment]
enabled = true
severity = "error"

[gates.deletion-rationale]
enabled = true
severity = "error"

[gates.time-estimates]
enabled = true
severity = "error"

[gates.pii]
enabled = true
severity = "error"

[gates.agent-scratch]
enabled = true
severity = "error"

[gates.config-integrity]
enabled = true
severity = "error"

[gates.golden-output]
enabled = true
severity = "error"

[gates.test-budget]
enabled = true
severity = "error"

Forge Access

require_open_pending_issues, issue-link’s verify_references, ratified-paths, review-threads, bench-regression citation freshness, directives.require_approval (pull-request reviews), the merged-pr-body directive source on a push event, discipline replay (each replayed change’s merged pull-request body) and discipline doctor read from the forge that hosts the repository; check --comment (opt-in) writes one pull-request comment. No gate needs the network otherwise. Requests are made by the binary itself over HTTPS (rustls; no OpenSSL, no gh, no curl), so they work the same in the static binary and the container.

Forge API base Token (optional for public repositories)
GitHub https://api.github.com; GitHub Enterprise Server <url>/api/v3; GHE.com https://api.<host> DISCIPLINE_FORGE_TOKEN, else GH_TOKEN, else GITHUB_TOKEN (sent as Bearer)
GitLab <url>/api/v4 DISCIPLINE_FORGE_TOKEN, else GITLAB_TOKEN (sent as Bearer)
Gitea <url>/api/v1 DISCIPLINE_FORGE_TOKEN, else GITEA_TOKEN (sent as token)
Forgejo <url>/api/v1 DISCIPLINE_FORGE_TOKEN, else FORGEJO_TOKEN, else GITEA_TOKEN

The forge is identified in this order: DISCIPLINE_FORGE (github, gitlab, gitea, forgejo, with DISCIPLINE_FORGE_URL and DISCIPLINE_FORGE_REPO when the origin remote does not give them); the CI runner (GITLAB_CI with CI_SERVER_URL and CI_PROJECT_PATH; FORGEJO_ACTIONS or GITEA_ACTIONS with GITHUB_SERVER_URL and GITHUB_REPOSITORY; GITHUB_ACTIONS); then the origin host (github.com, gitlab.com or a host containing gitlab, codeberg.org or a host containing forgejo, a host containing gitea); with no remote, GITHUB_REPOSITORY alone means GitHub. A self-hosted forge under another name needs DISCIPLINE_FORGE; without it, a check that needs the forge exits 2.

Response handling was verified against Gitea 1.24.7 and Forgejo 12.0.4 instances and gitlab.com’s public API: issue state, repository default branch, branch protection with and without an admin token.

Legacy Script Parity & Replacement Reference

Discipline provides universal static binary drop-in replacements for the legacy verification scripts in high-assurance repositories:

Gate Replaced Legacy Script Discipline Enhancements & Behavioral Differences
assertion-reduction (none — new capability) Multi-language AST extraction (14 language packs), callback-aware function tracking, compile-time assertions (static_assert, const _: () = assert!).
vacuous-tests (none — new capability) Language-specific AST helper detection (Python non-test methods, C/C++ non-zero return / throw helper recognition).
ignored-tests (none — new capability) Distinguishes newly arriving ignored tests from modified tests, configurable approved skip predicates (cfg_attr(miri, ignore)).
deletion-rationale scripts/check_deletion_rationale.py Line-anchored directive parsing, configurable require_scope and allow_hidden directive controls.
time-estimates scripts/check_docs_hygiene.py Clause-level (sentence-fragment) scoping of matches within a line, boundary lookarounds avoiding \b false positives on symbols (×, ~), diff-scoped mode (diff_only = true), operational term-of-art and wrap window exemptions, docs-lint: allow alias. Paragraph scope applies to allow_patterns only (they match across soft-wrapped lines); built-in patterns do not detect an estimate split across a line break.
pii scripts/check_docs_hygiene.py Full test code inspection without blind spots, JSON string unescaping, cross-tree agent config directory/playbook detection, secret-backed hostname denylist.
test-floor scripts/check_test_floors.py Automatic base-ref constant extraction, direct test_command execution, fail-closed handling on unresolvable base floors, allow-test-shrink: override.
ci-integrity scripts/check_ci_gate.py Complete rollup job needs: closure validation, 40-character commit SHA pinning, masked failure detection (continue-on-error, \|\| true, set +e), allow-ci-weakening: override.
ci-skip-set A rollup skip-set floor script Parses each job’s if: as an expression instead of splitting on \|\|, models GitHub’s implicit success() over transitive dependencies, reads filter outputs from the same toJson(needs) as the results, reports an absent boolean filter output by name, fails closed on unmodelled terms.
bench-regression scripts/perf_report.py, scripts/wasm_fuel.py In-job dual-file mode (--bench-base-file and --bench-head-file), iai-callgrind console line and neutral JSON parsers, two-tier threshold (single-worst above tolerance_pct + noise_margin_pct, or $\ge 2$ arms regressing above noise_floor_pct, default 0.5%; advisory advisory_pct, default 0.1%), declared arm exemptions, sourced overrides verifying CI URL or committed artifact and named arms, missing-baseline fatal fail-closed.
provenance-tags scripts/check_docs_hygiene.py Table numeric provenance ((measured: host, commit), (target), (projected)), mechanism claim hardware counter citations, wall-clock intervals, paired comparison tags ((workload: id)), a superseded-figure registry over markdown and JSON datasets, and pending-measurement issue citations checked for an open issue. Opt-in: a measured tag’s host and commit (the commit resolved in the local object database), the commit of each added result record, and each tagged figure against the data artifact its paragraph cites.
command Bespoke shell runner wrappers Universal fail-closed timeout wrapper, zero-tests guards, turnkey presets (cargo-public-api, miri, sanitizers, loom, cargo-deny, cargo-mutants).

Not replaced: a test that exercises a workflow’s path filters against a golden change-set → job-set table. ci-skip-set checks that the rollup’s skip set agrees with the filter outputs the run observed; it cannot tell whether a filter computed the right value from a correct change set. Keep that table in the repository’s own tests.


Repository Security Boundary & Workspace Ownership

Discipline inspects git history and diffs using libgit2. In CI (CI, GITHUB_ACTIONS, GITLAB_CI, GITEA_ACTIONS or FORGEJO_ACTIONS set), when the base ref cannot be resolved, it runs git fetch --no-tags against origin (30-second timeout) to deepen a shallow checkout; that is the only external git process it starts. The repository’s own .git/config is untrusted input, so the fetch runs with hooks, the file-system monitor, command transports (ext::, fd::, git://) and the upload-pack program overridden, and a command the repository’s own configuration sets for ssh, credentials or a password prompt (core.sshCommand, credential.helper, core.askPass) replaced by the runner’s global or system value (#364). DISCIPLINE_NO_NETWORK=1 skips it. Access to the underlying git repository enforces strict security boundaries that differ between host workstations and containerized environments:

Host Binary Enforcement (CVE-2022-24765 Protection)

On developer workstations and multi-tenant hosts, the native discipline binary enforces strict repository owner validation by default.

Container Image Relaxation

The official container image (ghcr.io/orieg/discipline) intentionally relaxes repository owner validation out-of-the-box (via system-wide safe.directory '*', an entrypoint wrapper registering the active directory, and ENV DISCIPLINE_TRUST_WORKSPACE=1).


Roadmap & Future Gates

All 42 gates across the six suites are implemented and shipped; discipline gates lists them with their effective state. Paired within-run ratio benchmarking shipped as bench-regression mode = "paired-ratio". Known limitations and candidate work are tracked in the “Outstanding Checks & Known Limitations” section of ROADMAP.md.