refactor(ux): consolidate BMAD skills, update design system, and clean up Prisma generated client

This commit is contained in:
Sepehr Ramezani
2026-04-19 19:21:27 +02:00
parent 5296c4da2c
commit 25529a24b8
2476 changed files with 127934 additions and 101962 deletions

163
.ralph/.ralphrc Normal file
View File

@@ -0,0 +1,163 @@
# .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.
# =============================================================================
# PLATFORM DRIVER
# =============================================================================
# Platform driver for Ralph loop (claude-code, codex, opencode, copilot, or cursor)
PLATFORM_DRIVER="${PLATFORM_DRIVER:-opencode}"
# =============================================================================
# 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 Code only.
# Ignored by the codex, opencode, cursor, and copilot drivers.
# Opt in to interactive pauses by adding AskUserQuestion manually.
ALLOWED_TOOLS="Write,Read,Edit,MultiEdit,Glob,Grep,Task,TodoWrite,WebFetch,WebSearch,EnterPlanMode,ExitPlanMode,NotebookEdit,Bash"
# Permission mode for Claude Code CLI (default: bypassPermissions)
# Options: auto, acceptEdits, bypassPermissions, default, dontAsk, plan
CLAUDE_PERMISSION_MODE="bypassPermissions"
# How Ralph responds when a driver reports permission denials:
# - continue: log the denial and keep looping (default for unattended mode)
# - halt: stop immediately and show recovery guidance
# - threshold: continue until the permission-denial circuit breaker trips
PERMISSION_DENIAL_MODE="continue"
# =============================================================================
# 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
# Auto-recovery: cooldown before retry (minutes, 0 = immediate)
CB_COOLDOWN_MINUTES=30
# Auto-reset circuit breaker on startup (bypasses cooldown)
# WARNING: Reduces circuit breaker safety for unattended operation
CB_AUTO_RESET=false
# =============================================================================
# QUALITY GATES
# =============================================================================
# Test command to verify TESTS_STATUS claims (e.g. "npm test", "pytest")
# When set, runs after each loop to confirm the agent's test status report.
# Leave empty to trust the agent's TESTS_STATUS without verification.
TEST_COMMAND="${TEST_COMMAND:-}"
# Semicolon-separated quality gate commands (e.g. "npm run lint;npm run type-check")
# Each command runs after the loop iteration completes.
# Commands must not contain literal semicolons; use wrapper scripts if needed.
QUALITY_GATES="${QUALITY_GATES:-}"
# Failure mode: warn | block | circuit-breaker
# warn - log failures, continue normally (default)
# block - suppress exit signal so the loop keeps running
# circuit-breaker - feed no-progress to circuit breaker
QUALITY_GATE_MODE="${QUALITY_GATE_MODE:-warn}"
# Timeout in seconds for each gate command
QUALITY_GATE_TIMEOUT="${QUALITY_GATE_TIMEOUT:-120}"
# Only run gates when the agent signals completion (EXIT_SIGNAL=true)
QUALITY_GATE_ON_COMPLETION_ONLY="${QUALITY_GATE_ON_COMPLETION_ONLY:-false}"
# =============================================================================
# PERIODIC CODE REVIEW
# =============================================================================
# Review mode: off, enhanced, or ultimate (set via 'bmalph run --review [mode]')
# - off: no code review (default)
# - enhanced: periodic review every REVIEW_INTERVAL loops (~10-14% more tokens)
# - ultimate: review after every completed story (~20-30% more tokens)
# The review agent analyzes git diffs and outputs findings for the next implementation loop.
# Currently supported on Claude Code only.
REVIEW_MODE="${REVIEW_MODE:-off}"
# (Legacy) Enables review — prefer REVIEW_MODE instead
REVIEW_ENABLED="${REVIEW_ENABLED:-false}"
# Number of implementation loops between review sessions (enhanced mode only)
REVIEW_INTERVAL="${REVIEW_INTERVAL:-5}"
# =============================================================================
# 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"

158
.ralph/@AGENT.md Normal file
View File

