* 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>
19 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Quick Reference
| Task | Command |
|---|---|
| Dev server | npx tsx src/index.ts web |
| Type check | tsc --noEmit |
| Lint | npm run lint (fix: npm run lint:fix) |
| Format | npm run format (check: npm run format:check) |
| Single test | npx vitest run test/<file>.test.ts |
| Production | npm run build && systemctl --user restart codeman-web |
CRITICAL: Session Safety
You may be running inside a Codeman-managed tmux session. Before killing ANY tmux or Claude process:
- Check:
echo $CODEMAN_MUX- if1, you're in a managed session - NEVER run
tmux kill-session,pkill tmux, orpkill claudewithout confirming - Use the web UI or
./scripts/tmux-manager.shinstead of direct kill commands
CRITICAL: Always Test Before Deploying
NEVER COM without verifying your changes actually work. For every fix:
- Backend changes: Hit the API endpoint with
curland verify the response - Frontend changes: Use Playwright to load the page and assert the UI renders correctly. Use
waitUntil: 'domcontentloaded'(notnetworkidle— SSE keeps the connection open). Wait 3-4s for polling/async data to populate, then check element visibility, text content, and CSS values - Only after verification passes, proceed with COM
The production server caches static files for 1 year (maxAge: '1y' in server.ts). After deploying frontend changes, users may need a hard refresh (Ctrl+Shift+R) to see updates.
COM Shorthand (Deployment)
Uses Semantic Versioning (MAJOR.MINOR.PATCH) via @changesets/cli.
When user says "COM":
- Determine bump type:
COM= patch (default),COM minor= minor,COM major= major - Create a changeset file (no interactive prompts). Write a
.mdfile in.changeset/with a random filename:Replacecat > .changeset/$(openssl rand -hex 4).md << 'CHANGESET' --- "aicodeman": patch --- Description of changes CHANGESETpatchwithminorormajoras needed. Include"xterm-zerolag-input": patchon a separate line if that package changed too. - Consume the changeset:
npm run version-packages(bumps versions inpackage.jsonfiles and updatesCHANGELOG.md) - Sync CLAUDE.md version: Update the
**Version**line below to match the new version frompackage.json - Commit and deploy:
git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web
Version: 0.3.0 (must match package.json)
Project Overview
Codeman is a Claude Code session manager with web interface and autonomous Ralph Loop. Spawns Claude CLI via PTY, streams via SSE, supports respawn cycling for 24+ hour autonomous runs.
Tech Stack: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, node-pty, xterm.js. Supports both Claude Code and OpenCode AI CLIs via pluggable CLI resolvers.
TypeScript Strictness (see tsconfig.json): noUnusedLocals, noUnusedParameters, noImplicitReturns, noImplicitOverride, noFallthroughCasesInSwitch, allowUnreachableCode: false, allowUnusedLabels: false.
Requirements: Node.js 18+, Claude CLI, tmux
Git: Main branch is master. SSH session chooser: sc (interactive), sc 2 (quick attach), sc -l (list).
Additional Commands
npm run dev = dev server. Default port: 3000. Commands not in Quick Reference:
| 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 |
CI: .github/workflows/ci.yml runs typecheck, lint, format:check on push to master (Node 22). Tests excluded (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
- Single-line prompts only —
writeViaMux()sends text and Enter separately; multi-line breaks Ink - Don't kill tmux sessions blindly — Check
$CODEMAN_MUXfirst; you might be inside one - Global regex
lastIndexsharing —ANSI_ESCAPE_PATTERN_FULL/SIMPLEhavegflag; usecreateAnsiPatternFull/Simple()factory functions for fresh instances in loops - 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
_isLoadingBufferis 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. Useawait import()for dynamic imports. (tsxmasks this in dev by shimming CJS/ESM) - Package name vs product name — npm package is
aicodeman, product is Codeman. Release workflow renamesaicodeman@X.Y.Ztags tocodeman@X.Y.Z
Import Conventions
- Utilities: Import from
./utils(re-exports all):import { LRUMap, stripAnsi } from './utils' - Types: Use type imports from barrel:
import type { SessionState } from './types'(re-exports fromsrc/types/domain files) - Config: Import from specific files:
import { MAX_TERMINAL_BUFFER_SIZE } from './config/buffer-limits'
Architecture
Core Files (by domain)
| 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 file (>50KB), contains complex state machines. Read docs/respawn-state-machine.md before modifying respawn/ralph.
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).
Config: src/config/ — 9 files (buffer limits, map limits, timeouts, SSE timing, auth, tunnel, terminal, AI, teams). Import from specific files.
Utilities: src/utils/ — re-exported via src/utils/index.ts. Key: CleanupManager, LRUMap, StaleExpirationMap, BufferAccumulator, stripAnsi, createAnsiPatternFull/Simple(), assertNever, Debouncer.
Data Flow
- Session spawns
claude --dangerously-skip-permissionsvia node-pty - PTY output buffered, ANSI stripped, parsed for JSON messages
- WebServer broadcasts to SSE clients at
/api/events - State persists to
~/.codeman/state.jsonvia StateStore
Key Patterns
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.
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.
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/.
Circuit breaker: Prevents respawn thrashing. States: CLOSED → HALF_OPEN → OPEN. Reset: /api/sessions/:id/ralph-circuit-breaker/reset.
Port interfaces: Routes declare dependencies via port interfaces (src/web/ports/). Routes use intersection types (e.g., SessionPort & EventPort).
Frontend
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).
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), 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
| 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 Registry
~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
~111 route handlers in src/web/routes/. Key groups:
| Group | Prefix | Count | Key endpoints |
|---|---|---|---|
| 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 |
| 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 |
| 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 insrc/web/routes/*-routes.ts, usecreateErrorResponse(). Validate with Zod schemas inschemas.ts. - SSE event: Add to
src/web/sse-events.ts+SSE_EVENTSinconstants.js, emit viabroadcast(), handle inapp.js(addListener() - Session setting: Add to
SessionState, include insession.toState(), callpersistSessionState() - Hook event: Add to
HookEventType, add hook inhooks-config.ts:generateHooksConfig(), updateHookEventSchema - 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()— seetest/routes/_route-test-utils.ts.
Validation: Zod v4 (different API from v3). Define schemas in schemas.ts, use .parse()/.safeParse().
State Files
| File | Purpose |
|---|---|
~/.codeman/state.json |
Sessions, settings, tokens, respawn config |
~/.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 |
Push notification subscriptions |
~/.codeman/session-lifecycle.jsonl |
Append-only audit log (QR auth, session events) |
Default Settings
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. Only run individual files:
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
Config: Vitest with globals: true, fileParallelism: false. Timeout 30s, teardown 60s.
Safety: test/setup.ts snapshots pre-existing tmux sessions and never kills them. Only registerTestTmuxSession() sessions 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. Route tests: app.inject() in test/routes/. Mobile tests: Playwright suite in mobile-test/ (135 device profiles).
Screenshots
"sc"/"screenshot" = uploaded mobile screenshots in ~/.codeman/screenshots/. View with Read tool. API: GET /api/screenshots (list), POST /api/screenshots (upload).
Debugging & Troubleshooting
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
| 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 {} |
Performance & Resource Limits
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).
Anti-flicker: PTY → Server Batching → DEC 2026 Wrap → SSE → Client rAF → xterm.js. See docs/terminal-anti-flicker.md.
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.
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 | 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
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
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, reproduce in browser, check terminal + ~/.codeman/state.json.
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: Study docs/respawn-state-machine.md first. Use MockSession from test/respawn-test-utils.ts.
Modifying mobile: Singletons have init()/cleanup() lifecycle. Re-initialized after SSE reconnect to prevent stale closures.
Tunnel Setup
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.