diff --git a/.claude/agents/blog-batch-orchestrator.md b/.claude/agents/blog-batch-orchestrator.md index 37f74901b..70b31ca43 100644 --- a/.claude/agents/blog-batch-orchestrator.md +++ b/.claude/agents/blog-batch-orchestrator.md @@ -14,16 +14,18 @@ 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, the digest, and an explicit - "premise audited: yes/no" flag (you did step 1, so say yes - it saves the - coordinator a duplicate GSC/dedup pull). WIP=1: wait for its + RANDOM % 20000)) bin/dev`); read the 20.09 plan **§13** queue + refresh-debt + rows (§12's R-queue is retired - do not draft from R1-R9). +1. **Pick next row** - run the `blog-next` skill's Stage A through C for the + row: premise audit FIRST (live GSC/GA4, plans decay), dedup check, research, + then the gated outline. Skipping Stage C here is how a batch run loses the + outline gate the single-post path gets. 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`** (`model: fable`) with the topic row, + branch, the dev-server port/base-URL from step 0, the digest, the gated + outline, and an explicit "premise audited: yes/no" flag (you did step 1, so + say yes - it saves the coordinator a duplicate GSC/dedup pull). 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, diff --git a/.claude/agents/blog-post-coordinator.md b/.claude/agents/blog-post-coordinator.md index 495d87b14..2dbc8b4b0 100644 --- a/.claude/agents/blog-post-coordinator.md +++ b/.claude/agents/blog-post-coordinator.md @@ -12,8 +12,11 @@ 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 +(never assume 1313; if missing, start your own on a session-random port), +any source-material digest (real-code mining, trends facts), and the approved +outline if one was gated upstream (the `blog-next` skill produces one; treat it +as the agreed structure and report back if the draft has to depart from it). +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. @@ -24,8 +27,9 @@ before asking anyone. 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. + blog-pipeline STEP 4-4d rules, verified internal links, the approved outline + if you were given one, 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, diff --git a/.claude/skills/blog-next/SKILL.md b/.claude/skills/blog-next/SKILL.md new file mode 100644 index 000000000..4e1ce07c7 --- /dev/null +++ b/.claude/skills/blog-next/SKILL.md @@ -0,0 +1,226 @@ +--- +name: blog-next +description: > + Decide WHAT to write next and whether it is worth writing - by auditing the + content plan against live search data, rebuilding the plan when it fails the + audit, researching primary sources, and gating an outline. This is the + find-the-topic half of the pipeline; it never writes a post. Use whenever the + user asks what blog post to write next, for the next post, a new post, a batch + of posts, or a blog sprint; when they point at the content-plan queue; when + they ask to groom, restock, rebuild or sanity-check the content plan; and when + they name a topic themselves, because the premise audit is what catches a + decayed row or a dedup collision before anyone spends a draft. + NOT for LinkedIn posts (use linkedin-post-jt), course chapters (see + docs/projects/2605-tech-for-non-technical-founders/60-69-policies/), + landing/services pages (page-cro, landing-page-optimization), or revising a + draft that already exists - that is blog-post-coordinator's job. +--- + +# Blog: what to write, and whether to write it + +Two halves, deliberately split. **You own "what and whether."** Drafting, +reviewing and shipping are the other half and belong to the agents below. + +| Stage | Owner | +|---|---| +| 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` | +| N-post sprint, cluster sweep, one PR, CI watch | `blog-batch-orchestrator` | + +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. + +**Read before starting; this skill does not copy them, so a correction lands +everywhere:** + +- `docs/workflows/blog-pipeline.md` - canonical process. STEP 1-3 is Stages A-B. +- `.okf/workflows/analytics-access.md` - properties and every measurement trap. + Stage A is unsafe without it. +- `docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md` + - §13 is the live queue (§12 retired). §13a/§13d record the traps, including + the ones that killed rows in this same file. + +## Stage A - find candidates, then audit them + +**Search demand cannot generate topics on this property; it can only veto them.** +Two audits in Aug 2026 falsified the queue's own lead item, and the reason was +structural: the blog earns ~0.69 named non-brand clicks/day and the ICP is +absent from the query data (`vibe*` ≈ 200 impressions / 0 clicks). A +search-driven queue here returns HOLD forever. So generate candidates from the +three sources below, then use search to kill the bad ones. + +### A1 - generate candidates (do this FIRST, before touching GSC) + +1. **Live trend research.** What is the audience arguing about right now? The + HN Algolia API is free and needs no auth: + ``` + curl -s "https://hn.algolia.com/api/v1/search?tags=story&numericFilters=created_at_i%3E,points%3E200&hitsPerPage=50" + ``` + (compute `` separately - inlining `$(date ...)` in the URL mangles it). + Then `?query=` to recover each story's real URL and HN id for citation. + **Look for a cluster, not a single story.** One hot post is noise; five + front-page posts arguing the same thing in three weeks is a conversation the + audience is already having, and a post that enters it has a reason to exist + that search data can never supply. +2. **Paul's raw material**, when he supplies it - one sentence about something + that happened is a topic ("the agents cancelled half their backlog"). Treat + this as the highest-value source when present; he may not always have it. +3. **Our real work** - this repo and `~/dev/elital`. The 2026-08-20 batch's best + material was a commit-documented outage. Sanitize: shapes and lessons yes; + prompts, model IDs, proprietary numbers no. + +A candidate is only worth writing when an external hook meets first-hand +evidence. A hot topic we have nothing to say about is somebody else's post. + +### A2 - audit the candidates (search as veto, never as source) + +1. **Pull live GSC/GA4** per `analytics-access.md`. Never query from memory of + property IDs. +2. **Run every trap against your own numbers before quoting them** - the + named-vs-page test (one-way: `named ≪ page` voids the page CTR; the converse + licenses nothing), truncation, Trap C (page-level *position* is an artifact + whenever the CTR is), Trap D (query-vs-query is the legitimate level; + page-vs-page carries almost no signal). +3. **Are these impressions human?** Before treating volume as demand, look at the + query strings. Combinatorial permutations of one stem, non-words, or 100+ + impressions at top-10 positions with a flat zero on clicks mean synthetic + traffic, not demand you are failing to convert. `analytics-access.md` carries + the tell and two worked examples. +4. **Name the post's job, then apply the matching floor.** A post is written to + rank, or to be a credible asset for a reader who already arrived from LinkedIn + or a conversation. These are judged differently and conflating them is how the + queue filled with rows nobody should have written. + - **Search-purposed:** convert to clicks/day against the blog's non-brand rate + (§13c: 62 named non-brand clicks / 90 days ≈ 0.69/day). Compute the ceiling - + best-case position at a defensible CTR, minus what the topic already earns, + minus cannibalisation. **A search post whose ceiling cannot move the number + is not a post.** Say so and stop. + - **Arrival-purposed:** the search floor does not apply and quoting it is a + category error. The test is whether the piece would survive a sceptical + founder reading it after a LinkedIn click - does it say something true that + the competition does not, backed by work we actually did. Most trend-driven + candidates are this kind. Say which kind it is in the topic row, so nobody + later kills a good asset with the wrong metric. +5. **Dedup** per `blog-pipeline.md` STEP 1/3b. A filename grep is not a coverage + audit - regexes are order-dependent and miss real pages. Confirm each hit and + check whether two of our pages already split the query. +6. **Reconcile with §13.** N1 is a downgraded hypothesis, not a win. +7. **P0 gate** (20.09 §1): if outreach is stalled, content halts. Read the plan's + own log - Paul overrode it 2026-08-20, scoped to one post - and report the + gate's state plus his last call rather than halting on the rule alone. + +**Overriding the queue:** only on *falsifiable* grounds - a dedup collision, a +dead page, a premise the numbers contradict. Record the verdict and the number +that killed it. Ordering *preference* is Paul's: flag it, take his row. + +### When the audit kills the row - rebuild, do not dead-end + +If the row fails, or the queue is dry, or the survivors all fail the demand +floor, **the deliverable becomes a rebuilt plan section**, not a shrug. Write it +into 20.09 as a new dated subsection. Each candidate carries: + +- the live figure it rests on, with its pull date, window and property +- which traps were run against it, and what they returned +- the demand ceiling in clicks/day and what it is being compared to +- the dedup result, naming any page that already owns the query +- a verdict: WRITE / UPGRADE / FLAG / DO-NOT-WRITE, with the reason + +Record DO-NOT-WRITE rows too, with their numbers. A row nobody logged gets +re-proposed from intuition next quarter. + +**When live data cannot settle it, name the cheap experiment** instead of +guessing - a date/device split, a look at the live SERP for what sits above us, +a title test with a measurable before/after. Fifteen minutes of checking beats +a spent content slot. + +**Gate Stage A before quoting it.** Its output is nothing but measurements, and +CLAUDE.md requires a measurement verified before it is reported. Hand a +`core-reviewer` the raw pulls and the row you propose - goal and artifact, never +your conclusion - and ask it to re-derive. Both Aug 2026 runs had a material +error caught here and nowhere earlier. + +Stage A output: a topic row (slug, angle, live figures, demand ceiling, dedup, +reviewer verdict), a rebuilt plan section, or a HOLD. + +## Stage B - research primary sources + +**Fetch sources; do not recall them.** Training memory is not a citation. + +1. **Web search** per `blog-pipeline.md` STEP 3 - official docs, release notes, + primary reports. This is Stage B's job; the coordinator starts at the writer + and never re-runs research. +2. **NotebookLM** to interrogate a body of sources: `notebook_create` → + `source_add` (`source_type: "url"`, `urls` takes a list) → `notebook_query`. + To *find* sources: `research_start` → `research_status` → + **`research_import`**; without the import nothing enters the notebook. Check + `server_info` first - `stale` means ask the user to run `nlm login`; + `unverified` means the check failed, not that credentials are bad. +3. Ask what a draft needs: what changed and when, the official position, where + practitioners disagree, the strongest counter-argument. Not "summarize this." +4. **Mine our real code** (this repo, `~/dev/elital`) for first-hand material. + Sanitize: shapes and lessons yes; prompts, model IDs, proprietary numbers no. +5. Verify every statistic against its source. Zero fabricated clients, stats, + quotes or personas - `.okf/content/claims-canon.md` records "Sarah" as banned. +6. Internal links per `blog-pipeline.md` STEP 3b. + +Stage B output: a sourced digest - every claim with its URL, links verified. + +## Stage C - outline, then gate it + +Write the outline: hook shape, H2 sequence, what the reader does differently +after each section, where internal links land, the CTA. + +Apply `blog-pipeline.md`'s cut test here, where a cut costs a line instead of a +section. Pick a cadence anchor and hook shape from +`docs/workflows/blog-writer-reference-samples.md`, checked against recent +siblings so the post does not repeat an overused shape - that file and STEP 4b +own the current count. + +**Gate it: author ≠ verifier.** A different agent type than whoever wrote the +outline - `core-reviewer`. Brief it with goal and artifact, never your +conclusions; ask for measurements. A panel handed your inference returns it +wearing independent confidence. + +An outline that fails is rewritten before any prose exists. + +## Hand off - and keep going + +**Default is unattended.** A gated outline is not a place to stop and wait; hand +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`. + +**Several posts** → `blog-batch-orchestrator` with N; it runs Stages A-C per row. + +Session dev server, once, never 1313: `PORT=$((20000 + RANDOM % 20000)) bin/dev`. + +Pass `model` explicitly on every spawn. Both agents declare `model: fable`; if a +fable spawn fails, fall back to `opus` and say you did. + +Full unattended contract, including the completion promise and why it must be +about gates rather than quality: `docs/workflows/autonomous-delivery-prompt.md` +§"Running CONTENT unattended". + +## Three exits, and only three + +A run ends in exactly one of these. **Two exits are not enough** - a loop whose +only outcomes are "shipped" or "try again" will always ship something. + +- **SHIPPED** - gates green, committed, PR open, verdicts quoted. +- **HOLD, with evidence** - a terminal success, not a failure. Every candidate + failed its floor, or the queue is dry after rescopes. Record the numbers that + killed each one so nobody re-proposes them from intuition next quarter. Never + invent a topic to fill the slot; on 2026-08-22 two runs correctly ended here. +- **BLOCKED on a named decision** - one of the five below, named explicitly, with + the conservative option already taken meanwhile where one exists. + +**What genuinely needs Paul** (everything else: decide, record, continue): +whether a claimed client engagement or number is real · publishing outward · +overriding a documented gate · pricing/naming/internal numbers · split and +irreversible calls. A figure you cannot source is not a decision for him - drop +the claim. diff --git a/.claude/skills/blog-next/evals/evals.json b/.claude/skills/blog-next/evals/evals.json new file mode 100644 index 000000000..93c88e648 --- /dev/null +++ b/.claude/skills/blog-next/evals/evals.json @@ -0,0 +1,26 @@ +{ + "skill_name": "blog-next", + "evals": [ + { + "id": 0, + "name": "plain-next-post-request", + "prompt": "what should we write next on the blog?", + "expected_output": "A topic row, a rebuilt plan section, or a HOLD - each grounded in a live GSC/GA4 pull with the measurement traps applied. Must NOT draft a post. Must report the 2607 P0 gate state.", + "files": [] + }, + { + "id": 1, + "name": "user-names-own-topic", + "prompt": "let's do a post on Rails 8 background jobs - solid queue vs sidekiq, that feels like a gap. can you get it started?", + "expected_output": "Runs the premise audit ANYWAY rather than accepting the named topic. Should find solid-queue-vs-sidekiq-complete-comparison and rails-8-solid-queue-migration-guide already exist and report a dedup collision / RESCOPE, not start writing.", + "files": [] + }, + { + "id": 2, + "name": "rebuild-the-stale-plan", + "prompt": "our content plan is out of date and I don't trust the queue anymore. rebuild it from what the search data actually says.", + "expected_output": "Produces a rebuilt plan section where each candidate carries its live figure + pull window, which traps were run, a demand ceiling in clicks/day, dedup result, and a WRITE/UPGRADE/FLAG/DO-NOT-WRITE verdict. DO-NOT-WRITE rows recorded with their numbers.", + "files": [] + } + ] +} diff --git a/.gitignore b/.gitignore index f316c0904..4cc46eab1 100644 --- a/.gitignore +++ b/.gitignore @@ -68,7 +68,14 @@ claude-flow.config.json # only - regenerate with /ruflo-core:init-project; hand-authored skills/agents # elsewhere under .claude/ stay tracked) .claude/helpers/ -.claude/skills/ +# Generated skills are ignored; hand-authored ones are un-ignored by name below. +# `*` not `/` so git descends into the directory and the negations can match. +.claude/skills/* +!.claude/skills/README.md +!.claude/skills/blog-next/ +# `: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 .claude/commands/claude-flow-*.md .claude/agents/browser/ .claude/agents/consensus/ diff --git a/.okf/log.md b/.okf/log.md index 445c4da41..7dbc0c470 100644 --- a/.okf/log.md +++ b/.okf/log.md @@ -174,6 +174,131 @@ banned claim on pages with ranking history; a repo-wide test would fail on first run and the honest fix is a scoping decision, not a regex. Scope is recorded in the canon rule itself so the next person does not mistake the absence of a test for the absence of a rule. +## 2026-08-22 - second blog-next run: the replacement topic died too, and the skill learned to rebuild + +The morning run falsified N2 and proposed "the Rails environment-setup / +dependency-installation neighbourhood" as its replacement. The afternoon run +killed that too, on dedup - **there is no neighbourhood.** The `install` query +family is complete at 27 rows (`has_more FALSE`) and totals ~129 impressions / +90 days; 84 of them are `rails install dependencies`, which we already own at +position 6.5 and 15.48% CTR. Ceiling on a new post: `84 x 0.30 - 13 = +12 clicks +/ 90 days = +0.13/day`, while cannibalising our own best converter. + +**The demand floor is the finding that outlasts both rows.** Across 500 sampled +`rails` query rows, seven earned any click - 23 clicks in 90 days. The whole +blog is 62 named non-brand clicks / 90 days = **0.69 clicks/day**. Any candidate +must be converted to clicks/day and compared against that before it is scheduled; +most "findings" at this volume are noise wearing a percentage. + +**Second candidate, also rejected.** The data-migration family: 533 impressions, +zero clicks, weighted position 11.19, and the owning page contains the exact +answer at `index.md:123-129`. The tempting reading is a snippet failure. It fits +worst: P(0 clicks | 1% CTR, n=189) = **15%**, an ordinary event; the family +carries the synthetic fingerprint recorded here this morning (7 permutations of +one stem, plus `evaluate rubie on data migration` at 79 impressions and position +5.6 - "rubie" is not a word); a near-exact-match title taking 0 of 533 is not how +a weak snippet fails; and zero-click SERPs are unexcluded, since per-query +position can never proxy for visibility. FLAGGED, not scheduled, behind a +15-minute date/device + live-SERP check. + +**Skill change (Paul, this session): split find-the-topic from deliver-the-post, +and give the finding half somewhere to go when the plan fails.** Both runs +dead-ended at "the queue needs re-grooming" with no path forward, because Stage A +could audit a row but not rebuild the plan. `blog-next` now produces one of three +things and never a draft: a topic row, a **rebuilt plan section**, or a HOLD. Two +new Stage A steps came directly from what these runs needed: an "are these +impressions human?" check before treating volume as demand, and the demand floor +above. When live data cannot settle a row, the skill now names the cheap +experiment instead of guessing. + +**Two errors of mine, caught by the Stage A gate and recorded so neither +propagates.** The dedup grep reported as 11 slugs returned 9, and listed a page +its own order-dependent `docker.*rails` clause never matched - a filename grep is +not a coverage audit. And 15.48% is n=13 clicks: 95% CI [7.7%, 23.2%], so no +x-fold claim to two significant figures survives it. + +## 2026-08-22 - the first /blog-next run falsified the queue's lead item + +Ran the new skill's Stage A against the §13 queue. The premise audit killed N2, +the row §13c called "the one finding here strong enough to act on". + +**The 52x was a denominator.** CTR ratios factorize: (14/165)/(10/6131) = +**1.4x clicks × 37.2x impressions**. Auto-install earned 14 clicks, the YJIT +page it "beats 52x" earned 10. N2's corroborating datapoint was worse - +custom-compression beats YJIT 24x on 4 clicks against 10. The foil fails §13a's +own test in one step: named/page = 85/6,131 = **1.4%**. + +Three new rules, written to `analytics-access.md` as Traps C and D rather than +into the plan, because the named-vs-page rule already had three copies and a +correction would have to be chased through all of them: + +- **When the CTR is an artifact, so is the position.** Page-level position is an + impression-weighted mean over the page's own query mix. YJIT reads 9.1 at page + level and 16.7 across every query GSC will name. So page position cannot serve + as the controlled variable in a cross-page comparison - which is exactly what + N2 used it for. +- **The fix for a bad comparison can be a worse one.** §13 correctly caught the + query-vs-page CTR splice, then over-corrected to page-vs-page, which carries + almost no signal: impressions vs CTR runs rho = -0.93 across 12 pages while + clicks vs impressions is +0.32 and not significant. Any two pages from opposite + ends of the impression range manufacture a ~50x result. Query-vs-query at + comparable position is the legitimate move. +- **Synthetic query families are not long tail.** The langchain page reads 36.9% + named - good coverage - but >=1,423 of those impressions are combinatorial + keyword strings at position 5.4 with zero clicks. Long tail is demand you + cannot see; synthetic is no demand at all. Addressable volume was ~550, not + 6,895. + +**Why the section missed it.** §13 is careful - it self-corrects four of its own +errors and applies both traps to N1 and N6. It applied the rule to every row +where the rule *disagreed* with the desired conclusion, and not to the row that +*was* the conclusion. + +**No post written.** The 2607 P0 gate has not cleared (backlog: 0 send-ready, #12 +blocked); Paul's 2026-08-20 override was scoped to one post. He took the +re-groom and declined the write. Stage A's own 4-eyes gate caught one over-read +of mine: 55% named coverage does not make a CTR *validated* - that is the +converse the one-way rule refuses. + +## 2026-08-22 - the blog lane gets a front end, and the gate it never had + +`/blog-next` now owns STEP 1-3 of the blog pipeline (pick from live GSC+GA4, +research primary sources, gate the outline) and hands off to +`blog-post-coordinator` / `blog-batch-orchestrator`. `blog-pipeline.md` stays +canonical for STEP 4 onward; the skill points rather than restates, because the +named-query-vs-page-total rule already had three copies before it and a +correction to `analytics-access.md:181-185` would have had to be chased into all +of them. + +**`reflexion-reflect` was never wired into the blog lane.** CLAUDE.md declares it +BLOCKING for "any LinkedIn/blog/marketing draft". Grep found it live in +`.okf/workflows/linkedin-post-pipeline.md`, `autonomous-delivery-prompt.md`, and +three 2605 course docs - and absent from `blog-pipeline.md` and both blog agents. +The LinkedIn lane had the gate, the course lane had it, the blog lane did not, +and nothing anywhere recorded the gap. Now `blog-pipeline.md` STEP 4e, modelled +on the LinkedIn pipeline's version. + +**Two review catches worth keeping:** + +*A skill can be inert and look installed.* The first draft went to +`.agents/skills/`, reasoning that `impeccable` lives there and appears in the +session roster. It appears because it is ALSO installed globally; +`async-first-communication` lives only there and is absent. `.agents/skills/` +and `.skills/` are Codex/course profiles Claude Code does not load. Confirm a +skill BY NAME in the roster, never by "that directory has skills in it". + +*And then the correct location was gitignored.* `.gitignore` ignored +`.claude/skills/` wholesale as claude-flow scaffolding, so the skill would have +worked locally and never reached the repo - `git status` shows nothing. Narrowed +to `.claude/skills/*` with name-based negations (`*` not `/`, or git will not +descend and the negation cannot match). Tested both directions: the skill is +tracked, a fake generated skill is still ignored. + +*Scale collision.* CLAUDE.md said `Slop <=25` (0-100, lower better) under a +header claiming the gates are "enforced by blog-pipeline.md", which enforces +`slop >= 8/10` (0-10, higher better). Two scales, one gate name. The <=25 line +was course-scoped text that had drifted into the blog section; both are now +labelled with their lane. ## 2026-08-22 - a re-record that fixed 21 of 22, and the one it could not diff --git a/.okf/workflows/analytics-access.md b/.okf/workflows/analytics-access.md index 44fc2e59f..762ba7fd5 100644 --- a/.okf/workflows/analytics-access.md +++ b/.okf/workflows/analytics-access.md @@ -189,6 +189,49 @@ Same family as the 2026-08-14 error of quoting an average position computed over look at - and, per Trap A, the denominator you did look at may itself be truncated. +*Trap A generalises: a FILTER truncates a denominator as readily as a row limit.* +Added 2026-08-22. `has_more: FALSE` means complete **given the filter**, not +complete. A `query contains "vertical ai"` pull returned 20 rows and read as +exhaustive; `get_search_by_page_query` on the same page returned **38**, and the +filter had structurally excluded both sibling pages because their queries say +`vertical llm`. Before treating a filtered set as a family, pull the same +question a second way - by page dimension, or a broader stem - and see whether +the row count moves. + +*Trap C - when the CTR is an artifact, so is the POSITION.* Added 2026-08-22. +Page-level position is an impression-weighted mean over that page's own query +mix, so a page whose impressions are mostly unattributable has an +unattributable position too. `ruby-3-4-yjit-performance-guide` reads page +position **9.1**; every query GSC will name for it averages **16.7**. +`auto-install-system-dependencies` reads **10.2** while the query that earns +93% of its clicks sits at **6.5**. The example in Trap B already contained this +- virtual-attributes reads 7.8 at page level against 3.7-5.9 on its head terms - +and it was recorded as a CTR finding only. Consequence: **page-level position +cannot be used as the "controlled" variable in a cross-page comparison.** A plan +row did exactly that on 2026-08-21 and the comparison collapsed on re-pull +(20.09 §13d). + +*Trap D - the fix for a bad comparison can be a worse comparison.* Query-level +CTR spliced against page-level CTR is a LEVEL mismatch and is banned. The +correction is query-vs-query, not page-vs-page: across 12 sampled pages, +impressions vs CTR runs **rho = -0.93** while clicks vs impressions is **+0.32 +(not significant)**. Clicks span 7x and impressions span 134x, so page CTR +mostly reproduces the impression ranking inverted, and any two pages picked from +opposite ends of the impression range yield a ~50x "finding" regardless of what +is being compared. Page-vs-page carries almost no signal; query-vs-query at +comparable position does. + +*Synthetic query families are not long tail.* Long tail means real demand you +cannot see; a synthetic family means no demand at all, and the two call for +opposite decisions. `getting-started-langchain-ruby` reads 36.9% named - good +coverage by the Trap B threshold - but >=1,423 of those named impressions are +combinatorial keyword strings (`langchainrb ruby gem openai` + `{2024}`, +`{2024 2025}`, `{integration 2024}`, 15+ variants) at average position **5.4 +with zero clicks**; P(0 clicks | 1% CTR) is about 6e-7, so they were not served +to humans. Human-addressable volume was ~550 impressions, not 6,895. **Tell:** a +shared stem with tokens appended, high volume per string, top-10 positions, and +a flat zero on clicks. + ## `/blog/` fires no `scroll_depth` - GA4 cannot see the blog index `themes/beaver/layouts/partials/page/analytics.html:72` gates the scroll diff --git a/.okf/workflows/autonomous-loops.md b/.okf/workflows/autonomous-loops.md index 5c93459c7..573950f77 100644 --- a/.okf/workflows/autonomous-loops.md +++ b/.okf/workflows/autonomous-loops.md @@ -66,6 +66,27 @@ shipped: so this is the discipline it erodes first - see the NULL CHANGE rule in [test-gates](/build/test-gates.md). +# Content can run in a loop, but only with three exits (2026-08-22) + +Content looks like it fails the "mechanically checkable" test above, because +"is this any good" is a judgement. It passes once the judgement is converted into +**gates that are allowed to fail the work** - a slop critic that did not write +the draft, a cold-eyes reviewer of a third agent type, every external claim +fetched rather than recalled. The promise is then about those gates, never about +quality. A promise containing "the post is good" is unfalsifiable, and an agent +under loop pressure will assert it. + +**The specific way a content loop goes wrong: it manufactures a post.** The +revert-rather-than-ship erosion described above has an exact content analogue - +stopping feels like failing, so the loop writes something. A loop whose only +exits are SHIPPED and ITERATE will always ship. Give it three: **SHIPPED**, +**HOLD-with-evidence** (a terminal success), **BLOCKED on a named decision**. + +Evidence this is real: on 2026-08-22 two `/blog-next` runs ended in HOLD, and +both were right - the groomed queue's lead row was an artifact (a 52x "finding" +that factorised to 1.4x clicks × 37.2x impressions), and its same-day replacement +failed dedup. A two-exit loop would have written both. + # Write the prompt with the traps in it The loop re-reads the prompt every iteration, so it is the cheapest place to put diff --git a/.okf/workflows/blog-pipeline.md b/.okf/workflows/blog-pipeline.md index fd6f0b01f..7611beb7b 100644 --- a/.okf/workflows/blog-pipeline.md +++ b/.okf/workflows/blog-pipeline.md @@ -22,9 +22,15 @@ is publish-ready when flipped to `false`). # Steps -1. **Pick topic** — from the active content plan - (`docs/projects/2510-seo-content-strategy/20-29-strategy/20.08-content-plan-data-driven-q3-2026.md`), - next 🔲 Planned topic. +Steps 1-4 are owned by the `/blog-next` skill (`.claude/skills/blog-next/`), +which adds the premise audit and an outline gate before any prose exists, then +hands off to `blog-post-coordinator` / `blog-batch-orchestrator`. Running them +inline is the fallback, not the default. + +1. **Pick topic** — from §13 of + `docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md`, + audited against live GSC/GA4 first: plans decay between grooming and + drafting. (§12's R-queue is retired; 20.08 is superseded as a schedule.) 2. **Read context (mandatory)** — [icp](/content-strategy/icp-primary-website-target.md), [voice-guide](/content-strategy/voice-guide.md), the topic's content-plan brief, and `.stitch/design.md` for the cover image. @@ -39,6 +45,10 @@ is publish-ready when flipped to `false`). structure: hook → primary keyword in first 100 words → numbered H2s → vibe-coding angle → "when NOT to" → JetThoughts proof point → CTA → related reading → 5+ external citations. +5b. **Self-critique (BLOCKING)** — `reflexion-reflect` on the draft before the + critic panel (STEP 4e), escalating to `reflexion-critique` after pushback on + the same draft. Added 2026-08-22: the LinkedIn and course lanes carried this + gate, the blog lane never did. 6. Cover image generation, SEO validation, `bin/hugo-build`, and the [visual-scroll-gate](/workflows/visual-scroll-gate.md) all run before the post is considered done — the pipeline does not stop after the diff --git a/CLAUDE.md b/CLAUDE.md index 581634432..aa72c8e79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,6 +90,7 @@ Operational knowledge lives in `.okf/` (`build/`, `content/`, `design/`, `workfl - **Zero**: generic AI language · unsupported claims · Hugo build breaks (`bin/hugo-build` validates) · ad hoc test scripts (header Test line commands only) · Python for analysis (use `rg`/`sed`/`awk`) · duplicate files (`*_new.*`, `*_backup.*`). - **Never leave the shell in a subdirectory (2026-08-19)**: Bash cwd persists between calls; a bare `cd` breaks every later repo-relative path and reads as "file missing". Use absolute paths, `cd <root> && cmd`, or `git -C`. If a known file reports missing, run `pwd` before believing it. - **New-doc locations — `docs/workflows/` is NOT a catch-all** (Paul 2026-08-01). Order: (1) engineering reference → its Johnny Decimal area (`docs/10-19-…` through `90-99-…`, naming `NN.NN-name-{reference|tutorial|how-to}.md`; check the area's README/existing numbers first); (2) company/OS → vault-owned, `docs/business/` stubs only; (3) opportunity validation → `docs/projects/<YYMM-slug>/`; (4) ADRs → `docs/adr/`, incidents → `docs/incidents/`, design system → `docs/design-system/`; (5) `docs/workflows/` ONLY for cross-cutting pipeline how-tos. Everything else edits an existing file. +- **New project skill → `.claude/skills/<name>/SKILL.md` + a `.gitignore` exception** (2026-08-22). `.gitignore` ignores `.claude/skills/*` as claude-flow scaffolding, so a hand-authored skill is silently untracked and `git status` will not show it — add `!.claude/skills/<name>/`. `.agents/skills/` and `.skills/` are Codex/course profiles that Claude Code does NOT load; a skill placed there is inert. Confirm by finding the skill BY NAME in the session roster, never by "that directory already has skills in it" (`impeccable` appears only because it is also installed globally). - **Stale-view check (2026-08-17)**: feedback quotes page text → grep current source FIRST; absent means stale tab/server — reply "already fixed, hard-refresh" with the link. Never fix a defect that no longer exists. - **Read CI at the JOB level (2026-08-22)**: `gh run view <id> --json conclusion` can report `success` for a run whose job concluded `failure` — trusting it produced a false "the re-run passed" handback on #566. Confirm with `--json jobs --jq '.jobs[].conclusion'` or `gh pr checks`. Likewise **"no checks reported" means not-yet-registered, not green**: a monitor waiting for `pending` to disappear fires instantly against an empty check list. Corollary that held when intuition didn't: identical difference_level across two runs = stale baseline, never flake. - **Handbacks end with local review links (Paul 2026-08-17/19)**: any change to something that renders locally ends with its `http://localhost:<port>/<path>/` link(s) — "renders locally" is the test (includes `linkedin-posts/` drafts), not "lives in content/". @@ -106,11 +107,12 @@ Operational knowledge lives in `.okf/` (`build/`, `content/`, `design/`, `workfl ### ✍️ Blog Post Pipeline (MANDATORY) -Any request to write/draft/schedule/publish a post executes `docs/workflows/blog-pipeline.md` end-to-end — do not stop after the draft step; a failing step is fixed and retried before the next (drafts too — publish-ready when flipped). Repo voice guides and workflow docs override generic writing/SEO/humanizer skill advice. Pre-writing reads: voice-guide 90.11, thoughtbot analysis, ICP 90.10. +**Entry point: `/blog-next`** — it owns STEP 1-3 (pick the topic from live GSC+GA4 against the 20.09 §13 queue, research primary sources, gate the outline) and hands off to `blog-post-coordinator` / `blog-batch-orchestrator`. Any request to write/draft/schedule/publish a post executes `docs/workflows/blog-pipeline.md` end-to-end — do not stop after the draft step; a failing step is fixed and retried before the next (drafts too — publish-ready when flipped). Repo voice guides and workflow docs override generic writing/SEO/humanizer skill advice. Pre-writing reads: voice-guide 90.11, thoughtbot analysis, ICP 90.10. **BLOCKING gates** — enforced by blog-pipeline.md; named here so none is skipped, detail in the canonical home: - **Voice / zero-tolerance AI patterns** — banned structural patterns + fixes: voice-guide; use `-` not `—`. -- **Slop ≤25 + shape-tell critic** — slop score is necessary, NOT sufficient; run the shape-tell critic after it passes; every revision runs the regression sweep before handback: `grep -rn "## Why this matters\|Founders who\|Most founders\|Founders we worked with"` across edited chapters + opener-shape count (no 3+ consecutive chapters share a template). Details: voice-guide + memories. +- **Slop + shape-tell critic** — blog posts gate on `slop >= 8/10` (blog-pipeline.md STEP 5b; the `Slop ≤25` 0-100 scale is the COURSE gate — different scale, different direction, never mix them). Score is necessary, NOT sufficient; run the shape-tell critic after it passes; every revision runs the regression sweep before handback: `grep -rn "## Why this matters\|Founders who\|Most founders\|Founders we worked with"` across edited files + opener-shape count (no 3+ consecutive sections share a template). Details: voice-guide + memories. +- **Self-critique** — `reflexion-reflect` before the critic panel: blog-pipeline.md STEP 4e. - **Cross-post repetition** (cluster posts) — anecdote + proof-signal scan: blog-pipeline.md. - **New-media visual gate** — chrome-devtools at 1280×800 AND 390×844, score 4 criteria (look / readable / earns scroll / helpful) and write the scores in the commit/report; NO on 3 or 4 = rollback. Then the full-page scroll gate (`visual-scroll-gate.md`). - **Cognitive load / F-pattern** (posts >800w) — hero visual first fold, labels INSIDE diagrams, one visual break per H2, no 6+ identical bullets/rows, no text bricks (>~5 rendered lines; full rule `.okf/content/voice-rules.md`), decision-aids as tables. Source: `docs/projects/2605-tech-for-non-technical-founders/10-19-research/10.05-content-organization-patterns-2026.md`. diff --git a/docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md b/docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md index d58f946de..5396a4469 100644 --- a/docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md +++ b/docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md @@ -695,9 +695,293 @@ Falcon snippet failure did not survive re-checking. The honest version of the argument is narrower: N2's page-vs-page comparison shows format matters at constant position, and that is the one finding here strong enough to act on. -Recommended order: **N2 first**, then N3. N1 only as an experiment with a -measurable before/after, not as a known win. N5 and N6 are separate decisions. - +Recommended order: ~~**N2 first**, then N3~~ — **superseded by §13d, 2026-08-22. +N2 is falsified.** N1 only as an experiment with a measurable before/after, not +as a known win. N5 and N6 are separate decisions. + +### 13d. N2 does not survive its own premise audit (2026-08-22, live GSC re-pull) + +**N2 is FALSIFIED. The 52x is a denominator, not a reader response.** CTR ratios +factorize into clicks ÷ impressions: + +``` +(14/165) / (10/6131) = 1.4 × 37.2 = 52.1x + clicks impressions +``` + +Auto-install got **14 clicks**; YJIT got **10**. The click ratio is 1.4x. The +headline measured how many unclickable impressions the YJIT page accumulated. +N2's corroborating datapoint is worse: `custom-compression` beats YJIT 24x while +earning **4 clicks to its 10**. + +**Three independent disqualifications, each sufficient:** + +1. **The foil fails §13a's own test in one step.** YJIT named/page = 85/6,131 = + **1.4%**. Its page CTR is noise by the rule this section wrote. A comparison + with one voided term has no ratio. `custom-compression` returns *no query data + at all* against 102 page impressions — 0% named, equally voided. The rule is + direction-agnostic: an unattributable high CTR is as meaningless as a low one. +2. **The "constant position" control is not constant.** Page-level position is an + impression-weighted mean over disjoint query mixes. Decomposed: auto-install's + clicking query sits at **6.5** (page-level says 10.2); every YJIT query GSC + will name averages **16.7** (page-level says 9.1). The two page-level figures + are near-identical to each other and near-unrelated to what they explain. +3. **A third page holds format constant and inverts the result.** + `solving-kamals-target-failed-become-healthy` is exactly the short + single-problem error post N2 prescribes: **1.33x** vs YJIT, 9th of 12 by page + CTR. At query level, controlling position: `rails install dependencies` 15.48% + at pos 6.5 vs `kamal healthcheck` **0/24 at pos 6.6** (p ≈ 1.8%). + +**Across the 12 sampled pages, impressions vs CTR is ρ = −0.93 (p < 0.01) while +clicks vs impressions is ρ = +0.32 (not significant).** Clicks span 7x, impressions +span 134x, so CTR reproduces the impression ranking inverted. N2 selected its +exemplar and foil from opposite ends of that range; the method manufactures a +~50x result from any such pair, regardless of format. + +**New trap for §13a — the fix for a bad comparison can be a worse comparison.** +§13 correctly caught the query-CTR-vs-page-CTR splice, then over-corrected to +page-vs-page, which carries no signal at all. Query-vs-query is a *level* match +and is legitimate; that is where the signal was. + +**And a twin for the §13a rule:** when `named ≪ page`, page-level **position** is +an artifact too, not just CTR. §13a's own worked example already contained this +(virtual-attributes: page 7.8, head terms 3.7-5.9) and it was not drawn out — then +N2 built its control on exactly that number. + +**N3's magnitude is overstated ~12x.** langchain named/page = 2,526/6,842 = 36.9%, +but ≥1,423 of those named impressions are a **synthetic keyword-stuffed family** +(`langchainrb ruby gem openai` + `{2024}`, `{2024 2025}`, `{integration 2024}`, … +15+ combinatorial variants) sitting at average position **5.4 with zero clicks** +— P(0 clicks | 1% CTR) ≈ 6×10⁻⁷. These were not served to humans. Human-addressable +volume is the residual: **~550 impressions / 90 days / 1 click**, about 6/day. +"Long-tail inflated" was the right verdict for the wrong reason: long tail means +demand you cannot see, synthetic means no demand at all. + +**What replaces N2** (topic, not format): write into the **Rails +environment-setup / dependency-installation** neighbourhood. Same-level, +same-position comparison, both pages near-exact title matches for their own head +term: + +| query | position | impr | clicks | CTR | +|---|---|---|---|---| +| `rails install dependencies` | 6.5 | 84 | 13 | **15.48%** | +| `langchain ruby` | 7.3 | 180 | 1 | **0.56%** | + +χ² = 25.4, p < 10⁻⁶. `rails install dependencies` is the only query in the sample +with demonstrated human demand *and* conversion — 13 of the 58 clicks across all +12 pages (22%). This does **not** license "short posts win": kamal holds format +constant and returns 0/24. The variable that moves is which specific query, and +whether it is a task someone is mid-way through executing. + +**Not licensed, stated so it is not re-read as licensed:** auto-install's 55.2% +named coverage does not make its CTR *validated*. That is the converse the +one-way rule refuses; 45% of its impressions are still uninspectable at an +inferred position ~13.1. It is the least-bad coverage in the sample, which is a +different claim. Also: the page-dimension pull returned `row_count=200, +has_more=true`, so no site-wide figure may be computed from these 12 rows. + +### 13e. §13d's own replacement fails dedup. Queue verdict: HOLD (2026-08-22, second pull) + +**§13d proposed "write into the Rails environment-setup / dependency-installation +neighbourhood." That does not survive a dedup check. Retracted the same day.** + +**There is no neighbourhood — there is one query, and we already win it.** Query +filter `contains "install"`, row_limit 500, **row_count 27, has_more FALSE** +(complete over named queries): **~129 impressions / 90 days = 1.4/day**, of which +`rails install dependencies` is **84**. The other 26 rows (~45 impressions, 0 +clicks) are langchain, firefox, laravel and kamal — different topics entirely. +Unclaimed Rails-setup demand: **zero**. + +Ceiling on a new post there, granting it pos 6.5 → pos 1 at a generous 30% CTR: + +``` +84 × 0.30 = 25 clicks − 13 today = +12 clicks / 90 days = +0.13 clicks/day +``` + +— while cannibalising our best-converting query. Verdict: **DO-NOT-WRITE.** + +And the neighbourhood's own evidence points somewhere uncomfortable: the page +winning 15.48% at pos 6.5 is `auto-install-system-dependencies-for-ruby-on-rails-programming` +— **249 words, a 2022 dev.to import, about wiring Homebrew into local setup.** +Read at face value it says a 249-word import beats everything written since, +which is exactly the format inference §13d refused to license. That the +neighbourhood produces that conclusion is the tell that it is not a writable +direction. + +**Second candidate examined and also rejected: the data-migration family.** +Query+page filter, **row_count 8, has_more FALSE**: 533 impressions, **0 clicks**, +impression-weighted position **11.19**, all but one row owned by +`data-migrations-with-rails-ruby`. The page contains the exact answer +(`rails g data_migration`, `db/data`) at `index.md:123-129`, buried under +"Possible solutions" behind a description promising "many tools at their disposal". + +The obvious reading — *ranks but the snippet does not earn the click* — is the +one the data fits **worst**: + +- **The single row is unremarkable.** P(0 clicks | 1% CTR, n=189) = **15%**. A + 1-in-7 event. Only the family aggregate is surprising (0.47% at a 1% floor), + and aggregating presumes the rows are independent human draws. +- **The family has the synthetic fingerprint** this document added to + `analytics-access.md` this morning: 7 permutations of {rails, data, + migration(s), gem}, plus **`evaluate rubie on data migration` — 79 impressions + at position 5.6.** "Rubie" is not a word and no human types that sentence. That + row alone is 15% of the family and carries its second-best position. +- **A near-exact-match title taking 0 of 533 is not how a weak snippet fails.** + A weak snippet loses most clicks, not all of them. Title `index.md:7` is + "Data migrations with Rails". +- **Zero-click SERP is unexcluded.** `rails generate data migration` is the most + AI-Overview-answerable shape there is. GSC reports organic rank, never how many + SERP features sit above it — so per-query position can never proxy for + visibility, which is what this claim needs. +- **Cannibalisation is already measured**, not hypothetical: `rails data + migrations` appears twice, owner at pos 14.7 and + `avoid-data-migrations-in-schema-for-rails-ruby` at 41.7. §4's "stop the + self-inflicted cannibalization" applies. + +Verdict: **FLAG, not a slot.** Prerequisite before any action: a date/device +split plus a look at the live SERP, ~15 minutes, to test whether those 533 +impressions are human at all. Also gated on CLAUDE.md's dev.to import ICP audit +(`source: dev_to`, `seo_override: true` — the snippet is already a deliberate +artifact, and overwriting it would erase a prior decision with no record). + +**The demand floor, which is the real finding.** Across 500 sampled `rails` +query rows, **seven** earned any click — 23 clicks in 90 days. §13c's whole-blog +figure is 62 named non-brand clicks / 90 days = **0.69 clicks/day**. The single +best query in this entire investigation is worth **0.14 clicks/day**, from people +with a Rails console open, while the ICP is `90.10`'s "Alex, the Burned +Non-Technical Founder — BBA, MBA, Business Admin, Marketing, NOT Computer +Science." Alex does not type `rails generate data migration`. + +**QUEUE VERDICT: HOLD.** No row in §13 currently clears the demand floor, the +P0 gate has not cleared (`2607/backlog.md:92`: "0 send-ready. The v1 list is +closed"), and 20.09 §1 still reads "Never ship a content sprint while the +outreach is stalled." Cheap fixes to existing pages remain defensible; a new post +does not. + +**Corrections to my own work in this session**, recorded so neither propagates: +the dedup grep reported here as "11 slugs" returned **9**, and +`rails-8-docker-deployment-production-guide` was listed among its hits although +the `docker.*rails` clause is order-dependent and never matched it — the grep +both undercounted real coverage and was misreported. And §13d's "15.48%" is +n=13 clicks: **95% CI ≈ [7.7%, 23.2%]**, so no x-fold claim to two significant +figures survives it. + +### 13f. Two rows killed, one scheduled (2026-08-22, third pull) + +**N7 "how AI helps developers" (Paul-proposed): DO-NOT-WRITE.** Three independent +kills. (1) **Corpus saturation** - a word-boundary match over `content/blog/` +returns **61 slugs**, containing six near-duplicate pairs already shipped +(`ai-personal-staff-for-everyone`/`-2881`, `ai-powered-code-reviews-…`/`-2025`, +`ai-revolution-what-nobody-else-seeing`/`-335c`, +`building-secure-ai-app-store-future`/`-e4f6`, `devtools-for-ai-agents`/`-45a4`, +`harnessing-ai-guide-for-startups`/`-9e56`). The documented failure mode of this +corpus is duplication. (2) **Zero demand** - query filter `contains "ai "` +returned **180 rows, complete, zero clicks**. The only AI-adjacent clicks on the +whole property are 11 library-tutorial clicks (`langchain tutorial` 5, etc.), a +family `analytics-access.md` already flags as largely synthetic. (3) **Wrong +reader** - it addresses developers; `90.10` is a non-technical founder. + +**N8 `vertical ai agents`: DO-NOT-WRITE A POST. FLAG the existing page, gated on +a 20-minute probe.** This is the first §13 candidate whose arithmetic clears the +demand floor rather than rounding to it - and it still should not be written. + +- Page `/blog/vertical-ai-agents/`: **4,716 impressions, 1 click, position 25.4**. + Named-query subtotal 3,853 = **81.7% named**, so Trap B does NOT fire and the + page position is trustworthy - the first clean case of that in this data. + The single click is on `site:dev.to`. Topical clicks: zero. +- Ceiling, stated as a band because the load-bearing number is one GSC cannot + supply: **0.5-3.5 clicks/day** at positions 5-10, against a 0.63-0.69/day + whole-blog baseline. The band spans 7x and must not be narrowed from this data. +- **The family is decaying**: head term 31.6 impr/day in the first 14 days of the + window, **16.0/day in the last 14**, position drifting 28 → 31. Any ceiling off + the 90-day average is ~2x current reality. +- **Human, but not for the reasons first proposed.** Device split is the real + evidence: **30.4% mobile against a 6% site-wide baseline** (5x), and mobile + ranks better (26.4 vs 28.4); scrapers are desktop-first. The date series is + smooth 8-50/day with no burst and never once page 1, so the synthetic tell + (combinatorial strings at top-10) does not fire. +- **Not a realistic content action.** The live page-1 SERP is Salesforce + Agentforce, IBM Think, Google Cloud, arXiv, a 976k-view YC video and Reddit. + Our asset is a **3-minute dev.to import** (`index.md:3` `source: dev_to`, + `:4` `seo_override: true`) that has held ~28 for 90 days and is drifting down. +- **Cheap probe before any slot is spent** (~20 min): third-party volume for the + exact term - the one number the whole ceiling rests on, and if it is <2k/mo the + ceiling collapses; then count what sits above organic #1 on the live SERP (an + AI Overview on a "what is X" query makes position 1 worth a fraction of its + modelled CTR); then check whether the 28→31 slide is our decay or new entrants. +- **No cannibalisation.** All three on-disk posts are indexed and alive, but + occupy disjoint slices: `vertical-ai-agents/` 4,716 impr, the LLM-variant post + 141 impr @ 15.2, `-next-big-thing` 16 impr @ 9.9 with 0% named. Not competitors. + +**N9 SCHEDULED: benchmark the AI code-search MCP tools on this repo** (Paul, +2026-08-22). Original first-party research, which is what every other row in this +section lacked. Six candidates are already installed and usable here - grepai, +tokensave, claude-context, semble, serena, qmd - against a grep+Read baseline, +over this repo's code AND docs. Measure retrieval correctness, token cost and +latency per task, pick a winner, publish the method with the numbers. Source +base: NotebookLM "Claude Code MCP Integration and Server Guide" (91 sources). +Purpose: **arrival-purposed and plausibly search-purposed** - unlike N7/N8 it +rests on evidence nobody else has rather than on a term we are losing. + +**Two method corrections earned this round, both mine, both already logged as +traps elsewhere in this file:** +1. **A named-query subtotal is not page performance.** I reported "3,848 + impressions, 0 clicks" as the page; the page is 4,716 / 1. Same shape as the + Falcon error in §13d. Quote the ratio, then the number. +2. **`has_more: FALSE` means complete GIVEN THE FILTER.** My `query contains + "vertical ai"` pull returned 20 rows and read as exhaustive; the real set is 38 + rows, and the filter structurally excluded both sibling pages (their queries + say `vertical llm`). **Trap A generalises: a filter truncates a denominator as + readily as a row limit.** Added to `analytics-access.md`. + + +### 13g. N10 — Paul's team-structure row resolves to an UPGRADE (2026-08-22, fourth run) + +**Row as given:** "how to structure team with AI harnesses without losing the +control" (Paul, 2026-08-22). + +**Verdict: UPGRADE `content/blog/claude-code-xp-team-workflow/index.md`. Do not +write a new post.** Approved by Paul the same day. + +**Why not new.** That post (2026-05-04, "How We Replicated a Full Product Team +With Claude Code Agents", 1,645 words) already owns both halves of the row. Its +H2s: the roles we replicated / how the team works together / the cadences we run +/ tools that make this possible / team mode beats solo mode / why the rules need +to change every sprint / where to start. It already carries the control +mechanisms - Driver/Navigator pairing, a critic panel after every green test, +WIP=1, and the arbitration rule "when two agents disagree, the rule file decides, +we don't let the last-run agent win by default." A new post restates it and the +two cannibalise. + +**Why upgrade rather than leave alone.** The 2026-08-22 session produced material +the post predates and cannot have: + +1. **Two exits are not enough.** A loop whose only outcomes are SHIPPED and + ITERATE always ships. The third exit is HOLD-with-evidence, as a terminal + success. The published post has no HOLD concept. +2. **Falsifiable evidence for (1):** two `/blog-next` runs the same day correctly + ended in HOLD (§13d, §13e). A two-exit loop writes both posts. +3. **Author ≠ verifier means a different agent TYPE**, not the same type with + fresh context - same type reproduces the author's tells. +4. **Four errors the gates caught that self-review passed**, each held with + confidence at the time: a skill placed where it silently could not load, a + `.gitignore` that would have swallowed the deliverable invisibly, a + named-query subtotal reported as page performance (§13f), and a filtered pull + read as complete (§13f). + +**Job: arrival-purposed.** The search floor does not apply and quoting it here +would be the category error §13e warns about. The test is whether it survives a +sceptical founder arriving from LinkedIn, and the answer rests on first-hand +operating evidence no competitor has. + +**Evidence class, stated plainly:** this verdict rests on READING the existing +post, not on a measurement chain, so it was not routed through the Stage A +reviewer gate that §13d/e/f verdicts were. The dedup finding is cheap to +re-verify - open the file. Flagged so nobody later cites it as gate-passed. | Date | Change | |---|---| +| 2026-08-22 | §13g added: N10 (Paul's "structure a team with AI harnesses") resolves to UPGRADE `claude-code-xp-team-workflow`, not a new post - the 2026-05-04 post already owns the roles/cadences/arbitration content. Upgrade carries what it predates: three exits vs two, HOLD as terminal success, author≠verifier as a different agent TYPE, and four gate-caught errors. Arrival-purposed; verdict rests on a file read, not a measurement chain, and was NOT routed through the Stage A reviewer gate. Approved by Paul. | +| 2026-08-22 | §13f added: N7 (Paul's "how AI helps developers") DO-NOT-WRITE on 61-slug saturation + 180-query zero-click + wrong reader. N8 `vertical ai agents` FLAG-not-write: first row to clear the demand floor (0.5-3.5 clicks/day) but decaying 2x, SERP held by Salesforce/IBM/Google, asset is a 3-min dev.to import. N9 SCHEDULED: first-party benchmark of AI code-search MCP tools. Trap A generalised to filters. | +| 2026-08-22 | §13e added: §13d's replacement direction retracted (no neighbourhood — one query, already won, ceiling +0.13 clicks/day). Data-migration family examined and FLAGGED not scheduled (synthetic fingerprint unexcluded). Demand floor introduced: whole blog = 0.69 non-brand clicks/day. Queue verdict HOLD. | +| 2026-08-22 | §13d added: N2 falsified on a live re-pull (52x = 1.4x clicks × 37.2x impressions; foil is 1.4% named). N3's addressable volume corrected 6,895 → ~550 (synthetic query family). Replacement topic: Rails dependency/setup. Two additions to §13a: position is an artifact when CTR is, and query-vs-query is the legitimate comparison. | | 2026-08-21 | §13 added: R-queue restocked from live GSC. Two measurement traps documented (anonymized-long-tail inflation, the upwork-login contaminant), F1's propshaft figure corrected, N1-N6 groomed with evidence. | diff --git a/docs/workflows/autonomous-delivery-prompt.md b/docs/workflows/autonomous-delivery-prompt.md index d51a69022..0572bbf82 100644 --- a/docs/workflows/autonomous-delivery-prompt.md +++ b/docs/workflows/autonomous-delivery-prompt.md @@ -46,6 +46,51 @@ without the `dtest` leg. decisions/corrections (prefixes + CLI-keyword-only gotcha: `CLAUDE.md` §Memory). +### Running CONTENT unattended (added 2026-08-22, Paul: "deliver confident and stable work without human in the loop") + +`.okf/workflows/autonomous-loops.md` says a loop fits work whose completion is +**mechanically checkable**. Content's completion looks like a judgement, which is +why it was never run unattended here. It can be, but only by converting the +judgement into gates that are allowed to FAIL the work. + +**The completion promise is about the gates, never about quality.** Never promise +"the post is good" - that is unfalsifiable and an agent under loop pressure will +assert it. Promise the checkable set: + +> slop >= 8/10 from a critic that did not write it · cold-eyes verdict +> PUBLISH-READY on all 9 checks · every external claim fetched and quoted from +> source, no claim written from memory · zero numbers absent from +> `.okf/content/claims-canon.md` or a cited primary source · `bin/hugo-build` +> green · rendered scroll gate desktop+mobile, zero console errors, zero 404s · +> committed on a branch with a PR open + +**HOLD is a terminal success state, and the promise must say so.** +`autonomous-loops.md` records that a loop "rewards visible progress, so [revert +rather than ship] is the discipline it erodes first." The content form of that +failure is manufacturing a post because stopping feels like failing. On +2026-08-22 two `/blog-next` runs correctly returned HOLD - every queue row was an +artifact - and a loop that could not terminate on HOLD would have written +something nobody should publish. **A loop whose only exits are "shipped" or +"iterate" will always ship.** Give it three exits: SHIPPED, HOLD-with-evidence, +BLOCKED-on-a-named-decision. + +**Author never verifies.** Writer and critic are different agent types, not the +same type with fresh context - same-type fresh context still carries the +writer's default tells. Cold-eyes runs a third type. Cap at 2 failed fix rounds, +then BLOCKED; a third round is the loop rationalising. + +**What still stops for Paul, unattended or not.** Narrow and explicit, so +everything else proceeds without asking: +1. Whether a claimed client engagement, number, or case study is **real**. +2. Publishing outward - LinkedIn, email, anything leaving the repo. +3. Overriding a documented gate (the 2607 P0 content gate; precedent for how an + override is recorded: `20.09 §1`, 2026-08-20). +4. Pricing, naming, disclosing internal numbers. +5. Split-and-irreversible calls. + +Everything else: decide, record the call with its evidence, continue. Parking a +reversible decision on Paul is the failure mode, not the safe choice. + ### Reviewer routing (contract §5's cost tiers resolve here) Internal sub-agents (distinct lens per call) for every per-stage review; diff --git a/docs/workflows/blog-pipeline.md b/docs/workflows/blog-pipeline.md index f833ff966..045fd3bb8 100644 --- a/docs/workflows/blog-pipeline.md +++ b/docs/workflows/blog-pipeline.md @@ -1,5 +1,11 @@ # Blog Pipeline — From Idea to Published Post +> **Entry point: the `/blog-next` skill owns STEP 1-3** (pick the topic from live +> GSC+GA4, research primary sources, gate the outline) and hands off to +> `blog-post-coordinator` / `blog-batch-orchestrator`. Prefer it over running +> STEP 1-3 inline: it carries the premise audit that catches a decayed queue row +> before anyone spends a draft. This file stays canonical for STEP 4 onward. + ## Quick start prompt Copy this prompt to write the next post: @@ -183,6 +189,13 @@ Pass 2 reliably strips ~15-25 points off the AI-detector's first-read score. The cost is ~30% more writer tokens; the value is review rounds spend their budget on real issues, not on rewriting textbook prose. +STEP 4e — SELF-CRITIQUE (BLOCKING — before the anti-AI pass and the critics) +Run `reflexion-reflect` on the draft. After pushback on the same draft, escalate +to `reflexion-critique`. Solo iteration misses pattern-level tells — this is the +CLAUDE.md gate for "any LinkedIn/blog/marketing draft", and the blog lane was the +one lane that never wired it in (the LinkedIn lane has carried it in +`.okf/workflows/linkedin-post-pipeline.md` since it was written). + STEP 5a — ANTI-AI WRITING PASS (MANDATORY — run BEFORE review loop) Run /humanizer on the draft. Scan every paragraph for these AI tells: - Rule of three (parallel triads) — break or combine diff --git a/docs/workflows/flow-router.md b/docs/workflows/flow-router.md index eab04c67d..207d0556f 100644 --- a/docs/workflows/flow-router.md +++ b/docs/workflows/flow-router.md @@ -9,6 +9,7 @@ Read this at session start to route tasks to the right workflow without explicit - **Identity, positioning, bet status** → the vault note `jt-business-os` (`~/Documents/pkm`) FIRST, then reflect into the `docs/business/` mirror - LinkedIn post creation or edits, especially `linkedin-posts/**` → `@docs/workflows/linkedin-post-pipeline.md` (it routes to the active campaign plan — check the campaign's status banner; the ICP campaign is LIVE since 2026-08-18, see `linkedin-posts/metrics-ledger.md`) - Content creation or edits → `@docs/workflows/blog-pipeline.md` (mandatory; its P0 gate can halt content entirely) +- **Which blog post to write next / a blog sprint → `/blog-next`** — picks the topic from live GSC+GA4 against the 20.09 §13 queue, researches primary sources, gates the outline, then hands off to `blog-post-coordinator` (one post) or `blog-batch-orchestrator` (N posts). It owns STEP 1-3 of the pipeline; the pipeline still owns STEP 4 onward. - Cover image work → `docs/workflows/cover-images.md` and `.stitch/design.md` - Image/cover requests (even without content) → `@docs/workflows/cover-images.md` and `@.stitch/design.md` - HTML/CSS changes → `@docs/workflows/css-consolidation.md` diff --git a/linkedin-posts/personal/assets/bip-agents-cancelled-own-backlog.jpg b/linkedin-posts/personal/assets/bip-agents-cancelled-own-backlog.jpg new file mode 100644 index 000000000..c3404e791 Binary files /dev/null and b/linkedin-posts/personal/assets/bip-agents-cancelled-own-backlog.jpg differ diff --git a/linkedin-posts/personal/bip-agents-cancelled-own-backlog.md b/linkedin-posts/personal/bip-agents-cancelled-own-backlog.md index 7a80e9971..8714a89b6 100644 --- a/linkedin-posts/personal/bip-agents-cancelled-own-backlog.md +++ b/linkedin-posts/personal/bip-agents-cancelled-own-backlog.md @@ -5,7 +5,7 @@ pillar: build-in-public author: paul-keen voice: personal-first-person icp_test: Does "the review layer, not the output, is where applied AI pays" land with founders running AI tools - and do they share what their AI talked them out of? -image: "" +image: "assets/bip-agents-cancelled-own-backlog.jpg" first_comment: | (value post - no link; reply-CTA only) utm_campaign: "" @@ -20,18 +20,44 @@ notes: | 6-item queue and killed 3 as already-done/policy-blocked (one deferred by a decision recorded the previous day); a reviewer agent found 3 blocking defects in another agent's PR before merge. Register B, no credential stamps, peer - question close. AI self-score: 1/10 (one borderline aphoristic beat-5 line, - "confident waste at machine speed" - kept, it carries the insight). + question close. + + REVISED 2026-08-22 for influencer voice (Paul: "not much close to the + influencers voice - same voice as influencer, plain english, personal"). + Rewritten to the idea-first skeleton (linkedin-post-pipeline.md:132, BLOCKING) + against reference-examples.md - Cutler/Rossi/Holub. Four changes: + (1) opener was a staged event ("Today my AI agents cancelled...") - now leads + with the belief, Rossi-shaped; (2) cut the editorialized takeaway ("the part + of my AI setup I'd keep if I had to drop everything else") - reference-examples + bans the tidy lesson-wrap; (3) cut the aphoristic close "confident waste at + machine speed" - crafted line, the shape Paul flagged 2026-08-13; (4) the + tactic is now delivered in full (give the agent the decisions; put a second + agent before merge) - the doctrine says advice IS the value, and the prior + draft delivered none. + + ACCURACY FIX: prior draft said one item had been "explicitly rejected the day + before". These notes record it as "deferred by a decision recorded the previous + day" - deferred, not rejected. Now reads "deferred". Also dropped "The same + morning" (unverifiable time claim) and the original "Today" (drafted 08-20, + now stale). + + 141 words, 0 em dashes. AI self-score: 1/10 (the only borderline item is the + two consecutive imperatives in the tactic beat, which read as instruction + rather than rhetorical parallelism). --- -Today my AI agents cancelled half of their own to-do list. +Most of the pitch for AI agents is throughput. They execute more, faster. + +The most useful part of my own setup refuses work instead. + +I handed my agents a six-item backlog. Before starting, they checked each item against decisions already on file, and killed three. Two were already done. One I had deferred the day before. -I asked them to work through the backlog. Instead of executing top to bottom, they checked each item against decisions already on file. Three of six were already done or no longer worth doing - one had been explicitly rejected the day before. +Running all six would have looked productive and produced three items of waste. -Doing them anyway would have looked great. A burned-down queue, green checks, and all of it waste. +A second agent then reviewed the first one's pull request and found three blocking defects before it merged. -The same morning, a second agent reviewed the first one's pull request and found three real problems before anything merged. That review step is the part of my AI setup I'd keep if I had to drop everything else. +Both are the same move, and it is cheap to copy. Give the agent the decisions, not just the task. Put a second agent between the first one and anything that merges. -Running AI without something positioned to say no gets you confident waste at machine speed. +An agent with no record of what you already decided will confidently redo it. -What's the last thing your AI talked you out of? +What has your AI talked you out of?