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
4 changes: 2 additions & 2 deletions inc/Abilities/WorkspaceAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -4605,7 +4605,7 @@ public static function prRebase( array $input ): array|\WP_Error {
* @return array
*/
public static function worktreeAdd( array $input ): array|\WP_Error {
$result = ( new WorktreeAllocationOperation() )->add(WorktreeAllocationRequest::from_input($input));
$result = ( new WorktreeAllocationOperation(new Workspace()) )->add(WorktreeAllocationRequest::from_input($input));
if ( is_array($result) && 'github_api' === ( $result['backend'] ?? null ) ) {
$result = self::decorate_remote_workspace_result('worktree_add', $result);
}
Expand Down Expand Up @@ -4651,7 +4651,7 @@ private static function worktree_add_response( array|\WP_Error $result, array $i

/** Plan a local worktree using the same typed fields and defaults as add. */
public static function worktreePlan( array $input ): array|\WP_Error {
return ( new WorktreeAllocationOperation() )->plan(WorktreeAllocationRequest::from_input($input));
return ( new WorktreeAllocationOperation(new Workspace()) )->plan(WorktreeAllocationRequest::from_input($input));
}

/** Apply a previously returned local worktree plan. */
Expand Down
3 changes: 2 additions & 1 deletion inc/Workspace/Workspace.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
require_once __DIR__ . '/WorkspaceRepositoryLifecycle.php';
require_once __DIR__ . '/WorkspacePrimaryRestore.php';
require_once __DIR__ . '/WorkspaceRowTriage.php';
require_once __DIR__ . '/WorktreeLifecycle.php';
require_once __DIR__ . '/WorkspaceWorktreeLifecycle.php';
require_once __DIR__ . '/WorktreeAgeFilter.php';
require_once __DIR__ . '/WorktreeCleanupSignal.php';
Expand All @@ -38,7 +39,7 @@
require_once __DIR__ . '/WorktreeCleanupClassifier.php';
require_once __DIR__ . '/WorkspaceSafeCleanupOrchestrator.php';

class Workspace {
class Workspace implements WorktreeLifecycle {
use WorkspaceCoreUtilities;
use WorkspaceActiveNoSignalCleanup;
use WorkspaceArtifactCleanup;
Expand Down
108 changes: 0 additions & 108 deletions inc/Workspace/WorkspaceWorktreeLifecycle.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,6 @@ trait WorkspaceWorktreeLifecycle {
*
* @return array<string,mixed>|\WP_Error
*/
public function worktree_plan( string $repo, string $branch, ?string $from = null, bool $inject_context = true, bool $bootstrap = true, bool $allow_stale = false, bool $rebase_base = false, bool $force = false, array $task = array(), bool $allow_unverified_freshness = false, bool $require_task_tracker = false, array $intent = array(), string $reuse_policy = 'reuse_compatible', bool $allow_percentage_byte_floor_exception = false ): array|\WP_Error {
return $this->worktree_plan_request(
new WorktreeAllocationRequest(
repo: $repo,
branch: $branch,
from: $from,
inject_context: $inject_context,
bootstrap: $bootstrap,
allow_stale: $allow_stale,
rebase_base: $rebase_base,
force: $force,
task: $task,
allow_unverified_freshness: $allow_unverified_freshness,
require_task_tracker: $require_task_tracker,
intent: $intent,
reuse_policy: $reuse_policy,
allow_percentage_byte_floor_exception: $allow_percentage_byte_floor_exception
)
);
}

/** Execute worktree planning from one explicit allocation contract. */
public function worktree_plan_request( WorktreeAllocationRequest $request ): array|\WP_Error {
$repo = $request->repo;
$branch = $request->branch;
Expand Down Expand Up @@ -1117,92 +1095,6 @@ private function worktree_plan_sort( mixed $value ): mixed {



/**
* Create a git worktree for a branch.
*
* Layout: `<workspace>/<repo>@<branch-slug>` is added as a worktree of
* `<workspace>/<repo>` checked out to `<branch>`. If the branch does not
* exist locally, it is created from `<from>` (default `origin/HEAD`).
*
* When `$inject_context` is true (default) and Data Machine's agent memory
* layer is available, the originating site context is rendered into the
* runtime projections registered by installed integrations. Projected paths
* are added to the worktree's per-checkout `info/exclude`. When the memory
* layer is absent the worktree is still created successfully; injection
* silently skips.
*
* When `$bootstrap` is true (default), a bootstrap pass runs after the
* worktree is created: `git submodule update --init --recursive` if
* `.gitmodules` is present, package-manager installs for root or one-level
* nested dependency roots with lockfiles (pnpm/bun/yarn/npm; submodule roots
* are excluded unless `.datamachine/worktree-bootstrap.json` opts them in), and
* `composer install` for root or one-level nested dependency roots with
* `composer.lock`. Steps are independent and each one is skipped gracefully
* when its tool is unavailable. A failing step is surfaced in the result
* but does not roll back the worktree — the checkout exists either way.
* Pass `$bootstrap = false` (or `--no-bootstrap` on the CLI) for a bare
* checkout when you only need to read code on that branch.
*
* When remote freshness cannot be verified, worktree creation is refused
* unless `$allow_unverified_freshness` is set. This keeps default operation
* fail-closed while preserving intentional offline workflows.
*
* When the branch/base is behind the remote default branch, worktree
* creation is refused unless `$allow_stale` is set. This check is
* zero-tolerance: any default-branch commits missing from the requested
* branch/base mean the worktree would start stale.
*
* When the materialized branch (or its local base) is more than
* `datamachine_worktree_stale_threshold` commits behind upstream and
* neither `$allow_stale` nor `$rebase_base` is set, the worktree is
* torn down and the call returns a `worktree_stale` WP_Error with
* remediation guidance. Pass `$allow_stale = true` to proceed anyway,
* or `$rebase_base = true` to auto-rebase onto the upstream tip before
* returning. On rebase conflicts the rebase is aborted (worktree stays
* at its pre-rebase state) and `rebase_failed: true` is surfaced in
* the response so the agent can resolve manually.
*
* @param string $repo Primary repo name (no @-suffix).
* @param string $branch Branch to check out (e.g. "fix/foo-bar").
* @param string|null $from Base ref when creating the branch.
* @param bool $inject_context Whether to inject site-agent context (default true).
* @param bool $bootstrap Whether to run submodule/package/composer install after creation (default true).
* @param bool $allow_stale Bypass the staleness gate (default false).
* @param bool $rebase_base Rebase onto upstream after creation (default false).
* @param bool $force Bypass the disk-budget refusal threshold (default false).
* @param array $task Optional task metadata recorded on the worktree.
* @param bool $allow_unverified_freshness Bypass fetch-failure freshness verification (default false).
* @param bool $require_task_tracker Reject creation without task metadata (default false).
* @param string $reuse_policy Existing-handle and same-task allocation policy.
* @param callable|null $progress_callback Best-effort lifecycle phase observer.
* @param bool $allow_percentage_byte_floor_exception Admit a bounded demand past only the percentage byte floor.
* @return array{success: bool, handle: string, path: string, branch: string, slug: string, created_branch: bool, message: string, disk_budget?: array, context_injected?: bool, context_files?: string[], context_skip_reason?: string, bootstrap?: array, fetch_failed?: bool, fetch_error?: string, fetch_attempts?: int, stale_commits_behind?: int, upstream?: string, base_stale_commits_behind?: int, base_upstream?: string, default_branch_commits_behind?: int, default_branch_ref?: string, gate_threshold?: int, rebase_attempted?: bool, rebase_succeeded?: bool, rebase_error?: string, rebase_target?: string}|\WP_Error
*/
public function worktree_add( string $repo, string $branch, ?string $from = null, bool $inject_context = true, bool $bootstrap = true, bool $allow_stale = false, bool $rebase_base = false, bool $force = false, array $task = array(), bool $allow_unverified_freshness = false, bool $require_task_tracker = false, array $intent = array(), string $reuse_policy = 'reuse_compatible', bool $remediate_capacity = false, bool $remediate_capacity_dry_run = false, ?callable $progress_callback = null, array $expected_freshness_identity = array(), bool $allow_percentage_byte_floor_exception = false ): array|\WP_Error {
return $this->worktree_add_request(
new WorktreeAllocationRequest(
repo: $repo,
branch: $branch,
from: $from,
inject_context: $inject_context,
bootstrap: $bootstrap,
allow_stale: $allow_stale,
rebase_base: $rebase_base,
force: $force,
task: $task,
allow_unverified_freshness: $allow_unverified_freshness,
require_task_tracker: $require_task_tracker,
intent: $intent,
reuse_policy: $reuse_policy,
remediate_capacity: $remediate_capacity,
remediate_capacity_dry_run: $remediate_capacity_dry_run,
progress_callback: $progress_callback,
expected_freshness_identity: $expected_freshness_identity,
allow_percentage_byte_floor_exception: $allow_percentage_byte_floor_exception
)
);
}

/** Execute worktree allocation from one explicit allocation contract. */
public function worktree_add_request( WorktreeAllocationRequest $request ): array|\WP_Error {
$repo = $request->repo;
Expand Down
11 changes: 5 additions & 6 deletions inc/Workspace/WorktreeAllocationOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,17 @@ final class WorktreeAllocationOperation {
* @param callable():bool|null $remote_enabled
*/
public function __construct(
private ?Workspace $workspace = null,
private WorktreeLifecycle $lifecycle,
private ?RemoteWorkspaceBackend $remote = null,
?callable $remote_enabled = null
) {
$this->workspace ??= new Workspace();
$this->remote ??= new RemoteWorkspaceBackend();
$this->remote_enabled = $remote_enabled ?? static fn(): bool => RemoteWorkspaceBackend::should_handle();
}

/** Execute a read-only allocation plan. */
public function plan( WorktreeAllocationRequest $request ): array|\WP_Error {
return $this->workspace->worktree_plan_request($request);
return $this->lifecycle->worktree_plan_request($request);
}

/** Execute allocation against the authoritative available backend. */
Expand All @@ -42,7 +41,7 @@ public function add( WorktreeAllocationRequest $request ): array|\WP_Error {
}

if ( ! $remote_enabled || $local_primary ) {
return $this->workspace->worktree_add_request($request);
return $this->lifecycle->worktree_add_request($request);
}

if ( $request->allow_percentage_byte_floor_exception ) {
Expand Down Expand Up @@ -81,7 +80,7 @@ public function add( WorktreeAllocationRequest $request ): array|\WP_Error {
$request->allow_unverified_freshness
);
if ( $this->should_fallback_to_local($result) ) {
return $this->workspace->worktree_add_request($request);
return $this->lifecycle->worktree_add_request($request);
}

return $result;
Expand All @@ -92,7 +91,7 @@ private function has_local_primary( string $repo ): bool {
return false;
}

$result = $this->workspace->show_repo($repo);
$result = $this->lifecycle->show_repo($repo);
if ( is_wp_error($result) ) {
return false;
}
Expand Down
22 changes: 22 additions & 0 deletions inc/Workspace/WorktreeLifecycle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
/**
* Local worktree allocation surface used by the application operation.
*
* @package DataMachineCode\Workspace
*/

namespace DataMachineCode\Workspace;

defined('ABSPATH') || exit;

interface WorktreeLifecycle {

/** Produce a non-mutating allocation decision from one typed request. */
public function worktree_plan_request( WorktreeAllocationRequest $request ): array|\WP_Error;

/** Execute allocation from one typed request. */
public function worktree_add_request( WorktreeAllocationRequest $request ): array|\WP_Error;

/** Resolve a local repository or worktree handle for primary detection. */
public function show_repo( string $handle, bool $refresh = false ): array|\WP_Error;
}
44 changes: 44 additions & 0 deletions tests/support/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,50 @@ function dmc_test_assert( bool $condition, string $message ): void {
}
}

require_once dirname(__DIR__, 2) . '/inc/Workspace/WorktreeAllocationRequest.php';

function dmc_test_allocation_request(
string $repo,
string $branch,
?string $from = null,
bool $inject_context = true,
bool $bootstrap = true,
bool $allow_stale = false,
bool $rebase_base = false,
bool $force = false,
array $task = array(),
bool $allow_unverified_freshness = false,
bool $require_task_tracker = false,
array $intent = array(),
string $reuse_policy = 'reuse_compatible',
bool $remediate_capacity = false,
bool $remediate_capacity_dry_run = false,
?callable $progress_callback = null,
array $expected_freshness_identity = array(),
bool $allow_percentage_byte_floor_exception = false
): \DataMachineCode\Workspace\WorktreeAllocationRequest {
return new \DataMachineCode\Workspace\WorktreeAllocationRequest(
repo: $repo,
branch: $branch,
from: $from,
inject_context: $inject_context,
bootstrap: $bootstrap,
allow_stale: $allow_stale,
rebase_base: $rebase_base,
force: $force,
task: $task,
allow_unverified_freshness: $allow_unverified_freshness,
require_task_tracker: $require_task_tracker,
intent: $intent,
reuse_policy: $reuse_policy,
remediate_capacity: $remediate_capacity,
remediate_capacity_dry_run: $remediate_capacity_dry_run,
progress_callback: $progress_callback,
expected_freshness_identity: $expected_freshness_identity,
allow_percentage_byte_floor_exception: $allow_percentage_byte_floor_exception
);
}

function dmc_test_source( string $relative_path ): string {
$source = file_get_contents(dirname(__DIR__, 2) . '/' . ltrim($relative_path, '/'));
if ( false === $source ) {
Expand Down
5 changes: 3 additions & 2 deletions tests/workspace-lock-sqlite-contention.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

const ARRAY_A = 'ARRAY_A';
define('ABSPATH', __DIR__ . '/fixtures/');
require_once __DIR__ . '/support/bootstrap.php';

final class WP_Error {
public function __construct(private string $code = '', private string $message = '', private mixed $data = null) {}
Expand Down Expand Up @@ -121,7 +122,7 @@ function lock_sqlite_worker(array $args): void {
putenv('DATAMACHINE_TASK_URL=HTTPS://Example.TEST:443/tracker/1247/?source=environment');
putenv('DATAMACHINE_TASK_REF=environment#1247');
$task = 'unsafe-lifecycle' === $mode ? array( 'task_url' => 'https://token:must-not-leak@example.test/tracker/1247' ) : array();
$result = (new Workspace())->worktree_add(
$result = (new Workspace())->worktree_add_request(dmc_test_allocation_request(
$repo,
"contention/quote'path;safe",
"refs/heads/base/quote'path;safe",
Expand All @@ -136,7 +137,7 @@ function lock_sqlite_worker(array $args): void {
array( 'purpose' => "review'purpose", 'owner_run_ref' => 'run;1247', 'cleanup_policy' => 'remove_on_success' ),
'isolated',
true
);
));
fwrite(STDOUT, json_encode(lock_sqlite_result($result)));
return;
}
Expand Down
12 changes: 11 additions & 1 deletion tests/workspace-operation-architecture.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@
$next = strpos($abilities, "\n\tpublic static function ", $start + 1);
$function = substr($abilities, $start, false === $next ? null : $next - $start);
dmc_test_assert(str_contains($function, 'WorktreeAllocationOperation'), sprintf('WorkspaceAbilities::%s() bypasses the allocation operation.', $method));
dmc_test_assert(! str_contains($function, 'new Workspace('), sprintf('WorkspaceAbilities::%s() reaches the lifecycle aggregate directly.', $method));
dmc_test_assert(str_contains($function, 'new WorktreeAllocationOperation(new Workspace())'), sprintf('WorkspaceAbilities::%s() does not pass the lifecycle contract into the operation.', $method));
dmc_test_assert(! str_contains($function, 'worktree_plan('), sprintf('WorkspaceAbilities::%s() still uses the positional plan adapter.', $method));
dmc_test_assert(! str_contains($function, 'worktree_add('), sprintf('WorkspaceAbilities::%s() still uses the positional add adapter.', $method));
}

$operation = dmc_test_source('inc/Workspace/WorktreeAllocationOperation.php');
dmc_test_assert(str_contains($operation, 'WorktreeLifecycle $lifecycle'), 'WorktreeAllocationOperation does not depend on the lifecycle contract.');
dmc_test_assert(! str_contains($operation, 'new Workspace('), 'WorktreeAllocationOperation still constructs the trait aggregate internally.');

$lifecycle = dmc_test_source('inc/Workspace/WorkspaceWorktreeLifecycle.php');
dmc_test_assert(1 !== preg_match('/function worktree_plan\s*\(\s*string/', $lifecycle), 'Positional worktree_plan() adapter was restored.');
dmc_test_assert(1 !== preg_match('/function worktree_add\s*\(\s*string/', $lifecycle), 'Positional worktree_add() adapter was restored.');

dmc_test_assert(str_contains($command, 'CleanupRunControlOperation'), 'WorkspaceCommand does not delegate job-backed cleanup control.');
dmc_test_assert(! str_contains($command, 'SystemTaskDrainability'), 'WorkspaceCommand still owns cleanup drainability decisions.');
dmc_test_assert(! is_file(dirname(__DIR__) . '/inc/Tools/WorkspaceTools.php'), 'Legacy workspace BaseTool wrappers were restored.');
Expand Down
Loading
Loading