Skip to content

Latest commit

 

History

History
977 lines (756 loc) · 37.9 KB

File metadata and controls

977 lines (756 loc) · 37.9 KB

Code Structure & Quality Findings

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.


Table of Contents

  1. Critical: server.ts God Object (6,736 LOC)
  2. Critical: app.js Monolith (15,196 LOC)
  3. Critical: CleanupManager Unused Despite Existing
  4. High: Duplicated Debounce/Timer Patterns
  5. High: Large Domain Files Need Splitting
  6. High: types.ts God File (1,443 LOC)
  7. High: Zod Schemas Duplicate TypeScript Types
  8. High: Test Coverage Gaps
  9. High: Duplicated Test Mocks
  10. Medium: Hardcoded Magic Values
  11. Medium: Frontend Global State Monolith
  12. Medium: Frontend Code Duplication
  13. Medium: Inconsistent Logging
  14. Medium: Utils Barrel Export Gaps
  15. Medium: Non-Null Assertion Risks
  16. Low: Dead Utility Functions
  17. Low: No Dependency Injection for File I/O
  18. Scorecard & Prioritized Roadmap

1. Critical: server.ts God Object

File: src/web/server.ts (6,736 lines) Severity: CRITICAL Impact: Hardest file to maintain, test, and extend. Imports 38 modules.

Problem

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

Current Structure (Bad)

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

Recommended Structure

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)

Duplication in server.ts

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.


2. Critical: app.js Monolith

File: src/web/public/app.js (15,196 lines) Severity: CRITICAL Impact: Untestable, hard to navigate, tightly coupled systems.

Extractable Modules (by priority)

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

CodemanApp Class (12,000+ LOC)

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

Recommended Split

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)

3. Critical: CleanupManager Unused

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.

Current State

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

Fix

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
}

4. High: Duplicated Debounce/Timer Patterns

Severity: HIGH Impact: 8+ files implement debounce independently. Bug fixes need to be applied everywhere.

Pattern Inventory

// 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;

Locations

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

Fix

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();

5. High: Large Domain Files Need Splitting

Severity: HIGH Impact: Complex state machines spanning 3,000+ lines are hard to understand and test.

ralph-tracker.ts (3,905 LOC)

5 responsibilities mixed:

  1. Output Parsing (~900 LOC) - Line-by-line parsing, state extraction
  2. Todo Management (~700 LOC) - Parsing, dedup, expiry
  3. Plan Tracking (~800 LOC) - Enhanced plan tasks, checkpoints
  4. Circuit Breaker (~400 LOC) - State machine for stuck detection
  5. 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)

respawn-controller.ts (3,611 LOC)

6 responsibilities mixed:

  1. State Machine (~1,000 LOC) - 6+ states, transitions
  2. Idle Detection (~800 LOC) - 5 layers + multi-signal combining
  3. AI Checkers (~600 LOC) - Idle + plan checkers integration
  4. Health Scoring (~500 LOC) - Metrics, circuit breaker, scoring
  5. Action Logging (~300 LOC) - Timeline, detection status
  6. 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)

session.ts (2,418 LOC)

8 responsibilities mixed:

  1. PTY Management (~600 LOC)
  2. Terminal I/O (~400 LOC)
  3. Token Tracking (~200 LOC)
  4. Task Tracking (~250 LOC)
  5. Ralph Integration (~200 LOC)
  6. Auto-Clear/Compact (~300 LOC)
  7. Image Watching (~100 LOC)
  8. 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)

6. High: types.ts God File

File: src/types.ts (1,443 lines, 72 exported definitions) Severity: HIGH Impact: Every file imports from types.ts. Hard to find relevant types.

Current Contents

  • 46 interfaces
  • 25 types
  • 1 enum (ApiErrorCode)
  • 9 factory functions (createInitialState, etc.)

Recommended Split

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.


7. High: Zod Schemas Duplicate TypeScript Types

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.

Problem

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(),
});

Fix

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

8. High: Test Coverage Gaps

Severity: HIGH Impact: Critical code paths untested. Regressions go unnoticed.

Untested Source Files

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

Test Quality Issues

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 instances
  • image-watcher.test.ts - 1 instance
  • task-queue.test.ts - 1 instance
  • Others scattered

9. High: Duplicated Test Mocks

Severity: HIGH Impact: Mock changes need updating in 4 places. Inconsistent mock behavior.

MockSession Defined 4 Times

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!

MockStateStore Defined 2 Times

File Usage
test/session-manager.test.ts Basic mock
test/ralph-loop.test.ts Separate implementation

Unused Test Utilities

test/respawn-test-utils.ts exports these utilities that no test file imports:

  • createTimeController() - Abstraction over vitest fake timers
  • MockAiIdleChecker - Fully mocked AI idle checker
  • MockAiPlanChecker - Fully mocked plan checker
  • Factory functions for pre-configured controllers

