<!-- researching-content -->
<!-- One Claude skill, flattened into a single markdown file. -->
<!-- To install: create ~/.claude/skills/researching-content/ and split the FILE blocks below back out. -->
<!-- SKILL.md is everything above the first FILE heading. -->

---
name: researching-content
description: Researches a content topic and returns ranked findings, gaps, and sourced numbers as a markdown list, without writing an outline or a draft. Analyzes the SERP, reads the top four competing pages, records what each page promises and fails to deliver, and optionally pulls AI-citation data when a visibility connector is available. Use when asked to research a topic, find content gaps, check what is ranking, gather data points for an article, size up competitors, or decide whether a topic is worth writing. Do not use when the request is for a brief (use briefing-content) or a draft (use drafting-content).
---

# Researching Content

Returns findings. Writes no outline, no draft, and no file unless asked for one by name.

That boundary is the point. A skill permitted to research and write will write, and the piece then rests on whatever surfaced first instead of on a decision someone made.

## Workflow

Copy this checklist and track progress:

```
Research progress:
- [ ] Step 1: Fix the reader and the question
- [ ] Step 2: Pull and filter the SERP
- [ ] Step 3: Read the top four
- [ ] Step 4: Rank the gaps
- [ ] Step 5: Pull citation data (skip if no connector)
- [ ] Step 6: Return the findings block
```

### Step 1: Fix the reader and the question

Write one line: who reads this, and what problem brings them here.

Ask the user if it is not stated, then continue. Every finding below gets filtered against that line, and an interesting finding that fails the filter gets dropped.

### Step 2: Pull and filter the SERP

Search the primary keyword and take the top 10 organic results.

Drop any result whose domain matches `reference/blocked-domains.md`. Keep the dropped set in a separate list. When a community or video result outranks the vendors, that is a finding about tone, and it belongs in the output.

### Step 3: Read the top four

Fetch each page. Record:

- Word count and format: guide, listicle, comparison, alternatives, how-to, glossary
- Full H2 and H3 outline
- Structural elements carried: tables, FAQ, code blocks, downloads, screenshots, original data
- Author: named person with a title, or brand voice
- Publish and last-updated dates
- The three things it covers best
- **What it says it will cover and does not**

The last field produces most gaps. Pages promise a walkthrough and deliver a summary far more often than they omit a topic outright.

### Step 4: Rank the gaps

A gap qualifies on all three counts:

1. The reader from Step 1 wants it
2. All four top pages are thin on it
3. It can be covered from evidence in hand or obtainable

Write each gap starting with a verb, so it reads as work. "Publish a working file" beats "nobody publishes files."

Rank by how hard a competitor would find it to copy. A gap anyone can fill next week is worth less than one needing data or an artifact you own.

### Step 5: Pull citation data

Skip this step when no AI-visibility connector is configured. Steps 1 to 4 stand on their own.

With a connector, use fully qualified MCP tool names (`ServerName:tool_name`) or the call will fail to resolve. Pull, in this order:

1. The citation category split for the last 90 days
2. Which page types on the owned domain earn citations
3. Which third-party domains get cited on the category's questions

Two failure modes mislead rather than announce themselves:

- **Workspace and account identifiers are typed.** Pass a string where the schema wants an integer and the failure reads like a permissions error.
- **Any tool that sets a current workspace holds session state.** Run those calls in sequence. In parallel, the data returned belongs to whichever call landed last.

An empty account is common and is not an error. Skip it and pick another. Stop sampling once new accounts only confirm ranges already seen.

For benchmark ranges observed across a B2B SaaS portfolio, see `reference/citation-benchmarks.md`.

### Step 6: Return the findings block

Output this in chat. Write nothing to disk unless asked.

```markdown
## Reader
[one line: who, and the problem]

## SERP shape
[dominant format, word-count range, what every page carries]

## Top four
1. [domain] — [format], [words] — best at: [x] — thin on: [y]

## Community and video results
[any that outranked vendors, and what that implies about tone]

## Gaps, ranked by defensibility
1. [verb-first sentence] — defensible because [reason]

## Numbers I can use
- [figure] — [source, linked] — supports [which point]

## Numbers I could not verify
- [figure] — [where it appears] — [why it failed]

## Recommended angle
[two sentences: the gap to lead on, and the format]
```

Keep the unverified list in the output. It stops the same figure getting picked up three articles later.

## Verification loop

Before returning, check each line:

1. Every gap traces to a page that was actually fetched
2. Every figure carries a source URL, or sits in the unverified list
3. Every finding speaks to the reader from Step 1

If any check fails, return to the step that produced the line and correct it.

## Test it

Three scenarios worth running:

1. A keyword where community results dominate the SERP. The output should flag tone as a finding.
2. A keyword with no connector configured. The output should still rank gaps from the SERP alone.
3. A request phrased as "write me an outline for X". The skill should return findings and decline the outline.

## What still needs a person

Which gap is worth a week. That depends on what can be proved, who the reader is, and what the site can rank for. Treat any tool claiming to make that call with suspicion.


---

## FILE: `researching-content/reference/blocked-domains.md`

Save this block at `~/.claude/skills/researching-content/reference/blocked-domains.md`

```markdown
# Blocked Domains

## Contents
- Why the filter exists
- The list
- Situational additions
- Matching rule

## Why the filter exists

Competitors get read to learn what page structure the SERP rewards. A discussion thread at position two is not a structure that can be copied into a layout, and counting it distorts every average calculated: word count, heading depth, format mix.

So it leaves the structural set and stays in the brief as a finding. Where community or video results own the top of a SERP, that says something about the tone the page needs, and that belongs in the intent section.

## The list

**Discussion and Q&A**
```
reddit.com
quora.com
stackoverflow.com
stackexchange.com
news.ycombinator.com
```

**Social and professional networks**
```
x.com
twitter.com
linkedin.com
facebook.com
instagram.com
tiktok.com
pinterest.com
threads.net
```

**Video**
```
youtube.com
vimeo.com
```

**Free-publish platforms**
```
medium.com
substack.com
slideshare.net
dev.to
hashnode.dev
```

**Marketplaces and review aggregators**
```
amazon.com
ebay.com
walmart.com
g2.com
capterra.com
trustradius.com
getapp.com
softwareadvice.com
```

Review aggregators are the judgment call. On a "best tools" query they are the competition and stay in. On a how-to or definitional query they are noise. Decide per brief and record which way it went.

## Situational additions

Add per brief when a domain clearly is not a ranking competitor for the query:

- Wikipedia, on anything non-definitional
- GitHub, on anything not developer-facing
- Government and standards bodies, where they rank as the source rather than a competing page
- The client's own domain, when the job is a refresh instead of a new page

## Matching rule

Lower-case the URL, match on substring, drop on any hit.

Substring matching catches subdomains and country variants (`uk.linkedin.com`, `old.reddit.com`) without a separate entry for each.
```


---

## FILE: `researching-content/reference/citation-benchmarks.md`

Save this block at `~/.claude/skills/researching-content/reference/citation-benchmarks.md`

```markdown
# Citation Benchmarks

## Contents
- Coverage and limits
- Category split
- Community source order
- Owned formats that earn citations
- How to use these

## Coverage and limits

Aggregate ranges across a B2B SaaS client portfolio, 90-day windows, never a single account. Accounts under 500 citations in a 90-day window are excluded, because small samples swing far enough to widen every band without adding signal.

Do not carry these into a category outside B2B SaaS.

## Category split

| Who earns the citation | Share of all citations |
|---|---|
| Sources that never mention the brand | 53% to 86% |
| A competitor | 4% to 20% |
| Independent media, analysts, vertical review sites | 0.7% to 19% |
| Community and video sources | 2% to 11% |
| The brand's own pages | 1% to 12% |
| Roundups that mention without citing | 0.2% to 5% |

The first row sets the ceiling on visibility work more often than any competitor does. When the owned share sits at the low end, writing more pages is rarely the whole answer.

## Community source order

Holds almost everywhere in B2B SaaS, in this order:

1. Reddit
2. YouTube
3. LinkedIn
4. Medium
5. Quora

Facebook, GitHub, and Stack Overflow appear and are category-dependent.

## Owned formats that earn citations

Five formats repeat across categories:

1. Category listicles
2. Alternatives and comparison pages
3. Explainers that define a category
4. Integration pages
5. Product announcements

## How to use these

Compare a fresh 90-day pull against the bands above. A pull that sits outside a band is either a genuine outlier worth investigating or a sign the query set is wrong. Check the query set first.

When a category returns the brand absent from most answers, the work is finding the questions it is missing from and covering those in the formats above.
```



