Three agents measure a company blog, argue with each other about the draft, and are not allowed to publish until separate deterministic code has independently read the result back.

Solo build2026Live, weekly schedule

01The problem

One person runs marketing for three companies, and the content programme was the part that quietly broke. A bulk publishing push in April 2026 shipped dozens of articles that produced no search impressions at all. The eventual diagnosis was that eighty-eight per cent of published posts carried a noindex tag inherited from a content-management default, so nearly the whole library was invisible to both search engines and answer engines. Nobody caught it because nobody had time to check.

Even when the articles were good, the multilingual step leaked. One run published Traditional and Simplified Chinese live and left the English version as a draft, breaking language alternates and losing the English market, and no human noticed. The pain was never writing speed. It was that quality, indexability and language parity were all being verified by eye, once a week, by someone with two other jobs.

02What I built

A closed loop that measures the site, decides what to write, writes it, argues with itself about quality, publishes in three languages, and then proves in code that it actually did. An auditor scores every published post across four weighted layers and applies only mechanical fixes. A writer and an editor run as two separate headless agents kept in deliberate tension — the writer advocates for the reader, the editor for search and answer-engine visibility — exchanging written comments until the editor accepts or the round limit bites. Every brand fact, threshold and language lives in a config directory, so nothing about a particular brand is compiled into the engine.

  • A quality gate at eighty out of a hundred, adjustable only inside a fixed envelope; a draft that fails twice escalates instead of publishing.
  • An editorial comment loop with a ninety-five point accept bar and a hard ceiling of four revision rounds — hitting the ceiling downgrades the run to draft and sends an alert.
  • Atomic trilingual publishing: all three languages ship with the same status or none of them do, enforced by deterministic code that re-reads each post rather than trusting the write.
  • A stated auto-fix boundary — metadata, headings, alt text, duplicate titles, sitemaps and indexability regressions are repaired without asking; rewrites, fact-checks, cannibalisation and ranking declines queue for a human.
  • Self-improving hero images: a generator proposes, a vision judge scores against a rubric, and the winning prompt is written back to a learnings file.
  • A learning layer that proposes but never applies — threshold changes are filed as written proposals, and only an explicit human approval moves a number into the live config.
Schematic of the weekly reportA drawn, abstracted representation of the engine’s only human-facing surface, an emailed report: a band of four score tiles, a gate history chart of eleven columns with the current run emphasised, and a three-row post table with score and status columns. All content is placeholder.WEEKLY REPORT SCORE, PLACEHOLDER GATE POST SCORE STATUS

Drawn, not captured. The engine has no web interface; its human-facing surface is an emailed report — a score band, the gate history with the current run picked out, and a per-post table whose rows here are placeholder bars rather than real titles. Every value shown is illustrative.

03As a product

Who buys it
Mid-sized companies running a multilingual site with one or two marketing staff, and the small agencies that serve them. The buyer is the person who owns the content calendar and cannot personally check every post in three languages.
Value
It converts a weekly editorial process into a scheduled job that produces one original article and two translations, publishes them safely, and reports what moved — while refusing to publish anything that fails the quality bar or cannot be verified as indexable.
Positioning
Visibility trackers measure but do not write or publish. Plugins fix metadata but hold no editorial judgement. A freelance writer produces prose but no measurement loop and no parity guarantee. This sits in the gap: measurement, editorial judgement and publishing enforcement in one loop, with the brand’s own facts and red lines as config.
Status
Internal tool, built licensable. A refactor moved every brand-specific value into a per-brand directory and added a setup wizard and a config doctor, so a second brand can be onboarded without touching engine code. Not commercialised, no external tenant.

04How it works

