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
46 changes: 46 additions & 0 deletions inc/Abilities/WorkspaceAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,10 @@ private function registerAbilities(): void {
'type' => 'boolean',
'description' => 'With apply=true, schedule bounded metadata reconciliation page jobs instead of applying synchronously.',
),
'repo' => array(
'type' => 'string',
'description' => 'Optional primary repo or worktree handle scope. When supplied, pagination, evidence, and writes are constrained to matching rows.',
),
'limit' => array(
'type' => 'integer',
'description' => 'Positive page size for bounded dry-run, direct apply, budgeted apply, or job-backed apply.',
Expand Down Expand Up @@ -2060,6 +2064,10 @@ private function registerAbilities(): void {
'type' => 'string',
'description' => 'Compact time budget for this report page, such as 60s or 10m.',
),
'repo' => array(
'type' => 'string',
'description' => 'Optional primary repo or worktree handle scope. When supplied, pagination and evidence are constrained to matching rows.',
),
),
),
'output_schema' => array(
Expand Down Expand Up @@ -2103,6 +2111,10 @@ private function registerAbilities(): void {
'type' => 'string',
'description' => 'Compact time budget for the underlying active_no_signal report page, such as 60s or 10m.',
),
'repo' => array(
'type' => 'string',
'description' => 'Optional primary repo or worktree handle scope. When supplied, pagination, evidence, and writes are constrained to matching rows.',
),
),
),
'output_schema' => array(
Expand Down Expand Up @@ -2147,6 +2159,10 @@ private function registerAbilities(): void {
'type' => 'string',
'description' => 'Compact time budget for the underlying active_no_signal report page, such as 60s or 10m.',
),
'repo' => array(
'type' => 'string',
'description' => 'Optional primary repo or worktree handle scope. When supplied, pagination, evidence, and writes are constrained to matching rows.',
),
),
),
'output_schema' => array(
Expand Down Expand Up @@ -2191,6 +2207,10 @@ private function registerAbilities(): void {
'type' => 'string',
'description' => 'Compact time budget for the underlying active_no_signal report page, such as 60s or 10m.',
),
'repo' => array(
'type' => 'string',
'description' => 'Optional primary repo or worktree handle scope. When supplied, pagination, evidence, and writes are constrained to matching rows.',
),
),
),
'output_schema' => array(
Expand Down Expand Up @@ -2235,6 +2255,10 @@ private function registerAbilities(): void {
'type' => 'string',
'description' => 'Compact time budget for the underlying active_no_signal report page, such as 60s or 10m.',
),
'repo' => array(
'type' => 'string',
'description' => 'Optional primary repo or worktree handle scope. When supplied, pagination, evidence, and writes are constrained to matching rows.',
),
),
),
'output_schema' => array(
Expand Down Expand Up @@ -2296,6 +2320,10 @@ private function registerAbilities(): void {
'type' => 'string',
'description' => 'Caller source marker forwarded to underlying cleanup abilities.',
),
'repo' => array(
'type' => 'string',
'description' => 'Optional primary repo or worktree handle scope forwarded to underlying paginated cleanup abilities.',
),
),
),
'output_schema' => array(
Expand Down Expand Up @@ -4270,6 +4298,9 @@ public static function worktreeReconcileMetadata( array $input ): array|\WP_Erro
if ( isset($input['source']) && '' !== trim( (string) $input['source']) ) {
$opts['source'] = trim( (string) $input['source']);
}
if ( isset($input['repo']) && '' !== trim( (string) $input['repo']) ) {
$opts['repo'] = trim( (string) $input['repo']);
}

return $workspace->worktree_reconcile_metadata($opts);
}
Expand All @@ -4292,6 +4323,9 @@ public static function worktreeActiveNoSignalReport( array $input ): array|\WP_E
if ( isset($input['until_budget']) && '' !== trim( (string) $input['until_budget']) ) {
$opts['until_budget'] = trim( (string) $input['until_budget']);
}
if ( isset($input['repo']) && '' !== trim( (string) $input['repo']) ) {
$opts['repo'] = trim( (string) $input['repo']);
}

return $workspace->worktree_active_no_signal_report($opts);
}
Expand All @@ -4316,6 +4350,9 @@ public static function worktreeActiveNoSignalFinalizedApply( array $input ): arr
if ( isset($input['until_budget']) && '' !== trim( (string) $input['until_budget']) ) {
$opts['until_budget'] = trim( (string) $input['until_budget']);
}
if ( isset($input['repo']) && '' !== trim( (string) $input['repo']) ) {
$opts['repo'] = trim( (string) $input['repo']);
}

