← Tidelines/Best practices

The prompt is frozen. The artifact is not.

Peak season closes the change window, and the agent prompt goes in the freeze with everything else. Nine checks for the six weeks that follow, and the one instrument that can tell a documentation edit from a refund policy edit.

by TypeGlish team9 min read#best-practices
Same file. Different prompt.

TL;DR Freeze the artifact, not the file: three @@ notes, a // comment and a whole $TEST block leave a returns prompt on the same artifact hash c9f0aa8bef91 while taking its score from B (87/100) to A (91/100), a five-fold refund cap increase leaves every single facet byte-identical and moves only the hash to d2584c67c1e4, and an edit to an imported file nobody froze moves a byte-identical prompt from 649f5f35aac8 to 841aa8af3c0b.

Every contact centre has a version of this date. The change window closes some time in the autumn, the release train stops, and for six weeks nothing ships unless it is a sev-1. The agent prompt is now in scope for that, because it is a file in a repo with a pull request process, and whoever wrote the freeze policy wrote it about code.

Which leaves a question the policy does not answer: what is a change to a prompt? A freeze on a repo is enforceable because a commit is a discrete, greppable thing. A prompt has a second artefact behind the file, the thing the model is actually handed, and the two do not move together. Some edits change the file and not the prompt. At least one changes the prompt without touching the file at all.

These nine are ordered by when they apply: three before the window closes, two for the edits that are free, three for the edits that look free, and one for the day it reopens. Every one has a command behind it.

§1Before the window closes

The prompt for the rest of the post is a returns agent for a homeware retailer, gated on channel, with the desk refund limit finance signed off in March.

returns.tg - the file going into the freeze✓ B (87/100)
<$CONFIG>
  $REQUIRE variable channel: one of voice, chat
</$CONFIG>

# Role
You are a returns agent for Harlow Home.

# Constraints
- You MUST refund at most 50 pounds without a manager code.
- You MUST keep every reply to at most 4 sentences.
$IF @{channel} is equal to voice:
  You MUST read the returns address back to the customer.
$ELSE:
  You MUST send the returns address as a link.
0 error, 0 warning, 0 info, B (87/100), runtime 100 and hygiene 50. A perfectly ordinary file, which is the point: everything below happens to files in this state.

Check 1: freeze the artifact hash, not the commit. A commit SHA tells you the source did not move. It tells you nothing about what a host handed the model, which is the thing your freeze is actually protecting. typeglish build prints the artifact hash on the tick, and records it with the input bag and the check tier beside it.

.typeglish/build-manifest.json - the row to freeze
$ npx typeglish build returns.tg --vars '{"channel":"voice"}'
 built .typeglish/dist/returns.txt ← returns.tg (c9f0aa8bef91, full)

{
  "version": 1,
  "builds": [
    {
      "source": "returns.tg",
      "sourceSha256": "503f9b6ec5291e19ff792eef7969c9248af2e0e7edfd9a0bbfabce57d4521229",
      "artifact": ".typeglish/dist/returns.txt",
      "artifactSha256": "c9f0aa8bef91b9e4349ae6db710aa83bcd39b2987b49686fb42b67954e8d6000",
      "vars": {
        "channel": "voice"
      },
      "typeglish": "0.9.0",
      "checkMode": "full",
      "report": { "ok": true, "counts": { "error": 0, "warn": 0, "info": 0 }, "strict": false },
      "builtAt": "2026-08-26T09:26:11.496Z"
    }
  ]
}
The 12 hex digits after the artifact path are the front of artifactSha256. Note what is in the row beside it: the vars bag the build was resolved against, the compiler version, and checkMode. The manifest holds one row per source and rewrites it on each build, so archive the row, do not rely on it accumulating.

Check 2: freeze the input bag too. The same file with the channel bound and the channel unbound are two different prompts, and the unbound one is not a smaller version of the bound one. It ships both arms of every chain as prose for the model to sort out.

tg build returns.tg - unbound, the template build
$ npx typeglish build returns.tg
 built .typeglish/dist/returns.txt ← returns.tg (db6de4fd4888, full)

