mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +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:
+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;
|
||||
|
||||
Reference in New Issue
Block a user