refactor(ux): consolidate BMAD skills, update design system, and clean up Prisma generated client
This commit is contained in:
163
.ralph/.ralphrc
Normal file
163
.ralph/.ralphrc
Normal 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
158
.ralph/@AGENT.md
Normal 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
319
.ralph/PROMPT.md
Normal 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
460
.ralph/RALPH-REFERENCE.md
Normal 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
60
.ralph/REVIEW_PROMPT.md
Normal 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 `[]`.
|
||||
422
.ralph/drivers/DRIVER_INTERFACE.md
Normal file
422
.ralph/drivers/DRIVER_INTERFACE.md
Normal 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
187
.ralph/drivers/claude-code.sh
Executable 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
101
.ralph/drivers/codex.sh
Executable 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
105
.ralph/drivers/copilot.sh
Executable 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 '.'
|
||||
}
|
||||
13
.ralph/drivers/cursor-agent-wrapper.sh
Executable file
13
.ralph/drivers/cursor-agent-wrapper.sh
Executable 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
283
.ralph/drivers/cursor.sh
Executable 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
147
.ralph/drivers/opencode.sh
Executable 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
|
||||
}
|
||||
490
.ralph/lib/circuit_breaker.sh
Normal file
490
.ralph/lib/circuit_breaker.sh
Normal 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
123
.ralph/lib/date_utils.sh
Normal 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
820
.ralph/lib/enable_core.sh
Normal 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
|
||||
1562
.ralph/lib/response_analyzer.sh
Normal file
1562
.ralph/lib/response_analyzer.sh
Normal file
File diff suppressed because it is too large
Load Diff
611
.ralph/lib/task_sources.sh
Normal file
611
.ralph/lib/task_sources.sh
Normal 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
145
.ralph/lib/timeout_utils.sh
Normal 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
556
.ralph/lib/wizard_utils.sh
Normal 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
664
.ralph/ralph_import.sh
Executable 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
3044
.ralph/ralph_loop.sh
Executable file
File diff suppressed because it is too large
Load Diff
129
.ralph/ralph_monitor.sh
Executable file
129
.ralph/ralph_monitor.sh
Executable 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
|
||||
Reference in New Issue
Block a user