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
55 changes: 55 additions & 0 deletions .claude/agents/blog-batch-orchestrator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
name: blog-batch-orchestrator
description: Outer-loop orchestrator for N-post blog sprints - picks topics from the groomed queue with live-GSC premise audits, runs blog-post-coordinator per post (WIP=1), then batch gates, plan/OKF sync, one PR, and CI watch. Loops until N posts are delivered or the queue runs dry.
model: fable
---

# Blog Batch Orchestrator (outer loop)

Purpose: deliver N publish-ready posts end-to-end without stopping between
posts. Canonical process: `docs/workflows/blog-pipeline.md` (per-post pipeline
+ Batch mode section). You are the manager: sequence, dispatch, verify,
record - you write plans and reports, not post prose.

## The outer loop

0. **Setup once**: feature branch; session-random dev server (`PORT=$((20000 +
RANDOM % 20000)) bin/dev`); read the 20.09 plan §12 queue + refresh-debt
rows; optionally start a NotebookLM deep-research run for fresh angles.
1. **Pick next row** - premise audit FIRST: live GSC for the topic (plans
decay), dedup check. Collision → record the rescope verdict in the plan and
take the next row. Real-code mining (this repo, ~/dev/elital) happens here
so the coordinator gets a sanitized digest, not repo access questions.
2. **Dispatch ONE `blog-post-coordinator`** with the topic row, branch, the
dev-server port/base-URL from step 0, and the digest. WIP=1: wait for its
SHIPPED / RESCOPE / BLOCKED report before the next row. Independent
research/recon agents (read-only) may run in parallel; never a second
committing coordinator in the same checkout - if a second one must run,
it gets its own git worktree (shared-checkout commits race).
3. **Verify the report**: spot-check the commit exists, gates quoted, riskiest
claims noted. BLOCKED → record in the plan and move on; do not force gates.
4. Loop 1-3 until N SHIPPED or the queue is dry after rescopes.

## Batch close-out (after the last post)

5. **Cluster sweep** per `.okf/content/voice-rules.md` same-day-cluster rule:
grep the batch for shared intensifiers, metaphor families, verbatim 4+-word
phrases, cloned CTA tails, meta-narration templates. Fix across posts, then
run one slop/editor critic pass on the sweep DIFF (4-eyes is blocking for
every commit) before the polish commit.
6. **Records ride the branch**: 20.09 statuses + changelog, `/okf:okf maintain`
+ `/okf:validate .okf --strict`, blog index regen.
7. **One PR** for the batch: per-post verdict table, per-commit summary, gates
listed, flagged decisions surfaced to Paul. Merge conflicts with a moving
master are expected - merge master in, resolve (`.okf/log.md` appends: keep
both), rebuild, push.
8. **Watch CI** with a Monitor until settled; re-run known flakes (15-min
checkout timeout) once with evidence; report any real failure instead of
retrying blindly.

## Handback

End with: verdict table (slug / SHIPPED-RESCOPE-BLOCKED / commit), PR link,
localhost review link per post on your session port, and every decision you
flagged rather than made. A post is not "delivered" until it passed cold-eyes,
its gates are green, and its commit is on the PR.
60 changes: 60 additions & 0 deletions .claude/agents/blog-post-coordinator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
name: blog-post-coordinator
description: Inner-loop coordinator for ONE blog post - drives writer, 3-critic panel, cold-eyes gate, and ship gates in a loop until PUBLISH-READY and committed, or 2 failed fix rounds. Spawned per post by blog-batch-orchestrator or the main session.
model: fable
---

# Blog Post Coordinator (inner loop)

Purpose: take ONE assigned topic row from idea to a committed, publish-ready post.
You own the loop; you do not stop mid-post to ask questions that the canonical
docs already answer.

Input contract (from your spawner): the topic row (slug, angle, dedup notes),
the branch to commit on, the dev-server port/base-URL for the scroll gate
(never assume 1313; if missing, start your own on a session-random port), and
any source-material digest (real-code mining, trends facts). If a required input is missing, derive it from
`docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md`
before asking anyone.

## The loop

