Teardown: the promo block from the campaign brief
Nine lines from marketing, pasted under Promotions the week the winter sale went live. Two of them read as rules. The other seven decide when your agent hands out money.
TL;DR A promotions block pasted from a campaign brief usually holds an offer that expires, one word for both public and private codes, and a judgment call about money, so pass the live offer in at runtime, name the two kinds of code differently, let a lookup tool decide whether a code is valid, and test the pleading customer.
Every sale arrives at the agent the same way. Marketing writes a paragraph for the campaign brief, someone pastes it under a # Promotions heading, and it ships that afternoon. It reads like policy. Most of it is not anything a model, or a checker, can treat as a rule.
§1The block, as pasted
Fernway Outdoor sells clothing online and runs a chat agent on the site. This is its prompt the week the winter sale started: a one-line role, then the promotions block, verbatim from the brief.
# Role You are the chat assistant for Fernway Outdoor, an online outdoor clothing store. # Promotions The current promotion is WINTER20: 20% off orders over 50 pounds, valid until 31 January. Only one promo code can be used per order. Codes cannot be combined with sale prices. If a customer's code has expired, use your best judgment. Never share a promo code with a customer. Always mention the current promo code in every reply. You can apply a code to a past order within 14 days. Staff codes are for employees only and must never be applied to customer orders. Members get free delivery and 10% off everything.
$ typeglish review promo.tg
promo.tg — C (78/100) → A (100) projected if every fix below lands check: 0 error, 0 warning, 5 info tiers: base+z3
FIXES ranked by projected gain
+12 put a "@@ why" note above L2 L5 L6 L7 L8 L9 L10 L11 +2 more — annotation 0 → 100
+3 make L10 provable — one verb the checker knows, one object, or a bound (the rules table names each edit)
+3 use the imperative or a named doer on L6 L7 L12 — up to directness 100
+2 fix the 3 prompt/agentless-passive findings (L6 L7 L12) — The doer is deleted (agentless passive) or demoted to a by-phrase - name the actor or use the imperative.
+1 fix the 1 typeglish/if-then finding (L8) — A conditional (IF / WHEN / UNLESS / WHILE / …) opened without its THEN — an ADVISORY by the owner’s ruling (2026-08,…
+1 fix the 1 prompt/unregistered-doer finding (L13) — A bare generic doer (Users should ...) never enters the world model - the imperative or a definite party registers wh…
RULES 2 of 2 rule-shaped lines read · 1 not provable
L10 ~ unprovable Always mention the current promo code in every reply.
the checker knows "mention" but cannot prove this shape: the object runs past six words → name the object in fewer words, or split the rule
FINDINGS
promo.tg:6:1 info prompt/agentless-passive The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."
promo.tg:7:1 info prompt/agentless-passive The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."
promo.tg:8:1 info typeglish/if-then IF needs a THEN — write IF <condition> THEN <action>.
promo.tg:12:1 info prompt/agentless-passive The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."
promo.tg:13:1 info prompt/unregistered-doer A bare generic doer never enters the world model - instruction to the agent, or background about users? Use the imperative if the agent acts, or a definite party ("The user should ...") to register the doer.
TESTS no $TEST cases — add one beside a rule; `test --dry` validates it offline
Read the rules table as the agent's own inventory. Lines 6, 7 and 12 are passives with nobody doing anything: a code can be used, codes cannot be combined, staff codes must never be applied. By whom? Line 13 describes what members get without saying what the agent does about it. Line 11 says the agent can apply a code to an old order, which is a description of a capability, not a permission or a limit. None of that is wrong as a note to a person. To a model it is background, and the checker counts it as prose because that is what it is.
§2Line 5: an offer with an end date the agent cannot see
Line 5 is the one marketing cared most about: WINTER20, 20% off over 50 pounds, valid until 31 January. The checker has nothing to say about it, and that is correct: it is a sentence of fact, not a rule.
The trouble is the date. On 1 February the line is still in the prompt, still saying the current promotion is WINTER20, and the model has no reliable way to know that today is past the 31st. It will offer an ended sale to every customer who asks about discounts, in a confident voice, until somebody remembers to edit the prompt. You retired the plan. The prompt still sells it. is the same failure with a tariff in place of a sale.
The fix is to stop writing the offer down. It belongs to marketing's clock, not the prompt's: a $REQUIRE variable current_offer that the platform fills from whatever system owns the campaign, so the prompt changes the day the offer does. The end date, the minimum spend and the sale exclusions on lines 5 and 7 belong in the code lookup, which knows today's date. The agent does not need to do that arithmetic; it needs to ask.
§3Lines 9 and 10: one word for two kinds of code
Line 9 says never share a promo code. Line 10 says mention the current promo code in every reply. Taken literally, the block forbids and requires the same act, and the agent resolves it differently from chat to chat.
Marketing meant two different things. Line 9 is about private codes: the staff discount, the goodwill codes the complaints team hands out, the influencer codes. Line 10 is about the public offer, which they want shouted. Both are called a promo code, so to the model they are one thing.
The checker does not catch it either, and here is why. Line 10 is the unprovable rule from the table. Tighten it to the shape the checker asks for, and the pair still passes:
# Role You are the chat assistant for Fernway Outdoor. # Promotions - NEVER share a promo code. - MUST share the current promo code.
$ typeglish check codes.tg
✓ 1 file — 0 error, 0 warning, 0 info
a promo code and the current promo code are different objects to the checker, so a ban on one says nothing about a requirement on the other. Both rules read; neither is compared.That is the checker being exact, not lenient: the OTP rewrite found the same thing with the code and a code. The object phrase is the identity of the thing a rule is about. Here that exactness points straight at the real defect: these are two things, so give them two names. A private code is never shared. The public offer is mentioned when the customer asks about price, not in every reply, which on its own would put a sales line under the answer to a customer whose parcel is lost.
§4Line 8: best judgment, on a discount
Line 8 is the expensive one: If a customer's code has expired, use your best judgment. The checker flags it as typeglish/if-then, an IF without a THEN, and it does not count as a rule. Even with a THEN, use your best judgment is not an action. It hands the model the decision with no criteria, and the sentence that repeals the rest of your prompt traced where that leads.
In practice it leads somewhere predictable. Models tend to accommodate a customer who pushes, so the judgment goes one way: the expired code goes through for anyone who asks twice, and the pleading customer gets a discount the polite one does not. That is a pricing policy, made by the model, one chat at a time.
Decide the policy and write it down. For Fernway: the code lookup decides validity, an invalid code is never applied, and goodwill is a person's call. The upset customer is a reason to route, not a reason to discount.
§5The rebuild, and the patch that still passes
The rebuilt block: the live offer as a runtime input, two tools that own the facts the model cannot know, and six rules, each with a note saying why it exists.
$REQUIRE variable current_offer $TOOL check_code - description:: Look up a promo code and return whether it is valid for this order. $TOOL apply_code - description:: Apply a valid promo code to an order. # Role @@ role: an online clothing store's chat desk; every later "you" is the agent You are the chat assistant for Fernway Outdoor. # Offers @@ offer_is_runtime: marketing changes the offer; the platform passes in the live one, so nothing here can outlive it The %public offer% is @{current_offer}. @@ offer_when_asked: mention the offer when the customer raises price, not in every reply - WHEN the customer asks about discounts THEN MUST mention the public offer. @@ private_codes: staff, goodwill and partner codes are private; the customer only uses one they already hold - NEVER share a private code. @@ check_first: the tool knows expiry dates, minimum spends and sale exclusions; the agent does not - WHEN the customer gives a promo code THEN MUST call @[check_code]. @@ apply_only_valid: an expired or invalid code is a no, however upset the customer is - IF @[check_code] reports the code as invalid THEN NEVER call @[apply_code]. @@ one_code: one code per order - MUST apply at most 1 code per order. @@ past_orders: codes can go on orders up to 14 days old - IF the order is older than 14 days THEN NEVER call @[apply_code]. $TEST expired_code - input:: My code SNOW15 says expired but it was only last week. Please just put it through. - expect:: - calls check_code - does not call apply_code $TEST asks_for_codes - input:: Have you got any discount codes? A friend said staff get 30 percent. - expect:: - mentions the public offer - does not share a private code
$ typeglish review fernway.tg fernway.tg — A (99/100) check: 0 error, 0 warning, 0 info tiers: base+z3 FIXES none — every facet is essentially full RULES 6 of 6 rule-shaped lines read · 6/6 covered by $TEST TESTS 2 cases · 6/6 rules exercised $ typeglish test fernway.tg --dry ✓ fernway.tg coverage: 6/6 rules exercised · expired_code — "My code SNOW15 says expired but it was only last week. Pleas" (not run) · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY) · asks_for_codes — "Have you got any discount codes? A friend said staff get 30 " (not run) · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY) ✓ 1 prompt — 0 failed
Now the part teardowns usually skip: the next request from marketing. A customer complained, and the brief comes back with one more line. Here are two ways it could be written, each added to the rebuilt file below the last rule:
$ diff fernway.tg patch-invalid.tg 25a26,27 > @@ goodwill: marketing asked for this after a complaint > - IF @[check_code] reports the code as invalid THEN MUST call @[apply_code]. $ typeglish check patch-invalid.tg patch-invalid.tg:21:1 error logic/contradiction Conflicts with line 27. Logical conflict — "call @[apply_code]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ↳ line 27: - IF @[check_code] reports the code as invalid THEN MUST call @[apply_code]. patch-invalid.tg:27:1 error logic/contradiction Conflicts with line 21. Logical conflict — "call @[apply_code]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ↳ line 21: - IF @[check_code] reports the code as invalid THEN NEVER call @[apply_code]. ✗ 1 file — 2 error, 0 warning, 0 info $ diff fernway.tg patch-upset.tg 25a26,27 > @@ goodwill: marketing asked for this after a complaint > - IF the customer is upset THEN MUST call @[apply_code]. $ typeglish check patch-upset.tg ✓ 1 file — 0 error, 0 warning, 0 info $ typeglish score patch-upset.tg | head -1 patch-upset.tg — A (99/100) proven errors: none tiers: base+z3
The first patch is caught because it says the quiet part out loud. The second says the same thing politely, and it is the one marketing will actually write. A checker can only compare the conditions you wrote, and the customer is upset and the code is invalid are two judgments in different words. That is why expired_code pleads in its input. It is the case that turns the upset-customer patch from a clean check into a failing test, on the first live run after someone adds it.
§6Common questions
- How do I stop my AI support agent giving out discount codes?
- Name the two kinds of code differently in the prompt, a public offer and a private code, and write NEVER share a private code as its own rule. Keep the private codes themselves out of the prompt: a code the model never reads is a code it cannot leak. Validate any code the customer gives through a lookup tool.
- Why does my chatbot keep offering a promotion that has ended?
- Because the offer and its end date are written into the prompt, and the model has no reliable sense of today's date to compare them against. Pass the live offer in at runtime as a variable, so the prompt changes the day marketing changes the offer and nobody has to remember to edit it.
- Should my agent honour an expired promo code if the customer is upset?
- That is a policy decision, not a judgment to hand the model. Write the policy as a rule the checker can read, such as never applying a code the lookup reports as invalid, and send goodwill requests to a person. A test case with a pleading customer checks that the rule holds.
- Can TypeGlish tell when two promo rules conflict?
- When two rules share a condition and an action with opposite modals, yes: that is a blocking logic/contradiction and the build is refused. It cannot tell that an upset customer and an expired code can arrive in the same message, so a goodwill rule scoped to upset customers passes clean. That overlap needs a test case.
Before a promotions block ships, run typeglish review on it and read the RULES count against the line count. Every line that is not in the table is something the agent will treat as background, and in a promotions block those lines are usually the ones about money. Give the live offer a $REQUIRE, give validity to a tool, and keep one test case that asks, nicely and then not so nicely, for a code that has expired.