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.
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.
# 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.
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
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.
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.
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
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.
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
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.
$ 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
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.
<$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"
Three things moved, and each one turns a description into a referent.
- The object became a declared input.
$REQUIRE variable order_id: stringsays 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_orderit documents why the rule is written that way, costs zero runtime bytes, and takesannotationfrom 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.
$ 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.
{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.
$ 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
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-referencefires 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# Rolesection: 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-referenceandprompt/ambiguous-referenceare warnings,prompt/unintroduced-definiteis info, and none of them counts as a blocking error.check --strictdoes 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.
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.