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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user