You retired the plan. The prompt still sells it.
Platinum closed to new customers in March and was withdrawn in July. The support prompt has four rules about it, and exactly one of them is something a compiler can find.
TL;DR A retirement is only provable where the retired thing was a typed value: drop platinum from $REQUIRE variable plan: one of essential, complete, platinum and the arm that tested for it is a blocking structure/impossible-case that refuses to build, while three prose rules about the same dead plan are 0 error, 0 warning, 0 info before and after, hold B (82/100) on every facet, and ship in the artifact. Move every plan rule inside a $SWITCH ON @{plan} arm and the retirement locks in both directions.
Products leave. A plan gets closed to new customers, runs off for a quarter, and then goes. Pricing knows, billing knows, the CRM knows, and the entitlement flag stops arriving. The system prompt is the one artefact in the estate that has no idea, because nothing about withdrawing a product touches the document that tells an agent how to talk about it.
This is not the interesting part. The interesting part is that a checker can prove some of those leftover rules are dead and provably cannot prove it about the rest, and the line between the two has nothing to do with how important the rule is. It is drawn by whether the plan was ever a value the file declared.
§1Four rules about a product that no longer exists
Kestrel Fibre sells three broadband plans, and the chat support prompt gates the engineer-visit promise on which one the caller is on. The host sends the plan as a typed input, so the compiler knows the set.
<$CONFIG> $REQUIRE variable plan: one of essential, complete, platinum </$CONFIG> # Role You are a broadband support agent for Kestrel Fibre. # Constraints - You MUST keep every reply to at most 4 sentences. - IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee. - WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile. - You MUST offer a Platinum upgrade to any customer who mentions buffering. $IF @{plan} is equal to platinum: You MUST book a same-day engineer visit. $ELSE IF @{plan} is equal to complete: You MUST book a next-day engineer visit. $ELSE: You MUST book the next available engineer visit.
$ npx typeglish check plans.tg
✓ 1 file — 0 error, 0 warning, 0 info
plans.tg — B (82/100) proven errors: none tiers: base+z3
planes runtime 93 (what the model reads) · hygiene 50 (source only)
facets enforceability 83 x.21 · hardness 86 x.12 · directness 96 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).
@@ notes. Hold on to this number: it is the last honest reading the file will produce.§2One word out of the domain
July arrives. Platinum is gone, the entitlement stops being sent, and somebody does the tidy thing and takes the member out of the declared set. That is the whole diff: one word, on line 2.
<$CONFIG> $REQUIRE variable plan: one of essential, complete </$CONFIG> # Role You are a broadband support agent for Kestrel Fibre. # Constraints - You MUST keep every reply to at most 4 sentences. - IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee. - WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile. - You MUST offer a Platinum upgrade to any customer who mentions buffering. $IF @{plan} is equal to platinum: You MUST book a same-day engineer visit. $ELSE IF @{plan} is equal to complete: You MUST book a next-day engineer visit. $ELSE: You MUST book the next available engineer visit.
, platinum. Nothing else in the file moved: the three prose rules and all three chain arms are byte-identical to §1.$ npx typeglish check retired.tg retired.tg:13:1 error structure/impossible-case @{plan} is never platinum (it's one of essential, complete). ✗ 1 file — 1 error, 0 warning, 0 info $ npx typeglish build retired.tg typeglish build: retired.tg refused — nothing written retired.tg:13:1 error structure/impossible-case @{plan} is never platinum (it's one of essential, complete). ✗ 1 file — 1 error, 0 warning, 0 info
That error is the good news, and it is worth saying plainly what it is: the compiler has found a site in your prompt that still believes in a product you withdrew, and it will not let you ship until somebody looks. This is the same machinery that catches a member misspelled in a tier gate and a member added to a domain nobody re-read. Retirement is the third direction, and it is the one nobody plans for, because adding a plan is a project and removing one is a cleanup ticket.
§3The green file that still sells it
Now watch what the fix does. There is one error, it names one line, and the obvious repair is to delete the arm that can no longer fire. Two lines out, and the file is clean.
<$CONFIG> $REQUIRE variable plan: one of essential, complete </$CONFIG> # Role You are a broadband support agent for Kestrel Fibre. # Constraints - You MUST keep every reply to at most 4 sentences. - IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee. - WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile. - You MUST offer a Platinum upgrade to any customer who mentions buffering. $IF @{plan} is equal to complete: You MUST book a next-day engineer visit. $ELSE: You MUST book the next available engineer visit.
The build is the part to put in front of whoever asked for the retirement, because it is the only document in the loop that is about what the agent will actually be told.
$ npx typeglish build cleared.tg --vars '{"plan":"essential"}' ✓ built .typeglish/dist/cleared.txt ← cleared.tg (b99007c84615, full) # Role You are a broadband support agent for Kestrel Fibre. # Constraints - You MUST keep every reply to at most 4 sentences. - IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee. - WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile. - You MUST offer a Platinum upgrade to any customer who mentions buffering. You MUST book the next available engineer visit.
essential customer is instructed to waive a callout fee on a plan that no longer exists, to quote a 900 Mbps profile nobody is provisioned on, and to upsell the withdrawn product to anybody who mentions buffering. The chain resolved correctly. The prose did not resolve at all, because it was never a chain.The one rule the compiler could see is the one rule that got fixed. That is not a coincidence, it is the mechanism.
§4Why three of the four were unreachable
A closed domain is a promise about which values can occur. Once plan is one of essential, complete, every comparison against it is a claim that can be settled: is equal to platinum is decidable and the answer is no. That is the whole of what happened on line 13.
The other three rules never made a claim of that kind. IF the customer is on Platinum is a guard the model weighs at runtime against whatever it thinks it knows about the conversation, and Platinum there is a noun in a sentence. It is not a member of a set, so there is no set to check it against, so there is nothing to prove. The checker is not missing the rules. It has never been told they are about the same thing.
- The machine plane is where retirement is checkable. A typed input, a
$IFcondition, a$SWITCHarm, a<tag when=…>guard. These are read by the compiler, so a value that leaves the domain leaves an error behind at every site that referenced it. - The prose plane is where retirement is invisible. Anything that reads as an instruction is passed through, and a passed-through rule about a dead product is still a live instruction. This is the same split that lets a compile-time value and a runtime one sit in one file looking identical.
- Neither the score nor the count moves. B (82/100) before, B (82/100) after, and the counts are 0 / 0 / 0 in both. There is no facet for rules about things that no longer exist, and there could not be one: the compiler has no way to know Platinum went.
- Deleting is the direction with no gate. A rule added to a prompt gets reviewed, because somebody wrote it this week. A rule that should have been deleted has no author and no ticket, which is why a deletion nobody notices and a deletion nobody performs fail the same way.
§5Put the plan back inside the domain
So the fix is not a better search for the word Platinum. It is to stop writing plan rules as sentences about plans and start writing them as arms of the plan. A $SWITCH ON block is the strongest form of that, because it takes members of one typed domain and proves coverage by construction.
<$CONFIG> $REQUIRE variable plan: one of essential, complete, platinum </$CONFIG> # Role @@ role: inbound broadband support, plan-gated on the entitlement the host sends You are a broadband support agent for Kestrel Fibre. # Constraints @@ brevity: four sentences keeps a chat reply scannable - You MUST keep every reply to at most 4 sentences. @@ visits: every plan rule lives in an arm, so retiring a plan is one compile error here $SWITCH ON @{plan} - essential:: @@ essential_visit: no visit SLA is sold on the entry plan You MUST book the next available engineer visit. @@ upsell: buffering is the one complaint the next plan up actually answers IF the customer mentions buffering THEN you MUST offer a Complete upgrade. - complete:: @@ complete_visit: next-day is the sold SLA You MUST book a next-day engineer visit. - platinum:: @@ platinum_visit: same-day is the sold SLA You MUST book a same-day engineer visit. @@ platinum_fee: the waived callout fee is the paid-for part of the plan You MUST NOT charge an engineer callout fee. @@ platinum_speed: 900 Mbps is the only profile provisioned on this plan IF the customer asks about speed THEN you MUST quote the 900 Mbps profile. $TEST platinum_callout - input:: My line is down again and I am not paying another callout fee. - expect:: - contains "no callout fee"
enforceability 68 named as the remaining lever. No policy changed and no rule reworded. Every sentence that used to say Platinum now sits under the member instead of naming it.Then the retirement has two locks on it, and they close from opposite sides.
# 1. take the member out of the domain, leave the arm alone fixed-retired.tg:22:3 error structure/impossible-case "platinum" is not a member of @{plan}'s domain — one of essential, complete. ✗ 1 file — 1 error, 0 warning, 0 info # 2. delete the arm, leave the domain alone fixed-armless.tg:13:1 error structure/non-exhaustive-switch @{plan} can be platinum, but no arm handles it — add a "- <member>::" row for each (or a deliberate "- otherwise::" fallback). ✗ 1 file — 1 error, 0 warning, 0 info
And the payoff is the thing you can hand to the person who has to sign off that nothing else changed.
# Platinum still in the domain, arm still present $ npx typeglish build fixed.tg --vars '{"plan":"essential"}' ✓ built .typeglish/dist/fixed.txt ← fixed.tg (ad17a800993a, full) # Platinum out of the domain, arm deleted: the two-line retirement $ npx typeglish build retired-properly.tg --vars '{"plan":"essential"}' ✓ built .typeglish/dist/retired-properly.txt ← retired-properly.tg (ad17a800993a, full) # Role You are a broadband support agent for Kestrel Fibre. # Constraints - You MUST keep every reply to at most 4 sentences. You MUST book the next available engineer visit. IF the customer mentions buffering THEN you MUST offer a Complete upgrade.
ad17a800993a, before and after. Retiring the plan is provably a no-op for every customer who was not on it, and the word Platinum is not in the deployed prompt at all. The retired-properly file still checks 0 error, 0 warning, 0 info at A (93/100).$ npx typeglish test fixed.tg --dry ✓ fixed.tg coverage: 1/7 rules exercised · platinum_callout — "My line is down again and I am not paying another callout fe" (not run) ✓ contains "no callout fee" ✓ 1 prompt — 0 failed
§6Common questions
- Why does my AI agent still offer a product we discontinued?
- Because withdrawing a product removes it from your price list and from nothing else. A support prompt with four rules about a retired broadband plan checks at 0 error, 0 warning, 0 info and scores B (82/100), and the three of those rules written in English keep checking clean after the plan is gone, because a prose rule is an instruction to the model rather than a claim about a declared set. The build proves it: with the plan retired and the file green, the artifact still reads IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee, WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile, and You MUST offer a Platinum upgrade to any customer who mentions buffering.
- Does TypeGlish catch a rule about a plan that no longer exists?
- Only where the plan was a member of a typed domain. Remove
platinumfrom$REQUIRE variable plan: one of essential, complete, platinumand the chain arm that tested for it is a blockingstructure/impossible-casereading@{plan} is never platinum (it's one of essential, complete), andtypeglish buildanswers retired.tg refused, nothing written at exit 1. The three prose rules naming Platinum in the same file produce no diagnostic at all, before or after, and the grade does not move: B (82/100) with runtime 93 and consistency 100 on both files. - How do I remove a retired product from an agent system prompt safely?
- Put every rule about the product inside an arm of a
$SWITCH ONover the typed input, so the compiler holds the domain and the rules together. Then the retirement locks in both directions: takeplatinumout of the domain and the arm is a blockingstructure/impossible-case, delete the arm and leave the domain alone and it is a blockingstructure/non-exhaustive-switchreading@{plan} can be platinum, but no arm handles it. Doing both is the two-line diff that actually retires a plan, and it is provably safe for everybody else: theessentialbuild lands onad17a800993abefore the retirement andad17a800993aafter it. - Why did removing one word from my prompt break the build?
- Because the word was a member of a closed set and something still compared against it. A domain is a promise about which values can occur, so an arm testing for a value the domain excludes can never fire, and an arm that can never fire is dead code rather than a harmless leftover. That error is the useful half of a retirement: it is the compiler naming every site that still believes in the thing you withdrew. The rules it cannot name are the ones written as English about the product, and those are the ones that keep reaching the model.
The habit worth taking from this is about where a fact lives rather than how it is worded. Anything in your product catalogue that a rule depends on is a closed set, and a closed set belongs in the file as one, declared once, with the rules hanging off its members. Then the catalogue and the prompt cannot drift, because the compiler holds them together and says so at exit 1. Write the same rules as sentences and you have made your prompt a place where retired products go to keep working: no error, no warning, no score movement, and an agent cheerfully upselling something the billing system would refuse to provision. The four rules in this post were written by four different people over two years, and every one of them was correct on the day it landed. That is the ordinary way a prompt becomes wrong.