Stack
Python across seventy-six engine modules in eleven capability folders, orchestrated by shell scripts on launchd. Headless model agents fill the editor, writer and sentinel roles. Publishing goes through a content-management REST API behind a write proxy; measurement comes from rank, analytics and search-console data over service-account REST. Config is YAML and JSON with an envelope-clamped cascade.
Shape
Measure, then create, then prove. The two gates at the end are plain deterministic code, deliberately living outside the agent that did the writing.
The weekly loop, five levels, one selection A vertical flow in five levels. A scheduled run pulls four measurement sources, each individually retried, and audits over 95 published posts across four weighted layers; both of those levels are drawn as hollow bands of equal width because nothing about their volume was ever counted per branch. The drafting level is measured: three briefs become three drafts and the editor keeps exactly one, so the chosen band is half the width of the two that are shelved, and the shelved branch ends there and feeds nothing. Both levels below it are measured too, carrying the chosen draft at its own width through a score bar and an independent read-back, out to a publish of one article and two translations that ships three languages or none. MEASURE Four sources, each retried OVER 95 POSTS, SCORED EVERY RUN AUDIT Four weighted layers a post PROPOSALS WAIT FOR A HUMAN DRAFT Writer and editor, 4 rounds CHOSEN SHELVED GATE Score bar, then a read-back ONE ARTICLE, TWO TRANSLATIONS PUBLISH Three at once, once a week

Width measured — the drafting level is drawn to the engine’s own fixed ratio, read from the weekly script: three briefs, three drafts, one chosen and two shelved, and every level below carries the chosen draft’s width.Width uniform — the measurement and audit levels above were never counted per branch, so their bands claim no quantity and are drawn at equal width.

A write is never confirmed by its own response. An independent read fetches all three published posts back, and if they disagree every one of them rolls to draft; and no threshold in the engine ever moves without a human moving the number.

The decision I spent longest on

Moving the publish decision out of the agent and into deterministic code. The tempting design is to let the publishing agent report what it did, because it is the thing holding the context. Two incidents killed that. Once the agent published two languages, left English as a draft, and reported success. Once a gate correctly decided not to publish, the enforcement step failed on all three posts by parsing an empty response body, a bare exception handler swallowed the error and returned zero, and the pipeline published anyway. The rule that came out of it is that a write is confirmed by an independent read, never by the write’s own response — and that a gate which cannot fail loudly is not a gate.

05Retrieval architecture

Nothing here is retrieved from a store the engine controls. Every run refetches the site it is about to write for, which is slower and less clever than keeping an index, and is the only version that cannot be quietly wrong.

The subtlest failure in this system was a retrieval failure, and it was not a bad query. Every signal the topic picker consulted came from what had already been published, so a subject nobody had written about could never surface as a gap; the engine searched a biased pool faithfully and narrowed for months. The fix was a written topic catalogue with a rotation cap, which selects against the signal on purpose.

ModelAt query time only. Every model call happens inside the weekly run, against material fetched minutes earlier; nothing is embedded, cached or precomputed between runs.

Corpus
Two bodies, and neither belongs to the engine. The first is the site itself: every published post per language, refetched from the content-management API at the start of a run, alongside four measurement feeds covering rankings, analytics, search-console data and the site inventory. The second is written by hand: a brand configuration holding the topic catalogue, the statistics permitted per topic, the claims banned outright, and the facts the company has not settled internally.
Ingestion
One scheduled weekly run plus a daily audit pass, on launchd. The run waits for real network connectivity before it starts, and each of the four measurement fetches is individually wrapped in a retry, because a single transient reset used to kill an entire week of output rather than one number. Nothing persists between runs except the dated result files, so a stale fetch cannot silently outlive the run that made it.
Index
Built fresh and thrown away. The link resolver pulls every published post per language, a hundred at a time, and tokenises each title and slug into a lookup that lives in memory for the length of the run. It is a real index and deliberately a disposable one: the failure it is designed against is a cached index still pointing at a URL that has since changed, which produces an internal link resolving to nothing.
Query
A brief’s keywords, tokenised the same way the index was. There is no natural-language step and no rewriting, because the editor agent has already fixed the topic and the angle; what reaches the resolver is a short bag of terms with a topic identifier attached to it.
Selection
The mechanism is token overlap. Each indexed post is scored by how many query tokens its title and slug share, anything scoring zero is discarded, and the top three survive. The shaping is the interesting half: results are sorted by that one count and truncated, and where a genuinely new topic has no siblings to link to at all, the resolver falls back to that topic’s landing page from config rather than returning an empty list. The earlier behaviour failed the editor’s internal-link criterion and made new topics effectively unpublishable. There is no second pass and nothing to rerank, because there is only ever one score.
Grounding
The facts a writer may assert are a whitelist, not a memory. Each topic in the catalogue carries the statistics permitted for it, verbatim; topics marked qualitative-only carry an empty list, which means no numbers whatsoever, and the draft has to earn its structured-data score with comparison and process tables instead. Beside that list sit the claims banned outright, the client names that may not be published, and the facts the company has not settled. Downstream, a quality gate holds a draft at eighty out of a hundred with a second failure escalating rather than publishing, fact-checks are one of the classes the engine is forbidden to fix by itself, and two deterministic gates outside the writing agent read the published result back before anyone is told it worked.