1. **Premise + dedup** (skip only if spawner says already done): live GSC pull
for the topic, `ls content/blog | grep` + qmd search. Collision → report
RESCOPE verdict back to spawner instead of writing.
2. **Writer**: spawn a `content-creator` agent with the full packet - voice
guide 90.11, cadence anchor from `docs/workflows/blog-writer-reference-samples.md`,
blog-pipeline STEP 4-4d rules, verified internal links, frontmatter hard
rules, latest-versions-as-present framing, zero fabrication. Two-pass write.
3. **Critic panel** (parallel, resumable via SendMessage): `core-reviewer` as
(a) tech fact-checker - fetch every cited source, verify every claim and
every line of copyable code; (b) slop/SEO - AI-pattern scan, SEO checklist,
cross-post repetition gate vs siblings; (c) copy editor - thoughtbot cadence,
cut-test, diagram earns-its-place. Synthesize; apply fixes yourself
(surgical edit discipline). **Iterate: re-send changed sections to the
objecting critic until tech is clean, slop >= 8/10, editor <= minor.**
4. **Cold-eyes**: fresh-context `content-creator` with edit rights running the
blog-pipeline STEP 5c 9-check prompt verbatim. Loop 5c (not the panel) until
PUBLISH-READY. (The canonical loop's founder-resonance check is absorbed
here and by the editor critic; for founder-stream posts add the Alex persona
as a fourth panel critic.)
5. **Ship gates**: stitch 6-slot cover (render at 2400x1260x2, downsample,
VISUALLY inspect the PNG), `bin/render-mermaid` (min visible font x 390 /
viewBox-width >= 9px), `bin/hugo-build` + `ruby bin/check-post-visuals`
green, chrome-devtools scroll gate desktop 1280x800 + mobile 390x844 with
zero console errors and zero 404s.
6. **Commit** on the assigned branch, reviewer verdicts quoted in the message.

## Stop conditions (report, don't grind)

- Any gate - including the step-3 critic thresholds and step-4 cold-eyes -
still fails after **2 full fix rounds** → commit nothing, report BLOCKED
with the failing findings. The iterate instructions above are bounded by
this cap.
- Dedup collision (step 1) → report RESCOPE with the owning post named.
- Anything requiring a business decision (pricing, disclosure of internal
numbers, naming) → flag it in your report; make the conservative call in
the text meanwhile.

Report back: slug, verdict (SHIPPED / RESCOPE / BLOCKED), commit SHA, gate
results one line each, and the 3 riskiest claims a human should eyeball.
11 changes: 10 additions & 1 deletion .okf/build/test-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ generated: { by: claude/opus-4-8, at: 2026-08-12T20:20:00Z }
verified:
- { by: claude/fable-5, at: 2026-08-01T11:30:00Z }
- { by: claude/sonnet-5, at: 2026-08-20T00:00:00Z }
timestamp: 2026-08-20T00:00:00Z
---

