Use when producing an SEO article for the Tortoise blog — the standard 7-step process from example selection through research and brief to a human-reviewed handoff, CMS ingest, and publish. Covers comparison, pillar, and how-to article types.
Resources
1Install
npx skillscat add daniel-ospina/agent-infra/seo-content-generation Install via the SkillsCat registry.
⛔ This skill MUST be read in full — not skimmed. Three of its steps stop for a human —
gate 1, the owner's generation pass (step 5), and gate 2 — and step 6 writes to a CMS with no
revision history. Skipping steps here destroys work rather than merely lowering its quality.
SEO Content Generation
Source: Canonical copy at
skills/seo-content-generation/SKILL.md. Consumers link this dir into
theiroperations/skills/farm — edit here only.
The standard process for producing one SEO article for the Tortoise blog. It exists because the
pipeline is not "write an article, then publish it": the article is generated by the owner from a
reviewed brief, and the agent's job is to build the brief to a high enough standard that the owner's
generation pass is worth doing, then to land the result in the CMS correctly.
The deliverable is the CMS row, not a markdown file in a repo. A merged .md is not a published
article — see the ⛔ below.
Use this skill when: asked to write, plan, brief, or publish a Tortoise blog article; add a
comparison page; extend a pillar; or produce any SEO content destined for the blog CMS.
Article types covered: comparison (X vs Y, best X tools), pillar/explainer (category
definition), and how-to. Format norms differ per type — the type is chosen in step 3.
Repos this spans. Paths below are absolute only inside the repo that owns them:
| Path | Repo |
|---|---|
growth/seo/** — examples, article workspaces, keyword data |
premise-labs |
product/competition/** — the competitor registry |
premise-labs |
website/functions/blog/** — the CMS API and its SEO modules |
daniel-ospina/tortoise |
Run every growth/seo/… command (including the step-4 open -R) from the premise-labs main
checkout — not a linked worktree. growth/seo/** exists only in the main checkout, so a command
run from a worktree reads a nonexistent path silently instead of erroring. Resolve the root
explicitly rather than assuming it is unique:
PREMISE="$(dirname "$(git -C /Users/danielospina/Documents/GitHub/premise-labs rev-parse --path-format=absolute --git-common-dir)")"
ls "$PREMISE/growth/seo/keyword-universe.csv" # fail loudly if the root is wrong⛔ The publishing path is the CMS — never a merged markdown file
⚠️ Pinned citations. Every line reference in this skill was checked at a named commit, because
both files are under active edit — verify the symbol named, not the line number:
extensions/sequence-enforcer/index.ts— at agent-infrabf8164d: thecheckpointTokenOktoken_phaseguard, thehuman_approvalbranch ofgetExpectedToolsForStep, thetool_call
auto-advance loop, theagent_endhuman-gate advance, and the unknown-gate fall-through.website/functions/blog/**andblog-admin/.../PostEditor.tsx— at tortoiseacc5db1359da693ac42495a9e442981a3cb0cf16;sequence-enforcer.test.tsat agent-infrabf8164d.A line number whose file has moved is a citation that looks checkable and is not.
The blog publishes from the CMS at tortoise.premiselabs.co/blog. The repo .md is an import
seed, a handoff artifact — it is never the thing readers see, and merging it accomplishes nothing.
⚠️ Stop-gap. This skill constrains the damage; it cannot make the CMS the source of truth.
tortoise#8002— no agent GET path, no revision history — is the actual fix. Revisit this skill
when that issue closes: until then every guard here is a workaround around an open root cause,
and the step-6 "owner confirms the row" rule is replacing a read path that does not exist yet.
- Do not open a PR to "publish" an article. No text doc gets merged as the publishing act.
- Do not treat a merged doc as a deliverable. If the work stops at a merged file, the article is
not published and the step is not done. - The one exception is the process itself — this skill, the engine docs, the keyword data. Those
are repo documents and merge normally.
The process — 7 steps
| # | Step | Owner | Artifact | Workflow file |
|---|---|---|---|---|
| 1 | Find 3 example articles of the target type and add them to the example database | Agent | growth/seo/examples/*.md |
`workflow/01-examples.md` |
| 2 | Build the research doc — the facts, about us and competitors | Agent | research.md |
`workflow/02-research.md` |
| 3 | Build the brief — objective, strategy fit, keywords, reader, outline, CTA, examples | Agent | brief.md |
`workflow/03-brief.md` |
| — | 🛑 HUMAN GATE 1 — review 2 & 3 in-line and approve | Owner | — | — |
| 4 | Merge 2 + 3 + the 3 examples inlined into one markdown; open it in the file finder | Agent | handoff.md |
`workflow/04-handoff.md` |
| 5 | Take the handoff to ChatGPT, generate, and edit | Owner | — | (not ours) |
| 6 | Add the edited text to the CMS (agent). Meta title/description come from the owner-triggered SEO generator — the only path that injects our keywords; if the agent writes them instead, it must carry the keywordsFor(tag) terms itself. Then verify the keyword wiring |
Agent (+ Owner runs the generator) | CMS draft row | `workflow/05-cms-publish.md` |
| 7 | Review the preview in the editor and publish | Owner | Live post | — |
Steps 5 and 7 are the owner's, not the agent's. Do not attempt to generate the article text, and
do not publish. Step 5 is deliberate: the owner's own generation + editing pass is what makes the
voice right, and an agent-written draft short-circuits it.
The human stops
Three steps stop for a human — two approval gates (1 and 2) and the owner's generation pass
(step 5). All are hard stops. Present, then wait — do not "proceed and let them correct it".
- Gate 1 (after step 3). The research doc and the brief, in-line in the conversation —
not as file paths. The owner reviews facts and framing before any generation happens. This is the
cheap place to catch a wrong claim; catching it later means redoing the brief and the draft. - Gate 2 (after step 6). The owner reviews the CMS preview. The agent never publishes — and this
is a prose control, not a technical one: the posts API acceptsstatus: publishedwith the
agent key, so nothing but this instruction stops it.
⚠️ The enforcer is a backstop, not the gate — and it can fail open three ways. The three stops
are declaredgate: human_approval; when one is reached, every tool exceptread/web_search/web_fetch/loop_enforceris blocked
(extensions/sequence-enforcer/index.ts:1421-1423). But:
- It is usually never reached. The index only advances past
autosteps when a foreignSKILL.mdis opened (:1806-1824), so on a normal run — where you open no other skill — no gate
is ever announced and no enforcement fires.- A reached gate is a one-turn pause, not an approval.
agent_endadvances past it with no
approval signal (:1778).- A typo disables it silently. An unrecognised
gate:value falls through to "standard work
phase" and permitswrite/edit/bash(:1432-1433) — andfrontmatter-validate.mjsdoes not
check thegateenum, so a misspelling passes lint while removing every stop.These gates are therefore PROSE controls, not technical enforcement. Never read "no gate fired"
as "the owner approved" — the real gate is the explicit, spoken approval you wait for.
Artifact layout
growth/seo/
examples/ ← the example-article database (step 1 adds here)
README.md ← index: slug, product, type, measured format, verified date
<slug>.md ← one file per example, source text inlined
articles/
<slug>/ ← one workspace per article
research.md ← step 2
brief.md ← step 3
handoff.md ← step 4 (the combined drag-and-drop file)growth/seo/examples/ and its README.md do not exist yet — step 1 bootstraps both on the
first run. The per-article workspace is created on demand. growth/seo/article-briefs.md is the backlog —
the list of articles to produce, with their primary/secondary keywords. The brief.md in a workspace
is the working brief for one article and is what step 1–4 fill in.
Non-negotiables every article inherits
These come from growth/seo/README.md §2 and hold for every article, in every workflow file:
- Key takeaways block at the top — 5–7 bullets, each a defensible claim.
- Every number carries a primary source and a date. No number without one.
- At least one real HTML
<table>for any comparison or taxonomy — never a bulleted list
pretending to be a table. - Fair-but-favourable. For each competitor, name the job it is genuinely good for, and state our
weaknesses where they exist. A table that only flatters us is the one readers discount. - Video embed (one of V1–V3) plus a link to its transcript page.
- Internal links: the pillar page, 2 sibling articles, 1 docs page.
- A "test it yourself" section with a runnable step.
- Claim discipline. Any sentence of the form "nobody / only / never / the only / sole" must
name a checked source or be cut.
Three failure modes that produce real defects
- Never convert a searched absence into a positive assertion. "We found no published statement by
X" is supportable. "X publishes no…" is not — a failed search is not evidence of absence. - A first-person shipped/not-shipped claim about Tortoise is P0 until verified against
daniel-ospina/tortoiseat a named commit. Grant docs, research briefs and this skill are
not admissible evidence, and a reworded claim is not a fixed one — re-derive the fact from the
source, or remove the claim. - A claim about the process has no artifact to check, so it re-stales on every edit. "we narrowed
this later", "the word count is now", "this is the final version" — delete it. Do not reword it, and
do not narrate the journey instead of the change: describe the change and the evidence for it, never
the sequence of edits that produced it.
Marketing is about "best for", not uniqueness. The spine of an article is a positive, argued
positioning claim — never a negative universal about the field. A negative-universal spine is the
single largest source of non-converging review: it sends the writer hunting for a claim strong enough
to survive an adversary, instead of arguing the one we can support.
What fails if you skip this process
| Skipped | Failure it caused |
|---|---|
| Step 1 (examples) | Articles written to taste, not to the measured format of what actually ranks. |
| Step 2 (research) | Unverifiable numbers printed as fact — two were caught only by a reviewer checking the registry they were attributed to. |
| Gate 1 | The owner's generation pass runs on wrong facts; the brief and the draft both have to be redone. |
| Step 3's keyword targets | The article ranks for nothing it was meant to; the keyword data exists and was not used. |
| Step 6's wiring check | Our keywords are never injected into the meta fields — the post publishes with generic SEO text. |
Any merged-.md "publish" |
Work is reported as delivered that no reader can reach. |
Related skills
⛔ Never open another
SKILL.mdin this session — dispatch every other skill as atask
sub-agent. Reading a foreignSKILL.mdmakes the sequence-enforcer auto-advance this skill past
every consecutivegate: autostep (extensions/sequence-enforcer/index.ts:1806-1824), landing you
on a human gate that blockswrite/edit/bash/task. A sub-agent reads the file in its own
process; this session's step index does not move.
research— the engine for step 2's external facts. Never callweb_searchdirectly. Dispatch
it as atasksub-agent — seeworkflow/02-research.mdfor the prompt it needs.competitor-research— for a fresh competitor profile feeding step 2. Dispatch as atask
sub-agent.- The post-generation review passes are bespoke
taskreviewers — not thecontent-*skills.content-reviewer-breadthandcontent-reviewer-depthare the El Dato editorial/guide/deal
pipeline: they need a deal DB,geography_idpage metadata, neighbourhood and price-tier coverage
(BR2/BR3),page_typein {editorial, guide, deal, refresh}, and an ES/EN resident-vs-expat audience
split. A Tortoise technical article has none of those, so they cannot be run against one.content-fact-checker-writingis different: only itspage_type = "deal"branch needs the deal DB,
while its editorial branch needs a verified research brief and a language — which step 2 does
produce. It is still El Dato-shaped (its brief format comes fromcontent-fact-checker-research), so
it needs adapting rather than running as-is — but a claim-vs-brief check against step 2'sresearch.md
is exactly the right shape, and hand-rolling it means losing a battle-tested checker. Dispatch a
freshtaskreviewer given the article, the research doc and thegrowth/seo/README.md§2 furniture
list, and ask for findings read-only — never a rewrite of the owner's text. content-humanizeris also El Dato-specific (it calibrates against El Dato's Voice DNA and
preserves partner/deal-embed markup), and it is a rewrite pass. It must not run on the owner's
edited text — step 5 is his editing pass, and a naturalisation pass over it undoes the edits that
are the point of that step. If one is ever wanted, the owner asks for it.
Continuity
If context is lost, re-read this file and resume at the first incomplete step. The artifacts on
disk are the state — check growth/seo/articles/<slug>/ for which of research.md, brief.md,handoff.md exists. The CMS row is not agent-inspectable (no GET path — tortoise#8002), so if
the step-6 create response was lost, ask the owner; do not re-POST or re-PATCH blind. Do not
re-derive a step whose artifact
is already on disk and approved; do not skip a step because a later artifact exists (a handoff.md
does not prove gate 1 was passed).
Continue following the workflow as mandated by this skill. Do not skip steps.