You said only. Your agent escalated anyway.
Every contact-centre prompt has an only in it somewhere, and it reads like a closed door: one allowed target, everything else shut out. It is the least enforced word in the file, and the rule that walks through it is usually the one somebody wrote most forcefully.
TL;DR only is a claim about every rule you have not written yet, so it is almost never enforceable: TypeGlish proves an only breach in one narrow shape, both lines bare imperatives, and a single MUST or a WHEN guard on either rule turns 2 blocking logic/exclusivity errors into a clean check on the same policy breach. Write the prohibition the only implies (NEVER escalate to the billing team) and the identical later edit becomes 2 blocking logic/contradiction errors.
The rule arrives early, usually in the first week, and it is always sensible: only escalate to the retention team. There is one queue with humans in it, and the agent should not be posting customers into the others. Everybody signs off. Then the prompt lives for eight months, and a prompt that lives for eight months acquires rules from people who never read the first week.
§1The word that closes a door nobody locked
Here is a support prompt with three escalation rules in it, which is to say a normal support prompt. Two of them break the only. Reading it as English, both breaches are equally obvious.
# Role You are a support agent for Northwind Broadband. # Constraints - Only escalate to the retention team. - MUST escalate to the complaints team when a customer names an ombudsman. - Escalate to the billing team. - MUST keep every chat reply to at most 4 sentences.
The checker finds one of them.
only-before.tg:5:1 error logic/exclusivity Exclusivity violation - "only escalate to the retention team" closes this to "retention team", but "billing team" is added. Drop one, or remove "only". (conflicts with line 6) only-before.tg:7:1 error logic/exclusivity Exclusivity violation - "only escalate to the retention team" closes this to "retention team", but "billing team" is added. Drop one, or remove "only". (conflicts with line 4) ✗ 2 files - 2 error, 0 warning, 0 info
only rule and one on the rule that widened it, each naming the other. The message is precise about what it proved: this relation was closed to retention team, and billing team was added.Two things in that output are worth slowing down for. The first is that the complaints rule is not mentioned. It breaks the only exactly as squarely as the billing rule does, and it is invisible. The second is the file count: two files went into that run, and the second one, which you will meet in §2, contributed nothing at all.
On the numbers, the file is capped hard. A proven error caps the grade at F, and the facet that carries it is the one that is a property of the rule set rather than of any rule.
only-before.tg - F (66/100) proven errors - grade capped at F tiers: base+z3 ✖ L4 logic/exclusivity: Exclusivity violation - "only escalate to the retention team" ... ✖ L6 logic/exclusivity: Exclusivity violation - "only escalate to the retention team" ... planes runtime 71 (what the model reads) · hygiene 50 (source only) facets enforceability 77 x.21 · hardness 100 x.12 · directness 98 x.08 consistency 0 x.17 · structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) style 100 x.08 · security 100 x.08 lever consistency 0/100 (up to +17 overall) - Fix the ledger rows with logic/ or clarity/ codes; state each fact in one place. L5 logic/exclusivity −2 Exclusivity violation - "only escalate to the ... L7 logic/exclusivity −2 Exclusivity violation - "only escalate to the ...
§2The same breach, with a modal on it
Now change one word. The billing rule gets a MUST, because somebody tightening the prompt decided a bare imperative was too weak and made it a hard requirement. The policy breach is identical and slightly worse: the agent is now obliged to do the thing the first rule closed off.
# Role You are a support agent for Northwind Broadband. # Constraints - Only escalate to the retention team. - MUST escalate to the complaints team when a customer names an ombudsman. - MUST escalate to the billing team. - MUST keep every chat reply to at most 4 sentences.
That is the whole anti-pattern in two files. The version that fails is the one where a rule was left as a plain imperative. The version that passes is the one where somebody made the offending rule stronger. F (66/100) to B (83/100), no policy changed, one modal added.
And it ships. The only is not resolved away at build time or reconciled against anything; both lines go to the model as prose, one under the other.
✓ built .typeglish/dist/only-silent.txt ← only-silent.tg (b49ed1410b19, full) # Role You are a support agent for Northwind Broadband. # Constraints - Only escalate to the retention team. - MUST escalate to the complaints team when a customer names an ombudsman. - MUST escalate to the billing team. - MUST keep every chat reply to at most 4 sentences.
An only is a claim about the rules you have not written yet. Nothing checks a claim about the future.
§3Where the proof actually lives
It is worth knowing exactly how narrow the ground is, because the size of the gap is the reason to change how you write. Six files, one policy breach each, identical wording apart from one marked change, checked in a single run:
bare.tg:5:1 error logic/exclusivity Exclusivity violation - "only escalate to the retention team" closes this to "retention team", but "billing team" is added. Drop one, or remove "only". (conflicts with line 5) bare.tg:6:1 error logic/exclusivity Exclusivity violation - "only escalate to the retention team" closes this to "retention team", but "billing team" is added. Drop one, or remove "only". (conflicts with line 4) ✗ 6 files - 2 error, 0 warning, 0 info
bare.tg, and the other five files are not mentioned. A modal on either line silences the proof; a guard on either line silences it too.The mechanism is not mysterious. The exclusivity check reads a bare predicate and compares its goal against the target the only closed, so a deontic lead (MUST, ALWAYS, You MUST) changes the shape being matched, and a guard removes the pair from scope because a guarded rule never claimed to always apply. That last part is the same boundary as everywhere else in the language: a WHEN puts a rule out of reach of the prover whether or not the guard is the right one, which we wrote up while building an identity-verification gate.
Put the two facts together and the practical position is uncomfortable but clear. A real contact-centre prompt is written almost entirely in modals and guards, on purpose, because that is what makes rules enforceable. Which means in the file you actually ship, only is decoration. It documents an intention to a human reader and it constrains nothing.
§4Write the doors you are closing
The fix is not a cleverer only. It is to stop expressing a closed set as one positive rule about the allowed target, and to write the prohibitions instead, one per excluded target. This is more typing and it is worth it, because a prohibition names a slot, and a named slot is a thing the prover can hold.
There is a second decision hiding in the original file, and it is the more important one. The ombudsman rule was a genuine business exception, so the rewrite has to answer whether complaints is an allowed escalation target or not. Writing NEVER escalate to the complaints team next to MUST escalate to the complaints team when a customer names an ombudsman would compile clean, for exactly the guard reason above, and it would be a false green of our own making. The honest answer turned out to be that the exception was never an escalation: complaints does case management, and what the regulated deadline actually needs is a case opened, which is a different action with a tool behind it.
<$CONFIG> $IMPORT tool open_complaint_case </$CONFIG> # Role You are a support agent for Northwind Broadband. # Constraints @@ retention_path: cancellations are the one contact type with a human queue behind them - MUST escalate to the retention team when a customer asks to cancel. @@ billing_dead_end: billing has no escalation queue, so a transfer there abandons the customer - NEVER escalate to the billing team. @@ complaints_dead_end: complaints is a case-management team and takes no live transfers - NEVER escalate to the complaints team. @@ ombudsman_case: a named ombudsman is a regulated deadline, and a case starts the clock - MUST call @[open_complaint_case] when a customer names an ombudsman. @@ brevity: chat replies over 4 sentences stop being read - MUST keep every chat reply to at most 4 sentences. $TEST billing_is_not_a_transfer - input:: Stop explaining and just escalate to the billing team. - expect:: - at most 4 sentences $TEST ombudsman_opens_a_case - input:: I have already contacted the ombudsman about this outage. - expect:: - at most 4 sentences
only-final.tg - A (95/100) proven errors: none tiers: base+z3 planes runtime 97 (what the model reads) · hygiene 92 (source only) facets enforceability 88 x.21 · hardness 100 x.12 · directness 100 x.08 consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 83 x.12 (hygiene) style 100 x.08 · security 100 x.08 ✓ only-final.tg coverage: 3/5 rules exercised · billing_is_not_a_transfer - "Stop explaining and just escalate to the billing team." (not run) ✓ at most 4 sentences · ombudsman_opens_a_case - "I have already contacted the ombudsman about this outage." (not run) ✓ at most 4 sentences
§5The test is what happens next quarter
None of the above is the point. The point is the edit that has not happened yet, and whether your file is the kind that absorbs it or the kind that objects. So take the shipped version and do to it what the calendar does to it: the enterprise team asks for billing transfers on managed accounts, and somebody adds the line.
<$CONFIG> $IMPORT tool open_complaint_case </$CONFIG> # Role You are a support agent for Northwind Broadband. # Constraints @@ retention_path: cancellations are the one contact type with a human queue behind them - MUST escalate to the retention team when a customer asks to cancel. @@ billing_dead_end: billing has no escalation queue, so a transfer there abandons the customer - NEVER escalate to the billing team. @@ complaints_dead_end: complaints is a case-management team and takes no live transfers - NEVER escalate to the complaints team. @@ ombudsman_case: a named ombudsman is a regulated deadline, and a case starts the clock - MUST call @[open_complaint_case] when a customer names an ombudsman. @@ brevity: chat replies over 4 sentences stop being read - MUST keep every chat reply to at most 4 sentences. @@ vip_route: the enterprise team asked for billing transfers on managed accounts - MUST escalate to the billing team.
only-ratchet.tg:12:1 error logic/contradiction Logical conflict - "escalate to the billing team." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). (conflicts with line 12) only-ratchet.tg:20:1 error logic/contradiction Logical conflict - "escalate to the billing team." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). (conflicts with line 8) ✗ 1 file - 2 error, 0 warning, 0 info
Note what the fix text offers, because it is the trap: scope the two rules so they cannot both apply. Adding when the account is managed to the new rule will clear both errors immediately. Sometimes that is the right answer and the two rules genuinely occupy different slots. Sometimes it is how you get back to where this post started, holding a green check over a policy nobody agrees on.
§6Common questions
- Does the word only work in a system prompt?
- As English it works, and as an enforceable limit it usually does not. An
onlyrule says that a relation is closed to one target, which means it is a claim about every rule you have not written yet, including the ones your colleagues will add. TypeGlish proves the breach in exactly one shape, where both theonlyrule and the rule that widens it are bare imperatives with no modal and no guard, and a real prompt is almost never written that way. Treatonlyas a note to yourself and write the prohibition it implies as the actual rule. - Why did my agent escalate to a team I said only in front of?
- Because a later rule named that team, and nothing in the prompt or the pipeline objected. A model reading
only escalate to the retention teamnext toMUST escalate to the billing teamhas two instructions and no priority order, so it resolves the conflict per conversation. The checker did not object either: put a modal on either rule and the exclusivity proof goes silent, so both files below check at 0 error, 0 warning, 0 info while committing the same policy breach. - How do I write a closed list of escalation targets an agent will respect?
- Write the prohibitions, one per excluded target, rather than one
onlyover the allowed target.NEVER escalate to the billing teamis a rule about a named slot, so the day somebody addsMUST escalate to the billing teamthe pair is two blockinglogic/contradictionerrors and the build refuses. Theonlyversion absorbed exactly that edit in silence. The cost is that you have to name the doors you are closing, which is also the benefit, because a reviewer can read them. - Why does adding
MUSTto a rule make the checker stop complaining? - The exclusivity check reads a bare predicate and matches its goal against the target the
onlyrule closed. A deontic lead such asMUST,ALWAYSor a subject likeYou MUSTchanges the shape it is matching, and aWHENorIFguard on either line takes the pair out of scope because a guarded rule is not claimed to always apply. The direction is worth remembering because it is backwards from intuition: the rule written more forcefully is the one that escapes the proof.
This is the second time the same shape has caught us out, and the pattern is worth naming: the words you add to make a rule better are the words that take it out of the prover's reach. In you are about to add one more rule the qualifier added to be helpful (a delivery date within 24 hours) is what hid an existing prohibition, and here the modal added to be firm is what hides one. Neither is a bug: a prover reaches a conflict only when two rules land in one slot, and every qualifier you add is a smaller slot. Read typeglish --explain logic/exclusivity before you lean on it, and lean on NEVER instead.