Fix

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

10. Medium: Hardcoded Magic Values

Severity: MEDIUM Impact: Hard to tune, inconsistent when same value appears in multiple places.

Already Centralized (Good)

  • src/config/buffer-limits.ts - All buffer sizes
  • src/config/map-limits.ts - All collection limits

NOT Centralized (40+ values scattered)

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 params

Fix

Create 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)

11. Medium: Frontend Global State Monolith

Severity: MEDIUM Impact: All state in single CodemanApp class. Tight coupling between unrelated systems.

60+ State Variables in CodemanApp Constructor (lines 2667-2805)

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

Problems

  1. 18 Map instances with complex cross-references (no garbage collection strategy)
  2. No domain separation: Session, subagent, notification, UI, and network state mixed
  3. Implicit dependencies: selectSession() requires 5+ Maps to be in consistent state
  4. terminalBufferCache has no max size - can grow unbounded with many sessions

Recommended Domain Split

// 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;
}

12. Medium: Frontend Code Duplication

Severity: MEDIUM Impact: Repeated patterns increase maintenance burden and inconsistency risk.

Duplicated Patterns

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, '&lt;')), some without
  • No consistent DOM creation pattern

typeof app !== 'undefined' checks (20+ instances):

  • Lines 458, 467, 481, 614, 617, 1549, etc.
  • Fix: Ensure app is 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.

Event Listener Issues

  • 152 addEventListener calls 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

13. Medium: Inconsistent Logging

Severity: MEDIUM Impact: Hard to debug in production. Can't filter by severity or component.

Current State

  • 345 console calls across source files
  • No structured logging - all console.log/error directly
  • No log levels (DEBUG, INFO, WARN, ERROR)

Inconsistent Prefixes

// 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);

Positive: CleanupManager Has Debug Mode

src/utils/cleanup-manager.ts has a debugMode flag for conditional debug logging - good pattern not replicated elsewhere.

Fix

Either:

  1. Enforce consistent [ComponentName] prefixes via lint rule
  2. Create lightweight logger abstraction (not a heavy framework)

14. Medium: Utils Barrel Export Gaps

File: src/utils/index.ts Severity: MEDIUM Impact: Forces deep imports, unclear public API.

Missing Exports