<!-- ============================================ -->


<!-- briefing-content -->
<!-- One Claude skill, flattened into a single markdown file. -->
<!-- To install: create ~/.claude/skills/briefing-content/ and split the FILE blocks below back out. -->
<!-- SKILL.md is everything above the first FILE heading. -->

---
name: briefing-content
description: Turns research findings into a writer-ready content brief using a section order that cannot be reordered or skipped. Fills meta, reader, search intent, competitor analysis, angle, keyword map, title options, and a section-by-section outline carrying word budgets, format tags, and the evidence each point rests on. Use when asked for a content brief, an article outline, a writer brief, or a content plan, or when a topic has been researched and is ready to plan. Do not use to research a topic from scratch (use researching-content) or to write prose (use drafting-content).
---

# Briefing Content

## The one rule

The section order in `reference/brief-template.md` never changes. Nothing gets moved, skipped, or added.

A section missing from the brief goes missing from the draft, and that surfaces two stages later when the draft already exists. An inapplicable field costs one line reading "not applicable" plus the reason. A deleted field costs a rewrite.

## Workflow

Copy this checklist and track progress:

```
Brief progress:
- [ ] Step 1: Collect the four inputs
- [ ] Step 2: Filter the competitor set
- [ ] Step 3: Set the word budget per section
- [ ] Step 4: Map secondary keywords to sections
- [ ] Step 5: Write the outline with evidence
- [ ] Step 6: Run the checks, then output
```

### Step 1: Collect the four inputs

Ask for whatever is missing, then continue:

1. Primary keyword
2. The reader in one line, and the problem bringing them to the page
3. Research findings, or permission to run `researching-content` first
4. Content type: guide, listicle, comparison, alternatives, how-to, glossary, or data piece

Everything else is derivable.

### Step 2: Filter the competitor set

Drop blocked domains from the structural analysis using `reference/blocked-domains.md`, and record the reason in the intent section rather than deleting the observation.

### Step 3: Set the word budget per section

Total target first, from the top four. Then divide across sections.

Skipping this step is why drafts come back with a 900-word introduction and a two-line conclusion. A section carrying a number gets written to that number.

Tag every section with a format: `paragraph`, `bullets`, `table`, `code`, `diagram`, or `mixed`. Two consecutive sections sharing a tag is a flag; change one.

### Step 4: Map secondary keywords to sections

Split into must-use and nice-to-have, and give each one a destination section.

An unmapped keyword gets force-fitted by whoever drafts. A mapped one lands in a sentence that was going to exist anyway.

### Step 5: Write the outline with evidence

Per section: heading, word target, format tag, the points to make, and the evidence each point rests on.

Where a point needs a figure, write the figure and its source into the brief. A brief saying "add a stat here" produces a draft with an invented stat.

### Step 6: Run the checks, then output

Fill `reference/brief-template.md` exactly and output as markdown in chat, or to a file when asked.

## Verification loop

Run every check. Fix and re-run until all pass.

| Check | Fails when |
|---|---|
| Word targets | Any section carries no number |
| Format variety | Two consecutive sections share a tag |
| Keyword map | A must-use keyword has no section |
| Evidence | A point needs a figure and the brief says "add a stat" |
| Reader | "What they already know" is empty, so the draft will over-explain |
| Angle | "What we are leaving out" is empty, so scope will drift |
| Titles | Fewer than two title options carry the primary keyword |

## Test it

1. A brief where no research was supplied. The skill should ask for findings or offer to run `researching-content`.
2. A topic where one section genuinely does not apply. The output should carry "not applicable" and a reason, with the heading intact.
3. A brief for a comparison page. Format tags should vary and at least one section should be tagged `table`.

## What still needs a person

The angle, and what to leave out. The brief is the last point where a wrong call is cheap, so those two fields stay human. Everything after them is bookkeeping that this skill does so nothing goes missing between the decision and the draft.


---

## FILE: `briefing-content/reference/blocked-domains.md`

Save this block at `~/.claude/skills/briefing-content/reference/blocked-domains.md`

```markdown
# Blocked Domains

## Contents
- Why the filter exists
- The list
- Situational additions
- Matching rule

## Why the filter exists

Competitors get read to learn what page structure the SERP rewards. A discussion thread at position two is not a structure that can be copied into a layout, and counting it distorts every average calculated: word count, heading depth, format mix.

So it leaves the structural set and stays in the brief as a finding. Where community or video results own the top of a SERP, that says something about the tone the page needs, and that belongs in the intent section.

## The list

**Discussion and Q&A**
```
reddit.com
quora.com
stackoverflow.com
stackexchange.com
news.ycombinator.com
```

**Social and professional networks**
```
x.com
twitter.com
linkedin.com
facebook.com
instagram.com
tiktok.com
pinterest.com
threads.net
```

**Video**
```
youtube.com
vimeo.com
```

**Free-publish platforms**
```
medium.com
substack.com
slideshare.net
dev.to
hashnode.dev
```

**Marketplaces and review aggregators**
```
amazon.com
ebay.com
walmart.com
g2.com
capterra.com
trustradius.com
getapp.com
softwareadvice.com
```

Review aggregators are the judgment call. On a "best tools" query they are the competition and stay in. On a how-to or definitional query they are noise. Decide per brief and record which way it went.

## Situational additions

Add per brief when a domain clearly is not a ranking competitor for the query:

- Wikipedia, on anything non-definitional
- GitHub, on anything not developer-facing
- Government and standards bodies, where they rank as the source rather than a competing page
- The client's own domain, when the job is a refresh instead of a new page

## Matching rule

Lower-case the URL, match on substring, drop on any hit.

Substring matching catches subdomains and country variants (`uk.linkedin.com`, `old.reddit.com`) without a separate entry for each.
```


---

## FILE: `briefing-content/reference/brief-template.md`

Save this block at `~/.claude/skills/briefing-content/reference/brief-template.md`

```markdown
# Brief Template

## Contents
- The template
- Checks before handing over

## The template

Fixed order. Nothing moved, skipped, or added. An inapplicable section carries "not applicable" and a reason.

```markdown
# CONTENT BRIEF: [title]

## META
- Primary keyword:
- URL slug:
- Meta title: [under 60 characters]
- Meta description: [150 to 160 characters, keyword front-loaded]
- Target word count:
- Content type: [guide | listicle | comparison | alternatives | how-to | glossary | data]
- Funnel stage: [TOFU | MOFU | BOFU]
- Author:

## READER
- Who:
- The problem bringing them here:
- What they already know (do not explain these):
- What they do next if the page works:

## SEARCH INTENT
- Primary intent: [informational | commercial | transactional | navigational]
- Competing intents in the SERP, and how well each is served:
- The intent nobody serves:
- How the query shows up in AI assistants:

## COMPETITOR ANALYSIS
Top four, blocked domains excluded:
1. [url] — [position] — [type] — [words] — best at: — thin on:

- Structural elements every one carries:
- Structural elements none carries:
- Word count benchmark: avg [N], target [N], and why
- Community or video results that outranked vendors, and what that implies:

## ANGLE
- The gap we lead on:
- Why it is defensible:
- What we are leaving out:

## SECONDARY KEYWORDS
Must-use:
- [keyword] -> [section]

Nice-to-have:
- [keyword] -> [section]

## TITLE OPTIONS
Chosen: [title] — because [reason]
Alternates:
-

## OUTLINE

### H2: [heading]
- Words: [N]
- Format: [paragraph | bullets | table | code | diagram | mixed]
- Points to make:
- Evidence each point rests on (figure + source):
- Writing note: [what this section does for the reader, and how it hands off to the next]

