Context
WordPress developers — and the coding agents working alongside them — need a fast, local loop to find and fix accessibility (WCAG2AA) problems in a plugin or theme. pa11y is already the org's established accessibility engine (installed and configured by the setup/pa11y scaffold), but there is no local, agent-driven command that turns pa11y's output into something an agent can act on and trace back to source.
This task adds a wp-tooling a11y command that runs the consumer-installed pa11y-ci command-line tool — pa11y's local, multi-URL runner (the -ci is only the package name; running it starts no CI and reads no CI results, it just runs the pa11y checks in a headless browser) — and hands back its accessibility results as a normalized report, plus a companion Claude Code accessibility skill that maps each violation's DOM selector to the template, block, or PHP that rendered it and fixes it with consent. Running the command gives you pa11y results locally, on demand — no separate browser setup of our own.
Accessibility is a rendered-DOM concern, so this stays pure Node reusing pa11y — no WordPress ability, no MCP. The selector→source mapping is done by grepping the repo the agent already has open; the WordPress runtime is never consulted.
Expected Outcome
A wp-tooling a11y command (a library core plus a thin CLI adapter) plus a scaffolded accessibility skill. pa11y-ci is installed into the consumer by the existing setup/pa11y scaffold — it is never a dependency of @rtcamp/wp-tooling, preserving the zero-runtime-dependencies rule.
src/a11y/
errors.js RunnerError { code, ...details } for the runner's failure modes
resolve-bin.js locate the consumer-installed pa11y-ci (local / hoisted / npx) + version probe
urls.js resolve URLs from --url flags or the .pa11yci.json urls[] list
normalize.js pa11y-ci --json → a stable normalized report (pure, unit-testable)
run.js runA11y() library core + runCli() CLI adapter
index.js barrel exposed as @rtcamp/wp-tooling/a11y
src/cli/commands/a11y.js { name, summary, run } — auto-discovered by the dispatcher
package.json add "./a11y" to the exports map
tests/a11y/ normalize + urls + resolve-bin + cli specs, with fixtures
skills/accessibility/SKILL.md + evals/evals.json
scaffolds/setup/claude-skills/ templates/accessibility-SKILL.md + accessibility-evals.json,
two files[] entries, and a description update
skills/README.md list the new skill in "What's here" + the copy snippets
CHANGELOG.md Unreleased entry
Normalized report shape (the contract the skill and tests depend on):
{
tool: 'pa11y-ci',
standard: 'WCAG2AA',
summary: { urls, violations, errors, warnings, notices, passedUrls },
results: [
{
url,
violations: [
{
id, // pa11y issue code, e.g. WCAG2AA.Principle1.Guideline1_1.1_1_1.H37
wcagCriterion, // parsed criterion, e.g. '1.1.1' (null for axe rule ids)
impact, // 'error' | 'warning' | 'notice'
runner, // 'htmlcs' | 'axe'
message,
selector, // CSS selector of the offending node
context, // the node's HTML snippet
domHints: { tagName, classList, idAttr, attrs }, // extracted for the grep-to-source step
},
],
},
],
}
CLI behaviour (wp-tooling a11y):
--url <url> — URL to scan; repeatable; overrides the .pa11yci.json url list.
--config <path> — path to the pa11y config (default .pa11yci.json).
--output <text|json> — output format (default text).
--dry-run — print the resolved binary, resolved URLs, and the exact command; run nothing.
- Exit codes:
0 clean · 1 run failure · 2 usage error or pa11y-ci missing · 3 violations found.
Implementation guidance
Runner
- A pure library function returns the normalized report; a
runCli(argv) adapter parses flags, supports --dry-run, and emits text or json. Invoke pa11y-ci through execFileSync with piped stdio and no shell.
pa11y-ci exits non-zero (typically 2) when it finds violations — treat that as a successful run and read the report from the error's stdout; only a run with no parseable report is a failure (EBINFAIL).
- Resolve the binary by walking up
node_modules/.bin, then fall back to npx --no-install so it never fetches from the network; use a --version probe to drive availability and the --dry-run output. When it is missing, surface the wp-tooling add setup/pa11y install hint.
- Parse the WCAG criterion from the HTMLCS code (
…1_1_1.H37 → 1.1.1); axe rule ids that carry no criterion map to null. Extract domHints from the context HTML so the skill can grep for source without re-parsing markup.
Skill
- Add
skills/accessibility/SKILL.md (frontmatter name + description) and skills/accessibility/evals/evals.json. The SKILL.md should plan-and-announce a TODO list first, then walk a numbered workflow, and close with a hard-rules section. tests/skills/evals-json.test.js auto-discovers the directory and enforces the eval schema, so the evals file is required.
- Skill workflow: check preconditions (
.pa11yci.json present and a reachable site) → run npx wp-tooling a11y --output json → triage by criterion and impact → map each selector to its source by grepping the repo (id or distinctive class → theme templates and block render.php / save.js; then literal attribute values; else the nearest wp-block-* ancestor → that block's source) → propose the minimal fix with consent → re-run to confirm → report file:line and the before/after delta.
- Evals (at least three, with unique numeric ids): a missing-
alt violation mapped to its template; pa11y-ci absent → surface wp-tooling add setup/pa11y rather than installing; a colour-contrast violation on a dynamic block mapped via its wp-block-* ancestor.
Scaffold wiring
- Extend the
setup/claude-skills scaffold so it also copies the accessibility skill into a consumer's .claude/skills/. Add two raw: true files[] entries — templates/accessibility-SKILL.md → {{skills_dir}}/accessibility/SKILL.md and templates/accessibility-evals.json → {{skills_dir}}/accessibility/evals/evals.json — add those two template files, and mention the new skill in the manifest description. Both the SKILL.md and the evals are shipped.
- Do not modify
setup/pa11y, the existing skill entries, or package.json files[].
Acceptance Criteria
Notes
- The skill ships both the SKILL.md and the evals into consumers. There is no packaging change —
skills/ stays out of package.json files[].
- A companion
performance skill (a Lighthouse lab runner, with an optional stitch to the wp-dev-tools field web-vitals and server telemetry) was discussed and is intentionally out of scope here — it belongs in a separate task.
- PR target:
release/v1.0.0. Branch: v1.0.0/task/a11y-skill. Commit subject: feat(a11y): add wp-tooling a11y runner and accessibility skill.
Context
WordPress developers — and the coding agents working alongside them — need a fast, local loop to find and fix accessibility (WCAG2AA) problems in a plugin or theme. pa11y is already the org's established accessibility engine (installed and configured by the
setup/pa11yscaffold), but there is no local, agent-driven command that turns pa11y's output into something an agent can act on and trace back to source.This task adds a
wp-tooling a11ycommand that runs the consumer-installedpa11y-cicommand-line tool — pa11y's local, multi-URL runner (the-ciis only the package name; running it starts no CI and reads no CI results, it just runs the pa11y checks in a headless browser) — and hands back its accessibility results as a normalized report, plus a companion Claude Code accessibility skill that maps each violation's DOM selector to the template, block, or PHP that rendered it and fixes it with consent. Running the command gives you pa11y results locally, on demand — no separate browser setup of our own.Accessibility is a rendered-DOM concern, so this stays pure Node reusing pa11y — no WordPress ability, no MCP. The selector→source mapping is done by grepping the repo the agent already has open; the WordPress runtime is never consulted.
Expected Outcome
A
wp-tooling a11ycommand (a library core plus a thin CLI adapter) plus a scaffoldedaccessibilityskill.pa11y-ciis installed into the consumer by the existingsetup/pa11yscaffold — it is never a dependency of@rtcamp/wp-tooling, preserving the zero-runtime-dependencies rule.Normalized report shape (the contract the skill and tests depend on):
CLI behaviour (
wp-tooling a11y):--url <url>— URL to scan; repeatable; overrides the.pa11yci.jsonurl list.--config <path>— path to the pa11y config (default.pa11yci.json).--output <text|json>— output format (defaulttext).--dry-run— print the resolved binary, resolved URLs, and the exact command; run nothing.0clean ·1run failure ·2usage error or pa11y-ci missing ·3violations found.Implementation guidance
Runner
runCli(argv)adapter parses flags, supports--dry-run, and emitstextorjson. Invokepa11y-cithroughexecFileSyncwith piped stdio and no shell.pa11y-ciexits non-zero (typically2) when it finds violations — treat that as a successful run and read the report from the error'sstdout; only a run with no parseable report is a failure (EBINFAIL).node_modules/.bin, then fall back tonpx --no-installso it never fetches from the network; use a--versionprobe to drive availability and the--dry-runoutput. When it is missing, surface thewp-tooling add setup/pa11yinstall hint.…1_1_1.H37→1.1.1); axe rule ids that carry no criterion map tonull. ExtractdomHintsfrom the context HTML so the skill can grep for source without re-parsing markup.Skill
skills/accessibility/SKILL.md(frontmattername+description) andskills/accessibility/evals/evals.json. The SKILL.md should plan-and-announce a TODO list first, then walk a numbered workflow, and close with a hard-rules section.tests/skills/evals-json.test.jsauto-discovers the directory and enforces the eval schema, so the evals file is required..pa11yci.jsonpresent and a reachable site) → runnpx wp-tooling a11y --output json→ triage by criterion and impact → map each selector to its source by grepping the repo (id or distinctive class → theme templates and blockrender.php/save.js; then literal attribute values; else the nearestwp-block-*ancestor → that block's source) → propose the minimal fix with consent → re-run to confirm → report file:line and the before/after delta.altviolation mapped to its template;pa11y-ciabsent → surfacewp-tooling add setup/pa11yrather than installing; a colour-contrast violation on a dynamic block mapped via itswp-block-*ancestor.Scaffold wiring
setup/claude-skillsscaffold so it also copies the accessibility skill into a consumer's.claude/skills/. Add tworaw: truefiles[]entries —templates/accessibility-SKILL.md→{{skills_dir}}/accessibility/SKILL.mdandtemplates/accessibility-evals.json→{{skills_dir}}/accessibility/evals/evals.json— add those two template files, and mention the new skill in the manifestdescription. Both the SKILL.md and the evals are shipped.setup/pa11y, the existing skill entries, orpackage.jsonfiles[].Acceptance Criteria
wp-tooling a11yruns the pa11y accessibility check locally (using thepa11y-cicommand-line tool) and emits the normalized report in bothtextandjson.wp-tooling add setup/pa11yhint is shown when it is missing;pa11y-ciis never added to@rtcamp/wp-toolingdependencies.--dry-runruns nothing.npm run lintis clean.accessibilityskill (SKILL.md + evals) is added and auto-validated bytests/skills/evals-json.test.js.setup/claude-skillsscaffold copies the skill (SKILL.md + evals), and the bundled scaffold-manifest tests pass.skills/README.mdlists the skill, and aCHANGELOG.mdentry is added under## Unreleased.Notes
skills/stays out ofpackage.jsonfiles[].performanceskill (a Lighthouse lab runner, with an optional stitch to thewp-dev-toolsfield web-vitals and server telemetry) was discussed and is intentionally out of scope here — it belongs in a separate task.release/v1.0.0. Branch:v1.0.0/task/a11y-skill. Commit subject:feat(a11y): add wp-tooling a11y runner and accessibility skill.