Skip to content

Commit 56ec027

Browse files
committed
Package agent skill across harnesses
1 parent 98e8c18 commit 56ec027

12 files changed

Lines changed: 994 additions & 75 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Changed
6+
7+
- **The bundled agent skill is now canonical and cross-harness.** Its source lives at
8+
`skills/block-runner/`, where the directory matches the frontmatter name, with the detailed
9+
guide under `references/`. `skill --install` copies the complete bundle to both
10+
`.agents/skills/block-runner/` and `.claude/skills/block-runner/` by default; `--scope`,
11+
`--target`, and `--dir` cover user-wide, single-harness, and arbitrary roots.
12+
- **Skill installation is inspectable and guarded.** `--dry-run` resolves destinations without
13+
writes, repeat installs are idempotent, managed files carry hashes, local changes are refused
14+
unless `--force` is explicit, and installed runtime commands are pinned to the installing
15+
package version while the explicit update command stays on `@latest`.
16+
17+
### Fixed
18+
19+
- The skill metadata now states its Node/shell/registry requirements, and the guide no longer
20+
describes an uncached `npx` run as fully offline or treats frontend-scraped HTML as supported
21+
authored input.
22+
- Pre-manifest 0.7.x installations now fail closed on their first upgrade. After review, rerun
23+
with `--force`; the obsolete root-level `GUIDE.md` is preserved rather than deleted.
24+
- Skill installs reject symlinked discovery roots, and default dual-target installs preflight
25+
ordinary write permissions across both destinations before changing either one.
26+
327
## 0.7.1
428

529
### Fixed