## ASSETS
| # | Asset | Type | Section | What it has to show |
|---|---|---|---|---|

## LINKS
- Internal, with anchor text (2 to 4 words):
- External, with the claim each supports:

## SCHEMA
- Types:
- FAQ questions, if any:

## DEFINITION OF DONE
- [ ] Every section at its word target
- [ ] No two consecutive sections sharing a format tag
- [ ] Every must-use keyword placed
- [ ] Every figure sourced
- [ ] Assets produced with alt text
```

## Checks before handing over

| Check | Fails when |
|---|---|
| Word targets | Any section has no number |
| Format variety | Two consecutive sections share a tag |
| Keyword map | A must-use keyword has no section |
| Evidence | A point needs a figure and the brief says "add a stat" |
| Reader | "What they already know" is empty |
| Angle | "What we are leaving out" is empty |
```



<!-- ============================================ -->


<!-- drafting-content -->
<!-- One Claude skill, flattened into a single markdown file. -->
<!-- To install: create ~/.claude/skills/drafting-content/ and split the FILE blocks below back out. -->
<!-- SKILL.md is everything above the first FILE heading. -->

---
name: drafting-content
description: Writes a long-form draft from a finished content brief, then blocks it from shipping until a mechanical gate passes. Drafts section by section against the brief's word budgets and format tags, then runs scripts/quality_gate.py, which exits nonzero on banned vocabulary, reframe constructions, anaphora, banned words in headings, over-long paragraphs, cadence metronomes, and unsourced figures. Use when a brief exists and a draft, article, or first version is requested. Do not use without a brief (use briefing-content) or for the final polish pass (use editing-content).
---

# Drafting Content

## Workflow

Copy this checklist and track progress:

```
Draft progress:
- [ ] Step 1: Read the brief twice
- [ ] Step 2: Draft the body, section by section
- [ ] Step 3: Write the introduction last
- [ ] Step 4: Run the gate
- [ ] Step 5: Fix and re-run until it exits 0
```

### Step 1: Read the brief twice

First pass for what the article covers. Second pass for the word budgets and format tags.

The format tags carry more weight than they look. Two consecutive sections tagged `paragraph` read as a wall by the second one, however good the sentences are.

### Step 2: Draft the body, section by section

One section at a time, in the brief's order.

Before each section, re-read the one before it and answer one question: where is the reader's attention now, and where does it need to be for the next section to land. That answer is the transition. Write it into the prose instead of announcing it.

Follow `reference/prose-rules.md` while drafting. Reading it after the fact means rewriting rather than writing.

### Step 3: Write the introduction last

An introduction written before the body promises what the body does not deliver. Written afterward, it promises what arrived.

### Step 4: Run the gate

```bash
python3 scripts/quality_gate.py draft.md
```

Standard library only, no packages to install. Add `--verbose` to see every hit instead of the first eight.

The gate scans headings and list items for vocabulary, and prose only for cadence. A bullet is meant to be terse; counting one as a performed punch sentence flags good writing as bad.

Fenced code blocks and blockquotes are excluded from the vocabulary scan, so a banned word can be quoted as data. Do not move prose into a fence to clear a check.

### Step 5: Fix and re-run until it exits 0

Fix every `FAIL`. Read every `WARN` and decide.

Rewrite the sentence, never swap the word. A line that only worked with "seamless" in it was doing nothing.

## Verification loop

The gate covers what a script can check. These three need a person, and the draft is not finished without them:

1. **Value per sentence.** Every line carries information or rhythm. Cut the ones carrying neither, even when true.
2. **Side of the desk.** Read each paragraph and ask whether it is written from the reader's position or the writer's. A paragraph describing the article's own contents belongs to the writer.
3. **Read aloud.** Every sentence that would not be said to a colleague gets rewritten.

## Test it

1. A draft opening with a scene ("Imagine a marketer..."). The gate should refuse it on banned openers.
2. A draft carrying a banned word inside a fenced code block. The gate should pass it.
3. A draft where every sentence runs 12 to 16 words. The gate should flag a flat metronome.

## What still needs a person

Roughly seven tenths of the information in the output survives editing, and roughly one tenth of the sentences survive word for word. The what comes out mostly right and the how mostly wrong. Budget accordingly: less time than expected for drafting, considerably more for the pass after it.


---

## FILE: `drafting-content/reference/prose-rules.md`

Save this block at `~/.claude/skills/drafting-content/reference/prose-rules.md`

```markdown
# Prose Rules

## Contents
- Banned vocabulary
- Reframe constructions
- Anaphora and negation runs
- Openers
- Cadence and paragraph shape
- Figures and links
- What the gate cannot check

## Banned vocabulary

The gate fails the build on these, in prose, headings, and list items.

```
actually, shift, significantly, fundamentally, leverage, robust,
seamless, streamline, optimize (verb), delve, realm, harness,
unlock, elevate, empower, holistic, comprehensive, innovative,
game-changer, cutting-edge, transformative, revolutionize,
testament, pivotal, crucial, meticulously, showcase, foster,
underscore, tapestry, move the needle, real, really, fix,
ship, shipped, rather than, compound
```

Also banned: `serves as`, `stands as`, `marks a`, `boasts a`, `represents a`, `features a` where `is` or `has` would do. Say `is`.

`optimization` inside a fixed term such as "answer engine optimization" passes. The verb does not. `fixed` as a pre-modifier ("fixed order") passes; as a verb it does not.

Headings get scanned like everything else. A heading is the most read line on the page.

## Reframe constructions

Any sentence that negates a framing, then asserts the corrected one. The most reliable single tell in machine-written prose.

```
This isn't X. This is Y.
Not X. Y.
X, not Y.
It's not just about X, it's about Y.
Less X, more Y.
The question isn't X, it's Y.
You don't need X. You need Y.
X is dead. Y is the future.
```

Three disguises that clear most greps:

```
Sure, X works. But Y is where...
X gets all the attention, but Y...
which isn't X, it's Y
```

The repair never varies: delete everything before the positive claim and state the claim.

## Anaphora and negation runs

Three or more sentences opening on the same word. Also the negation list: "no outline, no brief, no draft." Both are rhythm a vocabulary scan walks straight past.

Rewrite to a single positive statement, or to a real list with distinct openers.

## Openers

The gate refuses a draft starting with any of these:

```
Imagine / Picture this / Picture the scene
It's 2am / It's early
In today's / In this article / Welcome to
Let's dive in / Let's explore / Let's unpack
```

Open on the reader's situation, never on a scene invented for them.

## Cadence and paragraph shape

Target spread inside every section:

| Sentence length | Share |
|---|---|
| 1 to 9 words | a good number, never three in a row |
| 10 to 18 words | the bulk |
| 19 to 28 words | a solid chunk |
| 29+ words | a few, each earning its length |

All-short reads as social filler. All-long reads as dense filler. Both are metronomes.

Paragraphs run one to three lines, ceiling of three, and the lengths rotate. Never two consecutive paragraphs of the same shape. Single-sentence paragraphs for impact: three in a whole piece, maximum.

No lists of three parallel items used to sound complete. Two, four, or the one that matters. Genuine factual sets are fine.

## Figures and links

Every figure carries a linked source. A proof point about your own company appears once, twice at the outside; a hard number lands the first time and reads as padding by the third.

Anchor text runs two to four words. Never one, never five.

## What the gate cannot check

Whether a sentence carries content value or stylistic value. Whether a paragraph is written from the reader's side of the desk. Whether a transition follows the reader's attention or the outline's logic.

Read the draft aloud at the end. The gate cannot hear.
```


---

## FILE: `drafting-content/scripts/quality_gate.py`

Save this block at `~/.claude/skills/drafting-content/scripts/quality_gate.py`

