Skip to main content

#!/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()