*/
final class Main {
/**
* The single instance of the class.
*
* @var AcrossAI_Abilities_Manager
* @since 0.0.1
*/
protected static $instance = null;
/**
* The autoloader instance.
*
* @since 0.0.1
* @access protected
* @var Autoloader $autoloader The plugin autoloader instance.
*/
protected $autoloader;
/**
* The loader that's responsible for maintaining and registering all hooks that power
* the plugin.
*
* @since 0.0.1
* @access protected
* @var AcrossAI_Abilities_Manager_Loader $loader Maintains and registers all hooks for the plugin.
*/
protected $loader;
/**
* The unique identifier of this plugin.
*
* @since 0.0.1
* @access protected
* @var string $plugin_name The string used to uniquely identify this plugin.
*/
protected $plugin_name;
/**
* The plugin dir path
*
* @since 0.0.1
* @access protected
* @var string $plugin_path The string for plugin dir path
*/
protected $plugin_path;
/**
* The current version of the plugin.
*
* @since 0.0.1
* @access protected
* @var string $version The current version of the plugin.
*/
protected $version;
/**
* Plugin directory path.
*
* @var string
*/
protected $plugin_dir;
/**
* Boot-resilience flag set by load_composer_dependencies() when
* vendor/autoload_packages.php is absent. When true, __construct()
* registers a manage_options-gated admin notice and returns before
* load_dependencies() — preventing the AcrossAI_Loader class-not-found
* fatal at line 228 that was the reproducer for Feature 038 (Constitution
* §V Integration Resilience).
*
* @since 0.0.1
* @var bool
*/
private bool $vendor_missing = false;
/**
* Define the core functionality of the plugin.
*
* Set the plugin name and the plugin version that can be used throughout the plugin.
* Load the dependencies and set the hooks for the admin area and
* the public-facing side of the site.
*
* @since 0.0.1
*/
private function __construct() {
$this->define_constants();
$this->plugin_name = 'acrossai-abilities-manager';
$this->version = ACROSSAI_ABILITIES_MANAGER_VERSION;
// Load the autoloader class manually before registering it.
$this->plugin_dir = ACROSSAI_ABILITIES_MANAGER_PLUGIN_PATH;
$this->load_composer_dependencies();
// Feature 038 boot resilience (T011 / SEC-001 / DEC-FAIL-OPEN-NOTICE):
// When vendor/autoload_packages.php is absent the PSR-4 autoloader
// is not registered, so referencing any plugin-namespaced class
// would fatal. Register a self-contained admin notice (closure
// uses only WP globals — no $this->, no FQCN under
// AcrossAI_Abilities_Manager\…) and short-circuit before
// load_dependencies() and load_hooks().
if ( $this->vendor_missing ) {
add_action(
'admin_notices',
static function () {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
printf(
'
',
esc_html__( 'AcrossAI Abilities Manager:', 'acrossai-abilities-manager' ),
esc_html__( 'The Composer autoloader is missing. Run "composer install" inside the plugin directory to restore functionality.', 'acrossai-abilities-manager' )
);
}
);
return;
}
$this->load_dependencies();
$this->load_hooks();
}
/**
* Main AcrossAI_Abilities_Manager Instance.
*
* Ensures only one instance of WooCommerce is loaded or can be loaded.
*
* @since 0.0.1
* @static
* @see AcrossAI_Abilities_Manager()
* @return AcrossAI_Abilities_Manager - Main instance.
*/
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Define WCE Constants
*/
private function define_constants() {
$this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_BASENAME', plugin_basename( \ACROSSAI_ABILITIES_MANAGER_PLUGIN_FILE ) );
$this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_PATH', plugin_dir_path( \ACROSSAI_ABILITIES_MANAGER_PLUGIN_FILE ) );
$this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_URL', plugin_dir_url( \ACROSSAI_ABILITIES_MANAGER_PLUGIN_FILE ) );
$this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_NAME_SLUG', $this->plugin_name );
$this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_NAME', 'AcrossAI Abilities Manager' );
$this->define( 'ACROSSAI_ABILITIES_MANAGER_VERSION', '0.0.44' );
}
/**
* Define constant if not already set.
*
* @param string $acrossai_name The constant name.
* @param string|bool $acrossai_value The constant value.
*/
private function define( $acrossai_name, $acrossai_value ) {
if ( ! defined( $acrossai_name ) ) {
define( $acrossai_name, $acrossai_value );
}
}
/**
* Register all the hook once all the active plugins are loaded
*
* Uses the plugins_loaded to load all the hooks and filters
*
* @since 0.0.1
* @access private
*/
public function load_hooks() {
/**
* Check if plugin can be loaded safely or not
*
* @since 0.0.1
*/
if ( apply_filters( 'acrossai_abilities_manager_load', true ) ) {
$this->define_admin_hooks();
$this->define_public_hooks();
}
}
/**
* Load the required composer dependencies for this plugin.
*
* @since 0.0.1
* @access private
*/
private function load_composer_dependencies() {
/**
* Add composer file
*/
$plugin_path = ACROSSAI_ABILITIES_MANAGER_PLUGIN_PATH;
if ( file_exists( $plugin_path . 'vendor/autoload_packages.php' ) ) {
require_once $plugin_path . 'vendor/autoload_packages.php';
return;
}
// Feature 038 boot resilience: signal __construct() to short-circuit
// before load_dependencies() touches the PSR-4 autoloader. Surfaced
// to admins via the manage_options-gated admin notice in __construct().
$this->vendor_missing = true;
}
/**
* Load the required dependencies for this plugin.
*
* Include the following files that make up the plugin:
*
* - AcrossAI_Abilities_Manager\Admin\Loader. Orchestrates the hooks of the plugin.
* - AcrossAI_Abilities_Manager\Admin\Main. Defines all hooks for the admin area.
* - AcrossAI_Abilities_Manager_Public. Defines all hooks for the public side of the site.
*
* Create an instance of the loader which will be used to register the hooks
* with WordPress.
*
* @since 0.0.1
* @access private
*/
private function load_dependencies() {
$this->loader = AcrossAI_Loader::instance();
}
/**
* Register all of the hooks related to the admin area functionality
* of the plugin.
*
* @since 0.0.1
* @access private
*/
private function define_admin_hooks() {
$plugin_admin = new \AcrossAI_Abilities_Manager\Admin\Main( $this->get_plugin_name(), $this->get_version() );
$this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' );
$this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' );
$this->loader->add_action( 'plugin_action_links', $plugin_admin, 'plugin_action_links', 1000, 2 );
/**
* Add the Plugin Main Menu
*/
$main_menu = new \AcrossAI_Abilities_Manager\Admin\Partials\Menu( $this->get_plugin_name(), $this->get_version() );
$this->loader->add_action( 'admin_menu', $main_menu, 'register_submenu' );
// Priority 999: runs after every plugin sharing the `acrossai` parent has
// registered, so Quick Connect can be placed relative to Abilities rather
// than at an absolute slot other plugins are also competing for.
$this->loader->add_action( 'admin_menu', $main_menu, 'reorder_submenu', 999 );
// Quick Connect onboarding wizard (Feature 099). No page of its own — it
// renders through Partials\Menu when `?quick-connect=1` is present. The
// page class owns its enqueue per PATTERN-PARTIALS-SELF-ENQUEUE, keeping
// a page-specific bundle out of Admin\Main::enqueue_scripts().
$quick_connect_page = \AcrossAI_Abilities_Manager\Admin\Partials\QuickConnect\QuickConnectPage::instance();
$this->loader->add_action( 'admin_enqueue_scripts', $quick_connect_page, 'enqueue_assets' );
$this->loader->add_filter( 'admin_body_class', $quick_connect_page, 'add_body_class' );
$this->loader->add_action( 'in_admin_header', $quick_connect_page, 'suppress_admin_notices', 1000 );
// Feature 102: the retired Ability Integrations URL. `admin_page_access_denied` is the hook
// that actually catches it — core denies an unregistered `page` in
// wp-admin/includes/menu.php:384, reached from admin.php:163, which is BEFORE
// do_action( 'admin_init' ) at admin.php:180. An admin_init handler at any priority is too
// late and the visitor gets "Sorry, you are not allowed to access this page". admin_init
// priority 1 is registered too, for the case where something else still registers the
// legacy slug and access is therefore never denied.
$integrations_redirect = \AcrossAI_Abilities_Manager\Admin\Partials\Integrations_Redirect::instance();
$this->loader->add_action( 'admin_page_access_denied', $integrations_redirect, 'maybe_redirect', 1 );
$this->loader->add_action( 'admin_init', $integrations_redirect, 'maybe_redirect', 1 );
// Priority 5: early enough to redirect before other admin_init work
// renders anything, late enough that the plugin API is available.
$quick_connect_redirect = \AcrossAI_Abilities_Manager\Admin\Partials\QuickConnect\ActivationRedirect::instance();
$this->loader->add_action( 'admin_init', $quick_connect_redirect, 'maybe_redirect', 5 );
// Toolbar chip — the one entry point present on every admin screen, so
// the wizard stays reachable after its single automatic opening.
// Priority 100 settles the node to the right of core's own entries.
$quick_connect_bar = \AcrossAI_Abilities_Manager\Admin\Partials\QuickConnect\AdminBarEntry::instance();
$this->loader->add_action( 'admin_bar_menu', $quick_connect_bar, 'register_node', 100 );
// Settings sections (Feature 019; reparented to host page in Feature 038;
// migrated to instance-method API + tab-scoped option_group in Feature 045).
// Host page rendering owned by acrossai-co/main-menu — the plugin
// only contributes (a) an "Abilities" tab via the
// `acrossai_settings_tabs` filter, and (b) settings sections
// targeting the per-tab page slug derived by
// SettingsPage::get_settings_renderer()->tab_page_slug() (main-menu v0.0.14+).
// option_group is the SAME tab-scoped slug, so each tab has its own
// whitelist — preventing the cross-tab option-clobber bug that
// shared-`acrossai-settings` had in 0.0.12.
// Named variable before Loader call — Boot Flow Rule variable-first pattern (AC-HOOKS-MAIN).
$settings_menu = \AcrossAI_Abilities_Manager\Admin\Partials\SettingsMenu::instance();
$this->loader->add_filter( 'acrossai_settings_tabs', $settings_menu, 'register_tab' );
$this->loader->add_action( 'admin_init', $settings_menu, 'register_settings' );
// Feature 046 — Absorbed Core Settings field (extra MIME types) renders
// INSIDE the existing Abilities tab. Core_Settings_Menu does not register
// its own tab and does not register a second uninstall-opt-in; the
// manager's SettingsMenu owns both concerns. Variable-first per AC-HOOKS-MAIN.
$core_settings_menu = \AcrossAI_Abilities_Manager\Admin\Partials\Core_Settings_Menu::instance();
$this->loader->add_action( 'admin_init', $core_settings_menu, 'register_settings' );
// Feature 120 — the Integrations directory, a submenu of its own directly below Abilities.
// `register_submenu` runs at the default priority so it is in place before Menu's
// `reorder_submenu` at 999 moves it into position. Admin-only, so it belongs in
// define_admin_hooks() (Boot Flow Rule). Variable-first per AC-HOOKS-MAIN.
$integrations_menu = \AcrossAI_Abilities_Manager\Admin\Partials\Integrations_Menu::instance();
$this->loader->add_action( 'admin_menu', $integrations_menu, 'register_submenu' );
$this->loader->add_action( 'admin_init', $integrations_menu, 'register_settings' );
$this->loader->add_action( 'admin_enqueue_scripts', $integrations_menu, 'enqueue_assets' );
// The routes behind that screen. Separate controller rather than more routes on the
// abilities one: this is the only place in the plugin that reaches acrossai.co, and keeping
// it in its own file keeps that fact findable.
$integrations_rest = \AcrossAI_Abilities_Manager\Includes\Abilities\Rest\Integrations_Directory_Controller::instance();
$this->loader->add_action( 'rest_api_init', $integrations_rest, 'register_routes' );
// Uninstall Settings section is wired LAST — priority 20 pushes it below
// every default-priority section (Display Settings above, plus
// Core_Settings_Menu's Upload Media Abilities section). Sections render
// in the order they were added, so a later priority puts this at the
// bottom of the Abilities tab.
$this->loader->add_action( 'admin_init', $settings_menu, 'register_uninstall_settings', 20 );
// Feature 092 — File Manager settings tab (write allowlist + read
// allowlist + configurable secret redactor). Registers a new tab on
// the shared AcrossAI settings page and enqueues the React bundle.
$file_manager_settings = \AcrossAI_Abilities_Manager\Admin\Partials\File_Manager_Settings_Menu::instance();
$this->loader->add_filter( 'acrossai_settings_tabs', $file_manager_settings, 'register_tab' );
$this->loader->add_action( 'admin_init', $file_manager_settings, 'register_settings' );
$this->loader->add_action( 'admin_enqueue_scripts', $file_manager_settings, 'enqueue_assets' );
// Feature 092 — REST endpoints backing the File Manager settings tab.
$file_manager_rest = \AcrossAI_Abilities_Manager\Includes\Abilities\Rest\File_Manager_Settings_Controller::instance();
$this->loader->add_action( 'rest_api_init', $file_manager_rest, 'register_routes' );
// Feature 134 — Database settings tab (access mode + table denylist + row/time caps +
// redaction and mutation logging). Same wiring as the File Manager tab above, deliberately:
// both tabs register through `acrossai_settings_tabs`, host a React mount in a placeholder
// Settings API section, and save over REST rather than through the vendor form.
$database_settings = \AcrossAI_Abilities_Manager\Admin\Partials\Database_Settings_Menu::instance();
$this->loader->add_filter( 'acrossai_settings_tabs', $database_settings, 'register_tab' );
$this->loader->add_action( 'admin_init', $database_settings, 'register_settings' );
$this->loader->add_action( 'admin_enqueue_scripts', $database_settings, 'enqueue_assets' );
// Feature 134 — REST endpoints backing the Database settings tab.
$database_rest = \AcrossAI_Abilities_Manager\Includes\Abilities\Rest\Database_Settings_Controller::instance();
$this->loader->add_action( 'rest_api_init', $database_rest, 'register_routes' );
// Feature 134 — apply the database policy to every `acrossai-database` ability. Hooked here
// rather than added to nineteen separate execute() methods, so the policy has one enforcement
// point and a future database ability inherits it instead of shipping ungoverned.
\AcrossAI_Abilities_Manager\Includes\Abilities\Utilities\Db_Policy_Enforcer::register();
// Add-ons submenu page (Feature 026 → 030 → 039 → 053).
// As of acrossai-co/main-menu 0.0.21 the `\AcrossAI_Addon\AddonsPage`
// entry-class is removed and the Add-ons submenu is registered
// internally by `\AcrossAI_Main_Menu\MenuRegistrar` when the shared
// `\AcrossAI_Main_Menu\SettingsPage` bootstrap runs (done at
// `plugins_loaded @ P0` in this plugin's entry file, see
// acrossai-abilities-manager.php). Consumers extend the shared list
// via `add_filter( 'acrossai_addons', … )`; freemius integration was
// dropped upstream in 0.0.21 so no `fs_*` args are passed anymore.
// The submenu URL admin.php?page=acrossai-addons is unchanged.
// Abilities DB table setup — BerlinDB hooks maybe_upgrade() to admin_init.
// Named variable before Loader call — Boot Flow Rule variable-first pattern (AC-HOOKS-MAIN).
$abilities_table = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\Database\AcrossAI_Abilities_Table::instance();
// Register Access Control REST routes and library-missing notice (SAC-01).
// The library-missing notice is routed through the shared main-menu 0.0.30
// `acrossai_notices` filter — surfaces on AcrossAI → Notices submenu +
// the top-of-page summary banner instead of a raw admin_notices banner.
$abilities_ac = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\AcrossAI_Abilities_Access_Control::instance();
$this->loader->add_action( 'rest_api_init', $abilities_ac, 'register_rest_api' );
$this->loader->add_filter( 'acrossai_notices', $abilities_ac, 'register_library_notice' );
// Abilities module REST routes (Spec 009).
// Named variable before Loader call — Boot Flow Rule variable-first pattern.
$abilities_rest = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\Rest\AcrossAI_Abilities_Rest_Controller::instance();
$this->loader->add_action( 'rest_api_init', $abilities_rest, 'register_routes' );
// Feature 101: rewrite stored `acrossai-abilities-manager-*` category
// slugs to `acrossai-*`. Also called from Activator::activate(), but an
// in-place upgrade (composer, wp-cli, file replace) never re-activates,
// so admin_init is the path that actually catches most sites. Guarded by
// its own completion flag, so this is one option read per admin request
// once it has run. Priority 1 — the DB-defined abilities it repairs
// register on wp_abilities_api_init, which is later.
$category_slug_migration = \AcrossAI_Abilities_Manager\Includes\Modules\Library\AcrossAI_Category_Slug_Migration::class;
$this->loader->add_action( 'admin_init', $category_slug_migration, 'maybe_migrate', 1 );
// Library Registry — collect add-on definitions at init P99 (Feature 027).
// Named variable before Loader call — Boot Flow Rule variable-first pattern.
$ability_library_registry = \AcrossAI_Abilities_Manager\Includes\Modules\Library\AcrossAI_Ability_Library_Registry::instance();
$this->loader->add_action( 'init', $ability_library_registry, 'collect', 99 );
// Feature 099: publish the tab-group summary as a filter so consumers
// outside the Library module (the Quick Connect wizard) can read it
// without importing Library classes. Constitution Module Contract #3
// forbids sibling-module reach-through; #4 makes filters the sanctioned
// integration point. Consumers call:
// apply_filters( 'acrossai_ability_library_tab_group_summary', array() ).
$this->loader->add_filter( 'acrossai_ability_library_tab_group_summary', $ability_library_registry, 'get_tab_group_summary' );
// Feature 102: the same seam for the full definition rows. The gate translation lives in
// the Abilities module and needs whole definitions, not the tab-group summary — reading
// the Registry directly would be the sibling-module reach-through Module Contract #3
// forbids. Consumers call:
// apply_filters( 'acrossai_ability_library_definitions', array() ).
$this->loader->add_filter( 'acrossai_ability_library_definitions', $ability_library_registry, 'provide_definitions' );
}
/**
* Register all of the hooks related to the public-facing functionality
* of the plugin.
*
* @since 0.0.1
* @access private
*/
private function define_public_hooks() {
$plugin_public = new \AcrossAI_Abilities_Manager\Front\Main( $this->get_plugin_name(), $this->get_version() );
$this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
$this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
// Feature 102 — translate the retired library gate into per-ability overrides.
//
// init P100, i.e. immediately after AcrossAI_Ability_Library_Registry::collect() at init P99
// (see below). The translation reads that registry, so anything earlier sees zero
// definitions.
//
// This was originally plugins_loaded P1 and that was wrong in the worst way: the migration
// ran, found no definitions, translated nothing, and still retired the source option —
// silently discarding the configuration it existed to preserve. Do not move this earlier
// than init P100 without also proving get_definitions() is populated at the new point.
//
// It is deliberately NOT admin_init: the exposure this closes is reachable via REST and MCP,
// neither of which loads wp-admin, so an admin-only trigger would leave a site nobody
// administers serving every previously blocked ability. init fires on every request path.
//
// Public hook, so registered here rather than in define_admin_hooks() (Boot Flow Rule).
$gate_migration = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\AcrossAI_Library_Gate_Migration::instance();
$this->loader->add_action( 'init', $gate_migration, 'maybe_migrate', 100 );
// Adds isGlobalStylesUserThemeJSON + version to wp_global_styles records written before
// 0.0.41, which WordPress was ignoring outright. Same reasoning as the gate migration for the
// hook choice: an admin-only trigger would leave an unattended site serving theme defaults
// while its abilities reported a DB record as effective. It reads no request input and is
// claimed once per site.
$global_styles_flags = \AcrossAI_Abilities_Manager\Includes\Abilities\Utilities\Global_Styles\Global_Styles_Flag_Migration::instance();
$this->loader->add_action( 'init', $global_styles_flags, 'maybe_migrate', 100 );
// Feature 184: give abilities registered by other plugins a toolset. Hooked to the core
// wp_register_ability_args filter at P10 so the value is on the WP_Ability itself, which is
// what makes the tab strip, the counts, the Toolset column and the MCP dispatchers all agree
// without any of them knowing the tagger exists. Well before the override processor's
// P100000; the two touch different keys.
$group_tagger = \AcrossAI_Abilities_Manager\Includes\Utilities\AcrossAI_Ability_Group_Tagger::class;
$this->loader->add_filter( 'wp_register_ability_args', $group_tagger, 'tag', 10, 2 );
// Ability Override Processor — boot at plugins_loaded P20 and bust cache on override save.
// Named variable before Loader calls satisfies the Boot Flow Rule (SEC-PLAN-002).
$override_processor = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\AcrossAI_Ability_Override_Processor::instance();
$this->loader->add_action( 'plugins_loaded', $override_processor, 'boot_hook', 20 );
// CRITICAL-01 fix (sub-change 11d): wire bust_cache_hook to all three Abilities lifecycle hooks.
$this->loader->add_action( 'acrossai_abilities_after_create', $override_processor, 'bust_cache_hook' );
$this->loader->add_action( 'acrossai_abilities_after_update', $override_processor, 'bust_cache_hook' );
$this->loader->add_action( 'acrossai_abilities_after_delete', $override_processor, 'bust_cache_hook' );
// Abilities Processor — registers published source=db abilities at wp_abilities_api_init P10 (Spec 009).
// Named variable before Loader call — Boot Flow Rule variable-first pattern.
$abilities_processor = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\AcrossAI_Abilities_Processor::instance();
$this->loader->add_action( 'wp_abilities_api_init', $abilities_processor, 'register_abilities', 10 );
// Library Processor — registers add-on abilities at wp_abilities_api_init P5 (Feature 027).
// Runs before the DB processor at P10 so add-on abilities are gated first.
// Named variable before Loader call — Boot Flow Rule variable-first pattern.
$ability_library_processor = \AcrossAI_Abilities_Manager\Includes\Modules\Library\AcrossAI_Ability_Library_Processor::instance();
$this->loader->add_action( 'wp_abilities_api_init', $ability_library_processor, 'register_abilities', 5 );
// Feature 046 — Absorbed Core Abilities Bootstrap. Registers 17 category
// callbacks with the Loader (each is one $this->loader->add_action on
// wp_abilities_api_categories_init) and hooks the 176-class ability
// instantiation to plugins_loaded @ P20 matching the companion plugin's
// original hook point. See specs/046-absorb-core-abilities-into-manager/.
$core_abilities_bootstrap = \AcrossAI_Abilities_Manager\Includes\Abilities\AcrossAI_Core_Abilities_Bootstrap::instance();
$core_abilities_bootstrap->register_category_callbacks( $this->loader );
$this->loader->add_action( 'plugins_loaded', $core_abilities_bootstrap, 'register_abilities', 20 );
// Feature 060 — Third-party integration bootstraps. Each concrete
// subclass of AcrossAI_Integration_Ability_Base registers its own
// hooks from the constructor (plugins_loaded P20 for the enable-filter
// side-effect + acrossai_abilities_api_init P10 for the synthetic
// definition row). Both hook callbacks gate on subclass
// is_plugin_active() so a missing target plugin renders no card, no
// tab, and attaches no filter. Boot Flow Rule deviation is documented
// under DEC-ABILITY-DEFINITION-CTOR-HOOKS (Feature 060), sibling to
// DEC-EXTERNAL-PACKAGE-HOOK-CTOR. See
// specs/060-library-third-party-integration-toggles/plan.md
// Complexity Tracking for the justification.
//
// Instantiation happens synchronously here (from define_public_hooks(),
// which itself runs at plugins_loaded @ P0 via the plugin entry file)
// so the constructor's plugins_loaded P20 add_action registers before
// P20 fires. Adding another integration is a one-line change here.
// Feature 117 — seed the opt-ins whose default is ON before any of them is constructed. The
// base class reads the toggle at plugins_loaded P20 and we are at P0, so seeding here means
// a default-on integration is correct on the very first request rather than the second.
\AcrossAI_Abilities_Manager\Includes\Modules\Library\AcrossAI_Integration_Default_Opt_Ins::maybe_seed();
// And again once every plugin has loaded. At P0 a host's `is_active()` probe cannot see it
// yet — `class_exists( '\RankMath\Helper' )` is false this early — so the pass above only
// ever covers the opt-in slugs it is timed for. The second pass is what seeds the
// integrations discovered from the registry, and it is safe to run because seeding is
// idempotent per slug: anything the first pass settled is skipped, and a stored `false` is a
// decision the seed never overwrites.
$this->loader->add_action(
'init',
\AcrossAI_Abilities_Manager\Includes\Modules\Library\AcrossAI_Integration_Default_Opt_Ins::class,
'maybe_seed_discovered',
1
);
new \AcrossAI_Abilities_Manager\Includes\Abilities\Integrations\ACF();
new \AcrossAI_Abilities_Manager\Includes\Abilities\Integrations\Yoast_Seo_Opt_In();
new \AcrossAI_Abilities_Manager\Includes\Abilities\Integrations\WPForms();
// Both host their own gate: Elementor behind an option, Jetpack behind a filter its packages
// read while they load — which is why Jetpack asks for an earlier enable hook. Neither seeds
// on, because switching them on switches on a third-party feature as well as our grouping.
new \AcrossAI_Abilities_Manager\Includes\Abilities\Integrations\Elementor_Opt_In();
new \AcrossAI_Abilities_Manager\Includes\Abilities\Integrations\Jetpack_Opt_In();
// Issue #202 — start recording ability-name clashes before anything registers. Abilities
// register on wp_abilities_api_init (fired from init), so attaching here at plugins_loaded P0
// is early enough to see every attempt, including plugins that register before we do.
\AcrossAI_Abilities_Manager\Includes\Abilities\Utilities\Ability_Collision_Recorder::listen();
}
/**
* Run the loader to execute all of the hooks with WordPress.
*
* @since 0.0.1
*/
public function run() {
$this->loader->run();
}
/**
* The name of the plugin used to uniquely identify it within the context of
* WordPress and to define internationalization functionality.
*
* @since 0.0.1
* @return string The name of the plugin.
*/
public function get_plugin_name() {
return $this->plugin_name;
}
/**
* The reference to the class that orchestrates the hooks with the plugin.
*
* @since 0.0.1
* @return AcrossAI_Abilities_Manager_Loader Orchestrates the hooks of the plugin.
*/
public function get_loader() {
return $this->loader;
}
/**
* The reference to the autoloader instance.
*
* @since 0.0.1
* @return Autoloader The plugin autoloader instance.
*/
public function get_autoloader() {
return $this->autoloader;
}
/**
* Retrieve the version number of the plugin.
*
* @since 0.0.1
* @return string The version number of the plugin.
*/
public function get_version() {
return $this->version;
}
}