wordpress-agent-kit 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/wp-bootstrap/SKILL.md +314 -0
- package/.agents/skills/wp-bootstrap/references/composer-setup.md +275 -0
- package/.agents/skills/wp-bootstrap/references/monorepo-patterns.md +184 -0
- package/.agents/skills/wp-bootstrap/scripts/bootstrap.sh +151 -0
- package/.agents/skills/wp-bootstrap/scripts/detect-structure.mjs +466 -0
- package/.agents/skills/wp-bootstrap/scripts/package-wp.sh +173 -0
- package/.agents/skills/wp-bootstrap/scripts/playground-start.sh +148 -0
- package/.agents/skills/wp-bootstrap/scripts/playground-verify.sh +165 -0
- package/.agents/skills/wp-bootstrap/scripts/setup-github.sh +417 -0
- package/.agents/skills/wp-wpengine/SKILL.md +76 -12
- package/.agents/skills/wp-wpengine/references/github-actions-deploy.md +16 -9
- package/README.md +1 -1
- package/dist/cli.js +2 -0
- package/dist/commands/bootstrap.js +105 -0
- package/dist/lib/api.js +1 -0
- package/dist/lib/bootstrap.js +352 -0
- package/extensions/wp-agent-kit/index.ts +143 -3
- package/package.json +1 -1
- package/skills-custom/wp-bootstrap/SKILL.md +314 -0
- package/skills-custom/wp-bootstrap/references/composer-setup.md +275 -0
- package/skills-custom/wp-bootstrap/references/monorepo-patterns.md +184 -0
- package/skills-custom/wp-bootstrap/scripts/bootstrap.sh +151 -0
- package/skills-custom/wp-bootstrap/scripts/detect-structure.mjs +466 -0
- package/skills-custom/wp-bootstrap/scripts/package-wp.sh +173 -0
- package/skills-custom/wp-bootstrap/scripts/playground-start.sh +148 -0
- package/skills-custom/wp-bootstrap/scripts/playground-verify.sh +165 -0
- package/skills-custom/wp-bootstrap/scripts/setup-github.sh +417 -0
- package/skills-custom/wp-wpengine/SKILL.md +76 -12
- package/skills-custom/wp-wpengine/references/github-actions-deploy.md +16 -9
- package/.github/skills/blueprint/SKILL.md +0 -418
- package/.github/skills/wordpress-router/SKILL.md +0 -52
- package/.github/skills/wordpress-router/references/decision-tree.md +0 -55
- package/.github/skills/wp-abilities-api/SKILL.md +0 -108
- package/.github/skills/wp-abilities-api/references/delegate-helper-pattern.md +0 -241
- package/.github/skills/wp-abilities-api/references/domain-vs-projection.md +0 -113
- package/.github/skills/wp-abilities-api/references/error-code-vocabulary.md +0 -123
- package/.github/skills/wp-abilities-api/references/grouping-heuristic.md +0 -89
- package/.github/skills/wp-abilities-api/references/input-schema-gotchas.md +0 -265
- package/.github/skills/wp-abilities-api/references/php-registration.md +0 -94
- package/.github/skills/wp-abilities-api/references/plugin-family-patterns.md +0 -233
- package/.github/skills/wp-abilities-api/references/rest-api.md +0 -13
- package/.github/skills/wp-abilities-api/references/shared-core-service.md +0 -184
- package/.github/skills/wp-abilities-audit/SKILL.md +0 -199
- package/.github/skills/wp-abilities-audit/references/audit-schema.md +0 -300
- package/.github/skills/wp-abilities-audit/references/capability-gate-tracing.md +0 -197
- package/.github/skills/wp-abilities-audit/references/controller-enumeration.md +0 -116
- package/.github/skills/wp-abilities-verify/SKILL.md +0 -215
- package/.github/skills/wp-abilities-verify/references/annotation-correctness.md +0 -154
- package/.github/skills/wp-abilities-verify/references/audit-schema-validation.md +0 -131
- package/.github/skills/wp-abilities-verify/references/permission-roundtrip.md +0 -190
- package/.github/skills/wp-abilities-verify/references/runtime-harness.md +0 -462
- package/.github/skills/wp-abilities-verify/references/schema-lints.md +0 -118
- package/.github/skills/wp-abilities-verify/references/static-enumeration.md +0 -126
- package/.github/skills/wp-block-development/SKILL.md +0 -175
- package/.github/skills/wp-block-development/references/attributes-and-serialization.md +0 -22
- package/.github/skills/wp-block-development/references/block-json.md +0 -49
- package/.github/skills/wp-block-development/references/creating-new-blocks.md +0 -46
- package/.github/skills/wp-block-development/references/debugging.md +0 -36
- package/.github/skills/wp-block-development/references/deprecations.md +0 -24
- package/.github/skills/wp-block-development/references/dynamic-rendering.md +0 -23
- package/.github/skills/wp-block-development/references/inner-blocks.md +0 -25
- package/.github/skills/wp-block-development/references/registration.md +0 -30
- package/.github/skills/wp-block-development/references/supports-and-wrappers.md +0 -18
- package/.github/skills/wp-block-development/references/tooling-and-testing.md +0 -21
- package/.github/skills/wp-block-development/scripts/list_blocks.mjs +0 -121
- package/.github/skills/wp-block-themes/SKILL.md +0 -117
- package/.github/skills/wp-block-themes/references/creating-new-block-theme.md +0 -37
- package/.github/skills/wp-block-themes/references/debugging.md +0 -24
- package/.github/skills/wp-block-themes/references/patterns.md +0 -18
- package/.github/skills/wp-block-themes/references/style-variations.md +0 -14
- package/.github/skills/wp-block-themes/references/templates-and-parts.md +0 -16
- package/.github/skills/wp-block-themes/references/theme-json.md +0 -59
- package/.github/skills/wp-block-themes/scripts/detect_block_themes.mjs +0 -117
- package/.github/skills/wp-interactivity-api/SKILL.md +0 -180
- package/.github/skills/wp-interactivity-api/references/debugging.md +0 -29
- package/.github/skills/wp-interactivity-api/references/directives-quickref.md +0 -30
- package/.github/skills/wp-interactivity-api/references/server-side-rendering.md +0 -310
- package/.github/skills/wp-performance/SKILL.md +0 -147
- package/.github/skills/wp-performance/references/autoload-options.md +0 -24
- package/.github/skills/wp-performance/references/cron.md +0 -20
- package/.github/skills/wp-performance/references/database.md +0 -20
- package/.github/skills/wp-performance/references/http-api.md +0 -15
- package/.github/skills/wp-performance/references/measurement.md +0 -21
- package/.github/skills/wp-performance/references/object-cache.md +0 -24
- package/.github/skills/wp-performance/references/query-monitor-headless.md +0 -38
- package/.github/skills/wp-performance/references/server-timing.md +0 -22
- package/.github/skills/wp-performance/references/wp-cli-doctor.md +0 -24
- package/.github/skills/wp-performance/references/wp-cli-profile.md +0 -32
- package/.github/skills/wp-performance/scripts/perf_inspect.mjs +0 -128
- package/.github/skills/wp-phpstan/SKILL.md +0 -98
- package/.github/skills/wp-phpstan/references/configuration.md +0 -52
- package/.github/skills/wp-phpstan/references/third-party-classes.md +0 -76
- package/.github/skills/wp-phpstan/references/wordpress-annotations.md +0 -124
- package/.github/skills/wp-phpstan/scripts/phpstan_inspect.mjs +0 -263
- package/.github/skills/wp-playground/SKILL.md +0 -233
- package/.github/skills/wp-playground/references/blueprints.md +0 -36
- package/.github/skills/wp-playground/references/cli-commands.md +0 -39
- package/.github/skills/wp-playground/references/debugging.md +0 -16
- package/.github/skills/wp-playground/references/e2e-playwright.md +0 -115
- package/.github/skills/wp-plugin-development/SKILL.md +0 -113
- package/.github/skills/wp-plugin-development/references/data-and-cron.md +0 -19
- package/.github/skills/wp-plugin-development/references/debugging.md +0 -19
- package/.github/skills/wp-plugin-development/references/lifecycle.md +0 -33
- package/.github/skills/wp-plugin-development/references/security.md +0 -29
- package/.github/skills/wp-plugin-development/references/settings-api.md +0 -22
- package/.github/skills/wp-plugin-development/references/structure.md +0 -16
- package/.github/skills/wp-plugin-development/scripts/detect_plugins.mjs +0 -122
- package/.github/skills/wp-plugin-directory-guidelines/SKILL.md +0 -133
- package/.github/skills/wp-plugin-directory-guidelines/references/gpl-compliance.md +0 -217
- package/.github/skills/wp-plugin-directory-guidelines/references/guideline-review-checklist.md +0 -592
- package/.github/skills/wp-plugin-directory-guidelines/references/naming-rules.md +0 -121
- package/.github/skills/wp-project-triage/SKILL.md +0 -39
- package/.github/skills/wp-project-triage/references/triage.schema.json +0 -143
- package/.github/skills/wp-project-triage/scripts/detect_wp_project.mjs +0 -610
- package/.github/skills/wp-rest-api/SKILL.md +0 -115
- package/.github/skills/wp-rest-api/references/authentication.md +0 -18
- package/.github/skills/wp-rest-api/references/custom-content-types.md +0 -20
- package/.github/skills/wp-rest-api/references/discovery-and-params.md +0 -20
- package/.github/skills/wp-rest-api/references/responses-and-fields.md +0 -30
- package/.github/skills/wp-rest-api/references/routes-and-endpoints.md +0 -36
- package/.github/skills/wp-rest-api/references/schema.md +0 -22
- package/.github/skills/wp-wpcli-and-ops/SKILL.md +0 -124
- package/.github/skills/wp-wpcli-and-ops/references/automation.md +0 -30
- package/.github/skills/wp-wpcli-and-ops/references/cron-and-cache.md +0 -23
- package/.github/skills/wp-wpcli-and-ops/references/debugging.md +0 -17
- package/.github/skills/wp-wpcli-and-ops/references/multisite.md +0 -22
- package/.github/skills/wp-wpcli-and-ops/references/packages-and-updates.md +0 -22
- package/.github/skills/wp-wpcli-and-ops/references/safety.md +0 -30
- package/.github/skills/wp-wpcli-and-ops/references/search-replace.md +0 -40
- package/.github/skills/wp-wpcli-and-ops/scripts/wpcli_inspect.mjs +0 -90
- package/.github/skills/wp-wpengine/SKILL.md +0 -127
- package/.github/skills/wpds/SKILL.md +0 -59
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
# Delegate helper pattern
|
|
2
|
-
|
|
3
|
-
Once you've decided delegation is the right shape for an ability — that is, the backing REST controller is a pure data-fetch, the operation is a read, and the ability runs predominantly inside REST contexts (see `shared-core-service.md` for when the answer is "no, extract a shared service" instead) — extract a `delegate_to_rest_controller` helper rather than open-coding the request-build/dispatch/unwrap pipeline in each execute callback. This reference documents one helper shape that works, the three guards every implementation needs, the dual response-shape unwrap, and when NOT to use the helper at all. Adapt the exact signature to the plugin you're working in — the guards and unwrap matter more than the parameter list.
|
|
4
|
-
|
|
5
|
-
Read `plugin-family-patterns.md` first — the helper's exact shape depends on how the backing controller is constructed (shared API client vs. zero-arg).
|
|
6
|
-
|
|
7
|
-
## Why this helper exists
|
|
8
|
-
|
|
9
|
-
List-style read abilities typically repeat the same four steps in every execute callback:
|
|
10
|
-
|
|
11
|
-
1. Validate the plugin is initialized (class exists + dependencies resolved).
|
|
12
|
-
2. Build a `WP_REST_Request` and copy the ability's `$input` onto it as params.
|
|
13
|
-
3. Instantiate the backing controller and invoke the named method.
|
|
14
|
-
4. Unwrap the response (controllers return either a raw array or a `WP_REST_Response`).
|
|
15
|
-
|
|
16
|
-
Four abilities × this repetition = the boilerplate dominates the callback body. Extracting a helper keeps each execute callback a 3–4 line function that reads like pseudocode.
|
|
17
|
-
|
|
18
|
-
## When to extract
|
|
19
|
-
|
|
20
|
-
After the **second** list-style ability lands. Before the third ability gets added, refactor the duplicated code out of the first two execute callbacks into a private static helper. Convert subsequent abilities to call the helper as you add them.
|
|
21
|
-
|
|
22
|
-
If the plugin only ever ships one or two abilities, inline the code. The helper's payoff is at 3+ callers.
|
|
23
|
-
|
|
24
|
-
## Helper shape
|
|
25
|
-
|
|
26
|
-
```php
|
|
27
|
-
/**
|
|
28
|
-
* Delegate an ability's execute callback to a REST controller.
|
|
29
|
-
*
|
|
30
|
-
* Builds a WP_REST_Request from the ability's input, instantiates the
|
|
31
|
-
* backing controller with the plugin's shared API client, invokes the
|
|
32
|
-
* named method, and normalizes the return so callers always see
|
|
33
|
-
* array|\WP_Error. Controllers in this plugin return either a
|
|
34
|
-
* WP_REST_Response or a raw array; the helper unwraps both shapes.
|
|
35
|
-
*
|
|
36
|
-
* Not used by zero-arg abilities — those call their controller directly
|
|
37
|
-
* so we don't synthesize a WP_REST_Request just to discard it.
|
|
38
|
-
*
|
|
39
|
-
* @param string $controller_class Fully-qualified controller class (no leading backslash).
|
|
40
|
-
* @param string $method Controller method to invoke on the built request.
|
|
41
|
-
* @param string $route REST route string used when constructing WP_REST_Request.
|
|
42
|
-
* @param array|null $input Ability input; each key/value becomes a request param.
|
|
43
|
-
* @param string $http_method HTTP method for WP_REST_Request. Defaults to 'GET'; writes pass 'POST'.
|
|
44
|
-
* @return array|\WP_Error Response payload as an array, or WP_Error on failure.
|
|
45
|
-
*/
|
|
46
|
-
private static function delegate_to_rest_controller(
|
|
47
|
-
$controller_class,
|
|
48
|
-
$method,
|
|
49
|
-
$route,
|
|
50
|
-
$input,
|
|
51
|
-
$http_method = 'GET'
|
|
52
|
-
) {
|
|
53
|
-
$fqcn = '\\' . $controller_class;
|
|
54
|
-
|
|
55
|
-
// Guard 1: controller class is loaded.
|
|
56
|
-
if ( ! class_exists( $fqcn ) ) {
|
|
57
|
-
return new \WP_Error(
|
|
58
|
-
'<plugin>_not_initialized',
|
|
59
|
-
__( '<Plugin> is not initialized.', '<text-domain>' )
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Guard 2: shared API client is available.
|
|
64
|
-
$api_client = null;
|
|
65
|
-
if ( class_exists( '\<Plugin_Main_Class>' ) && method_exists( '\<Plugin_Main_Class>', 'get_<client_accessor>' ) ) {
|
|
66
|
-
$api_client = \<Plugin_Main_Class>::get_<client_accessor>();
|
|
67
|
-
}
|
|
68
|
-
if ( null === $api_client ) {
|
|
69
|
-
return new \WP_Error(
|
|
70
|
-
'<plugin>_not_initialized',
|
|
71
|
-
__( '<Plugin> is not initialized.', '<text-domain>' )
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Build the request.
|
|
76
|
-
$request = new \WP_REST_Request( $http_method, $route );
|
|
77
|
-
if ( null !== $input ) {
|
|
78
|
-
foreach ( $input as $param => $value ) {
|
|
79
|
-
$request->set_param( $param, $value );
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Invoke + guard 3: WP_Error short-circuit + dual-shape unwrap.
|
|
84
|
-
$controller = new $fqcn( $api_client );
|
|
85
|
-
$response = $controller->{$method}( $request );
|
|
86
|
-
|
|
87
|
-
if ( is_wp_error( $response ) ) {
|
|
88
|
-
return $response;
|
|
89
|
-
}
|
|
90
|
-
if ( $response instanceof \WP_REST_Response ) {
|
|
91
|
-
$data = $response->get_data();
|
|
92
|
-
return is_array( $data ) ? $data : [];
|
|
93
|
-
}
|
|
94
|
-
return is_array( $response ) ? $response : [];
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Positional (not named) arguments on purpose — the helper is called from every list ability and keyword-args for PHP 8 would add visual noise. Order is intentional: controller, method, route, input, http_method (the defaultable tail).
|
|
99
|
-
|
|
100
|
-
## The three guards
|
|
101
|
-
|
|
102
|
-
### 1. `class_exists` on the controller
|
|
103
|
-
|
|
104
|
-
Execute callbacks can be reached on sites where the plugin's classes haven't loaded (tests, WP-CLI with limited bootstrap, admin pages with conditional autoloading). Check is cheap; without it a missing class produces a fatal.
|
|
105
|
-
|
|
106
|
-
Note `'\\' . $controller_class` — accept controller class names without a leading backslash (readable in config arrays) and re-add it so `class_exists` and `new $fqcn(...)` both root-resolve.
|
|
107
|
-
|
|
108
|
-
### 2. Required dependency (API client) null-check
|
|
109
|
-
|
|
110
|
-
When the controller takes a shared API client as a constructor argument, the accessor is typically a `public static` method on the main plugin class. Guard both `class_exists` on the plugin class AND `method_exists` on the accessor because either could be absent during partial bootstraps. Treat a null return from the accessor as "not initialized".
|
|
111
|
-
|
|
112
|
-
Missing this argument produces `ArgumentCountError: Too few arguments to function <Controller>::__construct(), 0 passed` — a PHP fatal. The standardized `<plugin>_not_initialized` error code is documented in `error-code-vocabulary.md`.
|
|
113
|
-
|
|
114
|
-
When the controller takes no constructor args, skip this guard entirely. When it takes only simple scalars (e.g. a post-type string), pass them in via an optional `constructor_args` parameter on the helper.
|
|
115
|
-
|
|
116
|
-
### 3. `is_wp_error` short-circuit
|
|
117
|
-
|
|
118
|
-
Before trying to unwrap the response, check if it's already a `WP_Error`. Several controllers return `WP_Error` for both validation failures and upstream failures; that error needs to bubble up intact so the agent can see the upstream code.
|
|
119
|
-
|
|
120
|
-
## Dual response-shape unwrap
|
|
121
|
-
|
|
122
|
-
```php
|
|
123
|
-
if ( $response instanceof \WP_REST_Response ) {
|
|
124
|
-
$data = $response->get_data();
|
|
125
|
-
return is_array( $data ) ? $data : [];
|
|
126
|
-
}
|
|
127
|
-
return is_array( $response ) ? $response : [];
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Real WordPress REST controllers return either:
|
|
131
|
-
- A `WP_REST_Response` (the output of `rest_ensure_response( ... )` or `new WP_REST_Response( ... )`), or
|
|
132
|
-
- A raw array (typical when a controller delegates to an internal request-handler class that returns the decoded transport payload directly).
|
|
133
|
-
|
|
134
|
-
Both happen. The helper unwraps `WP_REST_Response` via `get_data()` and passes raw arrays through. The `is_array(...) ? ... : []` coalesce defends against a non-array return type that the ability's `output_schema` would have rejected downstream anyway — returning `[]` fails safely rather than leaking a surprising type.
|
|
135
|
-
|
|
136
|
-
## When NOT to use the helper
|
|
137
|
-
|
|
138
|
-
Three categories of abilities bypass the helper and call the backing directly:
|
|
139
|
-
|
|
140
|
-
### Backing request class can be invoked without the controller
|
|
141
|
-
|
|
142
|
-
If the REST controller's only role is to translate `WP_REST_Request` into a call to an underlying request class (e.g., `Things_Request::from_rest_request( $request )->execute()`) and adds no validation, permission logic, or orchestration on top, bypass the controller and call the request class directly:
|
|
143
|
-
|
|
144
|
-
```php
|
|
145
|
-
public static function execute_get_things( $input = null ) {
|
|
146
|
-
if ( ! class_exists( '\My_Plugin\Things_Request' ) ) {
|
|
147
|
-
return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
$request = new \WP_REST_Request( 'GET', '/my-plugin/v1/things' );
|
|
151
|
-
foreach ( (array) $input as $param => $value ) {
|
|
152
|
-
$request->set_param( $param, $value );
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
$things_request = \My_Plugin\Things_Request::from_rest_request( $request );
|
|
156
|
-
$rows = $things_request->execute();
|
|
157
|
-
|
|
158
|
-
return is_array( $rows ) ? $rows : [];
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
The `WP_REST_Request` object exists only to satisfy `from_rest_request()`'s parameter contract — nothing dispatches it. This is the shortest path through the layers: no controller construction, no controller-emitted side effects, and (when the request class is hit before any REST traffic in the lifecycle) no `rest_api_init` cost. See `shared-core-service.md` for the broader discussion of REST-path side effects and the first-call bootstrap cost on cold paths.
|
|
163
|
-
|
|
164
|
-
The tradeoff is real: bypassing the controller bypasses anything the controller does. If the controller runs validation that the request class doesn't, or emits an audit hook the request class doesn't, that work is lost on the ability path. Use this shape when the controller is genuinely a thin wrapper, not when it's doing work you'd want to keep.
|
|
165
|
-
|
|
166
|
-
### Zero-arg backing methods
|
|
167
|
-
|
|
168
|
-
If the backing method takes no `WP_REST_Request` parameter, don't synthesize a request just to discard it. Call the method directly:
|
|
169
|
-
|
|
170
|
-
```php
|
|
171
|
-
public static function execute_get_overview( $input = null ) {
|
|
172
|
-
if ( ! class_exists( '\My_Plugin_REST_Overview_Controller' ) ) {
|
|
173
|
-
return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
$api_client = /* ... same accessor check ... */;
|
|
177
|
-
if ( null === $api_client ) {
|
|
178
|
-
return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
$controller = new \My_Plugin_REST_Overview_Controller( $api_client );
|
|
182
|
-
$response = $controller->get_overview(); // No $request argument.
|
|
183
|
-
|
|
184
|
-
if ( is_wp_error( $response ) ) {
|
|
185
|
-
return $response;
|
|
186
|
-
}
|
|
187
|
-
return is_array( $response ) ? $response : [];
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Abilities that use a non-REST service
|
|
192
|
-
|
|
193
|
-
If the execute callback reaches a service directly (e.g. `My_Plugin::get_account_service()->get_cached_account_data()`) rather than a REST controller, stay on the direct path. The ability is not REST-backed and the helper would add a construction step for no gain.
|
|
194
|
-
|
|
195
|
-
## Rule of thumb
|
|
196
|
-
|
|
197
|
-
**If the backing method takes `WP_REST_Request` and the controller adds something beyond delegating to an underlying request class (validation, permissions, orchestration, side effects you want to keep), use the helper. If the controller is a thin wrapper over a request class, call the request class directly. Otherwise direct-path.**
|
|
198
|
-
|
|
199
|
-
## Conversion pattern — before and after
|
|
200
|
-
|
|
201
|
-
Before extraction (inline in the execute callback):
|
|
202
|
-
|
|
203
|
-
```php
|
|
204
|
-
public static function execute_get_things( $input = null ) {
|
|
205
|
-
if ( ! class_exists( '\My_Plugin_REST_Things_Controller' ) ) {
|
|
206
|
-
return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
|
|
207
|
-
}
|
|
208
|
-
$api_client = \My_Plugin::get_api_client();
|
|
209
|
-
if ( ! $api_client ) {
|
|
210
|
-
return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
|
|
211
|
-
}
|
|
212
|
-
$request = new \WP_REST_Request( 'GET', '/my-plugin/v1/things' );
|
|
213
|
-
foreach ( (array) $input as $param => $value ) {
|
|
214
|
-
$request->set_param( $param, $value );
|
|
215
|
-
}
|
|
216
|
-
$controller = new \My_Plugin_REST_Things_Controller( $api_client );
|
|
217
|
-
$response = $controller->get_things( $request );
|
|
218
|
-
// ... unwrap ...
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
After extraction:
|
|
223
|
-
|
|
224
|
-
```php
|
|
225
|
-
public static function execute_get_things( $input = null ) {
|
|
226
|
-
return self::delegate_to_rest_controller(
|
|
227
|
-
'My_Plugin_REST_Things_Controller',
|
|
228
|
-
'get_things',
|
|
229
|
-
'/my-plugin/v1/things',
|
|
230
|
-
is_array( $input ) ? $input : null
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Note the `is_array( $input ) ? $input : null` — the helper signature is `?array`, and passing `null` tells it to build the request with no params at all. This matters because the Abilities API sometimes invokes a callback with no input object.
|
|
236
|
-
|
|
237
|
-
## Related references
|
|
238
|
-
|
|
239
|
-
- `plugin-family-patterns.md` — choosing the helper's constructor shape.
|
|
240
|
-
- `error-code-vocabulary.md` — the `<plugin>_not_initialized` code and its siblings.
|
|
241
|
-
- `input-schema-gotchas.md` — callbacks for list abilities often need `per_page` pagination translation BEFORE calling the helper.
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# Domain capability vs projection — picking the layer to register at
|
|
2
|
-
|
|
3
|
-
> Three layers in this model: **domain**, **projection**, and an optional **workflow** layer that composes abilities into multi-step tasks. The title names the two primary decisions; workflow is introduced where it earns its keep.
|
|
4
|
-
|
|
5
|
-
The Abilities API can be used at two heights. You can register abilities as the surface an MCP/REST/Command-Palette client will consume directly. Or you can register abilities at the domain layer — "what can this plugin do, transport-neutrally?" — and let each consumer see a projection of that surface that suits its constraints. This reference covers why the second framing pays off, the three-layer model that operationalizes it, and how to use it when deciding what to register.
|
|
6
|
-
|
|
7
|
-
## Why this matters
|
|
8
|
-
|
|
9
|
-
Treating MCP exposure as the registration target conflates two decisions:
|
|
10
|
-
|
|
11
|
-
1. What capabilities does this plugin offer?
|
|
12
|
-
2. How should each consumer see them?
|
|
13
|
-
|
|
14
|
-
The first decision is stable. The second is volatile — token budgets shift, MCP clients improve, Command Palette gains new conventions, large plugins outgrow flat tool lists and adopt nested-discovery patterns. Coupling the two means re-registering abilities every time the projection question is reopened.
|
|
15
|
-
|
|
16
|
-
Decoupling them means registrations stay where they are while projections evolve underneath.
|
|
17
|
-
|
|
18
|
-
## The three layers
|
|
19
|
-
|
|
20
|
-
| Layer | Purpose | Examples |
|
|
21
|
-
|---|---|---|
|
|
22
|
-
| **Domain capability** | Stable, transport-neutral, permission-checked. The plugin's contract for "what can this do." | `myplugin/list-things`, `myplugin/update-thing`, `myplugin/get-overview` |
|
|
23
|
-
| **Workflow** *(optional)* | Compositions of one or more abilities into a human- or agent-friendly task. | "Onboard a new merchant" = `verify-account` → `enable-feature` → `send-welcome` |
|
|
24
|
-
| **Projection** | Consumer-specific view of the domain layer. Token-efficient compression for MCP, curated workflows for Command Palette, discoverable execution surface for REST, forms for admin UI, chainable commands for CLI. | The bundled WordPress MCP adapter projects every published ability through three meta-abilities (`mcp-adapter/discover-abilities`, `mcp-adapter/execute-ability`, `mcp-adapter/get-ability-info`) — agents traverse those three rather than seeing the full registry flat. Command Palette, by contrast, can show the same domain abilities flat because token budget is not a constraint. |
|
|
25
|
-
|
|
26
|
-
The domain layer is what the registry holds. The projection layer is what each consumer sees. The workflow layer is optional and exists when a user-or-agent-meaningful task needs more than one ability.
|
|
27
|
-
|
|
28
|
-
## The use-case-contract test
|
|
29
|
-
|
|
30
|
-
A registered ability is a use-case contract — a natural-language shortcut to an action a human can already perform in the UI. REST endpoints and CLI commands are transport contracts; they expose plumbing. Abilities expose actions.
|
|
31
|
-
|
|
32
|
-
Two consequences fall out of that framing:
|
|
33
|
-
|
|
34
|
-
### 1. Inclusion test — "would a human intentionally do this through a supported UI or workflow?"
|
|
35
|
-
|
|
36
|
-
If yes, the operation is a candidate for an ability. The lens is broader than wp-admin alone: a "supported UI or workflow" covers admin screens, public-facing UIs (storefront, account dashboard, course viewer, appointment booker), end-user self-service flows on the site front-end, and supported workflows in which another plugin or an agent calls the operation as part of a chain of actions. Abilities like `store/get-my-orders`, `events/list-available-tickets`, or `profile/update-public-profile` qualify just as much as admin-side abilities like `myplugin/list-pending-orders` or `myplugin/approve-submission`.
|
|
37
|
-
|
|
38
|
-
If no, the operation stays a REST endpoint, a CLI command, or an internal hook. Internal-only plumbing — cache invalidation, scheduler ticks, debug snapshots, lifecycle bookkeeping — does not belong as an ability even when it has a clean schema. There is no meaningful human invocation point, so there is no use-case contract to register.
|
|
39
|
-
|
|
40
|
-
The test is asymmetric: not everything a UI exposes should be an ability either (rule 1 in `grouping-heuristic.md` covers grouping). But anything that has no human-meaningful invocation surface almost certainly is not ability-meaningful.
|
|
41
|
-
|
|
42
|
-
### 2. Same code path as the UI
|
|
43
|
-
|
|
44
|
-
If the ability mirrors a UI action, it must share the UI's permissions, validation, and business rules. The mechanism for keeping that promise is in `shared-core-service.md`. The framing is here: the ability is the use-case; UI / REST / CLI / MCP are thin adapters over it.
|
|
45
|
-
|
|
46
|
-
## Implications
|
|
47
|
-
|
|
48
|
-
### You may register abilities you do NOT expose via MCP
|
|
49
|
-
|
|
50
|
-
The Abilities API is a WordPress core primitive. It establishes a formal contract for "run this code with these inputs under this permission check." MCP, REST, and Command Palette are exposure layers on top. You opt in.
|
|
51
|
-
|
|
52
|
-
This means a plugin can register abilities that the Command Palette uses but MCP does not see, or vice versa. The registration is transport-neutral; exposure is per-consumer.
|
|
53
|
-
|
|
54
|
-
### The same domain ability can appear in multiple projections with different shapes
|
|
55
|
-
|
|
56
|
-
A small plugin might expose its three abilities flat in MCP and flat in Command Palette. A larger plugin might keep the same three domain abilities but project them through a nested-discovery facade for MCP (three meta-tools that traverse to the underlying registrations) while showing them flat in the Command Palette where token budget is not a constraint.
|
|
57
|
-
|
|
58
|
-
The domain layer does not change. The projection layer does the consumer-specific work.
|
|
59
|
-
|
|
60
|
-
### Token-efficiency tradeoffs live at the projection layer
|
|
61
|
-
|
|
62
|
-
The choice between flat-with-full-schemas, semantic-grouping, single-tool-facade, and nested-discovery is a *projection* decision. Different shapes serve different consumer constraints. None of those shapes require changing what abilities are registered at the domain layer; they change how a consumer sees the registered set.
|
|
63
|
-
|
|
64
|
-
This is what "pattern-agnostic" means in practice: the registration is one decision, the projection is another, and a redesign of the second does not invalidate the first.
|
|
65
|
-
|
|
66
|
-
## Decision order
|
|
67
|
-
|
|
68
|
-
When designing a plugin's ability surface, work in this order:
|
|
69
|
-
|
|
70
|
-
1. **Domain first.** What can this plugin do, transport-neutrally? Use the inclusion test. Apply `grouping-heuristic.md` to pick the right granularity (semantic-intent over REST-atomization).
|
|
71
|
-
2. **Projection second.** Which surfaces does each capability appear on, and in what shape? For most plugins under ~10 abilities, flat-in-every-projection works. For larger plugins, consider nested-discovery for MCP while keeping flat for Command Palette and REST.
|
|
72
|
-
3. **Workflow third (only if needed).** Are there multi-ability tasks worth composing into a single user-or-agent-facing entry point? If yes, the workflow lives above the domain layer and references multiple abilities.
|
|
73
|
-
|
|
74
|
-
Most plugins never need step 3.
|
|
75
|
-
|
|
76
|
-
## Worked example — generic Notifications plugin
|
|
77
|
-
|
|
78
|
-
A "Notifications" plugin manages a per-user notification feed. It supports listing, marking read, and dismissing all.
|
|
79
|
-
|
|
80
|
-
### Domain layer
|
|
81
|
-
|
|
82
|
-
Three abilities, registered once:
|
|
83
|
-
|
|
84
|
-
- `notifications/list` — read; filter by `unread_only`, `since`, `category`.
|
|
85
|
-
- `notifications/mark-read` — write; takes a `notification_id`.
|
|
86
|
-
- `notifications/dismiss-all` — write; zero-arg.
|
|
87
|
-
|
|
88
|
-
These are use-case contracts. They are transport-neutral. They do not assume MCP or Command Palette or REST.
|
|
89
|
-
|
|
90
|
-
### Projection — MCP
|
|
91
|
-
|
|
92
|
-
Small surface, flat works. The MCP projection exposes the three abilities as-is. No nested-discovery needed.
|
|
93
|
-
|
|
94
|
-
### Projection — Command Palette
|
|
95
|
-
|
|
96
|
-
The Command Palette projection adds a small workflow that wraps `notifications/list` with default filters (`unread_only=true`) and opens the admin notifications page on selection. The other two abilities remain visible as flat commands. The domain registration did not change; the Command Palette layer added the workflow on top.
|
|
97
|
-
|
|
98
|
-
### Projection — REST
|
|
99
|
-
|
|
100
|
-
The REST projection exposes only the read ability. Writes go through admin UI for security-review reasons. The domain registration is unchanged; the REST layer just doesn't expose two of the three.
|
|
101
|
-
|
|
102
|
-
Same three registrations, three different projections, no re-registration as projection decisions evolve.
|
|
103
|
-
|
|
104
|
-
## Escape hatch — tiny plugins
|
|
105
|
-
|
|
106
|
-
For plugins with one to three admin-only abilities and no other surfaces, the projection layer is trivially the identity function. You don't need to *implement* layers; you need to *think* in layers so you don't paint yourself into a corner when the plugin grows or when MCP token budgets become a constraint.
|
|
107
|
-
|
|
108
|
-
Concretely: name the abilities at the domain level (`myplugin/get-thing`, not `myplugin-mcp/get-thing`), keep the registration permission-checked and transport-neutral, and skip the projection layer until something needs it.
|
|
109
|
-
|
|
110
|
-
## Related references
|
|
111
|
-
|
|
112
|
-
- `grouping-heuristic.md` — within-domain decisions: how many abilities, where to put filters.
|
|
113
|
-
- `shared-core-service.md` — the implementation mechanism for "same code path as the UI."
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# Error code vocabulary
|
|
2
|
-
|
|
3
|
-
Every `WP_Error` returned from an ability's execute callback should use a code drawn from this small vocabulary. A consistent vocabulary lets the agent (or the caller in your automation) build retry and recovery logic that matches on codes instead of parsing error messages.
|
|
4
|
-
|
|
5
|
-
## Why standardized codes matter
|
|
6
|
-
|
|
7
|
-
Agents that orchestrate multiple abilities need to know: "did this fail because of my input, or because something downstream is unreachable?" The answer drives retry strategy:
|
|
8
|
-
|
|
9
|
-
- Bootstrap failures → surface to the human, don't retry.
|
|
10
|
-
- Input shape errors → fix the input and retry the same call.
|
|
11
|
-
- Transient backend errors → retry with backoff; may succeed on its own.
|
|
12
|
-
- Upstream third-party errors → may need a different strategy entirely.
|
|
13
|
-
|
|
14
|
-
Without a convention, every plugin invents its own codes and agents can't reason uniformly across them. The categories below cover 95% of real-world ability errors; sticking to them means an agent that learned the retry logic for one plugin can reuse it for the next.
|
|
15
|
-
|
|
16
|
-
## Vocabulary
|
|
17
|
-
|
|
18
|
-
Substitute `<plugin>` with the plugin's slug in lowercase, underscores only. This matches WordPress error-code conventions. If the plugin already has a house style — for example, a single-word slug that deliberately omits underscores between elided words — mirror that. Consistency within a plugin trumps consistency across the vocabulary.
|
|
19
|
-
|
|
20
|
-
| Code | When to use | Agent behavior |
|
|
21
|
-
|---|---|---|
|
|
22
|
-
| `<plugin>_not_initialized` | Plugin class missing, shared service accessor returns null, required dependency isn't resolvable. | Not retriable from the agent side. Usually a config or bootstrap-ordering problem. Escalate. |
|
|
23
|
-
| `<plugin>_missing_<field>` | A required input field is missing or empty (your execute callback's own check, not the schema-validator's). | Agent should add the field and retry. |
|
|
24
|
-
| `<plugin>_invalid_<field>` | Field is present but semantically wrong (bad enum value, malformed date, wrong type). | Agent should correct the value and retry. |
|
|
25
|
-
| `<plugin>_<resource>_data_unavailable` | Backing service returned a transient error (cache miss + remote failure, upstream 5xx, stale-while-revalidate failed). | Agent can retry, probably with backoff. |
|
|
26
|
-
| `ability_invalid_input` | The Abilities API's own schema-validator rejected the input before the execute callback ran. | Equivalent to `<plugin>_missing_<field>` or `<plugin>_invalid_<field>`, just thrown earlier in the pipeline. Agent should fix input. |
|
|
27
|
-
|
|
28
|
-
### `ability_invalid_input` — the earlier path
|
|
29
|
-
|
|
30
|
-
When an ability is invoked via the Abilities API REST bridge, the registered `input_schema` runs first. Missing required fields or type mismatches produce `WP_Error( 'ability_invalid_input' )` BEFORE the execute callback fires. Direct invocation (unit tests, non-REST wrappers) bypasses schema validation and hits your execute callback's own checks instead, producing `<plugin>_missing_<field>` / `<plugin>_invalid_<field>`.
|
|
31
|
-
|
|
32
|
-
Both paths are acceptable — end-agent-facing behavior is equivalent (deterministic validation error, no side effects). Document the observation in the ability's PR description or "notes for reviewers" so reviewers know both codes are expected in different harness runs.
|
|
33
|
-
|
|
34
|
-
## Worked examples
|
|
35
|
-
|
|
36
|
-
```php
|
|
37
|
-
// Bootstrap incomplete — plugin class missing or accessor returned null.
|
|
38
|
-
return new \WP_Error(
|
|
39
|
-
'<plugin>_not_initialized',
|
|
40
|
-
__( '<Plugin> is not initialized.', '<text-domain>' )
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
// Required field missing (execute-callback check, schema was bypassed).
|
|
44
|
-
return new \WP_Error(
|
|
45
|
-
'<plugin>_missing_<field>',
|
|
46
|
-
__( 'A <field> is required to <do the thing>.', '<text-domain>' )
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
// Field present but malformed.
|
|
50
|
-
return new \WP_Error(
|
|
51
|
-
'<plugin>_invalid_<field>',
|
|
52
|
-
sprintf(
|
|
53
|
-
/* translators: %s: input parameter name. */
|
|
54
|
-
__( 'The "%s" parameter must be an ISO 8601 date-time string.', '<text-domain>' ),
|
|
55
|
-
$key
|
|
56
|
-
)
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
// Transient backend error.
|
|
60
|
-
return new \WP_Error(
|
|
61
|
-
'<plugin>_<resource>_data_unavailable',
|
|
62
|
-
__( 'Unable to retrieve <resource> data. The cache may be stale or the remote service returned an error.', '<text-domain>' )
|
|
63
|
-
);
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Upstream codes can bubble through — and usually should
|
|
67
|
-
|
|
68
|
-
If the backing controller talks to a third-party API (Stripe, WPCOM, another SaaS), its own error codes may surface verbatim in `WP_Error::get_error_code()` the ability returns. Typical examples:
|
|
69
|
-
|
|
70
|
-
- `resource_missing` — Stripe's "object ID not found" code. Tells an agent the specific ID was wrong.
|
|
71
|
-
- `rate_limited` — upstream throttling. Tells an agent to slow down.
|
|
72
|
-
|
|
73
|
-
**Let these through rather than re-wrapping.** A re-wrapped `<plugin>_<resource>_not_found` loses the information that would help the agent act. Document the upstream codes you've observed in the ability's `description` or PR notes so reviewers know they're expected.
|
|
74
|
-
|
|
75
|
-
## Code naming rules
|
|
76
|
-
|
|
77
|
-
1. **Lowercase, underscores only.** `<plugin>_missing_order_id`, not `<plugin>-missingOrderId` or `<Plugin>-MissingOrderId`.
|
|
78
|
-
2. **Plugin prefix first.** Multi-word slugs should normalize to a single-word prefix where the plugin already does (e.g. `woopayments` rather than `woo_payments`).
|
|
79
|
-
3. **Action second.** Use one of `missing`, `invalid`, `not_initialized`, `<resource>_data_unavailable`.
|
|
80
|
-
4. **Field or resource name last.** `<plugin>_missing_dispute_id`, not `<plugin>_dispute_id_missing`. This matches English phrasing ("a dispute_id is required") and is faster to skim in error logs.
|
|
81
|
-
|
|
82
|
-
## Internationalization
|
|
83
|
-
|
|
84
|
-
Error MESSAGES are translatable via `__()` or `sprintf(__())`. Error CODES are not — they're stable machine identifiers.
|
|
85
|
-
|
|
86
|
-
```php
|
|
87
|
-
// WRONG — don't translate the code.
|
|
88
|
-
return new \WP_Error( __( '<plugin>_missing_order_id', '<text-domain>' ), ... );
|
|
89
|
-
|
|
90
|
-
// RIGHT — code is a literal, message is translatable.
|
|
91
|
-
return new \WP_Error(
|
|
92
|
-
'<plugin>_missing_order_id',
|
|
93
|
-
__( 'An order_id is required.', '<text-domain>' )
|
|
94
|
-
);
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
When a message interpolates a parameter name or value, include a translator comment:
|
|
98
|
-
|
|
99
|
-
```php
|
|
100
|
-
return new \WP_Error(
|
|
101
|
-
'<plugin>_invalid_date',
|
|
102
|
-
sprintf(
|
|
103
|
-
/* translators: %s: input parameter name. */
|
|
104
|
-
__( 'The "%s" parameter must be an ISO 8601 date-time string.', '<text-domain>' ),
|
|
105
|
-
$key
|
|
106
|
-
)
|
|
107
|
-
);
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Don't over-specify
|
|
111
|
-
|
|
112
|
-
The goal is consistent codes across all of a plugin's abilities, not ultra-fine granularity. `<plugin>_missing_dispute_id` is the right level. `<plugin>_missing_dispute_id_in_submit_evidence_context` is not — the ability name belongs in `WP_Error::get_error_data()` or in the agent's calling context, not in the code.
|
|
113
|
-
|
|
114
|
-
## Summary — the four questions
|
|
115
|
-
|
|
116
|
-
When writing an execute callback, ask in order:
|
|
117
|
-
|
|
118
|
-
1. Can the plugin even service this call? → `<plugin>_not_initialized`.
|
|
119
|
-
2. Is the required input present? → `<plugin>_missing_<field>`.
|
|
120
|
-
3. Is the required input the right shape? → `<plugin>_invalid_<field>`.
|
|
121
|
-
4. Did the backing service choke? → `<plugin>_<resource>_data_unavailable`, or let the upstream code bubble through.
|
|
122
|
-
|
|
123
|
-
Four categories, one consistent code vocabulary per plugin. That's enough for agents to build reliable retry logic on top.
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# Grouping heuristic — domain-layer granularity
|
|
2
|
-
|
|
3
|
-
How to decide WHAT to register when a plugin already has a REST (or internal service) surface. The hard part of adopting the Abilities API is not the registration syntax — it's picking the right *domain-layer granularity* so abilities map to user-meaningful actions instead of HTTP plumbing.
|
|
4
|
-
|
|
5
|
-
> **Scope note.** This reference governs *domain-layer* decisions: how many abilities to register, where to put filters vs. where to introduce a new ability name. It does NOT govern projection-layer choices (flat-with-full-schemas vs single-tool facade vs nested-discovery vs semantic grouping in the consumer view). Those are separate decisions made *after* the domain layer is settled — see `domain-vs-projection.md` for the layering. A domain layer chosen well is reusable across multiple projections; conflating the two means re-registering every time a consumer's constraints change.
|
|
6
|
-
|
|
7
|
-
## Three observed approaches
|
|
8
|
-
|
|
9
|
-
| Approach | Shape | Example | Verdict (domain-layer) |
|
|
10
|
-
|---|---|---|---|
|
|
11
|
-
| **Action-bundle** | One ability bundles many sub-operations behind an action string. | `my_plugin_account` with `action: "get" \| "update" \| "delete"`. | **Avoid.** Hides the ability surface from the agent's tool-list and defeats the Abilities API's introspection model — agents can't see what a bundle can do until they invoke it. |
|
|
12
|
-
| **REST-atomization** | One ability per HTTP method per resource (typically 5 per resource: list, get, create, update, delete). | `orders-list`, `orders-get`, `orders-create`, `orders-update`, `orders-delete`. | **Avoid as the registration shape.** Couples ability names to HTTP plumbing rather than user intent — and forces re-registration if the projection layer ever needs to compress the surface. |
|
|
13
|
-
| **Semantic-intent** | One ability per real-world question or state transition. Filter parameters in `input_schema` collapse N variants into 1. | One `feedback/get-responses` ability with `status`, `is_unread`, `search`, and date-range filters in `input_schema` — replaces what would be 8+ atomized variants. | **Recommended for the domain layer.** |
|
|
14
|
-
|
|
15
|
-
## Why semantic-intent wins at the domain layer
|
|
16
|
-
|
|
17
|
-
1. **Users think in questions, not HTTP verbs.** "Which form responses are unread?" maps cleanly to one ability with an `is_unread` filter. It does NOT map to 8 abilities (`get-unread-responses`, `get-spam-responses`, `get-trashed-responses`, ...). Ability names are *use-case contracts* — see `./domain-vs-projection.md`.
|
|
18
|
-
2. **The Abilities API's `input_schema` is designed for rich inputs.** Enum constraints, date-time formats, and required-field validation do the variant-splitting job that atomization would delegate to the ability name.
|
|
19
|
-
3. **Writes stay narrow anyway.** A write ability should already be one state transition; atomization and semantic-intent converge for writes.
|
|
20
|
-
4. **Tool-list token cost is a downstream consequence, not the reason.** Semantic-intent registrations also serialize cheaper in flat MCP projections — but token cost is a *projection-layer* concern. If registrations are cheap by accident because the use-case framing happened to compress them, that's a happy side-effect; if they're expensive, the fix is at the projection layer (single-tool facade, nested-discovery), not by re-grouping the domain.
|
|
21
|
-
|
|
22
|
-
## Rules that make it work
|
|
23
|
-
|
|
24
|
-
### 1. Group reads by the question a user would type
|
|
25
|
-
|
|
26
|
-
Draft the question in plain English. That question is the ability. The filter parameters go in `input_schema`.
|
|
27
|
-
|
|
28
|
-
- WRONG: one ability per status value.
|
|
29
|
-
- RIGHT: one `get-<resource>` ability with `status: { type: "string", enum: [...] }`.
|
|
30
|
-
|
|
31
|
-
### 2. Keep writes narrow — one state transition per ability
|
|
32
|
-
|
|
33
|
-
A write ability should do exactly one thing the agent can reason about in isolation and explain to a user. Different state transitions → different abilities (different consequences, different annotations, different permission implications).
|
|
34
|
-
|
|
35
|
-
- WRONG: `update-resource` that branches internally on an action enum.
|
|
36
|
-
- RIGHT: `submit-evidence` and `close-resource` as separate abilities.
|
|
37
|
-
|
|
38
|
-
### 3. Prefer 1 ability with filter params over N abilities with no params
|
|
39
|
-
|
|
40
|
-
Ask: "if the backing added a new filter value, would that create a new ability?" If not, the filter belongs in `input_schema`, not in the ability name.
|
|
41
|
-
|
|
42
|
-
### 4. Zero-arg overview abilities are high-leverage
|
|
43
|
-
|
|
44
|
-
When enumerating the backing surface, specifically look for zero-argument aggregate or "overview" endpoints — "what's my balance?", "what's my next payout?", "what's my form response count?". These answer the highest-frequency user questions with zero input and zero room for agent error. Flag them even if they weren't in the original plan.
|
|
45
|
-
|
|
46
|
-
### 5. Don't ship abilities you can't explain in one sentence
|
|
47
|
-
|
|
48
|
-
Every ability's `label` + `description` should fit in an agent's tool-selection prompt. If you can't describe the ability in one sentence without "and", that's usually a sign it's two abilities.
|
|
49
|
-
|
|
50
|
-
## Worked example A — feedback/responses: 3 abilities for the whole responses surface
|
|
51
|
-
|
|
52
|
-
Consider a generic feedback or form-response plugin. Its admin screens expose: a list with 8+ filters, a detail view, bulk status changes (spam / trash / publish), read/unread toggles, and a count-by-status dashboard summary.
|
|
53
|
-
|
|
54
|
-
REST-atomization would ship ~6 abilities (list, get, delete, update, bulk-update, count). Semantic-intent registers **three**:
|
|
55
|
-
|
|
56
|
-
- `feedback/get-responses` — list/search, with `status`, `is_unread`, `search`, `before`, `after`, `parent` in `input_schema`.
|
|
57
|
-
- `feedback/update-response` — one write that covers status changes AND read-state toggles on a single response (semantically "modify a response").
|
|
58
|
-
- `feedback/get-status-counts` — the dashboard summary ability. Zero-arg-friendly (only optional filters).
|
|
59
|
-
|
|
60
|
-
Why three works: a user asking "show me spam responses from last week" uses one ability. An agent updating one response to `spam` uses one ability. The dashboard-style "how many unread?" uses one ability. The entire product surface fits in three tool-list entries.
|
|
61
|
-
|
|
62
|
-
## Worked example B — generic Tickets plugin: one ability with a status filter, not eight
|
|
63
|
-
|
|
64
|
-
A hypothetical `myplugin-tickets` plugin manages a support-ticket queue. Its REST endpoint `GET /myplugin/v1/tickets` accepts `status`, `priority`, `assigned_to`, `tag`, `date_before`, `date_after`, `search`. Status values include `new`, `triaged`, `in_progress`, `waiting_customer`, `waiting_internal`, `resolved`, `closed`, `reopened` — eight in total.
|
|
65
|
-
|
|
66
|
-
- Atomization would ship ~8 abilities (`get-tickets-new`, `get-tickets-resolved`, `get-tickets-closed`, ...).
|
|
67
|
-
- Semantic-intent ships **one** — `myplugin-tickets/get-tickets` with `status: { type: "string", enum: ["new", "triaged", "in_progress", "waiting_customer", "waiting_internal", "resolved", "closed", "reopened"] }`.
|
|
68
|
-
|
|
69
|
-
The user question "which tickets are waiting on the customer?" becomes one ability invocation with `status: "waiting_customer"`. The agent doesn't scan a list of 8 near-identical tool names; it scans one, and the enum documents what values are valid.
|
|
70
|
-
|
|
71
|
-
The same plugin also registers a zero-arg `myplugin-tickets/get-queue-summary` ability (open count + average response time + oldest unresolved) because "how is the queue today?" is the highest-frequency support-team question and the backing endpoint takes no arguments. That one ability has outsized value for one line of registration code — rule 4 in action.
|
|
72
|
-
|
|
73
|
-
## Escape hatch — when per-operation granularity is right (for reads)
|
|
74
|
-
|
|
75
|
-
Two cases where one-ability-per-operation IS appropriate on the read
|
|
76
|
-
side, despite the recommendation against REST-atomization for reads:
|
|
77
|
-
|
|
78
|
-
1. **Genuinely different permission models.** If `list-<resource>` and `search-<resource>` require different capabilities or different confirmation flows, splitting is honest.
|
|
79
|
-
2. **Different destructive / idempotent annotations.** An ability that both reads and writes cannot honestly declare `readonly: true`; split the read-only part into its own ability.
|
|
80
|
-
|
|
81
|
-
For writes, this escape hatch isn't needed: rule 2 ("one state
|
|
82
|
-
transition per ability") already establishes per-operation granularity
|
|
83
|
-
as the default. Splitting `submit-evidence` and `close-resource` into
|
|
84
|
-
separate abilities isn't an exception — it's rule 2 in action.
|
|
85
|
-
|
|
86
|
-
## Related references
|
|
87
|
-
|
|
88
|
-
- `domain-vs-projection.md` — granularity governs *domain-layer* decisions; that reference covers the projection layer where token-efficiency tradeoffs and consumer-shape choices live.
|
|
89
|
-
- `shared-core-service.md` — implementation mechanism for keeping abilities, REST handlers, CLI commands, and UI in lockstep on the domain layer.
|