For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a safe wp:quote normalization script that converts straightforward legacy WordPress quote blocks into native markdown blockquotes plus Source: lines, then use it to rewrite the clean batch of historical posts.
Architecture: Build one narrow Python normalizer that scans _posts/, parses only safe wp:quote shapes, reports skips explicitly, and defaults to dry-run mode. Drive the converter through TDD, keep the rewrite mechanical, then run the script across the repo and verify the resulting batch with unit tests, validators, and spot checks.
Tech Stack: Python 3, unittest, regex/string parsing, Jekyll markdown content, existing repository validators
Files:
tests/test_normalize_wp_quotes.pyCreate: scripts/normalize_wp_quotes.py
Write a test for a clean block like:
<!-- wp:quote -->
<blockquote class="wp-block-quote"><!-- wp:paragraph -->
<p>First paragraph.</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Second paragraph.</p>
<!-- /wp:paragraph --><cite><a href="https://example.com/story">Example Story</a></cite></blockquote>
<!-- /wp:quote -->
Assert the converted text becomes:
> First paragraph.
>
> Second paragraph.
Source: [Example Story](https://example.com/story)
Run: python3 -m unittest tests/test_normalize_wp_quotes.py
Expected: FAIL because the normalizer module does not exist yet
Write a test for a safe quote block without <cite> and assert the output contains only the markdown blockquote, with no Source: line added.
Run: python3 -m unittest tests/test_normalize_wp_quotes.py
Expected: FAIL because conversion helpers are still missing
Files:
scripts/normalize_wp_quotes.pyTest: tests/test_normalize_wp_quotes.py
Implement helpers that keep YAML front matter intact and operate only on the body content.
Implement a matcher that recognizes only a straightforward wp:quote block with:
no nested quote markers
Map each paragraph to a > line block, separated by blank quoted lines where needed.
Source: lineIf the quote has <cite><a ...>Title</a></cite>, render:
Source: [Title](url)
Run: python3 -m unittest tests/test_normalize_wp_quotes.py
Expected: PASS for the safe conversion cases
Files:
tests/test_normalize_wp_quotes.pyModify: scripts/normalize_wp_quotes.py
Write a test with nested wp:quote wrappers and assert the normalizer skips it with a reason instead of converting it.
Write a test for a post body with more than one wp:quote block and assert it is skipped as ambiguous.
Write a test for a quote block containing image markup and assert it is skipped.
Write a test for a missing closing block marker and assert it is skipped cleanly.
Run: python3 -m unittest tests/test_normalize_wp_quotes.py
Expected: FAIL because skip handling is not implemented yet
Files:
scripts/normalize_wp_quotes.pyTest: tests/test_normalize_wp_quotes.py
Track, per file:
whether a write would change the file
Refuse conversion when the body contains:
wp:quotemultiple wp:quote blocks in one post
Skip blocks containing image or unsupported inner markup.
Print a summary that clearly separates:
reason per skipped file
Run: python3 -m unittest tests/test_normalize_wp_quotes.py
Expected: PASS
Files:
scripts/normalize_wp_quotes.pyModify: tests/test_normalize_wp_quotes.py
Write a test that runs the normalizer without --write and asserts no files are modified.
Write a test that runs with --write and asserts the file content is updated in place for a safe candidate.
If useful for debugging, support restricting the run to:
a short explicit list of paths
Run: python3 -m unittest tests/test_normalize_wp_quotes.py
Expected: PASS with dry-run and write-mode behavior covered
Files:
_posts/*.mdVerify: scripts/normalize_wp_quotes.py
Run: python3 scripts/normalize_wp_quotes.py
Expected:
no files modified
Check that the candidate set looks reasonable and that the skip reasons match the design:
unsupported cite shape
Open a representative sample from older, middle, and newer posts to confirm the dry-run classification makes sense before any write.
Files:
_posts/Verify: scripts/normalize_wp_quotes.py
--writeRun: python3 scripts/normalize_wp_quotes.py --write
Expected:
summary lists converted and skipped files
Open at least:
Verify:
Source: line is correctsurrounding content did not drift
Run: git diff --stat
Expected: only the normalizer script, tests, plan/spec docs, and the converted safe posts appear
Files:
scripts/validate_posts.pytests/test_validate_posts.pyVerify: scripts/check_markdown_in_html.py
Run:
python3 -m unittest tests/test_normalize_wp_quotes.py tests/test_validate_posts.py
Expected: PASS
Run: python3 scripts/validate_posts.py --today "$(date +%F)"
Expected: PASS
Run: python3 scripts/check_markdown_in_html.py
Expected: PASS, because this cleanup only touches markdown posts and should not create raw markdown links in HTML files
Run: python3 scripts/normalize_wp_quotes.py
Expected: already-converted files no longer appear as candidates, and remaining skips still report cleanly
Files:
docs/superpowers/specs/2026-03-26-normalize-wp-quotes-design.mddocs/superpowers/plans/2026-03-26-normalize-wp-quotes-implementation-plan.mdscripts/normalize_wp_quotes.pytests/test_normalize_wp_quotes.pyModify: safe candidate posts in _posts/
Run:
python3 -m unittest tests/test_normalize_wp_quotes.py tests/test_validate_posts.py tests/test_generate_my_web_this_week.py tests/test_publish_social.py
python3 scripts/normalize_wp_quotes.py
python3 scripts/validate_posts.py --today "$(date +%F)"
python3 scripts/check_markdown_in_html.py
git status --short
Expected:
git status shows only the intended new script, tests, docs, and converted post files
git add docs/superpowers/specs/2026-03-26-normalize-wp-quotes-design.md docs/superpowers/plans/2026-03-26-normalize-wp-quotes-implementation-plan.md scripts/normalize_wp_quotes.py tests/test_normalize_wp_quotes.py _posts/*.md
git commit -m "feat: normalize legacy wp quote blocks"