Date: 2026-02-28 Scope: Full codebase analysis across 5 dimensions: frontend, backend, TypeScript, testing, and utilities/config.
This document contains detailed findings for agent teams to write implementation plans and execute improvements. Each section includes severity, specific locations, and recommended fixes.
- Critical: server.ts God Object (6,736 LOC)
- Critical: app.js Monolith (15,196 LOC)
- Critical: CleanupManager Unused Despite Existing
- High: Duplicated Debounce/Timer Patterns
- High: Large Domain Files Need Splitting
- High: types.ts God File (1,443 LOC)
- High: Zod Schemas Duplicate TypeScript Types
- High: Test Coverage Gaps
- High: Duplicated Test Mocks
- Medium: Hardcoded Magic Values
- Medium: Frontend Global State Monolith
- Medium: Frontend Code Duplication
- Medium: Inconsistent Logging
- Medium: Utils Barrel Export Gaps
- Medium: Non-Null Assertion Risks
- Low: Dead Utility Functions
- Low: No Dependency Injection for File I/O
- Scorecard & Prioritized Roadmap
File: src/web/server.ts (6,736 lines)
Severity: CRITICAL
Impact: Hardest file to maintain, test, and extend. Imports 38 modules.
The WebServer class handles everything: HTTP routing (~110 routes), authentication, SSE broadcasting, terminal data batching, state persistence, session lifecycle, respawn orchestration, file serving, tunnel management, plan orchestration, and subagent coordination.
Key metrics:
- 40+ private properties (Maps, timers, caches)
- 70+ methods
setupRoutes()is 2,000+ LOC of inline route handlers- Zero test coverage
WebServer class (6,736 LOC)
├── Auth session management (lines 469, 668-698)
├── SSE client management (lines 407-408, 5843-5880)
├── Terminal data batching (lines 414-416, 5909-5966)
├── Task update batching (line 426, 5995-6028)
├── State persistence batching (lines 429-430, 6028-6061)
├── Respawn lifecycle (lines 445-451, 5425-5534)
├── Session cleanup (lines 4769-4961)
├── Listener setup (lines 544-643)
└── setupRoutes() (lines 645+, 2000+ LOC)
├── /api/sessions/* (30+ routes inline)
├── /api/respawn/* (7 routes inline)
├── /api/subagents/* (7 routes inline)
├── /api/plan/* (5 routes inline)
├── /api/push/* (4 routes inline)
└── ... 60+ more inline
src/web/
├── server.ts (~500 LOC - HTTP setup, route registration only)
├── routes/
│ ├── session-routes.ts (session CRUD, input, resize)
│ ├── respawn-routes.ts (respawn control endpoints)
│ ├── subagent-routes.ts (background agent tracking)
│ ├── plan-routes.ts (plan generation & management)
│ ├── push-routes.ts (web push subscriptions)
│ ├── mux-routes.ts (tmux management)
│ ├── case-routes.ts (case management)
│ ├── file-routes.ts (file browsing/serving)
│ └── system-routes.ts (status, stats, config, settings)
├── middleware/
│ ├── auth.ts (Basic Auth + session cookies)
│ └── error-handler.ts (centralized error responses)
└── services/
├── sse-manager.ts (SSE client + broadcast)
├── terminal-batcher.ts (60fps terminal batching)
└── session-lifecycle.ts (listener setup/teardown)
Error response pattern repeated 189 times:
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');Fix: Extract findSessionOrFail() middleware:
const findSessionOrFail = (sessionId: string) => {
const session = this.sessions.get(sessionId);
if (!session) throw new NotFoundError('Session not found');
return session;
};Event listener setup copy-pasted for subagent watcher, image watcher, and team watcher (lines 544-643). Same attach/detach pattern duplicated 3 times.
File: src/web/public/app.js (15,196 lines)
Severity: CRITICAL
Impact: Untestable, hard to navigate, tightly coupled systems.
| Module | Lines | Current Location | Impact |
|---|---|---|---|
| Mobile handlers (MobileDetection, KeyboardHandler, SwipeHandler) | ~300 | lines 168-620 | High |
| Voice input (DeepgramProvider, VoiceInput) | ~830 | lines 631-1471 | High |
| NotificationManager | ~450 | lines 2218-2663 | High |
| xterm-zerolag-input (inlined copy from packages/) | ~400 | lines 1756-2153 | High |
| KeyboardAccessoryBar | ~195 | lines 1480-1680 | Medium |
| FocusTrap | ~60 | lines 1690-1748 | Medium |
The main CodemanApp class starting at line 2665 has:
- 60+ Maps/Sets in the constructor (lines 2667-2805)
- 18 Map instances with complex cross-references (subagents, parents, teams, windows)
- 10+ monolithic methods exceeding 100 lines each
Largest methods:
| Method | Lines | Size |
|---|---|---|
renderAppSettings() |
14400-14700 | ~300 LOC |
selectSession() |
6028-6250 | ~220 LOC |
batchTerminalWrite() |
7482-7700 | ~200 LOC |
renderSessionTabs() |
5814-6000 | ~180 LOC |
openSubagentWindow() |
11927-12100 | ~170 LOC |
handleInit() |
5183-5350 | ~170 LOC |
src/web/public/
├── app.js (~4000 LOC - core app, session mgmt, SSE)
├── mobile.js (~300 LOC - MobileDetection, KeyboardHandler, SwipeHandler)
├── voice.js (~830 LOC - DeepgramProvider, VoiceInput)
├── notifications.js (~450 LOC - NotificationManager)
├── keyboard-accessory.js (~200 LOC - KeyboardAccessoryBar)
├── api-client.js (~100 LOC - fetch wrapper with error handling)
└── config.js (~50 LOC - magic numbers, z-index layers)
File: src/utils/cleanup-manager.ts (320 lines)
Severity: CRITICAL
Impact: Memory leak risk. Well-designed utility exists but is never used. Every file manages cleanup manually.
CleanupManager is exported from the utils barrel but has 0 instantiations in production code. Instead, every file implements manual cleanup:
respawn-controller.ts (worst offender):
// 11 timer properties, manually cleared in stop()
private stepTimer: NodeJS.Timeout | null = null;
private completionConfirmTimer: NodeJS.Timeout | null = null;
private noOutputTimer: NodeJS.Timeout | null = null;
// ... 8 more
stop() {
if (this.stepTimer) clearTimeout(this.stepTimer);
if (this.completionConfirmTimer) clearTimeout(this.completionConfirmTimer);
// ... 9 more clearTimeout/clearInterval calls
}Files that should use CleanupManager:
| File | Timer/Listener Count | Current Cleanup |
|---|---|---|
respawn-controller.ts |
11 timers + intervals | 11 manual clearTimeout/clearInterval |
web/server.ts |
6+ timers, debounce map | Manual in stop(), some may leak |
state-store.ts |
2 debounce timers | Manual clearTimeout |
push-store.ts |
1 save timer | Manual clearTimeout |
subagent-watcher.ts |
debounce map + watchers | Manual clear + close |
ralph-tracker.ts |
3 debounce timers | Manual clear |
bash-tool-parser.ts |
1 debounce timer | Manual clear |
image-watcher.ts |
1 debounce map | Manual clear |
Migrate all timer management to use CleanupManager. Example for respawn-controller.ts:
// Before: 11 fields + 11 clearTimeout calls
private stepTimer: NodeJS.Timeout | null = null;
// ...
// After: 1 field, auto-cleanup
private cleanup = new CleanupManager();
startStep() {
this.cleanup.setTimeout(() => { ... }, 5000, 'step');
}
stop() {
this.cleanup.dispose(); // Clears everything
}Severity: HIGH Impact: 8+ files implement debounce independently. Bug fixes need to be applied everywhere.
// Pattern 1: Manual timer ref (used in 6 files)
private saveTimer: NodeJS.Timeout | null = null;
debouncedSave() {
if (this.saveTimer) clearTimeout(this.saveTimer);
this.saveTimer = setTimeout(() => this.save(), 500);
}
// Pattern 2: Timer Map (used in 3 files)
private fileDebouncers = new Map<string, NodeJS.Timeout>();
debounce(key: string) {
const existing = this.fileDebouncers.get(key);
if (existing) clearTimeout(existing);
this.fileDebouncers.set(key, setTimeout(() => { ... }, 100));
}
// Pattern 3: State flag (used in 2 files)
private isSaving = false;| File | Debounce Vars | Delay (ms) |
|---|---|---|
state-store.ts |
saveTimeout, ralphStateSaveTimeout |
500 |
push-store.ts |
saveTimer |
500 |
web/server.ts |
persistDebounceTimers (Map) |
500 |
subagent-watcher.ts |
fileDebouncers (Map) |
100 |
ralph-tracker.ts |
3 debounce timers | 50, 30000 |
bash-tool-parser.ts |
EVENT_DEBOUNCE_MS |
50 |
image-watcher.ts |
debounce map | 200 |
respawn-controller.ts |
11 timer fields | various |
Create a Debouncer utility:
// src/utils/debouncer.ts
export class Debouncer {
private timer: NodeJS.Timeout | null = null;
constructor(private readonly delayMs: number) {}
run(fn: () => void): void {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(fn, this.delayMs);
}
cancel(): void {
if (this.timer) clearTimeout(this.timer);
this.timer = null;
}
}
// Usage:
private saveDeb = new Debouncer(500);
this.saveDeb.run(() => this.save());
// cleanup: this.saveDeb.cancel();Severity: HIGH Impact: Complex state machines spanning 3,000+ lines are hard to understand and test.
5 responsibilities mixed:
- Output Parsing (~900 LOC) - Line-by-line parsing, state extraction
- Todo Management (~700 LOC) - Parsing, dedup, expiry
- Plan Tracking (~800 LOC) - Enhanced plan tasks, checkpoints
- Circuit Breaker (~400 LOC) - State machine for stuck detection
- File Watching (~300 LOC) - Monitor external state files
Recommended split:
ralph-tracker.ts (core output parsing, ~1200 LOC)
ralph-todo-manager.ts (todo parsing + management, ~700 LOC)
ralph-plan-tracker.ts (plan tasks + checkpoints, ~800 LOC)
ralph-circuit-breaker.ts (circuit breaker logic, ~400 LOC)
6 responsibilities mixed:
- State Machine (~1,000 LOC) - 6+ states, transitions
- Idle Detection (~800 LOC) - 5 layers + multi-signal combining
- AI Checkers (~600 LOC) - Idle + plan checkers integration
- Health Scoring (~500 LOC) - Metrics, circuit breaker, scoring
- Action Logging (~300 LOC) - Timeline, detection status
- Stuck-State Detection (~250 LOC) - Timeout tracking
Recommended split:
respawn-controller.ts (state machine core, ~1000 LOC)
respawn-idle-detection.ts (all 5 idle detection layers, ~800 LOC)
respawn-health-scorer.ts (metrics & health scoring, ~500 LOC)
8 responsibilities mixed:
- PTY Management (~600 LOC)
- Terminal I/O (~400 LOC)
- Token Tracking (~200 LOC)
- Task Tracking (~250 LOC)
- Ralph Integration (~200 LOC)
- Auto-Clear/Compact (~300 LOC)
- Image Watching (~100 LOC)
- CLI Detection (~150 LOC)
Recommended split:
session.ts (PTY + terminal I/O core, ~1000 LOC)
session-tracking.ts (token + task + Ralph, ~500 LOC)
session-auto-ops.ts (auto-clear/compact + image, ~300 LOC)
File: src/types.ts (1,443 lines, 72 exported definitions)
Severity: HIGH
Impact: Every file imports from types.ts. Hard to find relevant types.
- 46 interfaces
- 25 types
- 1 enum (ApiErrorCode)
- 9 factory functions (createInitialState, etc.)
src/types/
├── index.ts (barrel export - transparent migration)
├── session.ts (SessionState, SessionConfig, SessionMode, SessionColor)
├── task.ts (TaskState, TaskDefinition, TaskStatus)
├── respawn.ts (RespawnConfig, RespawnState, CircuitBreakerStatus)
├── ralph.ts (RalphLoopState, RalphTrackerState, RalphTodoItem)
├── api.ts (ApiResponse, ApiErrorCode, HookEventType, all route types)
├── lifecycle.ts (LifecycleEventType, LifecycleEntry)
└── common.ts (Disposable, BufferConfig, CleanupResourceType)
The barrel export makes this a transparent refactor - existing import from './types' continues to work.
File: src/web/schemas.ts (508 lines)
Severity: HIGH
Impact: When a type changes, the Zod schema must be manually updated too. Source of bugs.
Zod schemas manually duplicate TypeScript interfaces. Zero z.infer usage found.
// types.ts (manual interface)
export interface CreateSessionRequest {
workingDir?: string;
mode?: SessionMode;
name?: string;
}
// schemas.ts (manual Zod schema - duplicated!)
export const CreateSessionSchema = z.object({
workingDir: safePathSchema.optional(),
mode: z.enum(['claude', 'shell', 'opencode']).optional(),
name: z.string().max(100).optional(),
});Use z.infer to derive TypeScript types from Zod schemas (single source of truth):
// schemas.ts
export const CreateSessionSchema = z.object({
workingDir: safePathSchema.optional(),
mode: z.enum(['claude', 'shell', 'opencode']).optional(),
name: z.string().max(100).optional(),
});
// types.ts (auto-derived)
export type CreateSessionRequest = z.infer<typeof CreateSessionSchema>;Affected schemas (~10):
- CreateSessionSchema
- RunPromptSchema
- ResizeSchema
- CreateCaseSchema
- QuickStartSchema
- HookEventSchema
- RespawnConfigSchema
- ConfigUpdateSchema
- SettingsUpdateSchema
Severity: HIGH Impact: Critical code paths untested. Regressions go unnoticed.
| File | Lines | Risk |
|---|---|---|
src/web/server.ts |
6,736 | CRITICAL - Core REST API, 280+ routes |
src/plan-orchestrator.ts |
~500 | HIGH - Multi-agent plan generation |
src/tunnel-manager.ts |
~200 | MEDIUM - Cloudflare tunnel |
src/session-lifecycle-log.ts |
~150 | MEDIUM - JSONL audit log |
src/ai-plan-checker.ts |
~300 | MEDIUM - Plan completion detection |
src/templates/claude-md.ts |
~200 | LOW - CLAUDE.md generation |
src/utils/claude-cli-resolver.ts |
~100 | LOW - CLI path resolution |
src/utils/opencode-cli-resolver.ts |
~100 | LOW - OpenCode CLI support |
src/utils/regex-patterns.ts |
~100 | LOW - Used everywhere! |
src/utils/token-validation.ts |
~50 | LOW - Token counting |
10 "not.toThrow()" tests without behavior verification:
// BAD: Only checks it doesn't crash
expect(() => tracker.processMessage(null)).not.toThrow();
// GOOD: Also verify defensive behavior
expect(() => tracker.processMessage(null)).not.toThrow();
expect(tracker.getAllTasks().size).toBe(0);Locations:
task-tracker.test.ts- 5 instancesimage-watcher.test.ts- 1 instancetask-queue.test.ts- 1 instance- Others scattered
Severity: HIGH Impact: Mock changes need updating in 4 places. Inconsistent mock behavior.
| File | Usage |
|---|---|
test/respawn-controller.test.ts |
Full mock with event emitter |
test/session-manager.test.ts |
Simpler mock |
test/respawn-team-awareness.test.ts |
Copy of respawn-controller mock |
test/respawn-test-utils.ts |
Comprehensive mock - UNUSED! |
| File | Usage |
|---|---|
test/session-manager.test.ts |
Basic mock |
test/ralph-loop.test.ts |
Separate implementation |
test/respawn-test-utils.ts exports these utilities that no test file imports:
createTimeController()- Abstraction over vitest fake timersMockAiIdleChecker- Fully mocked AI idle checkerMockAiPlanChecker- Fully mocked plan checker- Factory functions for pre-configured controllers
Create test/mocks/ directory:
test/
├── mocks/
│ ├── mock-session.ts (single MockSession, used everywhere)
│ ├── mock-state-store.ts (single MockStateStore)
│ └── index.ts (barrel export)
├── utils/
│ └── time-controller.ts (from respawn-test-utils.ts)
└── ... test files
Severity: MEDIUM Impact: Hard to tune, inconsistent when same value appears in multiple places.
src/config/buffer-limits.ts- All buffer sizessrc/config/map-limits.ts- All collection limits
In server.ts (lines 145-194):
const TASK_UPDATE_BATCH_INTERVAL = 100;
const STATE_UPDATE_DEBOUNCE_INTERVAL = 500;
const SESSIONS_LIST_CACHE_TTL = 1000;
const SCHEDULED_CLEANUP_INTERVAL = 5 * 60 * 1000;
const SSE_HEALTH_CHECK_INTERVAL = 30 * 1000;
const MAX_TERMINAL_COLS = 500;
const MAX_TERMINAL_ROWS = 200;
const AUTH_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
const MAX_AUTH_SESSIONS = 100;
const AUTH_FAILURE_WINDOW_MS = 15 * 60 * 1000;
const STATS_COLLECTION_INTERVAL_MS = 2000;
const MAX_INPUT_LENGTH = 64 * 1024;In hooks-config.ts: timeout: 10000 hardcoded 6 times.
In respawn-controller.ts (lines 538-565): 10 timing constants.
In utils: EXEC_TIMEOUT_MS = 5000 duplicated in both claude-cli-resolver.ts and opencode-cli-resolver.ts.
In app.js:
// line 27: 600000 - stuck detection threshold
// line 24: 5000 - default scrollback
// lines 34-35: 128*1024, 256*1024 - chunk sizes
// lines 152-155: 150, 100 - keyboard detection thresholds
// lines 573-575: 80, 300, 100 - swipe detection paramsCreate additional config files:
src/config/
├── buffer-limits.ts (existing)
├── map-limits.ts (existing)
├── server-config.ts (NEW - web server intervals, auth, caching)
├── timing-config.ts (NEW - debounce delays, check intervals)
└── terminal-config.ts (NEW - max cols/rows, batch intervals)
Severity: MEDIUM Impact: All state in single CodemanApp class. Tight coupling between unrelated systems.
this.sessions = new Map(); // Session data
this.subagents = new Map(); // Agent tracking
this.subagentActivity = new Map(); // Tool call tracking
this.subagentToolResults = new Map(); // Result caching
this.subagentParentMap = new Map(); // Agent-to-session mapping
this.teams = new Map(); // Team tracking
this.teamTasks = new Map(); // Team task state
this.planSubagents = new Map(); // Plan agent tracking
this.pendingWrites = []; // Terminal write queue
this.terminalBufferCache = new Map(); // Buffer caching (unbounded!)
this.projectInsights = new Map(); // Bash tool insights
// ... 40+ more- 18 Map instances with complex cross-references (no garbage collection strategy)
- No domain separation: Session, subagent, notification, UI, and network state mixed
- Implicit dependencies:
selectSession()requires 5+ Maps to be in consistent state terminalBufferCachehas no max size - can grow unbounded with many sessions
// Instead of 60+ flat properties:
class SessionState {
sessions = new Map();
sessionOrder = [];
terminalBuffers = new Map();
tabAlerts = new Map();
}
class SubagentState {
subagents = new Map();
activity = new Map();
parentMap = new Map();
windows = new Map();
minimized = new Map();
}
class TeamState {
teams = new Map();
tasks = new Map();
teammates = new Map();
}
class UIState {
activeSessionId = null;
draggedTabId = null;
isLoadingBuffer = false;
}Severity: MEDIUM Impact: Repeated patterns increase maintenance burden and inconsistency risk.
API fetch calls (~50 instances):
// Repeated everywhere:
fetch(`/api/sessions/${sessionId}/...`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({...})
}).catch(() => {})Fix: Extract ApiClient class.
innerHTML usage (104 instances):
- Mix of template strings, createElement chains, and direct innerHTML
- Some with manual XSS escaping (
text.replace(/</g, '<')), some without - No consistent DOM creation pattern
typeof app !== 'undefined' checks (20+ instances):
- Lines 458, 467, 481, 614, 617, 1549, etc.
- Fix: Ensure
appis always defined as global singleton.
Element visibility toggling (212+ occurrences):
element.classList.add('active')
element.classList.remove('active')Fix: Create toggleClass(el, className, condition) utility.
- 152
addEventListenercalls with fragile cleanup - Mix of inline (
onclick="app.method()") and addEventListener - hard to track - Element cache (
_elemCache) never invalidated if DOM elements are recreated (line 2808) - Tab drag-and-drop listeners may not clean up if user switches tabs mid-drag
Severity: MEDIUM Impact: Hard to debug in production. Can't filter by severity or component.
- 345 console calls across source files
- No structured logging - all
console.log/errordirectly - No log levels (DEBUG, INFO, WARN, ERROR)
// Some files use brackets:
console.log('[Session] Starting interactive...');
console.log('[RalphLoop] Task assigned...');
console.log('[TunnelManager] Tunnel started');
// Others use no prefix:
console.error('Failed to spawn PTY:', err);
console.log('Server listening on port', port);src/utils/cleanup-manager.ts has a debugMode flag for conditional debug logging - good pattern not replicated elsewhere.
Either:
- Enforce consistent
[ComponentName]prefixes via lint rule - Create lightweight logger abstraction (not a heavy framework)
File: src/utils/index.ts
Severity: MEDIUM
Impact: Forces deep imports, unclear public API.
These functions are defined but NOT exported from the barrel:
createAnsiPatternFull()andcreateAnsiPatternSimple()(factory functions fromregex-patterns.ts)SAFE_PATH_PATTERN(fromregex-patterns.ts)validateTokenCounts()andvalidateTokensAndCost()(fromtoken-validation.ts)isSimilar(),isSimilarByDistance(),levenshteinDistance(),normalizePhrase()(fromstring-similarity.ts- though some are dead code, see finding #16)
Some files bypass the barrel unnecessarily:
// Could use barrel:
import { BufferAccumulator } from './utils/buffer-accumulator.js';
import { LRUMap } from './utils/lru-map.js';
// Must deep import (not in barrel):
import { SAFE_PATH_PATTERN } from './utils/regex-patterns.js';Add missing exports to src/utils/index.ts and update import sites.
Severity: MEDIUM Impact: Runtime crashes if assumptions violated. 37 instances found.
| File | Count | Risk Level |
|---|---|---|
src/web/server.ts |
10 | Low (auth flow verified) |
src/session.ts |
6 | High (mux/terminal refs) |
src/respawn-controller.ts |
4 | Low (config validated) |
src/lru-map.ts |
3 | Low (checked lookups) |
src/subagent-watcher.ts |
2 | Low (pending tool calls) |
| Others | 12 | Low |
// Line 915 - _mux could be null if startInteractive called during cleanup
`[Session] Starting interactive (with ${this._mux!.backend})`
// Line 954 - _muxSession could be null in race condition
this._muxSession!.muxNameAdd null guards before assertions, or document invariants:
// Before:
this._mux!.backend
// After:
if (!this._mux) throw new Error('Invariant: _mux must be initialized before startInteractive');
this._mux.backend- 0 instances of
as any - 0 instances of
@ts-ignoreor@ts-expect-error - TypeScript overall score: 8.5/10
File: src/utils/string-similarity.ts
Severity: LOW
Impact: Code clutter, confusion about what's actually used.
These are defined and exported but never imported anywhere:
isSimilar(a, b, threshold)- similarity check with thresholdisSimilarByDistance(a, b, maxDistance)- Levenshtein-based checklevenshteinDistance(a, b)- raw edit distancenormalizePhrase(phrase)- phrase normalization
Only these are imported from the barrel:
stringSimilarity()- used in ralph-tracker.tsfuzzyPhraseMatch()- used in ralph-tracker.tstodoContentHash()- used in ralph-tracker.ts
Delete unused functions or mark as @internal if kept for future use.
Severity: LOW (practical impact limited at current scale) Impact: Can't mock filesystem for unit tests. 68+ hard-coded filesystem calls.
// state-store.ts - directly imports and uses fs
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
// push-store.ts - hard-coded paths
const KEYS_FILE = join(DATA_DIR, 'push-keys.json');
const SUBS_FILE = join(DATA_DIR, 'push-subscriptions.json');
// ai-checker-base.ts - direct execSync
execSync(`tmux kill-session -t "${this.checkMuxName}"`, { timeout: 3000 });- The codebase uses integration tests (spawning real processes/tmux sessions) rather than unit tests
- Most filesystem operations are in infrastructure code, not business logic
- Adding DI would be a large refactor with limited near-term benefit
| Category | Before | After | Notes |
|---|---|---|---|
| TypeScript Safety | 8.5/10 | 9/10 | 0 any, 0 @ts-ignore, Zod z.infer eliminates type drift |
| Error Handling | 8/10 | 8/10 | Unchanged — already strong |
| Async/Promise Safety | 9.5/10 | 9.5/10 | Unchanged — already strong |
| Resource Cleanup | 7/10 | 8/10 | CleanupManager adopted in server.ts, subagent-watcher, bash-tool-parser; Debouncer in 6 files. Gaps: respawn-controller (10+ manual timers) and ralph-tracker (2 manual timers) not migrated |
| Module Organization | 5/10 | 8/10 | Routes extracted (12 modules), types split (14 domain files), domain files split (ralph: 7, respawn: 5, session: 6) |
| Test Coverage | 6/10 | 7.5/10 | Shared mock infrastructure, 12 route test files, MockSession/MockStateStore consolidated |
| Config Centralization | 6/10 | 9/10 | 9 config files, ~65 constants centralized, 0 cross-file duplicates |
| Frontend Architecture | 4/10 | 7/10 | 8 extracted modules (3,453 LOC), app.js reduced 24% (15.2K → 11.5K), xterm-zerolag-input vendor build |
| Code Duplication | 5/10 | 8/10 | Debouncer utility, shared test mocks, barrel exports, config consolidation |
Phase 1 - Quick Wins (1-2 days) ✅ COMPLETE
- ✅ Export missing functions from utils barrel (~30 min) —
createAnsiPatternFull,createAnsiPatternSimple,SAFE_PATH_PATTERN,validateTokenCounts,validateTokensAndCostall now exported fromsrc/utils/index.ts - ✅ Delete dead utility functions (~15 min) —
isSimilar()removed fromstring-similarity.ts;levenshteinDistance(),isSimilarByDistance(),normalizePhrase()made private (used internally byfuzzyPhraseMatch/stringSimilarity) - ✅ Consolidate duplicated
EXEC_TIMEOUT_MSconstant (~15 min) — Createdsrc/config/exec-timeout.tsas single source of truth;claude-cli-resolver.ts,opencode-cli-resolver.ts, andtmux-manager.tsall import from it - ✅ Add
z.inferto Zod schemas (~2 hours) —src/web/schemas.tsnow has 36z.infertype exports (lines 512-547) covering all schemas - ✅ Fix 10 weak "not.toThrow()" tests (~1 hour) — All
not.toThrow()calls now have behavior assertions:task-tracker.test.ts(6 instances all followed by state checks),image-watcher.test.ts(1 instance followed by length check),session-manager.test.ts(1 instance followed by count check)
Phase 2 - CleanupManager & Debounce (2-3 days) ✅ COMPLETE
- ✅ Create
Debouncerutility class (~1 hour) — Createdsrc/utils/debouncer.tswithDebouncerandKeyedDebouncerclasses; exported fromsrc/utils/index.ts - ✅ Migrate all 8 files from manual debounce to Debouncer —
state-store.ts(2 Debouncers),push-store.ts(1 Debouncer),bash-tool-parser.ts(1 Debouncer),image-watcher.ts(1 KeyedDebouncer),subagent-watcher.ts(2 KeyedDebouncers),server.ts(1 KeyedDebouncer for persist timers),ralph-tracker.ts(2 Debouncers replacing 4 manual fields:_todoUpdateTimer,_loopUpdateTimer,_todoUpdatePending,_loopUpdatePending) - ✅ Migrate respawn-controller to CleanupManager — 10 manual timer fields replaced with single
CleanupManagerinstance +timerIdsMap.startTrackedTimer()/cancelTrackedTimer()preserved as wrappers for UI countdown display and timer events.clearTimers()uses dispose-and-recreate pattern for state transitions. - ✅ Migrate server.ts timer cleanup to CleanupManager (~2 hours) —
private cleanup = new CleanupManager()present; terminal batch timers and pending respawn starts left as manual Maps (complex lifecycle) - ✅ Migrate remaining files —
bash-tool-parser.ts(CleanupManager ✅),subagent-watcher.ts(CleanupManager ✅),ralph-tracker.ts(Debouncer ✅)
Phase 3 - server.ts Route Extraction (3-4 days) ✅ COMPLETE
- ✅ Created
src/web/routes/with 12 domain route modules + index barrel (4,090 LOC total): session (909), system (768), ralph (533), plan (459), respawn (315), case, file, hook-event, mux, push, scheduled, team - ✅ Created
src/web/middleware/auth.ts(193 LOC) — Basic Auth, session cookies, rate limiting, security headers, CORS - ✅ Created
src/web/ports/with 7 typed port interfaces (142 LOC) — SessionPort, EventPort, RespawnPort, ConfigPort, InfraPort, AuthPort; routes declare dependencies via intersection types - ✅ Created
src/web/route-helpers.ts(154 LOC) —findSessionOrFail(),formatUptime(),sanitizeHookData(),autoConfigureRalph() - ✅ Reduced
server.tsfrom 6,736 → 2,697 LOC (60% reduction). Remaining LOC is justified infrastructure: session lifecycle, SSE broadcast engine, terminal batching, respawn integration, resource cleanup
Phase 4 - Domain File Splitting (2-3 days) ✅ COMPLETE
- ✅ Split
types.tsintosrc/types/directory — 14 domain files (1,469 LOC total): common, session, task, app-state, respawn, ralph, api, lifecycle, run-summary, tools, teams, push, plan + index barrel. Originaltypes.tsis now a 1-line re-export - ✅ Split
ralph-tracker.tsinto 7 files (exceeded plan of 4) — ralph-tracker (2,391), ralph-plan-tracker (477), ralph-status-parser (552), ralph-fix-plan-watcher (366), ralph-stall-detector (166), ralph-config (153), ralph-loop (522) - ✅ Split
respawn-controller.tsinto 5 files (exceeded plan of 3) — respawn-controller (3,228), respawn-health (229), respawn-metrics (229), respawn-patterns (131), respawn-adaptive-timing (134) - ✅ Split
session.tsinto 6 files (exceeded plan of 3) — session (2,168), session-manager (298), session-auto-ops (284), session-cli-builder (132), session-task-cache (101), session-lifecycle-log (114)
Phase 5 - Frontend Modularization (3-4 days) ✅ COMPLETE
- ✅ Extracted
constants.js(238 LOC) — shared constants, timing values, Z-index layers,escapeHtml(),extractSyncSegments() - ✅ Extracted
mobile-handlers.js(449 LOC) —MobileDetection,KeyboardHandler,SwipeHandler - ✅ Extracted
voice-input.js(853 LOC) —DeepgramProvider,VoiceInput - ✅ Extracted
notification-manager.js(445 LOC) —NotificationManagerclass (5-layer system) - ✅ Extracted
keyboard-accessory.js(279 LOC) —KeyboardAccessoryBar,FocusTrap - ✅ Extracted
api-client.js(70 LOC) —_api(),_apiJson(),_apiPost(),_apiPut() - ✅ Extracted
subagent-windows.js(1,119 LOC) — 13 subagent window methods - ✅ Removed inlined xterm-zerolag-input copy → built to
vendor/xterm-zerolag-input.jsfrompackages/xterm-zerolag-input/ - ✅ Reduced
app.jsfrom ~15,200 → 11,473 LOC (24% reduction). All scripts loaded in correct dependency order inindex.html
Phase 6 - Config Consolidation (1 day) ✅ COMPLETE
- ✅ Created 6 new domain-focused config files (better than plan's 2 generic files):
server-timing.ts(13 constants),auth-config.ts(5 constants),tunnel-config.ts(8 constants),terminal-limits.ts(4 constants),ai-defaults.ts(3 constants),team-config.ts(3 constants) - ✅ Total: 9 config files in
src/config/, ~65 constants centralized - ✅ Eliminated all cross-file duplicates:
STATS_COLLECTION_INTERVAL_MS(was in 2 files),timeout: 10000(was 6× inline in hooks-config.ts →HOOK_TIMEOUT_MS), AI model string (was in 5 files →AI_CHECK_MODEL),MAX_TRACKED_AGENTS(was shadowed in subagent-watcher.ts) - ✅ CLAUDE.md updated with config files table, import conventions, resource limits references
Phase 7 - Test Infrastructure (2-3 days) ✅ COMPLETE
- ✅ Created
test/mocks/directory with 5 files (541 LOC):mock-session.ts(312),mock-state-store.ts(60),mock-route-context.ts(121),test-helpers.ts(37),index.ts(11 — barrel export) - ✅ Consolidated MockSession into single shared definition — no duplicate class definitions remain (2
vi.mock()-based copies intentionally left in session-manager.test.ts and ralph-loop.test.ts) - ✅
respawn-test-utils.tsconverted to backward-compatibility shim — re-exports fromtest/mocks/, retains respawn-specific utilities (MockAiIdleChecker, TimeController, etc.) - ✅ Created initial 3 route test files with 58 total tests:
session-routes.test.ts(34 tests),respawn-routes.test.ts(13 tests),system-routes.test.ts(11 tests). Route test harness usesapp.inject()— no real ports needed - ✅ All 12 route modules now have dedicated test files in
test/routes/: session, respawn, system, ralph, plan, push, team, mux, file, scheduled, hook-event, case
| File | Before | After | Change |
|---|---|---|---|
src/web/server.ts |
6,736 | 2,697 | −60% (routes, auth, ports extracted) |
src/web/public/app.js |
15,196 | 11,473 | −24% (8 modules extracted) |
src/ralph-tracker.ts |
3,905 | 2,391 | −39% (6 companion files extracted) |
src/respawn-controller.ts |
3,611 | 3,228 | −11% (4 companion files extracted) |
src/session.ts |
2,418 | 2,168 | −10% (5 companion files extracted) |
src/types.ts |
1,443 | 1 | −99% (14 domain files in src/types/) |
| Directory | Files | Total LOC | Purpose |
|---|---|---|---|
src/web/routes/ |
13 | 4,090 | Domain route modules |
src/web/ports/ |
7 | 142 | Port interfaces for DI |
src/web/middleware/ |
1 | 193 | Auth middleware |
src/types/ |
14 | 1,469 | Domain type files |
src/config/ |
9 | ~450 | Centralized config |
test/mocks/ |
5 | 541 | Shared test mocks |
test/routes/ |
4 | ~500 | Route handler tests |
| Module | Lines | Purpose |
|---|---|---|
subagent-windows.js |
1,119 | Subagent window management |
voice-input.js |
853 | DeepgramProvider, VoiceInput |
mobile-handlers.js |
449 | MobileDetection, KeyboardHandler, SwipeHandler |
notification-manager.js |
445 | 5-layer notification system |
keyboard-accessory.js |
279 | KeyboardAccessoryBar, FocusTrap |
constants.js |
238 | Shared constants, timing, Z-index |
api-client.js |
70 | API fetch wrapper |
These patterns should be preserved, not refactored:
- Clean one-way dependency graph (no circular deps)
- EventEmitter-based decoupling between domain models
- Proper
import typeusage (19 files, consistent) - Utility type adoption (101 instances of Record, Partial, Omit, etc.)
assertNever()for exhaustive switch checkingStaleExpirationMapandLRUMapfor bounded collections- State persistence circuit breaker pattern
- TypeScript strict mode with all safety flags enabled
CleanupManagerfor centralized timer/watcher disposalDebouncer/KeyedDebouncerfor consistent debounce patterns- Port interfaces for route module dependency injection
Object.assign(CodemanApp.prototype, ...)for frontend module composition