Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .claude/skills/blog-next/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ reviewing and shipping are the other half and belong to the agents below.
| A. Plan health + pick, or REBUILD the plan | **this skill** |
| B. Research | **this skill** |
| C. Outline + outline gate | **this skill** |
| Draft → 3 critics → cold-eyes → ship gates → commit | `blog-post-coordinator` |
| Draft → 3 critics → cold-eyes → ship gates → commit | **`blog-write` skill** |
| N-post sprint, cluster sweep, one PR, CI watch | `blog-batch-orchestrator` |

**If the user wanted a POST and you return a verdict, say so in one line and
name `blog-write`.** This skill deciding not to write is a legitimate outcome,
but it is not what someone asking for a post expects, and leaving them to infer
the second half exists is how they end up thinking the pipeline is broken.

You produce one of three things, never a draft: a **topic row**, a **rebuilt
plan section**, or a **HOLD** saying the slot should not be spent.

Expand Down Expand Up @@ -192,8 +197,10 @@ it straight on and let the gates decide. Paul asked for delivery without a human
in the loop (2026-08-22), and the stop list below is the whole of what he still
owns.

**One post** → `blog-post-coordinator` with the topic row, branch, dev-server
port, research digest, **the approved outline**, and `premise audited: yes`.
**One post** → the **`blog-write` skill**, with the topic row, research digest,
**the approved outline**, and `premise audited: yes`. That skill owns STEP 4
onward and is what the user can also invoke directly; it delegates to
`blog-post-coordinator` when agent spawning is available.

**Several posts** → `blog-batch-orchestrator` with N; it runs Stages A-C per row.

Expand Down
106 changes: 106 additions & 0 deletions .claude/skills/blog-operator/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
name: blog-operator
description: >
ONE door for blog work. Decides for itself whether to pick a topic, write a
post, upgrade an existing one, or repair a credibility defect - then does it,
and keeps going. Use whenever the user wants the blog moved forward without
saying how: "work on the blog", "improve the blog", "run the blog", "do blog
work", "make the blog better for clients", "keep going on content", a blog
sprint, or any standing instruction to build the brand through content. Also
use when the user is unsure whether they need blog-next or blog-write - that
choice is this skill's job, not theirs.
NOT for LinkedIn (linkedin-post-jt), course chapters, or landing pages.
---

# Blog operator

**The blog exists to make a sceptical founder trust us enough to talk.** Traffic
is a proxy and sometimes a bad one. Judge every action by whether it moves that,
and you will pick differently than if you optimise clicks.

The reader is `docs/90-99-content-strategy/strategy-analysis/90.10-icp-primary-website-target.md`:
a non-technical founder who has been burned by a devshop. They arrive sceptical
and they are reading for reasons to disqualify us.

You own the decision of what to do next. `blog-next` and `blog-write` are your
two hands - do not ask the user which to run.

## Pick the next action

Check in this order and take the first that applies. The order is deliberate:
**credibility defects outrank new content**, because a founder who catches one
invented claim stops believing the rest of the page, and no new post recovers
that.

1. **REPAIR - is something published that damages trust?**
Run the fabricated-claim ratchet: `bundle exec ruby -Itest test/unit/marketing_copy_test.rb`.
Above baseline, or a known unswept carrier? Fix it. Rank by live impressions,
not by how bad the claim reads - `.okf/content/claims-canon.md` carries the
rule and the reasoning. Use `blog-write` in UPGRADE mode.

2. **UPGRADE - is an approved upgrade waiting?**
Check 20.09 §13 for UPGRADE verdicts with nothing shipped against them. An
upgrade to a page that already ranks beats a new post that has to earn its
position from zero. Hand it to `blog-write`.

3. **WRITE - is there an approved WRITE row?**
A row that passed the Stage A gate and has not been drafted. `blog-write`.

4. **RESTOCK - is the queue dry or stale?**
No actionable row, or the rows are older than the data they rest on. Run
`blog-next`, which will rebuild the plan section rather than dead-ending.

If nothing applies, say so with the check that proves it. "Nothing to do" is a
legitimate answer exactly once - if you return it twice running without the
inputs changing, the ordering above is wrong and needs revisiting rather than
repeating.

