From 5ad0a03ecbf2f389af7aee0caf3e3e4586ef942f Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 24 Jul 2026 08:49:37 -0400 Subject: [PATCH] Refine Data Machine agent routing guidance --- inc/Cli/CommandRegistry.php | 21 +-- inc/migrations/agents-md.php | 172 +++----------------- tests/agents-md-gated-composition-smoke.php | 70 +++----- 3 files changed, 52 insertions(+), 211 deletions(-) diff --git a/inc/Cli/CommandRegistry.php b/inc/Cli/CommandRegistry.php index 9d30b34d6..5721a6236 100644 --- a/inc/Cli/CommandRegistry.php +++ b/inc/Cli/CommandRegistry.php @@ -3,16 +3,14 @@ * Command Registry * * Single source of truth mapping `wp datamachine ...` command strings to their - * implementing command classes. Both the WP-CLI bootstrap (which calls - * WP_CLI::add_command for each entry) and the gated AGENTS.md `datamachine` - * section generator (which reflects over each class to enumerate real - * subcommands) read from this map, so the documented CLI surface can never - * drift from what is actually registered. + * implementing command classes. The WP-CLI bootstrap calls + * WP_CLI::add_command for each entry. Generated agent guidance advertises a + * bounded set of routing entrypoints and tests those roots against this map; + * live `--help` remains authoritative for the complete command surface. * - * This mirrors the pattern proven in extrachill-cli's CommandRegistry — the map - * is the registration source AND the documentation source, eliminating the - * hand-typed heredoc that previously narrated core's command surface from a - * downstream plugin (Extra-Chill/data-machine#2640, #2613). + * This keeps command registration centralized while Data Machine itself owns + * the concise routing guidance previously narrated by a downstream plugin + * (Extra-Chill/data-machine#2640, #2613). * * @package DataMachine\Cli */ @@ -28,12 +26,11 @@ class CommandRegistry { * * Keys are the exact strings passed to WP_CLI::add_command (the command * namespace, e.g. "datamachine memory" or "datamachine step-types"). - * Order here determines both registration order and documentation order. + * Order here determines registration order. * * Singular/plural aliases that resolve to the same class are intentionally * included so `WP_CLI::add_command` registers every accepted spelling. The - * AGENTS.md section generator de-duplicates by class when grouping, so the - * documented surface lists each command once. + * AGENTS.md introspection helpers de-duplicate by class when grouping. * * @return array */ diff --git a/inc/migrations/agents-md.php b/inc/migrations/agents-md.php index eabd7c313..7ea07453e 100644 --- a/inc/migrations/agents-md.php +++ b/inc/migrations/agents-md.php @@ -16,8 +16,8 @@ * no-noise behavior on installs with no coding agent. * - ON: core registers the AGENTS.md composable file (so `wp datamachine memory * compose AGENTS.md` writes it and auto-regeneration keeps it fresh) and the - * core-owned sections, including the `datamachine` section whose command list - * is REFLECTED from core's registered command classes (never hand-typed). + * core-owned sections, including concise Data Machine routing and discovery + * guidance. The live CLI help remains the complete command authority. * * Why composition lives in core: core already owns SectionRegistry, * MemoryFileRegistry, ComposableFileGenerator, ComposableFileInvalidation, and @@ -32,7 +32,6 @@ use DataMachine\Engine\AI\MemoryFileRegistry; use DataMachine\Engine\AI\SectionRegistry; -use DataMachine\Cli\CommandRegistry; /** * Whether AGENTS.md composition is enabled for this install. @@ -86,7 +85,7 @@ function datamachine_register_agents_md_file(): void { * * Sections registered here describe Data Machine itself. The external coding * runtime installer owns generic WordPress coding guidance, while Data Machine - * Code owns its `datamachine-code` section and workspace inventory. + * Code owns its workspace lifecycle section. * * @return void */ @@ -119,7 +118,7 @@ function () use ( $wp ) { array_merge( $core_meta, array( 'freshness' => 'generated' ) ) ); - // The reflected `datamachine` command-surface section. + // Data Machine routing and discovery guidance. SectionRegistry::register( 'AGENTS.md', 'datamachine', @@ -127,8 +126,8 @@ function () use ( $wp ) { 'datamachine_agents_md_render_datamachine_section', array( 'owner' => 'data-machine', - 'freshness' => 'snapshot', - 'conditions' => 'Registered when DATAMACHINE_COMPOSE_AGENTS_MD is enabled; command list reflected from registered command classes.', + 'freshness' => 'static', + 'conditions' => 'Registered when DATAMACHINE_COMPOSE_AGENTS_MD is enabled.', ) ); @@ -157,159 +156,30 @@ function () use ( $wp ) { /** * Render the `## Data Machine` AGENTS.md section. * - * The command list is reflected from CommandRegistry::map() — the same map that - * feeds WP-CLI registration in Bootstrap.php — so adding or renaming a - * `datamachine` subcommand surfaces here automatically with no heredoc edit, and - * a command removed from core (e.g. the now-relocated `analytics`) disappears - * from the documentation by construction. + * Persistent agent context carries ownership, routing, and discovery guidance. + * The live WP-CLI help remains authoritative for the complete command map. * * @return string */ function datamachine_agents_md_render_datamachine_section(): string { $wp = datamachine_agents_md_wp_cli_cmd(); - $lines = array(); - $lines[] = '## Data Machine'; - $lines[] = ''; - $lines[] = 'Data Machine is your operating layer — memory, automation, and orchestration via WP-CLI.'; - $lines[] = ''; - $lines[] = "Discover the full command surface: `{$wp} datamachine --help`. Always run `--help` on any subcommand to see its options."; - $lines[] = ''; + return << $subcommands ) { - $pipe = implode( '|', $subcommands ); +Data Machine is the WordPress-side operating layer for memory, automation, content operations, communication, agent configuration, and system execution. - if ( '' !== $pipe ) { - $lines[] = "- `{$wp} {$command} {$pipe}`"; - } else { - $lines[] = "- `{$wp} {$command}`"; - } - } - - $lines[] = ''; - $lines[] = 'Use `--help` on any command to discover options and subcommands.'; - - return implode( "\n", $lines ); -} - -/** - * Build the ordered command => subcommand-names map for the section. - * - * Reflects each command class via the shared CliCommandIntrospector (preferred) - * and degrades to a local reflection fallback when the shared helper is - * unavailable or lacks the method. De-duplicates by class so singular/plural - * aliases that resolve to the same class document once, preserving the first - * registered spelling. - * - * @return array Command label => ordered subcommand names. - */ -function datamachine_agents_md_command_groups(): array { - $groups = array(); - $seen = array(); - - foreach ( CommandRegistry::map() as $command => $command_class ) { - if ( isset( $seen[ $command_class ] ) ) { - continue; - } - $seen[ $command_class ] = true; - - $groups[ $command ] = datamachine_agents_md_subcommand_names( $command_class ); - } - - return $groups; -} - -/** - * Resolve a command class's subcommand names, preferring the shared helper. - * - * @param class-string $command_class Command class. - * @return string[] Ordered subcommand names ('__default' omitted). - */ -function datamachine_agents_md_subcommand_names( string $command_class ): array { - $introspector = '\\DataMachine\\Engine\\AI\\CliCommandIntrospector'; - - if ( class_exists( $introspector ) && method_exists( $introspector, 'describe_class' ) ) { - $subcommands = call_user_func( array( $introspector, 'describe_class' ), $command_class ); - $names = array(); - foreach ( (array) $subcommands as $sub ) { - $name = is_array( $sub ) ? ( $sub['name'] ?? '' ) : ''; - if ( '' === $name || '__default' === $name ) { - continue; - } - $names[] = $name; - } - - // describe_class returns [] for classes whose only subcommand is an - // __invoke (flat invoke commands) on helper versions that predate - // __invoke support (#2639). Fall back locally so those still document. - if ( ! empty( $names ) ) { - return $names; - } - } +**Default routing** +- Memory and composed context: `{$wp} datamachine memory --help` +- Flows and pipelines: `{$wp} datamachine flows` and `{$wp} datamachine pipelines` +- Job and worker state: `{$wp} datamachine jobs --help` and `{$wp} datamachine worker --help` +- Content and media operations: `{$wp} datamachine posts --help`, `{$wp} datamachine blocks --help`, or `{$wp} datamachine image --help` +- Communication and approval queues: `{$wp} datamachine email --help` and `{$wp} datamachine pending-actions --help` +- Agent and system configuration: `{$wp} datamachine agent --help` and `{$wp} datamachine system --help` - return datamachine_agents_md_reflect_subcommand_names( $command_class ); -} - -/** - * Local reflection fallback for subcommand names. - * - * Context-safe: reflects over the class source without touching the live WP-CLI - * runner. Mirrors WP-CLI's subcommand resolution — public, non-static, - * own-declared methods; `@subcommand ` annotation wins over the method - * name; magic methods are skipped except `__invoke`, which maps to the namespace - * itself ('__default') and is therefore omitted from the documented list. - * - * @param class-string $command_class Command class. - * @return string[] Ordered subcommand names. - */ -function datamachine_agents_md_reflect_subcommand_names( string $command_class ): array { - if ( ! class_exists( $command_class ) ) { - return array(); - } - - try { - $reflection = new \ReflectionClass( $command_class ); - } catch ( \Throwable $e ) { - return array(); - } - - $names = array(); - - foreach ( $reflection->getMethods( \ReflectionMethod::IS_PUBLIC ) as $method ) { - if ( $method->isStatic() ) { - continue; - } - if ( $method->getDeclaringClass()->getName() !== $reflection->getName() ) { - continue; - } - - $method_name = $method->getName(); - $doc = $method->getDocComment(); - $doc = is_string( $doc ) ? $doc : ''; - - $annotated = ''; - if ( preg_match( '/@subcommand\s+(\S+)/', $doc, $m ) ) { - $annotated = $m[1]; - } - - if ( '__invoke' === $method_name && '' === $annotated ) { - // Flat invoke command — the namespace itself, no sub-word. - continue; - } - - if ( '' !== $annotated ) { - $names[] = $annotated; - continue; - } - - if ( 0 === strpos( $method_name, '__' ) ) { - continue; - } - - $names[] = str_replace( '_', '-', $method_name ); - } - - return $names; +**Discovery** +Use `{$wp} datamachine --help` for the live command map and `{$wp} datamachine --help` for the current options and subcommands. +MD; } /** diff --git a/tests/agents-md-gated-composition-smoke.php b/tests/agents-md-gated-composition-smoke.php index 0a4dc73a1..ca064e26b 100644 --- a/tests/agents-md-gated-composition-smoke.php +++ b/tests/agents-md-gated-composition-smoke.php @@ -5,11 +5,9 @@ * Verifies: * 1. The gate `datamachine_agents_md_enabled()` is constant-only and default-OFF. * 2. When OFF, the file/section registration helpers are no-ops. - * 3. The reflected `datamachine` section renders core commands with subcommand - * names sourced from reflection — and the relocated `analytics` command is - * absent from the CommandRegistry map by construction. - * 4. The local reflection fallback resolves @subcommand annotations and treats - * a flat __invoke command as the namespace itself (omitted from the list). + * 3. The `datamachine` section renders bounded routing and live-help discovery + * rather than an exhaustive command map. + * 4. The CommandRegistry remains authoritative and excludes relocated commands. * * Run with: php tests/agents-md-gated-composition-smoke.php * @@ -77,10 +75,28 @@ function datamachine_assert( bool $cond, string $message, array &$failures ): vo datamachine_register_agents_md_sections(); datamachine_assert( true, 'File + section registration helpers are no-ops while gate OFF', $failures ); -// --- 3. CommandRegistry map: analytics is gone, real commands present. ------ +// --- 3. Generated section is routing-oriented, not an exhaustive CLI map. ---- + +$rendered = datamachine_agents_md_render_datamachine_section(); +datamachine_assert( str_contains( $rendered, '## Data Machine' ), 'Section has a Data Machine heading', $failures ); +datamachine_assert( str_contains( $rendered, '**Default routing**' ), 'Section provides default routing', $failures ); +datamachine_assert( str_contains( $rendered, 'datamachine memory --help' ), 'Section routes memory work', $failures ); +datamachine_assert( str_contains( $rendered, 'datamachine jobs --help' ), 'Section routes job and evidence work', $failures ); +datamachine_assert( str_contains( $rendered, 'datamachine --help' ), 'Section points to live command discovery', $failures ); +datamachine_assert( ! str_contains( $rendered, 'artifact-content|artifacts|cleanup' ), 'Section omits exhaustive reflected subcommands', $failures ); + +// --- 4. CommandRegistry map: analytics is gone, real commands present. ------ $map = \DataMachine\Cli\CommandRegistry::map(); +foreach ( array( 'memory', 'flows', 'pipelines', 'jobs', 'worker', 'posts', 'blocks', 'image', 'email', 'pending-actions', 'agent', 'system' ) as $advertised_root ) { + datamachine_assert( + isset( $map[ 'datamachine ' . $advertised_root ] ), + "Advertised routing root `datamachine {$advertised_root}` is registered", + $failures + ); +} + datamachine_assert( ! array_key_exists( 'datamachine analytics', $map ), 'CommandRegistry map does NOT contain a `datamachine analytics` entry (relocated to DMB)', @@ -101,48 +117,6 @@ function datamachine_assert( bool $cond, string $message, array &$failures ): vo $failures ); -// --- 4. Local reflection fallback resolves subcommands correctly. ----------- - -// @subcommand-annotated command. -final class DM_Smoke_AnnotatedCommand { - /** - * Read a thing. - * - * @subcommand read - */ - public function read(): void {} - - /** - * Write a thing. - * - * @subcommand write - */ - public function write(): void {} -} - -$names = datamachine_agents_md_reflect_subcommand_names( DM_Smoke_AnnotatedCommand::class ); -sort( $names ); -datamachine_assert( - array( 'read', 'write' ) === $names, - 'Reflection fallback resolves @subcommand-annotated methods (read, write)', - $failures -); - -// Flat __invoke command — the namespace itself, omitted from the list. -final class DM_Smoke_InvokeCommand { - /** - * Run the thing. - */ - public function __invoke(): void {} -} - -$invoke_names = datamachine_agents_md_reflect_subcommand_names( DM_Smoke_InvokeCommand::class ); -datamachine_assert( - array() === $invoke_names, - 'Reflection fallback omits a flat __invoke command from the subcommand list', - $failures -); - // --- Report. ---------------------------------------------------------------- if ( empty( $failures ) ) {