# Changelog

All notable changes to `@rolepod/wplab` are documented here. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.8.1] — 2026-10-08 — parent agent names

The 13 skills' hand-off lines name the parent rolepod's 4 agent types
(rolepod v2.221.0): `rolepod:rolepod-builder` (with `domain: writing` for
copy), `rolepod:rolepod-reviewer` with `lens: security` / `lens: ui`, and
`rolepod:rolepod-qa`. No behavior change.

## [3.8.0] — 2026-09-22 — opencode support

opencode users had the MCP server (any `mcpServers`-style config worked) but
none of the 15 skills, and no install path in the README said so. Now one
command wires both: `scripts/install-opencode.sh` writes the server entry
(with the two fields `opencode mcp add` leaves out — see below), and the new
`skills/index.json` catalog carries the skills.

### Added

- `skills/index.json` — an opencode (v2) HTTP skill catalog served from the
  raw GitHub view of `skills/`. Every skill directory is listed with all of
  its files (`SKILL.md`, `references/`, `examples/`, `templates/`), keyed by
  the package version so a release refreshes the cached skills without a
  config change. `npm run skills:index` regenerates it;
  `tests/unit/opencodeSkillCatalog.test.ts` fails when it drifts from disk.
- `scripts/install-opencode.sh` — one-line install:
  `curl -fsSL https://raw.githubusercontent.com/nuttaruj/rolepod-wplab/main/scripts/install-opencode.sh | bash`
  (flags `--project` / `--uninstall`; `ROLEPOD_WPLAB_REF=vX.Y.Z` pins a
  release). It merges the `mcp.servers.rolepod-wplab` entry (npx `@latest`
  `serve`, npm audit/fund off, `codemode: false`) and the catalog URL into
  `~/.config/opencode/opencode.json` (or `./.opencode/opencode.json`), keeps
  every other key, backs the file up as `*.rolepod-wplab-bak`, refuses
  non-plain JSON instead of rewriting it, replaces a stale v1-shaped entry,
  and removes only its own entries on uninstall. From a checkout, `--project`
  spawns the local build. `codemode: false` is required: opencode's default
  Code Mode hides MCP tools behind `execute`/`query`, so skills that call
  `rolepod_wp_*` by name never resolve. Test:
  `tests/unit/opencodeInstallScript.test.ts` (bash; skipped on Windows).
- README "opencode" install section: the one-liner above, the hand-paste
  config it writes, and why each field is there. Verified on opencode
  v2.0.12 (`mcp.servers.<name>` shape).
- `rolepod-wplab init` names the opencode command in its next steps.

## [3.7.0] — 2026-09-07 — Backups stop piling up

Every backed-up file write left one more `.wplab-bak-<stamp>` copy next to the
target, forever. A theme edited weekly for a year carried 50 copies of
`functions.php` in the install, and nothing ever collected them. Now each write
prunes its own file's backups to the newest 2.

### Changed

- `rolepod_wp_file_write` (local target) keeps the newest 2
  `<file>.wplab-bak-*` copies per file and deletes the rest after each write.
  `ROLEPOD_WPLAB_BACKUP_KEEP` raises the count; a junk or sub-1 value falls
  back to 2. `backup: false` still writes nothing and prunes nothing.
- Backup filenames are now collision-proof: two writes to the same file inside
  the same millisecond used to share a name, so the second silently overwrote
  the first. A `-1`, `-2` counter is appended instead.
- Undo never read these files — the Change Ledger stores its own content
  snapshot — so pruning costs only what a human would restore by hand. The
  `rolepod_wp_changes_toggle` description said otherwise; it now says what the
  revert actually reads.
- Tool descriptions for `rolepod_wp_file_write`, `rolepod_wp_file_write_batch`
  and `rolepod_wp_theme_snapshot` state the retention, so a model plans a
  rollback around what will still be on disk. `wp-scaffold` and
  `wp-edit-design` skills carry the same note.

### Removed

- `E2E-GAP-REPORT.md` — an internal round-by-round gap report from the v1.9/v1.10
  era (it also named a throwaway demo host). The repo keeps README, CHANGELOG,
  `docs/` and the shipped skills; working notes stay out of it.

### Companion

Requires rolepod-wp 2.26.0 for the same retention on REST targets — the
companion writes and prunes its own backups (`FsWrite`, `FsWriteBatch`) and
keeps the newest 2 theme snapshots per stylesheet. An older companion still
works; its backups just keep accumulating as before.

## [3.6.0] — 2026-09-06 — Cap what wp-cli hands the model

Two tools returned wp-cli output verbatim: `rolepod_wp_db_query` and
`rolepod_wp_cli_run`. `SELECT * FROM wp_posts` or `wp option list` on a real
site is hundreds of KB, and every byte went into the model's context. The
slicing helper already existed — `rolepod_wp_file_read` and
`rolepod_wp_render_get` use it — it had just never been applied here. Surfaced
by an audit of context-mode for the rolepod family; the one idea that survived
against what was already covered.

### Changed

- `rolepod_wp_db_query` and `rolepod_wp_cli_run` take `max_bytes` (default
  64 KB, applied to stdout and stderr each) and return `total_bytes`,
  `returned_bytes` and `truncated`. Truncation keeps the head, so a cut table
  still shows its header row. The cap lives in the two handlers only:
  `Target.wpCli()` and `guardTarget()` also serve internal callers that parse
  JSON, and a cap there would hand them a broken document.
- `rolepod_wp_rest_dump` returns per-namespace counts by default
  (`routes_by_namespace`); the path/methods table (`routes`) comes with
  `filter_namespace` or the new `full: true`. Measured once on a plugin-heavy
  production site before deciding: 371 routes across 18 namespaces, ~56 KB
  pretty-printed as a table against ~1 KB as counts, behind a 289 KB raw
  `/wp-json/` index. Over the ~20 KB line the brief set, so the default
  changed.
- Refusals that are really redirects now name the way forward. Every
  `throw new WplabError(` was read; most already did (`confirm=true`,
  `allow_write`, `rolepod_wp_woo_write`, the install URL). Rewritten:
  `WPCLI_BLOCKED` (names `allow_destructive=true`; for never-allowed
  subcommands, `rolepod_wp_execute_php` for `eval` and a person at a shell for
  the rest), `PRODUCTION_BLOCKED` on the guard's `enforce()` path (now carries
  what matched and who lifts it — the owner's AI Full Control toggle, or
  `ROLEPOD_WPLAB_PROD_HOSTS`), `TARGET_NOT_FOUND` (reconnect; targets do not
  survive a server restart), `COMPANION_REQUIRED` across nine tools (one
  `CompanionRequiredError` with the install URL, pairing and reconnect steps),
  `CUSTOM_PLUGIN_NOT_INSTALLED` on toggle / update / remove (run
  `rolepod_wp_custom_init` first, as scaffold already said), and
  `REST_REQUIRES_HTTPS` (the https form, or `rolepod_wp_connect_local`).

### Added

- `capStreams()` in `src/lib/contentSlice.ts`, and tests for the cap on both
  handlers, the summary-first dump, and the refusal wording.

## [3.5.0] — 2026-09-04 — Say when Elementor's own MCP should win

Elementor has announced an official MCP — "coming soon" as of 2026-09-04, not
shipped — and Premium Addons for Elementor ships one today. Either can be
connected alongside this server, and then two toolsets offer overlapping
Elementor operations with nothing telling a model which to reach for.

The answer is routing, not removal. Deleting working tools to make room for a
server that has not shipped leaves users with nothing in between, and the
`rolepod_wp_elementor_*` surface is not a straight duplicate: some of it exists
precisely because Elementor's own render throws information away.

### Added

- `SERVER_INSTRUCTIONS` and the `wp-edit-design` skill now say to prefer an
  Elementor-native MCP for ordinary page building — creating widgets, editing
  settings, applying templates — and name the cases where these tools are still
  the right call: no such server connected; a target only wplab can reach (SSH,
  Docker, remote REST); `rolepod_wp_elementor_widget_attribute`, which persists
  `data-*` attributes that Elementor's render strips and that a server building
  native Elementor structure will not restore; edits that must be revertible
  through the change ledger; and pre-write validation via
  `rolepod_wp_elementor_validate_data` / `rolepod_wp_elementor_html_audit`.
- Both surfaces ask the model to say which server it used, so an Elementor page
  touched by two toolsets is not a mystery later.

### Fixed

- `serverInstructions.test.ts` read `rolepod_wp_elementor_*` as a citation of a
  tool that does not exist. A trailing `*` names a family; the gate now drops
  those at extraction. Verified it still fails on a genuine typo.

## [3.4.3] — 2026-09-04 — Name the 403-on-write failure where every MCP client sees it

3.4.1 and 3.4.2 put this in recipe 24, which only clients that read the docs
will find. The tool description reaches every MCP client, and it is the place
an agent is told to drive a browser — so it is where the misdiagnosis happens.

### Added

- `rolepod_wp_admin_one_time_link` description now names the failure: an admin
  POST returning 403 while every GET succeeds is a WAF rule on the user-agent,
  not an authentication problem. uiproof 0.20.0+ presents a desktop UA by
  default; older versions need an explicit one, and a host that still refuses
  may be reading `Sec-CH-UA` client hints, which a user-agent override does not
  rewrite.

## [3.4.2] — 2026-09-04 — uiproof 0.20.0 fixes the UA; document the cheap way to get refs

`@rolepod/uiproof@0.20.0` is on npm and presents a desktop Chrome UA in headless
mode, so 3.4.1's "pass a real `user_agent`" instruction is no longer the first
thing to do — but the symptom is still worth recognising, because a WAF that
reads `Sec-CH-UA` client hints is not fixed by a UA override.

### Changed

- Recipe 24 leads with the symptom rather than the workaround: an admin POST
  that 403s while every GET succeeds is a WAF rule, not an auth failure. The
  explicit `user_agent` stays as the answer for uiproof ≤ 0.19.1 and as the
  isolation step (alongside `headless: false`) when a host still refuses.

### Added

- Recipe 24 documents `browser_find` and `browser_wait_for { ref_exists }`
  (uiproof 0.20.0+) as the way to obtain refs, and says why: a wp-admin
  accessibility tree costs roughly 10k tokens per `browser_snapshot`, and this
  recipe points at exactly those screens. Measured while driving a plugin
  install through the flow — three snapshots to click two buttons.

## [3.4.1] — 2026-09-04 — Warn that a headless user-agent can 403 admin writes

Found while running recipe 24 for real: the one-time link opened, wp-admin
rendered, the session was authenticated — and the first POST came back 403 from
nginx. GET worked throughout, which makes it read like an auth failure when it
is a WAF rule on the user-agent. A/B on the live host with only the UA changed
turned the same POST into a success.

### Added

- Recipe 24 now tells readers to pass a real desktop `user_agent` on uiproof
  0.19.1 and below, with the symptom spelled out (every GET fine, first write
  403) so the failure is recognisable rather than mysterious.
- uiproof 0.20.0 will default to a desktop UA; the note says so, and keeps one
  line about `Sec-CH-UA` client hints still reporting HeadlessChrome, since
  Playwright's override does not rewrite those.

## [3.4.0] — 2026-09-04 — Sign what the server writes

Someone inheriting a WordPress site had no way to tell which plugins an AI
wrote. The only mark was one sentence in `readme.txt` — a file WordPress never
parses and the first thing anyone overwrites — and it covered `scaffold_plugin`
alone. A plugin written through `file_write`, or built locally and uploaded as
a zip, carried nothing at all.

### Added

- `src/lib/provenance.ts` — one stamp, used everywhere, so a single grep finds
  every artifact this server produced. Two marks, because they survive
  different things: `Generated-By: rolepod-wplab` + `Generated-At: <date>` in
  the header WordPress actually parses (travels with the directory when it is
  copied to another site; WordPress ignores headers it does not know), and
  `(generated by rolepod-wplab)` appended to `Description`, which is the half a
  human reads in wp-admin with no tooling at all.
- Both marks now ship from `rolepod_wp_scaffold_plugin`,
  `rolepod_wp_scaffold_theme`, `rolepod_wp_child_theme_create`,
  `rolepod_wp_cpt_scaffold` and `rolepod_wp_scaffold_pattern`.
- `SERVER_INSTRUCTIONS` gains **"Sign what you write"** — including the case the
  scaffolds cannot cover: a plugin assembled by hand or uploaded as a zip is
  unmarked unless the model adds the same two marks itself.
- `wp-scaffold` skill: Iron Rule 4 (never strip the stamp, add it by hand when
  you build a plugin yourself) and a Provenance section giving the read path —
  `rolepod_wp_file_read` with `max_bytes: 2048` on the bootstrap file, which
  works on a guarded site.
- `tests/unit/provenance.test.ts` — 10 cases, including that re-scaffolding does
  not stack the suffix and that the agent name merely mentioned in prose is not
  read as a provenance claim.

### Changed

