From 7c17b487baa57ce80fe0c21796367860c9460ccf Mon Sep 17 00:00:00 2001 From: fahimreza-dev Date: Mon, 5 Oct 2026 23:33:52 +0600 Subject: [PATCH] feat(integrations): bridle and bridle kb are plugpress plugins Bridle (bridle/*) and Bridle KB (bridle-kb/*) enrol themselves through saddle_integrations. As FIRST_PARTY their tools are live as soon as the plugin is active, like Mailyard and SEO, with no switch under Services. Saddle Pro drops its knovia catalog entry, so Core's knovia- floor in integration_prefixes() goes too: Bridle KB is grouped by the live catalog. Old names leave the Core docblocks. Closes #338 Co-Authored-By: Claude Opus 5.5 --- includes/admin/class-saddle-rest.php | 5 +- includes/class-saddle-ecosystem.php | 2 +- includes/class-saddle-integration-engine.php | 10 ++-- includes/class-saddle-integrations.php | 4 +- readme.txt | 1 + tests/integrations-test.php | 57 +++++++++++++++++++- 6 files changed, 67 insertions(+), 12 deletions(-) diff --git a/includes/admin/class-saddle-rest.php b/includes/admin/class-saddle-rest.php index 35d70b8..151163a 100644 --- a/includes/admin/class-saddle-rest.php +++ b/includes/admin/class-saddle-rest.php @@ -934,8 +934,7 @@ public static function clear_agent_memory() { * future plugin doing the same would land there too — a grouping bug nobody * would think to look for in a REST controller. * - * The literals stay as a floor so nothing regroups on sites where a - * contributor enrols later than this runs (Saddle Pro's `knovia-`), and + * The literals stay as a floor: `waggle-` is the catalog default, and * `unsplash-` is here despite not being a wrapper at all: it is an external * service from the owner's point of view, which is what this grouping is * about. @@ -943,7 +942,7 @@ public static function clear_agent_memory() { * @return string[] */ private static function integration_prefixes() { - $prefixes = array( 'waggle-', 'knovia-', 'unsplash-' ); + $prefixes = array( 'waggle-', 'unsplash-' ); if ( class_exists( 'Saddle_Integrations' ) ) { foreach ( array_keys( Saddle_Integrations::integrations() ) as $slug ) { diff --git a/includes/class-saddle-ecosystem.php b/includes/class-saddle-ecosystem.php index 0b2d579..a8a0645 100644 --- a/includes/class-saddle-ecosystem.php +++ b/includes/class-saddle-ecosystem.php @@ -3,7 +3,7 @@ * PARKED — Phase 3 ecosystem integration. * * This class is intentionally NOT loaded or instantiated anywhere. It is a - * placeholder for cross-product orchestration (inbees/outbees/mailyard/ + * placeholder for cross-product orchestration (bridle/outbees/mailyard/ * formyard/flypops), which is explicitly out of scope for v0.1 and v0.2. * * Do NOT require or instantiate this without an explicit decision to reopen diff --git a/includes/class-saddle-integration-engine.php b/includes/class-saddle-integration-engine.php index 75d5ff1..f6eb6f0 100644 --- a/includes/class-saddle-integration-engine.php +++ b/includes/class-saddle-integration-engine.php @@ -31,7 +31,7 @@ * it IS a `saddle/` ability: `waggle/get-aeo-score` → `saddle/waggle-get-aeo-score`. * * One engine, many catalogs: free Saddle instantiates it for the free - * first-party catalog (Waggle), Saddle Pro for its own (Knovia). Each + * first-party catalog (Waggle), Saddle Pro for its own. Each * caller supplies only its catalog and filter names — the wrap/executor * safety logic exists exactly once, here. * @@ -257,14 +257,14 @@ public function register_wrappers() { * * @param string $slug Integration slug. * @param array $def Catalog definition (title, force_destructive, …). - * @param string $name Source ability name, e.g. 'knovia/create-doc'. + * @param string $name Source ability name, e.g. 'bridle-kb/create-doc'. * @param object $ability Source WP_Ability. * @param array $all The ability registry snapshot from this pass. */ private function wrap( $slug, array $def, $name, $ability, array $all ) { $title = $def['title']; $source_short = substr( $name, strpos( $name, '/' ) + 1 ); - $short = $slug . '-' . $source_short; // knovia-create-doc. + $short = $slug . '-' . $source_short; // bridle-kb-create-doc. $wrapper = 'saddle/' . $short; // Collision — never overwrite an existing saddle ability. (Checked @@ -388,8 +388,8 @@ private static function canonical( array $args ) { * (whose own permission_callback core re-checks inside execute()), gate * destructive calls, and log every mutation. * - * @param string $short Wrapper short name (knovia-create-doc). - * @param string $name Source ability name (knovia/create-doc). + * @param string $short Wrapper short name (bridle-kb-create-doc). + * @param string $name Source ability name (bridle-kb/create-doc). * @param string $title Integration title. * @param bool $destructive Whether the wrapper treats the source as destructive. * @param bool $is_readonly Whether the source is read-only. diff --git a/includes/class-saddle-integrations.php b/includes/class-saddle-integrations.php index 1752f7d..98d3ca7 100644 --- a/includes/class-saddle-integrations.php +++ b/includes/class-saddle-integrations.php @@ -29,10 +29,12 @@ class Saddle_Integrations { * * `rank` is Saddle Rank, Waggle renamed in its 2.0 (it enrols itself as * saddle-rank/*). `waggle` stays for the sites still running Waggle. + * `bridle` and `bridle-kb` are Bridle, the support inbox, and Bridle KB, + * its knowledge base; both enrol themselves (bridle/*, bridle-kb/*). * * @var string[] */ - const FIRST_PARTY = array( 'waggle', 'mailyard', 'analytics', 'rank', 'crm' ); + const FIRST_PARTY = array( 'waggle', 'mailyard', 'analytics', 'rank', 'crm', 'bridle', 'bridle-kb' ); /** * Option holding the third-party slugs the owner has switched on. diff --git a/readme.txt b/readme.txt index db7debf..6ad972c 100644 --- a/readme.txt +++ b/readme.txt @@ -209,6 +209,7 @@ The WordPress.org version never checks for its own updates. The version from plu == Changelog == = 1.5.2 = +* Changed: Bridle and Bridle KB are PlugPress plugins, so their tools are on as soon as the plugin is active, like Mailyard and SEO. You no longer switch them on under Services. * Fixed: When a step shows a preview first, such as moving a post to the trash, your AI app now shows you the preview and waits for your yes. Claude Code reads only the start of Saddle's guide, and it could confirm on its own. = 1.5.1 = diff --git a/tests/integrations-test.php b/tests/integrations-test.php index 158e243..fc6e8aa 100644 --- a/tests/integrations-test.php +++ b/tests/integrations-test.php @@ -619,7 +619,7 @@ public function test_the_prefix_list_picks_up_the_live_catalog() { $this->assertContains( 'mailyard-', $list, 'A self-enrolled integration must reach the UI grouping.' ); $this->assertContains( 'waggle-', $list ); - $this->assertContains( 'knovia-', $list, 'The literal floor keeps Pro’s grouping from regressing.' ); + $this->assertNotContains( 'knovia-', $list, 'Saddle Pro no longer wraps a knowledge base; Bridle KB enrols in free Saddle.' ); } /* -------- third-party integrations: owner-approved -------- */ @@ -752,7 +752,7 @@ static function () { * plugin's tools off on every site. */ public function test_every_plugpress_plugin_is_first_party() { - foreach ( array( 'waggle', 'mailyard', 'analytics', 'rank', 'crm' ) as $slug ) { + foreach ( array( 'waggle', 'mailyard', 'analytics', 'rank', 'crm', 'bridle', 'bridle-kb' ) as $slug ) { $this->assertTrue( Saddle_Integrations::is_first_party( $slug ), "{$slug} must be first-party" ); $this->assertContains( $slug, Saddle_Integrations::FIRST_PARTY ); } @@ -796,6 +796,59 @@ static function () { $this->assertSame( 'plugpress', $rows['analytics']['source'] ); } + /** + * Bridle and Bridle KB enrol themselves as `bridle` and `bridle-kb` (#338): + * PlugPress's, so live with nothing approved. Bridle's `bridle/` prefix must + * not pick up Bridle KB's `bridle-kb/` abilities, which share its start. + */ + public function test_bridle_and_bridle_kb_are_first_party_and_stay_apart() { + $add = static function ( $integrations ) { + $integrations['bridle'] = array( + 'prefix' => 'bridle/', + 'title' => 'Bridle', + ); + $integrations['bridle-kb'] = array( + 'prefix' => 'bridle-kb/', + 'title' => 'Bridle KB', + ); + return $integrations; + }; + add_filter( 'saddle_integrations', $add ); + $this->within_abilities_init( + static function () { + foreach ( array( 'bridle/list-conversations', 'bridle-kb/get-docs' ) as $name ) { + wp_register_ability( + $name, + array( + 'label' => $name, + 'description' => 'x', + 'category' => 'saddle', + 'input_schema' => array( + 'type' => 'object', + 'default' => (object) array(), + 'properties' => (object) array(), + ), + 'execute_callback' => '__return_empty_array', + 'permission_callback' => '__return_true', + 'meta' => array( 'annotations' => array( 'readonly' => true ) ), + ) + ); + } + Saddle_Integrations::register_wrappers(); + } + ); + $rows = array_column( Saddle_Integrations::listing(), null, 'slug' ); + remove_filter( 'saddle_integrations', $add ); + + $this->assertSame( array(), get_option( Saddle_Integrations::APPROVED_OPTION, array() ) ); + $this->assertNotNull( wp_get_ability( 'saddle/bridle-list-conversations' ), 'Bridle is live with nothing approved.' ); + $this->assertNotNull( wp_get_ability( 'saddle/bridle-kb-get-docs' ), 'Bridle KB is live with nothing approved.' ); + $this->assertSame( 'plugpress', $rows['bridle']['source'] ); + $this->assertSame( 'plugpress', $rows['bridle-kb']['source'] ); + $this->assertSame( 1, $rows['bridle']['tools'], 'Bridle counts only its own bridle/* abilities.' ); + $this->assertSame( 1, $rows['bridle-kb']['tools'] ); + } + /** * An empty prefix would wrap every ability on the site; `saddle/` and * `core/` would re-expose abilities that aren't the partner's to offer.