What AI may change
Your registry decides which words exist. Nothing in it says which of them matter.
That is a real gap, and it is easiest to see on a shop. "Change the wording of
this heading" and "change the price" are the same move: one configure, setting
one prop on one node. The runtime cannot tell them apart, because it has never
heard of your shop and has no way to learn that one of those two costs money.
So you tell it, in a small object called a policy. It is the second list you write, after the registry, and it is the shortest interesting file in a Loom deployment.
import { gatePolicySchema } from "@jam-overture/loom"
export const policy = gatePolicySchema.parse({
policyId: "acme-storefront-2026-09",
protectedPrimitiveTypes: ["commerce.checkout", "commerce.price-tag"],
})
Two lines of vocabulary, and every other setting left alone. That is a complete, working policy — the rest have defaults, and most deployments never touch them.
What one line of vocabulary buys
Below is one ask — the same ask — judged twice. The tree is the same, the change is the same, and the two policies differ by that one list.
What difference does one line of vocabulary make?
Asked: Make the main heading on this page a smaller one.
The policy you get for free
protectedPrimitiveTypes is empty — the runtime's own default.
Applied
accepted · low
Why, in the Gate’s words
reversible, within the stakes ceiling, and confidently interpreted
judged by default
This site's policy
protectedPrimitiveTypes is ["loom.heading"], and nothing else differs.
Held for a person
requires-confirmation · high
Why, in the Gate’s words
touches protected loom.heading
judged by loom-docs
Nothing about the change moved. The only difference between these two runs is that one policy had said a heading matters here.
Both runs were performed as this page was built, against the real Gate. Nothing here is a screenshot of a good day.
The thing to take from it: stakes are a property of what a change touches, not of how it is phrased. Nobody asking to make a heading smaller is being careless, and the wording of the ask is identical in both columns. What differs is that one deployment had said, in advance, that headings are the kind of thing it wants to look at.
Held and refused are different words
The obvious next question is how far this goes. If naming a primitive makes changes to it wait for a person, does naming enough primitives freeze the site?
No — because the runtime distinguishes changing a protected thing from destroying one, and it does that on its own.
Where does asking a person stop being enough?
Asked: Take the main heading off this page.
The policy you get for free
Nothing is protected, so this is an ordinary removal.
Applied
accepted · medium
Why, in the Gate’s words
reversible, within the stakes ceiling, and confidently interpreted
judged by default
This site's policy
The same one line — and destroying what it protects outranks reconfiguring it.
Refused
rejected · critical
Why, in the Gate’s words
destroys protected loom.heading; touches protected loom.heading; restructures at depth 1
judged by loom-docs
One word apart from the change above, under the same one-line policy, and past the refusal floor there is no button that turns it into a yes.
Same policy as the column above, same protected primitive, one word different in the ask. Demoting a heading is high stakes and gets held; deleting one is critical and is refused outright. That ordering is the runtime's, not something your policy set — which means a short protected list gives you both behaviors without you having to describe either.
Who asked is part of the question
Every ask says where it came from: a person typed it, a signal triggered it, a
schedule ran it, or a developer did it. That is the intent's origin, and the
policy gives each one its own ceiling.
The effect is worth seeing on the most harmless change on this site — adding one sentence to the end of a page.
Does it matter who wanted it?
Asked: Add a closing line to the end of this page.
Somebody asked
origin is user-instruction, whose ceiling is medium.
Applied
accepted · medium
Why, in the Gate’s words
reversible, within the stakes ceiling, and confidently interpreted
judged by loom-docs
Nobody asked
origin is scheduled-adaptation, whose ceiling is low.
Held for a person
requires-confirmation · medium
Why, in the Gate’s words
restructures at depth 1
judged by loom-docs
The same policy, the same change, the same page — and a person who asked for it gets it, while a schedule that nobody was watching has to check first.
The same policy, the same page, the same sentence. A person who asked for it gets it; a schedule nobody was watching has to check first.
Look at what the Gate says about the change itself: it is medium stakes in both columns, because adding a block to the top level of a page is a structural change however innocent it reads. Nothing about the change was reassessed. What moved is the line it had to stay under.
This is the setting most likely to matter to you and least likely to occur to you, because it only bites once something other than a human is proposing changes. The default is drawn where most deployments want it, and the direction to move it is downward: a source of changes you trust less gets a lower ceiling, never a higher one.
The whole of a policy
Fourteen settings, and you have now met three of them. Here is all of it — what each one is, what you do about it, and what you get if you say nothing.
Every default below is read out of the runtime as this page is built, so the number you see is the number in your build. The list itself is the policy's own type: a setting added to the runtime and not to this page stops the site compiling, rather than quietly leaving you a page that describes all of them but one.
What it is called
yours to write
Every decision the Gate records names the policy that made it, so the name has to mean one thing forever.
policyId
"default"
The name this policy answers to when a recorded decision is asked what judged it.
What you do about it. Pick a name you will still recognize in a year. Then treat it as naming the contents rather than the file: if you change what is protected, give the policy a new name, because every decision already written under the old one claims to have been judged by what that name meant then.
What your deployment calls consequential
yours to write
The runtime has never heard of your primitives, so it cannot know which of them matter. This is the part only you can write, and it is the part that changes answers.
protectedPrimitiveTypes
[]
The primitives you would rather nobody changed without asking you first.
What you do about it. Name the handful whose being wrong would cost you something real — a checkout, a price, a consent notice. Reconfiguring one of these is treated as high stakes and destroying one as critical, so a short list changes a lot of answers and a long one holds up everything.
protectedPropKeys
[]
Prop names that carry meaning rather than appearance, wherever they appear.
What you do about it. Use this for the value rather than the component: a price matters on every primitive that has one, and listing the primitives instead would miss the next one somebody writes.
outOfTreeEffectTypes
[]
Primitives whose configuration reaches out of the page and does something in the world — takes a payment, sends a message.
What you do about it. Name them, because undo cannot help here. The runtime undoes a change by editing the tree back, and nothing about editing the tree back un-sends an email — so a change touching one of these is never counted as reversible.
interactiveTypes
{}
Which of your primitives render a thing the reader aims at — a link, a button.
What you do about it. Do not write this one. interactiveTypesFor(registry) reads what each primitive already declared about itself, so the day somebody gives another component a link the policy knows. A hand-typed copy is wrong the first time the library moves, and nothing says so.
registeredPrimitiveTypes
[]
Every primitive your deployment can actually draw.
What you do about it. Do not write this one either. registeredTypesFor(registry) reads the same list the renderer resolves against, so the two cannot disagree. Declaring it turns a proposal naming a primitive you do not have into a refusal the model can act on, instead of a revision that renders a hole; leave it out and you keep the older behavior, where the change lands and the page reports it at render.
How much rope each asker gets
ships with answers
The same change is not equally safe depending on who wanted it and how sure the guess was. These four say where the lines are.
autoApplyCeiling
{"user-instruction":"medium","system-signal":"low","scheduled-adaptation":"low","developer":"high"}
The highest stakes each kind of asker may reach without a person being asked.
What you do about it. The default already draws the line most deployments want: somebody who typed a request gets more latitude than an adaptation nobody asked for. Lower an entry to make a source of changes quieter; there is no entry that lets anything through unconditionally.
refusalFloor
"critical"
The level at which a change stops being offered to a person at all.
What you do about it. Leave it at critical unless you have a reason. Lowering it turns changes a reviewer could have approved into changes nobody can, and a refusal has no button that turns it into a yes.
minimumConfidence
0.7
How sure the interpretation has to be before a change can apply on its own.
What you do about it. Only the model-backed interpreter grades itself below 1, so this does nothing until you connect one. Raise it if you would rather see more asks than have more of them land unwatched.
confidenceFloor
0.3
Below this, the change is refused rather than offered to a person.
What you do about it. This is the line between “ask somebody” and “do not waste their time”. A guess this unsure is usually a question the model did not understand, and handing it to a reviewer moves the confusion rather than resolving it.
How big is big
ships with answers
Questions about shape mean the same thing on every deployment — how much removal is a lot, how close to the top counts as restructuring — so these ship with answers and most sites never touch them.
removalThresholds
{"medium":3,"high":12}
How many nodes a change has to remove before it counts as a medium or a large removal.
What you do about it. Nothing, on most sites. Both numbers are about the tree rather than about your business, and the vocabulary lists above are the lever you actually want.
breadthThreshold
8
How many distinct nodes one change may touch before it counts as a broad one.
What you do about it. Nothing, usually. Lower it if your pages are small enough that eight nodes is most of one.
shallowDepthThreshold
1
How near the top of the page a structural change has to be before it counts as restructuring.
What you do about it. Nothing. Depth 1 is the page's own children — the blocks a reader sees as the page — and moving one of those around is the change worth noticing.
inverseRetentionBudget
200
How much of the old page an undo may have to carry before undo stops being practical.
What you do about it. Nothing. It exists so that a change whose undo would be enormous is not quietly called reversible, and 200 nodes is well past anything a page ought to be.
The two lists you should not write by hand
interactiveTypes and registeredPrimitiveTypes are the exceptions in that
table, and both are worth saying twice. The first answers "which of my
primitives render a thing the reader aims at?" — a link, a button — and every
primitive already declared that about itself when it was defined. The second
answers "which of them do I have at all?", which is the registry, written down
a second time.
import { interactiveTypesFor, registeredTypesFor } from "@jam-overture/loom/sdk"
export const policy = gatePolicySchema.parse({
policyId: "acme-storefront-2026-09",
protectedPrimitiveTypes: ["commerce.checkout", "commerce.price-tag"],
interactiveTypes: interactiveTypesFor(registry),
registeredPrimitiveTypes: registeredTypesFor(registry),
})
A hand-typed copy of either is wrong the first day somebody gives another
component an href, or adds a primitive and forgets the policy — and nothing
anywhere would say so. Read both off the registry instead, and they cannot
drift.
A name is a claim about contents
policyId looks like a label and is closer to a version number.
Every decision the Gate makes records the name of the policy that made it, so a change judged six months ago can still say what it was judged by. That only works while a name means one thing. Change what a policy protects, and give it a new name — otherwise every decision already written under the old one is claiming to have been judged by rules that no longer exist.
The date in acme-storefront-2026-09 above is one way to do that and not the
only one. What matters is that you never quietly redefine a name somebody's
audit trail is pointing at.
Handing it to the runtime
A policy is a value, so something has to give it to the runtime. That is
policySource — a function of the tree and the intent, because a deployment
serving two kinds of page may not want to judge them the same way.
import { fixedPolicy } from "@jam-overture/loom"
import type { CompositionRuntime } from "@jam-overture/loom"
const runtime: CompositionRuntime = {
interpreter,
policySource: fixedPolicy(policy),
events,
clock,
idFactory,
}
fixedPolicy is the one-policy answer and is what most sites want, this one
included. What your app has to
do is where that runtime object goes
next, and what your own code owes a person for each way an ask can end.