```python
#!/usr/bin/env python3
"""
quality_gate.py — pre-ship gate for a long-form draft.

Runs every deterministic check in one pass, prints PASS / WARN / FAIL per
check, and exits 1 if any hard check fails. A draft carrying a banned pattern
cannot reach the publish step even when you are in a hurry and want it to.

Usage:
    python3 quality_gate.py draft.md
    python3 quality_gate.py draft.md --verbose    # every hit, not the first 8

Scope rules, each of which exists because of a specific miss:

  * Headings ARE scanned for vocabulary. An earlier version skipped every line
    starting with "#", and a banned word sat in an H2 through four clean runs.
  * List items ARE scanned for vocabulary and are EXCLUDED from cadence. A
    bullet is meant to be terse; counting one as a performed punch sentence
    flags good writing as bad.
  * Fenced code blocks and blockquotes are excluded from vocabulary and
    construction scans, so a banned word can be quoted as data. That exemption
    is what makes the rest of the scan trustworthy. Do not move prose into a
    fence to clear a check.
  * Cadence checks need 25+ sentences. On a handful, "100% in one bucket" is
    arithmetic, not a metronome, and firing there teaches you to ignore it.

Standard library only.
"""

import re
import sys

# ---------------------------------------------------------------- vocabulary

BANNED_WORDS = [
    r"\bactually\b", r"\bshifts?\b", r"\bshifted\b", r"\bshifting\b",
    r"\bsignificantly\b", r"\bfundamentally\b", r"\bfundamental\b",
    r"\bleverag(?:e|es|ed|ing)\b", r"\brobust\b", r"\bseamless(?:ly)?\b",
    r"\bstreamlin(?:e|es|ed|ing)\b", r"\bdelve\b", r"\brealm\b",
    r"\bharness(?:es|ed|ing)?\b", r"\bunlock(?:s|ed|ing)?\b",
    r"\belevat(?:e|es|ed|ing)\b", r"\bempower(?:s|ed|ing)?\b",
    r"\bholistic\b", r"\bcomprehensive\b", r"\binnovative\b",
    r"\bgame[- ]changer\b", r"\bcutting[- ]edge\b", r"\btransformative\b",
    r"\brevolutioni[sz]e(?:s|d)?\b", r"\btestament\b", r"\bpivotal\b",
    r"\bcrucial\b", r"\bmeticulous(?:ly)?\b", r"\bshowcas(?:e|es|ed|ing)\b",
    r"\bfoster(?:s|ed|ing)?\b", r"\bunderscor(?:e|es|ed|ing)\b",
    r"\btapestry\b", r"\bmove the needle\b", r"\breally\b", r"\breal\b",
    r"\bfix(?:es|ing)?\b", r"\bfix\b",
    r"\bships?\b", r"\bshipped\b", r"\bshipping\b",
    r"\brather than\b", r"\bcompound(?:s|ed|ing)?\b",
]

# "optimize" the verb fails; "optimization" inside a fixed term passes.
BANNED_VERB_OPTIMIZE = r"\boptimi[sz]e(?:s|d)?\b"

# "fixed" is banned as a verb ("we fixed it") and allowed as a pre-modifier
# ("fixed order"), which is ordinary English and not a tell. Without the split
# the gate cries wolf on every legitimate use, and a gate you learn to ignore
# is worse than no gate.
BANNED_FIXED_VERB = (
    r"\bfixed\b(?!\s+(?:order|orders|section|sections|list|set|sequence|"
    r"width|height|size|cost|costs|price|prices|fee|fees|term|terms|"
    r"point|position|template|format|number|rate|window|schedule))"
)

COPULA_DODGES = [
    r"\bserves as\b", r"\bstands as\b", r"\bmarks a\b", r"\bboasts a\b",
    r"\brepresents a\b", r"\bfeatures a\b", r"\bholds the distinction\b",
]

# ------------------------------------------------------------- constructions

REFRAMES = [
    r"\b(?:this|that|it)\s+(?:isn't|is not)\s+[^.!?]{2,60}?[.,]\s*(?:this|that|it)?\s*(?:is|it's)\b",
    r"\bit'?s not (?:just )?about\b[^.!?]{2,60}?,?\s*it'?s about\b",
    r"\bnot only\b[^.!?]{2,60}?\bbut also\b",
    r"\bless\b\s+\w+,\s*\bmore\b\s+\w+",
    r"\bforget\b[^.!?]{2,40}?[.,]\s*(?:this is|here'?s)\b",
    r"\bthe question isn'?t\b[^.!?]{2,60}?,?\s*(?:it'?s|the question is)\b",
    r"\byou don'?t need\b[^.!?]{2,60}?[.,]\s*you need\b",
    r"\bis dead\b[^.!?]{0,40}?\bis the future\b",
    r"\bsure,\s+\w+[^.!?]{2,60}?\bbut\b[^.!?]{2,60}?\b(?:real|actual|where)\b",
    r"\bgets? all the attention,?\s*but\b",
    r"\bwhile\b[^.!?]{2,60}?\bmight seem\b[^.!?]{2,60}?,\s*\w+\s+is\b",
    r"\bstop thinking\b[^.!?]{2,40}?[.,]\s*start thinking\b",
    r"^\s*not\s+[^.!?]{2,40}\.\s*[A-Z]",
    # Mid-sentence "X, not Y" is the commonest form; an earlier version of this
    # list only caught it line-initial.
    r",\s*not\s+(?:a|an|the|on|in|at|to|from|for|by|with|because|when|where)\b[^.!?]{2,50}",
    r",\s*not\s+\w+ing\b[^.!?]{0,40}",
    # The relative-clause disguise, which slips past anything anchored on
    # this/that/it.
    r"\b(?:which|that|and it)\s+(?:isn'?t|is not|wasn'?t|was not)\b[^.!?]{2,60}?,\s*(?:it'?s|it is|they'?re)\b",
    # "Not X. Y." landing mid-paragraph rather than line-initial.
    r"(?<=[.!?])\s+Not\s+(?:the|a|an|his|her|their|its|my|your)\b[^.!?]{2,50}\.",
    # Concession-pivot: "sounds like X, and it's Y."
    r"\b(?:sounds|seems|looks|reads)\s+like\b[^.!?]{2,50}?\band it'?s\b",
    r"\bthe \w+ (?:isn'?t|is not)\b[^.!?]{2,50}?,\s*it'?s\b",
    # Broad catch-all, added after a third variant survived two rounds of
    # patching ("is usually not the competitor, it's absence"). Any negation
    # followed by a corrective "it's" inside one sentence is the skeleton,
    # whatever sits between them. Over-flags occasionally, which is the right
    # trade for the one pattern that matters most.
    r"\b(?:not|isn'?t|wasn'?t|aren'?t|never)\b[^.!?]{2,70}?,\s*it(?:'?s| is)\b",
    r"\b(?:not|isn'?t|wasn'?t|aren'?t|never)\b[^.!?]{2,70}?\.\s*It(?:'?s| is)\b",
]

BANNED_OPENERS = [
    r"^imagine\b", r"^picture this\b", r"^picture the scene\b",
    r"^it'?s 2\s?am\b", r"^it'?s early\b", r"^in today'?s\b",
    r"^in this (?:article|post|guide|piece)\b", r"^welcome to\b",
    r"^let'?s (?:dive in|explore|unpack)\b",
]

DEAD_PHRASES = [
    r"\bit'?s (?:important|worth) (?:to note|noting)\b",
    r"\bat the end of the day\b", r"\bmoving forward\b",
    r"\bin order to\b", r"\bfurthermore\b", r"\bmoreover\b",
    r"\badditionally\b", r"\bthat being said\b",
    r"\bto put this in perspective\b", r"\bin other words\b",
    r"\blet that sink in\b", r"\bread that again\b",
    r"\bwhat nobody tells you\b", r"\bmost people don'?t realize\b",
    r"\bhere'?s the thing\b",
]

# Writer-side scaffolding: sentences whose subject is the article rather than
# the work. These pass every vocabulary check and still read as a report.
SCAFFOLDING = [
    r"\bthe (?:top|first|last|second) (?:row|column|item) is\b",
    r"\bis the one worth\b", r"\bworth sitting with\b",
    r"\bhere is what (?:that|this) looks like\b",
    r"\bwhat follows is\b", r"\bthe rule that does the work\b",
    r"\bearn(?:s)? (?:its|their) place\b",
    r"\bworth something to you\b", r"\btraced end to end\b",
    r"\bknowing them makes\b", r"\bwhich is the point\b",
    r"\bin the section (?:above|below)\b",
    r"\bas (?:noted|mentioned) (?:above|earlier)\b",
]

EM_DASH = r"[—–]"


# ------------------------------------------------------------------ plumbing

def strip_exempt(text):
    """Blank fenced code, inline code, and blockquotes, preserving line count."""
    out, in_fence = [], False
    for line in text.split("\n"):
        if line.lstrip().startswith("```"):
            in_fence = not in_fence
            out.append("")
            continue
        if in_fence or line.lstrip().startswith(">"):
            out.append("")
            continue
        out.append(re.sub(r"`[^`]*`", "", line))
    return "\n".join(out)


