Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .agents/ralph/PROMPT_simplified.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Build Task

You are an autonomous coding agent. Implement the assigned story completely.

## Configuration

- Plan: {{PLAN_PATH}}
- Progress: {{PROGRESS_PATH}}
- PRD: {{PRD_NUMBER}}
- Story: {{STORY_ID}}

## Rules (Non-Negotiable)

1. **Scope**: Implement ONLY the selected story - nothing more, nothing less
2. **No Questions**: Do NOT ask the user questions - make reasonable decisions
3. **Complete Work**: No placeholders, no stubs, no TODOs - implement completely
4. **Verify First**: Read existing code before modifying - don't assume
5. **Commit**: Commit your changes when the story is complete

## What You CAN Do

- Read any file in the codebase
- Run tests: `npm test`, `pytest`, `go test`, etc.
- Run builds: `npm run build`, `cargo build`, etc.
- Create, edit, and delete files as needed for the story
- Git operations: `git add`, `git commit`, `git status`

## What You CANNOT Do

- Push to remote (`git push` is blocked)
- Merge branches (`git merge` to main is blocked)
- Skip tests or disable linting
- Modify files outside the project scope
- Use `--force` on git commands

## Completion Criteria

The story is complete when:
1. All acceptance criteria from the story are met
2. Code compiles/builds without errors
3. Tests pass (if applicable)
4. Changes are committed

When complete, output: `<promise>COMPLETE</promise>`

36 changes: 35 additions & 1 deletion .agents/ralph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,49 @@ ralph stream build 1 5 --model=haiku # Force Haiku for stream

If you try to use Codex/Droid with routing enabled, Ralph will warn and auto-disable routing.

## Simplified Loop (Experimental)

Ralph includes a simplified loop that delegates enforcement to Claude Code hooks:

```bash
# Run simplified loop
./simplified-loop.sh 5 # 5 iterations
./simplified-loop.sh --prd=2 # Specific PRD
./simplified-loop.sh --dry-run # Validate without running
```

### Hooks

| Hook | Event | Purpose |
|------|-------|---------|
| `hooks/pre-tool.sh` | PreToolUse | Block Edit without Read, block git push/merge |
| `hooks/post-tool.sh` | PostToolUse | Detect test failures, trigger rollback |
| `hooks/on-stop.sh` | Stop | Validate completion, cleanup |
| `hooks/pre-prompt.sh` | UserPromptSubmit | Reset session tracking |

**Key rule:** All hooks MUST exit 0 (even on errors) to avoid breaking Claude Code.

### Test Failure Detection

The post-tool hook detects failures from: Jest, Vitest, pytest, Go test, Mocha, RSpec, Bats.

## File Structure

