Orchestrata Ultimate › Gates
Gates — Master Index
A gate in orchestrata-ultimate is a mechanical check with a command, a failure behavior, and no discretion. This index lists every gate in the order a change meets it, with where it runs, what it blocks, the ruling or ADR behind it, and its page. The path itself, with diagrams and a worked example, is on Workflow.
The layers:
- Dispatch and session gates: Claude Code hooks that refuse a brief, a command or the end of a turn.
- The worker's stop gate: the commit gate run on a temporary index before a worker may finish.
- The commit gates:
scripts/hooks/pre-commitandscripts/hooks/commit-msg. - The push gates:
scripts/hooks/pre-pushand the stages ofscripts/run_tests.pyit calls (freshness, affected selection, infrastructure, five safety gates, reuse, rehearsal exclusion, the capped test phase). - The 24 registered battery gates: the
GATESregistry ofscripts/gates.pyplus the__ast__pre-step, cached in.cache/gates.json. - Ship, CI and deploy:
deploy/ship.shverify, the GitLab pipeline, the fleet-drift gates and the deploy launchers' checks. - The codetools gates and the process-level gates that live in workflow law.
The narrative context, where these gates sit in the design, decision and implementation lifecycle, is on Ruleset Engine — The Complete Picture (section 5, "Process-level gates").
The catalogue, in the order a change meets it
1. Dispatch and session (Claude Code hooks)
| Gate | Where it runs | What it blocks | Ruling / ADR | Page |
|---|---|---|---|---|
| dispatch-brief | PreToolUse Agent and Workflow hooks of the orchestrata plugin: task_context_hook.py, require-design-before-build.sh, workflow-default-sonnet.sh |
An Agent brief without the literal GOAL:, FILES:, CONSTRAINTS:, DONE WHEN:, VERIFY: labels; a build dispatch with neither a DESIGN_WAIVER: line nor a design contract; a Workflow script without the Sonnet default |
RUL-022, ADR-0048, RUL-026 | dispatch-brief |
| Session-hook guards (family: 30 guards and 2 plumbing files) | .claude/hooks/*.sh, registered in the repo's .claude/settings.json and re-emitted into the orchestrata plugin, which runs them through _dispatch.sh, on SessionStart, PreToolUse, PostToolUse, Stop and UserPromptSubmit |
Per guard: no-xdist.sh, force-ast-toolkit.sh (hand-reading Python), single-gate-entrypoint.sh, no-unbounded-wait.sh, force-test-observation.sh, no-doubles-py.sh, no-banned-deps.sh, forbidden-doubles-stop.sh and the rest |
per guard, on the family page | family-session-hooks |
| Shell write and delete guard | PreToolUse Bash intercept_bash.py; intercept_write.py and intercept_edit.py on Write and Edit |
Files written through the shell and any deletion command, in every project; a Write over an existing file ("Use Edit tool") | see page | /claude/hooks/intercept_bash |
| Test-run contention guard | test_run_context.py on PreToolUse Bash (also SessionStart and UserPromptSubmit for context) |
CPU-heavy commands while an authoritative run is measuring; ORCH_ALLOW_CONTENDING=1 overrides |
see page | /claude/hooks/test_run_context |
| Install and secret guard | install_secret_guard.py on PreToolUse Bash |
Unpinned package installs, registry overrides, credential materialization | see page | /claude/hooks/install_secret_guard |
| Convention guard | convention_guard.py on PreToolUse Bash, Write, Edit |
Asks rather than denies: a command or edit that contradicts a standing platform convention, confirmed with one keypress | see page | /claude/hooks/convention_guard |
| unshipped-guard | Stop hook in ~/.claude/settings.json (user level) |
Ending a turn while a watched repo has local-only commits this session made | scoping per RUL-035 | unshipped-guard |
2. The worker's stop
| Gate | Where it runs | What it blocks | Ruling / ADR | Page |
|---|---|---|---|---|
| commit-precheck | SubagentStop hook ~/.claude/hooks/precommit_gate_subagent.sh (in ~/.claude/settings.local.json), which runs ~/bin/precommit-check |
A worker's finish while its .py edits fail the commit gate; at most 3 blocks per agent |
RUL-035, RUL-008, RUL-041 | commit-precheck |
3. Commit (scripts/hooks/pre-commit, scripts/hooks/commit-msg)
| Gate | Where it runs | What it blocks | Ruling / ADR | Page |
|---|---|---|---|---|
| secrets | pre-commit, first stage: scripts/check_staged_secrets.py |
A credential-shaped value on an added staged line (exit 1); an unreadable staged set (exit 2, "never silently treated as clean") | none recorded by id; AD-85 (2026-10-17) added the punctuation-only exemption | secrets |
| Project interpreter check | pre-commit, when Python is staged | A commit from a checkout with no .venv/bin/python ("Commit blocked — no project interpreter"), since a checkout without one cannot review |
RUL-021 | review-staged |
| review-staged | pre-commit: scripts/gate.py review-staged (code review over src/, test review over tests/, end2end/, scripts/, verdict derived in commit_review.pl) |
Blocking findings (exit 1) or an invalid review (exit 2: not run, timed out, report that does not add up) | RUL-008, ADR-0101, ADR-0116 | review-staged |
| ai-attribution | commit-msg: check_ai_attribution.py --message-file; also ship verify (--range) and scripts/health_check.py |
An AI trailer (R1) or AI marker (R2) in a new commit message | RUL-007 | ai-attribution |
The pre-commit hook also runs three steps that never block: the stage-walls ledger
(reports/precommit-walls.ndjson, ADR-0101 rule 2), ruff check --fix on the staged
files, and the codebase-map refresh. They are described on
Workflow — the commit gate.
4. Push (scripts/hooks/pre-push and scripts/run_tests.py)
| Gate | Where it runs | What it blocks | Ruling / ADR | Page |
|---|---|---|---|---|
| pre-push-guards | scripts/hooks/pre-push |
A push from a worktree with master or main checked out; a missing origin/master (fail closed); skips a push whose exact source already passed |
RUL-040, RUL-032, RUL-021 | pre-push-guards |
| freshness-guard | run_tests.py step 0 |
A run that would consume stale inputs: a failed refresh aborts the run | the 2026-08-04 ruling in the script's docstring | freshness-guard |
| affected-selection | run_tests.py --affected step 0.75, before infrastructure |
A changed file the selector cannot bound: refused with the missing fact named, never a silent full run | RUL-040, RUL-029 | affected-selection |
| Infrastructure preflight | run_tests.py step 1: Redis, Graph, Gateway, Kernel in parallel, then Link |
Any test run when a service, or the gateway's link to the kernel, is down; the kernel is required | RUL-010 | family-suite-preflight |
| source-provenance | run_tests.py step 2, first of five concurrent safety gates |
A run whose installed package is not this checkout's non-editable install | RUL-021, ADR-0120 | source-provenance |
| parallel-safety | step 2 (also a battery gate) | Test patterns that collide under parallel execution | the 2026-07-10 ruling "no violations allowed" | parallel-safety |
| test-purity | step 2 (also a battery gate) | Mocks, fakes, stubs, monkeypatch, skips; banned-library imports | RUL-002 | test-purity |
| prolog-boundary | step 2 (also a battery gate) | A Python Prolog query aimed at a predicate or arity that does not exist | see page | prolog-boundary |
| rulings-lint | step 2, fifth of five | A ruling with no ## Enforcement section, or a declared mechanism that does not exist |
RUL-014 | rulings-lint |
| green-ledger | pre-push and ship (source stamp); run_tests.py steps 2b and 2c |
Never fails; removes proven modules from the run so a passing test does not run twice | RUL-032 | green-ledger |
| rehearsal-exclusion | run_tests.py, gate mode only |
Rehearsal-class modules in a promotion gate; a missing or wrong-version derivation artifact aborts the run | RUL-039 | rehearsal-exclusion |
| test-phase-cap | run_tests.py step 3; the observer plugin in every pytest a Claude session starts |
A test phase over 300 s (CAPPED, non-zero); a multi-file pytest session without the sanctioned-runner stamp |
RUL-038 amendment, RUL-009 | test-phase-cap |
5. Ship, CI and deploy
| Gate | Where it runs | What it blocks | Ruling / ADR | Page |
|---|---|---|---|---|
| ship-verify | deploy/ship.sh step 1 |
A ship whose ruff, black, RUL-007 range, suite, plugin check or env agreement fails; it then pushes with --no-verify |
RUL-007, RUL-032, RUL-038 | ship-verify |
| fleet-drift | check_infra_drift.py on the orchestrata-infra-drift timer and in health_check.py; check_env_fleet_agreement.py in ship |
Drift between the OpenTofu declaration and the live fleets; env files that disagree with terraform | ADR-0077 | fleet-drift |
| ci-pipeline | GitLab CI from .gitlab-ci.yml on the self-hosted Incus executor |
A failed job fails the pipeline; nothing local waits for it, so CI reports after the push | RUL-004 | ci-pipeline |
| Deploy-time checks | ct-install, tools-deploy, hermes-deploy, install-orchestrata.sh, install-claude-config.sh |
A deploy whose installed thing is not the recorded commit (identity), whose smoke check fails, or whose source is not a pinned ref; failed installs roll back | platform convention 4 (no deploy-in-place) | Deployment |
6. Elsewhere
| Gate | Where it runs | What it blocks | Ruling / ADR | Page |
|---|---|---|---|---|
| codetools gates | the codetools repo: codetools gate provision, readiness, run, cpuset, status; select-affected; the ct-install post-promote checks |
A red readiness probe (refusal class infra); a red batch (bisected); an affected batch with no full verify record |
codetools ADRs 0039, 0040, 0042, 0043, 0044; RUL-040 | codetools-gates |
| Process-level gates (family) | workflow moments: dispatch, landing, fold, commit, ratification, merge | briefs-before-dispatch, evidence-on-landing, fold-verifier, ADR drift, plugin currency, F13's traceability battery, Mohamed's ruling | per row, on the family page | family-process-gates |
Count. The catalogue has 51 entries: 27 in the tables above that are not battery gates (three of them groups with their own page: the 30 session guards, the process gates, the deploy launchers' checks) and the 24 battery gates below. Three gates run in both places (parallel-safety, test-purity, prolog-boundary) and are counted once, in the battery.
How the battery works (one command, three behaviors)
The registered gates have one entry point — enforced by the
single-gate-entrypoint.sh session hook:
python scripts/gates.py # changed gates only (the default)
python scripts/gates.py --all # ignore the cache, run everything
python scripts/gates.py --only ruff mypy # run only the named gate(s)
python scripts/gates.py --no-fix-ast # skip the AST refresh (on by default)
python scripts/gates.py --no-fix # skip the auto-format step
From scripts/gates.py's own docstring, the three behaviors that exist "because the
manual alternative kept costing whole minutes and whole sessions":
- Parallel. "The gates are independent read-only analyses over a tree nobody is mutating, so wall-clock is the slowest gate, not their sum. Measured 2026-08-07: ~50s serial -> ~25s parallel."
- Deduplicated. "Each gate's verdict is cached against a fingerprint of the
inputs it actually reads. Re-running with nothing changed is a no-op that prints
the cached PASS instead of re-deriving it." The cache lives in
.cache/gates.json._fingerprint(roots)(AST toolkit, 2026-10-23) is a sha256 over the relative path and the sha256 of the bytes of every file under the gate's declared input roots, so it is insensitive to mtime;_interestingkeeps only.py,.toml,.pl,.json,.mdand.shfiles and drops cache directories. One suffix set serves every gate. The function's docstring still says "from git's own object hashing"; the code reads the bytes itself. No gate mixes git state into its fingerprint, so a new commit or a movedmasterwith no content change does not re-runfeature-lossorwiring. Two gates (generated-artifacts,worktree-envs) opt out of caching entirely with the root!always(ALWAYS_RUN), because their subject is git or system state no content hash can see. Earlier versions of this page namedGATE_MASKSandGATE_GIT_STATE; neither exists in any commit ofscripts/gates.py(git log --all -S, 2026-10-23). The blind spots this leaves are listed on each gate's page; re-run one withpython scripts/gates.py --only <gate> --all. - Self-healing.
ruff --fix+blackrun before the gates (files written by scripts skip the editor-side autoformat hook), and a stale AST snapshot is refreshed automatically (__ast__— see its page) because "that two-step ordering was documented and still forgotten repeatedly."
Exit code of the battery: 0 only if every gate passed.
The 24 registered gates
Where they run: by hand, python scripts/gates.py. No git hook, CI job or ship step runs
the battery (grep of scripts/hooks/, .gitlab-ci.yml, .github/workflows/ and
deploy/ship.sh, 2026-10-23). Ship runs its own copy of the plugin-drift check, "which
this gate does not run" otherwise (see
ship-verify), and the pre-push suite runs
three of the gates as its safety gates. The battery exits non-zero when any gate fails.
| Gate | Family | What it refuses (one line) | Ruling / ADR | Command | Page |
|---|---|---|---|---|---|
__ast__ |
correctness / lint | Running the battery against a stale AST snapshot, or refreshing it in a way that destroys the runtime call graph | none names the pre-step; snapshot path ADR-0097 | (automatic pre-step) uv run ast-sync + scripts/build_runtime_callgraph.py reaugment |
page |
architecture |
structure | An import that violates the 6-layer model (a lower layer reaching up, a domain service importing a concrete peer) | RUL-008, ADR-0056 | python scripts/architecture_checker.py |
page |
check-deps |
structure | A third-party import that pyproject.toml does not declare |
none by id; the CLAUDE.md Key Rule | python scripts/serialize_codebase_ast.py check-deps --include-tests |
page |
coverage-claude |
delivery | The claude/ tree's measured test coverage dropping below its ratcheted floor |
RUL-034 | python scripts/coverage_ratchet.py --scope claude --paths claude --tests tests/claude tests/scripts/test_task_context_hook.py --min 19.5 |
page |
env-contract |
environment | A production entrypoint that needs infra config but never loads the env contract | RUL-019 | python scripts/check_env_contract.py |
page |
env-subnet-scheme |
environment | An environment profile whose addresses break the dedicated-octet scheme (prod 10.53.1.x / test 10.53.2.x / dev 10.53.3.x) | RUL-003, ADR-0080 | python scripts/check_env_subnet_scheme.py |
page |
external-tools |
structure | A non-PyPI executable that is undeclared, missing, or too old (both directions) | RUL-014, ADR-0089 | python scripts/check_external_tools.py |
page |
feature-loss |
traceability & loss | A requirement silently losing its implementation, or a spec/ADR being deleted while code still cites it | RUL-023, RUL-030 | python scripts/check_feature_loss.py |
page |
generated-artifacts |
traceability & loss | A generated artifact kept out of git, or a generator deleted while its artifacts survive | RUL-030 | python scripts/check_generated_artifacts.py |
page |
lexicon-collision |
lexicon | An intent-lexicon work-type word that also names a subsystem (so "wire the memory engine" misclassifies) | RUL-014 | python scripts/check_lexicon_collisions.py |
page |
lexicon-drift |
lexicon | The generated Prolog intent lexicon drifting from the Python vocabulary that is its source of truth | RUL-030 | python scripts/gen_intent_lexicon.py --check |
page |
mypy |
correctness / lint | A type error reaching CI that a local run should have caught | RUL-008 | uv run mypy --cache-dir .cache/mypy src/ |
page |
parallel-safety |
test discipline | A test pattern that WILL collide under parallel execution (unscoped Cypher, shared Redis keys, session-scope fixtures…) | the 2026-07-10 ruling "no violations allowed" | python scripts/gate_parallel_safety.py |
page |
plugin-drift |
delivery | The generated plugins/orchestrata plugin drifting from the claude/ tree it is derived from |
the "claude/ is production, not scaffolding" ruling | python claude/scripts/gen_plugin.py --check |
page |
prod-separation |
environment | A production systemd unit pointing any path-bearing directive at the development tree | RUL-027 | python scripts/check_prod_tree_separation.py |
page |
prolog-boundary |
structure | A Python Prolog query aimed at a predicate/arity that does not exist in the consulted rule base | see page | python scripts/check_prolog_boundary.py |
page |
prolog-gate |
policy-as-rules | A Key-Rule violation (relative import, bare os.environ[...], banned import, infra default) — proven over a fact base, with 0/1/2 outcomes |
the CLAUDE.md Key Rules | python scripts/gate.py run |
page |
requirement-corpus |
traceability & loss | Production code citing a requirement id the RTM cannot grade (DANGLING) | see page | python scripts/check_requirement_corpus.py |
page |
ruff |
correctness / lint | A lint violation under the project's ruff configuration | RUL-008 | uv run ruff check src/ tests/ scripts/ |
page |
test-purity |
test discipline | A mock/fake/stub/monkeypatch/skip in tests, or a banned-library import anywhere | RUL-002 | python scripts/audit_test_purity.py tests/ --banned-import-only src/ scripts/ |
page |
uncovered-policies |
policy-as-rules | (Advisory) A policy requirement with no enabled mechanism behind it going unseen | RUL-014 | python scripts/gate.py uncovered |
page |
validate-ci |
environment | A CI configuration that bypasses the canonical runner, hardcodes prod IPs, or breaks any of its 14 hard requirements | user ruling 2026-08-03 (requirement 12) | python scripts/validate_ci.py |
page |
wiring |
traceability & loss | A capability added by this branch but left inert — a hook registered nowhere, a module reachable from nothing | RUL-023 | python scripts/check_wiring.py |
page |
worktree-envs |
environment | A worktree whose .venv is a symlink into another checkout's virtualenv |
RUL-021 | python scripts/check_worktree_envs.py --checks venv-symlink |
page |
Counts, verified against scripts/gates.py on 2026-08-29 and again with the AST toolkit on
2026-10-23: 23 GATES registry entries + the __ast__ pre-step = 24 cached gate names.
python in the commands is sys.executable, the interpreter running the battery. ruff
and mypy run through plain uv run, which in a checkout without UV_NO_EDITABLE=1 can
re-sync the venv as an editable install; whether it does here is not measured (see
source-provenance).
The three families
| Family page | Covers | Fires |
|---|---|---|
| Suite preflight gates | source-provenance, parallel-safety (sub-gates 0.1–0.6), test-purity, prolog-boundary, rulings-lint — plus the Redis/Graph/Gateway/Kernel infra checks and the Link check | Before every scripts/run_tests.py suite run |
| .claude session hooks | The 32 .sh guards in .claude/hooks/ (30 registered + 2 shared plumbing files) |
At the moment of action inside a Claude Code session (SessionStart / PreToolUse / PostToolUse / Stop / UserPromptSubmit) |
| Process-level gates | briefs-before-dispatch, evidence-on-landing, author sweep, per-id ledger, fold-verifier, RUL-007 attribution, ADR drift, plugin currency, RUL-022, F13's traceability battery, session honesty, the suite, Mohamed | At workflow moments: dispatch, landing, fold, commit, ratification, merge |
Reading a battery run
A real battery run prints one line per gate. SKIP lines are cached verdicts being
reused ("unchanged since last PASS", with the previous evidence attached); OK/FAIL
lines are fresh subprocess runs with a timing and the last lines of output:
[gates] SKIP ruff unchanged since last PASS 0.1s | All checks passed!
[gates] OK check-deps 25.0s | Dependency audit (59 declared deps): ✅ All third-party imports are declared in pyproject.toml.
[gates] FAIL prolog-gate 23.5s | ... stale (2): scripts/gates.py (content_changed) ...
[gates] 2 gate(s) in 25.1s wall (21 reused)
[gates] FAILED: prolog-gate
(The OK/FAIL details above are real recorded outputs from .cache/gates.json.)
Where the truth lives
- Registry and fingerprints:
scripts/gates.py(GATES,ALWAYS_RUN,_fingerprint,_interesting). - Cache:
.cache/gates.json(per-gatefingerprint,ok, and where captured the lastdetailline). - Battery tests:
tests/scripts/test_gates.py,tests/scripts/test_gates_coverage.py. - Single-entry-point enforcement:
.claude/hooks/single-gate-entrypoint.sh(invoking gate scripts individually is denied;# gate-debugis the escape hatch for debugging one gate's own behavior). - Git hooks:
scripts/hooks/pre-commit,commit-msg,pre-push, wired by a relativecore.hooksPath(scripts/install-git-hooks.sh). - Claude Code hooks:
plugins/orchestrata/hooks/hooks.json(generated fromclaude/),~/.claude/settings.jsonand~/.claude/settings.local.json(the three user-level hooks:dispatch_ledger.py,unshipped_guard.py,precommit_gate_subagent.sh). - Test runner stages:
scripts/run_tests.py::_main.
Known gaps (2026-10-23)
| Gap | Page |
|---|---|
| The source-provenance message prints identical expected and actual paths for an editable install, so the reader cannot see what is wrong | source-provenance |
The battery's ruff and mypy, and ship's verify, call plain uv run; the hooks' uv run --no-sync fix (8a2cb42ad) is not pushed |
workflow improvements |
The three user-level hooks are in no repo; a config deploy with rsync --delete removed them on 2026-10-23 |
commit-precheck |
| The green ledger keys at module level; RUL-040 asks for a function-level ledger | green-ledger |
The rehearsal artifact's fingerprint is never checked, and nothing runs derive_test_categories.py --check |
rehearsal-exclusion |
require-design-before-build.sh matches the subagent type by exact name, so orchestrata:builder dispatches are not checked |
dispatch-brief |
| The last ten GitLab pipelines failed within about a second; the cause is not measured | ci-pipeline |
| The infra-drift gate is red since 2026-10-20 and its timer is disabled | fleet-drift |
Battery cache blind spots: env/*.env, deploy/systemd units, .j2 and .envrc files and git state are outside the fingerprints of the gates that read them (derived from the code, not measured) |
the affected gate pages, for example plugin-drift, prod-separation |
validate-ci fingerprints docker/Dockerfile.ci but reads docker/docker-compose.yml |
validate-ci |
proportionate-test-scope.sh still reads an escalate field that the affected oracle no longer returns since 2669f4882 (derived from the code, not run) |
family-session-hooks |
The wiring gate's W1 check expects repo-level .claude/settings.json registrations, which RUL-035 forbids; needs a ruling |
wiring |