# The suites
Expand All @@ -19,7 +20,7 @@ verified:
| `bin/rake test:critical` | Critical Minitest suite (34 runs / 53 screenshots), ~81s host / ~46s Docker since the 2026-08-01 skip_area fix | At component/task milestones and before every commit outside sprint micro-commit trains |
| `bin/test` | Visual regression on the host (baselines in `macos/` on a Mac; on Linux, comparable to `linux/` when run through `bin/setup-test-env`'s pinned stack) | ONCE at PR prep (branch head, before `gh pr create`) or on Paul's explicit confirmation - NOT per commit (Paul 2026-07-31: qtest is the routine gate) |
| `bin/dtest` | Same suite in Linux/Docker (baselines in `linux/`) - CI runs Linux | Same trigger as bin/test; a PR must never open without this leg (green-locally / red-in-CI otherwise) |
| `bin/check-post-visuals` | Ratchet: counts blog posts over 800 words with no mermaid/SVG/image. Fails when the count exceeds `FLOOR` (78 as of 2026-08-13) | Before publishing any post. Added 2026-08-13 - `diagram_rendering_test.rb` only proves diagrams RENDER, nothing proved posts HAVE them, and 25 of the 31 long posts published since 2026-04 shipped with none |
| `bin/check-post-visuals` | Ratchet: counts blog posts over 800 words with no mermaid/SVG/image. Fails when the count exceeds `FLOOR` (**72** as of 2026-08-20, down from 78) | Before publishing any post. Added 2026-08-13 - `diagram_rendering_test.rb` only proves diagrams RENDER, nothing proved posts HAVE them, and 25 of the 31 long posts published since 2026-04 shipped with none |

`bin/qtest` page keys mirror `themes/beaver/assets/css/pages/*.css` basenames
AND `critical/<name>-critical.css` basenames - the two sets differ (e.g.
Expand All @@ -40,6 +41,14 @@ extend it when adding components or critical files. The macOS full suite remains
verify by reading the built HTML or the render, not by trusting green. This
is the false-green class documented in
`docs/20-29-testing-qa/test-architecture-anti-masking.md`.
- **Flat-file posts are invisible to the visuals ratchet** (2026-08-20).
`bin/check-post-visuals` globs `content/blog/*/index.md`, so a post living as
a flat file (`content/blog/2025/<slug>.md`) is outside the ratchet population
AND outside page-bundle tooling - no pre-rendered mermaid, no local cover.
Converting one to a bundle preserves its URL as long as the `slug`
frontmatter is set, and adds it to the ratchet. The script self-reports slack
(`post-visuals: floor is loose, lower FLOOR to N`) - obey it: `FLOOR` dropped
78 -> 72 on 2026-08-20 on the script's own prompt.
- **`FORCE_SCREENSHOT_UPDATE=1` re-records EVERYTHING** (2026-08-14). On
`bin/dtest` it also disables the `git checkout -- .../linux` guard that
normally discards sub-tolerance Rosetta drift, so a run rewrites all 45
Expand Down
10 changes: 10 additions & 0 deletions .okf/content-strategy/content-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ verified:
at: 2026-08-13T09:56:20Z
- by: claude/opus-5
at: 2026-08-19T00:00:00Z
timestamp: 2026-08-20T00:00:00Z
---

# Plan of record (2026-08-07)
Expand Down Expand Up @@ -97,6 +98,15 @@ One test: **will someone search this in six months?**
Version-current content incurs refresh debt - budget 1 refresh slot/month. Proof
it compounds: the Kamal 1/Traefik post is now factually wrong and still ranking.

**Pick the refresh slot from live GSC, not from the plan (2026-08-20).** The
plan's own "still ranking" assertions decay within weeks. The August slot was
decided on a live pull at decision time: the row the plan named had fallen to 4
impressions, while `ruby-3-4-yjit-performance-guide` - unnamed anywhere in the
plan - was carrying 6,310 impressions at position 9.5 and turned out to be full
of fabricated claims (see [claims-canon](/content/claims-canon.md)). Same rule
as §"before writing a queued row": check the artifact, not the plan's memory of
it. The MCP pulls live - [analytics-access](/workflows/analytics-access.md).

# Overview

Supersedes the ICP-E-focused [20.07 plan](https://github.com/jetthoughts/jetthoughts.github.io/blob/emdash/seo/docs/projects/2510-seo-content-strategy/20-29-strategy/20.07-content-plan-icp-e-q2-2026.md).
Expand Down
28 changes: 28 additions & 0 deletions .okf/content/claims-canon.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ status: stable
generated:
by: claude/opus-5
at: 2026-08-20T00:00:00Z
timestamp: 2026-08-20T00:00:00Z
sources:
- id: hugo-config
resource: config/_default/hugo.toml
Expand Down Expand Up @@ -147,6 +148,33 @@ grep):
from staff experience ("our fractional CTOs average 15+ years") and
false-positives immediately. A ban that cannot tell those apart is noise.

# Ranking legacy posts are a live reservoir of fabricated claims (2026-08-20)

The fabricated-claims purge (PRs #475 / #477 / #481) swept landing and course
surfaces. It did not reach the blog archive, and the blog archive is where the
traffic is. `ruby-3-4-yjit-performance-guide` - **6,310 impressions at position
9.5**, one of the site's better-ranking pages - carried four classes of
invention at once:

| Fabrication | What it claimed |
|---|---|
| Invented third-party internals | Shopify saving "$2.4M" from YJIT |
| Invented third-party results | GitHub deployment outcomes |
| Invented client work | two JetThoughts case studies with no client behind them |
| Invented future | a Ruby 3.5 / 3.6 / 4.0 roadmap |

Rewritten in place 2026-08-20 as the Ruby 4.0 YJIT-vs-ZJIT guide with every
claim sourced. Two rules follow:

- **Extend the purge to ranking legacy posts, highest-impression first.** A
fabricated client story on a page nobody reads is a liability; on a page at
position 9.5 it is the first thing a prospect sees. SEO-era posts predate the
sourced-numbers policy and were never audited against it.
- **Audit the premise before refreshing.** The content plan's *named* refresh
candidate had decayed to 4 impressions while this unnamed post was the real
asset. Pull live GSC at decision time; see
[content-plan](/content-strategy/content-plan.md).

# Where this canon is repeated

Correcting a published fact means sweeping the **instruction layer**, not just
Expand Down
2 changes: 1 addition & 1 deletion .okf/content/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

* [Course structure](course-structure.md) - the 5-module spine, Going Further set, and course_sequence.yaml
* [Course canon](course-canon.md) - the numbers every chapter must agree on
* [Company claims canon](claims-canon.md) - founding date, tenure, rating; what JetThoughts may assert about itself and where it is ratcheted
* [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
16 changes: 14 additions & 2 deletions .okf/content/voice-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ generated:
by: process:okf-migrate
at: 2026-07-13T00:00:00Z
verified:
by: claude-opus/4.8
at: 2026-08-19T00:00:00Z
by: claude-fable/5
at: 2026-08-20T00:00:00Z
---

Full guides: `docs/90-99-content-strategy/strategy-analysis/90.11-voice-guide.md`
Expand Down Expand Up @@ -81,6 +81,18 @@ worked with", dual-source statistical openers, sustained staccato.
Sentence-level slop scoring is necessary but NOT sufficient - shape-tell
review (essay arc, pivot sentences, cloned scaffolding) runs alongside it.

**Same-day-cluster tells (2026-08-20, 4-post batch review).** When several posts
ship in one batch, a second sweep runs ACROSS them, because per-post review can't
see cluster fingerprints: (1) shared intensifier stamping - "genuinely" appeared in
all four posts of the 2026-08-20 batch; (2) a shared metaphor family - the
"X earns Y" economics figure appeared 5x across the batch (earn their keep / pays
rent / earns it / earn attention); cap at one per post; (3) verbatim phrase reuse
("covers that half of the build" word-for-word in two siblings); (4) meta-narration
templates ("X deserves its own paragraph" / "deserves more attention than it
gets"); (5) cloned CTA tails ("does exactly that" / "builds exactly this").
Sweep: grep the batch for the day's pet intensifiers and repeated 4+-word phrases
before the PR.

Regression sweep before any handback:

```bash
Expand Down
35 changes: 35 additions & 0 deletions .okf/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Bundle Update Log

## 2026-08-20 (YJIT refresh) - the fabricated-claims purge never reached the blog archive

* **A page at position 9.5 was inventing client work.**
`ruby-3-4-yjit-performance-guide` (6,310 impressions) carried invented Shopify
internals ("$2.4M saved"), invented GitHub deployment results, two fabricated
JetThoughts client case studies, and a fictional Ruby 3.5/3.6/4.0 roadmap.
Rewritten in place as the Ruby 4.0 YJIT-vs-ZJIT guide with every claim
sourced. PRs #475/#477/#481 swept landing and course surfaces; the blog
archive - where the traffic actually is - was never audited. Purge continues
highest-impression-first. Recorded in
[claims-canon](/content/claims-canon.md).
* **The plan's named refresh candidate had decayed to 4 impressions** while
this unnamed post was the real asset. The slot was chosen from a live GSC
pull at decision time, not from the plan's month-old "still ranking" claims.
Generalises the existing "check the artifact before drafting a queued row"
rule to refresh slots: [content-plan](/content-strategy/content-plan.md).
* **Flat-file posts sit outside two toolchains at once.**
`content/blog/2025/*.md` is missed by `bin/check-post-visuals` (globs
`content/blog/*/index.md`) and by page-bundle tooling (pre-rendered mermaid,
local covers). Bundle-izing preserves the URL when `slug` frontmatter is set
and adds the post to the ratchet population. `FLOOR` ratcheted 78 -> 72 on
the script's own "floor is loose" prompt.
[test-gates](/build/test-gates.md).

## 2026-08-20 (#29 re-source closed) - the v1 list is dead, and VERBATIM tags recorded intent, not verification

* **The whole 25-row v1 cold list is closed: 19 verified-and-dropped, 6
Expand Down Expand Up @@ -2139,3 +2163,14 @@ ridden the PR that needed them.

Recipe corrected in [ci-gates](build/ci-gates.md). Generalised check before any
automation call: *does this write to the repo, and if so, where does it land?*

## 2026-08-20 - Same-day-cluster voice tells (4-post AI+Rails batch)

Shipping four posts in one day exposed a review gap: per-post critics pass a
post while the BATCH carries fingerprints - a shared intensifier ("genuinely"
in all four), a shared metaphor family ("X earns Y" 5x), verbatim phrase reuse
between siblings, meta-narration templates, and cloned CTA tails. Added the
cross-batch sweep to `content/voice-rules.md`. Also: critic-tech caught that
ruby_llm 1.16 accepts a block only on `model` - `temperature {}` is a silent
no-op - which corrected the R5 post's published sketch (and exposed a likely
live bug in the source app's own AgentBase).
2 changes: 1 addition & 1 deletion bin/check-post-visuals
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# Usage: bin/check-post-visuals [dir] (default: content/blog)
# POST_VISUALS_LIST=1 to print the full burn-down list.

FLOOR = 78
FLOOR = 72
MIN_WORDS = 800
root = ARGV[0] || "content/blog"

Expand Down
Loading
Loading