282 lines
8.1 KiB
Bash
282 lines
8.1 KiB
Bash
#!/bin/bash
|
|
# Loki Mode Wrapper Script
|
|
# Provides true autonomy by auto-resuming on rate limits or interruptions
|
|
#
|
|
# How it works:
|
|
# 1. Launches Claude Code with Loki Mode prompt
|
|
# 2. Monitors the process - when Claude exits, checks exit code
|
|
# 3. On rate limit (exit code != 0), waits with exponential backoff
|
|
# 4. Restarts automatically, telling Claude to resume from checkpoint
|
|
# 5. Continues until successful completion or max retries exceeded
|
|
#
|
|
# Usage:
|
|
# ./scripts/loki-wrapper.sh [PRD_PATH]
|
|
# ./scripts/loki-wrapper.sh ./docs/requirements.md
|
|
# ./scripts/loki-wrapper.sh # Interactive mode
|
|
|
|
set -uo pipefail
|
|
|
|
# Configuration
|
|
MAX_RETRIES=${LOKI_MAX_RETRIES:-50} # Maximum retry attempts
|
|
BASE_WAIT=${LOKI_BASE_WAIT:-60} # Base wait time in seconds
|
|
MAX_WAIT=${LOKI_MAX_WAIT:-3600} # Max wait time (1 hour)
|
|
LOG_FILE=${LOKI_LOG_FILE:-.loki/wrapper.log} # Log file location
|
|
STATE_FILE=${LOKI_STATE_FILE:-.loki/wrapper-state.json}
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
log() {
|
|
local level="$1"
|
|
shift
|
|
local msg="$*"
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
echo -e "[$timestamp] [$level] $msg" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
log_info() { log "INFO" "$*"; }
|
|
log_warn() { log "${YELLOW}WARN${NC}" "$*"; }
|
|
log_error() { log "${RED}ERROR${NC}" "$*"; }
|
|
log_success() { log "${GREEN}SUCCESS${NC}" "$*"; }
|
|
|
|
# Ensure .loki directory exists
|
|
mkdir -p .loki
|
|
|
|
# Parse arguments
|
|
PRD_PATH="${1:-}"
|
|
INITIAL_PROMPT=""
|
|
|
|
if [ -n "$PRD_PATH" ]; then
|
|
if [ -f "$PRD_PATH" ]; then
|
|
INITIAL_PROMPT="Loki Mode with PRD at $PRD_PATH"
|
|
else
|
|
log_error "PRD file not found: $PRD_PATH"
|
|
exit 1
|
|
fi
|
|
else
|
|
INITIAL_PROMPT="Loki Mode"
|
|
fi
|
|
|
|
# Save wrapper state
|
|
save_state() {
|
|
local retry_count="$1"
|
|
local status="$2"
|
|
local last_exit_code="$3"
|
|
|
|
cat > "$STATE_FILE" << EOF
|
|
{
|
|
"retryCount": $retry_count,
|
|
"status": "$status",
|
|
"lastExitCode": $last_exit_code,
|
|
"lastRun": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"prdPath": "$PRD_PATH",
|
|
"pid": $$
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# Load wrapper state if resuming
|
|
load_state() {
|
|
if [ -f "$STATE_FILE" ]; then
|
|
if command -v python3 &> /dev/null; then
|
|
RETRY_COUNT=$(python3 -c "import json; print(json.load(open('$STATE_FILE')).get('retryCount', 0))" 2>/dev/null || echo "0")
|
|
else
|
|
RETRY_COUNT=0
|
|
fi
|
|
else
|
|
RETRY_COUNT=0
|
|
fi
|
|
}
|
|
|
|
# Calculate wait time with exponential backoff and jitter
|
|
calculate_wait() {
|
|
local retry="$1"
|
|
local wait_time=$((BASE_WAIT * (2 ** retry)))
|
|
|
|
# Add jitter (0-30 seconds)
|
|
local jitter=$((RANDOM % 30))
|
|
wait_time=$((wait_time + jitter))
|
|
|
|
# Cap at max wait
|
|
if [ $wait_time -gt $MAX_WAIT ]; then
|
|
wait_time=$MAX_WAIT
|
|
fi
|
|
|
|
echo $wait_time
|
|
}
|
|
|
|
# Check if this looks like a rate limit error
|
|
is_rate_limit() {
|
|
local exit_code="$1"
|
|
|
|
# Exit code 1 with rate limit indicators in log
|
|
if [ $exit_code -ne 0 ]; then
|
|
# Check recent .loki logs for rate limit indicators
|
|
if [ -d ".loki/logs" ]; then
|
|
if grep -r -l "rate.limit\|429\|too.many.requests\|quota.exceeded" .loki/logs/*.log 2>/dev/null | head -1 | grep -q .; then
|
|
return 0
|
|
fi
|
|
fi
|
|
# Assume rate limit on non-zero exit (conservative approach)
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Check if Loki Mode completed successfully
|
|
is_completed() {
|
|
# Check for completion markers
|
|
if [ -f ".loki/state/orchestrator.json" ]; then
|
|
if command -v python3 &> /dev/null; then
|
|
local phase=$(python3 -c "import json; print(json.load(open('.loki/state/orchestrator.json')).get('currentPhase', ''))" 2>/dev/null || echo "")
|
|
if [ "$phase" = "COMPLETED" ] || [ "$phase" = "complete" ]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Check for success file
|
|
if [ -f ".loki/COMPLETED" ]; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Build the resume prompt
|
|
build_resume_prompt() {
|
|
local retry="$1"
|
|
|
|
if [ $retry -eq 0 ]; then
|
|
echo "$INITIAL_PROMPT"
|
|
else
|
|
# Resume from checkpoint
|
|
if [ -n "$PRD_PATH" ]; then
|
|
echo "Loki Mode - Resume from checkpoint. PRD at $PRD_PATH. This is retry #$retry after rate limit. Check .loki/state/ for current progress and continue from where we left off."
|
|
else
|
|
echo "Loki Mode - Resume from checkpoint. This is retry #$retry after rate limit. Check .loki/state/ for current progress and continue from where we left off."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Main execution loop
|
|
main() {
|
|
log_info "=========================================="
|
|
log_info "Loki Mode Autonomous Wrapper"
|
|
log_info "=========================================="
|
|
log_info "PRD: ${PRD_PATH:-Interactive}"
|
|
log_info "Max retries: $MAX_RETRIES"
|
|
log_info "Base wait: ${BASE_WAIT}s"
|
|
log_info ""
|
|
|
|
load_state
|
|
local retry=$RETRY_COUNT
|
|
|
|
while [ $retry -lt $MAX_RETRIES ]; do
|
|
local prompt=$(build_resume_prompt $retry)
|
|
|
|
log_info "Attempt $((retry + 1))/$MAX_RETRIES"
|
|
log_info "Prompt: $prompt"
|
|
save_state $retry "running" 0
|
|
|
|
# Launch Claude Code
|
|
# The process exits when:
|
|
# 1. User types /exit or Ctrl+C (exit 0)
|
|
# 2. Rate limit hit (exit 1 or other non-zero)
|
|
# 3. Crash or error (non-zero exit)
|
|
# 4. Session completes naturally (exit 0)
|
|
|
|
local start_time=$(date +%s)
|
|
|
|
# Run Claude Code with the prompt
|
|
# Using -p for non-interactive prompt mode
|
|
set +e
|
|
claude --dangerously-skip-permissions -p "$prompt" 2>&1 | tee -a "$LOG_FILE"
|
|
local exit_code=${PIPESTATUS[0]}
|
|
set -e
|
|
|
|
local end_time=$(date +%s)
|
|
local duration=$((end_time - start_time))
|
|
|
|
log_info "Claude exited with code $exit_code after ${duration}s"
|
|
save_state $retry "exited" $exit_code
|
|
|
|
# Check for successful completion
|
|
if [ $exit_code -eq 0 ]; then
|
|
if is_completed; then
|
|
log_success "Loki Mode completed successfully!"
|
|
save_state $retry "completed" 0
|
|
exit 0
|
|
else
|
|
log_info "Claude exited cleanly but work may not be complete"
|
|
log_info "Checking if we should continue..."
|
|
|
|
# If session was short, might be intentional exit
|
|
if [ $duration -lt 30 ]; then
|
|
log_warn "Session was very short (${duration}s). User may have exited intentionally."
|
|
log_info "Waiting 10 seconds before checking again..."
|
|
sleep 10
|
|
|
|
# Re-check completion
|
|
if is_completed; then
|
|
log_success "Loki Mode completed!"
|
|
exit 0
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Handle non-zero exit (likely rate limit)
|
|
if is_rate_limit $exit_code; then
|
|
local wait_time=$(calculate_wait $retry)
|
|
log_warn "Rate limit detected. Waiting ${wait_time}s before retry..."
|
|
|
|
# Show countdown
|
|
local remaining=$wait_time
|
|
while [ $remaining -gt 0 ]; do
|
|
printf "\r${YELLOW}Resuming in ${remaining}s...${NC} "
|
|
sleep 10
|
|
remaining=$((remaining - 10))
|
|
done
|
|
echo ""
|
|
|
|
((retry++))
|
|
else
|
|
# Non-rate-limit error
|
|
log_error "Non-rate-limit error (exit code: $exit_code)"
|
|
|
|
# Still retry, but with shorter wait
|
|
local wait_time=$((BASE_WAIT / 2))
|
|
log_info "Retrying in ${wait_time}s..."
|
|
sleep $wait_time
|
|
((retry++))
|
|
fi
|
|
done
|
|
|
|
log_error "Max retries ($MAX_RETRIES) exceeded. Giving up."
|
|
save_state $retry "failed" 1
|
|
exit 1
|
|
}
|
|
|
|
# Trap signals for clean shutdown
|
|
cleanup() {
|
|
log_warn "Received interrupt signal. Saving state..."
|
|
save_state $RETRY_COUNT "interrupted" 130
|
|
exit 130
|
|
}
|
|
trap cleanup INT TERM
|
|
|
|
# Check for claude command
|
|
if ! command -v claude &> /dev/null; then
|
|
log_error "Claude Code CLI not found. Please install it first."
|
|
log_info "Visit: https://claude.ai/code"
|
|
exit 1
|
|
fi
|
|
|
|
# Run main
|
|
main "$@"
|