← Tidelines/Deep dives

Your prompt says it. The model picks one.

Every rule in an agent prompt is about a thing. Most of them do not say which thing, and the sentence still reads perfectly, which is the whole problem.

by TypeGlish team7 min read#deep-dives
Cancel it. Cancel which one?

TL;DR An agent prompt is full of objects named by a pronoun or a bare the, and TypeGlish has three findings for it: prompt/dangling-reference, prompt/ambiguous-reference and prompt/unintroduced-definite. Every one is advisory, none blocks a build, check --strict escalates none of them, and one persona line is enough to silence the strictest. Naming the object is your job: declare it as a typed input and write the rule over @{order_id}, so one identifier reaches the model wherever the rule mentions it.

Read the rule table of any support agent that has been live for a year and count the objects it names. Close it. Cancel the order. Tell them the reference number. Each of those sentences is about one specific thing in the world, and not one of them says which thing. That works, because the customer just said which order they meant and the transcript is sitting right there. It works until the customer has two.

The reason it survives review is that a reference is invisible to the reading you do. Nobody scans a prompt asking which order, because when you read the rule you already have one in mind. The model has the same experience, and no way to notice it is guessing.

§1The rule that does not say which order

Here is a returns prompt for a mobile network. Four content lines, all four the kind of thing that would pass a review, because each one is a real policy stated in a full sentence.

returns.tg - as inherited✗ C (72/100)
# Role
You are a returns agent for Cobalt Mobile.

# Constraints
- You MUST greet the customer by name.
- A customer may have two open orders.
- You MUST cancel the order within 24 hours.
- You MUST tell the customer the reference number.
Line 6 is context somebody added after a bad call. Line 7 is the SLA. Line 8 is the wrap-up rule. Read them in order and you can feel the reference slide: line 6 says there may be two orders, and line 7 says the order.
tg check returns.tg - output
returns.tg:7:1  warn   prompt/ambiguous-reference  Ambiguous reference — "the order" points at ONE order, but a claim proves there are 2 or more. Which one? (with line 5)
returns.tg:8:1  info   prompt/unintroduced-definite  "the reference number" retrieves something this document never introduces - a model must guess which number is meant. Introduce it on another line ("You manage a number.") or name it outright.

 1 file — 0 error, 1 warning, 1 info
Two findings on the two rules that name an object, and a tick. Note the two numbering schemes: 7:1 is the true file line of the cancel rule, while the line 5 inside the message counts content lines with the blanks stripped, which is the count claim on file line 6. The unintroduced-definite half of this is the noun problem we ran separately; what is new here is the line above it.

The score is where the difference in weight shows up. One of these findings costs four times what the other does.

tg score returns.tg - output
returns.tg — C (72/100)  proven errors: none  tiers: base+z3
  planes  runtime 79 (what the model reads) · hygiene 50 (source only)
  facets  enforceability 77 x.21 · hardness 75 x.12 · directness 84 x.08 · consistency 100 x.17
          structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 25 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).
  L7  prompt/ambiguous-reference   −1  Ambiguous reference — "the order" points at ONE order, but a claim proves ther
  L8  prompt/unintroduced-definite −0.25  "the reference number" retrieves something this document never introduces - a 
consistency 100, because nothing in this file contradicts anything. enforceability 77, because the rules do name actions and bounds. The ledger is the honest part: the ambiguous reference is a full point and the unintroduced noun is a quarter, which is roughly the ratio of how much each one can cost you in production.

§2The proof needs a claim to work with

Why did that warning fire at all? Not because the order is vague. It fired because line 6 is a claim the prover can hold: a customer may have two open orders puts a cardinality on the set, and a singular definite over a set of two or more is a provable mismatch rather than a stylistic complaint. Take the claim away and the finding goes with it, even though the prompt has become less specific, not more.

tg check claims/ - three files, one claim apart
claims/two-orders.tg:4:1  warn   prompt/ambiguous-reference  Ambiguous reference — "the order" points at ONE order, but a claim proves there are 2 or more. Which one? (with line 3)

 3 files — 0 error, 1 warning, 0 info
program: 3 independent files — no $IMPORT compositions, nothing co-checked
Three files, same shape, same cancel rule. two-orders.tg says A customer may have two open orders. and is flagged. many-orders.tg says An account holds many orders. and is clean. two-words.tg says A customer may have two escalation paths. beside follow the escalation path and is clean, because the head is two words.