def scan_lines(text):
    """Every line worth scanning for vocabulary: prose, headings, list items."""
    keep = []
    for i, line in enumerate(text.split("\n"), 1):
        s = line.strip()
        if not s or s.startswith(("|", "!", "---")):
            continue
        if s.startswith("#"):
            keep.append((i, s.lstrip("#").strip()))
            continue
        if re.match(r"^([-*+]|\d+\.)\s", s):
            keep.append((i, re.sub(r"^([-*+]|\d+\.)\s+", "", s)))
            continue
        keep.append((i, s))
    return keep


def headings(text):
    return [(i, l.strip().lstrip("#").strip())
            for i, l in enumerate(text.split("\n"), 1)
            if l.strip().startswith("#")]


def prose_only(text):
    """Prose lines, for cadence. No headings, no list items, no tables."""
    keep = []
    for line in text.split("\n"):
        s = line.strip()
        if not s or s.startswith(("#", "|", "!", "---")):
            continue
        if re.match(r"^([-*+]|\d+\.)\s", s):
            continue
        keep.append(s)
    return keep


def sentences(text):
    body = " ".join(prose_only(text))
    parts = re.split(r"(?<=[.!?])\s+(?=[A-Z\"'(])", body)
    return [p.strip() for p in parts if len(p.strip()) > 1]


def paragraphs(text):
    blocks, cur = [], []
    for line in text.split("\n"):
        s = line.strip()
        if not s:
            if cur:
                blocks.append(cur)
                cur = []
            continue
        if s.startswith(("#", "|", "!", "```", ">", "---")) or re.match(r"^([-*+]|\d+\.)\s", s):
            if cur:
                blocks.append(cur)
                cur = []
            continue
        cur.append(s)
    if cur:
        blocks.append(cur)
    return blocks


def scan(text, patterns):
    hits = []
    for num, line in scan_lines(text):
        for pat in patterns:
            for m in re.finditer(pat, line, re.I):
                hits.append((num, m.group(0).strip(), line[:88]))
    return hits


def anaphora_hits(text):
    """Repeated sentence openers, and negation-run lists. Both are AI rhythm
    that every vocabulary scan walks straight past."""
    hits = []
    for num, line in scan_lines(text):
        for m in re.finditer(r"\b(?:no|not|never)\b[^.!?]{0,120}", line, re.I):
            if len(re.findall(r"\bno\s+\w+", m.group(0), re.I)) >= 3:
                hits.append((num, "no X, no Y, no Z run", line[:88]))
                break
        opens = re.findall(r"(?:^|(?<=[.!?])\s)([A-Z][a-z']+)\s", line)
        for w in set(opens):
            if opens.count(w) >= 3:
                hits.append((num, f"'{w}' opens {opens.count(w)} sentences", line[:88]))
    sents = sentences(text)
    firsts = []
    for s in sents:
        m = re.match(r"^([A-Za-z']+)", s)
        firsts.append(m.group(1).lower() if m else "")
    run = 1
    for i in range(1, len(firsts)):
        if firsts[i] and firsts[i] == firsts[i - 1]:
            run += 1
            if run >= 3:
                hits.append((0, f"'{firsts[i]}' opens {run} consecutive sentences", sents[i][:88]))
        else:
            run = 1
    return hits


# -------------------------------------------------------------------- report

class Report:
    def __init__(self, verbose=False):
        self.fails = self.warns = 0
        self.verbose = verbose

    def line(self, status, name, detail=""):
        if status == "FAIL":
            self.fails += 1
        if status == "WARN":
            self.warns += 1
        print(f"  {status:<4}  {name}{('  ' + detail) if detail else ''}")

    def hits(self, status, name, hits, cap=8):
        if not hits:
            self.line("PASS", name)
            return
        self.line(status, name, f"{len(hits)} found")
        shown = hits if self.verbose else hits[:cap]
        for num, hit, ctx in shown:
            loc = f"L{num}" if num else "  "
            print(f'          {loc}: "{hit}"  |  {ctx}')
        if not self.verbose and len(hits) > cap:
            print(f"          ... {len(hits) - cap} more (--verbose for all)")