```
project/
├── .agents/ralph/ # Templates (customizable)
│ ├── loop.sh # Main execution loop
│ ├── loop.sh # Main execution loop (4,244 lines)
│ ├── simplified-loop.sh # Simplified loop (~200 lines)
│ ├── stream.sh # Multi-stream commands
│ ├── config.sh # Configuration overrides
│ ├── agents.sh # Agent command definitions
│ ├── hooks/ # Claude Code hooks
│ │ ├── pre-tool.sh # PreToolUse validation
│ │ ├── post-tool.sh # PostToolUse enforcement
│ │ ├── on-stop.sh # Stop cleanup
│ │ └── pre-prompt.sh # Session management
│ ├── lib/
│ │ └── minimal.sh # Minimal utilities for simplified loop
│ └── PROMPT_*.md # Prompt templates
└── .ralph/ # State (per-project)
├── PRD-1/ # First plan (isolated)
Expand Down
92 changes: 92 additions & 0 deletions .agents/ralph/hooks/on-stop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/bin/bash
# ─────────────────────────────────────────────────────────────────────────────
# Stop Hook - Validate completion state when Claude Code session ends
# ─────────────────────────────────────────────────────────────────────────────
# CRITICAL: This hook must ALWAYS exit 0 to avoid breaking Claude Code.
#
# Receives: JSON via stdin with stop_reason
# Actions:
# - Check if current story was completed
# - Log incomplete state for resumption
# - Cleanup temporary files
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail

# Error trap: always exit 0
trap 'exit 0' ERR

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RALPH_DIR="${RALPH_ROOT:-$(pwd)}/.ralph"
CURRENT_STORY_FILE="$RALPH_DIR/current-story"
SESSIONS_LOG="$RALPH_DIR/sessions.log"

# ─────────────────────────────────────────────────────────────────────────────
# Read hook data from stdin
# ─────────────────────────────────────────────────────────────────────────────
hook_data=""
if [[ ! -t 0 ]]; then
hook_data=$(cat)
fi

# Validate JSON if present
stop_reason=""
if [[ -n "$hook_data" ]]; then
if echo "$hook_data" | jq -e . >/dev/null 2>&1; then
stop_reason=$(echo "$hook_data" | jq -r '.stop_reason // empty')
fi
fi

# ─────────────────────────────────────────────────────────────────────────────
# Check completion state
# ─────────────────────────────────────────────────────────────────────────────
if [[ -f "$CURRENT_STORY_FILE" ]]; then
story_id=$(cat "$CURRENT_STORY_FILE" 2>/dev/null || echo "")

if [[ -n "$story_id" ]]; then
# Find the plan file (try multiple PRD directories)
plan_path=""
for prd_dir in "$RALPH_DIR"/PRD-*; do
if [[ -f "$prd_dir/plan.md" ]]; then
plan_path="$prd_dir/plan.md"
break
fi
done

# Validate story ID format (defense against regex injection)
if ! [[ "$story_id" =~ ^US-[0-9]+$ ]]; then
# Invalid story ID format, skip processing
rm -f "$CURRENT_STORY_FILE" 2>/dev/null || true
exit 0
fi

# Check if story was marked complete
story_completed=false
if [[ -n "$plan_path" ]] && [[ -f "$plan_path" ]]; then
# Use fixed-string grep for security (prevents regex injection)
if grep -qF "[x] " "$plan_path" 2>/dev/null && grep -qF "$story_id" "$plan_path" 2>/dev/null; then
# More precise check: story line has [x] and contains story ID
# Use grep -F for fixed string matching to prevent regex injection
if grep -F "$story_id" "$plan_path" 2>/dev/null | grep -qE '^\s*-\s*\[x\]'; then
story_completed=true
fi
fi
fi

# Log incomplete state for potential resumption
if [[ "$story_completed" == "false" ]]; then
mkdir -p "$(dirname "$SESSIONS_LOG")" 2>/dev/null || true
{
echo "incomplete:$story_id:$(date +%s):${stop_reason:-unknown}"
} >> "$SESSIONS_LOG" 2>/dev/null || true
fi
fi
fi

# ─────────────────────────────────────────────────────────────────────────────
# Cleanup temporary files
# ─────────────────────────────────────────────────────────────────────────────
rm -f "$CURRENT_STORY_FILE" 2>/dev/null || true
rm -f "$RALPH_DIR/.checkpoint" 2>/dev/null || true
rm -f "$RALPH_DIR/session.log" 2>/dev/null || true

exit 0
152 changes: 152 additions & 0 deletions .agents/ralph/hooks/post-tool.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/bin/bash
# ─────────────────────────────────────────────────────────────────────────────
# PostToolUse Hook - Enforce after tool execution
# ─────────────────────────────────────────────────────────────────────────────
# CRITICAL: This hook must ALWAYS exit 0 to avoid breaking Claude Code.
#
# Receives: JSON via stdin with tool_name, tool_output
# Actions:
# - Detect test failures from Bash output
# - Trigger rollback on test failure
# - Log failure context for retry
#
# Supports test frameworks: Jest, Vitest, pytest, Go test, Mocha, RSpec, Bats
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail

# Error trap: always exit 0
trap 'exit 0' ERR

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RALPH_DIR="${RALPH_ROOT:-$(pwd)}/.ralph"
CHECKPOINT_FILE="$RALPH_DIR/.checkpoint"
FAILURE_LOG="$RALPH_DIR/failure-context.log"
MAX_FAILURE_LOG_SIZE=100000 # 100KB

# ─────────────────────────────────────────────────────────────────────────────
# Read hook data from stdin
# ─────────────────────────────────────────────────────────────────────────────
hook_data=""
if [[ ! -t 0 ]]; then
hook_data=$(cat)
fi

# Empty input: nothing to do
if [[ -z "$hook_data" ]]; then
exit 0
fi

# Validate JSON
if ! echo "$hook_data" | jq -e . >/dev/null 2>&1; then
exit 0
fi

# ─────────────────────────────────────────────────────────────────────────────
# Extract fields
# ─────────────────────────────────────────────────────────────────────────────
tool_name=$(echo "$hook_data" | jq -r '.tool_name // empty')
tool_output=$(echo "$hook_data" | jq -r '.tool_output // ""')

# Only process Bash tool output
if [[ "$tool_name" != "Bash" ]]; then
exit 0
fi

# ─────────────────────────────────────────────────────────────────────────────
# Test failure detection (comprehensive multi-framework patterns)
# ─────────────────────────────────────────────────────────────────────────────
# Pattern 1: Universal failure indicators
# FAIL, FAILED, ✗, ✕, not ok, AssertionError, Failures:, failed,
#
# Pattern 2: Framework-specific
# Jest: "FAIL src/", "Tests: X failed"
# Vitest: "× FAIL", "Tests X failed"
# pytest: "FAILED", "= FAILURES =", "X failed,"
# Go: "--- FAIL:", "FAIL"
# Mocha: "failing", "✗ X)"
# RSpec: "Failures:", "F..", "X failures"
# Bats: "✗", "not ok"
# ─────────────────────────────────────────────────────────────────────────────

# Check for failure patterns
is_test_failure=false

# Framework-specific detection patterns (more reliable than universal)

# Jest: "Tests: X failed" or "Test Suites: X failed"
if [[ "$tool_output" =~ Tests:.*[0-9]+.*failed ]] || [[ "$tool_output" =~ Test\ Suites:.*[0-9]+.*failed ]]; then
is_test_failure=true
fi

# Vitest: "Test Files X failed" or "Tests X failed |" (note double spaces)
if [[ "$tool_output" =~ Test\ Files.*[0-9]+\ failed ]] || [[ "$tool_output" =~ Tests.*[0-9]+\ failed\ \| ]]; then
is_test_failure=true
fi

# pytest: "= FAILURES =" or "X failed," at end of summary
if [[ "$tool_output" =~ =.*FAILURES.*= ]] || [[ "$tool_output" =~ [0-9]+\ failed, ]]; then
is_test_failure=true
fi

# Go test: "--- FAIL:" or line starting with "FAIL<tab>package"
if [[ "$tool_output" =~ ---\ FAIL: ]] || [[ "$tool_output" =~ FAIL$'\t' ]]; then
is_test_failure=true
fi

# Mocha: "X failing"
if [[ "$tool_output" =~ [0-9]+\ failing ]]; then
is_test_failure=true
fi

# RSpec: "X examples, Y failures"
if [[ "$tool_output" =~ [0-9]+\ examples,.*[0-9]+\ failure ]]; then
is_test_failure=true
fi

# Bats: "not ok" at start of line
if [[ "$tool_output" =~ $'\n'not\ ok ]] || [[ "$tool_output" =~ ^not\ ok ]]; then
is_test_failure=true
fi

# npm/yarn test failure (catches any test runner run via npm)
if [[ "$tool_output" =~ npm\ ERR!\ Test\ failed ]] || [[ "$tool_output" =~ error\ Command\ failed ]]; then
is_test_failure=true
fi

# ─────────────────────────────────────────────────────────────────────────────
# Handle test failure: rollback and log
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$is_test_failure" == "true" ]]; then
# Get checkpoint SHA for rollback
checkpoint_sha=""
if [[ -f "$CHECKPOINT_FILE" ]]; then
checkpoint_sha=$(cat "$CHECKPOINT_FILE" 2>/dev/null || echo "")
fi

# Perform rollback
if [[ -n "$checkpoint_sha" ]]; then
git reset --hard "$checkpoint_sha" 2>/dev/null || true
fi

# Log failure context for retry (helps agent avoid repeating mistakes)
mkdir -p "$(dirname "$FAILURE_LOG")" 2>/dev/null || true
{
echo ""
echo "=== Test Failure $(date -Iseconds) ==="
echo "Checkpoint: $checkpoint_sha"
# Limit output to 100 lines to prevent log bloat
echo "$tool_output" | tail -100
echo "=== End Failure ==="
} >> "$FAILURE_LOG" 2>/dev/null || true

# Rotate log if too large
if [[ -f "$FAILURE_LOG" ]]; then
log_size=$(wc -c < "$FAILURE_LOG" 2>/dev/null || echo 0)
if [[ "$log_size" -gt "$MAX_FAILURE_LOG_SIZE" ]]; then
tail -500 "$FAILURE_LOG" > "${FAILURE_LOG}.tmp" 2>/dev/null || true
mv "${FAILURE_LOG}.tmp" "$FAILURE_LOG" 2>/dev/null || true
fi
fi
fi

exit 0
55 changes: 55 additions & 0 deletions .agents/ralph/hooks/pre-prompt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/bash
# ─────────────────────────────────────────────────────────────────────────────
# UserPromptSubmit Hook - Runs when user submits a prompt
# ─────────────────────────────────────────────────────────────────────────────
# CRITICAL: This hook must ALWAYS exit 0 to avoid breaking Claude Code.
#
# Receives: JSON via stdin with prompt content
# Actions:
# - Log prompt submission timestamp
# - Clear session log for new conversation
# - Optional: trigger TTS notification
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail

# Error trap: always exit 0
trap 'exit 0' ERR

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RALPH_DIR="${RALPH_ROOT:-$(pwd)}/.ralph"
SESSION_LOG="$RALPH_DIR/session.log"
ACTIVITY_LOG="$RALPH_DIR/activity.log"

# ─────────────────────────────────────────────────────────────────────────────
# Read hook data from stdin
# ─────────────────────────────────────────────────────────────────────────────
hook_data=""
if [[ ! -t 0 ]]; then
hook_data=$(cat)
fi

# ─────────────────────────────────────────────────────────────────────────────
# Start new session: clear read tracking log
# ─────────────────────────────────────────────────────────────────────────────
# This ensures Edit validation is per-conversation, not accumulated
mkdir -p "$(dirname "$SESSION_LOG")" 2>/dev/null || true
: > "$SESSION_LOG" 2>/dev/null || true

# ─────────────────────────────────────────────────────────────────────────────
# Log activity (optional)
# ─────────────────────────────────────────────────────────────────────────────
if [[ -n "$hook_data" ]]; then
# Extract prompt length for logging (don't log actual content for privacy)
prompt_length=0
if echo "$hook_data" | jq -e . >/dev/null 2>&1; then
prompt=$(echo "$hook_data" | jq -r '.prompt // ""')
prompt_length=${#prompt}
fi

mkdir -p "$(dirname "$ACTIVITY_LOG")" 2>/dev/null || true
{
echo "$(date -Iseconds) prompt_submit length=$prompt_length"
} >> "$ACTIVITY_LOG" 2>/dev/null || true
fi

exit 0
Loading
Loading