bhcto.com
Stage 9

Step 9 — The 3-Round Generation Pipeline

Each brief runs through three LLM rounds — draft, SEO rewrite, editorial polish — orchestrated end to end against the live Messages API. Here's the architecture, the model choice, and the guardrails.

Three rounds

1Draft
Prompt input
The brief.

A web-search-grounded first draft straight from the brief. Web search is enabled only in this round (up to 10 uses) so the model can pull current facts and citations before writing.

2SEO rewrite
Prompt input
Brief + Round-1 draft + Stage-0 intel JSON.

The draft is rewritten against the competitor intel — their titles, H1s, heading outlines, keyword density, and target lengths. Emits an optimized H1 and a meta-description comment.

3Editorial polish
Prompt input
Round-2 article + senior-editor mandate.

A final pass for voice, flow, accuracy, and NIR's safety framing. Output is publication-ready Markdown — the files in the next step.

Model & batch

Writing model

The pipeline probes claude-fable-5 first and, when it is available, runs on it — falling back to claude-opus-4-8 per item if the probe is gated. Effort is set to high with adaptive thinking.

probe → fallbackclaude-fable-5thinking: adaptive
Live Messages API

Each round runs the 15 posts against the live, streamed Messages API with bounded concurrency (4 at a time), rather than an overnight batch job. Round 2 only starts once Round 1 has fully landed, and so on.

R1 → await → R2 → await → R3web search in R1 only

The orchestration

Brief→
R1 Draft→
R2 SEO→
R3 Polish→
final/

Driven by content_supervisor.js; the per-round prompt builders live in lib/prompts.js. Full module map is on the methodology page.

Bugs we fixed (so we don't reintroduce them)

The first attempt surfaced several sharp edges. Each is now handled in code.

custom_id > 64 chars 400'd the whole batch
→ shortId() — truncate to 55 chars + an 8-char sha1 suffix.
Opus leaked a preamble before the H1
→ cleanArticle() strips any lead-in; every round's system prompt mandates the first character be the article.
Model fabricated a reviewing clinician
→ Forced the literal placeholder `[REVIEWER NAME, CREDENTIALS] — [REVIEW DATE]` — NIR fills it with a real clinician before publishing.
Scrape block-detection misfired on raw HTML size
→ Judge blocking by extracted body-text length, not byte size; wait-for-text + networkidle.
SerpApi 30s timeouts dropped 4 briefs
→ 60s timeout + 4 retries.
15 × 3
Posts × rounds
0
Failures
~25 min
Wall-clock
—
Actual cost
run cost not recorded
Action required before publishing
Every finished post still carries the literal placeholders [REVIEWER NAME, CREDENTIALS] and [REVIEW DATE]. NIR must replace these with a real reviewing clinician and date before anything goes live.