feat(enable): add ralph-enable wizard for existing projects (v0.11.0) (#124)

* feat(enable): add ralph-enable wizard for existing projects (v0.11.0)

Add interactive wizard and CI version for enabling Ralph in existing projects.

New commands:
- ralph-enable: Interactive 5-phase wizard for humans
- ralph-enable-ci: Non-interactive version with JSON output for CI/automation

New library components:
- lib/enable_core.sh: Shared logic for idempotency, project detection, templates
- lib/wizard_utils.sh: Interactive prompt utilities
- lib/task_sources.sh: Task import from beads, GitHub Issues, PRD documents

Features:
- Auto-detects project type (TypeScript, Python, Rust, Go)
- Auto-detects framework (Next.js, FastAPI, Django, Express)
- Imports tasks from beads, GitHub Issues, or PRD documents
- Generates .ralphrc project configuration file
- Idempotent: safe to run multiple times, respects existing files
- Exit codes: 0 (success), 1 (error), 2 (already enabled)

Updated:
- install.sh: Added new commands to global installation
- ralph_loop.sh: Loads .ralphrc configuration at startup

Tests: 75 new tests (30 enable_core + 23 task_sources + 22 integration)
Total: 396 tests passing (100% pass rate)

Closes #85, #121, #64, #87, #99

* fix(enable): address code review feedback

Fixes from PR #124 review:

1. sed -i portability (ralph_enable.sh:456)
   - Use portable sed + mv pattern instead of GNU-only sed -i

2. sed regex portability (lib/task_sources.sh)
   - Replace \s with POSIX [[:space:]] character class
   - Add sed -E flag for extended regex

3. jq availability check (ralph_enable_ci.sh:177)
   - Add check for jq when --json flag is used

4. Unused filter parameter (lib/task_sources.sh:44)
   - Pass filter to bd list --filter command

5. Word-splitting in select_multiple (ralph_enable.sh:322)
   - Return comma-separated indices instead of space-separated text
   - Update caller to parse indices correctly

6. Missing || true for check_existing_ralph (ralph_enable.sh:185)
   - Prevent set -e from exiting on non-zero return

7. select_multiple stdout corruption (lib/wizard_utils.sh)
   - Redirect interactive output to stderr
   - Only final result goes to stdout

8. Color variables not exported (lib/wizard_utils.sh:12)
   - Export WIZARD_* color variables for subshells

9. select_option infinite loop (lib/wizard_utils.sh:179)
   - Add guard for empty options array

* fix(tests): add missing mocks and exports for new enable feature

- Add RESPONSE_ANALYSIS_FILE export to test_session_continuity.bats setup
- Add mock ralph_enable.sh and ralph_enable_ci.sh to test_installation.bats
- Add mock lib files: enable_core.sh, wizard_utils.sh, task_sources.sh, timeout_utils.sh

All 396 tests now pass.

* fix(config): fix critical issues from PR review

1. .ralphrc Configuration Loading Fix:
   - Captured env var state BEFORE setting defaults with _env_* variables
   - load_ralphrc now only restores values explicitly set by environment
   - .ralphrc settings are now properly applied (not overwritten by defaults)

2. sed Command Injection Fix:
   - Replaced sed with awk for .ralphrc updates in ralph_enable.sh
   - awk -v pattern safely handles user input without shell injection risk

3. Shell Injection Fix in safe_create_file():
   - Replaced echo with printf '%s\n' for safer content handling
   - Prevents issues with backslashes, -n, and special characters

4. Specific Error Codes:
   - Added ENABLE_INVALID_ARGS=3 for argument errors
   - Added ENABLE_FILE_NOT_FOUND=4 for missing files
   - Added ENABLE_DEPENDENCY_MISSING=5 for missing deps (e.g., jq)
   - Added ENABLE_PERMISSION_DENIED=6 for permission errors
   - Updated ralph_enable.sh and ralph_enable_ci.sh to use specific codes

5. Added tests for .ralphrc loading pattern verification

Test count: 398 (up from 396)

* fix(enable): make --force flag actually overwrite existing files

The --force flag was accepted but safe_create_file() always skipped
existing files regardless of ENABLE_FORCE value.

Changes:
- safe_create_file() now checks ENABLE_FORCE environment variable
- When ENABLE_FORCE="true", overwrites existing files instead of skipping
- Added proper logging for overwrite operations

Added tests:
- Verify enable_ralph_in_directory actually changes file contents with --force
- Test safe_create_file overwrites when ENABLE_FORCE is true
- Test safe_create_file skips when ENABLE_FORCE is false

Test count: 400 (up from 398)

---------

Co-authored-by: Test User <test@example.com>
This commit is contained in:
Frank Bria 2026-01-25 14:37:25 -07:00 committed by GitHub
parent 019b8c738a
commit 910f794fcc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 4169 additions and 18 deletions

View file

@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
This is the Ralph for Claude Code repository - an autonomous AI development loop system that enables continuous development cycles with intelligent exit detection and rate limiting. This is the Ralph for Claude Code repository - an autonomous AI development loop system that enables continuous development cycles with intelligent exit detection and rate limiting.
**Version**: v0.10.1 | **Tests**: 321 passing (100% pass rate) | **CI/CD**: GitHub Actions **Version**: v0.11.0 | **Tests**: 396 passing (100% pass rate) | **CI/CD**: GitHub Actions
## Core Architecture ## Core Architecture
@ -22,6 +22,14 @@ The system consists of four main bash scripts and a modular library system:
- Uses modern Claude Code CLI with `--output-format json` for structured responses - Uses modern Claude Code CLI with `--output-format json` for structured responses
- Implements `detect_response_format()` and `parse_conversion_response()` for JSON parsing - Implements `detect_response_format()` and `parse_conversion_response()` for JSON parsing
- Backward compatible with older CLI versions (automatic text fallback) - Backward compatible with older CLI versions (automatic text fallback)
6. **ralph_enable.sh** - Interactive wizard for enabling Ralph in existing projects
- Multi-step wizard with environment detection, task source selection, configuration
- Imports tasks from beads, GitHub Issues, or PRD documents
- Generates `.ralphrc` project configuration file
7. **ralph_enable_ci.sh** - Non-interactive version for CI/automation
- Same functionality as interactive version with CLI flags
- JSON output mode for machine parsing
- Exit codes: 0 (success), 1 (error), 2 (already enabled)
### Library Components (lib/) ### Library Components (lib/)
@ -59,6 +67,23 @@ The system uses a modular architecture with reusable components in the `lib/` di
- `portable_timeout()` function for seamless cross-platform execution - `portable_timeout()` function for seamless cross-platform execution
- Automatic detection with caching for performance - Automatic detection with caching for performance
5. **lib/enable_core.sh** - Shared logic for ralph enable commands
- Idempotency checks: `check_existing_ralph()`, `is_ralph_enabled()`
- Safe file operations: `safe_create_file()`, `safe_create_dir()`
- Project detection: `detect_project_context()`, `detect_git_info()`, `detect_task_sources()`
- Template generation: `generate_prompt_md()`, `generate_agent_md()`, `generate_fix_plan_md()`, `generate_ralphrc()`
6. **lib/wizard_utils.sh** - Interactive prompt utilities for enable wizard
- User prompts: `confirm()`, `prompt_text()`, `prompt_number()`
- Selection utilities: `select_option()`, `select_multiple()`, `select_with_default()`
- Output formatting: `print_header()`, `print_bullet()`, `print_success/warning/error/info()`
7. **lib/task_sources.sh** - Task import from external sources
- Beads integration: `check_beads_available()`, `fetch_beads_tasks()`, `get_beads_count()`
- GitHub integration: `check_github_available()`, `fetch_github_tasks()`, `get_github_issue_count()`
- PRD extraction: `extract_prd_tasks()`, supports checkbox and numbered list formats
- Task normalization: `normalize_tasks()`, `prioritize_tasks()`, `import_tasks_from_sources()`
## Key Commands ## Key Commands
### Installation ### Installation
@ -84,6 +109,27 @@ cd existing-project
ralph-migrate ralph-migrate
``` ```
### Enabling Ralph in Existing Projects
```bash
# Interactive wizard (recommended for humans)
cd existing-project
ralph-enable
# With specific task source
ralph-enable --from beads
ralph-enable --from github --label "sprint-1"
ralph-enable --from prd ./docs/requirements.md
# Force overwrite existing .ralph/
ralph-enable --force
# Non-interactive for CI/scripts
ralph-enable-ci # Sensible defaults
ralph-enable-ci --from github # With task source
ralph-enable-ci --project-type typescript # Override detection
ralph-enable-ci --json # Machine-readable output
```
### Running the Ralph Loop ### Running the Ralph Loop
```bash ```bash
# Start with integrated tmux monitoring (recommended) # Start with integrated tmux monitoring (recommended)
@ -121,7 +167,7 @@ tmux attach -t <session-name>
### Running Tests ### Running Tests
```bash ```bash
# Run all tests (165 tests) # Run all tests (396 tests)
npm test npm test
# Run specific test suites # Run specific test suites
@ -132,6 +178,9 @@ npm run test:integration
bats tests/unit/test_cli_parsing.bats bats tests/unit/test_cli_parsing.bats
bats tests/unit/test_json_parsing.bats bats tests/unit/test_json_parsing.bats
bats tests/unit/test_cli_modern.bats bats tests/unit/test_cli_modern.bats
bats tests/unit/test_enable_core.bats
bats tests/unit/test_task_sources.bats
bats tests/unit/test_ralph_enable.bats
``` ```
## Ralph Loop Configuration ## Ralph Loop Configuration
@ -264,10 +313,10 @@ Templates in `templates/` provide starting points for new projects:
## Global Installation ## Global Installation
Ralph installs to: Ralph installs to:
- **Commands**: `~/.local/bin/` (ralph, ralph-monitor, ralph-setup, ralph-import, ralph-migrate) - **Commands**: `~/.local/bin/` (ralph, ralph-monitor, ralph-setup, ralph-import, ralph-migrate, ralph-enable, ralph-enable-ci)
- **Templates**: `~/.ralph/templates/` - **Templates**: `~/.ralph/templates/`
- **Scripts**: `~/.ralph/` (ralph_loop.sh, ralph_monitor.sh, setup.sh, ralph_import.sh, migrate_to_ralph_folder.sh) - **Scripts**: `~/.ralph/` (ralph_loop.sh, ralph_monitor.sh, setup.sh, ralph_import.sh, migrate_to_ralph_folder.sh, ralph_enable.sh, ralph_enable_ci.sh)
- **Libraries**: `~/.ralph/lib/` (circuit_breaker.sh, response_analyzer.sh, date_utils.sh, timeout_utils.sh) - **Libraries**: `~/.ralph/lib/` (circuit_breaker.sh, response_analyzer.sh, date_utils.sh, timeout_utils.sh, enable_core.sh, wizard_utils.sh, task_sources.sh)
After installation, the following global commands are available: After installation, the following global commands are available:
- `ralph` - Start the autonomous development loop - `ralph` - Start the autonomous development loop
@ -275,6 +324,8 @@ After installation, the following global commands are available:
- `ralph-setup` - Create a new Ralph-managed project - `ralph-setup` - Create a new Ralph-managed project
- `ralph-import` - Import PRD/specification documents to Ralph format - `ralph-import` - Import PRD/specification documents to Ralph format
- `ralph-migrate` - Migrate existing projects from flat structure to `.ralph/` subfolder - `ralph-migrate` - Migrate existing projects from flat structure to `.ralph/` subfolder
- `ralph-enable` - Interactive wizard to enable Ralph in existing projects
- `ralph-enable-ci` - Non-interactive version for CI/automation
## Integration Points ## Integration Points
@ -351,7 +402,7 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false
## Test Suite ## Test Suite
### Test Files (265 tests total) ### Test Files (396 tests total)
| File | Tests | Description | | File | Tests | Description |
|------|-------|-------------| |------|-------|-------------|
@ -366,6 +417,9 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false
| `test_installation.bats` | 14 | Global installation/uninstall workflows | | `test_installation.bats` | 14 | Global installation/uninstall workflows |
| `test_project_setup.bats` | 36 | Project setup (setup.sh) validation | | `test_project_setup.bats` | 36 | Project setup (setup.sh) validation |
| `test_prd_import.bats` | 33 | PRD import (ralph_import.sh) workflows + modern CLI tests | | `test_prd_import.bats` | 33 | PRD import (ralph_import.sh) workflows + modern CLI tests |
| `test_enable_core.bats` | 30 | Enable core library (idempotency, project detection, template generation) |
| `test_task_sources.bats` | 23 | Task sources (beads, GitHub, PRD extraction, normalization) |
| `test_ralph_enable.bats` | 22 | Ralph enable integration tests (wizard, CI version, JSON output) |
### Running Tests ### Running Tests
```bash ```bash
@ -381,6 +435,25 @@ bats tests/unit/test_cli_parsing.bats
## Recent Improvements ## Recent Improvements
### Ralph Enable Command (v0.11.0)
- Added `ralph-enable` interactive wizard for enabling Ralph in existing projects
- 5-phase wizard: Environment Detection → Task Source Selection → Configuration → File Generation → Verification
- Auto-detects project type (TypeScript, Python, Rust, Go) and framework (Next.js, FastAPI, Django)
- Imports tasks from beads, GitHub Issues, or PRD documents
- Generates `.ralphrc` project configuration file
- Added `ralph-enable-ci` non-interactive version for CI/automation
- JSON output mode (`--json`) for machine parsing
- Exit codes: 0 (success), 1 (error), 2 (already enabled)
- Override flags: `--project-name`, `--project-type`, `--from`, `--force`
- New library components:
- `lib/enable_core.sh` - Shared enable logic with idempotency checks
- `lib/wizard_utils.sh` - Interactive prompt utilities
- `lib/task_sources.sh` - Task import from beads/GitHub/PRD
- Updated `ralph_loop.sh` to load `.ralphrc` configuration at startup
- Added 75 new tests (30 enable_core + 23 task_sources + 22 integration)
- Test count: 396 (up from 321)
- Related issues: #85, #121, #64, #87, #99
### Stale Completion Indicators Fix (v0.10.1) - Issue #91 ### Stale Completion Indicators Fix (v0.10.1) - Issue #91
- Fixed premature exit caused by stale completion indicators persisting across sessions - Fixed premature exit caused by stale completion indicators persisting across sessions
- Root cause: `.exit_signals` and `.response_analysis` files retained old completion counts - Root cause: `.exit_signals` and `.response_analysis` files retained old completion counts

View file

@ -165,6 +165,28 @@ EOF
RALPH_HOME="$HOME/.ralph" RALPH_HOME="$HOME/.ralph"
exec "$RALPH_HOME/migrate_to_ralph_folder.sh" "$@" exec "$RALPH_HOME/migrate_to_ralph_folder.sh" "$@"
EOF
# Create ralph-enable command (interactive wizard)
cat > "$INSTALL_DIR/ralph-enable" << 'EOF'
#!/bin/bash
# Ralph Enable - Interactive Wizard for Existing Projects
# Adds Ralph configuration to an existing codebase
RALPH_HOME="$HOME/.ralph"
exec "$RALPH_HOME/ralph_enable.sh" "$@"
EOF
# Create ralph-enable-ci command (non-interactive)
cat > "$INSTALL_DIR/ralph-enable-ci" << 'EOF'
#!/bin/bash
# Ralph Enable CI - Non-Interactive Version for Automation
# Adds Ralph configuration with sensible defaults
RALPH_HOME="$HOME/.ralph"
exec "$RALPH_HOME/ralph_enable_ci.sh" "$@"
EOF EOF
# Copy actual script files to Ralph home with modifications for global operation # Copy actual script files to Ralph home with modifications for global operation
@ -176,15 +198,23 @@ EOF
# Copy migration script to Ralph home # Copy migration script to Ralph home
cp "$SCRIPT_DIR/migrate_to_ralph_folder.sh" "$RALPH_HOME/" cp "$SCRIPT_DIR/migrate_to_ralph_folder.sh" "$RALPH_HOME/"
# Copy enable scripts to Ralph home
cp "$SCRIPT_DIR/ralph_enable.sh" "$RALPH_HOME/"
cp "$SCRIPT_DIR/ralph_enable_ci.sh" "$RALPH_HOME/"
# Make all commands executable # Make all commands executable
chmod +x "$INSTALL_DIR/ralph" chmod +x "$INSTALL_DIR/ralph"
chmod +x "$INSTALL_DIR/ralph-monitor" chmod +x "$INSTALL_DIR/ralph-monitor"
chmod +x "$INSTALL_DIR/ralph-setup" chmod +x "$INSTALL_DIR/ralph-setup"
chmod +x "$INSTALL_DIR/ralph-import" chmod +x "$INSTALL_DIR/ralph-import"
chmod +x "$INSTALL_DIR/ralph-migrate" chmod +x "$INSTALL_DIR/ralph-migrate"
chmod +x "$INSTALL_DIR/ralph-enable"
chmod +x "$INSTALL_DIR/ralph-enable-ci"
chmod +x "$RALPH_HOME/ralph_monitor.sh" chmod +x "$RALPH_HOME/ralph_monitor.sh"
chmod +x "$RALPH_HOME/ralph_import.sh" chmod +x "$RALPH_HOME/ralph_import.sh"
chmod +x "$RALPH_HOME/migrate_to_ralph_folder.sh" chmod +x "$RALPH_HOME/migrate_to_ralph_folder.sh"
chmod +x "$RALPH_HOME/ralph_enable.sh"
chmod +x "$RALPH_HOME/ralph_enable_ci.sh"
chmod +x "$RALPH_HOME/lib/"*.sh chmod +x "$RALPH_HOME/lib/"*.sh
log "SUCCESS" "Ralph scripts installed to $INSTALL_DIR" log "SUCCESS" "Ralph scripts installed to $INSTALL_DIR"
@ -291,6 +321,8 @@ main() {
echo " ralph --monitor # Start Ralph with integrated monitoring" echo " ralph --monitor # Start Ralph with integrated monitoring"
echo " ralph --help # Show Ralph options" echo " ralph --help # Show Ralph options"
echo " ralph-setup my-project # Create new Ralph project" echo " ralph-setup my-project # Create new Ralph project"
echo " ralph-enable # Enable Ralph in existing project (interactive)"
echo " ralph-enable-ci # Enable Ralph in existing project (non-interactive)"
echo " ralph-import prd.md # Convert PRD to Ralph project" echo " ralph-import prd.md # Convert PRD to Ralph project"
echo " ralph-migrate # Migrate existing project to .ralph/ structure" echo " ralph-migrate # Migrate existing project to .ralph/ structure"
echo " ralph-monitor # Manual monitoring dashboard" echo " ralph-monitor # Manual monitoring dashboard"
@ -314,7 +346,7 @@ case "${1:-install}" in
;; ;;
uninstall) uninstall)
log "INFO" "Uninstalling Ralph for Claude Code..." log "INFO" "Uninstalling Ralph for Claude Code..."
rm -f "$INSTALL_DIR/ralph" "$INSTALL_DIR/ralph-monitor" "$INSTALL_DIR/ralph-setup" "$INSTALL_DIR/ralph-import" "$INSTALL_DIR/ralph-migrate" rm -f "$INSTALL_DIR/ralph" "$INSTALL_DIR/ralph-monitor" "$INSTALL_DIR/ralph-setup" "$INSTALL_DIR/ralph-import" "$INSTALL_DIR/ralph-migrate" "$INSTALL_DIR/ralph-enable" "$INSTALL_DIR/ralph-enable-ci"
rm -rf "$RALPH_HOME" rm -rf "$RALPH_HOME"
log "SUCCESS" "Ralph for Claude Code uninstalled" log "SUCCESS" "Ralph for Claude Code uninstalled"
;; ;;

