Not approving is not declining
Most guardrails in a support prompt are written as prohibitions, and for one family of verbs that is enough, because forbidding the thing is instructing its opposite. For the family that actually handles customers, it is not. Here is where the line falls and how to tell which side you are writing on.
TL;DR A refunds section written entirely as MUST NOT approve rules is 0 error, 0 warning, 0 info at B (86/100) with enforceability 93 and consistency 100 and still specifies no reply at all, because the emission verbs fold a prohibition onto its positive head (MUST NOT display plus MUST withhold is 2 clarity/duplicate warnings, one rule twice) while the response verbs never fold (MUST NOT approve plus MUST decline is 0 error, two rules), so a refusal has to be written as its own obligation or it does not exist.
Read the guardrail section of any support prompt and count the polarity. Almost all of it is never, do not, must not. That is a sensible instinct: the expensive failures are things the agent did, so the rules name the things it must not do. The instinct is also load-bearing in a way nobody checks, because it quietly assumes that forbidding an action tells the agent what to do instead.
For some verbs that assumption is exactly right, and the compiler will prove it to you. For the verbs that decide how a customer is answered, it is exactly wrong, and the compiler will prove that too, by staying silent in a way that turns out to be informative.
§1Four rules, no reply
Here is a refunds section of the kind a midmarket CX team ships in week two. Every rule in it came from a real incident. It is short, it is specific, and it has bounds.
# Role You are a refunds agent for Marlow Kitchens. You manage a refund request. # Constraints - You MUST NOT approve a refund over 100 dollars. - You MUST NOT approve a refund on a clearance item. - You MUST NOT approve a second refund on one order. - You MUST hand off to a human agent when the customer asks for one.
$ npx typeglish check refunds-forbid.tg ✓ 1 file — 0 error, 0 warning, 0 info $ npx typeglish score refunds-forbid.tg refunds-forbid.tg — B (86/100) proven errors: none tiers: base+z3 planes runtime 98 (what the model reads) · hygiene 50 (source only) facets enforceability 93 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08 lever annotation 0/100 (up to +12 overall) — Put a "@@ why" note directly above each statement ("@@ name: why" also names it). $ npx typeglish build refunds-forbid.tg ✓ built .typeglish/dist/refunds-forbid.txt ← refunds-forbid.tg (066d0e388abf, full) # 321 bytes, shipped as written
What actually happens in that conversation is decided by the model, per conversation. Sometimes it declines and explains the cap. Sometimes it declines and does not say why, which reads as a brush-off. Sometimes it hedges into a paragraph that never contains the word no, and the customer waits for an answer that is never coming, then asks again, then asks for a person. All three are compliant with every rule in the file.
§2The family that folds, and the family that does not
The reason this is a design principle rather than an oversight sits in the verb ontology. Directive heads parse against a curated catalog of verb families, and the families do not all behave the same way. Two lines, in two families, with the same grammatical shape.
# Role You are a refunds agent for Marlow Kitchens. You manage a reason code. # Constraints - You MUST NOT display the reason code. - You MUST withhold the reason code.
# Role You are a refunds agent for Marlow Kitchens. You manage a refund request. # Constraints - You MUST NOT approve the refund request. - You MUST decline the refund request.
$ npx typeglish check emission-pair.tg response-pair.tg emission-pair.tg:6:1 warn clarity/duplicate Duplicate rule — "display the reason code." is forbidden in 2 places (also line 6). State it once and reference it. emission-pair.tg:7:1 warn clarity/duplicate Duplicate rule — "display the reason code." is forbidden in 2 places (also line 5). State it once and reference it. ✓ 2 files — 0 error, 2 warning, 0 info
That is the whole principle, and it is deliberate. The emission family is one polarity system: withhold, suppress, hide, omit, redact, exclude are negative-pole verbs that fold onto their positive heads, so must not display and must withhold are the same rule stated twice. The response family, approve, reject, accept, decline, grant, refuse, is explicitly excluded from that fold, and the reason is one line of design rationale worth memorising:
Rejecting is an act, not an absence.
Withholding a reason code is what happens when you do not display it. There is no equivalent for a refusal. Not approving a refund is a state of the world; declining it is something the agent has to do, in words, to a person who is waiting. The compiler refuses to pretend otherwise, which is why it will not accept your prohibition as a substitute for an instruction.
It also means the redundancy warning is a service you only get on one side. Write the same policy twice in the emission family and the checker hands you clarity/duplicate. Write it twice in the response family and both lines survive, because to the compiler they were never the same thing. That asymmetry runs the other way too, and two rules can be exact opposites while sharing no vocabulary at all when the fold is what connects them.
§3The score cannot see the missing half
The natural next hope is that the grade catches what the check does not. It does not, and the way it fails is worth looking at directly, because it tells you what kind of defect this is.
$ npx typeglish score p-forbid.tg # - You MUST NOT approve a refund over 100 dollars. p-forbid.tg — B (87/100) proven errors: none tiers: base+z3 planes runtime 100 (what the model reads) · hygiene 50 (source only) facets enforceability 100 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08 $ npx typeglish score p-decline.tg # - You MUST decline a refund over 100 dollars. p-decline.tg — B (87/100) proven errors: none tiers: base+z3 planes runtime 100 (what the model reads) · hygiene 50 (source only) facets enforceability 100 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08
This is the same boundary that makes an unbounded retry outscore a bounded one, and it is a boundary rather than a bug. Enforceability asks whether a rule names a checkable action. Both do. Nothing in either sentence indicates that the first one leaves the customer-facing half of the decision unwritten, because the missing half is not in the sentence. It is in the space beside it.
Which gives the practical test, and it takes about four seconds per rule. Read your prohibition and ask what the transcript looks like if the agent obeys it and does nothing else. For must not display the reason code, the transcript is fine: the agent answers, and the code is not in it. For must not approve the refund, the transcript is a customer waiting.
§4A refusal owes the file three lines
Once you accept that the refusal has to be written, the shape of it falls out. Every refusal a support agent gives is three obligations, and prompts routinely ship one of them or none.
The refusal itself, so there is an answer. The reason, because a refusal with no reason reads as a fault and gets asked again in the next turn. And the exit, because a customer who has been told no and given nowhere to go asks for a person, which is the outcome the cap existed to avoid paying for.
The threshold belongs to the runtime rather than to the model. The order total is a number the host already has; asking the model to compare it is asking it to do arithmetic it may or may not do, when the compiler can do it once, at build time, and delete the arm that lost.
<$CONFIG> $IMPORT tool issue_refund $REQUIRE variable refund_amount: number </$CONFIG> # Role @@ role: refunds desk, capped spend, a human is always available above the cap You are a refunds agent for Marlow Kitchens. @@ subject: introduces the request every rule below decides on You manage a refund request. # Constraints @@ cap: 100 is the ceiling finance signed off, so above it a person owns the call $IF @{refund_amount} is greater than 100: @@ refuse: the prohibition left the reply undefined, so the refusal is the obligation - You MUST decline the refund request. @@ reason: a refusal with no reason reads as a fault, and the caller asks again - You MUST say that refunds over 100 dollars need a human agent. @@ exit: every refusal hands the customer somewhere to go next - You MUST offer a transfer to a human agent. $ELSE: @@ pay: under the cap the tool is the whole procedure - You MUST call @[issue_refund]. $TEST over_cap - input:: I want 240 dollars back on order 88120. - expect:: - contains "100" - at most 3 sentences
$ npx typeglish build refunds-final.tg --vars '{"refund_amount":240}'
✓ built .typeglish/dist/refunds-final.txt ← refunds-final.tg (2492a2d59f91, full)
# Role
You are a refunds agent for Marlow Kitchens. You manage a refund request.
# Constraints
- You MUST decline the refund request.
- You MUST say that refunds over 100 dollars need a human agent.
- You MUST offer a transfer to a human agent.
# 246 bytes. Three obligations, no prohibitions, and no decision left in the room.
$ npx typeglish build refunds-final.tg --vars '{"refund_amount":40}'
✓ built .typeglish/dist/refunds-final.txt ← refunds-final.tg (5347a93fd349, full)
# Constraints
- You MUST call issue_refund.
# 126 bytes. The whole refusal policy is absent, because it does not apply.
§5The gap the checker leaves open
One consequence of the no-fold rule deserves saying out loud, because it is the cost side of the design and you should know about it before you meet it in a review.
$ npx typeglish check both-ways.tg one-verb.tg # both-ways.tg: - You MUST approve the refund request. # - You MUST decline the refund request. # one-verb.tg: - You MUST decline the refund request. # - You MUST NOT decline the refund request. one-verb.tg:6:1 error logic/contradiction Conflicts with line 6. Logical conflict — "decline the refund request" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). one-verb.tg:7:1 error logic/contradiction Conflicts with line 5. Logical conflict — "decline the refund request" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ✗ 2 files — 2 error, 0 warning, 0 info # exit 1
Both files are impossible to obey and only one of them stops the build. That is the honest price of not folding: a prover that treated approve and decline as one axis would catch the first file, and would also have to conclude that MUST NOT approve already means MUST decline, which is the false equivalence this whole post is about. The design picks the error it prefers to make, and it picks the one that leaves the gap visible instead of papering over it.
So the review habit that closes it is yours, not the tool's: one verb per decision, and the decision written as what the agent does. The same discipline is what stops an apology policy written by three desks from turning into three policies that cannot argue with each other, and it is the reason a prompt is easier to review after you have deleted the word not from it a few times.
§6Common questions
- Why does my AI agent go quiet instead of saying no?
- Because the rule you wrote forbade the approval and never required the refusal, so the only behaviour the prompt specified is an absence. A refunds section of three
MUST NOT approvelines is 0 error, 0 warning, 0 info, clean under--strict, B (86/100) with enforceability 93 and consistency 100, and it builds to a 321-byte artifact in which no instruction tells the agent to reply anything at all. Add the obligation, not a sterner prohibition:MUST decline,MUST say why,MUST offer the next step. - Is MUST NOT approve the same as MUST decline in a system prompt?
- Not to the checker, and not to a customer. Put both in one file and it is 0 error, 0 warning, 0 info: two rules, both load-bearing. Write the emission equivalent,
MUST NOT display the reason codebesideMUST withhold the reason code, and the same shape is 2clarity/duplicatewarnings readingdisplay the reason code.is forbidden in 2 places, because the emission family folds a negative-pole verb onto its positive head and one of the two lines is redundant. Emission verbs fold; response verbs do not. - Does the TG score tell me a prohibition is incomplete?
- No, and it should not pretend to.
You MUST NOT approve a refund over 100 dollars.andYou MUST decline a refund over 100 dollars.both score B (87/100) with enforceability 100, hardness 100, directness 100 and consistency 100, facet for facet identical. Both sentences name a concrete action with a bound, which is what enforceability measures. The difference between them is not in either sentence, it is in the reply the first one never specifies, so no facet computed from the text can see it. - How do I write a refusal rule an AI agent will actually follow?
- Write three obligations rather than one prohibition: the refusal, the reason, and the exit. Move the threshold to a typed input so the compiler picks the branch, with
$REQUIRE variable refund_amount: numberand a$IF @{refund_amount} is greater than 100chain, so the over-cap build is a 246-byte artifact whose only instructions are to decline, state that refunds over 100 dollars need a human agent, and offer a transfer, while the under-cap build is 126 bytes and says only to call the tool. That file is 0 error, 0 warning, 0 info at A (100/100).
The figure that changed how we read guardrail sections is the two-file check in §2, and specifically that it takes one invocation. Same shape, same modals, same object pattern, and the compiler treats one pair as a restatement and the other as two independent facts, with no flag anywhere to say why. The rule is not written on the sentence, it is written in the family the verb belongs to, and you can read the families out of typeglish reference in about a minute: emission carries the polarity fold and the note that a negative-pole verb folds onto its positive head, response carries the note that rejecting is an act rather than an absence. Since then the first pass on any Constraints section has been a polarity count. If a section is all prohibitions and the verbs are response verbs, the prompt has described a wall and has not described a door, and the transcripts will show you exactly what a model does when it hits a wall with no door: something different every time.