# Role
You are a returns agent for Harlow Home.

# Constraints
- You MUST refund at most 50 pounds without a manager code.
- You MUST keep every reply to at most 4 sentences.
If channel is voice:
  You MUST read the returns address back to the customer.
Otherwise:
  You MUST send the returns address as a link.
db6de4fd4888 against c9f0aa8bef91 for one file and one compiler. If your host stops sending channel during the freeze, nothing in the repo changed and the agent is reading a conditional instead of an instruction. Pin the bag in the frozen row and fail the deploy on a template build, the way a missing input is not a missing rule.

Check 3: freeze the program, not the file. This is the one that catches teams out, and it is the check most worth doing this week rather than in November. If your prompt composes with anything, the file you froze is not the prompt you froze.

tg build prompts/returns.tg - before and after somebody else's edit
# freeze day
$ npx typeglish build prompts/returns.tg
 built .typeglish/dist/prompts/returns.txt ← prompts/returns.tg (649f5f35aac8, full)

# a week later, somebody edits prompts/house.tg: at most 4 sentences becomes at most 6
$ sha256sum prompts/returns.tg
5f8e429bda2567097805c6de70086270f3f91fc72f7ed40980cea6908f55d684  prompts/returns.tg

$ npx typeglish build prompts/returns.tg
 built .typeglish/dist/prompts/returns.txt ← prompts/returns.tg (841aa8af3c0b, full)

# Role
You are a returns agent for Harlow Home.

- You MUST keep every reply to at most 6 sentences.
- You MUST NOT promise a delivery date.

# Constraints
- You MUST refund at most 50 pounds without a manager code.
The frozen file is byte-identical and its prompt is not. prompts/returns.tg carries $IMPORT file "house.tg" as house and a @<house.Constraints> pointer, so a bound written in a file nobody put on the freeze list is now in the deployed prompt. Check and build the directory instead, and read the line under the tick: program: 1 root — prompts/returns.tg (2 files). That is your freeze scope, printed. It is the same blast radius that makes one paragraph in three prompts worth lifting into a shared file in the first place.

§2The edits that are free, and provably

Check 4: annotate freely, and prove it with the hash. A freeze is the best six weeks of the year for writing down why a rule exists, because nobody is allowed to change the rule. @@ notes, // comments, $TEST blocks and the <$CONFIG> section are all off the plane the model reads.

returns-annotated.tg - documentation only✓ A (91/100)
<$CONFIG>
  $REQUIRE variable channel: one of voice, chat
</$CONFIG>

# Role
You are a returns agent for Harlow Home.

# Constraints
@@ cap: the desk limit finance signed off in March
- You MUST refund at most 50 pounds without a manager code. // peak: audited weekly
@@ brevity: four sentences keeps a chat reply scannable
- You MUST keep every reply to at most 4 sentences.
$IF @{channel} is equal to voice:
  You MUST read the returns address back to the customer.
$ELSE:
  You MUST send the returns address as a link.

$TEST over_cap
  - input:: The lamp arrived broken, can you refund the 80 pounds I paid?
  - expect::
    - contains "manager"
Two @@ notes, one trailing // comment, and a four-line test case. Every rule the model reads is untouched.
tg build - the frozen file and the annotated one
$ npx typeglish build returns.tg --vars '{"channel":"voice"}'
 built .typeglish/dist/returns.txt ← returns.tg (c9f0aa8bef91, full)

$ npx typeglish build returns-annotated.tg --vars '{"channel":"voice"}'
 built .typeglish/dist/returns-annotated.txt ← returns-annotated.tg (c9f0aa8bef91, full)
Two source files, one artifact hash. That is not an argument that the edit was safe, it is a proof, and it is the shape of evidence a freeze board can accept in thirty seconds. Meanwhile the score goes B (87/100) to A (91/100) and hygiene 50 to 67, so the file gets measurably better while the prompt does not move at all.

Check 5: never gate a freeze on the score. Which brings the obvious trap. If a documentation edit gains four points, a scorer is measuring something other than what the freeze protects. Here is the same instrument pointed at an actual policy change.