## Keep going

**One unit at a time (WIP=1), then re-decide.** Finish the action, merge or open
its PR, then run the decision above again with the new state. Do not batch three
posts into one PR, and do not stop after one unit because a unit is "done" - the
user asked for the blog to move, not for one task.

Re-deciding matters: a repair can reveal three more carriers, and an upgrade can
falsify the row that was queued behind it. State changes under you.

## What "improves the brand" actually means here

Concrete, in the order these tend to pay:

- **Remove reasons to disbelieve us.** Invented case studies, unsourced numbers,
claims with no engagement behind them. This is why REPAIR is first.
- **Make the pages that already get read better**, rather than adding pages
nobody has found yet.
- **Say something only we can say.** First-hand operating evidence beats a
summary of public material - the latter is somebody else's post.
- **Write for the founder, not the developer.** A post that addresses developers
can be excellent and still be worth nothing here.

Volume is not on that list. A tenth mediocre post costs more than it earns,
because it dilutes the nine and gives the sceptic more surface to find a flaw.

## The gates are not yours to waive

Both hands carry their own blocking gates and they stay blocking. You may
sequence work and decide what to do; you may not decide a gate does not apply
today. If a gate blocks, that is the system working.

**Author ≠ verifier.** If agent spawning is unavailable, run the gates inline and
say plainly in the handback that no independent verifier ran.

## Three exits

- **SHIPPED** - one or more units delivered, each with its PR and gate numbers.
Say what you did and what you would do next.
- **HOLD, with evidence** - the checks ran and genuinely produced no action worth
taking. Quote the checks. Never invent work to look busy; a fabricated post is
the precise harm this skill exists to prevent.
- **BLOCKED on a named decision** - whether a claimed engagement or number is
real, publishing outward, overriding a documented gate, pricing/naming, or a
split-and-irreversible call. Name it, take the conservative option meanwhile,
and continue with everything not blocked by it.

Report in the user's terms: what a founder reading the blog would now see that
they would not have seen before.
123 changes: 123 additions & 0 deletions .claude/skills/blog-write/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
name: blog-write
description: >
Actually WRITE and ship a blog post - draft, critics, gates, commit, PR. This
is the delivery half of the pipeline; blog-next decides WHAT to write and this
writes it. Use whenever the user asks to write, draft, publish, ship or add a
blog post; when they hand over a topic and expect a post out the other end;
when they point at an approved row in the content plan; and when they ask to
UPGRADE or refresh an existing post rather than write a new one. Also use when
a /blog-next run ended in a WRITE or UPGRADE verdict and nothing has been
drafted yet.
NOT for choosing the topic (use blog-next), LinkedIn posts (linkedin-post-jt),
course chapters, or landing pages (page-cro, landing-page-optimization).
---

# Blog: write it and ship it

**You produce a published post.** Not a plan, not a recommendation. If you finish
without a committed post or a named blocker, the run failed.

`blog-next` owns "what and whether." You own everything after that.

| Stage | Owner |
|---|---|
| Topic, research, gated outline | `blog-next` |
| **Draft → critics → gates → commit → PR** | **this skill** |

**Read before starting; this skill deliberately does not copy them, so a
correction lands in one place:**

- `docs/workflows/blog-pipeline.md` - **canonical from STEP 4 onward.** Follow it
step by step. Every blocking gate is defined there.
- `docs/90-99-content-strategy/strategy-analysis/90.11-voice-guide.md` - voice.
- `docs/90-99-content-strategy/strategy-analysis/90.10-icp-primary-website-target.md` - the reader.
- `.okf/content/claims-canon.md` - what you are allowed to say. A number with no
in-repo source is a defect, not a detail.

## What you need before drafting

A topic row with an angle, the research digest, and a gated outline. If you have
all three, start at STEP 4.

**If you don't, get them - do not draft anyway.** Run `blog-next` first and come
back with its output. A post drafted without the premise audit is how a decayed
row or a dedup collision reaches a draft, which is the exact failure the split
exists to prevent.

