Bricks
Inject paper cuts closed; element-name validator with did-you-mean shipped in v6.11.0; _cssGlobalClasses surfaced as a first-class property on every element schema
Stakeholder: A Bricks template author
Every adapter to deeper-intelligence parity.
Self-Heal v6.11 was the cracks-of-gold. Bloom v7.0 is the seven flowers, one per builder. Render-validation, universal trace, Variables CRUD, source-driven catalog, per-property validators: five cross-cutting invariants every adapter inherits, so the silent-fail family bugs that defined the v6.10.x cycle stop repeating across builders.
Functionally compatible with WordPress 7.0 RC2: 133 abilities register cleanly, zero deprecation or warning during init, the canonical REST surfaces respond on every running localhost env. The PHP 8.1+ gate is removed so WebMCP boots on PHP 7.4 / 8.0 sites. The composer abilities-api constraint accepts 0.4 / 0.5 / 1.0 so the plugin tracks the version core ships with at GA. The too-early-call guard returns a structured 425 instead of silent-empty when wp_get_abilities() runs before init.
The Abilities API in 7.0 is a real signal that core is taking AI editing seriously. Bloom works alongside the API, not against it. Source: our WP 7.0 stance.
Seven flowers
One vase per builder. Each adapter inherits the cross-cutting Phase A invariants and ships its own first-slice deeper-intelligence work in v7.0, with the rest of each track sequenced into v7.0.x point releases.
Inject paper cuts closed; element-name validator with did-you-mean shipped in v6.11.0; _cssGlobalClasses surfaced as a first-class property on every element schema
Stakeholder: A Bricks template author
Runtime-aware emit branching closes the silent-empty-render arc on Divi 4 sites; D5-shaped payloads downconvert to et_pb_* shortcodes when the site runs Divi 4
Stakeholder: A Divi 5 calibration trial
$$type envelope-preserving writer for atomic v4 widgets; round-trip preserves the typed prop-type contract instead of overwriting it with a bare string
Stakeholder: An Elementor v4 trial running migrated Variables + Global Classes
CSS regen force-load fixes the silent-empty-render arc; component primitive auto-catalog 7 → 42+ entries via filesystem scan
Stakeholder: An Oxygen Classic 4.9 trial customer
content_field_map 7 → 50 modules including PowerPack and Beaver Themer prefixes; Box-first design framework support
Stakeholder: A Beaver Pro agency adopting Box-module-first design
Widget shortcut map 20 → 70 entries covering every shipping EssentialElements element; render-fidelity F3 + F4 closed in v6.11.3
Stakeholder: A Breakdance pro-services integrator
Builder-slug aliasing (vc / js_composer / visual-composer all route correctly); catalog auto-scan 1 → 87 vc_* shortcodes via filesystem
Stakeholder: Recruiting production volunteer sites pre-tag
Cross-cutting Phase A
The Phase 5 foundation. Each shipped as its own alpha (alpha.1 through alpha.7) with standalone test coverage and per-builder integration. 342 passing assertions across the seven foundation files.
Post-write check on every adapter: HEAD-fetch the preview URL plus parser-walk the persisted blob using the parser the adapter declares (9 parsers ship). Either failing surfaces partial_write=true with structured parser_error / preview_error so REST callers can flag a write that landed but may not render.
Every adapter exposes build_write_trace($overrides) which returns the canonical response envelope: route, target, path_resolved, validator_warnings, render_validator_pass, partial_write, parser_error, preview_status, preview_response_ms, css_regen_strategy. Per-builder strategy declarations cover Bricks (bricks_generate_css_file_hook), Elementor (elementor_meta_only), Beaver (beaver_delete_asset_cache), Breakdance (breakdance_clear_static_cache), Divi (divi5_clear_and_warm / divi4_clear_and_warm), Oxygen (oxygen_vsb_force_cache_regen).
Universal contract for /respira/v1/builder/{builder}/variables/{type} across color, typography, spacing, breakpoints. Five REST routes (GET list, POST create, GET read, PATCH update, DELETE). A single Figma-to-builder pipeline can now operate without per-adapter URL knowledge. Adapters opt in via supports_variables() override.
Replaces hand-curated module registries with first-use filesystem scans. Each adapter declares its source dir and a per-file extractor; Respira_Module_Catalog_Scanner walks, runs the extractor, and caches the discovered catalog for 1 hour. WPBakery and Oxygen ship concrete scanners in v7.0; the rest follow in v7.0.x.
12 canonical type slugs (color / dimension / url / integer / number / boolean / string / enum / font_family / image_url / svg / class_list) with type-specific opts. Closes the data-vs-render gap shape from the v6.10.x family bugs: pre-Bloom values that looked valid at write time but mismatched the renderer slipped through; the byte-level verify confirmed persistence; the page silently rendered wrong. Bloom centralises type checks into one vocabulary every adapter speaks at write time.
Arcs
Four independent customer trials surfaced bugs that share a shape: API returns success: true while persistence is wrong for the version on the site. Bloom names the pattern and ships fixes as a class.
A trial customer running Oxygen Classic 4.9.6 spent two months reporting fresh respira_build_page calls produced postmeta blobs that looked structurally correct but rendered as a blank section between the active theme's header and footer. v6.8.3 fixed the prefixed-meta-key migration. v6.10.1 fixed a malformed -0 selector. v6.10.3 + v6.10.4 fixed a double-nested ct_options shape. v6.11.0 allocated integer ct_ids instead of string GUIDs. The empty render persisted into v6.11.2.
Fix. Bloom Phase B Oxygen 3.8.b: oxygen_vsb_cache_page_update is declared inside files Oxygen only includes on admin_init. REST writes happen before admin_init, so the function was undefined and the cache regen silently skipped on every Respira write, not just on the reporting site. The fix walks include locations and require_once's any that exist before invoking, so the per-page CSS file actually regenerates after every write.
A 34-element all-elements demo built via /respira/v2/builder/build returned success: true, element_count: 34 on both Divi 4 and Divi 5 envs. Divi 5 site fatal'd on render. Divi 4 site rendered an empty page (div.et_builder_inner_content with no children) because the adapter wrote D5 block format to a D4 site, and WordPress has no block-handler for divi/* blocks.
Fix. Bloom Phase B Divi F1: runtime-aware emit branching. D5 runtime always emits D5; D4 runtime never emits D5. D5-shaped payloads on D4 sites run through downconvert_divi5_tree_to_divi4() before the shortcode emitter so writes land as [et_pb_*] the D4 renderer understands. The render-validation gate from Phase A.1 + the alpha.2 trace surface partial_write so future regressions can't repeat this arc silently.
Three independent customer trials hit the same family-bug shape: API returns success: true while persistence is wrong for the version on the site. Different builders, same pattern. Breakdance trial abandoned mid-flight after stacked silent-fail tool calls. Beaver live-test 2026-05-07 surfaced an analogous silent-update flow. WPBakery vc_btn attribute drops on inject.
Fix. Bloom names the pattern and ships fixes as a class. Render-validation gate post-write. Universal trace fields. Per-property validators close the data-vs-render gap shape that produced the family. Auto-catalog from source so the LLM stops falling back to HTML it knows when the schema doesn't show the convention.
An Elementor v4 trial site running migrated Variables and Global Classes. Pre-Bloom respira_update_module on an atomic e-heading widget overwrote settings.title = { $$type: "html-v3", value: "..." } with the bare string from normalize_text_setting_value. The renderer crashed (ArrayUtility::get_value() on a string) or silently no-op'd because the typed-prop reader expected an array.
Fix. Bloom Phase B Elementor 1.1: $$type envelope-preserving writer. atomic_widget_content_field() maps each of the 12 atomic widgets to its content field + envelope type. apply_atomic_envelope_write() detects existing envelopes and updates value while preserving $$type and extra fields like classes. Audit P0-3 closed.
Customer ledger
Every customer fixture from the v6.11.x cycle becomes a permanent regression case in v7.0. Replay tests run as part of the release gate; future Bloom-shape arcs catch on the test suite before they ship.
Posture
Pre-Bloom adapters were written to "produce a persisted blob the builder accepts." Bloom adapters must also produce output the builder renders correctly. A higher bar that surfaces a class of bugs (silent empty renders, typed-envelope corruption, wrong-version emit, dropped attributes) the older bar didn't catch.
Render-validation post-write. Universal trace on every response. Variables CRUD shape across every builder. Source-driven catalog. Per-property typed validators. Five invariants every adapter inherits going forward, so the v6.10.x family-bug shape stops repeating.
Patch line
v7.0.x stays patch-only for customer-blocking bugs and completion of the v7.0.0 deep-intelligence promises. Architecture work belongs in v7.1. Each entry below ships the structural close behind a single customer arc or audit gap; no breaking changes; cumulative roll-forward from any 7.0.x.
v7.0.20 stopped build_page writing empty properties: {}, but a deeper layer was still off. Content landed at properties.content.<control> while Breakdance's renderer reads properties.content.content.<control> — the content key is doubled: properties.content is the tab, properties.content.content is the section within it. S.M. on the customer site reported the exact symptom after upgrading to v7.0.16: extract_builder_content round-tripped the content fine, but the live page rendered Breakdance defaults — "Click Here" button, no heading text — because the renderer found nothing at the path it reads. Confirmed against Breakdance 2.7.2's own element definitions: heading's defaultProperties() returns ['content' => ['content' => ['text' => '...']]], and the Button, Badge, Rich_Text, Text, Animated_Heading, Icon, Icon_Box, Blockquote, Basic_List, Checkmark_List, Pricing_Table renderer twigs all read content.content.*. 7.0.21 adds nest_breakdance_content_section(): for the 15 element types whose content section is confirmed to be content, it wraps the resolved content payload one content level deeper before persisting. Idempotent — a payload already nested under content (a fully-native payload, or one a prior pass wrapped) is left untouched; element types outside the confirmed set pass through unchanged, no worse than v7.0.20. Image2 is intentionally excluded (its content section is image, not content); HTML_img is included. Verified end-to-end on Studio (WP 6.9.4 + Breakdance 2.7.2): a build_page REST call with {type, data: {content: {text, tags}}} persists properties.content.content.{text,tags} in _breakdance_data, and the front-end renders <h1 class="bde-heading">REST PATH HEADING</h1> — the real content, not the "This is a heading." default. This closes the content path for the common Breakdance content elements; element-specific design properties (the Badge's colour, spacing, typography) live under properties.design.<section>.<control> where the section name varies per element type — a full per-element-type design schema is the remaining piece, queued for v7.1.
Reported by I.M. on the customer site (Breakdance 2.7.2): respira_build_page assembled the element tree correctly — every type, nesting level and column layout preserved — but every element persisted with empty "properties": {}, so the page rendered blank. He worked around it with 92 individual update_element calls (one per element) on a 9-section page. Same blank render another customer reported earlier on the same Breakdance version. Root cause: complexify_structure read content only from $item[attributes] — the canonical Respira simplified shape produced by extract_builder_content. But a build_page caller has no extract to copy from, so the agent invents a shape, and the shape it naturally invents is the near-native {type, data: {content: {...}}} (because that mirrors how Breakdance stores nodes and how extract_builder_content presents data.type + data.properties). $item[attributes] was empty for those payloads, so complexify_structure fell through to an empty properties object on every element. 7.0.20 adds resolve_breakdance_properties(), which accepts every shape an agent emits, in priority order: attributes.{content,design,_raw} (canonical Respira); data.properties (fully native, verbatim); data.{content,design} (the build_page near-native shape the customer hit); properties at the item root; content/design flat at the item root; and a flat attributes map for legacy back-compat. complexify_structure also now reads element type from data.type when it is not at the item root, so fully-native payloads do not silently default to EssentialElements\Text. Same report flagged a second bug: update_element returned 500 respira_no_applicable_updates on flat element-specific keys (build_page-style button_text, or title / description / items for IconBox / CheckmarkList / AdvancedAccordionContent) because apply_breakdance_updates recognised only text / content / design / attributes. It now folds any non-structural updates payload into content via array_replace_recursive — the same settings-patch normalisation the base-class update_element already does. Many Breakdance elements nest their fields under content (IconBox.content.title, CheckmarkList.content.items), so a flat patch lands correctly for those; for the few with a deeper shape it writes recoverable data instead of hard-failing. Full per-element-type key schemas are queued for v7.1. Verified end-to-end on Studio (WP 6.9.4 + Breakdance 2.7.2): build_page with the customer's exact {type, data: {content: {...}}} payload persists populated data.properties on every content element, and update_element with a flat non-structural key returns 200 instead of 500.
P.L. verified the v6.11.1 Divi 4 per-attribute writer end-to-end on a production install and filed three separate issues, all returning success: true while doing the wrong thing. #29: a flat updates payload like {"button_text":"Go"} silently no-op'd — the v6.11.1 writer only honoured the nested {"attributes":{...}} shape, so it walked the shortcode, ran wp_update_post, advanced post_modified, and persisted nothing. 7.0.19 accepts both shapes and returns a structured respira_no_updates_applied 422 when a write genuinely changes nothing, instead of faking success. #28: every update_element on a Divi 4 page ran the whole post_content through kses, stripping script/style tags out of unrelated et_pb_code modules — the v6.11.0 kses-bypass shipped for the duplicator was never extended to the per-attribute writer. 7.0.19 wraps the update_element and rewrite_legacy_shortcode writes in that same bypass via try/finally so the filters are always re-armed. #30: identifier_type=content resolved the outermost section instead of the deepest match, so a content update flattened the whole section (a 7568-byte page collapsed to 510 bytes in the repro), and identifier_type=type ignored match_content so it always hit the first shortcode of the type. 7.0.19 resolves content matches to the innermost shortcode (mirroring find_element), refuses a content update on a shortcode that still wraps nested modules, and threads match_content through the type resolver. Each fix ships with a regression test in tests/standalone/v6111-divi4-shortcode-write.php — 75 assertions, all green.
M.P. on the customer site field-diffed a working native Oxygen page against a Respira-built page that renders blank — same site, same theme, same Oxygen version, same Respira plugin — and pinned the cause: the _ct_builder_json root structure. Native Oxygen Classic stores _ct_builder_json as a single root object {id:0, name:"root", depth:0, children:[...sections...]}, and Oxygen's front-end renderer reads that meta directly and traverses .children off the root. Pre-7.0.18 every Respira write path (inject_content, create_code_block, renumber_page, and the extract_content self-heal) persisted a bare top-level array [...] — no .children, no name:"root" marker — so Oxygen treated the page as having no content and emitted nothing, with no PHP error. Worse, extract_content's self-heal write actively stripped the root wrapper off native Oxygen pages the first time Respira read them, so a page that rendered fine before a Respira read could go blank after one. 7.0.18 adds symmetric wrap_oxygen_root() / unwrap_oxygen_root() helpers: every write to _ct_builder_json now persists the root-wrapped shape, and every read unwraps it to a bare components array first. unwrap tolerates both shapes, so legacy Respira pages still stored as bare arrays keep working and self-heal up to the canonical shape on the next read. Verified end-to-end on Studio (WP 6.9.4 + Oxygen Classic 4.9.7): build_page writes the root wrapper, the front-end renders the headline, and an extract round-trip preserves the wrapper instead of stripping it. M.P.'s diff flagged three other differences from native pages — missing ct_sign_sha256 shortcode signatures, _wp_page_template=ct_template vs default, and the absent _ct_template_* postmeta family — but Studio renders a Respira-built page correctly with the root-wrapper fix alone and without any of those, so they are cosmetic / editor-GUI-state differences, not the render-blocker. They are queued for v7.1. Oxygen 6 sites are unaffected — they route through the Oxygen 6 adapter and _oxygen_data, a different storage format.
Two follow-up customer reports landed within hours of v7.0.16. P.S. on the customer site retested with traditional BB rows + columns and reported every column rendering blank — he confirmed via screenshot that the Column → Style → Width field was empty in the BB editor and that manually re-typing "100" lit up the page. Root cause: Respira flatten was writing column nodes with empty settings when the agent did not specify a `size` attribute (the canonical shape Claude and Codex emit when prompted for rows + columns + modules), and BB treats a missing settings.size as 0% width — every nested heading / text / button rendered behind a zero-width container. 7.0.17 defaults missing column `size` to "100" during flatten, so single-column rows render full-width as expected. Multi-column rows where the agent specifies neither per-column sizes nor a row-level column_structure still get "100" per column for now; equal-distribution math (100/N) lands in v7.1 alongside the broader Box-module-settings parity work. M.P. on the customer site retested on v7.0.14 with WP Rocket fully deactivated and confirmed Oxygen inject still returns HTTP 500 with the German WP critical-error page, so v7.0.12 closed one crash site but a second uncaught throw downstream is still active. The throw site is any third-party plugin hooked into Oxygen's `oxy_save_ct_builder_json_meta` action chain or the generic `updated_postmeta` hooks WP fires on every update_post_meta call. 7.0.17 wraps the entire Oxygen inject body in `try/catch \Throwable` and surfaces the exception via WP_Error with `exception_type` + `exception_file` + `exception_line` + a `hint`. The MCP `extractServerErrorDetails` extractor at v6.11.11+ already reads those fields and folds them into the `Debug details` line on every 500 response, so the customer sees the offending plugin's PHP file + line through the MCP client without needing WP_DEBUG_LOG access on the origin server. Verified end-to-end on Studio (WP 6.9.4 + BB Pro 2.10.2) — P.S.'s exact "Test Page 11" brief shape lands with `settings->size = "100"` on every flattened column.
Five customer reports rolled into one patch. Mihai on the customer site: Cursor agent could not call respira_read_page or the v2 GET /pages/{id} against Divi 4 financial calendar pages (IDs 82599 + 81974) because those pages are a custom post type, not the standard "page" type, and both handlers hard-coded 'page' !== post_type and returned a misleading 404. 7.0.16 drops the post_type gate on get_page (v1) and get_page_v2 — both now respond as long as the post exists. update_page_v2 also stopped hard-coding "page" when resolving the mutation target; it now reads the actual post_type, so CPT writes go through the same duplicate-before-edit safety flow as standard pages. N.M. on the customer site: respira_update_element reported "Element updated successfully" but writes never surfaced on the live page, while the workaround using update_module with editTarget:live worked. Root cause: element-ops endpoints wrote directly to the URL-path post_id regardless of duplicate state, while page-level surfaces routed through the duplicate-vs-live resolver. 7.0.16 routes update_element through the same resolve_mutation_target flow: editTarget=duplicate (the default) goes to the Respira duplicate and signals approval via Respira → Changes; editTarget=live writes straight to the published original when direct-edit is enabled. The response envelope always carries target_id, original_id, edit_target, is_duplicate, duplicate_created, post_status, and post_type. P.S. on the customer site: two distinct Beaver Builder issues. (a) build_page returned success: true + render_validator_pass: true for a rows-only structure but the rendered page had empty Row shells with no nested columns or modules — the typed-node normalizer was rewriting alternative child keys (columns, cols, modules, elements) to children only on untyped nodes, so a typed {type:"row", columns:[…]} payload slipped past unchanged and flatten_tree_recursive walked children and found nothing. Now the rewrite fires on typed nodes too, in-place, recursively, never overwriting an existing children array. (b) inject_builder_content silently overwrote existing content because mode defaults to replace with no confirmation gate. The endpoint now returns 409 respira_replace_confirmation_required when the page already has content and the caller hasn't passed mode="append" or mode="replace" + confirm_replace=true. Pages with no existing content still accept replace without the confirmation flag — that's a clean write, not an overwrite. K.B. on the customer site: respira_update_element against Bricks 2.3.4 reported "Element updated successfully" but find_element immediately after showed unchanged content. 100% reproduction across pages 25 + 30. Root cause: the base-class update_element did array_merge($element, $updates) at the element root, so update_element({...identifier..., updates: {content: "new"}}) added 'content' onto the element root where no renderer reads it (Bricks reads settings.content for heading, settings.text for text-basic; Elementor reads settings.title / settings.text; etc.). Worse, when the agent did pass {settings: {text: "new"}} the shallow array_merge overwrote the entire settings dict and lost every other styling key on the element. 7.0.16 normalises updates in two passes: (a) if updates carries no element-structural keys, wrap it as {settings: updates} so the documented contract works; (b) when updates does set settings, deep-merge that patch into the existing element's settings via array_replace_recursive instead of overwriting. Affects every builder that uses the base-class update_element — Bricks, Elementor (delegates to parent after the atomic-elements check), Divi 5 (delegates to parent), Beaver, Oxygen, Brizy, WPBakery, Visual Composer, Thrive, Flatsome. Breakdance and Divi 4 use their own writers and stay unchanged. Same K.B. report also caught the MCP server self-identifying as v6.11.4 after a clean npm upgrade — the constant was hand-bumped at the 6.11.4 release and never tracked subsequent publishes. v6.11.13 reads the version from package.json at module load via the same helper pattern already used by MCP_CLIENT_VERSION in wordpress-client.ts, so the handshake, the instructions block, and the version-checker all report the actual installed version. D.D. on the customer site, Divi 5.3.3: respira_build_page + respira_update_module correctly persisted background_overlay_color to the section block (extract_builder_content showed overlay.enabled: "on" plus the right color) but the front-end emitted no overlay div and no overlay CSS, so the image rendered at 100% opacity and text on top was unreadable. Root cause: the Divi 5 mapper wrote overlay.enabled (past participle) while the Divi 5.3.3 section renderer reads overlay.enable (verb form), matching the convention Respira already uses for button.decoration.button.desktop.value.enable. 7.0.16 writes BOTH keys defensively so the overlay renders on every Divi 5 minor release regardless of which key the renderer is currently observing. Covers section, row, column, and every other Divi 5 module that exposes background_overlay_color through the shared generic mapper. Companion @respira/wordpress-mcp-server v6.11.13 makes wordpress_extract_builder_content auto-detect the active site builder when the builder arg is omitted, exposes editTarget on wordpress_update_element, threads confirm_replace through wordpress_inject_builder_content, fixes the stale version self-report, and teaches wordpress_diagnose_connection to detect edge-layer write blocks: the diagnostic now probes OPTIONS /wp-json/respira/v1/ping alongside GET, and when GET returns 2xx but OPTIONS returns 4xx/5xx it surfaces a CF-specific recommendation (the customer's CF custom rule "Bad Bot - Action Block" blocked PUT/PATCH/DELETE/OPTIONS at the edge for every path, including /wp-json/respira/, so every Respira write failed at the edge with no plugin-side trace; the fix is the literal CF expression `and not (http.request.uri.path contains "/wp-json/respira/")` appended to the rule).
Four customer-facing fixes in one hotfix. Mihai on the customer site and P.S. on his own dashboard ("1 connected site, 1 needs attention, missing token, MCP Connection status: Pending") both reported the same shape: clicking Connect Automatically left the dashboard exactly where it was, no respira.press handshake, no license saved. Root cause: handle_license_form_submission was only called from the page-render callbacks, which run after wp-admin/admin-header.php has emitted output, so the wp_redirect to respira.press/wp-auth issued "Cannot modify header information" warnings and silently dropped on the floor. 7.0.15 wires the handler on admin_init so the redirect fires before any output, and replaces the null parent slug on the legacy hidden admin pages so PHP 8.1+ strpos(null,...) deprecations stop emitting HTML before admin_init runs (which on debug-display sites would re-break the same redirect). D.D. on the customer site reported two more: respira_install_plugin fataled with "Call to undefined function plugins_api()" on first call (the wp-admin/includes/plugin-install.php + plugin.php loads needed in REST context were missing), and respira_delete_page returned 200 with site metadata but never deleted the page (the MCP client typed deletePage as Promise<void> and discarded the WP response, never sent confirm_live_edit=true alongside force=true). Both fixed: install now adds the two required wp-admin includes before the upgrader runs, delete now returns the WP body verbatim and auto-sends confirm_live_edit=true. K.B. also reported two Bricks bugs (wrong meta key, flattened hierarchy) in the same window — investigated on Studio with Bricks 2.3.4 and both already-fixed on v7.0.14: Bricks 2.x own source defines BRICKS_DB_PAGE_CONTENT = "_bricks_page_content_2" (Respira writes there), and the section→container→element parent references round-trip cleanly. K.B.'s MCP 6.11.4 was stale relative to v7.0.14. All four real fixes verified end-to-end on Studio (WP 6.9.4 + PHP 8.4 + Bricks 2.3.4) against the v7.0.14 customer code line.
Supersedes the yanked 7.0.13. 7.0.13 shipped the Beaver Builder fix below but failed to activate on every PHP 7.4 site with the customer site error "Plugin could not be activated because it triggered a fatal error. Parse error: syntax error, unexpected 'public' (T_PUBLIC), expecting variable (T_VARIABLE) in.../includes/sdk/class-tool-descriptor.php on line 37" — the 7.0.x add-on SDK used PHP 8.0+ constructor property promotion in four files and the bundled WebMCP bridge used a match expression, union return types, and named-argument call syntax, but the plugin header still declares Requires PHP: 7.4. 7.0.14 rewrites every PHP 8.0+ construct to PHP 7.4-compatible equivalents — no behaviour change on PHP 8.x — and carries the Beaver fix forward unchanged. Beaver fix recap (reported by P.S. / after a dozen test pages on LocalWP): every Box-rooted inject_builder_content call against BB 2.10.2 failed with code=validation_failed message="Module X must have a parent column." Claude Desktop, on the back of those failures, concluded "Respira does not support BB 2.x Box modules." The plugin actually does — v6.11.2 had already taught complexify_structure and flatten_tree_recursive to treat Box (plus wrapper, *-container, fl-* component primitives) as root-capable layout nodes and not wrap them in row/column. But Respira_Beaver_Validator::validate_module was still running the legacy strict rule on the flat node map and rejecting any module with parent=null. P.S.'s Claude was emitting BB's flat-node-map shape ({nodeId: {type, parent, settings: {type: 'box'}}}), which takes the strict legacy path. The validator now exempts root-capable BB primitives from the "must have a parent column" rule, matching is_root_capable_beaver_node on the builder class. Non-root-capable orphans still trip the rule, but complexify_structure auto-wraps them in row/column before validation runs. Verified end-to-end on Studio with BB Pro 2.10.2 (flat-node-map Box payload → 200 with fl-module-box markup) and on a real PHP 7.4.33 + WordPress 6.9.4 differential test (unmodified 7.0.13 reproduces the customer site fatal verbatim and stays inactive; 7.0.14 activates clean, full Respira admin UI renders, and the REST surface registers respira/v1, respira/v2, and webmcp/v1).
Reported by M.P. on the customer site (Oxygen Classic 4.9.6 + WP Rocket). Every Respira write against an Oxygen page (inject_builder_content, add_heading, add_text, every widget shortcut) returned HTTP 500 internal_server_error with the WordPress German critical-error page. Pre-7.0.9 the same calls returned an orderly validation_failed; 7.0.9 loosened the validator so writes routinely reached a code path with two latent traps. Trap one: force_load_oxygen_cache_helpers called do_action('admin_init') synchronously inside a REST request as a "last resort" to load Oxygen's cache helper, which fired every other plugin's admin_init hooks mid-write — WP Rocket's cache-purge handler in particular is not designed to run from REST. Trap two: nothing wrapped the cache-invalidation hooks (oxygen_after_save_components_meta especially) so any third-party hook that throws fatals the entire REST request even though the DB write already succeeded. Customer-visible side effect on M.P.'s site: the live page rendered blank because WP Rocket purged the cache on the failing write and rebuilt against an inconsistent render state. Fix removes the do_action('admin_init') last resort entirely (Oxygen rebuilds its cache on next front-end render anyway) and wraps clear_oxygen_cache in a try/catch \Throwable. End-to-end verified on the Studio Oxygen 4.9.7 env: revert the patch + add a throwing oxygen_after_save_components_meta hook → identical HTTP 500 internal_server_error to M.P.'s report; re-apply → 201 with the data written. Recovery for affected sites: clear WP Rocket cache for the affected page once after upgrading to 7.0.12.
Reported by Mihai 2026-05-11 against the Respira → Changes admin page: clicking the "Delete all" button on a post card opened the confirmation modal positioned in the document-flow top-left corner above the WP admin chrome, instead of as a centered overlay. Root cause: the modal HTML in admin/views/changes.php uses class names.respira-modal-backdrop /.respira-modal-dialog /.respira-modal-footer that had no CSS anywhere in the plugin, and the only existing.respira-modal rules were scoped under.respira-admin while the page wraps in.respira-page-body. The same shape affected the Preview Modal that opens when clicking a version. Fix adds ID-scoped CSS for both #respira-confirm-modal and #respira-preview-modal (so positioning is independent of wrapper class) covering position:fixed inset:0 wrapper, semi-opaque backdrop, centered dialog with max-width, header / body / footer layout, and a mobile fallback at ≤600px. Validated end-to-end via headless browser on the Studio test env: modal renders centered with a working backdrop click-to-close.
Reported by Mihai on a customer site: every non-homepage page rendered blank while the Respira plugin was active. Disabling the plugin restored the pages immediately. Trigger was a Codex session that edited a few pages via Respira, toggled a plugin setting, and published a post — one of those steps left the frontend in a broken state that only manifested with Respira loaded. Root cause: a wp_head priority-100 hook (output_page_custom_css, in the codebase since v1.0.2) was reading _et_pb_custom_css (Divi per-page CSS) or _elementor_page_settings.custom_css from the current post and emitting it as a <style> tag at the very end of <head>. Divi and Elementor render their own per-page custom CSS via their native frontend pipelines (Divi's static CSS file generator, Elementor's page-settings regenerator). The Respira-side duplicate was unsafe: ran at priority 100 so anything it carried won the cascade; read live postmeta so if _et_pb_custom_css was newer than Divi's static cache the override could blank the page; is_singular('page') excluded the homepage on most sites so the homepage rendered fine while every other page broke (matching the reported symptom exactly). Any respira_update_page call carrying custom_css fed straight into _et_pb_custom_css and rendered through the unsafe hook before Divi's cache regenerated. Removed the add_action('wp_head',...) registration. The function stays in the codebase for back-compat with any direct callers. Divi/Elementor render their custom CSS independently as before, so removing the hook does not regress any feature. Recovery: update + clear page caches; affected sites should restore immediately.
Reported by M.P. on the customer site: five inject_builder_content variants (A through E) against the same Oxygen page all failed with the opaque "HTTP 500 Content validation failed. code=validation_failed" message and no insight into which rule fired. The original page was intact; the validator was simply rejecting before the DB write. Plugin side: the Oxygen validator now accepts ct_id as a fallback for the generic id field on native Oxygen payloads (the v6.11.0 numeric-ct_id allocator mints integer ct_ids further down the pipeline, so this teaches the validator that ct_id IS the id Oxygen cares about). Option-value semantic checks (color formats, enum allow-lists, numeric coercion) dropped from the blocking path — the Oxygen renderer ignores unknown keys silently, so pre-write rejection on those was producing false blockers. Shape check (options must be an array) stays. WP_Error message now reads "Content validation failed (N rules, first: <rule>)" and the data envelope carries errors, warnings, rule_count, hint inline. MCP 6.11.11: extractServerErrorDetails reads data.errors / data.warnings / rule_count and renders them as validator_errors=<rule1>; <rule2>... in the Debug details line (pre-6.11.11 these were silently dropped). Also strips UTF-8 BOM in readConfigFromPath + readConfigFromBase64 so PowerShell Set-Content -Encoding UTF8 no longer fails JSON.parse with an "Unexpected token '\ufeff'" error (reported as Cowork bug B2 on Windows 11).
Reported by Mihai on a customer site (Divi 4.27.6, PHP 8.2.25, plugin 7.0.5): every GET /wp-json/respira/v1/options/{name} returned WordPress's generic "There has been a critical error" HTML with no JSON body and no X-Respira-Plugin-Version header. POST/DELETE on the same route worked. /wp-json/respira/v1/diagnostic/report and /wp-admin/post-new.php also 500'd. The v7.0.4 key-backfill option carried only the opaque failed:<timestamp> stamp with no diagnostic context. Root cause: the read-option handler called wp_cache_delete('alloptions', 'options') before reading. That global flush triggers WordPress's pre_cache_alloptions rebuild which fatals on certain third-party / PHP 8.2 strict-typing combinations. The per-option flush stays (covers the original LiteSpeed staleness case the 6.7.3 fix addressed). Added defensive envelopes so the next bug report carries the actual cause: get_option REST handler wrapped in try/catch, surfaces respira_option_read_failed with class/message/file/line; Respira_Diagnostic::get_report wraps every section independently so individual helper failures render _section_error/_fallback instead of taking the whole report down; maybe_auto_backfill wraps run_backfill in try/catch and writes a structured respira_keys_backfilled_v704_last_error option carrying class/message/file/line/status alongside the existing failed flag.
Live-verified the v7.0.0 promises against licensed Oxygen Classic 4.9.7, Oxygen 6.1.0-beta.1, and Breakdance 2.7.2 on the new Studio test env. Fixed: Oxygen Classic empty-render on fresh installs (CT_VERSION ≥ 4.8.3 forces prefixed meta keys regardless of admin-init migration options, same shape as the M.P. arc but reproducible on a virgin install); Oxygen 6 reporting the wrong version (4.9.7 instead of 6.1.0-beta.1) and the wrong primitives (Classic ct_* docs instead of OxygenElements); postmeta-orphan detection false-positive after Oxygen 6 to Breakdance switch; Breakdance catalog 68 to 158 elements (scanner now walks subplugins/breakdance-elements/elements with the registerElementForEditing identifier source); render-validation gate skips non-published posts (no more false-positive partial_write=true on every draft inject). Added Oxygen Classic Variables CRUD (color and breakpoints), closing the last 501 on the universal Variables shape.
Latent bug present since v6.11.0, never observed in production because the canonical pilot trial auto-updated past v6.11.3 straight to v7.0.5 where the v7.0.5 verification trace surfaced 76 parent==child Text duplicate pairs in the converted Breakdance tree on a 100KB+ source page. Root cause: coalesce_redundant_text_wrappers cleared only content.text on the collapsed parent, but Breakdance's Text writer reads content.html first and falls back to content.text only when html is missing, so the writer kept emitting the parent Text widget from the still-populated html field. Fix clears both keys; standalone test extended to assert both unset after collapse.
Until this release, tokens minted via /dashboard/mcp on respira.press never made it into the plugin's local wp_respira_api_keys table. They were validated at request time via introspection only, which meant the plugin's "Your API keys" admin couldn't list them and Disconnect was silently a no-op. v7.0.5 ships POST / GET / DELETE /respira/v1/admin/keys plus a one-time backfill rake. Also strips the v7.1 alpha preview banner and submenu that leaked on production sites running WP_DEBUG, and folds in the v7.0.4 approval-flow URL-route-params fix (preflight now merges $request->get_url_params() with get_params() so plugin tool slugs survive into the signed approval token).
A counter built via build_page rendered "%" with no number even though the payload sent number: "97". Root cause: complexify_divi5_blocks routed number into the typed innerContent.desktop.value envelope, but the canonical Divi 5 schema declares number-counter self-closing. The renderer reads number, percent_sign, and subtitle straight off the top-level attribute bag. Same family as the v7.0.2 gallery show_title_and_caption fix; same hotfix shape. New tests/standalone/v703-divi5-number-counter-flat-keys.php (8 assertions) locks the routing.
Two Divi 5 adapter bugs with clean repros, shipped same-day. divi/gallery silently dropped gallery_ids, posts_number, show_title_and_caption, fullwidth (now route through the same simplifier+complexifier path divi/slider has used since 6.10.1). Schema endpoint returns 200 for divi/gallery, divi/slider, divi/slide instead of 404. build_page response now emits the v7.0 Bloom universal write-trace envelope. inject_builder_content already had it; build_page was the one route never wired up, so callers that fail-fast on partial_write were running blind on every build_page write since 7.0 GA.
The post-Bloom hardening pass. Variables CRUD fans out concrete adapter implementations across every remaining builder (Bricks → bricks_color_palette / bricks_global_variables / bricks_breakpoints, Beaver → respira_beaver_variables 4-slot option, Divi → et_global_colors native shape, Elementor → Atomic v4 $$type envelope preserved through round-trip, Breakdance already shipped in Bloom alpha.26). Per-builder catalog auto-scan fans out (Bricks ~70 elements, Beaver primary-file invariant, Breakdance getMetadata, Divi 4 ET_Builder_Module, Elementor widget_v3 vs widget_v4 by base class). PHPStan level-5 CI gate scoped to the 4 Phase A high-leverage classes; szepeviktor/phpstan-wordpress + WordPress stubs. WordPress 7.0 GA hardening: abilities-api composer constraint widened to ^0.4 || ^0.5 || ^1.0; Tested up to 6.9. 1300 standalone assertions, 0 failures.
Cumulative roll-forward from any v6.x release with zero breaking changes.
The Respira community is where agency owners debug Divi migrations at 11pm, where vibe coders swap prompts that actually ship, and where the roadmap gets written out loud. Breathe with us.