Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion data-machine-code.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ));
317 changes: 18 additions & 299 deletions inc/Runtime/AgentsMdSections.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand All @@ -86,27 +73,33 @@ 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);

return <<<MD
## Data Machine Code

{$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.
{$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.

- Workspace root: `{$workspace_path}`
- **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.
- **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 <repo>`, or `{$wp} datamachine-code workspace hygiene`
- Create isolated work: `{$wp} datamachine-code workspace worktree add <repo> <branch> --from=origin/<base>`
- Record PR/final state: `{$wp} datamachine-code workspace worktree finalize <repo@slug> --pr=<url>`
- Refresh an authoritative primary: `{$wp} datamachine-code workspace git pull <repo> --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.',
Expand All @@ -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.
Expand All @@ -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 <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.
$default = <<<MD
- **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.
- **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.
- **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;

/**
Expand All @@ -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;
Expand Down Expand Up @@ -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 <<<MD
## Workspace Inventory

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.

Snapshot summary: {$primary_count} primary {$primary_label} and {$worktree_count} {$worktree_label} under `{$workspace_path}`.

{$attention_block}
MD;
}

return <<<MD
## Workspace Inventory

Detailed snapshot from cloned repos in `{$workspace_path}`. `{$wp} datamachine-code workspace list` remains the source of truth for current worktrees and branches.

Refresh this file with `{$wp} datamachine memory compose AGENTS.md{$agent_suffix}` after workspace changes.

{$attention_block}

{$body}
MD;
}

private static function render_primary_freshness_summary( array $attention_lines, string $wp ): string {
$count = count($attention_lines);
if ( 0 === $count ) {
return '';
}

return sprintf(
'Primary checkout attention: %d %s need%s inspection. Run `%s datamachine-code workspace list` for details.',
$count,
1 === $count ? 'checkout' : 'checkouts',
1 === $count ? 's' : '',
$wp
);
}

private static function primary_freshness_needs_attention( ?array $freshness ): bool {
if ( null === $freshness ) {
return false;
}

$status = (string) ( $freshness['status'] ?? '' );
return in_array($status, array( 'stale', 'diverged', 'detached', 'unknown', 'no_upstream', 'ahead' ), true);
}

private static function primary_freshness_needs_refresh( ?array $freshness ): bool {
if ( null === $freshness ) {
return false;
}

$status = (string) ( $freshness['status'] ?? '' );
return in_array($status, array( 'stale', 'diverged' ), true);
}

private static function format_primary_freshness_badge( ?array $freshness ): string {
if ( ! self::primary_freshness_needs_attention($freshness) ) {
return '';
}

$status = (string) ( $freshness['status'] ?? 'unknown' );
$parts = array( 'primary ' . $status );
if ( isset($freshness['behind']) && is_numeric($freshness['behind']) && (int) $freshness['behind'] > 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 <<<MD
**Primary Checkout Attention**

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.

{$body}
MD;
}
}
5 changes: 2 additions & 3 deletions inc/Workspace/WorkspaceCoreUtilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -1149,9 +1149,8 @@ private function protect_directory( string $path ): void {
/**
* Fire the shared workspace-lifecycle action after a successful mutation.
*
* Listeners use this signal to refresh derived state (e.g. invalidating
* the composable AGENTS.md so its workspace-inventory section reflects the
* change). Only emitted on success, AFTER the on-disk change is durable.
* Listeners use this signal to refresh optional derived workspace state.
* Only emitted on success, AFTER the on-disk change is durable.
* The payload mirrors the workspace `name`/`repo` taxonomy used elsewhere
* in this class:
*
Expand Down
Loading