AI Agent Skills
This page highlights the most important structured skills an AI agent needs to work with the better-route library, aligned with the v1.1.0 release. See Release Notes — v1.1.0 for the full changelog and v1.0.0 for the previous baseline.
The complete, maintained skill set lives in one place: Lonsdale201/wp-agent-skills → better-route — 23 task-scoped skills covering routing, resources, auth, write safety, public clients, OpenAPI, and WooCommerce. This page keeps only the highlights inline; everything else is linked from the skill index below so the skills are not maintained in two places.
Skill: Install better-route
When: The user wants to add better-route to a WordPress project.
Requirements:
- PHP
^8.1 - WordPress with REST API (
rest_api_inithook) - Composer
- OpenSSL extension (for
Rs256JwksJwtVerifier— v0.6.0)
Install:
composer require better-route/better-route:^1.1
Only add a VCS repositories entry (pointing at https://github.com/Lonsdale201/better-route) if you need to track an unreleased branch or a fork.
Rules:
- Plain
composer requireworks — norepositoriesblock needed since v1.0.0. - All route registration must happen inside a
rest_api_initaction hook — since v1.1.0 the default dispatcher throws outside of it. - Available quality commands:
composer test,composer analyse,composer cs-check.
Full version: br-install-and-migrate
Skill: Migrate a project to v1.1.0
When: The user is upgrading from v1.0.0 (or earlier) to v1.1.0.
Steps:
- Bump the constraint to
^1.1and runcomposer update better-route/better-route. - Add explicit intent to every raw
Routerroute.GETandOPTIONSroutes without->permission(),->protectedByMiddleware(), or->publicRoute()now return403(write methods have denied since v0.4.0). This is the one change that will break previously-working reads. - Remove explicit preflight
OPTIONSroutes (or give the ones you keep explicit intent) — the WordPress CORS bridge answers preflight for every route carryingCorsMiddleware, before dispatch. - Review the write-safety defaults: failed atomic-idempotency requests now stay reserved until TTL (
releaseOnThrowabledefaults tofalse),ArrayAtomicIdempotencyStoreis tests-only, and optimistic locking runs in a MySQL advisory-lock critical section. - If you configure
maxLifetimeSecondson a JWT verifier, ensure the issuer emits bothiatandexp— tokens missing either are now rejected. WpObjectCacheRateLimiternow throws without a persistent external object cache — switch toTransientRateLimiteron default hosting.- WooCommerce:
'actions' => []now disables a resource (previously fell back to the full set); the registrar installs a durable wpdb idempotency store by default; strict payload validation rejects unknown nested keys with400. - Walk the full v1.1.0 behavior change checklist. For older upgrades, the v1.0.0 / v0.5.0 / v0.4.0 / v0.3.0 checklists live in their release notes.
Verification:
- Every endpoint that should be public still responds
200— grep for raw routes withoutpermission|protectedByMiddleware|publicRoute. - Browser clients still pass preflight (the bridge responds
204with the policy headers). - Idempotent writes and coupon-code updates behave under concurrency (
409 idempotency_in_progress,409 coupon_write_in_progress).
Full version: br-install-and-migrate
Skill: Register custom REST routes
When: The user wants to create custom REST endpoints (non-WooCommerce).
Steps:
- Create a
RouterviaBetterRoute::router('vendor', 'v1'). - Use
->get(),->post(),->put(),->patch(),->delete()to define routes. - Declare intent on every route with
->permission(),->protectedByMiddleware(), or->publicRoute()— since v1.1.0 all methods (includingGET/OPTIONS) deny by default. - Register inside a
rest_api_initaction hook (throws outside of it since v1.1.0).
Example:
add_action('rest_api_init', function () {
$router = \BetterRoute\BetterRoute::router('myapp', 'v1');
$router->get('/ping', function ($context) {
return \BetterRoute\Http\Response::ok(['pong' => true]);
})
->publicRoute();
$router->post('/articles', $createArticle)
->permission(static fn () => current_user_can('edit_posts'));
$router->post('/secure/articles', $createArticle)
->protectedByMiddleware('bearerAuth');
$router->post('/webhooks/intake', $intake)
->publicRoute();
$router->register();
});
Rules (v1.1.0):
- Raw
Routerroutes without an explicit permission callback deny by default at the WordPress permission layer — every HTTP method,GETandOPTIONSincluded (write methods since v0.4.0). ->protectedByMiddleware($security = null)defers authorization to the better-route middleware pipeline.->publicRoute()marks the route as intentionally public and clears OpenAPIsecurityfor the operation.register()fails loudly outsiderest_api_initor when WordPress core rejects a route.- Static
[Controller::class, 'method']handlers are supported without instantiation; handler classes requiring constructor arguments must be passed as instances.
Rules (v0.3.0):
- Route handlers receive
idfrom the URL route parameters first; query/bodyidis only consulted if the URL does not provide one. - Inbound
X-Request-IDis accepted only if it matches^[A-Za-z0-9._:-]{1,128}$; otherwise a fresh id is generated.
Full version: br-routes
Skill: Configure atomic idempotency for side-effectful writes
When: The user has a write endpoint where concurrent duplicate execution would cause real-world harm (payments, notifications, external API calls, customer-visible mutations).
Steps:
- Run
(new WpdbAtomicIdempotencyStore())->installSchema()once on plugin activation — since v1.1.0 it both creates and migrates the table (lease-token column). The table is separate from the existingWpdbIdempotencyStoretable. - Attach
AtomicIdempotencyMiddlewareto the route or group. - Place it inside the auth boundary (after auth middleware, before the handler).
Example:
use BetterRoute\Middleware\Write\AtomicIdempotencyMiddleware;
use BetterRoute\Middleware\Write\WpdbAtomicIdempotencyStore;
register_activation_hook(__FILE__, function (): void {
(new WpdbAtomicIdempotencyStore())->installSchema();
});
$store = new WpdbAtomicIdempotencyStore();
$router->post('/actions/charge', $handler)
->middleware([
new AtomicIdempotencyMiddleware(
store: $store,
ttlSeconds: 900,
requireKey: true
),
])
->protectedByMiddleware('bearerAuth');
Behavior (v1.1.0):
- First request with key K, fingerprint F: reserves
(K, F)under an unforgeable lease token, runs handler, stores the response via data-only serialization (StoredResponseCodec). - Concurrent identical request:
409 idempotency_in_progress. - Later identical request after completion: replays response with
Idempotency-Replayed: true. - Same K, different fingerprint (deep-canonical,
Support\Canonicalizer):409 idempotency_conflict. - Handler throws: the reservation is kept until TTL expiry by default (
releaseOnThrowable: falsesince v1.1.0) so an uncertain side effect cannot run twice; retry deliberately with a new key. - Missing
Idempotency-KeywithrequireKey: true:400 idempotency_key_required. Keys longer thanmaxKeyLength(default 200):400 idempotency_key_invalid. ArrayAtomicIdempotencyStoreis for tests only — use the wpdb store (or your ownLeaseAwareAtomicIdempotencyStoreInterface) in production.
Full version: br-atomic-idempotency
Skill: Understand the error contract
When: The agent needs to interpret or handle API errors.
Default envelope:
{
"error": {
"code": "error_code",
"message": "Human-readable message",
"requestId": "unique-request-id",
"details": {}
}
}
OAuth RFC 6749 envelope (v0.6.0, route opt-in):
{
"error": "invalid_request",
"error_description": "Invalid request."
}
Routes opt in via meta(['error_format' => 'oauth_rfc6749']). internal_error is rewritten to server_error for 5xx responses on those routes.
Common error codes:
400—validation_failed,invalid_request,idempotency_key_required,idempotency_key_invalid(v1.1.0),single_use_token_required(v0.6.0)401—invalid_token,unauthorized,invalid_signature(v0.6.0),signature_required(v0.6.0),stale_signature(v0.6.0),invalid_signature_timestamp(v0.6.0),invalid_single_use_token(v0.6.0)403—forbidden,cors_origin_denied(v0.5.0; since v1.1.0 also emitted on preflight by the WordPress CORS bridge),client_ip_unavailable(v0.6.0),client_ip_not_allowed(v0.6.0)404— resource not found409—idempotency_conflict,idempotency_in_progress(v0.5.0),single_use_token_reused(v0.6.0),coupon_exists,coupon_write_in_progress(v1.1.0), duplicate email412—precondition_failed,optimistic_lock_failed428—precondition_required(v1.0.0; missing optimistic-lock precondition)429—rate_limited(carriesRetry-AfterandX-RateLimit-*headers since v1.1.0)503—hpos_required(v1.0.0, was409),woo_unavailable
Rules:
- For
status >= 500from non-ApiExceptionfailures, the message is normalized to"Unexpected error."anddetailsis empty — internal exception class and message never leak. - For
status === 400from non-ApiExceptionfailures,details.exceptionstill includes the class name (developer aid for misuse). - Validation failures (
validation_failed) includedetails.fieldErrorsmapping each invalid field to its error messages. - (v1.1.0)
WP_Errordetails are allowlisted before entering the envelope, and response/error headers are validated against header injection.
Full version: br-error-contract
Full skill index
Everything below is maintained in the wp-agent-skills repository — link the agent there instead of duplicating instructions here.
| Domain | Skills |
|---|---|
| Routing & core | br-routes · br-error-contract · br-install-and-migrate |
| Resources | br-resource-cpt · br-resource-table · br-resource-policy · br-write-schema · br-owned-resource-guards |
| Auth & identity | br-auth-middleware · br-jwks-jwt-auth · br-hmac-signature · br-single-use-token · br-crypto |
| Write safety | br-atomic-idempotency · br-idempotency · br-optimistic-locking |
| Public clients & network | br-cors-public-client · br-rate-limiting · br-etag-cache · br-network-security · br-audit-enrichment |
| OpenAPI & WooCommerce | br-openapi · br-woo-routes |