refactor: SSE event handlers, LLM context optimization, @fileoverview docs (#29)

* refactor: extract SSE event handlers into named class methods

Replace ~80 inline addListener closures in connectSSE() with a
declarative _SSE_HANDLER_MAP array that drives registration in a
single loop. Each handler is now a named _on* method on CodemanApp,
making them individually addressable for LLM navigation.

Add SSE_EVENTS constant object in constants.js to eliminate magic
event-type strings scattered across the frontend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: fix inaccuracies in CLAUDE.md

- Fix types barrel path: src/types.ts → src/types/index.ts
- Update app.js line count: ~12K → ~11.5K
- Correct route handler counts (113 → 111, per-group fixes)
- Add code style, ESM gotcha, env vars, route test, lifecycle log docs
- Add Node 22 CI note, test teardown timeout, port range

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add mobile screenshots and QR auth security writeup to README

Add 3 mobile screenshots (landing, idle, active) and expand the
mobile section with QR auth security design details and a
touch-optimized interface subsection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: bundle xterm-zerolag-input as vendor IIFE and add pre-commit hook

Build and postinstall now bundle the local xterm-zerolag-input package
as an IIFE at vendor/xterm-zerolag-input.js with global LocalEchoOverlay
shim. Add git pre-commit hook that runs prettier --check on staged .ts
files to catch format issues before CI.

Also bump constants.js and app.js cache-bust versions to 0.3.0 and add
tunnel upload URL display row in settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add cloudflared install support and interactive launch menu

- Add optional cloudflared dependency detection and installation
  across 6 distro families (macOS, Debian, Fedora, Arch, Alpine, SUSE)
- Add tunnel systemd service setup helper
- Replace post-install instructions with interactive launch menu
  (run now / systemd service / skip)
- Uninstall now cleans up both codeman-web and codeman-tunnel services

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: gitignore readme-preview.mjs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: WIP — SSE event constants, @fileoverview docs, CLAUDE.md compression

- Migrate broadcast() string literals → SseEvent.* typed constants
- Add @fileoverview with cross-domain references to all 13 type domain files
- Add @fileoverview to frontend JS modules (constants, mobile, voice, etc.)
- Add section dividers to route files for LLM scanability
- Compress CLAUDE.md: flat file list → domain table, fix counts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: optimize codebase for LLM context window efficiency

CLAUDE.md: 456 → 309 lines (32% reduction)
- Merge Commands into compact table, remove redundant bash block
- Convert Security section to dense table format
- Merge Performance + Resource Limits, Debugging + Troubleshooting
- Compress Tunnel, Memory Leak, Scripts, Screenshots sections
- Remove Key Patterns that duplicate @fileoverview in source files

Backend @fileoverview enhancements (10 priority files):
- session.ts: key methods, events, cross-domain refs
- respawn-controller.ts: state machine, idle detection layers
- ralph-tracker.ts: exports, circuit breaker, events
- ralph-loop.ts: lifecycle, persistence, events
- subagent-watcher.ts: watched patterns, teammate detection
- server.ts: coordination list, port interfaces
- state-store.ts: dual-file persistence, migration
- session-manager.ts: lifecycle methods, mutex guard
- hooks-config.ts: hook events list, categories
- sse-events.ts: category breakdown (~90 events, 17 categories)

Frontend app.js: add 6 section dividers, update @fileoverview line refs

Fix: escape glob `*/` in JSDoc that broke ESLint parser

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #29 review bugs

- server.ts: replace hardcoded 'session:needsRefresh' with SseEvent constant
- install.sh: fix Alpine cloudflared install for non-root (download to tmpfile first)
- install.sh: replace Arch pacman (AUR-only) with direct binary download
- index.html: bump all 8 remaining cache-bust versions from v0.2.9 to v0.3.0
- mobile-handlers.js: fix @dependency annotation (keyboard-accessory.js, not constants.js)
- types/push.ts: fix layer number (4, not 5)
- subagent-watcher.ts: fix watched pattern path to include {session} segment
- constants.js: fix SSE_EVENTS count in @fileoverview (~73, not ~65)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ark0N
2026-03-03 22:40:18 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 14462f7bfe
commit 2ee9ad72e8
52 changed files with 3393 additions and 1860 deletions
+121 -408
View File
@@ -64,39 +64,23 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
**Requirements**: Node.js 18+, Claude CLI, tmux
## Commands
**Git**: Main branch is `master`. SSH session chooser: `sc` (interactive), `sc 2` (quick attach), `sc -l` (list).
**Note**: `npm run dev` starts the web server (equivalent to `npx tsx src/index.ts web`).
## Additional Commands
**Default port**: `3000` (web UI at `http://localhost:3000`)
`npm run dev` = dev server. Default port: `3000`. Commands not in Quick Reference:
```bash
# Setup
npm install # Install dependencies
| Task | Command |
|------|---------|
| Dev with TLS | `npx tsx src/index.ts web --https` |
| Continuous typecheck | `tsc --noEmit --watch` |
| Test coverage | `npm run test:coverage` |
| Production start | `npm run start` |
| Production logs | `journalctl --user -u codeman-web -f` |
# Development
npx tsx src/index.ts web # Dev server (RECOMMENDED)
npx tsx src/index.ts web --https # With TLS (only needed for remote access)
npm run typecheck # Type check
tsc --noEmit --watch # Continuous type checking
npm run lint # ESLint
npm run lint:fix # ESLint with auto-fix
npm run format # Prettier format
npm run format:check # Prettier check only
**CI**: `.github/workflows/ci.yml` runs `typecheck`, `lint`, `format:check` on push to master (Node 22). Tests excluded (they spawn tmux).
# Testing (see "Testing" section for CRITICAL safety warnings)
npx vitest run test/<file>.test.ts # Single file (SAFE)
npx vitest run -t "pattern" # Tests matching name
npm run test:coverage # With coverage report
# Production
npm run build # esbuild via scripts/build.mjs (not tsc)
npm run start # node dist/index.js (production)
systemctl --user restart codeman-web
journalctl --user -u codeman-web -f
```
**CI**: `.github/workflows/ci.yml` runs `typecheck`, `lint`, and `format:check` on push to master. Tests are intentionally excluded from CI (they spawn tmux).
**Code style**: Prettier (`singleQuote: true`, `printWidth: 120`, `trailingComma: "es5"`). ESLint allows `no-console`, warns on `@typescript-eslint/no-explicit-any`. Does not lint `app.js` or `scripts/**/*.mjs`.
## Common Gotchas
@@ -106,6 +90,8 @@ journalctl --user -u codeman-web -f
- **DEC 2026 sync blocks** — Never discard incomplete sync blocks (START without END); buffer up to 50ms then flush. See `app.js:extractSyncSegments()`
- **Terminal writes during buffer load** — Live SSE writes are queued while `_isLoadingBuffer` is true to prevent interleaving with historical data
- **Local echo prompt scanning** — Does NOT use `buffer.cursorY` (Ink moves it); scans buffer bottom-up for visible `>` prompt marker
- **ESM dynamic imports** — Never use `require()` in this codebase; it breaks in production ESM builds. Use `await import()` for dynamic imports. (`tsx` masks this in dev by shimming CJS/ESM)
- **Package name vs product name** — npm package is `aicodeman`, product is **Codeman**. Release workflow renames `aicodeman@X.Y.Z` tags to `codeman@X.Y.Z`
## Import Conventions
@@ -115,103 +101,32 @@ journalctl --user -u codeman-web -f
## Architecture
### Core Files
### Core Files (by domain)
| File | Purpose |
|------|---------|
| `src/index.ts` | CLI entry point: global error recovery, uncaught exception guard, `MAX_CONSECUTIVE_ERRORS` auto-restart |
| `src/session.ts` | PTY wrapper: `runPrompt()`, `startInteractive()`, `startShell()` |
| `src/mux-interface.ts` | `TerminalMultiplexer` interface + `MuxSession` type |
| `src/mux-factory.ts` | Create tmux multiplexer instance |
| `src/tmux-manager.ts` | tmux session management |
| `src/session-manager.ts` | Session lifecycle, cleanup |
| `src/session-auto-ops.ts` | Automatic session operations (auto-compact, etc.) |
| `src/session-cli-builder.ts` | CLI argument construction for session spawning |
| `src/session-task-cache.ts` | Task description caching for subagent correlation |
| `src/state-store.ts` | State persistence to `~/.codeman/state.json` |
| `src/respawn-controller.ts` | State machine for autonomous cycling |
| `src/respawn-adaptive-timing.ts` | Adaptive idle timing calculation |
| `src/respawn-health.ts` | Health scoring (0-100) for respawn loops |
| `src/respawn-metrics.ts` | Per-cycle outcome metrics tracking |
| `src/respawn-patterns.ts` | Pattern matching for stuck/error states |
| `src/ralph-tracker.ts` | Detects `<promise>PHRASE</promise>`, todos |
| `src/ralph-loop.ts` | Autonomous task execution loop (polls queue, assigns tasks) |
| `src/ralph-config.ts` | Parses `.claude/ralph-loop.local.md` plugin config |
| `src/ralph-fix-plan-watcher.ts` | Watches `@fix_plan.md` for changes |
| `src/ralph-plan-tracker.ts` | Plan iteration tracking |
| `src/ralph-stall-detector.ts` | Detects stuck Ralph loops |
| `src/ralph-status-parser.ts` | Parses Ralph status messages |
| `src/task.ts` | Task model for prompt execution |
| `src/task-queue.ts` | Priority queue for tasks with dependencies |
| `src/task-tracker.ts` | Background task tracker for subagent detection |
| `src/subagent-watcher.ts` | Monitors Claude Code's Task tool (background agents) |
| `src/team-watcher.ts` | Polls `~/.claude/teams/` for agent team activity; matches teams to sessions via `leadSessionId` |
| `src/run-summary.ts` | Timeline events for "what happened while away" |
| `src/ai-checker-base.ts` | Base class for AI-powered checkers (shared by idle + plan checkers) |
| `src/ai-idle-checker.ts` | AI-powered idle detection |
| `src/ai-plan-checker.ts` | AI-powered plan completion checker |
| `src/bash-tool-parser.ts` | Parses Claude's bash tool invocations from output |
| `src/transcript-watcher.ts` | Watches Claude's transcript files for changes |
| `src/hooks-config.ts` | Manages `.claude/settings.local.json` hook configuration |
| `src/push-store.ts` | VAPID key auto-gen + push subscription CRUD for Web Push |
| `src/session-lifecycle-log.ts` | Append-only JSONL audit log at `~/.codeman/session-lifecycle.jsonl` |
| `src/image-watcher.ts` | Watches for image file creation (screenshots, etc.) |
| `src/file-stream-manager.ts` | Manages `tail -f` processes for live log viewing |
| `src/plan-orchestrator.ts` | 2-agent plan generation: optional research agent → planner agent |
| `src/prompts/index.ts` | Barrel export for all agent prompts |
| `src/prompts/*.ts` | Agent prompts (research-agent, planner) |
| `src/templates/claude-md.ts` | CLAUDE.md generation for new cases |
| `src/tunnel-manager.ts` | Manages cloudflared child process for Cloudflare tunnel + QR auth token rotation |
| `src/cli.ts` | Command-line interface handlers |
| `src/web/server.ts` | Fastify server setup, SSE at `/api/events`, delegates to route modules |
| `src/web/routes/*.ts` | 12 domain route modules (session, respawn, ralph, plan, etc.) — each exports `register*Routes()` |
| `src/web/ports/*.ts` | Port interfaces (SessionPort, EventPort, etc.) — route modules declare dependencies via intersection types |
| `src/web/middleware/auth.ts` | Auth middleware: Basic Auth, session cookies, rate limiting, security headers, CORS |
| `src/web/route-helpers.ts` | Shared helper utilities for route modules |
| `src/web/schemas.ts` | Zod v4 validation schemas with path/env security allowlists |
| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows, mobile support (~12K lines) |
| `src/types.ts` | Barrel re-export from `src/types/` — 13 domain files (session, task, respawn, ralph, api, etc.) |
| Domain | Key files | Notes |
|--------|-----------|-------|
| **Entry** | `src/index.ts`, `src/cli.ts` | CLI entry point, global error recovery |
| **Session** | `src/session.ts` ★, `src/session-manager.ts`, `src/session-auto-ops.ts`, `src/session-cli-builder.ts` | PTY wrapper, lifecycle, auto-compact |
| **Mux** | `src/mux-interface.ts`, `src/mux-factory.ts`, `src/tmux-manager.ts` | tmux abstraction layer |
| **Respawn** | `src/respawn-controller.ts` ★ + 4 helpers (`-adaptive-timing`, `-health`, `-metrics`, `-patterns`) | Autonomous cycling state machine |
| **Ralph** | `src/ralph-tracker.ts` ★, `src/ralph-loop.ts` + 5 helpers (`-config`, `-fix-plan-watcher`, `-plan-tracker`, `-stall-detector`, `-status-parser`) | Completion tracking, autonomous task loop |
| **Agents** | `src/subagent-watcher.ts` ★, `src/team-watcher.ts`, `src/bash-tool-parser.ts`, `src/transcript-watcher.ts` | Background agent monitoring |
| **AI** | `src/ai-checker-base.ts`, `src/ai-idle-checker.ts`, `src/ai-plan-checker.ts` | AI-powered idle/plan detection |
| **Tasks** | `src/task.ts`, `src/task-queue.ts`, `src/task-tracker.ts` | Task model, priority queue |
| **State** | `src/state-store.ts`, `src/run-summary.ts`, `src/session-lifecycle-log.ts` | Persistence, timeline, audit log |
| **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts` | Hooks, push, tunnel, file watching |
| **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/claude-md.ts` | 2-agent plan generation |
| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (13 modules), `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | Fastify server, SSE event registry, REST API |
| **Frontend** | `src/web/public/app.js` ★ (~11.5K lines) + 8 JS modules | xterm.js UI, tabs, settings |
| **Types** | `src/types/index.ts` → 13 domain files | Barrel re-export, see `@fileoverview` in index.ts |
**Large files** (>50KB): `app.js`, `ralph-tracker.ts`, `respawn-controller.ts`, `session.ts`, `subagent-watcher.ts` — these contain complex state machines; read `docs/respawn-state-machine.md` before modifying.
★ = Large file (>50KB), contains complex state machines. Read `docs/respawn-state-machine.md` before modifying respawn/ralph.
### Local Packages
**Local package**: `packages/xterm-zerolag-input/` — instant keystroke feedback overlay for xterm.js. Source of truth for `LocalEchoOverlay`; copy embedded in `app.js`. Build: `npm run build` (tsup).
| Package | Purpose |
|---------|---------|
| `packages/xterm-zerolag-input/` | Instant keystroke feedback overlay for xterm.js — eliminates perceived input latency over high-RTT connections. Source of truth for `LocalEchoOverlay`; a copy is embedded in `app.js`. Build: `npm run build` (tsup). |
**Config**: `src/config/` — 9 files (buffer limits, map limits, timeouts, SSE timing, auth, tunnel, terminal, AI, teams). Import from specific files.
### Config Files (`src/config/`)
| File | Purpose |
|------|---------|
| `buffer-limits.ts` | Terminal/text buffer size limits |
| `map-limits.ts` | Global limits for Maps, sessions, watchers |
| `exec-timeout.ts` | Execution timeout configuration |
| `server-timing.ts` | Web server batching, SSE, scheduled run timing |
| `auth-config.ts` | Auth session TTL, rate limits, hook timeout |
| `tunnel-config.ts` | QR token rotation, tunnel process lifecycle |
| `terminal-limits.ts` | Terminal dimension and input validation limits |
| `ai-defaults.ts` | AI checker model and context limits |
| `team-config.ts` | Agent Teams polling and cache sizes |
### Utilities (`src/utils/`)
Re-exported via `src/utils/index.ts`. Key exports:
| File | Exports |
|------|---------|
| `cleanup-manager.ts` | `CleanupManager` — centralized disposal for timers, intervals, watchers, listeners, streams |
| `lru-map.ts` | `LRUMap` — bounded cache with eviction |
| `stale-expiration-map.ts` | `StaleExpirationMap` — TTL-based map with automatic cleanup |
| `regex-patterns.ts` | `ANSI_ESCAPE_PATTERN_FULL/SIMPLE`, `createAnsiPatternFull/Simple()`, `stripAnsi`, `TOKEN_PATTERN`, `SPINNER_PATTERN` |
| `buffer-accumulator.ts` | `BufferAccumulator` — batches rapid writes into single flushes |
| `claude-cli-resolver.ts` | `findClaudeDir`, `getAugmentedPath` — resolves Claude CLI paths |
| `opencode-cli-resolver.ts` | `resolveOpenCodeDir`, `isOpenCodeAvailable` — OpenCode CLI support |
| `string-similarity.ts` | `stringSimilarity`, `fuzzyPhraseMatch`, `todoContentHash` |
| `token-validation.ts` | `validateTokenCounts`, `validateTokensAndCost` |
| `nice-wrapper.ts` | `wrapWithNice` — wraps commands with `nice`/`ionice` for lower priority |
| `type-safety.ts` | `assertNever` — exhaustive switch/case guard |
| `debouncer.ts` | `Debouncer` — reusable debounce utility |
**Utilities**: `src/utils/` — re-exported via `src/utils/index.ts`. Key: `CleanupManager`, `LRUMap`, `StaleExpirationMap`, `BufferAccumulator`, `stripAnsi`, `createAnsiPatternFull/Simple()`, `assertNever`, `Debouncer`.
### Data Flow
@@ -222,133 +137,74 @@ Re-exported via `src/utils/index.ts`. Key exports:
### Key Patterns
**Input to sessions**: Use `session.writeViaMux()` for programmatic input (respawn, auto-compact). Uses tmux `send-keys -l` (literal text) + `send-keys Enter`. All prompts must be single-line.
**Terminal multiplexer**: `TerminalMultiplexer` interface (`src/mux-interface.ts`) abstracts the backend. `createMultiplexer()` from `src/mux-factory.ts` creates the tmux backend.
**Input**: `session.writeViaMux()` for programmatic input — tmux `send-keys -l` (literal) + `send-keys Enter`. Single-line only.
**Idle detection**: Multi-layer (completion message → AI check → output silence → token stability). See `docs/respawn-state-machine.md`.
**Token tracking**: Interactive mode parses status line ("123.4k tokens"), estimates 60/40 input/output split.
**Hook events**: Claude Code hooks trigger via `/api/hook-event`. Key events: `permission_prompt`, `elicitation_dialog`, `idle_prompt`, `stop`, `teammate_idle`, `task_completed`. See `src/hooks-config.ts`.
**Hook events**: Claude Code hooks trigger notifications via `/api/hook-event`. Key events: `permission_prompt` (tool approval needed), `elicitation_dialog` (Claude asking question), `idle_prompt` (waiting for input), `stop` (response complete), `teammate_idle` (Agent Teams), `task_completed` (Agent Teams). See `src/hooks-config.ts`.
**Agent Teams**: `TeamWatcher` polls `~/.claude/teams/`, matches to sessions via `leadSessionId`. Teammates are in-process threads appearing as subagents. Enable: `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. See `agent-teams/`.
**Web Push**: Layer 5 of the notification system. Service worker (`sw.js`) receives push events and shows OS-level notifications even when the browser tab is closed. VAPID keys auto-generated on first use and persisted to `~/.codeman/push-keys.json`. Per-subscription per-event preferences stored in `~/.codeman/push-subscriptions.json`. Expired subscriptions (410/404) auto-cleaned. Requires HTTPS or localhost. iOS requires PWA installed to home screen. See `src/push-store.ts`.
**Circuit breaker**: Prevents respawn thrashing. States: `CLOSED` → `HALF_OPEN` → `OPEN`. Reset: `/api/sessions/:id/ralph-circuit-breaker/reset`.
**Agent Teams (experimental)**: `TeamWatcher` polls `~/.claude/teams/` for team configs and matches teams to sessions via `leadSessionId`. Teammates are in-process threads (not separate OS processes) and appear as standard subagents. RespawnController checks `TeamWatcher.hasActiveTeammates()` before triggering respawn. Enable via `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` env var in `settings.local.json`. See `agent-teams/` for full docs.
**Port interfaces**: Routes declare dependencies via port interfaces (`src/web/ports/`). Routes use intersection types (e.g., `SessionPort & EventPort`).
**Circuit breaker**: Prevents respawn thrashing when Claude is stuck. States: `CLOSED` (normal) → `HALF_OPEN` (testing) → `OPEN` (blocked). Tracks consecutive no-progress, same-error-repeated, and tests-failing-too-long. Reset via API at `/api/sessions/:id/ralph-circuit-breaker/reset`.
### Frontend
**Respawn cycle metrics & health scoring**: `RespawnCycleMetrics` tracks per-cycle outcomes (success, stuck_recovery, blocked, error). `RalphLoopHealthScore` computes 0-100 health with component scores (cycleSuccess, circuitBreaker, iterationProgress, aiChecker, stuckRecovery). Available via respawn status API.
**Subagent-session correlation**: Session parses Task tool output via `BashToolParser` → `SubagentWatcher` discovers new agent → calls `session.findTaskDescriptionNear()` to match description for window title.
**Port interfaces**: Route modules declare their dependencies via port interfaces (`src/web/ports/`). `WebServer` implements all ports; routes use TypeScript intersection types (e.g., `SessionPort & EventPort`) to specify only what they need. This enables loose coupling between routes and the server.
### Frontend Files
| File | Purpose |
|------|---------|
| `src/web/public/index.html` | HTML entry point with inline critical CSS and async vendor loading |
| `src/web/public/constants.js` | Shared constants, timing values, Z-index layers, Web Push utilities |
| `src/web/public/api-client.js` | API fetch wrapper (`_api`, `_apiJson`, `_apiPost`, `_apiPut`) |
| `src/web/public/mobile-handlers.js` | `MobileDetection`, `KeyboardHandler`, `SwipeHandler` objects |
| `src/web/public/voice-input.js` | `DeepgramProvider`, `VoiceInput` objects for speech-to-text |
| `src/web/public/notification-manager.js` | `NotificationManager` class (5-layer notification system) |
| `src/web/public/keyboard-accessory.js` | `KeyboardAccessoryBar` and `FocusTrap` classes |
| `src/web/public/subagent-windows.js` | Subagent window management (open, close, drag, connection lines) |
| `src/web/public/app.js` | Core UI: xterm.js, tab management, settings |
| `src/web/public/ralph-wizard.js` | Ralph Loop wizard UI |
| `src/web/public/styles.css` | Main styling (dark theme, layout, components) |
| `src/web/public/mobile.css` | Responsive overrides for screens <1024px (loaded conditionally via `media` attribute) |
| `src/web/public/upload.html` | Screenshot upload page served at `/upload.html` |
| `src/web/public/sw.js` | Service worker for Web Push notifications |
| `src/web/public/manifest.json` | Minimal PWA manifest (required for push on Android) |
| `src/web/public/vendor/` | Self-hosted xterm.js + addons (eliminates CDN latency) |
**Script loading order** (index.html): `constants.js` → `mobile-handlers.js` → `voice-input.js` → `notification-manager.js` → `keyboard-accessory.js` → `app.js` → `ralph-wizard.js` → `api-client.js` → `subagent-windows.js`. All modules share global scope — order matters for dependencies.
### Frontend Architecture
The frontend is split across multiple vanilla JS modules (extracted from the original monolithic `app.js`). Key systems:
| System | Module | Key Classes/Functions | Purpose |
|--------|--------|----------------------|---------|
| **Terminal rendering** | `app.js` | `batchTerminalWrite()`, `flushPendingWrites()`, `chunkedTerminalWrite()` | 60fps batched writes with DEC 2026 sync |
| **Local echo overlay** | `app.js` | `LocalEchoOverlay` class | DOM overlay for instant mobile keystroke feedback |
| **Mobile support** | `mobile-handlers.js` | `MobileDetection`, `KeyboardHandler`, `SwipeHandler` | Touch input, viewport adaptation, swipe navigation |
| **Keyboard accessory** | `keyboard-accessory.js` | `KeyboardAccessoryBar`, `FocusTrap` | Mobile keyboard toolbar, modal focus management |
| **Subagent windows** | `subagent-windows.js` | `openSubagentWindow()`, `closeSubagentWindow()`, `updateConnectionLines()` | Floating terminal windows with parent connection lines |
| **Notifications** | `notification-manager.js` | `NotificationManager` class | 5-layer: in-app drawer, tab flash, browser API, web push, audio beep |
| **Voice input** | `voice-input.js` | `DeepgramProvider`, `VoiceInput` | Speech-to-text via Deepgram WebSocket |
| **SSE connection** | `app.js` | `connectSSE()`, `addListener()` | EventSource with exponential backoff (1-30s), offline queue (64KB) |
| **Settings** | `app.js` | `openAppSettings()`, `apply*Visibility()` | Server-backed + localStorage persistence |
Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. Load order: `constants.js`(1) → `mobile-handlers.js`(2) → `voice-input.js`(3) → `notification-manager.js`(4) → `keyboard-accessory.js`(5) → `app.js`(6) → `ralph-wizard.js`(7) → `api-client.js`(8) → `subagent-windows.js`(9).
**Z-index layers**: subagent windows (1000), plan agents (1100), log viewers (2000), image popups (3000), local echo overlay (7).
**Built-in respawn presets**: `solo-work` (3s idle, 60min), `subagent-workflow` (45s idle, 240min), `team-lead` (90s idle, 480min), `ralph-todo` (8s idle, 480min, works through @fix_plan.md tasks), `overnight-autonomous` (10s idle, 480min, full reset).
**Respawn presets**: `solo-work` (3s/60min), `subagent-workflow` (45s/240min), `team-lead` (90s/480min), `ralph-todo` (8s/480min), `overnight-autonomous` (10s/480min).
**Keyboard shortcuts**: Escape (close panels), Ctrl+? (help), Ctrl+Enter (quick start), Ctrl+W (kill session), Ctrl+Tab (next session), Ctrl+K (kill all), Ctrl+L (clear), Ctrl+Shift+R (restore size), Ctrl/Cmd +/- (font size).
**Keyboard shortcuts**: Escape (close), Ctrl+? (help), Ctrl+Enter (quick start), Ctrl+W (kill), Ctrl+Tab (next), Ctrl+K (kill all), Ctrl+L (clear), Ctrl+Shift+R (restore size), Ctrl/Cmd +/- (font).
### Security
- **HTTP Basic Auth**: Optional via `CODEMAN_USERNAME`/`CODEMAN_PASSWORD` env vars
- **QR Auth**: Single-use ephemeral 6-char tokens (60s TTL, 90s grace) for tunnel login without typing passwords. `TunnelManager` rotates tokens, serves cached SVG at `GET /api/tunnel/qr`, validates at `GET /q/:code`. Separate per-IP rate limit (10/15min) + global path limit (30/min). Desktop notification on consumption (QRLjacking detection). Audit logged as `qr_auth` in `session-lifecycle.jsonl`. See `docs/qr-auth-plan.md`.
- **Session cookies**: After Basic Auth or QR Auth, a 24h session cookie (`codeman_session`) is issued so credentials aren't re-sent on every request. Active sessions auto-extend. SSE works via same-origin cookie (`EventSource` can't send custom headers). Sessions store device context (IP + User-Agent) for audit via `AuthSessionRecord`.
- **Session revocation**: `POST /api/auth/revoke` revokes individual sessions or all sessions.
- **Rate limiting**: 10 failed auth attempts per IP triggers 429 rejection (15-minute decay window). Manual `StaleExpirationMap` counter — no `@fastify/rate-limit` needed. QR auth has its own separate rate limiter.
- **Hook bypass**: `/api/hook-event` POST is exempt from auth — Claude Code hooks curl this from localhost and can't present credentials. Safe: validated by `HookEventSchema`, only triggers broadcasts.
- **CORS**: Restricted to localhost only
- **Security headers**: X-Content-Type-Options, X-Frame-Options, CSP; HSTS if HTTPS
- **Path validation** (`schemas.ts`): Strict allowlist regex, no shell metacharacters, no traversal, must be absolute
- **Env var allowlist**: Only `CLAUDE_CODE_*` prefixes allowed; blocks `PATH`, `LD_PRELOAD`, `NODE_OPTIONS`, `CODEMAN_*` keys
- **File streaming TOCTOU protection**: `FileStreamManager` calls `realpathSync()` twice (at validation and before spawn) to catch symlink swaps
| Layer | Details |
|-------|---------|
| **Auth** | Optional HTTP Basic via `CODEMAN_USERNAME`/`CODEMAN_PASSWORD` env vars |
| **QR Auth** | Single-use 6-char tokens (60s TTL) for tunnel login. See `docs/qr-auth-plan.md` |
| **Sessions** | 24h cookie (`codeman_session`), auto-extend, device context audit |
| **Rate limit** | 10 failed auth/IP → 429 (15min decay). QR has separate limiter |
| **Hook bypass** | `/api/hook-event` exempt from auth (localhost-only, schema-validated) |
| **Env vars** | `CODEMAN_MUX` (managed session), `CODEMAN_API_URL` (auto-set for hooks) |
| **Validation** | Zod schemas, path allowlist regex, `CLAUDE_CODE_*` env prefix allowlist |
| **Headers** | CORS localhost-only, CSP, X-Frame-Options, HSTS if HTTPS |
### SSE Event Categories
### SSE Event Registry
~100 event types broadcast via `broadcast()`. Key categories:
| Category | Events | Purpose |
|----------|--------|---------|
| Session | `session:created/updated/deleted/working/idle/exit/error/completion` | Lifecycle |
| Terminal | `session:terminal`, `session:clearTerminal`, `session:needsRefresh` | Output streaming |
| Respawn | `respawn:stateChanged/cycleStarted/blocked/aiCheck*/planCheck*/timer*` | Respawn state machine |
| Subagent | `subagent:discovered/updated/completed/tool_call/progress` | Background agents |
| Ralph | `session:ralphLoopUpdate/ralphTodoUpdate/ralphCompletionDetected` | Ralph tracking |
| Hooks | `hook:{eventName}` (dynamic) | Claude Code hook events |
| Plan | `plan:started/progress/completed/cancelled/subagent` | Plan orchestration |
| Mux | `mux:created/killed/died/statsUpdated` | tmux process monitor |
| Tunnel | `tunnel:qrRotated/qrRegenerated/qrAuthUsed` | QR token lifecycle |
| Image | `image:detected` | Screenshot detection |
~100 event types in `src/web/sse-events.ts` (backend) and `SSE_EVENTS` in `constants.js` (frontend). Both must be kept in sync.
### API Route Categories
~113 route handlers split across `src/web/routes/` domain modules. Key groups:
~111 route handlers in `src/web/routes/`. Key groups:
| Group | Prefix | Count | Key endpoints |
|-------|--------|-------|---------------|
| Sessions | `/api/sessions` | 43 | CRUD, input, resize, interactive, shell |
| System | `/api/status`, `/api/stats`, `/api/config`, `/api/settings`, `/api/subagents` | 38 | App state, config, subagents |
| Ralph | `/api/sessions/:id/ralph-*` | 19 | state, status, config, circuit-breaker |
| Respawn | `/api/sessions/:id/respawn` | 17 | start, stop, enable, config |
| Plan | `/api/sessions/:id/plan/*` | 12 | task CRUD, checkpoint, history, rollback |
| System | `/api/status`, `/api/stats`, `/api/config`, `/api/settings`, `/api/subagents` | 35 | App state, config, subagents |
| Sessions | `/api/sessions` | 24 | CRUD, input, resize, interactive, shell |
| Ralph | `/api/sessions/:id/ralph-*` | 9 | state, status, config, circuit-breaker |
| Plan | `/api/sessions/:id/plan/*` | 8 | task CRUD, checkpoint, history, rollback |
| Respawn | `/api/sessions/:id/respawn` | 7 | start, stop, enable, config |
| Cases | `/api/cases` | 7 | CRUD, link, fix-plan |
| Scheduled | `/api/scheduled` | 6 | CRUD for scheduled runs |
| Files | `/api/sessions/:id/file*`, `tail-file` | 5 | Browser, preview, raw, tail stream |
| Mux | `/api/mux-sessions` | 5 | tmux management, stats |
| Scheduled | `/api/scheduled` | 4 | CRUD for scheduled runs |
| Push | `/api/push` | 4 | VAPID key, subscribe, update prefs, unsubscribe |
| Hooks | `/api/hook-event` | 4 | Hook event ingestion |
| Teams | `/api/teams` | 2 | list teams, get team tasks |
| Hooks | `/api/hook-event` | 1 | Hook event ingestion |
## Adding Features
- **API endpoint**: Types in `src/types/` (domain file), route in the appropriate `src/web/routes/*-routes.ts` module, use `createErrorResponse()`. Validate request bodies with Zod schemas in `schemas.ts`.
- **SSE event**: Emit via `broadcast()`, handle in `app.js` SSE listener section (search `addListener(`)
- **Session setting**: Add to `SessionState` in `types.ts`, include in `session.toState()`, call `persistSessionState()`
- **Hook event**: Add to `HookEventType` in `types.ts`, add hook command in `hooks-config.ts:generateHooksConfig()`, update `HookEventSchema` in `schemas.ts`
- **Mobile feature**: Add to relevant mobile singleton (`KeyboardHandler`, `KeyboardAccessoryBar`, etc.), test with `MobileDetection.isMobile()` guard
- **New test**: Pick unique port (search `const PORT =`), add port comment to test file header. Tests use ports 3150+.
- **API endpoint**: Types in `src/types/` domain file, route in `src/web/routes/*-routes.ts`, use `createErrorResponse()`. Validate with Zod schemas in `schemas.ts`.
- **SSE event**: Add to `src/web/sse-events.ts` + `SSE_EVENTS` in `constants.js`, emit via `broadcast()`, handle in `app.js` (`addListener(`)
- **Session setting**: Add to `SessionState`, include in `session.toState()`, call `persistSessionState()`
- **Hook event**: Add to `HookEventType`, add hook in `hooks-config.ts:generateHooksConfig()`, update `HookEventSchema`
- **Mobile feature**: Add to relevant singleton, guard with `MobileDetection.isMobile()`
- **New test**: Pick unique port (search `const PORT =`). Integration: ports 3099-3211. Route tests: `app.inject()` — see `test/routes/_route-test-utils.ts`.
**Validation**: Uses Zod v4 for request validation. Define schemas in `schemas.ts` and use `.parse()` or `.safeParse()`. Note: Zod v4 has different API from v3 (e.g., `z.object()` options changed, error formatting differs).
**Validation**: Zod v4 (different API from v3). Define schemas in `schemas.ts`, use `.parse()`/`.safeParse()`.
## State Files
@@ -358,239 +214,96 @@ The frontend is split across multiple vanilla JS modules (extracted from the ori
| `~/.codeman/mux-sessions.json` | Tmux session metadata for recovery |
| `~/.codeman/settings.json` | User preferences |
| `~/.codeman/push-keys.json` | VAPID key pair for Web Push (auto-generated) |
| `~/.codeman/push-subscriptions.json` | Registered push notification subscriptions |
| `~/.codeman/push-subscriptions.json` | Push notification subscriptions |
| `~/.codeman/session-lifecycle.jsonl` | Append-only audit log (QR auth, session events) |
## Default Settings
UI defaults are set in `src/web/public/app.js` using `??` fallbacks. To change defaults, edit `openAppSettings()` and `apply*Visibility()` functions.
**Key defaults:** Most panels hidden (monitor, subagents shown), notifications enabled (audio disabled), subagent tracking on, Ralph tracking off.
UI defaults in `app.js` using `??` fallbacks. Edit `openAppSettings()` and `apply*Visibility()` to change. Key defaults: most panels hidden (monitor, subagents shown), notifications on (audio off), subagent tracking on, Ralph tracking off.
## Testing
**CRITICAL: You are running inside a Codeman-managed tmux session.** Never run `npx vitest run` (full suite) — it spawns/kills tmux sessions and will crash your own session. Instead:
**CRITICAL: You are running inside a Codeman-managed tmux session.** Never run `npx vitest run` (full suite) — it spawns/kills tmux sessions and will crash your own session. Only run individual files:
```bash
# Safe: run individual test files
npx vitest run test/<specific-file>.test.ts
# Safe: run tests matching a pattern
npx vitest run -t "pattern"
# DANGEROUS from inside Codeman — will kill your tmux session:
# npx vitest run ← DON'T DO THIS
npx vitest run test/<specific-file>.test.ts # Single file (SAFE)
npx vitest run -t "pattern" # By name (SAFE)
# npx vitest run # DANGEROUS — DON'T DO THIS
```
**Ports**: Unit tests pick unique ports manually. Search `const PORT =` before adding new tests.
**Config**: Vitest with `globals: true`, `fileParallelism: false`. Timeout 30s, teardown 60s.
**Config**: Vitest with `globals: true`, `fileParallelism: false`. Unit timeout 30s.
**Safety**: `test/setup.ts` snapshots pre-existing tmux sessions and never kills them. Only `registerTestTmuxSession()` sessions get cleaned up.
**Safety**: `test/setup.ts` snapshots pre-existing tmux sessions at load time and never kills them. Only sessions registered via `registerTestTmuxSession()` get cleaned up.
**Ports**: Pick unique ports manually. Search `const PORT =` before adding new tests.
**Respawn tests**: Use MockSession from `test/respawn-test-utils.ts` to avoid spawning real Claude processes.
**Respawn tests**: Use `MockSession` from `test/respawn-test-utils.ts`. **Route tests**: `app.inject()` in `test/routes/`. **Mobile tests**: Playwright suite in `mobile-test/` (135 device profiles).
**Mobile tests**: Separate Playwright-based suite in `mobile-test/` with 135 device profiles. Run via `npx vitest run --config mobile-test/vitest.config.ts`. See `mobile-test/README.md`.
## Screenshots
## Screenshots ("sc")
"sc"/"screenshot" = uploaded mobile screenshots in `~/.codeman/screenshots/`. View with Read tool. API: `GET /api/screenshots` (list), `POST /api/screenshots` (upload).
When the user says "check the sc", "screenshot", or "sc", they mean uploaded screenshots from their mobile device. Screenshots are saved to `~/.codeman/screenshots/` and uploaded via `/upload.html` on the Codeman web UI. To view them, use the Read tool on the image files:
## Debugging & Troubleshooting
```bash
ls ~/.codeman/screenshots/ # List uploaded screenshots
# Then use Read tool on individual files — Claude Code can view images natively
```
API: `GET /api/screenshots` (list), `GET /api/screenshots/:name` (serve), `POST /api/screenshots` (upload multipart/form-data). Source: `src/web/public/upload.html`.
## Debugging
```bash
tmux list-sessions # List tmux sessions
tmux attach-session -t <name> # Attach (Ctrl+B D to detach)
curl localhost:3000/api/sessions # Check sessions
curl localhost:3000/api/status | jq # Full app state
cat ~/.codeman/state.json | jq # View persisted state
curl localhost:3000/api/subagents # List background agents
tmux list-sessions # List tmux sessions
curl localhost:3000/api/sessions | jq # Check sessions
curl localhost:3000/api/status | jq # Full app state
curl localhost:3000/api/subagents | jq # Background agents
curl localhost:3000/api/sessions/:id/run-summary | jq # Session timeline
cat ~/.codeman/state.json | jq # Persisted state
```
## Troubleshooting
| Problem | Fix |
|---------|-----|
| Session won't start | Kill orphaned tmux sessions, check Claude CLI installed |
| Port 3000 in use | `lsof -i :3000`, kill conflicting process or use `--port` |
| SSE not connecting | Check CORS, ensure server running, check browser console |
| Respawn not triggering | Enable respawn in session settings, check idle timeout |
| Terminal blank on tab switch | Check session exists, restart server |
| Tests failing on session limits | `tmux list-sessions \| grep test \| awk -F: '{print $1}' \| xargs -I{} tmux kill-session -t {}` |
| Problem | Check | Fix |
|---------|-------|-----|
| Session won't start | `tmux list-sessions` for orphans | Kill orphaned sessions, check Claude CLI installed |
| Port 3000 in use | `lsof -i :3000` | Kill conflicting process or use `--port` flag |
| SSE not connecting | Browser console for errors | Check CORS, ensure server running |
| Respawn not triggering | Session settings → Respawn enabled? | Enable respawn, check idle timeout config |
| Terminal blank on tab switch | Network tab for `/api/sessions/:id/buffer` | Check session exists, restart server |
| Tests failing on session limits | `tmux list-sessions \| wc -l` | Clean up: `tmux list-sessions \| grep test \| awk -F: '{print $1}' \| xargs -I{} tmux kill-session -t {}` |
| State not persisting | `cat ~/.codeman/state.json` | Check file permissions, disk space |
## Performance & Resource Limits
## Performance Constraints
Must stay fast with 20 sessions and 50 agent windows. Key: 60fps terminal (16ms batching + rAF), auto-trimming buffers, debounced state persistence (500ms), SSE adaptive batching (16-50ms), backpressure handling, cached endpoints (1s TTL for `/api/sessions` and `/api/status`).
The app must stay fast with 20 sessions and 50 agent windows:
- 60fps terminal (16ms batching + `requestAnimationFrame`)
- Auto-trimming buffers (2MB terminal max)
- Debounced state persistence (500ms)
- SSE adaptive batching: 16ms (normal), 32ms (moderate), 50ms (rapid); immediate flush at 32KB
- SSE backpressure handling: skip writes to backpressured clients, recover via `session:needsRefresh` on drain
- Cached endpoints: `/api/sessions` and `/api/status` use 1s TTL caches to avoid expensive serialization
- Frontend buffer loads: 128KB chunks via `requestAnimationFrame` to prevent UI jank
**Anti-flicker**: `PTY → Server Batching → DEC 2026 Wrap → SSE → Client rAF → xterm.js`. See `docs/terminal-anti-flicker.md`.
## Terminal Anti-Flicker System
**Limits** in `src/config/` (buffer-limits.ts, map-limits.ts, etc.). Key: terminal 2MB/1.5MB trim, text 1MB/768KB, messages 1000/800, max agents 500, max sessions 50, max SSE clients 100. Use `LRUMap` for bounded caches, `StaleExpirationMap` for TTL cleanup.
Claude Code uses Ink (React for terminals), which redraws the screen on every state change. Codeman implements a 6-layer anti-flicker pipeline for smooth 60fps output:
```
PTY Output → Server Batching (16-50ms) → DEC 2026 Wrap → SSE → Client rAF → xterm.js
```
**Key functions:** `server.ts:batchTerminalData()`, `server.ts:flushTerminalBatches()`, `app.js:batchTerminalWrite()`, `app.js:extractSyncSegments()`
**Typical latency:** 16-32ms. Optional per-session flicker filter adds ~50ms for problematic terminals.
See `docs/terminal-anti-flicker.md` for full implementation details (adaptive batching, DEC 2026 markers, edge cases).
## Resource Limits
Limits are centralized in `src/config/` — see `buffer-limits.ts`, `map-limits.ts`, `server-timing.ts`, `auth-config.ts`, `tunnel-config.ts`, `terminal-limits.ts`, `ai-defaults.ts`, `team-config.ts`.
**Buffer limits** (per session):
| Buffer | Max | Trim To |
|--------|-----|---------|
| Terminal | 2MB | 1.5MB |
| Text output | 1MB | 768KB |
| Messages | 1000 | 800 |
**Map limits** (global):
| Resource | Max |
|----------|-----|
| Tracked agents | 500 |
| Concurrent sessions | 50 |
| SSE clients total | 100 |
| File watchers | 500 |
Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-based cleanup.
## Where to Find More Information
## References
| Topic | Location |
|-------|----------|
| **Respawn state machine** | `docs/respawn-state-machine.md` |
| **Ralph Loop guide** | `docs/ralph-wiggum-guide.md` |
| **Claude Code hooks** | `docs/claude-code-hooks-reference.md` |
| **Terminal anti-flicker** | `docs/terminal-anti-flicker.md` |
| **Agent Teams (experimental)** | `agent-teams/README.md`, `agent-teams/design.md` |
| **API routes** | `src/web/routes/` domain modules, or README.md |
| **SSE events** | Search `broadcast(` in `server.ts` and route modules |
| **Session statuses** | `SessionStatus` in `src/types/session.ts` |
| **Error codes** | `createErrorResponse()` in `src/types/api.ts` |
| **Refactoring phases** | `docs/phase1-implementation-plan.md` through `docs/phase7-test-infrastructure-plan.md` |
| **Test utilities** | `test/respawn-test-utils.ts` |
| **Mobile test suite** | `mobile-test/README.md` |
| **OpenCode integration** | `docs/opencode-integration.md` |
| **Local echo overlay** | `docs/local-echo-overlay-plan.md` |
| **Performance investigation** | `docs/performance-investigation-report.md` |
| **First-load optimization** | `docs/first-load-optimization-plan.md`, `docs/perf-audit-first-load.md` |
| **Codebase quality / refactoring summary** | `docs/code-structure-findings.md` |
| **Dead code audit** | `docs/cleanup-findings.md` |
| **TypeScript improvements** | `docs/typescript-improvement-suggestions.md` |
| **Browser testing** | `docs/browser-testing-guide.md` |
| **Mobile testing report** | `docs/mobile-testing-report.md` |
| **Voice input** | `docs/voice-input-plan.md` |
| **Improvement roadmaps** | `docs/respawn-improvement-plan.md`, `docs/ralph-improvement-plan.md`, `docs/plan-improvement-roadmap.md` |
| **Background keystroke forwarding** | `docs/background-keystroke-forwarding-merged-plan.md` |
| **QR auth design** | `docs/qr-auth-plan.md` |
| **Run summary** | `docs/run-summary-plan.md` |
Additional design docs and investigation reports are in the `docs/` directory.
| Respawn state machine | `docs/respawn-state-machine.md` |
| Ralph Loop guide | `docs/ralph-wiggum-guide.md` |
| Claude Code hooks | `docs/claude-code-hooks-reference.md` |
| Terminal anti-flicker | `docs/terminal-anti-flicker.md` |
| Agent Teams | `agent-teams/README.md`, `agent-teams/design.md` |
| OpenCode integration | `docs/opencode-integration.md` |
| QR auth design | `docs/qr-auth-plan.md` |
| SSE events | `src/web/sse-events.ts` + `constants.js` |
| Types architecture | `src/types/index.ts` `@fileoverview` |
| API routes | `src/web/routes/` — each file has `@fileoverview` |
## Scripts
| Script | Purpose |
|--------|---------|
| `scripts/tmux-manager.sh` | Safe tmux session management (use instead of direct kill commands) |
| `scripts/monitor-respawn.sh` | Monitor respawn state machine in real-time |
| `scripts/watch-subagents.ts` | Real-time subagent transcript watcher (list, follow by session/agent ID) |
| `scripts/codeman-web.service` | systemd service file for production deployment |
| `scripts/codeman-tunnel.service` | systemd service file for persistent Cloudflare tunnel |
| `scripts/tunnel.sh` | Start/stop/check Cloudflare quick tunnel (`./scripts/tunnel.sh start\|stop\|url`) |
| `scripts/build.mjs` | esbuild-based production build (called by `npm run build`) |
| `scripts/postinstall.js` | npm postinstall hook for setup |
Additional scripts in `scripts/` for screenshots, demos, Ralph wizards, and browser testing.
Key: `scripts/tmux-manager.sh` (safe tmux mgmt), `scripts/tunnel.sh` (tunnel start/stop/url), `scripts/monitor-respawn.sh` (respawn monitoring), `scripts/watch-subagents.ts` (transcript watcher). Production services: `scripts/codeman-web.service`, `scripts/codeman-tunnel.service`.
## Memory Leak Prevention
Frontend runs long (24+ hour sessions); all Maps/timers must be cleaned up.
### Cleanup Patterns
When adding new event listeners or timers:
1. Store handler references for later removal
2. Add cleanup to appropriate `stop()` or `cleanup*()` method
3. For singleton watchers, store refs in class properties and remove in server `stop()`
**Backend**: Clear Maps in `stop()`, null promise callbacks on error, remove watcher listeners on shutdown. Use `CleanupManager` for centralized disposal — supports timers, intervals, watchers, listeners, streams. Guard async callbacks with `if (this.cleanup.isStopped) return`.
**Frontend**: Store drag/resize handlers on elements, clean up in `close*()` functions. SSE reconnect calls `handleInit()` which resets state. SSE listeners are tracked in an array and removed on reconnect to prevent accumulation.
Run `npx vitest run test/memory-leak-prevention.test.ts` to verify patterns.
24+ hour sessions require cleanup of all Maps/timers. Backend: use `CleanupManager`, clear Maps in `stop()`, guard async with `if (this.cleanup.isStopped) return`. Frontend: store handler refs, clean in `close*()`, SSE reconnect resets via `handleInit()`. Verify: `npx vitest run test/memory-leak-prevention.test.ts`.
## Common Workflows
**Investigating a bug**: Start dev server (`npx tsx src/index.ts web`), reproduce in browser, check terminal output and `~/.codeman/state.json` for clues.
**Investigating a bug**: Start dev server, reproduce in browser, check terminal + `~/.codeman/state.json`.
**Adding a new API endpoint**: Define types in the appropriate `src/types/*.ts` domain file, add route in the matching `src/web/routes/*-routes.ts` module, broadcast SSE events if needed, handle in `app.js:handleSSEEvent()`.
**Adding an API endpoint**: Types in `src/types/*.ts`, route in `src/web/routes/*-routes.ts`, broadcast SSE if needed, handle in `app.js:handleSSEEvent()`.
**Modifying respawn behavior**: Study `docs/respawn-state-machine.md` first. The state machine is in `respawn-controller.ts`. Use MockSession from `test/respawn-test-utils.ts` for testing.
**Modifying respawn**: Study `docs/respawn-state-machine.md` first. Use `MockSession` from `test/respawn-test-utils.ts`.
**Modifying mobile behavior**: Mobile singletons (`MobileDetection`, `KeyboardHandler`, `SwipeHandler`, `KeyboardAccessoryBar`) all have `init()`/`cleanup()` lifecycle. KeyboardHandler uses `visualViewport` API for iOS keyboard detection (100px threshold for address bar drift). All mobile handlers are re-initialized after SSE reconnect to prevent stale closures.
**Modifying mobile**: Singletons have `init()`/`cleanup()` lifecycle. Re-initialized after SSE reconnect to prevent stale closures.
**Adding a file watcher**: Use `ImageWatcher` as a template pattern — chokidar with `awaitWriteFinish`, burst throttling (max 20/10s), debouncing (200ms), and auto-ignore of `node_modules/.git/dist/`.
## Tunnel Setup
## Tunnel Setup (Remote Access)
Access Codeman from mobile/remote devices via Cloudflare quick tunnel.
```
Browser → Cloudflare Edge (HTTPS) → cloudflared → localhost:3000
```
**Prerequisites**: `cloudflared` installed (`cloudflared --version`), `CODEMAN_PASSWORD` set in environment.
### Quick Start
```bash
# Via CLI
./scripts/tunnel.sh start # Start tunnel, prints public URL
./scripts/tunnel.sh url # Show current URL
./scripts/tunnel.sh stop # Stop tunnel
# Via web UI: Settings → Tunnel → Toggle On
```
### systemd Service (Persistent)
```bash
# Install and enable
cp scripts/codeman-tunnel.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codeman-tunnel
# Check logs
journalctl --user -u codeman-tunnel -f
```
### Auth Flow
1. First request → browser shows Basic Auth prompt (username: `admin` or `CODEMAN_USERNAME`), or scan QR code from tunnel settings panel
2. On success → server issues `codeman_session` HttpOnly cookie (24h TTL, auto-extends on activity)
3. Subsequent requests → cookie authenticates silently (no more prompts)
4. SSE works automatically — `EventSource` sends same-origin cookies
5. 10 failed attempts per IP → 429 rate limit (15-minute decay)
### Security Requirements
- **Always set `CODEMAN_PASSWORD`** before exposing via tunnel — without it, anyone with the URL has full access
- Session cookies are `Secure` when using `--https` flag; through Cloudflare tunnel without `--https`, cookies are non-Secure but traffic is still encrypted end-to-end via Cloudflare
- `/api/hook-event` bypasses auth (localhost-only Claude Code hooks need unauthenticated access)
Remote access via Cloudflare quick tunnel: `./scripts/tunnel.sh start|stop|url`. Web UI: Settings → Tunnel. Persistent: `systemctl --user enable --now codeman-tunnel`. **Always set `CODEMAN_PASSWORD`** before exposing via tunnel.