815
lib/enable_core.sh Executable file
View file

@ -0,0 +1,815 @@
#!/usr/bin/env bash
# enable_core.sh - Shared logic for ralph enable commands
# Provides idempotency checks, safe file creation, and project detection
#
# Used by:
# - ralph_enable.sh (interactive wizard)
# - ralph_enable_ci.sh (non-interactive CI version)
# Exit codes - specific codes for different failure types
export ENABLE_SUCCESS=0 # Successful completion
export ENABLE_ERROR=1 # General error
export ENABLE_ALREADY_ENABLED=2 # Ralph already enabled (use --force)
export ENABLE_INVALID_ARGS=3 # Invalid command line arguments
export ENABLE_FILE_NOT_FOUND=4 # Required file not found (e.g., PRD file)
export ENABLE_DEPENDENCY_MISSING=5 # Required dependency missing (e.g., jq for --json)
export ENABLE_PERMISSION_DENIED=6 # Cannot create files/directories
# Colors (can be disabled for non-interactive mode)
export ENABLE_USE_COLORS="${ENABLE_USE_COLORS:-true}"
_color() {
if [[ "$ENABLE_USE_COLORS" == "true" ]]; then
echo -e "$1"
else
echo -e "$2"
fi
}
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Logging function
enable_log() {
local level=$1
local message=$2
local color=""
case $level in
"INFO") color=$BLUE ;;
"WARN") color=$YELLOW ;;
"ERROR") color=$RED ;;
"SUCCESS") color=$GREEN ;;
"SKIP") color=$CYAN ;;
esac
if [[ "$ENABLE_USE_COLORS" == "true" ]]; then
echo -e "${color}[$level]${NC} $message"
else
echo "[$level] $message"
fi
}
# =============================================================================
# IDEMPOTENCY CHECKS
# =============================================================================
# check_existing_ralph - Check if .ralph directory exists and its state
#
# Returns:
# 0 - No .ralph directory, safe to proceed
# 1 - .ralph exists but incomplete (partial setup)
# 2 - .ralph exists and fully initialized
#
# Outputs:
# Sets global RALPH_STATE: "none" | "partial" | "complete"
# Sets global RALPH_MISSING_FILES: array of missing files if partial
#
check_existing_ralph() {
RALPH_STATE="none"
RALPH_MISSING_FILES=()
if [[ ! -d ".ralph" ]]; then
RALPH_STATE="none"
return 0
fi
# Check for required files
local required_files=(
".ralph/PROMPT.md"
".ralph/@fix_plan.md"
".ralph/@AGENT.md"
)
local missing=()
local found=0
for file in "${required_files[@]}"; do
if [[ -f "$file" ]]; then
found=$((found + 1))
else
missing+=("$file")
fi
done
RALPH_MISSING_FILES=("${missing[@]}")
if [[ $found -eq 0 ]]; then
RALPH_STATE="none"
return 0
elif [[ ${#missing[@]} -gt 0 ]]; then
RALPH_STATE="partial"
return 1
else
RALPH_STATE="complete"
return 2
fi
}
# is_ralph_enabled - Simple check if Ralph is fully enabled
#
# Returns:
# 0 - Ralph is fully enabled
# 1 - Ralph is not enabled or only partially
#
is_ralph_enabled() {
check_existing_ralph || true
[[ "$RALPH_STATE" == "complete" ]]
}
# =============================================================================
# SAFE FILE OPERATIONS
# =============================================================================
# safe_create_file - Create a file only if it doesn't exist (or force overwrite)
#
# Parameters:
# $1 (target) - Target file path
# $2 (content) - Content to write (can be empty string)
#
# Environment:
# ENABLE_FORCE - If "true", overwrites existing files instead of skipping
#
# Returns:
# 0 - File created/overwritten successfully
# 1 - File already exists (skipped, only when ENABLE_FORCE is not true)
# 2 - Error creating file
#
# Side effects:
# Logs [CREATE], [OVERWRITE], or [SKIP] message
#
safe_create_file() {
local target=$1
local content=$2
local force="${ENABLE_FORCE:-false}"
if [[ -f "$target" ]]; then
if [[ "$force" == "true" ]]; then
# Force mode: overwrite existing file
enable_log "INFO" "Overwriting $target (--force)"
else
# Normal mode: skip existing file
enable_log "SKIP" "$target already exists"
return 1
fi
fi
# Create parent directory if needed
local parent_dir
parent_dir=$(dirname "$target")
if [[ ! -d "$parent_dir" ]]; then
if ! mkdir -p "$parent_dir" 2>/dev/null; then
enable_log "ERROR" "Failed to create directory: $parent_dir"
return 2
fi
fi
# Write content to file using printf to avoid shell injection
# printf '%s\n' is safer than echo for arbitrary content (handles backslashes, -n, etc.)
if printf '%s\n' "$content" > "$target" 2>/dev/null; then
if [[ -f "$target" ]] && [[ "$force" == "true" ]]; then
enable_log "SUCCESS" "Overwrote $target"
else
enable_log "SUCCESS" "Created $target"
fi
return 0
else
enable_log "ERROR" "Failed to create: $target"
return 2
fi
}
# safe_create_dir - Create a directory only if it doesn't exist
#
# Parameters:
# $1 (target) - Target directory path
#
# Returns:
# 0 - Directory created or already exists
# 1 - Error creating directory
#
safe_create_dir() {
local target=$1
if [[ -d "$target" ]]; then
return 0
fi
if mkdir -p "$target" 2>/dev/null; then
enable_log "SUCCESS" "Created directory: $target"
return 0
else
enable_log "ERROR" "Failed to create directory: $target"
return 1
fi
}
# =============================================================================
# DIRECTORY STRUCTURE
# =============================================================================
# create_ralph_structure - Create the .ralph/ directory structure
#
# Creates:
# .ralph/
# .ralph/specs/
# .ralph/examples/
# .ralph/logs/
# .ralph/docs/generated/
#
# Returns:
# 0 - Structure created successfully
# 1 - Error creating structure
#
create_ralph_structure() {
local dirs=(
".ralph"
".ralph/specs"
".ralph/examples"
".ralph/logs"
".ralph/docs/generated"
)
for dir in "${dirs[@]}"; do
if ! safe_create_dir "$dir"; then
return 1
fi
done
return 0
}
# =============================================================================
# PROJECT DETECTION
# =============================================================================
# Exported detection results
export DETECTED_PROJECT_NAME=""
export DETECTED_PROJECT_TYPE=""
export DETECTED_FRAMEWORK=""
export DETECTED_BUILD_CMD=""
export DETECTED_TEST_CMD=""
export DETECTED_RUN_CMD=""
# detect_project_context - Detect project type, name, and build commands
#
# Detects:
# - Project type: javascript, typescript, python, rust, go, unknown
# - Framework: nextjs, fastapi, express, etc.
# - Build/test/run commands based on detected tooling
#
# Sets globals:
# DETECTED_PROJECT_NAME - Project name (from package.json, folder, etc.)
# DETECTED_PROJECT_TYPE - Language/type
# DETECTED_FRAMEWORK - Framework if detected
# DETECTED_BUILD_CMD - Build command
# DETECTED_TEST_CMD - Test command
# DETECTED_RUN_CMD - Run/start command
#
detect_project_context() {
# Reset detection results
DETECTED_PROJECT_NAME=""
DETECTED_PROJECT_TYPE="unknown"
DETECTED_FRAMEWORK=""
DETECTED_BUILD_CMD=""
DETECTED_TEST_CMD=""
DETECTED_RUN_CMD=""
# Detect from package.json (JavaScript/TypeScript)
if [[ -f "package.json" ]]; then
DETECTED_PROJECT_TYPE="javascript"
# Check for TypeScript
if grep -q '"typescript"' package.json 2>/dev/null || \
[[ -f "tsconfig.json" ]]; then
DETECTED_PROJECT_TYPE="typescript"
fi
# Extract project name
if command -v jq &>/dev/null; then
DETECTED_PROJECT_NAME=$(jq -r '.name // empty' package.json 2>/dev/null)
else
# Fallback: grep for name field
DETECTED_PROJECT_NAME=$(grep -m1 '"name"' package.json | sed 's/.*: *"\([^"]*\)".*/\1/' 2>/dev/null)
fi
# Detect framework
if grep -q '"next"' package.json 2>/dev/null; then
DETECTED_FRAMEWORK="nextjs"
elif grep -q '"express"' package.json 2>/dev/null; then
DETECTED_FRAMEWORK="express"
elif grep -q '"react"' package.json 2>/dev/null; then
DETECTED_FRAMEWORK="react"
elif grep -q '"vue"' package.json 2>/dev/null; then
DETECTED_FRAMEWORK="vue"
fi
# Set build commands
DETECTED_BUILD_CMD="npm run build"
DETECTED_TEST_CMD="npm test"
DETECTED_RUN_CMD="npm start"
# Check for yarn
if [[ -f "yarn.lock" ]]; then
DETECTED_BUILD_CMD="yarn build"
DETECTED_TEST_CMD="yarn test"
DETECTED_RUN_CMD="yarn start"
fi
# Check for pnpm
if [[ -f "pnpm-lock.yaml" ]]; then
DETECTED_BUILD_CMD="pnpm build"
DETECTED_TEST_CMD="pnpm test"
DETECTED_RUN_CMD="pnpm start"
fi
fi
# Detect from pyproject.toml or setup.py (Python)
if [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]]; then
DETECTED_PROJECT_TYPE="python"
# Extract project name from pyproject.toml
if [[ -f "pyproject.toml" ]]; then
DETECTED_PROJECT_NAME=$(grep -m1 '^name' pyproject.toml | sed 's/.*= *"\([^"]*\)".*/\1/' 2>/dev/null)
# Detect framework
if grep -q 'fastapi' pyproject.toml 2>/dev/null; then
DETECTED_FRAMEWORK="fastapi"
elif grep -q 'django' pyproject.toml 2>/dev/null; then
DETECTED_FRAMEWORK="django"
elif grep -q 'flask' pyproject.toml 2>/dev/null; then
DETECTED_FRAMEWORK="flask"
fi
fi
# Set build commands (prefer uv if detected)
if [[ -f "uv.lock" ]] || command -v uv &>/dev/null; then
DETECTED_BUILD_CMD="uv sync"
DETECTED_TEST_CMD="uv run pytest"
DETECTED_RUN_CMD="uv run python -m ${DETECTED_PROJECT_NAME:-main}"
else
DETECTED_BUILD_CMD="pip install -e ."
DETECTED_TEST_CMD="pytest"
DETECTED_RUN_CMD="python -m ${DETECTED_PROJECT_NAME:-main}"
fi
fi
# Detect from Cargo.toml (Rust)
if [[ -f "Cargo.toml" ]]; then
DETECTED_PROJECT_TYPE="rust"
DETECTED_PROJECT_NAME=$(grep -m1 '^name' Cargo.toml | sed 's/.*= *"\([^"]*\)".*/\1/' 2>/dev/null)
DETECTED_BUILD_CMD="cargo build"
DETECTED_TEST_CMD="cargo test"
DETECTED_RUN_CMD="cargo run"
fi
# Detect from go.mod (Go)
if [[ -f "go.mod" ]]; then
DETECTED_PROJECT_TYPE="go"
DETECTED_PROJECT_NAME=$(head -1 go.mod | sed 's/module //' 2>/dev/null)
DETECTED_BUILD_CMD="go build"
DETECTED_TEST_CMD="go test ./..."
DETECTED_RUN_CMD="go run ."
fi
# Fallback project name to folder name
if [[ -z "$DETECTED_PROJECT_NAME" ]]; then
DETECTED_PROJECT_NAME=$(basename "$(pwd)")
fi
}
# detect_git_info - Detect git repository information
#
# Sets globals:
# DETECTED_GIT_REPO - true if in git repo
# DETECTED_GIT_REMOTE - Remote URL (origin)
# DETECTED_GIT_GITHUB - true if GitHub remote
#
export DETECTED_GIT_REPO="false"
export DETECTED_GIT_REMOTE=""
export DETECTED_GIT_GITHUB="false"
detect_git_info() {
DETECTED_GIT_REPO="false"
DETECTED_GIT_REMOTE=""
DETECTED_GIT_GITHUB="false"
# Check if in git repo
if git rev-parse --git-dir &>/dev/null; then
DETECTED_GIT_REPO="true"
# Get remote URL
DETECTED_GIT_REMOTE=$(git remote get-url origin 2>/dev/null || echo "")
# Check if GitHub
if [[ "$DETECTED_GIT_REMOTE" == *"github.com"* ]]; then
DETECTED_GIT_GITHUB="true"
fi
fi
}
# detect_task_sources - Detect available task sources
#
# Sets globals:
# DETECTED_BEADS_AVAILABLE - true if .beads directory exists
# DETECTED_GITHUB_AVAILABLE - true if GitHub remote detected
# DETECTED_PRD_FILES - Array of potential PRD files found
#
export DETECTED_BEADS_AVAILABLE="false"
export DETECTED_GITHUB_AVAILABLE="false"
declare -a DETECTED_PRD_FILES=()
detect_task_sources() {
DETECTED_BEADS_AVAILABLE="false"
DETECTED_GITHUB_AVAILABLE="false"
DETECTED_PRD_FILES=()
# Check for beads
if [[ -d ".beads" ]]; then
DETECTED_BEADS_AVAILABLE="true"
fi
# Check for GitHub (reuse git detection)
detect_git_info
DETECTED_GITHUB_AVAILABLE="$DETECTED_GIT_GITHUB"
# Search for PRD/spec files
local search_dirs=("docs" "specs" "." "requirements")
local prd_patterns=("*prd*.md" "*PRD*.md" "*requirements*.md" "*spec*.md" "*specification*.md")
for dir in "${search_dirs[@]}"; do
if [[ -d "$dir" ]]; then
for pattern in "${prd_patterns[@]}"; do
while IFS= read -r -d '' file; do
DETECTED_PRD_FILES+=("$file")
done < <(find "$dir" -maxdepth 2 -name "$pattern" -print0 2>/dev/null)
done
fi
done
}
# =============================================================================
# TEMPLATE GENERATION
# =============================================================================
# get_templates_dir - Get the templates directory path
#
# Returns:
# Echoes the path to templates directory
# Returns 1 if not found
#
get_templates_dir() {
# Check global installation first
if [[ -d "$HOME/.ralph/templates" ]]; then
echo "$HOME/.ralph/templates"
return 0
fi
# Check local installation (development)
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -d "$script_dir/../templates" ]]; then
echo "$script_dir/../templates"
return 0
fi
return 1
}
# generate_prompt_md - Generate PROMPT.md with project context
#
# Parameters:
# $1 (project_name) - Project name
# $2 (project_type) - Project type (typescript, python, etc.)
# $3 (framework) - Framework if any (optional)
# $4 (objectives) - Custom objectives (optional, newline-separated)
#
# Outputs to stdout
#
generate_prompt_md() {
local project_name="${1:-$(basename "$(pwd)")}"
local project_type="${2:-unknown}"
local framework="${3:-}"
local objectives="${4:-}"
local framework_line=""
if [[ -n "$framework" ]]; then
framework_line="**Framework:** $framework"
fi
local objectives_section=""
if [[ -n "$objectives" ]]; then
objectives_section="$objectives"
else
objectives_section="- Review the codebase and understand the current state
- Follow tasks in @fix_plan.md
- Implement one task per loop
- Write tests for new functionality
- Update documentation as needed"
fi
cat << PROMPTEOF
# Ralph Development Instructions
## Context
You are Ralph, an autonomous AI development agent working on the **${project_name}** project.
**Project Type:** ${project_type}
${framework_line}
## Current Objectives
${objectives_section}
## Key Principles
- ONE task per loop - focus on the most important thing
- Search the codebase before assuming something isn't implemented
- Write comprehensive tests with clear documentation
- Update @fix_plan.md with your learnings
- Commit working changes with descriptive messages
## Testing Guidelines
- LIMIT testing to ~20% of your total effort per loop
- PRIORITIZE: Implementation > Documentation > Tests
- Only write tests for NEW functionality you implement
## Build & Run
See @AGENT.md for build and run instructions.
## Status Reporting (CRITICAL)
At the end of your response, ALWAYS include this status block:
\`\`\`
---RALPH_STATUS---
STATUS: IN_PROGRESS | COMPLETE | BLOCKED
TASKS_COMPLETED_THIS_LOOP: <number>
FILES_MODIFIED: <number>
TESTS_STATUS: PASSING | FAILING | NOT_RUN
WORK_TYPE: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING
EXIT_SIGNAL: false | true
RECOMMENDATION: <one line summary of what to do next>
---END_RALPH_STATUS---
\`\`\`
## Current Task
Follow @fix_plan.md and choose the most important item to implement next.
PROMPTEOF
}
# generate_agent_md - Generate @AGENT.md with detected build commands
#
# Parameters:
# $1 (build_cmd) - Build command
# $2 (test_cmd) - Test command
# $3 (run_cmd) - Run command
#
# Outputs to stdout
#
generate_agent_md() {
local build_cmd="${1:-echo 'No build command configured'}"
local test_cmd="${2:-echo 'No test command configured'}"
local run_cmd="${3:-echo 'No run command configured'}"
cat << AGENTEOF
# Ralph Agent Configuration
## Build Instructions
\`\`\`bash
# Build the project
${build_cmd}
\`\`\`
## Test Instructions
\`\`\`bash
# Run tests
${test_cmd}
\`\`\`
## Run Instructions
\`\`\`bash
# Start/run the project
${run_cmd}
\`\`\`
## Notes
- Update this file when build process changes
- Add environment setup instructions as needed
- Include any pre-requisites or dependencies
AGENTEOF
}
# generate_fix_plan_md - Generate @fix_plan.md with imported tasks
#
# Parameters:
# $1 (tasks) - Tasks to include (newline-separated, markdown checkbox format)
#
# Outputs to stdout
#
generate_fix_plan_md() {
local tasks="${1:-}"
local high_priority=""
local medium_priority=""
local low_priority=""
if [[ -n "$tasks" ]]; then
high_priority="$tasks"
else
high_priority="- [ ] Review codebase and understand architecture
- [ ] Identify and document key components
- [ ] Set up development environment"
medium_priority="- [ ] Implement core features
- [ ] Add test coverage
- [ ] Update documentation"
low_priority="- [ ] Performance optimization
- [ ] Code cleanup and refactoring"
fi
cat << FIXPLANEOF
# Ralph Fix Plan
## High Priority
${high_priority}
## Medium Priority
${medium_priority}
## Low Priority
${low_priority}
## Completed
- [x] Project enabled for Ralph
## Notes
- Focus on MVP functionality first
- Ensure each feature is properly tested
- Update this file after each major milestone
FIXPLANEOF
}
# generate_ralphrc - Generate .ralphrc configuration file
#
# Parameters:
# $1 (project_name) - Project name
# $2 (project_type) - Project type
# $3 (task_sources) - Task sources (local, beads, github)
#
# Outputs to stdout
#
generate_ralphrc() {
local project_name="${1:-$(basename "$(pwd)")}"
local project_type="${2:-unknown}"
local task_sources="${3:-local}"
cat << RALPHRCEOF
# .ralphrc - Ralph project configuration
# Generated by: ralph enable
# Documentation: https://github.com/frankbria/ralph-claude-code
# Project identification
PROJECT_NAME="${project_name}"
PROJECT_TYPE="${project_type}"
# Loop settings
MAX_CALLS_PER_HOUR=100
CLAUDE_TIMEOUT_MINUTES=15
CLAUDE_OUTPUT_FORMAT="json"
# Tool permissions
# Comma-separated list of allowed tools
ALLOWED_TOOLS="Write,Read,Edit,Bash(git *),Bash(npm *),Bash(pytest)"
# Session management
SESSION_CONTINUITY=true
SESSION_EXPIRY_HOURS=24
# Task sources (for ralph enable --sync)
# Options: local, beads, github (comma-separated for multiple)
TASK_SOURCES="${task_sources}"
GITHUB_TASK_LABEL="ralph-task"
BEADS_FILTER="status:open"
# Circuit breaker thresholds
CB_NO_PROGRESS_THRESHOLD=3
CB_SAME_ERROR_THRESHOLD=5
CB_OUTPUT_DECLINE_THRESHOLD=70
RALPHRCEOF
}
# =============================================================================
# MAIN ENABLE LOGIC
# =============================================================================
# enable_ralph_in_directory - Main function to enable Ralph in current directory
#
# Parameters:
# $1 (options) - JSON-like options string or empty
# force: true/false - Force overwrite existing
# skip_tasks: true/false - Skip task import
# project_name: string - Override project name
# task_content: string - Pre-imported task content
#
# Returns:
# 0 - Success
# 1 - Error
# 2 - Already enabled (and no force flag)
#
enable_ralph_in_directory() {
local force="${ENABLE_FORCE:-false}"
local skip_tasks="${ENABLE_SKIP_TASKS:-false}"
local project_name="${ENABLE_PROJECT_NAME:-}"
local project_type="${ENABLE_PROJECT_TYPE:-}"
local task_content="${ENABLE_TASK_CONTENT:-}"
# Check existing state (use || true to prevent set -e from exiting)
check_existing_ralph || true
if [[ "$RALPH_STATE" == "complete" && "$force" != "true" ]]; then
enable_log "INFO" "Ralph is already enabled in this project"
enable_log "INFO" "Use --force to overwrite existing configuration"
return $ENABLE_ALREADY_ENABLED
fi
# Detect project context
detect_project_context
# Use detected or provided project name
if [[ -z "$project_name" ]]; then
project_name="$DETECTED_PROJECT_NAME"
fi
# Use detected or provided project type
if [[ -n "$project_type" ]]; then
DETECTED_PROJECT_TYPE="$project_type"
fi
enable_log "INFO" "Enabling Ralph for: $project_name"
enable_log "INFO" "Project type: $DETECTED_PROJECT_TYPE"
if [[ -n "$DETECTED_FRAMEWORK" ]]; then
enable_log "INFO" "Framework: $DETECTED_FRAMEWORK"
fi
# Create directory structure
if ! create_ralph_structure; then
enable_log "ERROR" "Failed to create .ralph/ structure"
return $ENABLE_ERROR
fi
# Generate and create files
local prompt_content
prompt_content=$(generate_prompt_md "$project_name" "$DETECTED_PROJECT_TYPE" "$DETECTED_FRAMEWORK")
safe_create_file ".ralph/PROMPT.md" "$prompt_content"
local agent_content
agent_content=$(generate_agent_md "$DETECTED_BUILD_CMD" "$DETECTED_TEST_CMD" "$DETECTED_RUN_CMD")
safe_create_file ".ralph/@AGENT.md" "$agent_content"
local fix_plan_content
fix_plan_content=$(generate_fix_plan_md "$task_content")
safe_create_file ".ralph/@fix_plan.md" "$fix_plan_content"
# Detect task sources for .ralphrc
detect_task_sources
local task_sources="local"
if [[ "$DETECTED_BEADS_AVAILABLE" == "true" ]]; then
task_sources="beads,$task_sources"
fi
if [[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]]; then
task_sources="github,$task_sources"
fi
# Generate .ralphrc
local ralphrc_content
ralphrc_content=$(generate_ralphrc "$project_name" "$DETECTED_PROJECT_TYPE" "$task_sources")
safe_create_file ".ralphrc" "$ralphrc_content"
enable_log "SUCCESS" "Ralph enabled successfully!"
return $ENABLE_SUCCESS
}
# Export functions for use in other scripts
export -f enable_log
export -f check_existing_ralph
export -f is_ralph_enabled
export -f safe_create_file
export -f safe_create_dir
export -f create_ralph_structure
export -f detect_project_context
export -f detect_git_info
export -f detect_task_sources
export -f get_templates_dir
export -f generate_prompt_md
export -f generate_agent_md
export -f generate_fix_plan_md
export -f generate_ralphrc
export -f enable_ralph_in_directory

550
lib/task_sources.sh Executable file
View file

@ -0,0 +1,550 @@
#!/usr/bin/env bash
# task_sources.sh - Task import utilities for Ralph enable
# Supports importing tasks from beads, GitHub Issues, and PRD files
# =============================================================================
# BEADS INTEGRATION
# =============================================================================
# check_beads_available - Check if beads (bd) is available and configured
#
# Returns:
# 0 - Beads available
# 1 - Beads not available or not configured
#
check_beads_available() {
# Check for .beads directory
if [[ ! -d ".beads" ]]; then
return 1
fi
# Check if bd command exists
if ! command -v bd &>/dev/null; then
return 1
fi
return 0
}
# fetch_beads_tasks - Fetch tasks from beads issue tracker
#
# Parameters:
# $1 (filter) - Filter string (optional, e.g., "status:open")
#
# Outputs:
# Tasks in markdown checkbox format, one per line
# e.g., "- [ ] [issue-001] Fix authentication bug"
#
# Returns:
# 0 - Success (may output empty if no tasks)
# 1 - Error fetching tasks
#
fetch_beads_tasks() {
local filter="${1:-status:open}"
local tasks=""
# Check if beads is available
if ! check_beads_available; then
return 1
fi
# Try to get tasks as JSON (pass filter if provided)
local json_output
if json_output=$(bd list --json --filter "$filter" 2>/dev/null); then
# Parse JSON and format as markdown tasks
if command -v jq &>/dev/null; then
tasks=$(echo "$json_output" | jq -r '
.[] |
select(.status != "closed") |
"- [ ] [\(.id)] \(.title)"
' 2>/dev/null)
fi
else
# Fallback: try plain text output
tasks=$(bd list 2>/dev/null | while IFS= read -r line; do
# Extract ID and title from bd list output
local id title
id=$(echo "$line" | grep -oE '^[a-z]+-[0-9]+' || echo "")
title=$(echo "$line" | sed 's/^[a-z]+-[0-9]* *//' || echo "$line")
if [[ -n "$id" ]]; then
echo "- [ ] [$id] $title"
fi
done)
fi
if [[ -n "$tasks" ]]; then
echo "$tasks"
return 0
else
return 0 # Empty is not an error
fi
}
# get_beads_count - Get count of open beads issues
#
# Returns:
# 0 and echoes the count
# 1 if beads unavailable
#
get_beads_count() {
if ! check_beads_available; then
echo "0"
return 1
fi
local count
if command -v jq &>/dev/null; then
count=$(bd list --json 2>/dev/null | jq '[.[] | select(.status != "closed")] | length' 2>/dev/null || echo "0")
else
count=$(bd list 2>/dev/null | wc -l | tr -d ' ')
fi
echo "${count:-0}"
return 0
}
# =============================================================================
# GITHUB ISSUES INTEGRATION
# =============================================================================
# check_github_available - Check if GitHub CLI (gh) is available and authenticated
#
# Returns:
# 0 - GitHub available and authenticated
# 1 - Not available
#
check_github_available() {
# Check for gh command
if ! command -v gh &>/dev/null; then
return 1
fi
# Check if authenticated
if ! gh auth status &>/dev/null; then
return 1
fi
# Check if in a git repo with GitHub remote
if ! git remote get-url origin 2>/dev/null | grep -q "github.com"; then
return 1
fi
return 0
}
# fetch_github_tasks - Fetch issues from GitHub
#
# Parameters:
# $1 (label) - Label to filter by (optional, default: "ralph-task")
# $2 (limit) - Maximum number of issues (optional, default: 50)
#
# Outputs:
# Tasks in markdown checkbox format
# e.g., "- [ ] [#123] Implement user authentication"
#
# Returns:
# 0 - Success
# 1 - Error
#
fetch_github_tasks() {
local label="${1:-}"
local limit="${2:-50}"
local tasks=""
# Check if GitHub is available
if ! check_github_available; then
return 1
fi
# Build gh command
local gh_args=("issue" "list" "--state" "open" "--limit" "$limit" "--json" "number,title,labels")
if [[ -n "$label" ]]; then
gh_args+=("--label" "$label")
fi
# Fetch issues
local json_output
if ! json_output=$(gh "${gh_args[@]}" 2>/dev/null); then
return 1
fi
# Parse JSON and format as markdown tasks
if command -v jq &>/dev/null; then
tasks=$(echo "$json_output" | jq -r '
.[] |
"- [ ] [#\(.number)] \(.title)"
' 2>/dev/null)
fi
if [[ -n "$tasks" ]]; then
echo "$tasks"
fi
return 0
}
# get_github_issue_count - Get count of open GitHub issues
#
# Parameters:
# $1 (label) - Label to filter by (optional)
#
# Returns:
# 0 and echoes the count
# 1 if GitHub unavailable
#
get_github_issue_count() {
local label="${1:-}"
if ! check_github_available; then
echo "0"
return 1
fi
local gh_args=("issue" "list" "--state" "open" "--json" "number")
if [[ -n "$label" ]]; then
gh_args+=("--label" "$label")
fi
local count
if command -v jq &>/dev/null; then
count=$(gh "${gh_args[@]}" 2>/dev/null | jq 'length' 2>/dev/null || echo "0")
else
count=$(gh issue list --state open 2>/dev/null | wc -l | tr -d ' ')
fi
echo "${count:-0}"
return 0
}
# get_github_labels - Get available labels from GitHub repo
#
# Outputs:
# Newline-separated list of label names
#
get_github_labels() {
if ! check_github_available; then
return 1
fi
gh label list --json name --jq '.[].name' 2>/dev/null
}
# =============================================================================
# PRD CONVERSION
# =============================================================================
# extract_prd_tasks - Extract tasks from a PRD/specification document
#
# Parameters:
# $1 (prd_file) - Path to the PRD file
#
# Outputs:
# Tasks in markdown checkbox format
#
# Returns:
# 0 - Success
# 1 - Error
#
# Note: For full PRD conversion with Claude, use ralph-import
# This function does basic extraction without AI assistance
#
extract_prd_tasks() {
local prd_file=$1
if [[ ! -f "$prd_file" ]]; then
return 1
fi
local tasks=""
# Look for existing checkbox items
local checkbox_tasks
checkbox_tasks=$(grep -E '^[[:space:]]*[-*][[:space:]]*\[[[:space:]]*[xX ]?[[:space:]]*\]' "$prd_file" 2>/dev/null)
if [[ -n "$checkbox_tasks" ]]; then
# Normalize to unchecked format
tasks=$(echo "$checkbox_tasks" | sed 's/\[x\]/[ ]/gi; s/\[X\]/[ ]/g')
fi
# Look for numbered list items that look like tasks
local numbered_tasks
numbered_tasks=$(grep -E '^[[:space:]]*[0-9]+\.[[:space:]]+' "$prd_file" 2>/dev/null | head -20)
if [[ -n "$numbered_tasks" ]]; then
while IFS= read -r line; do
# Convert numbered item to checkbox
local task_text
task_text=$(echo "$line" | sed -E 's/^[[:space:]]*[0-9]*\.[[:space:]]*//')
if [[ -n "$task_text" ]]; then
tasks="${tasks}
- [ ] ${task_text}"
fi
done <<< "$numbered_tasks"
fi
# Look for headings that might be task sections
local headings
headings=$(grep -E '^#{1,3}[[:space:]]+(TODO|Tasks|Requirements|Features|Backlog|Sprint)' "$prd_file" 2>/dev/null)
if [[ -n "$headings" ]]; then
# Extract content after these headings as potential tasks
while IFS= read -r heading; do
local section_name
section_name=$(echo "$heading" | sed -E 's/^#*[[:space:]]*//')
# This is informational - actual task extraction would need more context
done <<< "$headings"
fi
# Clean up and output
if [[ -n "$tasks" ]]; then
echo "$tasks" | grep -v '^$' | head -30 # Limit to 30 tasks
return 0
fi
return 0 # Empty is not an error
}
# convert_prd_with_claude - Full PRD conversion using Claude (calls ralph-import logic)
#
# Parameters:
# $1 (prd_file) - Path to the PRD file
# $2 (output_dir) - Directory to output converted files (optional, defaults to .ralph/)
#
# Outputs:
# Sets CONVERTED_PROMPT_FILE, CONVERTED_FIX_PLAN_FILE, CONVERTED_SPECS_FILE
#
# Returns:
# 0 - Success
# 1 - Error
#
convert_prd_with_claude() {
local prd_file=$1
local output_dir="${2:-.ralph}"
# This would call into ralph_import.sh's convert_prd function
# For now, we do basic extraction
# Full Claude-based conversion requires the import script
if [[ ! -f "$prd_file" ]]; then
return 1
fi
# Check if ralph-import is available for full conversion
if command -v ralph-import &>/dev/null; then
# Use ralph-import for full conversion
# Note: ralph-import creates a new project, so we need to adapt
echo "Full PRD conversion available via: ralph-import $prd_file"
return 1 # Return error to indicate basic extraction should be used
fi
# Fall back to basic extraction
extract_prd_tasks "$prd_file"
}
# =============================================================================
# TASK NORMALIZATION
# =============================================================================
# normalize_tasks - Normalize tasks to consistent markdown format
#
# Parameters:
# $1 (tasks) - Raw task text (multi-line)
# $2 (source) - Source identifier (beads, github, prd)
#
# Outputs:
# Normalized tasks in markdown checkbox format
#
normalize_tasks() {
local tasks=$1
local source="${2:-unknown}"
if [[ -z "$tasks" ]]; then
return 0
fi
# Process each line
echo "$tasks" | while IFS= read -r line; do
# Skip empty lines
[[ -z "$line" ]] && continue
# Already in checkbox format
if echo "$line" | grep -qE '^[[:space:]]*-[[:space:]]*\[[[:space:]]*[xX ]?[[:space:]]*\]'; then
# Normalize the checkbox
echo "$line" | sed 's/\[x\]/[ ]/gi; s/\[X\]/[ ]/g'
continue
fi
# Bullet point without checkbox
if echo "$line" | grep -qE '^[[:space:]]*[-*][[:space:]]+'; then
local text
text=$(echo "$line" | sed -E 's/^[[:space:]]*[-*][[:space:]]*//')
echo "- [ ] $text"
continue
fi
# Numbered item
if echo "$line" | grep -qE '^[[:space:]]*[0-9]+\.?[[:space:]]+'; then
local text
text=$(echo "$line" | sed -E 's/^[[:space:]]*[0-9]*\.?[[:space:]]*//')
echo "- [ ] $text"
continue
fi
# Plain text line - make it a task
echo "- [ ] $line"
done
}
# prioritize_tasks - Sort tasks by priority heuristics
#
# Parameters:
# $1 (tasks) - Tasks in markdown format
#
# Outputs:
# Tasks sorted with priority indicators
#
# Heuristics:
# - "critical", "urgent", "blocker" -> High priority
# - "important", "should", "must" -> High priority
# - "nice to have", "optional", "future" -> Low priority
#
prioritize_tasks() {
local tasks=$1
if [[ -z "$tasks" ]]; then
return 0
fi
# Separate into priority buckets
local high_priority=""
local medium_priority=""
local low_priority=""
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local lower_line
lower_line=$(echo "$line" | tr '[:upper:]' '[:lower:]')
# Check for priority indicators
if echo "$lower_line" | grep -qE '(critical|urgent|blocker|breaking|security|p0|p1)'; then
high_priority="${high_priority}${line}
"
elif echo "$lower_line" | grep -qE '(nice.to.have|optional|future|later|p3|p4|low.priority)'; then
low_priority="${low_priority}${line}
"
elif echo "$lower_line" | grep -qE '(important|should|must|needed|required|p2)'; then
high_priority="${high_priority}${line}
"
else
medium_priority="${medium_priority}${line}
"
fi
done <<< "$tasks"
# Output in priority order
echo "## High Priority"
[[ -n "$high_priority" ]] && echo "$high_priority"
echo ""
echo "## Medium Priority"
[[ -n "$medium_priority" ]] && echo "$medium_priority"
echo ""
echo "## Low Priority"
[[ -n "$low_priority" ]] && echo "$low_priority"
}
# =============================================================================
# COMBINED IMPORT
# =============================================================================
# import_tasks_from_sources - Import tasks from multiple sources
#
# Parameters:
# $1 (sources) - Space-separated list of sources: beads, github, prd
# $2 (prd_file) - Path to PRD file (required if prd in sources)
# $3 (github_label) - GitHub label filter (optional)
#
# Outputs:
# Combined tasks in markdown format
#
# Returns:
# 0 - Success
# 1 - No tasks imported
#
import_tasks_from_sources() {
local sources=$1
local prd_file="${2:-}"
local github_label="${3:-}"
local all_tasks=""
local source_count=0
# Import from beads
if echo "$sources" | grep -qw "beads"; then
local beads_tasks
if beads_tasks=$(fetch_beads_tasks); then
if [[ -n "$beads_tasks" ]]; then
all_tasks="${all_tasks}
# Tasks from beads
${beads_tasks}
"
((source_count++))
fi
fi
fi
# Import from GitHub
if echo "$sources" | grep -qw "github"; then
local github_tasks
if github_tasks=$(fetch_github_tasks "$github_label"); then
if [[ -n "$github_tasks" ]]; then
all_tasks="${all_tasks}
# Tasks from GitHub
${github_tasks}
"
((source_count++))
fi
fi
fi
# Import from PRD
if echo "$sources" | grep -qw "prd"; then
if [[ -n "$prd_file" && -f "$prd_file" ]]; then
local prd_tasks
if prd_tasks=$(extract_prd_tasks "$prd_file"); then
if [[ -n "$prd_tasks" ]]; then
all_tasks="${all_tasks}
# Tasks from PRD
${prd_tasks}
"
((source_count++))
fi
fi
fi
fi
if [[ -z "$all_tasks" ]]; then
return 1
fi
# Normalize and output
normalize_tasks "$all_tasks" "combined"
return 0
}
# =============================================================================
# EXPORTS
# =============================================================================
export -f check_beads_available
export -f fetch_beads_tasks
export -f get_beads_count
export -f check_github_available
export -f fetch_github_tasks
export -f get_github_issue_count
export -f get_github_labels
export -f extract_prd_tasks
export -f convert_prd_with_claude
export -f normalize_tasks
export -f prioritize_tasks
export -f import_tasks_from_sources

542
lib/wizard_utils.sh Executable file
View file

@ -0,0 +1,542 @@
#!/usr/bin/env bash
# wizard_utils.sh - Interactive prompt utilities for Ralph enable wizard
# Provides consistent, user-friendly prompts for configuration
# Colors (exported for subshells)
export WIZARD_CYAN='\033[0;36m'
export WIZARD_GREEN='\033[0;32m'
export WIZARD_YELLOW='\033[1;33m'
export WIZARD_RED='\033[0;31m'
export WIZARD_BOLD='\033[1m'
export WIZARD_NC='\033[0m'
# =============================================================================
# BASIC PROMPTS
# =============================================================================
# confirm - Ask a yes/no question
#
# Parameters:
# $1 (prompt) - The question to ask
# $2 (default) - Default answer: "y" or "n" (optional, defaults to "n")
#
# Returns:
# 0 - User answered yes
# 1 - User answered no
#
# Example:
# if confirm "Continue with installation?" "y"; then
# echo "Installing..."
# fi
#
confirm() {
local prompt=$1
local default="${2:-n}"
local response
local yn_hint="[y/N]"
if [[ "${default,,}" == "y" ]]; then
yn_hint="[Y/n]"
fi
while true; do
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} ${yn_hint}: "
read -r response
# Handle empty response (use default)
if [[ -z "$response" ]]; then
response="$default"
fi
case "${response,,}" in
y|yes)
return 0
;;
n|no)
return 1
;;
*)
echo -e "${WIZARD_YELLOW}Please answer yes (y) or no (n)${WIZARD_NC}"
;;
esac
done
}
# prompt_text - Ask for text input with optional default
#
# Parameters:
# $1 (prompt) - The prompt text
# $2 (default) - Default value (optional)
#
# Outputs:
# Echoes the user's input (or default if empty)
#
# Example:
# project_name=$(prompt_text "Project name" "my-project")
#
prompt_text() {
local prompt=$1
local default="${2:-}"
local response
if [[ -n "$default" ]]; then
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: "
else
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: "
fi
read -r response
if [[ -z "$response" ]]; then
echo "$default"
else
echo "$response"
fi
}
# prompt_number - Ask for numeric input with optional default and range
#
# Parameters:
# $1 (prompt) - The prompt text
# $2 (default) - Default value (optional)
# $3 (min) - Minimum value (optional)
# $4 (max) - Maximum value (optional)
#
# Outputs:
# Echoes the validated number
#
prompt_number() {
local prompt=$1
local default="${2:-}"
local min="${3:-}"
local max="${4:-}"
local response
while true; do
if [[ -n "$default" ]]; then
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: "
else
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: "
fi
read -r response
# Use default if empty
if [[ -z "$response" ]]; then
if [[ -n "$default" ]]; then
echo "$default"
return 0
else
echo -e "${WIZARD_YELLOW}Please enter a number${WIZARD_NC}"
continue
fi
fi
# Validate it's a number
if ! [[ "$response" =~ ^[0-9]+$ ]]; then
echo -e "${WIZARD_YELLOW}Please enter a valid number${WIZARD_NC}"
continue
fi
# Check range if specified
if [[ -n "$min" && "$response" -lt "$min" ]]; then
echo -e "${WIZARD_YELLOW}Value must be at least ${min}${WIZARD_NC}"
continue
fi
if [[ -n "$max" && "$response" -gt "$max" ]]; then
echo -e "${WIZARD_YELLOW}Value must be at most ${max}${WIZARD_NC}"
continue
fi
echo "$response"
return 0
done
}
# =============================================================================
# SELECTION PROMPTS
# =============================================================================
# select_option - Present a list of options for single selection
#
# Parameters:
# $1 (prompt) - The question/prompt text
# $@ (options) - Remaining arguments are the options
#
# Outputs:
# Echoes the selected option (the text, not the number)
#
# Example:
# choice=$(select_option "Select package manager" "npm" "yarn" "pnpm")
# echo "Selected: $choice"
#
select_option() {
local prompt=$1
shift
local options=("$@")
local num_options=${#options[@]}
# Guard against empty options array
if [[ $num_options -eq 0 ]]; then
echo ""
return 1
fi
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}"
echo ""
# Display options
local i=1
for opt in "${options[@]}"; do
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}"
((i++))
done
echo ""
while true; do
echo -en "Select option [1-${num_options}]: "
read -r response
# Validate it's a number in range
if [[ "$response" =~ ^[0-9]+$ ]] && \
[[ "$response" -ge 1 ]] && \
[[ "$response" -le "$num_options" ]]; then
# Return the option text (0-indexed array)
echo "${options[$((response - 1))]}"
return 0
else
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}"
fi
done
}
# select_multiple - Present checkboxes for multi-selection
#
# Parameters:
# $1 (prompt) - The question/prompt text
# $@ (options) - Remaining arguments are the options
#
# Outputs:
# Echoes comma-separated list of selected indices (0-based)
# Returns empty string if nothing selected
#
# Example:
# selected=$(select_multiple "Select task sources" "beads" "github" "prd")
# # If user selects first and third: selected="0,2"
# IFS=',' read -ra indices <<< "$selected"
# for idx in "${indices[@]}"; do
# echo "Selected: ${options[$idx]}"
# done
#
select_multiple() {
local prompt=$1
shift
local options=("$@")
local num_options=${#options[@]}
# Track selected state (0 = not selected, 1 = selected)
declare -a selected
for ((i = 0; i < num_options; i++)); do
selected[$i]=0
done
# Display instructions (redirect to stderr to avoid corrupting return value)
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" >&2
echo -e "${WIZARD_CYAN}(Enter numbers to toggle, press Enter when done)${WIZARD_NC}" >&2
echo "" >&2
while true; do
# Display options with checkboxes
local i=1
for opt in "${options[@]}"; do
local checkbox="[ ]"
if [[ "${selected[$((i - 1))]}" == "1" ]]; then
checkbox="[${WIZARD_GREEN}x${WIZARD_NC}]"
fi
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${checkbox} ${opt}" >&2
((i++)) || true
done
echo "" >&2
echo -en "Toggle [1-${num_options}] or Enter to confirm: " >&2
read -r response
# Empty input = done
if [[ -z "$response" ]]; then
break
fi
# Validate it's a number in range
if [[ "$response" =~ ^[0-9]+$ ]] && \
[[ "$response" -ge 1 ]] && \
[[ "$response" -le "$num_options" ]]; then
# Toggle the selection
local idx=$((response - 1))
if [[ "${selected[$idx]}" == "0" ]]; then
selected[$idx]=1
else
selected[$idx]=0
fi
else
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}" >&2
fi
# Clear previous display (move cursor up)
# Number of lines to clear: options + 2 (prompt line + input line)
for ((j = 0; j < num_options + 2; j++)); do
echo -en "\033[A\033[K" >&2
done
done
# Build result string (comma-separated indices)
local result=""
for ((i = 0; i < num_options; i++)); do
if [[ "${selected[$i]}" == "1" ]]; then
if [[ -n "$result" ]]; then
result="$result,$i"
else
result="$i"
fi
fi
done
echo "$result"
}
# select_with_default - Present options with a recommended default
#
# Parameters:
# $1 (prompt) - The question/prompt text
# $2 (default_index) - 1-based index of default option
# $@ (options) - Remaining arguments are the options
#
# Outputs:
# Echoes the selected option
#
select_with_default() {
local prompt=$1
local default_index=$2
shift 2
local options=("$@")
local num_options=${#options[@]}
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}"
echo ""
# Display options with default marked
local i=1
for opt in "${options[@]}"; do
if [[ $i -eq $default_index ]]; then
echo -e " ${WIZARD_GREEN}${i})${WIZARD_NC} ${opt} ${WIZARD_GREEN}(recommended)${WIZARD_NC}"
else
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}"
fi
((i++))
done
echo ""
while true; do
echo -en "Select option [1-${num_options}] (default: ${default_index}): "
read -r response
# Use default if empty
if [[ -z "$response" ]]; then
echo "${options[$((default_index - 1))]}"
return 0
fi
# Validate it's a number in range
if [[ "$response" =~ ^[0-9]+$ ]] && \
[[ "$response" -ge 1 ]] && \
[[ "$response" -le "$num_options" ]]; then
echo "${options[$((response - 1))]}"
return 0
else
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}"
fi
done
}
# =============================================================================
# DISPLAY UTILITIES
# =============================================================================
# print_header - Print a section header
#
# Parameters:
# $1 (title) - The header title
# $2 (phase) - Optional phase number (e.g., "1 of 5")
#
print_header() {
local title=$1
local phase="${2:-}"
echo ""
echo -e "${WIZARD_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${WIZARD_NC}"
if [[ -n "$phase" ]]; then
echo -e "${WIZARD_BOLD} ${title}${WIZARD_NC} ${WIZARD_CYAN}(${phase})${WIZARD_NC}"
else
echo -e "${WIZARD_BOLD} ${title}${WIZARD_NC}"
fi
echo -e "${WIZARD_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${WIZARD_NC}"
echo ""
}
# print_bullet - Print a bullet point item
#
# Parameters:
# $1 (text) - The text to display
# $2 (symbol) - Optional symbol (defaults to "•")
#
print_bullet() {
local text=$1
local symbol="${2:-}"
echo -e " ${WIZARD_CYAN}${symbol}${WIZARD_NC} ${text}"
}
# print_success - Print a success message
#
# Parameters:
# $1 (message) - The message to display
#
print_success() {
echo -e "${WIZARD_GREEN}${WIZARD_NC} $1"
}
# print_warning - Print a warning message
#
# Parameters:
# $1 (message) - The message to display
#
print_warning() {
echo -e "${WIZARD_YELLOW}${WIZARD_NC} $1"
}
# print_error - Print an error message
#
# Parameters:
# $1 (message) - The message to display
#
print_error() {
echo -e "${WIZARD_RED}${WIZARD_NC} $1"
}
# print_info - Print an info message
#
# Parameters:
# $1 (message) - The message to display
#
print_info() {
echo -e "${WIZARD_CYAN}${WIZARD_NC} $1"
}
# print_detection_result - Print a detection result with status
#
# Parameters:
# $1 (label) - What was detected
# $2 (value) - The detected value
# $3 (available) - "true" or "false"
#
print_detection_result() {
local label=$1
local value=$2
local available="${3:-true}"
if [[ "$available" == "true" ]]; then
echo -e " ${WIZARD_GREEN}${WIZARD_NC} ${label}: ${WIZARD_BOLD}${value}${WIZARD_NC}"
else
echo -e " ${WIZARD_YELLOW}${WIZARD_NC} ${label}: ${value}"
fi
}
# =============================================================================
# PROGRESS DISPLAY
# =============================================================================
# show_progress - Display a simple progress indicator
#
# Parameters:
# $1 (current) - Current step number
# $2 (total) - Total steps
# $3 (message) - Current step message
#
show_progress() {
local current=$1
local total=$2
local message=$3
local bar_width=30
local filled=$((current * bar_width / total))
local empty=$((bar_width - filled))
local bar=""
for ((i = 0; i < filled; i++)); do bar+="█"; done
for ((i = 0; i < empty; i++)); do bar+="░"; done
echo -en "\r${WIZARD_CYAN}[${bar}]${WIZARD_NC} ${current}/${total} ${message}"
}
# clear_line - Clear the current line
#
clear_line() {
echo -en "\r\033[K"
}
# =============================================================================
# SUMMARY DISPLAY
# =============================================================================
# print_summary - Print a summary box
#
# Parameters:
# $1 (title) - Summary title
# $@ (items) - Key=value pairs to display
#
# Example:
# print_summary "Configuration" "Project=my-app" "Type=typescript" "Tasks=15"
#
print_summary() {
local title=$1
shift
local items=("$@")
echo ""
echo -e "${WIZARD_BOLD}┌─ ${title} ───────────────────────────────────────┐${WIZARD_NC}"
echo "│"
for item in "${items[@]}"; do
local key="${item%%=*}"
local value="${item#*=}"
printf "${WIZARD_CYAN}%-20s${WIZARD_NC} %s\n" "${key}:" "$value"
done
echo "│"
echo -e "${WIZARD_BOLD}└────────────────────────────────────────────────────┘${WIZARD_NC}"
echo ""
}
# =============================================================================
# EXPORTS
# =============================================================================
export -f confirm
export -f prompt_text
export -f prompt_number
export -f select_option
export -f select_multiple
export -f select_with_default
export -f print_header
export -f print_bullet
export -f print_success
export -f print_warning
export -f print_error
export -f print_info
export -f print_detection_result
export -f show_progress
export -f clear_line
export -f print_summary

571
ralph_enable.sh Executable file
View file

@ -0,0 +1,571 @@
#!/bin/bash
# Ralph Enable - Interactive Wizard for Existing Projects
# Adds Ralph configuration to an existing codebase
#
# Usage:
# ralph enable # Interactive wizard
# ralph enable --from beads # With specific task source
# ralph enable --force # Overwrite existing .ralph/
# ralph enable --skip-tasks # Skip task import
#
# Version: 0.11.0
set -e
# Get script directory for library loading
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Try to load libraries from global installation first, then local
RALPH_HOME="${RALPH_HOME:-$HOME/.ralph}"
if [[ -f "$RALPH_HOME/lib/enable_core.sh" ]]; then
LIB_DIR="$RALPH_HOME/lib"
elif [[ -f "$SCRIPT_DIR/lib/enable_core.sh" ]]; then
LIB_DIR="$SCRIPT_DIR/lib"
else
echo "Error: Cannot find Ralph libraries"
echo "Please run ./install.sh first or ensure RALPH_HOME is set correctly"
exit 1
fi
# Source libraries
source "$LIB_DIR/enable_core.sh"
source "$LIB_DIR/wizard_utils.sh"
source "$LIB_DIR/task_sources.sh"
# =============================================================================
# CONFIGURATION
# =============================================================================
# Command line options
FORCE_OVERWRITE=false
SKIP_TASKS=false
TASK_SOURCE=""
PRD_FILE=""
GITHUB_LABEL=""
NON_INTERACTIVE=false
SHOW_HELP=false
# Version
VERSION="0.11.0"
# =============================================================================
# HELP
# =============================================================================
show_help() {
cat << EOF
Ralph Enable - Add Ralph to Existing Projects
Usage: ralph enable [OPTIONS]
Options:
--from <source> Import tasks from: beads, github, prd
--prd <file> PRD file to convert (when --from prd)
--label <label> GitHub label filter (when --from github)
--force Overwrite existing .ralph/ configuration
--skip-tasks Skip task import, use default templates
--non-interactive Run with defaults (no prompts)
-h, --help Show this help message
-v, --version Show version
Examples:
# Interactive wizard (recommended)
cd my-existing-project
ralph enable
# Import tasks from beads
ralph enable --from beads
# Import from GitHub issues with label
ralph enable --from github --label "ralph-task"
# Convert a PRD document
ralph enable --from prd --prd ./docs/requirements.md
# Skip task import
ralph enable --skip-tasks
# Force overwrite existing configuration
ralph enable --force
What this command does:
1. Detects your project type (TypeScript, Python, etc.)
2. Identifies available task sources (beads, GitHub, PRDs)
3. Imports tasks from selected sources
4. Creates .ralph/ configuration directory
5. Generates PROMPT.md, @fix_plan.md, @AGENT.md
6. Creates .ralphrc for project-specific settings
This command is:
- Idempotent: Safe to run multiple times
- Non-destructive: Never overwrites existing files (unless --force)
- Project-aware: Detects your language, framework, and build tools
For new projects, use: ralph-setup <project-name>
For migrating old structure, use: ralph-migrate
EOF
}
# =============================================================================
# ARGUMENT PARSING
# =============================================================================
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
--from)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
TASK_SOURCE="$2"
shift 2
else
echo "Error: --from requires a source (beads, github, prd)" >&2
exit $ENABLE_INVALID_ARGS
fi
;;
--prd)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
PRD_FILE="$2"
shift 2
else
echo "Error: --prd requires a file path" >&2
exit $ENABLE_INVALID_ARGS
fi
;;
--label)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
GITHUB_LABEL="$2"
shift 2
else
echo "Error: --label requires a label name" >&2
exit $ENABLE_INVALID_ARGS
fi
;;
--force)
FORCE_OVERWRITE=true
shift
;;
--skip-tasks)
SKIP_TASKS=true
shift
;;
--non-interactive)
NON_INTERACTIVE=true
shift
;;
-h|--help)
SHOW_HELP=true
shift
;;
-v|--version)
echo "ralph enable version $VERSION"
exit 0
;;
*)
echo "Unknown option: $1" >&2
echo "Use --help for usage information" >&2
exit $ENABLE_INVALID_ARGS
;;
esac
done
}
# =============================================================================
# PHASE 1: ENVIRONMENT DETECTION
# =============================================================================
phase_environment_detection() {
print_header "Environment Detection" "Phase 1 of 5"
echo "Analyzing your project..."
echo ""
# Check for existing Ralph setup (use || true to prevent set -e from exiting)
check_existing_ralph || true
case "$RALPH_STATE" in
"complete")
print_detection_result "Ralph status" "Already enabled" "true"
if [[ "$FORCE_OVERWRITE" != "true" ]]; then
echo ""
print_warning "Ralph is already enabled in this project."
echo ""
if [[ "$NON_INTERACTIVE" != "true" ]]; then
if ! confirm "Do you want to continue anyway?" "n"; then
echo "Exiting. Use --force to overwrite."
exit $ENABLE_ALREADY_ENABLED
fi
else
echo "Use --force to overwrite existing configuration."
exit $ENABLE_ALREADY_ENABLED
fi
fi
;;
"partial")
print_detection_result "Ralph status" "Partially configured" "false"
echo ""
print_info "Missing files: ${RALPH_MISSING_FILES[*]}"
echo ""
;;
"none")
print_detection_result "Ralph status" "Not configured" "false"
;;
esac
# Detect project context
detect_project_context
print_detection_result "Project name" "$DETECTED_PROJECT_NAME" "true"
print_detection_result "Project type" "$DETECTED_PROJECT_TYPE" "true"
if [[ -n "$DETECTED_FRAMEWORK" ]]; then
print_detection_result "Framework" "$DETECTED_FRAMEWORK" "true"
fi
# Detect git info
detect_git_info
if [[ "$DETECTED_GIT_REPO" == "true" ]]; then
print_detection_result "Git repository" "Yes" "true"
if [[ "$DETECTED_GIT_GITHUB" == "true" ]]; then
print_detection_result "GitHub remote" "Yes" "true"
fi
else
print_detection_result "Git repository" "No" "false"
fi
# Detect task sources
detect_task_sources
echo ""
echo "Available task sources:"
if [[ "$DETECTED_BEADS_AVAILABLE" == "true" ]]; then
local beads_count
beads_count=$(get_beads_count)
print_detection_result "beads" "$beads_count open issues" "true"
fi
if [[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]]; then
local gh_count
gh_count=$(get_github_issue_count)
print_detection_result "GitHub Issues" "$gh_count open issues" "true"
fi
if [[ ${#DETECTED_PRD_FILES[@]} -gt 0 ]]; then
print_detection_result "PRD files" "${#DETECTED_PRD_FILES[@]} found" "true"
fi
echo ""
}
# =============================================================================
# PHASE 2: TASK SOURCE SELECTION
# =============================================================================
phase_task_source_selection() {
print_header "Task Source Selection" "Phase 2 of 5"
# If task source specified via CLI, use it
if [[ -n "$TASK_SOURCE" ]]; then
echo "Using task source from command line: $TASK_SOURCE"
SELECTED_SOURCES="$TASK_SOURCE"
return 0
fi
# If skip tasks, use empty
if [[ "$SKIP_TASKS" == "true" ]]; then
echo "Skipping task import (--skip-tasks)"
SELECTED_SOURCES=""
return 0
fi
# Non-interactive mode: auto-select available sources
if [[ "$NON_INTERACTIVE" == "true" ]]; then
local auto_sources=""
[[ "$DETECTED_BEADS_AVAILABLE" == "true" ]] && auto_sources="beads"
[[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]] && auto_sources="${auto_sources:+$auto_sources }github"
SELECTED_SOURCES="$auto_sources"
echo "Auto-selected sources: ${SELECTED_SOURCES:-none}"
return 0
fi
# Build options list
local options=()
local option_keys=()
if [[ "$DETECTED_BEADS_AVAILABLE" == "true" ]]; then
local beads_count
beads_count=$(get_beads_count)
options+=("Import from beads ($beads_count issues)")
option_keys+=("beads")
fi
if [[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]]; then
local gh_count
gh_count=$(get_github_issue_count)
options+=("Import from GitHub Issues ($gh_count issues)")
option_keys+=("github")
fi
if [[ ${#DETECTED_PRD_FILES[@]} -gt 0 ]]; then
options+=("Convert PRD/spec document (${#DETECTED_PRD_FILES[@]} found)")
option_keys+=("prd")
fi
options+=("Start with empty task list")
option_keys+=("none")
# Interactive selection
if [[ ${#options[@]} -gt 1 ]]; then
echo "Where would you like to import tasks from?"
echo ""
local selected_indices
selected_indices=$(select_multiple "Select task sources" "${options[@]}")
# Parse selected indices (comma-separated)
SELECTED_SOURCES=""
if [[ -n "$selected_indices" ]]; then
IFS=',' read -ra indices <<< "$selected_indices"
for idx in "${indices[@]}"; do
if [[ "${option_keys[$idx]}" != "none" ]]; then
SELECTED_SOURCES="${SELECTED_SOURCES:+$SELECTED_SOURCES }${option_keys[$idx]}"
fi
done
fi
else
SELECTED_SOURCES=""
fi
echo ""
echo "Selected sources: ${SELECTED_SOURCES:-none}"
}
# =============================================================================
# PHASE 3: CONFIGURATION
# =============================================================================
phase_configuration() {
print_header "Configuration" "Phase 3 of 5"
# Project name
if [[ "$NON_INTERACTIVE" != "true" ]]; then
CONFIG_PROJECT_NAME=$(prompt_text "Project name" "$DETECTED_PROJECT_NAME")
else
CONFIG_PROJECT_NAME="$DETECTED_PROJECT_NAME"
fi
# API call limit
if [[ "$NON_INTERACTIVE" != "true" ]]; then
CONFIG_MAX_CALLS=$(prompt_number "Max API calls per hour" "100" "10" "500")
else
CONFIG_MAX_CALLS=100
fi
# GitHub label (if GitHub selected)
if echo "$SELECTED_SOURCES" | grep -qw "github"; then
if [[ -n "$GITHUB_LABEL" ]]; then
CONFIG_GITHUB_LABEL="$GITHUB_LABEL"
elif [[ "$NON_INTERACTIVE" != "true" ]]; then
CONFIG_GITHUB_LABEL=$(prompt_text "GitHub issue label filter" "ralph-task")
else
CONFIG_GITHUB_LABEL="ralph-task"
fi
fi
# PRD file selection (if PRD selected)
if echo "$SELECTED_SOURCES" | grep -qw "prd"; then
if [[ -n "$PRD_FILE" ]]; then
CONFIG_PRD_FILE="$PRD_FILE"
elif [[ "$NON_INTERACTIVE" != "true" && ${#DETECTED_PRD_FILES[@]} -gt 0 ]]; then
echo ""
echo "Found PRD files:"
CONFIG_PRD_FILE=$(select_option "Select PRD file to convert" "${DETECTED_PRD_FILES[@]}")
else
CONFIG_PRD_FILE="${DETECTED_PRD_FILES[0]:-}"
fi
fi
# Show configuration summary
echo ""
print_summary "Configuration" \
"Project=$CONFIG_PROJECT_NAME" \
"Type=$DETECTED_PROJECT_TYPE" \
"Max calls/hour=$CONFIG_MAX_CALLS" \
"Task sources=${SELECTED_SOURCES:-none}"
}
# =============================================================================
# PHASE 4: FILE GENERATION
# =============================================================================
phase_file_generation() {
print_header "File Generation" "Phase 4 of 5"
# Import tasks if sources selected
local imported_tasks=""
if [[ -n "$SELECTED_SOURCES" ]]; then
echo "Importing tasks..."
if echo "$SELECTED_SOURCES" | grep -qw "beads"; then
local beads_tasks
if beads_tasks=$(fetch_beads_tasks); then
imported_tasks="${imported_tasks}${beads_tasks}
"
print_success "Imported tasks from beads"
fi
fi
if echo "$SELECTED_SOURCES" | grep -qw "github"; then
local github_tasks
if github_tasks=$(fetch_github_tasks "$CONFIG_GITHUB_LABEL"); then
imported_tasks="${imported_tasks}${github_tasks}
"
print_success "Imported tasks from GitHub"
fi
fi
if echo "$SELECTED_SOURCES" | grep -qw "prd"; then
if [[ -n "$CONFIG_PRD_FILE" && -f "$CONFIG_PRD_FILE" ]]; then
local prd_tasks
if prd_tasks=$(extract_prd_tasks "$CONFIG_PRD_FILE"); then
imported_tasks="${imported_tasks}${prd_tasks}
"
print_success "Extracted tasks from PRD: $CONFIG_PRD_FILE"
fi
fi
fi
echo ""
fi
# Set up enable environment
export ENABLE_FORCE="$FORCE_OVERWRITE"
export ENABLE_SKIP_TASKS="$SKIP_TASKS"
export ENABLE_PROJECT_NAME="$CONFIG_PROJECT_NAME"
export ENABLE_TASK_CONTENT="$imported_tasks"
# Run core enable logic
echo "Creating Ralph configuration..."
echo ""
if ! enable_ralph_in_directory; then
print_error "Failed to enable Ralph"
exit $ENABLE_ERROR
fi
# Update .ralphrc with specific settings
# Using awk instead of sed to avoid command injection from user input
if [[ -f ".ralphrc" ]]; then
# Update max calls (awk safely handles the value without shell interpretation)
awk -v val="$CONFIG_MAX_CALLS" '/^MAX_CALLS_PER_HOUR=/{$0="MAX_CALLS_PER_HOUR="val}1' .ralphrc > .ralphrc.tmp && mv .ralphrc.tmp .ralphrc
# Update GitHub label if set
if [[ -n "$CONFIG_GITHUB_LABEL" ]]; then
awk -v val="$CONFIG_GITHUB_LABEL" '/^GITHUB_TASK_LABEL=/{$0="GITHUB_TASK_LABEL=\""val"\""}1' .ralphrc > .ralphrc.tmp && mv .ralphrc.tmp .ralphrc
fi
fi
echo ""
}
# =============================================================================
# PHASE 5: VERIFICATION
# =============================================================================
phase_verification() {
print_header "Verification" "Phase 5 of 5"
echo "Checking created files..."
echo ""
# Verify required files
local all_good=true
if [[ -f ".ralph/PROMPT.md" ]]; then
print_success ".ralph/PROMPT.md"
else
print_error ".ralph/PROMPT.md - MISSING"
all_good=false
fi
if [[ -f ".ralph/@fix_plan.md" ]]; then
print_success ".ralph/@fix_plan.md"
else
print_error ".ralph/@fix_plan.md - MISSING"
all_good=false
fi
if [[ -f ".ralph/@AGENT.md" ]]; then
print_success ".ralph/@AGENT.md"
else
print_error ".ralph/@AGENT.md - MISSING"
all_good=false
fi
if [[ -f ".ralphrc" ]]; then
print_success ".ralphrc"
else
print_warning ".ralphrc - MISSING (optional)"
fi
if [[ -d ".ralph/specs" ]]; then
print_success ".ralph/specs/"
fi
if [[ -d ".ralph/logs" ]]; then
print_success ".ralph/logs/"
fi
echo ""
if [[ "$all_good" == "true" ]]; then
print_success "Ralph enabled successfully!"
echo ""
echo "Next steps:"
echo ""
print_bullet "Review and customize .ralph/PROMPT.md" "1."
print_bullet "Edit tasks in .ralph/@fix_plan.md" "2."
print_bullet "Update build commands in .ralph/@AGENT.md" "3."
print_bullet "Start Ralph: ralph --monitor" "4."
echo ""
if [[ "$NON_INTERACTIVE" != "true" ]]; then
if confirm "Show current status?" "y"; then
echo ""
ralph --status 2>/dev/null || echo "(ralph --status not available)"
fi
fi
else
print_error "Some files were not created. Please check the errors above."
exit $ENABLE_ERROR
fi
}
# =============================================================================
# MAIN
# =============================================================================
main() {
# Parse arguments
parse_arguments "$@"
# Show help if requested
if [[ "$SHOW_HELP" == "true" ]]; then
show_help
exit 0
fi
# Welcome banner
echo ""
echo -e "\033[1m╔════════════════════════════════════════════════════════════╗\033[0m"
echo -e "\033[1m║ Ralph Enable - Existing Project Wizard ║\033[0m"
echo -e "\033[1m╚════════════════════════════════════════════════════════════╝\033[0m"
echo ""
# Run phases
phase_environment_detection
phase_task_source_selection
phase_configuration
phase_file_generation
phase_verification
exit $ENABLE_SUCCESS
}
# Run main
main "$@"

405
ralph_enable_ci.sh Executable file
View file

@ -0,0 +1,405 @@
#!/bin/bash
# Ralph Enable CI - Non-Interactive Version for Automation
# Adds Ralph configuration with sensible defaults
#
# Usage:
# ralph enable-ci # Auto-detect and enable
# ralph enable-ci --from beads # With specific task source
# ralph enable-ci --json # Output JSON result
#
# Exit codes:
# 0 - Success: Ralph enabled
# 1 - Error: General error
# 2 - Already enabled (use --force to override)
# 3 - Invalid arguments
# 4 - File not found (e.g., PRD file)
# 5 - Dependency missing (e.g., jq for --json)
#
# Version: 0.11.0
set -e
# Get script directory for library loading
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Try to load libraries from global installation first, then local
RALPH_HOME="${RALPH_HOME:-$HOME/.ralph}"
if [[ -f "$RALPH_HOME/lib/enable_core.sh" ]]; then
LIB_DIR="$RALPH_HOME/lib"
elif [[ -f "$SCRIPT_DIR/lib/enable_core.sh" ]]; then
LIB_DIR="$SCRIPT_DIR/lib"
else
echo '{"error": "Cannot find Ralph libraries", "code": 1}' >&2
exit 1
fi
# Disable colors for CI
export ENABLE_USE_COLORS="false"
# Source libraries
source "$LIB_DIR/enable_core.sh"
source "$LIB_DIR/task_sources.sh"
# =============================================================================
# CONFIGURATION
# =============================================================================
# Command line options
FORCE_OVERWRITE=false
TASK_SOURCE=""
PRD_FILE=""
GITHUB_LABEL="ralph-task"
PROJECT_NAME=""
PROJECT_TYPE=""
OUTPUT_JSON=false
QUIET=false
SHOW_HELP=false
# Version
VERSION="0.11.0"
# =============================================================================
# HELP
# =============================================================================
show_help() {
cat << EOF
Ralph Enable CI - Non-Interactive Version for Automation
Usage: ralph enable-ci [OPTIONS]
Options:
--from <source> Import tasks from: beads, github, prd, none
--prd <file> PRD file to convert (when --from prd)
--label <label> GitHub label filter (default: ralph-task)
--project-name <name> Override detected project name
--project-type <type> Override detected type (typescript, python, etc.)
--force Overwrite existing .ralph/ configuration
--json Output result as JSON
--quiet Suppress non-error output
-h, --help Show this help message
-v, --version Show version
Exit Codes:
0 - Success: Ralph enabled
1 - Error: General error
2 - Already enabled: Use --force to override
3 - Invalid arguments
4 - File not found (e.g., PRD file)
5 - Dependency missing (e.g., jq for --json)
Examples:
# Auto-detect and enable with defaults
ralph enable-ci
# Enable with beads tasks
ralph enable-ci --from beads
# Enable with GitHub issues
ralph enable-ci --from github --label "sprint-1"
# Enable with PRD conversion
ralph enable-ci --from prd --prd docs/requirements.md
# Force overwrite and output JSON
ralph enable-ci --force --json
# Override project detection
ralph enable-ci --project-name my-app --project-type typescript
JSON Output Format:
{
"success": true,
"project_name": "my-project",
"project_type": "typescript",
"files_created": [".ralph/PROMPT.md", ...],
"tasks_imported": 15,
"message": "Ralph enabled successfully"
}
EOF
}
# =============================================================================
# ARGUMENT PARSING
# =============================================================================
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
--from)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
TASK_SOURCE="$2"
shift 2
else
output_error "--from requires a source (beads, github, prd, none)"
exit $ENABLE_INVALID_ARGS
fi
;;
--prd)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
PRD_FILE="$2"
shift 2
else
output_error "--prd requires a file path"
exit $ENABLE_INVALID_ARGS
fi
;;
--label)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
GITHUB_LABEL="$2"
shift 2
else
output_error "--label requires a label name"
exit $ENABLE_INVALID_ARGS
fi
;;
--project-name)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
PROJECT_NAME="$2"
shift 2
else
output_error "--project-name requires a name"
exit $ENABLE_INVALID_ARGS
fi
;;
--project-type)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
PROJECT_TYPE="$2"
shift 2
else
output_error "--project-type requires a type"
exit $ENABLE_INVALID_ARGS
fi
;;
--force)
FORCE_OVERWRITE=true
shift
;;
--json)
if ! command -v jq &>/dev/null; then
echo "Error: --json requires jq to be installed" >&2
exit $ENABLE_DEPENDENCY_MISSING
fi
OUTPUT_JSON=true
shift
;;
--quiet)
QUIET=true
shift
;;
-h|--help)
SHOW_HELP=true
shift
;;
-v|--version)
if [[ "$OUTPUT_JSON" == "true" ]]; then
echo "{\"version\": \"$VERSION\"}"
else
echo "ralph enable-ci version $VERSION"
fi
exit 0
;;
*)
output_error "Unknown option: $1"
exit $ENABLE_INVALID_ARGS
;;
esac
done
}
# =============================================================================
# OUTPUT FUNCTIONS
# =============================================================================
# Track created files for JSON output
declare -a CREATED_FILES=()
TASKS_IMPORTED=0
output_message() {
if [[ "$QUIET" != "true" && "$OUTPUT_JSON" != "true" ]]; then
echo "$1"
fi
}
output_error() {
if [[ "$OUTPUT_JSON" == "true" ]]; then
echo "{\"error\": \"$1\", \"code\": 1}" >&2
else
echo "Error: $1" >&2
fi
}
output_success() {
local project_name="$1"
local project_type="$2"
if [[ "$OUTPUT_JSON" == "true" ]]; then
local files_json
files_json=$(printf '%s\n' "${CREATED_FILES[@]}" | jq -R . | jq -s .)
cat << EOF
{
"success": true,
"project_name": "$project_name",
"project_type": "$project_type",
"files_created": $files_json,
"tasks_imported": $TASKS_IMPORTED,
"message": "Ralph enabled successfully"
}
EOF
else
echo "Ralph enabled successfully for: $project_name ($project_type)"
echo "Files created: ${#CREATED_FILES[@]}"
echo "Tasks imported: $TASKS_IMPORTED"
fi
}
output_already_enabled() {
if [[ "$OUTPUT_JSON" == "true" ]]; then
echo '{"success": false, "code": 2, "message": "Ralph already enabled. Use --force to override."}'
else
echo "Ralph is already enabled in this project. Use --force to override."
fi
}
# =============================================================================
# MAIN LOGIC
# =============================================================================
main() {
# Parse arguments
parse_arguments "$@"
# Show help if requested
if [[ "$SHOW_HELP" == "true" ]]; then
show_help
exit 0
fi
output_message "Ralph Enable CI - Non-Interactive Mode"
output_message ""
# Check existing state (use || true to prevent set -e from exiting)
check_existing_ralph || true
if [[ "$RALPH_STATE" == "complete" && "$FORCE_OVERWRITE" != "true" ]]; then
output_already_enabled
exit $ENABLE_ALREADY_ENABLED
fi
# Detect project context
detect_project_context
output_message "Detected: $DETECTED_PROJECT_NAME ($DETECTED_PROJECT_TYPE)"
# Override with CLI options if provided
if [[ -n "$PROJECT_NAME" ]]; then
DETECTED_PROJECT_NAME="$PROJECT_NAME"
fi
if [[ -n "$PROJECT_TYPE" ]]; then
DETECTED_PROJECT_TYPE="$PROJECT_TYPE"
fi
# Auto-detect task source if not specified
if [[ -z "$TASK_SOURCE" ]]; then
detect_task_sources
if [[ "$DETECTED_BEADS_AVAILABLE" == "true" ]]; then
TASK_SOURCE="beads"
output_message "Auto-detected task source: beads"
elif [[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]]; then
TASK_SOURCE="github"
output_message "Auto-detected task source: github"
elif [[ ${#DETECTED_PRD_FILES[@]} -gt 0 ]]; then
TASK_SOURCE="prd"
PRD_FILE="${DETECTED_PRD_FILES[0]}"
output_message "Auto-detected task source: prd ($PRD_FILE)"
else
TASK_SOURCE="none"
output_message "No task sources detected, using defaults"
fi
fi
# Import tasks
local imported_tasks=""
case "$TASK_SOURCE" in
beads)
if beads_tasks=$(fetch_beads_tasks 2>/dev/null); then
imported_tasks="$beads_tasks"
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[' || echo "0")
output_message "Imported $TASKS_IMPORTED tasks from beads"
fi
;;
github)
if github_tasks=$(fetch_github_tasks "$GITHUB_LABEL" 2>/dev/null); then
imported_tasks="$github_tasks"
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[' || echo "0")
output_message "Imported $TASKS_IMPORTED tasks from GitHub"
fi
;;
prd)
if [[ -n "$PRD_FILE" && -f "$PRD_FILE" ]]; then
if prd_tasks=$(extract_prd_tasks "$PRD_FILE" 2>/dev/null); then
imported_tasks="$prd_tasks"
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[' || echo "0")
output_message "Extracted $TASKS_IMPORTED tasks from PRD"
fi
else
output_error "PRD file not found: $PRD_FILE"
exit $ENABLE_FILE_NOT_FOUND
fi
;;
none|"")
output_message "Skipping task import"
;;
*)
output_error "Unknown task source: $TASK_SOURCE"
exit $ENABLE_ERROR
;;
esac
# Set up enable environment
export ENABLE_FORCE="$FORCE_OVERWRITE"
export ENABLE_SKIP_TASKS="false"
export ENABLE_PROJECT_NAME="$DETECTED_PROJECT_NAME"
export ENABLE_PROJECT_TYPE="$DETECTED_PROJECT_TYPE"
export ENABLE_TASK_CONTENT="$imported_tasks"
# Run core enable logic
output_message ""
output_message "Creating Ralph configuration..."
# Suppress enable_ralph_in_directory output when in JSON mode
if [[ "$OUTPUT_JSON" == "true" ]]; then
if ! enable_ralph_in_directory >/dev/null 2>&1; then
output_error "Failed to enable Ralph"
exit $ENABLE_ERROR
fi
else
if ! enable_ralph_in_directory; then
output_error "Failed to enable Ralph"
exit $ENABLE_ERROR
fi
fi
# Track created files
[[ -f ".ralph/PROMPT.md" ]] && CREATED_FILES+=(".ralph/PROMPT.md")
[[ -f ".ralph/@fix_plan.md" ]] && CREATED_FILES+=(".ralph/@fix_plan.md")
[[ -f ".ralph/@AGENT.md" ]] && CREATED_FILES+=(".ralph/@AGENT.md")
[[ -f ".ralphrc" ]] && CREATED_FILES+=(".ralphrc")
# Verify required files exist
if [[ ! -f ".ralph/PROMPT.md" ]] || [[ ! -f ".ralph/@fix_plan.md" ]]; then
output_error "Required files were not created"
exit $ENABLE_ERROR
fi
# Output success
output_message ""
output_success "$DETECTED_PROJECT_NAME" "$DETECTED_PROJECT_TYPE"
exit $ENABLE_SUCCESS
}
# Run main
main "$@"

View file

@ -21,18 +21,30 @@ DOCS_DIR="$RALPH_DIR/docs/generated"
STATUS_FILE="$RALPH_DIR/status.json" STATUS_FILE="$RALPH_DIR/status.json"
PROGRESS_FILE="$RALPH_DIR/progress.json" PROGRESS_FILE="$RALPH_DIR/progress.json"
CLAUDE_CODE_CMD="claude" CLAUDE_CODE_CMD="claude"
MAX_CALLS_PER_HOUR=100 # Adjust based on your plan
VERBOSE_PROGRESS=false # Default: no verbose progress updates
CLAUDE_TIMEOUT_MINUTES=15 # Default: 15 minutes timeout for Claude Code execution
SLEEP_DURATION=3600 # 1 hour in seconds SLEEP_DURATION=3600 # 1 hour in seconds
CALL_COUNT_FILE="$RALPH_DIR/.call_count" CALL_COUNT_FILE="$RALPH_DIR/.call_count"
TIMESTAMP_FILE="$RALPH_DIR/.last_reset" TIMESTAMP_FILE="$RALPH_DIR/.last_reset"
USE_TMUX=false USE_TMUX=false
# Save environment variable state BEFORE setting defaults
# These are used by load_ralphrc() to determine which values came from environment
_env_MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-}"
_env_CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-}"
_env_CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-}"
_env_CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-}"
_env_CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-}"
_env_CLAUDE_SESSION_EXPIRY_HOURS="${CLAUDE_SESSION_EXPIRY_HOURS:-}"
_env_VERBOSE_PROGRESS="${VERBOSE_PROGRESS:-}"
# Now set defaults (only if not already set by environment)
MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-100}"
VERBOSE_PROGRESS="${VERBOSE_PROGRESS:-false}"
CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-15}"
# Modern Claude CLI configuration (Phase 1.1) # Modern Claude CLI configuration (Phase 1.1)
CLAUDE_OUTPUT_FORMAT="json" # Options: json, text CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-json}"
CLAUDE_ALLOWED_TOOLS="Write,Bash(git *),Read" # Comma-separated list of allowed tools CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-Write,Bash(git *),Read}"
CLAUDE_USE_CONTINUE=true # Enable session continuity CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-true}"
CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id" # Session ID persistence file CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id" # Session ID persistence file
CLAUDE_MIN_VERSION="2.0.76" # Minimum required Claude CLI version CLAUDE_MIN_VERSION="2.0.76" # Minimum required Claude CLI version
@ -73,6 +85,65 @@ MAX_CONSECUTIVE_TEST_LOOPS=3
MAX_CONSECUTIVE_DONE_SIGNALS=2 MAX_CONSECUTIVE_DONE_SIGNALS=2
TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only, flag it TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only, flag it
# .ralphrc configuration file
RALPHRC_FILE=".ralphrc"
RALPHRC_LOADED=false
# load_ralphrc - Load project-specific configuration from .ralphrc
#
# This function sources .ralphrc if it exists, applying project-specific
# settings. Environment variables take precedence over .ralphrc values.
#
# Configuration values that can be overridden:
# - MAX_CALLS_PER_HOUR
# - CLAUDE_TIMEOUT_MINUTES
# - CLAUDE_OUTPUT_FORMAT
# - ALLOWED_TOOLS (mapped to CLAUDE_ALLOWED_TOOLS)
# - SESSION_CONTINUITY (mapped to CLAUDE_USE_CONTINUE)
# - SESSION_EXPIRY_HOURS (mapped to CLAUDE_SESSION_EXPIRY_HOURS)
# - CB_NO_PROGRESS_THRESHOLD
# - CB_SAME_ERROR_THRESHOLD
# - CB_OUTPUT_DECLINE_THRESHOLD
# - RALPH_VERBOSE
#
load_ralphrc() {
if [[ ! -f "$RALPHRC_FILE" ]]; then
return 0
fi
# Source .ralphrc (this may override default values)
# shellcheck source=/dev/null
source "$RALPHRC_FILE"
# Map .ralphrc variable names to internal names
if [[ -n "${ALLOWED_TOOLS:-}" ]]; then
CLAUDE_ALLOWED_TOOLS="$ALLOWED_TOOLS"
fi
if [[ -n "${SESSION_CONTINUITY:-}" ]]; then
CLAUDE_USE_CONTINUE="$SESSION_CONTINUITY"
fi
if [[ -n "${SESSION_EXPIRY_HOURS:-}" ]]; then
CLAUDE_SESSION_EXPIRY_HOURS="$SESSION_EXPIRY_HOURS"
fi
if [[ -n "${RALPH_VERBOSE:-}" ]]; then
VERBOSE_PROGRESS="$RALPH_VERBOSE"
fi
# Restore ONLY values that were explicitly set via environment variables
# (not script defaults). The _env_* variables were captured BEFORE defaults were set.
# If _env_* is non-empty, the user explicitly set it in their environment.
[[ -n "$_env_MAX_CALLS_PER_HOUR" ]] && MAX_CALLS_PER_HOUR="$_env_MAX_CALLS_PER_HOUR"
[[ -n "$_env_CLAUDE_TIMEOUT_MINUTES" ]] && CLAUDE_TIMEOUT_MINUTES="$_env_CLAUDE_TIMEOUT_MINUTES"
[[ -n "$_env_CLAUDE_OUTPUT_FORMAT" ]] && CLAUDE_OUTPUT_FORMAT="$_env_CLAUDE_OUTPUT_FORMAT"
[[ -n "$_env_CLAUDE_ALLOWED_TOOLS" ]] && CLAUDE_ALLOWED_TOOLS="$_env_CLAUDE_ALLOWED_TOOLS"
[[ -n "$_env_CLAUDE_USE_CONTINUE" ]] && CLAUDE_USE_CONTINUE="$_env_CLAUDE_USE_CONTINUE"
[[ -n "$_env_CLAUDE_SESSION_EXPIRY_HOURS" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_env_CLAUDE_SESSION_EXPIRY_HOURS"
[[ -n "$_env_VERBOSE_PROGRESS" ]] && VERBOSE_PROGRESS="$_env_VERBOSE_PROGRESS"
RALPHRC_LOADED=true
return 0
}
# Colors for terminal output # Colors for terminal output
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@ -1053,6 +1124,12 @@ loop_count=0
# Main loop # Main loop
main() { main() {
# Load project-specific configuration from .ralphrc
if load_ralphrc; then
if [[ "$RALPHRC_LOADED" == "true" ]]; then
log_status "INFO" "Loaded configuration from .ralphrc"
fi
fi
log_status "SUCCESS" "🚀 Ralph loop starting with Claude Code" log_status "SUCCESS" "🚀 Ralph loop starting with Claude Code"
log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR" log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR"
@ -1087,10 +1164,11 @@ main() {
echo "" echo ""
echo "To fix this:" echo "To fix this:"
echo " 1. Create a new project: ralph-setup my-project" echo " 1. Enable Ralph in existing project: ralph-enable"
echo " 2. Import existing requirements: ralph-import requirements.md" echo " 2. Create a new project: ralph-setup my-project"
echo " 3. Navigate to an existing Ralph project directory" echo " 3. Import existing requirements: ralph-import requirements.md"
echo " 4. Or create .ralph/PROMPT.md manually in this directory" echo " 4. Navigate to an existing Ralph project directory"
echo " 5. Or create .ralph/PROMPT.md manually in this directory"
echo "" echo ""
echo "Ralph projects should contain: .ralph/PROMPT.md, .ralph/@fix_plan.md, .ralph/specs/, src/, etc." echo "Ralph projects should contain: .ralph/PROMPT.md, .ralph/@fix_plan.md, .ralph/specs/, src/, etc."
exit 1 exit 1

View file

@ -0,0 +1,95 @@
# .ralphrc - Ralph project configuration
# Generated by: ralph enable
# Documentation: https://github.com/frankbria/ralph-claude-code
#
# This file configures Ralph's behavior for this specific project.
# Values here override global Ralph defaults.
# Environment variables override values in this file.
# =============================================================================
# PROJECT IDENTIFICATION
# =============================================================================
# Project name (used in prompts and logging)
PROJECT_NAME="${PROJECT_NAME:-my-project}"
# Project type: javascript, typescript, python, rust, go, unknown
PROJECT_TYPE="${PROJECT_TYPE:-unknown}"
# =============================================================================
# LOOP SETTINGS
# =============================================================================
# Maximum API calls per hour (rate limiting)
MAX_CALLS_PER_HOUR=100
# Timeout for each Claude Code invocation (in minutes)
CLAUDE_TIMEOUT_MINUTES=15
# Output format: json (structured) or text (legacy)
CLAUDE_OUTPUT_FORMAT="json"
# =============================================================================
# TOOL PERMISSIONS
# =============================================================================
# Comma-separated list of allowed tools for Claude
# Common tools: Write, Read, Edit, Grep, Glob
# Bash patterns: Bash(git *), Bash(npm *), Bash(pytest)
ALLOWED_TOOLS="Write,Read,Edit,Bash(git *),Bash(npm *),Bash(pytest)"
# =============================================================================
# SESSION MANAGEMENT
# =============================================================================
# Enable session continuity (maintain context across loops)
SESSION_CONTINUITY=true
# Session expiration time in hours (start fresh after this time)
SESSION_EXPIRY_HOURS=24
# =============================================================================
# TASK SOURCES
# =============================================================================
# Where to import tasks from (comma-separated)
# Options: local, beads, github
TASK_SOURCES="local"
# GitHub label for task filtering (when github is in TASK_SOURCES)
GITHUB_TASK_LABEL="ralph-task"
# Beads filter for task import (when beads is in TASK_SOURCES)
BEADS_FILTER="status:open"
# =============================================================================
# CIRCUIT BREAKER THRESHOLDS
# =============================================================================
# Open circuit after N loops with no file changes
CB_NO_PROGRESS_THRESHOLD=3
# Open circuit after N loops with the same error
CB_SAME_ERROR_THRESHOLD=5
# Open circuit if output declines by more than N percent
CB_OUTPUT_DECLINE_THRESHOLD=70
# =============================================================================
# ADVANCED SETTINGS
# =============================================================================
# Minimum Claude CLI version required
CLAUDE_MIN_VERSION="2.0.76"
# Enable verbose logging
RALPH_VERBOSE=false
# Custom prompt file (relative to .ralph/)
# PROMPT_FILE="PROMPT.md"
# Custom fix plan file (relative to .ralph/)
# FIX_PLAN_FILE="@fix_plan.md"
# Custom agent file (relative to .ralph/)
# AGENT_FILE="@AGENT.md"

View file

@ -80,6 +80,43 @@ EOF
#!/bin/bash #!/bin/bash
# Mock migrate_to_ralph_folder.sh # Mock migrate_to_ralph_folder.sh
echo "Migration running" echo "Migration running"
EOF
cat > "$MOCK_SOURCE_DIR/ralph_enable.sh" << 'EOF'
#!/bin/bash
# Mock ralph_enable.sh
echo "Ralph enable running"
EOF
cat > "$MOCK_SOURCE_DIR/ralph_enable_ci.sh" << 'EOF'
#!/bin/bash
# Mock ralph_enable_ci.sh
echo "Ralph enable CI running"
EOF
# Create mock lib files for new enable functionality
cat > "$MOCK_SOURCE_DIR/lib/enable_core.sh" << 'EOF'
#!/bin/bash
# Mock enable_core.sh
check_existing_ralph() { :; }
EOF
cat > "$MOCK_SOURCE_DIR/lib/wizard_utils.sh" << 'EOF'
#!/bin/bash
# Mock wizard_utils.sh
confirm() { :; }
EOF
cat > "$MOCK_SOURCE_DIR/lib/task_sources.sh" << 'EOF'
#!/bin/bash
# Mock task_sources.sh
fetch_beads_tasks() { :; }
EOF
cat > "$MOCK_SOURCE_DIR/lib/timeout_utils.sh" << 'EOF'
#!/bin/bash
# Mock timeout_utils.sh
portable_timeout() { timeout "$@"; }
EOF EOF
chmod +x "$MOCK_SOURCE_DIR"/*.sh chmod +x "$MOCK_SOURCE_DIR"/*.sh

View file

@ -157,8 +157,9 @@ teardown() {
@test "CLAUDE_OUTPUT_FORMAT defaults to json" { @test "CLAUDE_OUTPUT_FORMAT defaults to json" {
# Verify by checking the default in ralph_loop.sh via grep # Verify by checking the default in ralph_loop.sh via grep
# The default is set via ${CLAUDE_OUTPUT_FORMAT:-json} pattern
run grep 'CLAUDE_OUTPUT_FORMAT=' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh" run grep 'CLAUDE_OUTPUT_FORMAT=' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
[[ "$output" == *'"json"'* ]] [[ "$output" == *"json"* ]]
} }
@test "CLAUDE_ALLOWED_TOOLS has sensible defaults" { @test "CLAUDE_ALLOWED_TOOLS has sensible defaults" {
@ -622,3 +623,29 @@ EOF
[[ "$found_prompt" == "true" ]] [[ "$found_prompt" == "true" ]]
} }
# =============================================================================
# .RALPHRC CONFIGURATION LOADING TESTS
# Tests for the environment variable precedence fix
# =============================================================================
@test "load_ralphrc uses env var capture pattern for precedence" {
# Verify the implementation pattern: _env_* variables capture state before defaults
# This test validates the pattern is correctly implemented in ralph_loop.sh
run grep '_env_MAX_CALLS_PER_HOUR=' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# Should capture env var state BEFORE setting defaults
[[ "$output" == *'${MAX_CALLS_PER_HOUR:-}'* ]]
}
@test "load_ralphrc restores only env var overrides, not defaults" {
# Verify that load_ralphrc uses _env_* pattern for restoration
# This ensures .ralphrc values are not overwritten by script defaults
run grep -A5 'Restore ONLY values' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# Should check _env_* variables (not saved_* which would always have values)
[[ "$output" == *'_env_MAX_CALLS_PER_HOUR'* ]]
[[ "$output" == *'_env_CLAUDE_TIMEOUT_MINUTES'* ]]
}

View file

@ -0,0 +1,388 @@
#!/usr/bin/env bats
# Unit tests for lib/enable_core.sh
# Tests idempotency, safe file creation, project detection, and template generation
load '../helpers/test_helper'
load '../helpers/fixtures'
# Path to enable_core.sh
ENABLE_CORE="${BATS_TEST_DIRNAME}/../../lib/enable_core.sh"
setup() {
# Create temporary test directory
TEST_DIR="$(mktemp -d)"
cd "$TEST_DIR"
# Source the library (disable set -e for testing)
set +e
source "$ENABLE_CORE"
set -e
}
teardown() {
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
cd /
rm -rf "$TEST_DIR"
fi
}
# =============================================================================
# IDEMPOTENCY CHECKS (5 tests)
# =============================================================================
@test "check_existing_ralph returns 'none' when no .ralph directory exists" {
check_existing_ralph || true
assert_equal "$RALPH_STATE" "none"
}
@test "check_existing_ralph returns 'complete' when all required files exist" {
mkdir -p .ralph
echo "# PROMPT" > .ralph/PROMPT.md
echo "# Fix Plan" > .ralph/@fix_plan.md
echo "# Agent" > .ralph/@AGENT.md
check_existing_ralph || true
assert_equal "$RALPH_STATE" "complete"
}
@test "check_existing_ralph returns 'partial' when some files are missing" {
mkdir -p .ralph
echo "# PROMPT" > .ralph/PROMPT.md
# Missing @fix_plan.md and @AGENT.md
check_existing_ralph || true
assert_equal "$RALPH_STATE" "partial"
[[ " ${RALPH_MISSING_FILES[*]} " =~ ".ralph/@fix_plan.md" ]]
[[ " ${RALPH_MISSING_FILES[*]} " =~ ".ralph/@AGENT.md" ]]
}
@test "is_ralph_enabled returns 0 when fully enabled" {
mkdir -p .ralph
echo "# PROMPT" > .ralph/PROMPT.md
echo "# Fix Plan" > .ralph/@fix_plan.md
echo "# Agent" > .ralph/@AGENT.md
run is_ralph_enabled
assert_success
}
@test "is_ralph_enabled returns 1 when not enabled" {
run is_ralph_enabled
assert_failure
}
# =============================================================================
# SAFE FILE OPERATIONS (5 tests)
# =============================================================================
@test "safe_create_file creates file that doesn't exist" {
run safe_create_file "test.txt" "test content"
assert_success
[[ -f "test.txt" ]]
[[ "$(cat test.txt)" == "test content" ]]
}
@test "safe_create_file skips existing file" {
echo "original content" > existing.txt
run safe_create_file "existing.txt" "new content"
assert_failure # Returns 1 for skip
assert_equal "$(cat existing.txt)" "original content"
[[ "$output" =~ "SKIP" ]] || [[ "$output" =~ "already exists" ]]
}
@test "safe_create_file creates parent directories" {
run safe_create_file "nested/dir/file.txt" "nested content"
assert_success
[[ -f "nested/dir/file.txt" ]]
[[ "$(cat nested/dir/file.txt)" == "nested content" ]]
}
@test "safe_create_dir creates directory that doesn't exist" {
run safe_create_dir "new_dir"
assert_success
[[ -d "new_dir" ]]
}
@test "safe_create_dir succeeds when directory already exists" {
mkdir existing_dir
run safe_create_dir "existing_dir"
assert_success
[[ -d "existing_dir" ]]
}
# =============================================================================
# DIRECTORY STRUCTURE (2 tests)
# =============================================================================
@test "create_ralph_structure creates all required directories" {
run create_ralph_structure
assert_success
[[ -d ".ralph" ]]
[[ -d ".ralph/specs" ]]
[[ -d ".ralph/examples" ]]
[[ -d ".ralph/logs" ]]
[[ -d ".ralph/docs/generated" ]]
}
@test "create_ralph_structure is idempotent" {
create_ralph_structure
echo "test" > .ralph/specs/test.txt
run create_ralph_structure
assert_success
[[ -f ".ralph/specs/test.txt" ]]
}
# =============================================================================
# PROJECT DETECTION (6 tests)
# =============================================================================
@test "detect_project_context identifies TypeScript from package.json" {
cat > package.json << 'EOF'
{
"name": "my-ts-project",
"devDependencies": {
"typescript": "^5.0.0"
}
}
EOF
detect_project_context
assert_equal "$DETECTED_PROJECT_TYPE" "typescript"
assert_equal "$DETECTED_PROJECT_NAME" "my-ts-project"
}
@test "detect_project_context identifies JavaScript from package.json" {
cat > package.json << 'EOF'
{
"name": "my-js-project"
}
EOF
detect_project_context
assert_equal "$DETECTED_PROJECT_TYPE" "javascript"
}
@test "detect_project_context identifies Python from pyproject.toml" {
cat > pyproject.toml << 'EOF'
[project]
name = "my-python-project"
EOF
detect_project_context
assert_equal "$DETECTED_PROJECT_TYPE" "python"
}
@test "detect_project_context identifies Next.js framework" {
cat > package.json << 'EOF'
{
"name": "nextjs-app",
"dependencies": {
"next": "^14.0.0"
}
}
EOF
detect_project_context
assert_equal "$DETECTED_FRAMEWORK" "nextjs"
}
@test "detect_project_context identifies FastAPI framework" {
cat > pyproject.toml << 'EOF'
[project]
name = "fastapi-app"
dependencies = ["fastapi>=0.100.0"]
EOF
detect_project_context
assert_equal "$DETECTED_FRAMEWORK" "fastapi"
}
@test "detect_project_context falls back to folder name" {
detect_project_context
# Should use the temp directory name
[[ -n "$DETECTED_PROJECT_NAME" ]]
}
# =============================================================================
# GIT DETECTION (3 tests)
# =============================================================================
@test "detect_git_info detects git repository" {
git init >/dev/null 2>&1
detect_git_info
assert_equal "$DETECTED_GIT_REPO" "true"
}
@test "detect_git_info detects non-git directory" {
detect_git_info
assert_equal "$DETECTED_GIT_REPO" "false"
}
@test "detect_git_info detects GitHub remote" {
git init >/dev/null 2>&1
git remote add origin git@github.com:user/repo.git 2>/dev/null || true
detect_git_info
assert_equal "$DETECTED_GIT_GITHUB" "true"
}
# =============================================================================
# TASK SOURCE DETECTION (2 tests)
# =============================================================================
@test "detect_task_sources detects .beads directory" {
mkdir -p .beads
detect_task_sources
assert_equal "$DETECTED_BEADS_AVAILABLE" "true"
}
@test "detect_task_sources finds PRD files" {
mkdir -p docs
echo "# Requirements" > docs/requirements.md
detect_task_sources
[[ ${#DETECTED_PRD_FILES[@]} -gt 0 ]]
}
# =============================================================================
# TEMPLATE GENERATION (4 tests)
# =============================================================================
@test "generate_prompt_md includes project name" {
output=$(generate_prompt_md "my-project" "typescript")
[[ "$output" =~ "my-project" ]]
}
@test "generate_prompt_md includes project type" {
output=$(generate_prompt_md "my-project" "python")
[[ "$output" =~ "python" ]]
}
@test "generate_agent_md includes build command" {
output=$(generate_agent_md "npm run build" "npm test" "npm start")
[[ "$output" =~ "npm run build" ]]
[[ "$output" =~ "npm test" ]]
}
@test "generate_ralphrc includes project configuration" {
output=$(generate_ralphrc "my-project" "typescript" "local,beads")
[[ "$output" =~ "PROJECT_NAME=\"my-project\"" ]]
[[ "$output" =~ "PROJECT_TYPE=\"typescript\"" ]]
[[ "$output" =~ "TASK_SOURCES=\"local,beads\"" ]]
}
# =============================================================================
# FULL ENABLE FLOW (3 tests)
# =============================================================================
@test "enable_ralph_in_directory creates all required files" {
export ENABLE_FORCE="false"
export ENABLE_SKIP_TASKS="true"
export ENABLE_PROJECT_NAME="test-project"
run enable_ralph_in_directory
assert_success
[[ -f ".ralph/PROMPT.md" ]]
[[ -f ".ralph/@fix_plan.md" ]]
[[ -f ".ralph/@AGENT.md" ]]
[[ -f ".ralphrc" ]]
}
@test "enable_ralph_in_directory returns ALREADY_ENABLED when complete and no force" {
mkdir -p .ralph
echo "# PROMPT" > .ralph/PROMPT.md
echo "# Fix Plan" > .ralph/@fix_plan.md
echo "# Agent" > .ralph/@AGENT.md
export ENABLE_FORCE="false"
run enable_ralph_in_directory
assert_equal "$status" "$ENABLE_ALREADY_ENABLED"
}
@test "enable_ralph_in_directory overwrites with force flag" {
mkdir -p .ralph
echo "old content" > .ralph/PROMPT.md
echo "old fix plan" > .ralph/@fix_plan.md
echo "old agent" > .ralph/@AGENT.md
export ENABLE_FORCE="true"
export ENABLE_PROJECT_NAME="new-project"
run enable_ralph_in_directory
assert_success
# Verify files were actually overwritten, not just skipped
local prompt_content
prompt_content=$(cat .ralph/PROMPT.md)
# Should contain new project name, not "old content"
[[ "$prompt_content" != "old content" ]]
[[ "$prompt_content" == *"new-project"* ]]
}
@test "safe_create_file overwrites existing file when ENABLE_FORCE is true" {
# Create existing file with old content
echo "original content" > test_file.txt
export ENABLE_FORCE="true"
run safe_create_file "test_file.txt" "new content"
assert_success
# Verify file was overwritten
local content
content=$(cat test_file.txt)
[[ "$content" == "new content" ]]
}
@test "safe_create_file skips existing file when ENABLE_FORCE is false" {
# Create existing file with old content
echo "original content" > test_file.txt
export ENABLE_FORCE="false"
run safe_create_file "test_file.txt" "new content"
# Should return 1 (skipped)
assert_failure
# Verify file was NOT overwritten
local content
content=$(cat test_file.txt)
[[ "$content" == "original content" ]]
}

View file

@ -0,0 +1,268 @@
#!/usr/bin/env bats
# Integration tests for ralph_enable.sh and ralph_enable_ci.sh
# Tests the full enable wizard flow and CI version
load '../helpers/test_helper'
load '../helpers/fixtures'
# Paths to scripts
RALPH_ENABLE="${BATS_TEST_DIRNAME}/../../ralph_enable.sh"
RALPH_ENABLE_CI="${BATS_TEST_DIRNAME}/../../ralph_enable_ci.sh"
setup() {
# Create temporary test directory
TEST_DIR="$(mktemp -d)"
cd "$TEST_DIR"
# Initialize git repo (required by some detection)
git init > /dev/null 2>&1
git config user.email "test@example.com"
git config user.name "Test User"
}
teardown() {
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
cd /
rm -rf "$TEST_DIR"
fi
}
# =============================================================================
# HELP AND VERSION (4 tests)
# =============================================================================
@test "ralph enable --help shows usage information" {
run bash "$RALPH_ENABLE" --help
assert_success
[[ "$output" =~ "Usage:" ]]
[[ "$output" =~ "--from" ]]
[[ "$output" =~ "--force" ]]
}
@test "ralph enable --version shows version" {
run bash "$RALPH_ENABLE" --version
assert_success
[[ "$output" =~ "version" ]]
}
@test "ralph enable-ci --help shows usage information" {
run bash "$RALPH_ENABLE_CI" --help
assert_success
[[ "$output" =~ "Usage:" ]]
[[ "$output" =~ "Exit Codes:" ]]
}
@test "ralph enable-ci --version shows version" {
run bash "$RALPH_ENABLE_CI" --version
assert_success
[[ "$output" =~ "version" ]]
}
# =============================================================================
# CI VERSION TESTS (8 tests)
# =============================================================================
@test "ralph enable-ci creates .ralph structure in empty directory" {
run bash "$RALPH_ENABLE_CI" --from none
assert_success
[[ -d ".ralph" ]]
[[ -f ".ralph/PROMPT.md" ]]
[[ -f ".ralph/@fix_plan.md" ]]
[[ -f ".ralph/@AGENT.md" ]]
}
@test "ralph enable-ci creates .ralphrc configuration" {
run bash "$RALPH_ENABLE_CI" --from none
assert_success
[[ -f ".ralphrc" ]]
}
@test "ralph enable-ci detects TypeScript project" {
cat > package.json << 'EOF'
{
"name": "test-ts-project",
"devDependencies": {
"typescript": "^5.0.0"
}
}
EOF
run bash "$RALPH_ENABLE_CI" --from none
assert_success
grep -q "PROJECT_TYPE=\"typescript\"" .ralphrc
}
@test "ralph enable-ci detects Python project" {
cat > pyproject.toml << 'EOF'
[project]
name = "test-python-project"
EOF
run bash "$RALPH_ENABLE_CI" --from none
assert_success
grep -q "PROJECT_TYPE=\"python\"" .ralphrc
}
@test "ralph enable-ci respects --project-name override" {
run bash "$RALPH_ENABLE_CI" --from none --project-name "custom-name"
assert_success
grep -q "PROJECT_NAME=\"custom-name\"" .ralphrc
}
@test "ralph enable-ci respects --project-type override" {
run bash "$RALPH_ENABLE_CI" --from none --project-type "rust"
assert_success
grep -q "PROJECT_TYPE=\"rust\"" .ralphrc
}
@test "ralph enable-ci returns exit code 2 when already enabled" {
# First enable
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
# Second enable without force
run bash "$RALPH_ENABLE_CI" --from none
assert_equal "$status" 2
}
@test "ralph enable-ci --force overwrites existing configuration" {
# First enable
bash "$RALPH_ENABLE_CI" --from none --project-name "old-name" >/dev/null 2>&1
# Second enable with force
run bash "$RALPH_ENABLE_CI" --from none --force --project-name "new-name"
assert_success
}
# =============================================================================
# JSON OUTPUT TESTS (3 tests)
# =============================================================================
@test "ralph enable-ci --json outputs valid JSON on success" {
run bash "$RALPH_ENABLE_CI" --from none --json
assert_success
# Validate JSON structure
echo "$output" | jq -e '.success == true'
echo "$output" | jq -e '.project_name'
echo "$output" | jq -e '.files_created'
}
@test "ralph enable-ci --json includes project info" {
cat > package.json << 'EOF'
{"name": "json-test"}
EOF
run bash "$RALPH_ENABLE_CI" --from none --json
assert_success
echo "$output" | jq -e '.project_name == "json-test"'
}
@test "ralph enable-ci --json returns proper structure when already enabled" {
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
run bash "$RALPH_ENABLE_CI" --from none --json
assert_equal "$status" 2
echo "$output" | jq -e '.code == 2'
}
# =============================================================================
# PRD IMPORT TESTS (2 tests)
# =============================================================================
@test "ralph enable-ci imports tasks from PRD file" {
mkdir -p docs
cat > docs/requirements.md << 'EOF'
# Project Requirements
- [ ] Implement user authentication
- [ ] Add API endpoints
- [ ] Create database schema
EOF
run bash "$RALPH_ENABLE_CI" --from prd --prd docs/requirements.md
assert_success
# Check that tasks were imported
grep -q "authentication\|API\|database" .ralph/@fix_plan.md
}
@test "ralph enable-ci fails gracefully with missing PRD file" {
run bash "$RALPH_ENABLE_CI" --from prd --prd nonexistent.md
assert_failure
}
# =============================================================================
# IDEMPOTENCY TESTS (3 tests)
# =============================================================================
@test "ralph enable-ci is idempotent with force flag" {
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
# Add a file to .ralph
echo "custom file" > .ralph/custom.txt
run bash "$RALPH_ENABLE_CI" --from none --force
assert_success
# Custom file should still exist (we don't delete extra files)
[[ -f ".ralph/custom.txt" ]]
}
@test "ralph enable-ci preserves existing .ralph subdirectories" {
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
# Add custom content
echo "spec content" > .ralph/specs/custom_spec.md
run bash "$RALPH_ENABLE_CI" --from none --force
assert_success
[[ -f ".ralph/specs/custom_spec.md" ]]
}
@test "ralph enable-ci does not overwrite existing files without force" {
mkdir -p .ralph
echo "original prompt" > .ralph/PROMPT.md
echo "original fix plan" > .ralph/@fix_plan.md
echo "original agent" > .ralph/@AGENT.md
run bash "$RALPH_ENABLE_CI" --from none
assert_equal "$status" 2
# Verify original content preserved
assert_equal "$(cat .ralph/PROMPT.md)" "original prompt"
}
# =============================================================================
# QUIET MODE TESTS (2 tests)
# =============================================================================
@test "ralph enable-ci --quiet suppresses output" {
run bash "$RALPH_ENABLE_CI" --from none --quiet
assert_success
# Output should be minimal
[[ -z "$output" ]] || [[ ! "$output" =~ "Detected" ]]
}
@test "ralph enable-ci --quiet still creates files" {
run bash "$RALPH_ENABLE_CI" --from none --quiet
assert_success
[[ -f ".ralph/PROMPT.md" ]]
}

View file

@ -27,6 +27,7 @@ setup() {
export CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id" export CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id"
export RALPH_SESSION_FILE="$RALPH_DIR/.ralph_session" export RALPH_SESSION_FILE="$RALPH_DIR/.ralph_session"
export RALPH_SESSION_HISTORY_FILE="$RALPH_DIR/.ralph_session_history" export RALPH_SESSION_HISTORY_FILE="$RALPH_DIR/.ralph_session_history"
export RESPONSE_ANALYSIS_FILE="$RALPH_DIR/.response_analysis"
export CLAUDE_MIN_VERSION="2.0.76" export CLAUDE_MIN_VERSION="2.0.76"
export CLAUDE_CODE_CMD="claude" export CLAUDE_CODE_CMD="claude"
export CLAUDE_USE_CONTINUE="true" export CLAUDE_USE_CONTINUE="true"

View file

@ -0,0 +1,269 @@
#!/usr/bin/env bats
# Unit tests for lib/task_sources.sh
# Tests beads integration, GitHub integration, PRD extraction, and task normalization
load '../helpers/test_helper'
load '../helpers/fixtures'
# Path to task_sources.sh
TASK_SOURCES="${BATS_TEST_DIRNAME}/../../lib/task_sources.sh"
setup() {
# Create temporary test directory
TEST_DIR="$(mktemp -d)"
cd "$TEST_DIR"
# Source the library
source "$TASK_SOURCES"
}
teardown() {
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
cd /
rm -rf "$TEST_DIR"
fi
}
# =============================================================================
# BEADS DETECTION (3 tests)
# =============================================================================
@test "check_beads_available returns false when no .beads directory" {
run check_beads_available
assert_failure
}
@test "check_beads_available returns false when bd command not found" {
mkdir -p .beads
# bd command likely won't exist in test environment
if command -v bd &>/dev/null; then
skip "bd command is available"
fi
run check_beads_available
assert_failure
}
@test "get_beads_count returns 0 when beads unavailable" {
run get_beads_count
assert_output "0"
}
# =============================================================================
# GITHUB DETECTION (3 tests)
# =============================================================================
@test "check_github_available returns false when no gh command" {
# gh command may not exist in test environment
if ! command -v gh &>/dev/null; then
run check_github_available
assert_failure
else
skip "gh command is available"
fi
}
@test "check_github_available returns false when not in git repo" {
run check_github_available
assert_failure
}
@test "get_github_issue_count returns 0 when GitHub unavailable" {
run get_github_issue_count
assert_output "0"
}
# =============================================================================
# PRD EXTRACTION (6 tests)
# =============================================================================
@test "extract_prd_tasks extracts checkbox items" {
cat > prd.md << 'EOF'
# Requirements
- [ ] Implement user authentication
- [x] Set up database
- [ ] Add API endpoints
EOF
run extract_prd_tasks "prd.md"
assert_success
[[ "$output" =~ "Implement user authentication" ]]
[[ "$output" =~ "Add API endpoints" ]]
}
@test "extract_prd_tasks extracts numbered list items" {
cat > prd.md << 'EOF'
# Requirements
1. Implement user authentication
2. Set up database
3. Add API endpoints
EOF
run extract_prd_tasks "prd.md"
assert_success
[[ "$output" =~ "Implement user authentication" ]]
}
@test "extract_prd_tasks returns empty for file without tasks" {
cat > prd.md << 'EOF'
# Empty Document
This document has no tasks.
EOF
run extract_prd_tasks "prd.md"
assert_success
}
@test "extract_prd_tasks returns error for missing file" {
run extract_prd_tasks "nonexistent.md"
assert_failure
}
@test "extract_prd_tasks normalizes checked items to unchecked" {
cat > prd.md << 'EOF'
- [x] Completed task
- [X] Another completed
EOF
run extract_prd_tasks "prd.md"
assert_success
[[ "$output" =~ "[ ]" ]]
[[ ! "$output" =~ "[x]" ]]
[[ ! "$output" =~ "[X]" ]]
}
@test "extract_prd_tasks limits output to 30 tasks" {
# Create PRD with 40 tasks
{
echo "# Tasks"
for i in {1..40}; do
echo "- [ ] Task $i"
done
} > prd.md
run extract_prd_tasks "prd.md"
# Count the number of task lines
task_count=$(echo "$output" | grep -c '^\- \[' || echo "0")
[[ "$task_count" -le 30 ]]
}
# =============================================================================
# TASK NORMALIZATION (5 tests)
# =============================================================================
@test "normalize_tasks converts bullet points to checkboxes" {
input="- First task
* Second task"
run normalize_tasks "$input"
assert_success
[[ "$output" =~ "- [ ] First task" ]]
[[ "$output" =~ "- [ ] Second task" ]]
}
@test "normalize_tasks converts numbered items to checkboxes" {
input="1. First task
2. Second task"
run normalize_tasks "$input"
assert_success
[[ "$output" =~ "- [ ]" ]]
}
@test "normalize_tasks preserves existing checkboxes" {
input="- [ ] Already a task"
run normalize_tasks "$input"
assert_success
[[ "$output" =~ "- [ ] Already a task" ]]
}
@test "normalize_tasks handles plain text lines" {
input="Plain text task"
run normalize_tasks "$input"
assert_success
[[ "$output" =~ "- [ ] Plain text task" ]]
}
@test "normalize_tasks handles empty input" {
run normalize_tasks ""
assert_success
}
# =============================================================================
# TASK PRIORITIZATION (3 tests)
# =============================================================================
@test "prioritize_tasks puts critical tasks in High Priority" {
input="- [ ] Critical bug fix
- [ ] Normal task"
output=$(prioritize_tasks "$input" || true)
[[ "$output" =~ "## High Priority" ]]
# Critical should be before Medium
high_section="${output%%## Medium*}"
[[ "$high_section" =~ "Critical bug fix" ]]
}
@test "prioritize_tasks puts optional tasks in Low Priority" {
input="- [ ] Nice to have feature
- [ ] Normal task"
run prioritize_tasks "$input"
assert_success
[[ "$output" =~ "## Low Priority" ]]
low_section="${output##*## Low Priority}"
[[ "$low_section" =~ "Nice to have" ]]
}
@test "prioritize_tasks puts regular tasks in Medium Priority" {
input="- [ ] Regular task"
output=$(prioritize_tasks "$input" || true)
[[ "$output" =~ "## Medium Priority" ]]
}
# =============================================================================
# COMBINED IMPORT (3 tests)
# =============================================================================
@test "import_tasks_from_sources handles prd source" {
mkdir -p docs
cat > docs/prd.md << 'EOF'
# Requirements
- [ ] Test task
EOF
run import_tasks_from_sources "prd" "docs/prd.md" ""
assert_success
[[ "$output" =~ "Test task" ]]
}
@test "import_tasks_from_sources handles empty sources" {
run import_tasks_from_sources "" "" ""
assert_failure
}
@test "import_tasks_from_sources handles none source" {
run import_tasks_from_sources "none" "" ""
# 'none' doesn't import anything, so fails
assert_failure
}