Abilities API Implement — WooCommerce extension playbook
This skill drives a WooCommerce extension from "no abilities registered" to "draft PR open, ability surface verified" in one pass. It consumes an audit doc produced upstream by wp-abilities-audit and walks through the phases: scaffold the registrar, land one commit per ability, run the integration harness, open the PR.
Hard handoff — this skill requires a valid audit doc and exits with an error if one cannot be found or parsed.
Relationship to the upstream skills
The portable parts of this playbook live in WordPress/agent-skills:
| Upstream skill |
What it owns |
wp-abilities-audit |
Produces the audit doc this skill consumes. Plugin-family detection, grouping heuristic, audit schema. |
wp-abilities-api |
Portable patterns — delegate-helper pattern, error-code vocabulary, input-schema gotchas (pagination key drift, schema defaults), plugin-family patterns. |
wp-abilities-verify |
Integration harness — wp-env enumerate / annotations / execute / permission-gate checks, audit-schema validation. |
This skill owns the WooCommerce-extension layer on top:
- Registrar scaffolding parameterized for a WooCommerce-extension's
namespace, autoload layout, test base class, and bootstrap entry point. Non-Woo plugin families (Jetpack-family, generic WordPress plugins) are out of scope for this playbook — use the upstream wp-abilities-api skill (WordPress/agent-skills) directly with the host project's own conventions.
- WooCommerce-extension primitives — namespace rules, MCP exposure choice,
the shared woocommerce category convention, the woocommerceabilitydefinition_classes loader path, required meta.mcp.public / meta.mcp.type defaults.
- One-atomic-commit-per-ability convention for clean review.
- PR template that respects the target repository's own
.github/pullrequesttemplate.md and provides the abilities-specific sections to fill in.
- Audit-precondition checks that catch the bugs we have repeatedly seen at
scaffold time (capability-gate drift, plugin-family mismatch, missing backing controller).
Overview
Given an audit doc, this skill produces:
- A new
Abilities_Registrar class under the extension's source tree.
- One atomic commit per registered ability (reference ability first, reads
next, writes last).
- Unit tests covering shape + permission + execute-path assertions.
- An integration harness run captured as a Markdown artifact.
- An open PR against the extension's repo.
The skill stops at push + PR. It does not post to issue trackers or chat. It does not attempt to resume from a partial run — if something breaks mid-run, fix up and re-invoke from the failing phase manually.
Loader-only — what you can rely on
This skill scaffolds the WC 10.9 AbilityDefinition + woocommerceabilitydefinitionclasses loader shape. On stores running WC < 10.9 the registrar silently no-ops — no polyfill, no fallback registration path. The structural gate is a classexists() check on \Automattic\WooCommerce\Internal\Abilities\AbilitiesLoader; the safety net during rollout is the per-plugin <pluginslug>abilities_enabled feature flag.
Read references/wc-1009-dependency.md before scaffolding — it covers why no polyfill ships, the lazy-autoload property that makes Domain classes safe to ship to WC < 10.9 stores, and the test-environment implications (CI matrices on WC < 10.9 skip cleanly via markTestSkipped).
Usage
Invoke from inside the extension's working tree:
/woocommerce:abilities-api-implement <path-to-audit-doc>
If no argument is passed, the skill auto-detects the most recent abilities-audit.md under plans/ (vault or extension-local).
Precondition — audit doc must be valid
Before any scaffolding, validate the audit doc produced by wp-abilities-audit.
Read references/audit-precondition.md now for the precondition checklist. It validates on top of — not in place of — wp-abilities-verify's canonical audit-schema validator. If the wp-abilities-verify skill from WordPress/agent-skills is installed, also read wp-abilities-verify/references/audit-schema-validation.md for the canonical schema checks; run those first, then the Woo-extension-specific checks in audit-precondition.md.
If the precondition check fails, STOP. Do not partially scaffold.
Plan of execution
The implementation follows a six-phase sequence. Each phase ends with one or more atomic commits.
Phase I — Scaffold the coordinator + Domain folder
Create:
AbilitiesRegistrar coordinator — init() with the feature-flag gate and the WC 10.9 loader gate, the empty ABILITYCLASSES const, the appendclasses() filter callback, the CATEGORYSLUG = 'woocommerce' constant, and the <CanCapHelper>() capability helper. The coordinator does NOT register the category — Woo Core owns the woocommerce slug (see woo-extension-primitives.md §5).
Domain/Abstract<Plugin>Ability — CATEGORYSLUG const (mirror of the registrar's) + delegatetorestcontroller() (used by Shape-2 reads in Phase III). Add the pagination helpers from paginated-output-envelope.md now if you already know the surface has list-shaped abilities, or defer them until the first list-shaped Domain class lands.
- Phase-I tests — six cases: feature-flag-off bail, loader-absent bail, loader-present filter wire,
append_classes() round-trip, capability helper, and init() idempotency under the $initialized static guard.
Wire Abilities_Registrar::init() into the extension's bootstrap.
Feature flag gate. The registrar's init() must short-circuit when the <pluginslug>abilities_enabled filter returns false (the default during rollout). This lets the registration scaffolding ship without committing to a final ability shape. The filter is per-site opt-in; flip the default to true once the surface is stable.
Read references/registrar-template.md now — it contains the WooCommerce-extension class template, test base class rules, feature-flag pattern, and bootstrap wiring locations. For the portable mechanics this file adapts (call-shape patterns, delegate-helper signature, error-code vocabulary), read wp-abilities-api's references/plugin-family-patterns.md, references/delegate-helper-pattern.md, and references/error-code-vocabulary.md first (if that skill is installed).
Read references/wc-1009-dependency.md now — it covers the loader-gate rationale: why the coordinator's wooabilitiesloaderavailable() check is the structural gate (and the feature flag is only the rollout safety net), why the ::class constants in ABILITYCLASSES don't autoload the Domain classes on WC < 10.9, and the markTestSkipped pattern for the per-ability tests in subsequent phases.
Read references/woo-extension-primitives.md now — it captures the WooCommerce-extension rules that the audit doc cannot encode by itself: ability namespace, the woocommerceabilitydefinition_classes loader, deprecated-MCP-endpoint exposure choice, required metadata defaults, and the shared woocommerce category convention (§5 — including the plugin-slug-as-category antipattern and the do-not-re-register rule). These rules apply to every ability registered in Phases II–IV.
Commit shape: feat(abilities): scaffold Abilities_Registrar coordinator + Domain base.
Phase II — Register the first ability (reference pattern)
Pick the simplest safe read — usually a zero-arg "get <thing>-summary" or "get <thing>-statuses" style ability that does not require constructing a WPRESTRequest. This commit establishes the Domain-class reference pattern the remaining abilities will copy: one Domain/<Name>.php file per ability, implementing AbilityDefinition with getname() / getregistrationargs() / execute() static methods, listed in AbilitiesRegistrar::ABILITY_CLASSES.
Read references/first-ability-template.md now — it walks through the registration helper, the execute callback with defensive guards, the dual-state return handling, and the two required tests (shape + permission).
Alternative backing controllers: if the audit's Notes for this ability mention an alternative backing controller (e.g. one controller in the admin namespace and another in a reports namespace with different output shapes), PAUSE and confirm with the user before scaffolding.
Commit shape: feat(abilities): add <plugin-slug>/<ability-name> ability.
Phase III — Register remaining reads
After ability #2 lands, pick the right path for each remaining read before extracting any helper.
Shape preference for the Woo-extension overlay:
| Shape |
Pattern |
When to use |
| 3 — Shared service |
Ability + REST + UI all call a shared service class (<PluginNamespace>\Internal\Service\<Thing>_Service — a sibling of the \Internal\Abilities namespace, not a child) |
Default. Mandatory for writes (Phase IV). The default for reads too: a shared domain/service helper consumes the same code path the UI and REST controller already use, and the ability layer curates output for agent consumption. |
| 2 — Delegate to REST |
Ability builds a WPRESTRequest and calls restdorequest() via the delegatetorest_controller helper |
Explicitly-reviewed escape hatch for low-risk reads. Acceptable only when the backing controller is verified side-effect-free AND the REST handler doesn't add transport artifacts (envelope shape, links, embedded hypermedia, header-derived fields) the ability would inherit unchanged AND request-context helpers (currentusercan paths, isadmin(), did_action, header-based behavior) the controller depends on behave the same outside a REST request lifecycle. |
| 1 — Re-implement |
Ability has its own SQL / validation |
Avoid. Drift is guaranteed. |
Why Shape 3 is the Woo-extension default (not "preferred"):
- REST output carries transport artifacts. Controllers shape their output for HTTP — envelope keys,
_links, embedded hypermedia, links to next/prev pages. The ability layer wants the curated payload, not the transport shape; Shape 3 lets the ability call the domain helper and assemble the agent-facing shape itself.
- Request-context helpers behave differently outside REST.
currentusercan checks behind nonce verification, isadmin() branches, didaction('restapiinit') guards, header-based capability resolution — all routinely pass under REST and break under CLI / cron / agent invocations. Shape 2 inherits all of those silently.
- The metric trap. Backing controllers routinely emit analytics events, fire WP hooks, or dispatch notifications. Every agent-driven Shape 2 invocation contaminates those counters — agents show up in dashboards as "humans hit this endpoint." Shape 3 lets the ability call the domain helper directly and emit its own ability-tagged side effects (or none).
- First-call REST bootstrap cost.
restdorequest() calls restgetserver(), which lazy-instantiates WPRESTServer and fires restapiinit once per request. Inside a REST request lifecycle the cost is amortized; outside it (CLI, cron, non-REST MCP transport, agent workflows) the first delegating ability call pays the full bootstrap.
Decision order at scaffold time:
- Is there already a domain/service helper for this operation? → Shape 3, call it.
- Does the audit's
side_effects field declare anything non-empty (telemetry hooks, audit logs, notifications, cache writes, lock acquisitions)? → Shape 3, extract a service if none exists.
- Is the ability predominantly invoked outside REST? → Shape 3.
- Does the REST handler emit transport artifacts or rely on request-context helpers (see "Why Shape 3 is the default" above)? → Shape 3.
- None of the above and the read is genuinely low-risk → Shape 2 is acceptable as an explicitly-reviewed escape hatch. Note the trade-off and the verified preconditions in the PR's "Notes for reviewers."
For Shape 2 in the loader-only pattern, delegatetorestcontroller() is a protected static method on Abstract<Plugin>Ability (the local abstract base — see registrar-template.md). Domain classes call it as self::delegatetorestcontroller(...).
For Shape 2, the canonical helper implementation, signature, dual-response- shape unwrap, and the two-exception rule for zero-arg abilities live upstream:
If the wp-abilities-api skill from WordPress/agent-skills is installed, read its references/delegate-helper-pattern.md now. Otherwise the template in references/registrar-template.md includes a self-contained helper that builds a WPRESTRequest, calls restdorequest(), and unwraps both WPRESTResponse and raw-array return shapes.
For pagination parameters, some backing controllers use a different key (e.g. pagesize instead of per_page):
If the wp-abilities-api skill is installed, read its references/input-schema-gotchas.md now (section: pagination key drift). Otherwise the detection heuristic is: grep the backing controller's getcollectionparams() (or equivalent) for pagesize, pagesize, or other non-perpage pagination keys, and translate at the ability boundary rather than propagating the non-standard key into the ability's input schema.
Commit shape:
- Service extraction (Shape 3):
refactor(<plugin>): extract <Thing>_Service for shared business logic.
- Helper extraction (Shape 2):
refactor(abilities): extract delegatetorest_controller helper.
- Each ability:
feat(abilities): add <plugin-slug>/<ability-name> ability.
Phase IV — Register writes
If the audit has any write abilities, land them after all reads are in. First-pass batches default to read-only. Adding a write widens the agent-facing action surface; the discipline below keeps batch scaffolding from doing so accidentally.
Explicit write gate — review all four before adding any write ability:
- Behavior. The operation is a genuine state transition (terminal close, refund, void, status change with downstream consequences) — not a mutating GET dressed as a POST and not a setter masquerading as a read. Cross-check against the audit doc's
intent and usecasefit fields.
- Permission gate. The cap is enforced at the canonical source recorded in the audit's
permission.source (the domain / service / admin-action layer, not just the REST controller's permissioncallback). The ability's permissioncallback consults that canonical source, not the REST callback by reflex. Cross-link upstream: wp-abilities-audit/references/capability-gate-tracing.md.
- Service path. Shape 3 is mandatory (Phase III table). A shared service class exists for the operation OR is extracted in a
refactor() commit landed before this write ability's commit.
- Annotations. Each value reflects the actual behavior, not a copy-paste from the read abilities —
readonly: false, destructive: <true|false> explicit, idempotent: <true|false> explicit. The verify skill's adversarial check will catch lies; better to set them honestly the first time.
If any of the four review steps surfaces an unknown, pause the write rather than guess. Writes are the most damaging surface to land sloppy.
Read references/write-ability-template.md now — it walks through the write-ability Domain-class shape (Shape 3 is mandatory — shared service, no HTTP-method-as-argument; never the abstract base's delegatetorest_controller() for writes), the required-input validation pattern, and the schema-defaults gotcha. For the portable input-schema and schema-defaults gotchas, cross-reference wp-abilities-api's references/input-schema-gotchas.md if installed.
Commit shape: feat(abilities): add <plugin-slug>/<ability-name> ability.
Phase V — Integration harness
Bring up wp-env, enumerate abilities, verify annotations, execute reads, exercise write input validation, confirm the permission gate. Capture output to a Markdown artifact for the PR.
Two verification levels. The smoke level catches bootstrap and gross-error regressions; the high-confidence level exercises each ability against the data shape it will see in production. Both go through the public ability boundary (wpgetability( $name )->execute( $input )), not the registrar's static methods.
- Level 1 — smoke execution (synthetic inputs). Confirm each read returns
OK or a vocabulary WPError; each write rejects missing-input with abilityinvalidinput or the plugin's <pluginslug>missing<field> code. Catches PHP fatals, un-bootstrapped services, registration failures, write input-validation gaps.
- Level 2 — high-confidence verification (representative seeded data). Preferred for every ability whose audit doc declares a non-null
seeddataneeds. Seed realistic plugin data per that field, call the ability with representative inputs (real ids, real slugs — not synthetic placeholders), and assert (a) the documented output shape, (b) the privacy contract (sensitive fields the ability doesn't promise must not appear), and (c) the permission behavior against real records (anon denied, subscriber denied, the intended role allowed). Level 2 is the one that catches wrong IDs returning the wrong record, cached sentinels being served, permission gates passing on synthetic IDs but not on real ones, filtered-vs-unfiltered label drift, and curated-output drift from a controller field the ability inherits.
When seeddataneeds: null for an ability, Level 2 reports pending (ask the implementer) and proceeds — dovetails with the WARN-not-FAIL posture on the audit-schema implementation-readiness fields.
Use the wp-abilities-verify skill from WordPress/agent-skills — it is the portable, authoritative version of the integration harness. Its references/runtime-harness.md Check 3 owns the canonical Level 1 / Level 2 split; its other checks cover annotation correctness, permission roundtrip, schema lints, and the readonly-but-writes detector. The skill emits the artifact format this PR template renders.
Critical gate — the readonly-but-writes detector. Every readonly: true ability in the audit must come back clean from the verify skill's adversarial annotation-correctness check (see wp-abilities-verify/references/annotation-correctness.md). An ability that claims readonly: true while calling $wpdb->update, updateoption, wpinsert_*, or delegating via a non-GET HTTP method is the bug class that unit tests do not catch — the mock looks just like the real writer. No PASS is allowed without this detector clean. If the detector flags an ability, either fix the annotation (set readonly: false) or fix the callback to be genuinely read-only.
If wp-abilities-verify is not installed, fall back to a manual harness run: enumerate abilities via wp eval 'printr( arraykeys( wpgetabilities() ) );', spot-check annotations and execute behavior via WP-CLI, and capture both commands and output in the artifact file.
Ship the integration harness in the plugin's own test suite, not only as a one-shot PR artifact. The wp-abilities-verify run produces the artifact attached to the PR; the in-plugin PHPUnit tests prove the public contract on every CI run thereafter. Read references/test-the-public-contract.md now — it covers the six contract dimensions (permissions, inputs, annotations, curated output, privacy, execution behavior) the in-plugin tests MUST assert through wpgetability() and not by calling the registrar's static methods directly.
Artifact: plans/<YYYY-MM-DD>-<plugin-slug>-abilities-harness-output.md.
Commit shape: if the harness catches a bug, fix it in a small standalone commit (e.g. fix(abilities): ...). The harness-output file itself is NOT committed to the extension repo — it lives in the user's plans directory and is referenced in the PR body.
Phase V.5 — Measure the token cost
Every ability set should ship with a token-cost number. The provisional budget is ~2,000 tokens per plugin in MCP tools/list.
If the wp-abilities-measure skill is installed, run it against the freshly-registered ability set and capture two numbers:
tools/list token cost for the plugin's <plugin-slug>/* namespace (all abilities sharing the extension's ability-ID prefix; see woo-extension-primitives.md §1).
- Per-ability average.
Budget gate. If tools/list exceeds ~2,000 tokens for the plugin:
- PAUSE before opening the PR. The plugin's ability surface is too large.
- The right redesign axis is the projection layer (e.g. switch to a
STRAP / facade / nested-discovery shape for how the abilities surface to MCP), not the registration layer. Reshaping registrations first locks the surface into a granularity decision before measurement tells us which shape works.
- Re-measure after the projection-layer redesign. If still over budget,
that's a signal to ship a smaller MVP and defer abilities.
Fallback when wp-abilities-measure is not installed. Produce a manual estimate: dump the plugin's tools/list payload as JSON via wp eval and count tokens with a tokenizer of your choice. Mark the number as provisional in the PR body so a reviewer knows the measurement floor.
The token cost number is required in the PR body's "Coverage numbers" section.
Phase VI — Push and open PR
Open the PR against the extension's default branch.
Read references/pr-template.md now — it contains the parameterized PR body template plus the rule for reading the extension repo's own .github/pullrequesttemplate.md first and filling its required sections rather than overwriting them.
Phase VI.1 — Drop the AGENTS.md drift guard
Drop one sentence into the extension's AGENTS.md (under an existing "Working in this area" or "When changing X" section, or create one if none exists):
When you change the code path behind a registered ability, audit the
registration for required updates (annotations, schema, description).
This is the low-cost human-loop guard against silent drift between the ability and its backing service / REST controller. Land it in its own commit:
chore(agents): note ability-registration audit when changing controller code
Ambiguity handling — where to pause
Pause and ask the user before proceeding when:
- Any
proposed_abilities entry in the audit has backing: null.
- Any proposed ability's Notes mention an alternative backing controller.
- The audit's
plugin_family differs from the family auto-detected from
CWD.
- The capability gate declared in the audit differs from what the base REST
controller resolves to when re-read at scaffold time.
- The
routeregistrationline / callback_line hints from the audit do
not resolve to a matching (class, callback_name) pair in the current branch (hints drift; re-grep is authoritative).
Every entry in this list pauses for the operator — there is no safe auto-resolution path.
Error codes vocabulary
Every WP_Error returned from an execute callback must use the standardized vocabulary so agents can build retry logic that does not depend on string matching. The vocabulary is defined upstream:
If the wp-abilities-api skill from WordPress/agent-skills is installed, read its references/error-code-vocabulary.md now. Otherwise the minimum set of error codes used by the templates in this skill is:
<pluginslug>invalid_input — input failed validation (mirrors the
Abilities API's own abilityinvalidinput).
<pluginslug>missing_<field> — a specific required field was missing
when the callback ran without schema validation (e.g. invoked directly).
<pluginslug>not_found — a referenced resource (order, product, etc.)
did not exist.
<pluginslug>permission_denied — capability check failed after the
permission_callback already passed (rare; usually indicates a data-dependent permission gate the callback re-checks).
Align with the extension's existing WPError code conventions and text domain. If the extension already uses <pluginslug>rest or woocommerce<ext>rest_ codes elsewhere, mirror that prefix rather than inventing a new namespace.
Done criteria
Before declaring done, walk the checklist:
logical change per commit is.
the extension's AGENTS.md or composer.json/package.json scripts).
plans/<YYYY-MM-DD>-<plugin-slug>-abilities-harness-output.md (via wp-abilities-verify if installed, otherwise the manual fallback).
.github/pullrequesttemplate.md.
When the checklist is clean, report the PR URL to the user.