42 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
⚠️ CRITICAL: Screen Session Safety
You may be running inside a Claudeman-managed screen session. Before killing ANY screen or Claude process:
- Check environment:
echo $CLAUDEMAN_SCREEN- if it returns1, you're in a managed session - NEVER run
screen -X quit,pkill screen, orpkill claudewithout first confirming you're not killing yourself - Safe debugging: Use
screen -lsto LIST sessions, but don't kill them blindly - If you need to kill screens: Use the web UI or
./scripts/screen-manager.shinstead of direct commands
Why this matters: Killing your own screen terminates your session mid-work, losing context and potentially corrupting state.
⚡ COM Shorthand (Deployment)
When user says "COM":
- Increment version in BOTH
package.jsonANDCLAUDE.md(keep them in sync) - Run:
git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web
Always bump version on every COM, even for small changes.
Project Overview
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
Mission: Rock-Solid Performance
The app MUST remain fast, responsive, and never hang — even with many tabs open and many subagent windows active. This is a core design principle, not a nice-to-have. Every feature must be built with this constraint in mind:
- 60fps terminal rendering with batched writes and
requestAnimationFrame - Auto-trimming buffers prevent memory bloat in long-running sessions
- Debounced state persistence (500ms) avoids disk thrashing
- SSE batching (16ms) reduces client-side event storms
- Lazy loading of agent transcripts and historical data
- No blocking operations in the main event loop
When adding new features, always ask: "Will this maintain responsiveness with 20 sessions and 50 agent windows?"
Version: 0.1398 (must match package.json)
Tech Stack: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
Key Dependencies: fastify (REST API), node-pty (PTY spawning), ink/react (TUI), xterm.js (web terminal), @modelcontextprotocol/sdk (MCP server for spawn protocol)
Requirements: Node.js 18+, Claude CLI (claude) installed, GNU Screen (apt install screen / brew install screen)
Note
:
claudedoes not need to be in the server process's PATH. Claudeman auto-discovers the binary from common install locations (~/.local/bin,~/.claude/local,/usr/local/bin, etc.) and augments PATH for spawned sessions.
Runtime: The web server runs as a systemd user service (
claudeman-web.service) on HTTPS port 3000 with a self-signed certificate. It auto-restarts and survives logout.
First-Time Setup
npm install
Commands
CRITICAL: npm run dev runs CLI help, NOT the web server. Use npx tsx src/index.ts web for development.
Quick reference:
- Dev server:
npx tsx src/index.ts web(orweb --httpsfor notifications) - Type check:
npx tsc --noEmit - Single test:
npx vitest run test/<file>.test.ts - Restart prod:
systemctl --user restart claudeman-web
Build & Clean
npm run build # Compile TS + copy static files + templates + make bins executable
npm run clean # Remove dist/
npm run typecheck # Type check without building (or: npx tsc --noEmit)
Web Server
npx tsx src/index.ts web # Dev mode - no build needed (RECOMMENDED)
npx tsx src/index.ts web -p 8080 # Dev mode with custom port
npx tsx src/index.ts web --https # Dev mode with self-signed TLS (enables browser notifications)
npm run web # After npm run build (shorthand)
node dist/index.js web # After npm run build
claudeman web # After npm link
TUI (Terminal User Interface)
npx tsx src/index.ts tui # Dev mode - prompts to start web if not running
claudeman tui # After npm link
claudeman tui --with-web # Auto-start web server if not running (no prompt)
claudeman tui --no-web # Skip web server check entirely
claudeman tui -p 8080 # Specify web server port
Testing
npm run test # Run all tests once
npm run test:watch # Watch mode
npm run test:coverage # With coverage report
npx vitest run test/session.test.ts # Single file
npx vitest run -t "should create session" # By pattern
Test Configuration (vitest.config.ts):
globals: true- no imports needed fordescribe/it/expecttestTimeout: 30000- 30s for integration teststeardownTimeout: 60000- 60s ensures cleanup runs even on failuresfileParallelism: false- sequential file execution to respect screen session limits- Coverage excludes entry points:
src/index.ts,src/cli.ts
Test Port Allocation (integration tests spawn servers):
| Port | Test File |
|---|---|
| 3099 | quick-start.test.ts |
| 3102 | session.test.ts |
| 3105 | scheduled-runs.test.ts |
| 3107 | sse-events.test.ts |
| 3110 | edge-cases.test.ts |
| 3115 | integration-flows.test.ts |
| 3120 | session-cleanup.test.ts |
| 3125 | ralph-integration.test.ts |
| 3127 | respawn-integration.test.ts (reserved) |
| 3130 | hooks-config.test.ts (Hook Event API) |
| 3131 | hooks-config.test.ts (Hook Data Sanitization) |
| 3150 | browser-e2e.test.ts (main browser tests) |
| 3151 | browser-e2e.test.ts (SSE events tests) |
| 3152 | browser-e2e.test.ts (hook events tests) |
| 3153 | browser-e2e.test.ts (Ralph panel tests) |
| 3154 | file-link-click.test.ts |
| 3155 | browser-playwright.test.ts |
| 3156 | browser-puppeteer.test.ts |
| 3157 | browser-agent.test.ts |
| 3158-3160 | browser-comparison.test.ts |
| 3180-3182 | scripts/browser-comparison.mjs (benchmark) |
| 3183 | test/e2e/workflows/quick-start.e2e.ts |
| 3184 | test/e2e/workflows/session-input.e2e.ts |
| 3185 | test/e2e/workflows/session-delete.e2e.ts |
| 3186 | test/e2e/workflows/multi-session.e2e.ts |
| 3187 | test/e2e/workflows/agent-interactions.e2e.ts |
| 3188 | test/e2e/workflows/input-interactions.e2e.ts |
| 3189 | test/e2e/workflows/respawn-flow.e2e.ts |
Next available port: 3190
Browser Testing: Three frameworks available (Playwright, Puppeteer, Agent-Browser). See docs/browser-testing-guide.md for full comparison and patterns.
# Run browser benchmark (standalone - recommended)
npx tsx scripts/browser-comparison.mjs
# Run existing browser E2E tests
npm test -- test/browser-e2e.test.ts
Browser Testing Key Points:
- Vitest hook issue: Browser tests using
beforeAll/afterAlltimeout even when tests pass. Use standalone scripts or run browser code directly in tests. - Recommended framework: Playwright for most cases (auto-waiting, debugging). Puppeteer for Chrome-specific/CDP features.
- Required browser args:
--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage - Install browsers:
npx playwright install chromiumafter npm install
E2E Test Suite
Real browser-based end-to-end tests using Playwright that validate actual user workflows. These tests catch issues that unit tests miss (like the cpulimit bug that broke screen creation).
Setup:
npm install # Install dependencies (pixelmatch, pngjs)
npx playwright install chromium # Install browser
Running E2E Tests:
npm run test:e2e # Run all E2E tests
npm run test:e2e:quick # Run quick-start test only (critical path)
npx vitest run test/e2e/workflows/quick-start.e2e.ts # Run single test file
npx vitest run test/e2e/ -t "should create session" # Run by pattern
Test Fixtures (test/e2e/fixtures/):
| Fixture | Purpose |
|---|---|
server.fixture.ts |
createServerFixture(port) / destroyServerFixture() - Server lifecycle |
browser.fixture.ts |
createBrowserFixture() / destroyBrowserFixture() - Playwright browser with required args |
cleanup.fixture.ts |
CleanupTracker - Tracks sessions, cases, screens for cleanup |
screenshot.fixture.ts |
captureAndCompare() - Visual regression testing with pixelmatch |
Workflow Tests (test/e2e/workflows/):
| Test | Port | What it validates |
|---|---|---|
quick-start.e2e.ts |
3183 | Critical path: click button → session created → screen created → terminal visible |
session-input.e2e.ts |
3184 | Terminal input, Ctrl+C cancel, multi-line input |
session-delete.e2e.ts |
3185 | Delete button → screen killed → UI updated |
multi-session.e2e.ts |
3186 | Multiple sessions, tab switching, Ctrl+Tab shortcut |
agent-interactions.e2e.ts |
3187 | Subagent windows, parent attachment, visibility |
input-interactions.e2e.ts |
3188 | Modals, checkboxes, Ctrl+Enter/W shortcuts |
respawn-flow.e2e.ts |
3189 | Respawn enable/start/stop via API and UI |
ralph-loop.e2e.ts |
3190 | Ralph Loop wizard: open, configure, start, verify tracker enabled |
Screenshot Validation:
- Baselines stored in
test/e2e/screenshots/baselines/ - Current screenshots in
test/e2e/screenshots/current/ - Diff images (on failure) in
test/e2e/screenshots/diffs/ - First run auto-creates baselines; subsequent runs compare
- Default threshold: 5% pixel difference allowed
Cleanup Behavior:
- All test cases use
e2e-test-*prefix for easy identification CleanupTracker.forceCleanupAll()removes ALLe2e-test-*resources- try/finally patterns ensure cleanup even on test failures
afterAllhooks call cleanup as safety net
E2E Test Pattern (avoids Vitest hook timeout issue):
it('should create session', async () => {
let browser: BrowserFixture | null = null;
const caseName = generateCaseName('test');
try {
serverFixture = await createServerFixture(PORT);
cleanup = new CleanupTracker(serverFixture.baseUrl);
cleanup.trackCase(caseName);
browser = await createBrowserFixture();
// ... test code ...
} finally {
if (browser) await destroyBrowserFixture(browser);
}
}, 90000);
E2E Installation Summary:
| Component | Details |
|---|---|
| Test Files | 14 TypeScript files in test/e2e/ |
| Fixtures | server, browser, cleanup, screenshot, index, pixelmatch types |
| Workflows | quick-start, session-input, session-delete, multi-session, agent-interactions, input-interactions, respawn-flow |
| Dependencies | playwright, pixelmatch, pngjs, @types/pngjs |
| Browser | Chromium via npx playwright install chromium |
| NPM Scripts | npm run test:e2e (all), npm run test:e2e:quick (critical path) |
| Ports | 3183-3189 (see workflow tests table above) |
| Gitignore | test/e2e/screenshots/current/ and diffs/ ignored; baselines/ tracked |
Unit tests (no server needed): respawn-controller, ralph-tracker, pty-interactive, task-queue, task, ralph-loop, session-manager, state-store, types, templates, ralph-config, spawn-detector, spawn-types, spawn-orchestrator, ai-idle-checker, ai-plan-checker
Test Utilities: test/respawn-test-utils.ts provides MockSession, MockAiIdleChecker, MockAiPlanChecker, time controller, state tracker, and event recorder for respawn controller testing. See test/respawn-test-plan.md for architecture and test/respawn-scenarios.md for comprehensive test scenarios.
Test Safety: test/setup.ts enforces max 10 concurrent screens, performs orphan cleanup, and protects its own process tree. You can safely run tests from within a Claudeman-managed session - the cleanup will not kill your own Claude instance. The respawn-controller tests use MockSession (not real screens).
Test Cleanup Patterns: Integration tests track resources in createdSessions and createdCases arrays, cleaned up by afterAll/afterEach hooks. However, some tests perform cleanup in the test body itself (e.g., edge-cases.test.ts:273-302 creates 5 sessions and cleans them in a loop). If assertions fail before cleanup code runs, resources leak.
Known Cleanup Issues (technical debt):
pty-interactive.test.ts: Usesawait session.stop()at end of each test, not inafterEach. Test failures leave sessions running.edge-cases.test.ts: Multiple sessions created in test body with cleanup at end; failures leak sessions.- Test cases (
~/claudeman-cases/): Cases namedflow-test-*,ralph-track-loop-*,session-detail-*may persist after test failures.
Manual Cleanup:
# Remove orphaned test cases
rm -rf ~/claudeman-cases/flow-test-* ~/claudeman-cases/ralph-track-loop-* ~/claudeman-cases/session-detail-*
# Kill orphaned test screens (only detached claudeman screens)
screen -ls | grep -E 'Detached.*claudeman' | cut -d. -f1 | xargs -I{} screen -S {} -X quit
MCP Server
npx tsx src/mcp-server.ts # Dev mode (stdio transport)
Configure in Claude Code's MCP settings:
{ "command": "node", "args": ["dist/mcp-server.js"], "env": { "CLAUDEMAN_API_URL": "http://localhost:3000", "CLAUDEMAN_SESSION_ID": "<id>" } }
Debugging
screen -ls # List GNU screen sessions
screen -r <name> # Attach to screen session (Ctrl+A D to detach)
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 Ralph loop state
Systemd Service
systemctl --user status claudeman-web # Check status
systemctl --user restart claudeman-web # Restart
systemctl --user stop claudeman-web # Stop
journalctl --user -u claudeman-web -f # Stream logs
Install: ln -sf scripts/claudeman-web.service ~/.config/systemd/user/
Enable: systemctl --user enable claudeman-web && loginctl enable-linger $USER
Kill Stuck Screens
screen -X -S <name> quit # Graceful quit
pkill -f "SCREEN.*claudeman" # Force kill all claudeman screens
CLI Commands
claudeman session [s] # Manage Claude sessions
start # Start new session
stop <id> # Stop session
list [ls] # List all
logs <id> # View output
claudeman task [t] # Manage tasks
add <prompt> # Add task
list [ls] # List tasks
status <id> # Task details
remove [rm] <id> # Remove task
clear # Clear completed/failed
claudeman ralph [r] # Control Ralph loop
start # Start loop
stop # Stop loop
status # Show status
claudeman web # Start web interface
claudeman tui # Start TUI
claudeman status # Overall status
claudeman reset # Reset all state
Architecture
Key Files
Core Session Management:
| File | Purpose |
|---|---|
src/session.ts |
Core PTY wrapper for Claude CLI. Modes: runPrompt(), startInteractive(), startShell() |
src/screen-manager.ts |
GNU screen persistence, ghost discovery, 4-strategy kill |
src/session-manager.ts |
Session lifecycle, task assignment, cleanup |
src/state-store.ts |
JSON persistence to ~/.claudeman/ with debounced writes |
src/types.ts |
All TypeScript interfaces |
Autonomous Features:
| File | Purpose |
|---|---|
src/respawn-controller.ts |
State machine for autonomous session cycling |
src/ai-idle-checker.ts |
Spawns Claude to analyze terminal output for IDLE/WORKING verdict |
src/ai-plan-checker.ts |
Spawns Claude to detect plan mode prompts for auto-accept |
src/ralph-tracker.ts |
Detects <promise>PHRASE</promise>, todos, loop status |
src/ralph-config.ts |
Parses .claude/ralph-loop.local.md and CLAUDE.md for Ralph config |
src/run-summary.ts |
Tracks session events for "what happened while away" summaries |
Spawn Protocol (Autonomous Agents):
| File | Purpose |
|---|---|
src/spawn-orchestrator.ts |
Full agent lifecycle: spawn, monitor, budget, queue, cleanup |
src/mcp-server.ts |
MCP server binary exposing spawn tools to Claude Code |
src/subagent-watcher.ts |
Monitors Claude Code background agents in ~/.claude/projects/*/subagents/*.jsonl |
Web & TUI:
| File | Purpose |
|---|---|
src/web/server.ts |
Fastify REST API + SSE at /api/events |
src/web/public/app.js |
Frontend: SSE handling, xterm.js, tab management |
src/tui/App.tsx |
TUI main component (Ink/React) |
src/tui/DirectAttach.ts |
Full-screen console attach with tab switching |
Data Flow
- Session spawns
claude -p --dangerously-skip-permissionsvianode-pty(PATH augmented to include claude's install directory) - PTY output is buffered, ANSI stripped, and parsed for JSON messages
- WebServer broadcasts events to SSE clients at
/api/events - Full session state (settings, tokens, respawn config, Ralph state) persists to
~/.claudeman/state.jsonvia StateStore - Screen metadata persists separately to
~/.claudeman/screens.jsonfor session recovery
Respawn State Machine
State machine for autonomous session cycling: watching → confirming_idle → ai_checking → sending_update → waiting_update → sending_clear → waiting_clear → sending_init → waiting_init → monitoring_init → (optionally) sending_kickstart. Steps can be skipped via config (sendClear: false, sendInit: false). After each step, waits for completionConfirmMs (10s) of output silence before proceeding. AI idle check uses a fresh Claude session to analyze terminal output for IDLE/WORKING verdict.
See docs/respawn-state-machine.md for the full state diagram, idle detection layers, and auto-accept behavior.
Spawn1337 Protocol (Autonomous Agents)
Spawned agents are full-power Claude sessions in their own screen sessions, managed via MCP tools (spawn_agent, list_agents, get_agent_status, get_agent_result, send_agent_message, cancel_agent). Max 5 concurrent, max depth 3, default timeout 30min. Agents communicate via filesystem (spawn-comms/) and signal completion via <promise>PHRASE</promise>.
See docs/spawn-protocol.md for the full protocol flow, directory structure, resource governance, and MCP configuration.
Subagent Watcher (Claude Code Background Agents)
Monitors Claude Code's internal background agents (the Task tool) in real-time. Watches ~/.claude/projects/{project}/{session}/subagents/agent-{id}.jsonl files and emits structured events.
Events: subagent:discovered, subagent:tool_call, subagent:progress, subagent:message, subagent:completed
API:
GET /api/subagents- List all known subagents (optional?minutes=60for recent only)GET /api/subagents/:agentId- Get subagent infoGET /api/subagents/:agentId/transcript- Get transcript (?limit=N,?format=formatted)DELETE /api/subagents/:agentId- Kill subagent processGET /api/sessions/:id/subagents- Get subagents for session's working directory
Status lifecycle: active → idle (30s no activity) → completed (process exited or file stale)
Settings: Can be disabled via App Settings → Display → "Enable Subagent Tracking" (default: enabled). Setting is stored in ~/.claudeman/settings.json as subagentTrackingEnabled.
Implementation: src/subagent-watcher.ts - singleton subagentWatcher started on server boot (if enabled).
Subagent Window Management (Frontend)
Floating subagent windows in the web UI show real-time activity from Claude Code's background agents. Each window displays tool calls, progress, and messages, with visual connection lines to the parent session tab.
Parent Session Discovery (app.js:findParentSessionForSubagent()):
- When a subagent is discovered via SSE event, the frontend must determine which Claudeman session spawned it
- Matching uses
workingDir→projectHashconversion:/home/user/projectbecomes-home-user-project - For each session, calls
/api/sessions/{id}/subagentswhich returns subagents matching that session'sworkingDir - First match wins: checks active session first, then iterates through other sessions
- Once found, caches
parentSessionIdandparentSessionNamein the agent object
Window Visibility (app.js:updateSubagentWindowVisibility()):
- "Show for Active Tab Only" setting controls whether windows are hidden when their parent session isn't active
- Windows with unknown parents (discovery pending) are always shown
- Minimized windows stay minimized regardless of visibility setting
Tab Badge System:
- Minimized subagents appear as a badge on their parent session's tab
- Badge dropdown allows restoring or permanently dismissing each agent
minimizedSubagentsMap tracks agentIds per sessionId
Parent Matching Algorithm (to handle multiple sessions with same workingDir):
-
Strategy 1 - Sibling matching: If another subagent with the same Claude
sessionIdalready has a parent, use that same parent. This ensures all subagents from the same Claude session go to the same Claudeman session. -
Strategy 2 - WorkingDir matching with heuristics: Find all sessions matching by
workingDir. If multiple match, prefer the most recently created session (newest session likely spawned the subagent).
Session Rename Handling: When a session is renamed, updateSubagentParentNames() updates all subagent objects and their window headers via the session:updated SSE handler.
Key Frontend Data Structures (app.js):
this.subagents = new Map(); // agentId → SubagentInfo (includes parentSessionId, parentSessionName)
this.subagentWindows = new Map(); // agentId → { element, minimized, hidden, position }
this.minimizedSubagents = new Map(); // sessionId → Set<agentId> (for tab badges)
Implementation Files:
- Server:
src/subagent-watcher.ts(discovery, monitoring, events) - Server:
src/web/server.ts(SSE broadcast, REST endpoints) - Frontend:
src/web/public/app.js:6183-6550(window management, parent discovery, visibility)
Session Modes
Sessions have a mode property (SessionMode type):
'claude': Runs Claude CLI for AI interactions (default)'shell': Runs a plain bash shell for debugging/testing
Screen-Aware Sessions
All Claude sessions spawned by Claudeman receive environment variables indicating they're running in a managed screen:
| Variable | Value | Purpose |
|---|---|---|
CLAUDEMAN_SCREEN |
1 |
Indicates session is managed by Claudeman |
CLAUDEMAN_SESSION_ID |
<uuid> |
Unique session identifier |
CLAUDEMAN_SCREEN_NAME |
claudeman-<name> |
GNU screen session name |
This prevents Claude from accidentally killing its own screen session. The default CLAUDE.md template includes guidance about this.
Implementation: Set in screen-manager.ts:createScreen() for screen-based sessions and session.ts:startInteractive()/startShell() for PTY-only sessions. Both paths also augment PATH with the claude binary's directory to ensure discovery in restricted environments (systemd, non-login shells).
Code Patterns
Memory Leak Prevention
The frontend (app.js) runs for extended periods and must avoid memory leaks. Key patterns:
SSE Reconnection: When EventSource reconnects, all event listeners are re-registered on the new instance. The old EventSource is closed, but any orphaned reconnect timeouts must be cleared:
// Clear pending reconnect timeout before creating new connection
if (this.sseReconnectTimeout) {
clearTimeout(this.sseReconnectTimeout);
this.sseReconnectTimeout = null;
}
Cleanup on Init: When handleInit() is called (SSE reconnect), clear all Maps and timers that could contain stale data:
idleTimers- Clear all timeouts, then clear the MapsubagentActivity,subagentToolResults- Clear to remove stale agent datapendingHooks,tabAlerts,_shownCompletions- Clear state tracking Sets
Interval Management: Always provide a stop method for intervals:
startSystemStatsPolling() {
this.stopSystemStatsPolling(); // Clear existing before starting
this.systemStatsInterval = setInterval(...);
}
stopSystemStatsPolling() {
if (this.systemStatsInterval) {
clearInterval(this.systemStatsInterval);
this.systemStatsInterval = null;
}
}
Session Cleanup: When a session is deleted, clean up ALL associated resources:
- Respawn state (
respawnStatus,respawnTimers,respawnCountdownTimers,respawnActionLogs) - Subagent data (windows, activity, tool results)
- Timers (idle timers, pending hooks)
Subagent Data: Clean up activity/toolResults for completed agents after 5 minutes to prevent unbounded growth during long sessions.
Pre-compiled Regex Patterns
For performance, regex patterns that are used frequently should be compiled once at module level:
// Good - compile once
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
// Bad - recompiles on each call
function parse(line: string) {
return line.replace(/\x1b\[[0-9;]*m/g, '');
}
Claude Message Parsing
Claude CLI outputs newline-delimited JSON. Strip ANSI codes before parsing:
const cleanLine = line.replace(ANSI_ESCAPE_PATTERN, '');
const msg = JSON.parse(cleanLine) as ClaudeMessage;
// msg.type: 'system' | 'assistant' | 'user' | 'result'
// msg.message?.content: Array<{ type: 'text', text: string }>
// msg.total_cost_usd: number (on result messages)
PTY Spawn Modes
All PTY spawns pass PATH: getAugmentedPath() in the env to ensure claude is discoverable even when the server runs in a restricted environment (e.g., systemd). The getAugmentedPath() function (in session.ts) resolves the claude binary's directory once at startup and prepends it to PATH. The screen-manager.ts equivalent (findClaudeDir()) does the same for screen-based spawns via export PATH="<dir>:$PATH" in the bash command.
// One-shot mode (JSON output for token tracking)
pty.spawn('claude', ['-p', '--dangerously-skip-permissions', '--output-format', 'stream-json', prompt], {
env: { ...process.env, PATH: getAugmentedPath(), ... }
})
// Interactive mode (tokens parsed from status line)
pty.spawn('claude', ['--dangerously-skip-permissions'], {
env: { ...process.env, PATH: getAugmentedPath(), ... }
})
// Shell mode (debugging/testing - no Claude CLI)
pty.spawn('bash', [], { ... })
PATH resolution search order (both session.ts and screen-manager.ts):
which claude(respects current PATH)~/.local/bin/claude~/.claude/local/claude/usr/local/bin/claude~/.npm-global/bin/claude~/bin/claude
Sending Input to Sessions
Two methods:
session.write(data)- Direct PTY write (used by/api/sessions/:id/inputendpoint)session.writeViaScreen(data)- Via GNU screen (RECOMMENDED for programmatic input). Used by RespawnController, auto-compact, auto-clear.
How writeViaScreen works internally (in screen-manager.ts:sendInput):
- Strips all
\nnewlines and\rcarriage returns from text - Sends text first:
screen -S name -p 0 -X stuff "text" - Sends Enter separately:
screen -S name -p 0 -X stuff "$(printf '\015')"
Why separate commands? Claude CLI uses Ink (React for terminals) which requires text and Enter as separate screen -X stuff commands. Combining them doesn't work. This is a critical implementation detail when debugging input issues.
IMPORTANT: All prompts sent via writeViaScreen (respawn updatePrompt, kickstartPrompt, auto-compact, etc.) must be single-line. Newlines are stripped, so multi-line prompts become one long line.
Idle Detection
Session: emits idle/working events on prompt detection + 2s activity timeout.
RespawnController: Multi-layer detection (completion message → AI idle check → output silence → token stability → working pattern absence). See docs/respawn-state-machine.md for full details.
Token Tracking
- One-shot mode: Uses
--output-format stream-jsonfor detailed token usage from JSON - Interactive mode: Parses tokens from Claude's status line (e.g., "123.4k tokens"), estimates 60/40 input/output split
Auto-Compact & Auto-Clear
| Feature | Default Threshold | Action |
|---|---|---|
| Auto-Compact | 110k tokens | /compact with optional prompt |
| Auto-Clear | 140k tokens | /clear to reset context |
Both wait for idle. Configure via session.setAutoCompact() / session.setAutoClear().
Ralph / Todo Tracking
Detects Ralph loops and todos inside Claude sessions. Disabled by default but auto-enables when Ralph-related patterns are detected (promise tags, TodoWrite, iteration patterns, etc.). See ralph-tracker.ts:shouldAutoEnable() for the full pattern list.
Auto-Configuration from Ralph Plugin State: When a session starts, Claudeman reads .claude/ralph-loop.local.md to auto-configure:
---
enabled: true
iteration: 5
max-iterations: 50
completion-promise: "COMPLETE"
---
Priority: 1) .claude/ralph-loop.local.md (official Ralph plugin state), 2) CLAUDE.md <promise> tags (fallback). See src/ralph-config.ts.
Completion Detection (multi-strategy):
- 1st occurrence of
<promise>PHRASE</promise>: Stores as expected phrase (likely in prompt) - 2nd occurrence: Emits
completionDetectedevent (actual completion) - Bare phrase detection: Also detects phrase without tags once expected phrase is known
- All complete detection: When "All X files/tasks created/completed" detected, marks all todos complete and emits completion
- If loop is already active (via
/ralph-loop:ralph-loop): Emits immediately on first occurrence
Session Lifecycle: Each session has its own independent tracker:
- New session → Fresh tracker (no carryover)
- Close tab → Tracker state cleared, UI panel hides
tracker.reset()→ Clears todos/state, keeps enabled statustracker.fullReset()→ Complete reset to initial statetracker.configure({ enabled?, completionPhrase?, maxIterations? })→ Partial config update
API:
GET /api/sessions/:id/ralph-state- Get loop state and todosPOST /api/sessions/:id/ralph-config- Configure tracker:{ enabled: boolean }- Enable/disable{ reset: true }- Soft reset (keep enabled){ reset: "full" }- Full reset
Terminal Display Fix
Tab switch/new session fix: clear xterm → write buffer → resize PTY → Ctrl+L redraw. Uses pendingCtrlL Set, triggered on session:idle/session:working events.
SSE Events
All events broadcast to /api/events with format: { type: string, sessionId?: string, data: any }.
Event prefixes: session:, task:, respawn:, spawn:, subagent:, hook:, scheduled:, case:, screen:, init.
Key events (see app.js:handleSSEEvent()):
session:idle,session:working- Status indicatorssession:terminal,session:clearTerminal- Terminal contentsession:ralphLoopUpdate,session:ralphTodoUpdate,session:ralphCompletionDetected- Ralph trackingrespawn:detectionUpdate- Idle detection statusspawn:queued,spawn:started,spawn:completed,spawn:failed- Agent lifecyclesubagent:discovered,subagent:tool_call,subagent:progress,subagent:message,subagent:completed- Claude Code background agentshook:idle_prompt,hook:permission_prompt,hook:elicitation_dialog,hook:stop- Claude Code hooks
Run Summary
Per-session event tracking for "what happened while you were away" view. Click the chart icon (📊) on any session tab to view.
Tracked Events: session start/stop, respawn cycles, state changes, idle/working transitions, token milestones (every 50k), auto-compact/clear, Ralph completions, AI check results, hook events, errors/warnings, state stuck warnings (>10min same state).
Stats: respawn cycles, peak tokens, active time, idle time, error/warning counts.
Storage: In-memory only (not persisted). Fresh tracker created per session; cleared when session is deleted.
Implementation: RunSummaryTracker class in src/run-summary.ts, integrated via setupSessionListeners() and setupRespawnListeners() in server.ts.
Frontend (app.js)
Vanilla JS + xterm.js. 60fps rendering: server batches terminal data every 16ms, client uses requestAnimationFrame to batch xterm.js writes.
HTTPS & Browser Notifications
HTTPS: The --https flag generates/reuses self-signed certificates in ~/.claudeman/certs/. Required for the Web Notification API.
Notifications (NotificationManager in app.js): In-app drawer, tab title flashing, Web Notification API (rate limited 3s), audio alerts (critical only), tab blinking (red=action, yellow=idle).
Hook Event Data: /api/hook-event forwards data field into SSE broadcast. Hook events: permission_prompt, elicitation_dialog, idle_prompt, stop.
State Store
Writes debounced (500ms) to ~/.claudeman/state.json via persistSessionState() on every meaningful change.
Per-session fields stored (SessionState in types.ts):
id,pid,status,name,mode- Core identityworkingDir,createdAt,lastActivityAt- Location and timestampsautoClearEnabled/Threshold,autoCompactEnabled/Threshold/Prompt- Context managementralphEnabled,ralphCompletionPhrase- Ralph tracker staterespawnEnabled,respawnConfig- Respawn controller statetotalCost,inputTokens,outputTokens- Token trackingparentAgentId,childAgentIds- Spawn agent tree
CLI commands (claudeman status/session list) read from state.json to display web-managed sessions.
TypeScript Config
Module resolution: NodeNext. Target: ES2022. Strict mode enabled with all additional strictness flags (noUnusedLocals, noUnusedParameters, noImplicitReturns, noFallthroughCasesInSwitch, etc.). No ESLint/Prettier configured - rely on TypeScript strict mode.
TUI uses React JSX (jsx: react-jsx, jsxImportSource: react) for Ink components.
Adding New Features
- API endpoint: Add types in
types.ts, route inserver.ts:buildServer(), usecreateErrorResponse()for errors - SSE event: Emit via
broadcast()in server.ts, handle inapp.js:handleSSEEvent()switch - Session event: Add to
SessionEventsinterface insession.ts, emit viathis.emit(), subscribe in server.ts, handle in frontend - Session setting: Add field to
SessionStateintypes.ts, include insession.toState(), callthis.persistSessionState(session)in server.ts after the change - MCP tool: Add tool definition in
mcp-server.tsusingserver.tool(), useapiRequest()to call Claudeman REST API - New test file: Create
test/<name>.test.ts, pick unique port (next available: 3190), add to port allocation table above
API Error Codes
Use createErrorResponse(code, details?) from types.ts:
| Code | Use Case |
|---|---|
NOT_FOUND |
Session/resource doesn't exist |
INVALID_INPUT |
Bad request parameters |
SESSION_BUSY |
Session is currently processing |
OPERATION_FAILED |
Action couldn't complete |
ALREADY_EXISTS |
Duplicate resource |
INTERNAL_ERROR |
Unexpected server error |
Session Lifecycle & Cleanup
- Limit: Web server:
MAX_CONCURRENT_SESSIONS = 50(server.ts:56), UI tab limit: 20, CLI default: 5 (types.ts:DEFAULT_CONFIG) - Kill (
killScreen()): child PIDs → process group → screen quit → SIGKILL - Ghost discovery:
reconcileScreens()finds orphaned screens on startup - Cleanup (
cleanupSession()): stops respawn, clears buffers/timers, kills screen, removes fromstate.json - State sync: Every session create/delete/update calls
persistSessionState()which writes full state (including respawn config from controller) tostate.json - Recovery on restart: Server reads
state.jsonfirst (has all settings), falls back toscreens.jsonfor any sessions not found. Settings (auto-compact, auto-clear, respawn config, Ralph state) restored to live session objects after screen reattachment.
TUI (WIP)
Ink/React-based TUI in src/tui/. Client to the web server, uses /api/* endpoints and attaches to screens via GNU screen.
Key files: App.tsx (main component), DirectAttach.ts (full-screen attach with tab switching), SessionList.tsx, SessionView.tsx.
Current state: Basic session list and direct attach work. Missing: full session management UI, settings panel, notifications.
Buffer Limits
| Buffer | Max Size | Trim To |
|---|---|---|
| Terminal | 2MB | 1.5MB |
| Text output | 1MB | 768KB |
| Messages | 1000 | 800 |
| Line buffer | 64KB | (flushed every 100ms) |
| Respawn buffer | 1MB | 512KB |
Tab switch uses tail=256KB for fast initial load, then chunked 64KB writes via requestAnimationFrame.
API Routes
All routes defined in server.ts:buildServer(). Key endpoint groups:
/api/events- SSE stream |/api/status- Full app state/api/sessions- CRUD +/input,/resize,/interactive/api/sessions/:id/respawn/*- Start/stop/enable/config respawn controller/api/sessions/:id/ralph-*- Ralph tracker config and state/api/sessions/:id/auto-compact,/auto-clear- Token threshold settings/api/quick-start- Create case + start session ({mode?: 'claude'|'shell'})/api/cases,/api/screens- Case and screen management/api/spawn/*- Agent lifecycle (list, status, result, messages, cancel, trigger)/api/subagents- List/get/kill Claude Code background agents, get transcripts/api/sessions/:id/subagents- Get subagents for a specific session's working directory/api/sessions/:id/run-summary- Get run summary (events, stats) for "what happened while away"/api/hook-event- Claude Code hook callbacks ({event, sessionId, data?})
State Files
| File | Purpose |
|---|---|
~/.claudeman/state.json |
Full session state (all settings, tokens, respawn config, Ralph state), tasks, app config |
~/.claudeman/state-inner.json |
Ralph loop/todo state per session (separate to reduce writes) |
~/.claudeman/screens.json |
Screen session metadata (for recovery after restart) |
~/.claudeman/settings.json |
User preferences (lastUsedCase, custom template path, subagentTrackingEnabled) |
~/.claudeman/certs/ |
Self-signed TLS certificates for --https mode |
Recovery: On restart, sessions restored from state.json (primary) with screens.json as fallback. All settings re-applied to live sessions. Cases created in ~/claudeman-cases/ by default.
CLAUDE.md Templates
New cases get a CLAUDE.md generated from src/templates/case-template.md (bundled with the project). Template resolution order:
- Custom path from
~/.claudeman/settings.json(defaultClaudeMdPathfield) - Bundled
case-template.md(copied todist/templates/during build) - Minimal fallback (if bundled template is missing)
Placeholders replaced:
[PROJECT_NAME]→ Case name[PROJECT_DESCRIPTION]→ Description[DATE]→ Current date (YYYY-MM-DD)
Screen Session Manager (CLI Tool)
./scripts/screen-manager.sh - Interactive bash tool for managing screen sessions. Commands: list, attach N, kill N,M, kill-all, info N. Requires jq and screen.
Web UI Keyboard Shortcuts
| Shortcut | Action |
|---|---|
Ctrl+Enter |
Quick-start session |
Ctrl+W |
Close session |
Ctrl+Tab |
Next session |
Ctrl+K |
Kill all sessions |
Ctrl+L |
Clear terminal |
Documentation
Reference docs (read these for deep dives):
docs/respawn-state-machine.md- Respawn controller states, idle detection, auto-acceptdocs/spawn-protocol.md- Spawn1337 agent protocol, MCP tools, resource governancedocs/ralph-wiggum-guide.md- Ralph Wiggum loop guide (plugin reference, prompt templates)docs/claude-code-hooks-reference.md- Claude Code hooks documentationdocs/browser-testing-guide.md- Browser testing frameworks comparison, patterns, and known issues
Internal planning (historical context, may be outdated):
docs/respawn-improvement-plan.md- Planned respawn improvementsdocs/run-summary-plan.md- Run summary feature design
Ralph Wiggum Loops
Core Pattern: <promise>PHRASE</promise> - The completion signal that tells the loop to stop.
Skill Commands:
/ralph-loop:ralph-loop # Start Ralph Loop in current session
/ralph-loop:cancel-ralph # Cancel active Ralph Loop
/ralph-loop:help # Show help and usage
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.
Ralph Loop Wizard: The web UI wizard (app.js:startRalphLoop()) provides a guided setup for Ralph Loops. It:
- Creates/selects a case and starts a session
- Configures the Ralph tracker with completion phrase and max iterations
- Optionally generates a task plan (
@fix_plan.md) - Sends the initial prompt with iteration protocol
Respawn for Ralph Loops: Disabled by default (checkbox unchecked). When enabled, the respawn controller uses Ralph-specific prompts:
- Update Prompt: Instructs Claude to document progress to CLAUDE.md, update planning files (
@fix_plan.md), mark completed tasks, and write a summary before/clear - Kickstart Prompt: After
/init, tells Claude it's in a Ralph Wiggum loop and to continue work by reading@fix_plan.mdand CLAUDE.md notes, then resume on uncompleted tasks