tg score - the refund cap raised from 50 pounds to 250
returns.tg — B (87/100)  proven errors: none  tiers: base+z3
  planes  runtime 100 (what the model reads) · hygiene 50 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 98 x.08 · consistency 100 x.17
          structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08

returns-cap.tg — B (87/100)  proven errors: none  tiers: base+z3
  planes  runtime 100 (what the model reads) · hygiene 50 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 98 x.08 · consistency 100 x.17
          structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08

$ npx typeglish build returns-cap.tg --vars '{"channel":"voice"}'
 built .typeglish/dist/returns-cap.txt ← returns-cap.tg (d2584c67c1e4, full)
Eight facets, two planes, one grade, and not one number moves for a five-fold increase in what an agent can refund without a manager. score --min B exits 0. The only signal in the toolchain that noticed is c9f0aa8bef91 becoming d2584c67c1e4.
A grade measures how the rules are written. A freeze is about what they say. Do not borrow one instrument for the other job.

§3The edits that look free

Check 6: fmt is not cosmetic. A formatter is the canonical safe-during-a-freeze change, and this one is not, because the thing it fixes is a line that reaches the model in the wrong shape. Here is a rule somebody hard-wrapped to keep it under a line length.

wrapped.tg - one rule, wrapped over two lines✗ 3 warning
# Role
You are a returns agent for Harlow Home.

# Constraints
- You MUST refund at most 50 pounds
  without a manager code.
- You MUST keep every reply to at most 4 sentences.
Three warnings, none of them blocking, so this file builds and ships: structure/missing-period, structure/bad-indent, and structure/wrapped-fragment reading Reads as a hard-wrapped continuation of line 4.
tg fmt --check then tg build - what formatting moved
$ npx typeglish build wrapped.tg
 built .typeglish/dist/wrapped.txt ← wrapped.tg (59cacac0a0b8, full)
# the artifact, verbatim:
- You MUST refund at most 50 pounds
without a manager code.

$ npx typeglish fmt wrapped.tg --check
wrapped.tg: 1 change needed
 1/1 file need formatting — run typeglish fmt

$ npx typeglish fmt wrapped-fmt.tg && npx typeglish build wrapped-fmt.tg
 built .typeglish/dist/wrapped-fmt.txt ← wrapped-fmt.tg (dc908bb971e8, full)
# the artifact, verbatim:
- You MUST refund at most 50 pounds without a manager code.
59cacac0a0b8 to dc908bb971e8. The pre-format artifact hands the model a bound on one line and the condition it applies to on the next, as a separate statement. Formatting is the right fix and it is a behaviour change, so it goes through the change process rather than round it.

Check 7: run one gate, not two. --strict escalates every correctness finding to a blocking error, so the same file passes on one machine and fails on another. Decide which gate the freeze runs on and put it in both places.

tg check wrapped.tg - the same file, two gates
$ npx typeglish check wrapped.tg
 1 file — 0 error, 3 warning, 0 info
# exit 0

$ npx typeglish check wrapped.tg --strict
wrapped.tg:5:1  error  structure/missing-period  Unterminated statement — end it with a period (or ! ?; a lead-in may end with ":"). Statement boundaries are a compile contract.
wrapped.tg:6:1  error  structure/bad-indent  Indentation mirrors section nesting: expected column 0 (0 levels deep), found 2. The indent unit is 2 spaces.
wrapped.tg:6:3  error  structure/wrapped-fragment  Reads as a hard-wrapped continuation of line 4 — it starts lowercase, classifies as plain prose, and line 4 has no terminator. If they are ONE statement, join them (typeglish fmt); if this line stands alone, capitalize its first word (and terminate line 4).

 1 file — 3 error, 0 warning, 0 info
# exit 1
Same bytes, same compiler, exit 0 and exit 1. A freeze with a strict laptop and a lenient CI has a queue of hotfixes nobody can reproduce, and a freeze with the reverse has a gate that never fires.

