#!/usr/bin/env python3 """ CODITECT Agent Checkpoint Command (H.8.1.8)
CLI wrapper for the CheckpointService (ADR-108) enabling manual checkpoint creation for autonomous agent loops.
Usage:
# Create checkpoint
python3 checkpoint-command.py --task-id H.8.1.8 --phase implementing
--completed "API endpoint" --pending "Unit tests"
# Show latest checkpoint
python3 checkpoint-command.py --task-id H.8.1.8 --show
# List checkpoint history
python3 checkpoint-command.py --task-id H.8.1.8 --history
# Generate continuation prompt
python3 checkpoint-command.py --task-id H.8.1.8 --resume
Author: CODITECT Framework Version: 2.0.0 Created: January 27, 2026 Updated: February 6, 2026 Task Reference: H.8.1.8, H.12.5 ADR Reference: ADR-108-agent-checkpoint-handoff-protocol.md, ADR-159 (Multi-Tenant) """
import argparse import json import os import sys from datetime import datetime, timezone from pathlib import Path
Add parent directories to path for imports
SCRIPT_DIR = Path(file).resolve().parent CORE_DIR = SCRIPT_DIR.parent.parent sys.path.insert(0, str(CORE_DIR)) sys.path.insert(0, str(SCRIPT_DIR))
try: from ralph_wiggum.checkpoint_protocol import ( CheckpointService, ExecutionState, ExecutionPhase, ContextSummary, CheckpointMetrics, HandoffProtocol, CheckpointNotFoundError, ) except ImportError as e: print(f"Error importing checkpoint_protocol: {e}") print("Ensure ralph_wiggum/checkpoint_protocol.py exists in scripts/core/") sys.exit(1)
ADR-159: Multi-tenant scope resolution (optional - fail gracefully)
try: from scope import resolve_scope, add_scope_args, scope_from_args HAS_SCOPE = True except ImportError: HAS_SCOPE = False
def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser( description="CODITECT Agent Checkpoint Command (ADR-108)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples:
Create checkpoint for current task
%(prog)s --task-id H.8.1.8 --phase implementing
Create checkpoint with completed items
%(prog)s --task-id A.9.1.3 --completed "API endpoint" "Unit tests"
Show latest checkpoint
%(prog)s --task-id H.8.1.8 --show
List checkpoint history
%(prog)s --task-id H.8.1.8 --history
Generate continuation prompt for handoff
%(prog)s --task-id H.8.1.8 --resume """ )
# Required
parser.add_argument(
"--task-id",
required=True,
help="Task identifier (e.g., H.8.1.8)"
)
# Checkpoint creation options
parser.add_argument(
"--agent-id",
help="Agent identifier (auto-generated if not provided)"
)
parser.add_argument(
"--iteration",
type=int,
default=1,
help="Loop iteration number (default: auto-increment from latest)"
)
parser.add_argument(
"--phase",
choices=["planning", "implementing", "testing", "reviewing", "handoff", "complete"],
default="implementing",
help="Execution phase (default: implementing)"
)
parser.add_argument(
"--completed",
nargs="*",
default=[],
help="Items completed in this iteration"
)
parser.add_argument(
"--pending",
nargs="*",
default=[],
help="Items still pending"
)
parser.add_argument(
"--blocked",
nargs="*",
default=[],
help="Items blocked by dependencies"
)
parser.add_argument(
"--focus",
help="Current work focus"
)
parser.add_argument(
"--decision",
action="append",
default=[],
help="Key decision made (can specify multiple)"
)
# Query options
parser.add_argument(
"--show",
action="store_true",
help="Display latest checkpoint for task"
)
parser.add_argument(
"--history",
action="store_true",
help="List checkpoint history"
)
parser.add_argument(
"--history-limit",
type=int,
default=10,
help="Number of checkpoints to show in history (default: 10)"
)
parser.add_argument(
"--resume",
action="store_true",
help="Generate continuation prompt from latest checkpoint"
)
# Output options
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format"
)
parser.add_argument(
"--quiet",
action="store_true",
help="Minimal output"
)
# ADR-159: Project scoping
if HAS_SCOPE:
add_scope_args(parser)
else:
parser.add_argument(
"--project",
help="Project scope for checkpoint (ADR-159). Auto-detected from CWD if not set."
)
return parser.parse_args()
def generate_agent_id(): """Generate a unique agent identifier.""" timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") return f"claude-{timestamp}"
def format_checkpoint_summary(checkpoint, include_prompt=False): """Format checkpoint as human-readable summary.""" meta = checkpoint.metadata state = checkpoint.execution_state context = checkpoint.context_summary metrics = checkpoint.metrics
lines = [
f"Checkpoint: {meta.checkpoint_id[:8]}...",
f" Task: {meta.task_id}",
f" Agent: {meta.agent_id}",
f" Iteration: {meta.iteration}",
f" Phase: {state.phase}",
f" Timestamp: {meta.timestamp}",
"",
"Execution State:",
f" Completed: {len(state.completed_items)} items",
f" Pending: {len(state.pending_items)} items",
f" Blocked: {len(state.blocked_items)} items",
f" Focus: {state.current_focus or 'None'}",
]
if state.completed_items:
lines.append("")
lines.append("Completed Items:")
for item in state.completed_items:
lines.append(f" - {item}")
if state.pending_items:
lines.append("")
lines.append("Pending Items:")
for item in state.pending_items:
lines.append(f" - {item}")
if state.blocked_items:
lines.append("")
lines.append("Blocked Items:")
for item in state.blocked_items:
lines.append(f" - {item}")
if context.key_decisions:
lines.append("")
lines.append("Key Decisions:")
for decision in context.key_decisions:
lines.append(f" - {decision}")
if metrics.files_modified:
lines.append("")
lines.append(f"Metrics:")
lines.append(f" Tokens: {metrics.tokens_consumed}")
lines.append(f" Tools: {metrics.tools_invoked}")
lines.append(f" Files: {len(metrics.files_modified)}")
lines.append("")
lines.append(f"Integrity: {checkpoint.compliance.hash[:16]}...")
if include_prompt and checkpoint.recovery.continuation_prompt:
lines.append("")
lines.append("=" * 60)
lines.append("CONTINUATION PROMPT FOR NEXT AGENT:")
lines.append("=" * 60)
lines.append(checkpoint.recovery.continuation_prompt)
return "\n".join(lines)
def create_checkpoint(args, service): """Create a new checkpoint.""" # Auto-generate agent ID if not provided agent_id = args.agent_id or generate_agent_id()
# Check for existing checkpoints to auto-increment iteration
iteration = args.iteration
if iteration == 1:
latest = service.get_latest_checkpoint(args.task_id)
if latest:
iteration = latest.metadata.iteration + 1
# Build execution state
execution_state = ExecutionState(
phase=args.phase,
completed_items=args.completed or [],
pending_items=args.pending or [],
blocked_items=args.blocked or [],
current_focus=args.focus or "",
)
# Build context summary
context_summary = ContextSummary(
key_decisions=args.decision or [],
)
# Get previous checkpoint ID before creating new one (for linking)
previous_checkpoint_id = None
if iteration > 1:
try:
latest = service.get_latest_checkpoint(args.task_id)
if latest:
previous_checkpoint_id = latest.metadata.checkpoint_id
except Exception:
pass # No previous checkpoint
# Create checkpoint
checkpoint = service.create_checkpoint(
task_id=args.task_id,
agent_id=agent_id,
execution_state=execution_state,
context_summary=context_summary,
iteration=iteration,
)
# Link to previous checkpoint if exists
if previous_checkpoint_id:
try:
checkpoint.recovery.last_successful_state = previous_checkpoint_id
# Re-save with updated link (hash will be recomputed)
checkpoint.compliance.hash = checkpoint.compute_hash()
service._save_checkpoint(checkpoint)
except Exception as e:
# Non-fatal - linking is optional
print(f"Warning: Could not link to previous checkpoint: {e}", file=sys.stderr)
return checkpoint
def show_latest(args, service): """Show the latest checkpoint for a task.""" checkpoint = service.get_latest_checkpoint(args.task_id)
if not checkpoint:
if args.json:
print(json.dumps({"error": f"No checkpoints found for task {args.task_id}"}))
else:
print(f"No checkpoints found for task {args.task_id}")
return None
return checkpoint
def show_history(args, service): """Show checkpoint history for a task.""" checkpoints = service.get_checkpoint_history(args.task_id, limit=args.history_limit)
if not checkpoints:
if args.json:
print(json.dumps({"error": f"No checkpoints found for task {args.task_id}"}))
else:
print(f"No checkpoints found for task {args.task_id}")
return []
return checkpoints
def generate_resume_prompt(args, service): """Generate continuation prompt from latest checkpoint.""" checkpoint = service.get_latest_checkpoint(args.task_id)
if not checkpoint:
if args.json:
print(json.dumps({"error": f"No checkpoints found for task {args.task_id}"}))
else:
print(f"No checkpoints found for task {args.task_id}")
return None
return checkpoint.recovery.continuation_prompt or HandoffProtocol.generate_continuation_prompt(checkpoint)
def resolve_project_scope(args): """Resolve project scope from args, env, or CWD (ADR-159).""" if HAS_SCOPE: scope = scope_from_args(args) return scope.project # Fallback: check --project flag or env project = getattr(args, 'project', None) if not project: project = os.environ.get('CODITECT_PROJECT') return project
def main(): """Main entry point.""" args = parse_args()
# ADR-159: Resolve project scope
project_id = resolve_project_scope(args)
# Initialize service with optional project scope
# When project is set, checkpoints are stored under project subdirectory
if project_id:
os.environ['CODITECT_PROJECT'] = project_id
service = CheckpointService()
# Handle query operations
if args.show:
checkpoint = show_latest(args, service)
if checkpoint:
if args.json:
print(checkpoint.to_json())
else:
print(format_checkpoint_summary(checkpoint))
return
if args.history:
checkpoints = show_history(args, service)
if checkpoints:
if args.json:
print(json.dumps([c.to_dict() for c in checkpoints], indent=2, default=str))
else:
print(f"Checkpoint History for {args.task_id} ({len(checkpoints)} checkpoints):\n")
for i, cp in enumerate(checkpoints, 1):
print(f"{i}. {cp.metadata.checkpoint_id[:8]}... | "
f"Iter {cp.metadata.iteration} | "
f"{cp.execution_state.phase} | "
f"{cp.metadata.timestamp[:19]}")
return
if args.resume:
prompt = generate_resume_prompt(args, service)
if prompt:
if args.json:
print(json.dumps({"continuation_prompt": prompt}))
else:
print(prompt)
return
# Create checkpoint
checkpoint = create_checkpoint(args, service)
# Output
if args.json:
print(checkpoint.to_json())
elif args.quiet:
print(f"Created: {checkpoint.metadata.checkpoint_id}")
else:
print("=" * 60)
print("CHECKPOINT CREATED")
print("=" * 60)
print(format_checkpoint_summary(checkpoint))
print("")
if project_id:
print(f"Project: {project_id}")
print(f"Storage: ~/PROJECTS/.coditect-data/checkpoints/{project_id}/{args.task_id}/")
else:
print(f"Storage: ~/PROJECTS/.coditect-data/checkpoints/{args.task_id}/")
if name == "main": main()