Present — this layer exists and runs.Absent by decision — the layer is not there, and the sentence beside it is the reason. Every one of the six is answered on every system in this chapter, so the rows can be read across pages.

06Numbers

MeasureFigureBasis
Quality gate pass threshold, adjustable only within 70 to 9280 / 100Verified
Editor accept bar, out of 10095Verified
Revision rounds before a run is downgraded4Verified
Posts scored per audit runover 95Verified
Share of the library found carrying a noindex tag88%Verified
Live posts still unindexed when the rendered check landed22Verified
Weekly unattended output, one original and two translations3 postsVerified
Human touchpoints in a clean run0Projected
Editorial hours displaced, per week6-10 hrsProjected

Verified — read from the engine’s own config files, its dated result files and the git log. Corpus and traffic figures are stated as magnitudes because they belong to a company, not to me. Projected — zero touchpoints is the happy-path figure derived from the design rather than an observed average — real runs have needed attention when a gate escalates or a credential expires. The hours are estimated from the manual equivalent of one article, two adapted translations, metadata, internal links, a hero image and a three-language publish-and-verify pass. No time-tracking data exists.

07Timeline

  1. 2026-05The three-agent framework is defined as written skill files — auditor, writer, creator — with a locked publishing cadence.
  2. 2026-06The engine migrates off an expiring hosted sandbox onto a local Mac. The same day, the noindex fault is found and fixed.
  3. 2026-06The shift-left redesign: quality is enforced at the publish gate instead of repaired afterwards, the writer and editor comment loop is introduced, and the engine is forked into a portable standalone repository.
  4. 2026-07The multi-brand architecture is specified and its first two phases ship — config cascade and learning layer. The standing rule that all three languages ship together becomes code rather than a habit.
  5. 2026-08Indexability verification moves to rendered HTML, the keyword measurement layer is rebuilt, and topic selection becomes config-driven to break a self-reinforcing narrowing loop.

08Looking back

What broke

Almost every real failure was a verification that could not fail. A status check that trusted the agent’s own report. An indexability check that read a field which was never registered in the API, so it was always absent, and absent read as fine — which is why twenty-two live posts sat unindexed for two months. A regex that only matched double quotes while the content system emitted single ones. Separately, transient network drops at arbitrary pipeline stages were a recurring cause of missed weeks, and one case had no exit code to catch at all: the model CLI printed a usage-limit message and then never exited, leaving processes hanging as zombies.

What it changed

The fix pattern was the same each time — make the failure loud, retry the idempotent steps, and confirm every write by an independent read. The subtlest problem was not mechanical: the system kept choosing the same topics, because every signal source it consulted was already biased toward those topics. Nothing was broken; the loop was simply feeding on its own output. That one needed a written topic catalogue with an explicit rotation cap, which is a reminder that an automated system will converge on whatever you let it measure itself against.

Where it stands

Live on a weekly schedule, internal, no revenue. The run log is honest about the cadence: some weeks are skipped when a run fails, and the longest observed gap between completed runs is twelve days. Open items are an expired visibility credential to renew, and a folder rename deferred because moving it breaks the scheduler.

Back to Top