Skip to content

Runbook — reviewing an atom proposal

Audience: a regime maintainer, and whoever administers the repository. Trigger: an atomize/* branch appears on the regime repository. Time: minutes, plus however long the reading of the law takes.

An obligation atom is a machine’s reading of a legal text. Merging one makes it part of what OpenRegs answers with, so a person with standing has to say yes first. This runbook is that step, and the rules below are not advice: the remote refuses a push that breaks any of them.


What is enforced by a machine, and what is not

Section titled “What is enforced by a machine, and what is not”

Read this table before trusting anything else in this document.

RuleEnforced by
main advances only by merging a branchthe pre-receive hook, mechanically
at least one approving review, from a code owner of every changed paththe hook, mechanically
an approval covers exactly one commit; new work dismisses itthe hook, mechanically
nobody approves a branch they wrote; a bot never approvesthe hook, mechanically
every required check green at the merged commit, published by the ci identitythe hook, mechanically
the merge commit records the reviewers it was made onthe hook, mechanically
force-pushes and deletions of mainthe hook, mechanically
the same rules on a hosted GitHub repositorya human, by hand — see below
who a pusher actually isthe transport, not the hook — see below

Two honest limits:

  1. GitHub is configured by a person. Branch protection on a hosted repository is server-side configuration, and no test in this project may talk to GitHub. openregs review github-settings renders .github/branch-protection.yaml into the exact API payload, and .github/branch-protection.github.json is that payload committed. Until somebody applies it, the hosted branch is protected by nothing. Three rules have no GitHub setting at all; the command names them every time it runs.
  2. The hook authorizes; it does not authenticate. The pushing identity arrives in OPENREGS_PUSHER. A real server sets it from the authenticated connection (an ssh forced command, a forge). Over a local file:// remote there is no authentication, so the client asserts its own name — which is why the fixture harness demonstrates that the rules bind, not that the names are true.

0. Ownership: one source, one generated file

Section titled “0. Ownership: one source, one generated file”

MAINTAINERS.yaml names the teams, the people in them, and which team answers for which regime. .github/CODEOWNERS is generated from it:

Terminal window
make gen-codeowners

Never edit .github/CODEOWNERS. The test suite runs the same generator with --check, which fails when the committed file and the roster have drifted apart:

Terminal window
python -m openregs.governance --check

(That command is a required status check by name — check-canon and test are in the policy’s contexts — but the repository’s own GitHub workflow is defined by the CI/CD task, not this one. Today make test is what runs it.)

For the fixture regime the routing the task cares about reads:

/regimes/fixreg/atoms/ @openregs/fixreg-maintainers
/regimes/fixreg/canon/ @openregs/fixreg-maintainers

Ownership resolves by last matching rule, so the file is written general-to-specific and the generator, not a person, decides the order.


Once, per remote. On a local bare repository — the only kind these commands are tested against:

Terminal window
REMOTE=$PWD/.test-remotes/atom-review.git
openregs review protect --repo $REMOTE

That installs hooks/pre-receive and stores the policy at openregs/branch-protection.yaml inside the repository directory — beside the object store, outside every branch, exactly as a forge keeps branch protection outside the branch it protects. A pull request therefore cannot weaken the rules being applied to it. The roster and CODEOWNERS are read from the current tip of the protected branch, so widening ownership needs a merge under the old ownership.

On a hosted repository, instead:

Terminal window
openregs review github-settings --out .github/branch-protection.github.json
gh api -X PUT repos/<owner>/<repo>/branches/main/protection \
--input .github/branch-protection.github.json

The second command is run by a human with admin rights. Nothing in CI runs it and nothing in CI can check that it was run.


openregs atomize stages its proposals on a branch and pushes it. Nothing else happens by itself:

Terminal window
git push $REMOTE atomize/<unit>/<date>

Look at what it would change, and who owns that:

Terminal window
openregs review status --repo $REMOTE --branch atomize/<unit>/<date>
branch atomize/eu-act-reg-2024-1-art_5__para_2/2025-06-01 @ 4f2a9c1b6d07
base 1a0b2c3d4e5f contains the tip
approvals: none
checks green: none
checks missing: lint, typecheck, test, check-canon
changed paths and their owners:
regimes/fixreg/atoms/FIXREG-Art5.2-Ob1.yaml @openregs/fixreg-maintainers

A result belongs to one commit and is published by the one identity the policy names. Anybody else’s result is refused at the push:

Terminal window
openregs review check --repo $REMOTE --sha <sha> \
--context check-canon --result success --as openregs-ci

--result failure records the failure as a ref of its own. A red result is a record, not a draft: it is not overwritten by a later pass, and while it stands the merge is refused. The way past it is new work on a new commit.


4. Approve — what you are actually saying

Section titled “4. Approve — what you are actually saying”

Read the proposed atom against the text it cites. Then:

Terminal window
openregs review approve --repo $REMOTE --sha <sha> --as <your-handle>

The approval is a ref naming you and that commit. Consequences worth knowing:

  • Push more work and the approval is gone. It named a commit, not a branch.
  • You cannot approve your own branch, and a machine identity cannot approve at all.
  • Approving does not merge. Someone still has to, and the merge is checked again.
  • Changed your mind: openregs review approve --sha <sha> --as <you> --withdraw.

Being a maintainer is not enough — you must own the paths the branch changes. An approval from a team that owns spec/ does not admit an atom into fixreg.


Terminal window
openregs review merge --repo $REMOTE --branch atomize/<unit>/<date> --as <your-handle>

The merge commit is built with the reviewers read from the approval refs on the remote:

Merge branch 'atomize/eu-act-reg-2024-1-art_5__para_2/2025-06-01'
Reviewed-by: Lieve Verheyen <l.verheyen@openregs.invalid>
Checks-passed: check-canon, lint, test, typecheck
Merged-by: Caesar Mukama <mcaesar@openregs.io>

The remote re-derives all of it. A merge commit whose trailers name somebody who did not approve that commit is refused, so the history’s record of who reviewed what is a checked fact rather than a convention. The approval and check refs stay in the repository afterwards — git for-each-ref refs/reviews/ is the audit.


Every refusal names the rule. The common ones:

MessageWhat to do
is not a merge commitdo not push to main; merge a branch
0 approving review(s)get a code owner to approve the current commit
no approval from a code ownerthe approver does not own that path; see CODEOWNERS
has not reported greenwait for CI, or publish the result as the ci identity
reported FAILEDfix it and push a new commit; a red result stands
Reviewed-by trailers do not matchmerge with openregs review merge
non-fast-forward update refusednever rewrite main; land a new commit
has no identitythe transport did not set OPENREGS_PUSHER

A machine can propose an obligation atom in a second and a person cannot check one in a second. The gap is the point: the corpus is used to answer questions about what the law requires, and an unreviewed answer with a citation attached is worse than no answer at all. Everything above exists so that the record shows which person accepted which reading of which text, and so that no amount of automation can quietly become that person.

Rendered from openregs/openregs@cbe9615:docs/runbooks/atom-review.md