def main():
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    verbose = "--verbose" in sys.argv
    if not args:
        print("usage: quality_gate.py draft.md [--verbose]")
        sys.exit(2)

    path = args[0]
    try:
        raw = open(path, encoding="utf-8").read()
    except OSError as e:
        print(f"cannot read {path}: {e}")
        sys.exit(2)

    text = strip_exempt(raw)
    r = Report(verbose)
    sents = sentences(text)
    words = len(" ".join(prose_only(text)).split())

    print(f"\nquality_gate: {path}")
    print(f"{words} prose words, {len(sents)} prose sentences\n")

    print("HARD CHECKS")
    r.hits("FAIL", "banned vocabulary", scan(text, BANNED_WORDS))
    r.hits("FAIL", "optimize (verb)", scan(text, [BANNED_VERB_OPTIMIZE]))
    r.hits("FAIL", "fixed (as verb)", scan(text, [BANNED_FIXED_VERB]))
    r.hits("FAIL", "reframe constructions", scan(text, REFRAMES))
    r.hits("FAIL", "anaphora and negation runs", anaphora_hits(text))
    r.hits("FAIL", "em and en dashes", scan(text, [EM_DASH]))
    r.hits("FAIL", "dead phrases", scan(text, DEAD_PHRASES))
    r.hits("FAIL", "writer-side scaffolding", scan(text, SCAFFOLDING))

    head_hits = []
    for num, h in headings(text):
        for pat in BANNED_WORDS + [BANNED_VERB_OPTIMIZE, BANNED_FIXED_VERB]:
            for m in re.finditer(pat, h, re.I):
                head_hits.append((num, m.group(0), f"heading: {h[:64]}"))
    r.hits("FAIL", "banned words in headings", head_hits)

    opener_hits = []
    for para in paragraphs(text):
        for pat in BANNED_OPENERS:
            if re.match(pat, para[0], re.I):
                opener_hits.append((0, para[0][:38], para[0][:88]))
    r.hits("FAIL", "banned openers", opener_hits)

    long_paras = [(0, f"{len(p)} lines", p[0][:88]) for p in paragraphs(text) if len(p) > 3]
    r.hits("FAIL", "paragraphs over 3 lines", long_paras)

    unsourced = []
    for num, line in scan_lines(text):
        if re.search(r"\b\d+(?:\.\d+)?%|\b\d{2,3}(?:,\d{3})+\b|\$\d", line) and "](" not in line:
            unsourced.append((num, "figure with no link", line[:88]))
    r.hits("WARN", "figures without a source link", unsourced)

    print("\nCADENCE")
    lengths = [len(s.split()) for s in sents]
    if lengths:
        total = len(lengths)
        b = {"1-9": sum(1 for n in lengths if n <= 9),
             "10-18": sum(1 for n in lengths if 10 <= n <= 18),
             "19-28": sum(1 for n in lengths if 19 <= n <= 28),
             "29+": sum(1 for n in lengths if n >= 29)}
        print("  ----  distribution   " +
              "  ".join(f"{k}: {v} ({round(100*v/total)}%)" for k, v in b.items()))
        if total < 25:
            r.line("PASS", "cadence spread", f"skipped, only {total} sentences")
        elif b["1-9"] / total > 0.42:
            r.line("FAIL", "punchy metronome", f"{round(100*b['1-9']/total)}% under 10 words")
        elif b["10-18"] / total > 0.70:
            r.line("FAIL", "flat metronome", f"{round(100*b['10-18']/total)}% in one bucket")
        else:
            r.line("PASS", "cadence spread")

        runs, cur = 0, 0
        for n in (lengths if total >= 25 else []):
            cur = cur + 1 if n <= 9 else 0
            if cur >= 3:
                runs += 1
        if runs:
            r.line("FAIL", "performed punch runs", f"{runs} run(s) of 3+ short sentences")
        else:
            r.line("PASS", "no punch-sentence runs")

    single = sum(1 for p in paragraphs(text) if len(p) == 1 and len(p[0].split()) <= 14)
    if single > 3:
        r.line("WARN", "single-sentence paragraphs", f"{single} found, ration to 3")
    else:
        r.line("PASS", "single-sentence paragraphs", str(single))

    shapes = [len([x for x in re.split(r"(?<=[.!?])\s", " ".join(p)) if x.strip()])
              for p in paragraphs(text)]
    worst, cur = 1, 1
    for i in range(1, len(shapes)):
        cur = cur + 1 if shapes[i] == shapes[i - 1] else 1
        worst = max(worst, cur)
    if worst >= 5:
        r.line("FAIL", "paragraph metronome", f"{worst} consecutive paragraphs of the same shape")
    elif worst == 4:
        r.line("WARN", "paragraph metronome", "4 consecutive paragraphs of the same shape")
    else:
        r.line("PASS", "paragraph shape varies", f"longest run {worst}")

    print("\nSTRUCTURE")
    triples = scan(text, [r"\b\w+,\s+\w+,\s+and\s+\w+\b(?!\s*[,:])"])
    if len(triples) > 3:
        r.line("WARN", "rule-of-three triplets", f"{len(triples)} found, use two or four")
    else:
        r.line("PASS", "rule-of-three triplets", str(len(triples)))

    r.hits("WARN", "copula dodges", scan(text, COPULA_DODGES))

    # Directive density. A piece written almost entirely in commands reads as a
    # lecture even when every sentence is clean. Mechanical steps ("copy the
    # folder in") are fine; judgement delivered as an order is not.
    IMPERATIVE = (r"^(Put|Write|Make|Get|Plan|Expect|Budget|Give|Take|Spend|Browse|"
                  r"Start|Leave|Fill|Store|Steal|Delete|Watch|Learn|Confirm|Flip|"
                  r"Skip|Verify|Stop|Avoid|Remember|Note|Consider|Ensure|Never|Always)\b")
    imp = [s_ for s_ in sents if re.match(IMPERATIVE, s_)]
    share = round(100 * len(imp) / len(sents)) if sents else 0
    if share > 12:
        r.line("FAIL", "directive density", f"{share}% of sentences open on a command")
        for s_ in imp[:6]:
            print(f'          "{s_[:86]}"')
    elif share > 7:
        r.line("WARN", "directive density", f"{share}% of sentences open on a command")
    else:
        r.line("PASS", "directive density", f"{share}%")

    anchors = re.findall(r"\[([^\]]+)\]\(", raw)
    bad = [(0, a, "anchor length") for a in anchors if not 2 <= len(a.split()) <= 4]
    r.hits("WARN", "anchors outside 2-4 words", bad)

    if words < 3000:
        r.line("WARN", "length", f"{words} words, target 3000+")
    else:
        r.line("PASS", "length", f"{words} words")

    print("\n" + "=" * 58)
    if r.fails:
        print(f"FAILED  {r.fails} hard check(s), {r.warns} warning(s). Not finished.")
        sys.exit(1)
    print(f"PASSED  0 hard failures, {r.warns} warning(s). Read every warning.")
    print("Now read it aloud. The gate cannot hear you.")
    sys.exit(0)


if __name__ == "__main__":
    main()
```



<!-- ============================================ -->


<!-- editing-content -->
<!-- One Claude skill, flattened into a single markdown file. -->
<!-- To install: create ~/.claude/skills/editing-content/ and split the FILE blocks below back out. -->
<!-- SKILL.md is everything above the first FILE heading. -->

---
name: editing-content
description: Edits a draft in two passes and returns the edited text plus a change log. The prose pass removes documented AI writing patterns using reference/ai-tells.md, working from the costliest pattern down. The fact pass verifies every number, percentage, date, and named claim against the organization it is credited to at that organization's own publication, using reference/stat-verification.md. Use when asked to edit, polish, humanize, de-AI, fact-check, or verify a draft or a published article. Do not use to write new sections (use drafting-content).
---

# Editing Content

Prose first, then facts. A fabricated figure inside a badly written paragraph is easy to miss and obvious once the paragraph reads clean.

## Workflow

Copy this checklist and track progress:

```
Edit progress:
- [ ] Step 1: Prose pass, top down through ai-tells.md
- [ ] Step 2: Fact pass, every figure to its primary source
- [ ] Step 3: Write the change log
- [ ] Step 4: Manual passes (read aloud, cadence, repetition)
- [ ] Step 5: Second full pass
```

### Step 1: Prose pass

Work down `reference/ai-tells.md` in order. It runs from the costliest pattern to the cheapest, so a partial pass still removes the worst.

Two rules about how to make each edit:

- **Rewrite the sentence, never swap the word.** Substituting a synonym keeps the sentence that needed the banned word. A line that only worked with "seamless" in it was doing nothing.
- **Read three sentences back before changing one.** A sentence edited in isolation stops connecting to its neighbours, and disconnected sentences are the thing being removed.

### Step 2: Fact pass

For every number, percentage, currency figure, date, and named claim, follow the four steps in `reference/stat-verification.md` and assign one of five verdicts.

A figure that cannot be sourced comes out. It does not soften into "many companies report".

### Step 3: Write the change log

```markdown
| # | Original | Change | Reason | Source |
|---|---|---|---|---|
```

Include the verified figures alongside the corrections. A log showing 14 checks and 2 corrections tells the next reader the work happened; a log showing 2 corrections reads as a list of mistakes.

### Step 4: Manual passes

Nothing above catches these.

| Check | What to listen for |
|---|---|
| Read the whole piece aloud | Any sentence you would not say to a colleague |
| Cadence | A run of short declaratives, or a run of long ones. Both are metronomes |
| Paragraph shape | Two consecutive paragraphs that look identical on screen |
| Repetition of point | The same idea made twice in different words, usually two sections apart |
| Value per sentence | Every line carries information or rhythm. Cut the rest |
| Side of the desk | A paragraph describing the article's own contents belongs to the writer |

### Step 5: Second full pass

Run Steps 1 to 4 again. The second pass finds what the first was too close to see.

## Verification loop

When a mechanical checker is available, run it between Step 1 and Step 2, fix every failure, and re-run until it exits clean. `drafting-content/scripts/quality_gate.py` covers vocabulary, reframes, anaphora, headings, cadence, and scaffolding.

A clean checker is the floor. Steps 4 and 5 are the ceiling and they have no script.

## Test it

1. A draft containing "This isn't a tool, it's a process." The prose pass should delete everything before the positive claim.
2. A draft citing a percentage to an aggregator. The fact pass should trace it to the primary source or remove it.
3. A draft where every paragraph runs two sentences. The manual pass should break the pattern.

## What still needs a person

Cadence. The writer who produced the draft is the worst judge of how it sounds, which is why the read-aloud step exists and why it is the step people skip.


---

## FILE: `editing-content/reference/ai-tells.md`

Save this block at `~/.claude/skills/editing-content/reference/ai-tells.md`

```markdown
# AI Tells

## Contents
- 1. Reframe constructions
- 2. Performed punch-sentences
- 3. Anaphora and negation runs
- 4. Writer-side scaffolding
- 5. Metronome rhythm
- 6. Significance inflation
- 7. Participle padding
- 8. Dead vocabulary
- 9. Copula avoidance
- 10. Rule of three
- 11. False ranges
- 12. Elegant variation
- 13. Inline-header lists
- 14. Dead phrases and transitions
- 15. Meta commentary
- 16. Hollow frames
- 17. Em dashes
- What this list cannot do

