AcrossAI Abilities Manager - full changelog
===========================================

The complete release history, kept here because WordPress.org caps the readme's
Changelog section at 5,000 words. The readme carries the most recent releases;
this file carries everything, in full, and is shipped inside the plugin.

= Unreleased =

(nothing yet)

= 0.0.34 - 2026-09-18 =

This is the largest release so far: 25 features, 19 new tabs and around 400 new abilities.
The theme is reach and honesty - most of the popular plugins a site actually runs can now be
driven directly, each behind this plugin's own permission floor, and every ability that cannot
do something says why and names the route that works instead.

**Breaking changes**

* **Breaking: abilities now require administrator rights unless you say otherwise.** If anyone below administrator drives this site through an AI client — a shop manager running a store, for example — they lose access on update until an administrator grants it. Set a rule on the individual ability under User Access, or move the site-wide floor with the `acrossai_default_ability_capability` filter.
* **Why: every plugin chose its own lock, and nobody was checking them.** Measured across the abilities installed on one site: three registered with no permission check at all, two were open to any logged-in subscriber, and one that *writes content* was open at contributor level. This plugin now decides who may run an ability, whoever registered it, and the answer comes from one place you can see and change.
* **Setting access used to be able to remove the lock.** Choosing "Everyone" on an ability replaced its built-in check with one that allowed anybody — an action that reads as tightening actually opened the door. Access rules now sit on top of a floor that cannot be removed by accident, and when the rule system cannot reach an answer it denies rather than allows.
* **The tools that route to abilities are deliberately exempt**, because their permission check does something a capability test cannot: it refuses an ability you have hidden from that server. Leaving those alone keeps "is this offered here" and "who may run it" as two separate questions. The abilities they route to are still gated, so nothing slips through.
* **The ability screen now says what no rule means.** It used to read "No user access added by admin", which sounded like no restriction; it now states the capability required.

**Site recovery**

* **New Backups tab - can this site be recovered? Nine abilities.** Whether a backup exists, when it last ran and whether it worked, what sets exist and what each contains, whether the archives are reachable over HTTP, and taking, labelling, deleting or restoring one. Ask this before any bulk or destructive change: it is the question of whether the change can be undone.
* **One set of abilities over two backup plugins.** UpdraftPlus and All-in-One WP Migration answer in the same shape, so "is this site backed up?" is a question about the site rather than about a vendor. Where the two genuinely differ, the difference is reported rather than papered over - and where one cannot do something, the reason says so and names the route that still works.
* **A backup archive that anyone can download is a total compromise, and this checks for it.** An archive holds the entire database: every password hash, every stored key, every customer address. Both plugins drop a .htaccess in their backup directory to prevent that, and on nginx, IIS and Caddy that file is never read - so the protection is present, looks correct, and does nothing. `Check Backup Exposure` asks the running web server for the real URL and reports the real status code instead of assuming. On the site this was built against it found both directories being served.
* **Starting a backup does not hold up the request.** The job is queued and a job identifier returned, so progress can be followed with `Get Backup Progress` while the backup runs in the background. A backup of a large site takes minutes to hours; an ability that waited for it would simply time out.
* **Restoring says plainly that it cannot be undone.** A restore replaces the current site with the backup, and everything written since - orders, posts, users, settings, uploads - is permanently gone. The confirmation says so in those words, and the response records what the site looked like beforehand, so what was given up is visible rather than left to be inferred. If the filesystem is not directly writable, the restore is refused up front rather than stopping half-way through with the site part-replaced.
* **Deleting the last backup tells you it was the last.** "One file removed" and "this site can no longer be restored" are different pieces of news.
* **No storage credential is ever read.** Remote backup destinations are reported by name and nothing else, because a destination keeps its access tokens in the same structure as its name.

**Store**

* **New WooCommerce tab — the abilities WooCommerce already ships, plus store health.** WooCommerce registers seven abilities of its own for finding products and orders, creating and updating products, changing order status and adding order notes. They were landing in no tab and no tool, so nothing could find them. They now appear on a WooCommerce tab with this plugin's permission floor applied. Nothing is re-registered; they are still WooCommerce's.
* **A store health check that explains why a change did not take effect.** WooCommerce keeps derived copies of its data that the shop actually reads — a lookup table behind product search and price sorting, a separate price field worked out from the regular and sale prices, and cached on-sale and featured lists. `Get Store Status` reports whether those copies are current, whether orders are kept in the modern order tables, and how many products are missing from the lookup table. That last number is how a silent corruption shows up: those products still look right in the admin list while being invisible to search and sorting.
* **Payment gateway settings are never readable.** The check reports which gateways exist and whether each is switched on, and nothing more — a gateway stores its API keys and webhook secrets in the same place as its name, so anything returning its settings would hand those out. Connecting a gateway stays a manual job.
* **Note that this tab has two permission models**, and it says so: WooCommerce's own abilities admit a shop manager, while everything this plugin adds requires an administrator.

* **WooCommerce catalogue, pricing and stock — 13 more abilities.** Everything WooCommerce's own abilities leave out: weight and dimensions, tax and shipping class, categories and tags, images, attributes, variable products and their variations, scheduled sales, bulk price changes, and stock. WooCommerce's own abilities still handle name, SKU, price, description and status; nothing here duplicates them.
* **Variable products can now be created at all.** WooCommerce's own create ability cannot make one — its list of types does not include them — so a product that comes in sizes or colours could not be built. `Create Variable Product` makes the product, and `Generate Variations` builds every combination of its options, showing you what it would create before it creates anything.
* **Stock changes go to the record that actually holds the number.** A variation's stock is often managed by its parent product, and writing to the variation instead changes a row nothing reads — the most common silent inventory mistake there is. `Get Stock` says plainly which record is in charge, and `Adjust Stock` always updates the right one and reports the level before and after.
* **A sale that would not have worked is refused rather than quietly ignored.** WooCommerce discards a sale price that is not below the regular price, so setting one would previously have reported success and changed nothing. And because sales are applied through WooCommerce's own save, the cached on-sale list is refreshed immediately rather than staying stale for up to thirty days.
* **Bulk price changes preview first.** Nothing is changed until you say so: the first call reports every product with its old and new price, and the change needs both an explicit go-ahead and a product list or category. There is deliberately no way to reprice an entire store in one call, and both bulk operations are capped.

* **WooCommerce orders, customers and sales figures — 10 more abilities.** Read an order in full including its lines, refunds and coupons; read the notes on it, which WooCommerce could add but never read back; record refunds; create and update customers, which WooCommerce offers nothing for at all; and get revenue, best sellers and order counts for any period.
* **Personal data is withheld unless you ask for it.** Most questions about an order — what was bought, what it cost, has it been refunded — need nobody's name, so names, addresses, emails and phone numbers are left out by default and the response lists what it held back. The customer's IP address and browser are never returned at all.
* **Exporting customers takes more than being an administrator.** It needs the request to be explicit, a written reason that is recorded alongside who asked and how many records were released, a confirmation, and a capability beyond administrator — because administering a site is not on its own a reason to release a list of named people with their addresses. It is capped, paged, and never includes password hashes, session tokens or the references payment providers use to charge stored cards.
* **No password is ever set by an assistant.** An account whose password an assistant chose is not the customer's account, so a random one is generated, never returned, and the customer sets their own through the usual reset.
* **A refund says plainly that no money moved.** It is recorded against the order and restores stock if asked, but returning funds is done in the payment provider's own dashboard — so the response says so rather than letting anyone assume the customer has been paid. Refunding more than remains is refused.
* **Sales figures always say where they came from.** They are worked out from the orders themselves rather than the analytics summary tables, which can be empty or part-way through an import and would otherwise give a confidently wrong answer. Only processing and completed orders count as revenue.

* **WooCommerce coupons, tax, shipping and store settings — 10 more abilities.** Coupons can be listed, read, created and updated. Tax rates and classes, shipping zones and shipping classes can be read, and a tax rate added. Store settings — currency, weight and dimension units, stock thresholds, the shop pages, guest checkout, reviews — can be read and changed.
* **Settings are a named list, and anything outside it is refused by name.** WooCommerce keeps hundreds of options under one prefix, and a payment gateway's API keys and webhook secrets sit among them. Only nineteen settings can be written, and asking for any other one comes back saying which one was refused rather than quietly dropping it — a silently ignored setting looks exactly like one that worked.
* **A tax rate added while tax calculation is switched off says so.** The rate is stored correctly and will never be applied to anything, which looks identical to success. The response states it plainly and names the setting that turns calculation on.
* **Tax and shipping report; they never advise.** What a rate ought to be is a question about a business and its jurisdiction, not about this software, and a confident wrong answer there under-charges tax for a year before anyone notices. The abilities describe what is configured and leave the judgement where it belongs.
* **Shipping explains the failure people actually hit.** A store with zones but no Rest of the World zone silently cannot be checked out from by anyone outside them, which reads as a broken cart rather than a missing zone, so its absence is called out.
* **A coupon code that already exists is refused, and so is updating one that does not.** Creating over an existing code would make a second coupon with the same code, which WooCommerce resolves unpredictably; updating a code that was never created would quietly make a new one. A discount type the store does not offer is refused rather than stored as a coupon that discounts nothing.

* **Fixed: editing a WooCommerce product or order through the generic content tools could silently corrupt the store.** Those tools accepted any post type, and nothing checked whether the record they were writing was the one the site actually reads. On a store the consequences were real and invisible: changing a price this way left the product's *displayed* price untouched, so the shop carried on charging the old amount while the tool reported success. Worse, saving the product properly afterwards did **not** fix it — WooCommerce saw the price field already changed, concluded nothing had happened, and never recalculated. Orders were worse still: modern stores keep them in their own tables, so a write went to a record nothing reads and WooCommerce eventually deleted it outright.
* **The tools now refuse the writes that cause this, and explain why.** Changing a product's custom fields, or writing an order at all, is declined with a message naming what would have gone stale and which ability to use instead. Editing a product's title, description or status still works normally — that genuinely is stored where you would expect. A new "Inspect Post Type" ability answers the question directly for any post type, and an override is available for anyone who has read the explanation and still wants the raw write.
* **Other plugins can register their own.** Anything that keeps its records outside the posts table, or derives data from them, can add itself to the same list through a filter rather than waiting for us to hear about it.

* **Three faults found by running the store abilities against a real shop.** Each was invisible to the test suite and only appeared under live use.
* **`Get Stock` could not answer the question it exists for.** Any product not tracking stock has no quantity at all, and the ability declared that number as always present — so asking "does this track stock?" did the work and then threw the answer away with a schema error. It now reports an absent quantity as absent.
* **A refusal sent people somewhere that could not help them.** Told a variation had no stock number to change, it pointed at WooCommerce's own product update — which refuses variations and variable products outright. Following the instruction produced a second refusal and no way forward. It now names `Update Variation`, which can switch stock tracking on, and for a variable parent it says plainly that no ability can do it rather than pointing at one that cannot.
* **The store health check called a corrupted product healthy.** It compared the search-and-sorting lookup table against the price field, but those two go stale together — so a product whose price had been written directly, and which the shop was charging the old price for, passed the check. It now also compares the charged price against the price it is supposed to be derived from, and says that saving the product again will not repair it, which is the part that catches people out. A product on sale still reports healthy, since its price correctly comes from the sale price.

**New integrations**