@@ -0,0 +1,158 @@
# Agent Build Instructions
## Project Setup
```bash
# Install dependencies (example for Node.js project)
npm install
# Or for Python project
pip install -r requirements.txt
# Or for Rust project
cargo build
```
## Running Tests
```bash
# Node.js
npm test
# Python
pytest
# Rust
cargo test
```
## Build Commands
```bash
# Production build
npm run build
# or
cargo build --release
```
## Development Server
```bash
# Start development server
npm run dev
# or
cargo run
```
## Key Learnings
- Update this section when you learn new build optimizations
- Document any gotchas or special setup requirements
- Keep track of the fastest test/build cycle
## Feature Development Quality Standards
**CRITICAL**: All new features MUST meet the following mandatory requirements before being considered complete.
### Testing Requirements
- **Minimum Coverage**: 85% code coverage ratio required for all new code
- **Test Pass Rate**: 100% - all tests must pass, no exceptions
- **Test Types Required**:
- Unit tests for all business logic and services
- Integration tests for API endpoints or main functionality
- End-to-end tests for critical user workflows
- **Coverage Validation**: Run coverage reports before marking features complete:
```bash
# Examples by language/framework
npm run test:coverage
pytest --cov=src tests/ --cov-report=term-missing
cargo tarpaulin --out Html
```
- **Test Quality**: Tests must validate behavior, not just achieve coverage metrics
- **Test Documentation**: Complex test scenarios must include comments explaining the test strategy
### Git Workflow Requirements
Before moving to the next feature, ALL changes must be:
1. **Committed with Clear Messages**:
```bash
git add .
git commit -m "feat(module): descriptive message following conventional commits"
```
- Use conventional commit format: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`, etc.
- Include scope when applicable: `feat(api):`, `fix(ui):`, `test(auth):`
- Write descriptive messages that explain WHAT changed and WHY
2. **Pushed to Remote Repository**:
```bash
git push origin <branch-name>
```
- Never leave completed features uncommitted
- Push regularly to maintain backup and enable collaboration
- Ensure CI/CD pipelines pass before considering feature complete
3. **Branch Hygiene**:
- Work on feature branches, never directly on `main`
- Branch naming convention: `feature/<feature-name>`, `fix/<issue-name>`, `docs/<doc-update>`
- Create pull requests for all significant changes
4. **Ralph Integration**:
- Update .ralph/@fix_plan.md with new tasks before starting work
- Mark items complete in .ralph/@fix_plan.md upon completion
- Update .ralph/PROMPT.md if development patterns change
- Test features work within Ralph's autonomous loop
### Documentation Requirements
**ALL implementation documentation MUST remain synchronized with the codebase**:
1. **Code Documentation**:
- Language-appropriate documentation (JSDoc, docstrings, etc.)
- Update inline comments when implementation changes
- Remove outdated comments immediately
2. **Implementation Documentation**:
- Update relevant sections in this @AGENT.md file
- Keep build and test commands current
- Update configuration examples when defaults change
- Document breaking changes prominently
3. **README Updates**:
- Keep feature lists current
- Update setup instructions when dependencies change
- Maintain accurate command examples
- Update version compatibility information
4. **@AGENT.md Maintenance**:
- Add new build patterns to relevant sections
- Update "Key Learnings" with new insights
- Keep command examples accurate and tested
- Document new testing patterns or quality gates
### Feature Completion Checklist
Before marking ANY feature as complete, verify:
- [ ] All tests pass with appropriate framework command
- [ ] Code coverage meets 85% minimum threshold
- [ ] Coverage report reviewed for meaningful test quality
- [ ] Code formatted according to project standards
- [ ] Type checking passes (if applicable)
- [ ] All changes committed with conventional commit messages
- [ ] All commits pushed to remote repository
- [ ] .ralph/@fix_plan.md task marked as complete
- [ ] Implementation documentation updated
- [ ] Inline code comments updated or added
- [ ] .ralph/@AGENT.md updated (if new patterns introduced)
- [ ] Breaking changes documented
- [ ] Features tested within Ralph loop (if applicable)
- [ ] CI/CD pipeline passes
### Rationale
These standards ensure:
- **Quality**: High test coverage and pass rates prevent regressions
- **Traceability**: Git commits and .ralph/@fix_plan.md provide clear history of changes
- **Maintainability**: Current documentation reduces onboarding time and prevents knowledge loss
- **Collaboration**: Pushed changes enable team visibility and code review
- **Reliability**: Consistent quality gates maintain production stability
- **Automation**: Ralph integration ensures continuous development practices
**Enforcement**: AI agents should automatically apply these standards to all feature development tasks without requiring explicit instruction for each task.

319
.ralph/PROMPT.md Normal file
View File

@@ -0,0 +1,319 @@
# Ralph Development Instructions
## Context
You are Ralph, an autonomous AI development agent working on a [YOUR PROJECT NAME] project.
## Current Objectives
1. Review .ralph/@fix_plan.md for current priorities
2. Search the codebase for related code — especially which existing files need changes to integrate your work
3. Implement the task from the loop context (or the first unchecked item in @fix_plan.md on the first loop)
4. Use parallel subagents for complex tasks (max 100 concurrent)
5. Run tests after each implementation
6. Update the completed story checkbox in @fix_plan.md and commit
7. Read .ralph/specs/* ONLY if the task requires specific context you don't already have
## Key Principles
- Write code within the first few minutes of each loop
- ONE task per loop — implement the task specified in the loop context
- Search the codebase before assuming something isn't implemented
- Creating new files is often only half the task — wire them into the existing application
- Use subagents for expensive operations (file searching, analysis)
- Toggle completed story checkboxes in .ralph/@fix_plan.md without rewriting story lines
- Commit working changes with descriptive messages
## Session Continuity
- If you have context from a previous loop, do NOT re-read spec files
- Resume implementation where you left off
- Only consult specs when you encounter ambiguity in the current task
## Progress Tracking (CRITICAL)
- Ralph tracks progress by counting story checkboxes in .ralph/@fix_plan.md
- When you complete a story, change `- [ ]` to `- [x]` on that exact story line
- Do NOT remove, rewrite, or reorder story lines in .ralph/@fix_plan.md
- Update the checkbox before committing so the monitor updates immediately
- Set `TASKS_COMPLETED_THIS_LOOP` to the exact number of story checkboxes toggled this loop
- Only valid values: 0 or 1
## 🧪 Testing Guidelines (CRITICAL)
- LIMIT testing to ~20% of your total effort per loop
- PRIORITIZE: Implementation > Documentation > Tests
- Only write tests for NEW functionality you implement
- Do NOT refactor existing tests unless broken
- Do NOT add "additional test coverage" as busy work
- Focus on CORE functionality first, comprehensive testing later
## Execution Guidelines
- Before making changes: search codebase using subagents
- After implementation: run ESSENTIAL tests for the modified code only
- If tests fail: fix them as part of your current work
- Keep .ralph/@AGENT.md updated with build/run instructions
- Document the WHY behind tests and implementations
- No placeholder implementations - build it properly
## Autonomous Mode (CRITICAL)
- do not ask the user questions during loop execution
- do not use AskUserQuestion, EnterPlanMode, or ExitPlanMode during loop execution
- make the safest reasonable assumption and continue
- prefer small, reversible changes when requirements are ambiguous
- surface blockers in the Ralph status block instead of starting a conversation
## Self-Review Checklist (Before Reporting Status)
Before writing your RALPH_STATUS block, review your own work:
1. Re-read the diff of files you modified this loop — check for obvious bugs, typos, missing error handling
2. Verify you did not introduce regressions in existing functionality
3. Confirm your changes match the spec in .ralph/specs/ for the story you worked on
4. Check that new functions have proper error handling and edge case coverage
5. Ensure you did not leave TODO/FIXME/HACK comments without justification
If you find issues, fix them before reporting status. This self-check costs nothing extra.
## 🎯 Status Reporting (CRITICAL - Ralph needs this!)
**IMPORTANT**: At the end of your response, ALWAYS include this status block:
```
---RALPH_STATUS---
STATUS: IN_PROGRESS | COMPLETE | BLOCKED
TASKS_COMPLETED_THIS_LOOP: 0 | 1
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---
```
### When to set EXIT_SIGNAL: true
Set EXIT_SIGNAL to **true** when ALL of these conditions are met:
1. ✅ All items in @fix_plan.md are marked [x]
2. ✅ All tests are passing (or no tests exist for valid reasons)
3. ✅ No errors or warnings in the last execution
4. ✅ All requirements from specs/ are implemented
5. ✅ You have nothing meaningful left to implement
### Examples of proper status reporting:
**Example 1: Work in progress**
```
---RALPH_STATUS---
STATUS: IN_PROGRESS
TASKS_COMPLETED_THIS_LOOP: 1
FILES_MODIFIED: 5
TESTS_STATUS: PASSING
WORK_TYPE: IMPLEMENTATION
EXIT_SIGNAL: false
RECOMMENDATION: Continue with next priority task from @fix_plan.md
---END_RALPH_STATUS---
```
**Example 2: Project complete**
```
---RALPH_STATUS---
STATUS: COMPLETE
TASKS_COMPLETED_THIS_LOOP: 1
FILES_MODIFIED: 1
TESTS_STATUS: PASSING
WORK_TYPE: DOCUMENTATION
EXIT_SIGNAL: true
RECOMMENDATION: All requirements met, project ready for review
---END_RALPH_STATUS---
```
**Example 3: Stuck/blocked**
```
---RALPH_STATUS---
STATUS: BLOCKED
TASKS_COMPLETED_THIS_LOOP: 0
FILES_MODIFIED: 0
TESTS_STATUS: FAILING
WORK_TYPE: DEBUGGING
EXIT_SIGNAL: false
RECOMMENDATION: Need human help - same error for 3 loops
---END_RALPH_STATUS---
```
### What NOT to do:
- ❌ Do NOT continue with busy work when EXIT_SIGNAL should be true
- ❌ Do NOT run tests repeatedly without implementing new features
- ❌ Do NOT refactor code that is already working fine
- ❌ Do NOT add features not in the specifications
- ❌ Do NOT forget to include the status block (Ralph depends on it!)
## 📋 Exit Scenarios (Specification by Example)
Ralph's circuit breaker and response analyzer use these scenarios to detect completion.
Each scenario shows the exact conditions and expected behavior.
### Scenario 1: Successful Project Completion
**Given**:
- All items in .ralph/@fix_plan.md are marked [x]
- Last test run shows all tests passing
- No errors in recent logs/
- All requirements from .ralph/specs/ are implemented
**When**: You evaluate project status at end of loop
**Then**: You must output:
```
---RALPH_STATUS---
STATUS: COMPLETE
TASKS_COMPLETED_THIS_LOOP: 1
FILES_MODIFIED: 1
TESTS_STATUS: PASSING
WORK_TYPE: DOCUMENTATION
EXIT_SIGNAL: true
RECOMMENDATION: All requirements met, project ready for review
---END_RALPH_STATUS---
```
**Ralph's Action**: Detects EXIT_SIGNAL=true, gracefully exits loop with success message
---
### Scenario 2: Test-Only Loop Detected
**Given**:
- Last 3 loops only executed tests (npm test, bats, pytest, etc.)
- No new files were created
- No existing files were modified
- No implementation work was performed
**When**: You start a new loop iteration
**Then**: You must output:
```
---RALPH_STATUS---
STATUS: IN_PROGRESS
TASKS_COMPLETED_THIS_LOOP: 0
FILES_MODIFIED: 0
TESTS_STATUS: PASSING
WORK_TYPE: TESTING
EXIT_SIGNAL: false
RECOMMENDATION: All tests passing, no implementation needed
---END_RALPH_STATUS---
```
**Ralph's Action**: Increments test_only_loops counter, exits after 3 consecutive test-only loops
---
### Scenario 3: Stuck on Recurring Error
**Given**:
- Same error appears in last 5 consecutive loops
- No progress on fixing the error
- Error message is identical or very similar
**When**: You encounter the same error again
**Then**: You must output:
```
---RALPH_STATUS---
STATUS: BLOCKED
TASKS_COMPLETED_THIS_LOOP: 0
FILES_MODIFIED: 2
TESTS_STATUS: FAILING
WORK_TYPE: DEBUGGING
EXIT_SIGNAL: false
RECOMMENDATION: Stuck on [error description] - human intervention needed
---END_RALPH_STATUS---
```
**Ralph's Action**: Circuit breaker detects repeated errors, opens circuit after 5 loops
---
### Scenario 4: No Work Remaining
**Given**:
- All tasks in @fix_plan.md are complete
- You analyze .ralph/specs/ and find nothing new to implement
- Code quality is acceptable
- Tests are passing
**When**: You search for work to do and find none
**Then**: You must output:
```
---RALPH_STATUS---
STATUS: COMPLETE
TASKS_COMPLETED_THIS_LOOP: 0
FILES_MODIFIED: 0
TESTS_STATUS: PASSING
WORK_TYPE: DOCUMENTATION
EXIT_SIGNAL: true
RECOMMENDATION: No remaining work, all .ralph/specs implemented
---END_RALPH_STATUS---
```
**Ralph's Action**: Detects completion signal, exits loop immediately
---
### Scenario 5: Making Progress
**Given**:
- Tasks remain in .ralph/@fix_plan.md
- Implementation is underway
- Files are being modified
- Tests are passing or being fixed
**When**: You complete a task successfully
**Then**: You must output:
```
---RALPH_STATUS---
STATUS: IN_PROGRESS
TASKS_COMPLETED_THIS_LOOP: 1
FILES_MODIFIED: 7
TESTS_STATUS: PASSING
WORK_TYPE: IMPLEMENTATION
EXIT_SIGNAL: false
RECOMMENDATION: Continue with next task from .ralph/@fix_plan.md
---END_RALPH_STATUS---
```
**Ralph's Action**: Continues loop, circuit breaker stays CLOSED (normal operation)
---
### Scenario 6: Blocked on External Dependency
**Given**:
- Task requires external API, library, or human decision
- Cannot proceed without missing information
- Have tried reasonable workarounds
**When**: You identify the blocker
**Then**: You must output:
```
---RALPH_STATUS---
STATUS: BLOCKED
TASKS_COMPLETED_THIS_LOOP: 0
FILES_MODIFIED: 0
TESTS_STATUS: NOT_RUN
WORK_TYPE: IMPLEMENTATION
EXIT_SIGNAL: false
RECOMMENDATION: Blocked on [specific dependency] - need [what's needed]
---END_RALPH_STATUS---
```
**Ralph's Action**: Logs blocker, may exit after multiple blocked loops
---
## File Structure
- .ralph/: Ralph-specific configuration and documentation
- specs/: Project specifications and requirements
- @fix_plan.md: Prioritized TODO list
- @AGENT.md: Project build and run instructions
- PROMPT.md: This file - Ralph development instructions
- logs/: Loop execution logs
- docs/generated/: Auto-generated documentation
- src/: Source code implementation
- examples/: Example usage and test cases
## Current Task
Implement the task specified in the loop context.
If no task is specified (first loop), pick the first unchecked item from .ralph/@fix_plan.md.
Remember: Quality over speed. Build it right the first time. Know when you're done.

460
.ralph/RALPH-REFERENCE.md Normal file
View File

@@ -0,0 +1,460 @@
# Ralph Reference Guide
This reference guide provides essential information for troubleshooting and understanding Ralph's autonomous development loop.
In bmalph-managed projects, start Ralph with `bmalph run`. When you need direct loop flags such as `--reset-circuit` or `--live`, invoke `bash .ralph/ralph_loop.sh ...` from the project root.
## Table of Contents
1. [Configuration Files](#configuration-files)
2. [Project Configuration (.ralph/.ralphrc)](#project-configuration-ralphralphrc)
3. [Session Management](#session-management)
4. [Circuit Breaker](#circuit-breaker)
5. [Exit Detection](#exit-detection)
6. [Live Streaming](#live-streaming)
7. [Troubleshooting](#troubleshooting)
---
## Configuration Files
Ralph uses several files within the `.ralph/` directory, plus an optional legacy fallback config at the project root:
| File | Purpose |
|------|---------|
| `.ralph/PROMPT.md` | Main prompt that drives each loop iteration |
| `.ralph/@fix_plan.md` | Prioritized task list that Ralph follows |
| `.ralph/@AGENT.md` | Build and run instructions maintained by Ralph |
| `.ralph/status.json` | Real-time status tracking (JSON format) |
| `.ralph/logs/` | Execution logs for each loop iteration |
| `.ralph/.ralph_session` | Current session state |
| `.ralph/.circuit_breaker_state` | Circuit breaker state |
| `.ralph/live.log` | Live streaming output file for monitoring |
| `.ralph/.loop_start_sha` | Git HEAD SHA captured at loop start for progress detection |
| `.ralph/.ralphrc` | Project-specific configuration installed by bmalph |
| `.ralphrc` (project root, legacy fallback) | Optional legacy configuration for older standalone Ralph layouts |
### Rate Limiting
- Default: 100 API calls per hour (configurable via `--calls` flag or `.ralph/.ralphrc`)
- Automatic hourly reset with countdown display
- Call tracking persists across script restarts
---
## Project Configuration (.ralph/.ralphrc)
In bmalph-managed projects, Ralph reads `.ralph/.ralphrc` for per-project settings.
For backward compatibility with older standalone Ralph layouts, it also falls back to a project-root `.ralphrc` when the bundled config file is missing.
### Precedence
Environment variables > Ralph config file > script defaults
### Available Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `PROJECT_NAME` | `my-project` | Project name for prompts and logging |
| `PROJECT_TYPE` | `unknown` | Project type (javascript, typescript, python, rust, go) |
| `MAX_CALLS_PER_HOUR` | `100` | Rate limit for API calls |
| `CLAUDE_TIMEOUT_MINUTES` | `15` | Timeout per loop driver invocation |
| `CLAUDE_OUTPUT_FORMAT` | `json` | Output format (json or text) |
| `ALLOWED_TOOLS` | `Write,Read,Edit,MultiEdit,Glob,Grep,Task,TodoWrite,WebFetch,WebSearch,EnterPlanMode,ExitPlanMode,NotebookEdit,Bash` | Claude Code only. Ignored by codex, cursor, and copilot |
| `CLAUDE_PERMISSION_MODE` | `bypassPermissions` | Claude Code only. Prevents interactive approval workflows from blocking unattended loops without relying on beta headers |
| `PERMISSION_DENIAL_MODE` | `continue` | How Ralph responds to permission denials: continue, halt, or threshold |
| `SESSION_CONTINUITY` | `true` | Maintain context across loops |
| `SESSION_EXPIRY_HOURS` | `24` | Session expiration time |
| `RALPH_VERBOSE` | `false` | Enable verbose logging |
| `CB_NO_PROGRESS_THRESHOLD` | `3` | Loops with no progress before circuit opens |
| `CB_SAME_ERROR_THRESHOLD` | `5` | Loops with same error before circuit opens |
| `CB_OUTPUT_DECLINE_THRESHOLD` | `70` | Output decline percentage threshold |
| `CB_COOLDOWN_MINUTES` | `30` | Minutes before OPEN auto-recovers to HALF_OPEN |
| `CB_AUTO_RESET` | `false` | Reset circuit to CLOSED on startup |
| `TASK_SOURCES` | `local` | Where to import tasks from (local, beads, github) |
### Generation
bmalph copies `ralphrc.template` to `.ralph/.ralphrc` during `bmalph init`. Untouched managed configs are updated on upgrade, while customized `.ralph/.ralphrc` files are preserved.
---
## Session Management
Ralph maintains session continuity across loop iterations using `--resume` with explicit session IDs.
### Session Continuity
Ralph uses `--resume <session_id>` instead of `--continue` to resume sessions. This ensures Ralph only resumes its own saved sessions and avoids hijacking unrelated active sessions.
This applies to every driver that exposes resumable IDs today:
- Claude Code
- OpenAI Codex
- Cursor
### Session Files
| File | Purpose |
|------|---------|
| `.ralph/.ralph_session` | Current Ralph session state (active or reset/inactive) |
| `.ralph/.ralph_session_history` | History of last 50 session transitions |
| `.ralph/.claude_session_id` | Persisted driver session ID (shared filename for historical reasons; used by Claude Code, Codex, and Cursor) |
### Session Lifecycle
Sessions are automatically reset when:
- Circuit breaker opens (stagnation detected)
- Manual interrupt (Ctrl+C / SIGINT)
- Project completion (graceful exit)
- Manual circuit breaker reset (`bash .ralph/ralph_loop.sh --reset-circuit`)
- Manual session reset (`bash .ralph/ralph_loop.sh --reset-session`)
### Session Expiration
Sessions expire after 24 hours (configurable via `SESSION_EXPIRY_HOURS` in `.ralph/.ralphrc`). When expired:
- A new session is created automatically
- Previous context is not preserved
- Session history records the transition
### Session State Structure
Active session payload:
```json
{
"session_id": "uuid-string",
"created_at": "ISO-timestamp",
"last_used": "ISO-timestamp"
}
```
Reset/inactive payload:
```json
{
"session_id": "",
"reset_at": "ISO-timestamp",
"reset_reason": "reason string"
}
```
---
## Circuit Breaker
The circuit breaker prevents runaway loops by detecting stagnation.
### States
| State | Description | Action |
|-------|-------------|--------|
| **CLOSED** | Normal operation | Loop continues |
| **HALF_OPEN** | Monitoring after recovery | Testing if issue resolved |
| **OPEN** | Halted due to stagnation | Loop stops |
### Thresholds
| Threshold | Default | Description |
|-----------|---------|-------------|
| `CB_NO_PROGRESS_THRESHOLD` | 3 | Open circuit after N loops with no file changes |
| `CB_SAME_ERROR_THRESHOLD` | 5 | Open circuit after N loops with repeated errors |
| `CB_OUTPUT_DECLINE_THRESHOLD` | 70% | Open circuit if output declines by >N% |
| `CB_PERMISSION_DENIAL_THRESHOLD` | 2 | Open circuit after N loops with permission denials |
| `CB_COOLDOWN_MINUTES` | 30 | Minutes before OPEN auto-recovers to HALF_OPEN |
| `CB_AUTO_RESET` | false | Reset to CLOSED on startup (bypasses cooldown) |
### Permission Denial Detection
When the active driver is denied permission to execute commands, Ralph:
1. Detects permission denials from the JSON output
2. Applies `PERMISSION_DENIAL_MODE` from `.ralph/.ralphrc`
3. Keeps `last_action: permission_denied` visible in the status file and dashboard
`PERMISSION_DENIAL_MODE` behavior:
- `continue` keeps looping and logs the denial
- `halt` stops immediately with reason `permission_denied`
- `threshold` keeps looping until `CB_PERMISSION_DENIAL_THRESHOLD` opens the circuit breaker
### Auto-Recovery Cooldown
After `CB_COOLDOWN_MINUTES` (default: 30) in OPEN state, the circuit auto-transitions to HALF_OPEN. From HALF_OPEN, if progress is detected, circuit goes to CLOSED; otherwise back to OPEN.
Set `CB_AUTO_RESET=true` in `.ralph/.ralphrc` to bypass cooldown entirely and reset to CLOSED on startup.
### Circuit Breaker State Structure
```json
{
"state": "CLOSED|HALF_OPEN|OPEN",
"consecutive_no_progress": 0,
"consecutive_same_error": 0,
"consecutive_permission_denials": 0,
"last_progress_loop": 5,
"total_opens": 0,
"reason": "string (when OPEN)",
"opened_at": "ISO-timestamp (when OPEN)",
"current_loop": 10
}
```
### Recovery
To reset the circuit breaker:
```bash
bash .ralph/ralph_loop.sh --reset-circuit
```
---
## Exit Detection
Ralph uses multiple mechanisms to detect when to exit the loop.
### Exit Conditions
| Condition | Threshold | Description |
|-----------|-----------|-------------|
| Consecutive done signals | 2 | Exit on repeated completion signals |
| Test-only loops | 3 | Exit if too many test-only iterations |
| Fix plan complete | All [x] | Exit when all tasks are marked complete |
| EXIT_SIGNAL + completion_indicators | Both | Dual verification for project completion |
### EXIT_SIGNAL Gate
The `completion_indicators` exit condition requires dual verification:
| completion_indicators | EXIT_SIGNAL | Result |
|-----------------------|-------------|--------|
| >= 2 | `true` | **Exit** ("project_complete") |
| >= 2 | `false` | **Continue** (agent still working) |
| >= 2 | missing | **Continue** (defaults to false) |
| < 2 | `true` | **Continue** (threshold not met) |
**Rationale:** Natural language patterns like "done" or "complete" can trigger false positives during productive work. By requiring an explicit `EXIT_SIGNAL` confirmation, Ralph avoids exiting mid-iteration.
When the agent outputs `STATUS: COMPLETE` with `EXIT_SIGNAL: false`, the explicit `false` takes precedence. This allows marking a phase complete while indicating more phases remain.
### RALPH_STATUS Block
The coding agent should include this status block at the end of each response:
```
---RALPH_STATUS---
STATUS: IN_PROGRESS | COMPLETE | BLOCKED
TASKS_COMPLETED_THIS_LOOP: 0 | 1
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---
```
### When to Set EXIT_SIGNAL: true
Set EXIT_SIGNAL to **true** when ALL conditions are met:
1. All items in `@fix_plan.md` are marked `[x]`
2. All tests are passing (or no tests exist for valid reasons)
3. No errors or warnings in the last execution
4. All requirements from specs/ are implemented
5. Nothing meaningful left to implement
### Progress Detection
Ralph detects progress through both uncommitted file changes AND git commits made within a loop. Before each loop, Ralph captures `git rev-parse HEAD`; if HEAD changes during the loop, committed files count as progress alongside working tree changes.
---
## Live Streaming
Ralph supports real-time streaming output with the `--live` flag.
### Usage
```bash
bash .ralph/ralph_loop.sh --live # Live streaming output
bash .ralph/ralph_loop.sh --monitor --live # Live streaming with tmux monitoring
```
### How It Works
- Live mode switches the active driver to its structured streaming format and pipes the stream through `jq`
- Cursor background loop execution stays on `json` output and switches to `stream-json` for live display
- Claude Code also uses `stream-json` for live display, while Codex streams its native JSONL events directly
- Shows text deltas and tool invocations in real-time
- Requires `jq` and `stdbuf` (from coreutils); falls back to background mode if unavailable
### Monitoring Layout
When using `--monitor` with `--live`, tmux creates a 3-pane layout:
- **Left pane:** Ralph loop with live streaming
- **Right-top pane:** `tail -f .ralph/live.log` (live driver output)
- **Right-bottom pane:** status dashboard (`bmalph watch` when available)
---
## Troubleshooting
### Common Issues
#### Ralph exits too early
**Symptoms:** Loop stops before work is complete
**Causes:**
- EXIT_SIGNAL set to true prematurely
- completion_indicators triggered by natural language
- All `@fix_plan.md` items marked complete
**Solutions:**
1. Ensure EXIT_SIGNAL is only true when genuinely complete
2. Add remaining tasks to `@fix_plan.md`
3. Check `.ralph/.response_analysis` for exit reasons
#### Ralph doesn't exit when complete
**Symptoms:** Loop continues with busywork
**Causes:**
- EXIT_SIGNAL not being set to true
- `@fix_plan.md` has unmarked items
- completion_indicators threshold not met
**Solutions:**
1. Ensure RALPH_STATUS block is included in responses
2. Set EXIT_SIGNAL: true when all work is done
3. Mark all completed items in `@fix_plan.md`
#### Circuit breaker opens unexpectedly
**Symptoms:** "OPEN - stagnation detected" message
**Causes:**
- Same error recurring across loops
- No file changes for multiple loops
- Output volume declining significantly
**Solutions:**
1. Check `.ralph/logs/` for the recurring error
2. Fix the underlying issue causing the error
3. Reset circuit breaker: `bash .ralph/ralph_loop.sh --reset-circuit`
#### Permission denied blocks progress
**Symptoms:** "OPEN - permission_denied" message
**Causes:**
- The active driver denied permission to run commands
- `ALLOWED_TOOLS` in `.ralph/.ralphrc` too restrictive for Claude Code
- The active non-Claude driver rejected a tool under its native permission model
**Solutions:**
1. For Claude Code, update `ALLOWED_TOOLS` in `.ralph/.ralphrc` to include needed tools
2. For Claude Code unattended loops, keep `CLAUDE_PERMISSION_MODE="bypassPermissions"` in `.ralph/.ralphrc`
3. For codex, cursor, and copilot, review the driver's native permission settings; `ALLOWED_TOOLS` is ignored
4. If you want unattended behavior, keep `PERMISSION_DENIAL_MODE="continue"` in `.ralph/.ralphrc`
5. Reset circuit breaker if needed: `bash .ralph/ralph_loop.sh --reset-circuit`
#### Session expires mid-project
**Symptoms:** Context lost, session age > 24h
**Causes:**
- Long gaps between loop iterations
- Session not being refreshed
**Solutions:**
1. Sessions are designed to expire after 24h (configurable via `SESSION_EXPIRY_HOURS`)
2. Start a new session with `bash .ralph/ralph_loop.sh --reset-session`
3. Context will be rebuilt from `@fix_plan.md` and `specs/`
#### Cursor preflight fails
**Symptoms:** `bmalph doctor` or `bmalph run --driver cursor` fails before the loop starts
**Causes:**
- `command -v jq` fails in the bash environment Ralph uses
- `command -v cursor-agent` fails in that same bash environment
- `cursor-agent status` reports an authentication problem
**Solutions:**
1. Run `command -v jq` in the same bash shell Ralph uses and install `jq` if missing
2. Run `command -v cursor-agent` and ensure the official Cursor CLI is on the bash `PATH`
3. Run `cursor-agent status` and sign in to Cursor before starting Ralph
### Diagnostic Commands
```bash
# Check Ralph status
bash .ralph/ralph_loop.sh --status
# Check circuit breaker state
bash .ralph/ralph_loop.sh --circuit-status
# Reset circuit breaker
bash .ralph/ralph_loop.sh --reset-circuit
# Auto-reset circuit breaker (bypasses cooldown)
bash .ralph/ralph_loop.sh --auto-reset-circuit
# Reset session
bash .ralph/ralph_loop.sh --reset-session
# Enable live streaming
bash .ralph/ralph_loop.sh --live
# Live streaming with monitoring
bash .ralph/ralph_loop.sh --monitor --live
```
### Log Files
Loop execution logs are stored in `.ralph/logs/`:
- Each loop iteration creates a timestamped log file
- Logs contain Claude's output and status information
- Use logs to diagnose issues with specific iterations
### Status File Structure
`.ralph/status.json`:
```json
{
"timestamp": "ISO-timestamp",
"loop_count": 10,
"calls_made_this_hour": 25,
"max_calls_per_hour": 100,
"last_action": "description",
"status": "running|completed|halted|paused|stopped|success|graceful_exit|error",
"exit_reason": "reason (if exited)",
"next_reset": "timestamp for rate limit reset"
}
```
`bmalph status` normalizes these raw bash values to `running`, `blocked`, `completed`, `not_started`, or `unknown`.
---
## Error Detection
Ralph uses two-stage error filtering to eliminate false positives.
### Stage 1: JSON Field Filtering
Filters out JSON field patterns like `"is_error": false` that contain the word "error" but aren't actual errors.
### Stage 2: Actual Error Detection
Detects real error messages:
- Error prefixes: `Error:`, `ERROR:`, `error:`
- Context-specific: `]: error`, `Link: error`
- Occurrences: `Error occurred`, `failed with error`
- Exceptions: `Exception`, `Fatal`, `FATAL`
### Multi-line Error Matching
Ralph verifies ALL error lines appear in ALL recent history files before declaring a stuck loop, preventing false negatives when multiple distinct errors occur.
---
## Further Reading
- [BMAD-METHOD Documentation](https://github.com/bmad-code-org/BMAD-METHOD)
- [Ralph Repository](https://github.com/snarktank/ralph)

60
.ralph/REVIEW_PROMPT.md Normal file
View File

@@ -0,0 +1,60 @@
# Code Review Instructions
You are a code reviewer for [YOUR PROJECT NAME].
Your role is to analyze recent code changes and provide structured quality feedback.
## CRITICAL RULES
- Do NOT modify any files
- Do NOT create any files
- Do NOT run any commands that change state
- ONLY read, analyze, and report
## Review Checklist
1. **Correctness**: Logic errors, off-by-one errors, missing edge cases
2. **Error handling**: Errors properly caught and handled, no swallowed exceptions
3. **Security**: Hardcoded secrets, injection vectors, unsafe patterns
4. **Performance**: N+1 queries, unnecessary iterations, memory leaks
5. **Code quality**: Dead code, duplicated logic, overly complex functions
6. **Test coverage**: New features tested, tests meaningful (not testing implementation)
7. **API contracts**: Public interfaces match their documentation and types
## What to Analyze
- Read the git log and diff summary provided below
- Check .ralph/specs/ for specification compliance
- Review modified files for broader context
- Focus on substantive issues, not style nitpicks
## Output Format
At the end of your analysis, include this block with a JSON payload:
```
---REVIEW_FINDINGS---
{"severity":"HIGH","issues_found":0,"summary":"No issues found.","details":[]}
---END_REVIEW_FINDINGS---
```
### JSON Schema
```json
{
"severity": "LOW | MEDIUM | HIGH | CRITICAL",
"issues_found": 0,
"summary": "One paragraph summary of findings",
"details": [
{
"severity": "HIGH",
"file": "src/example.ts",
"line": 42,
"issue": "Description of the issue",
"suggestion": "How to fix it"
}
]
}
```
The `severity` field at the top level reflects the highest severity among all issues found.
If no issues are found, set `severity` to `"LOW"`, `issues_found` to `0`, and `details` to `[]`.

View File

@@ -0,0 +1,422 @@
# Ralph Driver Interface Contract
## Overview
The Ralph loop loads a platform driver by sourcing `ralph/drivers/${PLATFORM_DRIVER}.sh`
inside `load_platform_driver()` (`ralph_loop.sh` line 296). The `PLATFORM_DRIVER` variable
defaults to `"claude-code"` and can be overridden via `.ralphrc`.
After sourcing, `ralph_loop.sh` immediately calls three functions to populate core globals:
1. `driver_valid_tools` -- populates `VALID_TOOL_PATTERNS`
2. `driver_cli_binary` -- stored in `CLAUDE_CODE_CMD`
3. `driver_display_name` -- stored in `DRIVER_DISPLAY_NAME`
**File naming convention:** `${PLATFORM_DRIVER}.sh` (e.g., `claude-code.sh`, `codex.sh`).
**Scope:** This documents the sourceable driver contract used by `ralph_loop.sh`. Helper
scripts like `cursor-agent-wrapper.sh` are out of scope.
**Calling conventions:**
- Data is returned via stdout (`echo`).
- Booleans are returned via exit status (`0` = true, `1` = false).
- Some functions mutate global arrays as side effects.
---
## Required Hooks
Called unconditionally by `ralph_loop.sh` with no `declare -F` guard or default stub.
Omitting any of these will break the loop at runtime.
### `driver_name()`
```bash
driver_name()
```
No arguments. Echo a short lowercase identifier (e.g., `"claude-code"`, `"codex"`).
Used at line 2382 to gate platform-specific logic.
### `driver_display_name()`
```bash
driver_display_name()
```
No arguments. Echo a human-readable name (e.g., `"Claude Code"`, `"OpenAI Codex"`).
Stored in `DRIVER_DISPLAY_NAME`, used in log messages and tmux pane titles.
### `driver_cli_binary()`
```bash
driver_cli_binary()
```
No arguments. Echo the CLI executable name or resolved path (e.g., `"claude"`, `"codex"`).
Stored in `CLAUDE_CODE_CMD`. Most drivers return a static string; cursor resolves
dynamically.
### `driver_valid_tools()`
```bash
driver_valid_tools()
```
No arguments. Must populate the global `VALID_TOOL_PATTERNS` array with the platform's
recognized tool name patterns. Used by `validate_allowed_tools()`.
### `driver_build_command(prompt_file, loop_context, session_id)`
```bash
driver_build_command "$prompt_file" "$loop_context" "$session_id"
```
Three string arguments:
| Argument | Description |
|----------|-------------|
| `$1` prompt_file | Path to the prompt file (e.g., `.ralph/PROMPT.md`) |
| `$2` loop_context | Context string for session continuity (may be empty) |
| `$3` session_id | Session ID for resume (empty string = new session) |
Must populate the global `CLAUDE_CMD_ARGS` array with the complete CLI command and
arguments. Return `0` on success, `1` on failure (e.g., prompt file not found).
**Reads globals:** `CLAUDE_OUTPUT_FORMAT`, `CLAUDE_PERMISSION_MODE` (claude-code only),
`CLAUDE_ALLOWED_TOOLS` (claude-code only), `CLAUDE_USE_CONTINUE`.
---
## Optional Overrides with Loop Defaults
`ralph_loop.sh` defines default stubs at lines 284 and 288. All existing drivers override
them, but a minimal driver can rely on the defaults.
### `driver_supports_tool_allowlist()`
```bash
driver_supports_tool_allowlist()
```
No arguments. Return `0` if the driver supports `--allowedTools` filtering, `1` otherwise.
**Default:** returns `1` (false). Currently only `claude-code` returns `0`.
### `driver_permission_denial_help()`
```bash
driver_permission_denial_help()
```
No arguments. Print platform-specific troubleshooting guidance when the loop detects a
permission denial.
**Reads:** `RALPHRC_FILE`, `DRIVER_DISPLAY_NAME`.
**Default:** generic guidance text.
---
## Optional Capability Hooks
Guarded by `declare -F` checks or wrapper functions in `ralph_loop.sh` (lines 1917-1954,
1576-1583). Safe to omit -- documented fallback behavior applies.
### `driver_supports_sessions()`
```bash
driver_supports_sessions()
```
No arguments. Return `0` if the driver supports session resume, `1` otherwise.
**If not defined:** assumed true (`0`).
Implemented by all 5 drivers; `copilot` returns `1`.
### `driver_supports_live_output()`
```bash
driver_supports_live_output()
```
No arguments. Return `0` if the driver supports structured streaming output (stream-json
or JSONL), `1` otherwise.
**If not defined:** assumed true (`0`).
`copilot` returns `1`; all others return `0`.
### `driver_prepare_live_command()`
```bash
driver_prepare_live_command()
```
No arguments. Transform `CLAUDE_CMD_ARGS` into `LIVE_CMD_ARGS` for streaming mode.
**If not defined:** `LIVE_CMD_ARGS` is copied from `CLAUDE_CMD_ARGS` unchanged.
| Driver | Behavior |
|--------|----------|
| claude-code | Replaces `json` with `stream-json` and adds `--verbose --include-partial-messages` |
| codex | Copies as-is (output is already suitable) |
| opencode | Copies as-is (output is already suitable) |
| cursor | Replaces `json` with `stream-json` |
### `driver_stream_filter()`
```bash
driver_stream_filter()
```
No arguments. Echo a `jq` filter expression that transforms raw streaming events into
displayable text.
**If not defined:** returns `"empty"` (no output).
Each driver has a platform-specific filter; `copilot` returns `'.'` (passthrough).
### `driver_extract_session_id_from_output(output_file)`
```bash
driver_extract_session_id_from_output "$output_file"
```
One argument: path to the CLI output log file. Echo the extracted session ID.
Tried first in the session save chain before the generic `jq` extractor. Only `opencode`
implements this (uses `sed` to extract from a `"session"` JSON object).
### `driver_fallback_session_id(output_file)`
```bash
driver_fallback_session_id "$output_file"
```
One argument: path to the output file (caller passes it at line 1583; the only
implementation in `opencode` ignores it).
Last-resort session ID recovery when both driver-specific and generic extractors fail.
Only `opencode` implements this (queries `opencode session list --format json`).
---
## Conventional Metadata Hooks
Present in every driver but NOT called by `ralph_loop.sh`. Consumed by bmalph's TypeScript
doctor/preflight checks in `src/platform/`. A new driver should implement these for
`bmalph doctor` compatibility.
### `driver_min_version()`
```bash
driver_min_version()
```
No arguments. Echo the minimum required CLI version as a semver string.
### `driver_check_available()`
```bash
driver_check_available()
```
No arguments. Return `0` if the CLI binary is installed and reachable, `1` otherwise.
---
## Global Variables
### Written by drivers
| Variable | Written by | Type | Description |
|----------------------|----------------------------------|-------|------------------------------------------------------|
| `VALID_TOOL_PATTERNS`| `driver_valid_tools()` | array | Valid tool name patterns for allowlist validation |
| `CLAUDE_CMD_ARGS` | `driver_build_command()` | array | Complete CLI command with all arguments |
| `LIVE_CMD_ARGS` | `driver_prepare_live_command()` | array | Modified command for live streaming |
### Read by drivers (set by ralph_loop.sh or .ralphrc)
| Variable | Used in | Description |
|-------------------------|--------------------------------------------|------------------------------------------------|
| `CLAUDE_OUTPUT_FORMAT` | `driver_build_command()` | `"json"` or `"text"` |
| `CLAUDE_PERMISSION_MODE`| `driver_build_command()` (claude-code) | Permission mode flag, default `"bypassPermissions"` |
| `CLAUDE_ALLOWED_TOOLS` | `driver_build_command()` (claude-code) | Comma-separated tool allowlist |
| `CLAUDE_USE_CONTINUE` | `driver_build_command()` | `"true"` or `"false"`, gates session resume |
| `RALPHRC_FILE` | `driver_permission_denial_help()` | Path to `.ralphrc` config file |
| `DRIVER_DISPLAY_NAME` | `driver_permission_denial_help()` | Human-readable driver name |
### Environment globals (cursor-specific)
| Variable | Used in | Description |
|---------------|--------------------------------------|-------------------------------------|
| `OS`, `OSTYPE`| `driver_running_on_windows()` | OS detection |
| `LOCALAPPDATA`| `driver_localappdata_cli_binary()` | Windows local app data path |
| `PATH` | `driver_find_windows_path_candidate()`| Manual PATH scanning on Windows |
### Set by ralph_loop.sh from driver output
| Variable | Source | Description |
|----------------------|-------------------------|-------------------------------|
| `CLAUDE_CODE_CMD` | `driver_cli_binary()` | CLI binary name/path |
| `DRIVER_DISPLAY_NAME`| `driver_display_name()` | Human-readable display name |
---
## Capability Matrix
| Capability | claude-code | codex | opencode | copilot | cursor |
|---------------------------------------------------------|:-----------:|:-----------:|:-----------:|:-----------:|:-----------:|
| Tool allowlist (`driver_supports_tool_allowlist`) | yes | no | no | no | no |
| Session continuity (`driver_supports_sessions`) | yes | yes | yes | no | yes |
| Structured live output (`driver_supports_live_output`) | yes | yes | yes | no | yes |
| Live command transform (`driver_prepare_live_command`) | transform | passthrough | passthrough | -- | transform |
| Stream filter (`driver_stream_filter`) | complex jq | JSONL select| JSONL select| passthrough | complex jq |
| Custom session extraction (`driver_extract_session_id_from_output`) | -- | -- | yes | -- | -- |
| Fallback session lookup (`driver_fallback_session_id`) | -- | -- | yes | -- | -- |
| Dynamic binary resolution (`driver_cli_binary`) | static | static | static | static | dynamic |
---
## Creating a New Driver
### Minimal driver skeleton
```bash
#!/usr/bin/env bash
# ralph/drivers/my-platform.sh
# Driver for My Platform CLI
#
# Sourced by ralph_loop.sh via load_platform_driver().
# PLATFORM_DRIVER must be set to "my-platform" in .ralphrc.
# ---------------------------------------------------------------------------
# Required hooks (5) -- omitting any of these breaks the loop
# ---------------------------------------------------------------------------
# Short lowercase identifier used to gate platform-specific logic.
driver_name() {
echo "my-platform"
}
# Human-readable name for log messages and tmux pane titles.
driver_display_name() {
echo "My Platform"
}
# CLI executable name or resolved path.
driver_cli_binary() {
echo "my-platform"
}
# Populate VALID_TOOL_PATTERNS with recognized tool name patterns.
# Used by validate_allowed_tools() to check allowlist entries.
driver_valid_tools() {
VALID_TOOL_PATTERNS=(
"Read"
"Write"
"Edit"
"Bash"
# Add your platform's tool patterns here
)
}
# Build the complete CLI command array.
# $1 = prompt_file Path to .ralph/PROMPT.md
# $2 = loop_context Context string for session continuity (may be empty)
# $3 = session_id Session ID for resume (empty = new session)
driver_build_command() {
local prompt_file="$1"
local loop_context="$2"
local session_id="$3"
if [[ ! -f "$prompt_file" ]]; then
return 1
fi
CLAUDE_CMD_ARGS=(
"my-platform"
"--prompt" "$prompt_file"
"--output-format" "${CLAUDE_OUTPUT_FORMAT:-json}"
)
# Append session resume flag if continuing a session
if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
CLAUDE_CMD_ARGS+=("--session" "$session_id")
fi
# Append context if provided
if [[ -n "$loop_context" ]]; then
CLAUDE_CMD_ARGS+=("--context" "$loop_context")
fi
return 0
}
# ---------------------------------------------------------------------------
# Optional overrides (2) -- loop provides default stubs
# ---------------------------------------------------------------------------
# Return 0 if the platform supports --allowedTools filtering, 1 otherwise.
driver_supports_tool_allowlist() {
return 1
}
# Print troubleshooting guidance on permission denial.
driver_permission_denial_help() {
echo "Permission denied. Check that $DRIVER_DISPLAY_NAME has the required permissions."
echo "See $RALPHRC_FILE for configuration options."
}
# ---------------------------------------------------------------------------
# Metadata hooks (2) -- used by bmalph doctor, not called by ralph_loop.sh
# ---------------------------------------------------------------------------
# Minimum required CLI version (semver).
driver_min_version() {
echo "1.0.0"
}
# Return 0 if the CLI binary is installed and reachable, 1 otherwise.
driver_check_available() {
command -v my-platform &>/dev/null
}
```
### Checklist
- [ ] All 5 required hooks implemented (`driver_name`, `driver_display_name`,
`driver_cli_binary`, `driver_valid_tools`, `driver_build_command`)
- [ ] `driver_valid_tools` populates `VALID_TOOL_PATTERNS` with your platform's tool names
- [ ] `driver_build_command` handles all three arguments correctly
(`prompt_file`, `loop_context`, `session_id`)
- [ ] `driver_check_available` returns `0` only when the CLI is installed
- [ ] File named `${platform_id}.sh` matching the `PLATFORM_DRIVER` value in `.ralphrc`
- [ ] Register corresponding platform definition in `src/platform/` for bmalph CLI integration
- [ ] Tested with `bmalph doctor`
---
## Session ID Recovery Chain
When the loop needs to persist a session ID for resume, it follows a three-step priority
chain (`ralph_loop.sh` lines 1574-1588):
1. **`driver_extract_session_id_from_output($output_file)`** -- Driver-specific extraction.
If the function exists (`declare -F` guard) and echoes a non-empty string, that value
is used. Only `opencode` implements this (uses `sed` to extract from a `"session"` JSON
object).
2. **`extract_session_id_from_output($output_file)`** -- Generic `jq` extractor from
`response_analyzer.sh`. Searches the output file for `.sessionId`,
`.metadata.session_id`, and `.session_id` in that order.
3. **`driver_fallback_session_id($output_file)`** -- CLI-based last-resort recovery. If the
function exists and the previous steps produced nothing, this is called. Only `opencode`
implements this (queries `opencode session list --format json`).
The first step that returns a non-empty string wins. If all three steps fail, no session ID
is saved and the next iteration starts a fresh session.

187
.ralph/drivers/claude-code.sh Executable file
View File

@@ -0,0 +1,187 @@
#!/bin/bash
# Claude Code driver for Ralph
# Provides platform-specific CLI invocation logic
# Driver identification
driver_name() {
echo "claude-code"
}
driver_display_name() {
echo "Claude Code"
}
driver_cli_binary() {
echo "claude"
}
driver_min_version() {
echo "2.0.76"
}
# Check if the CLI binary is available
driver_check_available() {
command -v "$(driver_cli_binary)" &>/dev/null
}
# Valid tool patterns for --allowedTools validation
# Sets the global VALID_TOOL_PATTERNS array
driver_valid_tools() {
VALID_TOOL_PATTERNS=(
"Write"
"Read"
"Edit"
"MultiEdit"
"Glob"
"Grep"
"Task"
"TodoWrite"
"WebFetch"
"WebSearch"
"AskUserQuestion"
"EnterPlanMode"
"ExitPlanMode"
"Bash"
"Bash(git *)"
"Bash(npm *)"
"Bash(bats *)"
"Bash(python *)"
"Bash(node *)"
"NotebookEdit"
)
}
driver_supports_tool_allowlist() {
return 0
}
driver_permission_denial_help() {
echo " 1. Edit $RALPHRC_FILE and keep CLAUDE_PERMISSION_MODE=bypassPermissions for unattended Claude Code loops"
echo " 2. If Claude was denied on an interactive approval step, ALLOWED_TOOLS will not fix it"
echo " 3. If Claude was denied on a normal tool, update ALLOWED_TOOLS to include the required tools"
echo " 4. Common ALLOWED_TOOLS patterns:"
echo " - Bash - All shell commands"
echo " - Bash(node *) - All Node.js commands"
echo " - Bash(npm *) - All npm commands"
echo " - Bash(pnpm *) - All pnpm commands"
echo " - AskUserQuestion - Allow interactive clarification when you want pauses"
echo ""
echo "After updating $RALPHRC_FILE:"
echo " bash .ralph/ralph_loop.sh --reset-session # Clear stale session state"
echo " bmalph run # Restart the loop"
}
# Build the CLI command arguments
# Populates global CLAUDE_CMD_ARGS array
# Parameters:
# $1 - prompt_file: path to the prompt file
# $2 - loop_context: context string for session continuity
# $3 - session_id: session ID for resume (empty for new session)
driver_build_command() {
local prompt_file=$1
local loop_context=$2
local session_id=$3
local resolved_permission_mode="${CLAUDE_PERMISSION_MODE:-bypassPermissions}"
# Note: We do NOT use --dangerously-skip-permissions here. Tool permissions
# are controlled via --allowedTools from CLAUDE_ALLOWED_TOOLS in .ralphrc.
# This preserves the permission denial circuit breaker (Issue #101).
CLAUDE_CMD_ARGS=("$(driver_cli_binary)")
if [[ ! -f "$prompt_file" ]]; then
echo "ERROR: Prompt file not found: $prompt_file" >&2
return 1
fi
# Output format
if [[ "$CLAUDE_OUTPUT_FORMAT" == "json" ]]; then
CLAUDE_CMD_ARGS+=("--output-format" "json")
fi
# Prevent interactive approval flows from blocking unattended -p loops.
CLAUDE_CMD_ARGS+=("--permission-mode" "$resolved_permission_mode")
# Allowed tools
if [[ -n "$CLAUDE_ALLOWED_TOOLS" ]]; then
CLAUDE_CMD_ARGS+=("--allowedTools")
local IFS=','
read -ra tools_array <<< "$CLAUDE_ALLOWED_TOOLS"
for tool in "${tools_array[@]}"; do
tool=$(echo "$tool" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [[ -n "$tool" ]]; then
CLAUDE_CMD_ARGS+=("$tool")
fi
done
fi
# Session resume
# IMPORTANT: Use --resume with explicit session ID instead of --continue.
# --continue resumes the "most recent session in current directory" which
# can hijack active Claude Code sessions. --resume with a specific session ID
# ensures we only resume Ralph's own sessions. (Issue #151)
if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
CLAUDE_CMD_ARGS+=("--resume" "$session_id")
fi
# Loop context as system prompt
if [[ -n "$loop_context" ]]; then
CLAUDE_CMD_ARGS+=("--append-system-prompt" "$loop_context")
fi
# Prompt content
local prompt_content
prompt_content=$(cat "$prompt_file")
CLAUDE_CMD_ARGS+=("-p" "$prompt_content")
}
# Whether this driver supports session continuity
driver_supports_sessions() {
return 0 # true
}
# Claude Code supports stream-json live output.
driver_supports_live_output() {
return 0 # true
}
# Prepare command arguments for live stream-json output.
driver_prepare_live_command() {
LIVE_CMD_ARGS=()
local skip_next=false
for arg in "${CLAUDE_CMD_ARGS[@]}"; do
if [[ "$skip_next" == "true" ]]; then
LIVE_CMD_ARGS+=("stream-json")
skip_next=false
elif [[ "$arg" == "--output-format" ]]; then
LIVE_CMD_ARGS+=("$arg")
skip_next=true
else
LIVE_CMD_ARGS+=("$arg")
fi
done
if [[ "$skip_next" == "true" ]]; then
return 1
fi
LIVE_CMD_ARGS+=("--verbose" "--include-partial-messages")
}
# Stream filter for raw Claude stream-json events.
driver_stream_filter() {
echo '
if .type == "stream_event" then
if .event.type == "content_block_delta" and .event.delta.type == "text_delta" then
.event.delta.text
elif .event.type == "content_block_start" and .event.content_block.type == "tool_use" then
"\n\n⚡ [" + .event.content_block.name + "]\n"
elif .event.type == "content_block_stop" then
"\n"
else
empty
end
else
empty
end'
}

101
.ralph/drivers/codex.sh Executable file
View File

@@ -0,0 +1,101 @@
#!/bin/bash
# OpenAI Codex driver for Ralph
# Provides platform-specific CLI invocation logic for Codex
driver_name() {
echo "codex"
}
driver_display_name() {
echo "OpenAI Codex"
}
driver_cli_binary() {
echo "codex"
}
driver_min_version() {
echo "0.1.0"
}
driver_check_available() {
command -v "$(driver_cli_binary)" &>/dev/null
}
# Codex tool names differ from Claude Code
driver_valid_tools() {
VALID_TOOL_PATTERNS=(
"shell"
"read_file"
"write_file"
"edit_file"
"list_directory"
"search_files"
)
}
driver_supports_tool_allowlist() {
return 1
}
driver_permission_denial_help() {
echo " - $DRIVER_DISPLAY_NAME uses its native sandbox and approval model."
echo " - ALLOWED_TOOLS in $RALPHRC_FILE is ignored for this driver."
echo " - Ralph already runs Codex with --sandbox workspace-write."
echo " - Review Codex approval settings, then restart the loop."
}
# Build Codex CLI command
# Codex uses: codex exec [resume <id>] --json "prompt"
driver_build_command() {
local prompt_file=$1
local loop_context=$2
local session_id=$3
CLAUDE_CMD_ARGS=("$(driver_cli_binary)" "exec")
if [[ ! -f "$prompt_file" ]]; then
echo "ERROR: Prompt file not found: $prompt_file" >&2
return 1
fi
# JSON output
CLAUDE_CMD_ARGS+=("--json")
# Sandbox mode - workspace write access
CLAUDE_CMD_ARGS+=("--sandbox" "workspace-write")
# Session resume — gated on CLAUDE_USE_CONTINUE to respect --no-continue flag
if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
CLAUDE_CMD_ARGS+=("resume" "$session_id")
fi
# Build prompt with context
local prompt_content
prompt_content=$(cat "$prompt_file")
if [[ -n "$loop_context" ]]; then
prompt_content="$loop_context
$prompt_content"
fi
CLAUDE_CMD_ARGS+=("$prompt_content")
}
driver_supports_sessions() {
return 0 # true - Codex supports session resume
}
# Codex JSONL output is already suitable for live display.
driver_supports_live_output() {
return 0 # true
}
driver_prepare_live_command() {
LIVE_CMD_ARGS=("${CLAUDE_CMD_ARGS[@]}")
}
# Codex outputs JSONL events
driver_stream_filter() {
echo 'select(.type == "item.completed" and .item.type == "agent_message") | (.item.text // ([.item.content[]? | select(.type == "output_text") | .text] | join("\n")) // empty)'
}

105
.ralph/drivers/copilot.sh Executable file
View File

@@ -0,0 +1,105 @@
#!/bin/bash
# GitHub Copilot CLI driver for Ralph (EXPERIMENTAL)
# Provides platform-specific CLI invocation logic for Copilot CLI.
#
# Known limitations:
# - No session continuity (session IDs not capturable from -p output)
# - No structured output (plain text only, no --json flag)
# - Coarse tool permissions (only shell, shell(git:*), shell(npm:*), write)
# - CLI is new (GA Feb 25, 2026) — scripting interface may change
driver_name() {
echo "copilot"
}
driver_display_name() {
echo "GitHub Copilot CLI"
}
driver_cli_binary() {
echo "copilot"
}
driver_min_version() {
echo "0.0.418"
}
driver_check_available() {
command -v "$(driver_cli_binary)" &>/dev/null
}
# Copilot CLI tool names
driver_valid_tools() {
VALID_TOOL_PATTERNS=(
"shell"
"shell(git:*)"
"shell(npm:*)"
"write"
)
}
driver_supports_tool_allowlist() {
return 1
}
driver_permission_denial_help() {
echo " - $DRIVER_DISPLAY_NAME uses its own autonomy and approval controls."
echo " - ALLOWED_TOOLS in $RALPHRC_FILE is ignored for this driver."
echo " - Ralph already runs Copilot with --no-ask-user for unattended mode."
echo " - Review Copilot CLI permissions, then restart the loop."
}
# Build Copilot CLI command
# Context is prepended to the prompt (same pattern as Codex driver).
# Uses --autopilot --yolo for autonomous mode, -s to strip stats, -p for prompt.
driver_build_command() {
local prompt_file=$1
local loop_context=$2
# $3 (session_id) is intentionally ignored — Copilot CLI does not
# expose session IDs in -p output, so resume is not possible.
CLAUDE_CMD_ARGS=("$(driver_cli_binary)")
if [[ ! -f "$prompt_file" ]]; then
echo "ERROR: Prompt file not found: $prompt_file" >&2
return 1
fi
# Autonomous execution flags
CLAUDE_CMD_ARGS+=("--autopilot" "--yolo")
# Limit auto-continuation loops
CLAUDE_CMD_ARGS+=("--max-autopilot-continues" "50")
# Disable interactive prompts
CLAUDE_CMD_ARGS+=("--no-ask-user")
# Strip stats for cleaner output
CLAUDE_CMD_ARGS+=("-s")
# Build prompt with context prepended
local prompt_content
prompt_content=$(cat "$prompt_file")
if [[ -n "$loop_context" ]]; then
prompt_content="$loop_context
$prompt_content"
fi
CLAUDE_CMD_ARGS+=("-p" "$prompt_content")
}
driver_supports_sessions() {
return 1 # false — session IDs not capturable from -p output
}
# Copilot CLI does not expose structured live output for jq streaming.
driver_supports_live_output() {
return 1 # false
}
# Copilot CLI outputs plain text only (no JSON streaming).
# Passthrough filter — no transformation needed.
driver_stream_filter() {
echo '.'
}

View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Wrap Windows .cmd execution so GNU timeout launches a bash script instead of the .cmd directly.
set -euo pipefail
cli_path=${1:-}
if [[ -z "$cli_path" ]]; then
echo "ERROR: Missing Cursor CLI path" >&2
exit 1
fi
shift
exec "$cli_path" "$@"

283
.ralph/drivers/cursor.sh Executable file
View File

@@ -0,0 +1,283 @@
#!/bin/bash
# Cursor CLI driver for Ralph
# Uses the documented cursor-agent contract for background execution and
# switches to stream-json only for live display paths.
driver_name() {
echo "cursor"
}
driver_display_name() {
echo "Cursor CLI"
}
driver_cli_binary() {
local binary
binary=$(driver_resolve_cli_binary)
if [[ -n "$binary" ]]; then
echo "$binary"
return 0
fi
echo "cursor-agent"
}
driver_min_version() {
echo "0.1.0"
}
driver_check_available() {
local cli_binary
cli_binary=$(driver_cli_binary)
if [[ -f "$cli_binary" ]]; then
return 0
fi
command -v "$cli_binary" &>/dev/null
}
driver_valid_tools() {
VALID_TOOL_PATTERNS=(
"file_edit"
"file_read"
"file_write"
"terminal"
"search"
)
}
driver_supports_tool_allowlist() {
return 1
}
driver_permission_denial_help() {
echo " - $DRIVER_DISPLAY_NAME uses its native permission model."
echo " - ALLOWED_TOOLS in $RALPHRC_FILE is ignored for this driver."
echo " - Ralph already runs Cursor with --force."
echo " - Review Cursor permissions or approval settings, then restart the loop."
}
driver_build_command() {
local prompt_file=$1
local loop_context=$2
local session_id=$3
local cli_binary
cli_binary=$(driver_cli_binary)
if [[ ! -f "$prompt_file" ]]; then
echo "ERROR: Prompt file not found: $prompt_file" >&2
return 1
fi
CLAUDE_CMD_ARGS=()
if [[ "$cli_binary" == *.cmd ]]; then
CLAUDE_CMD_ARGS+=("$(driver_wrapper_path)" "$cli_binary")
else
CLAUDE_CMD_ARGS+=("$cli_binary")
fi
CLAUDE_CMD_ARGS+=("-p" "--force" "--output-format" "json")
if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
CLAUDE_CMD_ARGS+=("--resume" "$session_id")
fi
local prompt_content
if driver_running_on_windows; then
prompt_content=$(driver_build_windows_bootstrap_prompt "$loop_context" "$prompt_file")
else
prompt_content=$(cat "$prompt_file")
if [[ -n "$loop_context" ]]; then
prompt_content="$loop_context
$prompt_content"
fi
fi
CLAUDE_CMD_ARGS+=("$prompt_content")
}
driver_supports_sessions() {
return 0
}
driver_supports_live_output() {
return 0
}
driver_prepare_live_command() {
LIVE_CMD_ARGS=()
local skip_next=false
for arg in "${CLAUDE_CMD_ARGS[@]}"; do
if [[ "$skip_next" == "true" ]]; then
LIVE_CMD_ARGS+=("stream-json")
skip_next=false
elif [[ "$arg" == "--output-format" ]]; then
LIVE_CMD_ARGS+=("$arg")
skip_next=true
else
LIVE_CMD_ARGS+=("$arg")
fi
done
if [[ "$skip_next" == "true" ]]; then
return 1
fi
}
driver_stream_filter() {
echo '
if .type == "assistant" then
[(.message.content[]? | select(.type == "text") | .text)] | join("\n")
elif .type == "tool_call" then
"\n\n⚡ [" + (.tool_call.name // .name // "tool_call") + "]\n"
else
empty
end'
}
driver_running_on_windows() {
[[ "${OS:-}" == "Windows_NT" || "${OSTYPE:-}" == msys* || "${OSTYPE:-}" == cygwin* || "${OSTYPE:-}" == win32* ]]
}
driver_resolve_cli_binary() {
local candidate
local resolved
local fallback
local candidates=(
"cursor-agent"
"cursor-agent.cmd"
"agent"
"agent.cmd"
)
for candidate in "${candidates[@]}"; do
resolved=$(driver_lookup_cli_candidate "$candidate")
if [[ -n "$resolved" ]]; then
echo "$resolved"
return 0
fi
done
fallback=$(driver_localappdata_cli_binary)
if [[ -n "$fallback" ]]; then
echo "$fallback"
return 0
fi
echo ""
}
driver_lookup_cli_candidate() {
local candidate=$1
local resolved
resolved=$(command -v "$candidate" 2>/dev/null || true)
if [[ -n "$resolved" ]]; then
echo "$resolved"
return 0
fi
if ! driver_running_on_windows; then
return 0
fi
driver_find_windows_path_candidate "$candidate"
}
driver_find_windows_path_candidate() {
local candidate=$1
local path_entry
local normalized_entry
local resolved_candidate
local path_entries="${PATH:-}"
local -a path_parts=()
if [[ "$path_entries" == *";"* ]]; then
IFS=';' read -r -a path_parts <<< "$path_entries"
else
IFS=':' read -r -a path_parts <<< "$path_entries"
fi
for path_entry in "${path_parts[@]}"; do
[[ -z "$path_entry" ]] && continue
normalized_entry=$path_entry
if command -v cygpath &>/dev/null && [[ "$normalized_entry" =~ ^[A-Za-z]:\\ ]]; then
normalized_entry=$(cygpath -u "$normalized_entry")
fi
resolved_candidate="$normalized_entry/$candidate"
if [[ -f "$resolved_candidate" ]]; then
echo "$resolved_candidate"
return 0
fi
done
}
driver_localappdata_cli_binary() {
local local_app_data="${LOCALAPPDATA:-}"
if [[ -z "$local_app_data" ]] || ! driver_running_on_windows; then
return 0
fi
if command -v cygpath &>/dev/null && [[ "$local_app_data" =~ ^[A-Za-z]:\\ ]]; then
local_app_data=$(cygpath -u "$local_app_data")
fi
local candidates=(
"$local_app_data/cursor-agent/cursor-agent.cmd"
"$local_app_data/cursor-agent/agent.cmd"
)
local candidate
for candidate in "${candidates[@]}"; do
if [[ -f "$candidate" ]]; then
echo "$candidate"
return 0
fi
done
}
driver_wrapper_path() {
local driver_dir
driver_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "$driver_dir/cursor-agent-wrapper.sh"
}
driver_build_windows_bootstrap_prompt() {
local loop_context=$1
local prompt_file=$2
cat <<EOF
Read these Ralph workspace files before taking action:
- .ralph/PROMPT.md
- .ralph/PROJECT_CONTEXT.md
- .ralph/SPECS_INDEX.md
- .ralph/@fix_plan.md
- .ralph/@AGENT.md
- relevant files under .ralph/specs/
Then follow the Ralph instructions from those files and continue the next task.
EOF
if [[ -n "$loop_context" ]]; then
cat <<EOF
Current loop context:
$loop_context
EOF
fi
if [[ "$prompt_file" != ".ralph/PROMPT.md" ]]; then
cat <<EOF
Also read the active prompt file if it differs:
- $prompt_file
EOF
fi
}

147
.ralph/drivers/opencode.sh Executable file
View File

@@ -0,0 +1,147 @@
#!/bin/bash
# OpenCode driver for Ralph
# Uses OpenCode's build agent with JSON event output and optional session resume.
driver_name() {
echo "opencode"
}
driver_display_name() {
echo "OpenCode"
}
driver_cli_binary() {
echo "opencode"
}
driver_min_version() {
echo "0.1.0"
}
driver_check_available() {
command -v "$(driver_cli_binary)" &>/dev/null
}
driver_valid_tools() {
VALID_TOOL_PATTERNS=(
"bash"
"read"
"write"
"edit"
"grep"
"question"
)
}
driver_supports_tool_allowlist() {
return 1
}
driver_permission_denial_help() {
echo " - $DRIVER_DISPLAY_NAME uses its native permission and approval model."
echo " - ALLOWED_TOOLS in $RALPHRC_FILE is ignored for this driver."
echo " - BMAD workflows can use OpenCode's native question tool when needed."
echo " - Review OpenCode permissions, then restart the loop."
}
driver_build_command() {
local prompt_file=$1
local loop_context=$2
local session_id=$3
CLAUDE_CMD_ARGS=("$(driver_cli_binary)" "run" "--agent" "build" "--format" "json")
if [[ ! -f "$prompt_file" ]]; then
echo "ERROR: Prompt file not found: $prompt_file" >&2
return 1
fi
if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
CLAUDE_CMD_ARGS+=("--continue" "--session" "$session_id")
fi
local prompt_content
prompt_content=$(cat "$prompt_file")
if [[ -n "$loop_context" ]]; then
prompt_content="$loop_context
$prompt_content"
fi
CLAUDE_CMD_ARGS+=("$prompt_content")
}
driver_supports_sessions() {
return 0
}
driver_supports_live_output() {
return 0
}
driver_prepare_live_command() {
LIVE_CMD_ARGS=("${CLAUDE_CMD_ARGS[@]}")
}
driver_stream_filter() {
echo 'select((.type == "message.updated" or .type == "message.completed") and .message.role == "assistant") | ([.message.parts[]? | select(.type == "text") | .text] | join("\n"))'
}
driver_extract_session_id_from_output() {
local output_file=$1
if [[ ! -f "$output_file" ]]; then
echo ""
return 1
fi
local session_id
session_id=$(sed -n 's/.*"session"[^{]*{[^}]*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$output_file" | head -n 1 | tr -d '\r')
echo "$session_id"
[[ -n "$session_id" && "$session_id" != "null" ]]
}
driver_fallback_session_id() {
local cli_binary
cli_binary=$(driver_cli_binary)
local sessions_json
sessions_json=$("$cli_binary" session list --format json 2>/dev/null) || {
echo ""
return 1
}
local session_ids
if command -v jq >/dev/null 2>&1; then
session_ids=$(printf '%s' "$sessions_json" | jq -r '
if type == "array" then
[.[]?.id // empty]
elif (.sessions? | type) == "array" then
[.sessions[]?.id // empty]
else
[]
end
| map(select(length > 0))
| .[]
' 2>/dev/null | tr -d '\r')
else
session_ids=$(printf '%s' "$sessions_json" | grep -oE '"id"[[:space:]]*:[[:space:]]*"[^"]+"' | sed 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/' | tr -d '\r')
fi
local -a session_id_candidates=()
local session_id
while IFS= read -r session_id; do
if [[ -n "$session_id" && "$session_id" != "null" ]]; then
session_id_candidates+=("$session_id")
fi
done <<< "$session_ids"
if [[ ${#session_id_candidates[@]} -ne 1 ]]; then
echo ""
return 1
fi
echo "${session_id_candidates[0]}"
return 0
}

View File

@@ -0,0 +1,490 @@
#!/bin/bash
# Circuit Breaker Component for Ralph
# Prevents runaway token consumption by detecting stagnation
# Based on Michael Nygard's "Release It!" pattern
# Source date utilities for cross-platform compatibility
source "$(dirname "${BASH_SOURCE[0]}")/date_utils.sh"
# Circuit Breaker States
CB_STATE_CLOSED="CLOSED" # Normal operation, progress detected
CB_STATE_HALF_OPEN="HALF_OPEN" # Monitoring mode, checking for recovery
CB_STATE_OPEN="OPEN" # Failure detected, execution halted
# Circuit Breaker Configuration
# Use RALPH_DIR if set by main script, otherwise default to .ralph
RALPH_DIR="${RALPH_DIR:-.ralph}"
CB_STATE_FILE="$RALPH_DIR/.circuit_breaker_state"
CB_HISTORY_FILE="$RALPH_DIR/.circuit_breaker_history"
# Configurable thresholds - override via environment variables:
# Example: CB_NO_PROGRESS_THRESHOLD=10 ralph --monitor
CB_NO_PROGRESS_THRESHOLD=${CB_NO_PROGRESS_THRESHOLD:-3} # Open circuit after N loops with no progress
CB_SAME_ERROR_THRESHOLD=${CB_SAME_ERROR_THRESHOLD:-5} # Open circuit after N loops with same error
CB_OUTPUT_DECLINE_THRESHOLD=${CB_OUTPUT_DECLINE_THRESHOLD:-70} # Open circuit if output declines by >70%
CB_PERMISSION_DENIAL_THRESHOLD=${CB_PERMISSION_DENIAL_THRESHOLD:-2} # Open circuit after N loops with permission denials (Issue #101)
CB_COOLDOWN_MINUTES=${CB_COOLDOWN_MINUTES:-30} # Minutes before OPEN → HALF_OPEN auto-recovery (Issue #160)
CB_AUTO_RESET=${CB_AUTO_RESET:-false} # Reset to CLOSED on startup instead of waiting for cooldown
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Initialize circuit breaker
init_circuit_breaker() {
# Check if state file exists and is valid JSON
if [[ -f "$CB_STATE_FILE" ]]; then
if ! jq '.' "$CB_STATE_FILE" > /dev/null 2>&1; then
# Corrupted, recreate
rm -f "$CB_STATE_FILE"
fi
fi
if [[ ! -f "$CB_STATE_FILE" ]]; then
jq -n \
--arg state "$CB_STATE_CLOSED" \
--arg last_change "$(get_iso_timestamp)" \
'{
state: $state,
last_change: $last_change,
consecutive_no_progress: 0,
consecutive_same_error: 0,
consecutive_permission_denials: 0,
last_progress_loop: 0,
total_opens: 0,
reason: ""
}' > "$CB_STATE_FILE"
fi
# Ensure history file exists before any transition logging
if [[ -f "$CB_HISTORY_FILE" ]]; then
if ! jq '.' "$CB_HISTORY_FILE" > /dev/null 2>&1; then
# Corrupted, recreate
rm -f "$CB_HISTORY_FILE"
fi
fi
if [[ ! -f "$CB_HISTORY_FILE" ]]; then
echo '[]' > "$CB_HISTORY_FILE"
fi
# Auto-recovery: check if OPEN state should transition (Issue #160)
local current_state
current_state=$(jq -r '.state' "$CB_STATE_FILE" 2>/dev/null || echo "$CB_STATE_CLOSED")
if [[ "$current_state" == "$CB_STATE_OPEN" ]]; then
if [[ "$CB_AUTO_RESET" == "true" ]]; then
# Auto-reset: bypass cooldown, go straight to CLOSED
local current_loop total_opens
current_loop=$(jq -r '.current_loop // 0' "$CB_STATE_FILE" 2>/dev/null || echo "0")
total_opens=$(jq -r '.total_opens // 0' "$CB_STATE_FILE" 2>/dev/null || echo "0")
log_circuit_transition "$CB_STATE_OPEN" "$CB_STATE_CLOSED" "Auto-reset on startup (CB_AUTO_RESET=true)" "$current_loop"
jq -n \
--arg state "$CB_STATE_CLOSED" \
--arg last_change "$(get_iso_timestamp)" \
--argjson total_opens "$total_opens" \
'{
state: $state,
last_change: $last_change,
consecutive_no_progress: 0,
consecutive_same_error: 0,
consecutive_permission_denials: 0,
last_progress_loop: 0,
total_opens: $total_opens,
reason: "Auto-reset on startup"
}' > "$CB_STATE_FILE"
else
# Cooldown: check if enough time has elapsed to transition to HALF_OPEN
local opened_at
opened_at=$(jq -r '.opened_at // .last_change // ""' "$CB_STATE_FILE" 2>/dev/null || echo "")
if [[ -n "$opened_at" && "$opened_at" != "null" ]]; then
local opened_epoch current_epoch elapsed_minutes
opened_epoch=$(parse_iso_to_epoch "$opened_at")
current_epoch=$(date +%s)
elapsed_minutes=$(( (current_epoch - opened_epoch) / 60 ))
if [[ $elapsed_minutes -ge 0 && $elapsed_minutes -ge $CB_COOLDOWN_MINUTES ]]; then
local current_loop
current_loop=$(jq -r '.current_loop // 0' "$CB_STATE_FILE" 2>/dev/null || echo "0")
log_circuit_transition "$CB_STATE_OPEN" "$CB_STATE_HALF_OPEN" "Cooldown elapsed (${elapsed_minutes}m >= ${CB_COOLDOWN_MINUTES}m)" "$current_loop"
# Preserve counters but transition state
local state_data
state_data=$(cat "$CB_STATE_FILE")
echo "$state_data" | jq \
--arg state "$CB_STATE_HALF_OPEN" \
--arg last_change "$(get_iso_timestamp)" \
--arg reason "Cooldown recovery: ${elapsed_minutes}m elapsed" \
'.state = $state | .last_change = $last_change | .reason = $reason' \
> "$CB_STATE_FILE"
fi
# If elapsed_minutes < 0 (clock skew), stay OPEN safely
fi
fi
fi
}
# Get current circuit breaker state
get_circuit_state() {
if [[ ! -f "$CB_STATE_FILE" ]]; then
echo "$CB_STATE_CLOSED"
return
fi
jq -r '.state' "$CB_STATE_FILE" 2>/dev/null || echo "$CB_STATE_CLOSED"
}
# Check if circuit breaker allows execution
can_execute() {
local state=$(get_circuit_state)
if [[ "$state" == "$CB_STATE_OPEN" ]]; then
return 1 # Circuit is open, cannot execute
else
return 0 # Circuit is closed or half-open, can execute
fi
}
# Record loop execution result
record_loop_result() {
local loop_number=$1
local files_changed=$2
local has_errors=$3
local output_length=$4
init_circuit_breaker
local state_data=$(cat "$CB_STATE_FILE")
local current_state=$(echo "$state_data" | jq -r '.state')
local consecutive_no_progress=$(echo "$state_data" | jq -r '.consecutive_no_progress' | tr -d '[:space:]')
local consecutive_same_error=$(echo "$state_data" | jq -r '.consecutive_same_error' | tr -d '[:space:]')
local consecutive_permission_denials=$(echo "$state_data" | jq -r '.consecutive_permission_denials // 0' | tr -d '[:space:]')
local last_progress_loop=$(echo "$state_data" | jq -r '.last_progress_loop' | tr -d '[:space:]')
# Ensure integers
consecutive_no_progress=$((consecutive_no_progress + 0))
consecutive_same_error=$((consecutive_same_error + 0))
consecutive_permission_denials=$((consecutive_permission_denials + 0))
last_progress_loop=$((last_progress_loop + 0))
# Detect progress from multiple sources:
# 1. Files changed (git diff)
# 2. Completion signal in response analysis (STATUS: COMPLETE or has_completion_signal)
# 3. Claude explicitly reported files modified in RALPH_STATUS block
local has_progress=false
local has_completion_signal=false
local ralph_files_modified=0
# Check response analysis file for completion signals and reported file changes
local response_analysis_file="$RALPH_DIR/.response_analysis"
if [[ -f "$response_analysis_file" ]]; then
# Read completion signal - STATUS: COMPLETE counts as progress even without git changes
has_completion_signal=$(jq -r '.analysis.has_completion_signal // false' "$response_analysis_file" 2>/dev/null || echo "false")
# Also check exit_signal (Claude explicitly signaling completion)
local exit_signal
exit_signal=$(jq -r '.analysis.exit_signal // false' "$response_analysis_file" 2>/dev/null || echo "false")
if [[ "$exit_signal" == "true" ]]; then
has_completion_signal="true"
fi
# Check if Claude reported files modified (may differ from git diff if already committed)
ralph_files_modified=$(jq -r '.analysis.files_modified // 0' "$response_analysis_file" 2>/dev/null || echo "0")
ralph_files_modified=$((ralph_files_modified + 0))
fi
# Track permission denials (Issue #101)
local has_permission_denials="false"
if [[ -f "$response_analysis_file" ]]; then
has_permission_denials=$(jq -r '.analysis.has_permission_denials // false' "$response_analysis_file" 2>/dev/null || echo "false")
fi
if [[ "${PERMISSION_DENIAL_MODE:-halt}" == "threshold" && "$has_permission_denials" == "true" ]]; then
consecutive_permission_denials=$((consecutive_permission_denials + 1))
else
consecutive_permission_denials=0
fi
# Determine if progress was made
if [[ $files_changed -gt 0 ]]; then
# Git shows uncommitted changes - clear progress
has_progress=true
consecutive_no_progress=0
last_progress_loop=$loop_number
elif [[ "$has_completion_signal" == "true" ]]; then
# Claude reported STATUS: COMPLETE - this is progress even without git changes
# (work may have been committed already, or Claude finished analyzing/planning)
has_progress=true
consecutive_no_progress=0
last_progress_loop=$loop_number
elif [[ $ralph_files_modified -gt 0 ]]; then
# Claude reported modifying files (may be committed already)
has_progress=true
consecutive_no_progress=0
last_progress_loop=$loop_number
else
consecutive_no_progress=$((consecutive_no_progress + 1))
fi
# Detect same error repetition
if [[ "$has_errors" == "true" ]]; then
consecutive_same_error=$((consecutive_same_error + 1))
else
consecutive_same_error=0
fi
# Determine new state and reason
local new_state="$current_state"
local reason=""
# State transitions
case $current_state in
"$CB_STATE_CLOSED")
# Normal operation - check for failure conditions
# Permission denials take highest priority (Issue #101)
if [[ $consecutive_permission_denials -ge $CB_PERMISSION_DENIAL_THRESHOLD ]]; then
new_state="$CB_STATE_OPEN"
reason="Permission denied in $consecutive_permission_denials consecutive loops"
elif [[ $consecutive_no_progress -ge $CB_NO_PROGRESS_THRESHOLD ]]; then
new_state="$CB_STATE_OPEN"
reason="No progress detected in $consecutive_no_progress consecutive loops"
elif [[ $consecutive_same_error -ge $CB_SAME_ERROR_THRESHOLD ]]; then
new_state="$CB_STATE_OPEN"
reason="Same error repeated in $consecutive_same_error consecutive loops"
elif [[ $consecutive_no_progress -ge 2 ]]; then
new_state="$CB_STATE_HALF_OPEN"
reason="Monitoring: $consecutive_no_progress loops without progress"
fi
;;
"$CB_STATE_HALF_OPEN")
# Monitoring mode - either recover or fail
# Permission denials take highest priority (Issue #101)
if [[ $consecutive_permission_denials -ge $CB_PERMISSION_DENIAL_THRESHOLD ]]; then
new_state="$CB_STATE_OPEN"
reason="Permission denied in $consecutive_permission_denials consecutive loops"
elif [[ "$has_progress" == "true" ]]; then
new_state="$CB_STATE_CLOSED"
reason="Progress detected, circuit recovered"
elif [[ $consecutive_no_progress -ge $CB_NO_PROGRESS_THRESHOLD ]]; then
new_state="$CB_STATE_OPEN"
reason="No recovery, opening circuit after $consecutive_no_progress loops"
fi
;;
"$CB_STATE_OPEN")
# Circuit is open - stays open (auto-recovery handled in init_circuit_breaker)
reason="Circuit breaker is open, execution halted"
;;
esac
# Update state file
local total_opens=$(echo "$state_data" | jq -r '.total_opens' | tr -d '[:space:]')
total_opens=$((total_opens + 0))
if [[ "$new_state" == "$CB_STATE_OPEN" && "$current_state" != "$CB_STATE_OPEN" ]]; then
total_opens=$((total_opens + 1))
fi
# Determine opened_at: set when entering OPEN, preserve when staying OPEN
local opened_at=""
if [[ "$new_state" == "$CB_STATE_OPEN" && "$current_state" != "$CB_STATE_OPEN" ]]; then
# Entering OPEN state - record the timestamp
opened_at=$(get_iso_timestamp)
elif [[ "$new_state" == "$CB_STATE_OPEN" && "$current_state" == "$CB_STATE_OPEN" ]]; then
# Staying OPEN - preserve existing opened_at (fall back to last_change for old state files)
opened_at=$(echo "$state_data" | jq -r '.opened_at // .last_change // ""' 2>/dev/null)
fi
jq -n \
--arg state "$new_state" \
--arg last_change "$(get_iso_timestamp)" \
--argjson consecutive_no_progress "$consecutive_no_progress" \
--argjson consecutive_same_error "$consecutive_same_error" \
--argjson consecutive_permission_denials "$consecutive_permission_denials" \
--argjson last_progress_loop "$last_progress_loop" \
--argjson total_opens "$total_opens" \
--arg reason "$reason" \
--argjson current_loop "$loop_number" \
'{
state: $state,
last_change: $last_change,
consecutive_no_progress: $consecutive_no_progress,
consecutive_same_error: $consecutive_same_error,
consecutive_permission_denials: $consecutive_permission_denials,
last_progress_loop: $last_progress_loop,
total_opens: $total_opens,
reason: $reason,
current_loop: $current_loop
}' > "$CB_STATE_FILE"
# Add opened_at if set (entering or staying in OPEN state)
if [[ -n "$opened_at" ]]; then
local tmp
tmp=$(jq --arg opened_at "$opened_at" '. + {opened_at: $opened_at}' "$CB_STATE_FILE")
echo "$tmp" > "$CB_STATE_FILE"
fi
# Log state transition
if [[ "$new_state" != "$current_state" ]]; then
log_circuit_transition "$current_state" "$new_state" "$reason" "$loop_number"
fi
# Return exit code based on new state
if [[ "$new_state" == "$CB_STATE_OPEN" ]]; then
return 1 # Circuit opened, signal to stop
else
return 0 # Can continue
fi
}
# Log circuit breaker state transitions
log_circuit_transition() {
local from_state=$1
local to_state=$2
local reason=$3
local loop_number=$4
local transition
transition=$(jq -n -c \
--arg timestamp "$(get_iso_timestamp)" \
--argjson loop "$loop_number" \
--arg from_state "$from_state" \
--arg to_state "$to_state" \
--arg reason "$reason" \
'{
timestamp: $timestamp,
loop: $loop,
from_state: $from_state,
to_state: $to_state,
reason: $reason
}')
local history
history=$(cat "$CB_HISTORY_FILE")
history=$(echo "$history" | jq ". += [$transition]")
echo "$history" > "$CB_HISTORY_FILE"
# Console log with colors
case $to_state in
"$CB_STATE_OPEN")
echo -e "${RED}🚨 CIRCUIT BREAKER OPENED${NC}"
echo -e "${RED}Reason: $reason${NC}"
;;
"$CB_STATE_HALF_OPEN")
echo -e "${YELLOW}⚠️ CIRCUIT BREAKER: Monitoring Mode${NC}"
echo -e "${YELLOW}Reason: $reason${NC}"
;;
"$CB_STATE_CLOSED")
echo -e "${GREEN}✅ CIRCUIT BREAKER: Normal Operation${NC}"
echo -e "${GREEN}Reason: $reason${NC}"
;;
esac
}
# Display circuit breaker status
show_circuit_status() {
init_circuit_breaker
local state_data=$(cat "$CB_STATE_FILE")
local state=$(echo "$state_data" | jq -r '.state')
local reason=$(echo "$state_data" | jq -r '.reason')
local no_progress=$(echo "$state_data" | jq -r '.consecutive_no_progress')
local last_progress=$(echo "$state_data" | jq -r '.last_progress_loop')
local current_loop=$(echo "$state_data" | jq -r '.current_loop')
local total_opens=$(echo "$state_data" | jq -r '.total_opens')
local color=""
local status_icon=""
case $state in
"$CB_STATE_CLOSED")
color=$GREEN
status_icon="✅"
;;
"$CB_STATE_HALF_OPEN")
color=$YELLOW
status_icon="⚠️ "
;;
"$CB_STATE_OPEN")
color=$RED
status_icon="🚨"
;;
esac
echo -e "${color}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${color}║ Circuit Breaker Status ║${NC}"
echo -e "${color}╚════════════════════════════════════════════════════════════╝${NC}"
echo -e "${color}State:${NC} $status_icon $state"
echo -e "${color}Reason:${NC} $reason"
echo -e "${color}Loops since progress:${NC} $no_progress"
echo -e "${color}Last progress:${NC} Loop #$last_progress"
echo -e "${color}Current loop:${NC} #$current_loop"
echo -e "${color}Total opens:${NC} $total_opens"
echo ""
}
# Reset circuit breaker (for manual intervention)
reset_circuit_breaker() {
local reason=${1:-"Manual reset"}
jq -n \
--arg state "$CB_STATE_CLOSED" \
--arg last_change "$(get_iso_timestamp)" \
--arg reason "$reason" \
'{
state: $state,
last_change: $last_change,
consecutive_no_progress: 0,
consecutive_same_error: 0,
consecutive_permission_denials: 0,
last_progress_loop: 0,
total_opens: 0,
reason: $reason
}' > "$CB_STATE_FILE"
echo -e "${GREEN}✅ Circuit breaker reset to CLOSED state${NC}"
}
# Check if loop should halt (used in main loop)
should_halt_execution() {
local state=$(get_circuit_state)
if [[ "$state" == "$CB_STATE_OPEN" ]]; then
show_circuit_status
echo ""
echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ EXECUTION HALTED: Circuit Breaker Opened ║${NC}"
echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${YELLOW}Ralph has detected that no progress is being made.${NC}"
echo ""
echo -e "${YELLOW}Possible reasons:${NC}"
echo " • Project may be complete (check .ralph/@fix_plan.md)"
echo " • The active driver may be stuck on an error"
echo " • .ralph/PROMPT.md may need clarification"
echo " • Manual intervention may be required"
echo ""
echo -e "${YELLOW}To continue:${NC}"
echo " 1. Review recent logs: tail -20 .ralph/logs/ralph.log"
echo " 2. Check recent driver output: ls -lt .ralph/logs/claude_output_*.log | head -1"
echo " 3. Update .ralph/@fix_plan.md if needed"
echo " 4. Reset circuit breaker: bash .ralph/ralph_loop.sh --reset-circuit"
echo ""
return 0 # Signal to halt
else
return 1 # Can continue
fi
}
# Export functions
export -f init_circuit_breaker
export -f get_circuit_state
export -f can_execute
export -f record_loop_result
export -f show_circuit_status
export -f reset_circuit_breaker
export -f should_halt_execution

123
.ralph/lib/date_utils.sh Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# date_utils.sh - Cross-platform date utility functions
# Provides consistent date formatting and arithmetic across GNU (Linux) and BSD (macOS) systems
# Get current timestamp in ISO 8601 format with seconds precision
# Returns: YYYY-MM-DDTHH:MM:SS+00:00 format
# Uses capability detection instead of uname to handle macOS with Homebrew coreutils
get_iso_timestamp() {
# Try GNU date first (works on Linux and macOS with Homebrew coreutils)
local result
if result=$(date -u -Iseconds 2>/dev/null) && [[ -n "$result" ]]; then
echo "$result"
return
fi
# Fallback to BSD date (native macOS) - add colon to timezone offset
date -u +"%Y-%m-%dT%H:%M:%S%z" | sed 's/\(..\)$/:\1/'
}
# Get time component (HH:MM:SS) for one hour from now
# Returns: HH:MM:SS format
# Uses capability detection instead of uname to handle macOS with Homebrew coreutils
get_next_hour_time() {
# Try GNU date first (works on Linux and macOS with Homebrew coreutils)
if date -d '+1 hour' '+%H:%M:%S' 2>/dev/null; then
return
fi
# Fallback to BSD date (native macOS)
if date -v+1H '+%H:%M:%S' 2>/dev/null; then
return
fi
# Ultimate fallback - compute using epoch arithmetic
local future_epoch=$(($(date +%s) + 3600))
date -r "$future_epoch" '+%H:%M:%S' 2>/dev/null || date '+%H:%M:%S'
}
# Get current timestamp in a basic format (fallback)
# Returns: YYYY-MM-DD HH:MM:SS format
get_basic_timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
# Get current Unix epoch time in seconds
# Returns: Integer seconds since 1970-01-01 00:00:00 UTC
get_epoch_seconds() {
date +%s
}
# Convert ISO 8601 timestamp to Unix epoch seconds
# Input: ISO timestamp (e.g., "2025-01-15T10:30:00+00:00")
# Returns: Unix epoch seconds on stdout
# Returns non-zero on parse failure.
parse_iso_to_epoch_strict() {
local iso_timestamp=$1
if [[ -z "$iso_timestamp" || "$iso_timestamp" == "null" ]]; then
return 1
fi
local normalized_iso
normalized_iso=$(printf '%s' "$iso_timestamp" | sed -E 's/\.([0-9]+)(Z|[+-][0-9]{2}:[0-9]{2})$/\2/')
# Try GNU date -d (Linux, macOS with Homebrew coreutils)
local result
if result=$(date -d "$iso_timestamp" +%s 2>/dev/null) && [[ "$result" =~ ^[0-9]+$ ]]; then
echo "$result"
return 0
fi
# Try BSD date -j (native macOS)
# Normalize timezone for BSD parsing (Z → +0000, ±HH:MM → ±HHMM)
local tz_fixed
tz_fixed=$(printf '%s' "$normalized_iso" | sed -E 's/Z$/+0000/; s/([+-][0-9]{2}):([0-9]{2})$/\1\2/')
if result=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$tz_fixed" +%s 2>/dev/null) && [[ "$result" =~ ^[0-9]+$ ]]; then
echo "$result"
return 0
fi
# Fallback: manual epoch arithmetic from ISO components
# Parse: YYYY-MM-DDTHH:MM:SS (ignore timezone, assume UTC)
local year month day hour minute second
if [[ "$normalized_iso" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2}) ]]; then
year="${BASH_REMATCH[1]}"
month="${BASH_REMATCH[2]}"
day="${BASH_REMATCH[3]}"
hour="${BASH_REMATCH[4]}"
minute="${BASH_REMATCH[5]}"
second="${BASH_REMATCH[6]}"
# Use date with explicit components if available
if result=$(date -u -d "${year}-${month}-${day} ${hour}:${minute}:${second}" +%s 2>/dev/null) && [[ "$result" =~ ^[0-9]+$ ]]; then
echo "$result"
return 0
fi
fi
return 1
}
# Convert ISO 8601 timestamp to Unix epoch seconds
# Input: ISO timestamp (e.g., "2025-01-15T10:30:00+00:00")
# Returns: Unix epoch seconds on stdout
# Falls back to current epoch on parse failure (safe default)
parse_iso_to_epoch() {
local iso_timestamp=$1
local result
if result=$(parse_iso_to_epoch_strict "$iso_timestamp"); then
echo "$result"
return 0
fi
# Ultimate fallback: return current epoch (safe default)
date +%s
}
# Export functions for use in other scripts
export -f get_iso_timestamp
export -f get_next_hour_time
export -f get_basic_timestamp
export -f get_epoch_seconds
export -f parse_iso_to_epoch_strict
export -f parse_iso_to_epoch

820
.ralph/lib/enable_core.sh Normal file
View File

@@ -0,0 +1,820 @@
#!/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
# Detect from pyproject.toml or setup.py (Python)
elif [[ -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
# Detect from Cargo.toml (Rust)
elif [[ -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"
# Detect from go.mod (Go)
elif [[ -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
- Toggle completed story checkboxes in @fix_plan.md without rewriting story lines
- Commit working changes with descriptive messages
## Progress Tracking (CRITICAL)
- Ralph tracks progress by counting story checkboxes in @fix_plan.md
- When you complete a story, change \`- [ ]\` to \`- [x]\` on that exact story line
- Do NOT remove, rewrite, or reorder story lines in @fix_plan.md
- Update the checkbox before committing so the monitor updates immediately
- Set \`TASKS_COMPLETED_THIS_LOOP\` to the exact number of story checkboxes toggled this loop
- Only valid values: 0 or 1
## 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: 0 | 1
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

File diff suppressed because it is too large Load Diff

611
.ralph/lib/task_sources.sh Normal file
View File

@@ -0,0 +1,611 @@
#!/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 (filterStatus) - Status filter (optional, default: "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 filterStatus="${1:-open}"
local tasks=""
# Check if beads is available
if ! check_beads_available; then
return 1
fi
# Build bd list command arguments
local bdArgs=("list" "--json")
if [[ "$filterStatus" == "open" ]]; then
bdArgs+=("--status" "open")
elif [[ "$filterStatus" == "in_progress" ]]; then
bdArgs+=("--status" "in_progress")
elif [[ "$filterStatus" == "all" ]]; then
bdArgs+=("--all")
fi
# Try to get tasks as JSON
local json_output
if json_output=$(bd "${bdArgs[@]}" 2>/dev/null); then
# Parse JSON and format as markdown tasks
# Note: Use 'select(.status == "closed") | not' to avoid bash escaping issues with '!='
# Also filter out entries with missing id or title fields
if command -v jq &>/dev/null; then
tasks=$(echo "$json_output" | jq -r '
.[] |
select(.status == "closed" | not) |
select((.id // "") != "" and (.title // "") != "") |
"- [ ] [\(.id)] \(.title)"
' 2>/dev/null || echo "")
fi
fi
# Fallback: try plain text output if JSON failed or produced no results
if [[ -z "$tasks" ]]; then
# Build fallback args (reuse status logic, but without --json)
local fallbackArgs=("list")
if [[ "$filterStatus" == "open" ]]; then
fallbackArgs+=("--status" "open")
elif [[ "$filterStatus" == "in_progress" ]]; then
fallbackArgs+=("--status" "in_progress")
elif [[ "$filterStatus" == "all" ]]; then
fallbackArgs+=("--all")
fi
tasks=$(bd "${fallbackArgs[@]}" 2>/dev/null | while IFS= read -r line; do
# Extract ID and title from bd list output
# Format: "○ cnzb-xxx [● P2] [task] - Title here"
local id title
id=$(echo "$line" | grep -oE '[a-z]+-[a-z0-9]+' | head -1 || echo "")
# Extract title after the last " - " separator
title=$(echo "$line" | sed 's/.*- //' || echo "$line")
if [[ -n "$id" && -n "$title" ]]; 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
# Note: Use 'select(.status == "closed" | not)' to avoid bash escaping issues with '!='
count=$(bd list --json 2>/dev/null | jq '[.[] | select(.status == "closed" | not)] | 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 (with line numbers to handle duplicates)
local heading_lines
heading_lines=$(grep -nE '^#{1,3}[[:space:]]+(TODO|Tasks|Requirements|Features|Backlog|Sprint)' "$prd_file" 2>/dev/null)
if [[ -n "$heading_lines" ]]; then
# Extract bullet items beneath each matching heading
while IFS= read -r heading_entry; do
# Parse line number directly from grep -n output (avoids duplicate heading issue)
local heading_line
heading_line=$(echo "$heading_entry" | cut -d: -f1)
[[ -z "$heading_line" ]] && continue
# Find the next heading (any level) after this one
local next_heading_line
next_heading_line=$(tail -n +"$((heading_line + 1))" "$prd_file" | grep -n '^#' | head -1 | cut -d: -f1)
# Extract the section content
local section_content
if [[ -n "$next_heading_line" ]]; then
local end_line=$((heading_line + next_heading_line - 1))
section_content=$(sed -n "$((heading_line + 1)),${end_line}p" "$prd_file")
else
section_content=$(tail -n +"$((heading_line + 1))" "$prd_file")
fi
# Extract bullet items from section and convert to checkboxes
while IFS= read -r line; do
local task_text=""
# Match "- item" or "* item" (but not checkboxes, already handled above)
if [[ "$line" =~ ^[[:space:]]*[-*][[:space:]]+(.+)$ ]]; then
task_text="${BASH_REMATCH[1]}"
# Skip checkbox lines — they are handled by the earlier extraction
if [[ "$line" == *"["*"]"* ]]; then
task_text=""
fi
# Match "N. item" numbered patterns
elif [[ "$line" =~ ^[[:space:]]*[0-9]+\.[[:space:]]+(.+)$ ]]; then
task_text="${BASH_REMATCH[1]}"
fi
if [[ -n "$task_text" ]]; then
tasks="${tasks}
- [ ] ${task_text}"
fi
done <<< "$section_content"
done <<< "$heading_lines"
fi
# Clean up and output
if [[ -n "$tasks" ]]; then
echo "$tasks" | grep -v '^$' | awk '!seen[$0]++' | head -30 # Deduplicate, limit to 30
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

145
.ralph/lib/timeout_utils.sh Normal file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env bash
# timeout_utils.sh - Cross-platform timeout utility functions
# Provides consistent timeout command execution across GNU (Linux) and BSD (macOS) systems
#
# On Linux: Uses the built-in GNU `timeout` command from coreutils
# On macOS: Uses `gtimeout` from Homebrew coreutils, or falls back to `timeout` if available
# Cached timeout command to avoid repeated detection
export _TIMEOUT_CMD=""
# Detect the available timeout command for this platform
# Sets _TIMEOUT_CMD to the appropriate command
# Returns 0 if a timeout command is available, 1 if not
detect_timeout_command() {
# Return cached result if already detected
if [[ -n "$_TIMEOUT_CMD" ]]; then
echo "$_TIMEOUT_CMD"
return 0
fi
local os_type
os_type=$(uname)
if [[ "$os_type" == "Darwin" ]]; then
# macOS: Check for gtimeout (from Homebrew coreutils) first
if command -v gtimeout &> /dev/null; then
_TIMEOUT_CMD="gtimeout"
elif command -v timeout &> /dev/null; then
# Some macOS setups might have timeout available (e.g., MacPorts)
_TIMEOUT_CMD="timeout"
else
# No timeout command available
_TIMEOUT_CMD=""
return 1
fi
else
# Linux and other Unix systems: use standard timeout
if command -v timeout &> /dev/null; then
_TIMEOUT_CMD="timeout"
else
# Timeout not found (unusual on Linux)
_TIMEOUT_CMD=""
return 1
fi
fi
echo "$_TIMEOUT_CMD"
return 0
}
# Check if a timeout command is available on this system
# Returns 0 if available, 1 if not
has_timeout_command() {
local cmd
cmd=$(detect_timeout_command 2>/dev/null)
[[ -n "$cmd" ]]
}
# Get a user-friendly message about timeout availability
# Useful for error messages and installation instructions
get_timeout_status_message() {
local os_type
os_type=$(uname)
if has_timeout_command; then
local cmd
cmd=$(detect_timeout_command)
echo "Timeout command available: $cmd"
return 0
fi
if [[ "$os_type" == "Darwin" ]]; then
echo "Timeout command not found. Install GNU coreutils: brew install coreutils"
else
echo "Timeout command not found. Install coreutils: sudo apt-get install coreutils"
fi
return 1
}
# Execute a command with a timeout (cross-platform)
# Usage: portable_timeout DURATION COMMAND [ARGS...]
#
# Arguments:
# DURATION - Timeout duration (e.g., "30s", "5m", "1h")
# COMMAND - The command to execute
# ARGS - Additional arguments for the command
#
# Returns:
# 0 - Command completed successfully within timeout
# 124 - Command timed out (GNU timeout behavior)
# 1 - No timeout command available (logs error)
# * - Exit code from the executed command
#
# Example:
# portable_timeout 30s curl -s https://example.com
# portable_timeout 5m npm install
#
portable_timeout() {
local duration=$1
shift
# Validate arguments
if [[ -z "$duration" ]]; then
echo "Error: portable_timeout requires a duration argument" >&2
return 1
fi
if [[ $# -eq 0 ]]; then
echo "Error: portable_timeout requires a command to execute" >&2
return 1
fi
# Detect the timeout command
local timeout_cmd
timeout_cmd=$(detect_timeout_command 2>/dev/null)
if [[ -z "$timeout_cmd" ]]; then
local os_type
os_type=$(uname)
echo "Error: No timeout command available on this system" >&2
if [[ "$os_type" == "Darwin" ]]; then
echo "Install GNU coreutils on macOS: brew install coreutils" >&2
else
echo "Install coreutils: sudo apt-get install coreutils" >&2
fi
return 1
fi
# Execute the command with timeout
"$timeout_cmd" "$duration" "$@"
}
# Reset the cached timeout command (useful for testing)
reset_timeout_detection() {
_TIMEOUT_CMD=""
}
# Export functions for use in other scripts
export -f detect_timeout_command
export -f has_timeout_command
export -f get_timeout_status_message
export -f portable_timeout
export -f reset_timeout_detection

556
.ralph/lib/wizard_utils.sh Normal file
View File

@@ -0,0 +1,556 @@
#!/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" == [yY] ]]; then
yn_hint="[Y/n]"
fi
while true; do
# Display prompt to stderr for consistency with other prompt functions
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} ${yn_hint}: " >&2
read -r response
# Handle empty response (use default)
if [[ -z "$response" ]]; then
response="$default"
fi
case "$response" in
[yY]|[yY][eE][sS])
return 0
;;
[nN]|[nN][oO])
return 1
;;
*)
echo -e "${WIZARD_YELLOW}Please answer yes (y) or no (n)${WIZARD_NC}" >&2
;;
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
# Display prompt to stderr so command substitution only captures the response
if [[ -n "$default" ]]; then
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: " >&2
else
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: " >&2
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
# Display prompt to stderr so command substitution only captures the response
if [[ -n "$default" ]]; then
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: " >&2
else
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: " >&2
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}" >&2
continue
fi
fi
# Validate it's a number
if ! [[ "$response" =~ ^[0-9]+$ ]]; then
echo -e "${WIZARD_YELLOW}Please enter a valid number${WIZARD_NC}" >&2
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}" >&2
continue
fi
if [[ -n "$max" && "$response" -gt "$max" ]]; then
echo -e "${WIZARD_YELLOW}Value must be at most ${max}${WIZARD_NC}" >&2
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
# Display prompt and options to stderr so command substitution only captures the result
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" >&2
echo "" >&2
# Display options
local i=1
for opt in "${options[@]}"; do
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}" >&2
((i++))
done
echo "" >&2
while true; do
echo -en "Select option [1-${num_options}]: " >&2
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}" >&2
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[@]}
# Display prompt and options to stderr so command substitution only captures the result
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" >&2
echo "" >&2
# 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}" >&2
else
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}" >&2
fi
((i++))
done
echo "" >&2
while true; do
echo -en "Select option [1-${num_options}] (default: ${default_index}): " >&2
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}" >&2
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
# Guard against division by zero
if [[ $total -le 0 ]]; then
local bar_width=30
local bar=""
for ((i = 0; i < bar_width; i++)); do bar+="░"; done
echo -en "\r${WIZARD_CYAN}[${bar}]${WIZARD_NC} 0/${total} ${message}"
return 0
fi
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

664
.ralph/ralph_import.sh Executable file
View File

@@ -0,0 +1,664 @@
#!/bin/bash
# Ralph Import - Convert PRDs to Ralph format using Claude Code
# Version: 0.9.8 - Modern CLI support with JSON output parsing
#
# DEPRECATED: This script is from standalone Ralph and references `ralph-setup`
# which does not exist in bmalph. Use `bmalph implement` for PRD-to-Ralph
# transition instead. This file is bundled for backward compatibility only.
set -e
# Configuration
CLAUDE_CODE_CMD="claude"
# Platform driver support
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
PLATFORM_DRIVER="${PLATFORM_DRIVER:-claude-code}"
# Source platform driver if available
if [[ -f "$SCRIPT_DIR/drivers/${PLATFORM_DRIVER}.sh" ]]; then
# shellcheck source=/dev/null
source "$SCRIPT_DIR/drivers/${PLATFORM_DRIVER}.sh"
CLAUDE_CODE_CMD="$(driver_cli_binary)"
fi
# Modern CLI Configuration (Phase 1.1)
# These flags enable structured JSON output and controlled file operations
CLAUDE_OUTPUT_FORMAT="json"
# Use bash array for proper quoting of each tool argument
declare -a CLAUDE_ALLOWED_TOOLS=('Read' 'Write' 'Bash(mkdir:*)' 'Bash(cp:*)')
CLAUDE_MIN_VERSION="2.0.76" # Minimum version for modern CLI features
# Temporary file names
CONVERSION_OUTPUT_FILE=".ralph_conversion_output.json"
CONVERSION_PROMPT_FILE=".ralph_conversion_prompt.md"
# Global parsed conversion result variables
# Set by parse_conversion_response() when parsing JSON output from Claude CLI
declare PARSED_RESULT="" # Result/summary text from Claude response
declare PARSED_SESSION_ID="" # Session ID for potential continuation
declare PARSED_FILES_CHANGED="" # Count of files changed
declare PARSED_HAS_ERRORS="" # Boolean flag indicating errors occurred
declare PARSED_COMPLETION_STATUS="" # Completion status (complete/partial/failed)
declare PARSED_ERROR_MESSAGE="" # Error message if conversion failed
declare PARSED_ERROR_CODE="" # Error code if conversion failed
declare PARSED_FILES_CREATED="" # JSON array of files created
declare PARSED_MISSING_FILES="" # JSON array of files that should exist but don't
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() {
local level=$1
local message=$2
local color=""
case $level in
"INFO") color=$BLUE ;;
"WARN") color=$YELLOW ;;
"ERROR") color=$RED ;;
"SUCCESS") color=$GREEN ;;
esac
echo -e "${color}[$(date '+%H:%M:%S')] [$level] $message${NC}"
}
# =============================================================================
# JSON OUTPUT FORMAT DETECTION AND PARSING
# =============================================================================
# detect_response_format - Detect whether file contains JSON or plain text output
#
# Parameters:
# $1 (output_file) - Path to the file to inspect
#
# Returns:
# Echoes "json" if file is non-empty, starts with { or [, and validates as JSON
# Echoes "text" otherwise (empty file, non-JSON content, or invalid JSON)
#
# Dependencies:
# - jq (used for JSON validation; if unavailable, falls back to "text")
#
detect_response_format() {
local output_file=$1
if [[ ! -f "$output_file" ]] || [[ ! -s "$output_file" ]]; then
echo "text"
return
fi
# Check if file starts with { or [ (JSON indicators)
# Use grep to find first non-whitespace character (handles leading whitespace)
local first_char=$(grep -m1 -o '[^[:space:]]' "$output_file" 2>/dev/null)
if [[ "$first_char" != "{" && "$first_char" != "[" ]]; then
echo "text"
return
fi
# Validate as JSON using jq
if command -v jq &>/dev/null && jq empty "$output_file" 2>/dev/null; then
echo "json"
else
echo "text"
fi
}
# parse_conversion_response - Parse JSON response and extract conversion status
#
# Parameters:
# $1 (output_file) - Path to JSON file containing Claude CLI response
#
# Returns:
# 0 on success (valid JSON parsed)
# 1 on error (file not found, jq unavailable, or invalid JSON)
#
# Sets Global Variables:
# PARSED_RESULT - Result/summary text from response
# PARSED_SESSION_ID - Session ID for continuation
# PARSED_FILES_CHANGED - Count of files changed
# PARSED_HAS_ERRORS - "true"/"false" indicating errors
# PARSED_COMPLETION_STATUS - Status: "complete", "partial", "failed", "unknown"
# PARSED_ERROR_MESSAGE - Error message if conversion failed
# PARSED_ERROR_CODE - Error code if conversion failed
# PARSED_FILES_CREATED - JSON array string of created files
# PARSED_MISSING_FILES - JSON array string of missing files
#
# Dependencies:
# - jq (required for JSON parsing)
#
parse_conversion_response() {
local output_file=$1
if [[ ! -f "$output_file" ]]; then
return 1
fi
# Check if jq is available
if ! command -v jq &>/dev/null; then
log "WARN" "jq not found, skipping JSON parsing"
return 1
fi
# Validate JSON first
if ! jq empty "$output_file" 2>/dev/null; then
log "WARN" "Invalid JSON in output, falling back to text parsing"
return 1
fi
# Extract fields from JSON response
# Supports both flat format and Claude CLI format with metadata
# Result/summary field
PARSED_RESULT=$(jq -r '.result // .summary // ""' "$output_file" 2>/dev/null)
# Session ID (for potential continuation)
PARSED_SESSION_ID=$(jq -r '.sessionId // .session_id // ""' "$output_file" 2>/dev/null)
# Files changed count
PARSED_FILES_CHANGED=$(jq -r '.metadata.files_changed // .files_changed // 0' "$output_file" 2>/dev/null)
# Has errors flag
PARSED_HAS_ERRORS=$(jq -r '.metadata.has_errors // .has_errors // false' "$output_file" 2>/dev/null)
# Completion status
PARSED_COMPLETION_STATUS=$(jq -r '.metadata.completion_status // .completion_status // "unknown"' "$output_file" 2>/dev/null)
# Error message (if any)
PARSED_ERROR_MESSAGE=$(jq -r '.metadata.error_message // .error_message // ""' "$output_file" 2>/dev/null)
# Error code (if any)
PARSED_ERROR_CODE=$(jq -r '.metadata.error_code // .error_code // ""' "$output_file" 2>/dev/null)
# Files created (as array)
PARSED_FILES_CREATED=$(jq -r '.metadata.files_created // [] | @json' "$output_file" 2>/dev/null)
# Missing files (as array)
PARSED_MISSING_FILES=$(jq -r '.metadata.missing_files // [] | @json' "$output_file" 2>/dev/null)
return 0
}
# check_claude_version - Verify Claude Code CLI version meets minimum requirements
#
# Checks if the installed Claude Code CLI version is at or above CLAUDE_MIN_VERSION.
# Uses numeric semantic version comparison (major.minor.patch).
#
# Parameters:
# None (uses global CLAUDE_CODE_CMD and CLAUDE_MIN_VERSION)
#
# Returns:
# 0 if version is >= CLAUDE_MIN_VERSION
# 1 if version cannot be determined or is below CLAUDE_MIN_VERSION
#
# Side Effects:
# Logs warning via log() if version check fails
#
check_claude_version() {
local version
version=$($CLAUDE_CODE_CMD --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ -z "$version" ]]; then
log "WARN" "Could not determine Claude Code CLI version"
return 1
fi
# Numeric semantic version comparison
# Split versions into major.minor.patch components
local ver_major ver_minor ver_patch
local min_major min_minor min_patch
IFS='.' read -r ver_major ver_minor ver_patch <<< "$version"
IFS='.' read -r min_major min_minor min_patch <<< "$CLAUDE_MIN_VERSION"
# Default empty components to 0 (handles versions like "2.1" without patch)
ver_major=${ver_major:-0}
ver_minor=${ver_minor:-0}
ver_patch=${ver_patch:-0}
min_major=${min_major:-0}
min_minor=${min_minor:-0}
min_patch=${min_patch:-0}
# Compare major version
if [[ $ver_major -lt $min_major ]]; then
log "WARN" "Claude Code CLI version $version is below recommended $CLAUDE_MIN_VERSION"
return 1
elif [[ $ver_major -gt $min_major ]]; then
return 0
fi
# Major equal, compare minor version
if [[ $ver_minor -lt $min_minor ]]; then
log "WARN" "Claude Code CLI version $version is below recommended $CLAUDE_MIN_VERSION"
return 1
elif [[ $ver_minor -gt $min_minor ]]; then
return 0
fi
# Minor equal, compare patch version
if [[ $ver_patch -lt $min_patch ]]; then
log "WARN" "Claude Code CLI version $version is below recommended $CLAUDE_MIN_VERSION"
return 1
fi
return 0
}
show_help() {
cat << HELPEOF
Ralph Import - Convert PRDs to Ralph Format
Usage: $0 <source-file> [project-name]
Arguments:
source-file Path to your PRD/specification file (any format)
project-name Name for the new Ralph project (optional, defaults to filename)
Examples:
$0 my-app-prd.md
$0 requirements.txt my-awesome-app
$0 project-spec.json
$0 design-doc.docx webapp
Supported formats:
- Markdown (.md)
- Text files (.txt)
- JSON (.json)
- Word documents (.docx)
- PDFs (.pdf)
- Any text-based format
This legacy helper is kept for standalone Ralph compatibility.
If you are using bmalph, use `bmalph implement` instead.
The command will:
1. Create a new Ralph project
2. Use Claude Code to intelligently convert your PRD into:
- .ralph/PROMPT.md (Ralph instructions)
- .ralph/@fix_plan.md (prioritized tasks)
- .ralph/specs/ (technical specifications)
HELPEOF
}
# Check dependencies
check_dependencies() {
if ! command -v ralph-setup &> /dev/null; then
log "WARN" "ralph-setup not found. If using bmalph, run 'bmalph init' instead."
log "WARN" "This script is deprecated — use 'bmalph implement' for PRD conversion."
return 1
fi
if ! command -v jq &> /dev/null; then
log "WARN" "jq not found. Install it (brew install jq | sudo apt-get install jq | choco install jq) for faster JSON parsing."
fi
if ! command -v "$CLAUDE_CODE_CMD" &> /dev/null 2>&1; then
log "WARN" "Claude Code CLI ($CLAUDE_CODE_CMD) not found. It will be downloaded when first used."
fi
}
# Convert PRD using Claude Code
convert_prd() {
local source_file=$1
local project_name=$2
local use_modern_cli=true
local cli_exit_code=0
log "INFO" "Converting PRD to Ralph format using Claude Code..."
# Check for modern CLI support
if ! check_claude_version 2>/dev/null; then
log "INFO" "Using standard CLI mode (modern features may not be available)"
use_modern_cli=false
else
log "INFO" "Using modern CLI with JSON output format"
fi
# Create conversion prompt
cat > "$CONVERSION_PROMPT_FILE" << 'PROMPTEOF'
# PRD to Ralph Conversion Task
You are tasked with converting a Product Requirements Document (PRD) or specification into Ralph's autonomous implementation format.
## Input Analysis
Analyze the provided specification file and extract:
- Project goals and objectives
- Core features and requirements
- Technical constraints and preferences
- Priority levels and phases
- Success criteria
## Required Outputs
Create these files in the .ralph/ subdirectory:
### 1. .ralph/PROMPT.md
Transform the PRD into Ralph development instructions:
```markdown
# Ralph Development Instructions
## Context
You are Ralph, an autonomous AI development agent working on a [PROJECT NAME] project.
## Current Objectives
[Extract and prioritize 4-6 main objectives from the PRD]
## Key Principles
- ONE task per loop - focus on the most important thing
- Search the codebase before assuming something isn't implemented
- Use subagents for expensive operations (file searching, analysis)
- Write comprehensive tests with clear documentation
- Toggle completed story checkboxes in @fix_plan.md without rewriting story lines
- Commit working changes with descriptive messages
## Progress Tracking (CRITICAL)
- Ralph tracks progress by counting story checkboxes in @fix_plan.md
- When you complete a story, change `- [ ]` to `- [x]` on that exact story line
- Do NOT remove, rewrite, or reorder story lines in @fix_plan.md
- Update the checkbox before committing so the monitor updates immediately
- Set `TASKS_COMPLETED_THIS_LOOP` to the exact number of story checkboxes toggled this loop
- Only valid values: 0 or 1
## 🧪 Testing Guidelines (CRITICAL)
- LIMIT testing to ~20% of your total effort per loop
- PRIORITIZE: Implementation > Documentation > Tests
- Only write tests for NEW functionality you implement
- Do NOT refactor existing tests unless broken
- Focus on CORE functionality first, comprehensive testing later
## Project Requirements
[Convert PRD requirements into clear, actionable development requirements]
## Technical Constraints
[Extract any technical preferences, frameworks, languages mentioned]
## Success Criteria
[Define what "done" looks like based on the PRD]
## Current Task
Follow @fix_plan.md and choose the most important item to implement next.
```
### 2. .ralph/@fix_plan.md
Convert requirements into a prioritized task list:
```markdown
# Ralph Fix Plan
## High Priority
[Extract and convert critical features into actionable tasks]
## Medium Priority
[Secondary features and enhancements]
## Low Priority
[Nice-to-have features and optimizations]
## Completed
- [x] Project initialization
## Notes
[Any important context from the original PRD]
```
### 3. .ralph/specs/requirements.md
Create detailed technical specifications:
```markdown
# Technical Specifications
[Convert PRD into detailed technical requirements including:]
- System architecture requirements
- Data models and structures
- API specifications
- User interface requirements
- Performance requirements
- Security considerations
- Integration requirements
[Preserve all technical details from the original PRD]
```
## Instructions
1. Read and analyze the attached specification file
2. Create the three files above with content derived from the PRD
3. Ensure all requirements are captured and properly prioritized
4. Make the PROMPT.md actionable for autonomous development
5. Structure @fix_plan.md with clear, implementable tasks
PROMPTEOF
# Append the PRD source content to the conversion prompt
local source_basename
source_basename=$(basename "$source_file")
if [[ -f "$source_file" ]]; then
echo "" >> "$CONVERSION_PROMPT_FILE"
echo "---" >> "$CONVERSION_PROMPT_FILE"
echo "" >> "$CONVERSION_PROMPT_FILE"
echo "## Source PRD File: $source_basename" >> "$CONVERSION_PROMPT_FILE"
echo "" >> "$CONVERSION_PROMPT_FILE"
cat "$source_file" >> "$CONVERSION_PROMPT_FILE"
else
log "ERROR" "Source file not found: $source_file"
rm -f "$CONVERSION_PROMPT_FILE"
exit 1
fi
# Build and execute Claude Code command
# Modern CLI: Use --output-format json and --allowedTools for structured output
# Fallback: Standard CLI invocation for older versions
# Note: stderr is written to separate file to avoid corrupting JSON output
local stderr_file="${CONVERSION_OUTPUT_FILE}.err"
if [[ "$use_modern_cli" == "true" ]]; then
# Modern CLI invocation with JSON output and controlled tool permissions
# --print: Required for piped input (prevents interactive session hang)
# --allowedTools: Permits file operations without user prompts
# --strict-mcp-config: Skip loading user MCP servers (faster startup)
if $CLAUDE_CODE_CMD --print --strict-mcp-config --output-format "$CLAUDE_OUTPUT_FORMAT" --allowedTools "${CLAUDE_ALLOWED_TOOLS[@]}" < "$CONVERSION_PROMPT_FILE" > "$CONVERSION_OUTPUT_FILE" 2> "$stderr_file"; then
cli_exit_code=0
else
cli_exit_code=$?
fi
else
# Standard CLI invocation (backward compatible)
# --print: Required for piped input (prevents interactive session hang)
if $CLAUDE_CODE_CMD --print < "$CONVERSION_PROMPT_FILE" > "$CONVERSION_OUTPUT_FILE" 2> "$stderr_file"; then
cli_exit_code=0
else
cli_exit_code=$?
fi
fi
# Log stderr if there was any (for debugging)
if [[ -s "$stderr_file" ]]; then
log "WARN" "CLI stderr output detected (see $stderr_file)"
fi
# Process the response
local output_format="text"
local json_parsed=false
if [[ -f "$CONVERSION_OUTPUT_FILE" ]]; then
output_format=$(detect_response_format "$CONVERSION_OUTPUT_FILE")
if [[ "$output_format" == "json" ]]; then
if parse_conversion_response "$CONVERSION_OUTPUT_FILE"; then
json_parsed=true
log "INFO" "Parsed JSON response from Claude CLI"
# Check for errors in JSON response
if [[ "$PARSED_HAS_ERRORS" == "true" && "$PARSED_COMPLETION_STATUS" == "failed" ]]; then
log "ERROR" "PRD conversion failed"
if [[ -n "$PARSED_ERROR_MESSAGE" ]]; then
log "ERROR" "Error: $PARSED_ERROR_MESSAGE"
fi
if [[ -n "$PARSED_ERROR_CODE" ]]; then
log "ERROR" "Error code: $PARSED_ERROR_CODE"
fi
rm -f "$CONVERSION_PROMPT_FILE" "$CONVERSION_OUTPUT_FILE" "$stderr_file"
exit 1
fi
# Log session ID if available (for potential continuation)
if [[ -n "$PARSED_SESSION_ID" && "$PARSED_SESSION_ID" != "null" ]]; then
log "INFO" "Session ID: $PARSED_SESSION_ID"
fi
# Log files changed from metadata
if [[ -n "$PARSED_FILES_CHANGED" && "$PARSED_FILES_CHANGED" != "0" ]]; then
log "INFO" "Files changed: $PARSED_FILES_CHANGED"
fi
fi
fi
fi
# Check CLI exit code
if [[ $cli_exit_code -ne 0 ]]; then
log "ERROR" "PRD conversion failed (exit code: $cli_exit_code)"
rm -f "$CONVERSION_PROMPT_FILE" "$CONVERSION_OUTPUT_FILE" "$stderr_file"
exit 1
fi
# Use PARSED_RESULT for success message if available
if [[ "$json_parsed" == "true" && -n "$PARSED_RESULT" && "$PARSED_RESULT" != "null" ]]; then
log "SUCCESS" "PRD conversion completed: $PARSED_RESULT"
else
log "SUCCESS" "PRD conversion completed"
fi
# Clean up temp files
rm -f "$CONVERSION_PROMPT_FILE" "$CONVERSION_OUTPUT_FILE" "$stderr_file"
# Verify files were created
# Use PARSED_FILES_CREATED from JSON if available, otherwise check filesystem
local missing_files=()
local created_files=()
local expected_files=(".ralph/PROMPT.md" ".ralph/@fix_plan.md" ".ralph/specs/requirements.md")
# If JSON provided files_created, use that to inform verification
if [[ "$json_parsed" == "true" && -n "$PARSED_FILES_CREATED" && "$PARSED_FILES_CREATED" != "[]" ]]; then
# Validate that PARSED_FILES_CREATED is a valid JSON array before iteration
local is_array
is_array=$(echo "$PARSED_FILES_CREATED" | jq -e 'type == "array"' 2>/dev/null)
if [[ "$is_array" == "true" ]]; then
# Parse JSON array and verify each file exists
local json_files
json_files=$(echo "$PARSED_FILES_CREATED" | jq -r '.[]' 2>/dev/null)
if [[ -n "$json_files" ]]; then
while IFS= read -r file; do
if [[ -n "$file" && -f "$file" ]]; then
created_files+=("$file")
elif [[ -n "$file" ]]; then
missing_files+=("$file")
fi
done <<< "$json_files"
fi
fi
fi
# Always verify expected files exist (filesystem is source of truth)
for file in "${expected_files[@]}"; do
if [[ -f "$file" ]]; then
# Add to created_files if not already there
if [[ ! " ${created_files[*]} " =~ " ${file} " ]]; then
created_files+=("$file")
fi
else
# Add to missing_files if not already there
if [[ ! " ${missing_files[*]} " =~ " ${file} " ]]; then
missing_files+=("$file")
fi
fi
done
# Report created files
if [[ ${#created_files[@]} -gt 0 ]]; then
log "INFO" "Created files: ${created_files[*]}"
fi
# Report and handle missing files
if [[ ${#missing_files[@]} -ne 0 ]]; then
log "WARN" "Some files were not created: ${missing_files[*]}"
# If JSON parsing provided missing files info, use that for better feedback
if [[ "$json_parsed" == "true" && -n "$PARSED_MISSING_FILES" && "$PARSED_MISSING_FILES" != "[]" ]]; then
log "INFO" "Missing files reported by Claude: $PARSED_MISSING_FILES"
fi
log "INFO" "You may need to create these files manually or run the conversion again"
fi
}
# Main function
main() {
local source_file="$1"
local project_name="$2"
# Validate arguments
if [[ -z "$source_file" ]]; then
log "ERROR" "Source file is required"
show_help
exit 1
fi
if [[ ! -f "$source_file" ]]; then
log "ERROR" "Source file does not exist: $source_file"
exit 1
fi
# Default project name from filename
if [[ -z "$project_name" ]]; then
project_name=$(basename "$source_file" | sed 's/\.[^.]*$//')
fi
log "INFO" "Converting PRD: $source_file"
log "INFO" "Project name: $project_name"
check_dependencies
# Create project directory
log "INFO" "Creating Ralph project: $project_name"
ralph-setup "$project_name"
cd "$project_name"
# Copy source file to project (uses basename since we cd'd into project)
local source_basename
source_basename=$(basename "$source_file")
if [[ "$source_file" == /* ]]; then
cp "$source_file" "$source_basename"
else
cp "../$source_file" "$source_basename"
fi
# Run conversion using local copy (basename, not original path)
convert_prd "$source_basename" "$project_name"
log "SUCCESS" "🎉 PRD imported successfully!"
echo ""
echo "Next steps:"
echo " 1. Review and edit the generated files:"
echo " - .ralph/PROMPT.md (Ralph instructions)"
echo " - .ralph/@fix_plan.md (task priorities)"
echo " - .ralph/specs/requirements.md (technical specs)"
echo " 2. Start autonomous development:"
echo " ralph --monitor # standalone Ralph"
echo " bmalph run # bmalph-managed projects"
echo ""
echo "Project created in: $(pwd)"
}
# Handle command line arguments
case "${1:-}" in
-h|--help|"")
show_help
exit 0
;;
*)
main "$@"
;;
esac

3044
.ralph/ralph_loop.sh Executable file

File diff suppressed because it is too large Load Diff

129
.ralph/ralph_monitor.sh Executable file
View File

@@ -0,0 +1,129 @@
#!/bin/bash
# Ralph Status Monitor - Live terminal dashboard for the Ralph loop
#
# DEPRECATED: Use `bmalph run` instead, which starts Ralph and shows the
# supported live dashboard.
# This script is kept for backward compatibility in tmux sessions.
set -e
STATUS_FILE=".ralph/status.json"
LOG_FILE=".ralph/logs/ralph.log"
REFRESH_INTERVAL=2
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m'
# Clear screen and hide cursor
clear_screen() {
clear
printf '\033[?25l' # Hide cursor
}
# Show cursor on exit
show_cursor() {
printf '\033[?25h' # Show cursor
}
# Cleanup function
cleanup() {
show_cursor
echo
echo "Monitor stopped."
exit 0
}
# Set up signal handlers
trap cleanup SIGINT SIGTERM EXIT
# Main display function
display_status() {
clear_screen
# Header
echo -e "${WHITE}╔════════════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${WHITE}║ 🤖 RALPH MONITOR ║${NC}"
echo -e "${WHITE}║ Live Status Dashboard ║${NC}"
echo -e "${WHITE}╚════════════════════════════════════════════════════════════════════════╝${NC}"
echo
# Status section
if [[ -f "$STATUS_FILE" ]]; then
# Parse JSON status
local status_data=$(cat "$STATUS_FILE")
local loop_count=$(echo "$status_data" | jq -r '.loop_count // "0"' 2>/dev/null || echo "0")
local calls_made=$(echo "$status_data" | jq -r '.calls_made_this_hour // "0"' 2>/dev/null || echo "0")
local max_calls=$(echo "$status_data" | jq -r '.max_calls_per_hour // "100"' 2>/dev/null || echo "100")
local status=$(echo "$status_data" | jq -r '.status // "unknown"' 2>/dev/null || echo "unknown")
echo -e "${CYAN}┌─ Current Status ────────────────────────────────────────────────────────┐${NC}"
echo -e "${CYAN}${NC} Loop Count: ${WHITE}#$loop_count${NC}"
echo -e "${CYAN}${NC} Status: ${GREEN}$status${NC}"
echo -e "${CYAN}${NC} API Calls: $calls_made/$max_calls"
echo -e "${CYAN}└─────────────────────────────────────────────────────────────────────────┘${NC}"
echo
else
echo -e "${RED}┌─ Status ────────────────────────────────────────────────────────────────┐${NC}"
echo -e "${RED}${NC} Status file not found. Ralph may not be running."
echo -e "${RED}└─────────────────────────────────────────────────────────────────────────┘${NC}"
echo
fi
# Driver Progress section
if [[ -f ".ralph/progress.json" ]]; then
local progress_data=$(cat ".ralph/progress.json" 2>/dev/null)
local progress_status=$(echo "$progress_data" | jq -r '.status // "idle"' 2>/dev/null || echo "idle")
if [[ "$progress_status" == "executing" ]]; then
local indicator=$(echo "$progress_data" | jq -r '.indicator // "⠋"' 2>/dev/null || echo "⠋")
local elapsed=$(echo "$progress_data" | jq -r '.elapsed_seconds // "0"' 2>/dev/null || echo "0")
local last_output=$(echo "$progress_data" | jq -r '.last_output // ""' 2>/dev/null || echo "")
echo -e "${YELLOW}┌─ Driver Progress ───────────────────────────────────────────────────────┐${NC}"
echo -e "${YELLOW}${NC} Status: ${indicator} Working (${elapsed}s elapsed)"
if [[ -n "$last_output" && "$last_output" != "" ]]; then
# Truncate long output for display
local display_output=$(echo "$last_output" | head -c 60)
echo -e "${YELLOW}${NC} Output: ${display_output}..."
fi
echo -e "${YELLOW}└─────────────────────────────────────────────────────────────────────────┘${NC}"
echo
fi
fi
# Recent logs
echo -e "${BLUE}┌─ Recent Activity ───────────────────────────────────────────────────────┐${NC}"
if [[ -f "$LOG_FILE" ]]; then
tail -n 8 "$LOG_FILE" | while IFS= read -r line; do
echo -e "${BLUE}${NC} $line"
done
else
echo -e "${BLUE}${NC} No log file found"
fi
echo -e "${BLUE}└─────────────────────────────────────────────────────────────────────────┘${NC}"
# Footer
echo
echo -e "${YELLOW}Controls: Ctrl+C to exit | Refreshes every ${REFRESH_INTERVAL}s | $(date '+%H:%M:%S')${NC}"
}
# Main monitor loop
main() {
echo "Starting Ralph Monitor..."
sleep 2
while true; do
display_status
sleep "$REFRESH_INTERVAL"
done
}
main