- fix(schema-compat): prefer 'string' in non-null scalar unions to fix SanitizeSchemaUnionItemsTypeTest
- fix(content): define featured_image output properties and return resilient empty object structure for posts without thumbnails
- feat(content): add get_allowed_protected_meta() with WooCommerce and paywall whitelist
- feat(user): add sanitized meta support to create-user, update-user, and get-user with privilege escalation guard
- feat(database): support 'query' argument alias in emcp-tools/query
- feat(woo): expand create/update-product with author/vendor ownership, virtual/downloadable, prices, categories, tags, and meta; add list-vendors
- test: add 14 new automated unit tests across User, Content, Woo, and Database suites
get_active_ability_names() probed wp_get_ability('emcp-tools/list-pages')
merely to trigger the lazy Abilities API init. wp_get_ability() routes
through WP_Abilities_Registry::get_registered(), which fires a
_doing_it_wrong() 'Ability not found' notice whenever the probed ability
is conditionally absent — list-pages is Elementor-gated, so on a site
without Elementor every admin-bar render logged it.
Trigger the init with the silent wp_has_ability() instead, and use the
same silent existence check for the core-ability guards in
register_mcp_server() and the dispatcher. resolve_ability() now fails
closed via wp_has_ability() before wp_get_ability().
Two fork-specific defects made "Activate" a dead end, ending at core's
"Sorry, you are not allowed to access this page." (403).
1. Self-deactivation. `.emcp-pro` is committed, so the build counts as
premium, and the premium guard retired `$emcp_tools_free_basename` --
which on the documented layout (wp-content/plugins/heretek-control-core)
IS this plugin. The first admin_init after activation deactivated the
site's only working copy. emcp_tools_retire_sibling() now refuses its own
basename, and sibling candidates inside our own folder are filtered out
(the same file loaded twice is deduplicated by require_once, so retiring
it can only ever switch the plugin off). Legacy folders -- emcp-tools/,
elementor-mcp/ -- are still retired whichever build runs.
2. Freemius owned the activation flow. Its post-activation redirect
(class-freemius.php:24546) is the only code in the plugin that builds
admin.php?page=emcp-tools, a URL that exists only if the bootstrap
finished; its _prepare_admin_menu also replaced our dashboard with the
opt-in/connect screen on a site that has never been connected. The fork
hard-codes every licensing answer to "unlocked", so both are dropped:
`redirect_on_activation` is filtered off and the _prepare_admin_menu hook
is removed.
When neither the real menu nor anything else owned the `emcp-tools` slug,
WordPress core ended the request from wp-admin/includes/menu.php -- which
runs during admin_menu, BEFORE admin_init -- with that 403 and no
explanation.
New EMCP_Tools_Admin_Fallback (includes/admin/class-admin-fallback.php)
makes the slug always resolve. At the end of admin_menu it registers a
diagnostics page when nothing else claimed the slug (a no-op on a healthy
install), and admin_page_access_denied bounces emcp-tools-* sub-screens
there instead of letting core kill the request. The page reports boot
state, MCP adapter resolution, PHP/WordPress versions, other active copies
of the plugin, and a file-integrity check that flags 0-byte files -- the
issue #100 signature of a host malware scanner emptying a file in place so
require_once succeeds while nothing is declared.
Also: the release workflow still packaged the zip under `elementor-mcp/`,
the legacy folder name this plugin's own conflict guards look for. Release
assets are now built under `heretek-control-core/`.
- Establish heretek-control-core.php as canonical main plugin bootstrap
- Convert emcp-tools.php to non-conflicting backward-compatibility forwarder
- Update readme.txt header and tagline removing trademarked terms per Rule 17
- Update bin/build-wporg-zip.sh to package slug heretek-control-core
- Regenerate assets-wporg banners and icons with HCC / Heretek Control Core branding
- Rebrand admin interface, menu items, and headers to Heretek Core / Heretek Control Core
- Update composer.json, release tooling, and README.md
Belt-and-braces reset for the WordPress core .form-table rules (forms.css /
common.css ship border-collapse: collapse; margin-top: .5em; width: 100%;
clear: both;) which, combined with an inherited flex stretch on the form,
can let the form table pull its parent's height instead of its cells'.
The form-level reset in #4 already nails the form itself; this kills the
table as a candidate for the same kind of stretch. Scoped to .elementor-mcp-admin
so nothing leaks outside admin pages.
Co-Authored-By: Claude <[email protected]>
A wholesale reset on every form inside any EMCP admin page so no ancestor
can stretch it past its content height: min-height: 0 !important,
height: auto !important, display: block. The AI Chat settings page
wrap-only rule from #3 was necessary but not sufficient — the stretch came
from an inherited rule (wp-admin/css/common.css in WP 7.1 uses
form { display:flex; flex-direction:column } combined with the shell's
min-height: calc(100vh - 32px), which resolved the inline form to
~1700 px on a tall page even with the wrap flush.
This scoped override does not affect non-admin pages (the selector is
gated by .elementor-mcp-admin) and is intentionally !important so it
defeats an inherited core style with the same specificity instead of
needing deeper !important chains for every ancestor.
Co-Authored-By: Claude <[email protected]>
The AI Chat settings page is one short form; with the shared .tab-content
padding/margin it sat at the top of a tall card and the form rows landed
near the bottom of the visible viewport at 100% zoom — looking like the
controls were dropped below where they should be.
Make the .elementor-mcp-ai-chat wrap flush (transparent bg, no padding/
margin/border) on top of the existing .tab-content card, tighten the h2
and description spacing, and leave the form's existing 650px max-width
alone so the controls sit high in the viewport at any zoom.
Co-Authored-By: Claude <[email protected]>
push_packets()/finalize() now refuse any archive outside this site's backups
dir (Packager::archive_path()/create_archive() already enforce this; the guard
is defense-in-depth at the sink). The packager's directory walk keeps its
NOSONAR with the confinement rationale on the flagged line. Clears the three
phpsecurity:S2083 blockers that regressed the Sonar security rating to 5.
Co-Authored-By: Claude <[email protected]>
Security:
- pair/issue + verify signatures now bind a per-request ts + single-use nonce
(canonical 'pair|issue|<ts>|<nonce>' / 'verify|<ts>|<nonce>'); the connector
rejects stale timestamps (+-300s) and replayed nonces (capped store), and the
plugin's verify_endpoint() signs the same canonical, so a captured signature
cannot be replayed to mint/verify pairing codes.
- Loopback decision for the secret-carrying exchange is now server-side
(home_url() host + SERVER_ADDR), never the client Host header.
- The connector admin secret is stored verbatim (wp_unslash only) — no
sanitize_text_field rewriting bytes used for HMAC.
Correctness:
- push_archive() defaults poll=true again (migrate-site/admin-push report the
true destination job state) and poll_job() enforces a ~60s wall-clock
deadline with per-request capped timeouts.
- Cross-prefix restores fail closed: manifests record db_prefix; source restore
and the connector refuse with prefix_mismatch instead of importing
foreign-prefixed tables.
- sync-to-live scope schema accepts 'all'/'none' or lists, and the description
no longer claims empty arrays mean full sync; scope file-root manifest keys
use the intended root_label for numeric keys.
- Targets store computes+validates the cipher before insert (no bare-IV rows);
packager add_root_files() realpath-checks containment so a symlink cannot
exfiltrate outside wp-content.
Admin UI:
- Push form wraps build/existing archive groups in the emcp-push-build /
emcp-push-existing classes migrate.js toggles (the toggle was previously dead)
and adds a "-- none --" destination option so raw URL+secret pushes work.
Co-Authored-By: Claude <[email protected]>
Dedupe the push/verify paths that existed in both the MCP abilities and the
admin module: EMCP_Tools_Migration_Engine::archive_for_push() now owns the
build-or-reuse-an-archive logic (used by migrate-site and the admin Push tab),
and EMCP_Tools_Migrate_Targets::verify_endpoint() (now public) backs the admin
Verify-target action instead of a hand-rolled request block.
Co-Authored-By: Claude <[email protected]>
Gate: clear the web a11y bugs on the new admin surfaces — labelled inputs +
ids in page-migrate.php (raw URL/secret fields, archive select) and the
connector settings page, which also drops its layout-table/th markup.
Duplication: fold the abilities + module destination resolvers into one
EMCP_Tools_Migration_Engine::destination_from_input() (the MCP tools and the
admin now share identical validation).
Code smells: sync_to_target/file_root_path/redeem_pairing_code/add_files/
execute_url_search_replace split into smaller helpers; unused params removed
from the connector pair/verify REST handlers and the scope-capability check;
"SELECT * FROM " literal const'd in the targets store; JS uses .dataset +
optional chaining; nested ternaries flattened.
Co-Authored-By: Claude <[email protected]>
- EMCP_Tools_Migration_Engine: shared transfer engine — the four private
helpers that lived in the migrate abilities moved here verbatim
(push_packets/finalize/poll_job) plus push_archive orchestration.
- EMCP_Tools_Migrate_Targets: {prefix}emcp_migrate_targets store, encrypted
secrets (never returned), single-use pairing-code redemption. Connector is
now 1.2.0: adds /pair/issue, open /pair/exchange (HTTPS-gated), /verify, a
settings page that issues codes, and advertises scope-restore.
- EMCP_Tools_Sync_Engine + Packager scope opts: full/selective .emcp sync
archives (db all|none|tables[], file_roots all|none|roots[]) with additive
scope+kind manifest keys (absent scope = full; format_version stays 2).
- Scope-aware restore on both sides: search-replace gated on a real DB import
and restricted to archived tables; connector no longer errors on DB-less
(files-only) archives.
- MCP surface: migrate-site gains optional target_id (raw remote_url+secret
back-compat kept); sync-to-live is now the documented scoped push; new
url-search-replace tool carries the old local search-replace body
(disabled-by-default via DEFAULTS_VERSION 37, drift-guard + catalog wired).
- Admin: Backup & Migrate tab split into Archives | Push to Site | Sync with
paired-target add/delete/verify + blocking push/sync handlers; adds
assets/js/migrate.js and pro-manifest entry.
- Tests: migrate/packager assert suite extended (scope normalization, root
traversal guards, scoped archive manifests, files-only has no DB dump).
Co-Authored-By: Claude <[email protected]>
Code-review findings verified against the current code; only still-valid issues
fixed, minimal churn, no .emcp-format or push-protocol changes.
- migrate-site: schema now accepts direction "pull" so the explicit
unsupported error is reachable (matches the docs); a restore is reported as
success only when the destination job state is "done" — pending/unknown/
queued return a WP_Error carrying the job details.
- search-replace engine: walk_table() with before_cap=0 no longer sets
partial (no before-image capture is not truncation).
- connector: hashless archives (no database_sha256) now import instead of
failing integrity; import statement errors stay under db stats (non-fatal)
so a partial import still gets the URL rewrite, while stream/hash failures
remain fatal and drive the job state; destination URL is captured before the
import replaces wp_options; rewrite_rows/search_replace report partial when
the per-table cap is hit so a run can never silently claim done.
- restore engine: destination URL captured before import; hash-mismatch abort
no longer double-closes/cleans (the finally block owns cleanup); DB stream
extraction requires a successful stream_copy_to_stream and closes handles on
every failure path.
- packager: list_archives() no longer hashes every archive (full read per row
on a plain listing); create_archive validates addFile/addFromString/close and
removes a partial archive on failure; default filenames gain a random suffix
and the backups dir gets .htaccess/index.html web-denial guards.
- search-replace wrapper guards on the engine class existing.
- migrate module: drains output buffers before streaming binary downloads.
- tests: extended harness with a walk_table before_cap=0 case (9 suites green).
Co-Authored-By: Claude <[email protected]>
The fork's Pro merge left Backup/Restore/Migrate/Sync as placeholders (4 naive
files): no restore path, migrate-site was a canned-success stub, sync-to-live
touched only wp_options, and the connector exposed only a /status route. This
makes the story real at a bounded scope and fixes the wiring/doc drift found in
a feature-parity review against upstream Pro.
- search-replace engine: byte-accurate serialized fixer (length-delta, never
unserializes), JSON escaped+unescaped URL pair replace (_elementor_data), and
a site-wide table walker; legacy EMCP_Tools_Search_Replace now delegates.
- backups: streamed DB export (bounded chunks, explicit-column INSERTs), richer
.emcp manifest (format version, site_url, php/wp/db, DB sha256 + row stats),
opt-in include_files bundling of uploads/plugins/themes.
- restore: hardened statement-aware importer (skips transaction-control
directives, ';'-in-string safe); new Restore_Engine (hash-verified -> import
-> URL rewrite -> traversal-guarded file placement); admin Restore/Download/
Delete + restore-history panel (nonce + manage_options, admin-only).
- migrate-site: real push to a standalone HMAC connector (2 MB signed packets ->
assemble -> verify -> self-contained restore -> pollable job). confirm:true
required; direction "pull" returns unsupported.
- sync-to-live: real site-wide serialized-safe URL search-replace across
options/posts/postmeta/comments/term+usermeta + prefixed tables, ledger-
recorded for History rollback.
- remove 3 never-registered ghost tools (list-syncable-changes,
sync-content-item, discard-sync-change) from catalog/slugs/seeding.
- AI Chat: canonical module id 'ai-chat' (admin toggle now works, legacy
ai_chat read-back), /emcp/v1/chat route gated on module state, default on.
- docs: correct README/readme.txt over-claims (WooCommerce op list, addon slugs,
atomic wording, SEO/forms counts), tidy pro-manifest.txt; bump 3.16.0.
Tests: extended tests/test-migrate-packager.php (8 suites, all passing) covering
byte-accurate SR, export/import round trip, directive-skip, quote-safe split,
and packager create/read/list/delete.
Co-Authored-By: Claude <[email protected]>
- Overhaul README.md with Adeptus Mechanicus / Heretek Control Core theme and updated badges
- Update REPO constant in class-github-updater.php to Heretek-AI/heretek-control-core
- Update Plugin URI in emcp-tools.php and help URLs across class-admin.php and page-dashboard.php
- Update clone and issue links in CONTRIBUTING.md
- Neutralize Freemius update hooks in class-freemius-abstract.php and class-fs-plugin-updater.php
- Add admin_init cache purge for stale Freemius update notices on update-core.php
- Rebrand plugin to Heretek Control Core: Unlocked Machine Rites
- Update admin menu to Heretek AI and transform submenus (Cogitator Deck, Machine Rites, Noosphere Uplink, Cyber-Theurgy, STC Archives, Engram Memory, Canticle Library, Adept Doctrines)
- Update app bar header to HERETEK CONTROL CORE with ARCH-HERETEK UNLOCKED badge
- Update help menu to Omnissiah Archives and drawer to Vox Transmissions
- Add bin/release.mjs with getNextVersion rollover (+0.0.1 where 0.0.9 -> 0.1.0)
- Add bin/release.test.mjs unit test suite for versioning
- Add .gitattributes to exclude tests and development cruft from release archives
- Add .github/workflows/release.yml for manual workflow_dispatch releases with ZIP asset publishing
- Broaden CSS custom properties to :root and body shells
- Enforce pure white color (#ffffff) on all h1-h6 headings across all EMCP screens
- Theme WordPress postboxes to dark metal cards (#111116) with dark headers (#16161d) and white titles
- Theme widefat and wp-list-table with dark zebra striping and high-contrast links
- Style all form controls (input, select, textarea) and buttons to Blood & Steel palette
- Elevate description text to readable #a1a1aa and labels to #ffffff
- Remove nested .wrap classes in page-migrate.php and page-memory.php
- Bump EMCP_TOOLS_VERSION to 3.14.5
- Set display: none on closed emcp-notif-drawer to remove it from layout calculation
- Remove backdrop-filter containing-block trap on .emcp-appbar
- Eliminate legacy negative margins on .elementor-mcp-stats
- Enforce overflow-x: hidden / clip across html, body, #wpwrap, and #wpcontent
- Normalize #wpbody-content to float: none and width: auto
- Bump EMCP_TOOLS_VERSION to 3.14.4
- Bump EMCP_TOOLS_VERSION to 3.14.3 and use filemtime for instant asset cache-busting
- Add is_emcp_screen helper in class-admin.php to ensure assets enqueue on all submenus
- High-contrast pure white typography and dark metal surface on elementor-mcp-low-mode-card
- High-contrast styling for form-table on AI Chat tab (white labels, readable descriptions, dark inputs)
- Deep obsidian pre/code background with bright monospace text on elementor-mcp-context-preview
- Lock notification drawer with visibility:hidden, opacity:0, and pointer-events:none when closed
- Replace inline #fff styles in page-redirects.php, page-marketplace.php, and page-pro-upsell.php with dark theme cards and tables
- Eliminate 10px light gap between wpadminbar and emcp-appbar
- Reset .wrap.elementor-mcp-admin margins and padding to 0
- Add admin_body_class hook to inject emcp-admin-page emcp-theme-dark
- Paint wpcontent, wpbody-content, and wrap canvas with #09090b Obsidian Void
- Re-theme remaining white sub-cards, filters, disclosures, notices, and footer to dark steel
- Remove Featured video guides section and YouTube tutorial embeds from the dashboard
- Remove EMCP Cloud and Pro marketing sidebar cards from the dashboard
- Remove sticky rotating announcement marquee bar advertising Cloud and LTD
- Remove app-bar Cloud connection nag button
- Refactor Explore your toolkit and Help & resources into clean full-width responsive grids
- Unlock settings sync entitlement and remove paid cloud upgrade prompts
LobeHub (and other) agents kept failing to call emcp-tools/comic-write
because its "scheme" was invisible: the static schema exposed only an
unconstrained operation string plus an opaque free-form arguments object,
and comic-write uniquely marked operation as REQUIRED, so the documented
"call with no operation to list the catalog" discovery call was blocked at
the schema level. The hand-maintained runtime catalog had also drifted from
the executors, omitting status (which defaults to publish), date, author,
taxonomies, content_warning, and comic_html_above/below — exactly the facts
agents were guessing at.
- comic-write no longer requires operation, so the empty-op discovery call
is legal; both tools' operation property now carries an enum of the real
operation names.
- Add op_schema() to the read and write operations classes as the single
source of truth (typed JSON Schema + a realistic example per op). The
discovery catalog is generated from it — each entry keeps the flat
arguments hints and gains typed schema + example — so the docs can never
drift from the executors again.
- Tool descriptions now state the discovery flow and the create-comic
contract (status defaults to "publish", date backdating, image flow,
author).
- Clarify in both emcp-comic-easel SKILL.md files that imported strips must
always pass "status": "draft" explicitly, since the tool's own default is
publish.
- Tests: new schema-exposure suite (no required operation, enum == spec
keys, catalog schema+example, status default/fields exposed, and a parity
guard pinning every executor field to the spec). All 8 suites pass.
Co-Authored-By: Claude <[email protected]>
The Sandbox admin page fatals on shad-base.com with
"Call to undefined method EMCP_Tools_Block_Store::user_has_access()"
(views/sandbox/overview.php:41). The store loads eagerly so class_exists()
passes, but the class shipped by the unlocked port (9176dd7) was never given
the API its own views call — pagination commit b7cfe45 updated blocks.php and
the widget/snippet stores but not the block store.
Add the missing consumer-facing methods, mirroring the widget store and the
EMCP_Tools_Sandbox_Store base:
- user_has_access() (overview.php:41, blocks.php:21, admin AJAX guards)
- summary() admin-table row: block_id/block_name, publish->active
- list_blocks() overview card counts
- list_blocks_page() blocks.php table via EMCP_Tools_Sandbox_List_Query
- get_asset() block.json/render.php code viewer (whitelisted)
- create_block() fixes latent fatal in apply_bundle() (bundle import)
- uninstall_cleanup() uninstaller.php:77
Harden create()/update()/compile() with a class_exists guard on the deferred
EMCP_Tools_Block_Generator (matches class-widget-store.php:372).
New tests/test-sandbox-blocks.php: 7 suites — method presence, user_has_access
gate, summary mapping, overview.php render smoke (the previously-fatal file),
get_asset whitelist round-trip, apply_bundle->create_block draft import, and
list_blocks_page shape. All pass; migrate/comic/skills suites still green.
Co-Authored-By: Claude <[email protected]>