Mechanical enforcement of static safety policy — the code behind docs/safety.md's Path Denylist and Auto-Merge Policy, and LOOP.md's "No auto-merge on main except trivial dependency patches" / "Denylist... without human review" rules. Nothing evaluated a proposed change against that prose before; loop-gate does.
Deliberately has no knowledge of run history. Stagnation, repeated failures, and token/daily budgets already belong to loop-context's circuit breaker — loop-gate only looks at what is being proposed (which paths, what action type), not how the run has behaved so far. Chain the two:
loop-context --check --ledger run.json ... || exit 2 # run-history based
loop-gate check --action auto-merge --paths a.ts,b.ts || exit 2 # policy based
do-the-mergenpx @cobusgreyling/loop-gate check --action auto-merge --paths docs/guide.mdFrom this repo:
cd tools/loop-gate
npm install
npm testloop-gate check --action <commit|merge|auto-merge> --paths <f1,f2,...> [--gate-file gate.yaml] [--json]| Flag | Default | Meaning |
|---|---|---|
--action <commit|merge|auto-merge> |
required | What the loop is about to do |
--paths <f1,f2,...> |
required | Comma-separated changed file paths |
--gate-file <path> |
gate.yaml (cwd) |
Policy file to evaluate against |
--json |
off | Machine-readable decision output |
Exit codes: 0 allowed · 2 escalate · 1 error (bad flags, missing/invalid config).
gate.yaml is the machine-readable twin of docs/safety.md — see templates/gate.yaml.template to scaffold your own, or this repo's own dogfood gate.yaml:
version: 1
denylist:
- ".env"
- "**/secrets/**"
- "auth/**"
maxFiles: 10
autoMergeAllowlist:
- "docs/**"
- "**/*.md"Checked in order (first match wins, same "most specific trigger first" convention loop-context's circuit breaker uses):
denylist— any changed path matching a glob here escalates, regardless of--action.maxFiles— more changed paths than this escalates (matchesdocs/safety.md's "Changes touching >N files" human gate).autoMergeAllowlist— only checked when--action auto-merge; every changed path must match one of these globs, or it escalates.
Glob matching is via minimatch (the same semantics .gitignore-style globs use — ** matches across path segments).
- Does not read a run ledger, and never will — that keeps it decoupled from
loop-contextat the source level, the same wayloop-worktreeandloop-contextstay independent while still being paired by convention in a control script. - Does not produce its own escalation summary format — fold its JSON decision into whatever your control script already assembles from
loop-context --injectwhen escalating to a human. - Does not enforce anything by itself — like
loop-worktree's locks, this is advisory: a control script that skips callingloop-gateis not physically blocked. The mechanism is only as good as the scripts that call it.
See docs/safety.md for the policy this codifies, and docs/primitives.md for where safety gates fit in the Five Building Blocks + Memory model.