Approval rules beta
Let comment-only and docs changes land on their own, and send risky paths to a person.
One file on your stalk, .gitstalk/policy.yml, says which kinds of change
land by themselves, which a small model triages, and which wait for a human. Like the
checks and automations, it is YAML: all .gitstalk/ configuration is.
Try it
-
Open Settings, Approval rules
Owners and maintainers edit. Everyone else with a role can read the file and sees why they cannot save.
-
Write a rule
The editor checks your YAML as you type, with the same parser the engine runs, so the messages and line numbers match.
-
Save
Saving pushes a bean with you as its author. It lands through the pre-land check like any change, and the rules apply once the stalk takes it.
- name: api
match: { paths: ["src/api/**"] }
route: maybe
The file
# yaml-language-server: $schema=https://gitstalk.dev/schemas/policy.json
groups:
security: ["@alice", "@bob"]
classes:
- name: comment-only
match: { syntax: comments-and-whitespace }
route: auto
- name: docs
match: { class: docs-only }
route: auto
- name: migration
match: { paths: ["src/db/migrations/**"] }
route: card
reviewers: ["@data"]
- name: protected
match: { paths: ["src/auth/**"] }
route: card
writers: ["@security"]
- name: api
match: { paths: ["src/api/**"] }
route: triage
- name: everything-else
match: { otherwise: true }
route: triage
| Key | Meaning |
|---|---|
groups |
Names for lists of people: security: ["@alice", "@bob"]. |
classes[].name |
Lower-case letters, digits and dashes, unique. policy-change is
built in.
|
description |
Optional: what the class is, in a sentence (at most 200 characters). Clef reads it when it triages the class's beans. |
match |
One of { paths: [...] } (any changed file matches a pattern; the
patterns are the same as protected paths),
{ syntax: comments-and-whitespace } (every changed file differs
only in comments and whitespace),
{ class: docs-only | tests-only | lockfile | comment-only } (a
built-in class), or { otherwise: true } (the
fallback class: everything no other class matched).
|
route |
auto, triage or card (below). |
reviewers |
Who the card is addressed to. Shown, not enforced yet. |
writers |
The only principals who may change this class (below). |
Parsing is strict: YAML 1.2 (so no stays a string), unique keys, and no
anchors, aliases or merge keys. A file that is invalid on the stalk leaves only the
built-in rules in force, and the push says so. With
no policy file at all, every bean is auto and pushes show
no rules lines, exactly as before.
Classes
A class is a kind of change. Classes are computed by code from the changed paths and the file contents, never by a model.
| Built-in class | Holds when every changed file is |
|---|---|
docs-only |
*.md, *.mdx, *.markdown,
*.txt, *.rst, *.adoc, or under
docs/
|
tests-only |
under test/, tests/, __tests__/, or named
*.test.*, *.spec.*, *_test.*,
test_*.py
|
lockfile |
a package manager's lockfile |
comment-only |
unchanged in code: only comments and whitespace differ (below) |
How comment-only is judged
Each side of each changed file is read into code tokens with comments and whitespace
dropped; the two streams must be equal. Strings, template literals, regular expressions
and Rust raw strings stay whole. It is judged for TypeScript and JavaScript (not JSX),
Rust, C and C++, Java and Go. Every doubt means not comment-only: an
added, deleted, binary or unknown file, anything unterminated, or a file over 512 KB. A
reformat such as )+( to ) + ( counts as code.
policy-change
Any change under .gitstalk/ (except backlog.md, which agents
tick with ordinary beans) is a policy-change. From an agent it is always a
card, whatever the file says. When it is a person's own push and they may decide (an
owner or maintainer with a personal token or SSH key, or the Settings editor), the push
is the decision and the other classes route it. The policy file is also always a
protected path.
Everything else: the fallback class
A class with match: { otherwise: true } applies
only when no other class matched, so "everything else is triaged" needs
no list of source folders. When another class matches, the fallback stays out of it
entirely: its route, reviewers and writers do not count. An agent's change to
.gitstalk/ is still a card, and a change made only inside
.gitstalk/ (your own save from Settings, an agent ticking the backlog) is
never routed by it. The built-in classes count only when one of your classes names them:
without a docs class, a docs-only change takes the fallback's route.
A file may have one fallback class, and otherwise cannot be combined with
paths, syntax or class; either mistake is shown
with its line and column. The fallback is an ordinary class otherwise: it has a name on
receipts, a description for Clef, and may have reviewers and writers.
Starter rules
A repository without a file opens Settings, Approval rules on these recommended rules; nothing changes until you save them.
classes:
- name: comments
description: Only comments and whitespace changed.
match: { syntax: comments-and-whitespace }
route: auto
- name: docs
description: Documentation only.
match: { class: docs-only }
route: auto
- name: tests
description: Tests only.
match: { class: tests-only }
route: auto
- name: lockfile
description: A package manager's lockfile only.
match: { class: lockfile }
route: auto
- name: migrations
description: Database schema and data migrations.
match: { paths: ["**/migrations/**", "**/*.sql"] }
route: card
- name: auth
description: Authentication, sessions, permissions and secrets handling.
match: { paths: ["**/auth/**", "**/permissions/**", "**/security/**"] }
route: card
- name: everything-else
description: Any other change.
match: { otherwise: true }
route: triage
Routes
- Classifypaths and syntax
- Autoland on its tests
- TriageClef picks auto, recheck or card
- Cardheld for a person
When several classes match, the strictest route wins: card over triage over auto. A
comment-only change takes the route of your comment-only class when you have one, even
inside a sensitive path, because no behaviour changed. When none matches, the
fallback class routes the bean if you have one; otherwise it is
auto.
| Route | What happens |
|---|---|
auto |
The bean lands and promotes as usual, on its tests. |
triage |
Clef, a small model that sees the classes and their descriptions, the paths, the
diff's size, the bean's intent and commit message, and the
beans it never saw (never file contents), picks
auto or card within 2.5 seconds, and can send the bean
back to its author to recheck. If it is late or
unavailable, fixed rules decide: auto when the bean is also comment-only,
docs-only or tests-only, otherwise card, never recheck. Clef holds a bean only
when it is at least 65% sure a card is needed; a less certain card lands on its
tests.
|
card |
Held before landing until an owner or maintainer approves it (the Held panel on the bean's page). The check answers a red, awaits approval by a person, that says nothing needs fixing. The approval is for that exact revision; a new push needs a new approval. |
Clef and Jev only pick or rank. They never write code. The receipt names the model that decided.
Recheck: beans it never saw beta
Two beans can each pass their tests, merge with no conflict and still break together.
One makes price() return dollars; another, written at the same time, adds
shipping as price() + 500 cents. Checkout now charges $600, and no test
fails unless one covers the combination. When a bean is triaged, Clef also reads the
beans it never saw: those that landed after it forked, and those
pushed before it and still being checked, which may land first. For
each it gets their intent and commit message, the files both changed, and how the two
connect: a file one changed imports a file the other changed, or a function, constant or
type one changed is used on the other's new lines. Clef answers one yes-or-no question
per bean (up to four, the most connected first): do the two disagree about something
both rely on, such as a unit, a format, a meaning or a value?
The clash questions go to Clef 27B, a larger Clef that answers them better than the model that picks the route. When it is at least 30% sure of a clash, the bean goes back to its author agent, not to a person: an ordinary red that names the other bean, its intent and the links. A recheck is cheaper than a card, so its bar is lower. The author reads the other bean, fixes theirs if the two disagree, and pushes again with a trailer naming what they checked. The trailer takes those beans off the list; the tests still decide. When a test of the other bean only pins the assumption the two disagree on (a unit, a format), the author updates that test so the other bean's intent still holds, and says so: reconciling, not weakening it. A clash goes to the agent first even when the change would also need a person; the fixed push is judged again, so a real risk still reaches a person and a simple misalignment never does. The bean pushed later is the one rechecked, so the bean that lands second owns the repair.
$ git push -o wait origin HEAD:refs/heads/bean/shipping
remote: gitstalk: approval rules: checkout -> recheck (clef, p=0.66)
remote: gitstalk: back to its author: recheck against price-dollars
remote: gitstalk: RED: shipping was not landed. Merged onto the sprout, these tests failed:
remote: gitstalk: - .gitstalk/policy.yml > recheck against beans that landed after it forked
remote: gitstalk: price-dollars: Price in dollars, not cents
remote: gitstalk: imports: src/checkout.mjs -> src/price.mjs
remote: gitstalk: it changed, you use: price
remote: gitstalk: Intent-checked: price-dollars
Intent-checked: price-dollars in the commit message.
Links are read for JavaScript and TypeScript, Python, Ruby, Java, Kotlin, C#, Swift, Go, Rust, C and C++, and PHP, from the changed files' text alone: no build system is run. They point Clef at a contract; they never decide. Recheck is for repositories; benchmark races list no other beans.
Writers
writers limits who may change a class. A bean from a principal outside the
list falls off at the pre-land check, the same way a protected path does, naming the
classes and who may change them.
| Principal | Means |
|---|---|
@handle |
A person: any push acting for them, by any credential. |
@group |
A name under groups. A group wins over a handle of the same name.
|
agent:a<slot> |
An agent slot in a benchmark race. Not used for ordinary repositories. |
Receipts
Every judged bean leaves a receipt: its classes, the route, which class decided, who
decided (policy, clef or rules), Clef's
probability, the approval state, and the policy file and stalk commit it was judged by.
The push prints it on its remote lines, the bean's page shows it in the Held panel, and
the MCP bean_status tool reports it with held: true, the
page's address (approve_url) and a next step when a bean is held.
$ git push -o wait origin HEAD:refs/heads/bean/token-refresh remote: gitstalk: approval rules: protected -> card (policy) remote: gitstalk: awaiting a person's approval remote: gitstalk: ...
api -> card (clef, p=0.81), or
checkout -> recheck (clef, p=0.66) when it goes back to its author
(its receipt is route auto with the beans in recheck).
Approving a held bean
A held bean shows a Held badge in its repository's Changes list, and a Held filter lists only those beans. Its page opens with the Held panel: the classes, the rule and who decided (the policy, Clef with its probability, or the triage rule), the revision waiting with a link to its diff, and the reviewers the policy names.
Revision 4e1c9a2 waits for a person. Nothing in it needs fixing.
| Question | Answer |
|---|---|
| Who approves |
The repository's owner and maintainers (for an organization's repository, its
owners too). Named reviewers are shown, not required.
|
| What is approved | One revision, the bean's pushed head. The page sends the revision it showed; if a new push arrived meanwhile, the approval is refused as stale. A new push needs a new approval. |
| What happens next | The bean is checked again at once and lands on the sprout if it passes. The page shows who approved and when, without a reload. |
| Where it is recorded | The repository's access log (Settings, Collaborators, for the owner): who approved, the client (web or MCP and its name), the revision and the note. |
From an agent: bean_approve
An agent session with the write scope, acting for an owner or maintainer,
can approve for its person with bean_approve (see
MCP tools). Approving is the person's decision, so the
tool tells agents to ask first and never to approve on their own initiative. The web
button and the tool make the same call, with the same checks.
Where the rules are read from
Always from the stalk, never from the bean being judged. A bean cannot change the rules that judge it. If the stalk cannot be read, the check is retried; it is never treated as "no policy".
Not done yet
-
reviewersare shown, not enforced: any owner or maintainer approves. - Approvals are for repositories. A benchmark race has no approve path, which is why races keep the rules off.
- Clef's answers are not yet compared with later human decisions.
- The recheck threshold (30%) comes from a labelled set of 35 pairs (16 of 17 clashes caught, 2 false rechecks of 18). Clashes between files that meet only over the network, such as an API and its client, have no links yet. The bean's page shows a recheck as a red; it has no panel of its own yet.