Medium Story — Hermes-Native Article Pipeline
Research, write, and publish technical Medium articles using Hermes tools directly. No tmux, no Claude Code CLI, no external agent dependencies.
Prerequisites
- Hermes Agent installed and configured
- A Git repository for your Medium articles (can be private or public)
- A Medium account with RSS feed enabled
- (Optional) A GitHub personal access token (
$GH_TOKEN) for API rate-limit bypass during research - Python 3 with standard library (for HTML conversion, stats parsing)
gitinstalled and configured for push access to your repo
Configuration Variables
Set these in your environment before running the pipeline, or document them in a .env file at your repo root:
| Variable | Description | Example |
|---|---|---|
{{MEDIUM_REPO_PATH}} |
Absolute path to your local Medium articles Git repository | ~/Medium-Articles |
{{GITHUB_REPO_URL}} |
HTTPS clone URL of your Medium articles repo | https://github.com/your-username/Medium-Articles.git |
{{MEDIUM_RSS_FEED_URL}} |
RSS feed URL for your Medium publication | https://your-username.medium.com/feed |
{{STORY_NUMBER}} |
Auto-incremented sequential story number (set by pipeline) | 42 |
{{STORY_SLUG}} |
URL-friendly slug derived from the article title | why-your-mfa-does-not-help |
{{PERSONA_FILE}} |
Path to your writing persona file | templates/persona-template.md |
{{CACHE_PATH}} |
Path to the medium feed cache XML | {{MEDIUM_REPO_PATH}}/medium_feed_cache.xml |
{{PUBLISHED_AUTHOR}} |
Byline name on published articles | Your Name |
{{BYLINE_DESCRIPTION}} |
One-line role description for the byline | Strategic Technology Leader |
{{FLEET_DOCS_PATH}} |
Path to your fleet/architecture docs for infrastructure verification | ~/docs/FLEET.md |
{{GH_TOKEN}} |
(Optional) GitHub personal access token for API rate-limit bypass | ghp_xxxxxxxxxxxx |
When to Use
- User says "write a Medium article about..." or "publish a story on..."
- User shares a news item and says "write about this"
- User asks for a blog post, LinkedIn article, or YouTube script for a technical topic
Outputs Produced
| File | Format | Agent |
|---|---|---|
medium-story.md |
Markdown (2,500+ words) | Writer agent |
revisor-fact-check-report.md |
Markdown | Revisor agent (parallel) |
video-script.md |
Markdown (90s Heygen script) | Heygen agent (parallel) |
linkedin-post.md |
Markdown → HTML | LinkedIn agent (parallel) |
youtube-script.md |
Markdown (8-12min screencast) | YouTube agent (parallel) |
medium-story.fragment.html |
HTML (bare, for Medium CMS) | md_to_html.py |
medium-story.full.html |
HTML (styled standalone) | md_to_html.py |
linkedin-post.fragment.html |
HTML (bare) | md_to_html.py |
linkedin-post.full.html |
HTML (styled standalone) | md_to_html.py |
Repo & Conventions
- Repo path:
{{MEDIUM_REPO_PATH}}/ - Published stories:
published_stories/{{STORY_NUMBER}}_{{STORY_SLUG}}/ - Unpublished stories:
unpublished_stories/{{STORY_NUMBER}}_{{STORY_SLUG}}/ - Root files:
CLAUDE.md,{{PERSONA_FILE}},published_index.md,md_to_html.py,{{CACHE_PATH}} - Story numbering: Sequential, auto-incremented from highest existing folder number across both
published_stories/andunpublished_stories/
Pipeline Steps
Step 0a: Pre-existing Research Brief Check
If the story folder already contains a research-brief.md, skip Step 4 entirely. Read the existing brief, the repo's CLAUDE.md, and {{PERSONA_FILE}} for voice guidance, then go directly to Step 5 (story directory) or Step 6 (writing). This is common when the research was prepared in a prior session.
Do NOT re-run research when the brief is already compiled — this wastes time and risks overwriting carefully curated source material with shallow web searches.
Step 0b: Pre-Flight Infrastructure Verification (CRITICAL — Do Not Skip)
Before writing about any component of the fleet (profiles, containers, services, infra), verify the live state first. I have a verified pattern of claiming infrastructure exists based on memory rather than checking actual state — this produces errors that cost the user multiple correction rounds.
Anonymisation rule (2026-08-13, user correction): for PUBLIC content (Medium articles, LinkedIn, YouTube, HeyGen), default to NOT exposing real infrastructure. Do not name hosts, hostnames, IPs, zones, services, ports, or functions unless the user is explicitly prescriptive about including them. Use generic descriptions: "a couple of VPSs and a handful of LXC containers across three network zones", "internet-facing hosts", "a known sleeper host". This applies to the article body AND all script outputs (video, YouTube, LinkedIn) AND SEO tags. The author was explicit: public content must not over-expose infrastructure unless they are very prescriptive about including it. This rule has been violated before and cost multiple correction rounds.
Run ALL of these before Step 1:
# 0a. What Docker containers are actually running?
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
Do NOT rely on memory of what was running in a previous session. Containers get replaced, renamed, or removed, and my memory does not update automatically. If I claimed "Portainer was replaced by Dockhand" but Portainer is still running, I was wrong. If I described "LXC containers on a residential machine" but everything is Docker on this VPS, I was wrong. Verify before writing.
# 0b. What cron jobs are active?
cronjob action=list
Read the cron job names, schedules, and last status. Do not guess about what runs or when.
# 0c. What Tailscale peers are online?
tailscale status 2>/dev/null
# 0d. Read the existing fleet and architecture docs for your environment
# These are the authoritative source for your roles and infrastructure design
cat {{FLEET_DOCS_PATH}}
If the existing doc says something different from what I remember, the doc is right and my memory is wrong. Update my understanding before writing.
When to escalate: If I cannot determine the real state of a claim (e.g. "what replaced Portainer?" — I don't know, user said Dockhand but Portainer is running), say "I need to check with the user" rather than guessing from partial memory. Do not fabricate certainty.
Step 1: Sync the repo
terminal(command="git -C {{MEDIUM_REPO_PATH}} pull --ff-only origin main", timeout=30)
If the repo doesn't exist locally, clone it:
terminal(command="git clone {{GITHUB_REPO_URL}} {{MEDIUM_REPO_PATH}}", timeout=60)
Step 2: Cross-reference — detect newly published stories
Before writing anything new, check if any unpublished_stories/ folders have gone live on Medium.
2a. Read the RSS feed cache:
Read {{MEDIUM_REPO_PATH}}/{{CACHE_PATH}} — extract all <title> values. These are the 10 most recently published story titles.
If the cache file is missing or stale (heartbeat older than 36 hours), fetch live:
terminal(command="curl -s {{MEDIUM_RSS_FEED_URL}} 2>/dev/null | grep -oP '(?<=<title>)(.*?)(?=</title>)' | tail -n +2")
2b. Load the published index:
Read published_index.md — the running list of all known published stories.
2c. Scan unpublished_stories/:
For each folder in unpublished_stories/:
- Find the main story file (try:
medium-story.md,MEDIUM_STORY.md,*draft*.md,article-final.md,article.md,story.md) - Extract the H1 title
- If that title appears in either the RSS feed titles OR
published_index.md, the story has gone live - Move the folder:
terminal(command="git -C {{MEDIUM_REPO_PATH}} mv unpublished_stories/FOLDER published_stories/FOLDER") - Append a row to
published_index.md
2d. Commit any movements:
terminal(command="cd {{MEDIUM_REPO_PATH}} && git add published_index.md published_stories/ unpublished_stories/ && git commit -m 'chore: move newly published stories to published_stories/' && git push", timeout=30)
Skip if nothing moved.
Step 3: Read context files and find next story number
Read these files:
CLAUDE.mdfrom repo root — project-level instructions{{PERSONA_FILE}}from repo root — writer persona
Find the next story number:
terminal(command="ls {{MEDIUM_REPO_PATH}}/published_stories/ {{MEDIUM_REPO_PATH}}/unpublished_stories/ 2>/dev/null | grep -E '^[0-9]+_' | sed 's/_.*//' | sort -n | tail -1")
Next = that value + 1.
Read the most recently published story's medium-story.md as a format reference.
Step 4: Run research
Spawn a research subagent via delegate_task with the topic, repo path, and persona. Use the parallel multi-source research methodology below rather than a single generic search.
Research Methodology: Parallel Multi-Source Web Research
When browser tools are unavailable (common on VPS/headless environments), use this curl-based parallel pattern for authoritative, verifiable research.
Phase 1 — Ecosystem discovery via HN Algolia API
The HN Algolia API returns community-vetted sources (highest-point posts correlate with quality and credibility):
# Discovery: find stories on the topic
curl -sL "https://hn.algolia.com/api/v1/search?query=<topic+keywords>&tags=story&hitsPerPage=10" -o /tmp/hn-discovery.json
# Get details + comments on a specific discussion
curl -sL "https://hn.algolia.com/api/v1/items/<OBJECTID>" -o /tmp/hn-item.json
Fire 3-5 facet-shifted queries in parallel (broad topic, specific tool, adjacent trend, critical/contrarian angle). Parse with python3 reading from the saved file.
Phase 2 — Statistics via GitHub API
For every tool, framework, or project mentioned in research, pull live ecosystem metrics:
curl -sL "https://api.github.com/repos/OWNER/REPO" -o /tmp/gh-stats.json
python3 -c "import json; d=json.load(open('/tmp/gh-stats.json')); print(f'Stars: {d.get(\"stargazers_count\")}, Forks: {d.get(\"forks_count\")}')"
# Ecosystem breadth
curl -sL "https://api.github.com/search/repositories?q=<topic>&sort=stars&per_page=5" -o /tmp/gh-search.json
python3 -c "import json; d=json.load(open('/tmp/gh-search.json')); print(f'Total repos: {d[\"total_count\"]}'); [print(f'{i[\"full_name\"]}: {i[\"stargazers_count\"]}') for i in d['items'][:5]]"
Phase 3 — Deep content extraction (browser bypass)
For plain HTML pages (not SPAs), use curl + Python for structured extraction without a browser:
curl -sL "https://example.com/article" -o /tmp/article.html
python3 -c "
import re, html
with open('/tmp/article.html') as f: content = f.read()
text = re.sub(r'<script[^>]*>.*?</script>', '', content, flags=re.DOTALL)
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
text = re.sub(r'<[^>]+>', ' ', text)
text = html.unescape(text)
text = re.sub(r'\s+', ' ', text)
for line in text.split('. '):
if any(kw in line.lower() for kw in ['agent', 'adopt', 'transition', 'ecosystem', 'workflow']):
print(line[:300])
"
Security scanner workaround: Always save curl output to /tmp/ first, then process with a separate python3 -c reading from the file. Piped curl | python3 is blocked by the security scanner.
Phase 4 — Multi-source triangulation
Cross-reference every claim across at least 2 of these source types:
- Primary source (GitHub README, official docs, company blog)
- Community signal (HN discussion points, Show HN projects, GitHub stars)
- Third-party analysis (TechCrunch, industry blogs, analyst reports)
- Ecosystem proof (related repo counts, fork activity, ecosystem trends)
Phase 5 — Compile research brief
The research agent must produce a structured brief (not a stream of notes) with these sections:
- Background — Role hierarchy, capabilities, scope of impact
- Why This Matters Now — Market trends, architectural convergence, timing
- The Gap — Control deficiencies, comparison tables, historical blind spots
- Real Incidents & Attack Patterns — 7+ documented vectors with vector/mechanism/impact/evidence tables
- Vendor Guidance — Official docs, direct quotes, gaps in vendor's own guidance
- Industry Standards — Framework mapping (NIST, CIS, NCSC, MITRE)
- Remediations — 10+ actionable controls with rationale, licensing, config notes
- Source List — 8-12 authoritative URLs with descriptions
Save to the story folder as research-brief.md. This becomes the reference input for the writer agent in Step 6.
Common pitfalls in research
- GitHub API rate limiting: Unauthenticated requests capped at 60/hr. Authenticate with
curl -H "Authorization: Bearer $GH_TOKEN"when doing more than a few calls. - Medium blocks VPS IPs: Live feed fetches may 403. Use cached RSS feed or alternative sources.
- HN Algolia empty results: Simplify the query. The API is brittle with long multi-word AND queries.
- Browser-required pages: SPAs and paywalled pages will not work with curl. Note these as gaps rather than fabricating content.
- Claim verification: Never conflate vendor marketing claims with independent data. Always attribute: "the vendor says X" vs "data confirms X."
- Research brief format: See
templates/research-brief-template.mdfor the canonical output format. Seereferences/session-63-claude-code-commands-research.mdfor a worked example.
Step 5: Create story directory
terminal(command="mkdir -p {{MEDIUM_REPO_PATH}}/unpublished_stories/{{STORY_NUMBER}}_{{STORY_SLUG}}/")
Step 6: Write the article
Write the article to unpublished_stories/{{STORY_NUMBER}}_{{STORY_SLUG}}/medium-story.md. The writer MUST write within the anti-slop constraints in Step 6b and references/anti-ai-slop-writing.md — load the reference before writing and apply it silently. Two approaches:
Approach A — Direct writing (use when you have full context): If you already hold all the research context, voice guidance, and style instructions in your active context (e.g. you just read the research brief in this session), write the article directly using write_file. This is faster and avoids context fragmentation. Run the pre-publish checklist against the saved file before finishing.
Approach B — Subagent via delegate_task (use when context is saturated or research is complex): Spawn a writer subagent with full context including the research output, CLAUDE.md, and {{PERSONA_FILE}}. The subagent writes the article, runs the checklist, and saves the result.
Mandatory Post-Write Em Dash Verification ⚠️
The writer subagent reliably produces em dashes despite explicit prohibition. This is a known pattern — neither direct instruction nor the self-administered pre-publish checklist reliably catches them. After the writer returns (BOTH approaches A and B), you MUST independently verify before proceeding to Step 7:
# Count all em dashes (U+2014) in the article
grep -c '—' /path/to/medium-story.md
# List them with line numbers to identify body vs sources section
grep -n '—' /path/to/medium-story.md
# Exclude the Sources section (lines 130+) to confirm body is clean
grep -n '—' /path/to/medium-story.md | grep -v '^1[3-9][0-9]:'
Expected result: 0 em dashes in the article body. The sources section (lines ~130+) uses — as a standard metadata separator between URLs and descriptions — those are acceptable and should NOT be replaced. FILE placeholders ([*FILES*: filename — description]) also use — and are acceptable.
If any body em dashes remain, fix them before dispatching output agents:
Apply these replacement patterns to fix common writer-generated em dashes:
| Em dash pattern (before) | Replace with (after) |
|---|---|
Word — explanatory text — |
Word (explanatory text) — use parentheses for paired asides |
important thing — here's why |
important thing: here's why — use colon for explanatory clauses |
X, Y and Z — verb phrase |
X, Y and Z verb phrase — remove, the listing flows without punctuation |
statement one — statement two |
statement one. Statement two — split into two sentences |
word — phrase (single, emphasis) |
word, phrase — use comma |
word — phrase (single, dramatic pause) |
word: phrase — use colon |
Most fixes are: paired dashes → parentheses, single explanatory dashes → colon, emphasis dashes → comma, and breaks between independent clauses → full stop + new sentence.
Do not fix em dashes in:
- The
## Sourcessection (metadata separators) [*FILES*: ...]placeholders (diagram descriptions)
Writer Voice & Style Guide (from published Medium articles)
This section is derived from analysing your published stories — the voice patterns, sentence structures, and rhythms he actually uses, not a theoretical style guide.
Core Voice
- Practitioner, not pundit. The voice speaks from experience managing real infrastructure at scale. He has the war stories and references them.
- British English always. -ise not -ize, colour, favour, behaviour, defence, whilst, amongst, licence, harbour, neighbour, realise, analyse, organise, recognised.
- Dry, understated wit. Not forced humour. Surfaces naturally when the situation is absurd (vendor overclaiming, predictable failures). Examples: "The catch, in this case, is that there is not one" / "I collect them the way people collect gym memberships: with great optimism and almost zero follow-through."
- Professional skepticism. Questions vendor claims. Distinguishes between "I assume" and "I have verified." Calls out lazy thinking directly but without aggression.
- Leads with impact, then explains mechanism. Never opens with theory and works towards relevance — does the opposite. The human/operational consequence comes first, the technical explanation second.
Sentence Construction
- Complex, multi-clause sentences. The voice writes long, grammatically precise sentences — 25-50 words is normal. Clauses separated by commas, colons, and parenthetical qualifiers.
- Parenthetical asides extensively used — "(a real risk with any relatively young cryptographic scheme)" / "(not months, two weeks)" — they add precision and personality.
- Very few one-sentence paragraphs. Reserved for emphasis. A one-sentence paragraph is a statement meant to land hard.
- Paragraphs are substantial — 4-8 sentences, 80-150 words. Dense with information. Not skimmable; designed to be read.
- Colons used extensively for explanation: "The word 'bearer' is doing enormous work here. Whoever holds the token has the access. Full stop." / "The idea behind a hybrid approach is elegant in its conservatism: you get the security of the new... and the security of the old..."
Structure Patterns
- Hook is a strong, often single paragraph that reframes the topic or states the surprising truth. Can be 3-7 sentences long with a pay-off at the end.
- Roadmap paragraph follows the hook — tells the reader what the article will cover, often as a single long sentence: "This article covers the full picture: how BITB works technically, who has built it into production phishing campaigns, what the statistics say..."
---horizontal rules separate major sections. They create natural pause points for readers.- Section headers are provocative or unexpected, not generic category labels. "Why Your MFA Does Not Help" vs "MFA Analysis." "The Bug I Actually Care About Most" vs "Critical Bug." "The Release Nobody Will Read Properly" vs "Overview."
- Subheadlines/bold leads under section headers set the scene before diving in.
- Ends with "What to Do" or "The Takeaway" — concrete, actionable, prioritised. Not a summary. New content that synthesises and directs.
- "Sources" section at the very end with live hyperlinks. Not a reference list — a short list of the key documents cited, each with a brief description.
Distinctive Phrasing Patterns
These patterns appear across articles — they are fingerprints of the voice:
| Pattern | Example |
|---|---|
| "This is not [X]. It is [Y]." | "This is not a clever exploit of a Microsoft bug." |
| "Not because... but because..." | "Not because the finding is complex... but because it is so straightforward..." |
| "Let us be precise about this, because the nuance matters operationally" | Direct quote — used when a distinction matters |
| "Read that again." | Breaks the fourth wall to emphasise — use sparingly, once per article max |
| "The [adjective] [noun] that should [verb]" | "The release that quietly closes a directory traversal issue..." |
| "I have seen [X] than I would like to admit" | Self-aware honesty about industry patterns |
| "This is the kind of [thing] that [consequence]" | "This is the kind of bug that erodes the thing infrastructure teams depend on most" |
| "I want to be unambiguous about that, because..." | Used when the voice wants to prevent misinterpretation |
| "Someone out there, right now, [is doing X]" | Places the reader in the real-world scenario |
| "That is not a [position]. That is someone who has [experience]." | Elevates or reframes a judgement |
What This Voice NEVER Writes
These patterns have been explicitly rejected and must NEVER appear:
- ❌ Mid-sentence dashes (em dashes or en dashes as parenthetical breaks) — use commas or parentheses
- ❌ Corporate jargon: "data estate", "uninsured liability", "measured in inconvenience"
- ❌ AI transition phrases: "Here's what matters", "What this means is", "Here's the thing", "I need you to understand something"
- ❌ Fluffy openers: "In today's digital landscape", "In an era of rapid technological change"
- ❌ Credentials-first openings: "With over 20 years of experience in infrastructure..."
- ❌ Ad-copy hooks: "Not hypothetically. In the next 60 seconds." / Emoji-lead headlines
- ❌ Paired oppositions: "measured in inconvenience" / "measured in survival" (language-model parallelism)
- ❌ "It is important to note that" — just note it
- ❌ "Furthermore", "Moreover", "Additionally" — use "And" or just continue the sentence
- ❌ American spellings — ever
The Test
Read the dialogue aloud. If it sounds like a LinkedIn post from someone trying to sound authoritative, cut it. If it sounds like someone who just came off a conference call with a vendor who overpromised and underdelivered — that is the voice.
Attribution / Byline Format
When writing under the author's name, use this attribution format:
{{PUBLISHED_AUTHOR}} — {{BYLINE_DESCRIPTION}}
Do NOT use the full LinkedIn header (Strategic Technology Leader | Platform Engineering & Agentic AI Governance | Infrastructure, Cybersecurity & Cloud...). The user prefers a focused byline that emphasises strategic technology leadership and the AI angle, not the full credential list.
The article title should never include "Medium" (avoid "Medium story", "on Medium"). Use "article", "story", or just the hook.
Pre-Publish Checklist
Before saving any article, run this grep check against the draft. In Hermes-native mode, use the search_files tool with path=<article-path>:
| Pattern | search_files command (Hermes-native) | Fix |
|---|---|---|
| "delve", "tapestry", "realm", "leverage" | search_files(pattern="delve|tapestry|realm|leverage") |
Replace with plain English |
| "in order to" | search_files(pattern="in order to") |
Replace with "to" |
| "it is worth [verb]ing" | search_files(pattern="it is worth") |
Replace with "Let me [verb]" |
| "it is important to note that" | search_files(pattern="important to note") |
Delete — just say the thing |
| "Furthermore," / "Moreover," | `search_files(pattern="Furthermore, | Moreover,")` |
| "In conclusion" / "To summarize" | `search_files(pattern="In conclusion | To summarize")` |
| "Not only... but also" | search_files(pattern="not only.*but also") |
Simplify |
| "By [verb]ing..." (sentence opener) | search_files(pattern="^By [A-Za-z]+ing") |
Rephrase |
| "One of the most [adjective]" | search_files(pattern="One of the most") |
Be specific or delete |
| "In today's [X]" / "In an era of" | `search_files(pattern="today's digital | In an era of")` |
| Mid-sentence dashes (em — or en –) | `search_files(pattern="— | –")` |
| American -ize/-ization spellings | search_files(pattern="ization") |
Replace with -isation/-isation (British). Note: proper nouns such as "Modernization" (Microsoft's Rapid Modernization Plan) are acceptable and should NOT be changed. |
| Direct quotes from US sources (NIST, CIS, vendor docs) | manual review of "quoted text" containing British -our/-ise/-isation spellings |
Preserve ORIGINAL spelling inside quotation marks. US government docs use "authorized", not "authorised". British English rules apply to your own prose, NOT to direct quotes. Flag any changed quote as FAIL in the revisor report. |
| Infrastructure claims | Verify against live state (docker ps, crontab -l, etc.) — never guess | |
| Diagram links | Use [*FILENAME.png*] placeholders, not excalidraw #json= URLs. Verify all [FILES] placeholders are present: search_files(pattern="\\[\\\\*FILES") |
|
| [FILES] placeholder count | search_files(pattern="FILES") — verify count matches expected number of diagrams |
|
| PII / real paths in article body | Check for any system paths, hostnames, or personal URLs in final text |
Executing the checklist (Hermes-native)
Run these searches in parallel (they are independent reads):
# Batch 1 — Forbidden vocabulary and AI-isms
search_files(pattern="delve|tapestry|realm|leverage|in order to|it is worth noting|furthermore|moreover|not only but also|in conclusion|in today's|in an era of")
# Batch 2 — Dashes and American spellings
search_files(pattern="—|–")
search_files(pattern="\\bize\\b|\\borganization\\b|\\bcolor\\b|\\bfavor\\b|\\bbehavior\\b|\\bdefense\\b|\\bcenter\\b|\\blicense\\b")
search_files(pattern="ization") # review results manually for proper nouns
# Batch 3 — [*FILES*] placeholder presence
search_files(pattern="FILES")
# Batch 4 — Forbidden transition phrases
search_files(pattern="here's what matters|here's the thing|what this means is|I need you to understand|In today's digital|In an era of|data estate|uninsured liability|measured in inconvenience")
False-positive note: The words "delve", "tapestry", "realm", "furthermore", "not only but also", and "in order to" may legitimately appear in a meta-reference — for example, when the article describes this checklist itself ("A pre-publish checklist blocks ten categories of AI-typical language: 'delve', 'tapestry', 'realm'..."). In that case, the words are being quoted, not used. The revisor should flag them in the report but note the meta-reference context. Do not rewrite the meta-reference line. Similarly, "Modernization" in "Microsoft's Rapid Modernization Plan" is a proper noun and should not be flagged.
Step 6b: Anti-Slop Writing Pass (Mandatory)
Before dispatching the output agents, verify the article against the anti-AI-slop constraint set in references/anti-ai-slop-writing.md (adapted from jalaalrd/anti-ai-slop-writing, MIT — Carnegie Mellon 2025, Wikipedia Signs of AI Writing, Buffer 52M post analysis). This is a quality gate on top of the Pre-Publish Checklist: the checklist targets the voice's specific failures, Step 6b is the broader statistical detection net.
Manual structural checks (cannot be grepped — review the draft for these):
- No Rule of Three — groupings of exactly three items/paragraphs in a row; break to two, four, or one
- No three consecutive sentences of the same length
- No parataxis — three or more short declarative sentences in a row; connect with subordinate clauses, conjunctions, semicolons
- No hedging seesaw — positions stated plainly; counterpoints get one sentence max
- No passive construction — "is being done", "was found to be" → active
- No "As [role], I..." openers
- Not every paragraph ends with a transition — let some end abruptly
- No fabricated data, studies, statistics, or quotes — hypotheticals flagged with "imagine" / "suppose"
- Punctuation limits: max one exclamation per 1,000 words, max one ellipsis per piece, semicolons used naturally
Grep checks (run in parallel with the em dash verification):
search_files(pattern="delve|tapestry|testament|vibrant|pivotal|crucial|intricate|meticulous|bolster|garner|underscore|interplay|multifaceted|nuanced|foster|leverage|utilize|commence|facilitate|encompass|paramount|groundbreaking|cutting-edge|game-changing|transformative|revolutionis|seamless|robust|comprehensive|endeavour|aforementioned|harnessing|spearheading|navigating|showcasing|highlighting|emphasizing|enhancing|unprecedented|remarkable|stunning|profound|synergy|pain points|value add|moving forward|touch base|circle back|rest assured|it goes without saying|at its core|in the realm|when it comes to|a testament to|not just.*but|at the end of the day|bottom line|here's the thing|here's the deal|without further ado|in a nutshell|buckle up|next level|unlock the power|empower|elevate|streamline|supercharge|bridge the gap|move the needle|firstly|secondly|thirdly")
Context rules: "robust" is banned outside engineering contexts; "empower/elevate/streamline" may legitimately appear in a direct vendor quote — flag but preserve quoted spelling per the revisor methodology. British forms of banned words (revolutionise, endeavour) count as violations too.
Fix policy: any hit that is a genuine violation gets fixed in the draft before Step 7. Hits inside direct quotes, the Sources section, or [FILES] placeholders are exempt.
Propagation to output agents: the other pipeline outputs (Heygen script, LinkedIn post, YouTube script) are written under the same constraints. Include references/anti-ai-slop-writing.md in the delegate_task context for Agents B, C, and D in Step 7.
Step 6c: Story illustrations — OpenRouter + recurring character (Recommended for stories with images)
- Load an image-generation skill (e.g.
openrouter-image-gen: cheap image models over a chat-completions endpoint, no GPU; the image comes back embedded in the JSON — walk the whole response recursively fordata:imagestrings). Generate the hero illustration after the article is final, before packaging. - The recurring character (series rule): define ONE cartoon character and reuse it in every story illustration so the series reads as a single body of work. Example spec you can customise:
- A cartoon person of your choosing (example used by the author: stout/bald man, short goatee, round glasses, warm smile, plain black T-shirt)
- Their workspace: cluttered desk, monitors with data visuals, shelves with books and electronics, warm lamp, string lights, night window
- A recurring motif: sci-fi paraphernalia (model starship, poster) — describe generically, never ask for trademarked logos
- Palette: cool blues/teals/purples with warm amber accents; wide 16:9; editorial cartoon style; "no real logos, no brand names, no watermark"
- The Illustration Engineer: treat illustration as a dedicated role. Either spawn a subagent ("Illustration Engineer") with the character spec, the image-generation skill and the article's topic as context, or run the generator directly. The engineer owns: prompt (topic-to-image translation), generation, verification, saving
illustration.pnginto the story folder, and returning the[*FILES*: illustration.png — description]line. - Verification (always): check the generated image with a vision-capable model — the generator can lie about what it drew. Confirm the character is present, requested props are present, and no text is garbled. Regenerate if any check fails.
- Animated assets (optional): for diagrams and force graphs, render animated GIFs programmatically from the actual data or physics (e.g. Pillow), not an image model. Keep them abstract — no real note content, nothing sensitive.
Step 7: Run four parallel output agents
Spawn agents via delegate_task in two batches because of the max_concurrent_children=3 limit (configurable in config.yaml under delegation.max_concurrent_children, but default is 3).
Batch 1 (3 agents — use delegate_task with tasks: [...]):
- Read each agent's file from the story folder after the batch completes to confirm output was written.
Batch 2 (1 agent — separate delegate_task call):
- The YouTube agent. Dispatch this immediately after Batch 1 — it does not need to wait for Batch 1's agents to finish; the two batches run concurrently. The total parallel count is 3 + 1 = 4, but they are submitted as separate calls so the system never sees more than 3 at once.
Important: Each agent must be told to write its output to the story folder using absolute paths. Do not rely on relative paths in subagent contexts.
Agent A — Revisor (revisor-fact-check-report.md)
- Improve the story: fix British English, remove dashes, sharpen headings
- Fact-check every statistic and claim
- Flag any unverifiable or misleading statements
- Check for mid-sentence dashes (em dashes or en dashes used as parenthetical breaks)
- Report findings as actionable items — do not rewrite the article
- See
references/revisor-methodology.mdfor the structured fact-checking methodology: GitHub API verification patterns, grep commands for British English and Americanism detection, dash audit commands, source URL validation, and report formatting.
Agent B — Heygen Script (video-script.md)
- 90-second teleprompter script for the author's HeyGen digital twin
- Timestamped blocks:
[HOOK] 00:00–00:05,[SETUP] 00:05–00:20,[INSIGHT 1] 00:20–00:40,[INSIGHT 2] 00:40–01:00,[PAYOFF] 01:00–01:20,[CTA] 01:20–01:30 - Each block uses inline colons (NOT markdown tables) with four fields:
Spoken: <text>— spoken word contentOn-screen text: <text>— max 6 words, reinforce don't duplicateStage direction: <text>— brief physical/performance cueB-roll / graphic: <text>— practical visual suggestion - Match the format from the
short-videosskill exactly — the old table-format (| **Spoken** | ... |) is deprecated. Seeshort_videos/folders for worked examples. - Target spoken word count: 200-230 words total
- British English throughout
- No mid-sentence dashes in spoken text
- CTA must be platform-neutral (no "swipe up", no "link in bio", no "link in description" — use "full article linked below" or "follow for more")
- Include a
CAPTION (universal)section (3-5 lines, 5 hashtags, no "link in bio") and aTHUMBNAIL CONCEPTline after the CTA block - Word count verification (mandatory): After writing, run this to confirm using the inline-colon format regex:
python3 -c "
import sys, re
text = open(sys.argv[1]).read()
blocks = re.findall(r'^Spoken:\s*(.*?)$', text, re.MULTILINE)
total = sum(len(b.split()) for b in blocks)
print(f'Spoken word count: {total}')
for i, b in enumerate(blocks):
print(f' Block {i+1}: {len(b.split())} words')
if total < 200: print('TOO LOW — expand spoken sections')
elif total > 230: print('TOO HIGH — trim spoken sections')
else: print('Within target range.')
"
Agent C — LinkedIn Post (linkedin-post.md)
- Standalone post — NOT marketing for the article. It stands alone.
- Hook line in first sentence
- 3-6 short paragraphs, each punchy and self-contained
- 3,000 character hard limit (LinkedIn cap)
- Exactly 5 hashtags at the end
- No "link in comments" — the Medium article URL goes IN the post body
- No "watch my video" language
- British English, no mid-sentence dashes
- No "I've been in infrastructure for X years" openings
- End with a question or provocation that invites comments
Agent D — YouTube Script (youtube-script.md)
- 8-12 minute screencast script for recording with OBS (screen capture + webcam)
- NOT an avatar/Heygen script — real person, real voice
- Format: conversational talking points the speaker can follow naturally
- Include: suggested screen content at each point, chapter markers, thumbnail concept
- YouTube SEO: title + description + tags
- British English
Step 8: HTML conversion
terminal(command="cd {{MEDIUM_REPO_PATH}} && python3 md_to_html.py {{STORY_NUMBER}} --select medium-story,linkedin-post", timeout=30)
Produces (in the story folder):
medium-story.fragment.html— bare HTML, paste into Mediummedium-story.full.html— styled standalone pagelinkedin-post.fragment.html— bare HTMLlinkedin-post.full.html— styled standalone page
Step 9: Commit and push
terminal(command="cd {{MEDIUM_REPO_PATH}} && git add unpublished_stories/{{STORY_NUMBER}}_{{STORY_SLUG}}/ && git commit -m 'Add story {{STORY_NUMBER}}: [topic summary]' && git push", timeout=30)
Pitfalls
- Feed cache staleness: If
{{CACHE_PATH}}is older than 36 hours, fetch live. If live fetch fails (403 from Medium's datacenter block), fall back to matching againstpublished_index.mdtitles only, and flag the staleness. - Story number collision: The pipeline auto-increments from folder listings. If you manually create folders, use the next available number.
- HTML conversion requires
md_to_html.pyin the repo root. Verify it exists before Step 8. If missing, skip HTML conversion and report. - Git authentication: If
git pushfails, the SSH key or credential helper may not be configured. Report the error — don't lose the article. - Mid-sentence dashes forbidden: The writer subagent produces em dashes despite explicit prohibition — this is a known pattern. The self-administered pre-publish checklist does NOT reliably catch them. You MUST independently grep the article body for em dashes after the writer returns (see Step 6 — Mandatory Post-Write Em Dash Verification) and fix them before dispatching output agents. Do NOT skip this step; the revisor will flag them but fixing them post-revisor wastes a round-trip.
- Infrastructure claims require live verification: Before describing any service, container, or tool in the article, verify against live state. Run
docker psto check what's actually running. Check the last backup date. An incorrect infrastructure claim in the article will be spotted by technical readers and erodes trust. When in doubt, say "let me check" rather than guessing from memory. - Research agent context window: For very broad topics, the research agent may produce more text than fits in
context. Keep the research brief to essential findings. Pass the full research as a file read reference rather than inline text. - LinkedIn character count: The 3,000 char limit is hard. The revisor checks it, but verify manually.
- Parallel agent limit (4 tasks):
delegate_taskhasmax_concurrent_children=3by default. You cannot dispatch all 4 agents in one call. Split into Batch A (3 agents: Revisor, Heygen, LinkedIn) and Batch B (1 agent: YouTube). Both batches run concurrently — the system sees 3 + 1 across two calls, never 4 at once. - Diagram PNG export for articles: If the article references Excalidraw diagrams, the
#json=URLs from excalidraw.com do not render inline in Medium. After generating the.excalidrawfile, render it to a transparent-background PNG and place in the story folder. Use[*FILENAME.png*]as a placeholder that the user replaces with the uploaded image when publishing. The rendering approach when browser tools are unavailable: usescripts/render_excalidraw_to_png.py(pip install Pillowfirst). Usage: `p
…(truncated)