Skip to content

Commit 396f1e8

Browse files
authored
Merge pull request #950 from Extra-Chill/feat/949-routing-agents-md
Refine workspace agent routing guidance
2 parents a993462 + 2cf1429 commit 396f1e8

5 files changed

Lines changed: 35 additions & 454 deletions

File tree

data-machine-code.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,4 +475,3 @@ function datamachine_code_load_chat_tools() {
475475
| in DataMachineCode\Runtime\AgentsMdSections.
476476
*/
477477
add_action('plugins_loaded', array( \DataMachineCode\Runtime\AgentsMdSections::class, 'register' ), 22);
478-
add_filter('datamachine_composable_invalidation_hooks', array( \DataMachineCode\Runtime\AgentsMdSections::class, 'register_invalidation_hooks' ));

inc/Runtime/AgentsMdSections.php

Lines changed: 18 additions & 299 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,9 @@ public static function register(): void {
4949

5050
self::register_auto_generated_marker($wp);
5151
self::register_datamachine_section($wp);
52-
self::register_workspace_inventory_section($wp);
5352
self::register_multisite_section($wp);
5453
}
5554

56-
/**
57-
* Register workspace change hooks that invalidate composable AGENTS.md.
58-
*/
59-
public static function register_invalidation_hooks( array $hooks ): array {
60-
if ( ! function_exists('datamachine_agents_md_enabled') || ! datamachine_agents_md_enabled() ) {
61-
return $hooks;
62-
}
63-
64-
$hooks[] = 'datamachine_code_workspace_changed';
65-
return $hooks;
66-
}
67-
6855
private static function register_auto_generated_marker( string $wp ): void {
6956
self::register_section(
7057
'AGENTS.md', 'auto-generated-marker', 0, function () use ( $wp ) {
@@ -86,27 +73,33 @@ private static function register_auto_generated_marker( string $wp ): void {
8673

8774
private static function register_datamachine_section( string $wp ): void {
8875
self::register_section(
89-
'AGENTS.md', 'datamachine-code', 10, function () use ( $wp ) {
76+
'AGENTS.md', 'datamachine-code', 20, function () use ( $wp ) {
9077
$workspace_path = self::resolve_workspace_path();
9178
$workspace_policy_intro = self::render_workspace_policy_intro($workspace_path);
9279
$workspace_policy_section = self::render_workspace_policy_section($workspace_path, $wp);
9380

9481
return <<<MD
9582
## Data Machine Code
9683
97-
{$workspace_policy_intro}DMC owns workspace lifecycle, evidence capture, and GitHub workflow glue; file CRUD inside a worktree uses whatever tool is fastest. Core's `datamachine` operating layer (memory, automation, communication, content ops, system) is documented in its own AGENTS.md section — run `{$wp} datamachine --help` to discover it.
84+
{$workspace_policy_intro}DMC owns authoritative repository, primary-checkout, worktree, and GitHub workspace state. Homeboy consumes DMC-managed worktrees for agent execution, deterministic gates, promotion, evidence, and publication.
9885
99-
- Workspace root: `{$workspace_path}`
100-
- **Workspace:** `{$wp} datamachine-code workspace --help` is the live lifecycle and file-I/O command reference. It enforces `<repo>@<slug>` handles and maintains workspace registry state.
101-
- **Worktrees:** `{$wp} datamachine-code workspace worktree --help` is the live branch lifecycle reference.
102-
- **GitHub:** `{$wp} datamachine-code github --help` is the live GitHub command reference.
103-
- **Editing inside a worktree:** any tool. Local agents on the same disk should use native file I/O and raw `git`; routing edits through workspace abilities is ceremony, not safety.
104-
- **Workspace lifecycle:** use `workspace clone` for primary checkout adoption/cloning and `workspace worktree add` for isolated branches. Use the CLI `--help` output for current flags and subcommands.
86+
**Default routing**
87+
- Inspect workspace state: `{$wp} datamachine-code workspace list`, `{$wp} datamachine-code workspace show <repo>`, or `{$wp} datamachine-code workspace hygiene`
88+
- Create isolated work: `{$wp} datamachine-code workspace worktree add <repo> <branch> --from=origin/<base>`
89+
- Record PR/final state: `{$wp} datamachine-code workspace worktree finalize <repo@slug> --pr=<url>`
90+
- Refresh an authoritative primary: `{$wp} datamachine-code workspace git pull <repo> --allow-primary-refresh`
91+
- Inspect or manage GitHub state: `{$wp} datamachine-code github --help`
92+
- Edit inside a local worktree with native file tools and raw `git`; routing local file I/O through workspace abilities is ceremony, not safety.
93+
94+
**Safety**
10595
{$workspace_policy_section}
96+
97+
**Discovery**
98+
Use `{$wp} datamachine-code workspace --help` and `{$wp} datamachine-code workspace worktree --help` for the live lifecycle contract. Query `workspace list`, `workspace show`, or `workspace hygiene` for current state instead of relying on an embedded inventory snapshot.
10699
MD;
107100
}, array(
108101
'label' => 'Data Machine Code',
109-
'description' => 'Workspace, worktree, and GitHub operations owned by Data Machine Code.',
102+
'description' => 'Authoritative workspace routing, lifecycle, and safety guidance.',
110103
'owner' => 'data-machine-code',
111104
'freshness' => 'snapshot',
112105
'conditions' => 'Always registered when Data Machine Code and composable memory section registration are available.',
@@ -115,7 +108,7 @@ private static function register_datamachine_section( string $wp ): void {
115108
}
116109

117110
private static function render_workspace_policy_intro( string $workspace_path ): string {
118-
$default = "All code changes happen in Data Machine Code worktrees under `{$workspace_path}`. ";
111+
$default = "All code changes happen in Data Machine Code worktrees. The controller workspace root is `{$workspace_path}`. ";
119112

120113
/**
121114
* Filters the site-owned workspace policy sentence rendered before DMC command facts.
@@ -133,10 +126,9 @@ private static function render_workspace_policy_intro( string $workspace_path ):
133126
}
134127

135128
private static function render_workspace_policy_section( string $workspace_path, string $wp ): string {
136-
$default = <<<'MD'
137-
- **Primary freshness:** before using a primary checkout for investigation or verification, inspect `workspace list|show|hygiene` freshness metadata. If the primary is stale, run `workspace git pull <repo> --allow-primary-refresh` or create the worktree from an explicit remote ref with `worktree add <repo> <branch> --from=origin/<base>`. Stale primary reads require an explicit `--allow-stale-primary` opt-in. Do not clone a second top-level primary for the same remote just to get fresh code.
129+
$default = <<<MD
130+
- **Primary freshness:** before using a primary checkout for investigation or verification, inspect `{$wp} datamachine-code workspace show <repo>` or `{$wp} datamachine-code workspace hygiene`. If the primary is stale, run `{$wp} datamachine-code workspace git pull <repo> --allow-primary-refresh` or create a worktree from `origin/<base>`. Stale primary reads require an explicit `--allow-stale-primary` opt-in. Do not clone a second top-level primary for the same remote just to get fresh code.
138131
- **Primary is read-only.** Never edit `<workspace>/<repo>` (no `@slug`). Safe primary refresh uses `--allow-primary-refresh`; primary commit, push, reset, and rebase require the stronger `--allow-dangerous-primary-mutation` approval. The primary tracks the deployed branch — operate on a worktree.
139-
- **Rule:** Never modify files under `wp-content/plugins/` or `wp-content/themes/` directly. Those paths are **read-only reference**. All code changes go through the workspace so they are tracked in git and reviewed via pull requests.
140132
MD;
141133

142134
/**
@@ -159,20 +151,6 @@ private static function render_workspace_policy_section( string $workspace_path,
159151
return trim($filtered);
160152
}
161153

162-
private static function register_workspace_inventory_section( string $wp ): void {
163-
self::register_section(
164-
'AGENTS.md', 'workspace-inventory', 15, function () use ( $wp ) {
165-
return self::render_workspace_inventory_section($wp);
166-
}, array(
167-
'label' => 'Workspace Inventory',
168-
'description' => 'Live snapshot of cloned repos + active worktrees in the workspace.',
169-
'owner' => 'data-machine-code',
170-
'freshness' => 'snapshot',
171-
'conditions' => 'Registered when Data Machine Code is active; content is omitted when no workspace inventory is available.',
172-
)
173-
);
174-
}
175-
176154
private static function register_multisite_section( string $wp ): void {
177155
if ( ! is_multisite() ) {
178156
return;
@@ -238,263 +216,4 @@ private static function resolve_workspace_path(): string {
238216

239217
return 'unavailable; run datamachine-code workspace path to diagnose';
240218
}
241-
242-
private static function resolve_agent_slug(): string {
243-
if ( class_exists('\DataMachine\Core\FilesRepository\DirectoryManager') ) {
244-
try {
245-
$directory_manager = new \DataMachine\Core\FilesRepository\DirectoryManager();
246-
$user_id = (int) $directory_manager->get_effective_user_id(0);
247-
$agent_slug = (string) $directory_manager->resolve_agent_slug(array( 'user_id' => $user_id ));
248-
249-
if ( '' !== trim($agent_slug) ) {
250-
return trim($agent_slug);
251-
}
252-
} catch ( \Throwable $e ) {
253-
return '';
254-
}
255-
}
256-
257-
return '';
258-
}
259-
260-
private static function render_workspace_inventory_section( string $wp ): string {
261-
if ( ! class_exists(Workspace::class) ) {
262-
return '';
263-
}
264-
265-
$workspace = new Workspace();
266-
$listing = $workspace->list_repos();
267-
268-
if ( is_wp_error($listing) || empty($listing['repos']) ) {
269-
return '';
270-
}
271-
272-
$by_repo = array();
273-
foreach ( $listing['repos'] as $entry ) {
274-
if ( empty($entry['git']) ) {
275-
continue;
276-
}
277-
278-
$repo = $entry['repo'] ?? $entry['name'] ?? '';
279-
if ( '' === $repo ) {
280-
continue;
281-
}
282-
283-
if ( ! isset($by_repo[ $repo ]) ) {
284-
$by_repo[ $repo ] = array(
285-
'primary' => null,
286-
'worktrees' => array(),
287-
);
288-
}
289-
290-
if ( ! empty($entry['is_worktree']) ) {
291-
$by_repo[ $repo ]['worktrees'][] = $entry;
292-
} else {
293-
$by_repo[ $repo ]['primary'] = $entry;
294-
}
295-
}
296-
297-
if ( empty($by_repo) ) {
298-
return '';
299-
}
300-
301-
ksort($by_repo, SORT_NATURAL | SORT_FLAG_CASE);
302-
303-
// Full snapshots remain available to sites that explicitly need them.
304-
$mode = apply_filters('datamachine_code_workspace_inventory_mode', 'summary');
305-
if ( ! in_array($mode, array( 'summary', 'full' ), true) ) {
306-
$mode = 'summary';
307-
}
308-
309-
$lines = array();
310-
$attention_lines = array();
311-
$worktree_count = 0;
312-
foreach ( $by_repo as $repo => $bucket ) {
313-
$primary = is_array($bucket['primary']) ? $bucket['primary'] : array();
314-
$worktrees = $bucket['worktrees'];
315-
$wt_count = count($worktrees);
316-
$worktree_count += $wt_count;
317-
$branch = $primary['branch'] ?? null;
318-
$remote = $primary['remote'] ?? null;
319-
$branch_str = ( null !== $branch && '' !== $branch ) ? sprintf(' (`%s`)', $branch) : '';
320-
$freshness = is_array($primary['primary_freshness'] ?? null) ? $primary['primary_freshness'] : null;
321-
322-
$attention = self::format_primary_freshness_attention($repo, $freshness);
323-
if ( '' !== $attention ) {
324-
$attention_lines[] = $attention;
325-
}
326-
327-
if ( 'summary' === $mode ) {
328-
continue;
329-
}
330-
331-
$header = sprintf('- **%s**%s', $repo, $branch_str);
332-
if ( null !== $remote && '' !== $remote ) {
333-
$header .= '' . $remote;
334-
}
335-
$freshness_badge = self::format_primary_freshness_badge($freshness);
336-
if ( '' !== $freshness_badge ) {
337-
$header .= ' · ' . $freshness_badge;
338-
}
339-
$lines[] = $header;
340-
341-
usort(
342-
$worktrees, function ( $a, $b ) {
343-
return strnatcasecmp( (string) ( $a['name'] ?? '' ), (string) ( $b['name'] ?? '' ) );
344-
}
345-
);
346-
foreach ( $worktrees as $wt ) {
347-
$slug = $wt['branch_slug'] ?? '';
348-
$wt_branch = $wt['branch'] ?? null;
349-
$wt_label = '' !== $slug ? sprintf('`%s`', $slug) : sprintf('`%s`', $wt['name'] ?? '?');
350-
if ( null !== $wt_branch && '' !== $wt_branch && $wt_branch !== $slug ) {
351-
$wt_label .= sprintf(' (`%s`)', $wt_branch);
352-
}
353-
$lines[] = ' - ' . $wt_label;
354-
}
355-
}
356-
357-
$body = implode("\n", $lines);
358-
$attention_block = 'full' === $mode
359-
? self::render_primary_freshness_attention_block($attention_lines)
360-
: self::render_primary_freshness_summary($attention_lines, $wp);
361-
$workspace_path = $listing['path'];
362-
$agent_slug = self::resolve_agent_slug();
363-
$agent_suffix = '' !== $agent_slug ? ' --agent=' . $agent_slug : '';
364-
365-
if ( 'summary' === $mode ) {
366-
$primary_count = count($by_repo);
367-
$primary_label = 1 === $primary_count ? 'checkout' : 'checkouts';
368-
$worktree_label = 1 === $worktree_count ? 'worktree' : 'worktrees';
369-
return <<<MD
370-
## Workspace Inventory
371-
372-
Live workspace state is intentionally not embedded here. Run `{$wp} datamachine-code workspace list` for the authoritative inventory, `{$wp} datamachine-code workspace show <repo>` for one checkout, or `{$wp} datamachine-code workspace hygiene` for freshness and cleanup diagnostics.
373-
374-
Snapshot summary: {$primary_count} primary {$primary_label} and {$worktree_count} {$worktree_label} under `{$workspace_path}`.
375-
376-
{$attention_block}
377-
MD;
378-
}
379-
380-
return <<<MD
381-
## Workspace Inventory
382-
383-
Detailed snapshot from cloned repos in `{$workspace_path}`. `{$wp} datamachine-code workspace list` remains the source of truth for current worktrees and branches.
384-
385-
Refresh this file with `{$wp} datamachine memory compose AGENTS.md{$agent_suffix}` after workspace changes.
386-
387-
{$attention_block}
388-
389-
{$body}
390-
MD;
391-
}
392-
393-
private static function render_primary_freshness_summary( array $attention_lines, string $wp ): string {
394-
$count = count($attention_lines);
395-
if ( 0 === $count ) {
396-
return '';
397-
}
398-
399-
return sprintf(
400-
'Primary checkout attention: %d %s need%s inspection. Run `%s datamachine-code workspace list` for details.',
401-
$count,
402-
1 === $count ? 'checkout' : 'checkouts',
403-
1 === $count ? 's' : '',
404-
$wp
405-
);
406-
}
407-
408-
private static function primary_freshness_needs_attention( ?array $freshness ): bool {
409-
if ( null === $freshness ) {
410-
return false;
411-
}
412-
413-
$status = (string) ( $freshness['status'] ?? '' );
414-
return in_array($status, array( 'stale', 'diverged', 'detached', 'unknown', 'no_upstream', 'ahead' ), true);
415-
}
416-
417-
private static function primary_freshness_needs_refresh( ?array $freshness ): bool {
418-
if ( null === $freshness ) {
419-
return false;
420-
}
421-
422-
$status = (string) ( $freshness['status'] ?? '' );
423-
return in_array($status, array( 'stale', 'diverged' ), true);
424-
}
425-
426-
private static function format_primary_freshness_badge( ?array $freshness ): string {
427-
if ( ! self::primary_freshness_needs_attention($freshness) ) {
428-
return '';
429-
}
430-
431-
$status = (string) ( $freshness['status'] ?? 'unknown' );
432-
$parts = array( 'primary ' . $status );
433-
if ( isset($freshness['behind']) && is_numeric($freshness['behind']) && (int) $freshness['behind'] > 0 ) {
434-
$parts[] = sprintf('behind %d', (int) $freshness['behind']);
435-
}
436-
if ( isset($freshness['ahead']) && is_numeric($freshness['ahead']) && (int) $freshness['ahead'] > 0 ) {
437-
$parts[] = sprintf('ahead %d', (int) $freshness['ahead']);
438-
}
439-
440-
return implode(', ', $parts);
441-
}
442-
443-
private static function format_primary_freshness_attention( string $repo, ?array $freshness ): string {
444-
if ( ! self::primary_freshness_needs_attention($freshness) ) {
445-
return '';
446-
}
447-
448-
$status = (string) ( $freshness['status'] ?? 'unknown' );
449-
$branch = (string) ( $freshness['branch'] ?? '' );
450-
$upstream = (string) ( $freshness['upstream'] ?? '' );
451-
$details = array();
452-
if ( '' !== $branch ) {
453-
$details[] = sprintf('branch `%s`', $branch);
454-
}
455-
if ( '' !== $upstream ) {
456-
$details[] = sprintf('upstream `%s`', $upstream);
457-
}
458-
if ( isset($freshness['behind']) && is_numeric($freshness['behind']) ) {
459-
$details[] = sprintf('behind %d', (int) $freshness['behind']);
460-
}
461-
if ( isset($freshness['ahead']) && is_numeric($freshness['ahead']) ) {
462-
$details[] = sprintf('ahead %d', (int) $freshness['ahead']);
463-
}
464-
465-
$line = sprintf('- **%s** primary is `%s`', $repo, $status);
466-
if ( ! empty($details) ) {
467-
$line .= ' (' . implode(', ', $details) . ')';
468-
}
469-
470-
$command = (string) ( $freshness['suggested_command'] ?? '' );
471-
if ( '' !== $command && self::primary_freshness_needs_refresh($freshness) ) {
472-
$line .= sprintf('. Refresh: `%s`', $command);
473-
}
474-
475-
return $line . '.';
476-
}
477-
478-
private static function render_primary_freshness_attention_block( array $attention_lines ): string {
479-
if ( empty($attention_lines) ) {
480-
return '';
481-
}
482-
483-
$max_lines = 20;
484-
$shown = array_slice($attention_lines, 0, $max_lines);
485-
$omitted = count($attention_lines) - count($shown);
486-
if ( $omitted > 0 ) {
487-
$shown[] = sprintf('- %d more primary checkout(s) need attention; run `wp datamachine-code workspace list` for the full set.', $omitted);
488-
}
489-
490-
$body = implode("\n", $shown);
491-
492-
return <<<MD
493-
**Primary Checkout Attention**
494-
495-
These primary checkouts may be stale or unsafe to read. Refresh them or create a worktree from an explicit remote ref before using them as source evidence.
496-
497-
{$body}
498-
MD;
499-
}
500219
}

inc/Workspace/WorkspaceCoreUtilities.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,9 +1149,8 @@ private function protect_directory( string $path ): void {
11491149
/**
11501150
* Fire the shared workspace-lifecycle action after a successful mutation.
11511151
*
1152-
* Listeners use this signal to refresh derived state (e.g. invalidating
1153-
* the composable AGENTS.md so its workspace-inventory section reflects the
1154-
* change). Only emitted on success, AFTER the on-disk change is durable.
1152+
* Listeners use this signal to refresh optional derived workspace state.
1153+
* Only emitted on success, AFTER the on-disk change is durable.
11551154
* The payload mirrors the workspace `name`/`repo` taxonomy used elsewhere
11561155
* in this class:
11571156
*

0 commit comments

Comments
 (0)