Skip to content

Prototype: recommendations that state a goal, for AI agents - #788

Open
ilicfilip wants to merge 17 commits into
filip/abilities-apifrom
filip/prototype-md-abilities
Open

ilicfilip wants to merge 17 commits into
filip/abilities-apifrom
filip/prototype-md-abilities

Conversation

@ilicfilip

@ilicfilip ilicfilip commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

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, an X-Robots-Tag header 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-indexed covers yoast-date-archive and aioseo-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.

goal in list-recommendations. A recommendation with a rule carries the instructions plus verified_by, reversible and needs_confirmation. If goal is present, the caller has to work out the method itself. It is left off recommendations complete-recommendation can apply, because a fix known to work beats one the caller has to work out.

Two ways to complete:

  • complete-recommendation applies a vetted fix. Called on a goal, it returns is_a_goal and names the right ability.
  • complete-server-recommendation marks a goal done after the agent satisfied and verified it. It refuses anything complete-recommendation can 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 through progress_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 a replaces: ID that names no existing provider. The format and the mapping are documented in docs/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_confirmation is 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

  1. A site on WordPress 6.9+ (it needs the Abilities API), running this branch.
  2. Turn the prototype on in wp-config.php:
    define( 'PROGRESS_PLANNER_MARKDOWN_RECOMMENDATIONS', true );
    Or use the progress_planner_markdown_recommendations filter. No other wiring is needed.
  3. An MCP bridge that exposes WordPress abilities, e.g. AI Connector with the MCP adapter. The abilities set meta.mcp.public, which comes from Expose site score and recommendations as WordPress abilities #782.
  4. Connect an agent to it, e.g. Claude Code (claude mcp add …, then /mcp to 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

Ask the agent Expected
"List my Progress Planner recommendations and say which you can do yourself" No md-* recommendations and no duplicates; goal only on recommendations with fixable: false
"Set my site timezone to Europe/Ljubljana" It uses complete-recommendation and gets completed. The recommendation leaves the list immediately, without opening wp-admin
"Set the About page" (give it a page) completed in the same call
"Rename the Uncategorized category" It reads the goal, renames the category with a term tool, verifies, and calls complete-server-recommendation; the score goes up by 1
"Mark 'Select your site locale' as done" Refused until you confirm, because only the owner can verify it
"Fix the PHP errors recommendation" No tool can edit wp-config.php, so it should stop and say so rather than work around it

Then 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:

  • all four abilities are discoverable, and there are no md-* or duplicate recommendations;
  • fixable recommendations never carry a goal;
  • timezone, date format, a Yoast setting and the About page each returned completed and left the list in the same session;
  • a goal (renaming "Uncategorized") completed end to end;
  • the wrong-route and missing-confirmation calls were refused;
  • the score went from 52 to 57, one point per completion, and the dashboard celebrated each one.

CS at default severity, PHPStan, the validator, and 526 tests on single site and multisite.

Known gaps

  • needs_confirmation is advice, not enforced. Only verified_by: owner_confirmation is checked by the site; asking the owner first depends on the agent.
  • Goals can ask for more than an agent's tools can do. Examples: reading default_category, or uploading a logo. Uploading a local file is an open issue in wp-agent-abilities.
  • Only tested live with Yoast. The AIOSEO mappings are covered by the validator but haven't been exercised on a live site.
  • Large responses. A 29-recommendation list is about 52 KB because the full goal text is inlined. Completed recommendations still carry their goal too.
  • Some fields are strings. reversible and needs_confirmation are "true" / "false", not booleans.
  • Leftovers from earlier builds of this prototype. Sites that ran them can have md-* recommendations stuck as waiting for a celebration, and extra provider terms. A fresh site has neither.
  • The rules are bundled on disk. The real source would be the Progress Planner server.

ilicfilip and others added 5 commits September 24, 2026 12:52
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>
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Test on Playground
Test this pull request on the Playground
or download the zip

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

✅ Code Coverage Report

Metric Value
Total Coverage 41.81% 📊
Base Coverage 38.46%
Difference 📈 3.35%

✅ Coverage meets minimum threshold (40%)

🎉 Great job maintaining/improving code coverage!

📊 File-level Coverage Changes (37 files)

🆕 New Files

Class Coverage Lines
🟢 Progress_Planner\Abilities\Server_Recommendations 100.00% 51/51
🟡 Progress_Planner\Suggested_Tasks\Markdown_Recommendations 64.21% 61/95
🟢 Progress_Planner\Suggested_Tasks\Providers\Markdown_Rule 81.25% 13/16

📈 Coverage Improved

Class Before After Change
Progress_Planner\Suggested_Tasks\Providers\Debug_Display 0.00% 100.00% +100.00%
Progress_Planner\Suggested_Tasks\Providers\Php_Version 0.00% 50.00% +50.00%
Progress_Planner\Suggested_Tasks\Providers\Remove_Inactive_Plugins 0.00% 37.50% +37.50%
Progress_Planner\Suggested_Tasks\Providers\Content_Review 0.00% 30.19% +30.19%
Progress_Planner\Suggested_Tasks\Providers\Reduce_Autoloaded_Options 0.00% 23.08% +23.08%
Progress_Planner\Suggested_Tasks\Providers\Core_Update 0.00% 22.73% +22.73%
Progress_Planner\Suggested_Tasks\Providers\Content_Create 0.00% 18.75% +18.75%
Progress_Planner\Suggested_Tasks\Providers\Set_Valuable_Post_Types 0.00% 17.57% +17.57%
Progress_Planner\Suggested_Tasks\Providers\Set_Date_Format 0.00% 16.52% +16.52%
Progress_Planner\Suggested_Tasks\Providers\Set_Page_About 0.00% 14.29% +14.29%
Progress_Planner\Suggested_Tasks\Providers\Set_Page_Contact 0.00% 14.29% +14.29%
Progress_Planner\Suggested_Tasks\Providers\Set_Page_FAQ 0.00% 14.29% +14.29%
Progress_Planner\Suggested_Tasks\Providers\Improve_Pdf_Handling 0.00% 9.76% +9.76%
Progress_Planner\Suggested_Tasks\Providers\Traits\Dismissable_Task 0.00% 7.58% +7.58%
Progress_Planner\Suggested_Tasks\Providers\Unpublished_Content 5.62% 12.36% +6.74%
Progress_Planner\Suggested_Tasks\Tasks_Manager 68.14% 73.91% +5.77%
Progress_Planner\Suggested_Tasks\Providers\SEO_Plugin 0.00% 5.17% +5.17%
Progress_Planner\Suggested_Tasks\Providers\Disable_Comment_Pagination 0.00% 4.76% +4.76%
Progress_Planner\Suggested_Tasks\Providers\Remove_Terms_Without_Posts 18.83% 23.38% +4.55%
Progress_Planner\Suggested_Tasks\Providers\User 4.55% 9.09% +4.54%
Progress_Planner\Suggested_Tasks\Providers\Search_Engine_Visibility 30.43% 34.78% +4.35%
Progress_Planner\Suggested_Tasks\Providers\Email_Sending 7.50% 11.25% +3.75%
Progress_Planner\Suggested_Tasks\Providers\Update_Term_Description 21.12% 24.84% +3.72%
Progress_Planner\Suggested_Tasks\Providers\Tasks 36.59% 40.24% +3.65%
Progress_Planner\Suggested_Tasks\Task 23.33% 26.67% +3.34%
Progress_Planner\Suggested_Tasks\Providers\Collaborator 0.00% 3.23% +3.23%
Progress_Planner\Suggested_Tasks\Providers\Sample_Page 0.00% 2.56% +2.56%
Progress_Planner\Suggested_Tasks\Providers\Select_Locale 23.40% 25.53% +2.13%
Progress_Planner\Abilities\Recommendations 87.40% 89.33% +1.93%
Progress_Planner\Suggested_Tasks\Providers\Rename_Uncategorized_Category 7.14% 8.93% +1.79%
Progress_Planner\Suggested_Tasks\Providers\Permalink_Structure 8.93% 10.71% +1.78%
Progress_Planner\Suggested_Tasks\Providers\Set_Page_Task 41.67% 43.33% +1.66%
Progress_Planner\Suggested_Tasks\Providers\Select_Timezone 19.70% 21.21% +1.51%
Progress_Planner\Abilities\Abilities 95.40% 96.08% +0.68%
ℹ️ About this report
  • All tests run in a single job with Xdebug coverage
  • Security tests excluded from coverage to prevent output issues
  • Coverage calculated from line coverage percentages

@github-actions

Copy link
Copy Markdown
Contributor

🔍 WordPress Plugin Check Report

⚠️ Status: Passed with warnings

📊 Report

🎯 Total Issues ❌ Errors ⚠️ Warnings
10 0 10

⚠️ Warnings (10)

📁 classes/suggested-tasks/data-collector/class-unpublished-content.php (1 warning)
📍 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

ilicfilip and others added 12 commits September 24, 2026 13:58
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
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>
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