Half your prompt arrives at runtime
Your platform prepends the help article, the customer record and this month’s promotion. The file you review is a fragment, and the seam between the two halves is the one place your gate has nothing to say.
TL;DR The prompt your model reads is your file plus whatever the platform injects at runtime, and a checker can only prove things about the half it can see: declare the seam with $REQUIRE section so citations are tracked rather than assumed, and move any text you actually own onto the compile plane with $IMPORT file, where the proofs come back.
Open the prompt for any contact-center agent shipped in the last two years and you are looking at a fragment. Above it, at runtime, your platform is pasting in the retrieved help article, the customer record, the last four turns of the conversation, and a promotion block somebody in marketing edits from a CMS. You reviewed one of those. You checked one of those. The model reads all of them as one document.
§1The document has a co-author
This is not a criticism of your platform. Runtime composition is the correct architecture: the help article has to be the one that matches this question, the customer record has to be this customer’s, and neither can live in a file. The problem is narrower and it is entirely about where your gate sits. Your gate sits on the file. The defects live on the join.
Here is the shape it takes in practice. A broadband support prompt with three rules, all three written well, all three pointing at a block of text the file has never heard of.
# Role @@ role: name the domain so "helpful" has a scope You are a support agent for Northwind Broadband. # Constraints @@ grounding: the article is the only source of fact in this conversation - You MUST answer only from <kb_article>. @@ no_invented_price: a price the article does not carry is a price nobody approved - You NEVER state a price that <kb_article> does not carry. @@ handoff: an uncovered question is a handoff, not a guess - WHEN <kb_article> does not cover a question THEN you MUST hand off to a human agent.
undeclared.tg:7:1 error structure/dangling-section-ref Reference to section <kb_article>, which is not defined. undeclared.tg:9:1 error structure/dangling-section-ref Reference to section <kb_article>, which is not defined. undeclared.tg:11:1 error structure/dangling-section-ref Reference to section <kb_article>, which is not defined. ✗ 1 file - 3 error, 0 warning, 0 info
F (85/100): proven errors, grade capped at F, with structure 0 and every other facet untouched. The rules are fine. The pointer is not.What the compiler is objecting to is not the sentence, it is the reference. A <tag> in TypeGlish is a place, the same way @{name} is a value and @[name] is a tool, and a pointer whose target does not exist is a dangling pointer whether or not the sentence around it reads well. Your file said answer only from over there and never said where over there is.
§2Name the hole
The fix is one line, and the reason it is worth a section is that the line is a declaration about your runtime, not about your prompt.
<$CONFIG> $REQUIRE section kb_article </$CONFIG> # Role @@ role: name the domain so "helpful" has a scope You are a support agent for Northwind Broadband. # Constraints @@ grounding: the article is the only source of fact in this conversation - You MUST answer only from <kb_article>. @@ no_invented_price: a price the article does not carry is a price nobody approved - You NEVER state a price that <kb_article> does not carry. @@ handoff: an uncovered question is a handoff, not a guess - WHEN <kb_article> does not cover a question THEN you MUST hand off to a human agent.
0 error, 0 warning, 0 info, and F (85/100) becomes A (98/100) on the strength of structure 0 going to 100.Now build it, because the artifact is where the mental model becomes concrete.
✓ built .typeglish/dist/declared.txt ← declared.tg (c30e46abb9aa, full) # Role You are a support agent for Northwind Broadband. # Constraints - You MUST answer only from <kb_article>. - You NEVER state a price that <kb_article> does not carry. - WHEN <kb_article> does not cover a question THEN you MUST hand off to a human agent.
$REQUIRE line is gone, stripped like every $-command. The <kb_article> tags survive verbatim. What the compiler shipped is a hole with a name on it, and the name is the contract your runtime has to honor.Two smaller behaviours make the declaration worth more than a comment. Declare a section and never cite it and you get structure/unused-import: Imported section "customer_record" is never used, which under check --strict is an error and exit 1. And the declaration is scoped to the name you wrote, so a citation of <kb_articles> beside a declaration of kb_article is still a blocking structure/dangling-section-ref plus an unused-import warning for the one you declared and never used. Both halves of the typo get reported.
A declared seam is a symbol. An undeclared seam is a hope with a bracket around it.
§3What crosses the seam is not checked
Here is the part that changes how you review a prompt, and it needs a controlled pair to see properly. Two files, same agent, same policy, differing in exactly one thing: whether the promotion text lives in the file or arrives at runtime.
The scenario is the ordinary one. Marketing runs a promotion this month, it is edited in a CMS, and your platform prepends it to the system prompt. Finance, separately, has a rule about fee waivers that has been in the agent prompt since launch. Nobody has read both sentences in the same sitting for a while.
<$CONFIG> $REQUIRE section kb_article </$CONFIG> # Role @@ role: name the domain so "helpful" has a scope You are a support agent for Northwind Broadband. # Context @@ fee_def: the charge is ours, so the model cannot know which one is meant The fee IS a one-off charge for connecting a new line. # Current promotion @@ promo: pasted out of the CMS block the platform injects this month - You MUST waive the fee. # Constraints @@ grounding: the article is the only source of fact in this conversation - You MUST answer only from <kb_article>. @@ no_waivers: a fee waiver is a retention decision and it costs margin - You NEVER waive the fee.
pasted.tg:15:1 error logic/contradiction Logical conflict - "waive the fee" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). (conflicts with line 12) pasted.tg:21:1 error logic/contradiction Logical conflict - "waive the fee" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). (conflicts with line 9) ✗ 1 file - 2 error, 0 warning, 0 info
F (79/100), runtime 72, consistency 0, and every other facet at 100. Exactly what you want a gate to do.Now move that promotion back where it actually lives, which is at runtime, behind a section your platform composes in. Same policy. Same words. Same two departments disagreeing.
<$CONFIG> $REQUIRE section kb_article, current_promotion </$CONFIG> # Role @@ role: name the domain so "helpful" has a scope You are a support agent for Northwind Broadband. # Context @@ fee_def: the charge is ours, so the model cannot know which one is meant The fee IS a one-off charge for connecting a new line. # Constraints @@ promo: the CMS block the platform injects this month - You MUST follow <current_promotion>. @@ grounding: the article is the only source of fact in this conversation - You MUST answer only from <kb_article>. @@ no_waivers: a fee waiver is a retention decision and it costs margin - You NEVER waive the fee.
0 error, 0 warning, 0 info. check --strict returns the same line and exit 0. A (98/100) with consistency 100, which is the facet that was 0 a moment ago.The two files ship the same contradiction to the same model. One of them is F (79/100) with two blocking errors and no build; the other is the best-scoring file in this post. Nothing was fixed in between. The text simply moved to the other side of the seam, where the prover cannot reach it.
Note what is not happening here. This is not a lenient default that --strict would tighten, and it is not an advisory finding somebody has to notice. There is no finding. There is no severity to escalate, because there is no text to analyse. consistency 100 is an honest report about the document the compiler was handed, and the document the compiler was handed is half the prompt.
§4Move the seam when you own the text
Which raises the question the rest of the post is about: does that promotion actually have to arrive at runtime? It changes monthly, not per conversation. It is written by people who work here. It is text somebody could put in a file.
Put it in one, and compose it with $IMPORT file instead.
# current_promotion - You MUST waive the fee.
0 error, 0 warning, 1 info, the info being prompt/unintroduced-definite on the fee, a noun this file does not introduce because the file that imports it does.<$CONFIG> $REQUIRE section kb_article $IMPORT file "promotion.tg" as promo </$CONFIG> # Role @@ role: name the domain so "helpful" has a scope You are a support agent for Northwind Broadband. # Context @@ fee_def: the charge is ours, so the model cannot know which one is meant The fee IS a one-off charge for connecting a new line. # Constraints @@ promo: the promotion is a file in this repo now, not a runtime injection @<promo.current_promotion> @@ grounding: the article is the only source of fact in this conversation - You MUST answer only from <kb_article>. @@ no_waivers: a fee waiver is a retention decision and it costs margin - You NEVER waive the fee.
@<promo.current_promotion>, which drops the section’s content in where the pointer sits.prompts/agent.tg:2:3 warn import/unmet-section-requirement $REQUIRE section kb_article is unmet - no file in this composition declares <kb_article>, so citations of it stay promises. Declare <kb_article> in a file this one composes with, or keep the $REQUIRE if the platform supplies the section at deploy time. prompts/agent.tg:16:1 error logic/contradiction Logical conflict - "waive the fee" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). (conflicts with line 12) prompts/agent.tg:20:1 error logic/contradiction Logical conflict - "waive the fee" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). (conflicts with line 10) prompts/promotion.tg:2:1 info prompt/unintroduced-definite "the fee" retrieves something this document never introduces - a model must guess which fee is meant. Introduce it on another line ("You manage a fee.") or name it outright. ✗ 2 files - 2 error, 1 warning, 1 info program: 1 root - prompts/agent.tg (2 files)
Read the warning at the top, because it is the compiler stating this post’s thesis in its own words: no file in this composition declares <kb_article>, so citations of it stay promises. That is the correct state for the help article, which genuinely does arrive at deploy time. It would be a bug report for anything else.
Two wrinkles worth knowing before they cost you ten minutes. The program pass runs on directory targets only, so tg check prompts is two errors and exit 1 while tg check prompts/agent.tg on the same tree is 0 error, 0 warning, 0 info and exit 0; a per-file CI loop is this gate with the proofs switched off, which the paragraph that lives in three prompts works through in full. And a transcluded rule is reported at the line of the pointer that pulled it in, not at its line in the imported file, so line 16 above is the @<promo.current_promotion> row.
§5Which seam for which content
The decision is not stylistic and it has one test: does this text change per conversation?
- Yes, per conversation. The retrieved article, the customer record, the order list, the transcript so far. These have to arrive at runtime, so declare them with
$REQUIRE sectionand cite them by name. You get a checked pointer and nothing more, which is the honest ceiling for content that does not exist yet when you check. - No, but it changes monthly. The promotion, the current SLA, the seasonal escalation path. This is the category that quietly moved to runtime because a CMS was convenient, and it is the category where you lose the most. Put it in a
.tgfile, compose it with$IMPORT file, and let the person editing it hit the same gate you do. - No, it changes when policy changes. Brand voice, refund caps, verification rules. If any of this is being injected by your platform rather than living beside the agent, that is not composition, that is a second prompt nobody is reviewing.
And then the rule that survives both seams: keep the rules about the injected text in the file. You cannot check the article, but You MUST answer only from <kb_article> is a rule in your file, on your plane, graded on enforceability and provable against everything else you wrote. The seam limits what the compiler can read. It does not limit what you can say about it.
Check the half you own. Declare the half you do not. Move the half that never needed to be over there.
§6Common questions
- Why does my agent ignore the knowledge base article my platform injects?
- Usually because nothing in the prompt tells it that the article outranks everything else it knows, and nothing in the prompt names the article as a place. A rule that says answer only from the help article is addressed to a reader who cannot see which text you mean. Cite the injected block as a section,
You MUST answer only from <kb_article>, and declare it with$REQUIRE section kb_articleso the citation is checked rather than hoped for. If the agent still drifts, read the built artifact: the tag is passed through verbatim, so if your runtime prepends the article under a different tag name, the rule is pointing at a place that never arrives. - How do I check a system prompt that my platform assembles at runtime?
- Check the half you own and declare the half you do not. Every block your runtime prepends gets a
$REQUIRE sectionline and a named citation in the rules, which turns a silent assumption into a symbol the compiler tracks: a citation with no declaration is a blockingstructure/dangling-section-ref, a declaration nothing cites isstructure/unused-import, and a misspelled citation still errors even when the correct name is declared. What the compiler cannot do is read text it never receives, so no diagnostic will ever fire on the content of an injected block. - What is $REQUIRE section for?
- It declares that a host composes a named section into the prompt at deploy time, so citations of that section resolve instead of dangling. It is a promise rather than a proof, and the compiler is explicit about that: run a directory check on a composition where nothing supplies the section and you get
import/unmet-section-requirement, whose message says citations of it stay promises. That warning is the correct state for a genuinely host-supplied block, and it is a bug report for a block you thought lived in a file. - Should our policy text live in the prompt file or be injected at runtime?
- Inject only what varies per conversation, and keep everything else in a file. The help article and the customer record change on every contact, so they have to arrive at runtime. This month’s promotion, the escalation matrix and the brand voice do not change per conversation, and every one of them that arrives at runtime instead is a rule your gate cannot see. Move those into a
.tgfile and compose them with$IMPORT file, and the proofs come back: the same two sentences that were invisible across a runtime seam are two blockinglogic/contradictionerrors across an import seam.
The compile-time and runtime planes inside a single file are laid out in your system prompt has a compile time, and this post is the same split seen from one level up: the file has a compile time, and the prompt has an assembly step that happens after your build. The habit that catches both is the same one. Build the file, read the artifact, and count what is missing from it, which is also how the policy doc somebody pasted into your agent turns out to be addressed to a reader who already had the context.