refactor: rename inner-loop-tracker to ralph-tracker with API improvements

- Rename inner-loop-tracker.ts → ralph-tracker.ts throughout codebase
- Add ralph-config.ts for parsing .claude/ralph-loop.local.md config
- Standardize API error responses using createErrorResponse()
- Add input validation for auto-compact/auto-clear thresholds
- Update UI labels to "Ralph / Todo Tracker" consistently
- Add 46 integration tests for Ralph tracking functionality
- Update test badge to 438 total tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-21 22:36:45 +01:00
co-authored by Claude Opus 4.5
parent 5be0babf01
commit 0d1ca37b6e
21 changed files with 2151 additions and 451 deletions
+6 -6
View File
@@ -9,7 +9,7 @@
* - **Main view**: Active session management with:
* - TabBar: Session tabs with switching
* - TerminalView: Live terminal output display
* - RalphPanel: Inner loop tracking (conditional)
* - RalphPanel: Ralph loop tracking (conditional)
* - StatusBar: Session info and keyboard hints
*
* Keyboard shortcuts are handled globally via Ink's useInput hook.
@@ -116,8 +116,8 @@ export function App(): React.ReactElement {
prevSession,
sendInput,
terminalOutput,
innerLoopState,
innerTodos,
ralphLoopState,
ralphTodos,
respawnStatus,
cases,
lastUsedCase,
@@ -427,7 +427,7 @@ export function App(): React.ReactElement {
}
// Check if Ralph panel should be visible (enabled and has data)
const showRalphPanel = innerLoopState?.enabled && (innerLoopState.active || innerTodos.length > 0);
const showRalphPanel = ralphLoopState?.enabled && (ralphLoopState.active || ralphTodos.length > 0);
// Adjust terminal height if Ralph panel is shown
const adjustedTerminalHeight = showRalphPanel ? terminalHeight - 6 : terminalHeight;
@@ -449,8 +449,8 @@ export function App(): React.ReactElement {
{showRalphPanel && (
<RalphPanel
loopState={innerLoopState}
todos={innerTodos}
loopState={ralphLoopState}
todos={ralphTodos}
/>
)}
+5 -5
View File
@@ -11,17 +11,17 @@
* - Progress through todos with visual indicators
* - Cycle count and elapsed time tracking
*
* @see {@link file://./../../inner-loop-tracker.ts} for detection logic
* @see {@link file://./../../ralph-tracker.ts} for detection logic
* @see {@link file://./../../../docs/ralph-wiggum-guide.md} for full documentation
*/
import React from 'react';
import { Box, Text } from 'ink';
import type { InnerLoopState, InnerTodoItem } from '../../types.js';
import type { RalphTrackerState, RalphTodoItem } from '../../types.js';
interface RalphPanelProps {
loopState: InnerLoopState | null;
todos: InnerTodoItem[];
loopState: RalphTrackerState | null;
todos: RalphTodoItem[];
visible?: boolean;
}
@@ -70,7 +70,7 @@ function formatElapsed(hours: number | null): string {
* Only renders when loop is enabled and has relevant data to show.
*
* @param props - Component props
* @param props.loopState - Current loop state from InnerLoopTracker
* @param props.loopState - Current loop state from RalphTracker
* @param props.todos - Array of todo items being tracked
* @param props.visible - Whether the panel should be visible (default: true)
* @returns The panel element or null if hidden/disabled
+24 -24
View File
@@ -7,7 +7,7 @@
* This hook provides a complete interface for managing Claude sessions:
* - Session discovery from ~/.claudeman/screens.json
* - Terminal output polling via GNU screen hardcopy
* - Inner loop state tracking for Ralph Wiggum loops
* - Ralph state tracking for Ralph Wiggum loops
* - Respawn status monitoring via the Claudeman API
*
* @example
@@ -29,12 +29,12 @@ import { homedir } from 'os';
import { join } from 'path';
import { execSync, exec } from 'child_process';
import { promisify } from 'util';
import type { ScreenSession, InnerLoopState, InnerTodoItem, InnerSessionState } from '../../types.js';
import type { ScreenSession, RalphTrackerState, RalphTodoItem, RalphSessionState } from '../../types.js';
const execAsync = promisify(exec);
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
const INNER_STATE_FILE = join(homedir(), '.claudeman', 'state-inner.json');
const RALPH_STATE_FILE = join(homedir(), '.claudeman', 'state-inner.json');
const SETTINGS_FILE = join(homedir(), '.claudeman', 'settings.json');
const OUTPUT_POLL_INTERVAL = 300; // Poll terminal output every 300ms (faster refresh)
@@ -149,8 +149,8 @@ interface SessionManagerState {
activeSessionId: string | null;
activeSession: ScreenSession | null;
terminalOutput: string;
innerLoopState: InnerLoopState | null;
innerTodos: InnerTodoItem[];
ralphLoopState: RalphTrackerState | null;
ralphTodos: RalphTodoItem[];
respawnStatus: RespawnStatus | null;
cases: CaseInfo[];
lastUsedCase: string | null;
@@ -237,18 +237,18 @@ function loadSessions(): ScreenSession[] {
*
* @description
* Reads ~/.claudeman/state-inner.json which contains per-session
* tracking of inner loops, todos, and completion phrases.
* tracking of Ralph loops, todos, and completion phrases.
*
* @param sessionId - The UUID of the session to load state for
* @returns The inner session state or null if not found
* @returns The Ralph session state or null if not found
*/
function loadInnerState(sessionId: string): InnerSessionState | null {
function loadRalphState(sessionId: string): RalphSessionState | null {
try {
if (!existsSync(INNER_STATE_FILE)) {
if (!existsSync(RALPH_STATE_FILE)) {
return null;
}
const data = readFileSync(INNER_STATE_FILE, 'utf-8');
const allStates = JSON.parse(data) as Record<string, InnerSessionState>;
const data = readFileSync(RALPH_STATE_FILE, 'utf-8');
const allStates = JSON.parse(data) as Record<string, RalphSessionState>;
return allStates[sessionId] || null;
} catch {
return null;
@@ -284,7 +284,7 @@ function loadLastUsedCase(): string | null {
* Provides complete session lifecycle management:
* - Automatic session discovery and status monitoring
* - Terminal output polling (500ms interval)
* - Inner loop state tracking (500ms interval)
* - Ralph state tracking (500ms interval)
* - Respawn status polling via API (2000ms interval)
* - Session CRUD operations via Claudeman API
*
@@ -311,8 +311,8 @@ export function useSessionManager(): SessionManagerState {
const [sessions, setSessions] = useState<ScreenSession[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const [terminalOutput, setTerminalOutput] = useState<string>('');
const [innerLoopState, setInnerLoopState] = useState<InnerLoopState | null>(null);
const [innerTodos, setInnerTodos] = useState<InnerTodoItem[]>([]);
const [ralphLoopState, setRalphTrackerState] = useState<RalphTrackerState | null>(null);
const [ralphTodos, setInnerTodos] = useState<RalphTodoItem[]>([]);
const [respawnStatus, setRespawnStatus] = useState<RespawnStatus | null>(null);
const [cases, setCases] = useState<CaseInfo[]>([]);
const [lastUsedCase, setLastUsedCase] = useState<string | null>(loadLastUsedCase);
@@ -346,30 +346,30 @@ export function useSessionManager(): SessionManagerState {
// Get active session
const activeSession = sessions.find((s) => s.sessionId === activeSessionId) || null;
// Poll inner state for active session
// Poll Ralph state for active session
useEffect(() => {
if (!activeSessionId) {
setInnerLoopState(null);
setRalphTrackerState(null);
setInnerTodos([]);
return;
}
const pollInnerState = () => {
const state = loadInnerState(activeSessionId);
const pollRalphState = () => {
const state = loadRalphState(activeSessionId);
if (state) {
setInnerLoopState(state.loop);
setRalphTrackerState(state.loop);
setInnerTodos(state.todos);
} else {
setInnerLoopState(null);
setRalphTrackerState(null);
setInnerTodos([]);
}
};
// Initial poll
pollInnerState();
pollRalphState();
// Set up polling interval (same as output polling)
const intervalId = setInterval(pollInnerState, OUTPUT_POLL_INTERVAL);
const intervalId = setInterval(pollRalphState, OUTPUT_POLL_INTERVAL);
return () => {
clearInterval(intervalId);
@@ -809,8 +809,8 @@ export function useSessionManager(): SessionManagerState {
activeSessionId,
activeSession,
terminalOutput,
innerLoopState,
innerTodos,
ralphLoopState,
ralphTodos,
respawnStatus,
cases,
lastUsedCase,