Skip to content

wp-abilities-audit: add new skill for surveying a plugin's REST surface and producing a structured audit doc#46

Draft
rtio wants to merge 25 commits intoWordPress:trunkfrom
rtio:feat/abilities-audit-skill
Draft

wp-abilities-audit: add new skill for surveying a plugin's REST surface and producing a structured audit doc#46
rtio wants to merge 25 commits intoWordPress:trunkfrom
rtio:feat/abilities-audit-skill

Conversation

@rtio
Copy link
Copy Markdown

@rtio rtio commented Apr 30, 2026

Adds a new skill wp-abilities-audit that surveys a plugin's REST surface and produces a structured audit document proposing Abilities API registrations. The audit doc is a planning artifact — readable by humans, parseable by tooling — that captures controller inventory, capability gates, proposed ability shapes, excluded items, and surfaced gaps in one place.

Depends on #44 and #45. This branch stacks on top of both: it inherits the strategy-layer references (#44) and the mechanics-layer references (#45) that the new skill cross-links into. For a focused view of just this PR's 5 commits, see feat/abilities-api-mechanics-refs...feat/abilities-audit-skill on the fork.

What lands here

File Purpose
skills/wp-abilities-audit/SKILL.md The skill entrypoint. 7-step procedure: enumerate controllers, extract backing fields, confirm capability gates, propose abilities via semantic-intent grouping, surface gaps, write the doc, optionally designate a reference ability.
references/audit-schema.md Canonical schema. Required top-level fields (plugin, repo, branch_audited, audited_at, auditor, baseline_abilities, capability_gate, plus optional plugin_family). Proposed_abilities array shape. Two legal capability_gate forms (single string vs structured {read, write}). Backing: null semantics. Required prose sections. Includes a copy-pasteable minimal example.
references/capability-gate-tracing.md Mechanism A (direct: base controller calls current_user_can() once) and Mechanism B (post-type-backed: controllers extend WP_REST_Posts_Controller-family, permission resolves dynamically via core's map_meta_cap()). Tracing recipes and YAML representation for each.
references/controller-enumeration.md Two enumeration paths — glob for standard layouts, grep as universal fallback — with guidance on monorepos, inherited routes, and the exhaustiveness rule.
eval/scenarios/wp-abilities-audit.json Eval scenario covering a plugin with non-standard REST layout, post-type-backed capabilities, and a missing backing endpoint.

This PR's substantive content (commits on top of #45): 5 files, 756 added lines.

Verification

Reviewer notes

rtio added 4 commits April 30, 2026 14:06
Agents that conflate ability registration with consumer exposure end up
re-registering abilities every time the projection question reopens
(token budgets, MCP shape, Command Palette conventions). Treating
those as one decision couples a stable layer (capabilities) to a
volatile one (consumer constraints).

This reference captures a three-layer model: domain capability,
optional workflow, and projection. Adds the use-case-contract test
("would a human ever do this in admin?"), the "same code path as the
UI" rule, and a decision order (domain first, projection second,
workflow third). Worked example walks a generic Notifications plugin
through three projections off the same three domain registrations.
Abilities that re-implement business logic instead of consuming the
same code path as the UI / REST surface drift apart over time:
permissions diverge, validation rules drop, side-effects fire on
UI-driven writes but not ability-driven ones. None of those breaks
immediately; the divergence becomes discoverable only when an agent
invokes the ability in production.