Ordered by cost. Work top down.

Most of these are catalogued in [Wikipedia's Signs of AI Writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing), which cites the research. Below are the ones that show up in marketing prose specifically.

## 1. Reframe constructions

Any sentence that negates a framing, then asserts the corrected one. The most reliable single tell.

- ✗ This isn't a productivity tool. It's a thinking partner.
- ✓ It's a thinking partner.

The negated half carries no information. Nobody needs to be told what something is not before learning what it is.

**Disguises that clear most greps:**

```
Sure, dashboards work. But the interesting part is...
Attribution gets all the attention, but retention is where...
While the first approach might seem right, the second is...
We didn't build a tool. We built a process.
X, not Y.                    (the two-word form, easiest to miss)
which isn't X, it's Y        (hidden in a relative clause)
is usually not X, it's Y     (no comma before "not")
```

Same skeleton every time: concession, pivot, corrected claim. Delete everything before the claim.

## 2. Performed punch-sentences

Short standalone declaratives that announce a point instead of making it.

- ✗ The goal was simple. Nobody wanted complexity. That was the idea.
- ✓ The goal was a brief in under 10 minutes, so anything adding a step got cut.

Three or more in a row reads machine-written even when every word is clean. Fold setup and point together with connective tissue: so, and, because, which, where, but.

Ration genuine single-sentence-for-impact lines to three in a whole piece.

## 3. Anaphora and negation runs

Three sentences opening on the same word. Or the negation list: "no outline, no brief, no draft."

Both survive every vocabulary scan. Rewrite to one positive statement, or to a real list with distinct openers.

## 4. Writer-side scaffolding

Sentences whose subject is the article rather than the work.

- ✗ The top row is the one worth sitting with.
- ✓ Between 53% and 86% of citations go to pages that never mention the brand.
- ✗ Two coverage limits, since a range without them is a claim you can't check.
- ✓ Don't carry those ranges into a category outside B2B SaaS.
- ✗ Five things accumulated in there, and each is worth something to you.
- ✓ Five rules ended up in mine.

The test: does the sentence describe the article, rate its own material, or announce a list? Every paragraph should open on an action taken, a thing that happened, a fact, or an instruction to the reader.

## 5. Metronome rhythm

Every sentence the same length, every paragraph the same number of lines.

Both directions are equally bad. All-short reads as social filler, all-long as dense filler. Aim for a spread: the bulk at 10 to 18 words, a good number shorter, a solid chunk at 19 to 28, a few over 29 that earn it.

## 6. Significance inflation

Claiming importance instead of demonstrating it.

- ✗ marking a pivotal moment in the evolution of content operations
- ✓ after that, briefs took under 10 minutes

State the fact. The reader decides what it means.

Related: the appended clause explaining why the previous fact mattered. "Which is the point." Cut it. A fact needing an explanation was not specific enough.

## 7. Participle padding

Trailing "-ing" phrases that look like analysis and contain none.

- ✗ ...cutting brief time in half, highlighting the value of templates.
- ✓ ...cutting brief time in half.

Analysis worth having gets its own sentence with a specific claim.

## 8. Dead vocabulary

```
actually, shift, significantly, fundamentally, leverage, robust,
seamless, streamline, optimize (verb), delve, realm, harness,
unlock, elevate, empower, holistic, comprehensive, innovative,
game-changer, cutting-edge, transformative, revolutionize,
testament, pivotal, crucial, meticulously, showcase, foster,
underscore, tapestry, move the needle, real, really, fix,
ship, shipped, rather than, compound
```

Rewrite the sentence, do not swap the word. Scan headings too: a heading is the most read line on the page.

## 9. Copula avoidance

Bloated verbs standing in for "is" and "has": serves as, stands as, marks a, boasts a, represents a, features a, holds the distinction of.

- ✗ The template serves as a mechanism that represents our approach.
- ✓ The template is a fixed section order.

## 10. Rule of three

Three parallel items used to make shallow coverage look complete. "Speed, efficiency, and scale."

Use two, four, or name the one that matters. Genuine factual sets are fine.

## 11. False ranges

"From ancient traditions to modern innovations." If no meaningful midpoint can be named, the range is decoration.

## 12. Elegant variation

Cycling synonyms to avoid repeating a word: the tool becomes the platform, then the solution, then the offering. Repeat the clearest word.

## 13. Inline-header lists

Bold labels acting as mini-headings inside a paragraph, each followed by a definition. Convert to flowing prose or a proper list. A paragraph of bolded stubs is a table that lost its formatting.

## 14. Dead phrases and transitions

```
It's important to note that / It's worth noting
In today's [anything]
At the end of the day
Moving forward
In order to (say "to")
Furthermore / Moreover / Additionally
That said / That being said
In other words
To put this in perspective
Here's the thing
Let that sink in / Read that again
What nobody tells you / Most people don't realize
In this article, I will
Let's dive in / Let's explore / Let's unpack
```

## 15. Meta commentary

Announcing the writing instead of doing it. "In this section we'll look at..." Say the thing.

## 16. Hollow frames

Sentences that sound structural and carry no new fact.

- ✗ This is where the magic happens.
- ✗ The edit stage is where good content becomes great content.

Test: does this sentence add a fact, or add texture? Texture is allowed occasionally as a breather. Two in a row is padding.

## 17. Em dashes

None. Commas, periods, colons, semicolons, parentheses, or "to" for ranges.

## What this list cannot do

It cannot tell whether a paragraph is written from the reader's side of the desk, and it cannot hear rhythm. Both need the read-aloud pass.
```


---

## FILE: `editing-content/reference/stat-verification.md`

Save this block at `~/.claude/skills/editing-content/reference/stat-verification.md`

```markdown
# Stat Verification

## Contents
- The rule
- The four steps
- The five verdicts
- Patterns that produce bad figures
- Where the primary source lives
- What cannot be verified here
- Output

## The rule

Every figure gets checked against the organization it is credited to, at that organization's own publication.

An aggregator repeating a figure is not a source. Neither is a blog post citing the report, a roundup listing the number, or a deck quoting it. Those are where fabricated figures survive, because each cites the last and none cites the study.

## The four steps

1. **Write the claim exactly.** Number, unit, year, and who it is credited to. Vague attribution ("studies show", "research suggests") fails before the search starts, because there is nothing to check.
2. **Search the primary source directly.** Query the number in quotes plus the organization plus the year: `"73%" Gartner 2026 marketing`. Searching the claim in prose form finds the articles repeating it.
3. **Open the report.** Not the press release summarizing the report, and not the blog post summarizing the release, unless the release is the only published form.
4. **Assign a verdict.**

## The five verdicts

| Verdict | Meaning | Action |
|---|---|---|
| Verified | At the source, as stated | Leave it. Add the link if missing |
| Misquoted | The source says something adjacent | Correct to the source's figure |
| Stale | Correct for an earlier year | Update, or state the year in the sentence |
| Miscredited | Exists, from a different organization | Recredit and relink |
| Not found | Not at the source credited | Remove, or replace with a verified figure from the same source |

## Patterns that produce bad figures

**The orphan percentage.** A clean round number with no organization attached. "Studies show 60% of marketers..." There is no study. Most common fabrication, easiest to spot.

**The citation chain.** A cites B cites C, and C cites nothing. Follow the chain to the end. If it terminates in a blog post, the figure is unsourced.

**The plausible drift.** A source said 34%; it travels as "over a third", then 40%, then "nearly half". Each step is a small rounding and the end is wrong. Check the original number, not the characterization.

**The wrong-year survival.** A genuine figure from three years ago, still circulating as current. Verified but stale, which needs the year stated rather than removal.

## Where the primary source lives

Go to the organization's own research or newsroom index rather than searching the open web. Research firms, analysts, and large vendors publish an index of their own studies, and that index is the fastest route to the figure.

Government statistics agencies and standards bodies publish theirs as datasets with a citation format attached, which is the easiest kind to verify and the kind least often cited correctly.

For a figure from a survey, find the methodology: sample size, sampling frame, date collected. A figure from an n of 40 self-selected respondents exists and does not support a claim about an industry.

## What cannot be verified here

Figures that exist only in rendered page output or behind an interactive element. Pricing pages are the usual case, since many serve their numbers by script, so a raw fetch returns nothing and looks identical to a fabricated claim.

Write "check current pricing at the source" and carry no figure. Do not guess, and do not carry a figure from a months-old screenshot.

**One caution learned expensively:** a fetch failure reads exactly like a fabricated claim. Before deleting a figure because it "was not on the page", confirm the page arrived. Redirects, timeouts, and bot filters each return an empty result that looks like proof of absence.

## Output

```markdown
| # | Figure | Credited to | Verdict | Corrected to | Source |
|---|---|---|---|---|---|
```

Log the verified figures alongside the corrections.
```



<!-- ============================================ -->


<!-- distributing-content -->
<!-- One Claude skill, flattened into a single markdown file. -->
<!-- To install: create ~/.claude/skills/distributing-content/ and split the FILE blocks below back out. -->
<!-- SKILL.md is everything above the first FILE heading. -->

---
name: distributing-content
description: Turns one published article into the channel assets that carry it, reading and writing a state file so openers, angles, and formats stop repeating week to week. Extracts the five reusable assets from a source piece, matches each to the format that fits it, applies a per-channel voice scrub, verifies every link resolves, and appends what went out to state.json. Use when asked to repurpose an article, write social posts from a piece, plan distribution, or turn a blog into LinkedIn posts or a newsletter segment. Do not use to write the source article (use drafting-content).
---

# Distributing Content

Every other content stage can be stateless, because each article arrives as a fresh problem. This one cannot: the model has no memory of what went out last week, so without a file to check, the same opener and the same handful of framings come back on a schedule.

## Workflow

Copy this checklist and track progress:

```
Distribution progress:
- [ ] Step 1: Read state.json
- [ ] Step 2: Extract the five assets
- [ ] Step 3: Choose formats, checked against state
- [ ] Step 4: Draft, then scrub per channel
- [ ] Step 5: Verify every link
- [ ] Step 6: Append to state.json
```

### Step 1: Read state.json

```bash
cat state.json
```

Create it when missing:

```json
{"posts": []}
```

Load the last 10 entries. Everything below gets checked against them.

### Step 2: Extract the five assets

From the source article, list:

1. The most surprising figure, with its source
2. The most concrete artifact: a file, a screenshot, a piece of output
3. The strongest opinion it takes
4. Whatever the piece admits does not work
5. Any table or chart worth reproducing

Item 4 usually makes the best post in the set and gets overlooked most often. A published admission travels further than a claim, because a claim asks the reader to take your word and an admission has already spent something.

### Step 3: Choose formats, checked against state

Pick two or three assets. Five forces posts nobody engages with.

| Asset | Format that fits |
|---|---|
| A number with a story behind it | Short post, figure in the first line, source linked |
| An artifact | Post with the image doing the work, minimal copy |
| An opinion | Text only, no visual, the claim in the opening line |
| A sequence or process | Carousel, one idea per panel |
| An admission | Text only, plainly worded, no framing device |

Check state before committing. When the last two posts were carousels, this one is not. When the last three opened on a figure, this one opens elsewhere.

### Step 4: Draft, then scrub per channel

Set the channel first, then apply `reference/voice-scrub.md`.

The pronoun rules invert between a company handle and a personal byline, and a post scrubbed for the wrong one reads wrong immediately.

### Step 5: Verify every link

Fetch each URL in each asset and confirm it resolves and points where intended.

A dead link in a live post cannot be corrected after the fact, and one pointing at the wrong company's domain is worse than dead.

### Step 6: Append to state.json

```json
{
  "source": "article-slug",
  "date": "YYYY-MM-DD",
  "channel": "linkedin",
  "format": "text",
  "opener": "the first eight words, verbatim",
  "angle": "one line describing the take",
  "asset_used": "admission"
}
```

Store the opener word for word. Paraphrasing defeats the purpose when the thing being caught is the phrasing coming back.

## Verification loop

Before handing the assets over:

1. No opener repeats a phrase in the last 10 state entries
2. No format repeats the last two entries for that channel
3. Every link fetched and confirmed
4. Every figure carries its source
5. The channel scrub matches the channel

Any failure sends the asset back to Step 3 or Step 4.

Keep a `rejections.md` alongside state: one line per thing that got sent back and why. It stops the same correction getting made twice.

## Test it

1. A source article whose last three posts all opened on a figure. The skill should open elsewhere.
2. An article written in first person, targeted at a company handle. The scrub should rewrite every singular pronoun to "we" and leave quoted speech alone.
3. A reference article with one angle. The skill should produce one asset, not five.

## What still needs a person

Whether the article deserves distribution at all. Some pieces are reference material that should sit and rank, and pushing them through a channel pipeline produces posts nobody engages with while diluting the ones that matter. This skill will build assets for any article handed to it, including the ones that should not have been.


---

## FILE: `distributing-content/reference/voice-scrub.md`

Save this block at `~/.claude/skills/distributing-content/reference/voice-scrub.md`

```markdown
# Voice Scrub

## Contents
- Channel 1: company handle
- Channel 2: personal byline
- Rules for both channels
- The last check

Set the channel first. Every rule below depends on it.

## Channel 1: company handle

The account speaks as the organization, so first-person singular comes out.

| Found | Rewrite to |
|---|---|
| I / I've / I'd / I'm | we / we've / we'd / we're |
| my / me / mine | our / us / ours |
| In my experience | In our experience, or: across the accounts we run |
| I tested | We tested |
| My take | Our read |
| I haven't seen | No sign of it in our accounts |

**One exception.** A direct quote from a named person keeps its original pronouns. The quote belongs to them. Outside the quotation marks, back to "we".

## Channel 2: personal byline

The opposite. First-person singular is the point, and "we" gets specific or goes.

| Found | Rewrite to |
|---|---|
| We believe / We think | I think |
| Our data shows | the numbers I pulled show |
| We tested | I tested |
| We (meaning the industry) | marketers, or name the group |

A vague "we" on a personal account is the tell that a company post got repurposed without editing.

**One more distinction that matters.** Aggregate portfolio data belongs to a team even on a personal byline. "The accounts our team tracks" is accurate; "the accounts I sample" claims one person ran 60 client workspaces. Keep "I" for personal craft and personal judgement, and "we" for anything the organization observed.

## Rules for both channels

### Banned words

```
game-changer, revolutionary, groundbreaking, transformative,
disruptive, paradigm shift, must-have, leverage, unlock,
harness, elevate, supercharge, dramatically, fundamentally,
significantly, actually, shift, robust, comprehensive, holistic,
cutting-edge, world-class, best-in-class, industry-leading,
seamless, streamline, next-gen, future-proof, innovative,
real, really, fix, 10x
```

### Banned constructions

**Reframes.** "This isn't X, it's Y." "X, not Y." "We didn't X, we Y." State the positive claim and delete what comes before it.

**The balanced-number slogan.** "Two things, one platform." "Three launches, one bet." A top headline tell, hard to unsee once noticed. Name the actual things.

**Generic titles.** "What's inside." "Why it matters." "The big picture." Apply the swap test: replace the brand with any other brand, and if the title still works it is filler. Every title needs a number, a named thing, or a specific claim.

**Openers to avoid.** "In today's [anything]." "Most marketers..." as a cold open. "5 ways to..." as the first line. "And that's a wrap."

**Mechanical connectors.** Furthermore, Moreover, Additionally, That said.

### Formatting

- No em dashes or en dashes. Commas, periods, colons, parentheses.
- No ALL CAPS for emphasis.
- Exclamation marks: none, unless quoting someone.
- Bold: one or two moments, and only where the platform renders it.
- Line breaks rotate. Three single-line paragraphs in a row reads as a template.
- Every figure carries a source. A number with no attribution invites a comment asking for it and leaves no good answer.

## The last check

Read the post aloud. If it sounds like a brand while posting as a person, or like a person while posting as a brand, the scrub ran against the wrong channel. Start again with the right one.
```
