Enable autonomous loop mode for long-running tasks
Starts autonomous Ralph Wiggum loop for long-running tasks with configurable limits.
/plugin marketplace add terrylica/cc-skills/plugin install ralph@cc-skills[-f <file>] [--poc | --production] [--no-focus] [<task description>...]Enable the Ralph Wiggum autonomous improvement loop. Claude will continue working until:
.claude/STOP_LOOP file created)-f <file>: Specify target file for completion tracking (plan, spec, or ADR)--poc: Use proof-of-concept settings (5 min / 10 min limits, 10/20 iterations)--production: Use production settings (4h / 9h limits, 50/99 iterations) - skips preset prompt--no-focus: Skip focus file tracking (100% autonomous, no plan file)--skip-constraint-scan: Skip constraint scanner (power users, v3.0.0+)<task description>: Natural language task prompt (remaining text after flags)Discover and auto-select focus files WITHOUT prompting the user (autonomous mode):
Check for explicit file: If -f <file> was provided, use that path. Skip to Step 2.
Check for --no-focus flag: If --no-focus is present, skip to Step 2 with NO_FOCUS=true.
Auto-discover focus file (if no explicit file):
For Alpha Forge projects (detected by outputs/research_sessions/ existing):
outputs/research_sessions/*/research_log.md filesFor other projects (⚠️ hooks will skip — see Alpha-Forge Exclusivity), discover in priority order:
implementation-status: in_progress in docs/design/*/spec.mdstatus: accepted in docs/adr/*.md.md file in .claude/plans/ (local or global)Only prompt if truly ambiguous (multiple ITP specs or ADRs with same priority):
If nothing discovered: Proceed with NO_FOCUS=true (exploration mode)
Purpose: Detect environment constraints before loop starts. Results inform Ralph behavior.
Skip if: --skip-constraint-scan flag provided (power users).
Run the constraint scanner:
# Use /usr/bin/env bash for macOS zsh compatibility
/usr/bin/env bash << 'CONSTRAINT_SCAN_SCRIPT'
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
ARGS="${ARGUMENTS:-}"
# Check for skip flag
if [[ "$ARGS" == *"--skip-constraint-scan"* ]]; then
echo "Constraint scan: SKIPPED (--skip-constraint-scan flag)"
exit 0
fi
# Find scanner script in plugin cache
RALPH_CACHE="$HOME/.claude/plugins/cache/cc-skills/ralph"
SCANNER_SCRIPT=""
if [[ -d "$RALPH_CACHE/local" ]]; then
SCANNER_SCRIPT="$RALPH_CACHE/local/scripts/constraint-scanner.py"
elif [[ -d "$RALPH_CACHE" ]]; then
# Get highest version
RALPH_VERSION=$(ls "$RALPH_CACHE" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
if [[ -n "$RALPH_VERSION" ]]; then
SCANNER_SCRIPT="$RALPH_CACHE/$RALPH_VERSION/scripts/constraint-scanner.py"
fi
fi
# Skip if scanner not found (older version without scanner)
if [[ -z "$SCANNER_SCRIPT" ]] || [[ ! -f "$SCANNER_SCRIPT" ]]; then
echo "Constraint scan: SKIPPED (scanner not found, upgrade to v9.2.0+)"
exit 0
fi
# Run scanner - discover UV with same fallback pattern as main script
UV_CMD=""
discover_uv() {
command -v uv &>/dev/null && echo "uv" && return 0
for loc in "$HOME/.local/bin/uv" "$HOME/.cargo/bin/uv" "/opt/homebrew/bin/uv" "/usr/local/bin/uv" "$HOME/.local/share/mise/shims/uv"; do
[[ -x "$loc" ]] && echo "$loc" && return 0
done
# Dynamic mise version discovery
local mise_base="$HOME/.local/share/mise/installs/uv"
if [[ -d "$mise_base" ]]; then
local ver=$(ls -1 "$mise_base" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+' | sort -V | tail -1)
if [[ -n "$ver" ]]; then
local plat=$(ls -1 "$mise_base/$ver" 2>/dev/null | head -1)
[[ -n "$plat" && -x "$mise_base/$ver/$plat/uv" ]] && echo "$mise_base/$ver/$plat/uv" && return 0
[[ -x "$mise_base/$ver/uv" ]] && echo "$mise_base/$ver/uv" && return 0
fi
fi
command -v mise &>/dev/null && mise which uv &>/dev/null 2>&1 && echo "mise exec -- uv" && return 0
return 1
}
UV_CMD=$(discover_uv) || { echo "Constraint scan: SKIPPED (uv not found)"; exit 0; }
echo "Running constraint scanner..."
SCAN_OUTPUT=$($UV_CMD run -q "$SCANNER_SCRIPT" --project "$PROJECT_DIR" 2>&1)
SCAN_EXIT=$?
if [[ $SCAN_EXIT -eq 2 ]]; then
echo ""
echo "========================================"
echo " CRITICAL CONSTRAINTS DETECTED"
echo "========================================"
echo ""
echo "$SCAN_OUTPUT" | jq -r '.constraints[] | select(.severity == "critical") | " ⛔ \(.description)"' 2>/dev/null || echo "$SCAN_OUTPUT"
echo ""
echo "Action: Address critical constraints before starting loop."
echo " Use --skip-constraint-scan to bypass (not recommended)."
exit 2
elif [[ $SCAN_EXIT -eq 0 ]]; then
# Parse and display summary
CRITICAL_COUNT=$(echo "$SCAN_OUTPUT" | jq '[.constraints[] | select(.severity == "critical")] | length' 2>/dev/null || echo "0")
HIGH_COUNT=$(echo "$SCAN_OUTPUT" | jq '[.constraints[] | select(.severity == "high")] | length' 2>/dev/null || echo "0")
TOTAL_COUNT=$(echo "$SCAN_OUTPUT" | jq '.constraints | length' 2>/dev/null || echo "0")
echo "Constraint scan complete:"
echo " Critical: $CRITICAL_COUNT | High: $HIGH_COUNT | Total: $TOTAL_COUNT"
# Save results for AUQ to read (NDJSON format with .jsonl extension)
mkdir -p "$PROJECT_DIR/.claude"
echo "$SCAN_OUTPUT" > "$PROJECT_DIR/.claude/ralph-constraint-scan.jsonl"
else
echo "Constraint scan: WARNING (scanner returned exit code $SCAN_EXIT)"
echo "$SCAN_OUTPUT" | head -5
fi
CONSTRAINT_SCAN_SCRIPT
If the scanner exits with code 2 (critical constraints), stop and inform user.
Purpose: Spawn Explore agents to discover constraints the static scanner cannot detect.
MANDATORY Skill tool call: ralph:constraint-discovery — activate NOW.
This skill spawns 5 parallel Explore agents that:
⛔ MANDATORY: Do NOT proceed to Step 1.5 until this gate passes
Claude MUST execute TaskOutput calls with block: true for each agent:
For EACH agent spawned by constraint-discovery skill:
TaskOutput(task_id: "<agent_id>", block: true, timeout: 30000)
Wait for ALL 5 agents (or timeout after 30s each). Extract NDJSON constraints from each agent's output.
Merge agent findings into constraint scan file:
/usr/bin/env bash << 'AGENT_MERGE_SCRIPT'
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
SCAN_FILE="$PROJECT_DIR/.claude/ralph-constraint-scan.jsonl"
# Claude MUST append each agent's NDJSON findings here:
# For each constraint JSON from agent output:
# echo '{"_type":"constraint","source":"agent-env","severity":"HIGH","description":"..."}' >> "$SCAN_FILE"
echo "=== AGENT FINDINGS MERGED ==="
echo "Constraints in scan file:"
wc -l < "$SCAN_FILE" 2>/dev/null || echo "0"
AGENT_MERGE_SCRIPT
Gate verification: Before proceeding, confirm:
.claude/ralph-constraint-scan.jsonlIf timeout on some agents: Proceed with available results. Log which agents timed out.
ALWAYS prompt for preset confirmation. Flags pre-select the option but user confirms before execution.
If --poc flag was provided:
Use AskUserQuestion with questions:
If --production flag was provided:
Use AskUserQuestion with questions:
If no preset flag was provided:
Use AskUserQuestion with questions:
Based on selection:
"Production Mode" → Proceed to Step 1.6 (if Alpha Forge) or Step 2 with production defaults
"POC Mode" → Proceed to Step 1.6 (if Alpha Forge) or Step 2 with POC settings
"Custom" → Ask follow-up questions for time/iteration limits:
Use AskUserQuestion with questions:
question: "Select time limits:" header: "Time" multiSelect: false options:
question: "Select iteration limits:" header: "Iterations" multiSelect: false options:
Only for Alpha Forge projects (detected by adapter). Other projects skip to Step 2.
MANDATORY Skill tool call: ralph:session-guidance — activate NOW.
This skill handles the complete guidance configuration workflow:
If user selects "Keep existing guidance": Skill returns early, proceed to Step 2.
After skill completes: Guidance is saved to .claude/ralph-config.json, proceed to Step 2.
# Use /usr/bin/env bash for macOS zsh compatibility (see ADR: shell-command-portability-zsh)
/usr/bin/env bash << 'RALPH_START_SCRIPT'
# Get project directory
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
SETTINGS="$HOME/.claude/settings.json"
MARKER="ralph/hooks/"
# ===== VERSION BANNER =====
# Retrieve version from cache directory (source of truth: installed plugin version)
# FAIL FAST: Exit if version cannot be determined (no fallbacks)
RALPH_CACHE="$HOME/.claude/plugins/cache/cc-skills/ralph"
RALPH_VERSION=""
RALPH_SOURCE="cache"
if [[ -d "$RALPH_CACHE" ]]; then
# Check for 'local' directory first (development symlink takes priority)
if [[ -d "$RALPH_CACHE/local" ]]; then
RALPH_SOURCE="local"
# Follow symlink to find source repo and read version from package.json
LOCAL_PATH=$(readlink -f "$RALPH_CACHE/local" 2>/dev/null || readlink "$RALPH_CACHE/local" 2>/dev/null)
if [[ -n "$LOCAL_PATH" ]]; then
# Navigate up from plugins/ralph to repo root to find package.json
REPO_ROOT=$(cd "$LOCAL_PATH" && cd ../.. && pwd 2>/dev/null)
if [[ -f "$REPO_ROOT/package.json" ]]; then
RALPH_VERSION=$(jq -r '.version // empty' "$REPO_ROOT/package.json" 2>/dev/null)
fi
fi
else
# Get highest semantic version from cache directories
RALPH_VERSION=$(ls "$RALPH_CACHE" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
fi
fi
# FAIL FAST: Version must be determined
if [[ -z "$RALPH_VERSION" ]]; then
echo "ERROR: Cannot determine Ralph version!"
echo ""
echo "Possible causes:"
echo " 1. Plugin not installed: Run /plugin install cc-skills"
echo " 2. Local symlink broken: Check ~/.claude/plugins/cache/cc-skills/ralph/local"
echo " 3. Missing package.json in source repo"
echo ""
echo "Cache directory: $RALPH_CACHE"
echo "Source: $RALPH_SOURCE"
exit 1
fi
echo "========================================"
echo " RALPH WIGGUM v${RALPH_VERSION} (${RALPH_SOURCE})"
echo " Autonomous Loop Mode"
echo "========================================"
echo ""
# ===== UV DISCOVERY =====
# Robust UV detection with multi-level fallback (matches canonical cc-skills pattern)
# Returns UV command (path or "mise exec -- uv") on stdout, exits 1 if not found
UV_CMD=""
discover_uv() {
# Priority 1: Already in PATH (shell configured, Homebrew, direct install)
if command -v uv &>/dev/null; then
echo "uv"
return 0
fi
# Priority 2: Common direct installation locations
local uv_locations=(
"$HOME/.local/bin/uv" # Official curl installer
"$HOME/.cargo/bin/uv" # cargo install
"/opt/homebrew/bin/uv" # Homebrew Apple Silicon
"/usr/local/bin/uv" # Homebrew Intel / manual
"$HOME/.local/share/mise/shims/uv" # mise shims
"$HOME/.local/share/mise/installs/uv/latest/uv" # mise direct
)
for loc in "${uv_locations[@]}"; do
if [[ -x "$loc" ]]; then
echo "$loc"
return 0
fi
done
# Priority 3: Find mise-installed uv dynamically (version directories)
local mise_uv_base="$HOME/.local/share/mise/installs/uv"
if [[ -d "$mise_uv_base" ]]; then
local latest_version
latest_version=$(ls -1 "$mise_uv_base" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+' | sort -V | tail -1)
if [[ -n "$latest_version" ]]; then
# Handle nested platform directory (e.g., uv-aarch64-apple-darwin/uv)
local platform_dir
platform_dir=$(ls -1 "$mise_uv_base/$latest_version" 2>/dev/null | head -1)
if [[ -n "$platform_dir" && -x "$mise_uv_base/$latest_version/$platform_dir/uv" ]]; then
echo "$mise_uv_base/$latest_version/$platform_dir/uv"
return 0
fi
# Direct binary
if [[ -x "$mise_uv_base/$latest_version/uv" ]]; then
echo "$mise_uv_base/$latest_version/uv"
return 0
fi
fi
fi
# Priority 4: Use mise exec as fallback
if command -v mise &>/dev/null && mise which uv &>/dev/null 2>&1; then
echo "mise exec -- uv"
return 0
fi
return 1
}
# Discover UV once at script start
if ! UV_CMD=$(discover_uv); then
echo "ERROR: 'uv' is required but not installed."
echo ""
echo "The Stop hook uses 'uv run' to execute loop-until-done.py"
echo ""
echo "Searched locations:"
echo " - PATH (command -v uv)"
echo " - \$HOME/.local/bin/uv"
echo " - /opt/homebrew/bin/uv"
echo " - \$HOME/.local/share/mise/shims/uv"
echo " - \$HOME/.local/share/mise/installs/uv/*/..."
echo ""
echo "Install with one of:"
echo " • curl -LsSf https://astral.sh/uv/install.sh | sh"
echo " • brew install uv"
echo " • mise use -g uv@latest"
exit 1
fi
# ===== STRICT PRE-FLIGHT CHECKS =====
# These checks ensure the loop will actually work before starting
INSTALL_TS_FILE="$HOME/.claude/ralph-hooks-installed-at"
# 1. Check if hooks were installed after session started (restart detection)
if [[ -f "$INSTALL_TS_FILE" ]]; then
INSTALL_TS=$(cat "$INSTALL_TS_FILE")
# Use .claude dir mtime as session start proxy
SESSION_TS=$(stat -f %m "$HOME/.claude" 2>/dev/null || stat -c %Y "$HOME/.claude" 2>/dev/null || echo "0")
# Also check projects dir
if [[ -d "$HOME/.claude/projects" ]]; then
PROJECTS_TS=$(stat -f %m "$HOME/.claude/projects" 2>/dev/null || stat -c %Y "$HOME/.claude/projects" 2>/dev/null || echo "0")
if [[ "$PROJECTS_TS" -gt "$SESSION_TS" ]]; then
SESSION_TS="$PROJECTS_TS"
fi
fi
if [[ "$INSTALL_TS" -gt "$SESSION_TS" ]]; then
echo "ERROR: Hooks were installed AFTER this session started!"
echo ""
echo "The Stop hook won't run until you restart Claude Code."
echo "Installed at: $(date -r "$INSTALL_TS" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -d "@$INSTALL_TS" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")"
echo ""
echo "ACTION: Exit and restart Claude Code, then run /ralph:start again"
exit 1
fi
fi
# 2. UV already verified by discover_uv() above - display discovered path
echo "UV detected: $UV_CMD"
# 3. Verify Python 3.11+ (required for Stop hook)
PY_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "")
if [[ -n "$PY_VERSION" ]]; then
PY_MAJOR="${PY_VERSION%%.*}"
PY_MINOR="${PY_VERSION#*.}"
if [[ "$PY_MAJOR" -lt 3 ]] || [[ "$PY_MAJOR" -eq 3 && "$PY_MINOR" -lt 11 ]]; then
echo "ERROR: Python 3.11+ required (found: $PY_VERSION)"
echo ""
echo "The Stop hook uses Python 3.11+ features."
echo ""
echo "Upgrade with: brew upgrade python@3.11"
exit 1
fi
else
echo "ERROR: Python not found"
echo ""
echo "Install with: brew install python@3.11"
exit 1
fi
# 4. Verify jq is available (required for config management)
if ! command -v jq &>/dev/null; then
echo "ERROR: 'jq' is required but not installed."
echo ""
echo "Install with: brew install jq"
exit 1
fi
# Check if hooks are installed
HOOKS_INSTALLED=false
if command -v jq &>/dev/null && [[ -f "$SETTINGS" ]]; then
HOOK_COUNT=$(jq '[.hooks | to_entries[] | .value[] | .hooks[] | select(.command | contains("'"$MARKER"'"))] | length' "$SETTINGS" 2>/dev/null || echo "0")
if [[ "$HOOK_COUNT" -gt 0 ]]; then
HOOKS_INSTALLED=true
fi
fi
# Warn if hooks not installed
if [[ "$HOOKS_INSTALLED" == "false" ]]; then
echo "WARNING: Hooks not installed!"
echo ""
echo "The loop will NOT work without hooks registered."
echo ""
echo "To fix:"
echo " 1. Run: /ralph:hooks install"
echo " 2. Restart Claude Code"
echo " 3. Run: /ralph:start again"
echo ""
exit 1
fi
# ===== ARGUMENT PARSING =====
# Syntax: /ralph:start [-f <file>] [--poc] [--no-focus] [<task description>...]
ARGS="${ARGUMENTS:-}"
TARGET_FILE=""
POC_MODE=false
NO_FOCUS=false
TASK_PROMPT=""
# Extract -f flag with regex (handles paths without spaces)
if [[ "$ARGS" =~ -f[[:space:]]+([^[:space:]]+) ]]; then
TARGET_FILE="${BASH_REMATCH[1]}"
# Remove -f and path from ARGS for remaining processing
ARGS="${ARGS//-f ${TARGET_FILE}/}"
fi
# Detect --poc flag
if [[ "$ARGS" == *"--poc"* ]]; then
POC_MODE=true
ARGS="${ARGS//--poc/}"
fi
# Detect --production flag (skips preset prompt, uses production defaults)
PRODUCTION_MODE=false
if [[ "$ARGS" == *"--production"* ]]; then
PRODUCTION_MODE=true
ARGS="${ARGS//--production/}"
fi
# Detect --no-focus flag
if [[ "$ARGS" == *"--no-focus"* ]]; then
NO_FOCUS=true
ARGS="${ARGS//--no-focus/}"
fi
# Detect --skip-constraint-scan flag (v3.0.0+)
SKIP_CONSTRAINT_SCAN=false
if [[ "$ARGS" == *"--skip-constraint-scan"* ]]; then
SKIP_CONSTRAINT_SCAN=true
ARGS="${ARGS//--skip-constraint-scan/}"
fi
# Remaining text after flags = task_prompt (trim whitespace)
TASK_PROMPT=$(echo "$ARGS" | xargs 2>/dev/null || echo "$ARGS")
# Resolve relative path to absolute
if [[ -n "$TARGET_FILE" && "$TARGET_FILE" != /* ]]; then
TARGET_FILE="$PROJECT_DIR/$TARGET_FILE"
fi
# Validate file exists (warn but continue)
if [[ -n "$TARGET_FILE" && ! -e "$TARGET_FILE" ]]; then
echo "WARNING: Target file does not exist: $TARGET_FILE"
echo " Loop will proceed but file discovery may be used instead."
echo ""
fi
# ===== STATE MACHINE TRANSITION =====
# State machine: STOPPED → RUNNING → DRAINING → STOPPED
mkdir -p "$PROJECT_DIR/.claude"
# Check current state (if any)
STATE_FILE="$PROJECT_DIR/.claude/ralph-state.json"
CURRENT_STATE="stopped"
if [[ -f "$STATE_FILE" ]]; then
CURRENT_STATE=$(jq -r '.state // "stopped"' "$STATE_FILE" 2>/dev/null || echo "stopped")
fi
# Validate state transition: STOPPED → RUNNING
if [[ "$CURRENT_STATE" != "stopped" ]]; then
echo "ERROR: Loop already in state '$CURRENT_STATE'"
echo " Run /ralph:stop first to reset state"
exit 1
fi
# Transition to RUNNING state
echo '{"state": "running"}' > "$STATE_FILE"
# ERROR TRAP: Reset state if script fails from this point forward
# This prevents orphaned "running" state when setup fails (e.g., adapter detection, config parsing)
cleanup_on_error() {
echo ""
echo "ERROR: Script failed after state transition. Resetting state to 'stopped'."
echo '{"state": "stopped"}' > "$STATE_FILE"
rm -f "$PROJECT_DIR/.claude/loop-enabled"
rm -f "$PROJECT_DIR/.claude/loop-start-timestamp"
rm -f "$PROJECT_DIR/.claude/ralph-config.json"
rm -f "$PROJECT_DIR/.claude/loop-config.json"
exit 1
}
trap cleanup_on_error ERR
# Create legacy markers for backward compatibility
touch "$PROJECT_DIR/.claude/loop-enabled"
date +%s > "$PROJECT_DIR/.claude/loop-start-timestamp"
# Clear previous stop reason cache (new session = fresh slate)
rm -f "$HOME/.claude/ralph-stop-reason.json"
# Build unified config JSON with all configurable values
# Note: --poc and --production flags skip preset prompts (backward compatibility)
if $POC_MODE; then
MIN_HOURS=0.083
MAX_HOURS=0.167
MIN_ITERS=10
MAX_ITERS=20
elif $PRODUCTION_MODE; then
MIN_HOURS=4
MAX_HOURS=9
MIN_ITERS=50
MAX_ITERS=99
else
# Default: Production settings (will be overridden by AskUserQuestion if no preset flag)
MIN_HOURS=${SELECTED_MIN_HOURS:-4}
MAX_HOURS=${SELECTED_MAX_HOURS:-9}
MIN_ITERS=${SELECTED_MIN_ITERS:-50}
MAX_ITERS=${SELECTED_MAX_ITERS:-99}
fi
# ===== STALE GUIDANCE DETECTION =====
# ADR: /docs/adr/2026-01-02-ralph-guidance-freshness-detection.md
# Clear guidance if timestamp is > 24h old (from previous session)
EXISTING_GUIDANCE='{}'
if [[ -f "$PROJECT_DIR/.claude/ralph-config.json" ]]; then
GUIDANCE_TS=$(jq -r '.guidance.timestamp // ""' "$PROJECT_DIR/.claude/ralph-config.json" 2>/dev/null)
if [[ -n "$GUIDANCE_TS" ]]; then
# Parse ISO 8601 timestamp (macOS format)
GUIDANCE_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$GUIDANCE_TS" +%s 2>/dev/null || echo "0")
NOW_EPOCH=$(date +%s)
if [[ "$GUIDANCE_EPOCH" -gt 0 ]]; then
AGE_HOURS=$(( (NOW_EPOCH - GUIDANCE_EPOCH) / 3600 ))
if [[ $AGE_HOURS -gt 24 ]]; then
echo "Clearing stale guidance (${AGE_HOURS}h old, threshold: 24h)"
# Clear stale guidance, keep empty structure
EXISTING_GUIDANCE='{"forbidden": [], "encouraged": [], "timestamp": ""}'
else
# Fresh guidance - preserve it
EXISTING_GUIDANCE=$(jq '.guidance // {}' "$PROJECT_DIR/.claude/ralph-config.json" 2>/dev/null || echo '{}')
fi
else
# Timestamp parse failed - treat as stale
echo "Clearing legacy guidance (no valid timestamp)"
EXISTING_GUIDANCE='{"forbidden": [], "encouraged": [], "timestamp": ""}'
fi
else
# No timestamp - legacy config, treat as stale
echo "Clearing legacy guidance (missing timestamp)"
EXISTING_GUIDANCE='{"forbidden": [], "encouraged": [], "timestamp": ""}'
fi
fi
# Generate unified ralph-config.json (v3.0.0 schema - Pydantic migration)
CONFIG_JSON=$(jq -n \
--arg state "running" \
--argjson poc_mode "$POC_MODE" \
--argjson production_mode "$PRODUCTION_MODE" \
--argjson no_focus "$NO_FOCUS" \
--argjson skip_constraint_scan "$SKIP_CONSTRAINT_SCAN" \
--arg target_file "$TARGET_FILE" \
--arg task_prompt "$TASK_PROMPT" \
--argjson min_hours "$MIN_HOURS" \
--argjson max_hours "$MAX_HOURS" \
--argjson min_iterations "$MIN_ITERS" \
--argjson max_iterations "$MAX_ITERS" \
--argjson existing_guidance "$EXISTING_GUIDANCE" \
'{
version: "3.0.0",
state: $state,
poc_mode: $poc_mode,
production_mode: $production_mode,
no_focus: $no_focus,
skip_constraint_scan: $skip_constraint_scan,
loop_limits: {
min_hours: $min_hours,
max_hours: $max_hours,
min_iterations: $min_iterations,
max_iterations: $max_iterations
}
}
+ (if $target_file != "" then {target_file: $target_file} else {} end)
+ (if $task_prompt != "" then {task_prompt: $task_prompt} else {} end)
+ (if $existing_guidance != {} then {guidance: $existing_guidance} else {} end)'
)
echo "$CONFIG_JSON" > "$PROJECT_DIR/.claude/ralph-config.json"
# Legacy config for backward compatibility
LEGACY_CONFIG=$(jq -n \
--argjson min_hours "$MIN_HOURS" \
--argjson max_hours "$MAX_HOURS" \
--argjson min_iterations "$MIN_ITERS" \
--argjson max_iterations "$MAX_ITERS" \
--argjson no_focus "$NO_FOCUS" \
--arg target_file "$TARGET_FILE" \
--arg task_prompt "$TASK_PROMPT" \
'{
min_hours: $min_hours,
max_hours: $max_hours,
min_iterations: $min_iterations,
max_iterations: $max_iterations,
no_focus: $no_focus
}
+ (if $target_file != "" then {target_file: $target_file} else {} end)
+ (if $task_prompt != "" then {task_prompt: $task_prompt} else {} end)'
)
echo "$LEGACY_CONFIG" > "$PROJECT_DIR/.claude/loop-config.json"
# ===== ADAPTER DETECTION =====
# Detect project-specific adapter using Python
# Use same path logic as version detection above
if [[ -d "$RALPH_CACHE/local" ]]; then
HOOKS_DIR="$RALPH_CACHE/local/hooks"
else
HOOKS_DIR="$RALPH_CACHE/$RALPH_VERSION/hooks"
fi
ADAPTER_NAME=""
if [[ -d "$HOOKS_DIR" ]]; then
ADAPTER_NAME=$(cd "$HOOKS_DIR" && python3 -c "
import sys
from pathlib import Path
sys.path.insert(0, '.')
try:
from core.registry import AdapterRegistry
AdapterRegistry.discover(Path('adapters'))
adapter = AdapterRegistry.get_adapter(Path('$PROJECT_DIR'))
if adapter:
print(adapter.name)
else:
print('')
except Exception:
print('')
" 2>/dev/null || echo "")
fi
# ===== STATUS OUTPUT =====
if $POC_MODE; then
echo "Ralph Loop: POC MODE"
echo "Time limits: 5 min minimum / 10 min maximum"
echo "Iterations: 10 minimum / 20 maximum"
elif $PRODUCTION_MODE; then
echo "Ralph Loop: PRODUCTION MODE (via --production flag)"
echo "Time limits: 4h minimum / 9h maximum"
echo "Iterations: 50 minimum / 99 maximum"
else
echo "Ralph Loop: PRODUCTION MODE"
echo "Time limits: ${MIN_HOURS}h minimum / ${MAX_HOURS}h maximum"
echo "Iterations: ${MIN_ITERS} minimum / ${MAX_ITERS} maximum"
fi
echo ""
if [[ "$ADAPTER_NAME" == "alpha-forge" ]]; then
echo "Adapter: alpha-forge"
echo " → Expert-synthesis convergence (WFE, diminishing returns, patience)"
echo " → Reads metrics from outputs/runs/*/summary.json"
else
echo "⚠️ WARNING: Not an Alpha Forge project"
echo " → Ralph hooks will SKIP this project (v8.0.2+)"
echo " → Ralph is designed exclusively for Alpha Forge ML workflows"
echo " → Detection: pyproject.toml, packages/alpha-forge-core/, outputs/runs/"
fi
if $NO_FOCUS; then
echo ""
echo "Focus mode: DISABLED (100% autonomous, no plan tracking)"
elif [[ -n "$TARGET_FILE" ]]; then
echo ""
echo "Target file: $TARGET_FILE"
fi
if [[ -n "$TASK_PROMPT" ]]; then
echo ""
echo "Task: $TASK_PROMPT"
fi
echo ""
echo "State: RUNNING (was: $CURRENT_STATE)"
echo "Config: $PROJECT_DIR/.claude/ralph-config.json"
echo ""
echo "To stop: /ralph:stop"
echo "Kill switch: touch $PROJECT_DIR/.claude/STOP_LOOP"
echo ""
echo "Note: If you just installed hooks, restart Claude Code for them to take effect."
RALPH_START_SCRIPT
Run the bash script above to enable loop mode.
If this is an Alpha Forge project (detected by outputs/research_sessions/ existing):
After enabling loop mode, begin the OODA cycle immediately:
outputs/research_sessions/*/research_summary.md (most recent)outputs/research_sessions/*/research_log.md for expert recommendationsROADMAP.md for current P0/P1 prioritiesUse the checkpoint gate:
Invoke /research with the appropriate strategy:
/research <path/to/strategy.yaml> --iterations=5 --objective=sharpe
Ralph is supplementary to alpha-forge's /research:
/research owns the inner loop (5 iterations, 5 expert subagents)Do NOT ask the user what to work on. Proceed autonomously through OODA.
/startInitiates the task orchestration workflow using the three-agent system (task-orchestrator, task-decomposer, and dependency-analyzer) to create a comprehensive execution plan.