Skip to content

Commit 2916da2

Browse files
authored
Merge pull request #2991 from Extra-Chill/feat/2990-routing-agents-md
Refine Data Machine agent routing guidance
2 parents 9fff571 + 5ad0a03 commit 2916da2

3 files changed

Lines changed: 52 additions & 211 deletions

File tree

inc/Cli/CommandRegistry.php

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@
33
* Command Registry
44
*
55
* Single source of truth mapping `wp datamachine ...` command strings to their
6-
* implementing command classes. Both the WP-CLI bootstrap (which calls
7-
* WP_CLI::add_command for each entry) and the gated AGENTS.md `datamachine`
8-
* section generator (which reflects over each class to enumerate real
9-
* subcommands) read from this map, so the documented CLI surface can never
10-
* drift from what is actually registered.
6+
* implementing command classes. The WP-CLI bootstrap calls
7+
* WP_CLI::add_command for each entry. Generated agent guidance advertises a
8+
* bounded set of routing entrypoints and tests those roots against this map;
9+
* live `--help` remains authoritative for the complete command surface.
1110
*
12-
* This mirrors the pattern proven in extrachill-cli's CommandRegistry — the map
13-
* is the registration source AND the documentation source, eliminating the
14-
* hand-typed heredoc that previously narrated core's command surface from a
15-
* downstream plugin (Extra-Chill/data-machine#2640, #2613).
11+
* This keeps command registration centralized while Data Machine itself owns
12+
* the concise routing guidance previously narrated by a downstream plugin
13+
* (Extra-Chill/data-machine#2640, #2613).
1614
*
1715
* @package DataMachine\Cli
1816
*/
@@ -28,12 +26,11 @@ class CommandRegistry {
2826
*
2927
* Keys are the exact strings passed to WP_CLI::add_command (the command
3028
* namespace, e.g. "datamachine memory" or "datamachine step-types").
31-
* Order here determines both registration order and documentation order.
29+
* Order here determines registration order.
3230
*
3331
* Singular/plural aliases that resolve to the same class are intentionally
3432
* included so `WP_CLI::add_command` registers every accepted spelling. The
35-
* AGENTS.md section generator de-duplicates by class when grouping, so the
36-
* documented surface lists each command once.
33+
* AGENTS.md introspection helpers de-duplicate by class when grouping.
3734
*
3835
* @return array<string, class-string>
3936
*/

inc/migrations/agents-md.php

Lines changed: 21 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
* no-noise behavior on installs with no coding agent.
1717
* - ON: core registers the AGENTS.md composable file (so `wp datamachine memory
1818
* compose AGENTS.md` writes it and auto-regeneration keeps it fresh) and the
19-
* core-owned sections, including the `datamachine` section whose command list
20-
* is REFLECTED from core's registered command classes (never hand-typed).
19+
* core-owned sections, including concise Data Machine routing and discovery
20+
* guidance. The live CLI help remains the complete command authority.
2121
*
2222
* Why composition lives in core: core already owns SectionRegistry,
2323
* MemoryFileRegistry, ComposableFileGenerator, ComposableFileInvalidation, and
@@ -32,7 +32,6 @@
3232

3333
use DataMachine\Engine\AI\MemoryFileRegistry;
3434
use DataMachine\Engine\AI\SectionRegistry;
35-
use DataMachine\Cli\CommandRegistry;
3635

3736
/**
3837
* Whether AGENTS.md composition is enabled for this install.
@@ -86,7 +85,7 @@ function datamachine_register_agents_md_file(): void {
8685
*
8786
* Sections registered here describe Data Machine itself. The external coding
8887
* runtime installer owns generic WordPress coding guidance, while Data Machine
89-
* Code owns its `datamachine-code` section and workspace inventory.
88+
* Code owns its workspace lifecycle section.
9089
*
9190
* @return void
9291
*/
@@ -119,16 +118,16 @@ function () use ( $wp ) {
119118
array_merge( $core_meta, array( 'freshness' => 'generated' ) )
120119
);
121120

122-
// The reflected `datamachine` command-surface section.
121+
// Data Machine routing and discovery guidance.
123122
SectionRegistry::register(
124123
'AGENTS.md',
125124
'datamachine',
126125
10,
127126
'datamachine_agents_md_render_datamachine_section',
128127
array(
129128
'owner' => 'data-machine',
130-
'freshness' => 'snapshot',
131-
'conditions' => 'Registered when DATAMACHINE_COMPOSE_AGENTS_MD is enabled; command list reflected from registered command classes.',
129+
'freshness' => 'static',
130+
'conditions' => 'Registered when DATAMACHINE_COMPOSE_AGENTS_MD is enabled.',
132131
)
133132
);
134133

@@ -157,159 +156,30 @@ function () use ( $wp ) {
157156
/**
158157
* Render the `## Data Machine` AGENTS.md section.
159158
*
160-
* The command list is reflected from CommandRegistry::map() — the same map that
161-
* feeds WP-CLI registration in Bootstrap.php — so adding or renaming a
162-
* `datamachine` subcommand surfaces here automatically with no heredoc edit, and
163-
* a command removed from core (e.g. the now-relocated `analytics`) disappears
164-
* from the documentation by construction.
159+
* Persistent agent context carries ownership, routing, and discovery guidance.
160+
* The live WP-CLI help remains authoritative for the complete command map.
165161
*
166162
* @return string
167163
*/
168164
function datamachine_agents_md_render_datamachine_section(): string {
169165
$wp = datamachine_agents_md_wp_cli_cmd();
170166

171-
$lines = array();
172-
$lines[] = '## Data Machine';
173-
$lines[] = '';
174-
$lines[] = 'Data Machine is your operating layer — memory, automation, and orchestration via WP-CLI.';
175-
$lines[] = '';
176-
$lines[] = "Discover the full command surface: `{$wp} datamachine --help`. Always run `--help` on any subcommand to see its options.";
177-
$lines[] = '';
167+
return <<<MD
168+
## Data Machine
178169
179-
foreach ( datamachine_agents_md_command_groups() as $command => $subcommands ) {
180-
$pipe = implode( '|', $subcommands );
170+
Data Machine is the WordPress-side operating layer for memory, automation, content operations, communication, agent configuration, and system execution.
181171
182-
if ( '' !== $pipe ) {
183-
$lines[] = "- `{$wp} {$command} {$pipe}`";
184-
} else {
185-
$lines[] = "- `{$wp} {$command}`";
186-
}
187-
}
188-
189-
$lines[] = '';
190-
$lines[] = 'Use `--help` on any command to discover options and subcommands.';
191-
192-
return implode( "\n", $lines );
193-
}
194-
195-
/**
196-
* Build the ordered command => subcommand-names map for the section.
197-
*
198-
* Reflects each command class via the shared CliCommandIntrospector (preferred)
199-
* and degrades to a local reflection fallback when the shared helper is
200-
* unavailable or lacks the method. De-duplicates by class so singular/plural
201-
* aliases that resolve to the same class document once, preserving the first
202-
* registered spelling.
203-
*
204-
* @return array<string, string[]> Command label => ordered subcommand names.
205-
*/
206-
function datamachine_agents_md_command_groups(): array {
207-
$groups = array();
208-
$seen = array();
209-
210-
foreach ( CommandRegistry::map() as $command => $command_class ) {
211-
if ( isset( $seen[ $command_class ] ) ) {
212-
continue;
213-
}
214-
$seen[ $command_class ] = true;
215-
216-
$groups[ $command ] = datamachine_agents_md_subcommand_names( $command_class );
217-
}
218-
219-
return $groups;
220-
}
221-
222-
/**
223-
* Resolve a command class's subcommand names, preferring the shared helper.
224-
*
225-
* @param class-string $command_class Command class.
226-
* @return string[] Ordered subcommand names ('__default' omitted).
227-
*/
228-
function datamachine_agents_md_subcommand_names( string $command_class ): array {
229-
$introspector = '\\DataMachine\\Engine\\AI\\CliCommandIntrospector';
230-
231-
if ( class_exists( $introspector ) && method_exists( $introspector, 'describe_class' ) ) {
232-
$subcommands = call_user_func( array( $introspector, 'describe_class' ), $command_class );
233-
$names = array();
234-
foreach ( (array) $subcommands as $sub ) {
235-
$name = is_array( $sub ) ? ( $sub['name'] ?? '' ) : '';
236-
if ( '' === $name || '__default' === $name ) {
237-
continue;
238-
}
239-
$names[] = $name;
240-
}
241-
242-
// describe_class returns [] for classes whose only subcommand is an
243-
// __invoke (flat invoke commands) on helper versions that predate
244-
// __invoke support (#2639). Fall back locally so those still document.
245-
if ( ! empty( $names ) ) {
246-
return $names;
247-
}
248-
}
172+
**Default routing**
173+
- Memory and composed context: `{$wp} datamachine memory --help`
174+
- Flows and pipelines: `{$wp} datamachine flows` and `{$wp} datamachine pipelines`
175+
- Job and worker state: `{$wp} datamachine jobs --help` and `{$wp} datamachine worker --help`
176+
- Content and media operations: `{$wp} datamachine posts --help`, `{$wp} datamachine blocks --help`, or `{$wp} datamachine image --help`
177+
- Communication and approval queues: `{$wp} datamachine email --help` and `{$wp} datamachine pending-actions --help`
178+
- Agent and system configuration: `{$wp} datamachine agent --help` and `{$wp} datamachine system --help`
249179
250-
return datamachine_agents_md_reflect_subcommand_names( $command_class );
251-
}
252-
253-
/**
254-
* Local reflection fallback for subcommand names.
255-
*
256-
* Context-safe: reflects over the class source without touching the live WP-CLI
257-
* runner. Mirrors WP-CLI's subcommand resolution — public, non-static,
258-
* own-declared methods; `@subcommand <name>` annotation wins over the method
259-
* name; magic methods are skipped except `__invoke`, which maps to the namespace
260-
* itself ('__default') and is therefore omitted from the documented list.
261-
*
262-
* @param class-string $command_class Command class.
263-
* @return string[] Ordered subcommand names.
264-
*/
265-
function datamachine_agents_md_reflect_subcommand_names( string $command_class ): array {
266-
if ( ! class_exists( $command_class ) ) {
267-
return array();
268-
}
269-
270-
try {
271-
$reflection = new \ReflectionClass( $command_class );
272-
} catch ( \Throwable $e ) {
273-
return array();
274-
}
275-
276-
$names = array();
277-
278-
foreach ( $reflection->getMethods( \ReflectionMethod::IS_PUBLIC ) as $method ) {
279-
if ( $method->isStatic() ) {
280-
continue;
281-
}
282-
if ( $method->getDeclaringClass()->getName() !== $reflection->getName() ) {
283-
continue;
284-
}
285-
286-
$method_name = $method->getName();
287-
$doc = $method->getDocComment();
288-
$doc = is_string( $doc ) ? $doc : '';
289-
290-
$annotated = '';
291-
if ( preg_match( '/@subcommand\s+(\S+)/', $doc, $m ) ) {
292-
$annotated = $m[1];
293-
}
294-
295-
if ( '__invoke' === $method_name && '' === $annotated ) {
296-
// Flat invoke command — the namespace itself, no sub-word.
297-
continue;
298-
}
299-
300-
if ( '' !== $annotated ) {
301-
$names[] = $annotated;
302-
continue;
303-
}
304-
305-
if ( 0 === strpos( $method_name, '__' ) ) {
306-
continue;
307-
}
308-
309-
$names[] = str_replace( '_', '-', $method_name );
310-
}
311-
312-
return $names;
180+
**Discovery**
181+
Use `{$wp} datamachine --help` for the live command map and `{$wp} datamachine <command> --help` for the current options and subcommands.
182+
MD;
313183
}
314184

315185
/**

tests/agents-md-gated-composition-smoke.php

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
* Verifies:
66
* 1. The gate `datamachine_agents_md_enabled()` is constant-only and default-OFF.
77
* 2. When OFF, the file/section registration helpers are no-ops.
8-
* 3. The reflected `datamachine` section renders core commands with subcommand
9-
* names sourced from reflection — and the relocated `analytics` command is
10-
* absent from the CommandRegistry map by construction.
11-
* 4. The local reflection fallback resolves @subcommand annotations and treats
12-
* a flat __invoke command as the namespace itself (omitted from the list).
8+
* 3. The `datamachine` section renders bounded routing and live-help discovery
9+
* rather than an exhaustive command map.
10+
* 4. The CommandRegistry remains authoritative and excludes relocated commands.
1311
*
1412
* Run with: php tests/agents-md-gated-composition-smoke.php
1513
*
@@ -77,10 +75,28 @@ function datamachine_assert( bool $cond, string $message, array &$failures ): vo
7775
datamachine_register_agents_md_sections();
7876
datamachine_assert( true, 'File + section registration helpers are no-ops while gate OFF', $failures );
7977

80-
// --- 3. CommandRegistry map: analytics is gone, real commands present. ------
78+
// --- 3. Generated section is routing-oriented, not an exhaustive CLI map. ----
79+
80+
$rendered = datamachine_agents_md_render_datamachine_section();
81+
datamachine_assert( str_contains( $rendered, '## Data Machine' ), 'Section has a Data Machine heading', $failures );
82+
datamachine_assert( str_contains( $rendered, '**Default routing**' ), 'Section provides default routing', $failures );
83+
datamachine_assert( str_contains( $rendered, 'datamachine memory --help' ), 'Section routes memory work', $failures );
84+
datamachine_assert( str_contains( $rendered, 'datamachine jobs --help' ), 'Section routes job and evidence work', $failures );
85+
datamachine_assert( str_contains( $rendered, 'datamachine --help' ), 'Section points to live command discovery', $failures );
86+
datamachine_assert( ! str_contains( $rendered, 'artifact-content|artifacts|cleanup' ), 'Section omits exhaustive reflected subcommands', $failures );
87+
88+
// --- 4. CommandRegistry map: analytics is gone, real commands present. ------
8189

8290
$map = \DataMachine\Cli\CommandRegistry::map();
8391

92+
foreach ( array( 'memory', 'flows', 'pipelines', 'jobs', 'worker', 'posts', 'blocks', 'image', 'email', 'pending-actions', 'agent', 'system' ) as $advertised_root ) {
93+
datamachine_assert(
94+
isset( $map[ 'datamachine ' . $advertised_root ] ),
95+
"Advertised routing root `datamachine {$advertised_root}` is registered",
96+
$failures
97+
);
98+
}
99+
84100
datamachine_assert(
85101
! array_key_exists( 'datamachine analytics', $map ),
86102
'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
101117
$failures
102118
);
103119

104-
// --- 4. Local reflection fallback resolves subcommands correctly. -----------
105-
106-
// @subcommand-annotated command.
107-
final class DM_Smoke_AnnotatedCommand {
108-
/**
109-
* Read a thing.
110-
*
111-
* @subcommand read
112-
*/
113-
public function read(): void {}
114-
115-
/**
116-
* Write a thing.
117-
*
118-
* @subcommand write
119-
*/
120-
public function write(): void {}
121-
}
122-
123-
$names = datamachine_agents_md_reflect_subcommand_names( DM_Smoke_AnnotatedCommand::class );
124-
sort( $names );
125-
datamachine_assert(
126-
array( 'read', 'write' ) === $names,
127-
'Reflection fallback resolves @subcommand-annotated methods (read, write)',
128-
$failures
129-
);
130-
131-
// Flat __invoke command — the namespace itself, omitted from the list.
132-
final class DM_Smoke_InvokeCommand {
133-
/**
134-
* Run the thing.
135-
*/
136-
public function __invoke(): void {}
137-
}
138-
139-
$invoke_names = datamachine_agents_md_reflect_subcommand_names( DM_Smoke_InvokeCommand::class );
140-
datamachine_assert(
141-
array() === $invoke_names,
142-
'Reflection fallback omits a flat __invoke command from the subcommand list',
143-
$failures
144-
);
145-
146120
// --- Report. ----------------------------------------------------------------
147121

148122
if ( empty( $failures ) ) {

0 commit comments

Comments
 (0)