Both of those misses are worth knowing before you lean on the finding. The bound has to be countable in the shape the prover reads, so two works where many does not. And the definite has to be a single-word head: the order resolves, the escalation path does not, which matters because contact-centre nouns are compound more often than not. Wrap a multi-word object in a %name literal% and it becomes one name the compiler can bind, which is the same fix that makes a jargon term resolvable.

The warning is not about the ambiguity. It is about the one sentence in your file that happened to prove it.

That inverts the usual reading of a green check. A prompt that never states how many orders a customer can have is a prompt where this finding cannot fire, and almost no prompt states it. The absence of the warning is evidence about your file, not about your references.

§3The floor is one persona line high

The pro-form finding has the same property in a sharper form. prompt/dangling-reference is the one that catches a bare it or them, and it fires only when the pro-form has nothing at all in the document to bind to. Anything counts, including the noun in your role line.

tg check pair/ - the same two rules, one line apart
pair/no-persona.tg:2:1  warn   prompt/dangling-reference  "it" points at nothing the prompt has introduced - a model will guess the referent. Name the target, or introduce it on an earlier line.

 2 files — 0 error, 1 warning, 0 info
program: 2 independent files — no $IMPORT compositions, nothing co-checked
Both files carry the identical pair of rules: You MUST close it within 24 hours. then You MUST send a confirmation. with-persona.tg adds # Role and You are a returns agent for Cobalt Mobile. above them, and that is the whole diff. The persona line introduces a noun, the noun satisfies it, and the warning is gone.

Which means that on any prompt shaped the way prompts are actually shaped, this finding will not fire. It is a real discourse check and it is doing what it says: it proves a pro-form has an antecedent. It never claims the antecedent is the right one, and it cannot, because there is nothing in a system prompt to check that against.

The last hope would be the strict gate, since check --strict escalates every IMPORTANT finding to a blocking error and is the flag you put in CI when you want to stop tolerating advisories. It does not reach this plane.

tg check returns.tg - with and without --strict
$ npx typeglish check returns.tg
 1 file — 0 error, 1 warning, 1 info
$ echo $?
0

$ npx typeglish check returns.tg --strict
 1 file — 0 error, 1 warning, 1 info
$ echo $?
0
Byte-identical output and the same exit code. All three reference findings are advisory rather than IMPORTANT, so strict has nothing to promote. Compare a hedge or an unused variable, which strict does turn into a blocking error on a file nobody edited.

So there is no gate to build here. That is not a gap to complain about, it is the shape of the problem: whether the order means the one the customer asked about is a fact about your runtime, and no amount of reading the prompt can settle it. What the prompt can do is stop asking.

§4Name it, and point at the name

The rewrite keeps every policy and changes only how each rule refers to its object. Whatever the host already resolved becomes a declared input; every rule points at the declaration instead of describing the thing.

returns-named.tg - the same policy, with referents✓ A (97/100)
<$CONFIG>
  $REQUIRE variable order_id: string
  $IMPORT tool cancel_order
</$CONFIG>

# Role
@@ role: returns handling for a mobile network
You are a returns agent for Cobalt Mobile.

# Constraints
@@ greeting: the name is on the contact record, so using it costs nothing
- You MUST greet the customer by name.
@@ one_order: a customer may hold several open orders, so every rule names the one in hand
- You MUST cancel order @{order_id} within 24 hours.
@@ reference: the agent quotes the id it was handed, never one it composed
- You MUST quote order @{order_id} in at most 1 sentence.
@@ no_guessing: with several orders open, the wrong one is one turn away
- You NEVER cancel an order that @[cancel_order] did not return.

$TEST which_order
  - input:: Please cancel order CM-40182.
  - expect::
    - contains "CM-40182"
0 error, 0 warning, 0 info at A (97/100), runtime 96 and hygiene 100. C (72/100) to A (97/100), and the policy is the same policy: cancel within 24 hours, quote the reference, greet by name.

