Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c8fa728
fix(site): preserve unmanaged post fields on update
noeltock Oct 13, 2026
5811bb6
perf(site): transmit only the selected mutation baseline
noeltock Oct 13, 2026
ffb4509
perf: defer theme schema and proof work
noeltock Oct 13, 2026
00d2296
fix: bound site asset preparation
noeltock Oct 13, 2026
5df9ccc
fix: seal compiled site artifacts
noeltock Oct 13, 2026
3d4c34c
fix(site): serialize applies across ownership namespaces
noeltock Oct 13, 2026
e5ce2f4
fix: preserve legacy site identities
noeltock Oct 13, 2026
a74b4ee
perf(site): reuse verified planning input and skip retained-item reads
noeltock Oct 13, 2026
714b9fc
test: refresh site media fixtures
noeltock Oct 13, 2026
961cb72
fix(site): require reciprocal ownership and site records
noeltock Oct 13, 2026
fb20147
perf(site): reuse fresh marker and media readback observations
noeltock Oct 13, 2026
e7d50c7
perf(site): hash only owned and referenced attachment files
noeltock Oct 13, 2026
a1be31e
fix(site): assign managed template metadata after native post updates
noeltock Oct 13, 2026
e61bca9
fix: reject cycles in site block trees
noeltock Oct 13, 2026
6b9e687
perf: reuse validated source for pattern compilation
noeltock Oct 13, 2026
40bdb1a
refactor(site): clarify executor flow and reuse compiled patterns
noeltock Oct 13, 2026
37863f7
fix(site): keep unresolved pattern media out of binding hashes
noeltock Oct 13, 2026
a7b3ffa
fix(site): preserve floating draft dates on update
noeltock Oct 13, 2026
d148ec1
refactor: use canonical prepared site identity
noeltock Oct 13, 2026
8ab18aa
refactor: require versioned attachment ownership
noeltock Oct 13, 2026
8485bf3
fix: verify media import asset root identity
noeltock Oct 13, 2026
0874d23
perf(site): verify captured source documents before effects
noeltock Oct 13, 2026
80d1f61
refactor(proof): remove fixture-only CLI constraints
noeltock Oct 13, 2026
3cb3e5c
fix(site): verify captured manifest before completion
noeltock Oct 13, 2026
00c1225
fix(site): scope precise file identities to source guards
noeltock Oct 13, 2026
38f11bf
fix(site): keep native publication dates on draft transitions
noeltock Oct 13, 2026
cf126f5
test(site): correct PHP clock response quoting
noeltock Oct 13, 2026
50be0e1
docs: align Node and Wesper requirements with current source
noeltock Oct 13, 2026
f79d658
test: run benchmark and tooling tests outside npm test
noeltock Oct 13, 2026
4762b3b
docs: cover native themes and the owned site lifecycle
noeltock Oct 13, 2026
e5864bb
docs: clarify theme and site proof inputs
noeltock Oct 13, 2026
6ec7d29
docs: reflect the separate repository tooling test command
noeltock Oct 13, 2026
681a1c9
test: remove redundant proof and plugin tests
noeltock Oct 13, 2026
5369ebe
test: remove redundant authoring tests and fix weak assertions
noeltock Oct 13, 2026
798c684
test: remove redundant site and theme tests
noeltock Oct 13, 2026
2931f93
test: remove redundant core and CLI tests
noeltock Oct 13, 2026
11dd2ab
ci: retain separate tooling checks for release candidates
noeltock Oct 13, 2026
60f539e
release: prepare 0.9.11 with native theme and site workflows
noeltock Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ jobs:
- run: npm ci --engine-strict
- name: Verify documentation and skill CLI contracts
run: npx --no-install vitest run dev/test/skill.test.ts dev/test/node-support.test.ts dev/test/cli.test.ts
- name: Verify benchmark and evidence tooling
run: npm run test:tools
- run: npm run check:private
- name: Check touched documentation whitespace and local links
env:
Expand Down Expand Up @@ -191,6 +193,9 @@ jobs:
- run: npm ci --engine-strict
- name: Verify
run: npm run verify
- name: Verify benchmark and evidence tooling
if: matrix.node == '22.13.0'
run: npm run test:tools
- name: Smoke the packed CLI in a clean engine-strict consumer
run: npm run smoke:package
- name: Reject unsupported Node before packed CLI dependencies load
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
# and changes pack --json shape — we support both, but stay on the proven line).
- run: npm install -g npm@11
- run: npm ci --engine-strict
- name: Verify benchmark and evidence tooling
run: npm run test:tools
- run: npx --no-install playwright install --with-deps chromium
- name: Collect native WordPress Heading control
env:
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 0.9.11 — 2026-10-13

