Every team has a moment when something becomes done. The art director looks at the key art and says ship it. QA signs the loot table. A second engineer blesses the migration. That moment is the most important state transition in the whole production pipeline, and here is the strange part: in most setups it isn't recorded in the version control system at all.
Ask where "approved" actually lives in a typical Git shop. It lives in the forge. GitHub or GitLab keeps the review record in its own database, attached to a pull request, and enforces required reviews as branch protection. Protection works as far as it goes: the forge is the receiving server, and it will reject a direct push to a protected branch. But look at what it is made of: configuration of the hosting layer, applied per branch, off by default, with bypass toggles an administrator can flip, and gone entirely the moment the repository exists anywhere the forge is not, whether that is a mirror, a second remote, or a self-hosted bare repo. The approval record itself lives outside the object database. Delete the pull request and the approval history goes with it; the commits stay.
The second gap has nothing to do with enforcement: half of what a game studio needs approved isn't code. Forges render image diffs well enough these days; what they lack is the role. There is no first-class way to say a PSD requires an art director's sign-off, a spawn table requires a designer's, and a pricing config requires a validation bot's, with the same weight a required review carries on code.
I'm building Grixel, a server-authoritative version control and data platform for game development and AI/ML work, and I think approval belongs inside the depot. In Grixel the gate is a committed file evaluated by the same server that owns the bytes, and a sign-off is a content-addressed record stored in the same depot as the content it blesses. This piece is about what that changes: sign-offs that name the exact bytes they approved, staged gate policies, promotion the server refuses until the gate is satisfied, and approvals that go stale on their own the moment the content moves underneath them.
· · ·
#A signature on a specific page
The plain-language version first.
When you sign a printed contract, your signature is on a specific piece of paper. If someone swaps a page afterwards, your signature does not migrate to the new page; the tampering is detectable because signature and content are physically bound. Now imagine instead signing a sticky note that says "I approve whatever the document currently says," and sticking it to the folder. That is an approval in a system where review record and content are only loosely associated.
Grixel's sign-off is the signature on the page. Every approval record is bound to a content hash, the exact bytes that were reviewed. The system answers "is this approved?" by checking for a valid sign-off at the current content hash. Edit the file and its hash changes, so every prior sign-off simply stops satisfying the policy. Nobody has to remember to dismiss stale reviews, and no configuration flag controls whether staleness matters. It is not a policy; it is arithmetic.
A second structural rule closes a hole most policy systems leave
open: a change is judged by the policy at its base, not by the policy it
introduces. You cannot weaken a gate in the same change that benefits
from the weakening. If a gate.json requires sign-off for
edits to gates, then relaxing that gate is itself gated by the
still-current rules. You cannot lower the drawbridge from outside the
castle.
· · ·
#Where the gate should bite
The obvious way to enforce review is to block unapproved submits. I decided against that, and the reasoning matters, because the whole workflow rests on it.
If the gate bites at submit, every push becomes a negotiation. Work in progress needs somewhere to live, and an agent drafting content produces intermediate states nobody should have to approve. Blocking the write path punishes exactly the messy, frequent operations you want to be cheap.
So in Grixel, the gate bites at promote. You submit freely
to a working ref. The content lands, reviewers look at it there,
sign-offs accumulate against its content hashes. Then
grixel review promote <src> <dst> stands
between that working ref and the protected ref: it verifies that every
gate-enforced target under the source is approved at its current head,
and only then moves the destination ref, atomically. If anything is
unapproved, nothing moves.
I should concede the placement immediately: structurally, this is
where a forge's merge gate sits too, between a working branch and a
protected one. Gate-at-promote is gate-at-merge in different clothes.
What differs is what the gate is made of, on two counts. First, the
policy and the sign-offs live in the depot: the gate is a versioned file
whose own edits are governed by the rules already in force, and an
approval is a content-addressed record in the history it protects, where
a forge keeps a row in its own database. Second, the evaluation happens
in grixeld, the server daemon that owns the data, at the
same chokepoint every write already passes; the CLI is a thin client
with no enforcement logic of its own. There is no separate hosting layer
whose configuration has to be present, per branch, and unbypassed for
the rule to hold.
The trade-off is real: unapproved content can reach a working ref. If your threat model is "nothing unreviewed may ever be stored," this is not that. No team actually wants that model once they live with it; what they want is that nothing unreviewed reaches the place that matters. The calmer workflow is to stage freely and review in place, then promote through a gate. Combined with ordinary path ACLs this makes a protected release ref: give contributors write on the staging ref but none on the live ref, and the only road into live is the gated promote.
Two more design choices round out the model. First, automated checks are first-class parties in a gate: a stage's approver can be a human group, a specific user, or a check, and there is a transcript of that below. Second, review assignment is an advisory overlay, deliberately. You can assign a stage to a person, or round-robin it across the least-loaded eligible reviewer, and it ranks the item to the top of their queue. But assignment never narrows who may sign. Eligibility stays with the group named in the policy, so a reviewer being on holiday cannot deadlock a release. Ownership is a suggestion; the gate is the rule.
Before the transcripts, the objection every engineering lead will raise: protected branches plus CODEOWNERS plus required status checks plus Git LFS gets you most of this, so why not that stack? On its own terms it is a good one. But walk through what it covers: code-review-shaped changes, on the branches an administrator remembered to protect, with the record held in the forge's database rather than in the history it governs. It has no role an art director can hold on a PSD, and no promote-time verification of an entire tree. Approvals go stale at pull-request granularity if the right checkbox is set, never per file against content. And none of it has any purchase on an AI agent that writes through a CLI instead of opening a pull request. The eighty percent is real; the missing twenty is where a multi-disciplinary team actually decides things are done.
· · ·
#Watching the gate work
Enough theory. Here is the machinery running, verbatim, against a
scratch depot. A note on the commands: they were run in local mode,
where the -as flag simply says who is acting. In a team
deployment you run grixel login once and drop the flag;
identity then comes from the server-verified token.
The cast: alice is an artist on a seasonal event project, maya is the art director. A gate policy is a JSON file in the depot like any other content, governing the subtree beneath it:
$ cat gate.json
{
"version": 1,
"rules": [
{
"appliesTo": "art/**",
"target": "each-file",
"enforce": "gate",
"stages": [
{ "role": "art-director", "by": "group:art-directors", "order": 1 }
]
}
]
}
Everything under art/ now needs an art director's
sign-off before it can be promoted. Alice has already submitted key art
to the working ref, which the gate did not block; asking about its
review state shows where it stands:
$ grixel review status -as alice /companies/acme/games/seasonal/art/keyart_main.psd
/companies/acme/games/seasonal/art/keyart_main.psd [content a724bb3c]
state: none
[ ] art-director group:art-directors open — needs 1 of group:art-directors
Note the [content a724bb3c]. The review state belongs to
a specific content hash, not to a file name. Now maya tries to promote
the working ref to the live ref. She is the art director; she owns the
live ref; she has every permission that matters. The gate does not
care:
$ grixel review promote -as maya /companies/acme/games/seasonal /companies/acme/games/seasonal-live
BLOCKED /companies/acme/games/seasonal → /companies/acme/games/seasonal-live — 1 gate target(s) not approved:
none /companies/acme/games/seasonal/art/keyart_main.psd
(The none on the left is the file's review state: no
sign-offs exist at its current content.) This is the point I most want a
founder reading this to absorb. The person running the promote is the
person the gate is waiting on, and the server still refuses; there is no
administrator toggle that waves it through. Maya checks what is
waiting:
$ grixel review outstanding -as maya
outstanding under / (1):
none /companies/acme/games/seasonal/art/keyart_main.psd (waiting: art-director)
Her actual review step is unglamorous: she syncs the working ref, opens the PSD from her checkout like any file on disk, looks at it, and returns to the terminal to sign. No review surface presents the image to her; more on that later. The sign-off carries a reason into the append-only record:
$ grixel approve -as maya -m "key art matches palette; ship it" /companies/acme/games/seasonal/art/keyart_main.psd
approve recorded on /companies/acme/games/seasonal/art/keyart_main.psd
/companies/acme/games/seasonal/art/keyart_main.psd [content a724bb3c]
state: approved
[✓] art-director group:art-directors signed by maya
And now the same promote succeeds:
$ grixel review promote -as maya /companies/acme/games/seasonal /companies/acme/games/seasonal-live
OK promoted /companies/acme/games/seasonal → /companies/acme/games/seasonal-live @ cf131439 (1 gate target(s) verified)
An approval, a reason, a verified gate, an atomic ref move. Four lines of terminal, and the "done" moment is now a fact the depot itself holds.
· · ·
#The approval that dies when it should
GitHub has a checkbox in this neighbourhood: "Dismiss stale pull request approvals when new commits are pushed." Turn it on and a protected branch drops a PR's approvals when the branch gains commits. But that is opt-in configuration at pull-request granularity: new commits dismiss the approval whether or not they touched what the reviewer looked at, and only within a PR's lifetime. Grixel's staleness is configured nowhere. A sign-off names exact bytes, so it expires per file, by the arithmetic from earlier, on any ref, with no pull-request-shaped object in sight.
Alice keeps working; the lanterns need to be brighter:
$ grixel submit -as alice -m "key art: brighten lanterns"
grixel: holding exclusive lock on /companies/acme/games/seasonal/art/keyart_main.psd (lockable type)
Committed: b645d563023b35ab638907a3831b717da50194cebff308bbb3e82fbd6d18db55
ref : companies/acme/games/seasonal
changes : 1
stored : 5.1 MiB new, 58.9 MiB deduped
parent : cf1314393f0b9dc05787715a93e37d07629f9937642ada4b1ff81aa2955fe12f
base : b645d563023b35ab638907a3831b717da50194cebff308bbb3e82fbd6d18db55 (advanced)
(The base : line is workspace bookkeeping: her checkout
advanced to the commit it just created.) Nobody dismissed a review.
Nobody ran a cleanup job. But ask about the file again:
$ grixel review status -as alice /companies/acme/games/seasonal/art/keyart_main.psd
/companies/acme/games/seasonal/art/keyart_main.psd [content 590ee1ea]
state: none
[ ] art-director group:art-directors open — needs 1 of group:art-directors
Compare the brackets. Maya signed content a724bb3c; the
file is now content 590ee1ea. Her sign-off still exists in
the audit record, permanently, but it no longer satisfies the policy,
because it names bytes that are no longer the bytes. The state fell back
to none the instant the content changed, and the stage
reopened. That is the signature-on-the-page property doing its job with
no human in the loop.
Meanwhile the live ref still points at the content maya actually approved. A server-side ref-to-ref diff shows the drift between what is staged and what is live:
$ grixel diff -as maya /companies/acme/games/seasonal /companies/acme/games/seasonal-live
diff companies/acme/games/seasonal@b645d563 companies/acme/games/seasonal-live@cf131439
0 added, 1 modified, 0 deleted
M art/keyart_main.psd
Staging has moved; live has not. Getting the brighter lanterns to players requires another sign-off and another gated promote.
· · ·
#When the reviewer is a bot and the author is an agent
Increasingly the author of a change is an AI agent, and at least one of the reviewers should be a machine. Grixel treats both as ordinary parties. Here is a second gate, this time over the project's config folder, with two ordered stages: a server-evaluated check first, humans second:
$ cat gate.json
{
"version": 1,
"rules": [
{
"appliesTo": "**/*.json",
"target": "each-file",
"enforce": "gate",
"stages": [
{ "role": "json-lint", "by": "check:json-valid", "order": 1 },
{ "role": "design-review", "by": "group:art-directors", "order": 2 }
]
}
]
}
check:json-valid is one of a small set of checks the
daemon evaluates itself, as a pure function of the content, at every
evaluation. (The others today: YAML validity, non-emptiness, and
leftover merge-conflict markers.) Nothing is stored, nothing can go
stale, and you cannot manually sign such a stage; the answer is computed
from the bytes. Submit a valid config and the lint stage is already
satisfied by the time you look:
$ grixel review status -as alice /companies/acme/games/seasonal/config/settings.json
/companies/acme/games/seasonal/config/settings.json [content c4e36cd4]
state: in-review
[✓] json-lint check:json-valid signed by check:json-valid
[ ] design-review group:art-directors open — needs 1 of group:art-directors
The machine has signed; the humans are up. Submit a config that does not parse and it never reaches them:
$ grixel review status -as alice /companies/acme/games/seasonal/config/broken.json
/companies/acme/games/seasonal/config/broken.json [content 0a840f4d]
state: blocked
[x] json-lint check:json-valid changes requested
[·] design-review group:art-directors waiting on earlier stage
The [·] marks a stage that has not opened: design review
is never asked to look at a file the lint check already rejected. For
the valid config, maya clears the second stage the same way she cleared
the art:
$ grixel approve -as maya -m "festival settings read sane" /companies/acme/games/seasonal/config/settings.json
approve recorded on /companies/acme/games/seasonal/config/settings.json
/companies/acme/games/seasonal/config/settings.json [content c4e36cd4]
state: approved
[✓] json-lint check:json-valid signed by check:json-valid
[✓] design-review group:art-directors signed by maya
For everything non-deterministic, the model is a service account. A
VLM that reviews key art against brand guidelines, or a CI pipeline that
runs the full test suite: each runs under its own credential, reads
policy and content through the read-only MCP surface, and records its
verdict through the same grixel approve a human uses, into
the same audit trail. The content binding does quiet work here too: a
bot's approval names the hash it evaluated, so a stale check run cannot
bless bytes it never saw.
All of this lands in one queue. Here is maya's outstanding list on the same depot, a little later in its life:
$ grixel review outstanding -as maya
outstanding under / (6):
none /companies/acme/games/seasonal-live/art/keyart_main.psd (waiting: art-director)
none /companies/acme/games/seasonal/art/boss_arena.uasset (waiting: art-director)
none /companies/acme/games/seasonal/art/keyart_main.psd (waiting: art-director)
none /companies/acme/games/seasonal/art/keyart_storefront.psd (waiting: art-director)
blocked /companies/acme/games/seasonal/config/broken.json
in-review /companies/acme/games/seasonal/config/gate.json (waiting: design-review)
There is texture in that listing: art files waiting on an art
director across more than one ref, and the broken config blocked where
the lint check left it. The last line is my favourite:
config/gate.json itself is in review, because its own rule
matches **/*.json. The gate that governs the configs is
governed by itself; changing the review policy queues for design review
like any other content. That is the drawbridge rule showing up in a
worklist.
One more property in a sentence: ask
grixel review status about a path that is hidden from you,
absent, or simply ungoverned, and the answer is the same "not found" in
all three cases; the review surface is not allowed to become an oracle
for paths you cannot see.
· · ·
#The verdict and the work in one system
Sign-off answers "who blessed this content." The neighbouring question is "what work was this change even for," and in most shops that answer lives in yet another system, a tracker drifting away from the code it describes. Grixel keeps jobs in the depot too, anchored to paths so the same visibility rules apply, and a submit can cite the job it resolves:
$ grixel submit -as alice --job job000001 -m "fix seams on hero texture"
grixel: holding exclusive lock on /companies/acme/games/seasonal/art/keyart_main.psd (lockable type)
Committed: 4bb92aee3ed1c442e92da6ecc482c0561231ca0631fdb91a4dfce77f90caf507
ref : companies/acme/games/seasonal
changes : 1
stored : 5.1 MiB new, 58.9 MiB deduped
parent : ad439db18fa79626577df2f8f35dd4225e7a131a325034bb135e1bc1731582bf
base : 4bb92aee3ed1c442e92da6ecc482c0561231ca0631fdb91a4dfce77f90caf507 (advanced)
jobs : 1 cited
(If the dedup numbers look identical to the lantern submit earlier, that is the demo rig showing through: its scripted edits flip similar-sized regions of the same PSD. Real edits vary more.) The link is bidirectional; the job knows its fixing commit:
$ grixel job show -as maya job000001
job000001 fixed (bug)
anchor : /companies/acme/games/seasonal
title : Hero texture seams on consoles
assignee : alice
platform : ps5
severity : major
created : 2026-07-05T23:32:40Z by maya
commits :
4bb92aee3ed1
parents :
job000002 open Spring festival content drop
Put the pieces side by side and the thesis appears. The bug, the commit that fixes it, the sign-off that approves the fix, and the promote that ships it are all records in the same system, covered by the same permission model and audit trail. That is what I mean when I call Grixel a database of record. Review is one brick of that wall, and it is a brick I am not willing to concede to a forge sitting on top; the approval record belongs inside the system that holds the truth.
· · ·
#The stages still open
There are no threaded review comments. The approve reason is one line; you saw maya's above. The back-and-forth of a real review, the "can you nudge the saturation on the left lantern" conversation, happens where it always did: in chat or on a call. Grixel records the verdict; the discussion that produced it lives elsewhere for now. I think verdict-first is the right order to build in, since the verdict is the part that must be tamper-evident, but the discussion layer is a real absence.
Review today is CLI-shaped. You watched maya open a PSD from a checkout and sign it in a terminal, and that is genuinely the whole experience. Engineers take to it immediately; art directors mostly do not live in terminals, and they will reasonably push back. External diff and merge tool integration helps, and the MCP surface means an assistant can read review state on your behalf, but a graphical client does not exist yet, and for the audience whose sign-off this system most wants to capture, that is the biggest gap on this page.
Granularity is the next objection. The transcripts above gate one file at a time, and a seasonal content drop can touch four hundred files; under an each-file rule that is four hundred sign-off records, and nobody should pretend a terminal makes that pleasant. The policy language has the right tool for it: a rule can set its target to a whole folder, meaning one sign-off for the subtree, keyed by the subtree's tree hash and staled by any file change within it. I have not shown it here because this demo did not exercise it, and bulk-approval ergonomics in general are exactly the kind of thing the CLI-shaped v1 has not smoothed yet.
A gate is only as good as its policy. Write a single stage satisfied by one approver from a group that rubber-stamps, and you have automated nothing except the record of the rubber stamp. The policy language gives you separation-of-duties defaults (an author cannot approve their own change unless the rule explicitly allows it), ordered stages like the lint-then-design pair above, and two-of-three-style rules for requiring independent reviewers, but it cannot supply judgment. Relatedly: a sign-off proves an eligible party recorded an approval against specific bytes. It cannot prove they looked carefully. No review system can, and I would distrust one that claimed to.
Two engineering caveats close the ledger. Promote-time verification walks every gated target under the source ref, and I have not measured that walk over thousands of gated targets; the transcripts above verify one. And Grixel is pre-release: the invariants are implemented and tested, enforced at the server, but they have not yet carried years of production traffic. That evidence only accumulates one way.
· · ·
#The record of done
Version control became very good at recording that content changed, and the record of acceptance drifted out to other systems. I think it belongs beside the content it accepts: bound to exact bytes and enforced by the server that owns them, expiring the moment the bytes move on. The transcripts above are that idea running against a real depot, from an art director signing a specific revision of a PSD to a lint check refusing a config that does not parse. What it needs next is the smoothing I just listed, starting with bulk approval and a review surface an art director would choose to use. That is the work in front of me.
Grixel is pre-release. Read the docs, or request early access.