feat(plans): implement checkpoint listing and management CLI commands

- Add 'agents plan checkpoint list <plan-id>' command to list all checkpoints for a plan
- Add 'agents plan checkpoint delete <checkpoint-id>' command to delete a checkpoint
- Both commands support multiple output formats (rich, table, json, yaml)
- List command shows checkpoint ID, creation time, type, reason, and size
- Delete command requires confirmation unless --yes flag is provided
- Implement BDD tests for checkpoint CLI commands
- Update CHANGELOG.md with feature description

Closes #8559
This commit is contained in:
2026-04-13 22:12:40 +00:00
parent c5afe1f3a7
commit d20dd9b11d
3 changed files with 334 additions and 0 deletions
+2
View File
@@ -7,6 +7,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added
- **Checkpoint Listing and Management CLI Commands** (#8559): Implemented `agents plan checkpoint list <plan-id>` and `agents plan checkpoint delete <checkpoint-id>` commands for the v3.3.0 milestone. The list command displays all checkpoints for a plan with ID, timestamp, type, and reason. The delete command removes checkpoints with confirmation. Both commands support multiple output formats (rich, table, json, yaml).
- **TDD Issue-Capture Test Activation** (#7025): Replaced 234 bare `@skip` tags
across 82 Behave feature files with the correct `@tdd_expected_fail @tdd_issue
@tdd_issue_<N>` tag system. Scenarios whose referenced bugs were already fixed
+124
View File
@@ -0,0 +1,124 @@
Feature: Checkpoint CLI commands for listing and managing checkpoints
Background:
Given a CleverAgents environment is initialized
And the checkpoint service is available
And a test plan with ID "01HXM8C2ZK4Q7C2B3F2R4VYV6J" exists
Scenario: List checkpoints for a plan with no checkpoints
When I run "agents plan checkpoint list 01HXM8C2ZK4Q7C2B3F2R4VYV6J"
Then the output contains "No checkpoints found"
And the exit code is 0
Scenario: List checkpoints for a plan with multiple checkpoints
Given the plan has 3 checkpoints:
| checkpoint_id | reason | created_at |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | Initial snapshot | 2026-04-13T10:00:00 |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6B | After step 1 | 2026-04-13T10:05:00 |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6C | After step 2 | 2026-04-13T10:10:00 |
When I run "agents plan checkpoint list 01HXM8C2ZK4Q7C2B3F2R4VYV6J"
Then the output contains "01HXM8C2ZK4Q7C2B3F2R4VYV6A"
And the output contains "01HXM8C2ZK4Q7C2B3F2R4VYV6B"
And the output contains "01HXM8C2ZK4Q7C2B3F2R4VYV6C"
And the output contains "Initial snapshot"
And the output contains "After step 1"
And the output contains "After step 2"
And the exit code is 0
Scenario: List checkpoints with JSON format
Given the plan has 2 checkpoints:
| checkpoint_id | reason | created_at |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | Checkpoint 1 | 2026-04-13T10:00:00 |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6B | Checkpoint 2 | 2026-04-13T10:05:00 |
When I run "agents plan checkpoint list --format json 01HXM8C2ZK4Q7C2B3F2R4VYV6J"
Then the output is valid JSON
And the JSON contains "checkpoint_count" with value 2
And the JSON contains "checkpoints" array with 2 items
And the exit code is 0
Scenario: List checkpoints with YAML format
Given the plan has 1 checkpoint:
| checkpoint_id | reason | created_at |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | Test CP | 2026-04-13T10:00:00 |
When I run "agents plan checkpoint list --format yaml 01HXM8C2ZK4Q7C2B3F2R4VYV6J"
Then the output is valid YAML
And the YAML contains "plan_id: 01HXM8C2ZK4Q7C2B3F2R4VYV6J"
And the exit code is 0
Scenario: List checkpoints with table format
Given the plan has 2 checkpoints:
| checkpoint_id | reason | created_at |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | Checkpoint 1 | 2026-04-13T10:00:00 |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6B | Checkpoint 2 | 2026-04-13T10:05:00 |
When I run "agents plan checkpoint list --format table 01HXM8C2ZK4Q7C2B3F2R4VYV6J"
Then the output contains "Checkpoint ID"
And the output contains "Created"
And the output contains "Type"
And the output contains "Reason"
And the exit code is 0
Scenario: List checkpoints with invalid plan ID
When I run "agents plan checkpoint list invalid-plan-id"
Then the output contains "not found"
And the exit code is non-zero
Scenario: Delete a checkpoint with confirmation
Given the plan has 1 checkpoint:
| checkpoint_id | reason | created_at |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | Test CP | 2026-04-13T10:00:00 |
When I run "agents plan checkpoint delete --yes 01HXM8C2ZK4Q7C2B3F2R4VYV6A"
Then the output contains "deleted successfully"
And the exit code is 0
And the checkpoint is no longer in the database
Scenario: Delete a checkpoint without --yes flag (interactive)
Given the plan has 1 checkpoint:
| checkpoint_id | reason | created_at |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | Test CP | 2026-04-13T10:00:00 |
When I run "agents plan checkpoint delete 01HXM8C2ZK4Q7C2B3F2R4VYV6A" with input "y"
Then the output contains "deleted successfully"
And the exit code is 0
Scenario: Cancel checkpoint deletion
Given the plan has 1 checkpoint:
| checkpoint_id | reason | created_at |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | Test CP | 2026-04-13T10:00:00 |
When I run "agents plan checkpoint delete 01HXM8C2ZK4Q7C2B3F2R4VYV6A" with input "n"
Then the output contains "Deletion cancelled"
And the exit code is non-zero
And the checkpoint still exists in the database
Scenario: Delete non-existent checkpoint
When I run "agents plan checkpoint delete 01HXM8C2ZK4Q7C2B3F2R4VYV6A --yes"
Then the output contains "not found"
And the exit code is non-zero
Scenario: Delete checkpoint with JSON format output
Given the plan has 1 checkpoint:
| checkpoint_id | reason | created_at |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | Test CP | 2026-04-13T10:00:00 |
When I run "agents plan checkpoint delete --format json --yes 01HXM8C2ZK4Q7C2B3F2R4VYV6A"
Then the output is valid JSON
And the JSON contains "status: deleted"
And the JSON contains "checkpoint_id: 01HXM8C2ZK4Q7C2B3F2R4VYV6A"
And the exit code is 0
Scenario: List checkpoints shows relative time
Given the plan has 1 checkpoint created 5 minutes ago:
| checkpoint_id | reason |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | Test CP |
When I run "agents plan checkpoint list 01HXM8C2ZK4Q7C2B3F2R4VYV6J"
Then the output contains "5 minute"
And the exit code is 0
Scenario: List checkpoints with checkpoint type information
Given the plan has checkpoints with different types:
| checkpoint_id | type | reason |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6A | pre_write | Before write |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6B | post_step | After step |
| 01HXM8C2ZK4Q7C2B3F2R4VYV6C | manual | Manual save |
When I run "agents plan checkpoint list 01HXM8C2ZK4Q7C2B3F2R4VYV6J"
Then the output contains "pre_write"
And the output contains "post_step"
And the output contains "manual"
And the exit code is 0
+208
View File
@@ -16,6 +16,8 @@ plan lifecycle.
| ``agents plan diff`` | Show ChangeSet as unified diff |
| ``agents plan artifacts`` | Show ChangeSet ID, sandbox refs, summary|
| ``agents plan errors`` | Show error decisions + recovery hints |
| ``agents plan checkpoint list`` | List checkpoints for a plan |
| ``agents plan checkpoint delete`` | Delete a checkpoint |
"""
from __future__ import annotations
@@ -4541,3 +4543,209 @@ def tree_decisions_cmd(
expand=False,
)
)
# Create checkpoint subcommand app
checkpoint_app = typer.Typer(help="Checkpoint management commands")
@checkpoint_app.command("list")
def checkpoint_list_cmd(
plan_id: Annotated[
str,
typer.Argument(help="Plan ID to list checkpoints for"),
],
fmt: Annotated[
str,
typer.Option(
"--format",
"-f",
help=_FORMAT_HELP,
),
] = "rich",
) -> None:
"""List all checkpoints for a plan.
Shows checkpoint ID, creation timestamp, and state summary for each
checkpoint associated with the given plan.
Examples:
agents plan checkpoint list 01HXM8C2ZK4Q7C2B3F2R4VYV6J
agents plan checkpoint list --format json 01HXM8C2ZK4Q7C2B3F2R4VYV6J
"""
from cleveragents.application.container import get_container
# Validate plan ID is a ULID
_validate_plan_ulid(plan_id)
container = get_container()
svc = container.checkpoint_service()
try:
checkpoints = svc.list_checkpoints(plan_id)
except Exception as e:
console.print(f"[red]Error listing checkpoints:[/red] {e}")
raise typer.Abort() from e
if not checkpoints:
console.print(f"No checkpoints found for plan '{plan_id}'.")
return
if fmt in (OutputFormat.JSON, OutputFormat.YAML):
# Format as structured data
data = {
"plan_id": plan_id,
"checkpoint_count": len(checkpoints),
"checkpoints": [
{
"checkpoint_id": cp.checkpoint_id,
"created_at": cp.created_at.isoformat(),
"checkpoint_type": cp.checkpoint_type,
"reason": cp.metadata.reason,
"source_tool": cp.metadata.source_tool,
"phase": cp.metadata.phase,
"size_bytes": cp.size_bytes,
"decision_id": cp.decision_id,
}
for cp in checkpoints
],
}
console.print(format_output(data, fmt))
elif fmt == OutputFormat.TABLE:
# Format as table
table = Table(title=f"Checkpoints for Plan {plan_id}", show_header=True)
table.add_column("Checkpoint ID", max_width=26)
table.add_column("Created", max_width=20)
table.add_column("Type")
table.add_column("Reason", max_width=30)
table.add_column("Tool", max_width=20)
table.add_column("Size (bytes)")
for cp in checkpoints:
relative_time = _format_relative_time(cp.created_at)
table.add_row(
cp.checkpoint_id,
relative_time,
cp.checkpoint_type,
cp.metadata.reason[:30] if cp.metadata.reason else "-",
cp.metadata.source_tool[:20] if cp.metadata.source_tool else "-",
str(cp.size_bytes) if cp.size_bytes is not None else "-",
)
console.print(table)
else:
# Rich or plain format
from rich.panel import Panel
checkpoint_lines = []
for i, cp in enumerate(checkpoints, 1):
relative_time = _format_relative_time(cp.created_at)
reason_display = (
f"'{cp.metadata.reason}'" if cp.metadata.reason else "(no reason)"
)
checkpoint_lines.append(
f"[bold]{i}.[/bold] {cp.checkpoint_id}\n"
f" Created: {relative_time}\n"
f" Type: {cp.checkpoint_type}\n"
f" Reason: {reason_display}"
)
console.print(
Panel(
"\n\n".join(checkpoint_lines),
title=f"Checkpoints for Plan {plan_id}",
expand=False,
)
)
@checkpoint_app.command("delete")
def checkpoint_delete_cmd(
checkpoint_id: Annotated[
str,
typer.Argument(help="Checkpoint ID to delete"),
],
yes: Annotated[
bool,
typer.Option(
"--yes",
"-y",
help="Skip confirmation prompt",
),
] = False,
fmt: Annotated[
str,
typer.Option(
"--format",
"-f",
help=_FORMAT_HELP,
),
] = "rich",
) -> None:
"""Delete a specific checkpoint.
Removes the checkpoint from the database. Requires confirmation unless
--yes is provided.
Examples:
agents plan checkpoint delete 01HXM8C2ZK4Q7C2B3F2R4VYV6J
agents plan checkpoint delete --yes 01HXM8C2ZK4Q7C2B3F2R4VYV6J
"""
from cleveragents.application.container import get_container
from cleveragents.core.exceptions import ResourceNotFoundError as RNF
container = get_container()
svc = container.checkpoint_service()
# Fetch checkpoint metadata for confirmation prompt
try:
checkpoint = svc.get_checkpoint(checkpoint_id)
except RNF:
console.print(f"[red]Error:[/red] Checkpoint '{checkpoint_id}' not found.")
raise typer.Abort() from None
except Exception as e:
console.print(f"[red]Error fetching checkpoint:[/red] {e}")
raise typer.Abort() from e
if not yes:
relative_time = _format_relative_time(checkpoint.created_at)
reason_display = (
f"'{checkpoint.metadata.reason}'"
if checkpoint.metadata.reason
else "(no reason)"
)
prompt_text = (
f"\nDelete checkpoint {checkpoint_id}\n"
f" Created: {relative_time}\n"
f" Reason: {reason_display}\n"
f" Plan: {checkpoint.plan_id}\n\n"
f"This action cannot be undone. Continue?"
)
confirm = typer.confirm(prompt_text)
if not confirm:
console.print("[yellow]Deletion cancelled.[/yellow]")
raise typer.Abort()
try:
svc.delete_checkpoint(checkpoint_id)
except Exception as e:
console.print(f"[red]Error deleting checkpoint:[/red] {e}")
raise typer.Abort() from e
# Output result
if fmt in (OutputFormat.JSON, OutputFormat.YAML):
data = {
"checkpoint_id": checkpoint_id,
"status": "deleted",
"plan_id": checkpoint.plan_id,
}
console.print(format_output(data, fmt))
else:
console.print(
f"[green]✓[/green] Checkpoint {checkpoint_id} deleted successfully."
)
# Register checkpoint subcommand app
app.add_typer(checkpoint_app, name="checkpoint", help="Checkpoint management")