- Add block-theme folder validation, in-place markup repair, WordPress 7.1 theme.json schema checks and local part/pattern/preset references.
- Align native attribute order with WordPress server definitions; send unknown-block warnings to stderr and preserve native Accordion icon placement.
- Compile caller-authored whole-site intent into a local block theme and page bundle, with explicit media, font and synced-pattern bindings.
- Add source/target-bound site planning, confirmed owned apply with durable journals, read-only drift and staged pull of saved template/part edits. Theme deployment and activation remain separate.
- Add mounted-editor theme save proof and controlled whole-site proof. Save persistence does not establish visual fidelity.
- Update Wesper to 0.5.0 and require Node.js ^22.13.0 || >=24.0.0, dropping Node 20 support.
- Preserve unmanaged page fields and native publication dates on apply, allow pending first-install pattern media, canonicalise site identity and reduce repeated compilation, snapshots and source scans.
- Document current theme/site routes and guarded manual interrupted-apply recovery; remove the public proof failure-seeding option and fixed five-page fixture requirement.
- Remove redundant tests and run benchmark/evidence tooling separately, with required CI and release coverage.

## 0.9.10 — 2026-10-04

- Clarify native intent styling versus HTML/CSS conversion, with runnable native-attribute, headerless-table and sequential library examples. Verified theme layout contracts can override generic block-mapping defaults.
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Block Runner

Turn HTML into editable WordPress blocks, or generate the source for a reusable registered block.
Turn HTML into editable WordPress blocks, generate reusable block source, or compile a native block theme and site.

