You did not narrow that rule. You moved it.
Every exception in an agent prompt is a few words added to a rule that was too strict. Some of those words keep the rule where a prover can reach it. Most of them do not, and the check stays green either way.
TL;DR An adjective on the object keeps a rule in the prover's reach, so NEVER send a transcript beside MUST send a short transcript is a blocking logic/modifier-contradiction, while a preposition, a recipient or an adverb moves the action into a different slot and the identical policy checks at 0 error, 0 warning, 0 info with consistency 100. The edits that change your policy stay refereed; the edits that change only your grammar are what switch the referee off. Strip every modifier from the pair and re-check to find out which you wrote, and put a real exception in a $IF arm, where exactly one half of it reaches the artifact.
Nobody writes a contradiction on purpose. What happens is smaller than that. A prohibition goes in during an incident, the prohibition turns out to be too wide, and somebody adds three words to a different line so that the case they need is allowed again. That edit is under a minute, it reads as obviously correct, and it is the most common way an agent prompt ends up telling the model two things at once.
The reason it survives is that the words feel like scope. Send a transcript and send a transcript to a verified caller are plainly not the same instruction to a person reading them, so it seems safe to assume they are not the same instruction to anything else either. They are not the same instruction to the compiler. That is the problem, not the reassurance it sounds like.
§1Three rules, one action, and a tick
Here is a transcript policy from a billing prompt. One prohibition and two obligations, added by three different people over about six months, all three about sending a transcript to the person on the phone.
# Role You are a billing support agent for Cobalt Mobile. # Constraints - You NEVER send a transcript. - You MUST send a short transcript after the call ends. - You MUST send the caller a transcript on request.
✓ 1 file — 0 error, 0 warning, 0 info
The score is where you would expect the disagreement to show up, since consistency is the facet that measures the rules against each other rather than one at a time.
transcripts.tg — B (85/100) proven errors: none tiers: base+z3
planes runtime 97 (what the model reads) · hygiene 50 (source only)
facets enforceability 90 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).
consistency 100. enforceability 90, because all three rules do name an action and two of them carry a bound. The only thing the score has to say about this file is that nobody wrote any @@ notes. B (85/100) is a grade you would ship.So the file is not clean because the policy is fine. It is clean because of six words: short, after the call ends, and the caller. Take them out and the same three rules become two proven errors.
§2Strip the modifiers and the file breaks
This is the one procedure worth taking away from the whole post, because it is the only way to find out from the outside whether your qualifiers are load-bearing. Copy the pair into a file on its own and delete every modifier from both lines.
# Role You are a billing support agent for Cobalt Mobile. # Constraints - You NEVER send a transcript. - You MUST send a transcript.
stripped.tg:5:1 error logic/contradiction Conflicts with line 5. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). stripped.tg:6:1 error logic/contradiction Conflicts with line 4. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ✗ 1 file — 2 error, 0 warning, 0 info
Which tells you what the tick in §1 was worth. It was not a statement about the policy. It was a statement about the grammar: the prover found no two rules landing on one action, because the modifiers had already taken them apart. That is the inverse of the case where a qualifier you added to be helpful hides a prohibition from a brand new rule, and it has the same cause, seen from the other end.
A green check on a pair of rules means the pair was never in one slot. It does not mean the pair agrees.
§3The map: which words the prover follows
Not every modifier is invisible. One shape of narrowing is followed all the way through, and it happens to be the shape people reach for last. Six files, identical except for the second rule, each one a different way of saying except sometimes.
narrow/adjective.tg:6:7 error logic/modifier-contradiction Conflicts with line 4. Contradiction through a modifier: line 5's "never" rule and this "must" rule share one action (send a transcript), and the prohibition covers everything the obligation demands - both cannot hold. Drop one, or narrow the prohibition's scope. narrow/guard.tg:5:1 error logic/contradiction Conflicts with line 5. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). narrow/guard.tg:6:1 error logic/contradiction Conflicts with line 4. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). narrow/nothing.tg:5:1 error logic/contradiction Conflicts with line 5. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). narrow/nothing.tg:6:1 error logic/contradiction Conflicts with line 4. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ✗ 6 files — 5 error, 0 warning, 0 info program: 6 independent files — no $IMPORT compositions
adverb.tg says You MUST immediately send a transcript. dative.tg says You MUST send the caller a transcript. preposition.tg says You MUST send a transcript to a verified caller. None of the three appears above.Sort that by what the edit actually did and the pattern is the opposite of comforting.
- Nothing, and a one-sided guard, are both caught.
nothing.tgis the bare pair.guard.tgis IF the caller requests a copy THEN you MUST send a transcript, and it is the same twologic/contradictionerrors, which is right: a blanket prohibition applies inside the guarded case as well, so guarding one side changed nothing about whether both rules can hold. It is the failed fix everybody tries, and the only one of the six the checker argues with by name. - An adjective on the object is caught, on its own code. a short transcript is still a transcript, so the prohibition covers everything the obligation demands, and
logic/modifier-contradictionsays so at6:7, the column of the word short rather than the start of the line. It also quotes the shared action, send a transcript, which is the folded key and not either sentence, the same reporting habit that makes two rules with no words in common resolvable at all. - A preposition, a recipient and an adverb are silent. These are the three that change no policy whatsoever. Send the caller a transcript is the same act as send a transcript with the recipient moved to the front. Immediately says when, not whether. And to a verified caller is the one people believe is a real restriction, which it is in English and is not in the file: the frame changed, so the action landed in a slot the prohibition never occupies.
So the checker is strictest about the edit that genuinely narrows behaviour and quietest about the edits that only rearrange the sentence. Both halves of that are defensible. A frame is where an action's arguments live, and two actions with different arguments are, formally, different claims. It just means the shape of your exception decides whether anything downstream can referee it.
§4The fold has a direction
Before leaning on the adjective case, know which way round it works. The fold is not symmetric, and the asymmetry is the sound part.
direction/require-short.tg:6:7 error logic/modifier-contradiction Conflicts with line 4. Contradiction through a modifier: line 5's "never" rule and this "must" rule share one action (send a transcript), and the prohibition covers everything the obligation demands - both cannot hold. Drop one, or narrow the prohibition's scope. ✗ 3 files — 1 error, 0 warning, 0 info program: 3 independent files — no $IMPORT compositions
require-short.tg forbids transcripts and requires a short one: reported. forbid-short.tg flips the modifier over, forbidding a short transcript and requiring a transcript: silent, and correctly so, because a long one satisfies both rules at once. recommend-short.tg keeps the shape of the reported file and softens the obligation to SHOULD: also silent, because a recommendation does not oblige, so there is nothing to be impossible.Read those three together and the code has a precise meaning: it fires when the prohibition entails the negation of the obligation, not whenever two lines look like they are arguing. Flip the modifier and the entailment goes the other way, so there is a world that satisfies both. Soften the modal and one side stops making a demand at all, which is the same reason a bound written under a soft rule never reaches the solver.
§5Put the exception where it is structural
Which leaves the real question. If to a verified caller is a genuine restriction, and it usually is, how do you write it so that it is one in the file too? Not by narrowing one side. By making it impossible for both sides to apply at once, and there are two planes to do that on.
The prose plane is the one people reach for, and it is the weaker of the two: narrow the prohibition with the complement of whatever narrows the obligation, so no situation matches both. That works and it is entirely on you to get the complement right. The compiler plane is stronger, because it takes the decision away from the model before the file is ever sent.
plane/flat.tg:5:1 error logic/contradiction Conflicts with line 5. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). plane/flat.tg:6:1 error logic/contradiction Conflicts with line 4. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). plane/one-arm.tg:10:3 error logic/contradiction Conflicts with line 9. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). A single serve can hold both - e.g. when caller_verified = true. plane/one-arm.tg:11:3 error logic/contradiction Conflicts with line 8. Logical conflict — "send a transcript." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). A single serve can hold both - e.g. when caller_verified = true. ✗ 3 files — 4 error, 0 warning, 0 info program: 3 independent files — no $IMPORT compositions
$IF over a declared boolean, in arms.tg: absent from this report entirely, because the compiler keeps one arm and the two rules provably never co-occur. Both of them inside the same arm: 2 errors again, and this time the message names the assignment that makes it happen, when caller_verified = true.That third file is the one that settles it. Arms are not a hiding place: the prover follows the pair inside an arm and gains a witness, because a chain over a declared input tells it which worlds exist. Nothing else in this post got a witness. The narrowing that lives on the compile-time plane is the only narrowing here that made the checker more capable rather than less.
So the transcript policy gets rewritten around the condition that was doing the work all along, which the host knows before the first turn.
<$CONFIG> $REQUIRE variable caller_verified: boolean </$CONFIG> # Role @@ role: billing support for a mobile network You are a billing support agent for Cobalt Mobile. # Constraints @@ transcript_gate: identity decides who gets the account conversation, so the exception is an arm and not an adjective $IF @{caller_verified}: @@ verified: a verified caller already holds the account, so a copy adds no exposure You MUST send a transcript at most 5 minutes after the call ends. $ELSE: @@ unverified: no bound narrows this one, because there is no safe size of transcript for the wrong recipient You MUST NOT send a transcript. @@ redaction: the transcript quotes card digits back, so every copy is redacted whoever receives it - You MUST redact every payment card number in a transcript. $TEST unverified_request - input:: Can you email me a transcript of this call? - expect:: - contains "verify"
Then the two builds are the proof that the exception is real, which is a thing you can put in a pull request rather than argue about.
$ npx typeglish build transcripts-gated.tg --vars '{"caller_verified":true}' ✓ built .typeglish/dist/transcripts-gated.txt ← transcripts-gated.tg (9d2b4d23ba3d, full) # Role You are a billing support agent for Cobalt Mobile. # Constraints You MUST send a transcript at most 5 minutes after the call ends. - You MUST redact every payment card number in a transcript. $ npx typeglish build transcripts-gated.tg --vars '{"caller_verified":false}' ✓ built .typeglish/dist/transcripts-gated.txt ← transcripts-gated.tg (aad4178253a0, full) # Role You are a billing support agent for Cobalt Mobile. # Constraints You MUST NOT send a transcript. - You MUST redact every payment card number in a transcript.
$ npx typeglish test transcripts-gated.tg --dry ✓ transcripts-gated.tg coverage: 2/3 rules exercised · unverified_request — "Can you email me a transcript of this call?" (not run) ✓ contains "verify" ✓ 1 prompt — 0 failed
§6Common questions
- Why does my agent break a NEVER rule that has an exception somewhere else in the prompt?
- Because the exception and the prohibition are not about the same thing any more. A blanket NEVER send a transcript covers every case, including the one your exception carved out, so the two rules are in genuine conflict and the model resolves it per turn. If the exception was written with a preposition, a recipient or an adverb (send a transcript to a verified caller, send the caller a transcript, immediately send a transcript), the checker cannot join the two either: the pair reports 0 error, 0 warning, 0 info. The fix is to narrow both halves, not one, so that no single situation matches both rules.
- Does TypeGlish catch a contradiction when one rule has extra words in it?
- Sometimes, and the boundary is worth memorising. An adjective on the object keeps the action key, so NEVER send a transcript beside MUST send a short transcript is one blocking
logic/modifier-contradiction, reported at the column of the modifier. A prepositional phrase, a dative recipient or an adverb changes the frame instead, and all three check clean against the same prohibition. A one-sided guard is caught, correctly: IF the caller requests a copy THEN you MUST send a transcript against a blanketNEVERis still two blockinglogic/contradictionerrors, because the blanket rule applies inside the guarded case too. - How do I test whether the qualifier on my rule is real?
- Strip it and re-check. Copy the prohibition and the obligation into a file on their own, delete every modifier from both, and run
typeglish checkon the pair. If the bare pair is a proven contradiction, then your modifiers are the only thing holding the file together, and whether they hold is a question about English rather than a question the compiler answered. A three-rule transcript policy that checks at 0 error, 0 warning, 0 info becomes 2 blockinglogic/contradictionerrors under that one edit. - Should an exception be a prose IF rule or a $IF chain?
- Use the chain when the condition is something your host already knows before the first turn. A prose
IFis a statement the model weighs at runtime, so both halves reach the model and a blanket rule beside it still conflicts. A$IFchain over a declared input is resolved by the compiler: the opposite pair sits in opposite arms, exactly one arm reaches the artifact, and the two hashes prove it. The same pair flat in a file is 2 blocking errors, in opposite arms it is clean, and both rules inside one arm is 2 blocking errors again with the assignment named in the message.
The uncomfortable part of this one is that the silent cases are not bugs to wait out. A frame really is part of what an action is, and a checker that folded every prepositional phrase into its bare verb would report a contradiction between half the rules in a normal prompt. So the boundary stays roughly where it is, and the habit has to sit with you: when you soften a rule by adding words, go and read the rule you are softening it against, and ask whether any single turn could match both. If one could, the words you added were a description of the case you had in mind, not a restriction on anything. The three-word edit is still the right instinct. It just belongs on both lines, or in an arm.