README.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ WordPress, editable in any editor with nothing proprietary to keep installed.
2929
npm install block-runner # requires Node 20+
3030
```
3131

32-
Then just ask your coding agent (Claude Code, Codex):
32+
Then just ask your coding agent:
3333

3434
> Use block-runner to convert this hero into a native Gutenberg block.
3535
@@ -49,20 +49,35 @@ block-runner convert hero.html --out hero.blocks.html
4949
Every run is checked against headless Gutenberg, so what comes back is guaranteed
5050
editor-valid, or Block Runner tells you exactly what wasn't and points at the line.
5151

52-
## Using this from an AI agent?
52+
## Using Block Runner from an AI agent
5353

5454
If you are the one deciding the structure, don't write HTML and convert it. Describe the
5555
structure as an intent tree and pipe it to `block-runner assemble` — deterministic code builds
5656
the markup, so it cannot come out invalid.
5757

58-
There is a guide covering that path, the block mappings, and how to check markup before saving
59-
it to a site:
58+
Block Runner ships a canonical skill in the open Agent Skills layout. Install it into the
59+
current project (ask the user before writing files):
6060

6161
```sh
62-
npx block-runner skill # print the guide — nothing is installed or written
63-
npx block-runner skill --install # install it as a skill (ask the user first)
62+
npx block-runner skill --install
6463
```
6564

65+
That installs the same skill to the cross-agent `.agents/skills/block-runner` location and
66+
Claude Code's `.claude/skills/block-runner` compatibility location. Project scope is the
67+
default so the instructions can travel with a repository. Use user scope or one target when
68+
that is what you want:
69+
70+
```sh
71+
npx block-runner skill --install --scope user
72+
npx block-runner skill --install --target agents
73+
npx block-runner skill --install --target claude
74+
```
75+
76+
For a harness with another skills directory, use `--dir <skills-directory>`. With no skill
77+
system, `npx block-runner skill` prints the complete harness-neutral guide to stdout and writes
78+
nothing. Project discovery is the most portable choice; user-wide discovery paths still vary
79+
between harnesses, so use `--dir` when a client documents a different global root.
80+
6681
## Benchmark
6782

6883
![Fidelity benchmark: raw Claude and Codex writing block markup themselves score 35 to 73, while Block Runner with the same models scores 93 to 99, across simple and complex blocks](https://cdn.jsdelivr.net/gh/humanmade/block-runner@main/assets/benchmark.png)
@@ -178,6 +193,25 @@ All commands:
178193
| `--wp-user <user>` | WordPress username for `rest` resolution. |
179194
| `--wp-app-password-env <name>` | Env var holding a WordPress application password. |
180195

196+
`skill --install` adds installation flags:
197+
198+
| Flag | Description |
199+
| --- | --- |
200+
| `--scope project\|user` | Install for the current project (default) or the current user. |
201+
| `--target all\|agents\|claude` | Install both discovery copies (default), only `.agents/skills`, or only `.claude/skills`. |
202+
| `--dir <path>` | Install under one explicit skills directory; cannot be combined with `--scope` or `--target`. |
203+
| `--dry-run` | Show resolved destinations without writing files. |
204+
| `--force` | Replace locally changed or unmanaged files at canonical bundle paths. |
205+
206+
Installed instructions pin runtime commands to the package version that installed them, while
207+
their explicit update command stays on `@latest`. Re-run
208+
`npx block-runner@latest skill --install` to update them. Existing local edits are refused
209+
unless `--force` is explicit.
210+
211+
An installation made by 0.7.x predates the managed manifest, so the first upgrade is
212+
deliberately refused as unmanaged. Review that copy, rerun once with `--force`, and remove the
213+
preserved root-level `GUIDE.md` after confirming the new `references/GUIDE.md` copy.
214+
181215
### Exit codes
182216

183217
- `0`: clean

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"LICENSE",
4949
"CHANGELOG.md",
5050
"examples",
51-
"skill",
51+
"skills",
5252
"scripts/prune-wp-vips.mjs"
5353
],
5454
"engines": {

scripts/check-private-refs.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ const output = execFileSync(
2121
);
2222

2323
const files = packFilePaths(output);
24+
const requiredPaths = [
25+
'skills/block-runner/SKILL.md',
26+
'skills/block-runner/references/GUIDE.md',
27+
];
28+
const missingPaths = requiredPaths.filter((file) => !files.includes(file));
29+
if (missingPaths.length > 0) {
30+
console.error(`Required public files are missing from the package:\n${missingPaths.join('\n')}`);
31+
process.exit(1);
32+
}
2433
const forbiddenPaths = [/^md\//, /^AGENTS\.md$/, /^CLAUDE\.md$/, /^\.env/];
2534
const forbiddenTerms = [
2635
'dogfood',

scripts/engines/engine-skill.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Engine Skill — measures the SHIPPED guide, not the tuned prompt.
33
*
4-
* Engine C sends the tuner-owned `INTENT_PROMPT`. This engine sends `skill/GUIDE.md` — the
4+
* Engine C sends the tuner-owned `INTENT_PROMPT`. This engine sends the canonical skill guide — the
55
* text we actually publish — plus the minimal task framing a real agent would supply. Same
66
* `realize()` on the other side, so the only variable is the instructions.
77
*
@@ -20,7 +20,7 @@ import type { ConvertOptions, BlockRunnerReport } from '../../src/types.js';
2020
import { realize } from './intent.js';
2121

2222
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
23-
const GUIDE = readFileSync(path.join(ROOT, 'skill', 'GUIDE.md'), 'utf8');
23+
const GUIDE = readFileSync(path.join(ROOT, 'skills', 'block-runner', 'references', 'GUIDE.md'), 'utf8');
2424

2525
// The framing an agent supplies around the guide when it has been handed a design to convert.
2626
// Deliberately thin: any lifting here is lifting the guide is not doing.
Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
---
22
name: block-runner
33
description: >-
4-
Produce, convert, and validate WordPress Gutenberg block markup so it lands in the editor as
5-
real, native, editable blocks instead of one frozen Custom HTML blob. Use whenever you are
6-
building WordPress page content or sections (hero, pricing table, FAQ, CTA band, feature
7-
grid), converting HTML or a design-tool export into blocks, or checking block markup before
8-
saving it to a site. Also use before any WordPress write that carries block markup. Runs
9-
offline and deterministically via `npx block-runner` — no API key. NOT for WordPress admin
10-
tasks, plugin or theme code, or non-WordPress HTML.
4+
Turn WordPress content or authored design HTML into valid, native, editable Gutenberg blocks.
5+
Use when creating WordPress page content or sections, converting authored HTML or a design-tool
6+
export into block markup, validating or repairing Gutenberg markup, or before writing block
7+
markup to WordPress. Do not use for general WordPress administration, plugin or theme code,
8+
frontend-scraped HTML, or non-WordPress HTML.
9+
license: GPL-2.0-or-later
10+
compatibility: Requires Node.js 20+ and shell access. An uncached npx run requires npm registry access.
1111
---
1212

1313
# Block Runner
1414

15-
Read `GUIDE.md` next to this file — it is the full contract and is the same guide shipped in
16-
the npm package.
15+
Read `references/GUIDE.md` for the full contract. It is the same guide shipped in the npm
16+
package.
1717

1818
## The short version
1919

@@ -23,7 +23,7 @@ Three paths. Pick by what you have:
2323
describing which blocks and how they nest) and pipe it to
2424
`npx -y block-runner@latest assemble - --json`. Deterministic code builds the markup, so it
2525
cannot come out invalid. This is the best path and the one to reach for by default.
26-
- **You have someone else's HTML**`npx -y block-runner@latest convert - --json`. The only
26+
- **You have authored source HTML**`npx -y block-runner@latest convert - --json`. The only
2727
path that carries CSS; use it when the styling matters (`--styling relaxed` is the default).
2828
- **You have block markup to check**`validate``fix``validate`. Never save markup that
2929
is still invalid after `fix`.
@@ -33,7 +33,8 @@ Three paths. Pick by what you have:
3333
- **Finish the job — the markup has to land somewhere.** Write it where the user asked; or
3434
offer to write it through a WordPress connection if one is available; or show it to them
3535
with the paste instruction (**Options ⋮ → Code editor**, or `Ctrl+Shift+Alt+M` — pasting
36-
into the *visual* editor produces a mess). Never leave it in a temp file. See `GUIDE.md` §5.
36+
into the *visual* editor produces a mess). Never leave it in a temp file. See
37+
`references/GUIDE.md` §5.
3738
- **Always pass `--json`.** Without it the report items are dropped and you will miss
3839
fallbacks, warnings, and source locations.
3940
- **Never hand-write `<!-- wp:... -->` markup.** That is how invalid output happens. Describe
@@ -47,4 +48,4 @@ Three paths. Pick by what you have:
4748
say so — never block the user on it.
4849

4950
Block structure rules, the full node schema, per-section mappings, token and media resolution,
50-
exit codes, and failure posture are all in `GUIDE.md`.
51+
exit codes, and failure posture are all in `references/GUIDE.md`.
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ gets it into the editor as real, native, editable blocks instead of one frozen H
66
This guide is harness-neutral. Read it and act on it directly, or install it as a skill —
77
see the end.
88

9-
Everything runs offline and deterministically. Block Runner never calls a model and never
10-
needs an API key. **You** are the model in this pipeline.
9+
Conversion, assembly, and validation run locally and deterministically. Block Runner never
10+
calls a model and never needs an API key. An uncached `npx` invocation still needs npm registry
11+
access to fetch the package. **You** are the model in this pipeline.
1112

1213
---
1314

@@ -16,7 +17,7 @@ needs an API key. **You** are the model in this pipeline.
1617
| You have | Use | Why |
1718
|---|---|---|
1819
| A design in your head, or HTML you are about to write | **`assemble`** | You describe the structure; Block Runner builds valid blocks from it. Best structural results. |
19-
| Someone else's HTML — a design tool export, a paste, scraped markup | **`convert`** | Rule-based translation of existing markup, and the only path that carries CSS. |
20+
| Authored source HTML — a design tool export, source file, or paste | **`convert`** | Rule-based translation of existing markup, and the only path that carries CSS. Do not use frontend-scraped render output. |
2021
| Block markup you already produced, before saving it to WordPress | **`validate`****`fix`****`validate`** | Proves the editor will accept it. |
2122

2223
The single most common mistake is reaching for `convert` when you were about to author the
@@ -275,12 +276,28 @@ harmless. Read results from stdout or `--json`. Users who run this often can
275276

276277
## 8. Installing this as a skill
277278

278-
If your harness supports skills, this guide can be installed as one:
279+
If your harness supports skills, install the canonical skill into the current project:
279280

280281
```bash
281282
npx -y block-runner@latest skill --install
282283
```
283284

285+
That writes the same skill to the cross-agent `.agents/skills/block-runner` location and to
286+
Claude Code's `.claude/skills/block-runner` compatibility location. Narrow it when needed:
287+
288+
```bash
289+
npx -y block-runner@latest skill --install --target agents
290+
npx -y block-runner@latest skill --install --target claude
291+
npx -y block-runner@latest skill --install --scope user
292+
npx -y block-runner@latest skill --install --dir .another-agent/skills
293+
npx -y block-runner@latest skill --install --dry-run
294+
```
295+
296+
Project discovery is the most portable choice. User-wide discovery paths still vary between
297+
harnesses, so use `--dir` when a client documents a different global skills root.
298+
The installer pins runtime examples to its own package version so the guide and CLI contract
299+
stay aligned. To update later, rerun `npx -y block-runner@latest skill --install`.
300+
284301
**Ask the user first.** This writes files into their project, which is their call, not yours.
285302
If they decline, or their harness has no skill system, nothing is lost — reading this guide is
286303
the same information. `npx -y block-runner@latest skill` prints it without installing anything.

src/cli.ts

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
#!/usr/bin/env node
22
import { createRequire } from 'node:module';
33
import { existsSync } from 'node:fs';
4-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
4+
import { readFile, writeFile } from 'node:fs/promises';
5+
import { homedir } from 'node:os';
56
import path from 'node:path';
67
import process from 'node:process';
7-
import { Command, CommanderError } from 'commander';
8+
import { Command, CommanderError, Option } from 'commander';
89
import fg from 'fast-glob';
910
import { canonicalize } from './gate/canonicalize.js';
1011
import { validate } from './gate/validate.js';
1112
import { convert } from './convert/assemble.js';
1213
import { realize } from './intent/index.js';
1314
import { loadConfig } from './config/load.js';
1415
import { collectSiteContext } from './context/run.js';
16+
import { installCanonicalSkill, readCanonicalSkillGuide, SkillScope, SkillTarget } from './skill.js';
1517
import { BlockRunnerReport, CommonOptions, HeadlessBootError } from './types.js';
1618

1719
const { version: packageVersion } = createRequire(import.meta.url)('../package.json') as {
@@ -38,6 +40,10 @@ interface ContextCliOptions {
3840
interface SkillCliOptions {
3941
install?: boolean;
4042
dir?: string;
43+
scope?: SkillScope;
44+
target?: SkillTarget;
45+
dryRun?: boolean;
46+
force?: boolean;
4147
}
4248

4349
const program = new Command();
@@ -205,26 +211,38 @@ program
205211
.command('skill')
206212
.description('Print or install the agent guide.')
207213
.option('--install', 'install the agent skill files')
208-
.option('--dir <path>', 'install root (default: .claude/skills)')
214+
.addOption(new Option('--scope <scope>', 'installation scope (default: project)').choices(['project', 'user']))
215+
.addOption(new Option('--target <target>', 'skill discovery target (default: all)').choices(['all', 'agents', 'claude']))
216+
.option('--dir <path>', 'install under an explicit skills directory')
217+
.option('--dry-run', 'show destinations without writing files')
218+
.option('--force', 'replace locally changed or unmanaged skill files')
209219
.action(async (options: SkillCliOptions) => {
210220
if (!options.install) {
211-
process.stdout.write(await readSkillSource('GUIDE.md'));
221+
if (options.dir || options.scope || options.target || options.dryRun || options.force) {
222+
program.error('error: --dir, --scope, --target, --dry-run, and --force require --install');
223+
}
224+
process.stdout.write(await readCanonicalSkillGuide());
212225
return;
213226
}
214227

215-
const skill = await readSkillSource('SKILL.md');
216-
const guide = await readSkillSource('GUIDE.md');
217-
const destination = path.resolve(options.dir ?? '.claude/skills', 'block-runner');
218-
await mkdir(destination, { recursive: true });
219-
220-
for (const [filename, content] of [
221-
['SKILL.md', skill],
222-
['GUIDE.md', guide],
223-
] as const) {
224-
const target = path.join(destination, filename);
225-
const status = existsSync(target) ? 'updated' : 'installed';
226-
await writeFile(target, content, 'utf8');
227-
console.log(`${status} ${target}`);
228+
const results = await installCanonicalSkill({
229+
cwd: process.cwd(),
230+
home: process.env.HOME || homedir(),
231+
packageVersion,
232+
directory: options.dir,
233+
scope: options.scope,
234+
target: options.target,
235+
dryRun: options.dryRun,
236+
force: options.force,
237+
});
238+
for (const result of results) {
239+
const status = result.dryRun && result.status !== 'unchanged'
240+
? `would ${result.status === 'installed' ? 'install' : 'update'}`
241+
: result.status;
242+
console.log(`${status} ${result.destination}`);
243+
for (const warning of result.warnings) {
244+
console.error(`warning: ${warning}`);
245+
}
228246
}
229247
});
230248

@@ -400,17 +418,6 @@ function emitHint(report: BlockRunnerReport): void {
400418
}
401419
}
402420

403-
async function readSkillSource(filename: 'SKILL.md' | 'GUIDE.md'): Promise<string> {
404-
const source = filename === 'GUIDE.md'
405-
? new URL('../skill/GUIDE.md', import.meta.url)
406-
: new URL('../skill/SKILL.md', import.meta.url);
407-
try {
408-
return await readFile(source, 'utf8');
409-
} catch {
410-
throw new Error(`skill source file is missing: ${source.pathname}`);
411-
}
412-
}
413-
414421
function formatTextReport(report: BlockRunnerReport): string {
415422
const status = report.ok ? 'ok' : 'problems found';
416423
const lines = [

0 commit comments

Comments
 (0)