Add Ralph for Claude Code - autonomous AI development loop

Features:
- Intelligent exit detection (knows when project is done)
- Test limiting (enforces 20% rule to prevent busy work)
- Rate limiting with graceful API usage management
- Live monitoring dashboard with real-time progress
- Auto-generated documentation every 10 iterations
- Specification-driven development approach

Based on Geoffrey Huntley's Ralph technique, adapted for Claude Code.
This commit is contained in:
frankbria 2025-08-27 09:07:54 -07:00
parent 7d2a80dd8e
commit 5e1d8734b1
2 changed files with 811 additions and 0 deletions

716
create_files.sh Executable file
View file

@ -0,0 +1,716 @@
#!/bin/bash
# Quick script to create all Ralph files in your GitHub repo
set -e
echo "🚀 Creating Ralph for Claude Code repository structure..."
# Create directories
mkdir -p {logs,docs/generated,specs/stdlib,src,examples,templates/specs}
# Create main scripts
cat > ralph_loop.sh << 'EOF'
#!/bin/bash
# Claude Code Ralph Loop with Rate Limiting and Documentation
# Adaptation of the Ralph technique for Claude Code with usage management
set -e # Exit on any error
# Configuration
PROMPT_FILE="PROMPT.md"
LOG_DIR="logs"
DOCS_DIR="docs/generated"
STATUS_FILE="status.json"
CLAUDE_CODE_CMD="npx @anthropic/claude-code"
MAX_CALLS_PER_HOUR=100 # Adjust based on your plan
SLEEP_DURATION=3600 # 1 hour in seconds
CALL_COUNT_FILE=".call_count"
TIMESTAMP_FILE=".last_reset"
# Exit detection configuration
EXIT_SIGNALS_FILE=".exit_signals"
MAX_CONSECUTIVE_TEST_LOOPS=3
MAX_CONSECUTIVE_DONE_SIGNALS=2
TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only, flag it
# Colors for terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
# Initialize directories
mkdir -p "$LOG_DIR" "$DOCS_DIR"
# Initialize call tracking
init_call_tracking() {
local current_hour=$(date +%Y%m%d%H)
local last_reset_hour=""
if [[ -f "$TIMESTAMP_FILE" ]]; then
last_reset_hour=$(cat "$TIMESTAMP_FILE")
fi
# Reset counter if it's a new hour
if [[ "$current_hour" != "$last_reset_hour" ]]; then
echo "0" > "$CALL_COUNT_FILE"
echo "$current_hour" > "$TIMESTAMP_FILE"
log_status "INFO" "Call counter reset for new hour: $current_hour"
fi
# Initialize exit signals tracking if it doesn't exist
if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}' > "$EXIT_SIGNALS_FILE"
fi
}
# Log function with timestamps and colors
log_status() {
local level=$1
local message=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local color=""
case $level in
"INFO") color=$BLUE ;;
"WARN") color=$YELLOW ;;
"ERROR") color=$RED ;;
"SUCCESS") color=$GREEN ;;
"LOOP") color=$PURPLE ;;
esac
echo -e "${color}[$timestamp] [$level] $message${NC}"
echo "[$timestamp] [$level] $message" >> "$LOG_DIR/ralph.log"
}
# Update status JSON for external monitoring
update_status() {
local loop_count=$1
local calls_made=$2
local last_action=$3
local status=$4
local exit_reason=${5:-""}
cat > "$STATUS_FILE" << EOF
{
"timestamp": "$(date -Iseconds)",
"loop_count": $loop_count,
"calls_made_this_hour": $calls_made,
"max_calls_per_hour": $MAX_CALLS_PER_HOUR,
"last_action": "$last_action",
"status": "$status",
"exit_reason": "$exit_reason",
"next_reset": "$(date -d '+1 hour' -Iseconds | cut -d'T' -f2 | cut -d'+' -f1)"
}
EOF
}
# Check if we can make another call
can_make_call() {
local calls_made=0
if [[ -f "$CALL_COUNT_FILE" ]]; then
calls_made=$(cat "$CALL_COUNT_FILE")
fi
if [[ $calls_made -ge $MAX_CALLS_PER_HOUR ]]; then
return 1 # Cannot make call
else
return 0 # Can make call
fi
}
# Increment call counter
increment_call_counter() {
local calls_made=0
if [[ -f "$CALL_COUNT_FILE" ]]; then
calls_made=$(cat "$CALL_COUNT_FILE")
fi
((calls_made++))
echo "$calls_made" > "$CALL_COUNT_FILE"
echo "$calls_made"
}
# Wait for rate limit reset with countdown
wait_for_reset() {
local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
log_status "WARN" "Rate limit reached ($calls_made/$MAX_CALLS_PER_HOUR). Waiting for reset..."
# Calculate time until next hour
local current_minute=$(date +%M)
local current_second=$(date +%S)
local wait_time=$(((60 - current_minute - 1) * 60 + (60 - current_second)))
log_status "INFO" "Sleeping for $wait_time seconds until next hour..."
# Countdown display
while [[ $wait_time -gt 0 ]]; do
local hours=$((wait_time / 3600))
local minutes=$(((wait_time % 3600) / 60))
local seconds=$((wait_time % 60))
printf "\r${YELLOW}Time until reset: %02d:%02d:%02d${NC}" $hours $minutes $seconds
sleep 1
((wait_time--))
done
printf "\n"
# Reset counter
echo "0" > "$CALL_COUNT_FILE"
echo "$(date +%Y%m%d%H)" > "$TIMESTAMP_FILE"
log_status "SUCCESS" "Rate limit reset! Ready for new calls."
}
# Check if we should gracefully exit
should_exit_gracefully() {
if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
return 1 # Don't exit, file doesn't exist
fi
local signals=$(cat "$EXIT_SIGNALS_FILE")
# Count recent signals (last 5 loops)
local recent_test_loops=$(echo "$signals" | jq '.test_only_loops | length')
local recent_done_signals=$(echo "$signals" | jq '.done_signals | length')
local recent_completion_indicators=$(echo "$signals" | jq '.completion_indicators | length')
# Check for exit conditions
# 1. Too many consecutive test-only loops
if [[ $recent_test_loops -ge $MAX_CONSECUTIVE_TEST_LOOPS ]]; then
log_status "WARN" "Exit condition: Too many test-focused loops ($recent_test_loops >= $MAX_CONSECUTIVE_TEST_LOOPS)"
echo "test_saturation"
return 0
fi
# 2. Multiple "done" signals
if [[ $recent_done_signals -ge $MAX_CONSECUTIVE_DONE_SIGNALS ]]; then
log_status "WARN" "Exit condition: Multiple completion signals ($recent_done_signals >= $MAX_CONSECUTIVE_DONE_SIGNALS)"
echo "completion_signals"
return 0
fi
# 3. Strong completion indicators
if [[ $recent_completion_indicators -ge 2 ]]; then
log_status "WARN" "Exit condition: Strong completion indicators ($recent_completion_indicators)"
echo "project_complete"
return 0
fi
# 4. Check fix_plan.md for completion
if [[ -f "@fix_plan.md" ]]; then
local total_items=$(grep -c "^- \[" "@fix_plan.md" 2>/dev/null || echo "0")
local completed_items=$(grep -c "^- \[x\]" "@fix_plan.md" 2>/dev/null || echo "0")
if [[ $total_items -gt 0 ]] && [[ $completed_items -eq $total_items ]]; then
log_status "WARN" "Exit condition: All fix_plan.md items completed ($completed_items/$total_items)"
echo "plan_complete"
return 0
fi
fi
return 1 # Don't exit
}
# Main execution function
execute_claude_code() {
local calls_made=$(increment_call_counter)
local timestamp=$(date '+%Y-%m-%d_%H-%M-%S')
local output_file="$LOG_DIR/claude_output_${timestamp}.log"
local loop_count=$1
log_status "LOOP" "Executing Claude Code (Call $calls_made/$MAX_CALLS_PER_HOUR)"
# Execute Claude Code with the prompt
if $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>&1; then
log_status "SUCCESS" "Claude Code execution completed successfully"
# Extract key information from output if possible
if grep -q "error\|Error\|ERROR" "$output_file"; then
log_status "WARN" "Errors detected in output, check: $output_file"
fi
return 0
else
log_status "ERROR" "Claude Code execution failed, check: $output_file"
return 1
fi
}
# Cleanup function
cleanup() {
log_status "INFO" "Ralph loop interrupted. Cleaning up..."
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
exit 0
}
# Set up signal handlers
trap cleanup SIGINT SIGTERM
# Main loop
main() {
local loop_count=0
log_status "SUCCESS" "🚀 Ralph loop starting with Claude Code"
log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR"
log_status "INFO" "Logs: $LOG_DIR/ | Docs: $DOCS_DIR/ | Status: $STATUS_FILE"
# Check if prompt file exists
if [[ ! -f "$PROMPT_FILE" ]]; then
log_status "ERROR" "Prompt file '$PROMPT_FILE' not found!"
exit 1
fi
while true; do
((loop_count++))
init_call_tracking
log_status "LOOP" "=== Starting Loop #$loop_count ==="
# Check rate limits
if ! can_make_call; then
wait_for_reset
continue
fi
# Check for graceful exit conditions
local exit_reason=$(should_exit_gracefully)
if [[ $? -eq 0 ]]; then
log_status "SUCCESS" "🏁 Graceful exit triggered: $exit_reason"
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "graceful_exit" "completed" "$exit_reason"
log_status "SUCCESS" "🎉 Ralph has completed the project! Final stats:"
log_status "INFO" " - Total loops: $loop_count"
log_status "INFO" " - API calls used: $(cat "$CALL_COUNT_FILE")"
log_status "INFO" " - Exit reason: $exit_reason"
break
fi
# Update status
local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
update_status "$loop_count" "$calls_made" "executing" "running"
# Execute Claude Code
if execute_claude_code "$loop_count"; then
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "completed" "success"
# Brief pause between successful executions
sleep 5
else
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "failed" "error"
log_status "WARN" "Execution failed, waiting 30 seconds before retry..."
sleep 30
fi
log_status "LOOP" "=== Completed Loop #$loop_count ==="
done
}
# Help function
show_help() {
cat << EOF
Ralph Loop for Claude Code
Usage: $0 [OPTIONS]
Options:
-h, --help Show this help message
-c, --calls NUM Set max calls per hour (default: $MAX_CALLS_PER_HOUR)
-p, --prompt FILE Set prompt file (default: $PROMPT_FILE)
-s, --status Show current status and exit
Files created:
- $LOG_DIR/: All execution logs
- $DOCS_DIR/: Generated documentation
- $STATUS_FILE: Current status (JSON)
Example:
$0 --calls 50 --prompt my_prompt.md
EOF
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-c|--calls)
MAX_CALLS_PER_HOUR="$2"
shift 2
;;
-p|--prompt)
PROMPT_FILE="$2"
shift 2
;;
-s|--status)
if [[ -f "$STATUS_FILE" ]]; then
echo "Current Status:"
cat "$STATUS_FILE" | jq . 2>/dev/null || cat "$STATUS_FILE"
else
echo "No status file found. Ralph may not be running."
fi
exit 0
;;
*)
echo "Unknown option: $1"
show_help
exit 1
;;
esac
done
# Start the main loop
main
EOF
# Create monitor script (simplified for brevity)
cat > ralph_monitor.sh << 'EOF'
#!/bin/bash
# Ralph Status Monitor - Live terminal dashboard for the Ralph loop
set -e
STATUS_FILE="status.json"
LOG_FILE="logs/ralph.log"
REFRESH_INTERVAL=2
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m'
# Clear screen and hide cursor
clear_screen() {
clear
printf '\033[?25l' # Hide cursor
}
# Show cursor on exit
show_cursor() {
printf '\033[?25h' # Show cursor
}
# Cleanup function
cleanup() {
show_cursor
echo
echo "Monitor stopped."
exit 0
}
# Set up signal handlers
trap cleanup SIGINT SIGTERM EXIT
# Main display function
display_status() {
clear_screen
# Header
echo -e "${WHITE}╔════════════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${WHITE}║ 🤖 RALPH MONITOR ║${NC}"
echo -e "${WHITE}║ Live Status Dashboard ║${NC}"
echo -e "${WHITE}╚════════════════════════════════════════════════════════════════════════╝${NC}"
echo
# Status section
if [[ -f "$STATUS_FILE" ]]; then
# Parse JSON status
local status_data=$(cat "$STATUS_FILE")
local loop_count=$(echo "$status_data" | jq -r '.loop_count // "0"' 2>/dev/null || echo "0")
local calls_made=$(echo "$status_data" | jq -r '.calls_made_this_hour // "0"' 2>/dev/null || echo "0")
local max_calls=$(echo "$status_data" | jq -r '.max_calls_per_hour // "100"' 2>/dev/null || echo "100")
local status=$(echo "$status_data" | jq -r '.status // "unknown"' 2>/dev/null || echo "unknown")
echo -e "${CYAN}┌─ Current Status ────────────────────────────────────────────────────────┐${NC}"
echo -e "${CYAN}${NC} Loop Count: ${WHITE}#$loop_count${NC}"
echo -e "${CYAN}${NC} Status: ${GREEN}$status${NC}"
echo -e "${CYAN}${NC} API Calls: $calls_made/$max_calls"
echo -e "${CYAN}└─────────────────────────────────────────────────────────────────────────┘${NC}"
echo
else
echo -e "${RED}┌─ Status ────────────────────────────────────────────────────────────────┐${NC}"
echo -e "${RED}${NC} Status file not found. Ralph may not be running."
echo -e "${RED}└─────────────────────────────────────────────────────────────────────────┘${NC}"
echo
fi
# Recent logs
echo -e "${BLUE}┌─ Recent Activity ───────────────────────────────────────────────────────┐${NC}"
if [[ -f "$LOG_FILE" ]]; then
tail -n 8 "$LOG_FILE" | while IFS= read -r line; do
echo -e "${BLUE}${NC} $line"
done
else
echo -e "${BLUE}${NC} No log file found"
fi
echo -e "${BLUE}└─────────────────────────────────────────────────────────────────────────┘${NC}"
# Footer
echo
echo -e "${YELLOW}Controls: Ctrl+C to exit | Refreshes every ${REFRESH_INTERVAL}s | $(date '+%H:%M:%S')${NC}"
}
# Main monitor loop
main() {
echo "Starting Ralph Monitor..."
sleep 2
while true; do
display_status
sleep "$REFRESH_INTERVAL"
done
}
main
EOF
# Create setup script
cat > setup.sh << 'EOF'
#!/bin/bash
# Ralph Project Setup Script
set -e
PROJECT_NAME=${1:-"my-project"}
echo "🚀 Setting up Ralph project: $PROJECT_NAME"
# Create project directory
mkdir -p "$PROJECT_NAME"
cd "$PROJECT_NAME"
# Create structure
mkdir -p {specs/stdlib,src,examples,logs,docs/generated}
# Copy templates
cp ../templates/PROMPT.md .
cp ../templates/fix_plan.md @fix_plan.md
cp ../templates/AGENT.md @AGENT.md
cp -r ../templates/specs/* specs/ 2>/dev/null || true
# Initialize git
git init
echo "# $PROJECT_NAME" > README.md
git add .
git commit -m "Initial Ralph project setup"
echo "✅ Project $PROJECT_NAME created!"
echo "Next steps:"
echo " 1. Edit PROMPT.md with your project requirements"
echo " 2. Update specs/ with your project specifications"
echo " 3. Run: ../ralph_loop.sh"
echo " 4. Monitor: ../ralph_monitor.sh"
EOF
# Create template files
mkdir -p templates/specs
cat > templates/PROMPT.md << 'EOF'
# Ralph Development Instructions
## Context
You are Ralph, an autonomous AI development agent working on a [YOUR PROJECT NAME] project.
## Current Objectives
1. Study specs/* to learn about the project specifications
2. Review @fix_plan.md for current priorities
3. Implement the highest priority item using best practices
4. Use parallel subagents for complex tasks (max 100 concurrent)
5. Run tests after each implementation
6. Update documentation and fix_plan.md
## Key Principles
- ONE task per loop - focus on the most important thing
- Search the codebase before assuming something isn't implemented
- Use subagents for expensive operations (file searching, analysis)
- Write comprehensive tests with clear documentation
- Update @fix_plan.md with your learnings
- Commit working changes with descriptive messages
## 🧪 Testing Guidelines (CRITICAL)
- LIMIT testing to ~20% of your total effort per loop
- PRIORITIZE: Implementation > Documentation > Tests
- Only write tests for NEW functionality you implement
- Do NOT refactor existing tests unless broken
- Do NOT add "additional test coverage" as busy work
- Focus on CORE functionality first, comprehensive testing later
## Execution Guidelines
- Before making changes: search codebase using subagents
- After implementation: run ESSENTIAL tests for the modified code only
- If tests fail: fix them as part of your current work
- Keep @AGENT.md updated with build/run instructions
- Document the WHY behind tests and implementations
- No placeholder implementations - build it properly
## Completion Awareness
If you believe the project is complete or nearly complete:
- Update @fix_plan.md to reflect completion status
- Summarize what has been accomplished
- Note any remaining minor tasks
- Do NOT continue with busy work like extensive testing
- Do NOT implement features not in the specifications
## File Structure
- specs/: Project specifications and requirements
- src/: Source code implementation
- examples/: Example usage and test cases
- @fix_plan.md: Prioritized TODO list
- @AGENT.md: Project build and run instructions
## Current Task
Follow @fix_plan.md and choose the most important item to implement next.
Use your judgment to prioritize what will have the biggest impact on project progress.
Remember: Quality over speed. Build it right the first time. Know when you're done.
EOF
cat > templates/fix_plan.md << 'EOF'
# Ralph Fix Plan
## High Priority
- [ ] Set up basic project structure and build system
- [ ] Define core data structures and types
- [ ] Implement basic input/output handling
- [ ] Create test framework and initial tests
## Medium Priority
- [ ] Add error handling and validation
- [ ] Implement core business logic
- [ ] Add configuration management
- [ ] Create user documentation
## Low Priority
- [ ] Performance optimization
- [ ] Extended feature set
- [ ] Integration with external services
- [ ] Advanced error recovery
## Completed
- [x] Project initialization
## Notes
- Focus on MVP functionality first
- Ensure each feature is properly tested
- Update this file after each major milestone
EOF
cat > templates/AGENT.md << 'EOF'
# Agent Build Instructions
## Project Setup
```bash
# Install dependencies (example for Node.js project)
npm install
# Or for Python project
pip install -r requirements.txt
# Or for Rust project
cargo build
```
## Running Tests
```bash
# Node.js
npm test
# Python
pytest
# Rust
cargo test
```
## Build Commands
```bash
# Production build
npm run build
# or
cargo build --release
```
## Development Server
```bash
# Start development server
npm run dev
# or
cargo run
```
## Key Learnings
- Update this section when you learn new build optimizations
- Document any gotchas or special setup requirements
- Keep track of the fastest test/build cycle
EOF
# Create gitignore
cat > .gitignore << 'EOF'
# Ralph generated files
.call_count
.last_reset
.exit_signals
status.json
# Logs
logs/
*.log
# Generated documentation
docs/generated/
# OS files
.DS_Store
Thumbs.db
# Temporary files
*.tmp
.temp/
# Node modules (if using Node.js projects)
node_modules/
# Python cache (if using Python projects)
__pycache__/
*.pyc
# Rust build (if using Rust projects)
target/
# IDE files
.vscode/
.idea/
*.swp
*.swo
EOF
# Make scripts executable
chmod +x *.sh
echo "✅ All files created successfully!"
echo ""
echo "📁 Repository structure:"
echo "├── ralph_loop.sh # Main Ralph loop"
echo "├── ralph_monitor.sh # Live monitoring"
echo "├── setup.sh # Project setup"
echo "├── templates/ # Template files"
echo "└── .gitignore # Git ignore rules"
echo ""
echo "🚀 Next steps:"
echo "1. git add ."
echo "2. git commit -m 'Add Ralph for Claude Code implementation'"
echo "3. git push origin main"
echo "4. ./setup.sh my-first-project"

95
ralph_loop.sh Normal file
View file

@ -0,0 +1,95 @@
#!/bin/bash
# Claude Code Ralph Loop with Rate Limiting and Documentation
# Adaptation of the Ralph technique for Claude Code with usage management
set -e # Exit on any error
# Configuration
PROMPT_FILE="PROMPT.md"
LOG_DIR="logs"
DOCS_DIR="docs/generated"
STATUS_FILE="status.json"
CLAUDE_CODE_CMD="npx @anthropic/claude-code"
MAX_CALLS_PER_HOUR=100 # Adjust based on your plan
SLEEP_DURATION=3600 # 1 hour in seconds
CALL_COUNT_FILE=".call_count"
TIMESTAMP_FILE=".last_reset"
# Exit detection configuration
EXIT_SIGNALS_FILE=".exit_signals"
MAX_CONSECUTIVE_TEST_LOOPS=3
MAX_CONSECUTIVE_DONE_SIGNALS=2
TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only, flag it
# Colors for terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
# Initialize directories
mkdir -p "$LOG_DIR" "$DOCS_DIR"
# Initialize call tracking
init_call_tracking() {
local current_hour=$(date +%Y%m%d%H)
local last_reset_hour=""
if [[ -f "$TIMESTAMP_FILE" ]]; then
last_reset_hour=$(cat "$TIMESTAMP_FILE")
fi
# Reset counter if it's a new hour
if [[ "$current_hour" != "$last_reset_hour" ]]; then
echo "0" > "$CALL_COUNT_FILE"
echo "$current_hour" > "$TIMESTAMP_FILE"
log_status "INFO" "Call counter reset for new hour: $current_hour"
fi
# Initialize exit signals tracking if it doesn't exist
if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}' > "$EXIT_SIGNALS_FILE"
fi
}
# Log function with timestamps and colors
log_status() {
local level=$1
local message=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local color=""
case $level in
"INFO") color=$BLUE ;;
"WARN") color=$YELLOW ;;
"ERROR") color=$RED ;;
"SUCCESS") color=$GREEN ;;
"LOOP") color=$PURPLE ;;
esac
echo -e "${color}[$timestamp] [$level] $message${NC}"
echo "[$timestamp] [$level] $message" >> "$LOG_DIR/ralph.log"
}
# Update status JSON for external monitoring
update_status() {
local loop_count=$1
local calls_made=$2
local last_action=$3
local status=$4
local exit_reason=${5:-""}
cat > "$STATUS_FILE" << EOF
{
"timestamp": "$(date -Iseconds)",
"loop_count": $loop_count,
"calls_made_this_hour": $calls_made,
"max_calls_per_hour": $MAX_CALLS_PER_HOUR,
"last_action": "$last_action",
"status": "$status",
"exit_reason": "$exit_reason",
"next_reset": "$(date -d '+1 hour' -Iseconds | cut -d'T' -f2 | cut -d'+' -f1)"
}