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. = 0.0.41 - 2026-09-30 = * **Fixed: Global Styles saved through the abilities were stored but never applied.** Every write to the database record (`blocks/create-global-style`, `blocks/update-global-style` with `source: "db"`) saved `settings` and `styles` and nothing else. WordPress only uses that record when it also carries `"isGlobalStylesUserThemeJSON": true` — `WP_Theme_JSON_Resolver::get_user_data()` discards the whole thing otherwise and falls back to the theme's `theme.json`, without logging anything. So a palette, a font, a layout width would be written, read back correctly, reported as `origin: "db"` and `effective: true`, and the site would go on rendering theme defaults. Adding the key by hand did not help either: it was rejected as an unknown top-level key, because the record was being validated against the list of keys a `theme.json` *file* may carry. Both flags — the marker and `version`, set to `WP_Theme_JSON::LATEST_SCHEMA` — are now stamped on the server for every DB write, including section writes, merges, replaces and migrations, so nothing has to be passed by the caller. The DB source is validated against the shape core actually reads from it (`version`, `isGlobalStylesUserThemeJSON`, `settings`, `styles`, `title`); `templateParts`, `customTemplates` and `patterns` are refused there by name, with the message saying they belong in a `theme.json` file and which ability writes one. * **Fixed: a font stack containing double quotes wiped the entire Global Styles record, and still reported success.** `"fontFamily": "\"EB Garamond\", Georgia, serif"` is an ordinary value; saving it destroyed everything else in the record. The encoded JSON was handed to `wp_update_post()` unslashed, and core runs `wp_unslash()` over every field it is given, so each `\"` inside the JSON lost its backslash and the stored content stopped being JSON. It then decoded to null, which the read path reported as an *empty* record — so the next merge-write started from empty and saved only the section it was given, silently dropping the colors, layout and spacing saved minutes earlier. The ability reported `success: true` and a non-zero `content_bytes` throughout. Content is now slashed on the way in, exactly as core's own Global Styles REST controller does; every write is read back and decoded before it is called a success, and one that does not decode is rolled back to the previous content and returned as `success: false`. A merge into a record that does not decode is refused outright rather than treated as empty — that conversion is what turned one bad write into total loss. The same fix is applied to block style variation records, which are written through the same path and had the same defect. * **Global Styles reads now say when WordPress is ignoring the record.** `read-global-style` reported `origin: "db"` and `list-global-styles` reported `effective: true` for records WordPress was not using at all. `origin` is now `"db:ignored"` for a record that is missing the marker or does not decode, with a warning naming the reason; `list-global-styles` reports `applied_by_wordpress` on every row and marks the `theme.json` copy the site is really serving as the effective one. Records written by earlier versions are repaired automatically by the next write, by a one-time upgrade routine that runs on activation and on the first request after updating, or on demand with `update-global-style` and `repair: true`, which adds the flags and changes nothing else. The upgrade routine touches only records whose content decodes and that carry core's `wp-global-styles-` naming; content that does not parse is left alone for hand-recovery, and block style variations are skipped, since they must not carry the marker. * **Section writes no longer discard what they were given in silence.** A `section: "typography"` write carrying `styles.elements` saved the typography and dropped every heading, link and button style in the same call, reporting success. A `section: "blockStyles"` write carrying only `styles.elements` saved nothing at all and also reported success. Element styles now have a section of their own — `section: "elements"` — anything a section write does not save is named back in `warnings` as `ignored: styles.elements`, and a section write that would save nothing is refused with a message naming what was ignored and where it belongs. * **`blocks/get-style-guide` now labels where each preset came from.** It listed theme and user presets for the same slug side by side with nothing to tell them apart, so an overridden colour read as a duplicate; and for fonts, font sizes, duotones and gradients it read the theme's presets only, so user presets were missing from the style guide entirely. Each preset now carries an `origin` of `theme` or `custom`, a slug appears once — the value the site actually renders — and where a user preset replaced a theme one it says so with `overrides`. = 0.0.40 - 2026-09-28 = * **Fixed: the plugin's Description was being truncated on WordPress.org.** Every import reported "The Description section is too long and was truncated. A maximum of 2,500 words is supported" — a warning only the plugin's committers can see, so the listing was silently losing its tail for anyone reading it. The cause was not obvious: the Description itself was well inside the limit at around 1,800 words, but WordPress.org folds sections it does not recognise into the Description, and this readme carries two of them — External Services and Privacy Policy, both required disclosures totalling another 750. Together they crossed the limit. The Description is now tightened to 2,372 effective words with every point kept, leaving room for the next few releases, and neither disclosure was touched. * **The WordPress.org listing title now says what the plugin does.** It read "AcrossAI Abilities Manager", which tells a search engine nothing, while the sibling plugins carry a descriptive title. It is now "AcrossAI Abilities Manager – WordPress Abilities for Claude, ChatGPT & Any AI Agent". The name shown inside wp-admin is unchanged. The tags move from `abilities, mcp, access control, site management, ai` to `abilities, ai assistant, chatgpt, claude, mcp` — the terms people actually search, with `abilities` kept because it is the one word that distinguishes this plugin from an MCP server. * **The listing no longer counts toolsets as this plugin's feature.** It led with "357 ready-made WordPress abilities across 14 toolsets" and explained the toolset dispatch model as something this plugin gives you. That attribution is wrong: AcrossAI MCP Manager owns the toolset layer now. The counts of abilities stay, because those are what this plugin ships; the counts of toolsets are gone, and the explanation of why an AI sees a dozen tools instead of hundreds now credits the MCP server for the grouping. The per-plugin toolsets — Elementor, Rank Math, Site Kit, UpdraftPlus, All-in-One and the rest — are still described here, because they still come from this plugin and MCP Manager deliberately does not carry them. Nothing about the code changed in this release. * **Tested up to WordPress 7.1, and the plugin's own one-line description rewritten.** The header still described this as a way to "manage and customize the abilities of AcrossAI … tailor the AI's capabilities", which is what an AI settings panel does, not a plugin that ships 357 abilities. That line is what WordPress shows under the plugin name in wp-admin, so it now says what you actually get. * **The listing now says plainly that this works with any Abilities API consumer, not just one MCP server.** Every ability here is registered through WordPress 6.9's own Abilities API, so anything that reads that API can expose them — AcrossAI MCP Manager, the WordPress MCP Adapter, another MCP server, a REST client, or a plugin calling the Abilities API directly. That was previously one clause at the end of a paragraph; it is now three named routes and an explicit statement that nothing here is proprietary or bound to a particular transport. * **The integration list is shorter, because each entry now links to its own page.** Every integration gained a link in 0.0.39; the inline paragraph describing each one was then saying what the linked page says at length. Each is now a single line naming the area it covers. Verified: the Description parses at 2,372 of 2,500 words with the two unrecognised sections folded in, as WordPress.org counts them. = 0.0.39 - 2026-09-28 = * **Fixed: asking All-in-One WP Migration for a backup told you to install UpdraftPlus.** The All-in-One abilities detect their plugin correctly, but when it was missing they reported the failure using UpdraftPlus's error code and UpdraftPlus's message. So calling `all-in-one/get-status` on a site without All-in-One named the wrong plugin — and following that advice installed the wrong plugin, after which the ability still failed. Worse for anything automated, both suites returned the same `updraftplus_missing` code, so a caller checking which plugin was absent could not tell them apart, which is the one question a typed error code exists to answer. All-in-One now returns `all_in_one_missing` and a message about its own archives and exports. Every guard in the plugin is now checked for a shared code, so this cannot recur quietly. * **Rank Math SEO scores now record where they came from and when.** Rank Math stores a score as a bare number, so nothing distinguished a score its own browser analyzer wrote months ago from one an AI client worked out this morning — and "are these scores current?" had no answer. `rank-math/update-seo-scores` now stamps each score it writes with a timestamp and a source, and takes a new optional `source`: `agent` (the default, meaning an AI client graded the post against the rubric Rank Math's own `rank-math/analyze-post-content` hands out) or `rank-math-analyzer` (a number Rank Math's client-side analyzer produced). The two do not always agree, and saying which one you have is the point. `rank-math/audit-content-seo` reports both back as `seo_score_at` and `seo_score_source`. Scores Rank Math wrote itself report null for both, which is the honest answer rather than a guess. Nothing about the score itself changes, and the existing single-argument call still works. * **`rank-math/audit-content-seo` can now audit an exact list of posts.** It could sweep a post type or search for text, but there was no way to ask about five specific posts — which is exactly what you need before and after changing them. Pass `post_ids` and it reports on those posts, in the order you gave, all in one response. Because naming ids means naming the posts, it also stops applying the post/page and published-only defaults that would quietly drop a draft or a custom post type and leave it looking like it did not exist, and it lists healthy posts alongside problem ones instead of returning a shorter list with no explanation. Passing `post_types`, `post_statuses` or `only_issues` yourself still overrides all of that, and a sweep with no `post_ids` behaves exactly as before. Verified against Rank Math 1.0.278. * **New: a Site Kit by Google toolset — 11 abilities under `site-kit/*`.** Site Kit connects a WordPress site to Search Console, Analytics 4, PageSpeed Insights, AdSense and Tag Manager, and until now none of that was reachable. `site-kit/get-status` reports whether Site Kit is set up, whether the WordPress user making the call has connected their own Google account, which account that is, and what to do next. `site-kit/list-modules`, `site-kit/get-module-settings`, `site-kit/set-module-state`, `site-kit/get-sharing-settings` and `site-kit/list-module-datapoints` cover the modules — which are on, which are fully configured, which Google property each points at, and who can see the data. `site-kit/get-search-analytics` reads Search Console clicks, impressions, CTR and position by query, page, country, device or date; `site-kit/get-analytics-report` runs GA4 reports; `site-kit/get-pagespeed-insights` runs Lighthouse; `site-kit/get-adsense-report` reads earnings. `site-kit/get-module-data` reaches any read datapoint the named abilities do not cover. Everything is read through Site Kit's own module clients, so no request shape is reimplemented and no Google credential is ever returned. The toolset appears only when Site Kit is active, and like Rank Math's it is not in a new MCP server's default set — add it by hand or reach it through Integrations. * **Site Kit abilities say whose problem it is when there is no data.** Site Kit stores one Google token per WordPress user, so an administrator on a fully configured site can still see nothing because a colleague did the connecting. Four situations that all look like "no data" — Site Kit not set up, this user not connected, the module switched off, the module on but missing its property settings — each get their own error code and their own sentence naming who must do what. Search Console returning zero rows is reported as the ordinary result it usually is, with the two-day reporting lag named, rather than as a failure. * **PageSpeed Insights returns a summary, not half a megabyte.** Google's raw Lighthouse response for one page measured 529 KB — 199 KB of it a base64 screenshot no assistant can display, and 315 KB of audit detail tables — which overflowed the reply before any of it could be read. `site-kit/get-pagespeed-insights` now returns the category scores as percentages, the Core Web Vitals, any real-user field data Google holds for the URL, and just the audits that failed. Pass `detail: "audits"` for every audit's score without its tables, or `detail: "full"` for Google's whole response. The screenshot is dropped at every level. The ability also now says up front that a run takes ten to sixty seconds and can outlast a client's request timeout. * **Site Kit settings can now be changed, not just read.** The suite could report that a site sends its analytics to one Google property but could do nothing about it. `site-kit/update-module-settings` writes them: which Analytics 4 property and measurement ID the site reports to, which Search Console property it reads, which Tag Manager container it uses, whether each module places its snippet, and who is excluded from tracking. Send only the keys you want changed. A key the module does not have is named back to you rather than silently dropped, which is what Site Kit's own writer does and the reason a typo used to look like success. The response reports each key's old and new value read back after the write, because every module runs its own sanitiser and what you asked for is not always what got stored. Confirm-gated, since switching a snippet off stops measurement on the live site immediately. * **Changing a Site Kit connection setting moves who owns the module — and now says so.** Site Kit silently reassigns a module's owner to whoever changes its property or account, and that owner's Google credentials are what serve the module's data to everyone reading a shared dashboard. The write reports when that happened instead of leaving it to be discovered. `ownerID` itself cannot be written by hand, and neither can any credential key — an ability that hides secrets on read should not let you set one. * **New: read and change your Site Kit Key Metrics.** The row of tiles at the top of the Site Kit dashboard — new visitors, most popular content, top traffic source — was invisible here. `site-kit/get-key-metrics` reports which tiles the current user has chosen, whether the row is hidden, and who set it up for the site; `site-kit/update-key-metrics` changes them. The selection is stored per WordPress user, so both describe and change only the dashboard of the user making the call. The slug list lives in Site Kit's JavaScript and grows with ordinary releases, so an unrecognised tile is saved and flagged rather than refused — refusing would break on exactly the tiles a newer Site Kit just added. Not confirm-gated: it moves tiles on one admin screen and touches neither the site nor its measurement. Verified against Site Kit by Google 1.188.0 on a live site with Search Console, Analytics 4, Tag Manager and PageSpeed Insights connected. * **The 27 Elementor design audits now actually examine the page.** They were registered and callable, but the analysis inside each was never written: they returned a made-up score with no findings, wrapped in "Ran audit: …" and a claim to be grounded in Elementor's official documentation. On a test page built as four identical 50/50 sections carrying the same button, `audit-generic-layout-patterns` used to answer 100 out of 100. It now answers 46, and names the three reasons — four repeated 50/50 rows, every row splitting evenly, and a stock split hero opening the page. All 27 are implemented against a shared model of the document, so every audit means the same thing by a row, a lane and a ratio. * **The eleven design fixes now change the page, and say exactly what they changed.** Each one is confirm-gated, writes through a single audited path, and returns every setting it touched with its previous value so a change can be put back by hand. Running one twice changes nothing the second time and does not re-save. The image-to-background conversion hides the original widget rather than deleting it, because that judgement is one you may want to reverse. * **Fixed: `elementor/evaluate-design` failed on every call, on every site.** It returned two fields its own output schema did not declare, and the schema forbids undeclared fields, so it errored every time it was used — it had never worked. The same fault was then found in the individual audits, which return their evidence under a field the schema also did not declare. Both are fixed and both are now pinned by tests. * **`elementor/evaluate-design` composes the audits it is supposed to.** Its registry was only ever filled in by the test suite, so on a real site it aggregated nothing — a separate fault from the skeletons, and one that fixing them would not have touched. Audits now enrol themselves, and the aggregate reports 16 running on a typical page. The mutating abilities deliberately do not enrol: an aggregate that rewrote the document as a side effect of being asked a question would be indefensible. * **Fixed a false positive found by building a good page rather than a bad one.** The native-widget audit counted icon elements across a whole row, so the standard three-up feature grid — one icon box per column — was told to become an Icon List, which is a vertical list inside a single column and not the same thing at all. It now counts within each column. Advice that is wrong about correct work is how a tool teaches people to ignore it. * **The aggregate score now says what it is.** It is a mean across audits, so audits finding nothing pull it up and a page with real problems can still read in the eighties. The response now carries that caveat and the lowest individual score alongside the average, so the number is read rather than trusted. = 0.0.38 - 2026-09-22 = * **Fixed: a toolset that happened to be empty disappeared from the tool list, and could never come back.** An AI assistant is handed its list of tools once, when it connects, and there is no way to hand it a new one. A toolset holding nothing was left off that list — so on a site where every ability already had a home, the Other toolset was missing, the Tools tab said fourteen while the assistant was served thirteen, and reconnecting did not help because it was still empty at that moment. The built-in toolsets are now always offered, empty or not; calling an empty one answers with a message rather than an error. Toolsets belonging to a specific plugin are unchanged: they still appear only when their plugin is there, and the Integrations toolset still reaches them either way. * **Fixed: the Integrations toolset could disappear too — the one thing that should never have.** Integrations exists so an assistant can reach a plugin installed after it connected. Its contents come only from plugin toolsets, so on a site with none active it was empty and therefore absent, exactly when it was most needed. It is now always present. * **Integrations and Other now say that their contents change.** Both fill and empty as plugins and themes are activated, so an assistant that asked once and remembered the answer was wrong from the next activation onwards with nothing to tell it. Their listings now carry a `volatile` marker and a note to ask again. An empty one says the emptiness is about this moment rather than settled. * **Fixed: Contact Form 7 mail tags written inside angle brackets were silently deleted.** `From: [your-name] <[your-email]>` is how a From line is normally written in a plain-text mail body. Sanitising read `<[your-email]>` as an unknown HTML tag and removed it, taking the mail tag with it — the save reported success, and checking the template afterwards reported it *valid*, because the tag that would have been flagged was gone. The tag survives now. Sanitising itself is unchanged and still removes real HTML; only this one shape, a bare mail tag between angle brackets, is let through. Updating a mail template also now names any field whose stored content was altered, so nothing is dropped quietly again. * **Fixed: a Contact Form 7 field option containing a space became several options.** Contact Form 7 splits a field tag on spaces, so `placeholder:+44 7700 900000` arrived as three separate options and the field ended up with a placeholder of `+44`. Option values are now quoted the way choice values always were. An option with a space and no `key:` in front of it cannot be repaired, so it is refused with the offending text named rather than silently mangled. * **Fixed: the Contact Form 7 field-type list advertised syntax that does not work.** Contact Form 7 registers `text` and `text*` as separate types, and the list appended an asterisk to each — producing a duplicate row for every type and the string `text**`, which Contact Form 7 does not understand. There is now one row per type, showing a required form only where one genuinely exists: `submit` and the captcha fields have none. The list goes from 35 entries to 24. * **Creating a Contact Form 7 form now accepts `template`, the name the other template abilities already use.** Creating a form called the markup `form` while reading and replacing it called the same thing `template`, so anything that learned one name was rejected by the next. `template` works everywhere now, and `form` still works for anything already using it. * **Enabling a Contact Form 7 autoresponder now warns when it would send to nobody.** A form whose fields were rewritten keeps Contact Form 7's stock autoresponder, which is addressed to `[your-email]` — on a form without that field the reply goes nowhere, and the visitor still sees a success message. Switching the autoresponder on now reports any mail tag in it that matches no field, so the problem is visible at the moment it is created. = 0.0.37 - 2026-09-21 = * **Fixed: the UpdraftPlus and All-in-One WP Migration tools were offered on sites without those plugins.** Both appeared in an MCP server's tool picker, and in the set a new server starts with, whether or not the backup plugin was anywhere on the site. Every other per-plugin toolset — Elementor, Rank Math, LiteSpeed — already excluded itself; these two, added in 0.0.35, did not. The tools themselves were never broken: they are still registered when their plugin is active, still addable by hand, and still reachable through the Integrations toolset. What changes is that they are no longer part of what a new server is given by default. If a server already has one and the plugin is not installed, "Reset to Type Defaults" clears it. = 0.0.36 - 2026-09-21 = * **Listing posts no longer forces you to download every body.** `content/list-posts`, `content/list-pages` and `content/list-cpt-items` returned every field of every item including the whole post_content — ten posts came to about 172 KB when almost all of it was content nobody had asked for yet. Pass `fields: "summary"` to get just what identifies an item: title, status, dates, slug, author, a trimmed excerpt, and content_bytes so you can size the follow-up read. Measured at 36-39x smaller. The default is unchanged, so nothing existing sees a difference. * **Fixed: the block outline reported the wrong total.** Asking for 3 blocks of a 40-block post reported `total: 3` — it counted what it returned rather than what matched, because it stopped walking the moment it had enough. It now reports `total: 40` with a new `returned: 3` alongside, so you can tell a small post from a truncated view of a large one. Each block also reports `subtree_bytes` next to `bytes`, which distinguishes a genuinely small block from a small wrapper around half the page. * **Site Health results can now be read as text.** The description and actions fields carry WordPress core's own markup — paragraph tags, icon spans that render as pictures and read as nothing, and screen-reader spans that repeat every link's text. Pass `format: "text"` for plain sentences with the link destinations kept. The default still returns the markup unchanged. * **Every ability now has to say whether it reads, destroys, or can be repeated.** Those three flags are how an AI client decides whether something is safe to try, safe to retry, and safe to run without asking, and a missing one reads as "not destructive" — the dangerous way to be wrong. Abilities missing them are now caught by the test suite, and reported on screen while `WP_DEBUG` is on. Nothing changes on a production site. * **The transient and object-cache abilities now point at the page cache when there is one.** Clearing transients is not what a visitor sees. On a site running LiteSpeed, these abilities now suggest `litespeed/purge-cache` for that — and say nothing on sites without it, rather than naming an ability that is not there. = 0.0.35 - 2026-09-21 = * **The backup abilities are now two tabs, one per plugin.** `UpdraftPlus` and `All-in-One WP Migration` each get their own tab, their own toolset and their own abilities, the same way Elementor, Rank Math, WPCode and every other integration works. 0.0.34 shipped them as a single "Backups" tab that reached both plugins through a shared layer; that made two genuinely different plugins look interchangeable and turned every real difference into a flag you had to go and check. * **Each suite now offers only what its plugin can actually do.** UpdraftPlus schedules backups and restores them, and stores no label - so it has no label ability. All-in-One labels its archives, and restoring belongs to their paid Unlimited Extension - so that ability asks the plugin and passes its own answer back, naming the manual import route, rather than refusing on its behalf. * **Breaking: the `backups/*` abilities are gone.** They are replaced by `updraftplus/*` and `all-in-one/*`. Anything holding a `backups/` slug needs updating; there are no aliases. The suite was one release old. * **Fixed: restoring never worked outside the admin screens.** The restore checked whether WordPress could write to the filesystem directly - the check that stops a restore dying half-way through - using a function WordPress only loads inside wp-admin. Every restore request therefore failed on that line before checking anything, whatever it was asked to do. This shipped in 0.0.34 and is fixed here. * **The exposure check is shared and reports per plugin.** Whether the web server will hand out a backup archive has nothing to do with which plugin wrote it, so that logic exists once - but each tab now reports on its own storage rather than on everything at once. = 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`; 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.` 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//`) + 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] Ability: file-manager/ File: User: (ID:) IP: Size: -> bytes Destination: (COPY / MOVE only) Backup: Context: **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 `.bak.