return $workspace->worktree_active_no_signal_finalized_apply($opts);
}
Expand All @@ -4340,6 +4377,9 @@ public static function worktreeActiveNoSignalEquivalentCleanApply( array $input
if ( isset($input['until_budget']) && '' !== trim( (string) $input['until_budget']) ) {
$opts['until_budget'] = trim( (string) $input['until_budget']);
}
if ( isset($input['repo']) && '' !== trim( (string) $input['repo']) ) {
$opts['repo'] = trim( (string) $input['repo']);
}

return $workspace->worktree_active_no_signal_equivalent_clean_apply($opts);
}
Expand All @@ -4364,6 +4404,9 @@ public static function worktreeActiveNoSignalMergedApply( array $input ): array|
if ( isset($input['until_budget']) && '' !== trim( (string) $input['until_budget']) ) {
$opts['until_budget'] = trim( (string) $input['until_budget']);
}
if ( isset($input['repo']) && '' !== trim( (string) $input['repo']) ) {
$opts['repo'] = trim( (string) $input['repo']);
}

return $workspace->worktree_active_no_signal_merged_apply($opts);
}
Expand All @@ -4388,6 +4431,9 @@ public static function worktreeActiveNoSignalRemoteCleanApply( array $input ): a
if ( isset($input['until_budget']) && '' !== trim( (string) $input['until_budget']) ) {
$opts['until_budget'] = trim( (string) $input['until_budget']);
}
if ( isset($input['repo']) && '' !== trim( (string) $input['repo']) ) {
$opts['repo'] = trim( (string) $input['repo']);
}

return $workspace->worktree_active_no_signal_remote_clean_apply($opts);
}
Expand Down
20 changes: 16 additions & 4 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3800,7 +3800,7 @@ public function worktree( array $args, array $assoc_args ): void {
}

