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
86 changes: 74 additions & 12 deletions inc/Abilities/WorkspaceAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -701,36 +701,47 @@ private function registerAbilities(): void {
'datamachine/workspace-worktree-add',
array(
'label' => 'Add Workspace Worktree',
'description' => 'Create a git worktree for a branch under `<repo>@<branch-slug>`. Branches are created off the supplied `from` ref (default `origin/HEAD`) when they do not yet exist locally.',
'description' => 'Create a git worktree for a branch under `<repo>@<branch-slug>`. Branches are created off the supplied `from` ref (default `origin/HEAD`) when they do not yet exist locally. When `inject_context` is true (default), the originating site\'s agent memory is snapshotted into `.claude/CLAUDE.local.md` and `.opencode/AGENTS.local.md` and added to the worktree\'s per-checkout `info/exclude`.',
'category' => 'datamachine-code-workspace',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'repo' => array(
'repo' => array(
'type' => 'string',
'description' => 'Primary repo name (no @-suffix).',
),
'branch' => array(
'branch' => array(
'type' => 'string',
'description' => 'Branch to check out in the worktree (e.g. fix/foo-bar). Slashes become dashes in the on-disk slug.',
),
'from' => array(
'from' => array(
'type' => 'string',
'description' => 'Base ref when creating the branch (default origin/HEAD).',
),
'inject_context' => array(
'type' => 'boolean',
'description' => 'Inject the originating site\'s agent context (MEMORY.md, USER.md, RULES.md) into the new worktree. Default true. Set false to create a bare worktree.',
),
),
'required' => array( 'repo', 'branch' ),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'success' => array( 'type' => 'boolean' ),
'handle' => array( 'type' => 'string' ),
'path' => array( 'type' => 'string' ),
'branch' => array( 'type' => 'string' ),
'slug' => array( 'type' => 'string' ),
'created_branch' => array( 'type' => 'boolean' ),
'message' => array( 'type' => 'string' ),
'success' => array( 'type' => 'boolean' ),
'handle' => array( 'type' => 'string' ),
'path' => array( 'type' => 'string' ),
'branch' => array( 'type' => 'string' ),
'slug' => array( 'type' => 'string' ),
'created_branch' => array( 'type' => 'boolean' ),
'message' => array( 'type' => 'string' ),
'context_injected' => array( 'type' => 'boolean' ),
'context_files' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
),
'context_exclude_path' => array( 'type' => 'string' ),
'context_skip_reason' => array( 'type' => 'string' ),
),
),
'execute_callback' => array( self::class, 'worktreeAdd' ),
Expand All @@ -739,6 +750,43 @@ private function registerAbilities(): void {
)
);

wp_register_ability(
'datamachine/workspace-worktree-refresh-context',
array(
'label' => 'Refresh Worktree Context',
'description' => 'Re-read the originating site\'s agent memory and rewrite the injected context files (`.claude/CLAUDE.local.md`, `.opencode/AGENTS.local.md`) in an existing worktree. Must be run from the site that created the worktree — cross-machine refresh is not supported.',
'category' => 'datamachine-code-workspace',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'handle' => array(
'type' => 'string',
'description' => 'Worktree handle (`<repo>@<branch-slug>`).',
),
),
'required' => array( 'handle' ),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'success' => array( 'type' => 'boolean' ),
'handle' => array( 'type' => 'string' ),
'path' => array( 'type' => 'string' ),
'written' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
),
'exclude_path' => array( 'type' => 'string' ),
'metadata' => array( 'type' => 'object' ),
'message' => array( 'type' => 'string' ),
),
),
'execute_callback' => array( self::class, 'worktreeRefreshContext' ),
'permission_callback' => fn() => PermissionHelper::can_manage(),
'meta' => array( 'show_in_rest' => false ),
)
);

wp_register_ability(
'datamachine/workspace-worktree-list',
array(
Expand Down Expand Up @@ -1126,13 +1174,27 @@ public static function gitPush( array $input ): array|\WP_Error {
*/
public static function worktreeAdd( array $input ): array|\WP_Error {
$workspace = new Workspace();
// Default inject_context=true; only false when explicitly provided.
$inject_context = array_key_exists( 'inject_context', $input ) ? (bool) $input['inject_context'] : true;
return $workspace->worktree_add(
$input['repo'] ?? '',
$input['branch'] ?? '',
$input['from'] ?? null
$input['from'] ?? null,
$inject_context
);
}

/**
* Refresh a worktree's injected context from the originating site.
*
* @param array $input Input parameters with 'handle'.
* @return array|\WP_Error
*/
public static function worktreeRefreshContext( array $input ): array|\WP_Error {
$workspace = new Workspace();
return $workspace->worktree_refresh_context( $input['handle'] ?? '' );
}

/**
* List worktrees in the workspace.
*
Expand Down
76 changes: 66 additions & 10 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -887,17 +887,26 @@ private function renderGitOperationResult( string $operation, array $result, arr
* ## OPTIONS
*
* <operation>
* : Worktree operation: add, list, remove, prune, cleanup.
* : Worktree operation: add, list, remove, prune, cleanup, refresh-context.
*
* [<repo>]
* : Primary repo name (required for add and remove).
* : Primary repo name (required for add and remove). For refresh-context,
* pass the full worktree handle (`<repo>@<branch-slug>`) here instead.
*
* [<branch>]
* : Branch name (required for add and remove).
*
* [--from=<ref>]
* : Base ref when creating a branch on add (default origin/HEAD).
*
* [--skip-context-injection]
* : Skip injecting the originating site's agent context into a new
* worktree (applies to `add` only). Default behavior is to write
* `.claude/CLAUDE.local.md` and `.opencode/AGENTS.local.md` containing
* the site's MEMORY.md / USER.md / RULES.md snapshot, and add both
* paths to the repository's `info/exclude`. The ability-level input
* is `inject_context=false`; this flag is the CLI shorthand.
*
* [--force]
* : Force-remove a worktree even if it is dirty (applies to `remove` and
* `cleanup`). Does NOT override the unpushed-commits safety in cleanup.
Expand Down Expand Up @@ -948,23 +957,30 @@ private function renderGitOperationResult( string $operation, array $result, arr
* # Ignore dirty working-tree safety (caution)
* wp datamachine workspace worktree cleanup --force
*
* # Create a worktree without injecting site-agent context
* wp datamachine workspace worktree add data-machine fix/foo --skip-context-injection
*
* # Re-read the originating site's agent memory into an existing worktree
* wp datamachine workspace worktree refresh-context data-machine@fix-foo
*
* @subcommand worktree
*/
public function worktree( array $args, array $assoc_args ): void {
$operation = $args[0] ?? '';

if ( '' === $operation ) {
WP_CLI::error( 'Usage: wp datamachine workspace worktree <add|list|remove|prune|cleanup> [<repo>] [<branch>] [--flags]' );
WP_CLI::error( 'Usage: wp datamachine workspace worktree <add|list|remove|prune|cleanup|refresh-context> [<repo>] [<branch>] [--flags]' );
return;
}

$ability_name = match ( $operation ) {
'add' => 'datamachine/workspace-worktree-add',
'list' => 'datamachine/workspace-worktree-list',
'remove' => 'datamachine/workspace-worktree-remove',
'prune' => 'datamachine/workspace-worktree-prune',
'cleanup' => 'datamachine/workspace-worktree-cleanup',
default => '',
'add' => 'datamachine/workspace-worktree-add',
'list' => 'datamachine/workspace-worktree-list',
'remove' => 'datamachine/workspace-worktree-remove',
'prune' => 'datamachine/workspace-worktree-prune',
'cleanup' => 'datamachine/workspace-worktree-cleanup',
'refresh-context' => 'datamachine/workspace-worktree-refresh-context',
default => '',
};

if ( '' === $ability_name ) {
Expand All @@ -983,14 +999,24 @@ public function worktree( array $args, array $assoc_args ): void {
switch ( $operation ) {
case 'add':
if ( empty( $args[1] ) || empty( $args[2] ) ) {
WP_CLI::error( 'Usage: worktree add <repo> <branch> [--from=<ref>]' );
WP_CLI::error( 'Usage: worktree add <repo> <branch> [--from=<ref>] [--skip-context-injection]' );
return;
}
$input['repo'] = $args[1];
$input['branch'] = $args[2];
if ( ! empty( $assoc_args['from'] ) ) {
$input['from'] = (string) $assoc_args['from'];
}
// --skip-context-injection disables the default-on injection step.
$input['inject_context'] = empty( $assoc_args['skip-context-injection'] );
break;

case 'refresh-context':
if ( empty( $args[1] ) ) {
WP_CLI::error( 'Usage: worktree refresh-context <handle>' );
return;
}
$input['handle'] = (string) $args[1];
break;

case 'list':
Expand Down Expand Up @@ -1118,6 +1144,36 @@ private function renderWorktreeResult( string $operation, array $result, array $
WP_CLI::log( sprintf( 'Path: %s', $result['path'] ?? '-' ) );
WP_CLI::log( sprintf( 'Branch: %s%s', $result['branch'] ?? '-', ! empty( $result['created_branch'] ) ? ' (created)' : '' ) );
}
if ( isset( $result['context_injected'] ) ) {
if ( ! empty( $result['context_injected'] ) ) {
$written = $result['context_files'] ?? array();
WP_CLI::log( sprintf( 'Context: injected (%d file%s)', count( $written ), 1 === count( $written ) ? '' : 's' ) );
foreach ( $written as $file ) {
WP_CLI::log( ' - ' . $file );
}
if ( ! empty( $result['context_exclude_path'] ) ) {
WP_CLI::log( sprintf( 'Excluded via: %s', $result['context_exclude_path'] ) );
}
} else {
$reason = $result['context_skip_reason'] ?? 'unknown';
WP_CLI::log( sprintf( 'Context: not injected (%s)', $reason ) );
}
}
return;

case 'refresh-context':
WP_CLI::success( $result['message'] ?? 'Worktree context refreshed.' );
WP_CLI::log( sprintf( 'Handle: %s', $result['handle'] ?? '-' ) );
WP_CLI::log( sprintf( 'Path: %s', $result['path'] ?? '-' ) );
foreach ( (array) ( $result['written'] ?? array() ) as $file ) {
WP_CLI::log( ' - ' . $file );
}
if ( ! empty( $result['exclude_path'] ) ) {
WP_CLI::log( sprintf( 'Exclude file: %s', $result['exclude_path'] ) );
}
if ( ! empty( $result['metadata']['site_url'] ) ) {
WP_CLI::log( sprintf( 'Originating site: %s', $result['metadata']['site_url'] ) );
}
return;

case 'remove':
Expand Down
Loading