From 0d1ca37b6e5aa9780a7961d3d270861608fc52f2 Mon Sep 17 00:00:00 2001
From: arkon
Date: Wed, 21 Jan 2026 22:36:45 +0100
Subject: [PATCH] refactor: rename inner-loop-tracker to ralph-tracker with API
improvements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
CLAUDE.md | 79 +-
README.md | 2 +-
src/ralph-config.ts | 153 +++
...inner-loop-tracker.ts => ralph-tracker.ts} | 66 +-
src/screen-manager.ts | 6 +-
src/session.ts | 92 +-
src/state-store.ts | 92 +-
src/tui/App.tsx | 12 +-
src/tui/components/RalphPanel.tsx | 10 +-
src/tui/hooks/useSessionManager.ts | 48 +-
src/types.ts | 40 +-
src/web/public/app.js | 124 +-
src/web/public/index.html | 14 +-
src/web/public/styles.css | 4 +-
src/web/server.ts | 270 +++--
test/ralph-config.test.ts | 420 +++++++
test/ralph-integration.test.ts | 1025 +++++++++++++++++
...-tracker.test.ts => ralph-tracker.test.ts} | 68 +-
test/session.test.ts | 7 +-
test/state-store.test.ts | 54 +-
test/types.test.ts | 16 +-
21 files changed, 2151 insertions(+), 451 deletions(-)
create mode 100644 src/ralph-config.ts
rename src/{inner-loop-tracker.ts => ralph-tracker.ts} (96%)
create mode 100644 test/ralph-config.test.ts
create mode 100644 test/ralph-integration.test.ts
rename test/{inner-loop-tracker.test.ts => ralph-tracker.test.ts} (91%)
diff --git a/CLAUDE.md b/CLAUDE.md
index c067ea80..2d19f86a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -65,8 +65,9 @@ npx vitest run -t "should create session" # By pattern
# 3110: edge-cases.test.ts
# 3115: integration-flows.test.ts
# 3120: session-cleanup.test.ts
-# Unit tests (no port needed): respawn-controller, inner-loop-tracker, pty-interactive, task-queue, task, ralph-loop, session-manager, state-store, types, templates
-# Next available: 3122+
+# 3125: ralph-integration.test.ts
+# Unit tests (no port needed): respawn-controller, ralph-tracker, pty-interactive, task-queue, task, ralph-loop, session-manager, state-store, types, templates, ralph-config
+# Next available: 3127+
# Tests mock PTY - no real Claude CLI spawned
# Test timeout: 30s (configured in vitest.config.ts)
@@ -82,7 +83,7 @@ screen -r # Attach to screen session (Ctrl+A D t
curl localhost:3000/api/sessions # Check active sessions
curl localhost:3000/api/status | jq . # Full app state including respawn
cat ~/.claudeman/state.json | jq . # View main state
-cat ~/.claudeman/state-inner.json | jq . # View inner loop state
+cat ~/.claudeman/state-inner.json | jq . # View Ralph loop state
# Kill stuck screen sessions
screen -X -S quit # Graceful quit
@@ -98,7 +99,8 @@ pkill -f "SCREEN.*claudeman" # Force kill all claudeman screens
| `src/session.ts` | Core PTY wrapper for Claude CLI. Modes: `runPrompt()`, `startInteractive()`, `startShell()` |
| `src/respawn-controller.ts` | State machine for autonomous session cycling |
| `src/screen-manager.ts` | GNU screen persistence, ghost discovery, 4-strategy kill |
-| `src/inner-loop-tracker.ts` | Detects `PHRASE `, todos, loop status in output |
+| `src/ralph-tracker.ts` | Detects `PHRASE `, todos, loop status in output |
+| `src/ralph-config.ts` | Parses `.claude/ralph-loop.local.md` and CLAUDE.md for Ralph config |
| `src/task-tracker.ts` | Parses background task output (agent IDs, status) from Claude CLI |
| `src/session-manager.ts` | Manages session lifecycle, task assignment, and cleanup |
| `src/state-store.ts` | JSON persistence to `~/.claudeman/` with debounced writes |
@@ -259,18 +261,35 @@ curl -X POST localhost:3000/api/sessions/:id/input \
Both wait for idle. Configure via `session.setAutoCompact()` / `session.setAutoClear()`.
-### Inner Loop Tracking
+### Ralph / Todo Tracking
Detects Ralph loops and todos inside Claude sessions. **Disabled by default** but auto-enables when any of these patterns are detected in terminal output:
- `/ralph-loop:ralph-loop` command
-- `PHRASE ` completion phrases
+- `PHRASE ` completion phrases (supports hyphens: `TESTS-PASS`, underscores: `ALL_DONE`, numbers: `TASK_123`)
- `TodoWrite` tool usage
- Iteration patterns (`Iteration 5/50`, `[5/50]`)
- Todo checkboxes (`- [ ]`/`- [x]`) or indicator icons (`☐`/`◐`/`✓`)
- "All tasks complete" messages
- Individual task completion signals (`Task 8 is done`)
-See `inner-loop-tracker.ts:shouldAutoEnable()` for detection logic.
+See `ralph-tracker.ts:shouldAutoEnable()` for detection logic.
+
+**Auto-Configuration from Ralph Plugin State**: When a session starts, Claudeman reads `.claude/ralph-loop.local.md` (the official Ralph Wiggum plugin state file) to auto-configure the tracker:
+
+```yaml
+---
+enabled: true
+iteration: 5
+max-iterations: 50
+completion-promise: "COMPLETE"
+---
+```
+
+Priority order for configuration:
+1. `.claude/ralph-loop.local.md` (official Ralph plugin state)
+2. `CLAUDE.md` `` tags (fallback)
+
+See `src/ralph-config.ts` for parsing logic.
**Completion Detection**: Uses multi-strategy detection:
- 1st occurrence of `PHRASE `: Stores as expected phrase (likely in prompt)
@@ -287,8 +306,8 @@ See `inner-loop-tracker.ts:shouldAutoEnable()` for detection logic.
- `tracker.fullReset()` → Complete reset to initial state
**API**:
-- `GET /api/sessions/:id/inner-state` - Get loop state and todos
-- `POST /api/sessions/:id/inner-config` - Configure tracker:
+- `GET /api/sessions/:id/ralph-state` - Get loop state and todos
+- `POST /api/sessions/:id/ralph-config` - Configure tracker:
- `{ enabled: boolean }` - Enable/disable
- `{ reset: true }` - Soft reset (keep enabled)
- `{ reset: "full" }` - Full reset
@@ -309,7 +328,7 @@ Key events for frontend handling (see `app.js:handleSSEEvent()`):
- `session:idle`, `session:working` - Status indicator updates
- `session:terminal`, `session:clearTerminal` - Terminal content
- `session:completion`, `session:autoClear`, `session:autoCompact` - Lifecycle events
-- `session:innerLoopUpdate`, `session:innerTodoUpdate`, `session:innerCompletionDetected` - Ralph tracking
+- `session:ralphLoopUpdate`, `session:ralphTodoUpdate`, `session:ralphCompletionDetected` - Ralph tracking
### Frontend (app.js)
@@ -337,9 +356,9 @@ Writes debounced to `~/.claudeman/state.json`. Batches rapid changes.
| Terminal batch interval | 16ms | `server.ts` (60fps) |
| Output batch interval | 50ms | `server.ts` |
| Task update batch interval | 100ms | `server.ts` |
-| Inner loop event debounce | 50ms | `inner-loop-tracker.ts` |
+| Ralph loop event debounce | 50ms | `ralph-tracker.ts` |
| Session tabs render debounce | 100ms | `app.js` |
-| Inner panel render debounce | 50ms | `app.js` |
+| Ralph panel render debounce | 50ms | `app.js` |
| Task panel render debounce | 100ms | `app.js` |
| Input batch interval | 16ms | `app.js` (60fps) |
| Idle activity timeout | 2s | `session.ts` |
@@ -354,8 +373,10 @@ Module resolution: NodeNext. Target: ES2022. Strict mode with additional checks:
| `noUnusedLocals` | Error on unused local variables |
| `noUnusedParameters` | Error on unused function parameters |
| `noImplicitReturns` | All code paths must return a value |
+| `noImplicitOverride` | Require `override` keyword for overridden methods |
| `noFallthroughCasesInSwitch` | Require break/return in switch cases |
| `allowUnreachableCode: false` | Error on unreachable code |
+| `allowUnusedLabels: false` | Error on unused labels |
TUI uses React JSX (`jsxImportSource: react`) for Ink components.
@@ -386,6 +407,32 @@ Use `createErrorResponse(code, details?)` from `types.ts`:
- **Ghost discovery**: `reconcileScreens()` finds orphaned screens on startup
- **Cleanup** (`cleanupSession()`): stops respawn, clears buffers/timers, kills screen
+## TUI Architecture (Ink/React)
+
+The TUI (`claudeman tui`) is built with [Ink](https://github.com/vadimdemedes/ink) (React for terminals).
+
+**Component Hierarchy:**
+```
+App.tsx
+├── StartScreen.tsx # Session/case list, navigation
+│ ├── List navigation (↑/↓, Enter, a/d/D)
+│ ├── Case creation flow
+│ └── Tab switcher menu
+├── TabBar.tsx # Session tabs (when attached)
+├── TerminalView.tsx # Viewport into screen session
+├── StatusBar.tsx # Bottom bar with status/tokens
+├── RalphPanel.tsx # Ralph loop progress display
+└── HelpOverlay.tsx # Keyboard shortcuts modal
+```
+
+**Key Hook:** `useSessionManager.ts` handles:
+- API polling for sessions/screens
+- Screen attach/detach flow
+- Keyboard input routing
+- State synchronization with web server
+
+**TUI ↔ Web Server:** The TUI is a client to the web server. It doesn't manage sessions directly - it uses `/api/*` endpoints and attaches to screens via GNU screen commands.
+
## Buffer Limits
Long-running sessions are supported with automatic trimming:
@@ -413,8 +460,8 @@ Long-running sessions are supported with automatic trimming:
| POST | `/api/sessions/:id/respawn/stop` | Stop respawn controller |
| POST | `/api/sessions/:id/respawn/enable` | Enable respawn with config + optional timer |
| PUT | `/api/sessions/:id/respawn/config` | Update config on running respawn |
-| POST | `/api/sessions/:id/inner-config` | Configure Ralph Wiggum loop settings |
-| GET | `/api/sessions/:id/inner-state` | Get Ralph loop state + todos |
+| POST | `/api/sessions/:id/ralph-config` | Configure Ralph / Todo Tracker settings |
+| GET | `/api/sessions/:id/ralph-state` | Get Ralph loop state + todos |
| POST | `/api/sessions/:id/auto-compact` | Configure auto-compact threshold |
| POST | `/api/sessions/:id/auto-clear` | Configure auto-clear threshold |
| POST | `/api/quick-start` | Create case + start session. Body: `{mode?: 'claude'\|'shell'}` |
@@ -478,7 +525,7 @@ Long-running sessions are supported with automatic trimming:
| File | Purpose |
|------|---------|
| `~/.claudeman/state.json` | Sessions, tasks, config |
-| `~/.claudeman/state-inner.json` | Inner loop/todo state (separate to reduce writes) |
+| `~/.claudeman/state-inner.json` | Ralph loop/todo state per session (separate to reduce writes) |
| `~/.claudeman/screens.json` | Screen session metadata |
Cases created in `~/claudeman-cases/` by default.
@@ -540,6 +587,6 @@ Extended documentation is available in the `docs/` directory:
/ralph-loop:help # Show help and usage
```
-**Claudeman Implementation**: The `InnerLoopTracker` class (`src/inner-loop-tracker.ts`) detects Ralph patterns in Claude output and tracks loop state, todos, and completion phrases. It auto-enables when Ralph-related patterns are detected.
+**Claudeman Implementation**: The `RalphTracker` class (`src/ralph-tracker.ts`) detects Ralph patterns in Claude output and tracks loop state, todos, and completion phrases. It auto-enables when Ralph-related patterns are detected.
See [`docs/ralph-wiggum-guide.md`](docs/ralph-wiggum-guide.md) for full documentation on best practices, prompt templates, and troubleshooting.
diff --git a/README.md b/README.md
index ad77470e..c3f0e160 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
---
diff --git a/src/ralph-config.ts b/src/ralph-config.ts
new file mode 100644
index 00000000..5f480519
--- /dev/null
+++ b/src/ralph-config.ts
@@ -0,0 +1,153 @@
+/**
+ * @fileoverview Ralph Wiggum configuration parser
+ *
+ * Parses the official Ralph Wiggum plugin state file (.claude/ralph-loop.local.md)
+ * and extracts configuration for session tracking.
+ *
+ * @module ralph-config
+ */
+
+import { existsSync, readFileSync } from 'node:fs';
+import { join } from 'node:path';
+
+// Pattern to extract completion phrase from CLAUDE.md
+// Matches PHRASE with optional whitespace
+// Allows: letters, numbers, underscores, hyphens (e.g., TESTS-PASS, ALL_DONE)
+const CLAUDE_MD_PROMISE_PATTERN = /\s*([A-Z0-9_-]+)\s*<\/promise>/gi;
+
+// Pattern to parse YAML frontmatter from ralph-loop.local.md
+// Extracts key: value pairs from between --- markers
+const YAML_FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---/;
+const YAML_LINE_PATTERN = /^([a-z-]+):\s*"?([^"\n]+)"?\s*$/gm;
+
+/**
+ * Ralph Loop configuration from .claude/ralph-loop.local.md
+ */
+export interface RalphLoopConfig {
+ enabled: boolean;
+ iteration: number;
+ maxIterations: number | null;
+ completionPromise: string | null;
+}
+
+/**
+ * Parse Ralph Wiggum loop config from .claude/ralph-loop.local.md
+ *
+ * The official Ralph Wiggum plugin stores state in this file with YAML frontmatter:
+ * ```yaml
+ * ---
+ * enabled: true
+ * iteration: 5
+ * max-iterations: 50
+ * completion-promise: "COMPLETE"
+ * ---
+ * # Original Prompt
+ * ...
+ * ```
+ *
+ * @param workingDir - Working directory to search in
+ * @returns Parsed config or null if file doesn't exist or is invalid
+ */
+export function parseRalphLoopConfig(workingDir: string): RalphLoopConfig | null {
+ const configPath = join(workingDir, '.claude', 'ralph-loop.local.md');
+
+ try {
+ if (!existsSync(configPath)) return null;
+ const content = readFileSync(configPath, 'utf-8');
+
+ return parseRalphLoopConfigFromContent(content);
+ } catch (err) {
+ console.error(`[ralph-config] Error reading ralph-loop.local.md: ${err}`);
+ return null;
+ }
+}
+
+/**
+ * Parse Ralph Wiggum loop config from file content.
+ * Exported for testing.
+ *
+ * @param content - File content to parse
+ * @returns Parsed config or null if content is invalid
+ */
+export function parseRalphLoopConfigFromContent(content: string): RalphLoopConfig | null {
+ // Extract YAML frontmatter
+ const frontmatterMatch = content.match(YAML_FRONTMATTER_PATTERN);
+ if (!frontmatterMatch) return null;
+
+ const yaml = frontmatterMatch[1];
+ const config: RalphLoopConfig = {
+ enabled: false,
+ iteration: 0,
+ maxIterations: null,
+ completionPromise: null,
+ };
+
+ // Parse each YAML line
+ let match;
+ YAML_LINE_PATTERN.lastIndex = 0;
+ while ((match = YAML_LINE_PATTERN.exec(yaml)) !== null) {
+ const key = match[1].toLowerCase();
+ const value = match[2].trim();
+
+ switch (key) {
+ case 'enabled':
+ config.enabled = value === 'true';
+ break;
+ case 'iteration':
+ config.iteration = parseInt(value, 10) || 0;
+ break;
+ case 'max-iterations':
+ config.maxIterations = parseInt(value, 10) || null;
+ break;
+ case 'completion-promise':
+ config.completionPromise = value.toUpperCase();
+ break;
+ }
+ }
+
+ return config;
+}
+
+/**
+ * Extract completion phrase from CLAUDE.md content.
+ * Looks for PHRASE pattern.
+ *
+ * Handles multiple variations:
+ * - Raw text: PHRASE
+ * - In backticks: `PHRASE `
+ * - With whitespace: PHRASE
+ * - Multiple occurrences: returns the first one
+ *
+ * @param claudeMdPath - Path to CLAUDE.md file
+ * @returns The completion phrase (uppercase), or null if not found
+ */
+export function extractCompletionPhrase(claudeMdPath: string): string | null {
+ try {
+ if (!existsSync(claudeMdPath)) return null;
+ const content = readFileSync(claudeMdPath, 'utf-8');
+
+ return extractCompletionPhraseFromContent(content);
+ } catch (err) {
+ console.error(`[ralph-config] Error reading CLAUDE.md: ${err}`);
+ return null;
+ }
+}
+
+/**
+ * Extract completion phrase from CLAUDE.md content string.
+ * Exported for testing.
+ *
+ * @param content - CLAUDE.md content to parse
+ * @returns The completion phrase (uppercase), or null if not found
+ */
+export function extractCompletionPhraseFromContent(content: string): string | null {
+ // Reset regex state (global flag)
+ CLAUDE_MD_PROMISE_PATTERN.lastIndex = 0;
+
+ // Find all matches and return the first one
+ const match = CLAUDE_MD_PROMISE_PATTERN.exec(content);
+ if (match && match[1]) {
+ return match[1].trim().toUpperCase();
+ }
+ return null;
+}
diff --git a/src/inner-loop-tracker.ts b/src/ralph-tracker.ts
similarity index 96%
rename from src/inner-loop-tracker.ts
rename to src/ralph-tracker.ts
index cb3c0219..93243d62 100644
--- a/src/inner-loop-tracker.ts
+++ b/src/ralph-tracker.ts
@@ -1,5 +1,5 @@
/**
- * @fileoverview Inner Loop Tracker for Ralph Wiggum detection
+ * @fileoverview Ralph Tracker - Detects Ralph Wiggum loops, todos, and completion phrases
*
* This module parses terminal output from Claude Code sessions to detect:
* - Ralph Wiggum loop state (active, completion phrase, iteration count)
@@ -10,15 +10,15 @@
* patterns are detected in the output stream, reducing overhead for
* sessions not using autonomous loops.
*
- * @module inner-loop-tracker
+ * @module ralph-tracker
*/
import { EventEmitter } from 'node:events';
import {
- InnerLoopState,
- InnerTodoItem,
- InnerTodoStatus,
- createInitialInnerLoopState,
+ RalphTrackerState,
+ RalphTodoItem,
+ RalphTodoStatus,
+ createInitialRalphTrackerState,
} from './types.js';
// ========== Configuration Constants ==========
@@ -56,6 +56,12 @@ const EVENT_DEBOUNCE_MS = 50;
* Matches completion phrase tags: `PHRASE `
* Used to detect when Claude signals task completion.
* Capture group 1: The completion phrase text
+ *
+ * Supports any characters between tags including:
+ * - Uppercase letters: COMPLETE, DONE
+ * - Numbers: TASK_123
+ * - Underscores: ALL_TASKS_DONE
+ * - Hyphens: TESTS-PASS, TIME-COMPLETE
*/
const PROMISE_PATTERN = /([^<]+)<\/promise>/;
@@ -194,17 +200,17 @@ const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
// ========== Event Types ==========
/**
- * Events emitted by InnerLoopTracker
+ * Events emitted by RalphTracker
* @event loopUpdate - Fired when loop state changes (active, iteration, completion phrase)
* @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
*/
-export interface InnerLoopTrackerEvents {
+export interface RalphTrackerEvents {
/** Emitted when loop state changes */
- loopUpdate: (state: InnerLoopState) => void;
+ loopUpdate: (state: RalphTrackerState) => void;
/** Emitted when todo list is modified */
- todoUpdate: (todos: InnerTodoItem[]) => void;
+ todoUpdate: (todos: RalphTodoItem[]) => void;
/** Emitted when completion phrase detected (loop finished) */
completionDetected: (phrase: string) => void;
/** Emitted when tracker auto-enables from disabled state */
@@ -212,7 +218,7 @@ export interface InnerLoopTrackerEvents {
}
/**
- * InnerLoopTracker - Parses terminal output to detect Ralph Wiggum loops and todos
+ * RalphTracker - Parses terminal output to detect Ralph Wiggum loops and todos
*
* This class monitors Claude Code session output to detect:
* 1. **Ralph Wiggum loop state** - Active loops, completion phrases, iteration counts
@@ -242,19 +248,19 @@ export interface InnerLoopTrackerEvents {
* @extends EventEmitter
* @example
* ```typescript
- * const tracker = new InnerLoopTracker();
+ * const tracker = new RalphTracker();
* tracker.on('completionDetected', (phrase) => {
* console.log('Loop completed with phrase:', phrase);
* });
* tracker.processTerminalData(ptyOutput);
* ```
*/
-export class InnerLoopTracker extends EventEmitter {
+export class RalphTracker extends EventEmitter {
/** Current state of the detected loop */
- private _loopState: InnerLoopState;
+ private _loopState: RalphTrackerState;
/** Map of todo items by ID for O(1) lookup */
- private _todos: Map = new Map();
+ private _todos: Map = new Map();
/** Buffer for incomplete lines from terminal data */
private _lineBuffer: string = '';
@@ -284,12 +290,12 @@ export class InnerLoopTracker extends EventEmitter {
private _autoEnableDisabled: boolean = false;
/**
- * Creates a new InnerLoopTracker instance.
+ * Creates a new RalphTracker instance.
* Starts in disabled state until Ralph patterns are detected.
*/
constructor() {
super();
- this._loopState = createInitialInnerLoopState();
+ this._loopState = createInitialRalphTrackerState();
}
/**
@@ -373,7 +379,7 @@ export class InnerLoopTracker extends EventEmitter {
this.clearDebounceTimers();
const wasEnabled = this._loopState.enabled;
- this._loopState = createInitialInnerLoopState();
+ this._loopState = createInitialRalphTrackerState();
this._loopState.enabled = wasEnabled; // Keep enabled status
this._todos.clear();
this._completionPhraseCount.clear();
@@ -394,7 +400,7 @@ export class InnerLoopTracker extends EventEmitter {
// Clear debounce timers
this.clearDebounceTimers();
- this._loopState = createInitialInnerLoopState();
+ this._loopState = createInitialRalphTrackerState();
this._todos.clear();
this._completionPhraseCount.clear();
this._lineBuffer = '';
@@ -489,7 +495,7 @@ export class InnerLoopTracker extends EventEmitter {
* Get a copy of the current loop state.
* @returns Shallow copy of loop state (safe to modify)
*/
- get loopState(): InnerLoopState {
+ get loopState(): RalphTrackerState {
return { ...this._loopState };
}
@@ -497,7 +503,7 @@ export class InnerLoopTracker extends EventEmitter {
* Get all tracked todo items as an array.
* @returns Array of todo items (copy, safe to modify)
*/
- get todos(): InnerTodoItem[] {
+ get todos(): RalphTodoItem[] {
return Array.from(this._todos.values());
}
@@ -1032,7 +1038,7 @@ export class InnerLoopTracker extends EventEmitter {
while ((match = TODO_CHECKBOX_PATTERN.exec(line)) !== null) {
const checked = match[1].toLowerCase() === 'x';
const content = match[2].trim();
- const status: InnerTodoStatus = checked ? 'completed' : 'pending';
+ const status: RalphTodoStatus = checked ? 'completed' : 'pending';
this.upsertTodo(content, status);
updated = true;
}
@@ -1057,7 +1063,7 @@ export class InnerLoopTracker extends EventEmitter {
TODO_STATUS_PATTERN.lastIndex = 0;
while ((match = TODO_STATUS_PATTERN.exec(line)) !== null) {
const content = match[1].trim();
- const status = match[2] as InnerTodoStatus;
+ const status = match[2] as RalphTodoStatus;
this.upsertTodo(content, status);
updated = true;
}
@@ -1099,9 +1105,9 @@ export class InnerLoopTracker extends EventEmitter {
* - Pending: `☐`, `○`, and anything else (default)
*
* @param icon - Single character icon
- * @returns Corresponding InnerTodoStatus
+ * @returns Corresponding RalphTodoStatus
*/
- private iconToStatus(icon: string): InnerTodoStatus {
+ private iconToStatus(icon: string): RalphTodoStatus {
switch (icon) {
case '✓':
case '✅':
@@ -1134,7 +1140,7 @@ export class InnerLoopTracker extends EventEmitter {
* @param content - Raw todo content text
* @param status - Status to set
*/
- private upsertTodo(content: string, status: InnerTodoStatus): void {
+ private upsertTodo(content: string, status: RalphTodoStatus): void {
// Skip empty or whitespace-only content
if (!content || !content.trim()) return;
@@ -1225,8 +1231,8 @@ export class InnerLoopTracker extends EventEmitter {
* Used for LRU eviction when at MAX_TODO_ITEMS limit.
* @returns Oldest todo item, or undefined if map is empty
*/
- private findOldestTodo(): InnerTodoItem | undefined {
- let oldest: InnerTodoItem | undefined;
+ private findOldestTodo(): RalphTodoItem | undefined {
+ let oldest: RalphTodoItem | undefined;
for (const todo of this._todos.values()) {
if (!oldest || todo.detectedAt < oldest.detectedAt) {
oldest = todo;
@@ -1334,7 +1340,7 @@ export class InnerLoopTracker extends EventEmitter {
* @fires todoUpdate - With empty array
*/
clear(): void {
- this._loopState = createInitialInnerLoopState(); // This sets enabled: false
+ this._loopState = createInitialRalphTrackerState(); // This sets enabled: false
this._todos.clear();
this._lineBuffer = '';
this._completionPhraseCount.clear();
@@ -1389,7 +1395,7 @@ export class InnerLoopTracker extends EventEmitter {
* @param loopState - Persisted loop state object
* @param todos - Persisted todo items array
*/
- restoreState(loopState: InnerLoopState, todos: InnerTodoItem[]): void {
+ restoreState(loopState: RalphTrackerState, todos: RalphTodoItem[]): void {
// Ensure enabled flag exists (backwards compatibility)
this._loopState = {
...loopState,
diff --git a/src/screen-manager.ts b/src/screen-manager.ts
index 292473dd..218e3287 100644
--- a/src/screen-manager.ts
+++ b/src/screen-manager.ts
@@ -588,11 +588,11 @@ export class ScreenManager extends EventEmitter {
}
}
- // Update inner loop (Ralph Wiggum) enabled state
- updateInnerLoopEnabled(sessionId: string, enabled: boolean): void {
+ // Update Ralph enabled state
+ updateRalphEnabled(sessionId: string, enabled: boolean): void {
const screen = this.screens.get(sessionId);
if (screen) {
- screen.innerLoopEnabled = enabled;
+ screen.ralphEnabled = enabled;
this.saveScreens();
}
}
diff --git a/src/session.ts b/src/session.ts
index 72d07ab6..177687a6 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -18,13 +18,13 @@
import { EventEmitter } from 'node:events';
import { v4 as uuidv4 } from 'uuid';
import * as pty from 'node-pty';
-import { SessionState, SessionStatus, SessionConfig, ScreenSession, InnerLoopState, InnerTodoItem } from './types.js';
+import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem } from './types.js';
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
-import { InnerLoopTracker } from './inner-loop-tracker.js';
+import { RalphTracker } from './ralph-tracker.js';
import { ScreenManager } from './screen-manager.js';
export type { BackgroundTask } from './task-tracker.js';
-export type { InnerLoopState, InnerTodoItem } from './types.js';
+export type { RalphTrackerState, RalphTodoItem } from './types.js';
export { withTimeout };
// ============================================================================
@@ -219,12 +219,12 @@ export interface SessionEvents {
autoClear: (data: { tokens: number; threshold: number }) => void;
/** Auto-compact triggered due to token threshold */
autoCompact: (data: { tokens: number; threshold: number; prompt?: string }) => void;
- /** Inner loop (Ralph Wiggum) state changed */
- innerLoopUpdate: (state: InnerLoopState) => void;
- /** Inner loop todo list updated */
- innerTodoUpdate: (todos: InnerTodoItem[]) => void;
- /** Inner loop completion phrase detected */
- innerCompletionDetected: (phrase: string) => void;
+ /** Ralph loop state changed */
+ ralphLoopUpdate: (state: RalphTrackerState) => void;
+ /** Ralph todo list updated */
+ ralphTodoUpdate: (todos: RalphTodoItem[]) => void;
+ /** Ralph completion phrase detected */
+ ralphCompletionDetected: (phrase: string) => void;
}
/**
@@ -320,8 +320,8 @@ export class Session extends EventEmitter {
private _screenSession: ScreenSession | null = null;
private _useScreen: boolean = false;
- // Inner loop tracking (Ralph Wiggum loops and todo lists inside Claude Code)
- private _innerLoopTracker: InnerLoopTracker;
+ // Ralph tracking (Ralph Wiggum loops and todo lists inside Claude Code)
+ private _ralphTracker: RalphTracker;
// Store handler references for cleanup (prevents memory leaks)
private _taskTrackerHandlers: {
@@ -331,9 +331,9 @@ export class Session extends EventEmitter {
taskFailed: (task: BackgroundTask, error: string) => void;
} | null = null;
- private _innerLoopHandlers: {
- loopUpdate: (state: InnerLoopState) => void;
- todoUpdate: (todos: InnerTodoItem[]) => void;
+ private _ralphHandlers: {
+ loopUpdate: (state: RalphTrackerState) => void;
+ todoUpdate: (todos: RalphTodoItem[]) => void;
completionDetected: (phrase: string) => void;
} | null = null;
@@ -369,16 +369,16 @@ export class Session extends EventEmitter {
this._taskTracker.on('taskCompleted', this._taskTrackerHandlers.taskCompleted);
this._taskTracker.on('taskFailed', this._taskTrackerHandlers.taskFailed);
- // Initialize inner loop tracker and forward events (store handlers for cleanup)
- this._innerLoopTracker = new InnerLoopTracker();
- this._innerLoopHandlers = {
- loopUpdate: (state) => this.emit('innerLoopUpdate', state),
- todoUpdate: (todos) => this.emit('innerTodoUpdate', todos),
- completionDetected: (phrase) => this.emit('innerCompletionDetected', phrase),
+ // Initialize Ralph tracker and forward events (store handlers for cleanup)
+ this._ralphTracker = new RalphTracker();
+ this._ralphHandlers = {
+ loopUpdate: (state) => this.emit('ralphLoopUpdate', state),
+ todoUpdate: (todos) => this.emit('ralphTodoUpdate', todos),
+ completionDetected: (phrase) => this.emit('ralphCompletionDetected', phrase),
};
- this._innerLoopTracker.on('loopUpdate', this._innerLoopHandlers.loopUpdate);
- this._innerLoopTracker.on('todoUpdate', this._innerLoopHandlers.todoUpdate);
- this._innerLoopTracker.on('completionDetected', this._innerLoopHandlers.completionDetected);
+ this._ralphTracker.on('loopUpdate', this._ralphHandlers.loopUpdate);
+ this._ralphTracker.on('todoUpdate', this._ralphHandlers.todoUpdate);
+ this._ralphTracker.on('completionDetected', this._ralphHandlers.completionDetected);
}
get status(): SessionStatus {
@@ -449,21 +449,21 @@ export class Session extends EventEmitter {
return this._taskTracker.getStats();
}
- // Inner loop tracking getters
- get innerLoopTracker(): InnerLoopTracker {
- return this._innerLoopTracker;
+ // Ralph tracking getters
+ get ralphTracker(): RalphTracker {
+ return this._ralphTracker;
}
- get innerLoopState(): InnerLoopState {
- return this._innerLoopTracker.loopState;
+ get ralphLoopState(): RalphTrackerState {
+ return this._ralphTracker.loopState;
}
- get innerTodos(): InnerTodoItem[] {
- return this._innerLoopTracker.todos;
+ get ralphTodos(): RalphTodoItem[] {
+ return this._ralphTracker.todos;
}
- get innerTodoStats(): { total: number; pending: number; inProgress: number; completed: number } {
- return this._innerLoopTracker.getTodoStats();
+ get ralphTodoStats(): { total: number; pending: number; inProgress: number; completed: number } {
+ return this._ralphTracker.getTodoStats();
}
// Token tracking getters and setters
@@ -582,10 +582,10 @@ export class Session extends EventEmitter {
enabled: this._autoClearEnabled,
threshold: this._autoClearThreshold,
},
- // Inner loop tracking state
- innerLoop: this._innerLoopTracker.loopState,
- innerTodos: this._innerLoopTracker.todos,
- innerTodoStats: this._innerLoopTracker.getTodoStats(),
+ // Ralph tracking state
+ ralphLoop: this._ralphTracker.loopState,
+ ralphTodos: this._ralphTracker.todos,
+ ralphTodoStats: this._ralphTracker.getTodoStats(),
};
}
@@ -726,8 +726,8 @@ export class Session extends EventEmitter {
this.emit('terminal', data);
this.emit('output', data);
- // Forward to inner loop tracker to detect Ralph loops and todos
- this._innerLoopTracker.processTerminalData(data);
+ // Forward to Ralph tracker to detect Ralph loops and todos
+ this._ralphTracker.processTerminalData(data);
// Parse token count from status line (e.g., "123.4k tokens" or "5234 tokens")
this.parseTokensFromStatusLine(data);
@@ -1344,7 +1344,7 @@ export class Session extends EventEmitter {
}
/**
- * Remove event listeners from TaskTracker and InnerLoopTracker.
+ * Remove event listeners from TaskTracker and RalphTracker.
* Prevents memory leaks by ensuring handlers don't persist after session stop.
*/
private cleanupTrackerListeners(): void {
@@ -1357,12 +1357,12 @@ export class Session extends EventEmitter {
this._taskTrackerHandlers = null;
}
- // Remove InnerLoopTracker handlers
- if (this._innerLoopHandlers) {
- this._innerLoopTracker.off('loopUpdate', this._innerLoopHandlers.loopUpdate);
- this._innerLoopTracker.off('todoUpdate', this._innerLoopHandlers.todoUpdate);
- this._innerLoopTracker.off('completionDetected', this._innerLoopHandlers.completionDetected);
- this._innerLoopHandlers = null;
+ // Remove RalphTracker handlers
+ if (this._ralphHandlers) {
+ this._ralphTracker.off('loopUpdate', this._ralphHandlers.loopUpdate);
+ this._ralphTracker.off('todoUpdate', this._ralphHandlers.todoUpdate);
+ this._ralphTracker.off('completionDetected', this._ralphHandlers.completionDetected);
+ this._ralphHandlers = null;
}
}
@@ -1527,6 +1527,6 @@ export class Session extends EventEmitter {
this._errorBuffer = '';
this._messages = [];
this._taskTracker.clear();
- this._innerLoopTracker.clear();
+ this._ralphTracker.clear();
}
}
diff --git a/src/state-store.ts b/src/state-store.ts
index 6128c752..b132af7f 100644
--- a/src/state-store.ts
+++ b/src/state-store.ts
@@ -8,7 +8,7 @@
* - `state.json`: Main app state (sessions, tasks, config)
* - `state-inner.json`: Inner loop state (todos, Ralph loop state per session)
*
- * The separation reduces write frequency since inner loop state changes rapidly
+ * The separation reduces write frequency since Ralph state changes rapidly
* during Ralph Wiggum loops.
*
* @module state-store
@@ -17,7 +17,7 @@
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
-import { AppState, createInitialState, InnerSessionState, createInitialInnerSessionState } from './types.js';
+import { AppState, createInitialState, RalphSessionState, createInitialRalphSessionState } from './types.js';
/** Debounce delay for batching state writes (ms) */
const SAVE_DEBOUNCE_MS = 500;
@@ -50,17 +50,17 @@ export class StateStore {
private dirty: boolean = false;
// Inner state storage (separate from main state to reduce write frequency)
- private innerStates: Map = new Map();
- private innerStatePath: string;
- private innerStateSaveTimeout: NodeJS.Timeout | null = null;
- private innerStateDirty: boolean = false;
+ private ralphStates: Map = new Map();
+ private ralphStatePath: string;
+ private ralphStateSaveTimeout: NodeJS.Timeout | null = null;
+ private ralphStateDirty: boolean = false;
constructor(filePath?: string) {
this.filePath = filePath || join(homedir(), '.claudeman', 'state.json');
- this.innerStatePath = this.filePath.replace('.json', '-inner.json');
+ this.ralphStatePath = this.filePath.replace('.json', '-inner.json');
this.state = this.load();
this.state.config.stateFilePath = this.filePath;
- this.loadInnerStates();
+ this.loadRalphStates();
}
private ensureDir(): void {
@@ -203,20 +203,20 @@ export class StateStore {
reset(): void {
this.state = createInitialState();
this.state.config.stateFilePath = this.filePath;
- this.innerStates.clear();
+ this.ralphStates.clear();
this.saveNow(); // Immediate save for reset operations
- this.saveInnerStatesNow();
+ this.saveRalphStatesNow();
}
// ========== Inner State Methods (Ralph Loop tracking) ==========
- private loadInnerStates(): void {
+ private loadRalphStates(): void {
try {
- if (existsSync(this.innerStatePath)) {
- const data = readFileSync(this.innerStatePath, 'utf-8');
- const parsed = JSON.parse(data) as Record;
+ if (existsSync(this.ralphStatePath)) {
+ const data = readFileSync(this.ralphStatePath, 'utf-8');
+ const parsed = JSON.parse(data) as Record;
for (const [sessionId, state] of Object.entries(parsed)) {
- this.innerStates.set(sessionId, state);
+ this.ralphStates.set(sessionId, state);
}
}
} catch (err) {
@@ -225,43 +225,43 @@ export class StateStore {
}
// Debounced save for inner states
- private saveInnerStates(): void {
- this.innerStateDirty = true;
- if (this.innerStateSaveTimeout) {
+ private saveRalphStates(): void {
+ this.ralphStateDirty = true;
+ if (this.ralphStateSaveTimeout) {
return; // Already scheduled
}
- this.innerStateSaveTimeout = setTimeout(() => {
- this.saveInnerStatesNow();
+ this.ralphStateSaveTimeout = setTimeout(() => {
+ this.saveRalphStatesNow();
}, SAVE_DEBOUNCE_MS);
}
// Immediate save for inner states
- private saveInnerStatesNow(): void {
- if (this.innerStateSaveTimeout) {
- clearTimeout(this.innerStateSaveTimeout);
- this.innerStateSaveTimeout = null;
+ private saveRalphStatesNow(): void {
+ if (this.ralphStateSaveTimeout) {
+ clearTimeout(this.ralphStateSaveTimeout);
+ this.ralphStateSaveTimeout = null;
}
- if (!this.innerStateDirty) {
+ if (!this.ralphStateDirty) {
return;
}
- this.innerStateDirty = false;
+ this.ralphStateDirty = false;
this.ensureDir();
- const data: Record = {};
- for (const [sessionId, state] of this.innerStates) {
+ const data: Record = {};
+ for (const [sessionId, state] of this.ralphStates) {
data[sessionId] = state;
}
- writeFileSync(this.innerStatePath, JSON.stringify(data, null, 2), 'utf-8');
+ writeFileSync(this.ralphStatePath, JSON.stringify(data, null, 2), 'utf-8');
}
/** Returns inner state for a session, or null if not found. */
- getInnerState(sessionId: string): InnerSessionState | null {
- return this.innerStates.get(sessionId) || null;
+ getRalphState(sessionId: string): RalphSessionState | null {
+ return this.ralphStates.get(sessionId) || null;
}
/** Sets inner state for a session and triggers a debounced save. */
- setInnerState(sessionId: string, state: InnerSessionState): void {
- this.innerStates.set(sessionId, state);
- this.saveInnerStates();
+ setRalphState(sessionId: string, state: RalphSessionState): void {
+ this.ralphStates.set(sessionId, state);
+ this.saveRalphStates();
}
/**
@@ -269,34 +269,34 @@ export class StateStore {
* Creates initial state if none exists.
* @returns The updated inner state.
*/
- updateInnerState(sessionId: string, updates: Partial): InnerSessionState {
- let state = this.innerStates.get(sessionId);
+ updateRalphState(sessionId: string, updates: Partial): RalphSessionState {
+ let state = this.ralphStates.get(sessionId);
if (!state) {
- state = createInitialInnerSessionState(sessionId);
+ state = createInitialRalphSessionState(sessionId);
}
state = { ...state, ...updates, lastUpdated: Date.now() };
- this.innerStates.set(sessionId, state);
- this.saveInnerStates();
+ this.ralphStates.set(sessionId, state);
+ this.saveRalphStates();
return state;
}
/** Removes inner state for a session and triggers a debounced save. */
- removeInnerState(sessionId: string): void {
- if (this.innerStates.has(sessionId)) {
- this.innerStates.delete(sessionId);
- this.saveInnerStates();
+ removeRalphState(sessionId: string): void {
+ if (this.ralphStates.has(sessionId)) {
+ this.ralphStates.delete(sessionId);
+ this.saveRalphStates();
}
}
/** Returns a copy of all inner states as a Map. */
- getAllInnerStates(): Map {
- return new Map(this.innerStates);
+ getAllRalphStates(): Map {
+ return new Map(this.ralphStates);
}
/** Flushes all pending saves (main and inner state). Call before shutdown. */
flushAll(): void {
this.saveNow();
- this.saveInnerStatesNow();
+ this.saveRalphStatesNow();
}
}
diff --git a/src/tui/App.tsx b/src/tui/App.tsx
index bd181e68..d7cf313d 100644
--- a/src/tui/App.tsx
+++ b/src/tui/App.tsx
@@ -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 && (
)}
diff --git a/src/tui/components/RalphPanel.tsx b/src/tui/components/RalphPanel.tsx
index 49338eb2..c7aac66e 100644
--- a/src/tui/components/RalphPanel.tsx
+++ b/src/tui/components/RalphPanel.tsx
@@ -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
diff --git a/src/tui/hooks/useSessionManager.ts b/src/tui/hooks/useSessionManager.ts
index 1965d36d..84dd5bdf 100644
--- a/src/tui/hooks/useSessionManager.ts
+++ b/src/tui/hooks/useSessionManager.ts
@@ -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;
+ const data = readFileSync(RALPH_STATE_FILE, 'utf-8');
+ const allStates = JSON.parse(data) as Record;
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([]);
const [activeSessionId, setActiveSessionId] = useState(null);
const [terminalOutput, setTerminalOutput] = useState('');
- const [innerLoopState, setInnerLoopState] = useState(null);
- const [innerTodos, setInnerTodos] = useState([]);
+ const [ralphLoopState, setRalphTrackerState] = useState(null);
+ const [ralphTodos, setInnerTodos] = useState([]);
const [respawnStatus, setRespawnStatus] = useState(null);
const [cases, setCases] = useState([]);
const [lastUsedCase, setLastUsedCase] = useState(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,
diff --git a/src/types.ts b/src/types.ts
index b6b13dba..0d766c3d 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -475,8 +475,8 @@ export interface ScreenSession {
name?: string;
/** Persisted respawn controller configuration (restored on server restart) */
respawnConfig?: PersistedRespawnConfig;
- /** Whether inner loop (Ralph Wiggum) tracking is enabled */
- innerLoopEnabled?: boolean;
+ /** Whether Ralph / Todo tracking is enabled */
+ ralphEnabled?: boolean;
}
/**
@@ -533,12 +533,12 @@ export const DEFAULT_CONFIG: AppConfig = {
*/
/** Status of a detected todo item */
-export type InnerTodoStatus = 'pending' | 'in_progress' | 'completed';
+export type RalphTodoStatus = 'pending' | 'in_progress' | 'completed';
/**
- * State of an inner loop (Ralph Wiggum loop inside Claude Code)
+ * State of per-session Ralph / Todo tracking (detected from Claude output)
*/
-export interface InnerLoopState {
+export interface RalphTrackerState {
/** Whether the tracker is actively monitoring (disabled by default) */
enabled: boolean;
/** Whether a loop is currently active */
@@ -560,27 +560,27 @@ export interface InnerLoopState {
/**
* A detected todo item from Claude Code output
*/
-export interface InnerTodoItem {
+export interface RalphTodoItem {
/** Unique identifier based on content hash */
id: string;
/** Todo item text content */
content: string;
/** Current status */
- status: InnerTodoStatus;
+ status: RalphTodoStatus;
/** Timestamp when detected */
detectedAt: number;
}
/**
- * Complete inner state for a session
+ * Complete Ralph/todo state for a session
*/
-export interface InnerSessionState {
+export interface RalphSessionState {
/** Session this state belongs to */
sessionId: string;
/** Loop tracking state */
- loop: InnerLoopState;
+ loop: RalphTrackerState;
/** Detected todo items */
- todos: InnerTodoItem[];
+ todos: RalphTodoItem[];
/** Timestamp of last update */
lastUpdated: number;
}
@@ -588,15 +588,15 @@ export interface InnerSessionState {
/**
* Map of session ID to inner state
*/
-export interface InnerStateRecord {
- [sessionId: string]: InnerSessionState;
+export interface RalphStateRecord {
+ [sessionId: string]: RalphSessionState;
}
/**
- * Creates initial inner loop state
- * @returns Fresh inner loop state with defaults
+ * Creates initial Ralph tracker state
+ * @returns Fresh Ralph tracker state with defaults
*/
-export function createInitialInnerLoopState(): InnerLoopState {
+export function createInitialRalphTrackerState(): RalphTrackerState {
return {
enabled: false, // Disabled by default, auto-enables when Ralph patterns detected
active: false,
@@ -610,14 +610,14 @@ export function createInitialInnerLoopState(): InnerLoopState {
}
/**
- * Creates initial inner session state
+ * Creates initial Ralph session state
* @param sessionId Session ID this state belongs to
- * @returns Fresh inner session state
+ * @returns Fresh Ralph session state
*/
-export function createInitialInnerSessionState(sessionId: string): InnerSessionState {
+export function createInitialRalphSessionState(sessionId: string): RalphSessionState {
return {
sessionId,
- loop: createInitialInnerLoopState(),
+ loop: createInitialRalphTrackerState(),
todos: [],
lastUpdated: Date.now(),
};
diff --git a/src/web/public/app.js b/src/web/public/app.js
index 94d026d7..80c3a22d 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -19,9 +19,9 @@ class ClaudemanApp {
this.pendingCloseSessionId = null; // Session pending close confirmation
this.screenSessions = []; // Screen sessions for process monitor
- // Inner loop/todo state per session
- this.innerStates = new Map(); // Map
- this.innerStatePanelCollapsed = true; // Default to collapsed
+ // Ralph loop/todo state per session
+ this.ralphStates = new Map(); // Map
+ this.ralphStatePanelCollapsed = true; // Default to collapsed
// Terminal write batching
this.pendingWrites = '';
@@ -29,7 +29,7 @@ class ClaudemanApp {
// Render debouncing
this.renderSessionTabsTimeout = null;
- this.renderInnerStatePanelTimeout = null;
+ this.renderRalphStatePanelTimeout = null;
this.renderTaskPanelTimeout = null;
this.renderScreenSessionsTimeout = null;
@@ -302,14 +302,14 @@ class ClaudemanApp {
const data = JSON.parse(e.data);
this.sessions.delete(data.id);
this.terminalBuffers.delete(data.id);
- this.innerStates.delete(data.id); // Clean up inner state for this session
+ this.ralphStates.delete(data.id); // Clean up ralph state for this session
if (this.activeSessionId === data.id) {
this.activeSessionId = null;
this.terminal.clear();
this.showWelcome();
}
this.renderSessionTabs();
- this.renderInnerStatePanel(); // Update inner panel after session deleted
+ this.renderRalphStatePanel(); // Update ralph panel after session deleted
});
this.eventSource.addEventListener('session:terminal', (e) => {
@@ -543,18 +543,18 @@ class ClaudemanApp {
}
});
- // Inner loop/todo events
- this.eventSource.addEventListener('session:innerLoopUpdate', (e) => {
+ // Ralph loop/todo events
+ this.eventSource.addEventListener('session:ralphLoopUpdate', (e) => {
const data = JSON.parse(e.data);
- this.updateInnerState(data.sessionId, { loop: data.state });
+ this.updateRalphState(data.sessionId, { loop: data.state });
});
- this.eventSource.addEventListener('session:innerTodoUpdate', (e) => {
+ this.eventSource.addEventListener('session:ralphTodoUpdate', (e) => {
const data = JSON.parse(e.data);
- this.updateInnerState(data.sessionId, { todos: data.todos });
+ this.updateRalphState(data.sessionId, { todos: data.todos });
});
- this.eventSource.addEventListener('session:innerCompletionDetected', (e) => {
+ this.eventSource.addEventListener('session:ralphCompletionDetected', (e) => {
const data = JSON.parse(e.data);
// Prevent duplicate notifications for the same completion
const completionKey = `${data.sessionId}:${data.phrase}`;
@@ -568,11 +568,11 @@ class ClaudemanApp {
// Clear after 30 seconds to allow re-notification if loop restarts
setTimeout(() => this._shownCompletions?.delete(completionKey), 30000);
- // Update inner state to mark loop as inactive
- const existing = this.innerStates.get(data.sessionId) || {};
+ // Update ralph state to mark loop as inactive
+ const existing = this.ralphStates.get(data.sessionId) || {};
if (existing.loop) {
existing.loop.active = false;
- this.updateInnerState(data.sessionId, existing);
+ this.updateRalphState(data.sessionId, existing);
}
});
}
@@ -586,14 +586,14 @@ class ClaudemanApp {
handleInit(data) {
this.sessions.clear();
- this.innerStates.clear();
+ this.ralphStates.clear();
data.sessions.forEach(s => {
this.sessions.set(s.id, s);
- // Load inner state from session data
- if (s.innerLoop || s.innerTodos) {
- this.innerStates.set(s.id, {
- loop: s.innerLoop || null,
- todos: s.innerTodos || []
+ // Load ralph state from session data
+ if (s.ralphLoop || s.ralphTodos) {
+ this.ralphStates.set(s.id, {
+ loop: s.ralphLoop || null,
+ todos: s.ralphTodos || []
});
}
});
@@ -807,15 +807,15 @@ class ClaudemanApp {
this.renderTaskPanel();
}
- // Update inner state panel for this session
+ // Update ralph state panel for this session
const session = this.sessions.get(sessionId);
- if (session && (session.innerLoop || session.innerTodos)) {
- this.updateInnerState(sessionId, {
- loop: session.innerLoop,
- todos: session.innerTodos
+ if (session && (session.ralphLoop || session.ralphTodos)) {
+ this.updateRalphState(sessionId, {
+ loop: session.ralphLoop,
+ todos: session.ralphTodos
});
}
- this.renderInnerStatePanel();
+ this.renderRalphStatePanel();
this.terminal.focus();
} catch (err) {
@@ -828,7 +828,7 @@ class ClaudemanApp {
await fetch(`/api/sessions/${sessionId}?killScreen=${killScreen}`, { method: 'DELETE' });
this.sessions.delete(sessionId);
this.terminalBuffers.delete(sessionId);
- this.innerStates.delete(sessionId);
+ this.ralphStates.delete(sessionId);
if (this.activeSessionId === sessionId) {
this.activeSessionId = null;
@@ -839,7 +839,7 @@ class ClaudemanApp {
} else {
this.terminal.clear();
this.showWelcome();
- this.renderInnerStatePanel(); // Clear inner panel when no sessions
+ this.renderRalphStatePanel(); // Clear ralph panel when no sessions
}
}
@@ -1088,7 +1088,7 @@ class ClaudemanApp {
// Apply global Ralph tracker setting
// If enabled: enable the tracker
// If disabled: disable auto-enable to prevent pattern-based activation
- await fetch(`/api/sessions/${createData.session.id}/inner-config`, {
+ await fetch(`/api/sessions/${createData.session.id}/ralph-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -1518,13 +1518,13 @@ class ClaudemanApp {
this.selectDurationPreset('');
// Populate Ralph Wiggum form with current session values
- const innerState = this.innerStates.get(sessionId);
+ const ralphState = this.ralphStates.get(sessionId);
this.populateRalphForm({
- enabled: innerState?.loop?.enabled ?? session.innerConfig?.enabled ?? false,
- completionPhrase: innerState?.loop?.completionPhrase || session.innerLoop?.completionPhrase || '',
- maxIterations: innerState?.loop?.maxIterations || session.innerLoop?.maxIterations || 0,
- maxTodos: session.innerConfig?.maxTodos || 50,
- todoExpirationMinutes: session.innerConfig?.todoExpirationMinutes || 60
+ enabled: ralphState?.loop?.enabled ?? session.ralphConfig?.enabled ?? false,
+ completionPhrase: ralphState?.loop?.completionPhrase || session.ralphLoop?.completionPhrase || '',
+ maxIterations: ralphState?.loop?.maxIterations || session.ralphLoop?.maxIterations || 0,
+ maxTodos: session.ralphConfig?.maxTodos || 50,
+ todoExpirationMinutes: session.ralphConfig?.todoExpirationMinutes || 60
});
document.getElementById('sessionOptionsModal').classList.add('active');
@@ -1728,7 +1728,7 @@ class ClaudemanApp {
const config = this.getRalphConfig();
try {
- const res = await fetch(`/api/sessions/${this.editingSessionId}/inner-config`, {
+ const res = await fetch(`/api/sessions/${this.editingSessionId}/ralph-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
@@ -2177,24 +2177,24 @@ class ClaudemanApp {
// ========== Enhanced Ralph Wiggum Loop Panel ==========
- updateInnerState(sessionId, updates) {
- const existing = this.innerStates.get(sessionId) || { loop: null, todos: [] };
+ updateRalphState(sessionId, updates) {
+ const existing = this.ralphStates.get(sessionId) || { loop: null, todos: [] };
const updated = { ...existing, ...updates };
- this.innerStates.set(sessionId, updated);
+ this.ralphStates.set(sessionId, updated);
// Re-render if this is the active session
if (sessionId === this.activeSessionId) {
- this.renderInnerStatePanel();
+ this.renderRalphStatePanel();
}
}
- toggleInnerStatePanel() {
+ toggleRalphStatePanel() {
// Preserve xterm scroll position to prevent jump when panel height changes
const xtermViewport = this.terminal?.element?.querySelector('.xterm-viewport');
const scrollTop = xtermViewport?.scrollTop;
- this.innerStatePanelCollapsed = !this.innerStatePanelCollapsed;
- this.renderInnerStatePanel();
+ this.ralphStatePanelCollapsed = !this.ralphStatePanelCollapsed;
+ this.renderRalphStatePanel();
// Restore scroll position and refit terminal after layout change
requestAnimationFrame(() => {
@@ -2213,19 +2213,19 @@ class ClaudemanApp {
if (!this.activeSessionId) return;
// Disable tracker via API
- await fetch(`/api/sessions/${this.activeSessionId}/inner-config`, {
+ await fetch(`/api/sessions/${this.activeSessionId}/ralph-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: false })
});
// Clear local state and hide panel
- this.innerStates.delete(this.activeSessionId);
- this.renderInnerStatePanel();
+ this.ralphStates.delete(this.activeSessionId);
+ this.renderRalphStatePanel();
}
toggleRalphDetach() {
- const panel = this.$('innerStatePanel');
+ const panel = this.$('ralphStatePanel');
const detachBtn = this.$('ralphDetachBtn');
if (!panel) return;
@@ -2245,7 +2245,7 @@ class ClaudemanApp {
// Detach as floating window
panel.classList.add('detached');
// Expand when detaching for better visibility
- this.innerStatePanelCollapsed = false;
+ this.ralphStatePanelCollapsed = false;
panel.classList.remove('collapsed');
if (detachBtn) {
detachBtn.innerHTML = '⊞'; // Attach icon (squared plus - dock back)
@@ -2254,11 +2254,11 @@ class ClaudemanApp {
// Setup drag functionality
this.setupRalphDrag();
}
- this.renderInnerStatePanel();
+ this.renderRalphStatePanel();
}
setupRalphDrag() {
- const panel = this.$('innerStatePanel');
+ const panel = this.$('ralphStatePanel');
const header = this.$('ralphSummary');
if (!panel || !header) return;
@@ -2313,23 +2313,23 @@ class ClaudemanApp {
header.addEventListener('mousedown', onMouseDown);
}
- renderInnerStatePanel() {
+ renderRalphStatePanel() {
// Debounce renders at 50ms to prevent excessive DOM updates
- if (this.renderInnerStatePanelTimeout) {
- clearTimeout(this.renderInnerStatePanelTimeout);
+ if (this.renderRalphStatePanelTimeout) {
+ clearTimeout(this.renderRalphStatePanelTimeout);
}
- this.renderInnerStatePanelTimeout = setTimeout(() => {
- this._renderInnerStatePanelImmediate();
+ this.renderRalphStatePanelTimeout = setTimeout(() => {
+ this._renderRalphStatePanelImmediate();
}, 50);
}
- _renderInnerStatePanelImmediate() {
- const panel = this.$('innerStatePanel');
+ _renderRalphStatePanelImmediate() {
+ const panel = this.$('ralphStatePanel');
const toggle = this.$('ralphToggle');
if (!panel) return;
- const state = this.innerStates.get(this.activeSessionId);
+ const state = this.ralphStates.get(this.activeSessionId);
// Check if there's anything to show
// Only show panel if tracker is enabled OR there's active state to display
@@ -2360,7 +2360,7 @@ class ClaudemanApp {
this.updateRalphStats(state?.loop, completed, total);
// Handle collapsed/expanded state
- if (this.innerStatePanelCollapsed) {
+ if (this.ralphStatePanelCollapsed) {
panel.classList.add('collapsed');
if (toggle) toggle.innerHTML = '▼'; // Down arrow when collapsed (click to expand)
} else {
@@ -2510,8 +2510,8 @@ class ClaudemanApp {
if (!grid) return;
if (todos.length === 0) {
- if (grid.children.length !== 1 || !grid.querySelector('.inner-state-empty')) {
- grid.innerHTML = 'No tasks detected
';
+ if (grid.children.length !== 1 || !grid.querySelector('.ralph-state-empty')) {
+ grid.innerHTML = 'No tasks detected
';
}
return;
}
diff --git a/src/web/public/index.html b/src/web/public/index.html
index 24e4d16a..5ca337fc 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -72,11 +72,11 @@
■
-
-
+
+
-
+
@@ -91,7 +91,7 @@
0%
-
Ralph Wiggum Tracker
+
Ralph / Todo Tracker
Idle
@@ -268,7 +268,7 @@
Respawn
- Ralph Wiggum
+ Ralph / Todo Tracker
@@ -374,7 +374,7 @@
- Enable Ralph Wiggum tracker for this session (auto-detects loops and todos)
+ Enable Ralph / Todo tracker for this session (auto-detects loops and todos)
@@ -505,7 +505,7 @@
Comma-separated list of tools to allow