← Tidelines/Deep dives

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.

by TypeGlish team8 min read#deep-dives
Three words, and the proof is gone.

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.

transcripts.tg - as inherited✗ B (85/100)
# 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.
Line 5 is the rule compliance asked for after a transcript went to the wrong mailbox. Line 6 is the rule the CX lead asked for a month later, softened with short because that felt safer. Line 7 is what support added when customers kept asking. Read as policy, the file forbids transcripts and requires them twice.
tg check transcripts.tg - output
 1 file — 0 error, 0 warning, 0 info
Nothing. Not a warning, not an info. This is a file whose second and third lines each require the exact action its first line forbids.

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.

tg score transcripts.tg - output
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.

stripped.tg - the same policy, no qualifiers✗ 2 error
# Role
You are a billing support agent for Cobalt Mobile.

# Constraints
- You NEVER send a transcript.
- You MUST send a transcript.
tg check stripped.tg - output
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
Two blocking errors, one per participating line, on a pair the checker was silent about thirty seconds ago. The action key is the whole finding: send a transcript is required on one line and forbidden on another, and the message quotes the key rather than either sentence.

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.

tg check narrow/ - six ways to write the exception
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
Every file carries You NEVER send a transcript. and one second rule. Three are reported and three are silent. 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.tg is the bare pair. guard.tg is IF the caller requests a copy THEN you MUST send a transcript, and it is the same two logic/contradiction errors, 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-contradiction says so at 6: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.

tg check direction/ - three files, one finding
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.

tg check plane/ - the same opposite pair, three placements
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
The identical pair, MUST send a transcript and MUST NOT send a transcript, in three places. Flat in the file: 2 errors. In opposite arms of a $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.

transcripts-gated.tg - the exception as an arm✓ A (98/100)
<$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"
0 error, 0 warning, 0 info at A (98/100), runtime 97 and hygiene 100. Three policy decisions became visible in the process of writing it this way: the transcript is gated on identity rather than on length, the unverified branch gets no bound because no size of transcript is safe for the wrong recipient, and redaction applies to every copy so it sits outside the chain.

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.

tg build transcripts-gated.tg --vars - both branches
$ 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.
Two artifacts, two hashes, and exactly one of the two transcript rules in each. The model is never asked to work out which rule wins, because the losing arm is not in the document it receives. Compare the original file, where all three rules shipped together and the tick said they were fine.
tg test transcripts-gated.tg --dry - output
$ 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
One deterministic assert, settled with zero model calls: the unverified case has to talk about verifying rather than send anything. It is worth writing even though the arms already prove the exclusion, because the arms prove which rules ship and the test is the only thing that says what the agent does when it declines.

§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 blanket NEVER is still two blocking logic/contradiction errors, 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 check on 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 blocking logic/contradiction errors 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 IF is a statement the model weighs at runtime, so both halves reach the model and a blanket rule beside it still conflicts. A $IF chain 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.
Field note

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.

∿ washed up Aug 16, 2026 ∿