diff --git a/features/plan_cli_coverage_r2.feature b/features/plan_cli_coverage_r2.feature index 8b269f7e9..f3e2e8e6c 100644 --- a/features/plan_cli_coverage_r2.feature +++ b/features/plan_cli_coverage_r2.feature @@ -178,8 +178,11 @@ Feature: Plan CLI coverage round 2 – remaining uncovered lines And a mocked checkpoint service for r2cov rollback that succeeds When I invoke r2cov plan rollback with --yes Then the r2cov command should exit normally + And the r2cov output should contain "Rollback Summary" + And the r2cov output should contain "Changes Reverted" + And the r2cov output should contain "Impact" + And the r2cov output should contain "Post-Rollback State" And the r2cov output should contain "Rollback complete" - And the r2cov output should contain "Restored files" Scenario: Rollback plan succeeds in json format Given a r2cov CLI runner diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index db8f7d3f8..649b2c838 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -3422,16 +3422,89 @@ def rollback_plan( if fmt != OutputFormat.RICH.value: console.print(format_output(data, fmt)) else: + # Rollback Summary panel + label = getattr(result, "label", None) or "" + label_line = f"\n[bold]Label:[/bold] {label}" if label else "" console.print( - f"[green]Rollback complete.[/green]\n" - f" Plan: {plan_id}\n" - f" Checkpoint: {result.from_checkpoint_id}\n" - f" Restored files: {result.restored_files_count}\n" - f" Elapsed: {elapsed:.3f}s\n" - f" Changed paths:" + Panel( + f"[bold]Plan:[/bold] {plan_id}\n" + f"[bold]Checkpoint:[/bold] {result.from_checkpoint_id}" + f"{label_line}\n" + f"[bold]Files:[/bold] {result.restored_files_count} reverted", + title="Rollback Summary", + expand=False, + ) ) - for path in result.changed_paths: - console.print(f" {path}") + + # Changes Reverted table + changes_table = Table(show_header=True, expand=False) + changes_table.add_column("File", style="cyan") + changes_table.add_column("Action", style="green") + changes_reverted = getattr(result, "changes_reverted", None) + if changes_reverted and isinstance(changes_reverted, list): + for entry in changes_reverted: + if isinstance(entry, dict): + changes_table.add_row( + str(entry.get("file", "")), + str(entry.get("action", "restored")), + ) + else: + changes_table.add_row(str(entry), "restored") + else: + for path in result.changed_paths: + changes_table.add_row(str(path), "restored") + console.print(Panel(changes_table, title="Changes Reverted", expand=False)) + + # Impact panel + child_plans_invalidated = getattr(result, "child_plans_invalidated", None) + sandbox_state = getattr(result, "sandbox", None) or ( + f"restored to {result.from_checkpoint_id}" + ) + decisions_after_cp = getattr(result, "decisions_after_cp", None) + tool_calls_after_cp = getattr(result, "tool_calls_after_cp", None) + impact_lines = [] + if child_plans_invalidated is not None: + impact_lines.append( + f"[bold]Child Plans Invalidated:[/bold] {child_plans_invalidated}" + ) + impact_lines.append(f"[bold]Sandbox:[/bold] {sandbox_state}") + if decisions_after_cp is not None: + impact_lines.append( + f"[bold]Decisions After CP:[/bold] {decisions_after_cp} discarded" + ) + if tool_calls_after_cp is not None: + impact_lines.append( + f"[bold]Tool Calls After CP:[/bold] {tool_calls_after_cp} undone" + ) + console.print( + Panel( + "\n".join(impact_lines), + title="Impact", + expand=False, + ) + ) + + # Post-Rollback State panel + phase = getattr(result, "phase", None) or "execute" + state = getattr(result, "state", None) or "queued" + checkpoints_remaining = getattr(result, "checkpoints_remaining", None) + post_rollback_lines = [ + f"[bold]Phase:[/bold] {phase}", + f"[bold]State:[/bold] {state}", + ] + if checkpoints_remaining is not None: + post_rollback_lines.append( + f"[bold]Checkpoints Remaining:[/bold] {checkpoints_remaining}" + ) + console.print( + Panel( + "\n".join(post_rollback_lines), + title="Post-Rollback State", + expand=False, + ) + ) + + console.print("[green]\u2713 OK[/green] Rollback complete") except BusinessRuleViolation as e: console.print(f"[red]Rollback blocked:[/red] {e.message}")