VerificationLesson
Merge gate
When agents write most of the changes, nobody can read every one closely. A gate is a fixed set of machine checks that every change must pass before it merges, so a change can be trusted without trusting whoever wrote it.
Know merge gates already? Skip to In Terrarium.
Outside the box · General
The concept
A merge gate runs the same checks on every change: the code lints, type-checks and builds, the tests pass, the content is valid. A change merges only when every check is green. Because each check asks a question a machine can answer, the gate's verdict means the same thing whoever opened the change, a person or an agent.
The same checks run in two places. The author runs them before pushing, to find a failure early, and CI runs them on the pull request, where the result decides the merge. A gate is only as good as what it checks: it can't see intent, so a clean build can still do harm when it runs.
Slow checks tempt you to skip them. A safe skip needs proof that the change can't affect the check, not a guess that it probably doesn't.
Full referenceGitHub Docs: Status checks
In Terrarium
Terrarium's is pnpm gate in package.json. Its steps split into two tiers, which scripts/gate/gate.ts names:
17export const FLOOR = ['verify:skills-lock', 'verify:mermaid', 'lint', 'typecheck', 'validate:content'] as const
18export const HEAVY = ['test', 'build', 'test:e2e'] as const
⋯// …
25export function isInert(path: string): boolean {
26 return (path.endsWith('.md') && !path.startsWith('layers/')) || path.startsWith('.claude/skills/')
27}
The is the quick checks, including the type check and content validation, and always runs. The tier is the slow checks: the unit tests, the build and the browser tests. pnpm gate:scoped diffs the branch against main and skips Heavy only when every changed path is : a Markdown file outside layers/, or a link in .claude/skills/. The comments beside isInert give the reason for each.
A change of only Inert paths and Skill files (.agents/skills/, .claude/workflows/) skips just the build and the browser tests, and anything the script can't settle, such as a missing diff base, runs every step.
CI runs the same steps from .github/actions/gate/action.yml, and its run is the one that decides. It doesn't copy the Inert rule into YAML; it asks scripts/gate/gate.ts --decide and guards the Heavy steps on the answer. If that step fails, Heavy runs anyway, so a broken shortcut costs time and never skips a check. tests/unit/gate-parity.spec.ts fails when package.json, scripts/gate/gate.ts and the action disagree on the steps.
Before a branch's first push, the author also reviews its diff and runs pnpm gate:scoped on the final commit (Pull requests).
A green Gate is not always enough to merge: some PRs wait for a person. An agent merges with scripts/github/merge-pr.ts, which polls the PR's checks, merges on green, and first refuses the PRs it can recognise, such as one that changes a file. It reads that list, the paths of .claude/rules/adr-0004-human-merge.md, from main (Protected paths).
Why it's built this way
- Checks, not judgement. Agents open too many PRs for a person to read, so machine checks decide the merge. Checks can't see intent, and a clean build can still do harm, so a fixed list of exceptions waits for a person: a list is checkable, where rating risk rests on judgement. The cost: some exceptions, such as untested behaviour, rest on the agent noticing them. ADR-0004 (docs/adr/0004-objective-safety-gate.md).
- Skip only on proof. Slow checks make docs-only edits wait, but skipping on a guess would weaken green. So the Gate skips them only for paths, proven unread by any check; any doubt runs everything. The cost: over-caution, since a comment-only script edit forced full reruns, so Skill files got the narrower skip above.
- One skip rule. Two copies of the skip rule would drift apart unnoticed. So CI asks the script for its answer instead of restating it in YAML, and treats a failed answer as "don't skip": a broken shortcut costs time, never an unrun check. .github/actions/gate/action.yml.
- Steps in an action file. Agents can't edit GitHub's workflow files, so a new check reached the local gate while its CI step waited for a person, and CI checked less. The steps now live in an action file agents can edit, and a test fails when the copies disagree. The cost: that test sees which steps run, not what each does. ADR-0026 (docs/adr/0026-gate-workflow-thin-shell.md).
What to watch for
- Green means the checks passed, not that the change is right. A PR can pass it and still wait for a person; ADR-0004 lists when.
- Inert is about the path, not the size of the change. A rewrite of a whole doc outside
layers/is Inert; a one-character change to a page underlayers/is not, because the build renders it. - One run per checkout. A second
pnpm gate:scopedin the same checkout exits at once while the first is alive, since two would race on the build output. Each linked worktree has its own lock, and a run that was killed leaves a dead pid the next run takes over. - A pass covers the tree it started on. If
HEADor the working tree changes whilepnpm gate:scopedruns, it aborts before its next step, or warns after the last one; re-run it on the final tree. - Changing the Gate needs a person. scripts/gate/gate.ts, the action and the workflow file are on the list. A change to what the Gate checks waits for a person to merge it.
Try it in your project
Write down the one command that decides whether a change may merge in your project, make CI run exactly that command, and run it yourself before every push. If you add a skip, make it fail closed: run every check unless you can prove a change can't reach it.
Where next
- LessonProtected paths
- LessonLinting
- LessonType checking
- LessonTesting
- LessonContent validation
Walkthroughs through this Lesson
- WalkthroughA change goes live
- WalkthroughAdding a site
- WalkthroughAsking an agent for a change
Last updated · Latest source on GitHub ↗