[![npm version](https://img.shields.io/npm/v/block-runner.svg)](https://www.npmjs.com/package/block-runner)
[![npm downloads](https://img.shields.io/npm/dm/block-runner.svg)](https://www.npmjs.com/package/block-runner)
Expand All @@ -9,7 +9,9 @@ Turn HTML into editable WordPress blocks, or generate the source for a reusable

![Block Runner driven from Claude Code: preview the registered block, confirm, write it into the plugin](https://cdn.jsdelivr.net/gh/humanmade/block-runner@main/demo/demo.gif)

Block Runner is an open-source CLI and JavaScript library from Human Made. It converts supported HTML into native Gutenberg blocks and checks the generated markup with the Gutenberg packages used by the WordPress editor. It can also generate a static registered block: source files you build into a plugin, with native editable blocks inside.
Block Runner is an open-source CLI and JavaScript library from Human Made. It converts supported HTML into native Gutenberg blocks and checks the generated markup with the Gutenberg packages used by the WordPress editor. It can also generate a static registered block: source files you build into a plugin, with native editable blocks inside. Whole-site intent produces a block theme and page content, with separate planning and confirmed apply to WordPress.

Theme-folder and whole-site workflows, Wesper 0.5.0 and the Node 22.13/24 floor were introduced in 0.9.11. See the [release notes](CHANGELOG.md#0911--2026-10-13) and check your installed version before using these routes.

**The package makes no AI model calls.** Built-in rules can convert HTML on their own. An external agent can interpret a design and give Block Runner a structured plan to assemble or compile. The optional skill provides instructions for that agent.

Expand Down Expand Up @@ -48,6 +50,8 @@ The JSON report includes `ok`, block counts, warnings and source locations. Revi
| Build page content from an agent's block tree | CLI `assemble` | Gutenberg markup, assembled and validated |
| Create a reusable named block from a design | `author` | A plan and generated block source; preview and confirm before writing |
| Check or repair existing block markup | `validate` / `fix` | A report or canonicalised markup |
| Check or repair a block theme | `validate <theme-dir>` / `fix <theme-dir> --write` | Folder, schema and reference checks; eligible markup repairs |
| Compile and manage a native whole site | `site compile` / `plan` / `apply` / `drift` / `pull` | Local theme bundle, reviewed plan, owned WordPress changes and staged theme edits |

For a block theme, pass its directory to check each discovered template, part and pattern. Pattern
PHP metadata headers are preserved and excluded from block counts; patterns with PHP in the body
Expand All @@ -61,6 +65,8 @@ npx --no-install block-runner validate ./my-theme
npx --no-install block-runner fix ./my-theme --write
```

For whole sites, follow the [theme and site guide](skills/block-runner/references/SITE.md). Compile stays local; apply requires the exact reviewed confirmation and a durable journal. Deploy and activate the compiled theme separately before planning.

For straightforward HTML, start with `convert`. For a design that needs interpretation, an agent can choose the block structure and send it to CLI `assemble`. Both page-content routes finish with media resolution, theme-token handling and validation. Neither creates a registered block's source files.

### Registered-block authoring
Expand Down Expand Up @@ -105,7 +111,7 @@ See [skill installation options](https://github.com/humanmade/block-runner/blob/

- Supported HTML becomes native blocks. Unsupported structures can remain as Custom HTML, with warnings; `--strict` fails on fallback blocks and unresolved media.
- Media IDs can come from a supplied map, WP-CLI or REST. Collecting site context or using WP-CLI resolution needs a site and the selected external tooling.
- Styles can map to existing theme presets, native block attributes or supported CSS. Block Runner does not rewrite `theme.json`.
- Styles can map to existing theme presets, native block attributes or supported CSS. Conversion and registered-block authoring do not rewrite the destination's `theme.json`; site compilation emits the caller's explicit local theme configuration.
- Registered-block generation produces static source. It does not generate arbitrary PHP renderers, custom field editors or JavaScript interactions.

Use the original design HTML rather than scraped frontend markup. **Valid markup does not prove visual fidelity or compatibility with every site.** Inspect warnings and verify the result in its target WordPress environment. The [construction guide](https://github.com/humanmade/block-runner/blob/main/skills/block-runner/references/CONSTRUCTION-PATTERNS.md) explains other approaches when the static generator does not fit.
Expand Down Expand Up @@ -142,6 +148,7 @@ These are page-content conversion results, not measurements of registered-block
| --- | --- |
| Commands, flags, exit codes and CI usage | [CLI reference](https://github.com/humanmade/block-runner/blob/main/docs/reference.md#cli) |
| Reusable block delivery | [Source-to-ZIP walkthrough](https://github.com/humanmade/block-runner/blob/main/skills/block-runner/references/AUTHORING.md#shipped-notice-source-to-standalone-zip) |
| Theme folders, whole-site intent and confirmed apply | [Theme and site guide](skills/block-runner/references/SITE.md) |
| WordPress proof requirements and synced patterns | [Verification profiles](https://github.com/humanmade/block-runner/blob/main/docs/reference.md#wordpress-proof-requirements) and [pattern overrides](https://github.com/humanmade/block-runner/blob/main/docs/reference.md#synced-pattern-overrides-wordpress-71) |
| Library contracts and regeneration | [API reference](https://github.com/humanmade/block-runner/blob/main/docs/reference.md#library) |
| Media, theme tokens and Wesper context | [Configuration](https://github.com/humanmade/block-runner/blob/main/docs/reference.md#configuration) and [media resolution](https://github.com/humanmade/block-runner/blob/main/docs/reference.md#media-resolution) |
Expand All @@ -150,7 +157,7 @@ These are page-content conversion results, not measurements of registered-block
| Custom HTML conversion rules | [Runnable extension example](https://github.com/humanmade/block-runner/blob/main/docs/extending.md) |
| Repository checks and benchmarks | [Development guide](https://github.com/humanmade/block-runner/blob/main/docs/development.md) |

The bundled `context` command uses Wesper **0.4.1**. See the [site-context reference](https://github.com/humanmade/block-runner/blob/main/docs/reference.md#site-context-from-wesper) for what the manifest supplies and its limitations.
The bundled `context` command uses Wesper **0.5.0**. See the [site-context reference](https://github.com/humanmade/block-runner/blob/main/docs/reference.md#site-context-from-wesper) for what the manifest supplies and its limitations.

For a real WordPress Site Editor save check of a local block theme, run `block-runner proof --theme <theme-directory> --wp-env-config <wp-env.json>`. The config supplies PHP, a fixed local port and local dependency plugins; `autoPort` is refused. The proof verifies its browser target, then saves each template and part from the mounted editor state and compares a fresh REST read with the pinned source.

Expand Down
16 changes: 0 additions & 16 deletions dev/test/author.diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,6 @@ function sourceRef(html: string, tag: string): string {
}

describe('author diagnostics', () => {
it('locates stale proposal references without treating their old hash as a current binding', async () => {
const html = '<p>Hello</p>';
const report = await author(html, {
sourcePath: '/tmp/block-runner-diagnostic/stale.html',
author: { name: 'example/diagnostic-stale' },
proposal: { structure: [{ id: 'copy', block: 'core/paragraph', sourceRef: `${'0'.repeat(64)}:0-12` }] },
});
const item = report.items[0]!;
expect(report.ok).toBe(false);
expect(item).toMatchObject({ code: 'stale-proposal-source-ref', source: { path: '/tmp/block-runner-diagnostic/stale.html' } });
expect(item.source).not.toHaveProperty('offset');
expect(item.reason).not.toMatch(/offset|stale\.html:/i);
expect(item.details).toMatchObject({ sourceRef: `${'0'.repeat(64)}:0-12`, node: 'copy', action: 'refresh-source-analysis', referenceVerified: false });
expect(JSON.parse(JSON.stringify(report)).items[0].reason).toMatch(/stale, missing, or belongs to another source/i);
});

it('reports deterministic coverage differences and resolves them when the canonical plan is restored', async () => {
const html = '<p class="notice">Hello</p>';
const options = { author: { name: 'example/diagnostic-coverage', styles: { mode: 'css' as const, css: '.notice { color: red; }' } } };
Expand Down
17 changes: 0 additions & 17 deletions dev/test/author.native-style-adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,23 +230,6 @@ describe('native source-style adapters', () => {
expect(() => compileRegisteredBlock(forged)).toThrow(/without overriding generated image CSS/);
});

it('locates an unsupported relationship on its matched unwrapped anchor, not the first binding', async () => {
const html = '<div> <a class="move" href="/go">Go</a></div>';
const report = await author(html, {
sourcePath: '/Users/warden/Library/Application Support/Block Runner/previews/2026-09-05/export/long-project/preview.html',
author: { name: 'example/unwrapped-button', styles: { mode: 'css', css: 'div .move:hover { color: red; }' } },
proposal: { structure: [{ id: 'section', block: 'core/group', sourceRef: refs(html)('div') }, { id: 'button', block: 'core/button', sourceRef: refs(html)('a') }] },
});
const item = report.items.find((candidate) => candidate.code === 'invalid-proposal-relationship')!;
expect(report.package).toBeUndefined();
expect(item.source).toMatchObject({ path: '/Users/warden/Library/Application Support/Block Runner/previews/2026-09-05/export/long-project/preview.html', offset: 21, htmlLine: 1, htmlColumn: 22 });
expect(item.details).toMatchObject({
sourceRef: refs(html)('a'), node: 'button', selectedParent: null,
requiredRelationship: { parentBlock: 'core/buttons', relationship: 'direct-child' },
action: 'place-core-button-under-core-buttons', stage: 'final-proposal',
});
});

it('retains every matched unwrapped anchor for an unsupported relationship', async () => {
const html = '<div> <a class="move" href="/go">Go</a> <a class="move" href="/next">Next</a></div>';
const report = await author(html, {
Expand Down
3 changes: 2 additions & 1 deletion dev/test/author.proposal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,10 @@ describe('author proposal boundary', () => {
expect(reason).toContain('proposal sourceRef');
expect(reason).toMatch(/stale|another source/i);
expect(reason).not.toContain('(offset 12)');
expect(report.items[0]).toMatchObject({ code: 'stale-proposal-source-ref', source: { path: '/design/multiline.html' }, details: { sourceRef: foreignRef, referenceVerified: false } });
expect(report.items[0]).toMatchObject({ code: 'stale-proposal-source-ref', source: { path: '/design/multiline.html' }, details: { sourceRef: foreignRef, node: 'copy', action: 'refresh-source-analysis', referenceVerified: false } });
expect(report.items[0]!.source).not.toHaveProperty('offset');
expect(report.items[0]!.source).not.toHaveProperty('htmlLine');
expect(JSON.parse(JSON.stringify(report)).items[0].reason).toMatch(/stale, missing, or belongs to another source/i);
});

it('locates a source-decision stale reference from a foreign source hash', async () => {
Expand Down
9 changes: 4 additions & 5 deletions dev/test/author.style-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,17 @@ describe('target style decisions survive the public authoring workflow', () => {
expect(result.ok, JSON.stringify(result.items)).toBe(true);
const plan = result.package!.canonicalPlan!;
expect(plan.structure[0]!.attributes).toMatchObject({ style: { '@mobile': { typography: { fontSize: '16px' } } } });
expect(plan.coverage!.styles).toContainEqual(expect.objectContaining({
property: 'font-size', value: '16px', outcome: 'native', responsive: 'mobile', node: 'source.0', atRules: ['@media (width <= 480px)'],
}));
expect(result.package!.files['style.scss']).not.toContain('@media (width <= 480px)');
expect(() => validateCoverageFulfillment(plan)).not.toThrow();
const repeated = await author(source, { ...input, plan });
expect(repeated.ok, JSON.stringify(repeated.items)).toBe(true);
expect(repeated.package!.files).toEqual(result.package!.files);
});

it.each([
['font-size', '2rem', { typography: { fontSizes: [{ slug: 'large', size: '2rem' }] } }],
['font-family', 'Georgia, serif', { typography: { fontFamilies: [{ slug: 'serif', fontFamily: 'Georgia, serif' }] } }],
['margin-top', '1rem', { spacing: { spacingSizes: [{ slug: '40', size: '1rem' }] } }],
])('round-trips a matching %s preset, not only colors', async (property, value, targetSettings) => {
const input: AuthorOptions = { author: { name: 'example/style-roundtrip', styles: {
Expand Down Expand Up @@ -94,8 +96,6 @@ describe('target style decisions survive the public authoring workflow', () => {

it.each([
['<p class="card" style="font-size:20px">Keep this text</p>', '@media (width <= 480px) { .card { font-size:12px; } }'],
['<p class="card other">Keep this text</p>', '.other { font-size:20px!important; } @media (width <= 480px) { .card { font-size:12px; } }'],
['<p class="card">Keep this text</p>', 'p.card { font:20px sans-serif!important; } @media (width <= 480px) { .card { font-size:12px; } }'],
])('keeps responsive source CSS when native priority would change the cascade: %s', async (source, css) => {
const result = await author(source, { author: { name: 'example/style-roundtrip', styles: {
mode: 'css', css, context: { theme: { settings: { viewport: { mobile: '480px', tablet: '782px' } } } },
Expand All @@ -111,7 +111,6 @@ describe('target style decisions survive the public authoring workflow', () => {

it.each([
'@media (min-width: 600px) { .card { font-size: 16px; } }',
'.card:hover { font-size: 16px; }',
])('keeps base CSS out of inline attributes when it would defeat the residual rule: %s', async (conditional) => {
const result = await author('<h2 class="card">Keep this text</h2>', { author: {
name: 'example/style-roundtrip', styles: { mode: 'css', css: `.card { font-size: 32px; } ${conditional}` },
Expand Down
Loading
Loading