This reference captures three execute-callback shapes (re-implement /
delegate / shared service), the metric-trap argument for preferring
the third shape on telemetry-emitting endpoints, the MCP exposure
rule (don't expose REST + ability for the same op to the same client),
and the AGENTS.md sync rule for keeping registrations in lockstep when
the underlying code path changes. Worked example walks a generic
plugin through extracting a service class that ability + REST + UI
all consume.
Plugin authors landing abilities on top of an existing REST surface
have to choose domain-layer granularity. The three observed shapes
(action-bundle behind a single ability, REST-atomization at one
ability per HTTP method, semantic-intent at one ability per
user-meaningful question) each solve a different problem, and the
naive default — atomize because the REST surface already does — leaks
HTTP plumbing into the agent's tool list.

This reference recommends semantic-intent at the domain layer:
abilities map to user-meaningful actions, filter parameters live in
input_schema, writes stay narrow at one state transition per ability.
Five rules cover read grouping, write narrowness, filter-vs-name
choice, zero-arg overview abilities, and the one-sentence
explainability test. Two worked examples (Jetpack Forms and a generic
Tickets plugin) show the heuristic in action without committing the
reader to a specific projection-layer pattern.
The procedure section previously jumped from "Confirm version
constraints" to "Find existing usage" without surfacing the design-time
decisions that should precede registration: which layer to register at
(domain vs projection) and how to choose granularity. Agents following
the procedure verbatim reach for syntax before reasoning about shape.

Adds a layer-model paragraph at the top of the procedure pointing at
domain-vs-projection, plus two cross-links in step 4 — to
grouping-heuristic for granularity decisions and shared-core-service
for the drift-prevention pattern (including the AGENTS.md sync rule).
rtio added 21 commits April 30, 2026 18:28
Five points where references/php-registration.md drifted from
wordpress-develop/src/wp-includes/abilities-api/class-wp-ability.php:

1. permission_callback was marked optional. Per prepare_properties() at
   class-wp-ability.php:287-292, it throws InvalidArgumentException when
   missing — required on every registration with no implicit default.

2. The skill placed `readonly` at meta.readonly. Core puts the three
   semantic annotations at meta.annotations.* (lines 38-51 + validation
   at 313-317). Updated the table to show meta.annotations.readonly,
   .destructive, .idempotent at the canonical path.

3. Two of the three annotations (destructive, idempotent) were missing
   entirely. All three are first-class core annotations seeded via
   $default_annotations at lines 326-336.

4. Required-vs-optional was not flagged on label, description, category,
   execute_callback, or permission_callback — all five throw on missing
   per lines 262-292. Added a "Required?" column.

5. The recommended-patterns line "Always include a permission_callback
   for abilities that modify data" framed it as a write-only practice.
   Core requires it for all registrations. Reworded.

Adjacent fix while in the file: the namespacing example said
`my-plugin:feature.edit` (colon-and-dot) but every other example in
this skill uses slash-separated `my-plugin/get-info`. Aligned.

Updated the example registration block to register a permission_callback
and all three meta.annotations keys, so the example matches the contract
the table now describes.

Refs WordPress#44 review-id 4207902432
The inclusion test in references/domain-vs-projection.md framed the
ability question as "would a human ever do this in admin?". That lens
encodes a Woo/Jetpack-implementer's bias and excludes a real and
common case: abilities scoped to public authed/unauthed users on the
site front end (storefront `get-my-orders`, events
`list-available-tickets`, profile `update-public-profile`), and
abilities invoked as part of a workflow chain by another plugin or an
agent rather than by direct human input at all.

Reframed the test as "would a human intentionally do this through a
supported UI or workflow?". That criterion:

- Keeps admin-side abilities qualifying (the original case).
- Adds public-facing UIs and end-user self-service flows.
- Covers workflow / chain-of-actions invocation paths where the human
  intent is upstream of the ability call.
- Preserves the exclusion of internal-only plumbing (cache
  invalidation, scheduler ticks, debug snapshots, lifecycle
  bookkeeping).

Also dropped the asymmetric-test sentence that read "anything that
isn't admin-meaningful almost certainly isn't ability-meaningful" —
the same point survives in the rewritten version without the
admin-only framing.

Refs WordPress#44 review-id 4207902432
Same admin-only blind spot as the inclusion test in
domain-vs-projection.md (fixed in the previous commit), surfacing in
the opening sentence of references/shared-core-service.md.

The lockstep rule applies wherever the ability mirrors a supported
human workflow — admin screen, public-facing UI, customer self-service
flow, or a plugin/agent-driven workflow — not only admin actions.

Refs WordPress#44 review-id 4207902432
…eric responses scenario

Worked example A in references/grouping-heuristic.md was anchored to a
specific Automattic plugin's source layout, including a hardcoded link
to a file path under Automattic/jetpack/projects/packages/forms/. Two
problems:

1. WordPress/agent-skills is a generic, plugin-agnostic skill repo per
   CONTRIBUTING.md. Worked examples should not lean on one canonical
   plugin family.
2. File-path links into a fast-moving codebase rot — a rename or
   restructure makes the reference broken or worse, misleading.

Worked example B was already generic (Tickets plugin); aligning A to
the same approach.

The semantic-intent table row now uses a generic `feedback/get-responses`
example. The worked example A reframes around a generic feedback or
form-response plugin with the same structural lessons (filters in
`input_schema`, one write for "modify a response", a zero-arg
dashboard-summary ability). The hardcoded canonical-file link is
removed entirely.

Refs WordPress#44 review-id 4207902432
…ern, not a real callable

The shared-core-service reference repeatedly wrote
`delegate_to_rest_controller(...)` in code-shaped backticks (table row
example, body prose at four locations, a rule-of-thumb bullet). Read in
isolation, that string looks like a callable a plugin author should be
able to find or import. It is not — at least not in PR-44's scope. The
canonical helper of that name lives in another reference that is the
subject of a separate forthcoming PR; PR-44 cannot point at it without
phantom-dependency.

Two changes:

1. Replaced every `delegate_to_rest_controller(...)` occurrence with
   prose ("the delegation pattern", "the delegate-to-REST-controller
   route") so the text describes a pattern rather than naming a
   non-existent function.

2. Added a "What the delegation pattern looks like" subsection between
   the three-shapes table and the metric-trap discussion, with a small
   inline example showing the actual mechanism: construct a
   WP_REST_Request, set params, dispatch via rest_do_request(), handle
   the error case, return data. Three core symbols cited
   (WP_REST_Request, rest_do_request, WP_Error) verified against
   wordpress-develop trunk; method calls (set_query_params, is_error,
   as_error, get_data) similarly verified.

The example is intentionally minimal and self-contained. It does not
forward-reference any helper that lives outside this PR. A `php -l`
smoke-test on the extracted block on PHP 7.2 returns no syntax errors.

Refs WordPress#44 review-id 4207902432
…fects

The shared-core-service reference centred its argument for service
extraction on a "metric trap" — telemetry double-counting under agent
traffic. A reviewer pointed out that telemetry is one of several side
effects a REST handler may emit, and that the doc was elevating it
disproportionately. Plugin authors may track usage at any layer, and
the structural lesson — "do not let an ability trigger UI-scoped side
effects on agent invocations" — applies to audit logs, custom-event
hooks, notifications, cache invalidation, and lock acquisition just as
much as to telemetry.

Restructured the section accordingly:

- Renamed the H2 from "The metric trap" to "Side effects on the
  existing REST path".
- Replaced the telemetry-only intro paragraph with a five-bullet list
  of side-effect categories: usage telemetry, audit logs, custom-event
  hooks, email/notification dispatch, cache/schedule/lock side
  effects. Telemetry remains as the lead bullet because metric
  double-counting is the only failure mode in this set that is
  invisible by default — the system "works" while dashboards drift —
  which is worth one sentence even after demotion.
- Generalised the "fix" bullets ("emit telemetry" → "emit its side
  effects").
- Replaced the `my_plugin_track_event(...)` calls in both worked-example
  PHP blocks with `do_action( 'my_plugin/things_listed', $rows )`, a
  generic WP-idiomatic side-effect that does not bake telemetry into
  the example. Comments updated to "Side effects (audit log, hooks,
  notifications) live on the REST adapter."
- Generalised the rule-of-thumb bullets and the closing
  override-paragraph from telemetry-specific to side-effect-general
  framing.
- Updated the two stale pointers ("the metric trap that makes that
  distinction matter", 'read "the metric trap" below first') to point
  at the renamed section.

Net effect: telemetry is mentioned three times in the file (one bullet
in the side-effects list with the silent-failure callout, one
parenthetical in the worked-example closing, one parenthetical in the
rule-of-thumb), down from six. The original concern Darren raised —
telemetry as the centerpiece — is addressed; the genuinely-specific
risk it represents is preserved as one bullet of context.

php -l on both worked-example blocks returns no syntax errors on
PHP 7.2.

Refs WordPress#44 review-id 4207902432
The skill's php-registration reference covers `meta.show_in_rest` and the
three `meta.annotations` keys, but does not cover `meta.mcp.public` or
`meta.mcp.type`. The bundled WordPress MCP adapter (the package WP plugins
pull in for MCP exposure) reads those two keys to decide which abilities
to surface and how to project them. An ability registered with
`show_in_rest => true` but no `meta.mcp` block is technically valid yet
invisible to MCP clients connecting through that adapter — which is the
default discovery path for agents.

Surfaced as review feedback on a WooPayments pilot PR: a registration
that followed the skill exactly was discoverable on the abilities REST
namespace but missing from the MCP tool list. The reviewer's note was
"we should default all our registered abilities to use this unless we
know for sure it shouldn't be exposed."

Add both keys to the registration arguments table with their defaults
and constraints (mcp.public is bool default false; mcp.type is one of
'tool'/'resource'/'prompt' default 'tool', with silent coercion to
'tool' for any value outside the enum). Add a section that distinguishes
the abilities-REST namespace contract (`show_in_rest` → `wp-abilities/v1`)
from the MCP adapter projection (`mcp.public` → adapter's default MCP
server) so plugin authors don't conflate them. Update the canonical
example so the recommended starting shape opts the ability into MCP
discovery.

Verified against the MCP adapter package source: `mcp.public` read at
McpAbilityHelperTrait.php lines 37 and 61; `mcp.type` read at line 75
with the enum guard at line 78. PHP example block re-validated with
`php -l`.

Refs WordPress#44
Refs Automattic/woocommerce-payments#11679
…nded

The reference's table previously listed all three `meta.annotations`
keys (`readonly`, `destructive`, `idempotent`) as "Optional (default
null)". The post-table paragraph said plugins were "expected to populate
them honestly" but did not explain why a `null` annotation is materially
worse than a populated one.

Observed downstream consequence: an agent following the skill registered
an ability and shipped it without any annotations. The reviewer flagged
the omission. The skill teaches the keys exist and that they are hints
for tooling, but the cell-level "Optional" framing licensed the agent's
shortcut. The structural fix is to escalate the tone.

Reword the three table cells from "Optional" to "Strongly recommended"
while keeping the technically accurate "(default null)" footnote so
readers don't misread this as a core-enforced requirement. Expand the
post-table paragraph to spell out what `readonly: null` actually
broadcasts — "behavior unknown" — and why that is a worse signal than
either `true` or `false`. Strengthen the recommended-patterns bullet
along the same axis: explicit cost (three lines) vs. cost of omission
(opaque safety surface).

Verified core behavior at class-wp-ability.php lines 38-51 (the
`$default_annotations` array seeds all three keys to `null`) — the
table's "default null" claim still holds against trunk.

Refs WordPress#44
Refs Automattic/woocommerce-payments#11679
…effect

The "Side effects on the existing REST path" section enumerated five
semantic side effects of routing an ability through `rest_do_request()`
(telemetry, audit logs, custom-event hooks, notifications, cache/lock
side effects) but missed a category that is mechanically different but
just as important: performance.

`rest_do_request()` calls `rest_get_server()`, which is a lazy singleton
that fires `rest_api_init` the first time it's instantiated in a request
lifecycle. Inside an HTTP REST request that cost is paid before the
abilities layer ever runs. Outside one — CLI invocations, cron jobs,
agent runs hitting an ability through a non-REST MCP transport — the
*first* delegating ability call pays it. Every plugin's
`register_rest_route()` callback wires up at that point, which on a
cold path is measurably slow.

Surfaced as review feedback on a WooPayments pilot PR: the reviewer
flagged the delegation pattern with the question "what about using
List_Transactions::from_rest_request directly?" — the underlying
concern being that the ability is invoked from CLI, agents, and MCP
contexts where the REST bootstrap cost lands on the user.

Add a sixth bullet to the side-effects list, framed as "performance,
not semantics" so readers don't blur it together with the existing
five. Cite the lazy-instantiation guard explicitly so the cost-once-
per-request behavior is clear and readers don't over-correct toward
"never delegate." Add a corresponding rule-of-thumb row for "read
predominantly invoked outside REST" and update the override paragraph
so the non-REST-context rule joins side-effect and write as a reason
to extract a service even when delegation would otherwise be cheapest.

Verified against `wp-includes/rest-api.php` lines 619-651: the
`empty( $wp_rest_server )` guard at line 623, the instantiation at
line 635, and the `do_action( 'rest_api_init', $wp_rest_server )` at
line 647.

Refs WordPress#44
Refs Automattic/woocommerce-payments#11679
When adopting the Abilities API inside an existing plugin, the plugin's
architecture dictates how execute callbacks call into existing business
logic. Two patterns cover most real-world plugins, and picking the
wrong shape costs in tests, error-code semantics, and bootstrap
correctness.

Adds a reference covering Pattern A (shared API client, common in
plugins talking to a remote service) and Pattern B (zero-arg
controllers, common in plugins delegating primarily to WordPress core
mechanisms). Includes detection heuristics for each, minimal
skeletons for both, a hybrid-case note for plugins that legitimately
use both, an anti-pattern warning against inventing a fake API client
to fit Pattern A's helper shape, and a quick-pick table.
List-style read abilities backed by an existing REST controller repeat
the same four steps in every execute callback: validate the plugin is
initialized, build a WP_REST_Request from input, instantiate the
controller, and unwrap the response (which can be either a raw array
or a WP_REST_Response). At three or more abilities the boilerplate
dominates; copy-paste between callbacks is also where drift starts.

Adds a reference documenting the canonical delegate_to_rest_controller
helper: signature, the three guards every implementation needs
(class_exists on the controller, null-check on the shared dependency,
is_wp_error short-circuit before unwrap), the dual response-shape
unwrap, and explicit guidance on when NOT to use the helper (zero-arg
backings, non-REST services). Builds on plugin-family-patterns.md to
cover both shape variants.
Agents orchestrating multiple abilities need to know whether a failure
was caused by their input, by the plugin not being initialized, or by
a transient downstream error — the answer drives retry strategy. When
every plugin invents its own WP_Error codes, agents can't reason
uniformly across the surface they're given.

Adds a reference covering a small standardized vocabulary that handles
the 95% case: <plugin>_not_initialized for bootstrap failures,
<plugin>_missing_<field> and <plugin>_invalid_<field> for input
problems caught in the execute callback, <plugin>_<resource>_data_unavailable
for transient backend failures, and ability_invalid_input for the
schema-validator path that fires earlier than the callback. Documents
why upstream third-party codes (e.g. Stripe's resource_missing) should
bubble through verbatim rather than being re-wrapped, plus naming
rules and i18n discipline for codes vs messages.
Registering an ability with an input_schema doesn't guarantee that the
execute callback sees the schema applied at runtime. Three surprises
have shown up in real plugin work after the schema looked correct and
unit tests passed.

Adds a reference covering the three: (1) the Abilities API does NOT
inject schema 'default' values into callback input — agents that omit
optional fields receive callback input without those keys, so callbacks
must normalize defaults explicitly; (2) pagination parameter-name drift
when an ability exposes per_page but the backing controller delegates
to an internal class that reads pagesize, silently breaking pagination;
(3) PHP empty() false-rejecting "0" and 0 as IDs, which matters for
order/post/row IDs that can legitimately be zero.

Each gotcha includes detection patterns, the corrective code, and a
combined worked example showing all three guards in one callback.
Step 4 (Register abilities) gated on grouping and shared-core but did
not yet point at the family-pattern decision (shared-API-client vs
zero-arg controllers), the delegate-helper, or the standardized
WP_Error vocabulary. Failure modes also did not point at
input-schema-gotchas, so debugging "execute returns surprising errors"
required re-deriving the three known traps.

Adds two cross-links in step 4 (plugin-family-patterns + delegate-helper,
plus error-code-vocabulary) and one in Failure modes
(input-schema-gotchas).
The reference covered three input-schema gotchas — schema property
defaults not being injected, pagination key drift, and `empty()`
false-rejecting "0" — all of which assume the callback has already
been invoked with some input. It missed a structural concern one
layer up: HOW the callback gets invoked, and what runs first.

`wp_get_ability(...)->execute()` runs `normalize_input()` then
`validate_input()` (then `check_permissions()`) before invoking the
callback. Direct invocation of the static method skips all of that.
The two paths diverge in three places:

1. Validation rejects null input on the indirect path when the schema
   declares an object type — the callback never runs, the PHP-level
   `$input = null` default in the signature is dead code.
2. The schema's top-level `default` IS substituted by `normalize_input()`
   on null inputs (different layer from the property-level `default`
   that Gotcha 1 covers).
3. WordPress only forwards `$input` to the callback when the input
   schema is non-empty; without one, indirect invocation produces
   `ArgumentCountError` unless the signature has a default.

Surfaced on a pilot ability registration when the reviewer noted that
indirect callers validate input first, so a callback signature like
`function execute( $input = null )` does nothing for them — null gets
rejected during validation, never reaching the callback. The reviewer's
suggested fix (`default => []` at the schema root) works because
`normalize_input()` honors it; documenting that mechanism is what the
new gotcha section adds.

Add Gotcha 4 covering the direct/indirect split, the three concrete
divergences, the fix (top-level schema `default`, ideally
`(object) array()` for object-typed schemas to avoid PHP's array/object
ambiguity at the validator boundary), and how this relates to Gotcha 1.
Rename "Putting the three together" to "Putting gotchas 1-3 together"
so the existing example doesn't imply Gotcha 4 is included — Gotcha 4
is structurally about callback-invocation, not callback-robustness, and
doesn't fit the same worked example.

Verified against `wordpress-develop` `trunk`: `WP_Ability::execute()`
order at class-wp-ability.php lines 612-669; `normalize_input()` at
lines 444-455; `validate_input()` at 465-496 (delegates to
`rest_validate_value_from_schema()` which validates only and does not
inject property-level defaults); `invoke_callback()` at 507-526 with
the input-schema-gated arg-forwarding at line 509. PHP example block
lints clean on PHP 7.2.

Refs WordPress#45
Refs Automattic/woocommerce-payments#11679
… bypass

The "When NOT to use the helper" section listed two bypass categories
(zero-arg backings and non-REST services), but missed the case in the
middle: backing methods that DO take `WP_REST_Request` but route
straight to a request class via something like
`Things_Request::from_rest_request($request)->execute()` — where the
controller adds nothing beyond the construction step.

Surfaced as feedback on a pilot ability registration where the
reviewer suggested calling the request class directly instead of going
through the controller. The shape is structurally distinct from both
existing bypass categories: it still uses `WP_REST_Request` (so the
zero-arg subsection doesn't fit), but bypasses the controller layer
(so the non-REST-service subsection doesn't fit either).

Add a third "Backing request class can be invoked without the
controller" subsection between the introduction and "Zero-arg backing
methods", with a worked example that constructs a `WP_REST_Request`
purely to satisfy `from_rest_request()`'s signature, then calls the
request class directly. Cross-link to `shared-core-service.md` for the
adjacent first-call REST bootstrap-cost discussion. Note the explicit
tradeoff: bypassing the controller bypasses anything the controller
does (validation, side effects), so this shape is for cases where the
controller is genuinely a thin wrapper, not when it's doing work
worth keeping.

Update the rule of thumb so the three shapes (helper, request-class
direct, fully direct-path) compose into one decision tree instead of
the previous binary helper-or-direct-path framing.

PHP example block lints clean on PHP 7.2. Diff hygiene checks pass:
no filesystem paths, no private identifiers, no PHP 7.4+ syntax, no
internal URLs, no plugin-specific cap strings.

Refs WordPress#45
Refs Automattic/woocommerce-payments#11679
A standardized audit document is only useful if the structure is stable
across plugins and consumable without parsing surprises. Without a written
schema, every audit invents its own field names and downstream tools (or
human reviewers) have to map between variants.

Adds the canonical schema reference: required top-level fields (plugin,
repo, branch_audited, audited_at, auditor, baseline_abilities,
capability_gate, plus optional plugin_family), proposed_abilities array
shape (with backing, permission, return_type, effort, annotations, notes,
risks, optional reference_ability), excluded_from_mvp and surfaced_gaps
arrays, plus required prose sections (Controller Inventory, Notes and
Surprises). Documents capability_gate's two legal shapes (single string
vs structured {read, write} object), backing: null semantics for known
gaps, and the full minimal valid example with both a read and a write
ability.
The audit's capability_gate field has to reflect what the controller
actually gates on, not what its docblock says. Two mechanisms cover most
plugins, and naming them explicitly stops auditors from hard-coding one
plugin family's assumptions.

Adds a reference covering Mechanism A (direct: a base controller calls
current_user_can('<some_cap>') once and every route inherits the gate)
and Mechanism B (post-type-backed: controllers extend
WP_REST_Posts_Controller or a subclass; permission resolution is
dynamic at request time, with read and write often resolving to
distinct caps via core's map_meta_cap). Each mechanism gets identifying
signs, a tracing recipe, and a YAML representation example. Includes
guidance for the legacy compound-string capability_gate form
(accepted-but-not-preferred) and the __return_true REST-layer pitfall
(record verbatim, but never copy into the ability's own
permission_callback).
The first step of every audit is producing an exhaustive list of the
plugin's REST controllers. Real plugins use at least two layouts (the
standard includes/admin/class-*-rest-*-controller.php glob, and arbitrary
non-standard locations like includes/api/, src/Rest/, or monorepo
package directories), and missing controllers means missing abilities.

Adds a reference documenting the two enumeration paths — glob for
standard layouts, grep as the universal fallback — with explicit
guidance on when each works, how to combine them for monorepos, and
how to handle inherited routes (record on the child class with
backing.inherited_from + null line numbers). Closes with an
exhaustiveness rule (the Controller Inventory table must list every
controller found, not just MVP-backers) so reviewers can answer "why
isn't X in the MVP?" by pointing at the inventory.
A skill that produces a structured audit doc needs an eval scenario
demonstrating both routing (which skill to invoke for "I want to plan
abilities for plugin X") and the produced-doc shape (YAML block, prose
sections, structured capability_gate, surfaced_gaps, etc).

Adds eval/scenarios/wp-abilities-audit.json. Scenario covers a plugin
with a non-standard REST layout (includes/api/), a post-type-backed
base controller with distinct read/write capabilities, and a missing
backing endpoint that should surface as a gap. Success criteria
include routing to wp-abilities-audit, structured {read, write}
capability_gate, at least 3 proposed_abilities with at least one read
and one write, and a non-empty surfaced_gaps array.
When planning Abilities API adoption on a non-trivial plugin, the gap
between "we have a REST surface" and "we have a coherent set of
abilities to register" is real survey work — enumerate controllers,
trace capability gates, decide granularity, identify gaps. Without a
standardized output, every survey produces a differently-shaped
artifact and downstream readers (or tooling) have to re-derive the
shape each time.

Adds the wp-abilities-audit skill: a 7-step procedure that consumes a
plugin checkout, runs controller enumeration (references
controller-enumeration.md), traces capability gates (references
capability-gate-tracing.md), proposes abilities via semantic-intent
grouping (references wp-abilities-api/grouping-heuristic.md), and
writes a doc conforming to the canonical schema in audit-schema.md.
The doc is plugin-agnostic — any WP plugin that exposes a REST
surface is in scope.
@rtio rtio force-pushed the feat/abilities-audit-skill branch from 5e6a82f to 2441fb0 Compare May 6, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant