Skip to content

Commit 844902e

Browse files
authored
Merge pull request #51 from Extra-Chill/feat-worktree-bootstrap
feat(workspace): bootstrap worktrees by default (closes #50)
2 parents 386c41a + 73527eb commit 844902e

6 files changed

Lines changed: 673 additions & 28 deletions

File tree

inc/Abilities/WorkspaceAbilities.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ private function registerAbilities(): void {
706706
'datamachine/workspace-worktree-add',
707707
array(
708708
'label' => 'Add Workspace Worktree',
709-
'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`.',
709+
'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`. When `bootstrap` is true (default), submodule init + package-manager install + composer install run after creation so the worktree is immediately test/build-ready; set false to create a bare checkout.',
710710
'category' => 'datamachine-code-workspace',
711711
'input_schema' => array(
712712
'type' => 'object',
@@ -727,6 +727,10 @@ private function registerAbilities(): void {
727727
'type' => 'boolean',
728728
'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.',
729729
),
730+
'bootstrap' => array(
731+
'type' => 'boolean',
732+
'description' => 'Run detected bootstrap steps (submodule init, package-manager install, composer install) after creating the worktree. Default true. Steps are skipped gracefully when their trigger file or tool is missing. Set false for a bare checkout (e.g. when only reading code).',
733+
),
730734
),
731735
'required' => array( 'repo', 'branch' ),
732736
),
@@ -747,6 +751,10 @@ private function registerAbilities(): void {
747751
),
748752
'context_exclude_path' => array( 'type' => 'string' ),
749753
'context_skip_reason' => array( 'type' => 'string' ),
754+
'bootstrap' => array(
755+
'type' => 'object',
756+
'description' => 'Present only when bootstrap=true. Contains success/ran_any booleans and a steps array.',
757+
),
750758
),
751759
),
752760
'execute_callback' => array( self::class, 'worktreeAdd' ),
@@ -1181,11 +1189,14 @@ public static function worktreeAdd( array $input ): array|\WP_Error {
11811189
$workspace = new Workspace();
11821190
// Default inject_context=true; only false when explicitly provided.
11831191
$inject_context = array_key_exists( 'inject_context', $input ) ? (bool) $input['inject_context'] : true;
1192+
// Default bootstrap=true; only false when explicitly provided.
1193+
$bootstrap = array_key_exists( 'bootstrap', $input ) ? (bool) $input['bootstrap'] : true;
11841194
return $workspace->worktree_add(
11851195
$input['repo'] ?? '',
11861196
$input['branch'] ?? '',
11871197
$input['from'] ?? null,
1188-
$inject_context
1198+
$inject_context,
1199+
$bootstrap
11891200
);
11901201
}
11911202

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,19 @@ private function renderGitOperationResult( string $operation, array $result, arr
907907
* paths to the repository's `info/exclude`. The ability-level input
908908
* is `inject_context=false`; this flag is the CLI shorthand.
909909
*
910+
* [--skip-bootstrap]
911+
* : Skip the default bootstrap pass (applies to `add` only). By default,
912+
* `worktree add` runs detected setup so the checkout is immediately
913+
* test/build-ready:
914+
* - git submodule update --init --recursive (if .gitmodules)
915+
* - pnpm/bun/yarn/npm install (based on lockfile)
916+
* - composer install --no-interaction (if composer.lock)
917+
* Each step is skipped gracefully when its trigger file or tool is
918+
* missing. Pass `--skip-bootstrap` to create a bare checkout for pure
919+
* read use (faster, no deps installed). The ability-level input is
920+
* `bootstrap=false`; this flag is the CLI shorthand (matches the
921+
* existing `--skip-context-injection` convention).
922+
*
910923
* [--force]
911924
* : Force-remove a worktree even if it is dirty (applies to `remove` and
912925
* `cleanup`). Does NOT override the unpushed-commits safety in cleanup.
@@ -960,6 +973,9 @@ private function renderGitOperationResult( string $operation, array $result, arr
960973
* # Create a worktree without injecting site-agent context
961974
* wp datamachine workspace worktree add data-machine fix/foo --skip-context-injection
962975
*
976+
* # Create a bare worktree (skip the default bootstrap pass)
977+
* wp datamachine workspace worktree add data-machine fix/foo --skip-bootstrap
978+
*
963979
* # Re-read the originating site's agent memory into an existing worktree
964980
* wp datamachine workspace worktree refresh-context data-machine@fix-foo
965981
*
@@ -999,7 +1015,7 @@ public function worktree( array $args, array $assoc_args ): void {
9991015
switch ( $operation ) {
10001016
case 'add':
10011017
if ( empty( $args[1] ) || empty( $args[2] ) ) {
1002-
WP_CLI::error( 'Usage: worktree add <repo> <branch> [--from=<ref>] [--skip-context-injection]' );
1018+
WP_CLI::error( 'Usage: worktree add <repo> <branch> [--from=<ref>] [--skip-context-injection] [--skip-bootstrap]' );
10031019
return;
10041020
}
10051021
$input['repo'] = $args[1];
@@ -1009,6 +1025,8 @@ public function worktree( array $args, array $assoc_args ): void {
10091025
}
10101026
// --skip-context-injection disables the default-on injection step.
10111027
$input['inject_context'] = empty( $assoc_args['skip-context-injection'] );
1028+
// --skip-bootstrap disables the default-on bootstrap step.
1029+
$input['bootstrap'] = empty( $assoc_args['skip-bootstrap'] );
10121030
break;
10131031

10141032
case 'refresh-context':
@@ -1159,6 +1177,17 @@ private function renderWorktreeResult( string $operation, array $result, array $
11591177
WP_CLI::log( sprintf( 'Context: not injected (%s)', $reason ) );
11601178
}
11611179
}
1180+
if ( isset( $result['bootstrap'] ) && is_array( $result['bootstrap'] ) ) {
1181+
$bs = $result['bootstrap'];
1182+
$ok = ! empty( $bs['success'] );
1183+
$ran_any = ! empty( $bs['ran_any'] );
1184+
$label = $ok ? ( $ran_any ? 'Bootstrap: ok' : 'Bootstrap: nothing to do' ) : 'Bootstrap: one or more steps FAILED';
1185+
WP_CLI::log( $label );
1186+
WP_CLI::log( \DataMachineCode\Workspace\WorktreeBootstrapper::format( $bs ) );
1187+
if ( ! $ok ) {
1188+
WP_CLI::warning( 'Worktree was created but bootstrap had failures. Re-run the failing step manually, or remove and retry.' );
1189+
}
1190+
}
11621191
return;
11631192

11641193
case 'refresh-context':

inc/Workspace/Workspace.php

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -861,13 +861,24 @@ public function git_diff( string $name, ?string $from = null, ?string $to = null
861861
* absent the worktree is still created successfully; injection silently
862862
* skips.
863863
*
864+
* When `$bootstrap` is true (default), a bootstrap pass runs after the
865+
* worktree is created: `git submodule update --init --recursive` if
866+
* `.gitmodules` is present, a package-manager install if a lockfile is
867+
* present (pnpm/bun/yarn/npm), and `composer install` if `composer.lock`
868+
* is present. Steps are independent and each one is skipped gracefully
869+
* when its tool is unavailable. A failing step is surfaced in the result
870+
* but does not roll back the worktree — the checkout exists either way.
871+
* Pass `$bootstrap = false` (or `--no-bootstrap` on the CLI) for a bare
872+
* checkout when you only need to read code on that branch.
873+
*
864874
* @param string $repo Primary repo name (no @-suffix).
865875
* @param string $branch Branch to check out (e.g. "fix/foo-bar").
866876
* @param string|null $from Base ref when creating the branch.
867877
* @param bool $inject_context Whether to inject site-agent context (default true).
868-
* @return array{success: bool, handle: string, path: string, branch: string, slug: string, created_branch: bool, message: string, context_injected?: bool, context_files?: string[], context_skip_reason?: string}|\WP_Error
878+
* @param bool $bootstrap Whether to run submodule/package/composer install after creation (default true).
879+
* @return array{success: bool, handle: string, path: string, branch: string, slug: string, created_branch: bool, message: string, context_injected?: bool, context_files?: string[], context_skip_reason?: string, bootstrap?: array}|\WP_Error
869880
*/
870-
public function worktree_add( string $repo, string $branch, ?string $from = null, bool $inject_context = true ): array|\WP_Error {
881+
public function worktree_add( string $repo, string $branch, ?string $from = null, bool $inject_context = true, bool $bootstrap = true ): array|\WP_Error {
871882
$repo = $this->sanitize_name( $repo );
872883
$branch = trim( $branch );
873884

@@ -929,29 +940,29 @@ public function worktree_add( string $repo, string $branch, ?string $from = null
929940
if ( ! $inject_context ) {
930941
$response['context_injected'] = false;
931942
$response['context_skip_reason'] = 'inject_context flag disabled';
932-
return $response;
933-
}
934-
935-
$payload = WorktreeContextInjector::build_payload();
936-
if ( null === $payload ) {
937-
$response['context_injected'] = false;
938-
$response['context_skip_reason'] = 'agent memory layer unavailable';
939-
return $response;
940-
}
941-
942-
$injection = WorktreeContextInjector::inject( $wt_path, $payload );
943-
if ( is_wp_error( $injection ) ) {
944-
$response['context_injected'] = false;
945-
$response['context_skip_reason'] = 'inject failed: ' . $injection->get_error_message();
946-
return $response;
943+
} else {
944+
$payload = WorktreeContextInjector::build_payload();
945+
if ( null === $payload ) {
946+
$response['context_injected'] = false;
947+
$response['context_skip_reason'] = 'agent memory layer unavailable';
948+
} else {
949+
$injection = WorktreeContextInjector::inject( $wt_path, $payload );
950+
if ( is_wp_error( $injection ) ) {
951+
$response['context_injected'] = false;
952+
$response['context_skip_reason'] = 'inject failed: ' . $injection->get_error_message();
953+
} else {
954+
WorktreeContextInjector::store_metadata( $wt_handle, $payload );
955+
$response['context_injected'] = true;
956+
$response['context_files'] = $injection['written'];
957+
if ( ! empty( $injection['exclude_path'] ) ) {
958+
$response['context_exclude_path'] = $injection['exclude_path'];
959+
}
960+
}
961+
}
947962
}
948963

949-
WorktreeContextInjector::store_metadata( $wt_handle, $payload );
950-
951-
$response['context_injected'] = true;
952-
$response['context_files'] = $injection['written'];
953-
if ( ! empty( $injection['exclude_path'] ) ) {
954-
$response['context_exclude_path'] = $injection['exclude_path'];
964+
if ( $bootstrap ) {
965+
$response['bootstrap'] = WorktreeBootstrapper::bootstrap( $wt_path );
955966
}
956967

957968
return $response;

0 commit comments

Comments
 (0)