Run ID: 2026-05-04T23-33-00_heim
Generated: 2026-05-05T14:39:18.020Z
Plugin version: 1.3.1
Sessions processed: 7
Sessions with errors: 1
| Category | Count |
|---|---|
| Problems | 20 |
| Questions | 6 |
| Improvements | 11 |
| Praises | 12 |
| Severity | Count |
|---|---|
| critical | 2 |
| major | 7 |
| minor | 9 |
| trivial | 2 |
| Area | Critical | Major | Minor | Trivial | Risk score |
|---|---|---|---|---|---|
| Customizer loading + WooCommerce dependency | 1 | 0 | 0 | 0 | 4 |
| Setup wizard AJAX security | 1 | 0 | 0 | 0 | 4 |
| Block patterns registration | 0 | 1 | 0 | 0 | 3 |
| Setup wizard — demo content import | 0 | 1 | 0 | 0 | 3 |
| Color mode switcher — JS/PHP sync, cookie value injection | 0 | 1 | 0 | 0 | 3 |
| Color mode switcher — auto mode persistence and UX | 0 | 1 | 0 | 0 | 3 |
| Customizer color settings + theme.json/style.css conflict | 0 | 1 | 0 | 0 | 3 |
| Mobile menu / keyboard accessibility | 0 | 1 | 0 | 0 | 3 |
| Setup wizard content import | 0 | 1 | 0 | 0 | 3 |
| Block styles / Editor-frontend parity | 0 | 0 | 1 | 0 | 2 |
| Block patterns / Content portability | 0 | 0 | 1 | 0 | 2 |
| Customizer — Fonts > Font Sizes | 0 | 0 | 1 | 0 | 2 |
| Customizer — Fonts > Font Types | 0 | 0 | 1 | 0 | 2 |
| Frontend rendering — blog home | 0 | 0 | 1 | 0 | 2 |
| Color mode switcher — cookie security | 0 | 0 | 1 | 0 | 2 |
| Mobile menu / desktop navigation | 0 | 0 | 1 | 0 | 2 |
| Comments / avatar rendering | 0 | 0 | 1 | 0 | 2 |
| Setup wizard lifecycle / deactivation | 0 | 0 | 1 | 0 | 2 |
| Block patterns / Dark mode | 0 | 0 | 0 | 1 | 1 |
| Frontend rendering — header | 0 | 0 | 0 | 1 | 1 |
| Block styles / WooCommerce | 0 | 0 | 0 | 0 | 0 |
| Block styles registration | 0 | 0 | 0 | 0 | 0 |
| Block patterns / Discoverability | 0 | 0 | 0 | 0 | 0 |
| Setup wizard — step 2 WooCommerce install | 0 | 0 | 0 | 0 | 0 |
| Setup wizard — error messages | 0 | 0 | 0 | 0 | 0 |
| Customizer — General settings | 0 | 0 | 0 | 0 | 0 |
| Customizer — Colors | 0 | 0 | 0 | 0 | 0 |
| Frontend rendering — header search panel | 0 | 0 | 0 | 0 | 0 |
| Frontend rendering — skip link | 0 | 0 | 0 | 0 | 0 |
| Frontend rendering — 404 page | 0 | 0 | 0 | 0 | 0 |
| Frontend rendering — empty states | 0 | 0 | 0 | 0 | 0 |
| Color mode switcher — JS validation | 0 | 0 | 0 | 0 | 0 |
| Color mode switcher — auto mode UX | 0 | 0 | 0 | 0 | 0 |
| Color mode switcher — CSS auto mode support | 0 | 0 | 0 | 0 | 0 |
| Color mode switcher — PHP cookie sanitization | 0 | 0 | 0 | 0 | 0 |
| Customizer color settings | 0 | 0 | 0 | 0 | 0 |
| WooCommerce-dependent Customizer settings | 0 | 0 | 0 | 0 | 0 |
| Mobile menu / screen reader landmark | 0 | 0 | 0 | 0 | 0 |
| Avatar rendering / progressive enhancement | 0 | 0 | 0 | 0 | 0 |
| Accessibility fundamentals | 0 | 0 | 0 | 0 | 0 |
Risk score = 4·critical + 3·major + 2·minor + 1·trivial
1. [CRITICAL] Customizer blocked indefinitely when WooCommerce is not active — PHP fatal from unconditional wc_get_page_permalink() call in add_scripts()
- Area: Customizer loading + WooCommerce dependency
- Persona affected: admin
- Confidence: 1
- Session:
customizer-wc-dependency
Steps to reproduce:
- Install and activate Heim theme WITHOUT WooCommerce
- Navigate to Appearance > Customize (wp-admin/customize.php)
- Observe: Customizer displays 'Loading...' in the page title and never progresses
- Check PHP error log: 'Fatal error: Call to undefined function wc_get_page_permalink()' in class-heim-customize.php on line 79
Expected: The Customizer should load and display all available settings panels regardless of whether WooCommerce is installed. WC-specific sections (Shop) are conditionally registered, but the core Customizer controls should be accessible without WooCommerce.
Actual: The Customizer hangs at 'Loading...' indefinitely. PHP fatal error occurs in add_scripts() which calls wc_get_page_permalink('shop') unconditionally in the customize_controls_print_scripts hook. This kills the Customizer JS initialization, making ALL theme settings (colors, fonts, header, footer, blog) completely inaccessible to admins without WooCommerce.
Evidence:
·
· console
Notes: Source location: class-heim-customize.php:72-86 (add_scripts() method), line 79 calls wc_get_page_permalink('shop') directly in PHP without function_exists() guard. The hook is registered unconditionally at line 66: add_action('customize_controls_print_scripts', array($this, 'add_scripts'), 30). Fix: wrap the add_scripts registration (or the call within add_scripts) with if (function_exists('wc_get_page_permalink')) or heim_woocommerce_active() check. Confirmed: after installing WooCommerce, Customizer loads successfully and title changes from 'Customize: Loading...' to 'Customize: My WordPress Website'.
2. [CRITICAL] AJAX handlers plugin_install and content_install lack capability check — any subscriber can trigger plugin installation and content import
- Area: Setup wizard AJAX security
- Persona affected: admin
- Confidence: 1
- Session:
setup-wizard
Steps to reproduce:
- Create a subscriber user account
- Log in as the subscriber
- Visit any admin page (e.g. /wp-admin/profile.php) — the heim_setup_nonce is embedded in all admin pages via admin_enqueue_scripts
- Extract the nonce: window.heim_setup_data.wpnonce in browser console
- POST to /wp-admin/admin-ajax.php with body: action=content_install&wpnonce=&task=pages
- Observe HTTP 200 response with content import output instead of a capability error
Expected: The handler should call current_user_can('manage_options') and return a capability error (wp_send_json_error or die(-1)) for non-admin users
Actual: The handler (class-heim-setup.php:441) calls only check_ajax_referer('heim_setup_nonce', 'wpnonce') and proceeds to execute content installation. Any authenticated user (Subscriber or above) who can read any admin page obtains a valid nonce, which is sufficient to trigger privileged AJAX operations including plugin installation (plugin_install handler at line 426).
Evidence:
· console
Notes: The nonce is exposed to all authenticated users via admin_enqueue_scripts hook (line 107), which fires on every admin page load. wp_create_nonce() is called for every admin page view regardless of the user's role. The plugin_install handler (line 426) shares the same vulnerability — it accepts plugin slug from POST and attempts to install/activate, also with no capability check. Root cause: missing current_user_can('manage_options') in both plugin_install() and content_install() methods.
3. [MAJOR] Block patterns inaccessible via REST API and non-admin contexts due to is_admin() + admin_init double gate
- Area: Block patterns registration
- Persona affected: developer
- Confidence: 0.98
- Session:
block-patterns-styles
Steps to reproduce:
- Make authenticated REST API request: curl -H 'Authorization: Basic ' http://site/wp-json/wp/v2/block-patterns/patterns
- Observe that 0 Heim Theme patterns appear in response (only 58 core patterns returned)
- Attempt to use block patterns in a headless or REST-driven context
- Observe patterns are completely unavailable outside browser-based admin
Expected: Block patterns should be registered on the 'init' hook (not 'admin_init') so they are available in REST API contexts, WP-CLI, and headless/decoupled WordPress setups
Actual: functions.php lines 48-50 wrap the include of block-patterns.php inside is_admin(). Inside block-patterns.php, lines 20 and 349 hook heim_register_block_pattern_category and heim_register_block_patterns on 'admin_init'. These two gates together mean patterns are never registered outside browser-based admin requests. REST API returns 0 Heim patterns even with application-password-authenticated requests.
Evidence:
· console
Notes: Root cause: functions.php lines 48-49 (is_admin() gate on include) and block-patterns.php lines 20 and 349 (add_action admin_init). Fix: remove is_admin() gate from include; change both add_action calls from admin_init to init. register_block_pattern() is safe on init and has no admin-only requirement.
4. [MAJOR] Setup wizard demo content import triggers 500 Internal Server Error when products task runs, showing raw technical error message to admin
- Area: Setup wizard — demo content import
- Persona affected: admin
- Confidence: 0.95
- Session:
breadth-tour-admin
Steps to reproduce:
- Navigate to /wp-admin/themes.php?page=heim-theme-setup
- Click Next on step 1 (Welcome)
- On step 2 (Install WooCommerce), click Next without installing WooCommerce (or WC not pre-installed)
- On step 3 (Import Content), click Import
- Observe the error message after import starts
Expected: Import either (a) completes successfully if WooCommerce is present, or (b) shows a user-friendly message explaining that WooCommerce must be installed before importing demo products
Actual: Import starts, completes attachments and posts tasks, then fails with 'Setup error: _taskDoIntall > products: Internal Server Error' displayed to the admin. The error message includes an internal function name typo (_taskDoIntall instead of _taskDoInstall) and a raw HTTP status. Admin-ajax.php returns 500.
Evidence:
· [console](sessions/breadth-tour-admin/console-logs.txt — [ERROR] Failed to load resource)
Notes: Root cause: install_products() in class-heim-setup.php:634 checks class_exists('woocommerce') and calls exit after echo when WC absent — causing the AJAX handler to return a malformed response. The wizard's step 2 'Next' button installs WooCommerce silently but the install takes time; if the user skips step 2 or if it fails, step 3 proceeds without WC. Additionally, the function name _taskDoIntall (line 175 of setup.js) is a typo that leaks into the error message.
5. [MAJOR] Inline JS in header.php injects raw cookie value into data-color-mode attribute without whitelist validation
- Area: Color mode switcher — JS/PHP sync, cookie value injection
- Persona affected: guest
- Confidence: 0.95
- Session:
color-mode-cookie
Steps to reproduce:
- Set the heim_theme_color_mode cookie to an arbitrary value (e.g. 'xss-test-value') via browser devtools console: document.cookie='heim_theme_color_mode=xss-test-value; path=/'
- Reload any page of the Heim theme frontend
- Inspect document.body.getAttribute('data-color-mode') in console
Expected: The inline JS should validate the cookie value against an allowlist of 'light', 'dark', and 'auto' before calling setAttribute. Any value outside this set should be ignored.
Actual: The inline JS at header.php:26 reads the cookie and calls document.body.setAttribute('data-color-mode', colorMode) with NO validation. Cookie value 'xss-test-value' was confirmed injected as the attribute value. PHP sanitizes the cookie server-side (template-tags.php:71: only allows 'light', defaults others to 'dark'), but the client-side JS override bypasses PHP sanitization entirely on every page load.
Evidence:
· console
Notes: Direct XSS via data-color-mode attribute is not currently exploitable (CSS selectors only match 'light'/'dark'/'auto', so an arbitrary value just results in unstyled page rendering). However, the unvalidated injection violates defense-in-depth and could affect JS that reads back the attribute value. The PHP whitelist in template-tags.php:71-73 is effectively bypassed by the inline JS on every page load. Source: header.php:26 vs scripts.js:354-380.
6. [MAJOR] Mobile menu overlay has no keyboard-close affordance -- Escape key does not close overlay
- Area: Mobile menu / keyboard accessibility
- Persona affected: guest
- Confidence: 0.95
- Session:
frontend-a11y
Steps to reproduce:
- Navigate to site frontend at any viewport width
- Click the hamburger/Menu button to open the mobile menu overlay
- Press the Escape key
- Observe whether the overlay closes
Expected: Pressing Escape should close the mobile menu overlay and return keyboard focus to the hamburger button (trigger element) per WCAG 2.1 SC 2.1.2 No Keyboard Trap
Actual: Escape key press has no effect; overlay remains open with body classes overlay-show and mobile-menu-open unchanged. Only the Close link (anchor element, not a button) can close the overlay via mouse click or Enter key navigation
Evidence:
· console
Notes: Source confirmed: grep of entire assets/js/ directory for Escape, keydown, keyup, keypress returns zero matches. The mobile menu toggle mechanism uses CSS class toggling on body without any keyboard event listener. This is a WCAG 2.1 violation for dismissable overlays (Pattern requires Escape key dismissal).
- Area: Setup wizard content import
- Persona affected: admin
- Confidence: 0.95
- Session:
setup-wizard
Steps to reproduce:
- Activate Heim theme and complete the setup wizard content import step
- Note post count via WP-CLI: wp post list --post_type=post --format=count
- Navigate to /wp-admin/themes.php?page=heim-theme-setup
- Click Next twice to reach Step 3 (Import Content)
- Click 'Import Again'
- Wait for import to complete
- Check post count again: wp post list --post_type=post --format=count
Expected: Import Again should check for existing posts and skip or update them, not create duplicates
Actual: Post count increased from 1 to 6 after Import Again (5 new duplicate blog posts created). Additionally, the first import already created duplicate Cart and Checkout pages (IDs 7/16 and 8/17) because WooCommerce creates those pages on installation and the wizard also creates its own copies. The install_posts() function uses get_page_by_title() (deprecated) for existence checks which fails to detect existing posts by title.
Evidence:
·
· console
Notes: Source at class-heim-setup.php:645 uses deprecated get_page_by_title() for product existence checks. The install_posts() method (line ~1190) also uses get_page_by_title() which is deprecated since WP 6.2 and subject to returning unexpected results. The 'Note: recommended on empty installation' warning in the wizard UI suggests the developers are aware of this risk but have not implemented proper idempotency.
- Area: Color mode switcher — auto mode persistence and UX
- Persona affected: guest
- Confidence: 0.9
- Session:
color-mode-cookie
Steps to reproduce:
- Configure theme with color_mode='auto' (Customizer or WP-CLI: studio wp theme mod set color_mode auto)
- Visit the frontend as a guest (no cookie set) — verify data-color-mode='auto' in body
- Click the Day/Night toggle — cookie is set to 'dark' (or 'light' in dark browser preference)
- Navigate to another page — cookie persists, data-color-mode is now 'dark' or 'light'
- Try to return to 'auto' mode using only the frontend Day/Night toggle
- Click toggle again — it switches to the opposite manual mode, never back to 'auto'
Expected: When the site is configured for 'auto' color mode, users should be able to return to auto (follow-browser-preference) behavior after manually toggling. A three-state cycle, a dedicated 'Auto' option, or at minimum documentation/UI hint about cookie clearing would satisfy this expectation.
Actual: The JS colorModeSwitch function (scripts.js:354-380) only alternates between 'light' and 'dark'. The 'auto' branch fires only when data-color-mode is currently 'auto' — which it never is once a cookie is set, because the inline JS override (header.php:26) replaces 'auto' with the cookie value on every page load. Users who click Day/Night once are permanently locked into manual mode with no visible path to reset. Only clearing browser cookies restores auto behavior.
Evidence:
· console
Notes: This is a UX/design defect that creates an irrecoverable state for users. Only manifests when color_mode is set to 'auto' in theme settings. Source: includes/template-tags.php:71-73 (PHP reads cookie first, overriding theme_mod) + header.php:26 (JS re-reads cookie and overrides PHP-rendered attribute) + scripts.js:354-380 (colorModeSwitch only writes 'light'/'dark').
9. [MAJOR] Customizer color settings have no effect on hardcoded style.css values — CSS custom property updated but rendered color unchanged
- Area: Customizer color settings + theme.json/style.css conflict
- Persona affected: admin
- Confidence: 0.9
- Session:
customizer-wc-dependency
Steps to reproduce:
- Install WooCommerce (required for Customizer to load)
- Set color_body to #ff0000 via WP-CLI: studio wp theme mod set color_body '#ff0000'
- Navigate to site homepage
- Inspect computed color on body, article, and p elements
- Observe: --global--color-body CSS custom property = #ff0000 but body/article/p computed color = rgb(40, 40, 40)
Expected: Setting a custom body color via the Customizer (or theme mod) should change the rendered text color consistently across all page elements including those styled by theme.json token references and style.css rules.
Actual: The CSS custom property --global--color-body is updated to #ff0000, but all text elements (body, article, p) render as rgb(40, 40, 40) — the hardcoded color from style.css. The hardcoded values take precedence over the CSS custom property, meaning Customizer color settings have no visible effect.
Evidence:
· console
Notes: Empirically confirmed: CSS custom property --global--color-body = '#ff0000' (set by theme mod), but computed color of body/article/p = rgb(40, 40, 40). style.css is 121K and contains hardcoded color values. The Customizer color settings are likely designed to update CSS custom properties, but style.css rules use hardcoded values instead of var(--global--color-body), creating an inconsistency. This means the Customizer color settings surface (2400 lines of settings, per charter) effectively does nothing visible for any admin without WooCommerce, and partially broken for those with WooCommerce.
10. [MINOR] Font size inputs accept out-of-range values (0-9, 27+) with no server-side validation, allowing admin to render 0px body text
- Area: Customizer — Fonts > Font Sizes
- Persona affected: admin
- Confidence: 0.95
- Session:
breadth-tour-admin
Steps to reproduce:
- Navigate to /wp-admin/customize.php
- Open Fonts > Font Sizes section
- Change Base body text spinner to 0 (or any value below 10 or above 26)
- Click Publish
- Visit the frontend
Expected: Values outside the documented 10-26px range should be rejected or clamped server-side. The Customizer UI should show a validation error for out-of-range values.
Actual: Value 0 is accepted and saved. Frontend renders --global--font-size-body: 0px as CSS custom property, making all body text invisible. WP-CLI confirms: theme mod get font_size_base returns 0.
Evidence:
· [console](sessions/breadth-tour-admin/JS eval)
Notes: Root cause: sanitize_callback is 'absint' which converts to a non-negative integer but does not enforce min/max range. The input_attrs min:10, max:26 are HTML attributes only (client-side) and can be bypassed by typing in the spinner or via direct API calls. Fix: use a custom sanitize callback that clamps to [10, 26].
None.
- [Block styles / WooCommerce] Does registering the woocommerce/featured-category block style on 'init' without WooCommerce installed cause PHP notices or editor errors in WP 6.9?
- Why it matters: If WordPress logs notices for block styles registered for non-existent block types, this could pollute PHP error logs in production and affect site health checks for non-WooCommerce installations
- [Setup wizard — step 2 WooCommerce install] Does clicking 'Next' on step 2 (Install WooCommerce) trigger a silent WooCommerce installation, or was WooCommerce already installed from a prior session?
- Why it matters: During testing, after clicking Next on step 2, the Customizer (previously stuck loading) started working. WP-CLI confirmed WooCommerce became active. If the wizard silently installs WooCommerce on Next (without a progress indicator), this is a significant UX gap — users may not know WC was installed.
- [Customizer — Fonts > Font Sizes] Should the Customizer UI show a validation error when font size values outside 10-26 are entered, or is the restriction purely advisory (the UI says 'between 14 and 18 is recommended')?
- Why it matters: The Customizer help text says 'A size between 14 and 18 is recommended', which is different from the input_attrs min:10, max:26 HTML constraints. The intended valid range is unclear, and the server accepts any absint value.
- [Frontend rendering — header search panel] Is the Escape key expected to close the header search panel?
- Why it matters: Pressing Escape did not close the header-search-open panel — only a second click on the Search toggle closed it. Escape-to-close is a standard keyboard interaction pattern for overlays (WCAG 2.1 SC 2.1.2: No Keyboard Trap). If intentional, the documentation or ARIA pattern should clarify.
- [Customizer color settings] Are the Customizer color settings intended to update CSS custom properties only (for theme.json-driven elements) or also intended to override hardcoded style.css values? If the former, which specific page elements actually use the CSS custom properties?
- Why it matters: If the design intent is that Customizer colors update theme.json tokens only, then the 121K style.css file has no mechanism to reflect those changes, making most of the 2400 lines of Customizer settings cosmetic rather than functional.
- [Mobile menu / screen reader landmark] Is the Secondary menu nav landmark (containing utility tools: color-mode switcher, search) intentionally labeled as a navigation landmark, or should it have role=toolbar or similar to distinguish it from page navigation?
- Why it matters: Screen readers navigate by landmark regions. A nav labeled Secondary menu that contains only UI tools (not page links) may mislead users who tab to landmarks expecting navigation options.
- [Block patterns registration] Change block-patterns.php include from is_admin() gate to unconditional include, and change add_action('admin_init', ...) to add_action('init', ...) for both heim_register_block_pattern_category and heim_register_block_patterns (effort: low) (impact: high)
- Rationale: Makes patterns available in REST API, WP-CLI, and headless WordPress contexts. register_block_pattern() is safe to call on init and has no admin-only requirement. The current gate breaks pattern discovery for any non-browser consumer.
- [Block patterns / Content portability] Document in theme notes that pattern images become static URLs when inserted. Consider importing starter-content images to the Media Library on theme activation so pattern image references resolve to attachments rather than theme asset paths. (effort: medium) (impact: medium)
- Rationale: Users migrating sites or switching hosting may not realize that inserted pattern images are tied to the original theme asset path and install URL. Media Library attachments survive site URL changes via WordPress's built-in URL replacement tools.
- [Setup wizard — demo content import] Add a prerequisite check before the Import button on step 3: if WooCommerce is not installed, disable or hide the Import button and show a message 'WooCommerce must be installed to import demo products'. Alternatively, provide a partial import path that imports non-WC content only. (effort: low) (impact: medium)
- Rationale: The current flow allows users to click Import without WooCommerce, triggering a partial import that fails partway through with a technical error. This leaves the site in an inconsistent state (some demo content imported, products not).
- [Setup wizard — error messages] Replace technical AJAX error messages ('_taskDoIntall > products: Internal Server Error') with user-friendly messages ('Could not import demo products: WooCommerce is required. Please install WooCommerce and try again.'). Also fix the function name typo _taskDoIntall → _taskDoInstall in setup.js:175. (effort: low) (impact: medium)
- Rationale: Admin users shouldn't see internal function names and HTTP status strings. The typo in the function name is also embarrassing in error output.
- [Frontend rendering — header search panel] Add Escape key handler to close the header search panel when it is open (effort: low) (impact: medium)
- Rationale: The search panel is an overlay that opens with a toggle. Standard keyboard UX and WCAG 2.1 SC 2.1.2 expects Escape to close such overlays. Currently only a second click on the Search button closes the panel.
- [Frontend rendering — blog home] Replace display:none on body.blog .page-title with a visually-hidden pattern so the H1 is accessible to screen readers while remaining invisible visually (effort: low) (impact: medium)
- Rationale: The blog home uses display:none on H1 'Blog' for aesthetic reasons, completely removing it from the accessibility tree. Using a visually-hidden class (like the existing .screen-reader-text pattern used for the skip link) would satisfy both the design goal and WCAG heading requirements.
- [Color mode switcher — cookie security] Add explicit SameSite=Lax to the cookie assignment string in scripts.js:374. For HTTPS deployments, also add the Secure flag. Updated string: 'heim_theme_color_mode='+newColorMode+'; path=/; SameSite=Lax; Secure' (effort: low) (impact: low)
- Rationale: Explicit SameSite prevents reliance on browser defaults (which vary across browser versions). Secure prevents the cookie from being sent over unencrypted connections on production HTTPS sites.
- [Color mode switcher — JS validation] Add a whitelist check in the inline JS before calling setAttribute: const allowed = ['light','dark','auto']; if (colorMode && allowed.includes(colorMode)) { document.body.setAttribute('data-color-mode', colorMode); } (effort: low) (impact: medium)
- Rationale: Prevents arbitrary cookie values from being injected into the data-color-mode attribute, following defense-in-depth principles even if the immediate risk is low.
- [Color mode switcher — auto mode UX] When color_mode is 'auto', modify colorModeSwitch() to cycle through three states: auto -> dark -> light -> auto (or add a third explicit 'Auto' button). Store 'auto' as a valid cookie value and handle it in both the JS override and the PHP whitelist in template-tags.php. (effort: medium) (impact: medium)
- Rationale: Currently users with auto mode configured have no way to return to browser-preference following after clicking the toggle. A three-state cycle or explicit 'Auto' option restores the expected behavior and eliminates the irrecoverable-state UX defect.
- [Mobile menu / keyboard accessibility] Add keydown event listener for Escape key that calls the same close function as the Close link, and restores focus to the hamburger button after close (effort: low) (impact: medium)
- Rationale: WCAG 2.1 SC 2.1.2 requires that components opened via keyboard can be closed via keyboard without moving focus. The mobile menu overlay is a dismissable UI pattern that should follow the ARIA dialog/disclosure pattern with Escape-key close.
- [Setup wizard content import] Add idempotency check to install_posts() — query for existing posts by slug rather than title using WP_Query, and skip or update instead of creating duplicates on re-run (effort: medium) (impact: high)
- Rationale: The 'Import Again' button is surfaced prominently in the wizard UI but using it reliably creates duplicate content. Replacing get_page_by_title() (deprecated since WP 6.2) with WP_Query by post_name would be more reliable and future-proof.
- [Block styles registration] Block styles are correctly registered on 'init' (correct hook), distinct from the block patterns which use the incorrect admin_init
- Why: Using init for block styles means they are available in all contexts including REST API and editor stylesheet generation. This correct pattern should be applied to the pattern registration as well.
- [Block patterns / Discoverability] Heim Theme patterns are organized in a clearly labeled 'Heim Theme' category in the block inserter, with live pattern thumbnails rendered in the editor preview
- Why: The pattern browsing experience is smooth — patterns load quickly, are named descriptively (Cover banner, Quote and profile info, Cover feature banners), and the Heim Theme category is easy to find. Users can easily discover and insert patterns from the block inserter.
- [Customizer — General settings] The content animation toggle immediately reflects on the Customizer preview iframe when changed — no page reload needed.
- Why: Instant preview feedback makes Customizer settings much easier to evaluate. The frontend body class update was correctly reflected after publish.
- [Customizer — Colors] The color_mode select (Light/Dark/Auto) has a clear, descriptive option label 'Auto (use browser theme)' that explains exactly what the Auto mode does.
- Why: Many themes obscure what 'Auto' means. This label sets correct user expectations about the browser preference-based behavior.
- [Frontend rendering — skip link] Skip-to-content link uses the screen-reader-text pattern correctly — hidden at rest via clip, becomes fully visible on keyboard focus, and functionally jumps to #content
- Why: This is a best-practice accessible skip link implementation. The clip-based hiding keeps the link in both the visual and accessibility tree. Works end-to-end: Tab reveals it, Enter activates it.
- [Frontend rendering — 404 page] 404 page provides a search form and homepage link rather than a dead end
- Why: Heim's 404.php includes a search form as the primary recovery mechanism and a clear 'Back to homepage' link, giving users two actionable paths out of the error state.
- [Frontend rendering — empty states] Empty blog state (no published posts) renders gracefully without PHP errors
- Why: Heim handles the zero-post case with a helpful message and gracefully degraded widget areas ('No archives to show', 'No categories'). Empty states are easy to overlook in theme development.
- [Color mode switcher — CSS auto mode support] The CSS correctly implements auto mode using @media (prefers-color-scheme: dark) with [data-color-mode=auto] selectors across style.css, woocommerce.css, and woocommerce-rtl.css — ensuring auto mode produces correct rendering without a flash of unstyled content.
- Why: Many theme authors implement auto mode purely in JS (which causes FOUC). This implementation sets the initial data-color-mode attribute server-side via PHP and styles it with CSS media queries — an elegant and performant approach that works even before JS runs.
- [Color mode switcher — PHP cookie sanitization] PHP function heim_color_mode_get_state() in includes/template-tags.php sanitizes the cookie value with a whitelist: only 'light' passes through, all other values default to 'dark'.
- Why: This prevents arbitrary cookie values from corrupting the server-rendered HTML attribute, showing security awareness on the PHP side. The gap is in the client-side JS override path.
- [WooCommerce-dependent Customizer settings] The class_required enforcement at class-heim-customize.php:2125 correctly gates all WC-dependent controls behind a class_exists('woocommerce') check during settings registration, including mobile_menu_product_categories and related settings.
- Why: This prevents PHP errors and unwanted UI clutter for themes activated without WooCommerce — once the blocking fatal in add_scripts() is fixed, all WC-dependent settings will correctly hide/show based on WC presence.
- [Avatar rendering / progressive enhancement] The avatar character fallback system (replacing missing Gravatars with styled initials) is a thoughtful progressive enhancement that gracefully handles the common case of missing avatars without showing broken image icons
- Why: Many themes show broken images or blank boxes for commenters without Gravatars. The character-based fallback creates a consistent, branded look while respecting privacy.
- [Accessibility fundamentals] Skip-to-content link is present on all pages including the blank template, and both nav landmarks have distinct aria-label values
- Why: These are baseline accessibility requirements that many themes miss. Distinct aria-labels for nav landmarks is particularly important for screen reader navigation.
| Session | Status | Turns | Flows | Notes |
|---|---|---|---|---|
block-patterns-styles |
complete | 8/8 | 4/4 | All 4 hypotheses probed. H10 confirmed as major bug (Heim patterns inaccessible via REST API due to double gate: is_admin() wrapper in functions.php + admin_init hook in block-patterns.php). H19 confirmed (hardcoded install-URL in pattern image src attributes). H9 probed and refuted for the cover-banner pattern specifically (white on dark image is readable), but the structural non-adaptability is noted as trivial. H16: editor uses .wp-block-post-content.is-root-container while frontend uses .entry-content, causing column margin scoping differences — confirmed via static analysis. Bonus finding: this theme is labeled block-theme in ecosystem metadata but is a hybrid classic theme using PHP templates. |
breadth-tour-admin |
complete | 18/18 | 5/5 | All 8 charter hypotheses probed. Customizer was initially blocked (recon S2/S8 confirmed) but auto-unblocked after setup wizard Flow 1 installed WooCommerce. All Customizer sections (General, Fonts, Colors, Header, Mobile Menu, Footer, Shop, Blog) were accessible. 3+ probes achieved for each section. Font size boundary value test confirmed out-of-range values (0) persist without server-side range enforcement. Demo content import failed with technical error message when products import step hit Internal Server Error. |
breadth-tour-frontend |
complete | 18/18 | 9/10 | All 9 charter hypotheses (BT-F1 through BT-F9) probed. Special character/long title data probe added as bonus. Search panel Escape-key dismiss was probed and found not to work. WooCommerce header class pollution found as minor/trivial finding. Sample page (static page template) verified. No PHP errors on any template route. |
color-mode-cookie |
complete | 8/8 | 4/4 | All four hypotheses (H1, H2, H12, H20) probed with empirical confirmation. H1 confirmed: cookie lacks Secure and explicit SameSite (browser defaults to Lax). H2 confirmed: inline JS injects raw cookie value into data-color-mode without whitelist validation. H12 confirmed as pass: auto mode renders correctly via @media (prefers-color-scheme: dark) CSS. H20 confirmed: no frontend UI path to return to 'auto' once cookie is set. |
customizer-wc-dependency |
complete | 8/8 | 4/4 | H4 empirical probe deferred — out of budget. H-S8 confirmed both via source analysis AND empirical test (Customizer stuck loading without WC, loads correctly with WC installed). H3 confirmed empirically: CSS custom property --global--color-body was set to #ff0000 but body/article/p computed color remained rgb(40, 40, 40) from hardcoded style.css. H14 refuted via source analysis + WC-state observation (class_required at line 2125 correctly skips WC settings registration when WooCommerce class not present). |
frontend-a11y |
complete | 8/8 | 4/4 | All 4 charter hypotheses probed within budget. H7 refuted by both source inspection (blank.php calls get_header/get_footer) and live browser verification. H11 partially confirmed: hamburger menu visible on 1280px desktop (mobile-menu-desktop body class) but mobile overlay adds no 3rd nav element. Mobile menu Escape keyboard trap confirmed with source code verification (zero keydown/Escape handlers in theme JS). H17 confirmed: guard condition commented out in heim_avatar_remove_size_atts, unconditionally strips width/height from all avatar img tags. |
setup-wizard |
complete | 8/8 | 3/3 | All three charter hypotheses H5, H6, H15 were probed empirically. H5 confirmed with live AJAX call as subscriber. H6 confirmed — Import Again created 5 duplicate blog posts (count went from 1 to 6); pages already had duplicates from first import creating Cart/Checkout alongside WooCommerce's pages. H15 confirmed via WP-CLI before/after theme switch. |
These are signals observed during the run that point at test-environment quirks (Studio + SQLite shim, WP-CLI Phar, WC stack interactions), NOT plugin defects. Apply extra scrutiny to findings in affected areas — some Problems may be false positives caused by the environment, and some real bugs may be masked.
| Session | Warning |
|---|---|
block-patterns-styles |
This theme is labeled ecosystem: block-theme in site metadata but is a hybrid classic PHP theme. It uses template-parts/post/content-single.php with .entry-content wrapping, not full FSE block templates. The editor uses .wp-block-post-content.is-root-container while the frontend uses .entry-content — this is a genuine architectural difference, not a test environment artifact. The P2 finding (column margin parity gap) is real. |
block-patterns-styles |
REST API Basic auth (admin:password) over localhost returned 401 — application passwords were required for REST authentication. This is normal WordPress behavior when Application Passwords is configured as the REST auth mechanism. |
breadth-tour-admin |
Customizer was initially blocked ('Loading...' indefinitely) on first load — consistent with recon S2 observation about wc_get_page_permalink() fatal when WooCommerce is not installed. After WooCommerce was installed (via setup wizard), Customizer loaded correctly. The recon blocker was not an independent environment issue but a real theme bug (S8). |
breadth-tour-admin |
WooCommerce auto-installation during wizard testing changed the site state mid-session. All Customizer probes after turn 8 had WooCommerce active, which may differ from a fresh-install state without WC. This is expected behavior, not an environment issue. |
breadth-tour-frontend |
SQLite integration (sqlite-database-integration MU-plugin) in use — some query behaviors may differ from MySQL production. No SQLite-specific errors observed during this session. |
setup-wizard |
WooCommerce was pre-installed during provisioning and created its own Cart/Checkout/Shop pages before the wizard's content import ran. This created a pre-existing duplicate page scenario (IDs 7/8 vs 16/17 for Cart/Checkout) that is a real production scenario (user installs WooCommerce, then runs wizard) but was accelerated by the provisioning script order. |
- No report.json produced
Computed from Claude Code transcripts at ~/.claude/projects/<proj-hash>/. Rates from config/pricing.json.
Window: 2026-05-04T23:33:00Z → 2026-05-05T14:39:17Z (with ±10min buffer for dispatch drift).
Estimated total cost for this run: $44.18
| Category | Cost | % of total |
|---|---|---|
| Fresh input | $0.30 | 0.7% |
| Output | $3.33 | 7.5% |
| Cache-create (5m) | $11.95 | 27.1% |
| Cache-create (1h) | $2.34 | 5.3% |
| Cache-read | $26.27 | 59.5% |
Total: $5.46
| Model | Messages | Input | Output | Cache-5m | Cache-1h | Cache-read | Cost |
|---|---|---|---|---|---|---|---|
claude-sonnet-4-6 |
72 | 79 | 16,381 | 0 | 389,562 | 9,584,797 | $5.46 |
Total: $38.72
| Model | Messages | Input | Output | Cache-5m | Cache-1h | Cache-read | Cost |
|---|---|---|---|---|---|---|---|
claude-opus-4-6 |
75 | 58,424 | 32,822 | 840,144 | 0 | 4,744,684 | $8.74 |
claude-sonnet-4-6 |
1005 | 1,067 | 150,719 | 1,786,892 | 0 | 70,058,990 | $29.98 |
Per-subagent breakdown (10 sessions)
| Agent ID | Type | Models | Cost |
|---|---|---|---|
a0050f6aec1f0f7f7 |
planner | claude-opus-4-6 | $2.32 |
a0acefd0882b8bdf9 |
tester | claude-sonnet-4-6 | $3.46 |
a426c099e3e4c2e3e |
tester | claude-sonnet-4-6 | $2.22 |
a4e9febd1ca8bfd6e |
tester | claude-sonnet-4-6 | $5.89 |
a799a5b34eb5bb1bc |
planner | claude-opus-4-6 | $6.41 |
a8089689e04e05efe |
tester | claude-sonnet-4-6 | $5.02 |
a824d70781c373cac |
tester | claude-sonnet-4-6 | $3.47 |
ab0cc350f066fe9b5 |
tester | claude-sonnet-4-6 | $2.88 |
ad37ae07e38e5c331 |
tester | claude-sonnet-4-6 | $4.38 |
ae34b32d1dcb36b41 |
tester | claude-sonnet-4-6 | $2.66 |
- Triage Customizer loading + WooCommerce dependency first — highest risk score (4)
- Address 2 critical problem(s) before release
- Follow up on 7 session(s) with incomplete coverage
- Investigate 1 session(s) that failed to produce valid reports