**If the user handed you a topic directly**, that is not a licence to skip the
audit - it is the case where the audit matters most, because nobody has checked
it against the corpus yet. Run `blog-next`'s Stage A on it, then continue here.

## New post or upgrade

Both are this skill's job, and the choice is `blog-next`'s verdict, not yours.

- **WRITE** - a new post at `content/blog/<slug>/index.md`.
- **UPGRADE** - edit the existing post in place. Do NOT create a second post on
the same topic; that is the cannibalisation the verdict exists to prevent.
Keep the URL, keep what still holds, and rewrite what the new material
changes. An upgrade that only appends a section has usually missed the point -
if the thesis moved, the shape moves with it.

## Running the pipeline

Follow `blog-pipeline.md` STEP 4 → STEP 7. It carries the cadence quotas, the
BAD/GOOD pairs, the two-pass writing rule, and the gate definitions. Do not
paraphrase it from memory - open it.

The gates that most often get skipped, named here so they are not:

- **STEP 4e self-critique** (`reflexion-reflect`) before the critic panel.
- **STEP 5a anti-AI pass** before the review loop, not after.
- **STEP 5b slop gate: `slop >= 8/10`.** This is the blog scale, 0-10, higher is
better. The course's `Slop <= 25` is a different scale in the other direction -
never mix them.
- **STEP 5c cold-eyes gate** runs LAST, by a reviewer that did not write the draft.
- **STEP 6b pre-publish checklist**, then **STEP 7 validate**.

**Author ≠ verifier is the point of the panel.** Spawn a different agent type for
each critic role. If agent spawning is unavailable in this session, run the gates
inline against the written criteria and **say plainly in the handback that no
independent verifier ran** - a self-reviewed draft that claims a passed 4-eyes
gate is worse than one that admits it had none.

## Gates before commit

Content-only diff (markdown prose and frontmatter, no template/CSS/body HTML):
`bin/hugo-build` plus the rendered scroll gate. The visual suites do not apply.

The moment the diff touches a template, stylesheet or body HTML, the full visual
gate applies - `bin/qtest --changed` before the commit. Check the actual diff,
not what you intended to change.

New media gets the visual gate in `blog-pipeline.md`: 1280x800 and 390x844, four
criteria scored, and the scores written into the commit message.

## Ship

Feature branch, commit, `gh pr create` with the evidence. Never push to master.

**End the handback with the local review link** - `http://localhost:<port>/blog/<slug>/`.
One dev server per session, never 1313:

```
PORT=$((20000 + RANDOM % 20000)) bin/dev
```

## Three exits, and only three

- **SHIPPED** - committed, PR open, gate verdicts quoted with their numbers.
- **BLOCKED on a named decision** - one of: whether a claimed client engagement or
number is real, publishing outward, overriding a documented gate,
pricing/naming/internal numbers, a split-and-irreversible call. Name which one,
and take the conservative option meanwhile where one exists.
- **FAILED the gates twice** - stop, hand back the draft with both critic reports
and what you changed between rounds. Do not iterate a third time silently; two
failed rounds on the same draft usually means the outline was wrong, which is
`blog-next`'s problem and not something more prose will fix.

**HOLD is not an exit here.** By the time work reaches this skill the decision to
write has already been made and audited. If you find a reason the post should not
exist, that is a falsified premise - say so explicitly and hand it back to
`blog-next` rather than quietly producing nothing.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ claude-flow.config.json
.claude/skills/*
!.claude/skills/README.md
!.claude/skills/blog-next/
!.claude/skills/blog-write/
!.claude/skills/blog-operator/
# `:33` (.claude/**/*.json) still matches inside an un-ignored directory, so a
# hand-authored skill's JSON needs its own file-level negation to ship.
!.claude/skills/blog-next/**/*.json
Expand Down
78 changes: 78 additions & 0 deletions .okf/content/fabrication-ratchet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
type: Validator
title: Fabricated-claim ratchet
description: A structural gate over blog source that counts invented-client-work shapes (case-study headings, "in our experience", tagged-but-unremoved figures) and fails when the count rises.
resource: test/unit/marketing_copy_test.rb
tags: [validation, blog, claims, regression]
generated:
by: claude/opus-5
at: 2026-08-22T12:20:00Z
timestamp: 2026-08-22T12:20:00Z
---

