From 5361afee1caaaee60eda3c59e2b33acd0f6008fb Mon Sep 17 00:00:00 2001 From: "homeboy-ci[bot]" <266378653+homeboy-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:33:24 -0400 Subject: [PATCH 1/2] Compact worktree cleanup operator JSON --- inc/Cli/Commands/WorkspaceCommand.php | 17 ++++++++- inc/Cli/WorkspaceCompactOutput.php | 14 +++++-- tests/workspace-compact-output.php | 53 +++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/inc/Cli/Commands/WorkspaceCommand.php b/inc/Cli/Commands/WorkspaceCommand.php index 6855b934..a90d45b9 100644 --- a/inc/Cli/Commands/WorkspaceCommand.php +++ b/inc/Cli/Commands/WorkspaceCommand.php @@ -5346,6 +5346,9 @@ private function render_worktree_metadata_reconciliation_result( array $result, private function render_worktree_active_no_signal_report_result( array $result, array $assoc_args ): void { $format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table'; if ( 'json' === $format ) { + if ( empty($assoc_args['verbose']) ) { + $result = WorkspaceCompactOutput::cleanup_result($result); + } $this->renderer()->json($result); return; } @@ -5436,6 +5439,9 @@ private function render_worktree_active_no_signal_report_result( array $result, private function render_worktree_active_no_signal_finalized_apply_result( array $result, array $assoc_args ): void { $format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table'; if ( 'json' === $format ) { + if ( empty($assoc_args['verbose']) ) { + $result = WorkspaceCompactOutput::cleanup_result($result); + } $this->renderer()->json($result); return; } @@ -5526,6 +5532,9 @@ private function render_worktree_active_no_signal_finalized_apply_result( array private function render_worktree_active_no_signal_equivalent_clean_apply_result( array $result, array $assoc_args ): void { $format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table'; if ( 'json' === $format ) { + if ( empty($assoc_args['verbose']) ) { + $result = WorkspaceCompactOutput::cleanup_result($result); + } $this->renderer()->json($result); return; } @@ -5616,6 +5625,9 @@ private function render_worktree_active_no_signal_equivalent_clean_apply_result( private function render_worktree_active_no_signal_merged_apply_result( array $result, array $assoc_args ): void { $format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table'; if ( 'json' === $format ) { + if ( empty($assoc_args['verbose']) ) { + $result = WorkspaceCompactOutput::cleanup_result($result); + } $this->renderer()->json($result); return; } @@ -5706,6 +5718,9 @@ private function render_worktree_active_no_signal_merged_apply_result( array $re private function render_worktree_active_no_signal_remote_clean_apply_result( array $result, array $assoc_args ): void { $format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table'; if ( 'json' === $format ) { + if ( empty($assoc_args['verbose']) ) { + $result = WorkspaceCompactOutput::cleanup_result($result); + } $this->renderer()->json($result); return; } @@ -5945,7 +5960,7 @@ private function flatten_artifact_cleanup_rows( array $rows ): array { private function render_worktree_bounded_cleanup_eligible_apply_result( array $result, array $assoc_args ): void { $format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table'; if ( 'json' === $format ) { - $report = ! empty($assoc_args['verbose']) ? $result : $this->compact_worktree_bounded_cleanup_eligible_apply_json($result); + $report = ! empty($assoc_args['verbose']) ? $result : WorkspaceCompactOutput::cleanup_result($result); $this->renderer()->json($report); return; } diff --git a/inc/Cli/WorkspaceCompactOutput.php b/inc/Cli/WorkspaceCompactOutput.php index 3eb32f81..e80cf603 100644 --- a/inc/Cli/WorkspaceCompactOutput.php +++ b/inc/Cli/WorkspaceCompactOutput.php @@ -15,8 +15,8 @@ class WorkspaceCompactOutput { public static function cleanup_result( array $result ): array { $summary = (array) ( $result['summary'] ?? array() ); - $candidates = (array) ( $result['candidates'] ?? $result['artifact_candidates'] ?? array() ); - $removed = (array) ( $result['removed'] ?? $result['removed_worktrees'] ?? $result['removed_artifacts'] ?? array() ); + $candidates = (array) ( $result['candidates'] ?? $result['artifact_candidates'] ?? $result['worktree_candidates'] ?? $result['rows'] ?? $result['planned'] ?? array() ); + $removed = (array) ( $result['removed'] ?? $result['removed_worktrees'] ?? $result['removed_artifacts'] ?? $result['written'] ?? array() ); $skipped = (array) ( $result['skipped'] ?? array() ); return self::filter_empty( @@ -144,7 +144,7 @@ public static function lock_result( array $result ): array { private static function row_counts( array $result ): array { $counts = array(); - foreach ( array( 'candidates', 'artifact_candidates', 'worktree_candidates', 'removed', 'removed_artifacts', 'removed_worktrees', 'skipped', 'written', 'proposals', 'pass_results' ) as $key ) { + foreach ( array( 'candidates', 'artifact_candidates', 'worktree_candidates', 'rows', 'planned', 'removed', 'removed_artifacts', 'removed_worktrees', 'written', 'skipped', 'proposals', 'pass_results' ) as $key ) { if ( isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) { $counts[ $key ] = count( $result[ $key ] ); } @@ -204,6 +204,14 @@ private static function next_commands( array $result, array $summary ): array { $commands[] = (string) $summary[ $field ]; } } + foreach ( array( 'pagination', 'continuation' ) as $bucket ) { + if ( ! empty($result[ $bucket ]['next_command']) ) { + $commands[] = (string) $result[ $bucket ]['next_command']; + } + if ( ! empty($summary[ $bucket ]['next_command']) ) { + $commands[] = (string) $summary[ $bucket ]['next_command']; + } + } $deduped = array(); $seen = array(); foreach ( $commands as $command ) { diff --git a/tests/workspace-compact-output.php b/tests/workspace-compact-output.php index c493b59f..8670daef 100644 --- a/tests/workspace-compact-output.php +++ b/tests/workspace-compact-output.php @@ -71,6 +71,59 @@ function compact_output_large_rows( int $count ): array { compact_output_assert(count((array) ( $cleanup['samples']['skipped'] ?? array() )) <= 5, 'Compact cleanup output must sample skipped rows.'); compact_output_assert(! empty($cleanup['next_commands']), 'Compact cleanup output must preserve next commands.'); +$active_report = WorkspaceCompactOutput::cleanup_result( + array( + 'success' => true, + 'mode' => 'active_no_signal_report', + 'rows' => $large_rows, + 'summary' => array( + 'total_active_no_signal' => 40, + 'inspected' => 40, + 'by_suggested_action' => array( 'remote_tracking_clean' => 40 ), + ), + 'pagination' => array( + 'total' => 80, + 'offset' => 0, + 'limit' => 40, + 'next_offset' => 40, + 'next_command' => 'studio wp datamachine-code workspace worktree active-no-signal-report --limit=40 --offset=40 --format=json', + ), + ) +); + +compact_output_assert(! isset($active_report['rows']), 'Compact active/no-signal report must omit full rows array.'); +compact_output_assert(40 === ( $active_report['row_counts']['rows'] ?? null ), 'Compact active/no-signal report must preserve row count.'); +compact_output_assert(40 === ( $active_report['pagination']['next_offset'] ?? null ), 'Compact active/no-signal report must preserve pagination.'); +compact_output_assert(in_array('studio wp datamachine-code workspace worktree active-no-signal-report --limit=40 --offset=40 --format=json', (array) ( $active_report['next_commands'] ?? array() ), true), 'Compact active/no-signal report must expose next page command.'); + +$active_apply = WorkspaceCompactOutput::cleanup_result( + array( + 'success' => true, + 'mode' => 'active_no_signal_remote_clean_apply', + 'dry_run' => true, + 'planned' => $large_rows, + 'written' => $large_rows, + 'skipped' => $large_rows, + 'summary' => array( + 'inspected' => 40, + 'planned' => 40, + 'written' => 40, + 'skipped' => 40, + 'skipped_by_reason' => array( 'not_remote_tracking_clean' => 40 ), + ), + 'pagination' => array( + 'next_command' => 'studio wp datamachine-code workspace worktree active-no-signal-remote-clean-apply --dry-run --limit=40 --offset=40 --format=json', + ), + ) +); + +compact_output_assert(! isset($active_apply['planned']), 'Compact active/no-signal apply must omit full planned array.'); +compact_output_assert(! isset($active_apply['written']), 'Compact active/no-signal apply must omit full written array.'); +compact_output_assert(40 === ( $active_apply['row_counts']['planned'] ?? null ), 'Compact active/no-signal apply must preserve planned count.'); +compact_output_assert(40 === ( $active_apply['row_counts']['written'] ?? null ), 'Compact active/no-signal apply must preserve written count.'); +compact_output_assert(40 === ( $active_apply['blockers']['not_remote_tracking_clean']['count'] ?? null ), 'Compact active/no-signal apply must preserve blocker counts from summary.'); +compact_output_assert(in_array('studio wp datamachine-code workspace worktree active-no-signal-remote-clean-apply --dry-run --limit=40 --offset=40 --format=json', (array) ( $active_apply['next_commands'] ?? array() ), true), 'Compact active/no-signal apply must expose next page command.'); + $locks = WorkspaceCompactOutput::lock_result( array( 'active' => 2, From cfc6881eec3e76cef78fb018da5b924600819067 Mon Sep 17 00:00:00 2001 From: "homeboy-ci[bot]" <266378653+homeboy-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:41:10 -0400 Subject: [PATCH 2/2] Remove obsolete bounded cleanup formatter --- inc/Cli/Commands/WorkspaceCommand.php | 232 -------------------------- 1 file changed, 232 deletions(-) diff --git a/inc/Cli/Commands/WorkspaceCommand.php b/inc/Cli/Commands/WorkspaceCommand.php index a90d45b9..ef72d6b1 100644 --- a/inc/Cli/Commands/WorkspaceCommand.php +++ b/inc/Cli/Commands/WorkspaceCommand.php @@ -6198,57 +6198,6 @@ private function render_worktree_cleanup_eligible_drain_result( array $result, a } } - /** - * Compact bounded cleanup JSON for chat/operator output. - * - * @param array $result Full bounded apply result. - * @return array - */ - private function compact_worktree_bounded_cleanup_eligible_apply_json( array $result ): array { - $summary = (array) ( $result['summary'] ?? array() ); - $candidates = (array) ( $result['candidates'] ?? array() ); - $removed = (array) ( $result['removed'] ?? array() ); - $skipped = (array) ( $result['skipped'] ?? array() ); - $buckets = $this->build_cleanup_blocker_buckets($skipped); - $actions = $this->build_cleanup_next_actions($buckets, (array) ( $summary['skipped_next_commands'] ?? array() )); - - $compact_summary = array_merge( - $summary, - array( - 'processed' => (int) ( $summary['processed'] ?? $summary['inspected'] ?? count($candidates) ), - 'would_remove' => (int) ( $summary['would_remove'] ?? ( ! empty($result['dry_run']) ? count($candidates) : 0 ) ), - 'removed' => (int) ( $summary['removed'] ?? count($removed) ), - 'skipped' => max( (int) ( $summary['skipped'] ?? 0 ), count($skipped) ), - 'bytes_reclaimed' => (int) ( $summary['bytes_reclaimed'] ?? 0 ), - 'skipped_by_reason' => array_map(fn( $bucket ) => (int) ( $bucket['count'] ?? 0 ), $buckets), - 'blocker_bucket_count' => count($buckets), - ) - ); - - $report = array( - 'success' => (bool) ( $result['success'] ?? true ), - 'mode' => (string) ( $result['mode'] ?? 'bounded_cleanup_eligible_apply' ), - 'dry_run' => ! empty($result['dry_run']), - 'destructive' => ! empty($result['destructive']), - 'workspace_path' => $result['workspace_path'] ?? null, - 'generated_at' => $result['generated_at'] ?? null, - 'summary' => $compact_summary, - 'blocker_buckets' => $buckets, - 'next_actions' => $actions, - 'active_no_signal_triage' => (array) ( $result['active_no_signal_triage'] ?? array() ), - 'candidates' => $this->compact_cleanup_rows($candidates, 25), - 'removed' => $this->compact_cleanup_rows($removed, 25), - 'continuation' => $this->compact_cleanup_continuation( (array) ( $result['continuation'] ?? $result['pagination'] ?? array() ) ), - 'evidence' => $this->compact_cleanup_evidence( (array) ( $result['evidence'] ?? array() ), $skipped ), - ); - - if ( ! empty($result['job_backed']) ) { - $report['job_backed'] = true; - } - - return array_filter($report, fn( $value ) => null !== $value); - } - /** * Render concise active/no-signal triage preview from bounded cleanup output. * @@ -6298,187 +6247,6 @@ private function render_active_no_signal_triage_preview( array $preview ): void } } - /** - * Build skipped blocker buckets with bounded examples. - * - * @param array> $rows Skipped rows. - * @return array> - */ - private function build_cleanup_blocker_buckets( array $rows ): array { - $buckets = array(); - foreach ( $rows as $row ) { - $reason_code = (string) ( $row['reason_code'] ?? 'unknown' ); - if ( ! isset($buckets[ $reason_code ]) ) { - $buckets[ $reason_code ] = array( - 'count' => 0, - 'examples' => array(), - 'reason' => (string) ( $row['reason'] ?? '' ), - ); - } - ++$buckets[ $reason_code ]['count']; - if ( count($buckets[ $reason_code ]['examples']) < 3 ) { - $buckets[ $reason_code ]['examples'][] = $this->compact_cleanup_row($row); - } - } - - ksort($buckets); - return $buckets; - } - - /** - * Build compact next actions for unresolved blocker classes. - * - * @param array> $buckets Blocker buckets. - * @param array> $commands Existing cleanup command hints. - * @return array> - */ - private function build_cleanup_next_actions( array $buckets, array $commands ): array { - $by_reason = array(); - foreach ( $commands as $command ) { - $reason_code = (string) ( $command['reason_code'] ?? '' ); - if ( '' !== $reason_code ) { - $by_reason[ $reason_code ] = $command; - } - } - - $defaults = array( - 'active_no_signal' => array( - 'command' => 'studio wp datamachine-code workspace worktree active-no-signal-report --limit=25 --offset=0 --format=json', - 'destructive' => false, - ), - 'needs_metadata_reconcile' => array( - 'command' => 'studio wp datamachine-code workspace worktree reconcile-metadata --dry-run --limit=25 --offset=0 --until-budget=30s --format=json', - 'destructive' => false, - ), - 'lifecycle_reconciliation_candidate' => array( - 'command' => 'studio wp datamachine-code workspace worktree cleanup --dry-run --format=json', - 'destructive' => false, - ), - 'dirty_worktree' => array( - 'command' => 'git -C status --short --branch --untracked-files=normal', - 'destructive' => false, - ), - 'unpushed_commits' => array( - 'command' => 'git -C log --oneline --decorate @{u}..HEAD', - 'destructive' => false, - ), - 'stale_worktree_marker' => array( - 'command' => 'git -C worktree prune --dry-run --verbose', - 'destructive' => false, - ), - 'primary_missing' => array( - 'command' => 'studio wp datamachine-code workspace show ', - 'destructive' => false, - ), - 'submodule_worktree' => array( - 'command' => 'git -C submodule status --recursive', - 'destructive' => false, - ), - 'remove_timeout' => array( - 'command' => 'studio wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --limit=25 --remove-timeout=', - 'destructive' => true, - ), - ); - - $actions = array(); - foreach ( $buckets as $reason_code => $bucket ) { - $hint = $by_reason[ $reason_code ] ?? $defaults[ $reason_code ] ?? array( - 'command' => 'Re-run with --verbose --format=json and inspect this reason_code before retrying cleanup.', - 'destructive' => false, - ); - $actions[] = array( - 'reason_code' => $reason_code, - 'count' => (int) ( $bucket['count'] ?? 0 ), - 'command' => (string) ( $hint['command'] ?? '' ), - 'alternative' => (string) ( $hint['alternative'] ?? '' ), - 'destructive' => ! empty($hint['destructive']), - ); - } - - return $actions; - } - - /** - * Compact a bounded cleanup continuation without full handle lists. - * - * @param array $continuation Continuation payload. - * @return array - */ - private function compact_cleanup_continuation( array $continuation ): array { - if ( empty($continuation) ) { - return array(); - } - - $handles = array_values(array_filter(array_map('strval', (array) ( $continuation['remaining_handles'] ?? array() )))); - unset($continuation['remaining_handles']); - if ( ! isset($continuation['remaining_total']) && isset($continuation['total']) ) { - $continuation['remaining_total'] = (int) $continuation['total']; - } - $continuation['remaining_handles_count'] = count($handles); - $continuation['remaining_handles_examples'] = array_slice($handles, 0, 10); - if ( count($handles) > 10 ) { - $continuation['remaining_handles_truncated'] = true; - } - - return $continuation; - } - - /** - * Compact evidence while removing full skipped handle lists. - * - * @param array $evidence Evidence payload. - * @param array> $skipped Skipped rows. - * @return array - */ - private function compact_cleanup_evidence( array $evidence, array $skipped ): array { - $skipped_handles = array_values(array_filter(array_map(fn( $row ) => (string) ( $row['handle'] ?? '' ), $skipped))); - unset($evidence['skipped_handles']); - $evidence['skipped_handles_count'] = count($skipped_handles); - $evidence['skipped_handles_examples'] = array_slice($skipped_handles, 0, 10); - if ( count($skipped_handles) > 10 ) { - $evidence['skipped_handles_truncated'] = true; - } - $evidence['full_detail_hint'] = 'Re-run with --verbose --format=json for full skipped rows and handle lists.'; - - return $evidence; - } - - /** - * Compact cleanup rows to the fields operators need first. - * - * @param array> $rows Rows to compact. - * @param int $limit Maximum rows. - * @return array> - */ - private function compact_cleanup_rows( array $rows, int $limit ): array { - return array_map(fn( $row ) => $this->compact_cleanup_row( (array) $row ), array_slice($rows, 0, $limit)); - } - - /** - * Compact one cleanup row. - * - * @param array $row Cleanup row. - * @return array - */ - private function compact_cleanup_row( array $row ): array { - $compact = array( - 'handle' => $row['handle'] ?? null, - 'repo' => $row['repo'] ?? null, - 'branch' => $row['branch'] ?? null, - 'reason_code' => $row['reason_code'] ?? $row['signal'] ?? null, - 'reason' => isset($row['reason']) ? $this->shorten_cleanup_reason( (string) $row['reason'] ) : null, - 'path' => $row['path'] ?? null, - ); - - foreach ( array( 'size_bytes', 'artifact_size_bytes', 'dirty', 'unpushed', 'created_at', 'liveness', 'pr_url' ) as $field ) { - if ( array_key_exists($field, $row) ) { - $compact[ $field ] = $row[ $field ]; - } - } - - return array_filter($compact, fn( $value ) => null !== $value && '' !== $value && array() !== $value); - } - private function render_worktree_emergency_cleanup_result( array $result, array $assoc_args ): void { $format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table'; if ( 'json' === $format ) {