diff --git a/data-machine-code.php b/data-machine-code.php index 454a406..286bb6b 100644 --- a/data-machine-code.php +++ b/data-machine-code.php @@ -475,4 +475,3 @@ function datamachine_code_load_chat_tools() { | in DataMachineCode\Runtime\AgentsMdSections. */ add_action('plugins_loaded', array( \DataMachineCode\Runtime\AgentsMdSections::class, 'register' ), 22); -add_filter('datamachine_composable_invalidation_hooks', array( \DataMachineCode\Runtime\AgentsMdSections::class, 'register_invalidation_hooks' )); diff --git a/inc/Runtime/AgentsMdSections.php b/inc/Runtime/AgentsMdSections.php index 4df3ca6..bf64294 100644 --- a/inc/Runtime/AgentsMdSections.php +++ b/inc/Runtime/AgentsMdSections.php @@ -49,22 +49,9 @@ public static function register(): void { self::register_auto_generated_marker($wp); self::register_datamachine_section($wp); - self::register_workspace_inventory_section($wp); self::register_multisite_section($wp); } - /** - * Register workspace change hooks that invalidate composable AGENTS.md. - */ - public static function register_invalidation_hooks( array $hooks ): array { - if ( ! function_exists('datamachine_agents_md_enabled') || ! datamachine_agents_md_enabled() ) { - return $hooks; - } - - $hooks[] = 'datamachine_code_workspace_changed'; - return $hooks; - } - private static function register_auto_generated_marker( string $wp ): void { self::register_section( 'AGENTS.md', 'auto-generated-marker', 0, function () use ( $wp ) { @@ -86,7 +73,7 @@ private static function register_auto_generated_marker( string $wp ): void { private static function register_datamachine_section( string $wp ): void { self::register_section( - 'AGENTS.md', 'datamachine-code', 10, function () use ( $wp ) { + 'AGENTS.md', 'datamachine-code', 20, function () use ( $wp ) { $workspace_path = self::resolve_workspace_path(); $workspace_policy_intro = self::render_workspace_policy_intro($workspace_path); $workspace_policy_section = self::render_workspace_policy_section($workspace_path, $wp); @@ -94,19 +81,25 @@ private static function register_datamachine_section( string $wp ): void { return <<@` handles and maintains workspace registry state. -- **Worktrees:** `{$wp} datamachine-code workspace worktree --help` is the live branch lifecycle reference. -- **GitHub:** `{$wp} datamachine-code github --help` is the live GitHub command reference. -- **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. -- **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. +**Default routing** +- Inspect workspace state: `{$wp} datamachine-code workspace list`, `{$wp} datamachine-code workspace show `, or `{$wp} datamachine-code workspace hygiene` +- Create isolated work: `{$wp} datamachine-code workspace worktree add --from=origin/` +- Record PR/final state: `{$wp} datamachine-code workspace worktree finalize --pr=` +- Refresh an authoritative primary: `{$wp} datamachine-code workspace git pull --allow-primary-refresh` +- Inspect or manage GitHub state: `{$wp} datamachine-code github --help` +- Edit inside a local worktree with native file tools and raw `git`; routing local file I/O through workspace abilities is ceremony, not safety. + +**Safety** {$workspace_policy_section} + +**Discovery** +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. MD; }, array( 'label' => 'Data Machine Code', - 'description' => 'Workspace, worktree, and GitHub operations owned by Data Machine Code.', + 'description' => 'Authoritative workspace routing, lifecycle, and safety guidance.', 'owner' => 'data-machine-code', 'freshness' => 'snapshot', '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 { } private static function render_workspace_policy_intro( string $workspace_path ): string { - $default = "All code changes happen in Data Machine Code worktrees under `{$workspace_path}`. "; + $default = "All code changes happen in Data Machine Code worktrees. The controller workspace root is `{$workspace_path}`. "; /** * 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 ): } private static function render_workspace_policy_section( string $workspace_path, string $wp ): string { - $default = <<<'MD' -- **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 --allow-primary-refresh` or create the worktree from an explicit remote ref with `worktree add --from=origin/`. 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. + $default = <<` or `{$wp} datamachine-code workspace hygiene`. If the primary is stale, run `{$wp} datamachine-code workspace git pull --allow-primary-refresh` or create a worktree from `origin/`. 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. - **Primary is read-only.** Never edit `/` (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. -- **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. MD; /** @@ -159,20 +151,6 @@ private static function render_workspace_policy_section( string $workspace_path, return trim($filtered); } - private static function register_workspace_inventory_section( string $wp ): void { - self::register_section( - 'AGENTS.md', 'workspace-inventory', 15, function () use ( $wp ) { - return self::render_workspace_inventory_section($wp); - }, array( - 'label' => 'Workspace Inventory', - 'description' => 'Live snapshot of cloned repos + active worktrees in the workspace.', - 'owner' => 'data-machine-code', - 'freshness' => 'snapshot', - 'conditions' => 'Registered when Data Machine Code is active; content is omitted when no workspace inventory is available.', - ) - ); - } - private static function register_multisite_section( string $wp ): void { if ( ! is_multisite() ) { return; @@ -238,263 +216,4 @@ private static function resolve_workspace_path(): string { return 'unavailable; run datamachine-code workspace path to diagnose'; } - - private static function resolve_agent_slug(): string { - if ( class_exists('\DataMachine\Core\FilesRepository\DirectoryManager') ) { - try { - $directory_manager = new \DataMachine\Core\FilesRepository\DirectoryManager(); - $user_id = (int) $directory_manager->get_effective_user_id(0); - $agent_slug = (string) $directory_manager->resolve_agent_slug(array( 'user_id' => $user_id )); - - if ( '' !== trim($agent_slug) ) { - return trim($agent_slug); - } - } catch ( \Throwable $e ) { - return ''; - } - } - - return ''; - } - - private static function render_workspace_inventory_section( string $wp ): string { - if ( ! class_exists(Workspace::class) ) { - return ''; - } - - $workspace = new Workspace(); - $listing = $workspace->list_repos(); - - if ( is_wp_error($listing) || empty($listing['repos']) ) { - return ''; - } - - $by_repo = array(); - foreach ( $listing['repos'] as $entry ) { - if ( empty($entry['git']) ) { - continue; - } - - $repo = $entry['repo'] ?? $entry['name'] ?? ''; - if ( '' === $repo ) { - continue; - } - - if ( ! isset($by_repo[ $repo ]) ) { - $by_repo[ $repo ] = array( - 'primary' => null, - 'worktrees' => array(), - ); - } - - if ( ! empty($entry['is_worktree']) ) { - $by_repo[ $repo ]['worktrees'][] = $entry; - } else { - $by_repo[ $repo ]['primary'] = $entry; - } - } - - if ( empty($by_repo) ) { - return ''; - } - - ksort($by_repo, SORT_NATURAL | SORT_FLAG_CASE); - - // Full snapshots remain available to sites that explicitly need them. - $mode = apply_filters('datamachine_code_workspace_inventory_mode', 'summary'); - if ( ! in_array($mode, array( 'summary', 'full' ), true) ) { - $mode = 'summary'; - } - - $lines = array(); - $attention_lines = array(); - $worktree_count = 0; - foreach ( $by_repo as $repo => $bucket ) { - $primary = is_array($bucket['primary']) ? $bucket['primary'] : array(); - $worktrees = $bucket['worktrees']; - $wt_count = count($worktrees); - $worktree_count += $wt_count; - $branch = $primary['branch'] ?? null; - $remote = $primary['remote'] ?? null; - $branch_str = ( null !== $branch && '' !== $branch ) ? sprintf(' (`%s`)', $branch) : ''; - $freshness = is_array($primary['primary_freshness'] ?? null) ? $primary['primary_freshness'] : null; - - $attention = self::format_primary_freshness_attention($repo, $freshness); - if ( '' !== $attention ) { - $attention_lines[] = $attention; - } - - if ( 'summary' === $mode ) { - continue; - } - - $header = sprintf('- **%s**%s', $repo, $branch_str); - if ( null !== $remote && '' !== $remote ) { - $header .= ' — ' . $remote; - } - $freshness_badge = self::format_primary_freshness_badge($freshness); - if ( '' !== $freshness_badge ) { - $header .= ' · ' . $freshness_badge; - } - $lines[] = $header; - - usort( - $worktrees, function ( $a, $b ) { - return strnatcasecmp( (string) ( $a['name'] ?? '' ), (string) ( $b['name'] ?? '' ) ); - } - ); - foreach ( $worktrees as $wt ) { - $slug = $wt['branch_slug'] ?? ''; - $wt_branch = $wt['branch'] ?? null; - $wt_label = '' !== $slug ? sprintf('`%s`', $slug) : sprintf('`%s`', $wt['name'] ?? '?'); - if ( null !== $wt_branch && '' !== $wt_branch && $wt_branch !== $slug ) { - $wt_label .= sprintf(' (`%s`)', $wt_branch); - } - $lines[] = ' - ' . $wt_label; - } - } - - $body = implode("\n", $lines); - $attention_block = 'full' === $mode - ? self::render_primary_freshness_attention_block($attention_lines) - : self::render_primary_freshness_summary($attention_lines, $wp); - $workspace_path = $listing['path']; - $agent_slug = self::resolve_agent_slug(); - $agent_suffix = '' !== $agent_slug ? ' --agent=' . $agent_slug : ''; - - if ( 'summary' === $mode ) { - $primary_count = count($by_repo); - $primary_label = 1 === $primary_count ? 'checkout' : 'checkouts'; - $worktree_label = 1 === $worktree_count ? 'worktree' : 'worktrees'; - return <<` for one checkout, or `{$wp} datamachine-code workspace hygiene` for freshness and cleanup diagnostics. - -Snapshot summary: {$primary_count} primary {$primary_label} and {$worktree_count} {$worktree_label} under `{$workspace_path}`. - -{$attention_block} -MD; - } - - return << 0 ) { - $parts[] = sprintf('behind %d', (int) $freshness['behind']); - } - if ( isset($freshness['ahead']) && is_numeric($freshness['ahead']) && (int) $freshness['ahead'] > 0 ) { - $parts[] = sprintf('ahead %d', (int) $freshness['ahead']); - } - - return implode(', ', $parts); - } - - private static function format_primary_freshness_attention( string $repo, ?array $freshness ): string { - if ( ! self::primary_freshness_needs_attention($freshness) ) { - return ''; - } - - $status = (string) ( $freshness['status'] ?? 'unknown' ); - $branch = (string) ( $freshness['branch'] ?? '' ); - $upstream = (string) ( $freshness['upstream'] ?? '' ); - $details = array(); - if ( '' !== $branch ) { - $details[] = sprintf('branch `%s`', $branch); - } - if ( '' !== $upstream ) { - $details[] = sprintf('upstream `%s`', $upstream); - } - if ( isset($freshness['behind']) && is_numeric($freshness['behind']) ) { - $details[] = sprintf('behind %d', (int) $freshness['behind']); - } - if ( isset($freshness['ahead']) && is_numeric($freshness['ahead']) ) { - $details[] = sprintf('ahead %d', (int) $freshness['ahead']); - } - - $line = sprintf('- **%s** primary is `%s`', $repo, $status); - if ( ! empty($details) ) { - $line .= ' (' . implode(', ', $details) . ')'; - } - - $command = (string) ( $freshness['suggested_command'] ?? '' ); - if ( '' !== $command && self::primary_freshness_needs_refresh($freshness) ) { - $line .= sprintf('. Refresh: `%s`', $command); - } - - return $line . '.'; - } - - private static function render_primary_freshness_attention_block( array $attention_lines ): string { - if ( empty($attention_lines) ) { - return ''; - } - - $max_lines = 20; - $shown = array_slice($attention_lines, 0, $max_lines); - $omitted = count($attention_lines) - count($shown); - if ( $omitted > 0 ) { - $shown[] = sprintf('- %d more primary checkout(s) need attention; run `wp datamachine-code workspace list` for the full set.', $omitted); - } - - $body = implode("\n", $shown); - - return <</` (no `@slug`).', $default, 'default workspace policy section missing' ); - assert_contains('- **Workspace:** `wp datamachine-code workspace --help` is the live lifecycle and file-I/O command reference.', $default, 'workspace command discovery pointer missing'); - assert_contains('- **Worktrees:** `wp datamachine-code workspace worktree --help` is the live branch lifecycle reference.', $default, 'worktree command discovery pointer missing'); - assert_contains('- **GitHub:** `wp datamachine-code github --help` is the live GitHub command reference.', $default, 'GitHub command discovery pointer missing'); + assert_contains('**Default routing**', $default, 'DMC default routing missing'); + assert_contains('workspace worktree add --from=origin/', $default, 'worktree creation route missing'); + assert_contains('workspace worktree finalize --pr=', $default, 'worktree finalization route missing'); + assert_contains('**Discovery**', $default, 'DMC discovery guidance missing'); assert_not_contains('adopt|clone|list|show|path|hygiene', $default, 'enumerated workspace commands returned'); + assert_not_contains('wp-content/plugins/', $default, 'DMC duplicated WordPress source guidance'); + assert_not_contains('Snapshot summary:', $default, 'DMC embedded workspace inventory state'); add_test_filter( 'datamachine_code_workspace_policy_intro', @@ -109,10 +116,10 @@ static function (): string { ); $filtered = $render(); - assert_contains('Use local project policy for `unavailable; run datamachine-code workspace path to diagnose`. DMC owns workspace lifecycle', $filtered, 'workspace policy intro filter was not applied'); + assert_contains('Use local project policy for `unavailable; run datamachine-code workspace path to diagnose`. DMC owns authoritative repository', $filtered, 'workspace policy intro filter was not applied'); assert_contains('- **Local policy:** caller-owned workspace rules.', $filtered, 'workspace policy section filter was not applied'); assert_not_contains('- **Primary is read-only.** Never edit `/` (no `@slug`).', $filtered, 'default policy section remained after filter override'); - assert_contains('- **Workspace:** `wp datamachine-code workspace --help` is the live lifecycle and file-I/O command reference.', $filtered, 'DMC command discovery pointer changed after policy filter'); + assert_contains('**Default routing**', $filtered, 'DMC routing changed after policy filter'); fwrite(STDOUT, "agents-md sections smoke passed\n"); } diff --git a/tests/smoke-agents-md-workspace-freshness.php b/tests/smoke-agents-md-workspace-freshness.php deleted file mode 100644 index db975f6..0000000 --- a/tests/smoke-agents-md-workspace-freshness.php +++ /dev/null @@ -1,143 +0,0 @@ - true, - 'path' => '/tmp/dmc-workspace', - 'repos' => array( - array( - 'name' => 'current-repo', - 'repo' => 'current-repo', - 'git' => true, - 'is_worktree' => false, - 'branch' => 'main', - 'remote' => 'https://example.com/current-repo.git', - 'primary_freshness' => array( - 'status' => 'current', - 'branch' => 'main', - 'upstream' => 'origin/main', - 'behind' => 0, - 'ahead' => 0, - ), - ), - array( - 'name' => 'stale-repo', - 'repo' => 'stale-repo', - 'git' => true, - 'is_worktree' => false, - 'branch' => 'trunk', - 'remote' => 'https://example.com/stale-repo.git', - 'primary_freshness' => array( - 'status' => 'stale', - 'branch' => 'trunk', - 'upstream' => 'origin/trunk', - 'behind' => 7, - 'ahead' => 0, - 'suggested_command' => 'wp datamachine-code workspace git pull stale-repo --allow-primary-refresh', - ), - ), - array( - 'name' => 'stale-repo@fix-example', - 'repo' => 'stale-repo', - 'git' => true, - 'is_worktree' => true, - 'branch_slug' => 'fix-example', - 'branch' => 'fix/example', - ), - ), - ); - } - } -} - -namespace { - if ( ! defined('ABSPATH') ) { - define('ABSPATH', '/var/www/html'); - } - - function datamachine_agents_md_enabled(): bool { - return true; - } - - function is_multisite(): bool { - return false; - } - - function apply_filters( string $hook_name, mixed $value, mixed ...$args ): mixed { - if ( 'datamachine_code_workspace_inventory_mode' === $hook_name && isset($GLOBALS['datamachine_code_test_inventory_mode']) ) { - return $GLOBALS['datamachine_code_test_inventory_mode']; - } - - return $value; - } - - function is_wp_error( mixed $thing ): bool { - return false; - } - - function assert_contains( string $needle, string $haystack, string $message ): void { - if ( ! str_contains($haystack, $needle) ) { - throw new RuntimeException($message); - } - } - - function assert_not_contains( string $needle, string $haystack, string $message ): void { - if ( str_contains($haystack, $needle) ) { - throw new RuntimeException($message); - } - } - - require_once dirname(__DIR__) . '/inc/Runtime/AgentsMdSections.php'; - - \DataMachineCode\Runtime\AgentsMdSections::register(); - - $sections = \DataMachine\Engine\AI\SectionRegistry::$sections; - if ( ! isset($sections['workspace-inventory']) ) { - throw new RuntimeException('workspace-inventory section was not registered'); - } - - $render = $sections['workspace-inventory']['callback']; - $rendered = $render(); - - assert_contains('Live workspace state is intentionally not embedded here.', $rendered, 'live inventory pointer missing'); - assert_contains('workspace list` for the authoritative inventory', $rendered, 'workspace list pointer missing'); - assert_contains('Snapshot summary: 2 primary checkouts and 1 worktree under `/tmp/dmc-workspace`.', $rendered, 'deterministic workspace summary changed'); - assert_contains('Primary checkout attention: 1 checkout needs inspection.', $rendered, 'bounded primary attention summary missing'); - assert_not_contains('https://example.com/stale-repo.git', $rendered, 'default inventory embedded repository detail'); - if ( strlen($rendered) > 700 ) { - throw new RuntimeException('default workspace inventory exceeded its compact size budget'); - } - - $GLOBALS['datamachine_code_test_inventory_mode'] = 'full'; - $full = $render(); - assert_contains('Detailed snapshot from cloned repos in `/tmp/dmc-workspace`.', $full, 'detailed snapshot heading missing'); - assert_contains('**Primary Checkout Attention**', $full, 'detailed primary freshness attention block missing'); - assert_contains('- **stale-repo** primary is `stale` (branch `trunk`, upstream `origin/trunk`, behind 7, ahead 0). Refresh: `wp datamachine-code workspace git pull stale-repo --allow-primary-refresh`.', $full, 'detailed stale primary facts missing'); - assert_contains('- **stale-repo** (`trunk`) — https://example.com/stale-repo.git · primary stale, behind 7', $full, 'detailed repository facts missing'); - - fwrite(STDOUT, "agents-md workspace freshness smoke passed\n"); -}