Blog delivery contract skill
The contract every blog must pass before being presented to the user.
by AgriciDaniel·MIT license·★ 2,219 Stars on the repo·GitHub ↗
Files of Blog delivery contract
AgriciDaniel/
Show the full text162 lines
Blog Delivery Contract
The contract every blog must pass before being presented to the user. Five gates that fire automatically between content generation and delivery, plus an iteration loop that retries failures up to three times before escalating.
This contract is the v1.9.0 answer to a failure pattern from the v1.8.x cycle: skills had reviewers, but the reviewer ran as advisory and the writer presented sloppy drafts anyway. The fix is infrastructure, not effort. Same shape as scripts/lint_prose.py (v1.8.4) and tests/test_installer_sync.py (v1.8.6).
Gate summary
| Gate | What it enforces | Failure mode | Implementation |
|---|---|---|---|
| 1. Capability Discovery | Required tools + agents are available before write begins | Block if no valid local hero and no permitted image path; block if reviewer agent missing | scripts/blog_preflight.py --gate 1 |
| 2. Format Completeness | .md + .html + .pdf + hero.png all present |
Block on any missing artifact | scripts/blog_render.py produces all three |
| 3. Visual Verification | Rendered HTML has no SVG overflow, no console errors, valid JSON-LD | Block on any defect; preserve screenshots | scripts/blog_preflight.py --gate 3 via patchright |
| 4. Content Review | blog-reviewer scores ≥ 90/100 AND zero P0 issues |
Block + iterate | agents/blog-reviewer.md (now blocking) |
| 5. Asset + Link Integrity | Every <img> resolves, every <a> returns 200, schema validates |
Block on any 404 or count mismatch | scripts/blog_preflight.py --gate 5 |
All gates run sequentially. First failure halts the chain and triggers the iteration loop. Successful drafts ship with preflight-report.json + review.md + preview/*.png in the draft folder.
Gate 1: Capability Discovery
Runs once at the start of /blog write or /blog rewrite. Enumerates the project's available capabilities and writes <draft-folder>/capabilities.json. Every later gate consumes this artifact rather than re-detecting.
What gets enumerated
- MCP servers loaded:
nanobanana-mcp,dataforseo-mcp, others. Detected via tool availability, not by reading.mcp.json(the file may declare servers that failed to start). - Env vars present:
GOOGLE_AI_API_KEY,UNSPLASH_ACCESS_KEY,PEXELS_API_KEY,PIXABAY_API_KEY. Key names only; values never read or logged. - Optional Python deps:
patchright,weasyprint,google-genai,requests. Probed viaimportlib.util.find_spec(). - Project-root context files:
BRAND.md,VOICE.md,DISCOURSE.md. Loaded viascripts/load_untrusted_root.py(the existing v1.8.3 helper). - Agents available:
blog-revieweris mandatory;blog-researcher,blog-writer,blog-seo,blog-translatorare optional. - Helper scripts present:
scripts/lint_prose.py,scripts/analyze_blog.py, the newscripts/blog_preflight.pyitself.
Failure modes
- No hero path at all (no valid local
hero.pngor.jpg, no Banana MCP, no Gemini key, no stock API key, and Openverse unreachable): BLOCK with explicit setup instructions. - Reviewer agent missing: BLOCK. Cannot enforce Gate 4 without it.
- Capability declared but unused: WARN (informational, not blocking). Example:
dataforseo-mcpis loaded but the post topic doesn't need keyword research.
Gate 2: Format Completeness
Every delivered blog ships with four artifacts in <draft-folder>/:
<slug>.md: canonical source of truth. Frontmatter + prose + figure references. The.htmland.pdfare rendered from this; they cannot diverge by construction.<slug>.html: self-contained, valid HTML5, JSON-LDBlogPostingschema, Open Graph + Twitter Card meta, dark-mode-aware CSS viaprefers-color-scheme, real<img>tag for hero (never a chart-as-hero).<slug>.pdf: generated from rendered HTML viapatchright'spage.pdf(), orweasyprintas fallback when Playwright is unavailable.hero.png(or.jpg): 1200×630 raster image. Either generated by an image-gen path or downloaded from a CC-licensed stock source. Never hot-linked; always lives in the draft folder.
Implementation lives in scripts/blog_render.py. Failure to produce any of the four artifacts blocks delivery.
Optional hygiene pass (non-blocking)
After rendering and before Gate 3, you may run python3 scripts/blog_hygiene.py --md <slug>.md --html <slug>.html --apply to auto-apply judgment-free fixes: add loading="lazy" to images that lack it and insert a Table of Contents on posts over 2000 words. It is optional, never blocks delivery, and reports any images missing alt text (which a human must fill in). It does not replace any gate.
Gate 3: Visual Verification
Renders the .html in headless patchright at three viewport widths and checks for visual defects. This is the "review before present" step.
Viewport widths
- 375×812 (mobile, iPhone SE class)
- 768×1024 (tablet, iPad portrait)
- 1280×800 (desktop)
Checks per viewport
- Full-page screenshot: saved to
<draft-folder>/preview/<width>.pngfor inspection. - SVG bounding box check: for every
<svg>and<figure>element, querygetBoundingClientRect()on the element and on every descendanttext,path,rect, andimagechild. Assert no descendant overflows its parent SVGviewBox. This catches the exact class of defect from the rankenstein.pro draft: labels positioned outside the chart area, dashed lines passing through annotation text. - Dark-mode pass: re-render with
prefers-color-scheme: darkemulation. Assert the bodybackground-colordiffers from light mode. This catches thevar()in attribute regression where dark mode silently fails to swap colors because CSS custom properties were used in SVG XML attributes (which don't reliably resolve). - Console errors: capture browser console output during render. Assert zero errors.
- JSON-LD validation: parse the
<script type="application/ld+json">block. Assert valid JSON. Assert@type: BlogPostingwith required fields (headline,image,datePublished,author).
Renderer requirement
Strict delivery requires patchright or an equivalent renderer. If no renderer is available, Gate 3 blocks and marks the draft non-shippable. Operators may use --no-strict for an intermediate local preview, but the output must not be presented as passed.
Gate 4: Content Review (BLOCKING)
The existing blog-reviewer agent (agents/blog-reviewer.md) runs against the rendered .html (not the raw .md). Reviewer output is now blocking, not advisory.
Blocking decision rules
- Overall score < 90/100 → BLOCK
- Any P0 issue from
editorial-heuristics.md→ BLOCK (a draft can score 95 and still have one load-bearing fabricated stat; P0 is an absolute filter independent of the numeric score) - AI-detection burstiness flag OR more than 3 known AI phrases OR vocabulary diversity (TTR) below 0.4 → BLOCK
- All clear → proceed to Gate 5
The blocking decision is emitted as the last line of the reviewer scorecard, in the format:
BLOCKING: true (Overall 87/100 below threshold; P0 on heuristic 5)
BLOCKING: false (cleared all gates)
Machine-readable by scripts/blog_preflight.py so the orchestrator does not have to parse the human-readable scorecard.
Reviewer report saved to <draft-folder>/review.md. Shown to the user on success ("here is why this passed") and on final failure ("here is why this is still blocked after 3 iterations").
Gate 5: Asset Existence + Link Integrity
- Every
<img src="...">resolves. Local paths must stay under the draft root afterresolve(), refuse symlinks, and use slug-sanitized filenames. Absolute URLs must usehttporhttpsonly; rejectjavascript:,data:,file:, protocol-relative URLs, credentials in URLs, and invalid hosts. - External URL checks must resolve DNS before connecting and reject loopback, private, link-local, multicast, reserved, unspecified, and cloud-metadata IP ranges. Do not follow redirects automatically; if redirects are allowed, validate every redirect target with the same checks. Use 5s timeouts, response-size caps, and no request bodies.
- Validate links with
HEADfirst, then fall back toGETwith a small range request when servers blockHEAD. Accept valid 2xx or 3xx responses, and allow documented 403 or 405 cases only through the per-projectexternal-links.allowedconfig. og:imageURL resolves with the same SSRF, redirect, size, and timeout rules; this is the load-bearing social-preview asset.- Every
<a href="https://...">resolves under the same policy, or is in the per-projectexternal-links.allowedconfig. - Every
<code>filename.ext</code>mention either references a real file in the project (verified viaPath.exists()) or is wrapped in a "hypothetical example" marker. <link rel="canonical">is set and well-formed.- JSON-LD
wordCountmatches actual<article>word count within ±5%. Catches the "I claimed 1,715 words but the body is 1,400" honesty defect.
Hero Image Generation Ladder
Tried in order. First success wins. Skip steps for capabilities not available per Gate 1's capabilities.json.
- Banana MCP (
nanobanana-mcploaded as a tool, not just declared in.mcp.json): call itsgenerate_imagetool with an optimized six-component prompt (Subject + Action + Context + Composition + Lighting + Style) targeting 1200×630. - Direct Gemini API (
GOOGLE_AI_API_KEYpresent, MCP not loaded): call thegoogle-genaiSDK withgemini-3.1-flash-imageby default. Fallback, in order, togemini-3.1-flash-lite-imageandgemini-3-pro-image. Seehttps://ai.google.dev/gemini-api/docs/image-generation. - Premium stock APIs (
UNSPLASH_ACCESS_KEY,PEXELS_API_KEY, orPIXABAY_API_KEYpresent): search via the official API using post title + top tags as query. Do not scrape or construct raw CDN URLs. Capture each source's license metadata and attribution requirements, download the asset locally, and writehero-credit.txt. Unsplash, Pexels, and Pixabay use their own licenses; do not treat them as CC sources. - Openverse public API (no key required):
GET https://api.openverse.org/v1/images/?q=<query>&aspect_ratio=wide&license=cc0,by. Pick the top relevance match with complete attribution. Always download tohero.<ext>plushero-credit.txtfor CC attribution. - Block with clear error: "Hero image required but no generation path available. Configure Banana MCP, set GOOGLE_AI_API_KEY, set UNSPLASH/PEXELS/PIXABAY key, or place a 1200×630 hero.png in the draft folder manually."
Implementation: scripts/generate_hero.py. Always writes hero-credit.txt next to hero.<ext> for attribution compliance, even when generation paths 1-2 (AI-generated, no attribution needed) are used. The file then contains "AI-generated; no attribution required."
Iteration Loop
When any gate fails, the orchestrator (skills/blog/SKILL.md) drives a retry loop:
- Capture diagnostic: which gate failed, which specific check, screenshot if visual, scorecard if content.
- Construct iteration prompt keyed to the failing gate:
- Gate 2 missing artifact → re-run
scripts/blog_render.pyafter fixing the source.md - Gate 3 visual fail → re-run
scripts/blog_render.pywith adjusted layout (e.g. shrink SVG inner area, wrap long labels) - Gate 4 score < 90 → re-dispatch
blog-writeragent with the reviewer report as input and an instruction to fix the lowest-scoring category first - Gate 4 P0 issue → re-dispatch with a targeted instruction for that specific P0
- Gate 5 404 → re-dispatch with instruction to remove or replace the broken URL
- Gate 2 missing artifact → re-run
- Re-run all five gates from Gate 1. (Re-running Gate 1 catches the case where an iteration changed capabilities, e.g. installed a missing dep.)
- On pass: present draft to user with a one-line summary ("Delivered after N iteration(s)").
- On fail after 3 iterations: STOP. Show the user the failure diagnostic for each remaining defect, the partial draft, the latest reviewer report, and an explicit "Manual fix required" message. Do not iterate further automatically.
The orchestrator holds the loop counter. Sub-skills never loop themselves; that would risk infinite loops if two sub-skills disagree.
Bypass Mechanism
Strict mode is the default. Users can override only via an explicit operator-controlled --no-strict flag on scripts/blog_preflight.py or signed/trusted project config. Draft frontmatter is untrusted content and must never disable delivery gates. Any bypass logs loudly:
WARNING: Delivery contract bypassed. Failed gates: [Gate 3, Gate 5].
The draft is being presented anyway per --no-strict. Do not publish without manual review.
Bypass is intended for two cases: (1) the contract has a false positive the user has verified, (2) the user is iterating on a draft and wants to see intermediate output before the gates pass. It is not intended for shipping. A future CI workflow will reject merges where preflight-report.json shows "blocked": true && "strict": false (the bypassed-and-published case); that enforcement is not yet implemented.
References
skills/blog/references/quality-scoring.md: the 100-point numeric scoring rubric used by Gate 4skills/blog/references/editorial-heuristics.md: the P0-P3 ordinal scoring used for the P0 filter in Gate 4skills/blog/references/visual-media.md: image and asset standards consumed by Gate 5skills/blog/references/schema-stack.md: JSON-LD structure validated by Gate 3 step 5agents/blog-reviewer.md: the reviewer agent that produces the Gate 4 scorecardscripts/load_untrusted_root.py: the v1.8.3 helper used for project-root file loading in Gate 1scripts/lint_prose.py: the v1.8.4 prose linter run as part of Gate 4's editorial-heuristics scoringtests/test_blog_delivery_contract.py: coherence test that asserts this contract and its implementation stay in sync
How this contract maps to the v1.8.x lesson
The rankenstein.pro draft failure was a Category-3 defect: a contradiction between what the project's tooling could do (Banana MCP, blog-reviewer, /blog image, Playwright) and what the writer actually invoked (none of them). The v1.8.x lesson says: convert Category-3 into a gate or a test, not into more discipline. This contract is exactly that. Five gates fire automatically. The writer cannot forget to use the tools because the tools are wired into the gates themselves.
| 1 | # Blog Delivery Contract |
| 2 | |
| 3 | The contract every blog must pass before being presented to the user. Five gates that fire automatically between content generation and delivery, plus an iteration loop that retries failures up to three times before escalating. |
| 4 | |
| 5 | This contract is the v1.9.0 answer to a failure pattern from the v1.8.x cycle: skills had reviewers, but the reviewer ran as advisory and the writer presented sloppy drafts anyway. The fix is infrastructure, not effort. Same shape as `scripts/lint_prose.py` (v1.8.4) and `tests/test_installer_sync.py` (v1.8.6). |
| 6 | |
| 7 | ## Gate summary |
| 8 | |
| 9 | | Gate | What it enforces | Failure mode | Implementation | |
| 10 | |---|---|---|---| |
| 11 | | 1. Capability Discovery | Required tools + agents are available before write begins | Block if no valid local hero and no permitted image path; block if reviewer agent missing | `scripts/blog_preflight.py --gate 1` | |
| 12 | | 2. Format Completeness | `.md` + `.html` + `.pdf` + `hero.png` all present | Block on any missing artifact | `scripts/blog_render.py` produces all three | |
| 13 | | 3. Visual Verification | Rendered HTML has no SVG overflow, no console errors, valid JSON-LD | Block on any defect; preserve screenshots | `scripts/blog_preflight.py --gate 3` via `patchright` | |
| 14 | | 4. Content Review | `blog-reviewer` scores ≥ 90/100 AND zero P0 issues | Block + iterate | `agents/blog-reviewer.md` (now blocking) | |
| 15 | | 5. Asset + Link Integrity | Every `<img>` resolves, every `<a>` returns 200, schema validates | Block on any 404 or count mismatch | `scripts/blog_preflight.py --gate 5` | |
| 16 | |
| 17 | All gates run sequentially. First failure halts the chain and triggers the iteration loop. Successful drafts ship with `preflight-report.json` + `review.md` + `preview/*.png` in the draft folder. |
| 18 | |
| 19 | ## Gate 1: Capability Discovery |
| 20 | |
| 21 | Runs once at the start of `/blog write` or `/blog rewrite`. Enumerates the project's available capabilities and writes `<draft-folder>/capabilities.json`. Every later gate consumes this artifact rather than re-detecting. |
| 22 | |
| 23 | ### What gets enumerated |
| 24 | |
| 25 | **MCP servers loaded**: `nanobanana-mcp`, `dataforseo-mcp`, others. Detected via tool availability, not by reading `.mcp.json` (the file may declare servers that failed to start). |
| 26 | **Env vars present**: `GOOGLE_AI_API_KEY`, `UNSPLASH_ACCESS_KEY`, `PEXELS_API_KEY`, `PIXABAY_API_KEY`. Key names only; values never read or logged. |
| 27 | **Optional Python deps**: `patchright`, `weasyprint`, `google-genai`, `requests`. Probed via `importlib.util.find_spec()`. |
| 28 | **Project-root context files**: `BRAND.md`, `VOICE.md`, `DISCOURSE.md`. Loaded via `scripts/load_untrusted_root.py` (the existing v1.8.3 helper). |
| 29 | **Agents available**: `blog-reviewer` is mandatory; `blog-researcher`, `blog-writer`, `blog-seo`, `blog-translator` are optional. |
| 30 | **Helper scripts present**: `scripts/lint_prose.py`, `scripts/analyze_blog.py`, the new `scripts/blog_preflight.py` itself. |
| 31 | |
| 32 | ### Failure modes |
| 33 | |
| 34 | **No hero path at all** (no valid local `hero.png` or `.jpg`, no Banana MCP, no Gemini key, no stock API key, and Openverse unreachable): BLOCK with explicit setup instructions. |
| 35 | **Reviewer agent missing**: BLOCK. Cannot enforce Gate 4 without it. |
| 36 | **Capability declared but unused**: WARN (informational, not blocking). Example: `dataforseo-mcp` is loaded but the post topic doesn't need keyword research. |
| 37 | |
| 38 | ## Gate 2: Format Completeness |
| 39 | |
| 40 | Every delivered blog ships with four artifacts in `<draft-folder>/`: |
| 41 | |
| 42 | `<slug>.md`: canonical source of truth. Frontmatter + prose + figure references. The `.html` and `.pdf` are rendered from this; they cannot diverge by construction. |
| 43 | `<slug>.html`: self-contained, valid HTML5, JSON-LD `BlogPosting` schema, Open Graph + Twitter Card meta, dark-mode-aware CSS via `prefers-color-scheme`, real `<img>` tag for hero (never a chart-as-hero). |
| 44 | `<slug>.pdf`: generated from rendered HTML via `patchright`'s `page.pdf()`, or `weasyprint` as fallback when Playwright is unavailable. |
| 45 | `hero.png` (or `.jpg`): 1200×630 raster image. Either generated by an image-gen path or downloaded from a CC-licensed stock source. Never hot-linked; always lives in the draft folder. |
| 46 | |
| 47 | Implementation lives in `scripts/blog_render.py`. Failure to produce any of the four artifacts blocks delivery. |
| 48 | |
| 49 | ### Optional hygiene pass (non-blocking) |
| 50 | |
| 51 | After rendering and before Gate 3, you may run `python3 scripts/blog_hygiene.py --md <slug>.md --html <slug>.html --apply` to auto-apply judgment-free fixes: add `loading="lazy"` to images that lack it and insert a Table of Contents on posts over 2000 words. It is optional, never blocks delivery, and reports any images missing alt text (which a human must fill in). It does not replace any gate. |
| 52 | |
| 53 | ## Gate 3: Visual Verification |
| 54 | |
| 55 | Renders the `.html` in headless `patchright` at three viewport widths and checks for visual defects. This is the "review before present" step. |
| 56 | |
| 57 | ### Viewport widths |
| 58 | |
| 59 | 375×812 (mobile, iPhone SE class) |
| 60 | 768×1024 (tablet, iPad portrait) |
| 61 | 1280×800 (desktop) |
| 62 | |
| 63 | ### Checks per viewport |
| 64 | |
| 65 | **Full-page screenshot**: saved to `<draft-folder>/preview/<width>.png` for inspection. |
| 66 | **SVG bounding box check**: for every `<svg>` and `<figure>` element, query `getBoundingClientRect()` on the element and on every descendant `text`, `path`, `rect`, and `image` child. Assert no descendant overflows its parent SVG `viewBox`. This catches the exact class of defect from the rankenstein.pro draft: labels positioned outside the chart area, dashed lines passing through annotation text. |
| 67 | **Dark-mode pass**: re-render with `prefers-color-scheme: dark` emulation. Assert the body `background-color` differs from light mode. This catches the `var()` in attribute regression where dark mode silently fails to swap colors because CSS custom properties were used in SVG XML attributes (which don't reliably resolve). |
| 68 | **Console errors**: capture browser console output during render. Assert zero errors. |
| 69 | **JSON-LD validation**: parse the `<script type="application/ld+json">` block. Assert valid JSON. Assert `@type: BlogPosting` with required fields (`headline`, `image`, `datePublished`, `author`). |
| 70 | |
| 71 | ### Renderer requirement |
| 72 | |
| 73 | Strict delivery requires `patchright` or an equivalent renderer. If no renderer is available, Gate 3 blocks and marks the draft non-shippable. Operators may use `--no-strict` for an intermediate local preview, but the output must not be presented as passed. |
| 74 | |
| 75 | ## Gate 4: Content Review (BLOCKING) |
| 76 | |
| 77 | The existing `blog-reviewer` agent (`agents/blog-reviewer.md`) runs against the rendered `.html` (not the raw `.md`). Reviewer output is now **blocking**, not advisory. |
| 78 | |
| 79 | ### Blocking decision rules |
| 80 | |
| 81 | Overall score **< 90/100** → BLOCK |
| 82 | **Any P0 issue** from `editorial-heuristics.md` → BLOCK (a draft can score 95 and still have one load-bearing fabricated stat; P0 is an absolute filter independent of the numeric score) |
| 83 | AI-detection burstiness flag OR more than 3 known AI phrases OR vocabulary diversity (TTR) below 0.4 → BLOCK |
| 84 | All clear → proceed to Gate 5 |
| 85 | |
| 86 | The blocking decision is emitted as the last line of the reviewer scorecard, in the format: |
| 87 | |
| 88 | |
| 89 | BLOCKING: true (Overall 87/100 below threshold; P0 on heuristic 5) |
| 90 | BLOCKING: false (cleared all gates) |
| 91 | |
| 92 | |
| 93 | Machine-readable by `scripts/blog_preflight.py` so the orchestrator does not have to parse the human-readable scorecard. |
| 94 | |
| 95 | Reviewer report saved to `<draft-folder>/review.md`. Shown to the user on success ("here is why this passed") and on final failure ("here is why this is still blocked after 3 iterations"). |
| 96 | |
| 97 | ## Gate 5: Asset Existence + Link Integrity |
| 98 | |
| 99 | Every `<img src="...">` resolves. Local paths must stay under the draft root after `resolve()`, refuse symlinks, and use slug-sanitized filenames. Absolute URLs must use `http` or `https` only; reject `javascript:`, `data:`, `file:`, protocol-relative URLs, credentials in URLs, and invalid hosts. |
| 100 | External URL checks must resolve DNS before connecting and reject loopback, private, link-local, multicast, reserved, unspecified, and cloud-metadata IP ranges. Do not follow redirects automatically; if redirects are allowed, validate every redirect target with the same checks. Use 5s timeouts, response-size caps, and no request bodies. |
| 101 | Validate links with `HEAD` first, then fall back to `GET` with a small range request when servers block `HEAD`. Accept valid 2xx or 3xx responses, and allow documented 403 or 405 cases only through the per-project `external-links.allowed` config. |
| 102 | `og:image` URL resolves with the same SSRF, redirect, size, and timeout rules; this is the load-bearing social-preview asset. |
| 103 | Every `<a href="https://...">` resolves under the same policy, or is in the per-project `external-links.allowed` config. |
| 104 | Every `<code>filename.ext</code>` mention either references a real file in the project (verified via `Path.exists()`) or is wrapped in a "hypothetical example" marker. |
| 105 | `<link rel="canonical">` is set and well-formed. |
| 106 | JSON-LD `wordCount` matches actual `<article>` word count within ±5%. Catches the "I claimed 1,715 words but the body is 1,400" honesty defect. |
| 107 | |
| 108 | ## Hero Image Generation Ladder |
| 109 | |
| 110 | Tried in order. First success wins. Skip steps for capabilities not available per Gate 1's `capabilities.json`. |
| 111 | |
| 112 | **Banana MCP** (`nanobanana-mcp` loaded as a tool, not just declared in `.mcp.json`): call its `generate_image` tool with an optimized six-component prompt (Subject + Action + Context + Composition + Lighting + Style) targeting 1200×630. |
| 113 | **Direct Gemini API** (`GOOGLE_AI_API_KEY` present, MCP not loaded): call the `google-genai` SDK with `gemini-3.1-flash-image` by default. Fallback, in order, to `gemini-3.1-flash-lite-image` and `gemini-3-pro-image`. See `https://ai.google.dev/gemini-api/docs/image-generation`. |
| 114 | **Premium stock APIs** (`UNSPLASH_ACCESS_KEY`, `PEXELS_API_KEY`, or `PIXABAY_API_KEY` present): search via the official API using post title + top tags as query. Do not scrape or construct raw CDN URLs. Capture each source's license metadata and attribution requirements, download the asset locally, and write `hero-credit.txt`. Unsplash, Pexels, and Pixabay use their own licenses; do not treat them as CC sources. |
| 115 | **Openverse public API** (no key required): `GET https://api.openverse.org/v1/images/?q=<query>&aspect_ratio=wide&license=cc0,by`. Pick the top relevance match with complete attribution. Always download to `hero.<ext>` plus `hero-credit.txt` for CC attribution. |
| 116 | **Block with clear error**: "Hero image required but no generation path available. Configure Banana MCP, set GOOGLE_AI_API_KEY, set UNSPLASH/PEXELS/PIXABAY key, or place a 1200×630 hero.png in the draft folder manually." |
| 117 | |
| 118 | Implementation: `scripts/generate_hero.py`. Always writes `hero-credit.txt` next to `hero.<ext>` for attribution compliance, even when generation paths 1-2 (AI-generated, no attribution needed) are used. The file then contains "AI-generated; no attribution required." |
| 119 | |
| 120 | ## Iteration Loop |
| 121 | |
| 122 | When any gate fails, the orchestrator (`skills/blog/SKILL.md`) drives a retry loop: |
| 123 | |
| 124 | **Capture diagnostic**: which gate failed, which specific check, screenshot if visual, scorecard if content. |
| 125 | **Construct iteration prompt** keyed to the failing gate: |
| 126 | Gate 2 missing artifact → re-run `scripts/blog_render.py` after fixing the source `.md` |
| 127 | Gate 3 visual fail → re-run `scripts/blog_render.py` with adjusted layout (e.g. shrink SVG inner area, wrap long labels) |
| 128 | Gate 4 score < 90 → re-dispatch `blog-writer` agent with the reviewer report as input and an instruction to fix the lowest-scoring category first |
| 129 | Gate 4 P0 issue → re-dispatch with a targeted instruction for that specific P0 |
| 130 | Gate 5 404 → re-dispatch with instruction to remove or replace the broken URL |
| 131 | **Re-run all five gates** from Gate 1. (Re-running Gate 1 catches the case where an iteration changed capabilities, e.g. installed a missing dep.) |
| 132 | **On pass**: present draft to user with a one-line summary ("Delivered after N iteration(s)"). |
| 133 | **On fail after 3 iterations**: STOP. Show the user the failure diagnostic for each remaining defect, the partial draft, the latest reviewer report, and an explicit "Manual fix required" message. Do not iterate further automatically. |
| 134 | |
| 135 | The orchestrator holds the loop counter. Sub-skills never loop themselves; that would risk infinite loops if two sub-skills disagree. |
| 136 | |
| 137 | ## Bypass Mechanism |
| 138 | |
| 139 | Strict mode is the default. Users can override only via an explicit operator-controlled `--no-strict` flag on `scripts/blog_preflight.py` or signed/trusted project config. Draft frontmatter is untrusted content and must never disable delivery gates. Any bypass logs loudly: |
| 140 | |
| 141 | |
| 142 | WARNING: Delivery contract bypassed. Failed gates: [Gate 3, Gate 5]. |
| 143 | The draft is being presented anyway per --no-strict. Do not publish without manual review. |
| 144 | |
| 145 | |
| 146 | Bypass is intended for two cases: (1) the contract has a false positive the user has verified, (2) the user is iterating on a draft and wants to see intermediate output before the gates pass. It is not intended for shipping. A future CI workflow will reject merges where `preflight-report.json` shows `"blocked": true && "strict": false` (the bypassed-and-published case); that enforcement is not yet implemented. |
| 147 | |
| 148 | ## References |
| 149 | |
| 150 | `skills/blog/references/quality-scoring.md`: the 100-point numeric scoring rubric used by Gate 4 |
| 151 | `skills/blog/references/editorial-heuristics.md`: the P0-P3 ordinal scoring used for the P0 filter in Gate 4 |
| 152 | `skills/blog/references/visual-media.md`: image and asset standards consumed by Gate 5 |
| 153 | `skills/blog/references/schema-stack.md`: JSON-LD structure validated by Gate 3 step 5 |
| 154 | `agents/blog-reviewer.md`: the reviewer agent that produces the Gate 4 scorecard |
| 155 | `scripts/load_untrusted_root.py`: the v1.8.3 helper used for project-root file loading in Gate 1 |
| 156 | `scripts/lint_prose.py`: the v1.8.4 prose linter run as part of Gate 4's editorial-heuristics scoring |
| 157 | `tests/test_blog_delivery_contract.py`: coherence test that asserts this contract and its implementation stay in sync |
| 158 | |
| 159 | ## How this contract maps to the v1.8.x lesson |
| 160 | |
| 161 | The rankenstein.pro draft failure was a Category-3 defect: a contradiction between what the project's tooling could do (Banana MCP, blog-reviewer, /blog image, Playwright) and what the writer actually invoked (none of them). The v1.8.x lesson says: convert Category-3 into a gate or a test, not into more discipline. This contract is exactly that. Five gates fire automatically. The writer cannot forget to use the tools because the tools are wired into the gates themselves. |
| 162 |
Discussion
Alternatives
Browse more free Claude skills or everything in Legal & compliance.