Conversation
Exploratory. Nothing reads these files yet; they exist to find out whether recommendations can be expressed as data rather than PHP, and what the format would have to carry. All 49 existing providers written out as markdown, which produced 42 files: every Yoast recommendation has an All in One SEO twin expressing the same goal, and once a rule states an outcome instead of a setting, the pair collapses into one. That collapse is the first evidence the approach works -- one rule, no plugin named, and a model works out how to satisfy it on whatever the site actually has. Two findings shaped the format. verified_by is the load-bearing field. Thirty rules the site can answer for itself by reading an option, querying content or fetching a URL. Twelve it cannot: d/m/Y and m/d/Y are both valid date formats, and which is right depends on who reads the site. For those the goal is that the owner has looked once, which is exactly what the existing PHP checks by consulting the activity log. That is not a missing state check; it is the correct check for the goal. Structured fields are read by the model, not by PHP. The set produced 13 applies_when predicates and 10 target.find keys, most used once, which would be runaway DSL growth if something had to interpret them. Nothing does. The structure is precision for the model -- unambiguous and close to the query it will build -- so a new key costs nothing and needs no release. It also means incoming_internal_links is expressible at all, which it would not be in PHP without reading an SEO plugin's link index. The validator checks what review misses: that the prose and the frontmatter agree. It found six files where verified_by said owner_confirmation while the How to verify section described a perfectly good check, because the two axes -- can the site tell, and should a person decide -- had been conflated. It deliberately does not check the applies_when or target.find vocabulary, since constraining those would reintroduce the coupling the format exists to avoid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tjxa3zi5Z7eTKWoaxB3HGT
Turns the rules in /recommendations into real tasks so the format can be exercised end to end without a server: drop a file in, see the task appear, have a model act on it. Off unless a site opts in via the PROGRESS_PLANNER_MARKDOWN_RECOMMENDATIONS constant or the matching filter -- this is a harness, not a feature. The loader deliberately does not interpret the rule. applies_when and target.find are read by the model, and building an interpreter here would recreate the coupling the format exists to remove. The single exception is any_plugin_active, checked before a task is created: without it a bare site collects every SEO rule it can never satisfy and the model spends a call discovering that. Detection reuses the constants and classes the SEO data collector already knows, so the two cannot disagree about what "Yoast is active" means. Verified on a live site: 42 rules parse, 36 become tasks, a second run creates none, and a rule requiring only an inactive plugin is correctly skipped. The nine SEO rules list both Yoast and AIOSEO as alternatives, so one being active satisfies them all. Two things the testing corrected. The provider prefix was md: and became md-, because the provider ID ends up as a taxonomy term slug and sanitize_title() silently drops a colon -- md:foo stored as mdfoo and broke every prefix check downstream. And the summary pattern terminated on a newline, so it matched nothing on a wrapped paragraph, which is every paragraph. Rules now name Yoast by the slug the plugin itself uses, wordpress-seo, rather than yoast-seo, so no alias table is needed anywhere. Known gap: list-recommendations drops any task whose provider class is missing, which is every task created here. Fixing that belongs with the abilities work on the other branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tjxa3zi5Z7eTKWoaxB3HGT
Each rule now becomes a Markdown_Rule provider, registered through the same progress_planner_suggested_tasks_providers filter third parties already use. The previous commit wrote tasks straight into the database with nothing behind them, which left every consumer needing a special case for "a task whose provider does not exist" -- the abilities layer drops exactly those, so all 36 were invisible to a model. Registering a provider removes the special case rather than patching around it: the dashboard renders these, points and badges count them, capability filtering applies, and the abilities layer lists them with no change at all. Verified: 36 visible, none dropped. Everything the interface asks for comes from the rule's frontmatter. The two methods that cannot are evaluate_task() and is_task_completed(), which always return false. A goal like "attachment URLs must not be indexable" is satisfied by a redirect on one site and a header on another, which is why the rule is prose for a model rather than a condition for PHP, so completion arrives from outside and is never inferred here. Verified on a live site: 42 providers built, Tasks_Manager holds 83 without complaint, 36 tasks inject through the normal pipeline, and each resolves back to its provider. The six that do not inject are the per_item templates, which need a model to find their targets first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tjxa3zi5Z7eTKWoaxB3HGT
A recommendation defined in markdown states an outcome and leaves the method open, because the method depends on which plugins the site runs and what its settings already say. Until now none of that survived the trip: prepare() returned title, description and url, so a goal arrived looking like any other one-line recommendation and the goal, the way to verify it and the bounds stayed on the server. The caller was being asked to satisfy something it could not read. So a recommendation now carries a goal object when it has one, holding the instructions as prose plus the three flags a caller has to act on: whether the site can verify the result itself or only a person can, whether the change can be undone, and whether to ask first. Only those are sent. The raw frontmatter is left out because it duplicates what is already there and exposes how the file happens to be parsed, and id, title and points are left out because they are already top-level fields. Recommendations the plugin knows how to apply are unchanged: the key is absent rather than empty, so its presence is the signal that the caller has to work the problem out instead of calling complete-recommendation. Verified on the test site: of 52 recommendations, the 28 from markdown carry a goal and the 24 backed by PHP providers carry exactly the nine fields they did before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A recommendation that states a goal has had no way to be finished. The other completion ability applies one the plugin knows how to satisfy: it looks the provider up in a table and writes the option named there. A goal has no entry, because the way to satisfy it depends on which plugins the site runs, so the caller does the work and the recommendation stays open no matter what the site looks like afterwards. This closes that. Completion is the same event either way: it calls the method the email link calls, so one activity row is written and points, badges, streaks and the score move exactly as they do when a person clicks the button. WHAT IT TRUSTS A goal verifiable from the site's own state is taken on trust, because the caller can fetch the URL or read the option and so can anyone checking afterwards. One marked `verified_by: owner_confirmation` is not: the answer lives somewhere the caller cannot reach -- whether an email arrived is the case that prompted it -- so it is refused unless the caller states the owner confirmed it. That asymmetry is the whole trust model. There is deliberately no audit trail here; what an agent did belongs at the layer every tool call passes through, not in each plugin that offers one. Two details worth knowing: - Completion sets the status to `pending`, which is what completion means for a recommendation and what the email link sets. Not `trash`: a trashed task cannot be read back, so a second call would report the recommendation missing rather than already done. - The guard against completing twice reads the stored post status rather than the task object's copy. update_recommendation() does not invalidate the cache the object is rebuilt from, so within one request the object still reports the status the task had before it was completed, and a second call would score it again. Verified on the test site: completing a goal moved the score 58 to 59 and wrote one activity row; a second call in a new request returned already_completed with no second row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
Test on Playground |
Contributor
✅ Code Coverage Report
✅ Coverage meets minimum threshold (40%) 🎉 Great job maintaining/improving code coverage! 📊 File-level Coverage Changes (37 files)🆕 New Files
📈 Coverage Improved
ℹ️ About this report
|
Contributor
🔍 WordPress Plugin Check Report
📊 Report
|
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
103 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information. |
📁 classes/suggested-tasks/providers/class-content-review.php (4 warnings)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
232 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information. |
377 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information. |
381 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information. |
388 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information. |
📁 classes/activities/class-query.php (2 warnings)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
71 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $table_name used in $wpdb->query()\n$table_name assigned unsafely at line 58. |
163 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $where_args used in $wpdb->get_results()\n$where_args assigned unsafely at line 153. |
📁 classes/suggested-tasks/data-collector/class-yoast-orphaned-content.php (1 warning)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
111 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $query used in $wpdb->get_row()\n$query assigned unsafely at line 98. |
📁 classes/suggested-tasks/data-collector/class-terms-without-description.php (1 warning)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
108 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $query used in $wpdb->get_results()\n$query assigned unsafely at line 106. |
📁 classes/suggested-tasks/data-collector/class-terms-without-posts.php (1 warning)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
120 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $query used in $wpdb->get_results()\n$query assigned unsafely at line 118. |
🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check
A goal-shaped recommendation had nothing a person could click. There is no popover, because there is no single setting to change, and the task carried no actions, so someone reading the dashboard saw a recommendation they could not act on or clear. Only an agent could finish one. The dashboard already offers "Mark as complete" on any dismissable task, so this is the provider opting into it rather than anything new: one property, no changes to shared code, and the button appears where the other actions already do. A person marking it done and an agent calling the ability now end in the same place -- one activity row in the same category, the same points -- which is what makes the two ways of finishing a goal equivalent rather than parallel. Verified in the browser: "Mark as complete | Snooze | Info" renders on a markdown task, clicking it strikes the title through, and the completion writes exactly one suggested_task activity row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Snooze came with the base provider, so a goal-shaped recommendation offered it without anything behind it: a caller can list snoozed recommendations but cannot snooze one, and deferring is a decision a caller has more reason to make than a person does. A goal that cannot be satisfied on this site -- the plugin it needs is not installed, the rule puts it out of bounds -- should be put aside rather than retried every day. Until there is an ability for that, the button let a person hide a recommendation in a way an agent could neither see the reasoning for nor do itself. "Mark as complete" is now the only action, and it means the same thing whoever uses it. Verified in the browser: a markdown recommendation shows "Mark as complete | Info", while the recommendations backed by PHP providers keep their Snooze. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An agent satisfied a goal, verified it, and then could not close it. It called complete-recommendation, which looks up the provider in the fix table, finds nothing, and answers "manual: this needs a person, open the link to handle it" -- with an empty link, because a goal has no admin screen. Nothing in the answer said another tool existed, so a correctly completed piece of work stayed pending. Three changes, all in what a caller reads: - complete-recommendation now answers "is_a_goal" for a goal and names complete-server-recommendation, instead of claiming it needs a person. - Its own description and the goal field both say which tool finishes a goal, so the route is discoverable before a call is made as well as after one fails. The same run turned up something worse. Asked to turn off comment pagination, the agent found no ability could read page_comments, and used a general-purpose PHP executor instead. It got the right answer, by exactly the means the ability surface exists to prevent. Out of bounds in each rule bounds the outcome -- leave the comments, do not detach the media -- and nothing bounded the means. So the goal field now carries the one bound that holds for every rule: use purpose-built tools only, and when nothing offered can make the change, stop and report it and leave the recommendation open. It lives there rather than in forty-two files because a rule repeated forty-two times is one that eventually gets left out of the forty-third. A refusal is the site saying the change is not the caller's to make. Working around one produces an unreviewable change nobody approved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…prototype-md-abilities # Conflicts: # classes/abilities/class-schemas.php
…prototype-md-abilities
…prototype-md-abilities
With the prototype on, every markdown rule became a recommendation of its own next to the PHP one it described: "Set the site tagline" showed twice, one copy the plugin could apply and one goal. The goal copies never closed on their own -- is_task_completed() is always false and applies_when is not evaluated -- so they sat on the dashboard on sites that already met them, and an agent saw both copies with no sign they were the same work. Each rule now names the PHP providers it describes under `replaces:`. All 42 do: 25 by the same ID, the rest mostly as a Yoast and an AIOSEO provider for one goal. A rule with `replaces:` is not registered as a provider; its goal is attached to those providers in list-recommendations, and the PHP provider keeps deciding when the task is shown and when it is done. A rule without `replaces:` still becomes its own provider -- the long-term direction, a recommendation defined entirely in markdown. A recommendation complete-recommendation can apply gets no goal: a fix known to work beats one the caller has to work out, and the goal's presence is what tells a caller to use complete-server-recommendation. The plugin now registers the loader itself when the constant or filter is on. Before, nothing hooked it, so enabling the prototype on a fresh site did nothing. bin/validate-recommendations.php rejects a `replaces:` entry that names no existing provider, which would otherwise silently drop the goal. docs/recommendation-format.md describes the mapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ability description told callers it returns "manual" for a goal recommendation. It returns "is_a_goal", and has since goals were routed to complete-server-recommendation. An agent reading the description would look for a status it never gets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
complete-server-recommendation kept its own copy of the completion steps: set pending, flush the task cache, insert the activity. The same three steps now live in Suggested_Tasks::mark_completed(), which the admin_init path and complete-recommendation use, so every route completes a task the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MCP exposure test lists the abilities by name, and this branch has one the base branch does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prototype, not for merge. Based on #782, so merge that first if any of this is worth keeping. Off unless a site opts in.
A recommendation that states a goal instead of a procedure, so an AI agent can satisfy it whatever the site runs.
Why
complete-recommendation(#782) works from a table: provider ID in, option written. That covers 22 recommendations and cannot cover the rest, because the way to satisfy them depends on which plugins are active. "Attachment pages must not be indexable" is a redirect on one site, a robots meta tag on another, anX-Robots-Tagheader on a third.So the recommendation describes the outcome and leaves the method open. The agent reads it, works out how, does it, verifies, and says so.
How it works
42 rules in
/recommendations: markdown with frontmatter, one per existing recommendation. The body has five sections: why it matters, the goal, how to verify it, hints for common setups, and what to leave alone.Rules add a goal to the recommendations the plugin already has. Each rule lists the PHP providers it describes under
replaces:. 25 use the same ID. Most of the rest name a Yoast provider and an AIOSEO provider for the same goal, e.g.date-archives-not-indexedcoversyoast-date-archiveandaioseo-date-archive. The PHP provider still decides when a recommendation is shown and when it is done, so nothing appears twice, and a goal the site already meets never shows up. The dashboard looks exactly as it does without the prototype. The difference is only visible to an agent.goalinlist-recommendations. A recommendation with a rule carries the instructions plusverified_by,reversibleandneeds_confirmation. Ifgoalis present, the caller has to work out the method itself. It is left off recommendationscomplete-recommendationcan apply, because a fix known to work beats one the caller has to work out.Two ways to complete:
complete-recommendationapplies a vetted fix. Called on a goal, it returnsis_a_goaland names the right ability.complete-server-recommendationmarks a goal done after the agent satisfied and verified it. It refuses anythingcomplete-recommendationcan apply.Both record completion through the same
Suggested_Tasks::mark_completed()the dashboard uses, so points, badges and the score move exactly as when a person completes the recommendation.Long-term direction, still in place: a rule without
replaces:still becomes a recommendation of its own throughprogress_planner_suggested_tasks_providers, i.e. a recommendation defined entirely in markdown. No bundled rule uses that path yet.bin/validate-recommendations.php, run in CI as "Validate recommendations", checks each rule's frontmatter and sections, and rejects areplaces:ID that names no existing provider. The format and the mapping are documented indocs/recommendation-format.md.What it trusts
A goal that can be verified from the site's own state is taken on trust. The agent can fetch the URL or read the option, and so can anyone checking afterwards. A goal marked
verified_by: owner_confirmationis not: whether an email arrived isn't visible from the site, so completing it is refused unless the caller states that the owner confirmed it.No audit trail here. What an agent did belongs at the layer every tool call passes through, not in each plugin that offers one.
How to test
Setup
wp-config.php:progress_planner_markdown_recommendationsfilter. No other wiring is needed.meta.mcp.public, which comes from Expose site score and recommendations as WordPress abilities #782.claude mcp add …, then/mcpto confirm).The Playground link doesn't cover this. It shows the dashboard, which looks the same with or without the prototype, and an agent can't connect to a Playground site.
What to try
md-*recommendations and no duplicates;goalonly on recommendations withfixable: falsecomplete-recommendationand getscompleted. The recommendation leaves the list immediately, without opening wp-admincompletedin the same callcomplete-server-recommendation; the score goes up by 1wp-config.php, so it should stop and say so rather than work around itThen reload the Progress Planner dashboard. Recommendations the agent completed should celebrate and disappear.
Verified
On a local site through the MCP adapter, with a separate agent driving it:
md-*or duplicate recommendations;goal;completedand left the list in the same session;CS at default severity, PHPStan, the validator, and 526 tests on single site and multisite.
Known gaps
needs_confirmationis advice, not enforced. Onlyverified_by: owner_confirmationis checked by the site; asking the owner first depends on the agent.default_category, or uploading a logo. Uploading a local file is an open issue in wp-agent-abilities.reversibleandneeds_confirmationare"true"/"false", not booleans.md-*recommendations stuck as waiting for a celebration, and extra provider terms. A fresh site has neither.