| name | wr-newsletter:generate |
| description | Draft a weekly Windy Road newsletter. Defaults to The Shift (persona=leader, target Engineering Leaders, publishes Monday mornings AEST per ADR 030). Pass persona=developer to draft Tokens Spent (target working Developers). Pipeline runs in three phases (ADR 017): pass phase=prep for pre-publish-day research and drafting, phase=finalise on the persona's publish-day for tier-1 refresh and publish, or phase=full (default) for the legacy single-shot run. Collects news from multiple sources, filters candidates through the Wardley precondition and three-lens criterion, updates the AI Engineering Landscape Wardley map, updates the map analysis, produces a brief that reports on what changed on the map, and runs voice + content-risk + SW-critic review gates with 3-round iteration. Saves the result to src/newsletters/drafts/<persona>/YYYY-MM-DD/YYYY-MM-DD.md (or .prep.md during the prep phase). Run weekly when Tom is ready to produce a new issue. |
Windy Road newsletter generator
Weekly pipeline for either The Shift (persona=leader) or Tokens Spent (persona=developer). Persona and phase are both resolved at step 0 from $ARGUMENTS; everything downstream reads the resolved persona's config bundle and branches on phase. The brief is structured as commentary on a living Wardley map of the AI engineering landscape (ADR 014), with the map updated before the brief is drafted. The map and the source-fetch tier are shared across personas; weighting, voice addendum, headline, CTA, and save path differ per persona. Six review gates run on the outputs: voice (ADR 012), content-risk (ADR 012 + ADR 015 + ADR 018), SW-critic (ADR 016), editor (ADR 020), adversarial skeptic (ADR 042; claim-evidence calibration read adversarially, the gate that owns the most common external-review catch), and cognitive accessibility (P053; one-round-with-optional-remediation pass against WCAG 2.2 cognitive SC + reading-grade-level target). Phase boundaries (ADR 017, refined by ADR 030) split the pipeline so the time-expensive work runs in the days before the persona's publish-day (prep) and publish-day morning is reserved for a tier-1 refresh plus publish (finalise).
Reference
- Plan:
docs/ai-engineering-brief/PLAN.md, docs/ai-engineering-brief/developer-newsletter-concept.md
- ADRs:
docs/decisions/011-ai-brief-orchestration-via-claude-code.proposed.md, 012-ai-generated-content-review-gates.proposed.md, 013-no-automated-linkedin-scraping.proposed.md, 014-wardley-mapping-as-strategic-lens.proposed.md, 015-reader-respect-and-gate-rejection-policy.proposed.md, 016-sw-critic-subagents-and-iteration-loop.superseded.md (superseded by ADR 033), 017-ai-brief-prep-and-finalise-phases.proposed.md, 018-content-risk-subagent.proposed.md, 019-capture-transcript-artifact.proposed.md, 020-newsletter-editor-subagent.proposed.md, 025-pass-with-author-overrides-verdict-for-sw-critic.proposed.md (amended by ADR 035), 026-reviews-and-meta-content-to-sibling-files.proposed.md, 033-domain-specific-critic-agents-supersede-parameterised-sw-critic.accepted.md, 035-critic-rubric-shape-is-strengths-weaknesses-plus-context.accepted.md
- Voice:
docs/VOICE-AND-TONE.md (base) plus persona addendum from personas/<persona>.md
- Personas:
docs/JOBS_TO_BE_DONE.md (J1-J4 leader, J5 founder, J6-J11 developer)
- Persona configs:
.claude/skills/wr-newsletter/personas/leader.md, .claude/skills/wr-newsletter/personas/developer.md
- Landscape map:
docs/ai-engineering-brief/ai-landscape.owm, docs/ai-engineering-brief/ai-landscape.md
- Rubrics:
.claude/skills/wr-newsletter/assets/wardley-critic-rubric.md, .claude/skills/wr-newsletter/assets/newsletter-critic-rubric.md
- Critic agents (ADR 033):
.claude/agents/wr-newsletter-critic.md (newsletter draft), .claude/agents/wr-wardley-critic.md (Wardley artifact). .claude/agents/wr-sw-critic.md is the legacy parameterised critic retained during Phase 2 -> Phase 3 transition; no live call sites in this skill.
- Filter:
.claude/skills/wr-newsletter/assets/three-lens-filter.md
- Template:
.claude/skills/wr-newsletter/assets/draft-template.md
Phase model (ADR 017)
The pipeline runs in one of three phases, selected by the phase argument at step 0:
| phase | When to run | Steps executed | Saves |
|---|
prep | Days before <publish-day> (e.g. Sat-Sun for The Shift's Monday publish per ADR 030) | 0, 1, 2 (all tiers), 3, 4, 4b, 5-9, 9.5, 10, 11, 11.5 (URL verify), 12 (image), 13, 14, 15, 15.25, 16 (as .prep.md + .reviews.md), 17 | <draft-folder>/<publication-date>.prep.md (brief) and <publication-date>.reviews.md (sibling) per ADR-026 |
finalise | <publish-day> morning (Monday morning AEST for The Shift) | 0, 0.5 (load prep state), 2-prime (tier-1 refresh only), 1-prime (inbox diff), 10-prime (per-item capture on new items only), late-story branch (steps 5-9 if map-moving), 11a-prime (theme-anchor re-confirm gate per ADR-037), 11b-prime (re-draft only changed sections), 11.5-prime (URL re-verify on new/changed URLs), 12 (re-render image only if hook changed), 13, 14, 15, 15.25, 15.5 (LinkedIn post), 16 (rename .prep.md to .md, refresh .reviews.md, write .linkedin.md), 17 | <draft-folder>/<publication-date>.md (brief), .reviews.md, .linkedin.md siblings per ADR-026 |
full (default if no phase argument) | First-time use, one-off editions, or weeks where no mid-week prep ran | 0, 1, 2, 3, 4, 4b, 5-9, 9.5, 10, 11, 11.5 (URL verify), 12 (image), 13, 14, 15, 15.25, 15.5 (LinkedIn post), 16, 17 | <draft-folder>/<publication-date>.md (brief), .reviews.md, .linkedin.md siblings per ADR-026 |
Default behaviour when no phase argument is present: phase=full (preserves the original single-shot run for backward compatibility per ADR 017 line 51).
The "prime" suffix on a step number means the finalise variant of that step. Where the prime variant is materially different (sources fetched, candidates filtered, items captured, sections drafted), the body of the step calls out the difference explicitly.
The critic gates (steps 9, 13, 14, 15, 15.25) run independently in prep and finalise. ADR 016's 3-round cap is read per-artifact-pass, not per-edition: prep produces .prep.md (one artifact pass, up to 3 rounds), finalise produces the final .md (a second artifact pass, up to 3 rounds). See ADR 017 lines 39-41.
Pipeline
Execute in order. Each numbered step is a distinct phase.
0. Resolve persona and phase
Parse $ARGUMENTS for persona=<value> and phase=<value> pairs (order does not matter; both are optional).
Persona handling (unchanged):
- Default to
persona=leader if absent.
- Valid values:
leader, developer. Reject any other value with: ERROR: unknown persona '<value>'. Valid personas: leader, developer. See .claude/skills/wr-newsletter/personas/ for available configs.
Phase handling (new in ADR 017):
- Default to
phase=full if absent (preserves the original single-shot behaviour).
- Valid values:
prep, finalise, full. Reject any other value with: ERROR: unknown phase '<value>'. Valid phases: prep, finalise, full. See ADR 017 for phase semantics.
- Record the resolved phase alongside the persona in the draft summary and in any commit messages produced from this run.
Read the resolved persona config: .claude/skills/wr-newsletter/personas/<persona>.md. Bind the following variables for the rest of the pipeline:
<publication-name>: e.g. "The Shift" or "Tokens Spent". Used in headline subtitle and Tom-summary.
<target-reader>: e.g. "Engineering Leader (J1-J4)". Used in Tom-summary.
<source-weighting>: tier ordering specific to the persona. Used at step 4 to break ties when shortlisting.
<three-lens-weighting>: e.g. "human > operational > technical" or the inverse. Used at step 4 (lens scoring) and step 9.5 (persona-weighted ranking, once the map has been updated).
<voice-addendum>: persona-specific voice notes (vocabulary preferences, evidence-stance language). Combined with the base docs/VOICE-AND-TONE.md rules at steps 11a (theme anchor) + 11b (body draft) per ADR-037.
<cta-invitation>: invitation variants from the persona config; pick one per edition, rotating week-to-week to avoid repetition. There is no <cta-description> (services-pitch) variant: the CTA is invitation + closing line only (P090; ADR-023 funnel pause).
<welcome-line>: persona-specific first-edition welcome text.
<headline-pattern>: e.g. "# <Title>\n\n*The Shift, AI engineering, week ending YYYY-MM-DD*" or the Tokens Spent variant. The YYYY-MM-DD in "week ending" is <week-ending> (the Sunday), not <publication-date> (the publish day).
<draft-folder>: e.g. src/newsletters/drafts/leader/ or src/newsletters/drafts/developer/. Per ADR-040, each draft edition lives in a per-date sub-directory of this folder, mirroring the ADR-039 published-side layout: <draft-folder>/<publication-date>/<publication-date>.<ext>. The persona-archive README.md stays at the persona-folder root. Every <draft-folder>/<publication-date>.X reference below resolves through this binding to <draft-folder>/<publication-date>/<publication-date>.X.
<published-folder>: e.g. src/newsletters/published/leader/ or src/newsletters/published/developer/. Per ADR-039, each published edition lives in a per-date sub-directory of this folder: <published-folder>/<publication-date>/<publication-date>.<ext>. The persona-archive README.md stays at the persona-folder root.
<publish-day>: the day-of-week the persona publishes (e.g. Monday for leader / The Shift per ADR 030). Read from the persona-config frontmatter publish-day field. Required.
<publish-timezone>: the IANA timezone the publish-day is anchored to (e.g. Australia/Sydney for leader, which is AEST/AEDT). Read from the persona-config frontmatter publish-timezone field. Required. The day-of-week comparison in <publication-date> uses this timezone, not the host's local time, so prep and finalise resolve the same publish-day regardless of where Tom runs them.
If either publish-day or publish-timezone is absent from the resolved persona config, do NOT guess or default to a hard-coded day. Surface to Tom via AskUserQuestion (per ADR 030 backward-compatibility clause):
- question:
"Persona '<persona>' config is missing publish-day and/or publish-timezone. ADR 030 requires both to be set per persona. Backfill now, or abort?"
- options:
Backfill now: ask Tom for the day and timezone, then write them to the persona config frontmatter before continuing.
Abort: stop the pipeline. Tom can edit the persona config and re-invoke.
- multiSelect: false
<publication-date> (ADR-026, P040, ADR 030): the ISO-format date (YYYY-MM-DD) used for all draft and companion-file paths in this run. Compute as follows: if today in <publish-timezone> is <publish-day>, use today; otherwise use the next <publish-day> after today. The brief is published on <publish-day>, so all <draft-folder>/<publication-date>.X paths share the publication-day date regardless of when prep runs. This binding is the single date source for all FILE PATHS (steps 10, 11, 12, 16, and 17).
<week-ending> (ADR 030): the reader-facing "week ending" label, distinct from <publication-date>. A week does not end on the publication Monday, so the label uses the Sunday that ends the editorial week: the most recent Sunday on or before <publication-date>. For the leader persona's Monday publish-day this is <publication-date> minus one day (e.g. publication-date 2026-05-25 gives week-ending 2026-05-24). <week-ending> is a DISPLAY LABEL ONLY: it appears in the headline subtitle, the cover "WEEK ENDING" stamp, and the capture-transcript header, and it NEVER appears in a file path (P040's single path-source binding remains <publication-date>). Use <week-ending> everywhere the text reads "week ending"; use <publication-date> for every filename.
Branch on phase:
phase=full or phase=prep: continue to step 1.
phase=finalise: continue to step 0.5.
0.5. Load prior prep state (phase=finalise only)
Skip this step unless phase=finalise.
Locate the most recent <draft-folder>/*/*.prep.md (per ADR-040, drafts live in per-date sub-directories):
Glob: <draft-folder>/*/*.prep.md
Pick the file with the most recent prep-date frontmatter value (tie-break by filename ISO date). If no .prep.md is found, surface to Tom via AskUserQuestion:
- question:
"phase=finalise was requested but no .prep.md draft was found in <draft-folder>. Run as phase=full instead, or abort?"
- options:
Run as phase=full: rebind phase variable to full and continue to step 1.
Abort: stop the pipeline. Tom can re-invoke with phase=prep.
- multiSelect: false
If a .prep.md is found, read it and parse its frontmatter. Required fields:
phase: prep (sanity check)
prep-date: YYYY-MM-DD
prep-source-cutoff: <ISO timestamp>
source-failures: list of tier-1/tier-2/tier-3 source URLs that failed during prep
map-mutation-status: mutated or skipped (with reason if skipped)
Bind the prior-prep state for downstream steps:
<prep-draft-body>: the full body content after the frontmatter (per ADR-026, the prep .prep.md contains only frontmatter + body; no review-block separator is present).
<prep-shortlist-snapshot>: the list of items represented in the prep draft (parse the ### Item N blocks).
<prep-inbox-snapshot>: the inbox files that were resolved during the prep run (recorded in the .prep.md frontmatter or, if absent, derived from the prep-date by re-globbing src/newsletters/inbox/*.md and filtering to mtime <= prep-source-cutoff).
<prep-tier1-snapshot>: the tier-1 candidate set as recorded in prep metadata (or, if absent, the tier-1 items resolvable from the prep-draft Source lines).
<prep-image-path>: the path to the cover image generated in step 12 of the prep run, if any.
<prep-source-failures>: the list of source URLs that failed in prep.
<prep-map-mutation-status>: whether the map was mutated in prep.
<prep-reviews-path>: <draft-folder>/<publication-date>.reviews.md (per ADR-026). Read this sibling file to extract prep-time review blocks (Voice Review, Content Risk Review, Critic Review (Newsletter), Editor Review, Skeptic Review, Cognitive Accessibility Review, Critic Review (Wardley Artifacts), Map Delta, URL Verification) for the carry-forward sections in the finalise reviews file at step 16.
Continue to step 1-prime.
1. Check the inbox
Glob: src/newsletters/inbox/*.md
Read any matching files. Each file contains a link plus a one-sentence note (see src/newsletters/inbox/README.md). Carry these as candidates. They skip the source-fetch step but still pass through the Wardley precondition and the three-lens filter.
Phase variant 1-prime (phase=finalise only): instead of treating every inbox file as a fresh candidate, diff the current inbox glob against <prep-inbox-snapshot>. New files (those not in the snapshot) are flagged as new-inbox-since-prep and surfaced via per-item interactive capture at step 10-prime. Files already present in the snapshot are carried forward via <prep-shortlist-snapshot> and do not need re-filtering.
2. Fetch all sources
Fetch the following in parallel where possible. Each source has an extraction prompt attached.
Phase variant 2-prime (phase=finalise only): re-fetch tier 1 only (all tier-1 sources; see the tier-1 block below). Tier 2 and tier 3 are carried forward from <prep-shortlist-snapshot> and the prep-time source_failures list. The tier-1 refresh is the entire point of finalise: it picks up launches that landed after the prep cutoff and could not have been seen mid-prep. For The Shift specifically (Monday-morning AEST publish, ADR 030), this window captures Friday US/EU launches that broke after the weekend prep run. New tier-1 items are diffed against <prep-tier1-snapshot> (by URL); only the new entries are carried into step 3-prime (build candidate list).
Tier 1 (critical, fail the map update if any of these fail):
-
Anthropic news: WebFetch https://www.anthropic.com/news with prompt: "Extract the 10 most recent news items. For each: title, absolute URL, date if visible, one-sentence summary. Return as numbered list."
-
OpenAI news (Playwright cache primary; Google News RSS fallback per P010). Walk these rungs in order (ADR-029 tier-1 fallback precedence). The first rung that yields items wins; do NOT call later rungs once a rung returns items. The map-update gate (step 5) reads source_failures only; it does not care which rung produced the items.
- Playwright cache at
.cache/newsletters/openai-news/<YYYY-MM-DD>.json (P014a fetcher; ADR-029). If the most-recent file exists AND its fetched_at is within 48 hours of now, Read the JSON and consume the items array. Each item already has {title, url, date, summary} per the ADR-029 schema; canonical openai.com/index/... URLs (not Google News landers).
- Auto-invoke fetcher (P014c). If rung 1 finds no fresh cache, run
Bash npm run fetch:newsroom -- --source=openai from the project root. The helper writes the canonical cache file synchronously (~3-4s). After the Bash returns successfully, retry rung 1 to consume the new cache. If the helper exits 2 (zero items extracted) or non-zero (Playwright failure), advance to rung 3. Do NOT retry the auto-invoke within the same SKILL run.
- Google News RSS fallback (P010 workaround):
WebFetch https://news.google.com/rss/search?q=site:openai.com&hl=en-US&gl=US&ceid=US:en with prompt: "Extract the 10 most recent items syndicated from openai.com. For each: title, absolute URL (prefer the openai.com URL where Google News provides it; fall back to the Google News redirect URL otherwise), date if visible, one-sentence summary. Return as numbered list." Note: the direct page https://openai.com/news/ returns HTTP 403 to WebFetch (Cloudflare bot protection); Google News RSS scoped to site:openai.com is the documented fallback per P010. URLs may resolve to help.openai.com or platform.openai.com landers rather than the canonical announcement page; downstream steps can follow the Google News redirect to surface the primary URL where needed.
- All rungs empty or failed: record
openai-news in source_failures per the tier-1 failure policy.
-
Google DeepMind blog: WebFetch https://deepmind.google/discover/blog/ with prompt: "Extract the 10 most recent blog posts. For each: title, absolute URL, date if visible, one-sentence summary. Return as numbered list."
-
US/UK AI-policy news (via Google News RSS, P051): WebFetch https://news.google.com/rss/search?q=(site:politico.com+OR+site:axios.com+OR+site:reuters.com)+AI&hl=en-US&gl=US&ceid=US:en with prompt: "Extract the 10 most recent items from politico.com, axios.com, or reuters.com that mention AI, artificial intelligence, AI policy, AI regulation, AI safety, AI executive order, or AI Act. For each: title, absolute URL (prefer the outlet's direct URL where Google News provides it; fall back to the Google News redirect URL otherwise), date if visible, one-sentence summary. Return as numbered list." Note: reporter coverage of US and UK AI policy actions, often surfacing days or weeks before the regulator's own page updates. Single combined query (Politico OR Axios OR Reuters) is treated as ONE source for failure-tracking purposes per the OpenAI precedent (P010); this halves the map-update-block surface vs three separate queries.
Tier 2 (important, continue on fail):
- Hacker News AI frontpage:
WebFetch https://hnrss.org/frontpage?q=AI with prompt: "Extract the top 10 items. For each: title, link URL, one-sentence summary. Skip items clearly not about AI."
- Hacker News AI /newest:
WebFetch https://hnrss.org/newest?q=AI&points=50 with prompt: "Extract the top 10 items with at least 50 points. For each: title, link URL, one-sentence summary. Skip items clearly not about AI."
Reddit coverage (P014b landed): reddit.com URLs are refused by the WebFetch tool layer; the Playwright helper from P014 reads shreddit-post web-component attributes from the modern Reddit HTML page (anonymous, no consent gate fires). Both Reddit entries below follow the ADR-029 three-rung precedence applied at tier-2 continue-on-fail policy: (1) Playwright cache fresh, (2) skip (no RSS fallback for Reddit), (3) record source_failures and continue.
-
Reddit r/LocalLLaMA (Playwright cache primary; no RSS fallback). Walk these rungs in order; first rung that yields items wins.
- Playwright cache at
.cache/newsletters/reddit-locallama/<YYYY-MM-DD>.json (P014b fetcher; ADR-029). If the most-recent file exists AND its fetched_at is within 48 hours of now, Read the JSON and consume the items array. Each item already has {title, url, date, summary} per the ADR-029 schema (canonical reddit.com/r/LocalLLaMA/comments/... permalinks; summary is <score> upvotes, <count> comments for the filter to use as a signal hint).
- Auto-invoke fetcher (P014c). If rung 1 finds no fresh cache, run
Bash npm run fetch:newsroom -- --source=reddit-locallama from the project root. The helper writes the canonical cache file synchronously (~5-8s including 2 scroll cycles). After the Bash returns successfully, retry rung 1. If the helper exits 2 (zero items, possible Reddit DOM drift) or non-zero (Playwright failure), advance to rung 3. Do NOT retry the auto-invoke within the same SKILL run.
- Empty or failed: record
reddit-locallama in source_failures per the tier-2 continue-on-fail policy.
-
Reddit r/MachineLearning (Playwright cache primary; no RSS fallback). Same rung structure as r/LocalLLaMA.
- Playwright cache at
.cache/newsletters/reddit-machinelearning/<YYYY-MM-DD>.json (P014b fetcher; ADR-029). Filter the items downstream: focus on research results and industry developments, skip pure academic discussion.
- Auto-invoke fetcher (P014c). If rung 1 finds no fresh cache, run
Bash npm run fetch:newsroom -- --source=reddit-ml from the project root. After the Bash returns successfully, retry rung 1. Exit 2 or non-zero advances to rung 3. Do NOT retry within the same SKILL run.
- Empty or failed: record
reddit-machinelearning in source_failures per the tier-2 continue-on-fail policy.
-
Simon Willison weekly notes: WebFetch https://simonwillison.net/atom/everything/ with prompt: "Extract the 5 most recent posts. For each: title, URL, date, one-sentence summary."
-
AI Daily Brief (beehiiv): WebFetch https://aidailybrief.beehiiv.com/ with prompt: "Extract the 10 most recent posts or episodes. For each: title, absolute URL to the post, publish date, one-sentence summary. Prioritise items that cover societal, policy, personnel, or industry-dynamics stories (for example: attacks on AI-company executives, AI-regulation votes, major hires or departures, enterprise-adoption shifts, public-reaction events). Deprioritise items that are pure product announcements from Anthropic, OpenAI, or Google DeepMind, since those are already covered by tier-1 sources. If the archive is teaser-only, return the surface-level title and date; the downstream step can follow links as needed."
-
Thoughtworks Radar: WebFetch https://www.thoughtworks.com/radar with prompt: "Extract any new or moved AI, LLM, or agent-related blips in the most recent edition. For each: blip name, quadrant (Techniques/Tools/Platforms/Languages & Frameworks), ring (Adopt/Trial/Assess/Hold), one-sentence description."
-
ArXiv cs.AI recent: WebFetch https://arxiv.org/list/cs.AI/recent with prompt: "Extract the 10 most recent papers from this listing. For each: title, arxiv abstract URL, authors, one-sentence summary. Focus on applied and systems-oriented work, skip pure theory."
-
US AI-business news (via Google News RSS, P051): WebFetch https://news.google.com/rss/search?q=(site:nytimes.com+OR+site:bloomberg.com+OR+site:wsj.com)+AI&hl=en-US&gl=US&ceid=US:en with prompt: "Extract the 10 most recent items from nytimes.com, bloomberg.com, or wsj.com that mention AI, artificial intelligence, machine learning, or AI policy. For each: title, absolute URL (prefer the outlet's direct URL where Google News provides it; fall back to the Google News redirect URL otherwise), date if visible, one-sentence summary. Return as numbered list." Note: paywalled outlets accessed via Google News snippets per P051; full article access requires subscription. Single combined query (NYT OR Bloomberg OR WSJ) is treated as ONE source for failure-tracking purposes; tier-2 policy is continue-on-fail, so a Google News quota or rate-limit issue is non-blocking.
Tier 3 (regulatory, continue on fail):
- Australia OAIC:
WebFetch https://www.oaic.gov.au/newsroom with prompt: "Extract the 5 most recent news items mentioning AI, automated decision-making, or data privacy. For each: title, URL, date, one-sentence summary."
- EU AI Act updates:
WebFetch https://artificialintelligenceact.eu/ with prompt: "Extract the most recent developments, implementation updates, or guidance documents. For each: title, URL, date, one-sentence summary."
- UK AI Safety Institute:
WebFetch https://www.aisi.gov.uk/work with prompt: "Extract the 5 most recent publications or announcements. For each: title, URL, date, one-sentence summary."
- NIST AI:
WebFetch https://www.nist.gov/news-events/news/search?key=&topic-op=or&topic-area-fieldset%5B%5D=2753736 with prompt: "Extract the 5 most recent AI-related news items or publications. For each: title, URL, date, one-sentence summary." (URL corrected 2026-05-24: the prior /artificial-intelligence/news path 404s. This is the NIST news-search endpoint; the topic-area-fieldset[]=2753736 query param is the load-bearing filter, where 2753736 is NIST's topic ID for the AI topic area. If NIST renumbers its topic taxonomy this feed breaks silently; re-derive the ID from the AI filter on the news-search page. NIST is low-velocity, so expect the most recent items to be weeks or months old.)
- US FTC AI actions:
WebFetch https://www.ftc.gov/feeds/press-release.xml with prompt: "From this FTC press-release RSS feed, extract the 5 most recent items that mention artificial intelligence, AI, machine learning, or automated decision-making. For each: title, URL, date, one-sentence summary." (P067: the HTML page at /news-events/news/press-releases is WAF-blocked 403 by deeper-than-User-Agent bot detection and even the P014 Playwright helper does not bypass it, but the official machine-readable feed https://www.ftc.gov/feeds/press-release.xml returns 200. Use the feed; do NOT repoint to the HTML page, and do NOT pursue a stealth/bot-evasion layer, which is an arms race and the wrong approach per the no-scraping posture.)
- OECD AI:
WebFetch https://oecd.ai/en/wonk with prompt: "From the OECD.AI 'The AI Wonk' blog index, extract the 5 most recent posts. For each: title, URL, date, one-sentence summary." (P067 follow-up: the prior /en/wonk/news path 404s, URL drift; the /en/wonk Wonk-blog index returns 200. Use /en/wonk.)
For any source whose URL 404s or whose extraction fails, note the failure in a local source_failures list and continue. Do not retry more than once per source. Record the ISO timestamp of completion of this step as <source-cutoff>; this is the value that gets written into prep-source-cutoff when phase=prep saves at step 16.
Other previously-blocked sources (P010 investigation, now resolved): FTC is resolved (P067) by switching its tier-3 source to the official RSS feed https://www.ftc.gov/feeds/press-release.xml (returns 200); only the FTC HTML page is WAF-blocked, and the stealth-fetch option was deliberately rejected (arms race + no-scraping posture). OECD is resolved (P067 follow-up) by correcting the URL drift: the prior https://oecd.ai/en/wonk/news 404s; the https://oecd.ai/en/wonk Wonk-blog index returns 200 and is now the OECD tier-3 source. These are tier-3 regulatory sources, so failures are non-blocking under the existing policy. If WebFetch returns 403 (bot protection) on any tier-1, tier-2, or tier-3 source, the Google News RSS fallback used for OpenAI is the first thing to try; if WebFetch returns a tool-layer refusal (Reddit-style), defer to P014's Playwright helper.
3. Build the candidate list
Combine inbox items, tier-1, tier-2, and tier-3 extracted items into one candidate list. Deduplicate where the same story appears in multiple feeds (prefer the primary source, for example Anthropic's own announcement over an HN discussion of it).
Record per-tier counts and the source_failures list so the Tom-summary can report on source coverage.
Phase variant 3-prime (phase=finalise only): the candidate list is <prep-shortlist-snapshot> plus any new tier-1 items from step 2-prime plus any new inbox items from step 1-prime. Carry the prep-time tier-2 and tier-3 candidates forward unchanged; finalise does not re-fetch them and does not re-filter them. Only the new (post-prep) candidates need filtering at step 4-prime.
4. Apply the Wardley preference + three-lens filter
Read .claude/skills/wr-newsletter/assets/three-lens-filter.md and docs/ai-engineering-brief/ai-landscape.md.
For every candidate:
- Wardley preference: tag each candidate
MAP_ANCHORED or NO_MAP_ANCHOR against the previous week's map. Map anchoring is a preference, not a precondition; a candidate without a clean map anchor can still qualify on three-lens strength alone (see the asset three-lens-filter.md for the full policy). The map update itself runs next at step 5, before per-item voice capture.
- Three-lens scoring: for surviving candidates, score yes or no on technical, operational, human. Keep those scoring yes on at least two. Each yes-scored lens MUST record a
named_action: <one-sentence> field in the candidate's metadata, drawn from the persona-conditional action vocabulary in three-lens-filter.md (the action the <target-reader> should take this quarter). A lens with no nameable persona-conditional action scores no, regardless of topical fit. The named-action requirement absorbs the prior post-filter "significant for the Engineering Leader persona" check (P043 Option C).
- Source tagging: for each surviving candidate, inspect the URLs on the candidate. Tag
HAS_PRIMARY_SOURCE if any URL is a tier-1 primary outlet (any tier-1 source per the tier-1 block at step 2, or any outlet in the primary-outlet allowlist in three-lens-filter.md). Tag NO_PRIMARY_SOURCE if the only URLs are secondary aggregators (AI Daily Brief, Stratechery, podcast notes, letsdatascience.com, aidailybrief.beehiiv.com). The corroboration sub-step 4b acts on the NO_PRIMARY_SOURCE set.
The filtered list is the interim shortlist passed to step 4b (corroboration) and then to steps 5 through 9. There is no upper cap. Minimum three. If fewer than three candidates clear the filter, note the shortfall in the summary for Tom rather than padding. Persona-weighted ranking runs after the map is updated (see step 9.5), because the ranking needs this week's map positions, not last week's.
Phase variant 4-prime (phase=finalise only): apply the filter only to the new (post-prep) candidates from step 3-prime. The prep-time shortlist is already filter-passed; it does not need re-filtering. New candidates that clear the filter join the shortlist; new candidates that fail are dropped silently (they were not in the prep edition and never made it to a per-item capture).
4b. Corroborate aggregator-only candidates (P016)
For every candidate tagged NO_PRIMARY_SOURCE at step 4 that also scored yes on at least two lenses, attempt multi-outlet corroboration before finalising the shortlist. This step closes the P016 failure mode where the filter silently dropped the Tim Cook transition from the 2026-04-24 edition despite multi-outlet primary coverage being one query away.
For each qualifying candidate:
-
Extract the named entity from the candidate title (person, company, product, policy, or event).
-
URL-encode the named entity and issue one Google News RSS query:
WebFetch https://news.google.com/rss/search?q=<encoded-entity>&hl=en-US&gl=US&ceid=US:en
Extraction prompt: "Extract the top 5-10 items. For each: outlet (publisher name), headline, absolute URL, publish date. Skip items that are clearly re-posts of a wire story (opening with 'per Reuters', 'via AP', etc.) rather than independent primary reporting."
-
Count distinct primary outlets (see the primary-outlet allowlist in three-lens-filter.md). The same outlet domain counts once regardless of how many Google News entries it has; aggregator re-posts do not count.
-
Apply the threshold:
- 3+ distinct primary outlets: tag the candidate
CORROBORATED_PRIMARY. Attach the 3 strongest outlet/URL pairs to the candidate's metadata (this becomes the Source line's attribution in step 11b). Move the candidate into the main shortlist as if it were tier-1-sourced.
- 0 to 2 distinct primary outlets: tag the candidate
WEAK_ATTRIBUTION. Do NOT drop. Carry on a separate weak-attribution list that step 10 surfaces to Tom for explicit keep/drop/ask-for-help resolution.
-
Record per-candidate corroboration outcome in internal metadata (count of distinct primary outlets, query used, top 3 outlets). The Tom-summary at step 17 reports on the corroboration pass.
Placement rationale (P016 investigation task resolution): corroboration runs AFTER three-lens scoring so weak-three-lens candidates are cheap-dropped before burning a Google News query, and BEFORE the map-mutation gate (step 5) so CORROBORATED_PRIMARY candidates can legitimately anchor to map movement in step 6. Tier-1-sourced candidates bypass this step entirely; the expected fan-out is 0-2 Google News queries per edition in a typical week.
Precedent: the Google News RSS mechanism is already established in the pipeline for OpenAI tier-1 source-fetch fallback (see docs/ai-engineering-brief/ai-landscape.md Source-coverage notes and P010). This sub-step reuses the same mechanism for a second use case; a follow-up ADR amendment codifying Google News RSS as a first-class pipeline primitive for both tier-1 fallback and aggregator corroboration is advisable (see docs/decisions/031-google-news-redirect-resolution-as-pipeline-primitive.proposed.md).
Phase variant 4b-prime (phase=finalise only): corroborate only new (post-prep) candidates that landed in NO_PRIMARY_SOURCE. Prep-time corroboration outcomes are carried forward in <prep-shortlist-snapshot>.
5. Map-mutation gate (ADR 016 failure-mode rule)
Before touching the map, check the source_failures list. If any tier-1 source (any tier-1 source; see the tier-1 block at step 2) failed this run, do not mutate the map this week.
In the tier-1-fail case:
- Skip steps 6, 7, 8 (map update, render, analysis update).
- Produce the brief against the unchanged map from the previous run.
- Note the skip in the summary: "MAP_UPDATE_SKIPPED: tier-1 source(s) failed, running against previous map."
- Each item's Map movement line refers to existing movement in the current analysis, not to any update this run.
If all tier-1 sources succeeded, proceed to step 6. Tier-2 or tier-3 failures do not block map mutation; record them in a note on the commit message when the map is updated.
Phase variant 5-prime (phase=finalise only): late-breaking map-moving story branch (ADR 017 line 74). If step 2-prime surfaced new tier-1 items (and step 4-prime kept any of them), evaluate whether any of the kept new items is map-moving: would including it require a new component, a moved component, an evolution-arrow change, or a dependency edit relative to the map state recorded in <prep-map-mutation-status>? Use the same map-anchoring rubric as step 4.
Branch:
- No new map-moving stories: skip steps 6-prime through 9-prime. The map state from prep is carried forward as the published map this week. Continue to step 9.5-prime.
- At least one new map-moving story: surface to Tom via
AskUserQuestion:
- question:
"Late-breaking item lands as map-moving: <story summary>. Treat as Also-worth-noting (no map mutation), or Restructure (re-mutate map, re-run Wardley critic)?"
- options:
Also-worth-noting: keep the item but do not mutate the map. Add as Also-worth-noting in step 11b-prime. Continue to step 9.5-prime.
Restructure: re-run steps 6, 7, 8, 9 against the prep-time map plus the new map-moving items. The Wardley critic gets a fresh per-artifact-pass budget per ADR 017 lines 39-41. Continue to step 9.5-prime once the critic passes.
- multiSelect: false
Record the branch decision in internal metadata for Tom-summary reporting at step 17.
6. Update ai-landscape.owm
Read docs/ai-engineering-brief/ai-landscape.owm.
For each shortlisted candidate, propose one or more of:
- Component position change: update a component's
[visibility, evolution] coordinates.
- Evolution arrow update: change an
evolve X <target> line's target, or add/remove an arrow.
- New component: add a
component <Name> [v, e] line.
- Dependency change: add or remove a
A->B link.
- Component removal or merge: remove a
component line (only if a component is genuinely gone from the landscape, not just unmentioned this week).
Apply the changes to the .owm file. Preserve any hand-edited positions from prior Tom overrides; only change what this week's evidence warrants.
7. Re-render the map
node ~/.claude/plugins/cache/windyroad/wr-wardley/0.1.0/skills/generate/owm-to-svg.mjs docs/ai-engineering-brief/ai-landscape.owm docs/ai-engineering-brief/ai-landscape.svg
If the render fails, revert the .owm edit and note the failure in the summary.
8. Update ai-landscape.md
Read the current docs/ai-engineering-brief/ai-landscape.md and update:
- The Differentiation / Evolution / Risk / Decisions sections to reflect the new map state.
- Add a short "This week" note near the top of the Analysis section summarising what moved and why.
- Check that every
evolve arrow in the .owm has a corresponding Evolution-section explanation. The simplified Wardley critic (ADR 035) will flag missing explanations as a weakness; landing this consistency at draft time avoids round-2 rework.
- Check that every component in Genesis or Custom-Built phase appears in Differentiation. Silent omissions of custom components surface as critic weaknesses.
9. Critic loop on the Wardley artifacts (ADR 016, ADR 025, ADR 035)
Run the critic agent against the updated map and analysis. Per ADR 035, the rubric is a brief editorial prompt (STRENGTHS, WEAKNESSES, optional RELEVANT CONTEXT); no structured numbered-check list, no accepted_overrides allowlist. The critic is an editorial reader of the analytical quality of the artifact.
Agent subagent_type: wr-wardley-critic
prompt: "Review the Wardley artifact.
artifact_path: /Users/tomhoward/Projects/windyroad/docs/ai-engineering-brief/ai-landscape.md
rubric_path: /Users/tomhoward/Projects/windyroad/.claude/skills/wr-newsletter/assets/wardley-critic-rubric.md
round_number: 1
prior_weaknesses: n/a"
Parse the returned CRITIC_REVIEW block:
- If
VERDICT: PASS: proceed to step 9.5.
- If
VERDICT: WEAKNESSES_FOUND: fix each listed weakness in ai-landscape.md (or .owm if the weakness is structural). Re-invoke the critic with round_number: 2 and prior_weaknesses: <round-1 weaknesses verbatim>.
- Repeat for up to round 3. On
VERDICT: PASS_WITH_AUTHOR_OVERRIDES: the drafter has accepted a critic-flagged weakness as an editorial choice (named verbatim in the saved review block per ADR 035). Proceed to step 9.5 because the variant is publish-ready.
- On
VERDICT: REJECTED with REJECTED_REASON: critic-loop-exhausted, save the review block with the artifacts and note the unresolved weaknesses in the summary, but still proceed to step 9.5. The map is the best we have this week; a weak map still beats no map.
Capture the final critic block for inclusion in the saved draft.
Phase variant 9-prime (phase=finalise only): runs only when the late-story branch (step 5-prime) chose Restructure. The critic gets a fresh per-artifact-pass budget (up to 3 rounds) under ADR 017 lines 39-41, distinct from the prep-time critic budget. If Also-worth-noting was chosen, skip 9-prime and carry the prep-time Wardley critic block forward.
9.5. Persona-weighted ranking
Among the candidates that cleared step 4, and using this week's updated map (steps 5-8) plus the Wardley critic's latest verdict (step 9), order by <three-lens-weighting> from the persona config. Items strong on the persona's primary lens rank above items strong only on secondary lenses. Ties broken by <source-weighting> (a primary-tier-source item beats a secondary-tier-source item on the same lens score). Map movement this week is explicit input to the ordering: candidates whose related map component moved this week rank above candidates with comparable lens scores whose components did not move.
Output: the final persona-weighted shortlist. This is the input to step 10's per-item voice capture.
Phase variant 9.5-prime (phase=finalise only): re-rank only if new items joined the shortlist at step 4-prime, or if the late-story branch (step 5-prime) chose Restructure (which changes map state and may shift the ranking input). Otherwise carry <prep-shortlist-snapshot>'s ranking forward unchanged.
10. Per-item interactive voice capture (AskUserQuestion)
For each shortlisted candidate, call the AskUserQuestion tool with:
- question:
"Item <N>: <one-sentence story summary>. Our take: <1-2 sentences on why it matters or the angle we'd emphasise, grounded in this week's map movement from step 8's analysis>. Source: <URL>. Agree, Adjust, or Drop?"
- options:
Agree: carry the candidate forward with the original take.
Adjust: Tom edits or adds via the "Other" free-text escape hatch. His text becomes source material for the item's Why-it-matters and Human-angle lines.
Drop: remove from shortlist; record Tom's reason in internal metadata.
- multiSelect: false
Because the map has already been updated (steps 5-8) and the Wardley critic has already passed (step 9), the "Our take" presented per item is informed by this week's actual landscape movements, not by last week's analysis applied to raw candidates. Tom's adjusts land against the richer substrate, so the "From Tom" opener and Why-it-matters lines have more to push off.
Capture per-item responses. Adjusts feed the drafter (step 11b): Why-it-matters and Human-angle lines incorporate Tom's phrasing where he gave any, otherwise use the original take. The "From Tom" opener (step 11b) is assembled from the strongest POV across Tom's adjusts that week.
If zero candidates get an Adjust with a strong POV, the opener defaults to a meta-observation about the week's theme (not a model-guess at Tom's voice). Note in the summary.
After the main shortlist per-item capture completes, run a second pass for any candidates tagged WEAK_ATTRIBUTION at step 4b (P016). For each weak-attribution candidate, call AskUserQuestion with:
- question:
"Weak-attribution item <N>: <one-sentence story summary>. Aggregator-only source: <aggregator URL>. Corroboration outcome: <N> distinct primary outlets found (<list of outlet names>); threshold for primary-sourced treatment is 3. Keep as Also-worth-noting, Drop, or Ask for help?"
- options:
Keep as Also-worth-noting: include in the draft as an Also-worth-noting entry (not a full Item block). Attribution line names the aggregator plus any primary outlets that did surface.
Drop: remove from consideration. Record reason in internal metadata.
Ask for help: Tom wants to discuss further before deciding. Free-text capture via "Other". The drafter pauses on this item; the Tom-summary surfaces the unresolved question.
- multiSelect: false
Weak-attribution handling preserves the signal that the earlier filter would have silently dropped. A weak-attribution candidate that Tom keeps joins the main draft as an Also-worth-noting entry (short paragraph, not a full Item). Tom's Drop reasons feed future filter tuning.
Capture transcript artifact (ADR 019). After both passes (main shortlist plus weak-attribution) complete, write the per-item capture decisions to <draft-folder>/<publication-date>.capture.md. This file is the persisted reference the drafter (step 11b) reads against to preserve verbatim spans, and the comparison surface for human editorial review. Format per ADR 019:
---
persona: <leader|developer>
edition: <N from step 11a>
date: <YYYY-MM-DD>
phase-written: <prep|finalise|full>
phase-last-appended: <prep|finalise|full>
---
# Capture transcript: <publication-name>, week ending <week-ending>
## Item N: <one-sentence story summary>
- **Outcome**: <Agree | Adjust | Drop | Keep as Also-worth-noting | Ask for help>
- **Original take presented**: <the "Our take" sentence shown in the AskUserQuestion prompt>
- **Source**: <URL>
- **Adjust text** (if outcome is Adjust):
> <Tom's free-text verbatim, indented as quoted block>
- **Drop reason** (if outcome is Drop): <Tom's reason>
- **Ask for help question** (if outcome is Ask for help): <Tom's question>
For phase=full and phase=prep, write the file once after capture completes. Today's consumer is the drafter at step 11b; future consumers (possible voice / content-risk extensions) are scoped per ADR 019. The voice gate (step 13), content-risk gate (step 14), and SW-critic (step 15) do not read this file in the current pipeline.
Phase variant 10-prime (phase=finalise only): run per-item capture only on:
- New tier-1 items from step 2-prime that cleared step 4-prime, plus new inbox items from step 1-prime.
- Late-story
Also-worth-noting items from the step 5-prime branch (if Also-worth-noting was chosen).
- New
WEAK_ATTRIBUTION candidates from step 4b-prime (a separate pass, same as step 10's second pass).
Prep-time per-item responses (Agree, Adjust, Drop) are carried forward via <prep-shortlist-snapshot> and not re-asked. Tom's prep-time Adjusts and Drops still drive the corresponding Item blocks in the finalise draft.
Capture transcript append (ADR 019). Read the existing <draft-folder>/<publication-date>.capture.md written during prep. Append new-item sections (only the items captured in 10-prime: new tier-1, late-story Also-worth-noting, new WEAK_ATTRIBUTION). Update frontmatter phase-last-appended: finalise. Do not rewrite or delete prep-time entries. The append step must check for an existing item-section anchor before writing to avoid duplicates on a partial-failure re-run (ADR 019 Consequences/Bad).
Missing-capture-file handling (ADR 019). If the expected <draft-folder>/<publication-date>.capture.md is absent at the start of 10-prime, surface to Tom via AskUserQuestion:
- question:
"phase=finalise expected a capture transcript at <expected-path>. Continue without capture transcript (drafter discipline rule has no input for prep-time items), Recreate transcript from prep-time shortlist (best-effort, Adjust text will be empty), or Abort?"
- options:
Continue without capture transcript, Recreate transcript from prep-time shortlist, Abort.
- multiSelect: false
Default branch when Tom is unavailable: Continue without capture transcript. The drafter loses the verbatim-preservation reference for prep-time items but does not block publication. Note the missing-file event in the Tom-summary at step 17.
11a. Compose theme anchor (H1 + cover hook + theme statement) per ADR-037
Before drafting, determine the edition number per the persona config's ## Edition counting rule: scan files matching the canonical brief shape YYYY-MM-DD.md (eight digits and dashes, then .md) across BOTH <published-folder>/*/<YYYY-MM-DD>.md (per-date sub-directory shape per ADR-039) and <draft-folder>/*/<YYYY-MM-DD>.md (per-date sub-directory shape per ADR-040), read each match's frontmatter edition: value, take the maximum, and add one for the current draft. The YYYY-MM-DD.md basename filter excludes ADR-026 sibling files (.linkedin.md, .reviews.md, .capture.md) and folder index files (README.md) by construction; do NOT use a plain *.md glob (P062). If no prior edition file exists in either folder, the next edition is 1. Assert that the computed edition number is exactly one greater than the highest prior edition: value before writing the draft frontmatter; if not, surface to Tom and abort rather than publishing with a wrong issue number. Write the edition number into the draft frontmatter (edition: N) so the critic (ADR 035) can reason about first-edition vs ongoing framing when deciding whether to flag a missing welcome line as a weakness. For edition 1, include the persona's <welcome-line> above the voice opener; for edition >=2, drop or freshly reframe the welcome line rather than repeating the first-edition text.
Read docs/VOICE-AND-TONE.md (base) and the <voice-addendum> from the persona config, plus .claude/skills/wr-newsletter/assets/draft-template.md and docs/ai-engineering-brief/ai-landscape.md.
Compose the theme anchor (text only at 11a; cover image render stays at step 12 unchanged per ADR-037 sub-decision 1 / Option C, Tom-pinned 2026-06-03):
- Headline (H1): a unique POV-carrying H1 (6-12 words). The H1 must pass the
docs/VOICE-AND-TONE.md "competent CTO from a non-AI-native company" standalone-readability test: do not stack two or more specialist terms without a gloss (Issue 07 shipped "substrate provider" undefined in the H1, P075). Follow on the next non-blank line with the persona's <headline-pattern> subtitle (e.g. *The Shift, AI engineering, week ending YYYY-MM-DD* for leader, or *Tokens Spent, AI engineering for developers, week ending YYYY-MM-DD* for developer). The YYYY-MM-DD is <week-ending> (the Sunday), not <publication-date>.
- Cover hook line 1 (white, around 28 chars max per P063 LinkedIn-crop budget) and line 2 (accent orange, around 45 chars max). Text only at 11a; the cover image renders at step 12 (status quo).
- One-paragraph theme statement that names the deep items by their shared constraint and previews the variation each item shows. This statement IS the "thesis-first intro" element 1 from ADR-032's three-deep-items shape; the body opener at 11b will elaborate it. Keep to one paragraph; specific (named constraint, named variation); honors the voice rules below.
Voice rules at 11a (subset of step-13 voice gate, applied to the anchor only):
- Team voice ("we"), not "I" (ADR 010). The H1 + theme statement carry the editorial position; the "From Tom" opener is at 11b.
- Direct, specific, confident. Name the constraint, name the variation.
- No em-dashes. Use commas, periods, colons, or parentheses.
- No hype words.
- Respect the reader's team (ADR 015).
11a Tom-approval gate (per ADR-037)
After composing the theme anchor, fire an AskUserQuestion (per ADR-013 Rule 1):
header: "Theme anchor"
question: "Theme anchor for Issue . H1: . Hook lines: <line 1> / <line 2>. Theme statement: . Approve?"
- Options:
- Accept (Recommended) - proceed to 11b with the approved anchor.
- Refine - Tom edits the H1, hook lines, or theme statement via the "Other" free-text escape hatch; 11a re-runs the approval gate with the refined anchor.
- Reject - back to step 9.5 (re-ranking) or step 10 (per-item capture) with a note on what failed.
Do NOT proceed to 11b until the gate returns Accept. The approved anchor is the load-bearing frame the body work amortises against (per ADR-037 Decision Drivers: theme anchor first).
11b. Draft body using approved theme anchor (per ADR-037)
Using the approved H1, hook lines, and theme statement from 11a, produce the full draft body:
- The approved headline + subtitle from 11a.
- One-sentence intro that elaborates the 11a theme statement (the body opener leads with the approved theme; do not author a fresh intro that competes with the anchor).
- For developer persona, label each item's evidence stance as shipped, benchmarked, demo, or not yet (J9 + J11 paired). For leader persona, the evidence label is optional; business-consequence framing carries primary weight.
- One
### Item N block per shortlisted candidate (minimum 3, no maximum), ordered by <three-lens-weighting>. Each item has: What happened (with the primary claim inline-linked), Why it matters to your team, The human angle. Do NOT append a separate **Source:** block when the What-happened text already carries inline links (structural invariant, P089).
- Item Why-it-matters lines reference the 11a theme where natural (per ADR-037: body threads the approved frame).
- Closing CTA: pick one
<cta-invitation> from the persona config (rotate week to week to avoid verbatim repetition), followed by the closing line windyroad.com.au. Do NOT emit a Windy Road services-description sentence (P090): the CTA is invitation + linked windyroad.com.au only. This is enforced at save by check-newsletter-structure.sh check (g).
Heading craft (P075). The H1 and each ### Item N: heading must each pass the docs/VOICE-AND-TONE.md "competent CTO from a non-AI-native company" standalone-readability test on its own, without the body. Two disciplines:
- Do NOT default to the colon-flourish "X: Y" framing on Item headings. Vary the heading shape across the edition: no more than half the items in one edition may share a single framing pattern. (Issue 07 ran the colon-flourish on 5 of 7 items and Tom rejected the set.)
- Apply gloss-on-first-use at HEADING granularity, not just body copy. A specialist term whose first appearance in the edition is in a heading is glossed in that heading or immediately in the body; do not stack two or more in-group specialist terms without a gloss in a single heading (Issue 07 examples: "substrate provider", bare "Starlette", "services-arm", stacked "tier-1" plus "eval-governance" plus "eval harness", "sandboxing patterns").
The step-15 critic's "Headline craft" rubric section is the catch-it-in-review backstop; this drafter discipline prevents producing the failure in the first place.
Voice rules (enforced by step 13 voice gate):
-
Team voice ("we"), not "I" (ADR 010). The "From Tom" opener is the only place where "I" is permitted.
-
Direct, specific, confident. Name the org, name the artifact, name the date.
-
No em-dashes. Use commas, periods, colons, or parentheses.
-
No hype words.
-
Respect the reader's team (ADR 015). Describe industry baselines and situations, not the reader's team's competence. This applies to both personas; the developer-audience equivalent is "do not call a developer's tool choice incompetent; criticise via evidence."
-
Apply the persona's <voice-addendum> for vocabulary preferences (leader-coded vs developer-coded language).
-
No editorial-process meta-commentary in body copy (P036, interim). The body of an Item block describes what happened and why it matters to the reader's team. It is not a place to narrate the editorial process by which the item arrived in this brief. Banned patterns include:
- Corroboration assurance: "(corroborated across LA Times, CNBC, and Business Insider, week of April 20)". The corroboration discipline (step 4b) is internal; the reader does not need reassurance.
- Time-window tags about the editorial cycle: ", week of April 28.", "the same week", "surfaced via Simon Willison and Hacker News on April 30". These narrate when the editorial team noticed the story rather than when the story happened.
- Evidence-stance hedge prose ABOUT the rating system itself: "(qualitative observation, no published comparative benchmarks cited here)", "we treat this as a qualitative observation rather than a quantitative finding". This is meta-prose about evidence stance, not evidence itself. The evidence-stance LABELS themselves ("shipped", "benchmarked", "demo", "not yet", "varied widely") are JTBD-205 + ADR-019 + P035 required and are NOT banned by this rule. Use the labels; do not narrate the rating system.
- Business-state disclosure: "Tom is currently in a full-time engineering role, so the Windy Road consulting practice is fully booked". Internal context about the publisher; not part of the reader-facing artefact.
- Surfacing-attribution prose in body sentences: "We're flagging this, via Stratechery, as worth watching", "per The Register, the bug was…". Narrates editorial process inside body prose. Outlet names in the Source line and as inline citations pointing at a verifiable URL are REQUIRED attribution per JTBD-205 + JTBD-203 + content-risk rubric check_4 and ADR-026, and are NOT banned by this rule. The rule targets editorial-process narration in body sentences, not legitimate citation.
- Self-referential editorial commitment: "I call that test theatre" (process artefact framing); rephrase as the claim the editorial position rests on, not as the editorial position itself.
This is interim discipline rule (mirroring ADR-019's inline-discipline-rule pattern for capture-fidelity). Reassessment trigger: if Tom logs more than one editorial-meta correction per edition for the next 4 editions after this rule lands, escalate to a detector subagent (wr-newsletter:editorial-meta-detector, suggested by P036's original Fix Strategy). The reassessment count starts on the first edition that runs against this rule. Until escalation, the rule is enforced by the step 13 voice gate plus the next reviewer's manual reading of the brief body.
Structural invariants (P089, P090). The five LLM review gates check voice, content-risk, editorial quality, reader-experience, and cross-edition consistency; none checks deterministic structural format. Produce these seven invariants by construction (a deterministic lint blocks the step 16 save when any is violated):
- No trailing
**Source:** block in an item whose What-happened text already carries inline links. Inline-link the claim; do not also append a Source line.
- Do not name two or more outlets on a line without linking them ("corroborated by Reuters, FT, NYT, and WSJ" is banned). Link the outlet when you name it; a single back-reference to an already-linked article is fine.
- Keep the
### Also worth noting section as a standalone closing coda; do not fold it into the items.
- H1 carries the
Issue NN: prefix matching published editions (# Issue 09: <one-liner>), set from the edition number at step 11a.
- A
--- horizontal rule precedes the closing CTA block.
- Model names are consistent across the brief and the
.linkedin.md sibling (write "Gemma 4 12B" in both, not "Gemma 4 12B" in the brief and "Gemma 4" in the post).
- No services-pitch sentence in the closing CTA (P090). The CTA block (everything after the final
--- horizontal rule) carries at most one non-blank, non-link prose line: the rotating invitation. The windyroad.com.au closing line and blank lines do not count. A Windy Road / Tokens Spent services-description sentence is a disallowed second prose line.
These are codified in assets/draft-template.md (the "Structural invariants" section) and enforced at save by scripts/check-newsletter-structure.sh.
Unresolvable source URLs: carry to the ask, do not degrade (P091). When the drafter names an outlet but cannot resolve a canonical deep link for it (Google News RSS surfaced only the bare publisher domain, or the article sits behind a paywall / bot wall), do NOT satisfy invariant 2 by dropping the outlet name, and do NOT emit a name-without-link. Carry the citation (outlet, claim, any bare-domain URL) into the step 11.5 Unresolvable-URL terminal fallback, which batches all such citations into a single AskUserQuestion for the canonical URLs before any drop. The user is present to help (P091: "If you cannot resolve a URL, ask me"). The P089 lint check (b) is the deterministic backstop at save; this discipline prevents producing the name-without-link in the first place.
Capture fidelity (P015 + ADR 019). When composing each Item block from a step-10 Adjust capture, preserve the load-bearing noun-phrases, first-person observations, and named artifacts from the Adjust text verbatim wherever the LinkedIn column can carry them. Specifically:
- Named tools, products, vendors, model versions, repository names, dates, and quantitative claims appear verbatim in the rendered Item.
- First-person observations Tom supplied (e.g. "we shipped this last quarter and it broke under load") survive as quoted or paraphrase-with-quote spans, not as flattened third-person commentary.
- Paraphrase only connective tissue (transitions, headline framing, the lens-anchor sentence). Never paraphrase the load-bearing claim.
- Optionally during generation, mark verbatim spans with
{{verbatim}}…{{/verbatim}} markers so the drafter can self-check that fidelity holds before save. Strip these markers before the step-16 save; they must not appear in the final draft body or LinkedIn post.
- Read
<draft-folder>/<publication-date>.capture.md (written at step 10) as the persisted reference. The AskUserQuestion conversation history is also available in-context; both inputs feed the same fidelity discipline.
The drafter is an inline main-assistant pass (not a subagent). The capture-fidelity rule lives in this skill prose because the inline drafter reads the skill as its working instructions. Promoting the drafter to a fresh-context subagent would lose access to the AskUserQuestion conversation history; ADR 019 documents the rationale.
If <draft-folder>/<publication-date>.capture.md is absent (e.g. phase=finalise chose Continue without capture transcript), the drafter falls back to the in-context AskUserQuestion turn for any items captured in this session, and treats prep-time items without a transcript reference as best-effort (verbatim preservation cannot be checked against a persisted source).
Source-article quantitative-claim fidelity (P035, interim). The capture-fidelity rule above governs Tom's Adjust text. A separate discipline applies to numbers cited from the source article body. When an Item references a count, percentage, ratio, range, or currency value drawn from the source article (as distinct from Tom's Adjust capture), the value must appear verbatim from the article body, not paraphrased from the news-fetch one-sentence summary. Specifically:
- Do not round or "tidy" counts (write
26,904 not 27,000; 42.7% not ~40%).
- Do not collapse a range into a single value or replace a specific count with a magnitude phrase ("thousands of", "tens of thousands").
- Do not duplicate one value across distinct denominators (e.g. "27,000 queries got 27,000 different answers" when the source's count of queries and count of distinct answers are not the same number).
- If the article body is unavailable in this session (Cloudflare block, Playwright fetch failure, paywall, or the news-fetch surfaced only a redirect URL), cite the qualitative claim ("varied widely", "across thousands of queries") rather than fabricating a number.
This rule is interim defence-in-depth. ADR-024 (URL verification gate) owns this responsibility structurally. Its fresh-context subagent compares article body against the brief's specific claim and returns SUPPORTED / REFUTED / NOT MENTIONED, which catches quantitative drift as a side-effect of body-content semantic comparison. Until ADR-024 confirmation criterion 1 is met (step 11.5 documented and exercised across one full prep-finalise cycle without user intervention on URLs), the drafter operates this rule as in-context discipline. Once met, this sub-section reduces to a one-line cross-reference to ADR-024.
Phase variant 11a-prime + 11b-prime (phase=finalise only) per ADR-037:
11a-prime always re-runs the theme-anchor approval gate (per ADR-037 sub-decision 2 / Option B, Tom-pinned 2026-06-03). Fire an AskUserQuestion carrying the prep-time approved anchor as the default:
header: "Theme anchor (finalise)"
question: "Prep-time theme anchor for Issue : H1: . Hook lines: <prep line 1> / <prep line 2>. Theme statement: . Still right at finalise?"
- Options:
- Accept (Recommended) - prep-time anchor carries over unchanged; proceed to 11b-prime.
- Refine - Tom edits via "Other" free-text; 11a-prime re-runs the gate. The refined anchor replaces the prep-time anchor for finalise.
This catches Friday/Saturday/Sunday theme-drift between prep and finalise even when ADR-017's Restructure rule does not fire. Cost is one approval gate per finalise; the default-Accept path keeps the friction low.
11b-prime starts from <prep-draft-body> rather than drafting from scratch. Apply changes only for:
- New Item blocks for kept new tier-1 items from step 10-prime.
- New Also-worth-noting blocks for
Also-worth-noting items from step 5-prime, kept new WEAK_ATTRIBUTION items from step 10-prime, and any prep-time Ask for help items Tom resolved between phases.
- Re-ranked Item ordering if step 9.5-prime re-ranked.
- Updated headline / intro / theme statement if 11a-prime gate returned Refine OR if the late-story branch (step 5-prime) chose
Restructure and the week's theme genuinely shifted.
- Updated Map movement lines on existing items if the map was re-mutated at step 6-prime.
If no material changes apply AND 11a-prime gate returned Accept, 11b-prime is a no-op and <prep-draft-body> is the finalise draft body unchanged.
11.4. Cross-edition thesis-consistency gate (ADR-038)
After 11b body draft completes, invoke the wr-newsletter-cross-edition-consistency subagent to check the current draft's load-bearing theses against the prior N=8 published editions in the same persona series. Catches the failure mode where Issue N silently contradicts Issue N-K (witnessed in the 2026-06-01 Issue 07 prep run where Item 1 contradicted Issue 06's "capacity not smaller team" thesis; the 5 in-isolation gates shipped past it without flagging).
Prior-edition window per Tom-pinned sub-decision 1 / Option C (ADR-038): N=8 (rolling two-month window at weekly cadence).
Subagent input shape per Tom-pinned sub-decision 3 / Option A (ADR-038): full prior-edition bodies for all N editions.
Resolve prior-edition paths:
GLOB="<published-folder>/*/<YYYY-MM-DD>.md"
If fewer than 8 prior editions exist in the persona's published folder (early in the series), pass what is available. The subagent surfaces the count in its verdict.
Invocation:
Delegate to wr-newsletter-cross-edition-consistency via the Skill tool (subagent_type matches the agent's name: frontmatter field). Pass:
artifact_path: absolute path to the current draft (<draft-folder>/<publication-date>.md for phase=prep writing .prep.md, OR the resolved .prep.md / .md for finalise).
prior_edition_paths: the up-to-8 absolute paths from the glob above, sorted by edition number descending.
persona: leader or developer.
publication_date: the resolved <publication-date> from step 0.
The subagent returns a structured block beginning with CROSS_EDITION_CONSISTENCY_VERDICT: <SUPPORTED|CONTRADICTS|NEUTRAL> on its own line. Parse the verdict and route per the save-gate semantics below.
Save-gate semantics per Tom-pinned sub-decision 2 / Option A (ADR-038): block save until Tom resolves on CONTRADICTS.
| Verdict | Action |
|---|
SUPPORTED | Gate passes silently. Append verdict block to .reviews.md ## Cross-Edition Consistency (see step 16). Proceed to step 11.5. |
NEUTRAL | Gate passes silently. Append verdict block to .reviews.md ## Cross-Edition Consistency. Proceed to step 11.5. |
CONTRADICTS | Block save. Fire AskUserQuestion (per ADR-013 Rule 1) with the contradictions surfaced. Three options: |
AskUserQuestion on CONTRADICTS:
header: "Cross-edition contradiction"
question: name the count of contradictions and the prior-edition dates involved (for example, "1 contradiction found vs Issue 06 (2026-05-25). Resolve?"). Include the first contradiction's current-draft and prior-edition quoted passages inline for at-a-glance context.
- Options:
- Rewrite (Recommended on a contradiction Tom did not intend) - the drafter re-runs on the conflicting passages with the prior-edition quoted context attached; 11.4 gate re-runs after rewrite.
- Override with reason - Tom records a one-line reason via "Other" free-text; gate passes with
PASS_WITH_AUTHOR_OVERRIDES-equivalent verdict (per ADR-025); reason is logged to .reviews.md ## Cross-Edition Consistency.
- Accept as deliberate evolution - Tom records a one-line reason naming the position-shift via "Other" free-text (for example, "Q2 evidence changes the capacity argument; intentional position update"); gate passes; reason is logged. The next-edition drafter should reference this evolution in the new edition's framing.
When Rewrite is chosen, the drafter loop runs once more on the affected items only (not a full 11b re-draft); the gate re-fires after the rewrite. When Override or Accept-as-evolution is chosen, the gate passes immediately.
Audit trail: append the verdict block (plus override / evolution reason where applicable) to .reviews.md ## Cross-Edition Consistency per ADR-026 sibling-file pattern. Every edition's .reviews.md carries this section; SUPPORTED and NEUTRAL verdicts contribute a one-line "no findings" entry, CONTRADICTS verdicts carry the full Findings block.
Phase variant 11.4-prime (phase=finalise only): re-run only if 11a-prime gate returned Refine OR 11b-prime introduced new items or theme changes. If 11a-prime returned Accept AND 11b-prime was a no-op AND no new items landed, carry the prep-time verdict forward (append (carried from prep) to the .reviews.md ## Cross-Edition Consistency entry). Default when in doubt: re-run.
11.5. URL verification gate (ADR-024)
For every URL that appears in the draft body, verify the URL resolves and the article body matches the brief's specific claim. Block save on REFUTED or 404. This step is non-skippable in phase=prep, phase=finalise, and phase=full; ADR-024 owns the decision shape and P034 documents the failure mode this gate prevents.
Scope boundary (P102). This gate only checks claims that carry a URL. A rhetorical "X once happened" historical claim or a vivid analogy stated as fact carries no URL and is therefore invisible here. Those claims are caught by the content-risk gate's factual axis (content-risk-rubric.md check_1), which requires a historical anecdote or load-bearing analogy to be sourced or flagged as illustrative.
URL scope. Verify every URL that appears in any of the following surfaces in the step-11 draft:
- Item Source lines.
- Inline citation URLs inside any Item body (
What happened, Map movement, Why it matters to your team, The human angle).
- Also-worth-noting block URLs.
- The From-Tom opener (rare; some editions cite a personal artefact).
- Frontmatter source links (if any).
Do NOT verify outbound CTA URLs that are part of the persona's <cta-invitation> rotation or the closing line (windyroad.com.au and friends); those are owned by the persona config and verified there, not per-edition.
Transport selection (per URL). Choose the fetch transport based on publisher class:
- JS-protected publishers (CNBC, Business Insider, NYT, LA Times, Bloomberg, FT, WSJ, The Information, The Verge, TechCrunch, Wired, Reuters when JS-required, and similar): use
node scripts/playwright-fetch.mjs <url>. The helper returns TITLE, FINAL_URL, and the first 4000 chars of document.body.innerText. Inspect the title and FINAL_URL for resolution (HTTP-level 404 surfaces as a TITLE like "Page not found"; redirect chains surface as a non-matching FINAL_URL). Pass the BODY output to the semantic verification subagent below.
- Static / RSS-style hosts (status.claude.com, anthropic.com news, GitHub blog, status.openai.com, dev.to, GitHub README files): use
curl -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' -L -s -o /tmp/wr-url-<short-hash>.html -w '%{http_code}' <url>. HTTP 200 plus a body that matches the URL slug content counts as resolved; 404 / 403 / 5xx counts as 404 for the save-gate.
- Cloudflare-blocked from this environment (openai.com article pages, anthropic.com article pages when fronted by Cloudflare bot protection, web.archive.org snapshots, some Substack writers): a DuckDuckGo HTML search (
curl -A '<browser-UA>' 'https://html.duckduckgo.com/html/?q=<urlencoded-query>') is used to confirm the URL exists on the public web, then the semantic verification step runs against the search-result snippet rather than the article body. Tag the URL INDIRECT_CONFIRMED as a transport-class outcome (set BEFORE the semantic step runs); it is not a fourth semantic verdict. The semantic subagent still returns SUPPORTED / REFUTED / NOT MENTIONED against the snippet.
Choice of transport is a per-URL judgement; do not run all three. Start with the cheapest viable transport and escalate only on failure.
Semantic verification subagent contract. For each fetched body (or DuckDuckGo snippet for INDIRECT_CONFIRMED), spawn a fresh-context subagent:
Agent subagent_type: general-purpose
prompt: "Compare the article body below against the specific claim. Return one of SUPPORTED, REFUTED, or NOT MENTIONED, plus a verbatim quote from the article body that justifies the verdict where applicable.
CLAIM (verbatim from the brief): <quoted claim sentence from the Item block>
ARTICLE BODY:
<paste the playwright-fetch BODY output, or the curl-fetched HTML body text, or the DuckDuckGo snippet for INDIRECT_CONFIRMED URLs>
Rules:
- SUPPORTED: the article body asserts the claim (verbatim or by clear paraphrase).
- REFUTED: the article body asserts something incompatible with the claim (different number, different actor, different outcome).
- NOT MENTIONED: the article body does not address the claim either way (the brief inferred or extrapolated beyond what the source says).
- You do NOT have access to the brief outside the claim above; do not infer from headline tokens. Treat the article body as the only ground truth.
- Return verdict on the first line, then a one-line verbatim quote on the second line (or 'no supporting quote' if NOT MENTIONED).
"
Fresh-context isolation matters: the subagent must NOT see the rest of the brief, the per-item capture transcript, or the Wardley map. ADR 016 / 018 / 020 establish this pattern; the URL verifier is a direct application of the same isolation rule.
Save-gate semantics. Block the save unless every URL clears:
| Verdict / outcome | Action |
|---|
SUPPORTED | Pass. Proceed to step 12 (cover image) once all URLs report. |
INDIRECT_CONFIRMED + SUPPORTED (semantic) | Pass. Note the transport outcome in the audit trail; readers cannot tell the difference but Tom can audit which URLs ran via DuckDuckGo at retro. |
REFUTED | Fix the brief (rewrite the claim to match the article body, citing the verbatim quote) OR replace the URL with one whose body supports the claim. Re-run step 11.5 on the affected URL. Do NOT save while any URL remains REFUTED. |
| HTTP 404 (or 403/5xx) | Replace the URL with a verified canonical URL (use DuckDuckGo HTML search to find the canonical). If no automated transport can resolve a canonical URL, do NOT drop the source link yet: carry the citation to the Unresolvable-URL terminal fallback below (ask the user) before any drop. Re-run step 11.5 on the replacement URL. |
NOT MENTIONED | Escalate to Tom via AskUserQuestion with the claim and the article-body excerpt. Default action: treat as REFUTED (fix the brief or swap the URL). Tom may explicitly approve an inferred-but-unstated framing, in which case the verdict is downgraded to NOT MENTIONED (author-approved inference) and recorded in the audit trail. |
Unresolvable-URL terminal fallback: ask the user before dropping (P091). The transport ladder above (Playwright for JS-protected publishers, curl for static / RSS hosts, DuckDuckGo HTML search for Cloudflare-blocked hosts) is the automated resolution path. When a source URL cannot be resolved by ANY of these transports (Google News RSS exposed only a bare publisher domain with no deep link, curl returned 403 for a paywall / bot wall, the Playwright fetch failed, AND the DuckDuckGo search surfaced no canonical URL), the citation is unresolvable by machine, not unresolvable. The user is present and willing to supply the canonical URL (Issue 09: every paywalled item was attributed in minutes once asked). Do NOT drop the outlet name and do NOT emit a name-without-link. Ask.
Ordered resolution sequence. The terminal step is the ask, not the drop:
- Automated transports first. Exhaust the per-URL transport ladder above for every URL in the draft. Most URLs resolve here and never reach this fallback.
- Collect, do not drop. Accumulate every citation that survives the ladder unresolved into a single unresolved-citations list (outlet name, the specific claim it attributes, and any partial / bare-domain URL the news fetch surfaced). Keep working through the rest of the draft; do not degrade attribution mid-pass.
- Ask the user with one batched question (terminal fallback before any drop). After the ladder has run for every URL, if the unresolved-citations list is non-empty, fire a SINGLE batched
AskUserQuestion (per ADR-013 Rule 1) that lists every unresolved citation and asks the user for the canonical URLs. Brief each citation's outlet plus claim in the prompt so the user can answer without filesystem access (P350 brief-before-ID discipline). Re-run step 11.5 on each URL the user supplies through the normal transport ladder plus semantic verification.
- Drop only after the user declines. A citation is dropped (outlet name removed; the claim re-sourced or cut) ONLY after the user explicitly declines to supply a URL for it via the batched question. Dropping a citation the user was never asked about is the P091 defect this fallback closes.
AFK / non-interactive carve-out (ADR-013 Rule 6). When AskUserQuestion is unavailable (the /wr-newsletter run is orchestrated AFK or otherwise non-interactive), do NOT silently drop. Record every unresolved citation (outlet, claim, partial URL) in the ## URL Verification block of <draft-folder>/<publication-date>.reviews.md with verdict UNRESOLVED (awaiting user URL), and surface the list at finalise (step 17 names the count) so the user resolves the URLs on the next interactive pass before publish. The brief is held with the unresolved citations flagged, not published with degraded attribution.