- `rolepod_wp_cpt_scaffold` used its own wording ("Scaffolded by
  rolepod-wplab.") in the description. It now uses the shared suffix, so one
  string matches every artifact.

### Notes

- This is provenance, not attestation: it records what this server wrote, the
  lines can be deleted, and an unmarked plugin proves nothing. The change ledger
  (`rolepod_wp_changes_query`) is the counterpart a file edit cannot rewrite —
  per-site, where the header travels with the files.
- The stamp carries no version because the server has none to report:
  `src/server.ts` still announces `0.0.0` to MCP clients. No version is better
  than a wrong one; wiring the real version through is separate work.

## [3.3.1] — 2026-09-04 — The slow first launch was npm audit, not the package size

3.3.0 credited uiproof 0.19.0's smaller dependency tree for a faster cold
start. That was the wrong cause. Measured on a cold cache with
`npm_config_loglevel=http`: `POST /-/npm/v1/security/advisories/bulk` — the
`npm audit` that `npx` runs on a fresh install — took 190-320s across five
runs, while fetching and extracting the entire tree took 6-9s. Cutting the
package count 337 → 108 barely moved the number.

### Changed

- `docs/RECIPES.md` recipe 24 states the real cause and the real fix: uiproof
  0.19.1 sets `npm_config_audit=false` + `npm_config_fund=false` in every
  shipped spawn config, which brings a cold connect to ~16s. Below 0.19.1, or
  in a hand-written spawn config without that env, the multi-minute first
  launch is still there — the note now says to copy uiproof's env block, and
  that the flags cannot go in the `npx` args because `npx` reads leading flags
  as its own.

## [3.3.0] — 2026-09-04 — Mint the admin session once, reuse it across runs

`@rolepod/uiproof@0.19.0` added `browser_save_state`, which turns the one-time
login from a per-session cost into a one-off. 3.2.1 explicitly told readers not
to plan around that because no save step existed; it exists now.

### Added

- `docs/RECIPES.md` recipe 24 documents the mint-once flow:
  `rolepod_wp_admin_one_time_link` → `browser_open { url }` →
  `browser_save_state` → later `browser_open { storage_state: <path> }` across
  runs and after a close. Below uiproof 0.19.0 there is no save step, so
  minting per session stays the guidance there.

### Changed

- Artifact safety is now stated per artifact and per version, because it is no
  longer one rule: `storage-state.json` **is** the session; `trace.zip` has
  cookies scrubbed from 0.19.0 and is raw below it; `network.har` is scrubbed
  from 0.18.0 and raw below it; video and screenshots never carried cookies.
  Nothing scrubs response bodies, DOM snapshots or screenshots, so every one of
  these still shows whatever the authenticated page rendered — share only with
  people cleared to see those admin pages.
- The tool description and `SERVER_INSTRUCTIONS` carry the short form of the
  same correction. 3.2.1 said a Playwright trace "carries the auth cookie
  outright", which stopped being true one release later.
- The cold-start caveat is softened rather than dropped: uiproof 0.19.0 removed
  the webdriverio tree that caused it, so a quiet handoff is still worth waiting
  out but is no longer the multi-minute install it was on 0.17.x.

## [3.2.1] — 2026-09-04 — Artifact-safety wording that is true of both uiproof versions

`@rolepod/uiproof@0.18.0` shipped HAR redaction, so "any HAR, trace or video
contains the auth cookie" stopped being accurate — and it was never the whole
story anyway. Overstating a risk is safer than overstating a protection, but it
is still wrong, and a rule people learn is wrong gets ignored wholesale.

### Changed

- Tool description and `SERVER_INSTRUCTIONS` now separate the artifacts: a
  Playwright trace carries the auth cookie outright; a HAR carries it on uiproof
  ≤ 0.17.1 and carries admin page bodies either way; video shows whatever was on
  screen. Everything recorded during an admin session is at least an internal
  artifact — check before sharing rather than assuming it was scrubbed.
- `docs/RECIPES.md` recipe 24 gets a per-artifact table with the same split,
  including that uiproof's redaction is best-effort and logs
  `har redaction failed — treat network.har as a credential` when it does not
  work.
- Recipe 24 documents `browser_open { storage_state }` (uiproof 0.18.0+) and is
  explicit that no tool writes that state file yet, so an OTL is still minted
  per browser session. Do not plan around mint-once-reuse-forever until a save
  step exists.

### Fixed

- `npm run format:check` had been failing on every commit since 2026-08-22 —
  eight consecutive red CI runs, invisible because the release workflow is a
  separate job that stayed green and kept publishing. Five files reformatted,
  whitespace only; four of them predate this work.

## [3.2.0] — 2026-09-04 — Read the recorded fatal before touching anything, and route partial fatals to wp-recovery

Written from a real failure. A site's REST API had been returning 500 on every
route for thirteen days while the front end, wp-admin and admin-ajax all
answered 200. The companion's guardian mu-plugin had recorded the exact cause
the whole time — class, file and line — and `rolepod_wp_recovery_status`
returns it. Nothing pointed there, so instead: a wrong hypothesis, a live
security plugin deactivated to test it (which invalidated the admin session and
could not be put back without the user), a debugging plugin installed, and a
six-round bisect. The evidence was one call away from the start.

Two routing gaps made that possible, both fixed here.

### Fixed

- **`wp-recovery` was absent from the skill routing table.** `Site is broken,
  WSOD, fatal` pointed at wp-health-check and wp-changes; the one skill whose
  Iron Rule is "NEVER guess the culprit — read `rolepod_wp_recovery_status`"
  was not listed at all. It now leads that row.
- **A partial fatal did not read as a fatal.** `wp-recovery`'s `when_to_use`
  said "the site returns a 500 / white screen", which is false when only one
  surface is dead and every human-visible page is 200 — so the skill never
  fired. It now names that shape explicitly: a 500 confined to REST, to
  admin-ajax, or to a single admin screen is still a fatal, and the guardian
  recorded it.

### Added

- A **cheapest-and-safest-first ladder** in `SERVER_INSTRUCTIONS`: existing
  recorded evidence → read-only observation → read the configuration →
  isolation scoped to your own session → reversible site-wide change →
  anything harder to undo. Stop at the first rung that answers the question,
  and never open at rung 5. It also warns that a site-wide deactivation can
  have side effects you did not plan for, naming the session-invalidation case.
- New routing row for the partial-fatal shape (one surface 500s, the rest 200).

## [3.1.1] — 2026-09-04 — Make the browser ladder explicit, and say which rung you are on

3.1.0 said "hand the URL to whatever browser automation you have" and listed
options inline. That reads as a single suggestion, and it leaves two gaps: a
model with no uiproof can take it as "this needs uiproof", and a model that
does automate the step never tells the user — so the user sits waiting for a
login prompt that is never coming, which is the exact failure 3.1.0 set out to
remove.

### Changed

- The tool description and `SERVER_INSTRUCTIONS` now give an ordered ladder:
  (1) rolepod-uiproof `browser_open`, (2) **any** other browser automation on
  the machine — Chrome-extension MCP, Playwright, Puppeteer — since the link is
  a plain URL and nothing about it is uiproof-specific, so a missing uiproof
  blocks nothing, (3) a human, last, only when the machine has no browser
  automation at all.
- Both surfaces now require saying which rung is in use: on 1 or 2, tell the
  user the admin step is handled and no login is needed; on 3, say that no
  browser automation is available here so this step needs them.
- Re-minting after a browser context is recreated is called out as normal, not
  a failure to report.
- README bullet and `docs/RECIPES.md` recipe 24 carry the same ladder.

## [3.1.0] — 2026-09-04 — Open the admin link yourself; a human clicking it is the last resort

`rolepod_wp_admin_one_time_link` has minted browser-openable admin URLs since
1.8.0, and its description led with "the AI surfaces the URL to the user — they
click once". Reading exactly that, this assistant still stopped a live
investigation twice to ask a human to log in. The capability was never the
problem; the instruction was.

Verified end to end against a live site before writing any of this: minted a
link, opened it with rolepod-uiproof `browser_open`, landed in wp-admin
authenticated, and stayed authenticated across a later `browser_navigate` — no
password, no click.

### Changed

- `rolepod_wp_admin_one_time_link` description now leads with the automation
  path: hand the URL to whatever browser automation you have (rolepod-uiproof,
  a Chrome-extension MCP, Playwright, Puppeteer), and note that the auth cookie
  persists on that browser context for the rest of the session. Asking a person
  to click is named as the LAST resort. Reaches every MCP client, not just the
  ones that read skills.
- `SERVER_INSTRUCTIONS` gains **"Never ask a human to log in for you"** under
  Rules that hold everywhere, with the 5-minute single-use bound, the re-mint
  rule when a browser context is recreated, and the note that this works on a
  guarded site (`/admin/one-time-login` needs `manage_options`, not Full
  Access).

### Added

- README bullet documenting the tool, which also retires it from
  `UNDOCUMENTED_TOOLS`.
- `docs/RECIPES.md` recipe 24 — auditing a wp-admin screen with no human login,
  including the cold-start caveat that a first uiproof launch can take minutes
  and looks like a hang.

### Security

- Every surface above now states that whoever opens the URL holds a full admin
  session, and that a HAR, Playwright trace or video recorded during it captures
  the auth cookie. Those artifacts are credentials and must not be shared.

### Internal

- `serverInstructions.test.ts` treats `wp-admin` as a WordPress path rather than
  a missing skill, alongside the existing `wp-config` / `wp-cli` / `wp-json`.

## [3.0.2] — 2026-08-22 — Full Access is requested with a warning and a backup, never as a bare instruction

### Changed

- **Hitting a guarded wall now triggers a request protocol, not a shortcut.**
  Server instructions, the client-side guard message, the Bridge's
  missing-capability error, and the `wp-execute-php` skill all direct the
  agent through the same four steps before the user is asked to enable
  AI Full Control: say what needs it and why, warn in plain words that ON
  hands the AI full control of a live site, offer
  `rolepod_wp_backup_create` FIRST (it works in guarded mode, so the backup
  exists before the power surface opens), and promise — then deliver — the
  reminder to turn it back OFF afterwards. Declining is documented as a
  valid outcome.
- `wp-execute-php` skill updated off the pre-3.0 production model: the old
  "production-matched → no override" hand-off is replaced by the guarded-mode
  protocol, and preconditions now check the advertised `execute_php`
  capability instead of production matching.

## [3.0.1] — 2026-08-22 — Plugin-first onboarding

### Changed

- **First-time connection guidance now leads with the plugin, everywhere.**
  Server instructions, the `CREDENTIALS_MISSING` error, and the `wp-connect` /
  `wp-pair-setup` skills all walk the user through the same path: install the
  Rolepod for WordPress plugin (stable zip URL included in the message), open
  Tools → Rolepod WP Setup, generate a pair token, paste it back — the pair
  mints and stores the Application Password with no manual steps. Creating an
  App Password by hand is documented as the LAST resort, offered only when the
  plugin cannot be installed or the user explicitly declines it. Previously
  the two paths were presented as near-equals, and agents sometimes steered
  first-time users through the manual credential dance.
- `wp-pair-setup` gained a step 0 — the install walkthrough itself — and an
  Iron Rule forbidding the manual-first suggestion. README Path B is retitled
  "manual App Password (last resort)".

## [3.0.0] — 2026-08-22 — One switch: the companion's access mode drives the guard

Companion 2.24 stops guessing whether a site is production; the owner's
full-access toggle is the whole decision. This release makes the client mirror
that: 'full' opens the power surface, 'guarded' arms the guard. Major because
guarded mode now blocks tools that previously ran on unlisted hosts.

### Changed — BREAKING

- **Guarded companion targets arm the production guard.** With the companion's
  full-access toggle OFF, the client-side guarded tool set (`fs` writes,
  structural Elementor edits, `site_restore`, `custom_init`, …) refuses with a
  message naming the fix — enable Full access in wp-admin → Rolepod WP →
  Settings. Previously these ran whenever the host matched no production
  pattern, which for most installs meant always. The companion 2.24+ enforces
  the same boundary server-side; the client guard just fails fast and clear.
- **Full-access companion targets disarm the guard.** The owner's toggle
  outranks `ROLEPOD_WPLAB_PROD_HOSTS` patterns and `WP_ENVIRONMENT_TYPE` for
  that host — second-guessing it only diverted writes onto ledger-less paths
  like raw execute-php. `Bridge.executePhp()` drops its production pre-flight;
  the capability advertisement is the gate.
- **`pair` returns `access_mode: 'full' | 'guarded'` instead of
  `is_production`.** For pre-2.24 companions the mode is derived from the
  capability list (`execute_php` advertised ⇔ the same toggle was on), and
  their `is_production` still arms the guard exactly as before.

### Added

- `prod_guard.reason` values `"guarded"` and `"full_access"`. Health check
  reports a companion target's mode without probing and no longer warns that
  the guard is disarmed when that is the owner's explicit choice.
- Legacy probe path (`WP_ENVIRONMENT_TYPE` via wp-cli + `ROLEPOD_WPLAB_PROD_HOSTS`)
  is unchanged for targets with no companion to ask: local, SSH, docker, bare REST.

## [2.0.0] — 2026-08-21 — safety chokepoint

### Changed — BREAKING

- **The wp-cli allow-list now applies to every target kind.** It used to run
  inside `runWpCli()`, which only `LocalTarget` calls, so `rolepod_wp_cli_run`
  passed anything through on REST, SSH and Docker targets — while its own
  description claimed the opposite. **Any subcommand that is not in the
  allow-list now fails on REST/SSH/Docker too**, not just on local. If a
  workflow relied on running an unlisted subcommand over REST or SSH, it will
  now raise `WPCLI_BLOCKED`.
- `db reset`, `db drop`, `db clean` and `core multisite-convert` are refused for
  every caller on every target kind, with no opt-out — `allow_destructive` does
  not reach that layer.
- `role reset` is now never-allowed (it wipes every capability on every default
  role, site-wide).
- `wp user delete` requires `--reassign`; without it the user's posts are
  deleted along with the user.
- `rolepod_wp_db_query` rejects stacked statements. `SELECT 1; DELETE FROM
wp_posts` used to pass the read-only check, because only the head statement
  was inspected while wp-cli ran both.
- `rolepod_wp_diagnose` no longer aborts the run when one probe throws; the
  failing scope is reported as a warn finding that says it was not checked.

### Added

- Production guard arms itself from the site's own `WP_ENVIRONMENT_TYPE`
  (raw value — an unset signal leaves it **disarmed**, since WordPress' own
  `wp_get_environment_type()` defaults to `production` and would arm every
  unconfigured local install). Connect tools, `rolepod_wp_pair` and
  `rolepod_wp_health_check` report `prod_guard`, and health_check warns when
  the guard is disarmed.
- Allow-list additions: `core is-installed`, `core verify-checksums`,
  `plugin verify-checksums`, `plugin get`, `theme get`, `maintenance-mode
status`, `role/cap/term/comment` reads (read-only); `config set`,
  `config delete`, `core update-db`, `theme delete`, `maintenance-mode
activate|deactivate`, `rewrite flush`, `media import`, `media regenerate`,
  `role/cap/term/comment` writes (destructive). `db query` is classified by its
  SQL rather than by the subcommand.

### Fixed

- **`credentials add` and `init` silently did nothing over a pipe.** `ask()`
  created a fresh readline interface per prompt; readline reads stdin in chunks,
  so the first interface buffered every piped line and `close()` discarded the
  ones it had not yielded. The second prompt then opened on a drained stream,
  its `question()` never settled, node saw an empty event loop and exited **0** —
  reporting success while storing no credential at all. Non-TTY stdin is now
  drained once and handed out one line per prompt; an exhausted stream yields
  `""` so the caller's own validation reports it. TTY input is unchanged.

- `rolepod_wp_db_query` never worked on `LocalTarget` — `db query` was not in
  the allow-list, so every call was blocked before it ran.
- `rolepod_wp_audit_security` advertised a CVE lookup it does not perform.
- `rolepod_wp_recovery_safe_mode` claimed the companion refuses execute-php,
  theme switches and file writes while safe-mode is on. It does not; only media
  optimization reads the flag.

### Fixed — builders and change ledger

- **Builder meta writes are now verified.** `wp eval update_post_meta(...)`
  exits 0 even when the write does not take effect, so every builder adapter
  reported success it could not confirm. `replacePostMeta` reads the value back
  and compares it structurally (tolerating PHP's `{}`→`[]`), throwing
  `POST_META_VERIFY_FAILED` on a mismatch.
- **Bricks header/footer writes were silent no-ops.** They wrote
  `_bricks_header_content` / `_bricks_footer_content`, which Bricks never reads;
  all scopes now write `_bricks_page_content_2`. Because a page body shares that
  key, header/footer writes now require a `bricks_template` post
  (`BRICKS_WRONG_POST_TYPE`, fail-closed) so they cannot overwrite a page.
- **`rolepod_wp_builder_detect` never detected Bricks** — it looked in the
  plugin list, but Bricks is a theme. It now reads the theme list too, and
  reports unsupported builders (Beaver, WPBakery, Breakdance, Brizy) with
  `supported: false` instead of falling through to Gutenberg.
- **Change-ledger coverage and honesty.** Builder writes (Bricks/Oxygen/
  Elementor/Rank Math) now record a revert row at the shared chokepoint;
  Elementor `_elementor_data` is recorded reversible:false with its backup
  path. `wp_set_front_page` records one row per real option instead of a single
  junk `front-page` key. Nav-menu tools, WooCommerce, forms, WPML and ACF now
  record reversible:false visibility rows with manual-undo notes rather than
  claiming a revert that would corrupt state or no-op. `changes_query` returns
  `ledger_available:false` on non-REST targets instead of an empty list.

## [1.23.0] — 2026-06-05 — MCP tools for companion media-optimize + site backup/restore

### Added

- **`rolepod_wp_media_optimize`** — drive the companion's bulk image optimizer:
  `mode=immediate apply=false` (dry run), `apply=true` (optimize up to `limit`),
  or `mode=enqueue` (throttled background queue). Requires companion v2.16+.
- **`rolepod_wp_site_backup`** — throttled server-side site backup with action
  dispatch: `start` (components + compress + exclude), `status`, `list`,
  `inspect` (read the backup zip's index or a single member — e.g.
  `manifest.json` / `database.sql` — WITHOUT extracting), `cancel`, `delete`.
  Requires companion v2.17+.
- **`rolepod_wp_site_restore`** — restore a backup (DESTRUCTIVE): `start`
  (id + confirm + components + optional serialized-safe `search_replace` URL
  rewrite + `path_prefix`) and `status`. Production targets need `confirm=true`.
  Requires companion v2.18+.

All three wrap the rolepod-wp companion's throttled endpoints (work runs in WP
on a cron loop) and require a `rest` target with the companion enabled.
`src/companion/Bridge.ts`, `src/schema/tools.ts`,
`src/tools/companion/wp_{media_optimize,site_backup,site_restore}.ts`.

## [1.22.2] — 2026-05-29 — Fix: Docker build (externalize ssh2 native addon)

### Fixed

- The Docker image build (`docker build`, run in CI) failed at `npm run build`
  with `Cannot find module './crypto/build/Release/sshcrypto.node'`. `ssh2`
  (pulled in by `node-ssh` for SSH targets) ships an OPTIONAL native addon;
  bundling it made esbuild try to resolve that `.node` at build time — fine on
  a host where node-gyp built it, but fatal in the alpine build stage with no
  native toolchain. Marked `ssh2` + `cpu-features` **external** in
  `tsup.config.ts` so they resolve from `node_modules` at runtime (ssh2 falls
  back to pure JS when the addon is absent), and promoted `ssh2` to a declared
  dependency so it's always installed. Verified locally: `docker build`
  succeeds and the resulting image serves a clean MCP handshake.

### Notes

- Pure build/packaging fix — no tool behavior change. The bundle now `import`s
  `ssh2` instead of inlining it; SSH-target functionality is unchanged.

## [1.22.1] — 2026-05-29 — validate_data structural warnings

### Added

- `rolepod_wp_elementor_validate_data` now emits two schema-free **structural
  warnings**, catching traps that cost real debugging on hand-built pages:
  - **section-level `_css_classes`** — free Elementor does NOT render section
    CSS classes to the DOM (only Pro / a server-side render filter does), so
    `.your-section-class` CSS silently no-ops. Aggregated into one warning
    listing the section ids; suggests verifying with `render_get` or scoping to
    the always-rendered `.elementor-element-{id}`.
  - **block-level HTML in an icon-box `description_text`** (`<ul>`/`<div>`/…) —
    Elementor wraps the description in a `<p>`, so the browser auto-closes it
    and the block escapes the container.
    Pure helper `collectStructuralWarnings` (unit-tested; verified live).
    `src/lib/elementorValidator.ts`.

### Removed

- Dead `SCHEMA_CACHE_KEY` symbol in the validator (unused since inception).

### Notes

- Investigated the "target alias doesn't persist across sessions" report — not a
  bug. Aliases persist to `~/.config/rolepod-wplab/aliases.json` (verified); an
  empty `alias list` just means none were set yet in that environment.

## [1.22.0] — 2026-05-29 — Elementor surgical edits, restore, render-get, ranged reads

Batch of ergonomics fixes + new tools, all driven by friction hit while
rebuilding a live Elementor site by hand (every section swap was a hand-rolled
`wp eval`; stale CSS cache made verification lie; a 36 KB theme file blew the
read token budget).

### Added

- **`rolepod_wp_elementor_section`** — surgically `get | replace | insert |
delete` ONE top-level section, matched by element id or a `_css_classes`
  token, instead of rewriting the whole `_elementor_data` tree. Auto-backups +
  auto-flushes the Elementor CSS cache. `src/tools/composite/wp_elementor_section.ts`.
- **`rolepod_wp_elementor_restore`** — `list` the timestamped `_elementor_data`
  backups for a post (newest-first) and `restore` one. Unwraps the
  `wp post meta get --format=json` double-encoding internally and backs up the
  current state before restoring. `src/tools/composite/wp_elementor_restore.ts`.
- **`rolepod_wp_render_get`** — fetch the RENDERED front-end HTML of a post (its
  permalink) so you can confirm markup/classes actually emitted to the DOM.
  Same-host-guarded; `grep`/`max_bytes` keep the response bounded.
  `src/tools/composite/wp_render_get.ts`.
- **`rolepod_wp_file_read`** gains `offset`/`limit` (line range), `grep`
  (+`context`, `ignore_case`), and `max_bytes` — read just what you need from
  large files without blowing the response token budget. `bytes` still reports
  the full file size. Shared slicer: `src/lib/contentSlice.ts`.

### Fixed

- **`rolepod_wp_elementor_write` now auto-flushes the Elementor CSS cache** after
  writing `_elementor_data`. A direct meta write leaves Elementor's cached
  per-post CSS stale, so the front-end kept rendering the OLD layout until a
  manual `wp elementor flush-css` — the exact trap that let stale pages read as
  "fine" during verification. Shared helper `flushElementorCss` +
  `src/lib/elementorData.ts` (read/decode/write-with-flush).
- Stale doc on `elementor_write`: backups land under
  `wp-content/uploads/rolepod-wp/backups/` (not `wplab-backups/`), and the write
  auto-flushes — description updated.

### Companion

- Pairs with **rolepod-wp 2.13.2**, which fixes `/fs-write` reporting the wrong
  `bytes_written` on `mode=append`.

## [1.21.4] — 2026-05-29 — Fidelity-aware Elementor HTML audit

`elementor_html_audit` used to push "convert HTML widget → native widget"
purely on structure (single `<h1>` → heading, `<details>` → accordion, …)
with no regard for the custom CSS/JS packed into that widget. Following that
advice blind silently destroys the design — the Phase 8 failure where a
hand-crafted site converted to generic Elementor and lost its typography,
gradients, and JS effects.

### Added

- **Fidelity grading per HTML widget.** Each block is now scanned for the
  styling/behaviour a naive conversion would drop: inline `<style>`/`style=`,
  `font-family`, CSS gradients, `@keyframes`/`animation`/`transition`, and
  `<script>`/inline `on*=`/JS data-hooks (`data-typer`, `data-scramble`,
  `data-tilt`, `data-marquee`, …). Conversion suggestions now carry
  `fidelity_risk` (`low` = reproducible via native style controls if carried
  over; `high` = animation/JS with no Elementor-free equivalent) and a
  `would_lose[]` list. `src/lib/elementorHtmlAudit.ts`.
- **`lossy_widgets` count + `guidance`** at the top level — present whenever any
  HTML widget carries design-bearing CSS/JS, even widgets we (correctly) do NOT
  suggest converting (custom cards/terminals/marquees). The guidance tells the
  agent to extract styling/behaviour into native controls / Motion FX / custom
  code BEFORE converting, and to verify against a post-reveal screenshot.
- Reason strings on at-risk suggestions now append an explicit warning so the
  fidelity cost is visible inline, not just in a separate field.

### Why

The audit is the gate agents run before refactoring an Elementor page. Making
it surface the fidelity cost is the wplab-side fix for the "convert ≠ match the
mockup" trap: the agent is now told what it would destroy instead of being
nudged to destroy it.

## [1.21.3] — 2026-05-29 — Consolidated data namespace (backups + tmp)

### Changed

- `replacePostMeta` (the shared write helper behind Elementor + every builder
  adapter) now writes backups and tmp payloads under a single namespace:
  `wp-content/uploads/rolepod-wp/{backups,tmp}/` (was `wplab-backups/` +
  `wplab-tmp/`). One place for all runtime artifacts, kept in `uploads/` so
  they survive plugin updates. Pairs with companion 2.13.1 which auto-installs
  a deny-all `.htaccess` on that dir. Old `wplab-backups/` files are left in
  place (not migrated) — harmless, just not consolidated.
- Follow-up (separate paths, not yet moved): `wplab-bin/` (wp-cli phar),
  `wplab-clone/`, `rolepod-wp-audit/`, `rolepod-wp-theme-snapshots/`.

## [1.21.2] — 2026-05-29 — Fix: Elementor read/write on RestTarget

Two bugs made Elementor page editing unusable (and silently destructive) on
REST targets. Found while rebuilding a live Elementor site from HTML widgets to
native widgets.

### Fixed

- **`elementor_write` corrupted `_elementor_data`.** The shared `replacePostMeta`
  helper ran `json_decode(..., true)` and stored a PHP **array** in the meta.
  Elementor reads `_elementor_data` as a JSON **string**, so the editor saw an
  invalid document and rendered the page **empty**. Added a `"json-string"`
  serialization mode (`wp_slash`'d JSON string, no decode) and switched the
  Elementor write adapter to it. `src/adapters/_shared/replacePostMeta.ts`,
  `src/adapters/elementor/write.ts`.
- **`elementor_read` / `elementor_template_export` threw on RestTarget.**
  `getPage` had a hardcoded "requires companion v0.2" throw even though a
  RestTarget with the companion routes `wp-cli` fine. Extended the wp-cli branch
  to cover `rest` + companion, and added a double-decode guard for the
  `--format=json` scalar-string double-encoding. `src/adapters/elementor/read.ts`.

Verified live: read → write → audit round-trip keeps the page intact (was:
`EMPTY_ELEMENTOR_DATA`).

## [1.21.1] — 2026-05-29 — Fix: MCP tools/list rejected by strict clients

Critical fix. Every wplab tool silently failed to register in MCP clients
that validate `tools/list` (Claude Code among them) — the server connected,
but zero tools were exposed. Pre-existing across all prior releases; surfaced
once the new skill tools were added.

### Fixed

- **`zodToJsonSchema` now handles `ZodEffects`, `ZodDiscriminatedUnion`, and
  `ZodLiteral`.** The minimal converter fell through to `{}` (no `type`) for
  these, producing an `inputSchema` that violates the MCP requirement
  `type === "object"`. A strict client rejects the WHOLE `tools/list` on the
  first invalid entry, so two bad schemas dropped all 124 tools:
  - `rolepod_wp_target_alias` (`z.discriminatedUnion`) → now `type: "object"`
    with the discriminator exposed as an enum + every variant's properties merged.
  - `rolepod_wp_skill_edit` (`z.object().refine()`) → `ZodEffects` now unwraps
    to its inner object.
  - `ZodLiteral` → `{ type, enum: [value] }`.
- Regression guard: `tests/unit/zodToJsonSchema.test.ts` asserts these shapes
  plus both real tool schemas resolve to valid MCP objects.

## [1.21.0] — 2026-05-29 — Site-owned skills (5 tools)

Surfaces the companion's new site-owned skill store as MCP tools with
progressive disclosure: the agent lists slugs + trigger descriptions, then
loads one skill's full body only when a description matches the task. Skills
live ON the WordPress install, so per-site/per-project procedural knowledge
travels with the site instead of the client. Requires companion v2.13.0+
(capability `skills`).

### Added

- **5 skill tools** (`src/tools/companion/wp_skill_*.ts`):
  - `rolepod_wp_skill_catalog` — slug + trigger description only (no bodies)
  - `rolepod_wp_skill_get` — load one skill's full SKILL.md body on match
  - `rolepod_wp_skill_write` — create/update (`on_conflict` fail|replace|rename)
  - `rolepod_wp_skill_edit` — patch specific fields (iterative refinement)
  - `rolepod_wp_skill_delete` — trash (recoverable)
- **Bridge skill methods** (`src/companion/Bridge.ts`) — capability-gated;
  mutations carry a session token with one 401 re-handshake retry. Errors
  surface the companion's structured repair hints (`suggested_slug`,
  `warnings`) through `WplabError.meta` so the agent self-corrects without a
  second probe.
- Schemas in `src/schema/tools.ts`; tool count 119 → 124; smoke `tools/list`
  snapshot updated; unit coverage in `tests/unit/skillBridge.test.ts`.

### Notes

- `MIN_COMPANION_VERSION` stays at 2.1.0 — skill tools self-gate on the
  `skills` capability and fail with a clear upgrade hint against older
  companions, so non-skill operations keep working on 2.1+.

## [1.20.1] — 2026-05-28 — Hotfix: smoke test tool list

Pure test fix. No runtime change.

### Fixed

- `tests/smoke/mcp-handshake.test.ts` — expected `tools/list` snapshot
  was missing tools added across Phases 4–7 (`builder_detect`,
  `custom_init` + `custom_task_*` x5, `dir_ensure`,
  `elementor_html_audit`, `elementor_publish`,
  `elementor_template_apply`, `elementor_template_export`,
  `elementor_validate_data`, `elementor_widget_attribute`,
  `elementor_widget_schema`, `file_copy`, `file_list`,
  `file_write_batch`, `job_create`, `job_status`, `target_alias`).
  CI publish was blocked because the test failed in a fresh build
  even though tools shipped correctly. List rebuilt from
  `src/tools` source — 119 tools total.

## [1.20.0] — 2026-05-28 — Phase 7: catalog accuracy verification

MCP-only release. No companion bump. No tool changes — catalog-only update.

### Why

After Phase 6 shipped, user flagged that builder catalogs may have been
AI-generated from memory rather than verified against the actual page
builder runtime. Phase 7 closes the accuracy gap by re-deriving the
Elementor + Gutenberg catalogs from live runtime introspection and
tagging every builder catalog with explicit `verified_from` metadata
so future maintainers (and the AI itself) know which entries are
authoritative.

### Added — verified frontmatter metadata

Every builder catalog now carries YAML frontmatter declaring its
verification source, target version, audit tool, and date:

- `elementor.md` → `verified_from: live runtime introspection`,
  Elementor 4.1.1, 38/149 widgets audited via
  `Plugin::instance()->widgets_manager->get_widget_types()`.
- `gutenberg.md` → `verified_from: live runtime introspection`,
  WordPress 7.0, 102 core blocks + 3 plugin blocks audited via
  `WP_Block_Type_Registry::get_instance()`.
- `bricks.md`, `divi.md`, `oxygen.md` → `verified_from:
docs-and-ai-memory`, with explicit `to_verify:` instructions
  documenting how to swap to a live audit when a target with that
  builder is available.
- `patterns.md` → mixed verification status per builder, accurately
  reflected per-recipe.

### Changed — `skills/wp-edit-design/references/builders/elementor.md`

Full rewrite of the widget catalog section. Each widget block now
includes the verified control name + type + default + (where present)
the live enum options, pulled from the runtime. Universal /
promotional / responsive-hide controls (`*_pro`,
`display_conditions_*`, `scrolling_effects_*`, `mouse_effects_*`,
`sticky_*`, `hide_*`, `custom_attributes_*`, `custom_css_*`,
`animation_*`) are dropped from each widget table since they exist
on every widget and add no signal.

Added widget entries:

- `image-box`, `image-carousel`, `image-gallery`, `video`,
  `social-icons`, `alert`, `star-rating`, `menu-anchor`,
  `blockquote` — newly tabled with verified defaults.
- `nested-accordion` + `nested-tabs` — Elementor 4.x successors to
  legacy `accordion` / `tabs` (109 / 107 controls each). Catalog now
  recommends `nested-accordion` for new builds and notes the legacy
  `accordion` deprecation message.

Pro-only widgets explicitly noted as registered-but-unintrospectable
on free-tier (Pro injects controls at runtime): `animated-headline`,
`countdown`, `flip-box`, `form`, `nav-menu`, `portfolio`, `posts`,
`price-table`, `price-list`, `slides`, `testimonial-carousel`.

### Changed — `skills/wp-edit-design/references/builders/gutenberg.md`

Full rewrite of the block catalog section, indexed by Gutenberg
category (design, text, media, widgets, embed, theme, reusable) with
parent / ancestor relationships from the live registry. Notable
verified-live additions on the WP 7.0 target:

- `core/accordion` + `core/accordion-item` + `core/accordion-heading`
  - `core/accordion-panel` — native FAQ block family. P-003 FAQ
    recipe updated to prefer this on WP 7.0+ and fall back to
    `core/details` on WP 6.x.
- `core/math` — LaTeX rendering block.
- `core/breadcrumbs` — native breadcrumb trail (parallel to Yoast).
- `core/icon` — SVG icon block.
- `core/terms-query` + `core/term-template` + `core/term-name` +
  `core/term-description` + `core/term-count` — taxonomy term loop
  family (parallel to Query Loop for posts).
- `core/post-time-to-read` + `core/query-total` — Query Loop helpers.

Deprecation markers added for `core/post-author` and
`core/text-columns` per registry signals.

Verified third-party blocks on demo target (Contact Form 7 + Yoast
SEO) are listed in their own section so users know what to expect on
a typical install.

### Tooling note

Live introspection used existing wplab tools — no new code shipped.
Audit recipe:

```
rolepod_wp_cli_run target_id args=["eval", "$reg = WP_Block_Type_Registry::get_instance(); ..."]
rolepod_wp_cli_run target_id args=["eval", "$widgets = Plugin::instance()->widgets_manager->get_widget_types(); ..."]
```

To re-run on a future Elementor/WP version bump:

```
rolepod_wp_elementor_widget_schema target_id widget=<name>   # single widget
rolepod_wp_rest_request target_id /wp/v2/block-types          # all blocks
```

## [1.19.0] — 2026-05-28 — Phase 6: builder catalogs + native-first decision rule

MCP-only release. No companion bump.

### Why

Post-ship audit of the WalnutZtudio build found that ~78% of page
content was in HTML widgets — defeating the page builder's value
proposition (user can't visually edit). User pointed out the right
fix: per-builder element reference + pattern recipes so the AI knows
the universe of native widgets BEFORE reaching for HTML.

### Added — skill references (LLM-facing)

- **`skills/wp-edit-design/references/builders/elementor.md`** — full
  Elementor 4.x widget catalog: section/column primitives, every
  widget settings shape (heading, text-editor, button, image, icon,
  icon-box, counter, accordion, tabs, image-carousel, divider,
  spacer, html, shortcode, all Pro-only widgets flagged),
  CSS-to-widget-DOM map, gotchas (accordion icon Array bug,
  section `_css_classes` rendering quirk, counter screenshot
  limitation, native widget data-attr stripping), quick reference
  card mapping mockup patterns → native widgets.
- **`skills/wp-edit-design/references/builders/bricks.md`** — Bricks
  element catalog + pattern recipes.
- **`skills/wp-edit-design/references/builders/divi.md`** — Divi
  module catalog covering all `et_pb_*` shortcodes + pattern recipes.
- **`skills/wp-edit-design/references/builders/gutenberg.md`** —
  Gutenberg core block catalog + popular block library callouts
  (Ultimate Blocks, Kadence, GenerateBlocks, Spectra, Stackable).
- **`skills/wp-edit-design/references/builders/oxygen.md`** — Oxygen
  primitive catalog.
- **`skills/wp-edit-design/references/patterns.md`** — builder-
  agnostic mockup pattern → recipe map. 12 canonical patterns
  (P-001 hero, P-002 feature grid, P-003 FAQ accordion, P-004 stat
  row, P-005 pricing, P-006 process, P-007 portfolio, P-008
  marquee, P-009 terminal, P-010 big CTA, P-011 header, P-012
  footer) with builder-specific recipes + theme CSS pairs + rules
  - quirks.
- Updated **`skills/wp-edit-design/SKILL.md`** with the native-first
  Iron Rule (#1) + 9-step workflow including builder catalog read +
  validate_data + html_audit + publish gates.

### Added — tools

- **`rolepod_wp_builder_detect`** — auto-detect active page builder(s)
  on the target. Returns ranked list (Elementor / Bricks / Divi /
  Oxygen / Gutenberg) with version + capability flags + Pro state.
- **`rolepod_wp_elementor_html_audit`** — walk an Elementor page's
  `_elementor_data` tree, count widget types, compute HTML widget %,
  and inspect each HTML block's content for patterns that have a
  clean native widget replacement (single `<h1>` → heading widget,
  `<details>/<summary>` → accordion, `data-count` → counter, icon +
  heading + paragraph → icon-box). Returns `over_threshold:true` when
  HTML % > 30 (default) and per-widget suggestions. USE after every
  Elementor page build before publishing.

### Decision rule (now enforced via skill)

```
For each section of the mockup:
1. Pattern match in references/patterns.md? → Use the recipe exactly.
2. Otherwise look up a widget in the builder catalog that covers
   ≥70% of the visual.
3. Compose: section + column + that widget + _css_classes.
4. Only when no native widget fits → HTML widget per CARD
   (NOT per section).
5. After build:
   - rolepod_wp_elementor_validate_data → catches setting shape errors
   - rolepod_wp_elementor_html_audit → flags HTML overuse
   - rolepod_wp_elementor_publish → flush + warm-fetch
```

### Internal

- New libs: `src/lib/elementorHtmlAudit.ts` — heuristic content
  pattern matcher with conservative suggestions.
- New tools: `src/tools/composite/wp_builder_detect.ts`,
  `src/tools/composite/wp_elementor_html_audit.ts`.
- Schemas: `WpBuilderDetectInput/OutputSchema`,
  `WpElementorHtmlAuditInput/OutputSchema`.
- 12 new unit tests for the html-audit logic (counts native widgets
  correctly, flags single-heading / single-paragraph / single-button
  / FAQ details / data-count / icon-box content, does NOT flag
  custom marquee / terminal blocks, recurses inner sections).
- Total suite: 216 passing.
- TypeScript clean.

### How a page build looks NOW

1. AI calls `rolepod_wp_builder_detect` → "primary: elementor v4.1.1"
2. AI reads `skills/wp-edit-design/references/builders/elementor.md`
   and `references/patterns.md`
3. For each mockup section: pattern match → native recipe
4. Compose `_elementor_data` tree
5. `rolepod_wp_elementor_validate_data` — catches setting errors
6. `rolepod_wp_elementor_template_apply` — commits
7. `rolepod_wp_elementor_html_audit` — verifies HTML % within budget
8. `rolepod_wp_elementor_publish` — flushes + warm-fetches

User opens Elementor editor → sees real heading widgets, real button
widgets, real counter widgets — drag/drop and edit work as expected.

## [1.18.0] — 2026-05-28 — Rolepod Custom plugin scaffolder

MCP-only release. No companion bump required.

Introduces a **lazy-install bespoke-feature plugin** scaffolder. The
"Rolepod Custom" plugin is NOT shipped with rolepod-wp — it's written
to the target site on demand when the user asks the AI to add a
per-site feature.

### Architectural rationale

Per WordPress best-practice (separation of concerns + update safety):

- **rolepod-wp** = AI bridge core (REST endpoints, recovery, ledger).
  Updates independently. Stays minimal.
- **rolepod-custom** = per-site bespoke features. One plugin per
  client site. Lifecycle independent of rolepod-wp.
- Deactivating one doesn't kill the other. Updating one doesn't
  touch the other.

Within rolepod-custom, **each feature is a "task"** — an isolated
module under `inc/Modules/<PascalCase>Task.php` extending
`BaseTask`. Each task has its own:

- Stable id (kebab-case slug)
- Title + description (human-facing)
- Settings schema (auto-rendered as admin form)
- register_hooks() / uninstall() lifecycle
- Per-task enabled flag (toggle without code change)
- Submenu under "Rolepod Custom" → user discovers + edits the right
  task by name

User says "ปรับ contact snippet field email" → AI knows = task id
`contact-snippet` → grep `Modules/ContactSnippetTask.php` →
`rolepod_wp_custom_task_update` with the right id.

### Added

- **`rolepod_wp_custom_init`** — lazy-install the plugin via
  fs-write-batch (atomic, all 8 skeleton files or none) + activate via
  wp-cli. Idempotent.
- **`rolepod_wp_custom_task_scaffold`** — drops a new
  `<PascalCase>Task.php` module under `inc/Modules/`. Auto-runs init
  if the plugin isn't installed yet (auto_init default true). Refuses
  if a task with the same id already exists.
- **`rolepod_wp_custom_task_list`** — enumerates registered tasks by
  scanning the modules dir, parsing each for id/title/description,
  and reading each enabled flag from wp_options.
- **`rolepod_wp_custom_task_update`** — modifies an existing task in
  place. Pass only the fields you want to change; the rest is read
  from the current module file. Writes via fs-write-batch so the
  PHP-lint preflight runs on the new content (catches typos before
  commit).
- **`rolepod_wp_custom_task_toggle`** — flips
  `rolepod_custom_<task>_enabled` option. Task's
  `register_hooks()` calls `$this->is_enabled()` first, so hooks
  short-circuit instantly without a code reload.
- **`rolepod_wp_custom_task_remove`** — runs the task's `uninstall()`
  method via wp eval (deletes options, drops CPTs the task created,
  unhooks callbacks) then deletes the module file. Recorded in the
  Change Ledger with the old content snapshot for revert.

### Internal

- New libs:
  - `src/lib/rolepodCustomTemplates.ts` — PHP plugin file templates
    - per-task PHP code generator with proper quote escaping.
  - `src/lib/rolepodCustomOps.ts` — shared lazy-install + parse + run
    helpers used by all 6 tools.
- New tools under `src/tools/composite/wp_custom_*`.
- 8 unit tests for the template generator. Total suite: 204 passing.

### How a "small feature with settings" lands

1. User: "add a contact snippet shortcode `[rc_contact]` that shows
   our email + LINE id, editable from admin"
2. AI calls `rolepod_wp_custom_task_scaffold`:
   ```
   task_id: "contact-snippet"
   title: "Contact Snippet"
   description: "Renders contact info via [rc_contact] shortcode."
   settings: [{ key: "email", ... }, { key: "line", ... }]
   hooks_body: "if ( ! $this->is_enabled() ) return; add_shortcode('rc_contact', [$this, 'render']);"
   extra_methods: "public function render() { ... }"
   ```
3. The plugin auto-installs (first task → init runs).
4. Module file lands at `inc/Modules/ContactSnippetTask.php`.
5. User sees "Contact Snippet" submenu under "Rolepod Custom" in
   wp-admin with the auto-generated settings form.
6. User edits email later → form save → option updated.
7. Later user asks AI to "ปรับ contact snippet เพิ่ม field tel" →
   AI calls `rolepod_wp_custom_task_update` with the same task_id
   and merged settings array. Done.

## [1.17.1] — 2026-05-28 — Phase 4: post-ship polish findings (gaps #19 + #20)

Pairs with rolepod-wp 2.12.1. Two new gaps surfaced during the
WalnutZtudio polish session — both fixed.

### Fixed

- **`elementor_publish` now bumps theme asset filemtime (#20)** — added
  `bump_theme_assets` (default true). After flushing object cache, the
  tool walks every `*.css` / `*.js` under `wp-content/themes/<active>/
assets/` and `touch()`-es each one. The theme's enqueue layer derives
  the `?ver=` query string from `filemtime()`, so this forces the CDN
  / browser cache to miss on the next asset request — closes the class
  of bug where the AI edits `walnut.css` but the served body keeps
  showing the old rules because Varnish caches the old `?ver` query
  string.

### Compat note (paired companion fix)

- **Section `_css_classes` now renders on the public `<section>` tag (#19)** —
  the fix ships inside rolepod-wp 2.12.1's new `ElementorCompat` class.
  See the companion changelog for the detection trail and why a
  `the_content` filter is the correct workaround instead of the
  `elementor/frontend/before_render` hook (which Elementor 4.x fires
  only for widgets, not sections).

### Internal

- `WpElementorPublishInputSchema` adds `bump_theme_assets: boolean`
  (default true).
- `WpElementorPublishOutputSchema` adds `theme_assets_bumped: { ok,
files_touched, theme_dir }`.
- `bumpThemeAssets()` runs via `wp eval` + RecursiveIteratorIterator
  so the existing companion fs-list endpoint doesn't have to be live
  for this to work.

### Field-tested

After hot-deploying companion 2.12.1's ElementorCompat to the
WalnutZtudio Hostinger demo:

- Section tag now shows: `class="elementor-section ... wnz-sec wnz-hero"`
- Hero h1 wraps to mockup's 3-line layout (was 2 lines because
  `.wnz-hero` selector never matched any DOM node before this fix)
- `elementor_publish` reports `theme_assets_bumped.files_touched: 2`
  on a build that touched walnut.css + walnut.js.

### Gap status

20/20 gaps closed. The wplab capability-gap audit is now an empty
historical ledger.

## [1.17.0] — 2026-05-28 — Phase 3.2 final: 18/18 gaps closed

Pairs with rolepod-wp 2.12.0. Closes every remaining item from
`brief/wplab-capability-gaps.md`. The "what's left" column of that
document is now empty.

### Added

- **`rolepod_wp_elementor_widget_attribute`** (closes #6 + #18) —
  Persist arbitrary `data-*` attributes on Elementor widgets by element
  id. Stored in `_rolepod_widget_attrs` post meta; companion emits a
  JSON+JS bridge in `wp_footer` that the theme JS reads to apply
  `data-scramble` / `data-magnet` / `data-tilt` / `data-typer` style
  attrs before effect init scans run. Closes both "Elementor sanitizer
  strips raw HTML widget attrs" (#6) and "no data-attr binding on native
  widgets" (#18).
- **`rolepod_wp_elementor_template_apply`** (closes #10) — Companion
  counterpart to `template_export`. Takes a sections array, optionally
  runs find/replace string substitutions, regenerates element ids so
  the clone doesn't collide with the source, then writes the result
  - Elementor flags to a target post. Refuses to overwrite when target
    has data unless overwrite=true.
- **`wp-content/private/` write zone** (closes #13) — All four scoped
  endpoints (FsWrite, FsWriteBatch, DirEnsure, FsCopy) now accept
  paths under `wp-content/private/`. The companion auto-installs a
  `Require all denied` `.htaccess` in that dir on first use so dev
  scratch files are never publicly readable over HTTP.
- **`rolepod_wp_job_create` + `rolepod_wp_job_status`** (closes #15) —
  Fire-and-poll wp-cli runner. Spawns wp-cli detached via popen,
  captures pid, stores a transient-backed job record. Pair with a
  polling loop. Useful for db migrations, theme switches with full
  cache rebuild, media regeneration — anything past the synchronous
  wp-cli 120s hard cap. Returns 503 EXEC_DISABLED if the host disables
  `exec()`.
- **Elementor Accordion CSS reset documented** (closes #17) — Added
  the working `display:none / .elementor-active { display:block }`
  reset to `brief/walnutztudio-best-practice-notes.md` for future
  programmatic Elementor builds.

### Final gap-status

All 18 gaps shipped. The `brief/wplab-capability-gaps.md` table marks
every row as shipped.

### Internal

- 4 new MCP tools, 4 new schemas, 4 new Bridge methods, 4 new companion
  PHP endpoints.
- Companion `rolepod-wp.php` registers the 4 endpoints + the
  `wp_footer` bridge emitter + the `.htaccess` auto-installer.
- Test suite: 196 passing (unchanged — new tools tested live against
  the demo target rather than mocked, per the policy of "wire-level
  contracts are tested by exercise, not by mock").

## [1.16.0] — 2026-05-28 — Phase 3.1: publish pipeline + widget validator + block-theme detector

MCP-only release. No companion bump required.

### Added

- **`rolepod_wp_elementor_publish`** (closes gap #8) — collapses the
  three-step publish dance into one tool call. Resolves the post's
  permalink, runs `wp elementor flush-css`, `wp cache flush`, and
  (optional) fetches the permalink with a cache-busting query param so
  Varnish pre-caches a hot copy. Returns per-phase status.
- **`rolepod_wp_elementor_validate_data`** (closes gap #16) — walks an
  `_elementor_data` JSON tree, fetches each widget's controls schema via
  the companion's `/wplab/v1/elementor/widget-schema` endpoint
  (Phase 2), and reports every setting key with the wrong shape OR not
  declared on that widget. Catches the WalnutZtudio class of bug where
  passing `icon: { value, library }` to a legacy `icon` control rendered
  `<i class="... Array">` instead of a Font Awesome glyph. Schema is
  fetched once per widget type per call.
- **Block-theme `wp_body_open` risk warning** (closes gap #11) — adds a
  detector to `connect_rest`'s warning sweep: when the active theme
  reports `is_block_theme: true` (FSE/twentytwentyfive-class), surface a
  warning that `wp_body_open()` may not fire in FSE templates, so
  anything injected via that hook (ambient layers, overlays, pixels)
  can silently fail. Includes the exact fix options.

### Internal

- New libs: `src/lib/elementorValidator.ts`.
- New tools: `src/tools/composite/wp_elementor_publish.ts`,
  `src/tools/composite/wp_elementor_validate_data.ts`.
- Extended `src/lib/connectWarnings.ts` with the block-theme detector.
- New schemas: `WpElementorPublishInputSchema/OutputSchema`,
  `WpElementorValidateDataInputSchema/OutputSchema`.
- Unit tests: `elementorValidator.test.ts` (6 cases including the
  WalnutZtudio bug reproduction), updated `connectWarnings.test.ts`
  with 3 block-theme cases. Total suite: 196 passing.

### Gap status

11 + 3 = 14 of 18 gaps closed (78%), 1 partial. Remaining (Phase 3.2):
#6 register_inline_attr, #10 template_apply, #13 wp-content/private/,
#15 async job runner, #17 accordion CSS reset, #18 widget_attribute
data-attr binding. Several share a "widget rehydration" surface and
will land together.

## [1.15.0] — 2026-05-28 — Companion-coupled Phase 2 (atomic batch + fs primitives + Elementor introspection)

Pairs with rolepod-wp 2.11.0. Closes the remaining items from
`brief/wplab-capability-gaps.md` that needed companion-side endpoints.
Six new tools. Each tool description carries the `Requires rolepod-wp
companion v2.11+` floor — the global `MIN_COMPANION_VERSION` is
unchanged so older deployments still talk to the MCP server fine for
the unrelated tool surface.

### Added

- **`rolepod_wp_file_write_batch`** (gap #1, #14) — atomic multi-file
  write with cross-file `require`/`include` chain resolution. Stage →
  preflight (`php -l` per entry + virtual-FS require check) → commit
  via per-file `rename()`. Failure rolls every entry back from backups.
  The MCP tool wraps the companion endpoint, records one Change Ledger
  row per file (so the existing revert UI can undo individual entries),
  and tops out at 100 entries per call.
- **`rolepod_wp_dir_ensure`** (gap #3) — `mkdir -p` for scoped paths.
- **`rolepod_wp_file_copy`** (gap #3) — file → file copy within scoped
  paths. Auto-creates the destination's parent dir. Refuses to
  overwrite unless `overwrite=true`.
- **`rolepod_wp_file_list`** (gap #12) — recursive listing with type,
  size, mtime. Read-only; works on production.
- **`rolepod_wp_elementor_widget_schema`** (gap #7) — fetches the
  Elementor widget's controls registry so agents can build
  `_elementor_data` JSON against the live shape instead of
  reverse-engineering it. Pass `widget` for one widget; omit for the
  full registered-widget list.
- **`rolepod_wp_elementor_template_export`** (gap #7, #10) — exports an
  existing Elementor page's `_elementor_data` plus the deduplicated
  list of widget types it uses. Lets agents clone editor-built pages
  programmatically.

### Internal

- New tools live under `src/tools/companion/` (5 files).
- New Bridge methods: `CompanionBridge.fileWriteBatch`,
  `.dirEnsure`, `.fileCopy`, `.fileList`,
  `.elementorWidgetSchema`, `.elementorTemplateExport`.
- New schemas: `WpFileWriteBatchInputSchema/OutputSchema`,
  `WpDirEnsureInputSchema/OutputSchema`,
  `WpFileCopyInputSchema/OutputSchema`,
  `WpFileListInputSchema/OutputSchema`,
  `WpElementorWidgetSchemaInputSchema/OutputSchema`,
  `WpElementorTemplateExportInputSchema/OutputSchema`.

### Verified live

Tested end-to-end against the WalnutZtudio demo target after companion
2.11.0 upload:

- `fs-write-batch` correctly REJECTS a one-entry batch that requires a
  missing `inc/setup.php` (the WalnutZtudio incident reproduced) with
  `error_code: REQUIRE_CHAIN_BROKEN` + `missing_requires` array.
- `fs-write-batch` ACCEPTS the same payload when `inc/setup.php` is
  added as a second entry — both files commit atomically.
- `dir-ensure` creates nested dirs idempotently.
- `fs-copy` copies between scoped paths.
- `fs-list` returns recursive tree of `walnutztudio-child/inc/`.
- `elementor/widget-schema` reports the Counter widget has 179
  controls and surfaces the canonical control names.
- `elementor/template-export` returns the Home page's 9 sections +
  widget-type list `[counter, heading, html, text-editor]`.

### Status table

`brief/wplab-capability-gaps.md` updated. Phase 1 + Phase 2 shipped:
gaps #1, #2, #3, #4, #5, #7, #9, #12, #14 closed. Phase 3 items
remain (#6, #8, #10, #11, #13, #15, #16, #17, #18).

## [1.14.0] — 2026-05-28 — Build-resilience hardening (MCP-only Phase 1)

Closes the four MCP-only items from `brief/wplab-capability-gaps.md`
identified during the WalnutZtudio Elementor rebuild. All four are
additive — no companion plugin update required.

### Added

- **`rolepod_wp_target_alias` tool + persistent alias store** (closes gap
  #4). Aliases live at `~/.config/rolepod-wplab/aliases.json` (mode 0600).
  Any tool now accepts `target_id: "@<alias>"` — the dispatcher resolves
  to a live `tgt_<hex>` session transparently and **auto-reconnects on
  `TARGET_NOT_FOUND`** (idle-closed mid-build). Actions: `set` / `list`
  / `rm` / `resolve`. Replaces the "reconnect every 10 min" loop that
  bit every long Elementor build.
- **`require_once` chain pre-flight on bootstrap files** (closes gap #2
  for the runtime-fatal class of bugs that `php -l` cannot catch). When
  writing `functions.php`, `header.php`, `footer.php`,
  `mu-plugins/*.php`, or `wp-config.php`, the tool now scans the new
  content for `require` / `include` statements with literal `*.php`
  paths, resolves them relative to the source file, and **refuses the
  write** if any required file would be missing on the target. Catches
  the exact ordering bug that fatal'd the WalnutZtudio refactor.
- **Auto-chain `recovery_status` on HTTP 5xx** (closes gap #5). When
  any tool call hits a 5xx from the companion, the dispatcher
  transparently calls the guardian's `recovery_status`, extracts
  `lastFatal`, and enriches the error message with file/line/message of
  the PHP fatal that caused the 500. No more "HTTP 500" with zero
  context.
- **HTTPS siteurl mismatch warning on `connect_rest`** (closes gap #9).
  When you connect to a site over `https://` but its stored `siteurl`
  / `home` options are `http://`, the connect response now carries a
  warning with the exact `wp option update` commands to fix it.
  Prevents the Elementor mixed-content failure (browser blocks
  `http://…/wp-content/uploads/elementor/css/base-desktop.css`).

### Changed

- `TargetIdSchema` now accepts either `tgt_<8+ hex>` (live session) **or**
  `@<alias>` (persistent alias). All existing tools transparently accept
  the new form.
- `ConnectRestOutputSchema` adds an optional `warnings: [{ code,
message, suggested_fix }]` field. Empty when clean.

### Internal

- New libs: `src/lib/targetAliases.ts`, `src/lib/aliasResolver.ts`,
  `src/lib/requireChain.ts`, `src/lib/recoveryAutoChain.ts`,
  `src/lib/connectWarnings.ts`.
- New tool: `src/tools/atomic/wp_target_alias.ts`.
- New unit tests: `requireChain`, `targetAliases`, `connectWarnings`.
- Test suite: 187 passing, 0 failing.

### Pending (Phase 2 / Phase 3 — see `brief/wplab-capability-gaps.md`)

These require companion-side endpoints (rolepod-wp PR + version bump)
and ship in a coordinated release:

- `rolepod_wp_file_write_batch` w/ atomic staging (gap #1)
- `rolepod_wp_dir_ensure`, `rolepod_wp_file_copy`, `rolepod_wp_file_list`
  (gaps #3, #12)
- `rolepod_wp_elementor_widget_schema`,
  `rolepod_wp_elementor_template_export` (gap #7)

Phase 3 items (#6, #8, #10, #11, #13, #15, #16, #17, #18) deferred —
several depend on Phase 2 deliverables.

## [1.13.0] — 2026-05-28 — Marker-only detection + manifest wiring (Protocol v1 fix-up)

v1.12.0 shipped Extension Protocol v1 frontmatter on 8 skills plus a
`rolepodEvidence.ts` helper. Two gaps made combined mode non-functional
end-to-end and are fixed here.

### Fixed

- **Detection mechanism swapped from env var to filesystem marker.**
  v1.12.0 read `process.env.ROLEPOD_PARENT === "1"`. Claude Code's
  SessionStart hooks run in a subprocess and their env vars cannot
  propagate to Claude's Bash / MCP tool calls — the env-var path never
  fired in practice. v1.13.0 reads `<git-root>/.rolepod/parent-active`
  (UTF-8 single line, contents `v1`). The parent v2.7 SessionStart hook
  writes this file; its Stop hook removes it. Cross-subprocess, durable,
  one source of truth.
- **Evidence path now resolves relative to the git root.** Previously
  `process.cwd()`-relative — a skill invoked from a subdir wrote
  evidence to that subdir's `.rolepod/` instead of the project's.
  `detectRolepodParent()` now shells out to
  `git rev-parse --show-toplevel` once and uses that for the evidence
  dir.

### Added — manifest emission on the 2 phase-aligned tools

`writeManifest()` existed in v1.12.0 but had zero call sites. Wired in:

- **`rolepod_wp_health_check`** (verify phase) — writes `health.json`
  - `manifest.json` to
    `<git-root>/.rolepod/evidence/<ts>-rolepod-wplab-wp-health-check/`
    when the marker is present. Status is `fail` when `db_ok` or
    `wp_cli_ok` is false, `warn` when warnings are non-empty otherwise,
    `pass` otherwise. Evidence emission failure is caught and
    surfaced as a stderr warning — it never breaks the tool's
    primary contract.
- **`rolepod_wp_changes_query`** (review phase) — writes `diff.json`
  - `manifest.json`. Status is `warn` when any change in the result
    set is non-reversible, `pass` otherwise. Same failure-mode
    treatment as health-check.

### Changed — 8 SKILL.md mode-selection blocks

All 8 phase-flavored skill markdown files (`wp-diagnose`,
`wp-health-check`, `wp-changes`, `wp-full`, `wp-scaffold`,
`wp-edit-{design,plugin,theme}`) swap the env-var check for the
marker-file check:

```bash
GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || GIT_ROOT="$PWD"
if [ -f "$GIT_ROOT/.rolepod/parent-active" ]; then MODE=with-rolepod; else MODE=standalone; fi
```

Skill bodies for `wp-health-check` and `wp-changes` no longer document
manual `writeManifest` calls — the MCP tools handle emission
automatically when the marker is present.

### Removed

- `isUnderRolepodParent()` and `rolepodProtocolVersion()` (env-var
  helpers). Replaced by `detectRolepodParent()` which returns a
  `ParentState` record (`{ active, protocol, gitRoot }`).

### Tests

Existing `rolepodEvidence.test.ts` rewritten for the marker-based API
(9 tests, all passing — covers marker present/absent, protocol
mismatch warning, env-var ignored, non-git fallback, subdir
invocation, manifest schema, timestamp format).

### Unchanged

- All 14 skills work standalone identically to v1.12.0.
- 89+ MCP tools — only 2 enriched with conditional manifest emission;
  the other 87 are unchanged.
- Skill names — no rename, no removal.

## [1.12.0] — 2026-05-27 — Rolepod Extension Protocol v1 (forward-compatible child mode)

Implements the spec from `brief/handoff-wplab-v1.9.md` shipped here as
v1.12 (npm version drift since the brief was written for 1.8.0 → 1.9.0
and we've since shipped 1.9–1.11). Plugin-manifest version
(`.claude-plugin/plugin.json`) catches up from 1.8.0 → 1.12.0.

### Why

The sibling `rolepod` parent plugin (v2.7+) will introduce Extension
Protocol v1 — it sets `ROLEPOD_PARENT=1` + `ROLEPOD_PROTOCOL=v1` via
its SessionStart hook so children can discover each other and divide
responsibility. Without protocol support, wplab's phase-flavored
skills (`wp-diagnose`, `wp-health-check`, `wp-changes`, etc.) overlap
with the parent's own phase skills (`debug-issue`, `check-work`,
`review-code`) and create user confusion about which to call.

### Added — Extension Protocol v1 support

- **`src/lib/rolepodEvidence.ts`** — new helper
  - `isUnderRolepodParent()` — env-var detection
  - `resolveEvidenceDir(skill, ts)` — picks `.rolepod-wplab/artifacts/`
    standalone vs `.rolepod/evidence/<ts>-rolepod-wplab-<skill>/`
    with-parent
  - `writeManifest(dir, input)` — emits the `manifest.json` schema
    (`protocol: "rolepod/v1"`, plugin, skill, phase, status, summary,
    timestamps, artifacts, metadata)
  - `makeRunTimestamp()` — ISO-compact UTC stamp suitable for dir names
- **`tests/unit/rolepodEvidence.test.ts`** — 7 tests covering env
  detection, dir paths in both modes, manifest schema, snake_case
  serialization, timestamp format. All passing.

### Changed — 8 skill frontmatter blocks

Added `mode:` declaration + "Mode selection" body block to:

- `wp-diagnose` — standalone debug entry → with-parent evidence provider
  for `rolepod:debug-issue`
- `wp-health-check` — standalone smoke test → with-parent snapshot
  provider for `rolepod:check-work` (writes manifest.json)
- `wp-changes` — standalone change audit → with-parent diff summary
  provider for `rolepod:review-code` (writes manifest.json)
- `wp-full` — standalone tour → with-parent flat tool inventory for
  `rolepod:using-rolepod` (body branches on mode)
- `wp-scaffold` — standalone scaffold + guide → with-parent files-only
  primitive for `rolepod:implement-plan`
- `wp-edit-design`, `wp-edit-plugin`, `wp-edit-theme` — standalone
  edit + guide + verify → with-parent diff-only primitives for
  `rolepod:implement-plan`

### Changed — README

New "Standalone vs Combined" section explaining both modes, the
6-skill mode summary table, install combos, and evidence path
conventions.

### Forward-compat posture

Parent rolepod is currently at v2.6.5. Until parent ships v2.7 with
the SessionStart hook that sets `ROLEPOD_PARENT=1`, the `with-rolepod`
mode path stays dormant — standalone behavior is unchanged for every
existing user. When the parent ships, no further action is required
on the wplab side.

### Unchanged

- 89+ MCP tools — zero surface change
- 6 other skills (`wp-connect`, `wp-pair-setup`, `wp-introspect`,
  `wp-execute-php`, `wp-content`, `wp-migrate`) — already pure tools,
  no mode logic needed
- Standalone artifact path — `.rolepod-wplab/artifacts/` still used
  when no parent

## [1.11.11] — 2026-05-27 — Root cause: `replacePostMeta` shared helper + opt-in RestTarget+companion E2E smoke test

Round 6 produced 7 production bugs across copy-pasted post-meta-replace
helpers in elementor / bricks / oxygen / rankmath / divi / wpml / forms
adapters. Each adapter reinvented the same sequence (verify shell, read
prior meta for backup, write tmp file, run `wp post meta update
--from-file` or `wp eval update_post_meta`, error format) and drifted
independently. v1.11.6 → 1.11.10 patched the symptoms; this release
patches the root cause.

### Added — `src/adapters/_shared/replacePostMeta.ts`

```ts
await replacePostMeta(target, postId, "_elementor_data", widgetTree, {
  backupPrefix: "elementor",
  serialization: "json", // or "raw" for plain string payloads
});
```

Centralizes:

- Shell-capable-OR-RestTarget+companion gate (was 9 duplicated checks).
- Backup-read via `wp post meta get`, with stamped backup file under
  `wp-content/uploads/wplab-backups/`.
- Tmp file payload write under `wp-content/uploads/wplab-tmp/`, with
  `absolutePath || relPath` fallback for older companions (≤2.7.2).
- `wp eval update_post_meta(id, key, json_decode(file_get_contents(path), true))`
  invocation (`--from-file` is NOT a valid flag on `post meta update`).
- Error normalization — `stderr` then `stdout` (companion bridges merge).

### Refactored — 4 adapters now thin wrappers

- `elementorWrite.updatePageData`
- `bricksWrite.updatePageContent / updateHeaderContent / updateFooterContent`
- `oxygenWrite.updatePageShortcodes`
- `rankmathWrite.setPostMeta` (the `noindex` → `rank_math_robots` path)

Net diff: ~140 lines of duplicated logic → ~20 lines of helper calls.
Future R6-style bugs in this pattern fix in one place.

### Added — `tests/smoke/rest-companion-e2e.test.ts`

Opt-in E2E test that runs the full RestTarget + companion path against a
real WordPress install. Skipped by default; enabled when these env vars
are set:

- `WPLAB_TEST_URL` HTTPS URL of a throwaway WP install
- `WPLAB_TEST_USERNAME` WP user login
- `WPLAB_TEST_APP_PASSWORD` WP Application Password

The test installs Elementor (idempotent), creates a draft page, calls
`elementorWrite.updatePageData`, verifies the meta persisted, and
cleans up. Catches the entire R6 bug class — shell-only gates, missing
flags, schema drift, companion response shape mismatches — before ship.

### Unaffected

- Adapters that don't use the temp-file pattern (yoast, jetengine,
  metabox, pods, wpml, woo, acf, divi, forms) — they call `wp post meta
update <id> <key> <value>` directly with positional args and are fine.
- Unit tests still pass (147/147).

## [1.11.10] — 2026-05-27 — `wp eval` path falls back to relative when companion fs-write omits absolute_path

R6-7 v1.11.9 fix shipped but caught a second bug on RestTarget+companion:
companion's `/fs-write` response shape lacks `absolute_path`. CompanionBridge
defaults missing field to `""` → adapters built `wp eval file_get_contents("")`
→ `PHP Fatal error: Uncaught ValueError: Path must not be empty`.

wp-cli runs with `getcwd() === ABSPATH`, so a relative path resolves
correctly. Each affected adapter (elementor/bricks/oxygen/rankmath) now
prefers `tmpWrite.absolutePath` when present and falls back to the
relative tmp path otherwise.

```ts
const filePath = tmpWrite.absolutePath || tmpRel;
```

Companion-side patch (add `absolute_path` to `/fs-write` response) is
nice-to-have for callers that need the absolute path explicitly, but is
no longer required for this code path.

## [1.11.9] — 2026-05-27 — Replace `wp post meta update --from-file` with `wp eval` (elementor/bricks/oxygen/rankmath)

Round 6 page-builder swap test (post-v1.11.8 client restart) revealed
the shell-only gate fix unblocked the adapters but a different bug
fired next: `wp post meta update` does NOT support `--from-file` —
that flag is exclusive to a handful of subcommands (`option update`,
`site meta update`, etc.) and yields `Error: Parameter errors: unknown
--from-file parameter` on post meta.

Root cause: 4 adapters dropped a JSON payload to a temp file under
`wp-content/uploads/wplab-tmp/` and called `wp post meta update <id>
<key> --from-file=<path>`. The Target.wpCli wrapper has no stdin
pump, so the indirect-via-file approach was the only available path
for large/JSON values. But the flag never existed for that subcommand.

### Fixed — Route via `wp eval` reading the temp file + update_post_meta

```ts
const phpScript = `update_post_meta(${postId}, ${JSON.stringify(metaKey)}, json_decode(file_get_contents(${JSON.stringify(path)}), true));`;
await target.wpCli(["eval", phpScript], { allowDestructive: true });
```

Applied to:

- `elementor_write` (\_elementor_data, JSON-decoded)
- `bricks_write` (\_bricks_page_content_2 / \_bricks_header_content / \_bricks_footer_content, JSON-decoded)
- `oxygen_write` (ct_builder_shortcodes, raw string — no json_decode)
- `rankmath_write` (rank_math_robots, JSON-decoded)

Keeps the temp file + backup logic intact (`update_post_meta` runs
under WordPress, so hooks/sanitization still fire). Removes the
broken CLI flag dependency.

## [1.11.8] — 2026-05-27 — 6 more adapter writes accept RestTarget+companion (bricks/oxygen/divi/wpml/elementor/forms)

Round 6 page-builder swap test surfaced more adapters with the same stale
shell-only gate that yoast/rankmath had. All 6 patched to accept
`target.kind === "rest" && target.companion?.enabled`:

- elementor_write
- bricks_write
- oxygen_write
- divi_write
- wpml_write
- forms_write

Same one-liner change as v1.11.7 — extend the kind check, leave the rest
of the path (target.wpCli + scoped fileWrite) untouched. Companion v2.7.x
already proxies wp-cli and file-write through REST; the adapter just had
to stop refusing the connection kind.

After this patch all 13 `_write` adapters work on RestTarget+companion.

## [1.11.7] — 2026-05-27 — JsonObjectSchema + yoast/rankmath write accept RestTarget+companion

Round 6 Batch 4 (adapter writes) surfaced 2 issues fixed here.

### Fixed — `woo_write fields` object-as-string preprocess (R6-3)

MCP clients sometimes JSON-stringify nested object parameters as they
travel through the transport. `woo_write { fields: { regular_price: "650" } }`
arrived at the tool handler as a string `'{"regular_price":"650"}'`,
which `z.record(z.string(), z.unknown())` rejected with
`"Expected object, received string"`.

New `JsonObjectSchema` preprocess attempts `JSON.parse` if the input is
a string, then validates against `z.record(z.string(), z.unknown())`.
Real objects pass through unchanged; pre-stringified ones get parsed.
Used by `woo_write.fields`; can be applied to any future schema field
that accepts a JSON object.

### Fixed — `yoast_write` + `rankmath_write` work on RestTarget+companion (R6-4)

Both adapters threw `"requires a shell-capable target. RestTarget needs
companion v0.2 fs/exec"` for RestTarget — stale assumption from v0.1.
Companion v2.7.x has wp-cli proxy via `Bridge.wpCli()`, and `target.wpCli`
already routes through it for RestTarget. The adapter just needed to
stop refusing.

Fix: relax the kind gate to also accept `target.kind === "rest" &&
target.companion?.enabled`. No code-path change otherwise; the existing
`target.wpCli(["post", "meta", "update", ...])` call already works via
companion route.

### Known still-untested

- `forms_write` — Gravity Forms required, none installed
- `elementor_write` — requires Elementor-built page with widget tree
- `jetengine_write` / `metabox_write` / `pods_write` — plugins not
  installed on demo (free versions exist; deferred)
- `acf_write` — free ACF doesn't expose `/wp-json/acf/v3/` REST namespace
  (Pro-only). Detection returns `ADAPTER_NOT_DETECTED` correctly on free.

## [1.11.6] — 2026-05-27 — `pair_token` regex matches companion-issued `rolepod_wp_pair_` prefix

Round 6 stress test of `wp_pair` flow exposed schema regex out-of-date.
Companion `/wp-json/wplab/v1/pair/generate` issues tokens prefixed
`rolepod_wp_pair_<48hex>` (since v2.0 rename) but `PairInputSchema`
regex still required legacy `wplab_pair_<48hex>` → tool rejected real
tokens with "must match the companion-issued format" error.

Fix: regex updated to `/^rolepod_wp_pair_[a-f0-9]{48}$/` + error
message aligned. Companion already issues correct format; this is
pure MCP-side schema drift cleanup.

## [1.11.5] — 2026-05-27 — Recovery tools no longer gate on companion handshake

Round 5 stress test exposed: all 7 `rolepod_wp_recovery_*` MCP tools were
routed through `bridgeFor(target)` which calls `handshake()`. When WP
WSODs (the exact scenario the recovery tools exist for), main companion's
`/wplab/v1/handshake` returns 500 → `bridgeFor` throws
`COMPANION_UNAVAILABLE` → user cannot recover via MCP.

The guardian's `/wplab-recovery/v1/*` endpoints authenticate via WP-native
Application Password (no session token), specifically so they survive
main-plugin death. Bridge methods for the recovery namespace don't need
the handshake at all.

### Fix — `bridgeForRecovery(target)` helper

New helper builds a `CompanionBridge` instance without calling
`handshake()`. All 7 recovery tools (`wp_recovery_status`,
`wp_recovery_disable_plugin`, `wp_recovery_disable_file`,
`wp_recovery_restore_file`, `wp_recovery_restore_snapshot`,
`wp_recovery_list_changes`, `wp_recovery_safe_mode`) switched from
`bridgeFor(target)` to `bridgeForRecovery(target)`.

### Verified live

Demo coffee-bloom theme functions.php fatal → main `/wplab/v1/handshake`
500 → guardian `/wplab-recovery/v1/status` 200 (via curl, proves
companion-side path). After v1.11.5 publish, MCP recovery tools will work
in the same scenario.

### No companion change

`/wplab-recovery/v1/*` endpoints already work without session token (App
Password auth). Pure MCP-side routing fix.

## [1.11.4] — 2026-05-27 — broken_images via /db-query + yoast/rankmath postMeta via /db-query

Continues Round 4 closeout (v1.11.3 already shipped ConfirmTrueSchema sweep).

### Fixed — `diagnose broken_images` scope routes via `/db-query`

Last diagnose scope still using `wp db query` directly. Routed via
`bridge.dbQuery` for RestTarget+companion (same pattern as
slow_queries/large_options in v1.11.0). Falls back to wp-cli for
shell-capable targets without companion. SQL contains embedded single
quotes (`'<img'`, `'src="'`, `'%<img%'`) that escapeshellarg mangled.

### Fixed — `yoast_read` + `rankmath_read` postMeta on RestTarget

Both adapter `postMeta` paths returned only `{ post_id }` on RestTarget
because:

- Yoast meta keys (`_yoast_wpseo_*`) aren't `register_meta(show_in_rest)`,
  so `/wp/v2/posts/<id>?_fields=meta` excludes them.
- Rank Math meta keys (`rank_math_*`) — same.

Fix: when `target.kind === "rest" && companion?.enabled`, read raw
postmeta via parameterised db-query:

```ts
bridge.dbQuery(
  "SELECT meta_key, meta_value FROM {prefix}postmeta WHERE post_id = %d AND meta_key IN (...)",
  [postId],
);
```

Verified live: pages with `_yoast_wpseo_focuskw` + `_yoast_wpseo_metadesc`
set via `wp_seo_set` (Round 2) now read back correctly through
`wp_yoast_read`.

### No companion change

Reuses `/db-query` shipped in companion v2.7.1. MIN_COMPANION_VERSION
unchanged.

## [1.11.3] — 2026-05-27 — Patch: ConfirmTrueSchema applied to all 17 `allow_destructive` fields

Round 4 cross-tool stress test surfaced 4 issues. All fixed.

### Fixed — ConfirmTrueSchema applied to all 17 `allow_destructive` fields

v1.10.0 introduced `ConfirmTrueSchema` (`true | "true"` union → boolean) for
`confirm` parameters on `execute_php` + `mail_test`. Round 4 caught that
**17 other schemas** still used raw `z.literal(true)` for
`allow_destructive`, so `scaffold_theme`, `scaffold_plugin`,
`scaffold_block`, `db_query` write paths, and all 11 page builder + SEO
adapter `_write` tools rejected `"true"` from MCP clients that
JSON-stringify booleans.

Fix: bulk-replace all `allow_destructive: z.literal(true)` →
`allow_destructive: ConfirmTrueSchema`. Moved `ConfirmTrueSchema`
declaration to top of schemas file (used by schemas earlier than its
previous position → TS2454).

### Fixed — `diagnose broken_images` now routes via `/db-query`

`brokenImagesProbe` was the only diagnose scope still using `wp db query`
directly. Stress test on RestTarget showed `"image scan deferred (db
query unsupported)"` because the SQL contained embedded single quotes
(`'<img'`, `'src="'`, `'%<img%'`) that escapeshellarg mangled. Same fix
as v1.11.0 slow_queries/large_options: route via `bridge.dbQuery()` for
RestTarget + companion.

### Fixed — `yoast_read` + `rankmath_read` postMeta on RestTarget

Both adapter `postMeta` paths returned only `{ post_id }` on RestTarget
because:

- Yoast meta keys (`_yoast_wpseo_*`) aren't `register_meta` show_in_rest,
  so `/wp/v2/posts/<id>?_fields=meta` excludes them.
- Rank Math meta keys (`rank_math_*`) — same.
- Previous fallback to `yoast_head_json` was incomplete.

Fix: when target.kind === "rest" && companion enabled, read raw postmeta
via `bridge.dbQuery("SELECT meta_key, meta_value FROM {prefix}postmeta
WHERE post_id = %d AND meta_key IN (...)", [postId])` — gets the real
values directly from DB. Falls back to existing wp-cli + REST shim for
non-companion targets.

Verified live: pages with `_yoast_wpseo_focuskw` + `_yoast_wpseo_metadesc`
set via `wp_seo_set` now read back correctly through `wp_yoast_read`.

### Pairs with companion v2.7.1 (unchanged)

No companion-side change required — these routes already use the
existing `/db-query` endpoint shipped in v2.7.1.

## [1.11.2] — 2026-05-27 — Patch: diagnose large_options autoload filter (WP 6.6+ values)

WP 6.6+ extended `wp_options.autoload` from `yes/no` to `yes/no/on/off/auto`.
Existing installs typically have `on` (autoloaded) + `off` (not) + `auto`
(new behavior). The hard-coded `WHERE autoload='yes'` in
`largeOptionsProbe` matched zero rows on Hostinger demo. Fix: filter
`autoload IN ('yes','on','auto')` to cover both legacy + current values.

Verified on demo: 269 `off` + 205 `on` + 94 `auto` + 1 `yes` rows. New
filter catches the actual autoload-enabled rows.

## [1.11.1] — 2026-05-27 — Patch: profile enum schema missing `strict` + `personal`

v1.11.0 added `profile.active` field to `connect_rest` response but the
zod enum was `["default", "power"]` — only matches MCP-tool wording, not
the actual `ProfileSchema` values from `profile/load.ts` which are
`["strict", "personal", "power"]`. `connect_rest` failed validation with
`"Expected 'default' | 'power', received 'strict'"` because new installs
default to `strict`.

Fix: align enum to the canonical `ProfileSchema` — `strict | personal | power`.
Description updated to explain what each tier unlocks.

No behavior change in tools. Pure schema fix.

## [1.11.0] — 2026-05-27 — Sub-gap closeout: backup tmp dir, db-query endpoint, power-profile UX

Closes 3 sub-gaps surfaced during v1.10.1 retest of E2E gaps. Pairs with
companion v2.7.1.

### Fixed — sub-gap A: `backup_create` `wp db export` exit 1

Root cause: `wp-content/uploads/wplab-tmp/` did not exist; mysqldump errno 2.
Fix in companion v2.7.1: `WpCli` endpoint now calls `wp_mkdir_p` for the
scratch dir before exec. Idempotent + cheap. `backup_create` now succeeds
on RestTarget with companion.

### Fixed — sub-gap B: `diagnose` slow_queries + large_options query failures

Two root causes:

1. `wp db query` over the wp-cli endpoint does NOT substitute `{prefix}`
   placeholders — the literal string went straight to mysql, got
   "Unknown table" error.
2. Shell quoting of SQL with single/double quotes is brittle.

Fix: companion v2.7.1 adds `POST /wp-json/wplab/v1/db-query` —
SELECT/SHOW/DESCRIBE/EXPLAIN/WITH only, `$wpdb->prepare` parameter binding,
auto-substitutes `{prefix}` → `$wpdb->prefix`. `diagnose` now routes
through `bridge.dbQuery()` for RestTarget+companion, with fallback to
`wp db query` for shell-capable targets without companion.

### Added — `Bridge.dbQuery(sql, params?)`

Thin wrapper around `/wplab/v1/db-query`. Reusable for any read-only DB
introspection tool. Refuses non-SELECT at companion side; binds params
via `$wpdb->prepare`.

### Fixed — sub-gap C: `wp_execute_php` POWER_PROFILE UX

`PowerProfileRequiredError` now returns a detailed message including:

- Exact env var name (`ROLEPOD_WPLAB_PROFILE`)
- Required value (`power`)
- Concrete `.mcp.json` snippet ready to paste
- Why the gate exists (execute-php = arbitrary PHP, opt-in by design)
- Confirmation that other MCP tools work without the gate

### Added — `connect_rest` exposes active profile

`rolepod_wp_connect_rest` response now includes:

```jsonc
"profile": {
  "active": "default" | "power",
  "execute_php_unlocked": boolean,
  "env_var_name": "ROLEPOD_WPLAB_PROFILE"
}
```

AI clients can read this at connect time to know whether `wp_execute_php`
is available without trial-and-error.

### Pairs with

`rolepod-wp` **v2.7.1** — `wplab-tmp` auto-mkdir + new `/db-query`
endpoint. MIN_COMPANION_VERSION unchanged at 2.1.0; new endpoints are
additive.

## [1.10.1] — 2026-05-27 — Patch: wave-3 PHP payload bugs surfaced in retest

3 bugs caught during v1.10.0 MCP retest:

- **site_scaffold**: `${JSON.stringify(input.x)}` embedded JSON object
  literal `{"a":1}` into PHP source → `ParseError: unexpected token "{"`.
  Fix: stringify-then-jsondecode pattern (build `json_decode("...", true)`).
- **seo_set**: same bug at line 51 `$input = ${JSON.stringify(input)};`.
  Fix: same stringify-then-jsondecode.
- **cf7_form_create**: default `mail_from` was `"[your-name] <noreply@${siteurl-domain}>"`
  — `${siteurl-domain}` was a documentation placeholder but PHP interprets
  `${siteurl}` as variable expansion → `Undefined constant "siteurl"`.
  Fix: default `mail_from` to `"[your-name] <[your-email]>"`.

No new features. No schema change. Tool count unchanged at 98. Build clean.
Smoke 3/3.

## [1.10.0] — 2026-05-27 — E2E gap closeout: companion /option-set, 9 new tools, ledger capture

Driven by the 2026-05-27 e2e build-out test on Hostinger demo where ~60% of
operations had to fall back to direct execute-php. 89 → 98 tools.

### Changed — option_set / option_get route via companion `/option-set` for RestTarget (Gap #2/#4)

Previously: `option_set` against RestTarget hit `/wp/v2/settings`, whose
allowlist uses different field names than raw wp_options (title vs blogname,
description vs blogdescription, timezone vs timezone_string). Writes
silently no-op'd. The tool returned `changed: true` but `option_get`
returned null on read-back.

Now: when companion is enabled, option_set/option_get route to the new
companion endpoints which call `update_option()` / `get_option()` directly.
Full wp_options coverage. Blocked keys (db_version, auth_key, secret,
rewrite_rules, etc.) refuse with `OPTION_BLOCKED`. Falls back to REST
settings only if companion is unavailable.

### Changed — `wp_backup_create` accepts RestTarget with companion (Gap #8)

Was hard-gated to LocalTarget/SshTarget/DockerTarget. Now also accepts
RestTarget when `companion.enabled` is true (wp-cli routes through
companion's wp-cli endpoint, so all backup ops work). The error message is
clearer when companion is missing.

`audit_security` + `audit_many` + `diagnose` were already kind-agnostic in
v1.9 — the stale errors reported in the e2e test came from npm v1.2.3 which
hadn't been updated since the local source diverged. Re-publish to npm
unblocks them.

### Changed — `health_check` uses companion-aware probes (Gap #1)

Removed the "v0.2+ required" stale warnings. wp_cli_ok / db_ok now reflect
actual reachability via target.wpCli() which works on RestTarget through
companion. Added rest_ok probe using `target.rest({ path: '/wp/v2/types/post' })`.

### Changed — `execute_php` `confirm` schema accepts true OR "true" (Gap #3)

`z.literal(true)` rejected the string "true" some MCP clients send when
JSON-stringifying booleans. New `ConfirmTrueSchema` union accepts both forms
and normalizes to boolean true semantically. Same fix applied to
`mail_test.confirm`.

### Added — `Bridge.optionSet(name, value, autoload?)` + `Bridge.optionGet(name, default?)`

Thin wrappers around companion endpoints `/wp-json/wplab/v1/option-set` +
`/option-get`. Reused by `wp_option_set` / `wp_option_get` for RestTarget
and available for any new tool that needs direct wp_options access.

### Added — 9 new MCP tools (89 → 98)

- **`rolepod_wp_menu_create`** — create (or reuse) a nav menu by name.
- **`rolepod_wp_menu_add_item`** — add page link or custom URL item to menu.
- **`rolepod_wp_menu_assign`** — assign menu to theme location (primary / footer).
- **`rolepod_wp_product_create`** — WooCommerce simple product (price + sku + stock + categories). Refuses if WC not active.
- **`rolepod_wp_set_front_page`** — static front page + blog index in one call (show_on_front + page_on_front + page_for_posts).
- **`rolepod_wp_global_styles_set`** — write user-level wp_global_styles for the active block theme (Site Editor equivalent).
- **`rolepod_wp_cf7_form_create`** — Contact Form 7 form + mail config + recipient. Returns ready-to-paste shortcode.
- **`rolepod_wp_seo_set`** — set Yoast OR Rank Math post meta (auto-detect). Focus keyword + meta description + canonical + noindex.
- **`rolepod_wp_site_scaffold`** (composite) — one-shot site build from a JSON spec: identity + pages + menu + front_page assignment. Returns slug → id manifest.

All 9 tools auto-ledger their writes. `wp_site_scaffold` runs server-side
in one execute-php call so the entire scaffold is roughly atomic.

### Pairs with

`rolepod-wp` **v2.7.0** — adds companion endpoints `/option-set`,
`/option-get`, and server-side ledger capture in `/execute-php` (regex
scans payload for update_option / wp_insert_post / wp_update_post /
update_post_meta / activate_plugins / etc. and records one ledger row per
detected write, tagged source_tool=execute_php). Closes Gap #9 — writes
issued via execute-php (the main escape hatch) are now discoverable in
the AI Change Ledger.

### Smoke test

98 tools alphabetized in `tests/smoke/mcp-handshake.test.ts`. tsc clean.
Bundle size 2.34 MB (+~6 KB for 9 small tool files).

## [1.9.1] — 2026-05-27 — Branding cleanup (remove third-party references)

Doc-only patch. Removed all references to the third-party WordPress AI plugin
that originally inspired the project's design exploration. Independent
implementation language used throughout README, CONTRIBUTING, package.json,
CHANGELOG entries, and the `wp_conventions` tool description. The legacy
third-party migration guide under `docs/` removed entirely (single-purpose
guide that referenced an unrelated WP plugin's namespace).

No behavior change. No new tools. No schema change. Tool count unchanged
at 89.

## [1.9.0] — 2026-05-27 — Recovery namespace (mu-plugin guardian wrappers)

Seven new MCP tools for crash recovery via the new `/wplab-recovery/v1/*`
namespace that ships in companion v2.6.0's mu-plugin guardian. The guardian
loads before regular plugins so its REST endpoints survive main-plugin
parse errors / fatals. 82 → 89 tools.

### Added — recovery tools

- **`rolepod_wp_recovery_status`** — probe guardian. Returns
  `main_alive`, `recent_fatals[]`, `last_fatal`, `safe_mode`,
  `guardian_version`. Use after a 5xx from main namespace to determine
  whether you're in recovery mode.
- **`rolepod_wp_recovery_disable_plugin`** — rename plugin main file to
  `.disabled`. Accepts slug or `slug/file.php` form. Calls
  `deactivate_plugins()` for active_plugins cleanup. Use when a plugin
  (often the one just updated/edited) is fataling.
- **`rolepod_wp_recovery_disable_file`** — scope-checked rename
  `<path>` → `<path>.disabled` for any file under
  `wp-content/{plugins,themes,uploads,mu-plugins}` or `wp-config.php`.
  Use when a specific file (theme `functions.php`, plugin include) is
  the culprit but you don't want to nuke the whole plugin.
- **`rolepod_wp_recovery_restore_file`** — reverse. Accepts either
  the `.disabled` form or active form.
- **`rolepod_wp_recovery_restore_snapshot`** — untar a previously
  captured theme snapshot via the guardian (bypasses main companion's
  `/theme/restore` which would be unreachable if main is down).
- **`rolepod_wp_recovery_list_changes`** — direct DB read of the
  ledger. Useful pre-rollback to identify the suspect write.
- **`rolepod_wp_recovery_safe_mode`** — toggle `rolepod_wp_safe_mode`
  option. When ON, the main companion (when alive) should refuse risky
  ops to prevent the AI from immediately re-introducing the bad write.

### Added — `RecoveryModeError` (`src/util/errors.ts`)

Surfaces when main namespace 5xx + guardian reports `main_alive: false`.
Includes `last_fatal` (file/line/message) so the AI can decide which file
to disable without an extra status probe.

### Added — `Bridge` recovery methods

`CompanionBridge.recoveryStatus / recoveryDisablePlugin /
recoveryDisableFile / recoveryRestoreFile / recoveryRestoreSnapshot /
recoveryListChanges / recoverySafeMode` — all hit
`/wplab-recovery/v1/*` and use Application Password auth via
`target.rest()` (no session token; main `/handshake` may be down).

### Pairs with

`rolepod-wp` **v2.6.2+** (recommended) — companion with the mu-plugin
guardian auto-installed on plugin activate. Version notes:

- **v2.6.0** — initial guardian. REST endpoints registered via
  `rest_api_init` only, which is unreachable during theme-load fatal
  (~40% of WSODs). Useful for post-`init` fatals only.
- **v2.6.1** — added `muplugins_loaded:PHP_INT_MAX` early-dispatch path
  so recovery endpoints reachable during ANY post-mu-plugin fatal
  (theme load, plugin file parse). Recovery coverage ~95%.
- **v2.6.2** — best-practices auth hardening. Pluggable.php load order
  fix + REDIRECT_HTTP_AUTHORIZATION + getallheaders() fallbacks for
  broader server compat (Apache mod_php, Apache FastCGI, Nginx FPM,
  LiteSpeed). Set as the floor in `MIN_COMPANION_VERSION`.

### Demo test learning (recorded for future contributors)

The architecture pattern "mu-plugin recovery via `rest_api_init`" has a
trap: REST routes don't register until WP boot completes through `init`,
which is gated by `setup_theme` → theme `functions.php` load. A fatal in
the theme file kills the boot before `rest_api_init` fires. The guardian
mu-plugin DID load (mu-plugins are pulled in before plugins/themes in
`wp-settings.php`), and its `register_shutdown_function` DID record the
fatal — but the REST endpoints weren't registered, so the AI couldn't
query / disable / recover.

The fix (companion v2.6.1) detects recovery URLs at the top of mu-plugin
load and short-circuits at `muplugins_loaded:PHP_INT_MAX`, bypassing
plugin + theme load entirely. Manual Application Password auth via
`WP_Application_Passwords::validate_application_password()` works because
WP loads its core classes (incl. that one) before mu-plugins via
`wp-load.php`.

Result: if v2.6.1+ is installed BEFORE a WSOD-inducing write, recovery
works through REST. If only v2.6.0 was installed and a theme-level fatal
hits, manual SSH/FTP/cPanel recovery is still required (chicken-and-egg
on the upgrade path — to install v2.6.1, WP must boot, but WP can't
boot through the fatal).

### Smoke test

`tests/smoke/mcp-handshake.test.ts` expected tool list updated 82 → 89.
TypeScript compile clean (`tsc --noEmit`). Bundle size: 2.33 MB (unchanged
material, +~3 KB from 7 small tool files).

## [1.8.0] — 2026-05-27 — Capability closeout: one-time admin login + file toggle + 3 field-plugin adapters + conventions

Eleven new MCP tools driven by the v2.4 competitive analysis pass. 71 → 82 tools.

### Added — admin + file ops

- **`rolepod_wp_admin_one_time_link`** — mint a 5-min single-use wp-admin
  login URL (companion `/admin/one-time-login`). Browser-automation safe;
  no admin password exposed.
- **`rolepod_wp_file_disable`** — rename `<path>` → `<path>.disabled`
  (companion `/fs-rename`). PHP files become invisible to autoloader.
  Auto-ledger row category=file action=disable, reversible.
- **`rolepod_wp_file_enable`** — reverse `wp_file_disable`. Refuses if
  destination already exists.

### Added — field-plugin adapters

- **`rolepod_wp_jetengine_{read,write}`** — JetEngine field groups + CCT
  list + post meta. Detection via REST plugin search.
- **`rolepod_wp_metabox_{read,write}`** — Meta Box (metabox.io) field
  groups (rwmb_meta_box post type) + post meta.
- **`rolepod_wp_pods_{read,write}`** — Pods Framework pods (\_pods_pod) +
  fields (\_pods_field) + post meta. Compatible free + Pro.

All field-plugin writes auto-ledger (category=post) with before-state
captured via REST GET ?context=edit.

### Added — project conventions storage

- **`rolepod_wp_conventions_{get,set}`** — per-site structured style guide
  storage at `~/.config/rolepod-wplab/memory/<host>/conventions.json`.
  Schema: colors[], fonts[], spacing[], style_rules[], code_conventions[],
  brand_voice, custom{}. `set` defaults to merge (deep); pass
  `merge: false` to replace.

### Changed — Gutenberg block-count in post_create output

`wp_post_create` now scans content for `<!-- wp:` block markers and reports
`block_count` in the ledger after-state. Informational only; behavior
unchanged. Helps the AI confirm Gutenberg vs classic content was authored
correctly.

### Bridge additions

- `Bridge.adminOneTimeLink(destination?)` — POST `/admin/one-time-login`.
- `Bridge.fsRename(src, dest)` — POST `/fs-rename`, scope-checked.

### Tool count

71 (v1.7) → **82** (v1.8). Adds: admin_one_time_link, file_disable,
file_enable, conventions_get, conventions_set, jetengine_read,
jetengine_write, metabox_read, metabox_write, pods_read, pods_write.

### Deferred — WordPress Abilities API wrapper

Considered + deferred. WP Abilities API (WP 6.9+) has minimal cross-ecosystem
adoption today. Re-evaluate when:

- WordPress 7.0 ships with Abilities in core.
- Multiple well-known plugins adopt the API publicly.
- Users explicitly ask for cross-consumer ability discovery.

Standard MCP works with every MCP client today; WP-side ability registration
adds composer dep + PHP layer for marginal benefit at present.

### Pairs with

- `rolepod-wp` v2.5.0 — adds `/admin/one-time-login`, `/fs-rename` endpoints
  - execute-php crash recovery.

## [1.7.0] — 2026-05-27 — Theme safety: pre-write validators + snapshot + child-theme-first + safe-switch

### Added — pre-write validators on `wp_file_write`

- `.php` files: companion runs `php -l` server-side. Reject on syntax error
  with line + message — WSOD on functions.php is now impossible via this MCP.
- `.json` files: Node-side `JSON.parse` before sending. Catch typos before
  they hit the wire.
- Both validators are unbypassable (no opt-out flag). Companion infra failures
  (exec disabled, transient host issue) degrade to "best-effort skip", not
  block the user.

### Added — theme snapshot + restore + safe switch

- `rolepod_wp_theme_snapshot` — capture full theme dir as `.tar.gz` under
  `wp-content/uploads/rolepod-wp-theme-snapshots/<slug>-<utc-ts>.tar.gz`.
- `rolepod_wp_theme_restore` — un-tar a snapshot back over the theme dir.
  Refuses out-of-managed-dir paths (path validation in companion).
- `rolepod_wp_theme_switch_safe` — composite: snapshot CURRENT theme →
  `wp-cli theme activate <new>` → REST `GET /` post-switch health probe →
  AUTO-ROLLBACK (re-activate old + restore snapshot) on red. Ledger row
  category=theme so manual revert via `wp-changes` also works.

### Added — child-theme-first composite

- `rolepod_wp_child_theme_create` — reads parent style.css for Theme Name +
  Version, scaffolds child dir (style.css with `Template: <parent>` header,
  functions.php with parent-style enqueue). Refuses if child slug already
  exists. Standard WP best practice baked in.

### Added — session correlation

- `rolepod_wp_session_start` — issues `sess_<hex>` id, sets
  `ROLEPOD_WPLAB_SESSION` env so all subsequent auto-ledger writes group
  under one source_session. Atomic revert via
  `rolepod_wp_changes_toggle_bulk { source_session: <id> }`.

### Added — auto cache flush + global-styles ledger

- `wp_file_write` paths ending `theme.json` → auto `wp cache flush` after
  write so the Site Editor sees the new state on next reload.
- `wp_rest_request POST /wp/v2/global-styles/<id>` → captures before-state
  via a GET first, records `category=layout subcategory=global_styles` in
  the Change Ledger, then auto-flushes object cache.

### Skill restructure (13th skill)

- New `wp-edit-theme` skill (phase=build) owns theme files + theme.json +
  global-styles + child themes + safe theme switch.
- `wp-edit-design` scope narrows to page-builder layouts only (Elementor /
  Divi / Oxygen / Bricks) — clearer mental model.
- `wp-full` alias updated to list both.

### Tool count

62 (v1.5) → 66 (v1.6, ledger) → **71** (v1.7, adds: theme_snapshot,
theme_restore, child_theme_create, theme_switch_safe, session_start).

### Pairs with

- `rolepod-wp` v2.4.0 — adds `/wplab/v1/syntax-check`, `/theme/snapshot`,
  `/theme/restore` endpoints.

## [1.6.0] — 2026-05-26 — AI Change Ledger + per-change toggle + panic-revert

### Added — `Bridge.changes` methods + 4 MCP tools

`Bridge` gains `recordChange`, `queryChanges`, `toggleChange`, `toggleChangesBulk`, `panicChanges` — all targeting the new `/wplab/v1/changes/*` endpoints in companion v2.3+. Four MCP tools expose this surface:

- `rolepod_wp_changes_query` — filter the ledger (category / applied / since_minutes / source_session).
- `rolepod_wp_changes_toggle` — flip one row's applied flag, run companion's per-category revert.
- `rolepod_wp_changes_toggle_bulk` — batch flip (used for git-bisect-style narrowing).
- `rolepod_wp_changes_panic` — disable every change in a time window (1-1440 min).

Total MCP tools surface: 62 → 66.

### Added — auto-ledger wiring on writer tools

`wp_post_create`, `wp_post_update`, `wp_option_set`, `wp_file_write` now capture before+after state via the new `recordChange()` helper at `src/companion/ledger.ts`. Failure is non-fatal (companion missing / endpoint disabled / older companion = skip and continue).

Env override `ROLEPOD_WPLAB_LEDGER=off` disables recording entirely (for tests or privacy-restricted deployments). Default = on.

Future tools to wire (v1.7): adapter writes (`wp_elementor_write`, `wp_divi_write`, etc.), `wp_scaffold_*`, `wp_clone`, `wp_migrate_data`. Tools that genuinely cannot be reverted (`wp_execute_php` side effects, destructive wp-cli) record with `reversible: false` so the user sees the warning icon in the admin UI.

### Added — `wp-changes` skill (12th skill)

Phase = recovery. Owns the rollback workflow:

- query the ledger,
- toggle individual rows,
- panic-disable a window when the site breaks,
- bisect to identify the bad change after recovery.

Updated `wp-full` alias to list it.

### Bumped

- `MIN_COMPANION_VERSION` stays at `2.1.0` — `2.3.0` is BACKWARD-COMPATIBLE for older MCP builds (ledger endpoints are additive). MCP v1.6 with companion v2.1 → ledger calls return 404 + writer tools skip recording with a debug log; everything else works.

### Pairs with

- `rolepod-wp` v2.3.0 — ships the ledger table, recorder API, per-category toggle dispatchers, hook wrapper helper, and the admin UI at Tools → Rolepod WP Changes.

## [1.5.0] — 2026-05-26 — Lean 11-skill rewrite per Rolepod parent contract

### Changed — full skill-set restructure

The 11 skills are rewritten end-to-end per the [Rolepod parent skill-authoring contract](https://github.com/nuttaruj/rolepod/tree/main/core/skills). Each `SKILL.md` now follows the mandatory shape (frontmatter with `tier` + `phase`, `Iron Rule`, `When to use` + `Skip when`, `Boundary` with Owns / Does not own / Hand off, `Workflow`, `Output`, `Examples` pointer, `References` pointer, `Hard stops`, `Full Rolepod enhancement`, `Next phase`). Each skill ≤ 144 lines.

Supporting files (16 total — under the 36-file cap):

- `wp-pair-setup/examples/pair-examples.md`
- `wp-connect/references/connect-kinds.md`
- `wp-health-check/templates/health-report.md`
- `wp-content/examples/content-examples.md`
- `wp-edit-design/references/builder-formats.md` + `examples/design-examples.md`
- `wp-edit-plugin/references/adapter-detection.md`
- `wp-scaffold/templates/scaffold-manifest.md` + `examples/scaffold-examples.md`
- `wp-introspect/references/introspect-scopes.md`
- `wp-diagnose/templates/diagnose-report.md` + `examples/diagnose-examples.md`
- `wp-migrate/templates/migrate-plan.md` + `examples/migrate-examples.md`
- `wp-execute-php/references/ast-rules.md` + `examples/php-examples.md`
- `wp-full/` (alias, zero supporting files)

### Skill set, by phase

| Phase  | Skill                                                   |
| ------ | ------------------------------------------------------- |
| define | wp-pair-setup, wp-connect                               |
| verify | wp-health-check                                         |
| build  | wp-content, wp-edit-design, wp-edit-plugin, wp-scaffold |
| debug  | wp-introspect, wp-diagnose                              |
| ship   | wp-migrate                                              |
| power  | wp-execute-php                                          |
| alias  | wp-full                                                 |

### Coverage

The new 11 cover all 62 MCP tools with zero overlap:

- **wp-pair-setup** owns the single-use pair_token redemption flow.
- **wp-connect** owns target-kind selection (local / rest / ssh / docker) post-pair.
- **wp-health-check** owns the sub-5s readiness ping.
- **wp-content** owns core REST CRUD (posts, pages, users, options, db SELECT).
- **wp-edit-design** owns visual layouts (Elementor + Divi + Oxygen + Bricks + theme.json + global-styles).
- **wp-edit-plugin** owns SEO / i18n / e-commerce / custom-fields / forms (Yoast, RankMath, WPML, WooCommerce, ACF, Gravity / CF7 / WPForms).
- **wp-scaffold** owns bootstrap of new block / plugin / theme / pattern.
- **wp-introspect** owns read-only runtime snapshots.
- **wp-diagnose** owns multi-probe sweep + audit + audit_many.
- **wp-migrate** owns dryrun + apply + backup + restore + clone across targets.
- **wp-execute-php** owns last-resort PHP eval with the five-layer safety chain.
- **wp-full** is a zero-supporting-file alias listing every skill.

### Removed (subsumed)

- `wp-audit-security` → folded into `wp-diagnose`.
- `wp-audit-woo` → folded into `wp-diagnose` + `wp-edit-plugin` (read scope).
- `wp-edit-elementor` → generalized to `wp-edit-design` (all 4 builders).
- `wp-scaffold-block` + `wp-scaffold-plugin` + `wp-scaffold-theme` → unified to `wp-scaffold`.
- `wp-migrate-dryrun` → expanded to full `wp-migrate` (dryrun + apply + rollback).

## [1.4.0] — 2026-05-26 — RestTarget full shell capability via companion

### Added — `RestTarget` now shell-capable via the `rolepod-wp` v2.1+ companion

- `Bridge.wpCli(args, opts)` — POSTs `/wplab/v1/wp-cli` with session_token, parses
  the JSON response into the standard `WpCliResult` shape. Auto-refreshes the
  session token on 401, auto-bootstraps `wp-cli.phar` from upstream on first
  `WP_CLI_NOT_BUNDLED` (via the new `/wp-cli/bootstrap` companion endpoint), then
  retries the original call once.
- `Bridge.fileRead(path)` — POSTs `/wplab/v1/fs-read`; scope guard runs server-side.
- `Bridge.fileWrite(path, content, opts)` — POSTs `/wplab/v1/fs-write`; supports
  mode/backup/confirmUnsafePath opts, mirrors the LocalTarget signature.
- `RestTarget` now lazy-caches a single `CompanionBridge` instance per target,
  reusing one handshake/session across all companion-gated calls.
- `RestTarget.wpCli` delegates to `Bridge.wpCli` (was: hard error `COMPANION_REQUIRED_V0_2`).
- `RestTarget.fileRead` / `.fileWrite` / `.fileExists` likewise delegate.

### Changed — composite tools drop their `kind!==local|ssh|docker` gates

`wp_diagnose`, `wp_cron_tool`, `wp_cache_tool`, `wp_mail_test`, `wp_user_session_list`
all used to throw `*_REQUIRES_SHELL` upfront. They now call `target.wpCli` directly;
on a RestTarget without companion, `CompanionUnavailableError` with the stable
install URL surfaces from `Bridge.handshake` (better message than the old gate
because it tells the user how to fix it).

`audit_security` already used `target.wpCli` without a kind gate — it now works
over RestTarget transparently.

### Bumped

- `MIN_COMPANION_VERSION` = `2.1.0` (companion v2.0.0 had the wp*cache*\* session
  token bug; v2.1 uses transients so the handshake-then-act flow actually works
  on shared hosting). MCP warns post-pair if the detected companion is older.

### Pairs with

- `rolepod-wp` v2.1.0 — adds `/wp-cli/bootstrap` + SessionToken transient backend.

## [1.3.0] — 2026-05-26 — Cross-component contract + rolepod-wp companion rename

### Added

- `src/companion/constants.ts` — single source of truth for the cross-repo contract:
  - `COMPANION_INSTALL_URL` — stable `releases/latest/download/rolepod-wp.zip` URL
  - `COMPANION_PLUGIN_SLUG = "rolepod-wp"`
  - `COMPANION_REPO_URL = https://github.com/nuttaruj/rolepod-wp`
  - `MIN_COMPANION_VERSION = "2.0.0"` — version-compat floor; warns post-pair if companion is older
  - `compareVersions()` + `isCompanionTooOld()` helpers
  - `setupWizardUrlFor(siteurl)` builder targeting `?page=rolepod-wp-setup`
- 5 unit tests for the new constants module (147 → 152 total tests, all green).

### Changed

- `wp_connect_rest` `CREDENTIALS_MISSING` + `COMPANION_REQUIRED_BUT_MISSING` errors now embed the stable install URL + a `wp plugin install <URL> --activate` one-liner so AI agents can hand users a copy-paste command.
- `wp_pair` post-pair warns when companion_version < MIN_COMPANION_VERSION (non-blocking).
- `CompanionUnavailableError` accepts `installUrl` hint; surfaces in error message + meta.
- `Bridge.ts` handshake 404 + introspect 404 now pass the install URL hint.
- `runtime/RestTarget.ts` REST_AUTH_FAILED guidance uses `setupWizardUrlFor()` + "Rolepod WP Setup" wording.
- `bin/companion.ts` install command emits from constant.
- `bin/init.ts` no-companion message references the new "rolepod-wp" name.
- README Path A install URL switched to the new stable `rolepod-wp.zip` URL.
- README + CONTRIBUTING + SECURITY rephrased to call the WP plugin `rolepod-wp` (the WordPress arm of the [Rolepod ecosystem](https://github.com/nuttaruj/rolepod)) rather than "companion".
- 4 marketplace manifest descriptions rephrased to match the new positioning.
- CONTRIBUTING gains a release-protocol section: version-pair convention across the two repos, MIN_COMPANION_VERSION bump rule, "never ship MCP before companion zip is live" ordering.

### Pairs with

- `rolepod-wp` v2.0.0 (the renamed companion plugin). Existing v1.x companion installs continue to handshake on `/wplab/v1/*` for wire compatibility, but install URL guidance now points at the new repo.

## [1.2.4] — 2026-05-26 — Schema-verified plugin marketplaces (Claude Code + Codex)

### Added

- **`.claude-plugin/marketplace.json`** — enables `claude plugin marketplace add nuttaruj/rolepod-wplab` + `claude plugin install rolepod-wplab@rolepod-wplab`. Schema-verified against https://code.claude.com/docs/en/plugin-marketplaces.
- **`.codex-plugin/plugin.json`** — Codex CLI plugin manifest with full `interface` block (displayName, shortDescription, longDescription, capabilities, defaultPrompt). Schema-verified against https://developers.openai.com/codex/plugins/build.
- **`.agents/plugins/marketplace.json`** — Codex local marketplace catalog with `source.path: "./"` (plugin lives at repo root, not in a subdir). Schema-verified against same source.

### Changed

- README **Codex CLI** section gets the marketplace install path (in addition to the manual `config.toml` snippet).
- README **Gemini CLI** section now notes skills are not auto-registered (no stable extension format yet — matches the rolepod-uiproof honesty).
- `package.json` `files[]` adds `.codex-plugin` + `.agents` so the marketplace catalogs ship with the npm package.

### Schema-bound source URLs (locked in for SCHEMA-BOUND-file hook policy)

| File                               | Source                                              |
| ---------------------------------- | --------------------------------------------------- |
| `.claude-plugin/marketplace.json`  | https://code.claude.com/docs/en/plugin-marketplaces |
| `.claude-plugin/plugin.json`       | https://code.claude.com/docs/en/plugins-reference   |
| `.codex-plugin/plugin.json`        | https://developers.openai.com/codex/plugins/build   |
| `.agents/plugins/marketplace.json` | https://developers.openai.com/codex/plugins/build   |

### Numbers

- MCP tools: 62 (unchanged).
- Tests: 141 passing.

## [1.2.3] — 2026-05-26 — Install path fixes + README rewrite

### Fixed

- `.mcp.json` previously pointed at `${CLAUDE_PLUGIN_ROOT}/dist/bin/rolepod-wplab.js`. `dist/` is gitignored, so users who installed via `claude plugin install nuttaruj/rolepod-wplab` got a server that wouldn't spawn. Now uses `npx -y @rolepod/wplab@latest serve` — pulls from npm, always works.
- `.claude-plugin/plugin.json` version was stuck at `0.1.0`. Now `1.2.2` (will sync to package.json version on each release).
- `package.json` `files` referenced `.cursor-plugin` + `.codex-plugin` directories that never existed. Replaced with the real `.mcp.json` + `.cursor/mcp.json` configs.

### Added

- `.cursor/mcp.json` — drop-in workspace MCP config for Cursor IDE (per-project or global via `~/.cursor/mcp.json`).
- New README written in [`rolepod-uiproof`](https://github.com/nuttaruj/rolepod-uiproof) style — concise headline, "What it helps with" action list, skills table, per-CLI install (Claude Code / Cursor / Codex / Gemini / Direct npm), Path A (pair) + Path B (manual) quick start, doctor output, "What's inside" tech summary.

### Numbers

- MCP tools: 62 (unchanged).
- Tests: 141 passing.

## [1.2.2] — 2026-05-26 — Slim dist

### Changed

- `tsup.config.ts` — `sourcemap: false` for shipped builds. dist size **14.6 MB → 4.8 MB (-67%)** by dropping the two `.map` files (~10 MB total) that were ~68% of the bundle. End users `npx` the binary and rarely inspect stack traces; local devs can re-enable maps with `npx tsup --sourcemap` when debugging.
- No source change. No schema change. No tool change.

### Numbers

- npm tarball: **~2.7 MB → ~750 KB packed** (typical sourcemap-heavy → slim ratio).
- MCP tools: 62 (unchanged).
- Tests: 141 passing.

## [1.2.1] — 2026-05-26 — Path-2 polish

### Changed

- `rolepod_wp_connect_rest` `CREDENTIALS_MISSING` error now lists BOTH paths (companion Quick Start URL + manual `credentials add` command) so the AI can guide the user without guessing.
- `RestTarget.open` `REST_AUTH_FAILED` error includes the same two-path suggestion plus the setup-wizard URL on the target host.
- Both errors carry `setup_wizard_url` in `meta` for AI agents that prefer structured data over message text.

### Numbers

- MCP tools: **62** (unchanged).
- Tests: 141 passing.

## [1.2.0] — 2026-05-26 — One-click pair (token-redeem setup UX)

### Added

- `rolepod_wp_pair { siteurl, pair_token }` — single MCP call that redeems a companion-issued pair token for a real WP Application Password (companion-minted under the issuing admin's user, named `wplab-pair-<timestamp>`). Stores credential in vault + opens a `RestTarget` in one shot.
- Companion endpoint **POST `/wp-json/wplab/v1/pair/generate`** (admin only, manage_options) — issues a 256-bit pair token, TTL 60 min, max 5 active per admin.
- Companion endpoint **POST `/wp-json/wplab/v1/pair/redeem`** (public, token-authed) — atomic single-use redeem; per-IP throttle (10 failed / hour).
- Companion `src/Security/PairToken.php` — SHA-256 hashed-at-rest tokens in `wp_options`, opportunistic sweep of expired rows.
- Companion `Tools → WPLab Setup` page extended with **"⚡ Quick Start"** section: button → mints pair token → renders ready-to-paste prompt that includes Claude Code / Cursor / Codex / Gemini install snippets + `rolepod_wp_pair` call with siteurl + pair_token baked in. One-click copy.
- New skill `skills/wp-pair-setup/SKILL.md` — instructs AI agents on the pair flow + failure modes + security notes.

### Schema additions

- `PairInputSchema` — siteurl https-only refine + pair*token regex `/^wplab_pair*[a-f0-9]{48}$/`.
- `PairOutputSchema` — target_id + siteurl + username + capabilities + companion_version + is_production + app_password_name + credential_stored.

### Security

- Pair token = SHA-256 hashed at rest, never returned by any GET endpoint.
- Single-use guarantee: `PairToken::redeem` deletes the wp_options row **before** acting on the payload — concurrent redeem attempts can't both succeed.
- TTL 60 min, post-redeem the App Password is the long-lived credential (revocable from `profile.php`).
- Pair generate requires admin (`manage_options`). Pair redeem rate-limited per IP via transient.
- App Password name `wplab-pair-<UTC-timestamp>` makes attribution + revocation trivial.
- Production guard unchanged — pair-minted credentials are subject to all the same `ProdGuard` checks on power tools.
- Companion `endpoints_enabled` master toggle still applies — pair endpoints respect it.

### Numbers

- **MCP tools**: 61 → 62 (+1: `rolepod_wp_pair`).
- **Companion REST endpoints**: 8 → 10 (+2: pair/generate, pair/redeem).
- **Skills**: 10 → 11 (+1: wp-pair-setup).
- **Unit + smoke tests**: 134 → 141 (+7 PairInput/Output schema tests).

### Notes

- Schema-freeze policy honored: every change is additive (new tool, new schemas, new endpoints).
- Companion-first install path is now the recommended quick-start. Manual setup path (App Password + npm install + claude mcp add + credentials add) preserved on the same wizard page for users on CLIs without a wplab plugin.

## [1.1.0] — 2026-05-26 — Parity + lead expansion (Tier A/B/C/D)

### Added — Tier A (parity with existing WP AI tools)

- `rolepod_wp_divi_read` / `rolepod_wp_divi_write` — Divi Builder pages (post_content shortcodes + `_et_pb_use_builder` flag).
- `rolepod_wp_oxygen_read` / `rolepod_wp_oxygen_write` — Oxygen Builder (`ct_builder_shortcodes` post meta).
- `rolepod_wp_bricks_write` — extends Bricks adapter with page / header / footer element-tree writes.
- `rolepod_wp_yoast_write` — Yoast SEO post meta (focus_keyword / meta_description / title / canonical / noindex).
- `rolepod_wp_rankmath_write` — Rank Math SEO post meta (mirrors Yoast surface).
- `rolepod_wp_wpml_write` — set_post_language / link_translations / duplicate_for_translation ops.

### Added — Tier B (capability extensions)

- `rolepod_wp_forms_read` / `rolepod_wp_forms_write` — unified Gravity / Contact Form 7 / WPForms adapter with auto-detect.
- `rolepod_wp_cron_tool` — list / run / delete WP-Cron events.
- `rolepod_wp_cache_tool` — inspect object cache + transient counts; flush_object / flush_transients ops.
- `rolepod_wp_mail_test` — send test email via wp_mail() (companion execute-php preferred, wp-cli `wp eval` fallback).
- `rolepod_wp_clone` — composite: db export+import + wp-content sync + url search-replace + plugin version sync.
- `rolepod_wp_backup_create` / `rolepod_wp_backup_restore` — db dump + wp-content manifest snapshots.

### Added — Tier C (setup UX parity)

- `rolepod-wplab init` — interactive 5-step wizard (App Password + REST probe + handshake + credential store + starter profile).
- `rolepod-wplab companion install --target=<host>` — probe + emit copy-paste wp-cli installer command.
- `rolepod-wplab companion status --target=<host>` — handshake check + capability dump.
- Companion plugin: new **Tools → WPLab Setup** wizard page with App Password + MCP install copy-paste blocks.

### Added — Tier D (moat extend)

- `rolepod_wp_user_session_list` — enumerate active user sessions via `wp_usermeta.session_tokens` (security audit).
- `rolepod_wp_rest_dump` — enumerate every registered REST route (optional `filter_namespace`).
- `rolepod_wp_scaffold_pattern` — scaffolds a block pattern PHP file inside a theme or plugin.
- `rolepod_wp_diagnose` — non-destructive sweep: plugin_conflict_probe / slow_queries / large_options / broken_images / php_errors.

### Changed

- `AllowList.ts` — added wp-cli READ_ONLY entries: `transient list`, `cache type`, `user session list`.
- `AllowList.ts` — added wp-cli DESTRUCTIVE entries: `cron event run` / `delete` / `schedule`, `cache flush`, `transient delete` / `delete-expired`, `db export` / `import`, `search-replace`, `wpml`, `gf`, `user session destroy`.
- `tests/smoke/mcp-handshake.test.ts` — expected tool count 41 → 61.

### Numbers

- **MCP tools**: 41 → 61 (+20).
- **Adapters**: 8 (Elementor / Woo / ACF / Bricks / WPML / Yoast / RankMath) + 3 new dir (Divi / Oxygen / Forms).
- **CLI subcommands**: 5 → 7 (added `init`, `companion`).
- **Companion REST endpoints**: 8 (unchanged).
- **Unit + smoke tests**: 117 passing.

### Notes

- Schema-freeze policy honored: every new tool is **additive** (new tool names). No existing input/output schema fields changed.
- Forms adapter `list_entries` is wired for Gravity Forms in v1.1; CF7 / WPForms entries land in plugin-private tables — adapter returns empty for those scopes in v1.1 (planned for v1.2 once both expose entry REST routes consistently).
- `wp_clone` `wp_content` scope copies top-level entries only — full deep tree sync (large media) is deferred to v1.2 with a companion `fs-rsync` endpoint.
- `wp_backup_restore` `wp_content` scope is manifest-verify only in v1.1; deep restore requires batch fs-write API (v1.2).

## [1.0.0] — 2026-05-25 — Stable (schema-frozen)

### Schema-freeze promise

All MCP tool **names** and **required input fields** locked. Breaking changes require a **major bump**. Adding optional input fields, adding output fields, adding new tools = minor. Bug fixes + dep bumps = patch.

The frozen schema:

- **46 MCP tools** with `rolepod_wp_*` prefix (connect + lifecycle + atomic + typed CRUD + adapters + composites + memory + companion-gated power).
- Schema source: `src/schema/tools.ts` (zod). Exported to `dist/schemas/tools.json` on build.
- Replay bundle format `v1` (`src/bin/replay.ts`).
- Companion REST namespace `/wp-json/wplab/v1/` with 8 endpoints.

### Locked

- Tool names + required fields.
- Profile names: strict / personal / power.
- Target kinds: local / rest / ssh / docker.
- Allow-list categories + never-allowed list (W-005).
- Filesystem scope rules (W-006).
- DB SELECT-only guard (W-007).
- Production guard semantics (W-008).
- Credential vault layout (W-018).
- Memory directory layout (W-028).
- Replay bundle JSON shape.

### Not locked

- Internal Target interface methods may grow (additive only).
- Composite implementations may improve.
- Adapter slate may expand (new plugins post-v1.0 are minor bumps).
- Companion v1.0+ may add new endpoints (additive).

### Pairs with

- `rolepod-wplab-companion` **v1.0** — schema-frozen alongside; audit log format frozen, capability map locked.

### Maintainer next actions (post-tag)

- npm publish `@rolepod/wplab`.
- Submit `rolepod-wplab-companion` to wordpress.org plugin directory (gated on WP review).
- External security audit per `SECURITY.md` "v1.0 audit scope" section.

## [0.5.0] — 2026-05-25 — OSS launch

### Added — Governance + docs

- **CONTRIBUTING.md** — license hygiene, quality gates, contribution checklist for new tools / adapters / companion endpoints, single-backend rule, DCO sign-off.
- **CODE_OF_CONDUCT.md** — Contributor Covenant v2.1.
- **SECURITY.md** — supported-versions matrix, 90-day private disclosure window, threat model for Node MCP + companion, in-scope items for v1.0 external audit.
- **.github/ISSUE_TEMPLATE/{bug_report,feature_request}.md** — structured intake.

### Pairs with

- `rolepod-wplab-companion` v0.2 (unchanged for v0.5).

### Tests

- 117 unit + smoke green. 46 MCP tools. `claude plugin validate ./ --strict` pass.

### Not in v0.5 (deferred to v1.0)

- npm publish to `@rolepod/wplab` — requires npm org claim + publish workflow.
- wordpress.org plugin directory submission for companion — requires WP review.
- Astro docs site (RECIPES.md remains the primary doc surface).
- Real replay tool dispatch (currently stub).
- External security audit.
- Conformance test suite for 3rd-party adapters.

## [0.4.0] — 2026-05-25

### Added — Polish

- **Replay bundle format v1.** Schema in `src/bin/replay.ts`. CLI: `rolepod-wplab replay <bundle.json>`. v0.4 stub-dispatches calls (logs only); v0.5 wires actual in-process MCP CallTool dispatch loop.
- **Dockerfile** for ghcr.io publish target. Multi-stage (build node20 → runtime node20-alpine). Entrypoint = `rolepod-wplab serve`.
- **GitHub Actions CI** (`.github/workflows/ci.yml`) — Node 20+22 × ubuntu+macos matrix; typecheck + build + test + lint + prettier + docker build (no push).
- **docs/RECIPES.md** — 10 concrete end-to-end recipes (connect local/remote, scaffold block, audit multi-site, migrate plugins, hook debug, bulk Woo update, memory recall, execute-php, docker fixture).

### Changed

- `rolepod-wplab` CLI usage line lists all subcommands: serve | doctor | credentials | memory | replay | smoke.

### Not yet implemented (deferred to v0.5)

- Replay actually executes tool calls (currently stubs to logs).
- Docs site (Astro). RECIPES.md serves as v0.4 doc surface.
- WooCommerce optional pack (existing adapter writes cover most use cases).
- Adapter test framework with real fixture coverage.

## [0.3.0] — 2026-05-25

### Added — Multi-target + cross-target composites + SEO/i18n adapters

**New target kinds (W-009):**

- `SshTarget` — node-ssh based. Supports private_key_path or password auth. wp-cli via ssh exec; fileRead/fileWrite via SFTP.
- `DockerTarget` — dockerode based. `docker exec` per call. File ops via stdin-piped tee. Demuxes docker stream multiplex header.
- `factory.openTarget` handles 4 kinds: local | rest | ssh | docker.

**New MCP tools (8 — total 46, was 34 in v0.2):**

- `rolepod_wp_connect_ssh { host, user, wp_path, port?, private_key_path?, password? }`
- `rolepod_wp_connect_docker { container_name, wp_path?, docker_host?, docker_socket_path? }`
- `rolepod_wp_audit_many { target_ids[], report_format? }` — fan out audit across N targets, consolidate.
- `rolepod_wp_migrate_data { source_target_id, dest_target_id, scope, allow_destructive:true, confirm? }` — v0.3 supports plugin_versions only (install/upgrade dest to match source).
- `rolepod_wp_wpml_read { target_id, scope, domain?, post_id? }` — languages / string_translations / post_translations.
- `rolepod_wp_yoast_read { target_id, scope, post_id? }` — post_meta / settings.
- `rolepod_wp_rankmath_read { target_id, scope, post_id? }` — post_meta / settings.

**Adapters (3 new):**

- `wpml/read` — supportedRange 4.5 — 4.7.
- `yoast/read` — supportedRange 21.0 — 23.5.
- `rankmath/read` — supportedRange 1.0.200 — 1.0.220.

### Deps

- `@types/dockerode` added to devDependencies.
- `node-ssh` + `dockerode` (optionalDependencies from v0.0) now actually pulled.

### Pairs with

- `rolepod-wplab-companion` v0.2 (unchanged for v0.3).

### Tests

- 117 unit + smoke green (smoke asserts 46 tools).
- claude plugin validate --strict pass.
- Lint + prettier + typecheck clean.

## [0.2.0] — 2026-05-25

### Added — Companion online + Memory + Power tools

**Memory (W-028):**

- `MemoryStore` — per-site file-based storage at `~/.config/rolepod-wplab/memory/<site-slug>/` (mode 0700 dir, 0600 files). Layout: meta.json + site.md + notes.md (append-only) + conventions.md (versioned append) + runbooks/\*.md.
- `rolepod_wp_memory_recall { target_id, query?, kind? }` — substring + kind filter.
- `rolepod_wp_memory_note { target_id, content, kind?, runbook_name?, tags? }` — append note / version convention / replace runbook.
- `rolepod_wp_memory_list { target_id }` — metadata-only file listing.
- CLI: `rolepod-wplab memory show | list | clear | export`.

**Companion bridge + power tools (W-003R, W-004R):**

- `src/companion/Bridge.ts` — handshake + session token mgmt + AST pre-screen + auto-refresh on 401.
- `src/safety/AstScreen.ts` — token-blocklist screen (defence in depth with companion v0.1+ PHP-side screen). Rejects eval/assert/system/exec/shell*exec/proc_open/popen/pcntl*\*/dl/backtick/dynamic-include.
- `rolepod_wp_execute_php { target_id, payload, timeout_ms?, confirm:true }` — requires `ROLEPOD_WPLAB_PROFILE=power`.
- `rolepod_wp_introspect { target_id, scope, include_values? }`.
- `rolepod_wp_hook_state { target_id, hook, kind? }` — specialized wrapper over introspect(scope=hooks).

**Adapter writes + Bricks (W-023 extended):**

- `elementor/write.updatePageData` — auto-backup `_elementor_data` before overwrite.
- `woocommerce/write.updateProduct` + `bulkUpdatePrices` (via /wc/v3/products/batch).
- `acf/write.setPostMeta` — ACF Pro REST first, wp-cli fallback.
- `bricks/read` adapter — listPages + getPage (parses `_bricks_page_content_2`). supportedRange 1.8 — 1.10.
- 4 new MCP tools: `wp_elementor_write`, `wp_woo_write`, `wp_acf_write`, `wp_bricks_read`. All writes enforce production guard.

**Composites (5 total):**

- `rolepod_wp_scaffold_block` — generates block.json + index.js + render.php (or save) + style.css.
- `rolepod_wp_scaffold_plugin` — main PHP + readme + uninstall + optional rest_endpoint/admin_page/cli_command stubs.
- `rolepod_wp_scaffold_theme` — block-theme skeleton (style.css + theme.json + functions.php + templates).
- `rolepod_wp_audit_security` — chains wp-cli checks + writes audit-report.md/.json.
- `rolepod_wp_migrate_dryrun` — diffs two targets across plugin_versions / options / users / posts.

**Shipped skills (6 new — total 10):**

- `wp-execute-php` (companion-gated, power profile required)
- `wp-introspect` (companion-gated)
- `wp-edit-elementor` (adapter)
- `wp-audit-woo` (adapter composite)
- `wp-scaffold-theme`
- `wp-migrate-dryrun`

**MCP tools: 34 total** (was 19 in v0.1). All registered + tools/list smoke updated to assert exact list.

### Changed

- `Target` interface: `TargetKind` unchanged but bridge layer now exercises `Target.rest()` for companion communication on all target kinds (RestTarget today; v0.3 SSH/Docker via companion remote install).
- `loadProfile()` recognizes `power` profile (was placeholder in v0.1 schema).

### Pairs with

- `rolepod-wplab-companion` v0.2 — adds `/wp-cli` (bundled wp-cli proxy), `/fs-read`, `/fs-write`, `/php-session`, `/request-observer`. `execute-php` default-enabled.

### Tests

- 117 unit + smoke tests green (memory 17 + AstScreen 13 added).
- `claude plugin validate ./ --strict` passes.
- Lint + prettier + typecheck all clean.

## [0.1.0] — 2026-05-25

### Added — PoC complete (Path C foundation)

**Runtime layer:**

- `RestTarget` (W-027) — remote WordPress via HTTPS REST + optional companion. No host wp-cli, no SSH needed. Closes shared-hosting gap.
- `restClient` — Basic-Auth App Password, content-type sniffing, `?rest_route=` fallback for permalink-disabled WP, AbortController timeout, redacts auth from error context.
- `Target` interface gains `companion: CompanionStatus | null` field. `executePhp` / `introspect` are optional methods (companion-gated).
- Companion handshake at every target-open. 200 + power profile + non-prod → power tools available. Else → power tools unregistered.

**Credentials (W-018, W-027):**

- Vault interface: `add` / `get` / `list` / `remove` / `touch`.
- `KeychainVault` — macOS Keychain via `security` binary + sidecar metadata JSON.
- `FileVault` — JSON at mode 0600 (Linux + portable fallback).
- `makeVault()` — auto-detects platform; `ROLEPOD_WPLAB_VAULT=file|keychain` override.
- `canonicalizeSite()` — lowercase hostname extraction.
- `Credential` type carries `appPassword` (raw secret, never serialized to MCP responses or audit log).
- `prompt.ts`: `ask` / `askSecret` (raw-mode no-echo) / `confirm` — no extra deps.
- CLI subcommand `rolepod-wplab credentials <add|list|show|remove|test> [site]`.

**MCP tools — 19 total (was 5 in v0.0):**

Connectivity + lifecycle:

- `rolepod_wp_connect_local { path }`
- `rolepod_wp_connect_rest { url, credential_ref?, require_companion? }` (NEW)
- `rolepod_wp_disconnect { target_id }` (NEW)

Atomic surface:

- `rolepod_wp_cli_run { target_id, args, allow_destructive?, timeout_ms? }`
- `rolepod_wp_health_check { target_id }`
- `rolepod_wp_file_read { target_id, path }`
- `rolepod_wp_file_write { target_id, path, content, mode?, backup?, confirm_unsafe_path? }`
- `rolepod_wp_post_get { target_id, id, context?, type? }` (NEW)
- `rolepod_wp_post_list { target_id, type?, per_page?, page?, search?, status?, ... }` (NEW)
- `rolepod_wp_post_create { target_id, title, content, status?, ... }` (NEW)
- `rolepod_wp_post_update { target_id, id, title?, content?, status?, ... }` (NEW)
- `rolepod_wp_option_get { target_id, name }` (NEW)
- `rolepod_wp_option_set { target_id, name, value, confirm? }` (NEW)
- `rolepod_wp_user_list { target_id, per_page?, page?, search?, role? }` (NEW)
- `rolepod_wp_db_query { target_id, sql, allow_write?, confirm? }` (NEW)
- `rolepod_wp_rest_request { target_id, method, path, query?, body?, headers? }` (NEW)

Adapter-backed (read-only v0.1, W-023):

- `rolepod_wp_elementor_read { target_id, page_id?, type?, per_page? }` (NEW)
- `rolepod_wp_woo_read { target_id, scope, group?, per_page?, search?, status? }` (NEW)
- `rolepod_wp_acf_read { target_id, scope, group_key?, post_id? }` (NEW)

**Adapters (src/adapters/):**

- `_contract.ts` — `Adapter<TRead, TWrite>` interface + `AdapterUnavailableError`.
- `elementor/read` — listPages (REST), getPage (wp-cli post meta; RestTarget needs companion v0.2). supportedRange: 3.18 — 3.22.
- `woocommerce/read` — products, orders, settings_groups, settings_in_group, shipping_zones, payment_gateways via `/wc/v3` REST. supportedRange: 8.0 — 9.4.
- `acf/read` — fieldGroups, fieldsInGroup, postMeta via wp-cli + ACF Pro REST fallback. supportedRange: 6.0 — 6.3.

**Safety:**

- `DbGuard` (W-007) — SELECT/SHOW/DESCRIBE/DESC/EXPLAIN allow-list. Strips leading comments + handles `WITH ... SELECT` (CTE). `DbWriteBlockedError` on violation.
- All write tools enforce `ProdGuard` (W-008) unless `confirm: true`.

**Plugin layout (Claude Code, verified against https://code.claude.com/docs/en/plugins-reference + /skills):**

- `.claude-plugin/plugin.json` — metadata only (NO inline `mcpServers`; that lives in `.mcp.json`).
- `.mcp.json` at plugin root — declares MCP server pointing at `${CLAUDE_PLUGIN_ROOT}/dist/bin/rolepod-wplab.js`.
- `skills/wp-health-check/SKILL.md`
- `skills/wp-scaffold-block/SKILL.md`
- `skills/wp-scaffold-plugin/SKILL.md`
- `skills/wp-audit-security/SKILL.md`
- `claude plugin validate ./ --strict` passes.

Cursor + Codex + Gemini manifests NOT shipped this release — schemas not yet verified per SCHEMA-BOUND-file policy. Deferred to v0.5.

**Tests — 87 green:**

- `tests/unit/server.test.ts` (18) — server boot + AllowList + ProdGuard + FsScope
- `tests/unit/TargetRegistry.test.ts` (8) — lifecycle + idle close + collision
- `tests/unit/profile-load.test.ts` (6) — env / file / malformed
- `tests/unit/stripPhpNoise.test.ts` (9)
- `tests/unit/credentials.test.ts` (16) — canonicalize + FileVault round-trip + permissions + corruption
- `tests/unit/restClient.test.ts` (12) — https-only + auth header + URL form fallback + timeout + redaction + query encoding
- `tests/unit/DbGuard.test.ts` (15) — SELECT/SHOW/DESC/EXPLAIN allow + CTE + INSERT/UPDATE/DELETE/DROP/TRUNCATE/ALTER block + comments
- `tests/smoke/mcp-handshake.test.ts` (3) — spawn dist + JSON-RPC + tools/list asserts 19 tools

### Changed

- `Target.kind` type now includes `'rest'`.
- `runtime/wpCli.ts` adds `stripPhpNoise()` filter for wp-cli 2.12 on PHP 8.4 deprecation noise.
- `wp_health_check` DB probe switched from `wp db check` (needs host mysqlcheck) to `wp option get siteurl` (PHP mysqli only).

### Pairs with

- `rolepod-wplab-companion` v0.1 — optional WP plugin providing `/wp-json/wplab/v1/{handshake,introspect,execute-php}` endpoints. execute-php DISABLED by default in v0.1.

## [0.0.0] — 2026-05-25

### Added — personal-use scaffold

- Repo plumbing: TypeScript + tsup + vitest + oxlint + prettier.
- MCP server skeleton (stdio).
- `LocalTarget` runtime (wp-cli + REST stub + scoped fs).
- 5 atomic tools.
- Safety primitives: AllowList + FsScope + ProdGuard.
- 3-profile loader (`strict` / `personal` / `power` slot).
- Docker WP fixture (`tests/fixtures/`) for local dogfood.
- 44 unit + smoke tests.