Three things moved, and each one turns a description into a referent.

  • The object became a declared input. $REQUIRE variable order_id: string says the host resolves which order before the first turn, which is true and was always true. Now the rules say it too. Delete the declaration and @{order_id} is a dangling pointer and a compile error, so the reference is load-bearing rather than decorative.
  • The count became an annotation. A customer may have two open orders was doing two jobs badly: telling the model something it could not act on, and giving the prover its only handle. As @@ one_order it documents why the rule is written that way, costs zero runtime bytes, and takes annotation from 0 to 100. The prover does not need it any more, because there is nothing left to disambiguate.
  • The reference got a provenance. NEVER cancel an order that @[cancel_order] did not return is the rule the original file was missing entirely. Naming the object is only half of it; the other half is saying where a legitimate value comes from. That is the same move as conditioning on the missing fact rather than on the model's confidence, applied to identity instead of certainty.

Then read the artifact, because the artifact is the only text the model sees.

tg build returns-named.tg - the artifact
$ npx typeglish build returns-named.tg
 built .typeglish/dist/returns-named.txt ← returns-named.tg (7a5febaa394f, full)

# Role
You are a returns agent for Cobalt Mobile.

# Constraints
- You MUST greet the customer by name.
- You MUST cancel order {order_id} within 24 hours.
- You MUST quote order {order_id} in at most 1 sentence.
- You NEVER cancel an order that cancel_order did not return.
The pointer compiles to {order_id}, a runtime hole the host fills per conversation, and it appears in both rules that are about that order. The tool pointer flattens to the bare name cancel_order. The annotations and the $TEST are gone. Three rules, one identifier, no pronouns.

The last line of that artifact is the one worth staring at. Before the rewrite, two rules said the order and a third said them, and nothing tied any of the three to each other. After it, the same string appears in both rules that act on the order, so a mismatch is a diff rather than an interpretation. The test pins it offline.

tg test returns-named.tg --dry - output
$ npx typeglish test returns-named.tg --dry
 returns-named.tg  coverage: 2/4 rules exercised
  · which_order — "Please cancel order CM-40182." (not run)
       contains "CM-40182"
 1 prompt — 0 failed
A deterministic assert, validated with zero model calls: the turn carries an id and the reply has to echo it back. coverage: 2/4 is the honest number, since one case cannot reach four rules. The point is that the assertion is expressible at all, which it was not while the rule said the order.

§5Common questions

Why does my AI agent act on the wrong order or the wrong ticket?
Usually because the rule never said which one. A rule like MUST cancel the order within 24 hours names a category, not an object, so at runtime the model resolves it against the transcript, and the transcript is whatever the customer happened to mention most recently. That resolution is not in your prompt and not in your tests. Declare the object as a typed input ($REQUIRE variable order_id: string) and write the rule over the pointer (MUST cancel order @{order_id} within 24 hours), so the identifier the host resolved is the identifier the rule is about.
Does TypeGlish catch pronouns with no antecedent in a system prompt?
Yes, but the trigger is narrower than it sounds. prompt/dangling-reference fires only when a pro-form has nothing at all in the document to bind to. Any noun anywhere satisfies it, including the noun in your persona line, so two files with identical rules differ by exactly one warning when one of them has a # Role section: the file without a persona is 1 warning, the file with one is clean. On a real prompt with a role line, this finding will essentially never fire.
Will the checker block a build over an ambiguous reference?
No. All three reference findings are advisory: prompt/dangling-reference and prompt/ambiguous-reference are warnings, prompt/unintroduced-definite is info, and none of them counts as a blocking error. check --strict does not escalate them either, so the same file reports 0 error, 1 warning, 1 info and exit 0 with and without the flag. The only instrument that moves is the score, where an ambiguous reference costs a full point on the ledger.
How do I write agent rules that name their object?
Three moves. Declare what the host already knows as a typed input and reference it with @{name}, so one identifier reaches the model wherever the rule mentions it. Put the count that used to be a claim into an @@ annotation, since the prover does not need it once the reference is pinned. And write the source of truth into the rule itself, as in NEVER cancel an order that @[cancel_order] did not return, so the object has a provenance and not just a name. That rewrite took a returns prompt from C (72/100) to A (97/100) without changing a single policy.
Field note

The reference plane is the widest gap between what a checker can prove and what a prompt has to get right, and that is worth being blunt about rather than dressing up. A prover can hold a contradiction because both halves are in the file. It cannot hold which order, because the answer is in a transcript that does not exist yet. So this one is not a gate you can install, it is a habit: every time a rule says it, them, this, or a bare the, ask whether the host already knows the answer. If it does, that answer belongs in the file as a name. If it does not, you have found a rule that was never going to work, and it looks exactly like the ones that do.

∿ washed up Aug 10, 2026 ∿