Delivery & CI Governance
Chapters 13 and 14 are about the quality of the agent. This chapter is about the quality
of the changes you make to the harness itself. The repo ships with a set of CI/CD
gates — the delivery rails — built to the Intent Driven Development delivery
standard. One rule underpins all of them: the agent proposes, a gate disposes.
An AI (or a human) can author any change it likes; nothing reaches main until a
mechanical or human gate has had its say.
This page is the developer-facing tour. The authoritative operator's runbook — exact
go-live steps, residual-risk analysis, and the shakedown procedure — is
.github/RAILS.md, which implements the Intent Driven Development delivery
standard (its rails and section numbers trace back to a the-rails.md
specification). When this page and RAILS.md disagree,
RAILS.md wins.
The six gates
Six gates stand between a change and main. Five of them run in GitHub Actions when
you open a pull request. The sixth runs on your own machine, and stops an agent from even
finishing its turn on a broken build.
There are three distinct levels of "seriousness" here, and it is worth separating them before you read the table, because the words look similar and mean very different things:
-
Required — GitHub itself refuses the merge button.
build-and-test, the OWASP gate, andsecurity-revieware in this set today. -
Blocking, but not yet required — the check genuinely fails, and it fails
closed if it cannot finish, but branch protection is not yet configured to insist on it. So
in practice it can be merged past.
correctness-reviewsits here deliberately, until the agentic gates are taken live (see below). -
Advisory — it leaves a comment and never fails. The
graderis the only one of these.
| Gate | Where | Fires on | Blocks or advises |
|---|---|---|---|
build-and-test | .github/workflows/ci.yml | Every PR + push to main | Blocks — build must compile, full xUnit suite must pass. |
OWASP Agentic Top-10 Gate | .github/workflows/ci.yml | Every PR (after build) | Blocks — any Verdict.Fail in the 10-row eval pack fails the merge. |
security-review | .github/workflows/security-review.yml | Every PR; reviews only when a gated path or risk:high label is present | Blocks on HIGH — the security-reviewer agent writes a PASS/BLOCK verdict; HIGH fails closed. |
correctness-review | .github/workflows/correctness-review.yml | Every non-draft PR; reviews only when compilable source under src/ changed | Blocks (capable), not yet required — a fresh agent hunts the diff for logic defects and writes a CORRECTNESS_VERDICT: PASS/BLOCK; a BLOCK fails the check (overridable by the audited accepted-risk:correctness label — but only for a real finding; the label deliberately cannot override a review that never finished). Fails closed if it can't complete. Held out of the required-check set until go-live. |
grader | .github/workflows/grader.yml | Every non-draft PR | Advises — a fresh agent grades the diff against the PR's stated intent and comments. Never blocks. |
| Stop gate | .claude/hooks/stop-build-gate.ps1 | An agent tries to end a Claude Code turn locally | Blocks a red build before it ever becomes a commit. |
Blocking vs advising — and why the split matters
A blocking gate is a mechanical or agentic check whose verdict the branch protection ruleset trusts to gate merge. An advising gate produces a signal for a human reviewer (the "Checker") to weigh, but cannot itself stop a merge. The grader is deliberately advisory: "the grader ran" is what the methodology requires; what it said is the human's call. Making a judgment-heavy LLM verdict a hard gate would either rubber-stamp or block-at-random — neither is honest.
Branch protection (.github/rulesets/main-branch-protection.json) makes exactly the
three blocking CI checks mandatory and additionally requires a non-author approval plus a
code-owner review. The required check contexts are:
build-and-test
OWASP Agentic Top-10 Gate
security-review
The grader is intentionally not in that list.
How a change flows
┌─────────────────────────────────────────────────────────────────┐
│ LOCAL (your machine) │
│ edit code ──▶ agent tries to finish its turn │
│ └▶ Stop gate: build must be green, else REFUSE │
│ commit ──▶ push branch ──▶ open PR │
├─────────────────────────────────────────────────────────────────┤
│ CI (GitHub Actions, on the PR) │
│ build-and-test ──────────────▶ blocks on red build/test │
│ OWASP Agentic Top-10 Gate ───▶ blocks on any eval Fail │
│ security-review ─────────────▶ blocks on HIGH (gated paths) │
│ correctness-review ──────────▶ blocks on logic defect (src/) │
│ grader ──────────────────────▶ comments a verdict (advisory) │
├─────────────────────────────────────────────────────────────────┤
│ MERGE │
│ branch protection: 3 required checks green │
│ + non-author approval + code-owner review │
└─────────────────────────────────────────────────────────────────┘
The three agentic gates
The grader, security-review, and correctness-review gates each run a Claude agent inside CI via
anthropics/claude-code-action@v1. Each reads a committed rubric and follows it
exactly — the rubric is the contract, version-controlled alongside the code it judges:
| Gate | Rubric | What it checks |
|---|---|---|
| grader | .github/grader-rubric.md | Does the diff do what the PR description claims? Scope creep, standards (file length, XML docs, Result<T>, keyed-DI tools, exception handling). |
| security-review | .github/security-review-rubric.md | Injection, broken access control, secret leakage, unsafe crypto on the gated files (Auth / Identity / Security / Migrations / infra/ / .github/). |
| correctness-review | .github/correctness-review-rubric.md | Plain logic and correctness defects the other rails miss — scoped to the changed-line anchor set under src/. Blocks only on a high-confidence defect. |
The security-review gate deserves a closer look, because it behaves in a way that
looks contradictory until you see the reasoning. It is a required check, so it runs on
every single PR — but it only actually reviews anything when a security-sensitive path
changed, or when someone applied the risk:high label.
That gives it three possible outcomes:
- Nothing sensitive changed → it reports success immediately without reviewing. It has to report something, or a required check would hang forever on routine PRs.
- Something sensitive changed, review runs → you get a real verdict.
- Something sensitive changed, but the review can't run — say the API key isn't configured yet → it fails closed. It blocks. An unreviewable security change is never waved through on the grounds that the reviewer was unavailable.
The local gate: the Stop hook
The cheapest place to catch a broken build is before it leaves your machine. The Stop hook
(.claude/hooks/stop-build-gate.ps1) runs when an agent attempts to end a Claude
Code turn: if the solution does not build, the hook refuses and hands the build error back to
the agent. The agent fixes it and tries again. By the time a commit exists, it already builds —
so CI's build-and-test is a backstop, not the first line of defence.
Taking the rails live
The rails are wired but dormant by default — the two agentic gates need credentials a human must provision. These are deliberate, outward-facing actions; nothing in the repo performs them for you. (Until they're done, the security-review gate fails closed on gated PRs by design, and the grader stays green as a no-op.)
-
Install the Claude GitHub App on the repo — run
/install-github-appin Claude Code, or install from github.com/apps/claude. Repo admin required. -
Add the
ANTHROPIC_API_KEYrepository secret (Settings → Secrets and variables → Actions). The grader and security-review steps call the Claude API; there is a real per-PR token cost. -
Apply branch protection from the version-controlled ruleset — never by
hand-editing rules in the GitHub UI:
Edit the JSON, re-run the script — that is the only sanctioned path to changing protection.scripts/rails/apply-branch-protection.sh --dry-run # review the plan scripts/rails/apply-branch-protection.sh # apply (prompts to confirm)
GitHub forbids approving your own PR, so on a single-maintainer repo the "non-author
approval" rule can't be self-satisfied. The ruleset ships armed with a
repo-admin owner bypass set to bypass_mode: pull_request — you can
self-merge today, but even the owner cannot push directly to main
skipping CI. The moment a second collaborator (or a review bot) joins, remove
the bypass actor from main-branch-protection.json and re-apply.
Gate integrity (known residual risk)
These workflows trigger on pull_request, which runs the PR's own copy of
the workflow, the rubric, and the gated-path regex. A same-repo branch could in principle try to
weaken its own gate. Two mitigations are in place, plus one real closure:
- The security verdict file is written and read outside the working tree, and any copy a PR committed into the tree is deleted before review — so a planted
PASScan't satisfy the gate. - Changes to
.github/**are themselves a gated path, so weakening a rail requires code-owner review. - The real closure is a non-author review of rails changes — exactly what branch protection enforces once a second reviewer exists.
pull_request_target for these gates
It would expose repository secrets and the write token to forked-PR code. The gates run
on pull_request on purpose; keep them there.
Prove the rails — the shakedown
A pipeline that has never caught anything is not proven, only present. Before trusting the rails, force each one to fail and confirm it's caught:
| Gate | How to break it on purpose | Expected |
|---|---|---|
| Stop gate | Break a .cs file under src/, then try to end a turn. | Hook refuses, hands back the build error. |
| grader | Open a PR whose description claims something the diff does not do. | Grader comment calls out the mismatch. |
| security-review | Throwaway PR touching a gated path (e.g. a comment under **/Auth/) with a planted HIGH issue. | Check goes red. Close it unmerged. |
| correctness-review | Throwaway PR that changes a .cs file under src/ with an obvious logic defect (e.g. an inverted condition). | Check goes red with a CORRECTNESS_VERDICT: BLOCK comment. Close it unmerged. |
| CI / OWASP | Already exercised by every real PR. | — |
The docs have a rail too
Documentation drifts faster than anything else in a template repo, so it gets its own rail. Here is what it does, step by step:
-
.github/workflows/docs-drift-check.ymlfires on every push tomain, filtered to code, workflow, and governance changes. -
It hands the merge diff to a Claude agent, along with the documentation map in
.github/docs-drift-rubric.md. - The agent asks one question: did this change ship something the docs still describe the old way?
- If so, it opens a pull request containing the doc updates — for a human to review and merge.
This is an advising rail. It proposes a doc PR; a human still disposes. And
like the other agentic gates, it stays dormant until the Claude GitHub App and
ANTHROPIC_API_KEY are configured.
What's deferred (and where to read about it)
The rails govern getting a change onto main. They deliberately do
not cover deploy/promotion, rollback rehearsal, IaC apply with what-if/policy
funnels, or production secret rotation — there is no cloud deployment yet. That target topology
(Container Apps, Bicep, canary promotion, cost tiers) is documented as the forward plan in the
Architecture Guide → Operations & Cost
page; it describes what a consumer would build on Azure, not what this repo ships today.
Where to go next
- Read
.github/RAILS.md— the operator's runbook this page summarizes. - Cross-reference Chapter 13 — Evaluation Framework: the OWASP Agentic gate runs an eval pack through that same harness.
- See Chapter 10 — Observability & Safety for the runtime governance (content safety, autonomy tiers) the rails complement at delivery time.
- The Architecture Guide's Operations & Cost page covers the deferred cloud delivery topology.