Put a quality check before anything goes out

Mechanical checks, an independent judge and frozen test cases.

Watchmaker inspecting a part through a loupe

Every text an agent writes should pass two gates before anyone outside sees it: a mechanical check that a script runs, and a judgement made by a second model that never saw how the text was produced. An agent never grades its own work. Add a set of frozen test cases that you rerun every time you touch the instructions, and you have caught most of what goes wrong before it costs you.

This is how the agents that run Derrick’s marketing work (I co-founded Derrick, so read that with the bias in mind). Every post, page and outbound message they write goes through the same two gates, and each area of work has a pilot, an agent or skill whose only job is to check the output before it ships.

Why the writer cannot be its own reviewer

A model that wrote a text has the same blind spots when it rereads it. It knows what it meant, so it reads what it meant. Ask it “is this good?” in the same conversation and it will usually say yes, then polish the wording around the actual problem.

So split the checks by what kind of error they catch.

Error Example Who catches it
Forbidden form Em dash, banned phrase, a competitor’s name A script, every time
Wrong fact A price that changed, a feature that does not exist A script against your facts file
Weak or off-brief content Generic opening, wrong angle, no proof An independent judge
A promise the product does not keep The post describes a gesture the tool does not do A check in the product itself
Regression after a change The new instructions broke an old case Frozen test cases

The first two lines are cheap and certain. Do them in code, never in a prompt, because a model asked to “avoid em dashes” will still produce one now and then, and a regular expression never misses one.

Gate one: a mechanical lint

The lint is a short script that reads the draft and exits with an error if anything matches. Here is the shape of mine, simplified.

import re, sys

text = open(sys.argv[1], encoding="utf-8").read()
errors = []

PATTERNS = {
    "dash used as punctuation": r"[\u2013\u2014]",
    "banned phrase": r"(?i)\b(best-in-class|cutting-edge|in a world where)\b",
    "competitor name": r"(?i)\b(acme|globex)\b",  # load the real list from a file
}
ALLOWED_PRICES = {"20", "47.50", "175", "320"}  # read from your product facts file

for label, pattern in PATTERNS.items():
    for m in re.finditer(pattern, text):
        errors.append(f"{label}: {m.group(0)!r}")

for m in re.finditer(r"€\s?(\d+(?:[.,]\d+)?)|(\d+(?:[.,]\d+)?)\s?€", text):
    price = (m.group(1) or m.group(2)).replace(",", ".")
    if price not in ALLOWED_PRICES:
        errors.append(f"price not in facts file: {m.group(0)}")

print("\n".join(errors) or "OK")
sys.exit(1 if errors else 0)

Two details matter more than the patterns. The lists live in files the rest of the system also reads, so the price in the lint and the price in the product facts can never disagree; the memory chapter explains where each fact should live. And the agent runs the lint itself, reads the errors and fixes them, so you only see drafts that already pass.

In my setup, a script also checks that every feature a text names actually exists before the text can cite it, and counts are computed, never typed by hand. A number written in a prompt goes stale the day the product changes.

Close inspection through an eye loupe

Gate two: a judge in a blank context

The judge is a separate call with an empty conversation. It gets the brief, the draft and a grid, and nothing else: no writer instructions, no earlier drafts, no reasoning. That is what makes it independent.

You review a draft written by someone else. You did not write it.

BRIEF: {{brief}}
DRAFT: {{draft}}

Score each criterion from 1 to 5 and quote the passage that justifies the score.
1. Answers the brief: the main point is in the first two sentences.
2. Proof: every claim is backed by a fact, an example or a number from the brief.
3. Voice: reads like the reference samples, no stock AI phrasing.
4. Reader value: the reader can do something after reading.

Return JSON only:
{"scores": {"brief": n, "proof": n, "voice": n, "value": n},
 "blocking_issues": ["..."], "verdict": "pass|revise|reject"}
Verdict is "pass" only if every score is 4 or more and there is no blocking issue.

Asking for a quote behind each score stops the judge from handing out fours by default. Writing the pass rule into the prompt means the verdict is a rule applied to the scores, not a mood. When the verdict is “revise”, the blocking issues go back to the writer as they are, and the new draft goes through both gates again.

For the grid itself, start from the rules in the agent’s instructions file. The judge should check what the writer was told to do, plus the things you only notice as a reader.

Check the promise in the product

Some errors pass both gates because the text is well written and factually plausible. In November 2026, a LinkedIn post for Derrick said “filter, then enrich”. Checking that claim against the code showed that the play button in the column header enriched hidden rows too, and billed them. The text was right about the method and wrong about what the product did with it, which no lint and no judge could see. The product was fixed.

The rule I took from it: when a text tells the reader to do something in a product, someone checks that gesture in the product before it ships. An agent with access to the code or a test account can do that check, but it has to be an explicit step in the routine, with the result written down.

Hands working under a magnifier

Freeze test cases and rerun them after every change

Instructions drift. You add a rule to fix one bad output and three good ones get worse, and you only find out weeks later. The fix is a set of frozen briefs, often called golden evals: real inputs you keep forever, each with what a good output must and must not contain.

id: linkedin-tuesday-03
skill: linkedin-writer
brief: "Tuesday data post. Fact: the free plan gives 100 credits a month."
must:
  - mentions "100 credits"
  - opens on the fact, not on a question
must_not:
  - any price other than the plan table
  - a rhetorical question as the first line
judge_min_scores: { brief: 4, proof: 4, voice: 4, value: 3 }

After any change to a writer’s instructions, every frozen case is rerun through the lint and the judge, and the change ships only if none of them regresses. Ten to twenty cases per writer is enough to start. Add one each time a real draft fails in a way the existing cases did not cover, so the set grows from your actual mistakes.

What happens after a failure

A failed draft is only half of the information. The other half is why it failed, and whether that will happen again.

Result What the pilot does What gets written down
Lint fails Writer fixes and reruns, no human involved Nothing, unless the same error repeats
Judge says revise Writer gets the blocking issues and rewrites The issue, if it is new
Judge says reject Draft dropped, brief reviewed by a person The brief and why it failed
Product check fails Text blocked, product or text fixed An entry in the lessons ledger
Same issue three times The rule goes into the writer’s instructions A new frozen test case

The last line is the one people skip. A lesson noted in a log does nothing on its own. In my system a durable lesson is written directly into the expert’s instructions, because a reminder is not an action. If you want to see the whole loop running on a weekly voice cadence, the LinkedIn agent chapter shows it end to end.

Your next step: take the last ten texts your agent produced, write down every correction you made by hand, and sort them into the five rows of the first table. The forbidden-form and wrong-fact rows become your lint this week; the rest become the judge’s grid and your first frozen cases.