if ( 'active-no-signal-drain' === $operation ) {
$result = $this->run_worktree_active_no_signal_drain($assoc_args);
$result = $this->run_worktree_active_no_signal_drain($assoc_args, isset($args[1]) ? (string) $args[1] : '');
if ( is_wp_error($result) ) {
$this->render_workspace_error($result);
return;
Expand Down Expand Up @@ -3871,7 +3871,7 @@ public function worktree( array $args, array $assoc_args ): void {
$input = array();
$input_builder = (string) ( $operation_config['input_builder'] ?? '' );
if ( '' !== $input_builder ) {
$input = $this->{$input_builder}($operation, $assoc_args);
$input = $this->{$input_builder}($operation, $assoc_args, isset($args[1]) ? (string) $args[1] : '');
}

switch ( $operation ) {
Expand Down Expand Up @@ -4028,6 +4028,9 @@ public function worktree( array $args, array $assoc_args ): void {
}
break;
case 'reconcile-metadata':
if ( ! empty($args[1]) ) {
$input['repo'] = (string) $args[1];
}
$input['dry_run'] = ! empty($assoc_args['dry-run']);
$input['apply'] = ! empty($assoc_args['apply']);
$input['via_jobs'] = ! empty($assoc_args['via-jobs']);
Expand Down Expand Up @@ -4149,8 +4152,11 @@ public function worktree( array $args, array $assoc_args ): void {
* @param array<string,mixed> $assoc_args CLI args.
* @return array<string,mixed>
*/
private function build_worktree_active_no_signal_input( string $operation, array $assoc_args ): array {
private function build_worktree_active_no_signal_input( string $operation, array $assoc_args, string $repo = '' ): array {
$input = array();
if ( '' !== trim($repo) ) {
$input['repo'] = trim($repo);
}

if ( in_array($operation, array( 'active-no-signal-finalized-apply', 'active-no-signal-equivalent-clean-apply', 'active-no-signal-merged-apply', 'active-no-signal-remote-clean-apply' ), true) ) {
$input['dry_run'] = ! empty($assoc_args['dry-run']);
Expand Down Expand Up @@ -4203,7 +4209,7 @@ private function run_worktree_abandoned_orchestration( array $assoc_args ): arra
* @param array<string,mixed> $assoc_args CLI args.
* @return array<string,mixed>|\WP_Error
*/
private function run_worktree_active_no_signal_drain( array $assoc_args ): array|\WP_Error {
private function run_worktree_active_no_signal_drain( array $assoc_args, string $repo = '' ): array|\WP_Error {
if ( ! empty($assoc_args['force']) ) {
return new \WP_Error('active_no_signal_drain_refuses_force', 'Active/no-signal drain will not force cleanup. Protected blockers remain blocked.', array( 'status' => 400 ));
}
Expand All @@ -4225,6 +4231,12 @@ private function run_worktree_active_no_signal_drain( array $assoc_args ): array
$input[ $key ] = $assoc_args[ $key ];
}
}
if ( '' !== trim($repo) ) {
$input['repo'] = trim($repo);
if ( empty($input['scope']) ) {
$input['scope'] = trim($repo);
}
}
if ( isset($assoc_args['until-budget']) ) {
$input['until_budget'] = $assoc_args['until-budget'];
}
Expand Down
18 changes: 16 additions & 2 deletions inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,15 @@ public function run( array $input ): array|\WP_Error {
$until_budget = isset($input['until_budget']) && '' !== trim( (string) $input['until_budget']) ? trim( (string) $input['until_budget']) : '';
$source = isset($input['source']) && '' !== trim( (string) $input['source']) ? trim( (string) $input['source']) : self::DEFAULT_SOURCE;
$scope = isset($input['scope']) && '' !== trim( (string) $input['scope']) ? $this->sanitize_scope( (string) $input['scope']) : '';
$deadline = null;
$stage_order = $this->stage_order();
$repo_scope = isset($input['repo']) && '' !== trim( (string) $input['repo']) ? trim( (string) $input['repo']) : '';
if ( '' === $scope && '' !== $repo_scope ) {
$scope = $this->sanitize_scope($repo_scope);
}
if ( '' === $repo_scope && '' !== $scope && $this->scope_is_worktree_filter($scope) ) {
$repo_scope = $scope;
}
$deadline = null;
$stage_order = $this->stage_order();

if ( ! isset($stage_order[ $stage ]) ) {
return new \WP_Error('invalid_worktree_abandoned_stage', 'Invalid stage value. Use reconcile, finalized, equivalent-clean, merged, remote-clean, or bounded.', array( 'status' => 400 ));
Expand All @@ -86,6 +93,9 @@ public function run( array $input ): array|\WP_Error {
if ( '' !== $scope ) {
$common_page['scope'] = $scope;
}
if ( '' !== $repo_scope ) {
$common_page['repo'] = $repo_scope;
}

if ( $apply ) {
$bounded = $this->run_bounded_apply($abilities['bounded_apply'], $result, true, $force, $limit, $source, 'initial');
Expand Down Expand Up @@ -300,6 +310,10 @@ private function sanitize_scope( string $scope ): string {
return substr($scope, 0, 120);
}

private function scope_is_worktree_filter( string $scope ): bool {
return 1 === preg_match('/^[a-zA-Z0-9._-]+(?:@[a-zA-Z0-9._-]+)?$/', $scope);
}

/** @return array<string,mixed> */
private function initial_result( bool $apply, bool $force, int $limit, string $stage, int $offset, int $passes, bool $active_no_signal_drain = false, string $scope = '' ): array {
return array(
Expand Down
17 changes: 13 additions & 4 deletions inc/Workspace/WorkspaceActiveNoSignalCleanup.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ public function worktree_active_no_signal_report( array $opts = array() ): array
? $opts['next_command_operation']
: 'active-no-signal-report';
$next_dry_run = 'active-no-signal-report' !== $next_operation && ! empty($opts['dry_run']);
$scope = $this->normalize_worktree_operation_scope(isset($opts['repo']) ? (string) $opts['repo'] : '');
if ( is_wp_error($scope) ) {
return $scope;
}
$scope_arg = $this->worktree_operation_scope_cli_arg($scope);
if ( $limit <= 0 ) {
return new \WP_Error('invalid_active_no_signal_limit', 'Active/no-signal report --limit must be greater than 0.', array( 'status' => 400 ));
}
Expand All @@ -39,7 +44,7 @@ public function worktree_active_no_signal_report( array $opts = array() ): array
}
}

$inventory = $this->worktree_cleanup_inventory_only('', '', false);
$inventory = $this->worktree_cleanup_inventory_only('', '', false, null, 0, null === $scope ? '' : (string) $scope['argument']);
if ( is_wp_error($inventory) ) {
return $inventory;
}
Expand Down Expand Up @@ -124,7 +129,7 @@ public function worktree_active_no_signal_report( array $opts = array() ): array
'partial' => null !== $next_offset,
'complete' => null === $next_offset,
'next_offset' => $next_offset,
'next_command' => null === $next_offset ? null : sprintf('studio wp datamachine-code workspace worktree %s%s --limit=%d --offset=%d%s --format=json', $next_operation, $next_dry_run ? ' --dry-run' : '', $limit, $next_offset, null !== $budget_context ? ' --until-budget=' . (string) $budget_context['label'] : ''),
'next_command' => null === $next_offset ? null : sprintf('studio wp datamachine-code workspace worktree %s%s%s --limit=%d --offset=%d%s --format=json', $next_operation, $scope_arg, $next_dry_run ? ' --dry-run' : '', $limit, $next_offset, null !== $budget_context ? ' --until-budget=' . (string) $budget_context['label'] : ''),
);
if ( $budget_stopped ) {
$pagination['partial'] = true;
Expand All @@ -140,7 +145,8 @@ public function worktree_active_no_signal_report( array $opts = array() ): array
'summary' => array_merge($summary, array( 'slow_rows' => $this->summarize_slow_worktree_rows($rows) )),
'pagination' => $pagination,
'evidence' => array(
'scope' => 'review-only active/no-signal and lifecycle reconciliation worktree evidence',
'scope' => null === $scope ? 'review-only active/no-signal and lifecycle reconciliation worktree evidence' : 'review-only active/no-signal and lifecycle reconciliation worktree evidence scoped to ' . (string) $scope['argument'],
'repo_scope' => $scope,
'safety' => 'No worktrees or remote branches are deleted. Dirty and unpushed probes are evidence only.',
'budget' => null === $budget_context ? null : $this->summarize_worktree_loop_budget_context($budget_context, $budget_stopped),
'probe_cache' => $probe_cache['stats'],
Expand Down Expand Up @@ -484,6 +490,8 @@ private function build_active_no_signal_apply_pagination( array $pagination, str
$budget_label = $matches[1];
}

$scope = $this->normalize_worktree_operation_scope(isset($opts['repo']) ? (string) $opts['repo'] : '');
$scope_arg = is_wp_error($scope) ? '' : $this->worktree_operation_scope_cli_arg($scope);
$budget_arg = '' !== $budget_label ? ' --until-budget=' . $budget_label : '';
$dry_run_arg = $dry_run ? ' --dry-run' : '';
$limit = (int) ( $pagination['limit'] ?? 25 );
Expand All @@ -494,8 +502,9 @@ private function build_active_no_signal_apply_pagination( array $pagination, str
}

$pagination['next_command'] = sprintf(
'studio wp datamachine-code workspace worktree %s%s --limit=%d --offset=%d%s --format=json',
'studio wp datamachine-code workspace worktree %s%s%s --limit=%d --offset=%d%s --format=json',
$operation,
$scope_arg,
$dry_run_arg,
$limit,
$next_offset,
Expand Down
64 changes: 64 additions & 0 deletions inc/Workspace/WorkspaceCoreUtilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,70 @@ public function parse_handle( string $handle ): array {
return WorkspaceHandle::parse($handle)->to_array();
}

/**
* Normalize an optional worktree cleanup/reconciliation scope.
*
* @param string|null $scope Optional primary repo or worktree handle.
* @return array{argument: string, repo: string, handle: string|null}|null|\WP_Error
*/
protected function normalize_worktree_operation_scope( ?string $scope ): array|null|\WP_Error {
$scope = null === $scope ? '' : trim($scope);
if ( '' === $scope ) {
return null;
}

$parsed = $this->parse_handle($scope);
if ( '' === $parsed['repo'] || '' === $parsed['dir_name'] ) {
return new \WP_Error('invalid_worktree_scope', sprintf('Worktree cleanup scope "%s" must be a primary repo or worktree handle.', $scope), array( 'status' => 400 ));
}

return array(
'argument' => $scope,
'repo' => $parsed['repo'],
'handle' => $parsed['is_worktree'] ? $parsed['dir_name'] : null,
);
}

/**
* Whether an inventory/worktree row matches a normalized operation scope.
*
* @param array<string,mixed> $row Inventory or evidence row.
* @param array{argument: string, repo: string, handle: string|null}|null $scope Optional normalized scope.
* @return bool
*/
protected function worktree_row_matches_operation_scope( array $row, ?array $scope ): bool {
if ( null === $scope ) {
return true;
}

$handle = (string) ( $row['handle'] ?? $row['name'] ?? '' );
if ( null !== $scope['handle'] ) {
return $handle === $scope['handle'];
}

$repo = (string) ( $row['repo'] ?? '' );
if ( '' === $repo && '' !== $handle ) {
$repo = $this->parse_handle($handle)['repo'];
}

return $repo === $scope['repo'];
}

/**
* Build the positional CLI scope segment for continuation commands.
*
* @param array{argument: string, repo: string, handle: string|null}|null $scope Optional normalized scope.
* @return string
*/
protected function worktree_operation_scope_cli_arg( ?array $scope ): string {
if ( null === $scope ) {
return '';
}

$argument = (string) $scope['argument'];
return ' ' . ( preg_match('/^[A-Za-z0-9._@-]+$/', $argument) ? $argument : escapeshellarg($argument) );
}

/**
* Require file-operation callers to name a workspace handle explicitly.
*
Expand Down
Loading
Loading