Check 8: record which tier proved it. The full check runs Z3 proofs when z3-solver is installed, and the build writes down which mode it ran in. "checkMode": "full" in the manifest and tiers: base+z3 on the score line are the two places that fact is visible. A freeze verified by a run that could not reach the prover proved less than the one you archived, and the difference is exactly the class of defect a peak-season prompt cannot afford: numeric bounds, measures, time intervals.

§4When it reopens

Check 9: unfreeze by coverage, not by queue order. Six weeks of held-back edits come back as a list sorted by whoever shouted loudest. The test runner gives you a better sort key, offline, in one command.

tg test returns-annotated.tg --dry - output
$ npx typeglish test returns-annotated.tg --dry
 returns-annotated.tg  coverage: 1/4 rules exercised
  · over_cap — "The lamp arrived broken, can you refund the 80 pounds I paid" (not run)
       contains "manager"
 1 prompt — 0 failed
coverage: 1/4 rules exercised, and 0 failed is honest rather than reassuring, because --dry validates the suite and counts coverage without sending a case to a model. Three of the four rules that ran your peak season have nothing pinning them. Those are the three to write cases for before the first held-back edit lands on them.

None of the nine is expensive. Eight are one command, and the ninth is a decision about which gate you run. What they add up to is a freeze that means something: a recorded artifact, a pinned input bag, a scoped program, an explicit list of edits that provably cannot change behaviour, and a hash to diff when somebody claims one did not.

§5Common questions

What counts as a change to a system prompt during a code freeze?
A change to the built artifact, not a change to the file. Adding three @@ notes, a // comment and a whole $TEST block to a frozen returns prompt leaves the artifact on the same hash it had before, c9f0aa8bef91, because none of those planes reach the model. Raising the refund cap from 50 pounds to 250 pounds moves the artifact to d2584c67c1e4. The second edit is one word and it is the only one of the two that is a change, so the thing to diff in the pull request is the output of typeglish build.
Does adding a comment or a test change my deployed prompt?
No, and you can prove it rather than believe it. @@ annotations, // comments, $TEST blocks and the <$CONFIG> section are source-plane or control-plane: they are stripped at compile and never reach the model. A returns prompt with three notes, one trailing comment and a four-line $TEST added builds to c9f0aa8bef91, byte for byte the same artifact as the file without them, while its score moves from B (87/100) to A (91/100) and hygiene from 50 to 67. That makes documentation the one edit a freeze should positively encourage.
Why did my prompt artifact change when I did not touch the file?
Because something it composes with changed. A frozen returns prompt that pulls shared rules in with $IMPORT file "house.tg" as house builds to 649f5f35aac8; after somebody edits house.tg from at most 4 sentences to at most 6, the same byte-identical returns.tg builds to 841aa8af3c0b and the artifact carries the new bound. A freeze on a file is not a freeze on a prompt. Freeze the program: build and check the directory, and read the line under the tick, which says program: 1 root, prompts/returns.tg (2 files).
Can I use the TG score as the gate for a prompt change freeze?
No. The score measures how the rules are written, so it is blind in exactly the direction a freeze cares about. Raising a refund cap five-fold produces an identical report: B (87/100) on both files, runtime 100 and hygiene 50 on both, and enforceability, hardness, directness, consistency, structure, annotation, style and security all unmoved. Meanwhile a documentation-only edit that provably cannot change behaviour gains four points. Gate a freeze on the artifact hash recorded in .typeglish/build-manifest.json and keep score --min as the quality floor it is.
Field note

The reason a prompt freeze goes wrong is not carelessness, it is that the file looks like the whole thing. With code there is a build step everybody can see, and nobody confuses the diff with the deploy. A prompt is text that looks deployable, so the file becomes the unit of control by default, and then three of the four ways it can change are outside the thing being controlled: the values a host binds into it, the files it composes with, and the formatter somebody ran because the linter asked. The habit that fixes it costs one command. Build the prompt, archive the row, and diff artifacts rather than sources for the whole six weeks. It also settles the argument in the other direction, which is the part teams appreciate more: when somebody asks whether they are allowed to write down why the refund cap is 50 pounds, the answer is a hash that did not move.

∿ washed up Aug 26, 2026 ∿