The [banned-phrase guard](/content/banned-strings-ratchet.md) catches a defect
made of specific WORDS. It cannot catch the class
[claims-canon](/content/claims-canon.md) names "invented client work", because a
fabricated case study is written in ordinary words. What that class does have is
a SHAPE, and a shape is greppable.

`test_blog_does_not_regress_on_fabricated_claim_markers` counts those shapes in
`content/blog/**/*.md` and fails when the count exceeds its baseline.

# Why a gate rather than another sweep

Three hand-sweeps on 2026-08-22 each missed carriers the previous one missed,
because each keyed on a different surface:

| Sweep | Keyed on | Missed |
|---|---|---|
| 1 | `N clients/companies/times` | everything phrased as a case study |
| 2 | case-study headings | everything phrased as "in our experience" |
| 3 | body prose | frontmatter `description` / `twitter_description` |

Sweep 2 found the two largest carriers by impressions, both absent from sweep
1's list. That is claims-canon's own finding reproducing itself inside a single
session: **manual sweeps under-count badly.** A ratchet does not need to
recognise a fabrication - it only has to notice the count going up.

# Rules

- **Markers are shape, not judgement.** A case-study heading, the
recurrence-generalisation hatch ("in our experience", "the pattern we see"),
and `(figures unverified)` - a number tagged instead of removed, where the tag
is the part a reader skips.
- **Prose markers match the whitespace-collapsed document, not line by line.**
`/\bin our experience\b/` returns false against `"in our\nexperience"`, so a
line-based scan has a hole that ordinary Markdown wrapping opens. Heading
markers stay line-based, because Markdown ends a heading at the newline, and
a line number is what you want when fixing one.
- **A case-study heading is a suspicion, not a verdict.** The shape is identical
whether the subject is real or invented. `VERIFIED_CASE_STUDIES` allows
specific headings by path; adding a line there asserts that someone checked.
The test is whether the write-up names a subject a reader could go verify -
this repo, a named client, a public postmortem. "A medium-sized content
platform" is not a subject, and that is the whole difference.
- **Set the baseline to the measured count, then prove it is exact** by dropping
it one lower and watching it fail. Slack in a ratchet swallows real defects -
the rendered baseline in the same file sat at 14 against an actual 11, and
those three spare hits absorbed a planted phrase whole.
- **dev.to imports are excluded**, derived from `source: dev_to` frontmatter, on
the same basis as the rendered pass: their stats belong to their original
authors. That is a TEST-scoping call and NOT editorial absolution - those
posts are still published on our domain.

# Prioritise by impressions, never by indignation

When clearing survivors, rank by live GSC impressions. The first sweep
prioritised a `featured: true` ICP-facing post carrying a "200+ clients" claim;
it had **4 impressions in 90 days**, while the top carrier had 40,025. `featured`
is a site-internal flag, not traffic. Claims-canon states the reasoning: a
fabricated story on a page nobody reads is a liability, on a ranking page it is
the first thing a prospect sees.

# Citations

- `test/unit/marketing_copy_test.rb` - `FABRICATION_HEADING_MARKERS`,
`FABRICATION_PHRASE_MARKERS`, `VERIFIED_CASE_STUDIES`, `FABRICATION_BASELINE`.
- [claims-canon](/content/claims-canon.md) - the standing purge policy and the
four fabrication classes.
1 change: 1 addition & 0 deletions .okf/content/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
* [Company claims canon](claims-canon.md) - founding date, tenure, rating; what JetThoughts may assert about itself, where it is ratcheted, and why ranking legacy blog posts still hold fabricated client stories
* [Voice rules](voice-rules.md) - Sam voice, banned patterns, and the em-dash rule
* [Banned-strings ratchet](banned-strings-ratchet.md) - how fixed prose defects stay fixed
* [Fabricated-claim ratchet](fabrication-ratchet.md) - the structural gate over blog source, why three hand-sweeps each missed what the last one missed, and why you rank by impressions rather than indignation
Loading
Loading