These functions are defined but NOT exported from the barrel:

  • createAnsiPatternFull() and createAnsiPatternSimple() (factory functions from regex-patterns.ts)
  • SAFE_PATH_PATTERN (from regex-patterns.ts)
  • validateTokenCounts() and validateTokensAndCost() (from token-validation.ts)
  • isSimilar(), isSimilarByDistance(), levenshteinDistance(), normalizePhrase() (from string-similarity.ts - though some are dead code, see finding #16)

Deep Import Anti-Pattern (16 instances)

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';

Fix

Add missing exports to src/utils/index.ts and update import sites.


15. Medium: Non-Null Assertion Risks

Severity: MEDIUM Impact: Runtime crashes if assumptions violated. 37 instances found.

Distribution

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

High-Risk Examples (session.ts)

// 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!.muxName

Fix

Add 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

Positive Notes

  • 0 instances of as any
  • 0 instances of @ts-ignore or @ts-expect-error
  • TypeScript overall score: 8.5/10

16. Low: Dead Utility Functions

File: src/utils/string-similarity.ts Severity: LOW Impact: Code clutter, confusion about what's actually used.

Unused Functions

These are defined and exported but never imported anywhere:

  • isSimilar(a, b, threshold) - similarity check with threshold
  • isSimilarByDistance(a, b, maxDistance) - Levenshtein-based check
  • levenshteinDistance(a, b) - raw edit distance
  • normalizePhrase(phrase) - phrase normalization

Actually Used

Only these are imported from the barrel:

  • stringSimilarity() - used in ralph-tracker.ts
  • fuzzyPhraseMatch() - used in ralph-tracker.ts
  • todoContentHash() - used in ralph-tracker.ts

Fix

Delete unused functions or mark as @internal if kept for future use.


17. Low: No Dependency Injection for File I/O

Severity: LOW (practical impact limited at current scale) Impact: Can't mock filesystem for unit tests. 68+ hard-coded filesystem calls.

Examples

// 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 });

Why This Is Lower Priority

  • 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

18. Scorecard & Prioritized Roadmap

Overall Scores (Post-Implementation)

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

Implementation Phases

Phase 1 - Quick Wins (1-2 days) ✅ COMPLETE

  1. ✅ Export missing functions from utils barrel (~30 min) — createAnsiPatternFull, createAnsiPatternSimple, SAFE_PATH_PATTERN, validateTokenCounts, validateTokensAndCost all now exported from src/utils/index.ts
  2. ✅ Delete dead utility functions (~15 min) — isSimilar() removed from string-similarity.ts; levenshteinDistance(), isSimilarByDistance(), normalizePhrase() made private (used internally by fuzzyPhraseMatch/stringSimilarity)
  3. ✅ Consolidate duplicated EXEC_TIMEOUT_MS constant (~15 min) — Created src/config/exec-timeout.ts as single source of truth; claude-cli-resolver.ts, opencode-cli-resolver.ts, and tmux-manager.ts all import from it
  4. ✅ Add z.infer to Zod schemas (~2 hours) — src/web/schemas.ts now has 36 z.infer type exports (lines 512-547) covering all schemas
  5. ✅ 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

  1. ✅ Create Debouncer utility class (~1 hour) — Created src/utils/debouncer.ts with Debouncer and KeyedDebouncer classes; exported from src/utils/index.ts
  2. ✅ 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)
  3. ✅ Migrate respawn-controller to CleanupManager — 10 manual timer fields replaced with single CleanupManager instance + timerIds Map. startTrackedTimer()/cancelTrackedTimer() preserved as wrappers for UI countdown display and timer events. clearTimers() uses dispose-and-recreate pattern for state transitions.
  4. ✅ 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)
  5. ✅ 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

  1. ✅ 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
  2. ✅ Created src/web/middleware/auth.ts (193 LOC) — Basic Auth, session cookies, rate limiting, security headers, CORS
  3. ✅ Created src/web/ports/ with 7 typed port interfaces (142 LOC) — SessionPort, EventPort, RespawnPort, ConfigPort, InfraPort, AuthPort; routes declare dependencies via intersection types
  4. ✅ Created src/web/route-helpers.ts (154 LOC) — findSessionOrFail(), formatUptime(), sanitizeHookData(), autoConfigureRalph()
  5. ✅ Reduced server.ts from 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

  1. ✅ Split types.ts into src/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. Original types.ts is now a 1-line re-export
  2. ✅ Split ralph-tracker.ts into 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)
  3. ✅ Split respawn-controller.ts into 5 files (exceeded plan of 3) — respawn-controller (3,228), respawn-health (229), respawn-metrics (229), respawn-patterns (131), respawn-adaptive-timing (134)
  4. ✅ Split session.ts into 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

  1. ✅ Extracted constants.js (238 LOC) — shared constants, timing values, Z-index layers, escapeHtml(), extractSyncSegments()
  2. ✅ Extracted mobile-handlers.js (449 LOC) — MobileDetection, KeyboardHandler, SwipeHandler
  3. ✅ Extracted voice-input.js (853 LOC) — DeepgramProvider, VoiceInput
  4. ✅ Extracted notification-manager.js (445 LOC) — NotificationManager class (5-layer system)
  5. ✅ Extracted keyboard-accessory.js (279 LOC) — KeyboardAccessoryBar, FocusTrap
  6. ✅ Extracted api-client.js (70 LOC) — _api(), _apiJson(), _apiPost(), _apiPut()
  7. ✅ Extracted subagent-windows.js (1,119 LOC) — 13 subagent window methods
  8. ✅ Removed inlined xterm-zerolag-input copy → built to vendor/xterm-zerolag-input.js from packages/xterm-zerolag-input/
  9. ✅ Reduced app.js from ~15,200 → 11,473 LOC (24% reduction). All scripts loaded in correct dependency order in index.html

Phase 6 - Config Consolidation (1 day) ✅ COMPLETE

  1. ✅ 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)
  2. ✅ Total: 9 config files in src/config/, ~65 constants centralized
  3. ✅ 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)
  4. ✅ CLAUDE.md updated with config files table, import conventions, resource limits references

Phase 7 - Test Infrastructure (2-3 days) ✅ COMPLETE

  1. ✅ 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)
  2. ✅ 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)
  3. respawn-test-utils.ts converted to backward-compatibility shim — re-exports from test/mocks/, retains respawn-specific utilities (MockAiIdleChecker, TimeController, etc.)
  4. ✅ 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 uses app.inject() — no real ports needed
  5. ✅ 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

Appendix: File Size Inventory (Post-Implementation)

Before vs After

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/)

New Infrastructure Created

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

Extracted Frontend Modules

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

What's Working Well

These patterns should be preserved, not refactored:

  • Clean one-way dependency graph (no circular deps)
  • EventEmitter-based decoupling between domain models
  • Proper import type usage (19 files, consistent)
  • Utility type adoption (101 instances of Record, Partial, Omit, etc.)
  • assertNever() for exhaustive switch checking
  • StaleExpirationMap and LRUMap for bounded collections
  • State persistence circuit breaker pattern
  • TypeScript strict mode with all safety flags enabled
  • CleanupManager for centralized timer/watcher disposal
  • Debouncer/KeyedDebouncer for consistent debounce patterns
  • Port interfaces for route module dependency injection
  • Object.assign(CodemanApp.prototype, ...) for frontend module composition