mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
feat: add RALPH_STATUS block parsing and circuit breaker pattern
Phase 1 Ralph enhancement implementation: - Parse RALPH_STATUS blocks from Claude output (status, tasks, files, tests) - Circuit breaker state machine (CLOSED → HALF_OPEN → OPEN) for stuck detection - Dual-condition exit gate (completion indicators >= 2 AND EXIT_SIGNAL) - Frontend: circuit breaker badge (yellow warning, red stuck), status block display - API: circuit breaker reset endpoint, ralph status endpoint - Notifications when circuit breaker opens or exit gate is met Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
# Ralph Loop Improvements Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This plan details improvements to Claudeman's Ralph Loop system based on best practices from the Ralph Claude Code repository (https://github.com/frankbria/ralph-claude-code).
|
||||
|
||||
## Key Concepts to Implement
|
||||
|
||||
### RALPH_STATUS Block Format
|
||||
|
||||
Claude outputs this structured block at the end of every response for better tracking:
|
||||
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: IN_PROGRESS | COMPLETE | BLOCKED
|
||||
TASKS_COMPLETED_THIS_LOOP: <number>
|
||||
FILES_MODIFIED: <number>
|
||||
TESTS_STATUS: PASSING | FAILING | NOT_RUN
|
||||
WORK_TYPE: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING
|
||||
EXIT_SIGNAL: false | true
|
||||
RECOMMENDATION: <one line summary of what to do next>
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
|
||||
### Dual-Condition Exit Gate
|
||||
|
||||
Exit requires BOTH conditions:
|
||||
1. `completion_indicators >= 2` (heuristic detection from natural language patterns)
|
||||
2. Claude's explicit `EXIT_SIGNAL: true` in the RALPH_STATUS block
|
||||
|
||||
### Circuit Breaker Pattern
|
||||
|
||||
Three states: CLOSED → HALF_OPEN → OPEN
|
||||
|
||||
| From State | Condition | To State |
|
||||
|------------|-----------|----------|
|
||||
| CLOSED | consecutive_no_progress >= 2 | HALF_OPEN |
|
||||
| CLOSED | consecutive_no_progress >= 3 | OPEN |
|
||||
| CLOSED | consecutive_same_error >= 5 | OPEN |
|
||||
| HALF_OPEN | progress detected | CLOSED |
|
||||
| HALF_OPEN | consecutive_no_progress >= 3 | OPEN |
|
||||
| OPEN | Manual reset | CLOSED |
|
||||
|
||||
### @fix_plan.md Structure
|
||||
|
||||
```markdown
|
||||
# Fix Plan
|
||||
|
||||
## High Priority (P0)
|
||||
- [ ] Critical: Fix authentication bug
|
||||
- [ ] Blocker: Database connection timeout
|
||||
|
||||
## Standard (P1)
|
||||
- [ ] Feature: Add user profile page
|
||||
|
||||
## Nice to Have (P2)
|
||||
- [ ] Improvement: Add dark mode
|
||||
|
||||
## Completed
|
||||
- [x] Setup: Initialize project structure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Quick Wins (1-2 days)
|
||||
|
||||
### 1.1 RALPH_STATUS Block Parsing
|
||||
|
||||
**What**: Add parsing support for the structured RALPH_STATUS block format in RalphTracker.
|
||||
|
||||
**Implementation**:
|
||||
- Add regex pattern to detect `---RALPH_STATUS---` blocks
|
||||
- Parse fields: STATUS, TASKS_COMPLETED_THIS_LOOP, FILES_MODIFIED, TESTS_STATUS, WORK_TYPE, EXIT_SIGNAL, RECOMMENDATION
|
||||
- Store in extended `RalphTrackerState` type
|
||||
- Emit new events: `ralphStatusUpdate`
|
||||
|
||||
**Files**: `ralph-tracker.ts`, `types.ts`
|
||||
|
||||
### 1.2 Enhanced Status Display in UI
|
||||
|
||||
**What**: Display RALPH_STATUS fields in the Ralph State Panel.
|
||||
|
||||
**Implementation**:
|
||||
- Add UI elements: WORK_TYPE indicator, TESTS_STATUS badge, FILES_MODIFIED count
|
||||
- Show RECOMMENDATION text in expanded view
|
||||
- Color-code status (IN_PROGRESS=blue, COMPLETE=green, BLOCKED=red)
|
||||
|
||||
**Files**: `app.js`, `styles.css`, `index.html`
|
||||
|
||||
### 1.3 Prompt Template Improvements
|
||||
|
||||
**What**: Add specification-by-example exit scenarios to prompts.
|
||||
|
||||
**Implementation**:
|
||||
- Add "Exit Scenarios" section to case-template.md
|
||||
- Document when to continue vs. when to output completion
|
||||
- Include testing limits guidance (max 20% effort on tests)
|
||||
- Add RALPH_STATUS block instructions
|
||||
|
||||
**Files**: `case-template.md`
|
||||
|
||||
### 1.4 Better Wizard Validation
|
||||
|
||||
**What**: Add client-side validation and helpful warnings.
|
||||
|
||||
**Implementation**:
|
||||
- Warn if task description < 50 chars
|
||||
- Warn if no success criteria mentioned
|
||||
- Suggest adding test requirements if none detected
|
||||
- Validate completion phrase is uppercase alphanumeric
|
||||
|
||||
**Files**: `app.js`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Core Improvements (3-5 days)
|
||||
|
||||
### 2.1 Circuit Breaker Pattern
|
||||
|
||||
**What**: Implement three-state circuit breaker to detect stuck loops.
|
||||
|
||||
**Implementation**:
|
||||
- Create `CircuitBreaker` class with CLOSED, HALF_OPEN, OPEN states
|
||||
- Track: files_modified, tasks_completed, error_patterns per iteration
|
||||
- Triggers: N consecutive no-progress, same error M times, tests failing K iterations
|
||||
- Emit events: `circuitBreakerStateChange`
|
||||
|
||||
**Files**: New `circuit-breaker.ts`, integrate into `ralph-tracker.ts`
|
||||
|
||||
### 2.2 Circuit Breaker UI
|
||||
|
||||
**What**: Visual indicator in Ralph panel.
|
||||
|
||||
**Implementation**:
|
||||
- Badge: green (CLOSED), yellow (HALF_OPEN), red (OPEN)
|
||||
- Warning before tripping
|
||||
- Notification when circuit opens
|
||||
- Manual reset button
|
||||
|
||||
**Files**: `app.js`, `styles.css`, `index.html`
|
||||
|
||||
### 2.3 @fix_plan.md Integration
|
||||
|
||||
**What**: Generate and track structured task plan file.
|
||||
|
||||
**Implementation**:
|
||||
- Generate `@fix_plan.md` in working directory when loop starts
|
||||
- Watch file for changes and sync with RalphTracker todos
|
||||
- Parse priority levels (P0, P1, P2)
|
||||
- Show priority in UI
|
||||
|
||||
**Files**: New `fix-plan.ts`, `ralph-tracker.ts`, `server.ts`
|
||||
|
||||
### 2.4 Wizard Plan Generation Step
|
||||
|
||||
**What**: Add third wizard step for AI-assisted plan generation.
|
||||
|
||||
**Implementation**:
|
||||
- Step 2: "Plan Generation" between Task Setup and Launch
|
||||
- Use Claude to break down task into fix plan items
|
||||
- Allow edit/reorder before launch
|
||||
- Generate @fix_plan.md with selected items
|
||||
|
||||
**Files**: `app.js`, `index.html`, `server.ts`
|
||||
|
||||
### 2.5 Smart Respawn Integration
|
||||
|
||||
**What**: Use RALPH_STATUS for respawn decisions.
|
||||
|
||||
**Implementation**:
|
||||
- Use EXIT_SIGNAL field for respawn decisions
|
||||
- If STATUS=BLOCKED, trigger circuit breaker instead of respawn
|
||||
- Pass RECOMMENDATION to respawn update prompt
|
||||
|
||||
**Files**: `respawn-controller.ts`, `ralph-tracker.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Advanced Features (5+ days)
|
||||
|
||||
### 3.1 Template Library
|
||||
- Bug Fix, Feature, Refactoring, Test Coverage, Documentation templates
|
||||
- Template selector in wizard
|
||||
- Custom templates in `~/.claudeman/templates/`
|
||||
|
||||
### 3.2 Tool Permissions
|
||||
- Configure allowed Claude tools per loop
|
||||
- Generate hook configuration
|
||||
- Store in session config
|
||||
|
||||
### 3.3 Per-Iteration Timeout
|
||||
- Max time per iteration (5-60 min)
|
||||
- Auto-continue on timeout
|
||||
- Log timeout events
|
||||
|
||||
### 3.4 Rate Limiting
|
||||
- Max tokens per iteration
|
||||
- Max API calls per minute
|
||||
- Cooldown between iterations
|
||||
|
||||
### 3.5 Metrics Dashboard
|
||||
- Time-series charts (files modified, tasks completed, tokens)
|
||||
- Aggregate statistics
|
||||
- Export to JSON/CSV
|
||||
|
||||
---
|
||||
|
||||
## Priority Matrix
|
||||
|
||||
| Item | Effort | Impact | Priority |
|
||||
|------|--------|--------|----------|
|
||||
| 1.1 RALPH_STATUS Parsing | Low | High | P0 |
|
||||
| 1.2 Status Display UI | Low | Medium | P0 |
|
||||
| 1.3 Prompt Templates | Low | High | P0 |
|
||||
| 1.4 Wizard Validation | Low | Medium | P1 |
|
||||
| 2.1 Circuit Breaker | Medium | High | P1 |
|
||||
| 2.2 Circuit Breaker UI | Medium | Medium | P1 |
|
||||
| 2.3 Fix Plan Integration | Medium | High | P1 |
|
||||
| 2.4 Plan Generation Step | Medium | Medium | P2 |
|
||||
| 2.5 Respawn Integration | Medium | High | P1 |
|
||||
| 3.1 Template Selection | High | Medium | P2 |
|
||||
| 3.2 Tool Permissions | High | Medium | P3 |
|
||||
| 3.3 Per-Iteration Timeout | High | Medium | P2 |
|
||||
| 3.4 Rate Limiting | High | Low | P3 |
|
||||
| 3.5 Metrics Dashboard | High | Medium | P3 |
|
||||
|
||||
---
|
||||
|
||||
## Reference: Ralph Claude Code Best Practices
|
||||
|
||||
### Testing Guidelines
|
||||
- LIMIT testing to ~20% of total effort per loop
|
||||
- PRIORITIZE: Implementation > Documentation > Tests
|
||||
- Only write tests for NEW functionality
|
||||
- Do NOT refactor existing tests unless broken
|
||||
|
||||
### 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
|
||||
- Do NOT add features not in specifications
|
||||
- Do NOT forget the status block
|
||||
|
||||
### Exit Scenarios (Specification by Example)
|
||||
|
||||
1. **Successful Completion**: All tasks done → EXIT_SIGNAL=true
|
||||
2. **Test-Only Loop**: No implementation, only testing → continue but warn
|
||||
3. **Stuck on Error**: Same error 5 times → circuit breaker opens
|
||||
4. **No Work Remaining**: All specs done → EXIT_SIGNAL=true
|
||||
5. **Making Progress**: Normal flow → continue
|
||||
6. **Blocked**: Needs human intervention → STATUS=BLOCKED
|
||||
@@ -0,0 +1,434 @@
|
||||
# Ralph Tracker Phase 1 Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This plan details how to enhance the existing RalphTracker with RALPH_STATUS block parsing, circuit breaker pattern, and dual-condition exit gate.
|
||||
|
||||
---
|
||||
|
||||
## 1. Current State Analysis
|
||||
|
||||
### What RalphTracker Already Does Well
|
||||
|
||||
- **Todo Detection**: Supports 5 formats (checkboxes, indicators, status in parentheses, native TodoWrite, checkmark-based)
|
||||
- **Completion Phrases**: Detects `<promise>PHRASE</promise>` with occurrence-based logic (1st = store, 2nd = complete)
|
||||
- **Loop State Tracking**: Tracks active/inactive, iteration counts, max iterations, elapsed hours, cycle counts
|
||||
- **Auto-Enable**: Disabled by default, auto-enables when Ralph patterns detected
|
||||
- **Event System**: Emits `loopUpdate`, `todoUpdate`, `completionDetected`, `enabled` events
|
||||
- **SSE Integration**: Events forwarded via `session:ralphLoopUpdate`, `session:ralphTodoUpdate`, `session:ralphCompletionDetected`
|
||||
- **Debouncing**: EVENT_DEBOUNCE_MS (50ms) for rapid updates to prevent UI jitter
|
||||
- **Cleanup**: MAX_TODO_ITEMS (50), TODO_EXPIRY_MS (1 hour), throttled cleanup
|
||||
|
||||
### Current Limitations
|
||||
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| RALPH_STATUS block parsing | Missing |
|
||||
| Circuit breaker pattern | Missing |
|
||||
| Priority-based todos (P0/P1/P2) | Missing |
|
||||
| Dual-condition exit gate | Missing |
|
||||
| Files modified tracking | Missing |
|
||||
| Tests status tracking | Missing |
|
||||
| Work type classification | Missing |
|
||||
|
||||
---
|
||||
|
||||
## 2. New Type Definitions (types.ts)
|
||||
|
||||
```typescript
|
||||
// ========== RALPH_STATUS Block Types ==========
|
||||
|
||||
export type RalphStatusValue = 'IN_PROGRESS' | 'COMPLETE' | 'BLOCKED';
|
||||
export type RalphTestsStatus = 'PASSING' | 'FAILING' | 'NOT_RUN';
|
||||
export type RalphWorkType = 'IMPLEMENTATION' | 'TESTING' | 'DOCUMENTATION' | 'REFACTORING';
|
||||
|
||||
/**
|
||||
* Parsed RALPH_STATUS block from Claude output.
|
||||
*/
|
||||
export interface RalphStatusBlock {
|
||||
status: RalphStatusValue;
|
||||
tasksCompletedThisLoop: number;
|
||||
filesModified: number;
|
||||
testsStatus: RalphTestsStatus;
|
||||
workType: RalphWorkType;
|
||||
exitSignal: boolean;
|
||||
recommendation: string;
|
||||
parsedAt: number;
|
||||
}
|
||||
|
||||
// ========== Circuit Breaker Types ==========
|
||||
|
||||
export type CircuitBreakerState = 'CLOSED' | 'HALF_OPEN' | 'OPEN';
|
||||
|
||||
export type CircuitBreakerReason =
|
||||
| 'normal_operation'
|
||||
| 'no_progress_warning'
|
||||
| 'no_progress_open'
|
||||
| 'same_error_repeated'
|
||||
| 'tests_failing_too_long'
|
||||
| 'progress_detected'
|
||||
| 'manual_reset';
|
||||
|
||||
export interface CircuitBreakerStatus {
|
||||
state: CircuitBreakerState;
|
||||
consecutiveNoProgress: number;
|
||||
consecutiveSameError: number;
|
||||
consecutiveTestsFailure: number;
|
||||
lastProgressIteration: number;
|
||||
reason: string;
|
||||
reasonCode: CircuitBreakerReason;
|
||||
lastTransitionAt: number;
|
||||
lastErrorMessage: string | null;
|
||||
}
|
||||
|
||||
// ========== Priority Todo Types ==========
|
||||
|
||||
export type RalphTodoPriority = 'P0' | 'P1' | 'P2' | null;
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
export function createInitialCircuitBreakerStatus(): CircuitBreakerStatus {
|
||||
return {
|
||||
state: 'CLOSED',
|
||||
consecutiveNoProgress: 0,
|
||||
consecutiveSameError: 0,
|
||||
consecutiveTestsFailure: 0,
|
||||
lastProgressIteration: 0,
|
||||
reason: 'Initial state',
|
||||
reasonCode: 'normal_operation',
|
||||
lastTransitionAt: Date.now(),
|
||||
lastErrorMessage: null,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. New Regex Patterns (ralph-tracker.ts)
|
||||
|
||||
```typescript
|
||||
// ---------- RALPH_STATUS Block Patterns ----------
|
||||
|
||||
const RALPH_STATUS_START_PATTERN = /^---RALPH_STATUS---\s*$/;
|
||||
const RALPH_STATUS_END_PATTERN = /^---END_RALPH_STATUS---\s*$/;
|
||||
const RALPH_STATUS_FIELD_PATTERN = /^STATUS:\s*(IN_PROGRESS|COMPLETE|BLOCKED)\s*$/i;
|
||||
const RALPH_TASKS_COMPLETED_PATTERN = /^TASKS_COMPLETED_THIS_LOOP:\s*(\d+)\s*$/i;
|
||||
const RALPH_FILES_MODIFIED_PATTERN = /^FILES_MODIFIED:\s*(\d+)\s*$/i;
|
||||
const RALPH_TESTS_STATUS_PATTERN = /^TESTS_STATUS:\s*(PASSING|FAILING|NOT_RUN)\s*$/i;
|
||||
const RALPH_WORK_TYPE_PATTERN = /^WORK_TYPE:\s*(IMPLEMENTATION|TESTING|DOCUMENTATION|REFACTORING)\s*$/i;
|
||||
const RALPH_EXIT_SIGNAL_PATTERN = /^EXIT_SIGNAL:\s*(true|false)\s*$/i;
|
||||
const RALPH_RECOMMENDATION_PATTERN = /^RECOMMENDATION:\s*(.+)$/i;
|
||||
|
||||
// ---------- Completion Indicator Patterns ----------
|
||||
|
||||
const COMPLETION_INDICATOR_PATTERNS = [
|
||||
/all\s+(?:tasks?|items?|work)\s+(?:are\s+)?(?:completed?|done|finished)/i,
|
||||
/(?:completed?|finished)\s+all\s+(?:tasks?|items?|work)/i,
|
||||
/nothing\s+(?:left|remaining)\s+to\s+do/i,
|
||||
/no\s+more\s+(?:tasks?|items?|work)/i,
|
||||
/everything\s+(?:is\s+)?(?:completed?|done)/i,
|
||||
];
|
||||
|
||||
// ---------- Priority Pattern ----------
|
||||
|
||||
const TODO_PRIORITY_PATTERN = /^\s*(?:\[.\])?\s*(?:Critical:|Blocker:|Feature:|Improvement:)?\s*\(?(P[012])\)?:?\s*/i;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. New State Properties (ralph-tracker.ts)
|
||||
|
||||
```typescript
|
||||
// Add to RalphTracker class
|
||||
|
||||
// Circuit breaker state tracking
|
||||
private _circuitBreaker: CircuitBreakerStatus;
|
||||
|
||||
// RALPH_STATUS block parsing state
|
||||
private _statusBlockBuffer: string[] = [];
|
||||
private _inStatusBlock: boolean = false;
|
||||
private _lastStatusBlock: RalphStatusBlock | null = null;
|
||||
|
||||
// Dual-condition exit tracking
|
||||
private _completionIndicators: number = 0;
|
||||
private _exitGateMet: boolean = false;
|
||||
|
||||
// Cumulative tracking
|
||||
private _totalFilesModified: number = 0;
|
||||
private _totalTasksCompleted: number = 0;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. New Methods to Implement
|
||||
|
||||
### 5.1 RALPH_STATUS Block Parsing
|
||||
|
||||
```typescript
|
||||
private processStatusBlockLine(line: string): void {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (RALPH_STATUS_START_PATTERN.test(trimmed)) {
|
||||
this._inStatusBlock = true;
|
||||
this._statusBlockBuffer = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._inStatusBlock && RALPH_STATUS_END_PATTERN.test(trimmed)) {
|
||||
this._inStatusBlock = false;
|
||||
this.parseStatusBlock(this._statusBlockBuffer);
|
||||
this._statusBlockBuffer = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._inStatusBlock) {
|
||||
this._statusBlockBuffer.push(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
private parseStatusBlock(lines: string[]): void {
|
||||
const block: Partial<RalphStatusBlock> = { parsedAt: Date.now() };
|
||||
|
||||
for (const line of lines) {
|
||||
// Parse each field...
|
||||
}
|
||||
|
||||
if (block.status !== undefined) {
|
||||
this._lastStatusBlock = fullBlock;
|
||||
this.handleStatusBlock(fullBlock);
|
||||
}
|
||||
}
|
||||
|
||||
private handleStatusBlock(block: RalphStatusBlock): void {
|
||||
this._totalFilesModified += block.filesModified;
|
||||
this._totalTasksCompleted += block.tasksCompletedThisLoop;
|
||||
|
||||
const hasProgress = block.filesModified > 0 || block.tasksCompletedThisLoop > 0;
|
||||
this.updateCircuitBreaker(hasProgress, block.testsStatus, block.status);
|
||||
|
||||
if (block.status === 'COMPLETE') {
|
||||
this._completionIndicators++;
|
||||
}
|
||||
|
||||
if (block.exitSignal && this._completionIndicators >= 2) {
|
||||
this._exitGateMet = true;
|
||||
this.emit('exitGateMet', { completionIndicators: this._completionIndicators, exitSignal: true });
|
||||
}
|
||||
|
||||
this.emit('statusBlockDetected', block);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Circuit Breaker Logic
|
||||
|
||||
```typescript
|
||||
private updateCircuitBreaker(
|
||||
hasProgress: boolean,
|
||||
testsStatus: RalphTestsStatus,
|
||||
status: RalphStatusValue
|
||||
): void {
|
||||
const prevState = this._circuitBreaker.state;
|
||||
|
||||
if (hasProgress) {
|
||||
this._circuitBreaker.consecutiveNoProgress = 0;
|
||||
this._circuitBreaker.lastProgressIteration = this._loopState.cycleCount;
|
||||
|
||||
if (this._circuitBreaker.state === 'HALF_OPEN') {
|
||||
this._circuitBreaker.state = 'CLOSED';
|
||||
this._circuitBreaker.reasonCode = 'progress_detected';
|
||||
}
|
||||
} else {
|
||||
this._circuitBreaker.consecutiveNoProgress++;
|
||||
|
||||
if (this._circuitBreaker.state === 'CLOSED') {
|
||||
if (this._circuitBreaker.consecutiveNoProgress >= 3) {
|
||||
this._circuitBreaker.state = 'OPEN';
|
||||
this._circuitBreaker.reasonCode = 'no_progress_open';
|
||||
} else if (this._circuitBreaker.consecutiveNoProgress >= 2) {
|
||||
this._circuitBreaker.state = 'HALF_OPEN';
|
||||
this._circuitBreaker.reasonCode = 'no_progress_warning';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prevState !== this._circuitBreaker.state) {
|
||||
this._circuitBreaker.lastTransitionAt = Date.now();
|
||||
this.emit('circuitBreakerUpdate', { ...this._circuitBreaker });
|
||||
}
|
||||
}
|
||||
|
||||
resetCircuitBreaker(): void {
|
||||
this._circuitBreaker = createInitialCircuitBreakerStatus();
|
||||
this._circuitBreaker.reasonCode = 'manual_reset';
|
||||
this.emit('circuitBreakerUpdate', { ...this._circuitBreaker });
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Update processLine Method
|
||||
|
||||
```typescript
|
||||
private processLine(line: string): void {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
// NEW: Check for RALPH_STATUS block
|
||||
this.processStatusBlockLine(trimmed);
|
||||
|
||||
// NEW: Check for completion indicators
|
||||
this.detectCompletionIndicators(trimmed);
|
||||
|
||||
// EXISTING: Rest of the detection methods...
|
||||
this.detectCompletionPhrase(trimmed);
|
||||
this.detectAllTasksComplete(trimmed);
|
||||
this.detectTaskCompletion(trimmed);
|
||||
this.detectLoopStatus(trimmed);
|
||||
this.detectTodoItems(trimmed);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. New Events to Add
|
||||
|
||||
```typescript
|
||||
export interface RalphTrackerEvents {
|
||||
// Existing events
|
||||
loopUpdate: (state: RalphTrackerState) => void;
|
||||
todoUpdate: (todos: RalphTodoItem[]) => void;
|
||||
completionDetected: (phrase: string) => void;
|
||||
enabled: () => void;
|
||||
|
||||
// New events
|
||||
statusBlockDetected: (block: RalphStatusBlock) => void;
|
||||
circuitBreakerUpdate: (status: CircuitBreakerStatus) => void;
|
||||
exitGateMet: (data: { completionIndicators: number; exitSignal: boolean }) => void;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Server Integration (server.ts)
|
||||
|
||||
```typescript
|
||||
// Add new SSE event handlers in setupSessionListeners()
|
||||
|
||||
session.on('ralphStatusBlockDetected', (block: RalphStatusBlock) => {
|
||||
this.broadcast('session:ralphStatusUpdate', { sessionId: session.id, block });
|
||||
});
|
||||
|
||||
session.on('ralphCircuitBreakerUpdate', (status: CircuitBreakerStatus) => {
|
||||
this.broadcast('session:circuitBreakerUpdate', { sessionId: session.id, status });
|
||||
});
|
||||
|
||||
session.on('ralphExitGateMet', (data) => {
|
||||
this.broadcast('session:exitGateMet', { sessionId: session.id, ...data });
|
||||
});
|
||||
|
||||
// Add API endpoint for circuit breaker reset
|
||||
this.app.post('/api/sessions/:id/ralph-circuit-breaker/reset', async (req) => {
|
||||
const session = this.sessions.get(req.params.id);
|
||||
if (!session) return { success: false, error: 'Session not found' };
|
||||
|
||||
session.ralphTracker?.resetCircuitBreaker();
|
||||
return { success: true };
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Frontend Changes (app.js)
|
||||
|
||||
### New SSE Event Listeners
|
||||
|
||||
```javascript
|
||||
this.eventSource.addEventListener('session:ralphStatusUpdate', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
this.updateRalphStatusBlock(data.sessionId, data.block);
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('session:circuitBreakerUpdate', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
this.updateCircuitBreaker(data.sessionId, data.status);
|
||||
});
|
||||
```
|
||||
|
||||
### New Rendering Methods
|
||||
|
||||
```javascript
|
||||
updateRalphStatusBlock(sessionId, block) {
|
||||
// Store and render status block
|
||||
}
|
||||
|
||||
renderRalphStatusBlock(block) {
|
||||
// Render STATUS, WORK_TYPE, TESTS_STATUS, RECOMMENDATION
|
||||
}
|
||||
|
||||
updateCircuitBreaker(sessionId, status) {
|
||||
// Store and render circuit breaker state
|
||||
}
|
||||
|
||||
renderCircuitBreaker(status) {
|
||||
// Render badge: green (CLOSED), yellow (HALF_OPEN), red (OPEN)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation Order
|
||||
|
||||
| Step | Task | Time |
|
||||
|------|------|------|
|
||||
| 1 | Add type definitions to `types.ts` | 30 min |
|
||||
| 2 | Add regex patterns to `ralph-tracker.ts` | 30 min |
|
||||
| 3 | Add state properties to RalphTracker class | 15 min |
|
||||
| 4 | Implement RALPH_STATUS parsing methods | 1.5 hr |
|
||||
| 5 | Implement circuit breaker logic | 1 hr |
|
||||
| 6 | Implement completion indicators | 30 min |
|
||||
| 7 | Update events interface | 15 min |
|
||||
| 8 | Add server SSE handlers and API endpoint | 45 min |
|
||||
| 9 | Add frontend event listeners and rendering | 1 hr |
|
||||
| 10 | Add CSS styles | 30 min |
|
||||
| 11 | Update HTML structure | 15 min |
|
||||
| 12 | Write unit tests | 1.5 hr |
|
||||
|
||||
**Total: ~8 hours**
|
||||
|
||||
---
|
||||
|
||||
## 10. Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/types.ts` | Add RalphStatusBlock, CircuitBreakerStatus, helper functions |
|
||||
| `src/ralph-tracker.ts` | Add patterns, state, parsing methods, circuit breaker |
|
||||
| `src/web/server.ts` | Add SSE handlers, circuit breaker reset endpoint |
|
||||
| `src/web/public/app.js` | Add event listeners, rendering methods |
|
||||
| `src/web/public/styles.css` | Add status block and circuit breaker styles |
|
||||
| `src/web/public/index.html` | Add UI elements to Ralph panel |
|
||||
| `test/ralph-tracker.test.ts` | Add tests for new functionality |
|
||||
|
||||
---
|
||||
|
||||
## 11. Test Cases to Add
|
||||
|
||||
1. **RALPH_STATUS Parsing**
|
||||
- Parse valid status block with all fields
|
||||
- Parse block with missing optional fields
|
||||
- Ignore malformed blocks
|
||||
- Handle multiple blocks in sequence
|
||||
|
||||
2. **Circuit Breaker State Transitions**
|
||||
- CLOSED → HALF_OPEN on 2 no-progress
|
||||
- HALF_OPEN → OPEN on 3 no-progress
|
||||
- HALF_OPEN → CLOSED on progress
|
||||
- Manual reset from OPEN
|
||||
|
||||
3. **Dual-Condition Exit Gate**
|
||||
- Exit when indicators >= 2 AND exitSignal = true
|
||||
- No exit when indicators >= 2 but exitSignal = false
|
||||
- No exit when exitSignal = true but indicators < 2
|
||||
|
||||
4. **Integration Tests**
|
||||
- SSE events broadcast correctly
|
||||
- UI updates on status block detection
|
||||
- Circuit breaker badge updates
|
||||
@@ -18,7 +18,13 @@ import {
|
||||
RalphTrackerState,
|
||||
RalphTodoItem,
|
||||
RalphTodoStatus,
|
||||
RalphStatusBlock,
|
||||
RalphStatusValue,
|
||||
RalphTestsStatus,
|
||||
RalphWorkType,
|
||||
CircuitBreakerStatus,
|
||||
createInitialRalphTrackerState,
|
||||
createInitialCircuitBreakerStatus,
|
||||
} from './types.js';
|
||||
|
||||
// ========== Configuration Constants ==========
|
||||
@@ -220,6 +226,78 @@ const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
|
||||
/** Maximum number of task number to content mappings to track */
|
||||
const MAX_TASK_MAPPINGS = 100;
|
||||
|
||||
// ---------- RALPH_STATUS Block Patterns ----------
|
||||
// Based on Ralph Claude Code structured status reporting
|
||||
|
||||
/**
|
||||
* Matches the start of a RALPH_STATUS block
|
||||
* Pattern: ---RALPH_STATUS---
|
||||
*/
|
||||
const RALPH_STATUS_START_PATTERN = /^---RALPH_STATUS---\s*$/;
|
||||
|
||||
/**
|
||||
* Matches the end of a RALPH_STATUS block
|
||||
* Pattern: ---END_RALPH_STATUS---
|
||||
*/
|
||||
const RALPH_STATUS_END_PATTERN = /^---END_RALPH_STATUS---\s*$/;
|
||||
|
||||
/**
|
||||
* Matches STATUS field in RALPH_STATUS block
|
||||
* Captures: IN_PROGRESS | COMPLETE | BLOCKED
|
||||
*/
|
||||
const RALPH_STATUS_FIELD_PATTERN = /^STATUS:\s*(IN_PROGRESS|COMPLETE|BLOCKED)\s*$/i;
|
||||
|
||||
/**
|
||||
* Matches TASKS_COMPLETED_THIS_LOOP field
|
||||
* Captures: number
|
||||
*/
|
||||
const RALPH_TASKS_COMPLETED_PATTERN = /^TASKS_COMPLETED_THIS_LOOP:\s*(\d+)\s*$/i;
|
||||
|
||||
/**
|
||||
* Matches FILES_MODIFIED field
|
||||
* Captures: number
|
||||
*/
|
||||
const RALPH_FILES_MODIFIED_PATTERN = /^FILES_MODIFIED:\s*(\d+)\s*$/i;
|
||||
|
||||
/**
|
||||
* Matches TESTS_STATUS field
|
||||
* Captures: PASSING | FAILING | NOT_RUN
|
||||
*/
|
||||
const RALPH_TESTS_STATUS_PATTERN = /^TESTS_STATUS:\s*(PASSING|FAILING|NOT_RUN)\s*$/i;
|
||||
|
||||
/**
|
||||
* Matches WORK_TYPE field
|
||||
* Captures: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING
|
||||
*/
|
||||
const RALPH_WORK_TYPE_PATTERN = /^WORK_TYPE:\s*(IMPLEMENTATION|TESTING|DOCUMENTATION|REFACTORING)\s*$/i;
|
||||
|
||||
/**
|
||||
* Matches EXIT_SIGNAL field
|
||||
* Captures: true | false
|
||||
*/
|
||||
const RALPH_EXIT_SIGNAL_PATTERN = /^EXIT_SIGNAL:\s*(true|false)\s*$/i;
|
||||
|
||||
/**
|
||||
* Matches RECOMMENDATION field
|
||||
* Captures: any text
|
||||
*/
|
||||
const RALPH_RECOMMENDATION_PATTERN = /^RECOMMENDATION:\s*(.+)$/i;
|
||||
|
||||
// ---------- Completion Indicator Patterns (for dual-condition exit) ----------
|
||||
|
||||
/**
|
||||
* Patterns that indicate potential completion (natural language)
|
||||
* Count >= 2 along with EXIT_SIGNAL: true triggers exit
|
||||
*/
|
||||
const COMPLETION_INDICATOR_PATTERNS = [
|
||||
/all\s+(?:tasks?|items?|work)\s+(?:are\s+)?(?:completed?|done|finished)/i,
|
||||
/(?:completed?|finished)\s+all\s+(?:tasks?|items?|work)/i,
|
||||
/nothing\s+(?:left|remaining)\s+to\s+do/i,
|
||||
/no\s+more\s+(?:tasks?|items?|work)/i,
|
||||
/everything\s+(?:is\s+)?(?:completed?|done)/i,
|
||||
/project\s+(?:is\s+)?(?:completed?|done|finished)/i,
|
||||
];
|
||||
|
||||
// ========== Event Types ==========
|
||||
|
||||
/**
|
||||
@@ -228,6 +306,9 @@ const MAX_TASK_MAPPINGS = 100;
|
||||
* @event todoUpdate - Fired when todo list changes (items added, status changed)
|
||||
* @event completionDetected - Fired when completion phrase is detected (task complete)
|
||||
* @event enabled - Fired when tracker auto-enables due to Ralph pattern detection
|
||||
* @event statusBlockDetected - Fired when a RALPH_STATUS block is parsed
|
||||
* @event circuitBreakerUpdate - Fired when circuit breaker state changes
|
||||
* @event exitGateMet - Fired when dual-condition exit gate is met
|
||||
*/
|
||||
export interface RalphTrackerEvents {
|
||||
/** Emitted when loop state changes */
|
||||
@@ -238,6 +319,12 @@ export interface RalphTrackerEvents {
|
||||
completionDetected: (phrase: string) => void;
|
||||
/** Emitted when tracker auto-enables from disabled state */
|
||||
enabled: () => void;
|
||||
/** Emitted when a RALPH_STATUS block is parsed */
|
||||
statusBlockDetected: (block: RalphStatusBlock) => void;
|
||||
/** Emitted when circuit breaker state changes */
|
||||
circuitBreakerUpdate: (status: CircuitBreakerStatus) => void;
|
||||
/** Emitted when dual-condition exit gate is met (completion indicators >= 2 AND EXIT_SIGNAL: true) */
|
||||
exitGateMet: (data: { completionIndicators: number; exitSignal: boolean }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,6 +402,32 @@ export class RalphTracker extends EventEmitter {
|
||||
/** Maps task numbers from "✔ Task #N" format to their content for status updates */
|
||||
private _taskNumberToContent: Map<number, string> = new Map();
|
||||
|
||||
// ========== RALPH_STATUS Block State ==========
|
||||
|
||||
/** Circuit breaker state tracking */
|
||||
private _circuitBreaker: CircuitBreakerStatus;
|
||||
|
||||
/** Buffer for RALPH_STATUS block lines */
|
||||
private _statusBlockBuffer: string[] = [];
|
||||
|
||||
/** Flag indicating we're inside a RALPH_STATUS block */
|
||||
private _inStatusBlock: boolean = false;
|
||||
|
||||
/** Last parsed RALPH_STATUS block */
|
||||
private _lastStatusBlock: RalphStatusBlock | null = null;
|
||||
|
||||
/** Count of completion indicators detected (for dual-condition exit) */
|
||||
private _completionIndicators: number = 0;
|
||||
|
||||
/** Whether dual-condition exit gate has been met */
|
||||
private _exitGateMet: boolean = false;
|
||||
|
||||
/** Cumulative files modified across all iterations */
|
||||
private _totalFilesModified: number = 0;
|
||||
|
||||
/** Cumulative tasks completed across all iterations */
|
||||
private _totalTasksCompleted: number = 0;
|
||||
|
||||
/**
|
||||
* Creates a new RalphTracker instance.
|
||||
* Starts in disabled state until Ralph patterns are detected.
|
||||
@@ -322,6 +435,7 @@ export class RalphTracker extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._loopState = createInitialRalphTrackerState();
|
||||
this._circuitBreaker = createInitialCircuitBreakerStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,6 +525,15 @@ export class RalphTracker extends EventEmitter {
|
||||
this._completionPhraseCount.clear();
|
||||
this._taskNumberToContent.clear();
|
||||
this._lineBuffer = '';
|
||||
// Reset RALPH_STATUS block state
|
||||
this._statusBlockBuffer = [];
|
||||
this._inStatusBlock = false;
|
||||
this._lastStatusBlock = null;
|
||||
this._completionIndicators = 0;
|
||||
this._exitGateMet = false;
|
||||
this._totalFilesModified = 0;
|
||||
this._totalTasksCompleted = 0;
|
||||
// Keep circuit breaker state on soft reset (it tracks across iterations)
|
||||
// Emit immediately on reset (no debounce)
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
this.emit('todoUpdate', this.todos);
|
||||
@@ -432,6 +555,15 @@ export class RalphTracker extends EventEmitter {
|
||||
this._completionPhraseCount.clear();
|
||||
this._taskNumberToContent.clear();
|
||||
this._lineBuffer = '';
|
||||
// Reset all RALPH_STATUS block and circuit breaker state
|
||||
this._statusBlockBuffer = [];
|
||||
this._inStatusBlock = false;
|
||||
this._lastStatusBlock = null;
|
||||
this._completionIndicators = 0;
|
||||
this._exitGateMet = false;
|
||||
this._totalFilesModified = 0;
|
||||
this._totalTasksCompleted = 0;
|
||||
this._circuitBreaker = createInitialCircuitBreakerStatus();
|
||||
// Emit immediately on reset (no debounce)
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
this.emit('todoUpdate', this.todos);
|
||||
@@ -689,6 +821,12 @@ export class RalphTracker extends EventEmitter {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
// Check for RALPH_STATUS block (structured status reporting)
|
||||
this.processStatusBlockLine(trimmed);
|
||||
|
||||
// Check for completion indicators (for dual-condition exit gate)
|
||||
this.detectCompletionIndicators(trimmed);
|
||||
|
||||
// Check for completion phrase
|
||||
this.detectCompletionPhrase(trimmed);
|
||||
|
||||
@@ -1493,6 +1631,15 @@ export class RalphTracker extends EventEmitter {
|
||||
this._taskNumberToContent.clear();
|
||||
this._lineBuffer = '';
|
||||
this._completionPhraseCount.clear();
|
||||
// Clear RALPH_STATUS block and circuit breaker state
|
||||
this._statusBlockBuffer = [];
|
||||
this._inStatusBlock = false;
|
||||
this._lastStatusBlock = null;
|
||||
this._completionIndicators = 0;
|
||||
this._exitGateMet = false;
|
||||
this._totalFilesModified = 0;
|
||||
this._totalTasksCompleted = 0;
|
||||
this._circuitBreaker = createInitialCircuitBreakerStatus();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
this.emit('todoUpdate', this.todos);
|
||||
}
|
||||
@@ -1555,4 +1702,304 @@ export class RalphTracker extends EventEmitter {
|
||||
this._todos.set(todo.id, { ...todo });
|
||||
}
|
||||
}
|
||||
|
||||
// ========== RALPH_STATUS Block Detection ==========
|
||||
|
||||
/**
|
||||
* Process a line for RALPH_STATUS block detection.
|
||||
* Buffers lines between ---RALPH_STATUS--- and ---END_RALPH_STATUS---
|
||||
* then parses the complete block.
|
||||
*
|
||||
* @param line - Single line to process (already trimmed)
|
||||
* @fires statusBlockDetected - When a complete block is parsed
|
||||
*/
|
||||
private processStatusBlockLine(line: string): void {
|
||||
// Check for block start
|
||||
if (RALPH_STATUS_START_PATTERN.test(line)) {
|
||||
this._inStatusBlock = true;
|
||||
this._statusBlockBuffer = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for block end
|
||||
if (this._inStatusBlock && RALPH_STATUS_END_PATTERN.test(line)) {
|
||||
this._inStatusBlock = false;
|
||||
this.parseStatusBlock(this._statusBlockBuffer);
|
||||
this._statusBlockBuffer = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Buffer lines while in block
|
||||
if (this._inStatusBlock) {
|
||||
this._statusBlockBuffer.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse buffered RALPH_STATUS block lines into structured data.
|
||||
*
|
||||
* @param lines - Array of lines between block markers
|
||||
* @fires statusBlockDetected - When parsing succeeds
|
||||
*/
|
||||
private parseStatusBlock(lines: string[]): void {
|
||||
const block: Partial<RalphStatusBlock> = {
|
||||
parsedAt: Date.now(),
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
// STATUS field
|
||||
const statusMatch = line.match(RALPH_STATUS_FIELD_PATTERN);
|
||||
if (statusMatch) {
|
||||
block.status = statusMatch[1].toUpperCase() as RalphStatusValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// TASKS_COMPLETED_THIS_LOOP field
|
||||
const tasksMatch = line.match(RALPH_TASKS_COMPLETED_PATTERN);
|
||||
if (tasksMatch) {
|
||||
block.tasksCompletedThisLoop = parseInt(tasksMatch[1], 10);
|
||||
continue;
|
||||
}
|
||||
|
||||
// FILES_MODIFIED field
|
||||
const filesMatch = line.match(RALPH_FILES_MODIFIED_PATTERN);
|
||||
if (filesMatch) {
|
||||
block.filesModified = parseInt(filesMatch[1], 10);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TESTS_STATUS field
|
||||
const testsMatch = line.match(RALPH_TESTS_STATUS_PATTERN);
|
||||
if (testsMatch) {
|
||||
block.testsStatus = testsMatch[1].toUpperCase() as RalphTestsStatus;
|
||||
continue;
|
||||
}
|
||||
|
||||
// WORK_TYPE field
|
||||
const workMatch = line.match(RALPH_WORK_TYPE_PATTERN);
|
||||
if (workMatch) {
|
||||
block.workType = workMatch[1].toUpperCase() as RalphWorkType;
|
||||
continue;
|
||||
}
|
||||
|
||||
// EXIT_SIGNAL field
|
||||
const exitMatch = line.match(RALPH_EXIT_SIGNAL_PATTERN);
|
||||
if (exitMatch) {
|
||||
block.exitSignal = exitMatch[1].toLowerCase() === 'true';
|
||||
continue;
|
||||
}
|
||||
|
||||
// RECOMMENDATION field
|
||||
const recMatch = line.match(RALPH_RECOMMENDATION_PATTERN);
|
||||
if (recMatch) {
|
||||
block.recommendation = recMatch[1].trim();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Only process if we have at least the STATUS field
|
||||
if (block.status !== undefined) {
|
||||
// Fill in defaults for missing fields
|
||||
const fullBlock: RalphStatusBlock = {
|
||||
status: block.status,
|
||||
tasksCompletedThisLoop: block.tasksCompletedThisLoop ?? 0,
|
||||
filesModified: block.filesModified ?? 0,
|
||||
testsStatus: block.testsStatus ?? 'NOT_RUN',
|
||||
workType: block.workType ?? 'IMPLEMENTATION',
|
||||
exitSignal: block.exitSignal ?? false,
|
||||
recommendation: block.recommendation ?? '',
|
||||
parsedAt: block.parsedAt!,
|
||||
};
|
||||
|
||||
this._lastStatusBlock = fullBlock;
|
||||
this.handleStatusBlock(fullBlock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a parsed RALPH_STATUS block.
|
||||
* Updates circuit breaker, checks exit conditions.
|
||||
*
|
||||
* @param block - Parsed status block
|
||||
* @fires statusBlockDetected - With the block data
|
||||
* @fires circuitBreakerUpdate - If state changes
|
||||
* @fires exitGateMet - If dual-condition exit triggered
|
||||
*/
|
||||
private handleStatusBlock(block: RalphStatusBlock): void {
|
||||
// Auto-enable tracker when we see a status block
|
||||
if (!this._loopState.enabled && !this._autoEnableDisabled) {
|
||||
this.enable();
|
||||
}
|
||||
|
||||
// Update cumulative counts
|
||||
this._totalFilesModified += block.filesModified;
|
||||
this._totalTasksCompleted += block.tasksCompletedThisLoop;
|
||||
|
||||
// Check for progress (for circuit breaker)
|
||||
const hasProgress = block.filesModified > 0 || block.tasksCompletedThisLoop > 0;
|
||||
|
||||
// Update circuit breaker
|
||||
this.updateCircuitBreaker(hasProgress, block.testsStatus, block.status);
|
||||
|
||||
// Check completion indicators
|
||||
if (block.status === 'COMPLETE') {
|
||||
this._completionIndicators++;
|
||||
}
|
||||
|
||||
// Check dual-condition exit gate
|
||||
if (block.exitSignal && this._completionIndicators >= 2 && !this._exitGateMet) {
|
||||
this._exitGateMet = true;
|
||||
this.emit('exitGateMet', {
|
||||
completionIndicators: this._completionIndicators,
|
||||
exitSignal: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Update loop state
|
||||
this._loopState.lastActivity = Date.now();
|
||||
|
||||
// Emit the status block
|
||||
this.emit('statusBlockDetected', block);
|
||||
this.emitLoopUpdateDebounced();
|
||||
}
|
||||
|
||||
// ========== Circuit Breaker ==========
|
||||
|
||||
/**
|
||||
* Update circuit breaker state based on iteration results.
|
||||
*
|
||||
* @param hasProgress - Whether this iteration made progress
|
||||
* @param testsStatus - Current test status
|
||||
* @param status - Overall status from RALPH_STATUS
|
||||
* @fires circuitBreakerUpdate - If state changes
|
||||
*/
|
||||
private updateCircuitBreaker(
|
||||
hasProgress: boolean,
|
||||
testsStatus: RalphTestsStatus,
|
||||
status: RalphStatusValue
|
||||
): void {
|
||||
const prevState = this._circuitBreaker.state;
|
||||
|
||||
if (hasProgress) {
|
||||
// Progress detected - reset counters, possibly close circuit
|
||||
this._circuitBreaker.consecutiveNoProgress = 0;
|
||||
this._circuitBreaker.consecutiveSameError = 0;
|
||||
this._circuitBreaker.lastProgressIteration = this._loopState.cycleCount;
|
||||
|
||||
if (this._circuitBreaker.state === 'HALF_OPEN') {
|
||||
this._circuitBreaker.state = 'CLOSED';
|
||||
this._circuitBreaker.reason = 'Progress detected, circuit closed';
|
||||
this._circuitBreaker.reasonCode = 'progress_detected';
|
||||
}
|
||||
} else {
|
||||
// No progress
|
||||
this._circuitBreaker.consecutiveNoProgress++;
|
||||
|
||||
// State transitions based on consecutive no-progress
|
||||
if (this._circuitBreaker.state === 'CLOSED') {
|
||||
if (this._circuitBreaker.consecutiveNoProgress >= 3) {
|
||||
this._circuitBreaker.state = 'OPEN';
|
||||
this._circuitBreaker.reason = `No progress for ${this._circuitBreaker.consecutiveNoProgress} iterations`;
|
||||
this._circuitBreaker.reasonCode = 'no_progress_open';
|
||||
} else if (this._circuitBreaker.consecutiveNoProgress >= 2) {
|
||||
this._circuitBreaker.state = 'HALF_OPEN';
|
||||
this._circuitBreaker.reason = 'Warning: no progress detected';
|
||||
this._circuitBreaker.reasonCode = 'no_progress_warning';
|
||||
}
|
||||
} else if (this._circuitBreaker.state === 'HALF_OPEN') {
|
||||
if (this._circuitBreaker.consecutiveNoProgress >= 3) {
|
||||
this._circuitBreaker.state = 'OPEN';
|
||||
this._circuitBreaker.reason = `No progress for ${this._circuitBreaker.consecutiveNoProgress} iterations`;
|
||||
this._circuitBreaker.reasonCode = 'no_progress_open';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track tests failure
|
||||
if (testsStatus === 'FAILING') {
|
||||
this._circuitBreaker.consecutiveTestsFailure++;
|
||||
if (this._circuitBreaker.consecutiveTestsFailure >= 5 && this._circuitBreaker.state !== 'OPEN') {
|
||||
this._circuitBreaker.state = 'OPEN';
|
||||
this._circuitBreaker.reason = `Tests failing for ${this._circuitBreaker.consecutiveTestsFailure} iterations`;
|
||||
this._circuitBreaker.reasonCode = 'tests_failing_too_long';
|
||||
}
|
||||
} else {
|
||||
this._circuitBreaker.consecutiveTestsFailure = 0;
|
||||
}
|
||||
|
||||
// Track blocked status
|
||||
if (status === 'BLOCKED' && this._circuitBreaker.state !== 'OPEN') {
|
||||
this._circuitBreaker.state = 'OPEN';
|
||||
this._circuitBreaker.reason = 'Claude reported BLOCKED status';
|
||||
this._circuitBreaker.reasonCode = 'same_error_repeated';
|
||||
}
|
||||
|
||||
// Emit if state changed
|
||||
if (prevState !== this._circuitBreaker.state) {
|
||||
this._circuitBreaker.lastTransitionAt = Date.now();
|
||||
this.emit('circuitBreakerUpdate', { ...this._circuitBreaker });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually reset circuit breaker to CLOSED state.
|
||||
* Use when user acknowledges the issue is resolved.
|
||||
*
|
||||
* @fires circuitBreakerUpdate
|
||||
*/
|
||||
resetCircuitBreaker(): void {
|
||||
this._circuitBreaker = createInitialCircuitBreakerStatus();
|
||||
this._circuitBreaker.reason = 'Manual reset';
|
||||
this._circuitBreaker.reasonCode = 'manual_reset';
|
||||
this.emit('circuitBreakerUpdate', { ...this._circuitBreaker });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current circuit breaker status.
|
||||
*/
|
||||
get circuitBreakerStatus(): CircuitBreakerStatus {
|
||||
return { ...this._circuitBreaker };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last parsed RALPH_STATUS block.
|
||||
*/
|
||||
get lastStatusBlock(): RalphStatusBlock | null {
|
||||
return this._lastStatusBlock ? { ...this._lastStatusBlock } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cumulative stats from status blocks.
|
||||
*/
|
||||
get cumulativeStats(): { filesModified: number; tasksCompleted: number; completionIndicators: number } {
|
||||
return {
|
||||
filesModified: this._totalFilesModified,
|
||||
tasksCompleted: this._totalTasksCompleted,
|
||||
completionIndicators: this._completionIndicators,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether dual-condition exit gate has been met.
|
||||
*/
|
||||
get exitGateMet(): boolean {
|
||||
return this._exitGateMet;
|
||||
}
|
||||
|
||||
// ========== Completion Indicator Detection ==========
|
||||
|
||||
/**
|
||||
* Check line for completion indicators (natural language patterns).
|
||||
* Used for dual-condition exit gate.
|
||||
*
|
||||
* @param line - Line to check
|
||||
*/
|
||||
private detectCompletionIndicators(line: string): void {
|
||||
for (const pattern of COMPLETION_INDICATOR_PATTERNS) {
|
||||
if (pattern.test(line)) {
|
||||
this._completionIndicators++;
|
||||
break; // Only count once per line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +319,12 @@ export interface SessionEvents {
|
||||
ralphTodoUpdate: (todos: RalphTodoItem[]) => void;
|
||||
/** Ralph completion phrase detected */
|
||||
ralphCompletionDetected: (phrase: string) => void;
|
||||
/** RALPH_STATUS block detected */
|
||||
ralphStatusBlockDetected: (block: import('./types.js').RalphStatusBlock) => void;
|
||||
/** Circuit breaker state changed */
|
||||
ralphCircuitBreakerUpdate: (status: import('./types.js').CircuitBreakerStatus) => void;
|
||||
/** Dual-condition exit gate met */
|
||||
ralphExitGateMet: (data: { completionIndicators: number; exitSignal: boolean }) => void;
|
||||
/** Bash tool with file paths started */
|
||||
bashToolStart: (tool: ActiveBashTool) => void;
|
||||
/** Bash tool completed */
|
||||
@@ -441,6 +447,9 @@ export class Session extends EventEmitter {
|
||||
loopUpdate: (state: RalphTrackerState) => void;
|
||||
todoUpdate: (todos: RalphTodoItem[]) => void;
|
||||
completionDetected: (phrase: string) => void;
|
||||
statusBlockDetected: (block: import('./types.js').RalphStatusBlock) => void;
|
||||
circuitBreakerUpdate: (status: import('./types.js').CircuitBreakerStatus) => void;
|
||||
exitGateMet: (data: { completionIndicators: number; exitSignal: boolean }) => void;
|
||||
} | null = null;
|
||||
|
||||
// Bash tool tracking (file paths for live log viewing)
|
||||
@@ -494,10 +503,16 @@ export class Session extends EventEmitter {
|
||||
loopUpdate: (state) => this.emit('ralphLoopUpdate', state),
|
||||
todoUpdate: (todos) => this.emit('ralphTodoUpdate', todos),
|
||||
completionDetected: (phrase) => this.emit('ralphCompletionDetected', phrase),
|
||||
statusBlockDetected: (block) => this.emit('ralphStatusBlockDetected', block),
|
||||
circuitBreakerUpdate: (status) => this.emit('ralphCircuitBreakerUpdate', status),
|
||||
exitGateMet: (data) => this.emit('ralphExitGateMet', data),
|
||||
};
|
||||
this._ralphTracker.on('loopUpdate', this._ralphHandlers.loopUpdate);
|
||||
this._ralphTracker.on('todoUpdate', this._ralphHandlers.todoUpdate);
|
||||
this._ralphTracker.on('completionDetected', this._ralphHandlers.completionDetected);
|
||||
this._ralphTracker.on('statusBlockDetected', this._ralphHandlers.statusBlockDetected);
|
||||
this._ralphTracker.on('circuitBreakerUpdate', this._ralphHandlers.circuitBreakerUpdate);
|
||||
this._ralphTracker.on('exitGateMet', this._ralphHandlers.exitGateMet);
|
||||
|
||||
// Initialize Bash tool parser and forward events (store handlers for cleanup)
|
||||
this._bashToolParser = new BashToolParser({ sessionId: this.id, workingDir: this.workingDir });
|
||||
@@ -1739,6 +1754,9 @@ export class Session extends EventEmitter {
|
||||
this._ralphTracker.off('loopUpdate', this._ralphHandlers.loopUpdate);
|
||||
this._ralphTracker.off('todoUpdate', this._ralphHandlers.todoUpdate);
|
||||
this._ralphTracker.off('completionDetected', this._ralphHandlers.completionDetected);
|
||||
this._ralphTracker.off('statusBlockDetected', this._ralphHandlers.statusBlockDetected);
|
||||
this._ralphTracker.off('circuitBreakerUpdate', this._ralphHandlers.circuitBreakerUpdate);
|
||||
this._ralphTracker.off('exitGateMet', this._ralphHandlers.exitGateMet);
|
||||
this._ralphHandlers = null;
|
||||
}
|
||||
|
||||
|
||||
+133
@@ -753,6 +753,139 @@ export interface RalphStateRecord {
|
||||
[sessionId: string]: RalphSessionState;
|
||||
}
|
||||
|
||||
// ========== RALPH_STATUS Block Types ==========
|
||||
|
||||
/**
|
||||
* Status values from RALPH_STATUS block.
|
||||
* - IN_PROGRESS: Work is ongoing
|
||||
* - COMPLETE: All tasks finished
|
||||
* - BLOCKED: Needs human intervention
|
||||
*/
|
||||
export type RalphStatusValue = 'IN_PROGRESS' | 'COMPLETE' | 'BLOCKED';
|
||||
|
||||
/**
|
||||
* Test status from RALPH_STATUS block.
|
||||
*/
|
||||
export type RalphTestsStatus = 'PASSING' | 'FAILING' | 'NOT_RUN';
|
||||
|
||||
/**
|
||||
* Work type classification for current iteration.
|
||||
*/
|
||||
export type RalphWorkType = 'IMPLEMENTATION' | 'TESTING' | 'DOCUMENTATION' | 'REFACTORING';
|
||||
|
||||
/**
|
||||
* Parsed RALPH_STATUS block from Claude output.
|
||||
*
|
||||
* Claude outputs this at the end of every response:
|
||||
* ```
|
||||
* ---RALPH_STATUS---
|
||||
* STATUS: IN_PROGRESS
|
||||
* TASKS_COMPLETED_THIS_LOOP: 3
|
||||
* FILES_MODIFIED: 5
|
||||
* TESTS_STATUS: PASSING
|
||||
* WORK_TYPE: IMPLEMENTATION
|
||||
* EXIT_SIGNAL: false
|
||||
* RECOMMENDATION: Continue with database migration
|
||||
* ---END_RALPH_STATUS---
|
||||
* ```
|
||||
*/
|
||||
export interface RalphStatusBlock {
|
||||
/** Overall loop status */
|
||||
status: RalphStatusValue;
|
||||
/** Number of tasks completed in current iteration */
|
||||
tasksCompletedThisLoop: number;
|
||||
/** Number of files modified in current iteration */
|
||||
filesModified: number;
|
||||
/** Current state of tests */
|
||||
testsStatus: RalphTestsStatus;
|
||||
/** Type of work being performed */
|
||||
workType: RalphWorkType;
|
||||
/** Whether Claude is signaling completion */
|
||||
exitSignal: boolean;
|
||||
/** Claude's recommendation for next steps */
|
||||
recommendation: string;
|
||||
/** Timestamp when this block was parsed */
|
||||
parsedAt: number;
|
||||
}
|
||||
|
||||
// ========== Circuit Breaker Types ==========
|
||||
|
||||
/**
|
||||
* Circuit breaker states for detecting stuck loops.
|
||||
* - CLOSED: Normal operation, all checks passing
|
||||
* - HALF_OPEN: Warning state, some checks failing
|
||||
* - OPEN: Loop is stuck, requires intervention
|
||||
*/
|
||||
export type CircuitBreakerState = 'CLOSED' | 'HALF_OPEN' | 'OPEN';
|
||||
|
||||
/**
|
||||
* Reason codes for circuit breaker state transitions.
|
||||
*/
|
||||
export type CircuitBreakerReason =
|
||||
| 'normal_operation'
|
||||
| 'no_progress_warning'
|
||||
| 'no_progress_open'
|
||||
| 'same_error_repeated'
|
||||
| 'tests_failing_too_long'
|
||||
| 'progress_detected'
|
||||
| 'manual_reset';
|
||||
|
||||
/**
|
||||
* Circuit breaker status for tracking loop health.
|
||||
*
|
||||
* Transitions:
|
||||
* - CLOSED -> HALF_OPEN: consecutive_no_progress >= 2
|
||||
* - CLOSED -> OPEN: consecutive_no_progress >= 3 OR consecutive_same_error >= 5
|
||||
* - HALF_OPEN -> CLOSED: progress detected
|
||||
* - HALF_OPEN -> OPEN: consecutive_no_progress >= 3
|
||||
* - OPEN -> CLOSED: manual reset only
|
||||
*/
|
||||
export interface CircuitBreakerStatus {
|
||||
/** Current state of the circuit breaker */
|
||||
state: CircuitBreakerState;
|
||||
/** Number of consecutive iterations with no progress */
|
||||
consecutiveNoProgress: number;
|
||||
/** Number of consecutive iterations with the same error */
|
||||
consecutiveSameError: number;
|
||||
/** Number of consecutive iterations with failing tests */
|
||||
consecutiveTestsFailure: number;
|
||||
/** Last iteration number that showed progress */
|
||||
lastProgressIteration: number;
|
||||
/** Human-readable reason for current state */
|
||||
reason: string;
|
||||
/** Reason code for programmatic handling */
|
||||
reasonCode: CircuitBreakerReason;
|
||||
/** Timestamp of last state transition */
|
||||
lastTransitionAt: number;
|
||||
/** Last error message seen (for same-error tracking) */
|
||||
lastErrorMessage: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates initial circuit breaker status.
|
||||
*/
|
||||
export function createInitialCircuitBreakerStatus(): CircuitBreakerStatus {
|
||||
return {
|
||||
state: 'CLOSED',
|
||||
consecutiveNoProgress: 0,
|
||||
consecutiveSameError: 0,
|
||||
consecutiveTestsFailure: 0,
|
||||
lastProgressIteration: 0,
|
||||
reason: 'Initial state',
|
||||
reasonCode: 'normal_operation',
|
||||
lastTransitionAt: Date.now(),
|
||||
lastErrorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Priority Todo Types ==========
|
||||
|
||||
/**
|
||||
* Priority levels for todo items.
|
||||
* Matches @fix_plan.md format.
|
||||
*/
|
||||
export type RalphTodoPriority = 'P0' | 'P1' | 'P2' | null;
|
||||
|
||||
/**
|
||||
* Creates initial Ralph tracker state
|
||||
* @returns Fresh Ralph tracker state with defaults
|
||||
|
||||
+167
-1
@@ -1523,6 +1523,43 @@ class ClaudemanApp {
|
||||
});
|
||||
});
|
||||
|
||||
// RALPH_STATUS block and circuit breaker events
|
||||
this.eventSource.addEventListener('session:ralphStatusUpdate', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
this.updateRalphState(data.sessionId, { statusBlock: data.block });
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('session:circuitBreakerUpdate', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
this.updateRalphState(data.sessionId, { circuitBreaker: data.status });
|
||||
// Notify if circuit breaker opens
|
||||
if (data.status.state === 'OPEN') {
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
this.notificationManager?.notify({
|
||||
urgency: 'critical',
|
||||
category: 'circuit-breaker',
|
||||
sessionId: data.sessionId,
|
||||
sessionName: session?.name || data.sessionId?.slice(0, 8),
|
||||
title: 'Circuit Breaker Open',
|
||||
message: data.status.reason || 'Loop stuck - no progress detected',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('session:exitGateMet', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
// Notify when exit gate is met
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
this.notificationManager?.notify({
|
||||
urgency: 'warning',
|
||||
category: 'exit-gate',
|
||||
sessionId: data.sessionId,
|
||||
sessionName: session?.name || data.sessionId?.slice(0, 8),
|
||||
title: 'Exit Gate Met',
|
||||
message: `Loop ready to exit (indicators: ${data.completionIndicators})`,
|
||||
});
|
||||
});
|
||||
|
||||
// Active Bash tool events (for clickable file paths)
|
||||
this.eventSource.addEventListener('session:bashToolStart', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
@@ -5185,8 +5222,10 @@ class ClaudemanApp {
|
||||
const isEnabled = state?.loop?.enabled === true;
|
||||
const hasLoop = state?.loop?.active || state?.loop?.completionPhrase;
|
||||
const hasTodos = state?.todos?.length > 0;
|
||||
const hasCircuitBreaker = state?.circuitBreaker && state.circuitBreaker.state !== 'CLOSED';
|
||||
const hasStatusBlock = state?.statusBlock !== undefined;
|
||||
|
||||
if (!isEnabled && !hasLoop && !hasTodos) {
|
||||
if (!isEnabled && !hasLoop && !hasTodos && !hasCircuitBreaker && !hasStatusBlock) {
|
||||
panel.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
@@ -5208,6 +5247,9 @@ class ClaudemanApp {
|
||||
// Update stats
|
||||
this.updateRalphStats(state?.loop, completed, total);
|
||||
|
||||
// Update circuit breaker badge
|
||||
this.updateCircuitBreakerBadge(state?.circuitBreaker);
|
||||
|
||||
// Handle collapsed/expanded state
|
||||
if (this.ralphStatePanelCollapsed) {
|
||||
panel.classList.add('collapsed');
|
||||
@@ -5269,6 +5311,67 @@ class ClaudemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
updateCircuitBreakerBadge(circuitBreaker) {
|
||||
// Find or create the circuit breaker badge container
|
||||
let cbContainer = this.$('ralphCircuitBreakerBadge');
|
||||
if (!cbContainer) {
|
||||
// Create container if it doesn't exist (we'll add it dynamically)
|
||||
const summary = this.$('ralphSummary');
|
||||
if (!summary) return;
|
||||
|
||||
// Check if it already exists
|
||||
cbContainer = summary.querySelector('.ralph-circuit-breaker');
|
||||
if (!cbContainer) {
|
||||
cbContainer = document.createElement('div');
|
||||
cbContainer.id = 'ralphCircuitBreakerBadge';
|
||||
cbContainer.className = 'ralph-circuit-breaker';
|
||||
// Insert after the status badge
|
||||
const statusBadge = this.$('ralphStatusBadge');
|
||||
if (statusBadge && statusBadge.nextSibling) {
|
||||
statusBadge.parentNode.insertBefore(cbContainer, statusBadge.nextSibling);
|
||||
} else {
|
||||
summary.appendChild(cbContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide if no circuit breaker state or CLOSED
|
||||
if (!circuitBreaker || circuitBreaker.state === 'CLOSED') {
|
||||
cbContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
cbContainer.style.display = '';
|
||||
cbContainer.classList.remove('half-open', 'open');
|
||||
|
||||
if (circuitBreaker.state === 'HALF_OPEN') {
|
||||
cbContainer.classList.add('half-open');
|
||||
cbContainer.innerHTML = `<span class="cb-icon">⚠</span><span class="cb-text">Warning</span>`;
|
||||
cbContainer.title = circuitBreaker.reason || 'Circuit breaker warning';
|
||||
} else if (circuitBreaker.state === 'OPEN') {
|
||||
cbContainer.classList.add('open');
|
||||
cbContainer.innerHTML = `<span class="cb-icon">🛑</span><span class="cb-text">Stuck</span>`;
|
||||
cbContainer.title = circuitBreaker.reason || 'Loop appears stuck';
|
||||
}
|
||||
|
||||
// Add click handler to reset
|
||||
cbContainer.onclick = () => this.resetCircuitBreaker();
|
||||
}
|
||||
|
||||
async resetCircuitBreaker() {
|
||||
if (!this.activeSessionId) return;
|
||||
try {
|
||||
const response = await fetch(`/api/sessions/${this.activeSessionId}/ralph-circuit-breaker/reset`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
console.log('Circuit breaker reset');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to reset circuit breaker:', err);
|
||||
}
|
||||
}
|
||||
|
||||
updateRalphStats(loop, completed, total) {
|
||||
// Time stat
|
||||
const timeEl = this.$('ralphStatTime');
|
||||
@@ -5352,6 +5455,69 @@ class ClaudemanApp {
|
||||
|
||||
// Render task cards
|
||||
this.renderRalphTasks(todos);
|
||||
|
||||
// Render RALPH_STATUS block if present
|
||||
this.renderRalphStatusBlock(state?.statusBlock);
|
||||
}
|
||||
|
||||
renderRalphStatusBlock(statusBlock) {
|
||||
// Find or create the status block container
|
||||
let container = this.$('ralphStatusBlockDisplay');
|
||||
const expandedContent = this.$('ralphExpandedContent');
|
||||
|
||||
if (!statusBlock) {
|
||||
// Remove container if no status block
|
||||
if (container) {
|
||||
container.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!container && expandedContent) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'ralphStatusBlockDisplay';
|
||||
container.className = 'ralph-status-block';
|
||||
// Insert at the top of expanded content
|
||||
expandedContent.insertBefore(container, expandedContent.firstChild);
|
||||
}
|
||||
|
||||
if (!container) return;
|
||||
|
||||
// Build status class
|
||||
const statusClass = statusBlock.status === 'IN_PROGRESS' ? 'in-progress'
|
||||
: statusBlock.status === 'COMPLETE' ? 'complete'
|
||||
: statusBlock.status === 'BLOCKED' ? 'blocked' : '';
|
||||
|
||||
// Build tests status icon
|
||||
const testsIcon = statusBlock.testsStatus === 'PASSING' ? '✅'
|
||||
: statusBlock.testsStatus === 'FAILING' ? '❌'
|
||||
: '⏸';
|
||||
|
||||
// Build work type icon
|
||||
const workIcon = statusBlock.workType === 'IMPLEMENTATION' ? '🔧'
|
||||
: statusBlock.workType === 'TESTING' ? '🧪'
|
||||
: statusBlock.workType === 'DOCUMENTATION' ? '📝'
|
||||
: statusBlock.workType === 'REFACTORING' ? '♻️' : '📋';
|
||||
|
||||
let html = `
|
||||
<div class="ralph-status-block-header">
|
||||
<span>RALPH_STATUS</span>
|
||||
<span class="ralph-status-block-status ${statusClass}">${statusBlock.status}</span>
|
||||
${statusBlock.exitSignal ? '<span style="color: #4caf50;">🚪 EXIT</span>' : ''}
|
||||
</div>
|
||||
<div class="ralph-status-block-stats">
|
||||
<span>${workIcon} ${statusBlock.workType}</span>
|
||||
<span>📁 ${statusBlock.filesModified} files</span>
|
||||
<span>✓ ${statusBlock.tasksCompletedThisLoop} tasks</span>
|
||||
<span>${testsIcon} Tests: ${statusBlock.testsStatus}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (statusBlock.recommendation) {
|
||||
html += `<div class="ralph-status-block-recommendation">${statusBlock.recommendation}</div>`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
renderRalphTasks(todos) {
|
||||
|
||||
@@ -843,6 +843,117 @@ body {
|
||||
50% { opacity: 0.6; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
/* Circuit Breaker Badge */
|
||||
.ralph-circuit-breaker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.ralph-circuit-breaker.half-open {
|
||||
background: rgba(255, 193, 7, 0.2);
|
||||
color: #ffc107;
|
||||
border: 1px solid rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
|
||||
.ralph-circuit-breaker.half-open:hover {
|
||||
background: rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.ralph-circuit-breaker.open {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
border: 1px solid rgba(244, 67, 54, 0.4);
|
||||
animation: circuit-breaker-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.ralph-circuit-breaker.open:hover {
|
||||
background: rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.ralph-circuit-breaker .cb-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ralph-circuit-breaker .cb-text {
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@keyframes circuit-breaker-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* RALPH_STATUS Block Display */
|
||||
.ralph-status-block {
|
||||
background: var(--bg-input);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ralph-status-block-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.4rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
.ralph-status-block-status {
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ralph-status-block-status.in-progress {
|
||||
background: rgba(33, 150, 243, 0.2);
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.ralph-status-block-status.complete {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.ralph-status-block-status.blocked {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.ralph-status-block-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.ralph-status-block-stats span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.ralph-status-block-recommendation {
|
||||
margin-top: 0.4rem;
|
||||
padding-top: 0.4rem;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Summary Stats */
|
||||
.ralph-summary-stats {
|
||||
display: flex;
|
||||
|
||||
@@ -118,6 +118,24 @@ const CLAUDE_BANNER_PATTERN = /\x1b\[1mClaud/;
|
||||
const CTRL_L_PATTERN = /\x0c/g;
|
||||
const LEADING_WHITESPACE_PATTERN = /^[\s\r\n]+/;
|
||||
|
||||
/**
|
||||
* Formats uptime in seconds to a human-readable string.
|
||||
*/
|
||||
function formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (days > 0) parts.push(`${days}d`);
|
||||
if (hours > 0) parts.push(`${hours}h`);
|
||||
if (minutes > 0) parts.push(`${minutes}m`);
|
||||
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes hook event data before broadcasting via SSE.
|
||||
* Extracts only relevant fields and limits total size to prevent
|
||||
@@ -444,6 +462,50 @@ export class WebServer extends EventEmitter {
|
||||
return { success: true, config: this.store.getConfig() };
|
||||
});
|
||||
|
||||
// Debug/monitoring endpoint - lightweight, only runs when called
|
||||
this.app.get('/api/debug/memory', async () => {
|
||||
const mem = process.memoryUsage();
|
||||
const spawnState = this.spawnOrchestrator.getState();
|
||||
return {
|
||||
memory: {
|
||||
rss: mem.rss,
|
||||
rssMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
|
||||
heapUsed: mem.heapUsed,
|
||||
heapUsedMB: Math.round(mem.heapUsed / 1024 / 1024 * 10) / 10,
|
||||
heapTotal: mem.heapTotal,
|
||||
heapTotalMB: Math.round(mem.heapTotal / 1024 / 1024 * 10) / 10,
|
||||
external: mem.external,
|
||||
externalMB: Math.round(mem.external / 1024 / 1024 * 10) / 10,
|
||||
arrayBuffers: mem.arrayBuffers,
|
||||
arrayBuffersMB: Math.round(mem.arrayBuffers / 1024 / 1024 * 10) / 10,
|
||||
},
|
||||
counts: {
|
||||
sessions: this.sessions.size,
|
||||
sseClients: this.sseClients.size,
|
||||
respawnControllers: this.respawnControllers.size,
|
||||
runSummaryTrackers: this.runSummaryTrackers.size,
|
||||
scheduledRuns: this.scheduledRuns.size,
|
||||
terminalBatches: this.terminalBatches.size,
|
||||
outputBatches: this.outputBatches.size,
|
||||
pendingRespawnStarts: this.pendingRespawnStarts.size,
|
||||
respawnTimers: this.respawnTimers.size,
|
||||
subagents: subagentWatcher.getSubagents().length,
|
||||
},
|
||||
spawn: {
|
||||
activeAgents: spawnState.activeCount,
|
||||
queuedAgents: spawnState.queuedCount,
|
||||
totalSpawned: spawnState.totalSpawned,
|
||||
totalCompleted: spawnState.totalCompleted,
|
||||
totalFailed: spawnState.totalFailed,
|
||||
},
|
||||
uptime: {
|
||||
seconds: Math.round(process.uptime()),
|
||||
formatted: formatUptime(process.uptime()),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
});
|
||||
|
||||
// Session management
|
||||
this.app.get('/api/sessions', async () => this.getSessionsState());
|
||||
|
||||
@@ -759,6 +821,39 @@ export class WebServer extends EventEmitter {
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Reset circuit breaker for Ralph tracker
|
||||
this.app.post('/api/sessions/:id/ralph-circuit-breaker/reset', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = this.sessions.get(id);
|
||||
|
||||
if (!session) {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
||||
}
|
||||
|
||||
session.ralphTracker.resetCircuitBreaker();
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Get Ralph status block and circuit breaker state
|
||||
this.app.get('/api/sessions/:id/ralph-status', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = this.sessions.get(id);
|
||||
|
||||
if (!session) {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
lastStatusBlock: session.ralphTracker.lastStatusBlock,
|
||||
circuitBreaker: session.ralphTracker.circuitBreakerStatus,
|
||||
cumulativeStats: session.ralphTracker.cumulativeStats,
|
||||
exitGateMet: session.ralphTracker.exitGateMet,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Run prompt in session
|
||||
this.app.post('/api/sessions/:id/run', async (req): Promise<ApiResponse> => {
|
||||
const { id } = req.params as { id: string };
|
||||
@@ -2348,6 +2443,49 @@ export class WebServer extends EventEmitter {
|
||||
if (tracker) tracker.recordRalphCompletion(phrase);
|
||||
});
|
||||
|
||||
// RALPH_STATUS block events
|
||||
session.on('ralphStatusBlockDetected', (block: import('../types.js').RalphStatusBlock) => {
|
||||
this.broadcast('session:ralphStatusUpdate', { sessionId: session.id, block });
|
||||
// Track in run summary
|
||||
const tracker = this.runSummaryTrackers.get(session.id);
|
||||
if (tracker) {
|
||||
tracker.addEvent(
|
||||
block.status === 'BLOCKED' ? 'warning' : 'idle_detected',
|
||||
block.status === 'BLOCKED' ? 'warning' : 'info',
|
||||
`Ralph Status: ${block.status}`,
|
||||
`Tasks: ${block.tasksCompletedThisLoop}, Files: ${block.filesModified}, Tests: ${block.testsStatus}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
session.on('ralphCircuitBreakerUpdate', (status: import('../types.js').CircuitBreakerStatus) => {
|
||||
this.broadcast('session:circuitBreakerUpdate', { sessionId: session.id, status });
|
||||
// Track state changes in run summary
|
||||
const tracker = this.runSummaryTrackers.get(session.id);
|
||||
if (tracker && status.state === 'OPEN') {
|
||||
tracker.addEvent(
|
||||
'warning',
|
||||
'warning',
|
||||
'Circuit Breaker Opened',
|
||||
status.reason
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
session.on('ralphExitGateMet', (data: { completionIndicators: number; exitSignal: boolean }) => {
|
||||
this.broadcast('session:exitGateMet', { sessionId: session.id, ...data });
|
||||
// Track in run summary
|
||||
const tracker = this.runSummaryTrackers.get(session.id);
|
||||
if (tracker) {
|
||||
tracker.addEvent(
|
||||
'ralph_completion',
|
||||
'success',
|
||||
'Exit Gate Met',
|
||||
`Indicators: ${data.completionIndicators}, EXIT_SIGNAL: ${data.exitSignal}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Bash tool tracking events (for clickable file paths)
|
||||
session.on('bashToolStart', (tool: ActiveBashTool) => {
|
||||
this.broadcast('session:bashToolStart', { sessionId: session.id, tool });
|
||||
|
||||
Reference in New Issue
Block a user