# Medium Story

> Full Medium article pipeline using native Hermes tools. Research → write → 4 parallel output agents → HTML conversion → git commit/push. Includes pre-flight infrastructure verification step, revisor-methodology fact-checking, and an anti-AI-slop writing pass (banned vocabulary, structural variety, accuracy rules).

- Skill: `ciberjohn/medium-story` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add ciberjohn/medium-story`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ciberjohn/medium-story/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: ciberjohn (https://skillmd.com/u/ciberjohn)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ciberjohn/medium-story

---


# 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)
- `git` installed 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/` and `unpublished_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:

```bash
# 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.

```bash
# 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.

```bash
# 0c. What Tailscale peers are online?
tailscale status 2>/dev/null
```

```bash
# 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

```bash
terminal(command="git -C {{MEDIUM_REPO_PATH}} pull --ff-only origin main", timeout=30)
```

If the repo doesn't exist locally, clone it:
```bash
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:
```bash
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:**
```bash
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.md` from repo root — project-level instructions
- `{{PERSONA_FILE}}` from repo root — writer persona

Find the next story number:
```bash
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):

```bash
# 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:

```bash
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:

```bash
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:

1. **Background** — Role hierarchy, capabilities, scope of impact
2. **Why This Matters Now** — Market trends, architectural convergence, timing
3. **The Gap** — Control deficiencies, comparison tables, historical blind spots
4. **Real Incidents & Attack Patterns** — 7+ documented vectors with vector/mechanism/impact/evidence tables
5. **Vendor Guidance** — Official docs, direct quotes, gaps in vendor's own guidance
6. **Industry Standards** — Framework mapping (NIST, CIS, NCSC, MITRE)
7. **Remediations** — 10+ actionable controls with rationale, licensing, config notes
8. **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.md` for the canonical output format. See `references/session-63-claude-code-commands-research.md` for a worked example.

### Step 5: Create story directory

```bash
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:

```bash
# 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 `## Sources` section (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,")` | Replace with "And" or nothing |
| "In conclusion" / "To summarize" | `search_files(pattern="In conclusion|To summarize")` | Delete — just end |
| "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")` | Delete entirely |
| Mid-sentence dashes (em — or en –) | `search_files(pattern="—|–")` | Replace with commas or parentheses |
| 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):

```bash
# 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](https://github.com/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):**

```bash
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 for `data:image` strings). 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.png` into 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.md`** for 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 content
  `On-screen text: <text>` — max 6 words, reinforce don't duplicate
  `Stage direction: <text>` — brief physical/performance cue
  `B-roll / graphic: <text>` — practical visual suggestion
- Match the format from the `short-videos` skill exactly — the old table-format (`| **Spoken** | ... |`) is deprecated. See `short_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 a `THUMBNAIL CONCEPT` line after the CTA block
- **Word count verification (mandatory):** After writing, run this to confirm using the inline-colon format regex:

```bash
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

```bash
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 Medium
- `medium-story.full.html` — styled standalone page
- `linkedin-post.fragment.html` — bare HTML
- `linkedin-post.full.html` — styled standalone page

### Step 9: Commit and push

```bash
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 against `published_index.md` titles 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.py`** in the repo root. Verify it exists before Step 8. If missing, skip HTML conversion and report.
- **Git authentication:** If `git push` fails, 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 ps` to 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_task` has `max_concurrent_children=3` by 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 `.excalidraw` file, 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: use `scripts/render_excalidraw_to_png.py` (`pip install Pillow` first). Usage: `p

…(truncated)