* **New Contact Form 7 toolset — 25 abilities.** Contact Form 7 registers no abilities of its own, so an assistant connected to a site running it could not read a form, let alone change one. It now gets its own tab and its own MCP tool, appearing only when Contact Form 7 is active, across five groups: **Forms** (list, read, find, create, update, duplicate, delete, and fetch a form's shortcode), **Fields & Template** (list a form's parsed fields and the field types this install supports, read and replace the raw template, add, update and remove individual fields), **Mail & Notifications** (read and edit both mail templates, switch the autoresponder on or off, list the mail tags a form offers, and check the tags a template uses actually resolve), **Validation Messages** (read and edit all 23), and **Settings & Validation** (read and edit the per-form behaviour switches, and run Contact Form 7's own configuration validator).
* **Writing a form field is the point.** Hand-writing form-tag markup is the fiddly part and where a model most reliably produces something broken, so `add-form-field` composes the tag, refuses a field type this install does not have, and refuses a name the form already uses; `remove-form-field` reports which mail templates still reference tags that now resolve to nothing.
* **Guarded harder than Contact Form 7 guards itself.** Every ability requires administrator rights rather than Contact Form 7's own capabilities, which it maps onto `publish_pages` and `edit_posts` — an Editor who can manage forms in wp-admin deliberately cannot manage them through an ability, because an ability is reachable by an AI client and wp-admin is not. Form markup and mail bodies are sanitised unconditionally, including for administrators, whom Contact Form 7 exempts. `additional_settings` is a whitelist of four keys, not a passthrough: it is the one property Contact Form 7 does not sanitise at all, and `skip_mail` silently stops a form emailing anything while it keeps reporting success.
* **Irreversible operations ask first.** Deleting a form bypasses the trash entirely, so it needs `confirm: true`, as does removing a field. Changing a mail recipient redirects every future submission, and changing `skip_mail` or `demo_mode` changes whether the form delivers at all — both confirm as well, but only when the change is actually one of those.

* **New LiteSpeed Cache toolset — 61 abilities.** LiteSpeed Cache registers no abilities of its own, so an assistant connected to a site running it could not clear a cache, read a setting, or say why a page was stale. It now gets its own tab and its own MCP tool, appearing only when LiteSpeed is active, across eight groups: **Purging & Cache Control** (purge by target, URL, post, taxonomy or raw cache tag, plus the automatic and scheduled purge rules), **Cache Settings** (the master switch, TTLs, what is cached for whom, the ten exclusion lists, vary rules and guest mode), **Page Optimisation** (CSS, JS, HTML, fonts, tuning, resource localisation and the exclusion lists that hold one broken script back without turning the feature off), **Media & Lazy Load**, **Crawler** (status, the crawler variants, run, reset and configuration), **Database** (what a cleanup would remove), **Object & Browser Cache** (including a connection test) and **Presets & Diagnostics** (apply and roll back the five shipped presets, export the configuration, read LiteSpeed's environment report).
* **One ability per job, not one per variant.** `purge-cache` takes a target (all, page cache, object, opcode, generated CSS/JS, unique CSS, front page) rather than shipping seven purge abilities; the exclusion abilities take a type and an add/remove/replace mode rather than shipping twenty-three. Expressed one-ability-per-variant the same surface would be roughly ninety, and an assistant would have to choose between near-identical tool names.
* **Settings are written the way LiteSpeed writes them.** Every change goes through LiteSpeed's own save path, so it also runs the conditional purge, the cron cleanup, the `.htaccess` rewrite and the CDN sync that make a setting actually take effect. Writing the option directly — the obvious shortcut — stores the value and changes nothing about how the site behaves. Each of the twelve settings writers accepts only the keys belonging to its own area, so a typo is refused by name rather than silently ignored.
* **Turning caching off asks first; turning it on does not.** Disabling the cache leaves the site working and simply stops caching it, with nothing on the front end to say so, so it needs confirmation. Applying a preset and restoring a backup confirm too — both rewrite the whole configuration, and LiteSpeed takes an automatic backup before a preset, which `litespeed/list-preset-backups` and `litespeed/restore-preset-backup` expose as the way back.
* **Database cleanup points at the abilities that can actually do it.** LiteSpeed's own cleanup cannot be run from an ability — it reads the request directly and ends in a redirect — so rather than leave a dead end, `litespeed/plan-database-cleanup` reports how many rows each cleanup type would affect alongside the ability that can remove them: `cache/delete-expired-transients` and `cache/flush-transients` for transients, `comments/delete-comment` for spam, trashed comments and trackbacks, and `database/delete-db-rows` for revisions, trashed posts, auto-drafts and orphaned meta. Rows are flagged safe or unsafe: the unsafe ones are raw row deletes that leave post meta behind and need a second pass. `litespeed/list-myisam-tables` likewise points at `database/convert-core-tables-to-innodb`.
* **Not included:** everything QUIC.cloud — image optimisation, critical and unique CSS generation, viewport images and CDN — because each needs a domain key and spends paid quota; direct `.htaccess` editing, because a bad write takes the whole site down; and the Cloudflare credentials.

* **New Advanced Custom Fields abilities — 16, joining the existing ACF tab.** ACF 6.8 added its own AI integration, but it covers the field *structure* and per-post-type record editing, not the field *values* on anything else. So an assistant could create a field group and could not set a value on a standard page, a theme's custom post type, a user, a term, a comment or an options page — nor touch a repeater row, nor an ACF block. These 16 close that gap, across three groups: **Field Values** (read one field or all of them, write one or many, clear one — on any of the five targets), **Repeater & Flexible Content** (add, patch, remove and reorder rows, and add or remove flexible-content layouts, all by 1-based index as ACF itself numbers them), and **ACF Blocks** (register a block, list what exists, read the fields behind one, insert an instance into a post, and patch its values).
* **Why this matters more than it sounds: writing ACF data as plain post meta destroys it, and looks fine while doing so.** ACF stores a complex field as several rows — a value row, a hidden field-key reference, and for a repeater one row per index per sub-field. Write the value on its own and ACF reads it back *correctly*, so everything appears healthy; the damage lands on the next operation. Verified on a live site: after a raw post-meta write of a two-row repeater, adding a third row left one row and silently deleted the other two. Every ability here goes through ACF's own API, which keeps those rows in step.
* **Free ACF gets 5 of the 16; ACF PRO gets all of them.** Repeaters and flexible content are PRO field types and ACF blocks are a PRO feature, so those 11 abilities only appear when PRO is the active edition — and say plainly that PRO is required if called anyway, rather than failing obscurely. The five field-value abilities work on either edition. Note the two editions cannot run together: PRO replaces free rather than extending it, and WordPress deactivates one automatically.
* **Adding the first row to an empty repeater now works.** ACF resolves a field by name through the hidden reference row it writes alongside a value, so a field that has never been filled in cannot be found that way — which made the most ordinary case of all, adding the first row, report that the field did not exist. These abilities fall back to ACF's own loose name lookup.
* **Fixed — the abilities inventory was under-reporting by 29.** `docs/abilities-inventory.md` recognised two ways an ability can declare its name and missed a third, silently skipping every ability whose class supplies its full slug rather than a suffix. The count moves from 537 to 566; 13 of those 29 were already shipped and simply undocumented.

* **New Yoast SEO toolset — 64 abilities, and Yoast's own two now have a home.** Yoast ships five abilities of its own, all of them about a single post's scores and metadata, and none of them reachable through a toolset — they landed in no tab and no MCP tool. Yoast SEO now gets its own tab that adopts them, plus 64 abilities covering everything they leave out, across eight groups: **Settings** (all fourteen option areas, plus discovery, export and import), **Content Types** (per post type and per taxonomy templates and archive rules), **Terms** (term SEO meta and primary terms — the gap Yoast's post-only abilities leave), **Indexables** (home page, post-type archives, author archives and the 404 and search pages), **Sitemaps** (status, index, settings, targeted and global invalidation), **Indexing** (status, counts, run, reset, cleanup), **Content** (cornerstone, internal links, orphaned pages, score summaries, keyphrase reuse) and **Tools** (site status, conflicting plugins, robots, first-time configuration, import status).
* **This suite works on staging; Yoast's own abilities do not.** Yoast disables its five whenever the environment is anything other than production, because indexables record permalinks and building them on a staging copy bakes in the wrong ones. That is right for indexables and wrong for settings, terms, sitemaps and tools, so these 64 do not inherit the gate — measured on a local install where Yoast's own abilities were absent and all 64 of these ran. The indexable abilities report an empty index rather than disappearing.
* **Settings keys are now read from the live site, not a snapshot.** Yoast mints a family of option keys for every registered post type and taxonomy — `title-{type}`, `noindex-tax-{taxonomy}`, `title-ptarchive-{type}` and so on. Deriving those once and freezing the result meant only the types present that day were writable: every custom post type and taxonomy was refused by name, and because no post type on that install had an archive, **no archive key existed at all** and the post-type-archive ability could not write anything on any site. They are now read from the live install on each request.
* **A settings write that Yoast rejects now says so.** Yoast validates per option group and silently keeps the old value when the new one fails — the schema and llms.txt keys accept only a fixed set of values, and anything else is dropped. The write path reported success regardless, naming a key it had not changed. Each key is now read back after saving, and a refused value is returned as an error naming what was requested, what Yoast kept, and which keys in the same call did apply.
* **Stored third-party credentials are not readable through these abilities.** Two keys in Yoast's options hold live OAuth access and refresh tokens for connected services. A settings reader that returns every key in an area would have handed those to any caller — which, over MCP, means off-site. This suite subtracts Yoast's own list of settings its admin screen refuses to render, which names those two along with several internal bookkeeping keys, and a test asserts the exclusion still matches Yoast's list so an upstream addition cannot quietly become readable.
* **Irreversible operations ask first.** Clearing a term's SEO meta, importing a settings export and resetting the indexing run all require `confirm: true`. Discouraging search engines confirms too — it noindexes the entire site, overriding every other setting — while allowing them back does not, since that protects nothing. Cleaning up orphaned indexables is deliberately not gated: those rows are derived data Yoast prunes on its own schedule.
* **Not included:** everything in Yoast SEO Premium — redirects, multiple keyphrases and internal-linking suggestions — because Premium is not installed here and anything gated behind it would ship unverified.

* **New Cookie Consent toolset — 22 abilities.** The cookies your site declares in its consent banner, the categories a visitor chooses between, the banner itself, the languages it is offered in, and Google Consent Mode. Only present when a supported consent plugin is active.
* **Filling in the cookie list is the job this actually solves.** Without a paid scanning service that list is typed in by hand, so it is usually empty — measured on a test site: five consent categories live, and not one cookie declared, which means the preference centre lists nothing under any of them. These abilities let an assistant fill it in from what the site actually loads.
* **Editing the consent tables directly leaves every visitor on the old banner.** The banner is rendered HTML, rebuilt only when the consent plugin's own save hooks fire — and nothing watches the tables. Measured: a row written straight to the database read back perfectly while the banner stayed byte-for-byte identical, so visitors kept seeing the previous version with no error anywhere. The same change made through these abilities refreshed it. Every write here goes through the plugin's own save path and then checks the banner really moved, and `Rebuild the Banner` repairs a site where something else has already written directly.
* **Features that need a paid account say so, and say what to do.** Consent statistics, pageview figures and cookie scan status are produced by the consent service, not by the plugin, so they are empty until the site is linked to an account. Those abilities refuse by name, point at the exact screen and button, and ask to be told once it is done — rather than failing vaguely and leaving an assistant to guess or retry. Everything else works without an account.
* **Linking the account is not something this plugin will do.** It authorises an external account, so it stays a person's decision. The stored API token and website key are never returned by any ability, and anything withheld from a settings read is listed so you can tell an empty setting from a hidden one.
* **It will not decide the law for you, or claim you are compliant.** Which privacy law a site answers to is a legal judgement about that site's situation. These abilities report the configuration and change it on request; nothing states or implies that using them makes a site compliant. Whether a category is "strictly necessary" is deliberately not changeable either — that is what decides if it loads before any consent is given.

* **New WPCode toolset — 21 abilities, plus a home for the five WPCode already had.** WPCode registers five read-only abilities of its own, and they landed in no tab and no MCP tool because nothing claimed their namespace. WPCode now gets its own tab that adopts those five, alongside 21 new ones covering what they leave out: the full snippet lifecycle for every code type, where each snippet is inserted, the conditional logic that decides when it loads, the site-wide header, body and footer scripts, snippet errors and safe mode, and installing from the WPCode library. The tab and its tool appear only on sites running WPCode.
* **Writing a snippet through the generic content abilities produces one that never runs.** Snippets are a custom post type, so they looked reachable already — but WPCode keeps a separate cache option that its loader actually reads, and only its own save routine rebuilds it. Write the post type directly and the snippet is correct in the database, reads back correctly, and does nothing, because the site keeps executing the old cache. Every ability here goes through WPCode's own save routine and then checks the cache really contains the snippet, since that, not the database row, is what decides whether it runs.
* **Snippet code no longer loses its backslashes.** WPCode hands a snippet's code straight to WordPress without escaping it first, which is safe from its own admin screen because form data arrives pre-escaped, and unsafe from anywhere else. Sent unescaped, WordPress strips one level of backslashes: a PHP namespace loses its separator, a newline escape becomes the letter n, and a regular expression stops matching. WPCode hit this itself and fixed it in one place only. These abilities escape every free-text field, and expose the same opt-out flag the rest of the plugin's writers use.
* **Activating a broken snippet now says so.** WPCode test-runs PHP and Universal snippets before switching them on and, if the code errors, quietly leaves the snippet switched off while still reporting a successful save. Reporting that as success would mean claiming a snippet is live when it is not, so the state is read back afterwards and a refusal is returned by name with the error WPCode recorded.
* **Clearing a snippet error used to be possible to get wrong silently.** WPCode's error setter ignores anything that is not a structured error, so the obvious way to clear one changes nothing while appearing to work. These abilities use WPCode's reset method, which also clears the aggregated error state, and verify the error is gone before reporting success.
* **Executable code asks first; everything else does not.** Creating, editing or activating a PHP or Universal snippet requires confirmation, and refuses outright while WPCode's own "completely disable PHP snippets" setting is on. CSS, JavaScript, HTML and text snippets are not gated, because a confirmation on something that cannot execute only teaches the habit of confirming without reading. Deactivating is never gated either — it only ever reduces what runs, and is the right first move when a snippet is suspected of breaking a site.
* **When the site is signed in to the WPCode library, three more things become possible.** You can list the installed library snippets that have a newer version upstream, pull that newer version down, and install a snippet someone shared with you by link. Nothing on a WordPress site tells you a library snippet has gone stale — it is copied in once and then never changes, so a fix published upstream never arrives unless somebody thinks to look. There is deliberately no separate "is the library connected" ability: that only matters when something else has just failed for want of it, so the calls that need the connection say so themselves.
* **Updating from the library keeps the snippet switched off if that is how you left it.** WPCode's own updater saves the library's copy wholesale, and the active state comes with it — so updating a snippet you had deliberately disabled would start executing it again as a side effect. The local state is captured first and restored afterwards. The update does replace the code completely, so a local edit is lost, which is why it asks for confirmation.
* **Searching the library works without an account; installing from it does not.** Measured by disconnecting and re-running everything: search and the pack list keep working because they read a cached public catalogue, while installing a snippet or a pack needs the account, because fetching the actual code goes through the library API. Those two used to report that the snippet "may not exist", which sends you hunting for the wrong thing; they now say plainly that the site is not signed in.
* **A library search that works while signed out says so in its answer.** Searching succeeds without an account, so it cannot simply fail — but replying "Done." would hide the one fact that decides what you can do next. The reply now reads "1 library snippet matched. Note: this site is not signed in to the WPCode library, so you can search and browse but NOT install…", followed by the address to open and a request to say when it is done. When the site is connected that note is absent entirely.
* **When the library is not connected, you are told exactly what to do about it.** Connecting means authorising a WPCode account, so no ability can do it and none pretends to. Instead the call that needed it hands back the exact wp-admin address of the Library page, says to use the Connect button there, and asks to be told once it is done so the work can carry on. The library reads also report whether the site is connected, and the link when it is not, so an assistant knows before it tries rather than after it fails.
* **The library credentials are never returned by any ability.** The connection is stored with an auth key, a webhook secret and a client id, and nothing reaches them.
* **Library snippets are installed switched off.** A snippet from the WPCode library is someone else's code that will run on the site, so installs — single or by pack — are confirm-gated and always land inactive, whatever the library says, leaving the decision to run it with a person who has read it.
* **Conditional logic refuses rules this edition cannot evaluate.** WPCode Lite understands page and user rules; device, schedule and the commerce rules are Pro. Lite stores an unsupported rule happily and then never matches it, so the snippet simply stops appearing with nothing reporting why. Those rules are now refused by name.
* **A new status ability answers "why is my snippet not working".** It reports safe mode, whether PHP snippets are disabled site-wide, how many snippets exist and are active, how many the loader cache holds, and whether those two numbers disagree — which is the usual sign that something is not running, or is still running after a change.

* **New Event Tickets toolset — 16 abilities.** Tickets, capacity, attendance, check-in, orders and sales, for a plugin that ships no abilities of its own. It is a separate tab from The Events Calendar because it is a separate plugin: Event Tickets does not require the calendar, and tickets attach to pages as well as events, so a site can sell tickets with no calendar installed at all.
* **Capacity is reported as what it actually is.** A ticket can keep its own stock, draw from a shared pool for the whole event, draw from that pool up to a limit, or be unlimited. Reporting a single number makes an unlimited ticket and a sold-out one look identical, so every read gives the number, the mode, the stock, and how many are sold, pending and still available — plus the event's shared pool, which a ticket in shared mode actually draws from.
* **Capacity changes go through the plugin's own save path, and that matters for money.** Event Tickets subtracts what is already pending and sold when it sets new stock. Writing the capacity value directly skips that and double-counts every sale already made, which on a paid event means overselling or underselling real seats.
* **Attendee details are aggregate unless you ask.** How many are coming and how many have checked in come back with nothing that identifies anyone. Names and email addresses are returned only when explicitly requested, results are always paginated, and the response says how many records it disclosed. The check-in security code is never returned at all — it is the credential printed on the ticket, and handing it out would let someone check in as another attendee.
* **Orders show the ledger, not the payment plumbing.** Status, total, currency, gateway name and date. Purchaser names and emails are opt-in on the same terms as attendees. Raw gateway payloads and processor customer references are never returned, and payment credentials are not readable here at all — Tickets Commerce keeps them outside the settings this reads.
* **Only present when Event Tickets is active.**
* **New Events Calendar toolset — 18 abilities, and a corruption path closed.** The Events Calendar keeps an event's timing in four places at once: post meta, the derived UTC and duration meta, and two custom database tables added in version 6. Writing the meta directly moves one of them. Measured on a real site: after a direct write the meta said 14:00, the UTC copy still held the old time, and both tables still said 09:00 — so the calendar views showed one time and the event's own page showed another, with no error anywhere. Every ability here goes through the calendar's own data layer, which is the only thing that keeps all four in step.
* **Find events by when they are, not just by title.** `events/list-events` filters on start and end dates, venue, organizer, category and featured flag, so "what is on next week" is a single call. Nothing we shipped before could express a date range over events at all.
* **Full coverage of the calendar's objects.** Events, venues and organizers each get list, read, create, update and trash. An event read comes back with its local and UTC times, timezone, all-day and multiday flags, cost, and its venue and organizers already resolved rather than as bare IDs.
* **It refuses the things it cannot do safely, by name.** A venue ID that is not a venue is rejected before the write, because the calendar would otherwise discard the link without saying so. Dates are read back afterwards, because the calendar silently throws away the whole date change when the end falls before the start and still reports success. Recurring events are refused outright — recurrence belongs to the Pro add-on and a series cannot be edited correctly from here.
* **Trash, never delete.** WordPress only sends ordinary posts and pages to the trash automatically; a calendar record handed to the delete function is gone for good. All three trash abilities use the trash and refuse if trash is switched off on the site. Trashing a venue or organizer tells you how many events still point at it, because the calendar leaves those links alone.
* **Map and import credentials are not readable.** The calendar's settings are one shared blob that also holds Google Maps, Meetup, Eventbrite and Facebook keys. The settings reader returns those as empty with a redacted flag, so you can see a setting exists without receiving its value.
* **Only present when The Events Calendar is active**, so the extra tool costs nothing on sites without it.
* **Fixed: the Integrations screen listed two ACF abilities that do not exist, and under-counted the rest.** It advertised three, named two that no plugin registers, and left out the three that create post types and taxonomies. Blocking or restricting one of the phantom entries did nothing at all, which is the worst kind of setting. It now lists all six correctly, and a test checks every listed ability against what the plugin actually registers so the list cannot drift again.
* **New: find out when two plugins claim the same ability name.** WordPress refuses a duplicate: whichever plugin registers first keeps the name and the other is discarded with no warning anyone will see. The loser can be a real, working feature, and which side loses depends only on load order — so the same two plugins can behave differently on two sites. The new "List Ability Name Collisions" diagnostic reports every contested name, which one is live, and which were thrown away. Run on a site with ACF it immediately found six.

* **New Translations toolset — 14 abilities.** Read and write the translations of any plugin, theme or WordPress itself: which text domains exist, what is still untranslated, editing the strings, creating a language, rebuilding a template, and downloading official language packs from WordPress.org. Only present when Loco Translate is active.
* **Editing a `.po` file by hand leaves the site showing the old words.** One save is really four files — the `.po` you edit, the compiled `.mo`, a `.l10n.php` cache, and a JSON fragment for anything translated in the block editor. Change the `.po` through the file abilities and the other three still hold the previous text, with no error anywhere: the file says one thing and every visitor sees another. WordPress 6.5 and later reads the `.l10n.php` in preference to the `.mo`, so even recompiling the `.mo` by hand — the obvious fix — still leaves a modern site rendering the old strings. Every ability here writes all four.
* **A save is confirmed file by file, from disk.** Loco reports a successful save when the compile step failed: it records a warning for the admin screen, writes zero bytes and returns normally, so the `.po` lands and nothing else does. Reporting that as success is how a translation goes live in name only. Each write now reports the real byte count of every file it produced and refuses by name if any of them is empty.
* **`Compile Translations` repairs a file someone already edited by hand**, rebuilding the compiled set from the `.po` — the way out for a site that has been editing translations directly and wondering why nothing changed.
* **Strings are found by their original text, not by position.** Syncing a translation against its template reorders and renumbers every entry, so anything that remembered "string number 12" would afterwards patch a different string. Context is included, which is what separates two identical words that translate differently.
* **Deleting a translation removes the whole set**, not just the `.po` — a leftover compiled file keeps serving the deleted translation.
* **Rebuilding a template asks first.** Re-scanning a plugin for translatable text can orphan translations whose original wording has since changed, so it is confirm-gated rather than silent.

* **New Email Delivery toolset — 4 abilities, plus a home for the ones the mail plugin already had.** Find out how the site sends email, whether it actually can, and prove it with a real test message. Only present when a supported mail plugin is active.
* **This is the answer to "the contact form does not email me".** Nothing in this plugin could explain why a message never arrived, even though it can create the forms that send them. `Diagnose Email Delivery` reports the mailer in use, whether its credentials are stored, whether a second plugin is also taking over email, and the last failure recorded. Measured on a test site: the mailer was PHP's own `mail()`, which is the default and the usual reason mail silently disappears.
* **A test send reports what actually happened, not what the mailer claimed.** Sending is accepted by PHP `mail()` even for an address that cannot possibly exist — measured, a message to a made-up domain came back as a success. So the result says the mailer *accepted* the message, says separately whether the sending domain passed its checks, and states plainly whether acceptance proves anything at all on this configuration. When the mailer refuses outright, its own error text is returned rather than a generic failure.
* **A test email goes to a real person, so it asks first** and the recipient must be given — it is never assumed to be the site administrator.
* **Passwords and API keys are never returned.** They sit in the mail plugin's settings and its own reader decrypts them on request, so a settings reader written the obvious way would hand them out. Each is reported only as set or not set, and everything withheld is listed. Nothing here can write a credential, and the mailer itself cannot be switched — changing it without the matching credentials stops all email on the site, password resets included.
* **Spam protection abilities now have a home too.** The anti-spam plugin registers its own abilities for spam figures and checking a comment; they were landing in no tab. They now appear on their own tab, still theirs, with this plugin's permission floor applied.

* **WPForms' own abilities now have a home, and form writing has an off switch.** WPForms registers eight abilities of its own — listing and reading forms, the form editing schema, form statistics, and creating forms, adding and updating fields and changing form settings. Nothing claimed them, so they sat in the catch-all group: live on the site but absent from the tabs and from the tools an AI client sees. They now appear on a WPForms tab. Nothing is re-registered; they are still WPForms' abilities, with this plugin's permission floor applied.
* **A switch for form writing, on by default, under Settings → Integrations.** On by default because that is already what the site does, not because writing is being opened up: WPForms ships its own switch for this, defaulting to off, and that switch has never worked on a site running this plugin. Its check lives in the permission callback that this plugin replaces in order to apply the administrator floor, and WPForms does not check again when the ability runs. Measured on a real site with WPForms' switch off: creating a form, adding a field and changing form settings were all permitted.
* **So the new switch is the one that works.** Turn it off and the four writing abilities are refused; the four reading abilities are unaffected, and no other plugin's abilities are touched. Turning it off and leaving it off survives updates — the default is applied once, and a deliberate "off" is never overwritten.
* **A plugin's own safety switches can now be honoured.** The floor introduced in the previous release raised weak locks, which was the point, but it also discarded everything else a plugin put in the same check — a kill switch, a per-object rule. Integrations can now refuse an ability the floor would otherwise allow. The hook only ever refuses: it cannot grant access, so no plugin on the site can use it to unlock anything.

**The abilities screen**

* **Integration tabs are now named after the plugin they drive.** Four were named after the topic instead — Cookie Consent, Email Delivery, Anti-Spam and Translations — which told you the subject but not whose plugin answers for it. They are now CookieYes, WP Mail SMTP, Akismet and Loco Translate, matching every other integration. This is the name an AI client sees when choosing between tools, and an integration that disappears when its plugin is deactivated should say which plugin that is.

* **Abilities from other plugins now belong to a toolset.** An ability registered by another plugin carries none of this plugin's metadata, so it belonged to no toolset: absent from every tab, `—` in the Toolset column, and — the part that mattered — unreachable through every MCP toolset. On a site running Rank Math and ACF that was 36 of 425 abilities, and nothing anywhere reported it. The Rank Math tab showed 61 of the 87 abilities in that namespace; the other 26, registered by Rank Math itself, were invisible.
* **New Advanced Custom Fields toolset.** ACF's own abilities get their own tab and their own MCP tool, appearing only when ACF is active.
* **Rank Math is whole.** Its tab and MCP tool now cover all 87 abilities — the ones this plugin provides and the ones Rank Math registers itself.
* **Every ability now has a toolset.** WordPress core's own abilities are placed individually (site and environment info under Diagnostics, user info under Users). Abilities from plugins with no toolset of their own go to a new **Other** toolset rather than disappearing — they are not filed into one of the curated groups, which would imply this plugin provides them. The tab counts now sum to the "All" total, which they did not before.
* **Adding an integration is one class.** Integrations declare their toolset — key, name, description, the ability names they own — and the tab, the counts, the filter and the MCP tool all follow. Third-party plugins can register their own through the new `acrossai_toolset_integrations` filter without changes here.
* **Note for MCP clients**: this changes what a connected assistant can reach. Abilities that existed but were unreachable through any toolset are now dispatchable through theirs.

* **One screen, one access model.** The Ability Integrations screen is removed. Its per-category on/off switch and its All/Specific ability picker did not hide abilities — they stopped those abilities being registered with WordPress at all, which is why switching a category off made its abilities vanish from the Custom Abilities table with nothing on screen explaining where they went. Searching that table for a switched-off category returned "No abilities found". Access is now controlled in one place: the per-ability **Site access** setting (Default / Force Allow / Force Block) on the Custom Abilities list, reachable from the edit form and from bulk actions.
* **Custom Abilities gains a toolset strip and a Toolset column.** The task groups that were the Integrations screen's tabs are now a filter over the one abilities table: **All** plus one tab per group with its count, data-driven so a group whose host plugin is inactive does not appear. The toolbar, the row checkboxes and the bulk actions behave identically on every tab. A new Toolset column sits between Category and Source, showing which group each ability belongs to and `—` for abilities that belong to none (anything you created yourself). `?tab=` deep links still work.
* **Migration — whatever your configuration blocked becomes a Force Block setting.** On upgrade, a one-time translation converts every ability in a switched-off category, and every unticked ability in a category set to Specific, into an explicit `Force Block`. An ability whose access you had already set explicitly is left exactly as it is — the translation never overwrites one. The set of reachable abilities is identical immediately before and after the upgrade; the difference is that it is now visible and editable. The `acrossai_library_config` option is removed once translated.
* **Third-party integration opt-ins moved to Settings → Integrations**, a new tab alongside Abilities and File Manager. The opt-in that asks a third-party plugin (currently ACF) to register its abilities is not a duplicate of anything — without it there is nothing for the abilities table to control — so it survives as a checkbox on the settings screen, keeping its current on/off state and its `acrossai_integration_toggle_capability` capability check. Absent still means off.
* **Removed — Enable All / Disable All.** Bulk actions on the abilities list already do this with an explicit selection and a confirmation step. The old buttons were also narrower than they read: their scope was the active tab, never the whole site.
* **Removed — the `acrossai-abilities-library/v1` REST namespace**, including `GET`/`POST /abilities/config`. It served only the retired screen. No external consumer is known.
* **`?page=acrossai-abilities-integrations` now redirects (301)** to the abilities page, preserving `?tab=`, since documentation links to it.
* **570 KB less on the page.** The retired screen inlined every ability definition — 389 rows on a typical site, each carrying its full input and output JSON Schema — as 584,016 bytes of JSON on every load. Nothing replaces it: the strip reads counts from one small endpoint and the column reads a field the table was already fetching.

* **Ability Integrations tabs regrouped into task groups.** The screen had 18 tabs, one of which — "Core" — held 105 of the ~450 abilities: not a subject, a bucket absorbing nine whole folders. It is replaced by 13 groups named for what an administrator is doing: **Content, Blocks, Appearance, Configuration, Users, Updates, Cron, Cache, Database, Files, Diagnostics**, plus **Elementor** and **Rank Math** when those plugins are active. `tab_group` is now assigned per ability rather than per folder, so an ability lands in the group that matches its job. 229 declarations changed; `core` is retired entirely.
* **Fixed — two categories rendered as half-cards in two different tabs.** `Settings/` had 10 abilities tagged `core` and 1 tagged `settings`; `SiteHealth/` was split 3/3. Because the tab filter keeps only the slugs matching the active tab, each appeared twice showing a different half of itself, with nothing on screen indicating the other half existed. Settings now appears whole (7 branding abilities under Appearance, 4 permalink abilities under Configuration, membership chips naming both); Site Health appears whole under Diagnostics.
* **Moved — 8 abilities relocated from `Content/` to `Block/`.** `blocks/add-block`, `blocks/remove-block`, `blocks/move-block`, `blocks/duplicate-block`, `blocks/update-post-block`, `blocks/get-post-blocks`, `blocks/outline-post-blocks` and `blocks/insert-pattern` registered under the `blocks/` namespace while declaring the `content` category. Their `category` is now `acrossai-abilities-manager-block` and their `sub_group` is `post-blocks`. **Ability slugs are unchanged, so no MCP client is affected.** The re-ticking this originally asked for no longer applies: Specific mode is gone, and the migration translates whatever your configuration blocked at upgrade time into per-ability Force Block settings you can then edit directly.
* ~~**Behaviour change — bulk Enable All / Disable All scope.**~~ Superseded within this same release: the buttons, the per-category toggle and the All/Specific mode are all removed (see the first entries above). Nothing about tab-scoped bulk enabling ships. The five categories that span two groups still do, and both of their tabs still list them — but as a filter over one table, so a category can no longer be "off in one place and on in another".
* **Ability category slugs shortened from `acrossai-abilities-manager-*` to `acrossai-*`.** `acrossai-abilities-manager-content` becomes `acrossai-content`, and so on for all 25 categories. This is the slug shown in the Category dropdown when editing an ability, and the value returned as `category` by the abilities REST API and MCP ability-info responses. **Ability slugs are unchanged.**
* **Migration — stored category slugs are rewritten automatically.** A category slug is a persistence key in two places: the keys of the `acrossai_library_config` option, and the `category` column of every ability you created yourself. WordPress refuses to register an ability whose category is not registered, so a rename without migration would have made your custom abilities silently stop existing. Both stores are rewritten once, on activation and on the next admin page load after an in-place upgrade, guarded by a completion flag. Third-party categories and the three similarly-prefixed asset handles are left untouched.

* **Deep links to retired tabs fall back to "All".** `?tab=core`, `?tab=media`, `?tab=themes`, `?tab=plugins`, `?tab=file-manager`, `?tab=comments`, `?tab=content-search`, `?tab=site-health`, `?tab=widgets` and `?tab=settings` no longer resolve. Invalid tab values were already handled silently — no error, no console warning. This still holds on the toolset strip that replaced those tabs.

**Content and editing**

* **New ability: find out how a page is built before you change it.** `content/inspect-post-builder` reports whether a post, page or custom post type is owned by Elementor, another page builder, the block editor or the classic editor — or is simply empty — and says plainly whether writing its content will do anything. Every content ability in this plugin writes `post_content`, and a page builder does not keep its layout there. On an Elementor page an edit therefore reports success, changes nothing a visitor sees, and is wiped the next time anyone saves the page in Elementor, because Elementor rewrites `post_content` from its own widget tree on every save. Two builders invert the problem: WPBakery and Fusion keep shortcodes in `post_content`, so an edit there does land — and destroys the layout. The ability answers with all three cases distinguished, says where the real content lives, and names what to use instead.
* **The warning now reaches the assistant, which is the harder half.** Ability suggestions were invisible on the toolset path — the dispatcher built its own response and never read them — so an assistant that went straight to running an ability saw nothing. Suggestions now appear on `action=info`, the "Content" and "Blocks" tool descriptions both name the new ability, and nineteen content and block writers point at it, sixteen of which carried no suggestions at all before. Discovery responses are unchanged, and the existing "Disable ability suggestions" setting still switches the lot off.
* **It keeps working when the builder does not.** A page stays Elementor-built after Elementor is deactivated, which is exactly when it is most likely to be mishandled — so the check is builder-agnostic, lives with the content abilities rather than the Elementor ones, and reports whether the builder is still installed as a separate fact, because the advice differs.
* **New Classic Editor toolset — four abilities, because the rest already existed.** Classic Editor registers no abilities of its own, but almost everything it stores is already reachable: the two settings through the option abilities, the per-post choice through the post-meta abilities, the per-user choice through the user abilities. Wrapping those again would just be a second way to do one job. What was not reachable is the part the plugin keeps to itself — the effective configuration and which layer produced it, which editor a given post will actually open in and why, and which editors each post type allows. Those three answers live behind private methods with no way in, so they are what this adds, alongside a settings writer that validates.
* **The stored value and the real answer are different things here, which is the point.** A site that has never opened the Classic Editor settings has no stored value at all and still behaves as classic, so reading the option directly reports "not configured" for a site that is actively configured by default. Every read reports the effective value, the stored value, and which of network, site, user or default decided it.
* **A remembered editor is not always used, and now says so.** With user switching turned off, Classic Editor has no per-post logic at all — a post that remembers the classic editor still opens in whatever the site default says, and the remembered value simply sits there. Reporting that value on its own would be actively misleading, so it is reported together with whether it is currently being consulted.
* **The settings writer refuses what the generic option writer accepts.** Writing `classic-editor-replace` as anything other than the two accepted values stores it happily and silently resolves to classic, because WordPress only runs a setting's sanitiser through its own settings screen and not through a plain option write. This ability runs Classic Editor's own validators, then reads the value back to confirm it took. Changing the site-wide default asks for confirmation, since it changes which editor every author gets; turning user choice on or off does not, and neither does setting the default to the value it already holds.
* **The tab and its tool exist only where Classic Editor does.** Verified by deactivating it: the abilities, the tab and the MCP tool all disappear together, and come back with it.
* **Not included:** the multisite network default and the network lock. They are real, but this was built and verified on a single-site install and shipping them unverified would be worse than leaving them out.

* **Widgets can now be changed, not just listed — nine new abilities.** The two widget abilities this plugin shipped were both read-only, so an assistant could see a site's sidebars and everything in them and could not move, edit, add or remove a single one. The nine that close that gap are: read what widget types exist and how many of each are in use, read one instance in full, report whether widget management means anything on this site at all, then add, update, move, reorder, deactivate and remove. They join the existing Appearance tab rather than adding a tool of their own.
* **This started as a Classic Widgets suite and deliberately is not one.** Classic Widgets stores nothing — the entire plugin removes one theme support and stops. What it changes is how the widgets screen looks, not what the site holds, and the widgets themselves belong to WordPress. So the plugin's mode is reported as one field of a status ability, and the abilities act on core.
* **Settings go through each widget's own save handler, which is the only sanitising route there is.** The text widget runs its title through WordPress's text sanitiser and, for anyone without the unfiltered-HTML capability, its body through the post-content filter. Writing the widget option directly — the only way to do this before — skips all of that, and also cannot tell that a widget *refused* the update, since a widget rejecting a value simply keeps the old one. These abilities go through the handler and read the result back, so a refused setting is reported as refused instead of as success.
* **A block theme registers no sidebars, and the widgets are still there.** Measured on the install this was built against: zero registered sidebars, while the stored layout held two of them with five widgets between them. Refusing to touch a sidebar the theme does not register would have made the whole suite do nothing on most modern sites, so those sidebars are accepted and reported as orphaned — the widgets are real, they simply render nowhere until a theme claims the area back.
* **Removing a widget left a hole in the stored layout, and now does not.** WordPress removes a widget from a sidebar without renumbering what is left, so taking out the middle of three left positions 0, 1 and 3. Anything reading that layout as JSON then sees an object where an array is expected, and the widgets screen renders the gap. Every placement change now renumbers, verified by running a full add, move, reorder, deactivate and remove cycle and confirming the stored layout came back byte-identical to how it started.
* **Deactivating and removing are different operations and are treated differently.** Deactivation moves a widget to the inactive store with its settings intact and can be undone by moving it back; removal deletes the settings. Only removal asks for confirmation — gating the reversible ones as well would just teach a caller to confirm everything without reading it.

**Fixes**

* **Fixed: switching an ability's lock to this plugin's control also silently removed the plugin's own safety checks.** The previous release made this plugin decide who may run every ability, which is what stops a plugin shipping a weak lock. It did that by replacing each ability's own check outright — and a plugin's check often carries more than "is this an administrator": a kill switch for a feature, or a rule about which specific form or record you may touch. All of that was being discarded. Measured on one site: 26 such checks, 9 of them about a specific record rather than the site as a whole.
* **Both now apply.** This plugin's rule runs first and can only tighten; if it allows, the plugin's own check still gets the final say and can still refuse. Nothing that was locked becomes unlocked. What changes is that a plugin's own switch works again — WPForms' form-writing switch is the clearest example, and its per-form permissions are honoured rather than collapsed into "any administrator, any form".
* **A refusal now says why.** When a plugin's own check declines it, its reason is passed through instead of being flattened into a bare "no", so an AI client is told the feature is switched off rather than being left to guess. A check that errors is treated as a refusal, and one badly behaved plugin can no longer break every ability on the site.

* **Fixed: WPCode's own five abilities were never actually adopted into its tab.** The previous release said they were. They were not — the namespace was claimed with a trailing slash, which can never match, so all five stayed in the catch-all group and the WPCode tab under-counted by five. They are now where they were always meant to be, and a test checks every integration for the same mistake.

= 0.0.33 - 2026-08-28 =
**Release theme: closing the cheap-edit loop.** A follow-up to 0.0.32 that closes the last two gaps between "locate a block cheaply" and "modify it cheaply". Two changes, both surgical and backwards-compatible.

**`return_content:false` default now covers the two block-tree writers.** `blocks/add-block` and `blocks/update-post-block` gain the same `return_content:{boolean, default:false}` input as the six content writers (PR #152) and nine block-editor writers (PR #153). When false (default), the response's `block` object strips its `innerHTML`, `innerContent`, and `innerBlocks` — leaving `blockName`, `attrs`, and `path` — and `content_bytes` reports the saved `innerHTML` size. Container blocks (columns, cover, group) previously echoed their entire innerBlocks subtree; now they don't unless the caller passes `return_content:true`. BREAKING for callers reading `response.block.innerHTML` on these two abilities — pass `return_content:true` explicitly. Every other block-tree read/write (mutate-block-tree, replace-block-text, remove-block, duplicate-block, move-block) already returned lightweight envelopes and is unchanged.

**`blocks/get-post-blocks` gains scoping inputs.** Three new optional inputs close the "read one block's markup" gap between `blocks/get-post-blocks` (full tree, full content) and `blocks/outline-post-blocks` (scoped but never returns content). `path: int[]` scopes the response to a subtree (uses the same raw parse_blocks() index scheme as add-block / update-post-block / remove-block, so returned paths interchange). `depth: integer` bounds descent below the subtree root (-1 unlimited, 0 subtree root only, N below). `include_html: boolean` (default true = backwards-compat) strips innerHTML + innerContent from every returned node when false. Backwards-compatible: existing callers passing only `post_id` see identical responses. An unresolvable `path` returns a standard error envelope with `error_code: invalid_path` naming which depth failed and how many blocks exist at that level.


= 0.0.32 - 2026-08-28 =
**Release theme: token-efficient AI callers.** Two closely related shifts. First, response payloads shrink dramatically for the common "small edit" and "just tell me the block structure" intents. Second, ability descriptions gain author-declared hints pointing AI callers at cheaper sibling abilities when their intent maps to one. Every change is either strictly additive or opt-out only via an admin toggle — no ability's execute() behaviour changes.

**Token-efficiency default for six content writers.** `content/create-page`, `content/update-page`, `content/create-post`, `content/update-post`, `content/create-cpt-item`, `content/update-cpt-item` gain a new optional input `return_content:boolean, default:false`. When false (the default), the response's `page` / `post` / `item` object strips three large fields — `post_content`, `post_content_filtered`, `post_excerpt` — and adds `content_bytes:integer` so callers still see the saved payload size at a glance.

**Why.** A single-word edit on a ~97 KB page via `content/update-page` previously round-tripped ~60 K LLM tokens (the caller sent the whole new body and the ability echoed the same body back). With this default, the echo drops to ~0 tokens — the caller pays only for the upload it already had to make. Fine-grained edits via `blocks/update-post-block` remain ~10× cheaper still because they never touch the surrounding content.

**BREAKING for callers reading `response.page.post_content` (or `.post` / `.item` equivalents).** Existing callers that need the saved content back — e.g. to diff against what they sent — must pass `return_content:true` explicitly. The three stripped fields remain queryable via `content/get-page` / `content/get-post` after the write.

**Not affected.** Every other content ability (get / list / delete / block-tree operations / meta ops) is unchanged. The block-tree writers (`blocks/update-post-block`, `blocks/add-block`, etc.) already returned just the modified block, not the whole page — nothing to strip.

**Same default now applies to nine block-editor writers.** `blocks/create-block-pattern`, `blocks/create-block-template`, `blocks/update-block-template`, `blocks/create-block-template-part`, `blocks/update-block-template-part`, `blocks/create-block-style-variation`, `blocks/update-block-style-variation`, `blocks/create-global-style`, and `blocks/update-global-style` gain the same `return_content:boolean, default:false` input. Response objects (`pattern` / `template` / `part` / `variation` / `record`) strip the large `content` (pattern/template/template-part markup) or `data` (variation/theme.json JSON) field by default and add `content_bytes:integer`. For `Variation_Db::to_row` and `Global_Styles_Db::to_row`, the writers now pass the caller's `$return_content` through instead of hardcoding `true` — the helpers skip `decode_content()` when the payload isn't wanted (CPU saving on the hot path). BREAKING for callers reading `response.pattern.content`, `response.template.content`, `response.part.content`, `response.variation.data`, or `response.record.data` — pass `return_content:true` explicitly. `blocks/update-block-pattern` is unchanged — it already returned a lightweight location descriptor.

**New ability `blocks/outline-post-blocks`.** Returns a flat, depth-first index of a post's block tree — canonical path, block type, child count, byte size, and a short text preview — without any block content. Cheap way for an LLM caller to locate a block before editing it: `blocks/get-post-blocks` on a large page can be hundreds of kilobytes because it returns every block's full `innerHTML`; this ability returns kilobytes for the same post. Paths use the same raw-`parse_blocks()` index scheme as `Block_Tree`, so a path returned here is drop-in usable with `blocks/add-block`, `blocks/update-post-block`, and `blocks/remove-block`. Paths are positional — a write can re-serialize the post and shift raw indices — so the response includes `post_modified_gmt` for staleness detection; re-outline after each write rather than caching paths. Filters (`block_names`, `contains`, `max_text`, `depth`, `include_attrs`, `max_results`) compose. `contains` matches only within the extracted text preview (up to `max_text` characters); raise `max_text` for deeper substring searches. Whitespace nodes (`parse_blocks` entries with null `blockName`) are excluded from output but still consume index positions — same convention `Block_Tree` already uses. `readonly`, `idempotent`, `non-destructive`.

**New — Ability Suggestions framework (Feature 095).** Ability authors can now declare a small list of other abilities an AI caller might use instead — a token-saving hint mechanism mirroring Feature 088's `suggested_plugins()`. Each ability class can override a new protected method `suggested_abilities()` returning `array<int, array{slug: string, reason: string, saves?: string}>`; entries surface under `args.meta.acrossai.suggested_abilities` on `mcp-adapter-get-ability-info` (not on discover-abilities — details-only surface, avoids discovery bloat). Hints are strictly advisory — nothing about the original ability's execution changes. Four initial ability overrides ship in this release: `content/update-page`, `content/update-post`, `content/update-cpt-item` each suggest `blocks/outline-post-blocks` + `blocks/update-post-block` for narrow edits (~29K tokens saved on a 97 KB page); `blocks/get-post-blocks` suggests `blocks/outline-post-blocks` when only paths are needed (~28K tokens saved on the same page). New admin toggle "Disable ability suggestions" on the Abilities settings tab (between "Plugin Suggestions" and "Uninstall Settings") strips the field site-wide (option key `acrossai_disable_ability_suggestions`, default `0` = feature enabled). Uninstall cleans up the option when "delete all data" is on. An ability with no override produces a byte-identical payload to what it produced before Feature 095 — no schema drift, no phantom empty list.

**Ten more `suggested_abilities()` overrides added to the Feature 095 hint catalog.** `content/get-page`, `content/get-post`, `content/get-cpt-item` each hint that `blocks/outline-post-blocks` is far cheaper (~20×) when the caller only needs to locate a block or see the structure. `blocks/read-theme-json` hints that `blocks/get-style-guide` returns a normalized token summary (~5–8×) when the caller wants design tokens, not the raw spec. `options/list-options`, `media/list-media`, `users/list-users`, `blocks/list-block-templates`, `blocks/list-block-patterns`, `blocks/list-global-styles` each hint that their corresponding targeted-read siblings (`options/get-option`, `media/get-media`, `users/get-user`, `blocks/read-block-template`, `blocks/read-block-pattern`, `blocks/read-global-style`) are 5–30× cheaper when the caller already knows the identifier — list is for discovery, get/read is for retrieval.

= 0.0.31 - 2026-08-26 =
**Release theme: File Manager consolidation + hardening + audit log.** Bundles the six-feature series (089 → 094) that turned `file-manager/*` from a loose collection of read/write primitives into a self-contained subsystem with an admin tab, allowlist-and-content-filter enforcement, and an append-only audit log with pre-image backups. Also cuts the inline `.bak.<timestamp>` scheme in `Delete_File` (BREAKING for callers of `response.backup` — new canonical field is `response.backup_path`).

**Feature 094 — File Manager Audit Log + Backup Harness (partial).** Consumes the four Backup & Audit option keys shipped as scaffold in PR #144 and enforced-toggle-only in PR #146. New `Audit_Trail` utility owns pre-image backups (into `wp-content/acrossai-file-manager-backups/<YYYY-MM-DD>/`) + append-only log (into `wp-content/acrossai-file-manager-logs/acrossai-file-manager.log`) + amortised 1-in-10 cleanup + stats. Both storage locations get a `Deny from all` `.htaccess` on first creation. All I/O goes through `WP_Filesystem`.

**New ability:** `file-manager/get-changelog` — tails the last N entries (default 100, max 500) via MCP. Honours the read allowlist. Empty log returns a friendly message, not an error. `manage_options` gated.

**Log entry format** (blank-line separated, one entry per mutation):

  [YYYY-MM-DD HH:MM:SS UTC] <OPERATION>
    Ability: file-manager/<slug>
    File: <abs path>
    User: <email> (ID:<id>) IP:<ip>
    Size: <before> -> <after> bytes
    Destination: <abs path>       (COPY / MOVE only)
    Backup: <abs path | SKIPPED (...) | FAILED (...) | DISABLED>
    Context: <sanitised text or empty>

**New action hook:** `do_action('acrossai_file_manager_log_entry', $entry)` fires after every log write. Subscribers (Slack, Datadog, SIEM…) receive the parsed entry as an assoc array. Zero cost when no subscribers.

**New optional `context` input field** on wired abilities (`delete-file`, `edit-file`, `create-directory` in this PR — more in the follow-up). Schema max 2000 chars; log writer truncates to 500 chars via `sanitize_text_field` before persisting.

**BREAKING — `file-manager/delete-file` `backup` response field.** The inline `<path>.bak.<time>` scheme is REPLACED by the centralised backup dir. When `backup_enabled=true` the response's new canonical field is `backup_path`; the legacy `backup` field is populated with the same value for one transition release and will be removed. When `backup_enabled=false` NO backup is written at all — callers who relied on the inline `.bak` return value now get `null` for both fields when the toggle is off. Direct callers to read `backup_path` instead of `backup`.

**Retention.** `backup_retention_days` deletes backup dirs older than N days; `audit_log_retention_days` trims log entries older than N days. Both fire probabilistically (1-in-10 per log write) — no WP-Cron dependency.

**Scope note (PARTIAL).** This PR wires three abilities end-to-end (`delete-file`, `edit-file`, `create-directory`) to prove the design across the backup + log-only paths. The remaining seven mutation abilities (`create-file`, `append-file`, `copy-file`, `move-file`, `delete-directory`, `edit-wp-config`, `clear-debug-log`) and the `BackupAuditPanel` scaffold-banner flip + `/backup-audit-stats` REST endpoint + uninstall extension are DEFERRED to a follow-up PR. Test coverage is scoped accordingly (35 new tests, mostly structural — behavioural I/O tests need a full-WP bootstrap and run in the CI matrix).

**Feature 093 — File Manager Hardening (enforcement pass for PR #144 scaffold).** The eight Content Filters knobs and the one sensitive-read denylist that PR #144 shipped as UI scaffold are now enforced at runtime by every file-manager write ability and by `file-manager/read-file`. Persistence layer (`Hardening_Settings`) unchanged; the new `Hardening_Enforcer` utility runs each check after the existing `File_Mods_Guard` + `Path_Allowlist_Guard` gates and returns the standard `{success:false, blocked_reason, path, message, …context}` envelope on refusal. Enforcement order (best-cheap-first): dangerous_extensions → block_double_extensions → sanitize_filename_check → strict_filename_filter → mime_type_check → htaccess_directive_scan → write_max_bytes. Empty list / false toggle is a no-op so callers upgrading with defaults get zero behaviour change beyond what defaults dictate.

**Six abilities wired** — `file-manager/{create-file, edit-file, append-file, copy-file, move-file}` for the seven content-filter checks; `file-manager/read-file` for the sensitive-read denylist. Copy/move check the DESTINATION basename; append-file scans only the appended bytes for `.htaccess` directives and caps size on `new_size = existing + appended`; copy/move use source file size for the size cap and read source content lazily for the `.htaccess` scan (only when destination basename is `.htaccess`). `mime_type_check` skips append-file (extension didn't change) and always allows `{php, txt, log, json, xml, css, js, md, html, htm, htaccess}` even with the check on — prevents breaking the mu-plugins deploy use case the write allowlist explicitly permits.

**Eight new `blocked_reason` values** — `extension_blocked` (+`extension`), `double_extension_blocked` (+`basename`), `htaccess_directive_blocked` (+`directive`), `filename_sanitize_failed` (+`input`, `sanitized`), `write_size_exceeded` (+`size`, `max_bytes`), `filename_strict_blocked` (+`marker`), `mime_type_blocked` (+`extension`), `sensitive_read_blocked` (+`basename`, `matched_pattern`). Every affected ability's `output_schema` declares the union of these context fields. Existing `path_not_allowed_for_write` / `path_not_allowed_for_read` / `protected_write` / `file_mods_disabled` envelopes are unchanged and take precedence — hardening refusals fire only when all earlier gates pass.

**Ordering guarantee** — sensitive-read denylist runs AFTER the read allowlist (spec FR-011). If the allowlist would refuse a path, that refusal (`path_not_allowed_for_read`) is returned; the denylist only fires when the allowlist would permit. This lets an admin safely widen the read allowlist and still keep `.env / *.key / id_rsa` blocked.

**Panel banner update** — the Content Filters panel drops its yellow "Scaffold only" `notice-warning` (added in PR #144) and replaces it with a small `notice-info` reading "This list now gates create-file / edit-file / append-file / copy-file / move-file." The Backup & Audit panel keeps its scaffold banner but the text now references `094-file-manager-audit-log` explicitly. REST GET `/acrossai/v1/file-manager-settings/content-filters` flips `scaffold_only:true → false` and `follow_up_spec` becomes `null`; `/backup-audit` unchanged.

**Dotfile carve-out for `sanitize_filename_check`.** Legitimate WordPress-adjacent dotfiles (`.htaccess`, `.htpasswd`, `.user.ini`) are exempted from the sanitize-filename roundtrip check because real WP's `sanitize_file_name()` strips leading dots — applied literally, the check would refuse every valid dotfile and starve the htaccess-directive scanner of any target. Other dotfiles (`.gitignore`, `.env`, etc.) are still refused when the check is on; admins who need them disable the check.

**Not touched.** `Delete_File`, `Delete_Directory`, `Create_Directory`, `File_Info`, `Read_Debug_Log` (fixed target), `Read_Wp_Config`, `Get_Wp_Config_Constant`, `Edit_Wp_Config`, all six zip abilities — spec explicitly excludes these.

**Feature 092 — File Manager admin tab: per-folder read/write allowlists + configurable secret redactor.** New "File Manager" tab at `admin.php?page=acrossai-settings` gives site admins three per-folder controls over what MCP clients can do via `file-manager/*` abilities. Also introduces a hardened secret-scrubber that runs on every read response.

**Write allowlist.** The 8 write-capable file-manager abilities (`create-file`, `edit-file`, `delete-file`, `copy-file`, `move-file`, `append-file`, `create-directory`, `delete-directory`) refuse any operation whose target path resolves outside the admin's allowlist. Default on activation: `['wp-content']` (writes only inside wp-content). `copy-file` and `move-file` check both source and destination. Refusal returns `{success:false, blocked_reason:"path_not_allowed_for_write", allowed_roots:[…]}`.

**Read allowlist.** The 2 content-reading abilities (`read-file`, `read-debug-log`) can also be gated. Default on activation: `[]` — unrestricted (every path readable). Admins can flip a "Restrict reads to specific folders" toggle and pick specific folders. Refusal returns `{success:false, blocked_reason:"path_not_allowed_for_read"}`. `list-directory` and `file-info` remain ungated (metadata only, no content leak).

**Secret redactor.** Every text response from `read-file` and `read-debug-log` is scrubbed before return. Ships one built-in pattern: **WordPress credentials** (DB_PASSWORD, DB_USER, all 8 auth keys/salts, SECRET_KEY) — value replaced but constant name preserved. Everything else the admin adds via the custom-literals textarea (case-sensitive string match). Third-party API-key regexes are intentionally NOT hardcoded — assumptions about someone else's key format belong in the site's own config, not in this plugin.

**Auto-scrub for AI-connector API keys.** If the WordPress **AI plugin** (github.com/WordPress/ai) is installed and any of its provider connectors has an API key configured, that value is added to the redactor's literal list transparently — the admin does not have to copy those keys into the custom-literals textarea. Currently covers OpenAI (`connectors_ai_openai_api_key`), Anthropic (`connectors_ai_anthropic_api_key`), and Google (`connectors_ai_google_api_key`). No configuration required; the redactor reads these options on every scrub call. Responses grow two fields: `redacted:bool` and `redaction_count:int`.

**REST endpoints.** Six new routes under `acrossai/v1`: GET/POST `/file-manager-settings/write-allowlist`, `/read-allowlist`, `/redaction`. `manage_options` + `X-WP-Nonce` required. GET responses include enumeration data (immediate ABSPATH children, `get_plugins()`, `wp_get_themes()`) so the React UI renders without a second round-trip.

**BREAKING — `file-manager/read-file`.** The previous outright refusal of `wp-config.php` and `.htaccess` (`blocked_reason:"protected_read"`) is REMOVED. Those files are now readable; sensitive content is redacted per the secret redactor above. Callers that programmatically handled `blocked_reason:"protected_read"` should switch to reading the returned content with `redacted:true`. Write-side refusals on `wp-config.php` / `.htaccess` for `create-file`, `edit-file`, `delete-file`, `copy-file`, `move-file`, `append-file` are UNCHANGED.

**Not touched.** `Ability_Definition`, `File_Mods_Guard`, `Wp_Filesystem_Init`, `read-wp-config`, `edit-wp-config`, `get-wp-config-constant`, `list-directory`, `file-info`, all 6 zip-backup abilities, and every ability outside `file-manager/*`.

= Feature 091 milestone =
**Feature 091 — WP_Filesystem migration for `file-manager/*` abilities.** Every filesystem read, write, list, delete, copy, move, and stat performed by 19 file-manager abilities now routes through WordPress's `WP_Filesystem` transport instead of raw PHP filesystem functions. On the majority of hosts (`FS_METHOD='direct'`) the behaviour is identical. On hosts where WordPress requires FTP / SSH credentials (`FS_METHOD='ftpext'` / `'ftpsockets'` / `'ssh2'`) the abilities now succeed via the same channel WordPress core's file editor uses instead of silently failing.

**Biggest wins — `wp-config.php` and `debug.log`:** `file-manager/read-wp-config`, `file-manager/edit-wp-config`, `file-manager/read-debug-log`, `file-manager/clear-debug-log` are the abilities most likely to touch files owned by the SSH user rather than the web-server user. Those calls previously failed on non-`direct` transports; they now work.

**New response value:** every migrated ability's `blocked_reason` enum widens by one value — `filesystem_unavailable` — returned when `WP_Filesystem()` initialisation fails (typically missing `FTP_HOST` / `FTP_USER` / `FTP_PASS` on a non-`direct` host).

**BREAKING — `file-manager/file-info` schema shrink:** the response no longer includes `ctime` or `atime` fields. `WP_Filesystem_Base` does not expose these consistently across transports (native `stat` returns them on `direct`, FTP/SSH transports don't). Callers programmatically reading `.ctime` or `.atime` must switch to `.mtime` or accept the loss. Every other field on the response is unchanged.

**Deferred to feature 092:** `file-manager/create-zip-backup`, `file-manager/extract-zip-backup`, and `file-manager/upload-zip-backup` remain on native PHP for now. `ZipArchive` requires direct filesystem access and has no `WP_Filesystem` equivalent, and chunked upload uses `fopen`/`fwrite`/`fclose` file-handle APIs that `WP_Filesystem` does not expose. These three abilities continue to work exactly as before on `direct` transports and continue to fail as before on FTP/SSH ones. Every file carries a `// TODO(feature-092)` marker.

**Housekeeping:** approximately 20 `phpcs:ignore WordPress.WP.AlternativeFunctions` suppressions removed from the 19 migrated files. `Ability_Definition` and `File_Mods_Guard` are unchanged (verified — sibling plugin `acrossai-buddyboss` continues to extend the former without issue).

**Feature 090 — file-manager additions.** Four new abilities extend the `file-manager/*` namespace to cover directory management and metadata: `file-manager/append-file` (append or prepend to an existing file; refuses missing files and refuses `wp-config.php` / `.htaccess`), `file-manager/create-directory` (recursive-by-default `mkdir` under ABSPATH; idempotent), `file-manager/delete-directory` (empty-only by default; opt-in `recursive:true`; requires `confirm:true`; refuses nine critical WordPress directories), and `file-manager/file-info` (read-only stat wrapper with optional POSIX owner/group names). Ability count 385 → 389.

**Feature 089 — file abilities consolidation.** Every file read / write / list / copy / move for the WordPress installation now flows through the `file-manager/*` namespace. Three new abilities added; six duplicate theme- and plugin-scoped abilities removed; a pre-existing security gap closed.

**Added — three new `file-manager/*` abilities:**

* **`file-manager/list-directory`** — recursive directory walk under ABSPATH. Bounded by `max_depth` (default 5, max 20) and `max_entries` (default 1000, max 5000); response sets `truncated:true` when a bound is reached. Symlinks are not followed. Replaces `themes/read-theme-structure` and `plugins/read-plugin-structure`.
* **`file-manager/copy-file`** — copy a file between two paths under ABSPATH. Default refuses when the destination exists; pass `overwrite:true` to replace. Refuses copies **onto** `wp-config.php` or `.htaccess` even with `overwrite:true`. Replaces the copy mode of `plugins/manage-plugin-files`.
* **`file-manager/move-file`** — rename/move a file between two paths under ABSPATH. Same overwrite semantics as `copy-file`, plus refuses moves **from** `wp-config.php` or `.htaccess`. Replaces the move mode of `plugins/manage-plugin-files`.

**Removed — six duplicate abilities (BREAKING):** any MCP client hardcoding these slugs will get an "unknown ability" error. Migrate to the `file-manager/*` replacement.

| Removed slug | Replacement |
|---|---|
| `themes/read-theme-code` | `file-manager/read-file` |
| `themes/edit-theme-file` | `file-manager/edit-file` |
| `themes/read-theme-structure` | `file-manager/list-directory` |
| `plugins/read-plugin-code` | `file-manager/read-file` |
| `plugins/read-plugin-structure` | `file-manager/list-directory` |
| `plugins/manage-plugin-files` | `file-manager/copy-file` or `file-manager/move-file` |

**Hardened — `file-manager/create-file` and `file-manager/edit-file` now refuse `wp-config.php` and `.htaccess`.** Before this release, these two abilities silently allowed overwriting those files even though `read-file` and `delete-file` refused them. This closes the last generic write path to those protected files; the specialized `file-manager/edit-wp-config` (single-constant edit with secret-key allowlist) remains the only supported way to modify `wp-config.php`.

**Kept as-is (not duplicates):** `file-manager/read-wp-config`, `file-manager/edit-wp-config`, `file-manager/get-wp-config-constant`, `file-manager/read-debug-log`, `file-manager/clear-debug-log`, `recovery/list-recent-fatal-errors`, and all theme / plugin lifecycle abilities (install / activate / update / delete-theme / lifecycle-context / checksums / etc.).

**Ability count:** 388 → 385 (-6 removed, +3 added). Full per-ability inventory refreshed at `docs/abilities-inventory.md`.

= 0.0.30 - 2026-08-19 =
**Ability namespace migration — every ability slug moves from `acrossai/*` to a topic-based prefix.** 388 abilities across 24 topic namespaces. No behavioural changes; this is a slug rename only. Delivered as four disjoint PRs merged in order: #134 (blocks), #135 (elementor), #136 (rank-math), #137 (remaining 21 domains).

**Breaking for every MCP client:** any reference to `acrossai/<slug>` must switch to `<topic>/<slug>`. There is no back-compat alias — discovery now returns the new names only.

* **`blocks/*` (40)** — every block-editor primitive: templates, template parts, patterns, style variations, global styles, theme.json, site-editor context, blocks/reusable blocks. Also includes the 7 block-tree ops that previously lived under `acrossai/*` in the Content folder (add-block, duplicate-block, get-post-blocks, insert-pattern, move-block, remove-block, update-post-block). Prefix-only rename — the second segment is preserved (e.g. `acrossai/create-block-template` → `blocks/create-block-template`).
* **`elementor/*` (62)** — every Elementor ability. Redundant `elementor-` fragment collapsed into the namespace, so `acrossai/elementor-add-widget` → `elementor/add-widget`, `acrossai/elementor-create-template` → `elementor/create-template`, etc. `Base_Audit_Ability` now builds slugs as `'elementor/' . audit_slug()` — all dynamic audit subclasses inherit the new prefix.
* **`rank-math/*` (61)** — every Rank Math ability. Redundant `rank-math-` fragment collapsed. `Base_Rank_Math_Ability` slug construction changed in one line (`'rank-math/' . slug()`) — the entire suite picks up the rename automatically. User-visible error messages that name specific slugs (e.g. `Utilities/RankMath/Maintenance_Tools`) refreshed to match.
* **Remaining 21 topic namespaces (225)** — prefix-only rename per domain:
  * `admin-menu/` (5), `cache/` (7), `comments/` (12), `content/` (29), `content-search/` (11), `core/` (6), `cron/` (16), `database/` (11), `file-manager/` (15), `fonts/` (8), `media/` (11), `menus/` (12), `options/` (7), `plugins/` (13), `recovery/` (7), `settings/` (11), `site-health/` (6), `taxonomies/` (10), `themes/` (10), `users/` (16), `widgets/` (2).
  * Examples: `acrossai/get-option` → `options/get-option`; `acrossai/list-db-tables` → `database/list-db-tables`; `acrossai/create-user` → `users/create-user`.
  * The second segment is unchanged from what shipped before — only the vendor prefix moves. No collapsing (unlike Elementor/Rank Math, where the redundant fragment was literally the namespace name).

**Why:** topic namespaces make the ability surface discoverable — a client fetching `mcp-adapter-discover-abilities` and filtering on the prefix gets exactly the domain it asked for. The old `acrossai/` prefix tagged ownership but carried no discovery information. Full per-ability inventory is now published at `docs/abilities-inventory.md`.

**Category taxonomy slugs (`acrossai-abilities-manager-*`) are unchanged.** Internal PHP class namespaces are unchanged. Tests, spec artifacts, and docstring cross-references were updated in the same commits as the slug renames — nothing left pointing at `acrossai/*`.

= 0.0.29 - 2026-08-18 =
**Feature 065 — safety envelope + payload enrichment across 9 existing abilities.** No new abilities. Plugin version bumped 0.0.28 → 0.0.29. Two changes are breaking for programmatic callers: `media/delete-media` and `file-manager/delete-file` now require an explicit `confirm: true`; `content/update-post` silently strips protected meta keys (reported back in `dropped_meta_keys`). Every guardrail-triggered refusal now returns `success: false` + a machine-readable `blocked_reason` + a human `message`, with no state mutation on the refusal path.

* **`plugins/deactivate-plugin` — protected-plugin guard.** Refuses to deactivate `acrossai-mcp-manager`, `acrossai-abilities-manager`, or `acrossai-pro` — the three plugins that host either the ability surface itself or the MCP transport the AI is using to reach the site. Match runs against the *resolved* plugin file path, so slug / partial-name / file-path variants that fuzzy-resolve to a protected plugin are all refused (`blocked_reason: "protected_plugin"`).
* **`media/delete-media` — explicit confirmation + trash-aware.** Requires `confirm: true` (refuses with `blocked_reason: "confirmation_required"` otherwise). Honours the `MEDIA_TRASH` constant — trashes when defined truthy and `force` is absent; permanent-deletes otherwise. Response now carries `deleted: "deleted" | "trashed"`.
* **`file-manager/delete-file` — confirmation + protected-write + backup + opcache invalidation.** Requires `confirm: true`. Refuses on `wp-config.php` / `.htaccess` at ABSPATH (`blocked_reason: "protected_write"`). Writes a `.bak.<timestamp>` copy next to the target before the delete and returns the backup path in `backup`. Calls `opcache_invalidate()` on the deleted path when OPcache is loaded.
* **`file-manager/read-file` — protected-read + size cap + binary detection.** Refuses on `wp-config.php` / `.htaccess` at ABSPATH (`blocked_reason: "protected_read"`) — this closes the highest-value accidental disclosure path (database password + eight auth constants). Refuses files over 5 MB without loading them into memory (`blocked_reason: "file_too_large"`; response reports observed size + cap). Non-UTF-8 payloads return `{ binary: true, size, path, message }` instead of raw bytes.
* **`media/list-media` — alt-text search.** `search` now matches against `_wp_attachment_image_alt` postmeta in addition to WP_Query's default `s` fields (title / caption / description). Results are de-duplicated by attachment ID, so an image matched by both title and alt-text appears once.
* **`media/update-media` — updated-fields report.** Response now carries an `updated` array naming each field that was actually written (subset of `title` / `caption` / `description` / `alt_text`), in the order fields were processed. Empty array when no update fields were passed.
* **`content/update-post` — writability + protected-meta + publish / author gates.** Refuses on post types that are neither `public: true` nor `show_in_rest: true` (matches WP-REST writability). Filters caller-supplied `meta` to drop `_`-prefixed keys and any key that `is_protected_meta()` reports; the `acrossai_allowed_protected_meta` filter opts specific keys back in. Dropped keys are reported in the response as `dropped_meta_keys`. Refuses `status: "publish"` (or any status entering a public state) unless the caller holds `publish_posts` for the post type. Refuses `author: <different_user_id>` unless the caller holds `edit_others_posts`.
* **`content/get-post` — hydrated payload.** Response now includes `terms` (object keyed by taxonomy, each entry `{ term_id, name, slug }`), `meta` (non-protected keys only — same allow-list filter as `update-post`), `featured_image` (`{ id, url, alt }` or `null`), `permalink`, `edit_link`, and `author: { id, name }`. Callers no longer need 4–5 follow-up hydration calls per post.
* **`content/delete-post` — suggested-redirect hint.** When the target was `publish` and `force: true` is passed, the response includes `suggested_redirect: { from: <permalink>, to: <parent-or-archive-or-root-url> }`. Omitted for drafts and for trash operations (URL may return on restore).

**Test coverage.** `Test_Feature_065_Safety_And_Payload` — 23 source-inspection tests covering all 23 FRs. Full suite green; PHPCS (WPCS strict) and PHPStan level 8 clean.

= 0.0.28 - 2026-08-17 =
**Feature 069 — Rank Math ability suite: 61 new abilities under a new "Rank Math" tab.** Gated on Rank Math SEO being active; absent entirely without it. Plugin version bumped 0.0.27 → 0.0.28.

Coverage baseline was deliberately narrow: Rank Math core ships **13** abilities of its own under `rank-math/`, and only those 13 counted as existing coverage. The third-party `mcp-abilities-rankmath` companion plugin was **not** treated as coverage — it is not ours to maintain, its writes go through raw `update_option()` blobs that bypass Rank Math's sanitizer, and it gates every ability on blanket `manage_options` regardless of the Role Manager. Slugs do not collide (`rank-math/` vs `rankmath/` vs `rank-math/`).

**Batch 1 — plumbing.** `RankMath\Category_Registrar` registers `acrossai-abilities-manager-rank-math`, guarded on `class_exists('\RankMath\Helper')`. `Base_Rank_Math_Ability` is the sole assembler of `ability()` and sole enforcer of the `execute()` guard order, which is what guarantees `tab_group => 'rank-math'` on all 61 — the Feature 078 regression class. `Rank_Math_Guard` holds every guard plus the response envelope.

**Batch 2 — typed settings (6 abilities).** `rank-math/get-settings` reads any of 20 panels with each field's type, allowed values, bounds and current value, which makes the writers' accepted keys discoverable at runtime. `-update-general-settings`, `-update-title-settings` and `-update-sitemap-settings` take a section/scope enum, replacing ~20 near-identical per-panel classes. `-update-instant-indexing-settings` and `-update-robots-txt` are separate because the first writes a different option and the second is conditional on state the caller cannot see. Titles & Meta templates — the global per-post-type and per-taxonomy layer — had no read or write anywhere before this.

**Batch 3 — Instant Indexing, modules, sitemap, routes (10 abilities).** `-submit-urls`, `-get-indexing-log`, `-clear-indexing-log`, `-reset-indexing-key`; `-list-modules` and `-set-module-state`; `-get-sitemap-status`, `-list-sitemap-urls`, `-invalidate-sitemap-cache`; `-get-llms-status` and `-refresh-llms-route`. `-set-module-state` replicates Rank Math's own `save_module()` in full including the rewrite-rule refresh and `rank_math/module_changed` action — omitting either leaves stale rewrite rules, so the sitemap and llms.txt routes 404 while the module reports itself active.

**Batch 4 — redirections, 404 logs, roles (13 abilities).** `-list-redirections` (with the `status=trashed` filter), `-find-redirection`, `-get-redirection-stats`, `-export-redirections`, `-create-redirection`, `-update-redirection`, `-change-redirection-status`, `-delete-redirections`, `-delete-trashed-redirections`; `-list-404-logs` and `-delete-404-logs`; `-get-role-capabilities` and `-reset-role-capabilities`. **`-update-redirection` fills a real gap: nothing could previously EDIT a redirection**, and emulating it by delete-then-recreate loses the rule's id, hit counter and creation date. Apache/Nginx export is a port of Rank Math's private formatters, since its own exporter reads `$_GET`, calls `check_admin_referer()`, echoes and exits.

**Batch 5 — status, maintenance, backups (8 abilities).** `-get-status` (5 panels behind an enum), `-run-maintenance-tool` (12 tools behind an enum), `-export-settings`, `-import-settings`, `-list-backups`, `-create-backup`, `-manage-backup`, `-detect-seo-plugins`, plus `-get-seo-analysis-results` for the cached audit.

**Batch 6 — analytics and post-level content (16 abilities).** `-get-analytics-summary` (6 reports), `-get-analytics-rows` (3 datasets), `-get-index-status`, `-inspect-url`; `-update-seo-meta`, `-bulk-update-meta`, `-update-seo-scores`, `-get-primary-term`, `-update-primary-term`, `-update-post-schemas`, `-delete-post-schemas`, `-get-schema-status`, `-get-rendered-head`, `-audit-content-seo`, `-get-inbound-links`, `-audit-faq-links`. `-get-inbound-links` answers which pages link **to** a page, including navigation-menu links — the opposite direction from every existing outbound-link ability.

**Batch 7 — entitlement-gated (6 abilities).** `-get-content-ai-status`, `-manage-content-ai-prompts`, `-manage-content-ai-output`, `-research-keyword`; `-get-ai-visibility-brand`, `-update-ai-visibility-object`. Registered **unconditionally** and gated at runtime, deliberately unlike `register_elementor_pro_abilities()`: Content AI and AI Visibility ship in Rank Math *free* and gate on cloud-account registration plus a credit balance, not on a separate plugin, so availability can change without an activation and cannot be decided at registration time.

**Security.** Every ability requires `manage_options` **and** Rank Math's own granular `rank_math_*` capability, matching the convention across the rest of the plugin's ability suites. The floor is uniform across all 61 and declared `final` so it cannot be lowered per ability. Revoking a capability in Rank Math's Role Manager therefore genuinely blocks the corresponding ability, which the companion plugin's blanket `manage_options` ignores. One documented filter, `acrossai_abilities_manager_rank_math_permission`, lets site owners relax the policy. Twelve abilities are irreversible and require `confirm: true`. The post-scoped writers additionally perform per-object `edit_post` / `edit_user` / `edit_terms` checks inside their handler as defence in depth, and the schema writers verify that a `schema-<meta_id>` row actually belongs to the named object before writing — Rank Math addresses schema rows by meta id and would otherwise update a different object's row.

**Data-loss prevention.** Rank Math's settings sanitizer defaults any field it was not told the type of to single-line text, which strips newlines, and its own field definitions use *legacy* CMB2 type names while the sanitizer's cases are the *React* names — 11 of 19 legacy types match no case. `Settings_Registry` therefore ships a declarative field-spec table for all 20 panels, mirroring the Rank Math source with a file citation per panel, and maps legacy names onto the sanitizer's vocabulary. Verified live with a control: writing `nofollow_domains` with the mapped `textarea` type preserves newlines, while the identical write using Rank Math's own declared `textarea_small` stores them collapsed onto one line. Nine multi-line settings were at risk.

**Notable.** No raw Rank Math option read/write ability ships — the plugin already provides generic option abilities, and adding Rank Math-branded raw writers would reintroduce exactly the data-loss path above. No bulk role-capability writer ships either, because `Helper::set_capabilities()` strips capabilities from roles omitted from the payload; the existing per-capability abilities cannot trigger that. The `.htaccess` editor, version rollback and beta opt-in are out of scope.

= 0.0.27 - 2026-08-14 =
**Patch release — UI polish + admin-surface rename following the 0.0.26 Feature 067 rollup.** No new abilities; both entries below are UX-affecting changes to the admin surface. Plugin version bumped 0.0.26 → 0.0.27.

* **Rename — "Ability Library" admin page is now "Ability Integrations".** The submenu label ("Library" → "Integrations"), page title ("Ability Library" → "Ability Integrations"), main heading, and URL slug (`page=acrossai-abilities-library` → `page=acrossai-abilities-integrations`) all updated. Bookmarks / external links to the old slug will 404 in wp-admin — update saved links to the new URL. Internal class names, hook names, REST endpoint namespace (`/wp-json/acrossai-abilities-library/v1/`), and the DOM mount id are unchanged (deliberately scoped rename — extending to the REST namespace would break external MCP callers).
* **UI fix — Elementor abilities now render under their own "Elementor" tab in the Ability Integrations screen, not "Core".** Every Elementor ability (all 88 under `elementor/*`) had its meta `tab_group` set to `'core'`, causing the group to appear in the Core tab with only a sub-heading identifying it as Elementor. Flipped every declaration to `tab_group => 'elementor'` (63 files including `Base_Audit_Ability`, which drives the 25 audit subclasses via inheritance). The Ability Integrations UI auto-derives tab names from distinct `tab_group` values, so a new "Elementor" tab appears without any frontend/asset rebuild.

= 0.0.26 - 2026-08-14 =
**Release rollup — 89 abilities total: 87 unreleased Elementor abilities (Feature 067 completion) + 2 native site maintenance-mode abilities.** Plugin version bumped 0.0.25 → 0.0.26. Elementor abilities gate on `class_exists('\Elementor\Plugin')` (with 8 additionally gated on Elementor Pro); site maintenance-mode toggle has no plugin dependency.

* **Native site maintenance-mode toggle (2 abilities):**
  * `site-health/set-site-maintenance-mode` — activate WordPress core maintenance mode by writing the `ABSPATH/.maintenance` marker file (the same file WP core writes during plugin/theme/core updates). A wp-cron event refreshes the marker every 5 minutes so the site stays down for the requested `duration_minutes` (default 60, hard-cap 1440). Requires `confirm=true` — blocks wp-admin as well as the frontend.
  * `site-health/unset-site-maintenance-mode` — deactivate: delete the marker, clear the refresh cron, drop the expiry option. Idempotent — safe to call when maintenance mode is already inactive. Reports `was_active` in the response.
  * Both live under the existing `acrossai-abilities-manager-site-health` category alongside `site-health/get-maintenance-mode-status` (Feature 063 read). No Elementor / plugin dependency — works on every WP install.

* **Feature 067 COMPLETE — 87 additional Elementor abilities ship in this release.** Combined with the 2 foundation abilities from 0.0.25, the full 88 planned abilities are now available under the `elementor/*` namespace. Design-audit ability logic is skeletal (`Base_Audit_Ability` skeleton returning empty findings) — real audit heuristics to be filled in follow-up work.

**Batch 10 — full-document replacement (closes the parity gap):**
  * `elementor/update-data` — overwrite the entire `_elementor_data` tree for a post with a caller-supplied element array; optional `page_settings` merge; `force_replace=true` required when the new payload is materially smaller than the existing document. Returns `element_count` + cache scope report.

**Batch 9 — 29 design-audit abilities (this commit):**

Aggregators + scorers (4):
  * `elementor/evaluate-design` — aggregate report from every registered design audit (score + findings + recommendations).
  * `elementor/suggest-design-fixes` — turn aggregated findings into concrete fix recommendations.
  * `elementor/score-distinctiveness` — neutral distinctiveness score for structural repetition.
  * `elementor/extract-design-tokens` — extract recurring colors / typography / spacing / dimensional tokens.

Individual audits (14):
  * Column: `audit-column-alignment-rhythm`, `audit-column-balance`, `audit-column-dominance`, `audit-column-necessity`, `audit-column-patterns`
  * Composition & emphasis: `audit-composition-rhythm`, `audit-emphasis-drift`, `audit-section-rivalry`, `audit-separator-discipline`, `audit-surface-overuse`
  * Layout & repetition: `audit-generic-component-repetition`, `audit-generic-layout-patterns`, `audit-layout-mechanism-fit`, `audit-native-widget-opportunities`

Subtree operations — destructive (7):
  * `apply-text-hierarchy`, `enforce-boundary-coherence`, `fix-visible-gap-rhythm`, `normalize-responsive-values`, `normalize-section-spacing-rhythm`, `reset-negative-margins-subtree`, `zero-container-padding-subtree`

Copy / sync / convert helpers — destructive (4):
  * `copy-lane-settings`, `copy-row-balance`, `image-widget-to-background-container`, `sync-component-variant`

New utility class `includes/Abilities/Elementor/Base_Audit_Ability.php` provides the shared skeleton for 27 of the 29 audit abilities — subclasses supply `audit_slug`, `audit_label`, `audit_description`, and `analyze()`. `Evaluate_Design` and `Suggest_Design_Fixes` are self-contained aggregators.

**Batch 8 — 8 Elementor Pro-gated abilities:**
  * `elementor/list-custom-code` — list Custom Code snippets from `elementor_snippet` CPT; optional location filter.
  * `elementor/get-custom-code` — read one snippet including its code body.
  * `elementor/create-custom-code` — create snippet with title, code, location (head / body_start / body_end / footer), priority, status.
  * `elementor/update-custom-code` — update snippet fields.
  * `elementor/delete-custom-code` — trash (default) or permanently delete with `force=true`.
  * `elementor/list-form-submissions` — list Form widget submissions from the `e_submissions` table; optional `form_id` filter + `include_values` flag. Graceful degradation when the Pro submissions table is missing.
  * `elementor/get-form-submission` — read one submission by ID; optional field values.
  * `elementor/delete-form-submission` — permanently delete submission + its `e_submissions_values` rows; requires `confirm=true`.

All 8 Pro abilities gated on **both** `class_exists( '\Elementor\Plugin' )` **and** `class_exists( '\ElementorPro\Plugin' ) || defined( 'ELEMENTOR_PRO_VERSION' )` — silently absent on sites without Elementor Pro. Runtime deactivation returns `error_code: elementor_pro_missing`.

**Batch 7 — 7 kits & site-settings abilities:**
  * `elementor/list-kits` — list all Elementor kits; marks active kit.
  * `elementor/get-kit-settings` — read kit settings (defaults to active kit).
  * `elementor/update-kit-settings` — merge new settings; `force_replace` for full overwrite; site-wide cache invalidation.
  * `elementor/set-active-kit` — switch site-wide active kit; invalidates cache.
  * `elementor/list-global-widgets` — list global (reusable) widgets from elementor_library CPT.
  * `elementor/list-experiments` — list feature flags with current + default state.
  * `elementor/update-experiment` — toggle experiment state (active | inactive | default).

**Batch 6 — 11 template abilities:**
  * `elementor/list-templates` — list saved templates with filters on `template_type` + `status` + pagination.
  * `elementor/get-template` — return one template's metadata + conditions + optional `_elementor_data`.
  * `elementor/create-template` — create a new template of type page / section / popup / header / footer / single / archive; sets taxonomy term + Elementor meta.
  * `elementor/update-template` — update title / page_settings / full data with `force_replace` guard.
  * `elementor/delete-template` — trash (default) or permanently delete with `force=true`.
  * `elementor/restore-template` — restore a trashed template.
  * `elementor/duplicate-template` — clone template preserving type + conditions + sub_type; regenerates element IDs.
  * `elementor/empty-trash` — permanently delete every trashed template; requires `confirm=true`.
  * `elementor/export-template` — export template as JSON-encodable object (title, template_type, sub_type, page_settings, content, conditions).
  * `elementor/import-template` — import from JSON export; regenerates element IDs; optional `overwrite_id` to replace an existing template.
  * `elementor/find-template-for-pattern` — rank saved templates by keyword match (title + tax term + widget-types in content); returns top N with scores.

**Batch 5 — 11 site-management abilities:**
  * `elementor/clear-cache` — clear Elementor cache at post / site / all scope; optional `regenerate_css=true` for a specific post.
  * `elementor/replace-urls` — bulk find/replace URLs across every Elementor document on the site with `dry_run=true` default preview.
  * `elementor/get-maintenance-mode` — read current maintenance mode settings (mode, template, exclude rules).
  * `elementor/update-maintenance-mode` — enable/disable maintenance mode with mode selection (maintenance | coming_soon).
  * `elementor/get-theme-builder-conditions` — read display conditions attached to an Elementor template.
  * `elementor/update-theme-builder-conditions` — replace display conditions; pass empty array to clear. Invalidates Elementor's condition cache.
  * `elementor/get-official-widget-catalog` — canonical widget catalog (Basic / Pro / Theme / WooCommerce) with 12-hour transient.
  * `elementor/get-official-pattern-guidance` — pattern & layout guidance (widgets / patterns / layouts topics) grounded in Elementor documentation.
  * `elementor/get-theme-context` — active theme + Elementor version + active kit + viewport settings snapshot.
  * `elementor/get-style-guide` — style-guide summary from active kit (colors, typography, buttons, forms, layout, custom CSS).
  * `elementor/evaluate-render-context` — inspect frontend template + canvas type + edit-mode flag for a post.

**Batch 4 — 9 page-composition abilities:**
  * `elementor/create-page` — insert a new post/page pre-configured for Elementor (sets `_elementor_edit_mode`, `_elementor_template_type`, `_elementor_version`; seeds empty `_elementor_data`). Returns edit URL.
  * `elementor/update-page-settings` — merge new page-level settings into `_elementor_page_settings`. `force_replace` guard on materially-smaller payloads.
  * `elementor/patch-data` — find/replace text within the raw Elementor JSON string; updates every widget containing the match in one pass.
  * `elementor/clone-data` — copy the full Elementor tree from one post to another with fresh element IDs throughout. Optionally include page settings. `force_replace` guard on populated targets.
  * `elementor/add-heading` — widget shortcut (title, header_size h1-h6, align, title_color).
  * `elementor/add-text-editor` — widget shortcut (editor HTML, align).
  * `elementor/add-image` — widget shortcut (image ID or URL, size, align, caption, link).
  * `elementor/add-button` — widget shortcut (text, link, size xs-xl, align).
  * `elementor/add-post-tabs` — higher-order shortcut: Nested Tabs widget where each tab contains a native Posts widget (optionally filtered by taxonomy term or query args).

**Batch 3 — 6 element-lifecycle abilities (previously in this section):**
  * `elementor/merge-element-settings` — deep-merge new settings into an element by ID. Additive (no force_replace guard needed); reports `changed_keys` in the response.
  * `elementor/delete-element` — remove an element by ID. Guarded by `force_delete=true` for top-level or populated (with-children) elements.
  * `elementor/remove-element` — safer alias for `delete-element` with identical semantics.
  * `elementor/move-element` — atomic move to a new parent/position with descendant-guard preventing cycle-creating moves into own subtree.
  * `elementor/duplicate-element` — deep-clone an element (all nested children included) with fresh IDs generated throughout the cloned subtree; inserted as the next sibling.
  * `elementor/reorder-elements` — reorder direct children of a parent (or root); children omitted from `ordered_element_ids` retain their prior relative order and are appended after.

**Batch 2 — 5 abilities merged earlier:**
  * `elementor/get-element` — read a single element by 7-char hex ID.
  * `elementor/find-elements` — search by `element_type` / `widget_type` / contains-text.
  * `elementor/update-element` — replace by ID with `force_replace` guard.
  * `elementor/add-container` — insert Elementor v3+ container.
  * `elementor/add-widget` — insert any registered widget (validated via `Widget_Controls`).

**Test coverage:** 43 (b2) + 40 (b3) + 53 (b4) + 64 (b5) + 53 (b6) + 33 (b7) + 45 (b8) + 8 (b9 manifest) = 339 new source-inspection tests across 58 test files. Full suite: 975 tests, 1991 assertions, 0 failures. phpcs (WPCS strict) and phpstan (level 8) both clean.

**Feature 067 ability surface complete: 88 of 88 abilities.** Foundation + 2 shipped in 0.0.25 + 86 in this release. Every planned ability shipped. Design-audit analysis logic is skeletal (returns empty findings + recommendations); the audit surface is registered and composable via `Design_Audit_Runner`, real heuristics to be filled in follow-up work.

= 0.0.25 =
* **New — Feature 067 Elementor Ability Suite (interim ship: foundation + 2 abilities).** First release of the planned 88-ability Elementor integration. This interim release delivers the full foundational infrastructure plus two highest-value abilities. Follow-up features (068+) will incrementally add the remaining 86 abilities.

**Foundation — 6 utility classes + category registrar under `includes/Abilities/Utilities/Elementor/` and `includes/Abilities/Elementor/`:**
  * `Category_Registrar` — registers the new `acrossai-abilities-manager-elementor` ability category. Self-guards on `class_exists( '\Elementor\Plugin' )` so the category is silently absent on non-Elementor sites.
  * `Document_Repository` — Elementor document I/O with mandatory `wp_slash()` policy on `_elementor_data` writes, cache invalidation (Elementor files manager + WP post cache + `_elementor_css` meta delete), and full tree helpers (find/insert/remove/reorder/replace by element ID, deep-clone with fresh IDs, descendant-guard).
  * `Widget_Controls` — schema-safe summariser over Elementor's `WidgetsManager::get_widget_types()` with case-insensitive control-name filtering.
  * `Template_Query` — `WP_Query` wrappers for the `elementor_library` CPT with tax filters + keyword-scoring for pattern-search abilities.
  * `Guidance_Catalog` — canonical Elementor.com widget catalog (60+ Basic/Pro/Theme/WooCommerce widgets seeded, 12-hour transient) + pattern & layout guidance data (nav-menu vs mega-menu, container vs section, Grid vs Flexbox for symmetric columns, etc.).
  * `Design_Audit_Runner` — orchestrator for the 28 design-audit abilities landing in follow-up features (register + run individual + run-all with aggregate score + findings + recommendations).

**Bootstrap gating** in `includes/Abilities/AcrossAI_Core_Abilities_Bootstrap.php`:
  * Two-layer gate: outer `class_exists( '\Elementor\Plugin' )` at `plugins_loaded` P20 (registration-time) plus per-ability defense-in-depth check at execution time (runtime deactivation returns clean `error_code: elementor_missing` envelope, no fatals).
  * Inner Pro gate: `class_exists( '\ElementorPro\Plugin' ) || defined( 'ELEMENTOR_PRO_VERSION' )` for the future Custom Code + Form Submissions abilities.
  * Split into two private methods `register_elementor_free_abilities()` + `register_elementor_pro_abilities()` — new `new Elementor\<Class>()` lines added as each ability class lands.

**Two shipped abilities under `elementor/*` namespace:**
  * `elementor/get-widget-controls` — schema-lookup primitive. Returns the schema-safe control summary for any registered Elementor widget on the current site (free + Pro + third-party). Enables clients to author valid add-widget / update-element payloads without hard-coded per-widget wrappers. Optional case-insensitive search filter.
  * `elementor/get-data` — the read primitive. Returns the parsed Elementor document tree + page settings for a post, plus recursive element count.

**Test coverage:** 44 new utility tests + 15 new ability tests = 59 additional PHPUnit assertions. Full suite: 636 tests, 1530 assertions, 0 failures. phpcs (WPCS strict) and phpstan (level 8) both clean.

**Test-bootstrap additions:** stubs for `wp_rand`, `get_transient`, `set_transient`, `delete_transient`, and `HOUR_IN_SECONDS` constant to support the new utilities under the unit-only bootstrap.

**Spec artifacts** at `specs/067-elementor-abilities/`: complete design for all 88 abilities documented in `spec.md` / `plan.md` / `research.md` / `data-model.md` / `contracts/abilities.md` / `quickstart.md` / `tasks.md` — follow-up features will implement Phases 3-13 tasks against these contracts.

= 0.0.24 =
* **New — 6 abilities and 1 enhancement for full Gutenberg block-tree control (feature 066).** Closes the gap between the plugin's existing block-registry surface and per-post block-tree manipulation. All abilities live under the existing `acrossai-abilities-manager-content` category.

**Feature 066 — Block tree mutation & nested editing (6 new abilities + 1 modified).**
  * `blocks/get-post-blocks` — return a post's parsed Gutenberg block tree with each block annotated with its canonical integer-array path (e.g. `[0, 2, 1]` = 2nd grandchild of the 3rd child of the 1st top-level block). Read-only, idempotent.
  * `blocks/add-block` — insert a new block into a post at `parent_path` + `index`. Appends when the requested index exceeds the current sibling count.
  * `blocks/remove-block` — remove the block at a canonical path; returns the removed payload so callers can undo/log.
  * `blocks/duplicate-block` — deep-clone the block at a path (including all inner blocks) and insert the clone as the next sibling.
  * `blocks/move-block` — atomically move a block from `from_path` to `to_parent_path` + `to_index`. Refuses moves into the source's own subtree (would create a cycle).
  * `blocks/insert-pattern` — resolve a saved block pattern by slug across database / active theme / installed plugins, then insert its constituent blocks at `parent_path` + `index`. Ambiguous slugs return `multiple_locations` so callers can disambiguate via `source` / `theme_type` / `plugin_slug`.
  * `blocks/update-post-block` (modified) — now accepts an optional `path` input for nested editing at any depth. Existing consumers using `block_index` or `block_name` + `occurrence` see **zero behaviour change** — the path branch is a strict addition.
  * All write abilities share the same guards as the existing `update-post-block`: `manage_options` + `edit_posts` globally, `edit_post` per-post, post-type whitelist against internal CPTs (revision / nav_menu_item / custom_css / customize_changeset / oembed_cache / user_request), block-name regex validation, and soft-fail attribute-schema validation against the registered block type.
  * Shared `Block_Tree` utility (`includes/Abilities/Utilities/Block_Tree.php`) centralises tree-path primitives — walk, get-at-path, insert / remove / replace / move, block-name and attribute-schema validation. Extracts what was previously private inline logic in `Update_Post_Block::execute`.
  * Test coverage: 82 new PHPUnit assertions across 8 test files.

= 0.0.23 =
* **New — 30 abilities across three feature spec drops (062, 063, 064).** Bulk expansion of the plugin's ability surface. No breaking changes.

**Feature 062 — Role & capability CRUD + site-wide DB search-replace (8 abilities).**
  * `users/add-role-capability`, `users/remove-role-capability`, `users/create-role`, `users/delete-role`, `users/reset-role`, `users/add-user-capability`, `users/remove-user-capability` — writers for the role/cap surface WordPress core REST does not expose. Every write is `destructive: true`.
  * `database/search-replace` — site-wide serialized-data-safe string replacement across every WordPress-managed table. **`dry_run: true` by default** — the ability returns a per-table / per-column match tally without mutating any row, and mutating writes only happen when the caller explicitly passes `dry_run: false`. Table allowlist mirrors `Update_Db_Rows.php` (validates every input table against `SHOW TABLES` before scanning). Skips `wp_posts.guid` unless the caller explicitly opts in via `include_guids: true` (safer default than WP-CLI). Recursive `maybe_unserialize` / `maybe_serialize` walk keeps serialized meta / options structurally valid.
  * Guardrails: `remove-role-capability` refuses to strip a WP-core administrator baseline capability from the `administrator` role; `delete-role` refuses on any of the 5 built-in roles AND when the role is still held by any user; `reset-role` accepts only the 5 built-in role slugs; `remove-user-capability` refuses to strip a WP-core admin cap from the last remaining administrator.

**Feature 063 — Site introspection reads + new Widgets category (11 abilities).**
  * `core/get-wp-version`, `database/get-db-prefix`, `file-manager/get-wp-config-constant`, `themes/list-theme-mods`, `settings/list-rewrite-rules`, `media/list-image-sizes`, `comments/get-comment-count`, `site-health/get-maintenance-mode-status`, `cron/test-wp-cron` — small single-purpose reads that WordPress does not expose through a public REST endpoint. Every ability is `readonly: true, idempotent: true, destructive: false`.
  * `widgets/list-widgets`, `widgets/list-sidebars` — legacy widget-system introspection under a new **Widgets** category (slug `acrossai-abilities-manager-widgets`).
  * Guardrails: `get-wp-config-constant` hard-blocks disclosure of `AUTH_KEY`, `SECURE_AUTH_KEY`, `LOGGED_IN_KEY`, `NONCE_KEY`, `AUTH_SALT`, `SECURE_AUTH_SALT`, `LOGGED_IN_SALT`, `NONCE_SALT`, and `DB_PASSWORD` regardless of the `manage_options` gate; `get-maintenance-mode-status` uses WordPress core's own 10-minute staleness threshold; `test-wp-cron` fires a single non-blocking `wp_remote_get()` with a 0.01s timeout so it never hangs a REST response.

**Feature 064 — Transient CRUD, nested option access, plugin lifecycle & checksum integrity (11 abilities).**
  * Transient CRUD (Cache category): `cache/get-transient`, `cache/list-transients` (paginated, search-filterable, expiry-aware), `cache/delete-transient`, `cache/delete-expired-transients` — closes the previous read-nothing / bulk-only-delete gap.
  * Nested option access (Options category): `options/get-nested-option-value` and `options/patch-option-value` — read or mutate one nested key inside a serialized option without round-tripping the whole blob. Guarded by `Update_Option::BLOCKED_OPTIONS` (extracted as a `public const` on `Update_Option` in this release so both classes share one authoritative block-list of 21 protected core options).
  * Post-meta append (Content category): `content/add-post-meta` — WordPress core `add_post_meta()` semantics with the WP-core `unique` flag. Complements the existing update / delete post-meta writers.
  * Plugin lifecycle (Plugins category): `plugins/search-wp-plugin-directory` (searches the WordPress.org plugin directory via `plugins_api()`; short description sanitised via `wp_kses_post()`), `plugins/uninstall-plugin` (fires the plugin's registered uninstall hook + deletes files via WP core `uninstall_plugin()`; refuses on active plugins and on sites with `DISALLOW_FILE_MODS`), `plugins/verify-plugin-checksums`.
  * Core integrity (Core category): `core/verify-core-checksums` — fetches the official `api.wordpress.org` checksums manifest via `wp_remote_get()` and compares `md5_file()` hashes; per-file `status: 'ok'|'modified'|'missing'|'added'` and a summary counter.

* **Every one of the 30 new abilities gates on `current_user_can( 'manage_options' )`** using the identical permission-callback pattern already used by all 219 existing abilities: `static function (): bool { return current_user_can( 'manage_options' ); }`. No cap escalation via filter.
* **One new ability category — Widgets** (`acrossai-abilities-manager-widgets`), registered via `includes/Abilities/Widgets/Category_Registrar.php` mirroring the shape of `includes/Abilities/Menus/`.
* **204 new PHPUnit test methods** on top of the previous 191 (final suite: ~395 methods across the 8.1 → 8.5 PHP CI matrix). Every new class file passes PHPStan level 8 and the plugin's PHPCS WPCS strict profile.
* **No breaking changes.** No ability slug rename. No REST endpoint change. No option-shape change. No new required capability. Existing 218 abilities behave identically. The `Update_Option::BLOCKED_OPTIONS` extraction in Feature 064 is a pure move of an inline literal into a `public const`; behaviour is unchanged. Safe upgrade from 0.0.22.

= 0.0.22 =
* **New — `content/delete-post-meta` ability under the Content category.** Deletes a single post meta row via WordPress core `delete_post_meta()`. Accepts `post_id` + `key` (with the WP-core-native `meta_key` alias) and an optional `value` (with `meta_value` alias). When a value is supplied, only rows matching that value are removed; otherwise every row for the given key is removed. Gated by `manage_options`; annotated `destructive: true`, `idempotent: true`. Mirrors the shape of `content/update-post-meta` for consistent client ergonomics.
* **Fixed — `content/update-post-meta` no longer rejects protected meta keys (#99).** The pre-0.0.22 `execute()` short-circuited with `success: false` whenever `is_protected_meta( $key, 'post' )` returned true, contradicting the class docblock ("Works for ANY meta key"). Now the ability writes any key the `manage_options` gate allows through — the capability check remains the sole access boundary. The registered `description` was also updated to match the new behaviour ("Works for any meta key, including protected keys.").
* **Composer dependency bump — `acrossai-co/main-menu` 0.0.30 → 0.0.33.** Rolls three shared-menu library releases into one hop; `composer.lock` regenerated to reference `e17e1e8`.
* **No breaking changes.** No ability slug rename. No REST endpoint change. No option-shape change. No new required capability. Existing 218 abilities behave identically. Safe upgrade from 0.0.21.

= 0.0.21 =
* **Composer dependency bump — `wpboilerplate/wpb-access-control` `^2.0.0` → `^3.1.0`.** Adopts two major releases of the shared access-control library in one hop:
  * **v3.0.0 (breaking, but not for this plugin).** The two plugin-dependent providers shipped in the library's v1.4.0 / v1.5.0 — `BuddyBossProfileTypeProvider` (`bb_profile_type`) and `MemberPressMembershipProvider` (`mepr_membership`) — were extracted into a separate WordPress add-on called **AcrossAI User Access Pro** (`acrossai/user-access-pro`), along with eight new integrations (LearnDash Group, LifterLMS Membership, Paid Memberships Pro, Restrict Content Pro, WooCommerce Memberships, s2Member Level, Wishlist Member Level, Memberium Membership). The library now ships only the three WordPress-native providers (`wp_role`, `wp_user`, `wp_capability`) plus a new `wpb_access_control_register_providers` global filter for add-on registration. **This plugin uses only `AccessControlManager` + `RuleTable` — neither of the removed provider classes.** No consumer-side code change is required; every existing Access Control rule shape is preserved and the per-consumer `AccessControlManager( $providers_filter, $table_slug )` constructor signature is unchanged.
  * **v3.1.0.** Adds a new `AccessControlManager::TYPE_AUTHENTICATED` (`'authenticated'`) sentinel rule type — grants access to any logged-in user without requiring a specific role or capability match. Rendered in the Access Control dropdown as "Any logged-in user", stored as a single sentinel row like `everyone`. Also renames the public option label from "Everyone (no restriction)" to "Public (no login required)" for clarity. Existing rules are untouched; the `everyone` key behaves identically.
* **New Access Control rule affordance on every ability.** Site administrators can now pick "Any logged-in user" from the Access Control dropdown on the ability edit panel — useful for abilities that should be reachable by every authenticated user (including subscribers) without curating a specific role list. Rules using the previous "Everyone" wording continue to work unchanged; the dropdown label just clarifies that `everyone` means "no login required."
* **Migration required only for sites vendoring the built assets.** Consumer plugins that pin `vendor/wpboilerplate/wpb-access-control/assets/build/` in their release bundle should `composer update` and rebuild to pick up the new dropdown option. This plugin re-vendors the library's compiled CSS via `admin/Main.php::enqueue_styles()` and the `composer update` this changelog entry documents already regenerates that asset path.
* **No breaking changes.** No ability slug rename. No REST endpoint change. No option-shape change. No new required capability. Every existing 218 abilities behave identically. Existing Access Control rules keep working — the removed BuddyBoss / MemberPress providers were never registered from this plugin (they defaulted to `is_available() === false` in the library's v1.6.0 – v2.0.x range on sites that had not explicitly opted in). Safe upgrade from 0.0.20.

= 0.0.20 =
* **Changed — access-control library-missing notice now routes through the shared AcrossAI notice hub.** The pre-0.0.20 `AcrossAI_Abilities_Access_Control::maybe_show_library_notice()` method was hooked on WordPress core `admin_notices` and printed a raw `.notice.notice-warning` banner on every admin screen when the `wpb-access-control` library wasn't loaded. It is renamed to `register_library_notice( array $notices ): array` and now registers into the new `acrossai_notices` filter shipped by `acrossai-co/main-menu` 0.0.30. The notice appears in two places instead: (1) as a card on the new **AcrossAI → Notices** submenu (only registered when at least one notice is present, with a WP-style count bubble on the menu label), and (2) as a single top-of-page WordPress-native `.notice.notice-warning.is-dismissible` summary banner ("AcrossAI has N notifications for your attention — View notices →") printed on every other admin page. Dismissal is fingerprint-persisted per user until the notice set changes. Notice record shape: `id=wpb_access_control_missing`, `type=warning`, `source=AcrossAI Abilities Manager`. Semantics are unchanged — the fail-open behaviour, the `manage_options` gate (enforced by the menu itself and the summary emitter), and the message copy are all preserved.
* **Composer dependency bump — `acrossai-co/main-menu` 0.0.29 → 0.0.30.** Ships the cross-plugin notice system this release routes through:
  * New `acrossai_notices` filter — any AcrossAI consumer plugin can push admin-notice records into a shared collection using a single documented record shape (`id`, `title`, `message`, `type`, optional `source`, optional `action { label, url }`). Later registrations of the same `id` are ignored (first-wins). Missing `id` or both `title` and `message` empty → the entry is dropped.
  * New **AcrossAI → Notices** submenu (slug `acrossai-notices`, class `NoticesPageRenderer`) — only registered when at least one notice exists. Menu label carries a WP-style count bubble (`.awaiting-mod`).
  * New top-of-page summary notice emitter (`SummaryNoticeEmitter`) — prints one WordPress-native dismissible banner on every other admin page linking to the Notices submenu. Dismissal is fingerprint-based (SHA-1 of sorted notice IDs stored in per-user meta `_acrossai_notices_summary_fp`) so the summary re-appears whenever the notice set changes.
  * New AJAX endpoint `wp_ajax_acrossai_notices_dismiss_summary` — nonce + `manage_options` guarded; server re-validates the client-supplied fingerprint against the current notice set as defense-in-depth against poisoning the user meta with an unrelated hash.
  * New public classes under `AcrossAI_Main_Menu\`: `Notices`, `NoticesPageRenderer`, `NoticesAjaxHandlers`, `SummaryNoticeEmitter`. New page-slug constant `SettingsPage::NOTICES_SLUG` and static accessor `SettingsPage::get_notices(): ?Notices` for consumers that want to inspect the current notice list programmatically.
* **Note — the vendor-missing boot-resilience notice in `Includes\Main::__construct()` remains on core `admin_notices`.** That code path fires precisely when the composer autoloader is absent — the moment when the shared main-menu package isn't loadable either — so the `acrossai_notices` filter cannot be reached from it. This is intentional and matches Constitution §V Integration Resilience.
* **No breaking changes.** No ability slug rename. No REST endpoint change. No option-shape change. No new required capability. Existing 218 abilities behave identically. Safe upgrade from 0.0.19.

= 0.0.19 =
* **New — MCP Manager promo callout on the ability edit form.** The MCP Exposure section (Section 3) of the Custom Abilities edit page now surfaces a blue-tinted informational callout advertising the sibling `acrossai-mcp-manager` plugin when it is not installed / active on the current site. The callout renders directly below the existing "Heads up" warning and offers two actions: an "Install from Add-ons" button that deep-links to the AcrossAI Add-ons page (`admin.php?page=acrossai-addons`), and a "Learn more" external link to `https://acrossai.co/mcp-manager/`. When the AcrossAI MCP Manager plugin IS active on the site, the callout is fully suppressed — zero UI on the edit form. Detection uses WordPress core `is_plugin_active( 'acrossai-mcp-manager/acrossai-mcp-manager.php' )` inside the admin script enqueue path; the resolved boolean plus the two URLs are injected into the existing `window.acrossaiAbilitiesManager` localize payload as `mcp_manager_active`, `mcp_manager_addons_url`, and `mcp_manager_info_url`. The callout also degrades gracefully on older bundles or a customised localize payload — the two action buttons render only when their corresponding URL keys are non-empty.
* **Composer dependency bump — `acrossai-co/main-menu` 0.0.27 → 0.0.29.** Two-hop bump rolled into one release:
  * *0.0.28* — refreshed the Add-ons page baseline catalogue. The hard-coded add-on list now surfaces three entries: **AcrossAI Abilities Manager** (wp.org), **AcrossAI MCP Manager** (wp.org), and **AI Connectors** (external "Get add-on ↗" link to `acrossai.co/ai-connectors/#pricing`). AcrossAI Model Manager and Turn Off AI Features are dropped from the hard-coded baseline — sites that still want them can register them via the `acrossai_addons` filter unchanged. All three baseline cards render the shared AcrossAI SVG logo from `acrossai.co` instead of per-plugin `ps.w.org` PNG icons, so the Add-ons page reads as one product surface. Icon fit switched from `cover` to `contain` (with 6px padding) so wide/horizontal SVG logos render fully instead of being cropped inside the 56×56 icon box. Grid pinned to a fixed 3-column layout (`repeat(3, minmax(0, 1fr))`) with responsive fallbacks (2 cols under 1100px, 1 col under 720px). New optional `learn_more_url` add-on field renders as a "Learn more" text link inside the card action row for every add-on regardless of `source`.
  * *0.0.29* — reworked the Add-ons card action states so the page reads as a **discovery surface, not a plugin manager**. Active add-ons now render a non-clickable green **"● Running"** pill instead of a "Deactivate" button; deactivation stays in Plugins → Installed Plugins where WP admins expect it (new CSS classes `.acrossai-addons__status` / `.acrossai-addons__status--active` / `.acrossai-addons__status-dot`). Installed non-`wordpress.org` add-ons now show an in-page **Activate** button instead of always rendering the external "Get add-on ↗" link — detection is source-agnostic and driven by `AddonsInstaller::find_plugin_file()`, so a paid/off-directory add-on that the admin uploaded via Plugins → Add New → Upload Plugin can be activated straight from the AcrossAI Add-ons page. The Install code path remains restricted to `wordpress.org` sources (WP.org guideline #8 — no change). The `AI Connectors` baseline entry declares `install_folder => 'acrossai-ai-connectors'` so install detection matches the actual plugin folder even though the registry slug (`ai-connectors`) differs — canonical example for consumers whose extracted folder ≠ slug.
* **No breaking changes.** No ability slug rename. No REST endpoint change. No option-shape change. No new required capability. Existing 218 abilities behave identically. Safe upgrade from 0.0.18.

= 0.0.18 =
* **New — Third-party integration framework (Feature 060) with Advanced Custom Fields as the first concrete integration.** Adds a new "Acf" tab to the Ability Library page (`/wp-admin/admin.php?page=acrossai-abilities-library&tab=acf`) with a single toggle labelled "Advanced Custom Fields (AI)". Flipping the toggle ON attaches `add_filter( 'acf/settings/enable_acf_ai', '__return_true' )` early enough in `plugins_loaded` (priority 20) that ACF picks it up on the same request and registers its FieldGroup / PostType / Taxonomy AI abilities. Flipping OFF leaves ACF's default (writes disabled) in place. Default is **OFF** for every integration — enabling AI-driven schema manipulation on a production site is always an explicit admin decision. The tab and card only appear when the target plugin (ACF) is installed AND active on the current site; deactivating ACF while the toggle is on preserves the saved state without leaking any error notice or fatal.
* **New — extensibility surface for third-party AcrossAI plugins.** Any WordPress plugin can now register its own regular ability cards on an integration's tab (alongside the integration's own toggle card) using a documented 3-step contract: (1) register the ability category on `wp_abilities_api_categories_init` via `wp_register_ability_category()`, (2) extend `\AcrossAI_Abilities_Manager\Includes\Modules\Library\Ability_Definition`, and (3) set `meta.acrossai.tab_group` on the ability's args to the integration's published `TAB_GROUP` constant (e.g. `\AcrossAI_Abilities_Manager\Includes\Abilities\Integrations\ACF::TAB_GROUP`). Reads from the new `AcrossAI_Integration_Ability_Base` docblock + the quickstart worked example under `specs/060-library-third-party-integration-toggles/quickstart.md`. This is the mechanism that lets the sibling `acrossai-acf-abilities` plugin surface its own cards on the same "Acf" tab.
* **New REST filter — `acrossai_integration_toggle_capability`.** Lets sites raise (never lower) the WordPress capability required to flip an integration toggle. Default is `manage_options` (matches the rest of the Ability Library page); a site can attach a filter returning e.g. `manage_network_options` and a `manage_options`-only user will then receive HTTP 403 on the REST write. Enforced server-side on the same write path that persists the toggle — cannot be bypassed by a crafted REST request even if the JS UI presented the toggle as interactive. Companion action `acrossai_integration_toggle_denied` fires immediately before the 403 so sites can wire audit logging without amending core code.
* **Bugfix — sparse-storage in `acrossai_library_config` was silently stripping integration ON entries.** The pre-Feature-060 sparse-storage rule in `AcrossAI_Ability_Library_Config::save_config()` assumed every category defaults to `enabled=true`, so a `{ enabled: true, mode: 'all', sub_keys: {} }` payload was stripped as "default state". Feature 060 integration categories invert that default (missing = OFF per FR-008), so the ON state was being silently dropped and the toggle appeared to turn itself off on reload. Fixed by teaching sparse-storage which slugs are integrations (via the new public helper `AcrossAI_Ability_Library_Registry::get_integration_slugs()`) and computing the correct default per-category before deciding whether to strip.
* **Composer dependency bump — `acrossai-co/main-menu` 0.0.23 → 0.0.27.** Two WordPress.org plugin directory guideline #8 fixes rolled into one bump:
  * *0.0.26* — the Consultations submenu (introduced in 0.0.24) previously embedded the Calendly widget via `assets.calendly.com/assets/external/widget.js` and an iframe pointed at `calendly.com/acrossai/using-ai-in-wordpress`. It now renders a self-contained call-to-action page that opens `calendly.com` in a new browser tab only when the admin clicks the button — no Calendly script, iframe, or asset is loaded inside wp-admin. Fixes the "using iframes for admin pages" prohibition.
  * *0.0.27* — the Add-ons page's install action is now WordPress.org-only. Cards whose `source` is `wordpress.org` continue to render an in-page Install / Activate / Deactivate button (routed through WordPress core's `plugins_api()` + `Plugin_Upgrader`). Cards with any other `source` (e.g. `github`, `freemius`, or any consumer-defined value) render an external "Get add-on ↗" link that opens the vendor's site in a new browser tab — users install those add-ons via WP admin's standard **Plugins → Add New → Upload Plugin** flow, or via the vendor's own installer. Fixes the "installing plugins/themes/add-ons from non-WordPress.org servers" prohibition. Same design pattern used by WooCommerce and GiveWP for their extension marketplaces. Also adds a new public helper `AddonsInstaller::is_installable_source( array $addon ): bool` and defense-in-depth rejection in the AJAX install handler.

  Purely additive on the AcrossAI parent-menu surface — no changes to any of our own submenus (Ability Library at priority 2, Settings at priority 20). `SettingsPage`'s public constructor signature is unchanged. Consumers that only ship wp.org-sourced add-ons see no visible change; consumers pushing GitHub/Freemius entries via the `acrossai_addons` filter will see those cards flip from Install button to "Get add-on ↗" link — no code change required on their side.
* **20 new PHPUnit tests + 12 new Jest tests** cover the base-class contract, the extension pattern end-to-end, resilience under target-plugin deactivation, the default-OFF safety property, and the sparse-storage bugfix regression. Full suite: 191 tests passing.
* **6 new durable memory entries** in `docs/memory/` (DECISIONS.md, ARCHITECTURE.md, BUGS.md, INDEX.md) capture the reusable patterns: `DEC-ABILITY-DEFINITION-CTOR-HOOKS`, `PATTERN-LIBRARY-INTEGRATION-BASE`, `PATTERN-LIBRARY-INTEGRATION-TAB-EXTENSION`, `PATTERN-FILTERABLE-CAPABILITY-RAISE-ONLY`, `BUG-WP-CORE-ABILITY-CATEGORY-PRE-REGISTRATION`, `BUG-SPARSE-STORAGE-UNIFORM-DEFAULT-ASSUMPTION`. Documented so the next contributor doesn't rediscover the same traps (specifically: WP core silently rejects abilities whose category isn't pre-registered via `wp_register_ability_category`, and asymmetric-default keys break naive sparse-storage optimisations).
* **No breaking changes.** No ability slug rename. No REST endpoint change. No option-shape change. No new required capability. Existing 218 abilities behave identically. Safe upgrade from 0.0.17.

= 0.0.17 =
* **New — 7 Recovery Mode abilities under a new `Recovery` category.** Adds `recovery/get-recovery-mode-status`, `recovery/list-paused-plugins`, `recovery/list-paused-themes`, `recovery/get-recovery-exit-url`, `recovery/unpause-plugin`, `recovery/unpause-theme`, and `recovery/list-recent-fatal-errors`. Together they let an AI agent driving the site over REST/MCP detect if WordPress has entered Recovery Mode after a fatal error, enumerate paused (fatally-erroring) plugins and themes with their captured error details, clear a paused entry so WP retries loading the extension on the next request, retrieve the admin-clickable exit URL, and pull grouped fatal-error signatures from `debug.log`. Every write action gates on `manage_options` + `File_Mods_Guard::blocked_response()`; fuzzy plugin/theme identifiers flow through the existing `Plugin_Helpers::resolve_plugin()` / `Theme_Helpers::resolve_theme()` resolvers.
* **Intentional non-goals: no programmatic recovery-mode trigger, no programmatic exit.** WP core has no public API to enter recovery mode from a REST call (the handler only fires on a real fatal at a protected endpoint), and exit is guarded by both a session cookie and a nonce that a normal admin REST session can't satisfy. The `get-recovery-exit-url` ability returns the URL for an admin (or a browser-driving agent) to follow instead.
* **Docs — recovery-mode compatibility noted on 3 existing abilities.** `activate-plugin`, `deactivate-plugin`, and `activate-theme` now mention in their `description` that they work in recovery mode (they only update the `active_plugins` / active-theme option and don't load the extension file).
* **Ships without JS, DB, or REST-controller changes.** Purely new PHP source under `includes/Abilities/Recovery/` plus 7-line bootstrap wiring. 21 new PHPUnit tests under the new `feature-059-unit` testsuite.

= 0.0.16 =
* **New — `core/reinstall-wp-core` ability under the Core category.** Reinstalls the currently-installed WordPress version by handing a synthetic offer with `response = 'reinstall'` to WP core's `Core_Upgrader::upgrade()` — same code path the WordPress dashboard's "Reinstall now" button uses. Uses `allow_relaxed_file_ownership=false` so ownership mismatches surface as errors instead of silent partial-upgrades. Requires BOTH `manage_options` AND `update_core`; honours `DISALLOW_FILE_MODS` via `File_Mods_Guard`; multisite-guarded; refuses when core is being actively updated to a different version (use `core/update-wp-core` for that). Complements the existing `core/update-wp-core`, `core/rollback-wp-core`, and `core/check-wp-core-update` abilities.
* **BREAKING — every ability slug has been renamed.** Two changes at once: (a) namespace shortens from `acrossai-abilities-manager/` (27 chars) to `acrossai/` (9 chars), and (b) suffixes flip to verb-first form. Every ability is now `acrossai/<verb>-<subject>` — e.g. `settings/get-site-title`, `themes/activate-theme`, `plugins/list-plugins` — instead of the pre-0.0.16 `acrossai-abilities-manager/<subject>-<verb>` form. 163 suffixes changed; 56 already-verb-first suffixes only had their namespace shortened. Every ability's `label` was already verb-first ("Get Site Title", "Activate Theme"), so the slug now reads the same word order as the label. Motivation: alignment with the WordPress core MCP adapter convention (`mcp-adapter/discover-abilities`, `mcp-adapter/execute-ability`) and with the broader function-calling / MCP naming used by every major LLM tool-use spec.
* **Class files and PHP class names flipped to verb-first too.** 162 files renamed to match their slugs — `Site_Title_Get.php` → `Get_Site_Title.php`, `Theme_Activate.php` → `Activate_Theme.php`, etc. Internal-only change; no external API surface. PSR-4 autoload picks up the new file/class names automatically.
* **No automatic data migration.** The plugin is still small enough that no auto-migration ships with this release. If you had a pre-0.0.16 install with saved overrides or ACL rules keyed on the old `acrossai-abilities-manager/<subject>-<verb>` slugs, wipe those rows manually (Custom Abilities admin page → clear overrides; Access Control admin page → clear rules) and re-create them under the new `acrossai/<verb>-<subject>` names. External callers (custom code, saved MCP client configs, scripts calling `/wp-json/wp-abilities/v1/abilities/acrossai-abilities-manager/<old-slug>/run`) must update their slug strings to `/wp-json/wp-abilities/v1/abilities/acrossai/<new-slug>/run`. No backwards-compatibility aliases are shipped.

= 0.0.15 =
* **Bulk Actions overhaul on the Custom Abilities admin page — Site Access / MCP Exposure / User Access / Overrides.** Replaces the misleading Publish / Unpublish / Delete dropdown (WP-CPT vocabulary that never mapped to how ability overrides behave) with four ability-native optgroups that mirror the per-row edit drawer: **Site Access** tri-state (Force Allow / Inherit / Force Block writing `site_allowed`), **MCP Exposure** tri-state (Enable / Default / Disable writing `show_in_mcp`), **User Access** (opens a modal that mounts the composer's `<AccessControl>` picker and applies one rule across every selected slug, plus a "Reset to Default — allow everyone" quick action), and **Overrides → Force Reset** (clears every override column per slug via the existing `DELETE /abilities/{slug}/override` endpoint). Destructive transitions (Force Block, MCP Disable, User Access Reset, Force Reset) prompt for confirmation before dispatch.
* **Row-level checkbox and Edit action now work on every ability regardless of Source.** The pre-0.0.15 checkbox gate limited selection to Custom (`db`) rows only — a hangover from the deleted Publish/Unpublish/Delete flow. Bulk tri-state operations apply to any Source, so every visible row now shows a checkbox and can be included in a bulk selection. The Edit action was already unconditional across sources; verified with the same release.
* **Full-screen busy overlay with WP-native spinner + body scroll-lock during every bulk apply.** Uses `<span class="spinner is-active">` (the same spinner WP admin shows next to Save Draft) over a backdrop-blurred wash; the underlying page is un-clickable and the body cannot scroll until the bulk request set resolves. Escape-to-dismiss on the User Access modal is suppressed while its apply is in flight to prevent half-applied state on the underlying multi-slug write.
* **Client-side only release. No PHP changes, no new REST endpoints, no new database tables, no new composer or npm packages.** All storage, sanitisation, capability enforcement, and REST controllers are unchanged. The feature loops the pre-existing per-slug endpoints under `Promise.all` inside three new Redux thunks (`bulkUpdateTristate`, `bulkClearOverrides`, `bulkSetUserAccessRule`); the composer package's provider enumeration and rule storage are reused verbatim.
* **25 new Jest tests across three suites** cover payload discipline (raw JSON `true` / `false` / `null` on tri-state writes), partial-failure re-throw discipline (operator sees an error and keeps the selection for retry instead of a silent success), composer null-response guard, and a slug-encoding regression guard (see below). Also adds two new architecture patterns and one new bug pattern to `docs/memory/`.
* **Fixed: composer User Access rule keys were storing the ability slug with the `/` character stripped when applied via the bulk path.** Root cause: client-side `encodeURIComponent(slug)` on the composer PUT URL was collapsed to nothing by the composer's key sanitizer (`%2F` was stripped rather than decoded back to `/`), producing orphan rows like `acrossai-abilities-managerblock-pattern-delete`. Fixed by matching the per-row edit drawer's pattern — passing the slug raw into the URL. Server-side `sanitize_ability_slug()` still validates independently, so no security regression. Guarded by a Jest regression test.

= 0.0.14 =
* **wp.org banner artwork refreshed + filenames renamed to the WP.org canonical convention.** `banner1544x500.png` → `banner-1544x500.png` and `banner772x250.png` → `banner-772x250.png`. WordPress.org's plugin directory only auto-detects banners at the dashed paths (`.wordpress-org/banner-{width}x{height}.png`) — the un-dashed variants shipped in 0.0.13 were not being surfaced on the plugin listing page. Both banners also carry updated artwork in this release. wp.org-assets-only change; no plugin code touched.

= 0.0.13 =
* **Docs — ability gap audit landed under `specs/054-ability-gap-audit/`.** Tracks 31 abilities across 10 domains that external AI-tool inventories expect but the plugin does not yet expose (Site editor / structure, Admin menu, Navigation, Users, Content index / search / linking, Content advanced, Taxonomy, Media, Site lifecycle, Comments). Current registered inventory: 187 abilities under `acrossai/*`, verified via grep against `wp_register_ability` and confirmed wired 1:1 into `AcrossAI_Core_Abilities_Bootstrap.php`. For every missing ability, the audit names the closest existing ability in the plugin (or explicitly declares the domain as absent) so future implementation waves do not accidentally duplicate work. Each missing ability becomes its own follow-up spec later. No runtime code changes; audit-only release.
* **wp.org assets — banner (1544×500 and 772×250) and a sixth screenshot added.** The plugin directory listing now shows a proper header banner (previously falling back to the WordPress.org default header since 0.0.4) and a sixth screenshot covering the Settings page (Display + Upload Media Abilities sections). Metadata-only change to `.wordpress-org/` — no plugin code touched.
* **31 new abilities across 10 domains — 187 → 218.** Ships the entire backlog surfaced by the external AI-tool inventory audit. Two new categories are added: `acrossai-abilities-manager-admin-menu` (5 abilities) and `acrossai-abilities-manager-content-search` (11 abilities). Single-item additions land in existing categories: `users-current-access` (Users), `taxonomy-set-term-image` (Taxonomies), `comments-bulk-update` (Comments), `media-rename-file` (Media), `navigation-get-context` + `navigation-list-locations` (Menus), `content-update-block` + `content-autosaves-inspect` (Content), `site-editor-get-context` + `site-editor-refresh-context` + `site-structure-list-reusable-blocks` + `site-structure-list-block-areas` (Block), `site-maintenance-report` (SiteHealth), `plugin-lifecycle-get-plugin` (Plugins), `theme-lifecycle-get-theme` (Themes).
* **New option-backed lifecycle event log.** `plugin-lifecycle-get-plugin` and `theme-lifecycle-get-theme` return `last_activated_at` / `last_deactivated_at` / `last_updated_at` timestamps from a rolling event log (`acrossai_abilities_manager_lifecycle_log` option, capped at 50 events per plugin/theme). Events are recorded from 0.0.13 forward — pre-0.0.13 lifecycle history is not backfilled and those timestamps read `0` until the next event fires.
* **New option-backed internal-link suggestion store.** The 5 `content-internal-link-*` abilities (create, list, review, apply, policy) plus `content-audit-internal-links` persist to `acrossai_abilities_manager_link_suggestions` (option-backed, capped at 500 total suggestions). Zero external HTTP; zero new database tables.
* **No breaking changes.** No existing ability slug / input schema / output schema / permission callback is altered. Every previously-registered ability still resolves to the same class.
* **Safety notes.** `media-rename-file` refuses filenames with a directory separator, null byte, or leading dot; enforces realpath containment inside the attachment's original upload sub-directory; refuses to clobber an existing target. `comments-bulk-update` requires `moderate_comments` and caps at 100 comment ids per call. `content-internal-link-suggestion-apply` requires `edit_others_posts`, re-validates the target as same-site, and only mutates on first-occurrence substring match.

= 0.0.12 =
* **New — WordPress core rollback ability under the Core category.** `core/rollback-wp-core` rolls back WordPress core to an earlier offered version via WP core's `Core_Upgrader::upgrade()` — the same class the WordPress dashboard uses for forward updates. Fetches the offer list from the WP.org Core API 1.7 endpoint (`https://api.wordpress.org/core/version-check/1.7/`) via `wp_remote_get()`, picks the requested version, and hands the offer directly to the upgrader. Uses only WordPress functions; no bundled updater code. Requires BOTH `manage_options` AND `update_core`; honours `DISALLOW_FILE_MODS` via `File_Mods_Guard`; multisite-guarded; refuses when the target version is equal to or newer than the currently-installed version (steers callers to `wp-core-update`). The per-locale offer list is cached in a site transient with a day-long TTL. Annotated `destructive=true` — rolling WordPress back is a real production operation and clients should surface it accordingly. Inspired by Andy Fragen's [core-rollback](https://github.com/afragen/core-rollback) plugin (MIT-licensed). See PR [#77](https://github.com/acrossai-co/acrossai-abilities-manager/pull/77).
* **First outbound HTTP request from the plugin.** Historically the plugin has made zero outbound HTTP requests on its own (the Add-ons page delegates to the WordPress plugin installer's own contact with WordPress.org; other abilities operate on the local site). `wp-core-rollback` introduces the plugin's first direct outbound request — to `api.wordpress.org/core/version-check/1.7/`. The URL is a hardcoded class constant (no SSRF surface), the request has a 15-second timeout, and only the sanitized locale is derived from user input. The per-locale offer list is cached in a site transient with a day-long TTL, so the request rate is bounded to at most one call per day per locale per site.

= 0.0.11 =
* **New — WordPress core update abilities under a new "Core" category.** Two new abilities: `core/check-wp-core-update` reports whether a WordPress core update is available (returns `current_version`, `new_version`, download URL, PHP / MySQL requirements — flattens WP's core update offer into a JSON-friendly shape); `core/update-wp-core` applies the update via WP core's `Core_Upgrader::upgrade()`. When called with no arguments it upgrades to the first `response=upgrade` offer from `get_core_updates()`; pass `version` (+ optional `locale`) to pin to a specific offer. Requires BOTH `manage_options` AND `update_core` (matches WP core's own admin gate). Honours `DISALLOW_FILE_MODS` via `File_Mods_Guard`. Multisite guard bails cleanly if the current user lacks network-level `update_core`. Idempotent — re-running when no update is available returns a clean success envelope with `updated=false`. Uses WP core functions exclusively; no bundled updater, no custom HTTP, no custom integrity checks. See PR [#75](https://github.com/acrossai-co/acrossai-abilities-manager/pull/75).
* **New Core category folder.** `includes/Abilities/Core/` joins the existing 17 Category folders (Plugins, Themes, FileManager, Cache, Database, Users, Block, Settings, Fonts, Content, Taxonomies, Media, Comments, Menus, Options, Cron, SiteHealth). Displayed as a new "Core" tab on the Ability Library page. Not a new module — Constitution §I locks the module count at five; Category folders are sub-partitions of the existing Custom Ability Registration module.
* **Backup filenames — human-readable and time-sortable.** Filenames produced by `zip-create` (and finalized zips from `zip-upload`) change from `backup-{type}-{slug}-{random-12-chars}.zip` to `{slug}-{unix-timestamp}-{ms}.zip` (e.g. `hello-dolly-1721260800-517.zip`). Lexicographic sort now equals chronological sort; the target is readable at a glance. The 3-digit millisecond suffix from `microtime(true)` prevents same-second collisions when two back-to-back calls target the same slug. Trade-off: dropping the 12-char random suffix removes the enumeration-by-guessing defense the old scheme provided. Mitigations still in force: the `.htaccess` in `wp-content/uploads/acrossai-backups/` still disables directory listing (`Options -Indexes`) and still blocks execution of PHP-group extensions, and `zip-list` / `zip-download` still require `manage_options`. Backups created on 0.0.9 / 0.0.10 with the old scheme continue to work — the filename change only affects new backups.
* **Spec-Kit backfill for Feature 041.** `specs/041-backup-restore-abilities-and-updates/` now exists with the full artifact set (spec, plan, tasks, checklists, security-constraints, memory-synthesis, architecture-review) documenting the 8 abilities that shipped in 0.0.9 and the 0.0.10 include_hidden fix. Same seven-file layout used by Feature 053's backfill.

= 0.0.10 =
* **Fix (Create_Zip_Backup) — `include_hidden=false` now applies recursively.** The 0.0.9 implementation used `RecursiveIteratorIterator::SELF_FIRST` with a per-entry basename check that only skipped the top-level hidden directory itself; the iterator kept descending into it, so files INSIDE a hidden directory (e.g. `.git/objects/xxx`) were still added because their basenames don't start with `.`. Fixed to check EVERY segment of the entry's relative path — same approach the reference `download-plugin` uses in its `app/Plugins/Base.php`. Applied to both the archive assembly (`append_dir_to_zip`) and the pre-write size guard (`estimate_tree_size`). If you called `zip-create` with `include_hidden=false` against a dev checkout in 0.0.9, the archive contained the full contents of every hidden directory beneath the source — regenerate any such archives on 0.0.10. See PR [#73](https://github.com/acrossai-co/acrossai-abilities-manager/pull/73).

= 0.0.9 =
* **New — six Zip abilities under FileManager for backup / restore workflows.** `file-manager/create-zip-backup` archives a plugin, theme, uploads folder, mu-plugins folder, or any ABSPATH-relative path into `wp-content/uploads/acrossai-backups/<random>.zip` and returns the download URL + SHA-256. `zip-upload` accepts a zip via base64, chunked (up to 8 MB per chunk / 64 MB per session, filterable), or a remote URL and finalizes it into the same directory after validating the `PK\x03\x04` magic bytes. `zip-extract` extracts a zip already on disk or fetched from a URL into a resolved target directory (plugin / theme / uploads / mu-plugins / path); every archive entry is audited for zip-slip (`..` segments, absolute paths, backslashes, null bytes) before extraction. `zip-download` returns a fresh URL + metadata for any managed zip. `zip-list` paginates the managed directories, newest first. `zip-delete` removes a zip from the managed directories idempotently. The backups directory is hardened on first use with an `.htaccess` that blocks PHP execution while keeping `.zip` downloads reachable, plus an empty `index.php` for enumeration defense. All abilities enforce `manage_options`; every mutating ability honours `DISALLOW_FILE_MODS` via the shared `File_Mods_Guard`. See PR #TBD.
* **New — `plugins/update-plugin` and `themes/update-theme` abilities.** The Plugins and Themes categories previously shipped an `update-check` reporter but no way to apply updates through the abilities API. The new abilities wrap WP core `Plugin_Upgrader::bulk_upgrade()` / `Theme_Upgrader::bulk_upgrade()` (same pattern as the existing `plugin-install` / `theme-install`), accept an array of plugin files / slugs or theme stylesheets, and return per-slug results with `from_version`, `to_version`, `updated`, and `message`. Update_Plugin additionally requires `update_plugins`; Update_Theme additionally requires `update_themes`. Idempotent: re-running when no update is available reports `updated_count: 0` with a clean success envelope.
* **Shared utilities: `Backups_Storage` and `Zip_Target_Resolver`.** New helpers under `includes/Abilities/Utilities/`. `Backups_Storage` manages the `acrossai-backups/` and `acrossai-staging/` directories under `wp-content/uploads/`, generates enumeration-resistant random filenames, resolves managed paths with `realpath()` boundary checks, and computes SHA-256 for the listing / download responses. `Zip_Target_Resolver` maps `(target_type, target)` to an absolute filesystem path — plugin slugs resolve via `Plugin_Helpers` (existing fuzzy resolver), theme stylesheets via `Theme_Helpers`, `uploads` via `wp_get_upload_dir()`, `mu-plugins` via `WPMU_PLUGIN_DIR`, and `path` values via a strict inside-ABSPATH realpath check.
* **Upload_Zip_Backup chunk sweeper cron.** A new daily cron (`acrossai_abilities_manager_zip_upload_sweep_chunks`) sweeps abandoned chunk sessions from `wp-content/uploads/acrossai-staging/` after the configurable TTL (default: 1 day, filterable via `acrossai_abilities_manager_zip_upload_session_ttl`). Mirrors the existing Upload_Media sweeper.
* **New configurable limits.** `acrossai_abilities_manager_zip_max_bytes` (default 512 MB) caps the decompressed size of any zip written or extracted. `acrossai_abilities_manager_zip_upload_chunk_max_bytes` (default 8 MB base64) and `acrossai_abilities_manager_zip_upload_session_max_bytes` (default 64 MB base64) cap the chunked upload flow.

= 0.0.8 =
* **Freemius integration removed entirely.** The `freemius/wordpress-sdk` composer dependency is dropped (upstream `acrossai-co/main-menu` 0.0.21+ no longer requires it). The plugin no longer sends any data to Freemius, no longer shows a Connect / Login / Buy affordance on the Add-ons page, and the entire Freemius vendored SDK tree (~2,000 files) is removed from the installable ZIP. If you previously connected a Freemius account tied to this plugin, that connection is now inert; any `fs_*` or `freemius_*` rows in `wp_options` are no longer read by anything and can be safely deleted (e.g. `wp option list --search='fs_*'` then `wp option delete <name>` for each). This supersedes the 0.0.6 changelog entry about Freemius credentials — those credentials are no longer used. See PR [#69](https://github.com/acrossai-co/acrossai-abilities-manager/pull/69).
* **Add-ons page — free-only, and this plugin excluded from its own listing.** The Add-ons page (`?page=acrossai-addons`) now lists only free companion plugins hosted on WordPress.org (Install / Activate / Deactivate via the standard WP plugin installer). This plugin no longer appears in its own Add-ons page — a small self-filter on the `acrossai_addons` hook removes it, since it's obviously already active when the page renders. Other AcrossAI companion plugins (MCP Manager, Model Manager, Turn Off AI Features) still list normally.
* **Library page — title and Enable All / Disable All buttons on a single horizontal row.** The "Ability Library" page heading and the bulk-action buttons introduced in 0.0.7 (Feature 052) now share one line at the top of the page (title anchored left, buttons anchored right). Saves vertical space; matches administrator expectations for admin page layouts.
* **Dependencies: `acrossai-co/main-menu` bumped from `0.0.14` to `0.0.23`.** Three-hop bump (0.0.21 → 0.0.22 → 0.0.23) accumulated during the release cycle. Consumer API surface (`SettingsPage`, `MenuRegistrar`, `AddonsPageRenderer`) preserved across each hop; no code changes required beyond removing the old `\AcrossAI_Addon\AddonsPage` instantiation (class deleted upstream in 0.0.21 — its responsibilities moved into `\AcrossAI_Main_Menu\MenuRegistrar` which is registered automatically when the shared `SettingsPage` bootstrap runs).

= 0.0.7 =
* **Library page — bulk Enable All / Disable All action buttons.** A new right-aligned header row above the tab strip on `?page=acrossai-abilities-library` renders two side-by-side buttons that toggle every ability category currently in view with a single click. Actions are scoped to the active tab: on the `All` tab they touch every registered category; on a specific tab (Core, Blocks, Themes, Users, Cache, File Manager, Cron, Database, Plugins) they only touch categories whose ability metadata declares that `tab_group`. Categories in other tabs pass through byte-for-byte unchanged. Each category's mode (All / Specific) and per-slug selections are preserved on both actions — a Disable All → Enable All cycle is a lossless round-trip. Persisted via the existing `POST /acrossai-abilities-library/v1/abilities/config` REST route (`manage_options` + nonce, unchanged). See PR [#68](https://github.com/acrossai-co/acrossai-abilities-manager/pull/68).
* **URL-synced tabs on the Library page.** The active tab is now reflected in the browser URL as `?tab=<slug>`. Deep-linkable, bookmarkable, and browser back / forward navigation re-syncs the visible tab. Direct-navigation to `?page=acrossai-abilities-library&tab=themes` opens the Themes tab on first paint. Invalid tab values silently fall back to the default `All` view — no error, no console warning. The default `All` view keeps the canonical URL clean by removing the `tab` query arg entirely.
* **Disabled-card UI refresh on the Library page.** Disabled category cards now show the master toggle + category label + chevron (visible whenever the category has at least one registered ability). Expanding the chevron on a disabled card reveals a readonly bullet-style preview of the abilities in that category (with descriptions). The All / Specific mode selector and interactive per-ability checkboxes remain hidden while the card is disabled — no interactive control can render on a disabled card even when the stored mode is `Specific`. The stored mode and per-slug selections are preserved so re-enabling restores the prior configuration exactly. Manual per-card disable and bulk `Disable All` produce identical card DOM.

= 0.0.6 =
* **BREAKING (downstream integrators) — 17 ability category slugs rebranded from `acrossai-core-abilities-<domain>` to `acrossai-abilities-manager-<domain>`, and 176 ability slugs rebranded from `acrossai-core-abilities/<verb>` to `acrossai/<verb>`.** The companion `acrossai-core-abilities` plugin's entire 201-file runtime (17 Category_Registrars, 176 ability classes, 8 helper classes, plus the extra-MIME-types admin field) is absorbed into this plugin. Every category and ability slug is renamed uniformly; ability payload shapes and permission callbacks are preserved verbatim. Downstream code (MCP servers, REST/WP-CLI callers, integration tests) that referenced the legacy `acrossai-core-abilities-*` slugs by string must update on cutover. Ability payloads themselves are unchanged. See PR [#65](https://github.com/acrossai-co/acrossai-abilities-manager/pull/65).
* **Absorbed extra-MIME-types Settings field lands under the Abilities tab.** The companion plugin's Core settings tab is retired; its "extra allowed upload MIME types" field now renders inside the shared Settings → Abilities tab. The companion's separate uninstall opt-in is folded into the manager's existing single `acrossai_abilities_uninstall_delete_data` opt-in — no second checkbox appears. Activation-time migration copies the legacy option (`acrossai_core_abilities_extra_mimes` → `acrossai_abilities_manager_extra_mimes`), OR-monotonically folds the legacy uninstall opt-in into the manager's opt-in (never demotes a manager-true value), and deletes both legacy option rows. Existing admin configuration is preserved. See PR [#65](https://github.com/acrossai-co/acrossai-abilities-manager/pull/65).
* **Retire the `acrossai-core-abilities` companion plugin.** After upgrading to 0.0.6, deactivate and uninstall the standalone `acrossai-core-abilities` plugin — all 176 abilities are now provided by this manager plugin directly. Keeping both plugins active will emit duplicate-registration notices from the WP Abilities API on every request. Removal of the companion plugin folder from production sites is an operational task, separate from this release.
* **Library page — Themes / Blocks / Plugins / Users / Database / Cron / Cache / File Manager get their own tabs.** The absorbed categories are promoted from the shared "Core" tab into their own top-level tabs on the Ability Library page (`?page=acrossai-abilities-library`). The "Core" tab stays pinned as the second option (immediately after "All") regardless of alphabetical ordering. The "No abilities registered yet" empty-state copy is updated for the new bundled reality.
* **Dependencies: `acrossai-co/main-menu` bumped from `0.0.11` to `0.0.14`.** Adopts the Tabs base class extraction (0.0.14) and tab-scoped `option_group` (0.0.13) — the latter fixes the cross-tab option-clobber bug where saving one Settings tab silently wiped other tabs' options. See PR [#66](https://github.com/acrossai-co/acrossai-abilities-manager/pull/66).
* **Freemius product identifiers rotated** — `fs_product_id` changed from `31230` to `34418`, `fs_public_key` rotated to `pk_d61a7ddb1a619f7697fbb4fc397b6`. If you have a Freemius account tied to the previous product ID, reconnect on the Account submenu after upgrade.

= 0.0.5 =
* **Dependencies: `acrossai-co/main-menu` bumped to `0.0.11`.** Picks up the latest AcrossAI shared parent menu / dashboard / settings / add-ons page code from that package. No plugin-owned code changes in this release — the bump is the only functional delta vs 0.0.4.

= 0.0.4 =
* **BREAKING (add-on developers) — Library display fields moved from top-level `$args` into `$args['meta']['acrossai']`.** The three Library-only fields introduced by Features 033 and 037 — `sub_group`, `sub_group_label`, and `tab_group` — are no longer read from the top level of the `$args` array passed to `wp_register_ability()`. They must now be nested under `$args['meta']['acrossai']`, matching the existing `meta.mcp` (MCP integration) and `meta.annotations` (WP-core annotations) convention. This is a hard cut with no back-compat shim: any add-on that still passes the fields at the top level will silently render its Library card without a sub-group heading or custom tab placement. Migration: change `'sub_group' => 'x'` to `'meta' => [ 'acrossai' => [ 'sub_group' => 'x' ] ]` (same for `sub_group_label` and `tab_group`). Only affects add-ons that extend `Ability_Definition` and use these Library display fields; abilities without them are unaffected. No end-user data migration, no DB schema change, no REST API change.
* **Plugin icon replaced with a vector (SVG) asset.** The WordPress.org plugin directory now serves `.wordpress-org/icon.svg` in place of the previous 128×128 / 256×256 JPG icons, so the icon renders sharp at any display density. Also removes the 772×250 and 1544×500 header banners from the directory listing — the plugin page will show the WordPress.org default header until banners are re-added. wp.org-assets-only change.

= 0.0.3 =
* **Fix: plugin now activates on installs from WordPress.org.** The 0.0.2 release ZIP shipped without the Composer autoloader (`vendor/autoload_packages.php`) because the WordPress.org deploy workflow did not run `composer install` before uploading. Users installing 0.0.2 from the WordPress.org plugin directory saw the plugin activation guard trigger: *"AcrossAI Abilities Manager cannot activate: the Composer autoloader is missing…"*. The 0.0.3 release ZIP includes the full production autoloader; no other code changes. If you already installed 0.0.2 and hit the activation error, delete the plugin folder and reinstall 0.0.3.

= 0.0.2 =
* **Composer dependency refresh** — `wpb-access-control` bumped to v2.0.0 (per-consumer database tables); `acrossai-co/main-menu` bumped to v0.0.10 (now bundles the Add-ons page and includes the JS-side rebrand-sync fix that restores Install / Activate / Deactivate button behavior). The standalone `acrossai-co/addons-page` package has been removed from direct dependencies; the same `AcrossAI_Addon\AddonsPage` class now ships from the `main-menu` package.
* **Per-consumer access-control storage** — this plugin now owns its own `{prefix}abilities_access_control` database table, keeping its rules fully isolated from any other plugin embedding the same access-control library. The dedicated table is created automatically on plugin activation.
* **Add-ons submenu URL changed** — the Add-ons page slug is now `acrossai-addons` (was `wpb-addons`). Any bookmarks or external links pointing at `wp-admin/admin.php?page=wpb-addons` should be updated to `wp-admin/admin.php?page=acrossai-addons`. The submenu location and behavior are otherwise unchanged.
* **BREAKING — Access Control rules from earlier releases are NOT migrated.** If you previously configured Access Control rules on any ability, those rules were stored in the shared `{prefix}wpb_access_control` table and are **no longer read** by this release. After upgrading, please audit every ability's Access Control panel and reconfigure any rules that were previously in place. The legacy table is left on disk (in case you need to reference the prior configuration) and can be dropped manually by a database administrator if desired: `DROP TABLE {prefix}wpb_access_control;` and `DELETE FROM {prefix}options WHERE option_name = 'wpb_access_control_db_version';`.
* **BREAKING — Ability execution logging removed.** The dedicated Logs admin page, the log-retention Settings field, the `{prefix}acrossai_ability_logs` database table, and the `/wp-json/acrossai-abilities-log/v1/logger/logs` REST endpoint are all removed. If you rely on ability-execution logging for security monitoring or auditing, install a compatible logging plugin or hook `wp_after_execute_ability` directly in your own consumer code — the upstream ability-execution events remain available. Bookmarks to `wp-admin/admin.php?page=acrossai-abilities-logs` receive the standard "page does not exist" response. External integrations polling the removed REST endpoint receive 404. On existing installs, the legacy logs table and its schema-version option are orphaned; opt into the "delete all data on uninstall" setting to drop them cleanly, or run manually: `DROP TABLE {prefix}acrossai_ability_logs;` and `DELETE FROM {prefix}options WHERE option_name IN ('acrossai_abilities_log_retention_days', 'acrossai_ability_logs_db_version');`.

= 0.0.1 =
* Initial release.
* Sitewide Ability Management: browse, toggle, edit, reset, bulk-action.
* Ability Library: enable/disable add-on ability groups with All/Specific mode controls.
* Add-ons page powered by wpb-addons-page with Freemius integration.
* MCP server listing via MCP Adapter integration.
