feat: add smart respawn integration and priority todos (Phase 2)

Smart Respawn Integration (2.5):
- Check circuit breaker OPEN state before respawn (blocks cycle)
- Check RALPH_STATUS EXIT_SIGNAL=true to stop respawn
- Check STATUS=BLOCKED to pause respawn for human intervention
- Use RECOMMENDATION from RALPH_STATUS in update prompt
- Emit respawn:blocked SSE event with reason and details
- Show notification when respawn is blocked
- Visual blocked state with pulsing red indicator

Priority-based Todos (part of 2.3):
- Add RalphTodoPriority type (P0/P1/P2/null)
- Parse priority from todo content (P0:, Critical:, High Priority, etc.)
- Add priority field to RalphTodoItem interface
- Update frontend to display priority badges with colors
- Sort todos by priority (P0 > P1 > P2) then by status
- Add CSS for priority badges and highlighted cards

Bump version to 0.1391

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-27 00:54:02 +01:00
co-authored by Claude Opus 4.5
parent 59b22337a5
commit 925ea09cca
8 changed files with 217 additions and 23 deletions
+70 -9
View File
@@ -1294,6 +1294,33 @@ class ClaudemanApp {
}
});
this.eventSource.addEventListener('respawn:blocked', (e) => {
const data = JSON.parse(e.data);
const session = this.sessions.get(data.sessionId);
const reasonMap = {
circuit_breaker_open: 'Circuit Breaker Open',
exit_signal: 'Exit Signal Detected',
status_blocked: 'Claude Reported BLOCKED',
};
const title = reasonMap[data.reason] || 'Respawn Blocked';
this.notificationManager?.notify({
urgency: 'critical',
category: 'respawn-blocked',
sessionId: data.sessionId,
sessionName: session?.name || data.sessionId?.slice(0, 8),
title,
message: data.details,
});
// Update respawn panel to show blocked state
if (data.sessionId === this.activeSessionId) {
const stateEl = document.getElementById('respawnStateLabel');
if (stateEl) {
stateEl.textContent = title;
stateEl.classList.add('respawn-blocked');
}
}
});
this.eventSource.addEventListener('respawn:stepSent', (_e) => {
// Step info is shown via state label (e.g., "Sending prompt", "Clearing context")
});
@@ -3000,7 +3027,10 @@ class ClaudemanApp {
}
updateRespawnBanner(state) {
this.$('respawnState').textContent = this.getStateLabel(state);
const stateEl = this.$('respawnState');
stateEl.textContent = this.getStateLabel(state);
// Clear blocked state when state changes (resumed from blocked)
stateEl.classList.remove('respawn-blocked');
}
updateDetectionDisplay(detection) {
@@ -5531,10 +5561,14 @@ class ClaudemanApp {
return;
}
// Sort: in_progress first, then pending, then completed
// Sort: by priority (P0 > P1 > P2 > null), then by status (in_progress > pending > completed)
const priorityOrder = { 'P0': 0, 'P1': 1, 'P2': 2, null: 3 };
const statusOrder = { in_progress: 0, pending: 1, completed: 2 };
const sorted = [...todos].sort((a, b) => {
const order = { in_progress: 0, pending: 1, completed: 2 };
return (order[a.status] || 1) - (order[b.status] || 1);
const priA = priorityOrder[a.priority] ?? 3;
const priB = priorityOrder[b.priority] ?? 3;
if (priA !== priB) return priA - priB;
return (statusOrder[a.status] || 1) - (statusOrder[b.status] || 1);
});
// Incremental DOM update - reuse existing elements where possible
@@ -5548,11 +5582,13 @@ class ClaudemanApp {
sorted.forEach((todo, i) => {
const card = existingCards[i];
const statusClass = `task-${todo.status.replace('_', '-')}`;
const priorityClass = todo.priority ? `task-priority-${todo.priority.toLowerCase()}` : '';
const icon = this.getRalphTaskIcon(todo.status);
// Update class if changed
if (!card.classList.contains(statusClass)) {
card.className = `ralph-task-card ${statusClass}`;
const newClass = `ralph-task-card ${statusClass} ${priorityClass}`.trim();
if (card.className !== newClass) {
card.className = newClass;
}
// Update icon if changed
@@ -5561,6 +5597,22 @@ class ClaudemanApp {
iconEl.textContent = icon;
}
// Update priority badge
let badgeEl = card.querySelector('.ralph-task-priority');
if (todo.priority) {
if (!badgeEl) {
badgeEl = document.createElement('span');
badgeEl.className = `ralph-task-priority priority-${todo.priority.toLowerCase()}`;
card.insertBefore(badgeEl, card.querySelector('.ralph-task-content'));
}
if (badgeEl.textContent !== todo.priority) {
badgeEl.textContent = todo.priority;
badgeEl.className = `ralph-task-priority priority-${todo.priority.toLowerCase()}`;
}
} else if (badgeEl) {
badgeEl.remove();
}
// Update content if changed
const contentEl = card.querySelector('.ralph-task-content');
if (contentEl && contentEl.textContent !== todo.content) {
@@ -5572,18 +5624,27 @@ class ClaudemanApp {
sorted.forEach(todo => {
const card = document.createElement('div');
const statusClass = `task-${todo.status.replace('_', '-')}`;
card.className = `ralph-task-card ${statusClass}`;
const priorityClass = todo.priority ? `task-priority-${todo.priority.toLowerCase()}` : '';
card.className = `ralph-task-card ${statusClass} ${priorityClass}`.trim();
const iconSpan = document.createElement('span');
iconSpan.className = 'ralph-task-icon';
iconSpan.textContent = this.getRalphTaskIcon(todo.status);
card.appendChild(iconSpan);
// Add priority badge if present
if (todo.priority) {
const prioritySpan = document.createElement('span');
prioritySpan.className = `ralph-task-priority priority-${todo.priority.toLowerCase()}`;
prioritySpan.textContent = todo.priority;
card.appendChild(prioritySpan);
}
const contentSpan = document.createElement('span');
contentSpan.className = 'ralph-task-content';
contentSpan.textContent = todo.content;
card.appendChild(iconSpan);
card.appendChild(contentSpan);
fragment.appendChild(card);
});
+45
View File
@@ -620,6 +620,17 @@ body {
font-size: 0.7rem;
}
.respawn-state.respawn-blocked {
background: rgba(244, 67, 54, 0.3);
color: #f44336;
animation: respawn-blocked-pulse 1.5s ease-in-out infinite;
}
@keyframes respawn-blocked-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* Respawn spacer removed - using flex layout */
.respawn-cycles {
@@ -1305,6 +1316,40 @@ body {
color: var(--text-dim);
}
/* Priority badges */
.ralph-task-priority {
font-size: 0.55rem;
font-weight: 700;
padding: 0.1rem 0.25rem;
border-radius: 2px;
flex-shrink: 0;
text-transform: uppercase;
}
.ralph-task-priority.priority-p0 {
background: rgba(244, 67, 54, 0.3);
color: #f44336;
}
.ralph-task-priority.priority-p1 {
background: rgba(255, 193, 7, 0.3);
color: #ffc107;
}
.ralph-task-priority.priority-p2 {
background: rgba(33, 150, 243, 0.2);
color: #2196f3;
}
/* Priority-based card highlighting */
.ralph-task-card.task-priority-p0 {
border-left-color: #f44336;
}
.ralph-task-card.task-priority-p1:not(.task-completed) {
border-left-color: #ffc107;
}
/* Empty state */
.ralph-state-empty {
color: var(--text-muted);
+7
View File
@@ -2532,6 +2532,13 @@ export class WebServer extends EventEmitter {
this.broadcast('respawn:cycleCompleted', { sessionId, cycleNumber });
});
controller.on('respawnBlocked', (data: { reason: string; details: string }) => {
this.broadcast('respawn:blocked', { sessionId, reason: data.reason, details: data.details });
// Track in run summary (lazy lookup)
const tracker = getTracker();
if (tracker) tracker.recordWarning(`Respawn blocked: ${data.reason}`, data.details);
});
controller.on('stepSent', (step: string, input: string) => {
this.broadcast('respawn:stepSent', { sessionId, step, input });
});