content agent · its critic is a linter I shipped · a human sends

galley

Drafts a post per channel, marks the AI tells with a linter I published, revises until clean, and holds for a human to send.

status public · loop proven, nothing sentrole designed + built end to end
13 to 0one LinkedIn brief: naive Claude marketing copy tripped 13 weighted tells, galley's proof scored 0.
1 passseeded with that naive draft, the revise loop drove a score of 5 to 0 in a single pass, on real model output.
recall 1.0the critic caught 8 of 8 labeled slop lines with 0 false alarms, and names the 2 tells it still misses.
0 sentgalley writes and revises. It has no send call. A person approves every proof.
What it solves

Every team ships AI-written content now, and it all carries the same tells: the em dash, the thrilled-to-announce opener, the triple adjective stack. Readers spot it in three words. A generator alone cannot fix this, because asking a model to grade its own copy for slop is asking the thing that produced the tell to catch it.

The constraint

House style stated up front means the first draft is usually clean already, which is good engineering and also a measurement problem: a revise loop that never runs proves nothing. The build had to show the loop firing on real output, not just carry one in the code.

Who it serves

Founders and marketers who write with AI but refuse to sound like it.

Who it is for

Teams hiring someone to automate content and still keep a hand on what goes out.

How it works
  1. Brief in, house style up frontThe generator is given the same ruleset the critic enforces, phrased as instruction, so it writes to the rules instead of having them scrubbed after.
  2. The copy desk marks itoff-default's scanner flags each writing tell on the draft with a weight, where a structural tell like the antithesis counts more than a single banned word.
  3. Revise against the marksUp to three passes, each one feeding the exact findings back to the model, until the weighted score is zero.
  4. Clean or heldA draft that will not come clean in three passes is held, with the tells that held it, never shipped. That path is the load-bearing test.
  5. A human sendsThere is no publish call anywhere in the code. A proof comes back clean or held, and a person decides.
The decision that was not obvious

The critic is not galley's own judgment. It imports off-default, a linter I published separately, and treats any non-zero score as a rejection. A model grading its own writing for slop is the fox minding the henhouse. A separate deterministic gate, versioned on its own, is a check the generator cannot talk its way past.

What I would change if I rebuilt it today

The two tells the critic misses, an inline emoji and an antithesis split across two sentences, are line-scoped gaps in off-default. I kept them in the eval as labeled misses rather than quietly passing them, and the next pass adds both rules upstream so every caller inherits the fix.

Built with
Python Claude (Sonnet) off-default copy-tells scanner injected generator for testable orchestration GitHub Actions eval gate