diff --git a/CLAUDE.md b/CLAUDE.md index f57623eb..3daa3a5e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,877 +2,156 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## ⚠️ CRITICAL: Screen Session Safety +## CRITICAL: Screen Session Safety **You may be running inside a Claudeman-managed screen session.** Before killing ANY screen or Claude process: -1. **Check environment**: `echo $CLAUDEMAN_SCREEN` - if it returns `1`, you're in a managed session -2. **NEVER run** `screen -X quit`, `pkill screen`, or `pkill claude` without first confirming you're not killing yourself -3. **Safe debugging**: Use `screen -ls` to LIST sessions, but don't kill them blindly -4. **If you need to kill screens**: Use the web UI or `./scripts/screen-manager.sh` instead of direct commands +1. Check: `echo $CLAUDEMAN_SCREEN` - if `1`, you're in a managed session +2. **NEVER** run `screen -X quit`, `pkill screen`, or `pkill claude` without confirming +3. Use the web UI or `./scripts/screen-manager.sh` instead of direct kill commands -**Why this matters**: Killing your own screen terminates your session mid-work, losing context and potentially corrupting state. - -## ⚡ COM Shorthand (Deployment) +## COM Shorthand (Deployment) When user says "COM": -1. Increment version in BOTH `package.json` AND `CLAUDE.md` (keep them in sync) +1. Increment version in BOTH `package.json` AND `CLAUDE.md` 2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` -Always bump version on every COM, even for small changes. +**Version**: 0.1402 (must match `package.json`) ## Project Overview -Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs. +Claudeman 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. -### Mission: Rock-Solid Performance +**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode with `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`), Node.js, Fastify, node-pty, xterm.js -**The app MUST remain fast, responsive, and never hang** — even with many tabs open and many subagent windows active. This is a core design principle, not a nice-to-have. Every feature must be built with this constraint in mind: - -- **60fps terminal rendering** with batched writes and `requestAnimationFrame` -- **Auto-trimming buffers** prevent memory bloat in long-running sessions -- **Debounced state persistence** (500ms) avoids disk thrashing -- **SSE batching** (16ms) reduces client-side event storms -- **Lazy loading** of agent transcripts and historical data -- **No blocking operations** in the main event loop - -When adding new features, always ask: "Will this maintain responsiveness with 20 sessions and 50 agent windows?" - -**Version**: 0.1400 (must match `package.json`) - -**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty - -**Key Dependencies**: fastify (REST API), node-pty (PTY spawning), ink/react (TUI), xterm.js (web terminal), @modelcontextprotocol/sdk (MCP server for spawn protocol) - -**Requirements**: Node.js 18+, Claude CLI (`claude`) installed, GNU Screen (`apt install screen` / `brew install screen`) - -> **Note**: `claude` does not need to be in the server process's PATH. Claudeman auto-discovers the binary from common install locations (`~/.local/bin`, `~/.claude/local`, `/usr/local/bin`, etc.) and augments PATH for spawned sessions. - -> **Runtime**: The web server runs as a systemd user service (`claudeman-web.service`) on HTTPS port 3000 with a self-signed certificate. It auto-restarts and survives logout. - -## First-Time Setup - -```bash -npm install -``` +**Requirements**: Node.js 18+, Claude CLI, GNU Screen ## Commands -**CRITICAL**: `npm run dev` runs CLI help, NOT the web server. Use `npx tsx src/index.ts web` for development. - -**Quick reference**: -- Dev server: `npx tsx src/index.ts web` (or `web --https` for notifications) -- Type check: `npx tsc --noEmit` -- Single test: `npx vitest run test/.test.ts` -- Restart prod: `systemctl --user restart claudeman-web` - -### Build & Clean +**CRITICAL**: `npm run dev` shows CLI help, NOT the web server. ```bash -npm run build # Compile TS + copy static files + templates + make bins executable -npm run clean # Remove dist/ -npm run typecheck # Type check without building (or: npx tsc --noEmit) -``` +# Development +npx tsx src/index.ts web # Dev server (RECOMMENDED) +npx tsx src/index.ts web --https # With TLS for notifications +npx tsc --noEmit # Type check -### Web Server +# Testing +npx vitest run # All tests +npx vitest run test/.test.ts # Single file +npm run test:e2e # Browser E2E tests -```bash -npx tsx src/index.ts web # Dev mode - no build needed (RECOMMENDED) -npx tsx src/index.ts web -p 8080 # Dev mode with custom port -npx tsx src/index.ts web --https # Dev mode with self-signed TLS (enables browser notifications) -npm run web # After npm run build (shorthand) -node dist/index.js web # After npm run build -claudeman web # After npm link -``` - -### TUI (Terminal User Interface) - -```bash -npx tsx src/index.ts tui # Dev mode - prompts to start web if not running -claudeman tui # After npm link -claudeman tui --with-web # Auto-start web server if not running (no prompt) -claudeman tui --no-web # Skip web server check entirely -claudeman tui -p 8080 # Specify web server port -``` - -### Testing - -```bash -npm run test # Run all tests once -npm run test:watch # Watch mode -npm run test:coverage # With coverage report -npx vitest run test/session.test.ts # Single file -npx vitest run -t "should create session" # By pattern -``` - -**Test Configuration** (vitest.config.ts): -- `globals: true` - no imports needed for `describe`/`it`/`expect` -- `testTimeout: 30000` - 30s for integration tests -- `teardownTimeout: 60000` - 60s ensures cleanup runs even on failures -- `fileParallelism: false` - sequential file execution to respect screen session limits -- Coverage excludes entry points: `src/index.ts`, `src/cli.ts` - -**Test Port Allocation** (integration tests spawn servers): - -| Port | Test File | -|------|-----------| -| 3099 | quick-start.test.ts | -| 3102 | session.test.ts | -| 3105 | scheduled-runs.test.ts | -| 3107 | sse-events.test.ts | -| 3110 | edge-cases.test.ts | -| 3115 | integration-flows.test.ts | -| 3120 | session-cleanup.test.ts | -| 3125 | ralph-integration.test.ts | -| 3127 | respawn-integration.test.ts (reserved) | -| 3130 | hooks-config.test.ts (Hook Event API) | -| 3131 | hooks-config.test.ts (Hook Data Sanitization) | -| 3150 | browser-e2e.test.ts (main browser tests) | -| 3151 | browser-e2e.test.ts (SSE events tests) | -| 3152 | browser-e2e.test.ts (hook events tests) | -| 3153 | browser-e2e.test.ts (Ralph panel tests) | -| 3154 | file-link-click.test.ts | -| 3155 | browser-playwright.test.ts | -| 3156 | browser-puppeteer.test.ts | -| 3157 | browser-agent.test.ts | -| 3158-3160 | browser-comparison.test.ts | -| 3180-3182 | scripts/browser-comparison.mjs (benchmark) | -| 3183 | test/e2e/workflows/quick-start.e2e.ts | -| 3184 | test/e2e/workflows/session-input.e2e.ts | -| 3185 | test/e2e/workflows/session-delete.e2e.ts | -| 3186 | test/e2e/workflows/multi-session.e2e.ts | -| 3187 | test/e2e/workflows/agent-interactions.e2e.ts | -| 3188 | test/e2e/workflows/input-interactions.e2e.ts | -| 3189 | test/e2e/workflows/respawn-flow.e2e.ts | - -**Next available port**: 3190 - -**Browser Testing**: Three frameworks available (Playwright, Puppeteer, Agent-Browser). See `docs/browser-testing-guide.md` for full comparison and patterns. - -```bash -# Run browser benchmark (standalone - recommended) -npx tsx scripts/browser-comparison.mjs - -# Run existing browser E2E tests -npm test -- test/browser-e2e.test.ts -``` - -**Browser Testing Key Points**: -- **Vitest hook issue**: Browser tests using `beforeAll`/`afterAll` timeout even when tests pass. Use standalone scripts or run browser code directly in tests. -- **Recommended framework**: Playwright for most cases (auto-waiting, debugging). Puppeteer for Chrome-specific/CDP features. -- **Required browser args**: `--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage` -- **Install browsers**: `npx playwright install chromium` after npm install - -### E2E Test Suite - -Real browser-based end-to-end tests using Playwright that validate actual user workflows. These tests catch issues that unit tests miss (like the cpulimit bug that broke screen creation). - -**Setup**: -```bash -npm install # Install dependencies (pixelmatch, pngjs) -npx playwright install chromium # Install browser -``` - -**Running E2E Tests**: -```bash -npm run test:e2e # Run all E2E tests -npm run test:e2e:quick # Run quick-start test only (critical path) -npx vitest run test/e2e/workflows/quick-start.e2e.ts # Run single test file -npx vitest run test/e2e/ -t "should create session" # Run by pattern -``` - -**Test Fixtures** (`test/e2e/fixtures/`): - -| Fixture | Purpose | -|---------|---------| -| `server.fixture.ts` | `createServerFixture(port)` / `destroyServerFixture()` - Server lifecycle | -| `browser.fixture.ts` | `createBrowserFixture()` / `destroyBrowserFixture()` - Playwright browser with required args | -| `cleanup.fixture.ts` | `CleanupTracker` - Tracks sessions, cases, screens for cleanup | -| `screenshot.fixture.ts` | `captureAndCompare()` - Visual regression testing with pixelmatch | - -**Workflow Tests** (`test/e2e/workflows/`): - -| Test | Port | What it validates | -|------|------|-------------------| -| `quick-start.e2e.ts` | 3183 | **Critical path**: click button → session created → screen created → terminal visible | -| `session-input.e2e.ts` | 3184 | Terminal input, Ctrl+C cancel, multi-line input | -| `session-delete.e2e.ts` | 3185 | Delete button → screen killed → UI updated | -| `multi-session.e2e.ts` | 3186 | Multiple sessions, tab switching, Ctrl+Tab shortcut | -| `agent-interactions.e2e.ts` | 3187 | Subagent windows, parent attachment, visibility | -| `input-interactions.e2e.ts` | 3188 | Modals, checkboxes, Ctrl+Enter/W shortcuts | -| `respawn-flow.e2e.ts` | 3189 | Respawn enable/start/stop via API and UI | -| `ralph-loop.e2e.ts` | 3190 | Ralph Loop wizard: open, configure, start, verify tracker enabled | - -**Screenshot Validation**: -- Baselines stored in `test/e2e/screenshots/baselines/` -- Current screenshots in `test/e2e/screenshots/current/` -- Diff images (on failure) in `test/e2e/screenshots/diffs/` -- First run auto-creates baselines; subsequent runs compare -- Default threshold: 5% pixel difference allowed - -**Cleanup Behavior**: -- All test cases use `e2e-test-*` prefix for easy identification -- `CleanupTracker.forceCleanupAll()` removes ALL `e2e-test-*` resources -- try/finally patterns ensure cleanup even on test failures -- `afterAll` hooks call cleanup as safety net - -**E2E Test Pattern** (avoids Vitest hook timeout issue): -```typescript -it('should create session', async () => { - let browser: BrowserFixture | null = null; - const caseName = generateCaseName('test'); - - try { - serverFixture = await createServerFixture(PORT); - cleanup = new CleanupTracker(serverFixture.baseUrl); - cleanup.trackCase(caseName); - - browser = await createBrowserFixture(); - // ... test code ... - } finally { - if (browser) await destroyBrowserFixture(browser); - } -}, 90000); -``` - -**E2E Installation Summary**: - -| Component | Details | -|-----------|---------| -| **Test Files** | 14 TypeScript files in `test/e2e/` | -| **Fixtures** | server, browser, cleanup, screenshot, index, pixelmatch types | -| **Workflows** | quick-start, session-input, session-delete, multi-session, agent-interactions, input-interactions, respawn-flow | -| **Dependencies** | `playwright`, `pixelmatch`, `pngjs`, `@types/pngjs` | -| **Browser** | Chromium via `npx playwright install chromium` | -| **NPM Scripts** | `npm run test:e2e` (all), `npm run test:e2e:quick` (critical path) | -| **Ports** | 3183-3189 (see workflow tests table above) | -| **Gitignore** | `test/e2e/screenshots/current/` and `diffs/` ignored; `baselines/` tracked | - -**Unit tests** (no server needed): `respawn-controller`, `ralph-tracker`, `pty-interactive`, `task-queue`, `task`, `ralph-loop`, `session-manager`, `state-store`, `types`, `templates`, `ralph-config`, `spawn-detector`, `spawn-types`, `spawn-orchestrator`, `ai-idle-checker`, `ai-plan-checker` - -**Test Utilities**: `test/respawn-test-utils.ts` provides MockSession, MockAiIdleChecker, MockAiPlanChecker, time controller, state tracker, and event recorder for respawn controller testing. See `test/respawn-test-plan.md` for architecture and `test/respawn-scenarios.md` for comprehensive test scenarios. - -**Test Safety**: `test/setup.ts` enforces max 10 concurrent screens, performs orphan cleanup, and protects its own process tree. You can safely run tests from within a Claudeman-managed session - the cleanup will not kill your own Claude instance. The respawn-controller tests use MockSession (not real screens). - -**Test Cleanup Patterns**: Integration tests track resources in `createdSessions` and `createdCases` arrays, cleaned up by `afterAll`/`afterEach` hooks. However, some tests perform cleanup in the test body itself (e.g., `edge-cases.test.ts:273-302` creates 5 sessions and cleans them in a loop). If assertions fail before cleanup code runs, resources leak. - -**Known Cleanup Issues** (technical debt): -- `pty-interactive.test.ts`: Uses `await session.stop()` at end of each test, not in `afterEach`. Test failures leave sessions running. -- `edge-cases.test.ts`: Multiple sessions created in test body with cleanup at end; failures leak sessions. -- Test cases (`~/claudeman-cases/`): Cases named `flow-test-*`, `ralph-track-loop-*`, `session-detail-*` may persist after test failures. - -**Manual Cleanup**: -```bash -# Remove orphaned test cases -rm -rf ~/claudeman-cases/flow-test-* ~/claudeman-cases/ralph-track-loop-* ~/claudeman-cases/session-detail-* - -# Kill orphaned test screens (only detached claudeman screens) -screen -ls | grep -E 'Detached.*claudeman' | cut -d. -f1 | xargs -I{} screen -S {} -X quit -``` - -### MCP Server - -```bash -npx tsx src/mcp-server.ts # Dev mode (stdio transport) -``` - -Configure in Claude Code's MCP settings: -```json -{ "command": "node", "args": ["dist/mcp-server.js"], "env": { "CLAUDEMAN_API_URL": "http://localhost:3000", "CLAUDEMAN_SESSION_ID": "" } } -``` - -### Debugging - -```bash -screen -ls # List GNU screen sessions -screen -r # Attach to screen session (Ctrl+A D to detach) -curl localhost:3000/api/sessions # Check active sessions -curl localhost:3000/api/status | jq . # Full app state including respawn -cat ~/.claudeman/state.json | jq . # View main state -cat ~/.claudeman/state-inner.json | jq . # View Ralph loop state -``` - -### Systemd Service - -```bash -systemctl --user status claudeman-web # Check status -systemctl --user restart claudeman-web # Restart -systemctl --user stop claudeman-web # Stop -journalctl --user -u claudeman-web -f # Stream logs -``` - -Install: `ln -sf scripts/claudeman-web.service ~/.config/systemd/user/` -Enable: `systemctl --user enable claudeman-web && loginctl enable-linger $USER` - -### Kill Stuck Screens - -```bash -screen -X -S quit # Graceful quit -pkill -f "SCREEN.*claudeman" # Force kill all claudeman screens -``` - -## CLI Commands - -```bash -claudeman session [s] # Manage Claude sessions - start # Start new session - stop # Stop session - list [ls] # List all - logs # View output - -claudeman task [t] # Manage tasks - add # Add task - list [ls] # List tasks - status # Task details - remove [rm] # Remove task - clear # Clear completed/failed - -claudeman ralph [r] # Control Ralph loop - start # Start loop - stop # Stop loop - status # Show status - -claudeman web # Start web interface -claudeman tui # Start TUI -claudeman status # Overall status -claudeman reset # Reset all state +# Production +npm run build +systemctl --user restart claudeman-web +journalctl --user -u claudeman-web -f ``` ## Architecture -### Key Files +### Core Files -**Core Session Management:** -| File | Purpose | -|------|---------| -| `src/session.ts` | Core PTY wrapper for Claude CLI. Modes: `runPrompt()`, `startInteractive()`, `startShell()` | -| `src/screen-manager.ts` | GNU screen persistence, ghost discovery, 4-strategy kill | -| `src/session-manager.ts` | Session lifecycle, task assignment, cleanup | -| `src/state-store.ts` | JSON persistence to `~/.claudeman/` with debounced writes | -| `src/types.ts` | All TypeScript interfaces | - -**Autonomous Features:** -| File | Purpose | -|------|---------| -| `src/respawn-controller.ts` | State machine for autonomous session cycling | -| `src/ai-idle-checker.ts` | Spawns Claude to analyze terminal output for IDLE/WORKING verdict | -| `src/ai-plan-checker.ts` | Spawns Claude to detect plan mode prompts for auto-accept | -| `src/ralph-tracker.ts` | Detects `PHRASE`, todos, loop status | -| `src/ralph-config.ts` | Parses `.claude/ralph-loop.local.md` and CLAUDE.md for Ralph config | -| `src/plan-orchestrator.ts` | Multi-agent plan generation with parallel analysis + verification | -| `src/run-summary.ts` | Tracks session events for "what happened while away" summaries | - -**Spawn Protocol (Autonomous Agents):** -| File | Purpose | -|------|---------| -| `src/spawn-orchestrator.ts` | Full agent lifecycle: spawn, monitor, budget, queue, cleanup | -| `src/mcp-server.ts` | MCP server binary exposing spawn tools to Claude Code | -| `src/subagent-watcher.ts` | Monitors Claude Code background agents in `~/.claude/projects/*/subagents/*.jsonl` | - -**Web & TUI:** | File | Purpose | |------|---------| +| `src/session.ts` | PTY wrapper: `runPrompt()`, `startInteractive()`, `startShell()` | +| `src/screen-manager.ts` | GNU screen persistence, ghost discovery | +| `src/session-manager.ts` | Session lifecycle, cleanup | +| `src/respawn-controller.ts` | State machine for autonomous cycling | +| `src/ralph-tracker.ts` | Detects `PHRASE`, todos | +| `src/plan-orchestrator.ts` | Multi-agent plan generation | | `src/web/server.ts` | Fastify REST API + SSE at `/api/events` | -| `src/web/public/app.js` | Frontend: SSE handling, xterm.js, tab management | -| `src/tui/App.tsx` | TUI main component (Ink/React) | -| `src/tui/DirectAttach.ts` | Full-screen console attach with tab switching | +| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows | +| `src/types.ts` | All TypeScript interfaces | ### Data Flow -1. **Session** spawns `claude -p --dangerously-skip-permissions` via `node-pty` (PATH augmented to include claude's install directory) -2. PTY output is buffered, ANSI stripped, and parsed for JSON messages -3. **WebServer** broadcasts events to SSE clients at `/api/events` -4. Full session state (settings, tokens, respawn config, Ralph state) persists to `~/.claudeman/state.json` via **StateStore** -5. Screen metadata persists separately to `~/.claudeman/screens.json` for session recovery +1. Session spawns `claude --dangerously-skip-permissions` via node-pty +2. PTY output buffered, ANSI stripped, parsed for JSON messages +3. WebServer broadcasts to SSE clients at `/api/events` +4. State persists to `~/.claudeman/state.json` via StateStore -### Respawn State Machine +### Key Patterns -State machine for autonomous session cycling: `watching` → `confirming_idle` → `ai_checking` → `sending_update` → `waiting_update` → `sending_clear` → `waiting_clear` → `sending_init` → `waiting_init` → `monitoring_init` → (optionally) `sending_kickstart`. Steps can be skipped via config (`sendClear: false`, `sendInit: false`). After each step, waits for `completionConfirmMs` (10s) of output silence before proceeding. AI idle check uses a fresh Claude session to analyze terminal output for IDLE/WORKING verdict. +**Input to sessions**: Use `session.writeViaScreen()` for programmatic input (respawn, auto-compact). Text and Enter sent as separate `screen -X stuff` commands due to Ink's requirements. All prompts must be single-line. -See `docs/respawn-state-machine.md` for the full state diagram, idle detection layers, and auto-accept behavior. +**Idle detection**: Multi-layer (completion message → AI check → output silence → token stability). See `docs/respawn-state-machine.md`. -### Spawn1337 Protocol (Autonomous Agents) +**Token tracking**: Interactive mode parses status line ("123.4k tokens"), estimates 60/40 input/output split. -Spawned agents are full-power Claude sessions in their own screen sessions, managed via MCP tools (`spawn_agent`, `list_agents`, `get_agent_status`, `get_agent_result`, `send_agent_message`, `cancel_agent`). Max 5 concurrent, max depth 3, default timeout 30min. Agents communicate via filesystem (`spawn-comms/`) and signal completion via `PHRASE`. +**Memory leak prevention**: Frontend runs long; clear all Maps/timers on SSE reconnect in `handleInit()`. -See `docs/spawn-protocol.md` for the full protocol flow, directory structure, resource governance, and MCP configuration. - -### Subagent Watcher (Claude Code Background Agents) - -Monitors Claude Code's internal background agents (the `Task` tool) in real-time. Watches `~/.claude/projects/{project}/{session}/subagents/agent-{id}.jsonl` files and emits structured events. - -**Events**: `subagent:discovered`, `subagent:tool_call`, `subagent:progress`, `subagent:message`, `subagent:completed` - -**API**: -- `GET /api/subagents` - List all known subagents (optional `?minutes=60` for recent only) -- `GET /api/subagents/:agentId` - Get subagent info -- `GET /api/subagents/:agentId/transcript` - Get transcript (`?limit=N`, `?format=formatted`) -- `DELETE /api/subagents/:agentId` - Kill subagent process -- `GET /api/sessions/:id/subagents` - Get subagents for session's working directory - -**Status lifecycle**: `active` → `idle` (30s no activity) → `completed` (process exited or file stale) - -**Settings**: Can be disabled via App Settings → Display → "Enable Subagent Tracking" (default: enabled). Setting is stored in `~/.claudeman/settings.json` as `subagentTrackingEnabled`. - -Implementation: `src/subagent-watcher.ts` - singleton `subagentWatcher` started on server boot (if enabled). - -### Subagent Window Management (Frontend) - -Floating subagent windows in the web UI show real-time activity from Claude Code's background agents. Each window displays tool calls, progress, and messages, with visual connection lines to the parent session tab. - -**Parent Session Discovery** (`app.js:findParentSessionForSubagent()`): -1. When a subagent is discovered via SSE event, the frontend must determine which Claudeman session spawned it -2. Matching uses `workingDir` → `projectHash` conversion: `/home/user/project` becomes `-home-user-project` -3. For each session, calls `/api/sessions/{id}/subagents` which returns subagents matching that session's `workingDir` -4. First match wins: checks active session first, then iterates through other sessions -5. Once found, caches `parentSessionId` and `parentSessionName` in the agent object - -**Window Visibility** (`app.js:updateSubagentWindowVisibility()`): -- "Show for Active Tab Only" setting controls whether windows are hidden when their parent session isn't active -- Windows with unknown parents (discovery pending) are always shown -- Minimized windows stay minimized regardless of visibility setting - -**Tab Badge System**: -- Minimized subagents appear as a badge on their parent session's tab -- Badge dropdown allows restoring or permanently dismissing each agent -- `minimizedSubagents` Map tracks agentIds per sessionId - -**Parent Matching Algorithm** (to handle multiple sessions with same workingDir): - -1. **Strategy 1 - Sibling matching**: If another subagent with the same Claude `sessionId` already has a parent, use that same parent. This ensures all subagents from the same Claude session go to the same Claudeman session. - -2. **Strategy 2 - WorkingDir matching with heuristics**: Find all sessions matching by `workingDir`. If multiple match, prefer the most recently created session (newest session likely spawned the subagent). - -**Session Rename Handling**: When a session is renamed, `updateSubagentParentNames()` updates all subagent objects and their window headers via the `session:updated` SSE handler. - -**Key Frontend Data Structures** (`app.js`): -```javascript -this.subagents = new Map(); // agentId → SubagentInfo (includes parentSessionId, parentSessionName) -this.subagentWindows = new Map(); // agentId → { element, minimized, hidden, position } -this.minimizedSubagents = new Map(); // sessionId → Set (for tab badges) -``` - -**Implementation Files**: -- Server: `src/subagent-watcher.ts` (discovery, monitoring, events) -- Server: `src/web/server.ts` (SSE broadcast, REST endpoints) -- Frontend: `src/web/public/app.js:6183-6550` (window management, parent discovery, visibility) - -### Session Modes - -Sessions have a `mode` property (`SessionMode` type): -- **`'claude'`**: Runs Claude CLI for AI interactions (default) -- **`'shell'`**: Runs a plain bash shell for debugging/testing - -### Screen-Aware Sessions - -All Claude sessions spawned by Claudeman receive environment variables indicating they're running in a managed screen: - -| Variable | Value | Purpose | -|----------|-------|---------| -| `CLAUDEMAN_SCREEN` | `1` | Indicates session is managed by Claudeman | -| `CLAUDEMAN_SESSION_ID` | `` | Unique session identifier | -| `CLAUDEMAN_SCREEN_NAME` | `claudeman-` | GNU screen session name | - -This prevents Claude from accidentally killing its own screen session. The default CLAUDE.md template includes guidance about this. - -**Implementation**: Set in `screen-manager.ts:createScreen()` for screen-based sessions and `session.ts:startInteractive()`/`startShell()` for PTY-only sessions. Both paths also augment `PATH` with the claude binary's directory to ensure discovery in restricted environments (systemd, non-login shells). - -## Code Patterns - -### Memory Leak Prevention - -The frontend (`app.js`) runs for extended periods and must avoid memory leaks. Key patterns: - -**SSE Reconnection**: When EventSource reconnects, all event listeners are re-registered on the new instance. The old EventSource is closed, but any orphaned reconnect timeouts must be cleared: -```javascript -// Clear pending reconnect timeout before creating new connection -if (this.sseReconnectTimeout) { - clearTimeout(this.sseReconnectTimeout); - this.sseReconnectTimeout = null; -} -``` - -**Cleanup on Init**: When `handleInit()` is called (SSE reconnect), clear all Maps and timers that could contain stale data: -- `idleTimers` - Clear all timeouts, then clear the Map -- `subagentActivity`, `subagentToolResults` - Clear to remove stale agent data -- `pendingHooks`, `tabAlerts`, `_shownCompletions` - Clear state tracking Sets - -**Interval Management**: Always provide a stop method for intervals: -```javascript -startSystemStatsPolling() { - this.stopSystemStatsPolling(); // Clear existing before starting - this.systemStatsInterval = setInterval(...); -} -stopSystemStatsPolling() { - if (this.systemStatsInterval) { - clearInterval(this.systemStatsInterval); - this.systemStatsInterval = null; - } -} -``` - -**Session Cleanup**: When a session is deleted, clean up ALL associated resources: -- Respawn state (`respawnStatus`, `respawnTimers`, `respawnCountdownTimers`, `respawnActionLogs`) -- Subagent data (windows, activity, tool results) -- Timers (idle timers, pending hooks) - -**Subagent Data**: Clean up activity/toolResults for completed agents after 5 minutes to prevent unbounded growth during long sessions. - -### Pre-compiled Regex Patterns - -For performance, regex patterns that are used frequently should be compiled once at module level: - -```typescript -// Good - compile once -const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g; -const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/; - -// Bad - recompiles on each call -function parse(line: string) { - return line.replace(/\x1b\[[0-9;]*m/g, ''); -} -``` - -### Claude Message Parsing - -Claude CLI outputs newline-delimited JSON. Strip ANSI codes before parsing: - -```typescript -const cleanLine = line.replace(ANSI_ESCAPE_PATTERN, ''); -const msg = JSON.parse(cleanLine) as ClaudeMessage; -// msg.type: 'system' | 'assistant' | 'user' | 'result' -// msg.message?.content: Array<{ type: 'text', text: string }> -// msg.total_cost_usd: number (on result messages) -``` - -### PTY Spawn Modes - -All PTY spawns pass `PATH: getAugmentedPath()` in the env to ensure `claude` is discoverable even when the server runs in a restricted environment (e.g., systemd). The `getAugmentedPath()` function (in `session.ts`) resolves the claude binary's directory once at startup and prepends it to PATH. The `screen-manager.ts` equivalent (`findClaudeDir()`) does the same for screen-based spawns via `export PATH=":$PATH"` in the bash command. - -```typescript -// One-shot mode (JSON output for token tracking) -pty.spawn('claude', ['-p', '--dangerously-skip-permissions', '--output-format', 'stream-json', prompt], { - env: { ...process.env, PATH: getAugmentedPath(), ... } -}) - -// Interactive mode (tokens parsed from status line) -pty.spawn('claude', ['--dangerously-skip-permissions'], { - env: { ...process.env, PATH: getAugmentedPath(), ... } -}) - -// Shell mode (debugging/testing - no Claude CLI) -pty.spawn('bash', [], { ... }) -``` - -**PATH resolution search order** (both `session.ts` and `screen-manager.ts`): -1. `which claude` (respects current PATH) -2. `~/.local/bin/claude` -3. `~/.claude/local/claude` -4. `/usr/local/bin/claude` -5. `~/.npm-global/bin/claude` -6. `~/bin/claude` - -### Sending Input to Sessions - -Two methods: -1. **`session.write(data)`** - Direct PTY write (used by `/api/sessions/:id/input` endpoint) -2. **`session.writeViaScreen(data)`** - Via GNU screen (RECOMMENDED for programmatic input). Used by RespawnController, auto-compact, auto-clear. - -**How `writeViaScreen` works internally** (in `screen-manager.ts:sendInput`): -1. Strips all `\n` newlines and `\r` carriage returns from text -2. Sends text first: `screen -S name -p 0 -X stuff "text"` -3. Sends Enter separately: `screen -S name -p 0 -X stuff "$(printf '\015')"` - -**Why separate commands?** Claude CLI uses Ink (React for terminals) which requires text and Enter as separate `screen -X stuff` commands. Combining them doesn't work. This is a critical implementation detail when debugging input issues. - -**IMPORTANT**: All prompts sent via `writeViaScreen` (respawn updatePrompt, kickstartPrompt, auto-compact, etc.) must be **single-line**. Newlines are stripped, so multi-line prompts become one long line. - -### Idle Detection - -**Session**: emits `idle`/`working` events on prompt detection + 2s activity timeout. - -**RespawnController**: Multi-layer detection (completion message → AI idle check → output silence → token stability → working pattern absence). See `docs/respawn-state-machine.md` for full details. - -### Token Tracking - -- **One-shot mode**: Uses `--output-format stream-json` for detailed token usage from JSON -- **Interactive mode**: Parses tokens from Claude's status line (e.g., "123.4k tokens"), estimates 60/40 input/output split - -### Auto-Compact & Auto-Clear - -| Feature | Default Threshold | Action | -|---------|------------------|--------| -| Auto-Compact | 110k tokens | `/compact` with optional prompt | -| Auto-Clear | 140k tokens | `/clear` to reset context | - -Both wait for idle. Configure via `session.setAutoCompact()` / `session.setAutoClear()`. - -### Ralph / Todo Tracking - -Detects Ralph loops and todos inside Claude sessions. **Disabled by default** but auto-enables when Ralph-related patterns are detected (promise tags, TodoWrite, iteration patterns, etc.). See `ralph-tracker.ts:shouldAutoEnable()` for the full pattern list. - -**Auto-Loading @fix_plan.md**: When a session starts, the Ralph tracker automatically: -1. Loads existing `@fix_plan.md` from the working directory (if present) -2. Imports todos and shows them in the Ralph panel -3. Watches the file for changes and reloads when modified by Claude -4. Auto-enables the tracker if todos are found - -This means you can create a session in a folder with an existing `@fix_plan.md` and the tracker will immediately show and track those tasks. - -**Auto-Configuration from Ralph Plugin State**: When a session starts, Claudeman reads `.claude/ralph-loop.local.md` to auto-configure: - -```yaml ---- -enabled: true -iteration: 5 -max-iterations: 50 -completion-promise: "COMPLETE" ---- -``` - -Priority: 1) `.claude/ralph-loop.local.md` (official Ralph plugin state), 2) `CLAUDE.md` `` tags (fallback). See `src/ralph-config.ts`. - -**Completion Detection** (multi-strategy): -- 1st occurrence of `PHRASE`: Stores as expected phrase (likely in prompt) -- 2nd occurrence: Emits `completionDetected` event (actual completion) -- **Bare phrase detection**: Also detects phrase without tags once expected phrase is known -- **All complete detection**: When "All X files/tasks created/completed" detected, marks all todos complete and emits completion -- If loop is already active (via `/ralph-loop:ralph-loop`): Emits immediately on first occurrence - -**Session Lifecycle**: Each session has its own independent tracker: -- New session → Fresh tracker (no carryover) -- Close tab → Tracker state cleared, UI panel hides -- `tracker.reset()` → Clears todos/state, keeps enabled status -- `tracker.fullReset()` → Complete reset to initial state -- `tracker.configure({ enabled?, completionPhrase?, maxIterations? })` → Partial config update - -**API**: -- `GET /api/sessions/:id/ralph-state` - Get loop state and todos -- `POST /api/sessions/:id/ralph-config` - Configure tracker: - - `{ enabled: boolean }` - Enable/disable - - `{ reset: true }` - Soft reset (keep enabled) - - `{ reset: "full" }` - Full reset - -### Terminal Display Fix - -Tab switch/new session fix: clear xterm → write buffer → resize PTY → Ctrl+L redraw. Uses `pendingCtrlL` Set, triggered on `session:idle`/`session:working` events. - -### SSE Events - -All events broadcast to `/api/events` with format: `{ type: string, sessionId?: string, data: any }`. - -Event prefixes: `session:`, `task:`, `respawn:`, `spawn:`, `subagent:`, `hook:`, `scheduled:`, `case:`, `screen:`, `init`. - -Key events (see `app.js:handleSSEEvent()`): -- `session:idle`, `session:working` - Status indicators -- `session:terminal`, `session:clearTerminal` - Terminal content -- `session:ralphLoopUpdate`, `session:ralphTodoUpdate`, `session:ralphCompletionDetected` - Ralph tracking -- `respawn:detectionUpdate` - Idle detection status -- `spawn:queued`, `spawn:started`, `spawn:completed`, `spawn:failed` - Agent lifecycle -- `subagent:discovered`, `subagent:tool_call`, `subagent:progress`, `subagent:message`, `subagent:completed` - Claude Code background agents -- `hook:idle_prompt`, `hook:permission_prompt`, `hook:elicitation_dialog`, `hook:stop` - Claude Code hooks - -### Run Summary - -Per-session event tracking for "what happened while you were away" view. Click the chart icon (📊) on any session tab to view. - -**Tracked Events**: session start/stop, respawn cycles, state changes, idle/working transitions, token milestones (every 50k), auto-compact/clear, Ralph completions, AI check results, hook events, errors/warnings, state stuck warnings (>10min same state). - -**Stats**: respawn cycles, peak tokens, active time, idle time, error/warning counts. - -**Storage**: In-memory only (not persisted). Fresh tracker created per session; cleared when session is deleted. - -**Implementation**: `RunSummaryTracker` class in `src/run-summary.ts`, integrated via `setupSessionListeners()` and `setupRespawnListeners()` in `server.ts`. - -### Frontend (app.js) - -Vanilla JS + xterm.js. 60fps rendering: server batches terminal data every 16ms, client uses `requestAnimationFrame` to batch xterm.js writes. - -### HTTPS & Browser Notifications - -**HTTPS**: The `--https` flag generates/reuses self-signed certificates in `~/.claudeman/certs/`. Required for the Web Notification API. - -**Notifications** (`NotificationManager` in `app.js`): In-app drawer, tab title flashing, Web Notification API (rate limited 3s), audio alerts (critical only), tab blinking (red=action, yellow=idle). - -**Hook Event Data**: `/api/hook-event` forwards `data` field into SSE broadcast. Hook events: `permission_prompt`, `elicitation_dialog`, `idle_prompt`, `stop`. - -### State Store - -Writes debounced (500ms) to `~/.claudeman/state.json` via `persistSessionState()` on every meaningful change. - -**Per-session fields stored** (`SessionState` in `types.ts`): -- `id`, `pid`, `status`, `name`, `mode` - Core identity -- `workingDir`, `createdAt`, `lastActivityAt` - Location and timestamps -- `autoClearEnabled/Threshold`, `autoCompactEnabled/Threshold/Prompt` - Context management -- `ralphEnabled`, `ralphCompletionPhrase` - Ralph tracker state -- `respawnEnabled`, `respawnConfig` - Respawn controller state -- `totalCost`, `inputTokens`, `outputTokens` - Token tracking -- `parentAgentId`, `childAgentIds` - Spawn agent tree - -CLI commands (`claudeman status/session list`) read from `state.json` to display web-managed sessions. - -### TypeScript Config - -Module resolution: NodeNext. Target: ES2022. Strict mode enabled with all additional strictness flags (`noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`, etc.). No ESLint/Prettier configured - rely on TypeScript strict mode. - -TUI uses React JSX (`jsx: react-jsx`, `jsxImportSource: react`) for Ink components. - -## Adding New Features - -- **API endpoint**: Add types in `types.ts`, route in `server.ts:buildServer()`, use `createErrorResponse()` for errors -- **SSE event**: Emit via `broadcast()` in server.ts, handle in `app.js:handleSSEEvent()` switch -- **Session event**: Add to `SessionEvents` interface in `session.ts`, emit via `this.emit()`, subscribe in server.ts, handle in frontend -- **Session setting**: Add field to `SessionState` in `types.ts`, include in `session.toState()`, call `this.persistSessionState(session)` in server.ts after the change -- **MCP tool**: Add tool definition in `mcp-server.ts` using `server.tool()`, use `apiRequest()` to call Claudeman REST API -- **New test file**: Create `test/.test.ts`, pick unique port (next available: 3190), add to port allocation table above - -### API Error Codes - -Use `createErrorResponse(code, details?)` from `types.ts`: - -| Code | Use Case | -|------|----------| -| `NOT_FOUND` | Session/resource doesn't exist | -| `INVALID_INPUT` | Bad request parameters | -| `SESSION_BUSY` | Session is currently processing | -| `OPERATION_FAILED` | Action couldn't complete | -| `ALREADY_EXISTS` | Duplicate resource | -| `INTERNAL_ERROR` | Unexpected server error | - -## Session Lifecycle & Cleanup - -- **Limit**: Web server: `MAX_CONCURRENT_SESSIONS = 50` (`server.ts:56`), UI tab limit: 20, CLI default: 5 (`types.ts:DEFAULT_CONFIG`) -- **Kill** (`killScreen()`): child PIDs → process group → screen quit → SIGKILL -- **Ghost discovery**: `reconcileScreens()` finds orphaned screens on startup -- **Cleanup** (`cleanupSession()`): stops respawn, clears buffers/timers, kills screen, removes from `state.json` -- **State sync**: Every session create/delete/update calls `persistSessionState()` which writes full state (including respawn config from controller) to `state.json` -- **Recovery on restart**: Server reads `state.json` first (has all settings), falls back to `screens.json` for any sessions not found. Settings (auto-compact, auto-clear, respawn config, Ralph state) restored to live session objects after screen reattachment. - -## TUI (WIP) - -Ink/React-based TUI in `src/tui/`. Client to the web server, uses `/api/*` endpoints and attaches to screens via GNU screen. - -**Key files**: `App.tsx` (main component), `DirectAttach.ts` (full-screen attach with tab switching), `SessionList.tsx`, `SessionView.tsx`. - -**Current state**: Basic session list and direct attach work. Missing: full session management UI, settings panel, notifications. - -## Buffer Limits - -| Buffer | Max Size | Trim To | -|--------|----------|---------| -| Terminal | 2MB | 1.5MB | -| Text output | 1MB | 768KB | -| Messages | 1000 | 800 | -| Line buffer | 64KB | (flushed every 100ms) | -| Respawn buffer | 1MB | 512KB | - -Tab switch uses `tail=256KB` for fast initial load, then chunked 64KB writes via `requestAnimationFrame`. - -## API Routes - -All routes defined in `server.ts:buildServer()`. Key endpoint groups: -- `/api/events` - SSE stream | `/api/status` - Full app state -- `/api/sessions` - CRUD + `/input`, `/resize`, `/interactive` -- `/api/sessions/:id/respawn/*` - Start/stop/enable/config respawn controller -- `/api/sessions/:id/ralph-*` - Ralph tracker config and state -- `/api/sessions/:id/auto-compact`, `/auto-clear` - Token threshold settings -- `/api/quick-start` - Create case + start session (`{mode?: 'claude'|'shell'}`) -- `/api/cases`, `/api/screens` - Case and screen management -- `/api/spawn/*` - Agent lifecycle (list, status, result, messages, cancel, trigger) -- `/api/subagents` - List/get/kill Claude Code background agents, get transcripts -- `/api/sessions/:id/subagents` - Get subagents for a specific session's working directory -- `/api/sessions/:id/run-summary` - Get run summary (events, stats) for "what happened while away" -- `/api/hook-event` - Claude Code hook callbacks (`{event, sessionId, data?}`) +## Adding Features +- **API endpoint**: Types in `types.ts`, route in `server.ts:buildServer()`, use `createErrorResponse()` +- **SSE event**: Emit via `broadcast()`, handle in `app.js:handleSSEEvent()` +- **Session setting**: Add to `SessionState` in `types.ts`, include in `session.toState()`, call `persistSessionState()` +- **New test**: Pick unique port (see below), add port comment to test file header ## State Files | File | Purpose | |------|---------| -| `~/.claudeman/state.json` | Full session state (all settings, tokens, respawn config, Ralph state), tasks, app config | -| `~/.claudeman/state-inner.json` | Ralph loop/todo state per session (separate to reduce writes) | -| `~/.claudeman/screens.json` | Screen session metadata (for recovery after restart) | -| `~/.claudeman/settings.json` | User preferences (lastUsedCase, custom template path, subagentTrackingEnabled) | -| `~/.claudeman/certs/` | Self-signed TLS certificates for `--https` mode | +| `~/.claudeman/state.json` | Sessions, settings, tokens, respawn config | +| `~/.claudeman/screens.json` | Screen metadata for recovery | +| `~/.claudeman/settings.json` | User preferences | -**Recovery**: On restart, sessions restored from `state.json` (primary) with `screens.json` as fallback. All settings re-applied to live sessions. Cases created in `~/claudeman-cases/` by default. +## Testing -### CLAUDE.md Templates +**Port allocation**: E2E tests use centralized ports in `test/e2e/e2e.config.ts` (E2E_PORTS: 3183-3190). Unit/integration tests pick unique ports manually (up to 3157). Search `const PORT =` or `TEST_PORT` in test files to see used ports. **Next available: 3191** -New cases get a CLAUDE.md generated from `src/templates/case-template.md` (bundled with the project). Template resolution order: -1. Custom path from `~/.claudeman/settings.json` (`defaultClaudeMdPath` field) -2. Bundled `case-template.md` (copied to `dist/templates/` during build) -3. Minimal fallback (if bundled template is missing) +**E2E tests**: Use Playwright. Run `npx playwright install chromium` first. See `test/e2e/fixtures/` for helpers. E2E config provides ports, timeouts, and helpers. -Placeholders replaced: -- `[PROJECT_NAME]` → Case name -- `[PROJECT_DESCRIPTION]` → Description -- `[DATE]` → Current date (YYYY-MM-DD) +**Test config**: Vitest runs with `globals: true` (no imports needed for `describe`/`it`/`expect`) and `fileParallelism: false` (files run sequentially to respect screen limits). -## Screen Session Manager (CLI Tool) +**Test safety**: `test/setup.ts` provides: +- Screen concurrency limiter (max 10) +- Pre-existing screen protection (never kills screens present before tests) +- Tracked resource cleanup (only kills screens/processes tests register) +- Safe to run from within Claudeman-managed sessions -`./scripts/screen-manager.sh` - Interactive bash tool for managing screen sessions. Commands: `list`, `attach N`, `kill N,M`, `kill-all`, `info N`. Requires `jq` and `screen`. +Respawn tests use MockSession to avoid spawning real Claude processes. -## Web UI Keyboard Shortcuts +## Debugging -| Shortcut | Action | -|----------|--------| -| `Ctrl+Enter` | Quick-start session | -| `Ctrl+W` | Close session | -| `Ctrl+Tab` | Next session | -| `Ctrl+K` | Kill all sessions | -| `Ctrl+L` | Clear terminal | - -## Documentation - -**Reference docs** (read these for deep dives): -- `docs/respawn-state-machine.md` - Respawn controller states, idle detection, auto-accept -- `docs/spawn-protocol.md` - Spawn1337 agent protocol, MCP tools, resource governance -- `docs/ralph-wiggum-guide.md` - Ralph Wiggum loop guide (plugin reference, prompt templates) -- `docs/claude-code-hooks-reference.md` - Claude Code hooks documentation -- `docs/browser-testing-guide.md` - Browser testing frameworks comparison, patterns, and known issues - -**Internal planning** (historical context, may be outdated): -- `docs/respawn-improvement-plan.md` - Planned respawn improvements -- `docs/run-summary-plan.md` - Run summary feature design - -### Ralph Wiggum Loops - -**Core Pattern**: `PHRASE` - The completion signal that tells the loop to stop. - -**Skill Commands**: ```bash -/ralph-loop:ralph-loop # Start Ralph Loop in current session -/ralph-loop:cancel-ralph # Cancel active Ralph Loop -/ralph-loop:help # Show help and usage +screen -ls # List screens +screen -r # Attach (Ctrl+A D to detach) +curl localhost:3000/api/sessions # Check sessions +curl localhost:3000/api/status | jq # Full app state +cat ~/.claudeman/state.json | jq # View persisted state ``` -The `RalphTracker` class (`src/ralph-tracker.ts`) detects Ralph patterns in Claude output and tracks loop state, todos, and completion phrases. It auto-enables when Ralph-related patterns are detected. +## Performance Constraints -**Ralph Loop Wizard**: The web UI wizard (`app.js:startRalphLoop()`) provides a guided setup for Ralph Loops. It: -1. Creates/selects a case and starts a session -2. Configures the Ralph tracker with completion phrase and max iterations -3. Optionally generates a task plan (`@fix_plan.md`) -4. Sends the initial prompt with iteration protocol +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 batching (16ms) -**Plan Generation Modes** (Step 2 of wizard): -| Mode | Description | API Endpoint | -|------|-------------|--------------| -| **Brief** | High-level milestones only | `/api/generate-plan` | -| **Standard** | Balanced implementation steps | `/api/generate-plan` | -| **Enhanced** | Multi-agent orchestration with verification | `/api/generate-plan-detailed` | +## Buffer Limits -**Enhanced Plan Generation** (`src/plan-orchestrator.ts`): When "Enhanced" mode is selected, the plan is generated using parallel subagent orchestration: -1. **Phase 1 - Parallel Analysis**: Spawns 4 specialist subagents simultaneously: - - Requirements Analyst → Extracts explicit/implicit requirements - - Architecture Planner → Identifies modules, interfaces, types - - TDD Specialist → Designs test-first approach, edge cases - - Risk Analyst → Identifies failure points, dependencies, blockers -2. **Phase 2 - Synthesis**: Merges outputs, deduplicates, orders by dependency -3. **Phase 3 - Verification**: Review subagent validates plan, assigns P0/P1/P2 priorities, identifies gaps +| Buffer | Max | Trim To | +|--------|-----|---------| +| Terminal | 2MB | 1.5MB | +| Text output | 1MB | 768KB | +| Messages | 1000 | 800 | -The enhanced mode takes longer (~60-90s) but produces more thorough plans with quality scores. Switching between modes auto-regenerates the plan. +## Where to Find More Information -**Respawn for Ralph Loops**: Disabled by default (checkbox unchecked). When enabled, the respawn controller uses Ralph-specific prompts: -- **Update Prompt**: Instructs Claude to document progress to CLAUDE.md, update planning files (`@fix_plan.md`), mark completed tasks, and write a summary before `/clear` -- **Kickstart Prompt**: After `/init`, tells Claude it's in a Ralph Wiggum loop and to continue work by reading `@fix_plan.md` and CLAUDE.md notes, then resume on uncompleted tasks +| Topic | Location | +|-------|----------| +| **Respawn state machine** | `docs/respawn-state-machine.md` | +| **Spawn agent protocol** | `docs/spawn-protocol.md` | +| **Ralph Loop guide** | `docs/ralph-wiggum-guide.md` | +| **Claude Code hooks** | `docs/claude-code-hooks-reference.md` | +| **Browser/E2E testing** | `docs/browser-testing-guide.md` | +| **API routes** | `src/web/server.ts:buildServer()` or README.md | +| **SSE events** | Search `broadcast(` in `server.ts` | +| **CLI commands** | `claudeman --help` | +| **Frontend patterns** | `src/web/public/app.js` (subagent windows, notifications) | +| **Session modes** | `SessionMode` type in `src/types.ts` | +| **Error codes** | `createErrorResponse()` in `src/types.ts` | +| **Test fixtures** | `test/e2e/fixtures/` | +| **Test utilities** | `test/respawn-test-utils.ts` | +| **Keyboard shortcuts** | README.md or App Settings in web UI | diff --git a/docs/plan-improvement-roadmap.md b/docs/plan-improvement-roadmap.md new file mode 100644 index 00000000..56d0bcca --- /dev/null +++ b/docs/plan-improvement-roadmap.md @@ -0,0 +1,247 @@ +# Ralph Loop Plan Improvement Roadmap + +> Research-backed improvements for rock-solid AI planning with auto-improvement capabilities. + +**Created**: 2026-01-27 +**Status**: Implementation in Progress + +--- + +## Table of Contents + +1. [Research Summary](#research-summary) +2. [Current State Analysis](#current-state-analysis) +3. [Proposed Improvements](#proposed-improvements) +4. [Implementation Plan](#implementation-plan) +5. [Sources](#sources) + +--- + +## Research Summary + +### Key Insights from Industry Best Practices + +#### 1. Self-Verification is Critical +> "Claude performs dramatically better when it can verify its own work, like run tests, compare screenshots, and validate outputs. Without clear success criteria, it might produce something that looks right but actually doesn't work." +> — [Anthropic Best Practices](https://www.anthropic.com/engineering/claude-code-best-practices) + +#### 2. Iterative Refinement Patterns (AWS) +> "A generator agent produces output, an evaluator agent reviews using evaluation rubric, and based on feedback, an optimizer agent revises the output. Loop repeats until criteria met." +> — [AWS Agentic AI Patterns](https://docs.aws.amazon.com/prescriptive-guidance/latest/agentic-ai-patterns/evaluator-reflect-refine-loop-patterns.html) + +#### 3. Dynamic Task Decomposition (TDAG Framework) +> "Dynamically decomposes complex tasks into smaller subtasks and assigns each to a specifically generated subagent, enhancing adaptability in diverse and unpredictable real-world tasks." +> — [TDAG Framework - arXiv](https://arxiv.org/abs/2402.10178) + +#### 4. Multi-Stage Verification Workflow +> "o3: Generate plan → Sonnet: Verify and create task list → Sonnet: Execute → Sonnet: Verify against plan → o3: Final verification → Issues bake back into plan" +> — [Claude Code Best Practices Community](https://rosmur.github.io/claudecode-best-practices/) + +#### 5. Self-Improving Agents +> "Through an iterative refinement process (analyze outcome → adjust approach → try again), the agent becomes more adept at handling tasks over time. It effectively builds a growing knowledge base of what strategies work best." +> — [Self-Improving Data Agents](https://powerdrill.ai/blog/self-improving-data-agents) + +#### 6. Memory Architecture for Planning +> "Agents use three memory layers: working memory for short-lived calculations, episodic memory for step-by-step histories, and semantic memory for long-term knowledge." +> — [LLM Agent Research](https://www.promptingguide.ai/research/llm-agents) + +--- + +## Current State Analysis + +### What We Have + +The current plan generation system (`/api/generate-plan` and `/api/generate-plan-detailed`): + +1. **Standard Mode**: Single Opus 4.5 call with TDD-focused prompt +2. **Enhanced Mode**: 4 parallel subagents (Requirements, Architecture, Testing, Risks) + Verification + +### Current Plan Item Structure + +```json +{ + "content": "Implement login endpoint", + "priority": "P0" +} +``` + +### Limitations + +| Issue | Impact | +|-------|--------| +| No verification criteria | Can't automatically validate completion | +| No test pairing | TDD not enforced structurally | +| Static plans | No adaptation during execution | +| No dependencies | Can't track blocking relationships | +| No failure tracking | Same errors repeat | +| No checkpoints | Plans run until completion or failure | + +--- + +## Proposed Improvements + +### Enhanced Plan Item Structure + +```typescript +interface EnhancedPlanItem { + id: string; // Unique identifier (e.g., "P0-001") + content: string; // Task description + priority: 'P0' | 'P1' | 'P2'; // Criticality + phase: 'setup' | 'test' | 'impl' | 'verify'; // Development phase + + // NEW: Verification + verificationCriteria: string; // How to know it's done + testCommand?: string; // Command to run for verification + + // NEW: Dependencies + dependencies: string[]; // IDs of tasks that must complete first + blockedBy?: string[]; // Runtime: tasks blocking this one + + // NEW: Execution tracking + status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked'; + attempts: number; // How many times attempted + lastError?: string; // Most recent failure reason + completedAt?: number; // Timestamp of completion + + // NEW: Metadata + estimatedComplexity: 'low' | 'medium' | 'high'; + rollbackStrategy?: string; // How to undo if needed + version: number; // Plan version this belongs to +} +``` + +### Runtime Plan Adaptation Flow + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ RUNTIME PLAN LOOP │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Execute │──▶│ Verify │──▶│ Success? │──▶│ Mark │ │ +│ │ Task │ │ Output │ │ │ │ Complete │ │ +│ └──────────┘ └──────────┘ └────┬─────┘ └──────────┘ │ +│ │ No │ +│ ▼ │ +│ ┌──────────┐ │ +│ │ Analyze │ │ +│ │ Failure │ │ +│ └────┬─────┘ │ +│ │ │ +│ ┌──────────────┼──────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Retry │ │ Add Fix │ │ Escalate │ │ +│ │ (< 3x) │ │ Sub-Task │ │ BLOCKED │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +### Checkpoint Review System + +At iterations 5, 10, 20, 30, 50: +1. Pause execution +2. Summarize progress (completed/failed/pending) +3. Identify stuck items (3+ failures) +4. Generate alternative approaches for stuck items +5. Update plan with new strategies +6. Continue with refined plan + +--- + +## Implementation Plan + +### Phase 1: Quick Wins (Implementing Now) + +#### 1.1 Add Verification Criteria to Plan Items +- Modify plan generation prompts to require `verificationCriteria` +- Update `PlanItem` interface in `types.ts` +- Update plan orchestrator prompts + +#### 1.2 Pair Test/Implementation Steps +- Ensure every implementation step has a corresponding test step +- Group items: test → implement → verify +- Add phase field to track TDD cycle + +#### 1.3 Checkpoint Review Prompts +- Add checkpoint logic to Ralph tracker +- At iterations 5, 10, 20: inject review prompt +- Generate progress summary and stuck item analysis + +### Phase 2: Medium Effort (Implementing Now) + +#### 2.1 Failure Tracking +- Track `attempts` and `lastError` per task +- After 3 failures, auto-generate debug sub-task +- Record failure patterns in plan history + +#### 2.2 Plan Versioning +- Add `version` field to plans +- Keep history in `@fix_plan.md` with version markers +- Allow rollback to previous versions +- Track which version each task belongs to + +#### 2.3 Dependency Tracking +- Add `dependencies` field to plan items +- Validate dependency graph (no cycles) +- Block tasks until dependencies complete +- Show dependency status in UI + +### Phase 3: Future Enhancements + +#### 3.1 Full Runtime Adaptation +- TDAG-style dynamic decomposition +- Auto-generate sub-tasks for complex items +- Learning from failure patterns + +#### 3.2 Multi-Model Verification +- Haiku: Fast initial generation +- Sonnet: Verification and refinement +- Opus: Final quality check + +#### 3.3 Plan Memory System +- Episodic memory: What worked/failed in this session +- Semantic memory: Patterns across projects +- Use for future plan generation + +--- + +## File Changes Required + +### New/Modified Files + +| File | Changes | +|------|---------| +| `src/types.ts` | Add `EnhancedPlanItem` interface | +| `src/plan-orchestrator.ts` | Update prompts, add versioning | +| `src/ralph-tracker.ts` | Add checkpoint logic, failure tracking | +| `src/web/server.ts` | New endpoints for plan updates | +| `src/web/public/app.js` | UI for enhanced plan display | + +### New Endpoints + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| PATCH | `/api/sessions/:id/plan/task/:taskId` | Update task status | +| POST | `/api/sessions/:id/plan/checkpoint` | Trigger checkpoint review | +| GET | `/api/sessions/:id/plan/history` | Get plan version history | +| POST | `/api/sessions/:id/plan/rollback/:version` | Rollback to version | + +--- + +## Sources + +- [Anthropic Claude Code Best Practices](https://www.anthropic.com/engineering/claude-code-best-practices) +- [AWS Agentic AI Patterns](https://docs.aws.amazon.com/prescriptive-guidance/latest/agentic-ai-patterns/evaluator-reflect-refine-loop-patterns.html) +- [TDAG: Multi-Agent Task Decomposition Framework](https://arxiv.org/abs/2402.10178) +- [Self-Improving Data Agents](https://powerdrill.ai/blog/self-improving-data-agents) +- [OpenAI Self-Evolving Agents Cookbook](https://cookbook.openai.com/examples/partners/self_evolving_agents/autonomous_agent_retraining) +- [Task Decomposition for Coding Agents](https://mgx.dev/insights/task-decomposition-for-coding-agents-architectures-advancements-and-future-directions/) +- [Claude Code Best Practices Community Guide](https://rosmur.github.io/claudecode-best-practices/) +- [LLM Agents Prompt Engineering Guide](https://www.promptingguide.ai/research/llm-agents) +- [Agentic AI Implementation Guide](https://www.sketchdev.io/blog/agentic-ai-implementation-guide) + +--- + +*This document is part of the Claudeman project. See [CLAUDE.md](../CLAUDE.md) for main documentation.* diff --git a/mobile-app-plan.json b/mobile-app-plan.json new file mode 100644 index 00000000..762c870f --- /dev/null +++ b/mobile-app-plan.json @@ -0,0 +1 @@ +{"success":true,"data":{"items":[{"content":"Cross-platform development framework that enables Linux-based testing","priority":"P1","source":"requirements","rationale":"Explicitly stated - development on Linux with proper testing capability is 'very important'","phase":2,"id":"task-0","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Development and testing must be possible entirely on Linux without macOS/Xcode","priority":"P1","source":"requirements","rationale":"Explicitly stated constraint - suggests React Native or Flutter over native Swift","phase":2,"id":"task-1","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"React Native or Flutter is acceptable for cross-platform development given Linux testing requirement","priority":"P1","source":"requirements","rationale":"Native iOS development requires macOS - needs validation that cross-platform approach is acceptable","phase":2,"id":"task-2","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Comprehensive test coverage with unit and integration tests runnable on Linux","priority":"P1","source":"requirements","rationale":"Explicitly emphasized that detailed feature testing is 'very important'","phase":2,"id":"task-3","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for API client connection to Claudeman server with valid host/port","priority":"P1","source":"testing","rationale":"Core functionality - app must establish connections to remote Claudeman instances","phase":2,"id":"task-4","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for SSE event stream parsing from /api/events endpoint","priority":"P1","source":"testing","rationale":"Real-time updates are critical - Claudeman uses SSE for terminal output, session state, and agent activity","phase":2,"id":"task-5","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for session list rendering with multiple sessions in different states","priority":"P1","source":"testing","rationale":"Users need to see all sessions at a glance with clear status indicators (idle, busy, error)","phase":2,"id":"task-6","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for terminal view rendering ANSI escape sequences correctly","priority":"P1","source":"testing","rationale":"Terminal output must display colors and formatting - Claudeman streams raw PTY output with ANSI codes","phase":2,"id":"task-7","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for server configuration persistence in secure storage","priority":"P1","source":"testing","rationale":"Users configure multiple servers - credentials must persist securely between app launches","phase":2,"id":"task-8","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for full session lifecycle: create -> send input -> receive output -> delete","priority":"P1","source":"testing","rationale":"End-to-end flow validation ensures all API endpoints work together correctly","phase":2,"id":"task-9","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for real-time session state synchronization via SSE","priority":"P1","source":"testing","rationale":"Mobile app must stay in sync with server state - sessions can be modified from web UI or other clients","phase":2,"id":"task-10","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for respawn controller start/stop from mobile app","priority":"P1","source":"testing","rationale":"Respawn is a key Claudeman feature - users need to enable autonomous loops from mobile","phase":2,"id":"task-11","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for subagent window display when Claude spawns Task agents","priority":"P1","source":"testing","rationale":"Claudeman tracks spawned subagents - mobile UI should show agent hierarchy and progress","phase":2,"id":"task-12","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for handling server disconnection during active session","priority":"P1","source":"testing","rationale":"Network reliability varies on mobile - app must gracefully handle dropped connections","phase":2,"id":"task-13","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for handling large terminal buffer (2MB+) without memory issues","priority":"P1","source":"testing","rationale":"Claudeman caps terminal buffers at 2MB - mobile app must handle this without crashing","phase":2,"id":"task-14","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for switching between multiple connected servers","priority":"P1","source":"testing","rationale":"Users may have Claudeman on home server and cloud - seamless switching is expected","phase":2,"id":"task-15","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for handling 20+ simultaneous sessions as per Claudeman design","priority":"P1","source":"testing","rationale":"Claudeman is designed for 20 concurrent sessions - mobile UI must scale","phase":2,"id":"task-16","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for invalid server URL input validation","priority":"P1","source":"testing","rationale":"User input validation prevents confusing errors and improves UX","phase":2,"id":"task-17","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for API error responses (404, 500, network timeout)","priority":"P1","source":"testing","rationale":"Users need clear feedback when operations fail - not cryptic error messages","phase":2,"id":"task-18","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for authentication failure when server requires HTTPS","priority":"P1","source":"testing","rationale":"Claudeman supports HTTPS mode - app should detect and handle TLS requirements","phase":2,"id":"task-19","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for iOS accessibility compliance (VoiceOver support)","priority":"P1","source":"testing","rationale":"iOS apps must be accessible - terminal content needs proper labels for screen readers","phase":2,"id":"task-20","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for dark mode support with proper color contrast","priority":"P1","source":"testing","rationale":"iOS dark mode is expected - terminal colors must remain readable","phase":2,"id":"task-21","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for haptic feedback on key interactions","priority":"P1","source":"testing","rationale":"iOS apps provide tactile feedback - improves perceived quality","phase":2,"id":"task-22","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for Ralph loop status display and todo list rendering","priority":"P1","source":"testing","rationale":"Ralph tracker shows autonomous loop progress - key feature for monitoring long-running sessions","phase":2,"id":"task-23","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for quick-start flow creating case and session in one tap","priority":"P1","source":"testing","rationale":"Quick-start is primary onboarding path - POST /api/quick-start creates case folder and session","phase":2,"id":"task-24","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for push notification when session completes or errors","priority":"P1","source":"testing","rationale":"Mobile users may background the app - notifications for important events improve UX","phase":2,"id":"task-25","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Write test for app backgrounding and foregrounding state restoration","priority":"P1","source":"testing","rationale":"iOS aggressively kills background apps - state must restore seamlessly","phase":2,"id":"task-26","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"Testing on Linux cannot fully validate iOS-specific behaviors like gestures, keyboard interactions, and system integrations","priority":"P1","source":"risks","rationale":"Use Expo Go on physical iOS device for real testing, document iOS-specific features needing manual verification before release","phase":2,"id":"task-27","verificationCriteria":"Tests pass without errors","status":"pending","attempts":0,"version":1},{"content":"iOS mobile application for Claudeman session management","priority":"P1","source":"requirements","rationale":"Explicitly stated as the primary deliverable - iOS first approach","phase":3,"id":"task-28","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Port existing Claudeman web features to the mobile app (session management, terminal view, SSE events, respawn control)","priority":"P1","source":"requirements","rationale":"Explicitly stated to 'keep main claudeman functions' and 'add more features from web'","phase":3,"id":"task-29","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Session lifecycle management (create, attach, detach, kill sessions)","priority":"P1","source":"requirements","rationale":"Implicit - core Claudeman functionality that must be preserved","phase":3,"id":"task-30","verificationCriteria":"Code compiles, no type errors","status":"pending","attempts":0,"version":1},{"content":"Modern, sleek, polished and responsive UI following iOS Human Interface Guidelines","priority":"P1","source":"requirements","rationale":"Explicitly stated UI requirements plus 'best standards for iOS apps'","phase":3,"id":"task-31","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Future support for macOS and Windows Claudeman hosts should be architecturally planned","priority":"P1","source":"requirements","rationale":"Explicitly mentioned as future requirement - architecture should not preclude this","phase":3,"id":"task-32","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"iOS development requires Xcode which only runs on macOS, but development is on Linux","priority":"P1","source":"risks","rationale":"Use React Native or Flutter which support Linux development with iOS simulator alternatives, or use Expo Go for testing on physical iOS devices during development","phase":3,"id":"task-33","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"App Store submission requires Apple Developer account ($99/year) and macOS for final builds","priority":"P1","source":"risks","rationale":"Plan for eventual macOS access for release builds; use CI/CD services like GitHub Actions with macOS runners or services like Codemagic/Bitrise","phase":3,"id":"task-34","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Terminal rendering on small mobile screens may truncate or wrap output incorrectly","priority":"P1","source":"risks","rationale":"Implement pinch-to-zoom, landscape mode, configurable font sizes, and smart text wrapping; test with various terminal output widths","phase":3,"id":"task-35","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"High-frequency SSE updates from active Claude sessions could drain battery rapidly","priority":"P1","source":"risks","rationale":"Implement intelligent batching, reduce update frequency when app is backgrounded, use iOS background task APIs appropriately","phase":3,"id":"task-36","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Rendering multiple concurrent Claude sessions with xterm-like output could overwhelm mobile GPU/memory","priority":"P1","source":"risks","rationale":"Limit concurrent visible sessions, implement virtual scrolling, lazy-load terminal buffers, cap buffer sizes more aggressively than web version","phase":3,"id":"task-37","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"React Native or Flutter terminal emulator libraries may lack full xterm.js feature parity","priority":"P1","source":"risks","rationale":"Evaluate react-native-terminal-view or similar libraries early, may need to use WebView with xterm.js as fallback for complex rendering","phase":3,"id":"task-38","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"iOS keyboard may conflict with terminal input expectations (special keys, escape sequences, Ctrl combinations)","priority":"P1","source":"risks","rationale":"Implement custom toolbar with common terminal shortcuts, support external keyboards, provide configurable key mappings","phase":3,"id":"task-39","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Screen session attachment from mobile could conflict with existing web UI attachments to same session","priority":"P1","source":"risks","rationale":"Implement session locking or multi-viewer mode, clearly show when another client is attached, handle concurrent input gracefully","phase":3,"id":"task-40","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Connect to and manage Claude Code sessions within GNU Screen sessions on remote Linux machines","priority":"P2","source":"requirements","rationale":"Core functionality explicitly requested - must interface with existing Claudeman PTY/screen architecture","phase":4,"id":"task-41","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Support connections to both home-hosted and cloud-hosted Linux servers","priority":"P2","source":"requirements","rationale":"Explicitly stated requirement for flexible deployment scenarios","phase":4,"id":"task-42","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Real-time terminal output streaming via SSE or WebSocket connection","priority":"P2","source":"requirements","rationale":"Implicit - required to replicate xterm.js functionality from web UI on mobile","phase":4,"id":"task-43","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Secure authentication for remote server connections (SSH keys, certificates, or token-based auth)","priority":"P2","source":"requirements","rationale":"Implicit security requirement - connecting to remote machines requires secure authentication","phase":4,"id":"task-44","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Offline capability for viewing cached session state and reconnection handling","priority":"P2","source":"requirements","rationale":"Implicit - mobile apps need graceful handling of network interruptions","phase":4,"id":"task-45","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Must integrate with existing Claudeman REST API and SSE endpoints","priority":"P2","source":"requirements","rationale":"Implicit - the mobile app is a client to the existing Fastify server infrastructure","phase":4,"id":"task-46","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"SSH/connection credentials stored on mobile device could be compromised if device is lost or jailbroken","priority":"P2","source":"risks","rationale":"Use iOS Keychain for secure storage, implement biometric authentication, consider session tokens with expiry rather than storing raw credentials","phase":4,"id":"task-47","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"WebSocket/SSE connections to remote Claudeman servers expose attack surface for man-in-the-middle attacks","priority":"P2","source":"risks","rationale":"Enforce TLS/HTTPS only, implement certificate pinning, validate server certificates properly","phase":4,"id":"task-48","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Network connectivity loss during active Claude session could cause data loss or corrupted state","priority":"P2","source":"risks","rationale":"Implement robust reconnection logic with exponential backoff, queue pending inputs locally, sync state on reconnect, show clear offline indicators","phase":4,"id":"task-49","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"iOS app suspension/termination when backgrounded will disconnect active sessions","priority":"P2","source":"risks","rationale":"Use iOS background modes for network connections where appropriate, clearly communicate session state to users, implement quick reconnect on foreground","phase":4,"id":"task-50","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Connecting to arbitrary Linux/macOS/Windows hosts means trusting user-provided endpoints","priority":"P2","source":"risks","rationale":"Validate host inputs, warn users about connecting to unknown servers, implement connection timeout limits, avoid storing sensitive data from untrusted hosts","phase":4,"id":"task-51","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1},{"content":"Different Claudeman server versions across connected machines could have API incompatibilities","priority":"P2","source":"risks","rationale":"Implement version negotiation on connect, gracefully handle missing endpoints, display server version to user","phase":4,"id":"task-52","verificationCriteria":"Task completed successfully","status":"pending","attempts":0,"version":1}],"costUsd":0.016,"metadata":{"subagentResults":[{"agentType":"requirements","items":[{"category":"functional","content":"iOS mobile application for Claudeman session management","rationale":"Explicitly stated as the primary deliverable - iOS first approach"},{"category":"functional","content":"Connect to and manage Claude Code sessions within GNU Screen sessions on remote Linux machines","rationale":"Core functionality explicitly requested - must interface with existing Claudeman PTY/screen architecture"},{"category":"functional","content":"Support connections to both home-hosted and cloud-hosted Linux servers","rationale":"Explicitly stated requirement for flexible deployment scenarios"},{"category":"functional","content":"Port existing Claudeman web features to the mobile app (session management, terminal view, SSE events, respawn control)","rationale":"Explicitly stated to 'keep main claudeman functions' and 'add more features from web'"},{"category":"functional","content":"Real-time terminal output streaming via SSE or WebSocket connection","rationale":"Implicit - required to replicate xterm.js functionality from web UI on mobile"},{"category":"functional","content":"Session lifecycle management (create, attach, detach, kill sessions)","rationale":"Implicit - core Claudeman functionality that must be preserved"},{"category":"non-functional","content":"Modern, sleek, polished and responsive UI following iOS Human Interface Guidelines","rationale":"Explicitly stated UI requirements plus 'best standards for iOS apps'"},{"category":"non-functional","content":"Cross-platform development framework that enables Linux-based testing","rationale":"Explicitly stated - development on Linux with proper testing capability is 'very important'"},{"category":"non-functional","content":"Secure authentication for remote server connections (SSH keys, certificates, or token-based auth)","rationale":"Implicit security requirement - connecting to remote machines requires secure authentication"},{"category":"non-functional","content":"Offline capability for viewing cached session state and reconnection handling","rationale":"Implicit - mobile apps need graceful handling of network interruptions"},{"category":"constraint","content":"Development and testing must be possible entirely on Linux without macOS/Xcode","rationale":"Explicitly stated constraint - suggests React Native or Flutter over native Swift"},{"category":"constraint","content":"Must integrate with existing Claudeman REST API and SSE endpoints","rationale":"Implicit - the mobile app is a client to the existing Fastify server infrastructure"},{"category":"assumption","content":"Future support for macOS and Windows Claudeman hosts should be architecturally planned","rationale":"Explicitly mentioned as future requirement - architecture should not preclude this"},{"category":"assumption","content":"React Native or Flutter is acceptable for cross-platform development given Linux testing requirement","rationale":"Native iOS development requires macOS - needs validation that cross-platform approach is acceptable"},{"category":"non-functional","content":"Comprehensive test coverage with unit and integration tests runnable on Linux","rationale":"Explicitly emphasized that detailed feature testing is 'very important'"}],"success":true,"durationMs":19265},{"agentType":"architecture","items":[],"success":false,"error":"I'll analyze the Claudeman codebase to understand its architecture and design a comprehensive iOS app architecture. Let me explore the existing system first.{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"tool_use_id\":\"toolu_01Ch9v9G2HYxwWtNjsbuupQP\",\"type\":\"tool_result\",\"content\":\" 1→/**\\n 2→ * @fileoverview Core PTY session wrapper for Claude CLI interactions.\\n 3→ *\\n 4→ * This module provides the Session class which manages a PTY (pseudo-terminal)\\n 5→ * process running the Claude CLI. It supports three operation modes:\\n 6→ *\\n 7→ * 1. **One-shot mode** (`runPrompt`): Execute a single prompt and get JSON response\\n 8→ * 2. **Interactive mode** (`startInteractive`): Start an interactive Claude session\\n 9→ * 3. **Shell mode**: Run a plain bash shell for debugging/testing\\n 10→ *\\n 11→ * The session can optionally run inside a GNU Screen session for persistence\\n 12→ * across disconnects. It tracks tokens, costs, background tasks, and supports\\n 13→ * auto-clear/auto-compact functionality when token limits are approached.\\n 14→ *\\n 15→ * @module session\\n 16→ */\\n 17→\\n 18→import { EventEmitter } from 'node:events';\\n 19→import { execSync } from 'node:child_process';\\n 20→import { existsSync } from 'node:fs';\\n 21→import { dirname } from 'node:path';\\n 22→import { v4 as uuidv4 } from 'uuid';\\n 23→import * as pty from 'node-pty';\\n 24→import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem, ActiveBashTool, NiceConfig, DEFAULT_NICE_CONFIG } from './types.js';\\n 25→import { TaskTracker, type BackgroundTask } from './task-tracker.js';\\n 26→import { RalphTracker } from './ralph-tracker.js';\\n 27→import { BashToolParser } from './bash-tool-parser.js';\\n 28→import { ScreenManager } from './screen-manager.js';\\n 29→\\n 30→export type { BackgroundTask } from './task-tracker.js';\\n 31→export type { RalphTrackerState, RalphTodoItem, ActiveBashTool } from './types.js';\\n 32→export { withTimeout };\\n 33→\\n 34→// ============================================================================\\n 35→// Buffer Size Constants\\n 36→// ============================================================================\\n 37→\\n 38→/** Maximum terminal buffer size in characters (2MB) - reduced from 5MB for better render performance */\\n 39→const MAX_TERMINAL_BUFFER_SIZE = 2 * 1024 * 1024;\\n 40→\\n 41→/** When trimming terminal buffer, keep the most recent portion (1.5MB) */\\n 42→const TERMINAL_BUFFER_TRIM_SIZE = 1.5 * 1024 * 1024;\\n 43→\\n 44→/** Maximum text output buffer size (1MB) - ANSI-stripped text */\\n 45→const MAX_TEXT_OUTPUT_SIZE = 1 * 1024 * 1024;\\n 46→\\n 47→/** When trimming text output, keep the most recent portion (768KB) */\\n 48→const TEXT_OUTPUT_TRIM_SIZE = 768 * 1024;\\n 49→\\n 50→/** Maximum number of Claude JSON messages to keep in memory */\\n 51→const MAX_MESSAGES = 1000;\\n 52→\\n 53→/** Maximum line buffer size (64KB) - prevents unbounded growth for long lines */\\n 54→const MAX_LINE_BUFFER_SIZE = 64 * 1024;\\n 55→\\n 56→/** Line buffer flush interval (100ms) - forces processing of partial lines */\\n 57→const LINE_BUFFER_FLUSH_INTERVAL = 100;\\n 58→\\n 59→// ============================================================================\\n 60→// Timing Constants\\n 61→// ============================================================================\\n 62→\\n 63→/** Timeout for exec commands like 'which claude' (5 seconds) */\\n 64→const EXEC_TIMEOUT_MS = 5000;\\n 65→\\n 66→/** Delay after screen creation before sending commands (300ms) */\\n 67→const SCREEN_STARTUP_DELAY_MS = 300;\\n 68→\\n 69→/** Delay before declaring session idle after last output (2 seconds) */\\n 70→const IDLE_DETECTION_DELAY_MS = 2000;\\n 71→\\n 72→/** Delay for auto-compact/clear retry attempts (2 seconds) */\\n 73→const AUTO_RETRY_DELAY_MS = 2000;\\n 74→\\n 75→/** Delay for auto-compact/clear initial check (1 second) */\\n 76→const AUTO_INITIAL_DELAY_MS = 1000;\\n 77→\\n 78→/** Graceful shutdown delay when stopping session (100ms) */\\n 79→const GRACEFUL_SHUTDOWN_DELAY_MS = 100;\\n 80→\\n 81→// Filter out terminal focus escape sequences (focus in/out reports)\\n 82→// ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences\\n 83→const FOCUS_ESCAPE_FILTER = /\\\\x1b\\\\[\\\\?1004[hl]|\\\\x1b\\\\[[IO]/g;\\n 84→\\n 85→// Pre-compiled regex patterns for performance (avoid re-compilation on each call)\\n 86→// Comprehensive ANSI escape pattern:\\n 87→// - SGR (colors/styles): ESC [ params m\\n 88→// - CSI sequences (cursor, scroll, etc.): ESC [ params letter\\n 89→// - OSC sequences (title, etc.): ESC ] ... BEL or ESC ] ... ST\\n 90→// - Single-char escapes: ESC = or ESC >\\n 91→const ANSI_ESCAPE_PATTERN = /\\\\x1b(?:\\\\[[0-9;?]*[A-Za-z]|\\\\][^\\\\x07\\\\x1b]*(?:\\\\x07|\\\\x1b\\\\\\\\)|[=>])/g;\\n 92→const TOKEN_PATTERN = /(\\\\d+(?:\\\\.\\\\d+)?)\\\\s*([kKmM])?\\\\s*tokens/;\\n 93→// Pattern to match Task tool invocations in terminal output\\n 94→// Matches: \\\"Explore(Description)\\\", \\\"Task(Description)\\\", \\\"Bash(Description)\\\", etc.\\n 95→// The prefix characters vary (●, ·, ✶, etc.) so we don't require them\\n 96→// We look for the tool name followed by (description)\\n 97→const TASK_TOOL_PATTERN = /\\\\b(Explore|Task|Bash|Plan|general-purpose)\\\\(([^)]+)\\\\)/g;\\n 98→\\n 99→// ============================================================================\\n 100→// Claude CLI PATH Resolution\\n 101→// ============================================================================\\n 102→\\n 103→/** Common directories where the Claude CLI binary may be installed */\\n 104→const CLAUDE_SEARCH_DIRS = [\\n 105→ `${process.env.HOME}/.local/bin`,\\n 106→ `${process.env.HOME}/.claude/local`,\\n 107→ '/usr/local/bin',\\n 108→ `${process.env.HOME}/.npm-global/bin`,\\n 109→ `${process.env.HOME}/bin`,\\n 110→];\\n 111→\\n 112→/** Cached PATH string with claude's directory prepended */\\n 113→let _augmentedPath: string | null = null;\\n 114→\\n 115→/**\\n 116→ * Returns a PATH string that includes the directory containing `claude`.\\n 117→ *\\n 118→ * Finds the claude binary (via `which` or common install locations), then\\n 119→ * prepends its directory to the current PATH if not already present.\\n 120→ * Result is cached for subsequent calls.\\n 121→ */\\n 122→export function getAugmentedPath(): string {\\n 123→ if (_augmentedPath) return _augmentedPath;\\n 124→\\n 125→ const currentPath = process.env.PATH || '';\\n 126→ let claudeDir: string | null = null;\\n 127→\\n 128→ // Try `which` first (respects current PATH)\\n 129→ try {\\n 130→ const result = execSync('which claude', { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }).trim();\\n 131→ if (result && existsSync(result)) {\\n 132→ claudeDir = dirname(result);\\n 133→ }\\n 134→ } catch {\\n 135→ // not in PATH, check common locations\\n 136→ }\\n 137→\\n 138→ // Fallback: check common installation directories\\n 139→ if (!claudeDir) {\\n 140→ for (const dir of CLAUDE_SEARCH_DIRS) {\\n 141→ if (existsSync(`${dir}/claude`)) {\\n 142→ claudeDir = dir;\\n 143→ break;\\n 144→ }\\n 145→ }\\n 146→ }\\n 147→\\n 148→ if (claudeDir && !currentPath.split(':').includes(claudeDir)) {\\n 149→ _augmentedPath = `${claudeDir}:${currentPath}`;\\n 150→ console.log('[Session] Augmented PATH with claude directory:', claudeDir);\\n 151→ } else {\\n 152→ _augmentedPath = currentPath;\\n 153→ }\\n 154→\\n 155→ return _augmentedPath;\\n 156→}\\n 157→\\n 158→// ============================================================================\\n 159→// Buffer Accumulator (reduces GC pressure from string concatenation)\\n 160→// ============================================================================\\n 161→\\n 162→/**\\n 163→ * High-performance buffer accumulator using array-based collection.\\n 164→ *\\n 165→ * Reduces GC pressure by avoiding repeated string concatenation (`+=`).\\n 166→ * Instead, chunks are pushed to an array and joined only when needed.\\n 167→ * Automatically trims when size limits are exceeded.\\n 168→ */\\n 169→class BufferAccumulator {\\n 170→ private chunks: string[] = [];\\n 171→ private totalLength: number = 0;\\n 172→ private readonly maxSize: number;\\n 173→ private readonly trimSize: number;\\n 174→\\n 175→ constructor(maxSize: number, trimSize: number) {\\n 176→ this.maxSize = maxSize;\\n 177→ this.trimSize = trimSize;\\n 178→ }\\n 179→\\n 180→ /** Append data to the buffer */\\n 181→ append(data: string): void {\\n 182→ if (!data) return;\\n 183→ this.chunks.push(data);\\n 184→ this.totalLength += data.length;\\n 185→\\n 186→ // Trim if exceeded max size\\n 187→ if (this.totalLength > this.maxSize) {\\n 188→ this.trim();\\n 189→ }\\n 190→ }\\n 191→\\n 192→ /** Get the full buffer content (joins all chunks) */\\n 193→ get value(): string {\\n 194→ if (this.chunks.length === 0) return '';\\n 195→ if (this.chunks.length === 1) return this.chunks[0];\\n 196→\\n 197→ // Consolidate chunks on access\\n 198→ const result = this.chunks.join('');\\n 199→ this.chunks = [result];\\n 200→ return result;\\n 201→ }\\n 202→\\n 203→ /** Get current buffer length without joining */\\n 204→ get length(): number {\\n 205→ return this.totalLength;\\n 206→ }\\n 207→\\n 208→ /** Clear the buffer */\\n 209→ clear(): void {\\n 210→ this.chunks = [];\\n 211→ this.totalLength = 0;\\n 212→ }\\n 213→\\n 214→ /** Set buffer to a specific value */\\n 215→ set(value: string): void {\\n 216→ this.chunks = value ? [value] : [];\\n 217→ this.totalLength = value?.length || 0;\\n 218→ }\\n 219→\\n 220→ /** Trim buffer to keep only the most recent data */\\n 221→ private trim(): void {\\n 222→ const full = this.chunks.join('');\\n 223→ const trimmed = full.slice(-this.trimSize);\\n 224→ this.chunks = [trimmed];\\n 225→ this.totalLength = trimmed.length;\\n 226→ }\\n 227→}\\n 228→\\n 229→/**\\n 230→ * Wraps a promise with a timeout to prevent indefinite hangs.\\n 231→ * If the promise doesn't resolve within the timeout, rejects with TimeoutError.\\n 232→ *\\n 233→ * @param promise - The promise to wrap\\n 234→ * @param timeoutMs - Timeout in milliseconds\\n 235→ * @param operation - Description of the operation for error messages\\n 236→ * @returns Promise that resolves/rejects with the original result or timeout error\\n 237→ */\\n 238→function withTimeout(\\n 239→ promise: Promise,\\n 240→ timeoutMs: number,\\n 241→ operation: string\\n 242→): Promise {\\n 243→ let timeoutId: NodeJS.Timeout;\\n 244→\\n 245→ const timeoutPromise = new Promise((_, reject) => {\\n 246→ timeoutId = setTimeout(() => {\\n 247→ reject(new Error(`${operation} timed out after ${timeoutMs}ms`));\\n 248→ }, timeoutMs);\\n 249→ });\\n 250→\\n 251→ return Promise.race([promise, timeoutPromise]).finally(() => {\\n 252→ clearTimeout(timeoutId);\\n 253→ });\\n 254→}\\n 255→\\n 256→/**\\n 257→ * Represents a JSON message from Claude CLI's stream-json output format.\\n 258→ * Messages are newline-delimited JSON objects parsed from PTY output.\\n 259→ */\\n 260→export interface ClaudeMessage {\\n 261→ /** Message type indicating the role or purpose */\\n 262→ type: 'system' | 'assistant' | 'user' | 'result';\\n 263→ /** Optional subtype for further classification */\\n 264→ subtype?: string;\\n 265→ /** Claude's internal session identifier */\\n 266→ session_id?: string;\\n 267→ /** Message content with optional token usage */\\n 268→ message?: {\\n 269→ content: Array<{ type: string; text?: string }>;\\n 270→ usage?: {\\n 271→ input_tokens: number;\\n 272→ output_tokens: number;\\n 273→ };\\n 274→ };\\n 275→ /** Final result text (on result messages) */\\n 276→ result?: string;\\n 277→ /** Whether this message represents an error */\\n 278→ is_error?: boolean;\\n 279→ /** Total cost in USD (on result messages) */\\n 280→ total_cost_usd?: number;\\n 281→ /** Total duration in milliseconds (on result messages) */\\n 282→ duration_ms?: number;\\n 283→}\\n 284→\\n 285→/**\\n 286→ * Event signatures emitted by the Session class.\\n 287→ * Subscribe using `session.on('eventName', handler)`.\\n 288→ */\\n 289→export interface SessionEvents {\\n 290→ /** Processed text output (ANSI stripped) */\\n 291→ output: (data: string) => void;\\n 292→ /** Parsed JSON message from Claude CLI */\\n 293→ message: (msg: ClaudeMessage) => void;\\n 294→ /** Error output from the session */\\n 295→ error: (data: string) => void;\\n 296→ /** Session process exited */\\n 297→ exit: (code: number | null) => void;\\n 298→ /** One-shot prompt completed with result and cost */\\n 299→ completion: (result: string, cost: number) => void;\\n 300→ /** Raw terminal data (includes ANSI codes) */\\n 301→ terminal: (data: string) => void;\\n 302→ /** Signal to clear terminal display (after screen attach) */\\n 303→ clearTerminal: () => void;\\n 304→ /** New background task started */\\n 305→ taskCreated: (task: BackgroundTask) => void;\\n 306→ /** Background task status changed */\\n 307→ taskUpdated: (task: BackgroundTask) => void;\\n 308→ /** Background task finished successfully */\\n 309→ taskCompleted: (task: BackgroundTask) => void;\\n 310→ /** Background task failed with error */\\n 311→ taskFailed: (task: BackgroundTask, error: string) => void;\\n 312→ /** Auto-clear triggered due to token threshold */\\n 313→ autoClear: (data: { tokens: number; threshold: number }) => void;\\n 314→ /** Auto-compact triggered due to token threshold */\\n 315→ autoCompact: (data: { tokens: number; threshold: number; prompt?: string }) => void;\\n 316→ /** Ralph loop state changed */\\n 317→ ralphLoopUpdate: (state: RalphTrackerState) => void;\\n 318→ /** Ralph todo list updated */\\n 319→ ralphTodoUpdate: (todos: RalphTodoItem[]) => void;\\n 320→ /** Ralph completion phrase detected */\\n 321→ ralphCompletionDetected: (phrase: string) => void;\\n 322→ /** RALPH_STATUS block detected */\\n 323→ ralphStatusBlockDetected: (block: import('./types.js').RalphStatusBlock) => void;\\n 324→ /** Circuit breaker state changed */\\n 325→ ralphCircuitBreakerUpdate: (status: import('./types.js').CircuitBreakerStatus) => void;\\n 326→ /** Dual-condition exit gate met */\\n 327→ ralphExitGateMet: (data: { completionIndicators: number; exitSignal: boolean }) => void;\\n 328→ /** Bash tool with file paths started */\\n 329→ bashToolStart: (tool: ActiveBashTool) => void;\\n 330→ /** Bash tool completed */\\n 331→ bashToolEnd: (tool: ActiveBashTool) => void;\\n 332→ /** Active Bash tools list updated */\\n 333→ bashToolsUpdate: (tools: ActiveBashTool[]) => void;\\n 334→}\\n 335→\\n 336→/**\\n 337→ * Session operation mode.\\n 338→ * - `'claude'`: Runs Claude CLI for AI interactions (default)\\n 339→ * - `'shell'`: Runs a plain bash shell for debugging/testing\\n 340→ */\\n 341→export type SessionMode = 'claude' | 'shell';\\n 342→\\n 343→/**\\n 344→ * Core session class that wraps a PTY process running Claude CLI or a shell.\\n 345→ *\\n 346→ * @example\\n 347→ * ```typescript\\n 348→ * // Create and start an interactive Claude session\\n 349→ * const session = new Session({\\n 350→ * workingDir: '/path/to/project',\\n 351→ * screenManager: screenManager,\\n 352→ * useScreen: true\\n 353→ * });\\n 354→ * await session.startInteractive();\\n 355→ *\\n 356→ * // Listen for events\\n 357→ * session.on('terminal', (data) => console.log(data));\\n 358→ * session.on('message', (msg) => console.log('Claude:', msg));\\n 359→ *\\n 360→ * // Send input\\n 361→ * session.write('Hello Claude!\\\\r');\\n 362→ *\\n 363→ * // Stop when done\\n 364→ * await session.stop();\\n 365→ * ```\\n 366→ *\\n 367→ * @fires Session#terminal - Raw terminal output\\n 368→ * @fires Session#message - Parsed Claude JSON message\\n 369→ * @fires Session#completion - One-shot prompt completed\\n 370→ * @fires Session#exit - Process exited\\n 371→ * @fires Session#autoClear - Token threshold reached, clearing context\\n 372→ * @fires Session#autoCompact - Token threshold reached, compacting context\\n 373→ */\\n 374→export class Session extends EventEmitter {\\n 375→ readonly id: string;\\n 376→ readonly workingDir: string;\\n 377→ readonly createdAt: number;\\n 378→ readonly mode: SessionMode;\\n 379→\\n 380→ private _name: string;\\n 381→ private ptyProcess: pty.IPty | null = null;\\n 382→ private _pid: number | null = null;\\n 383→ private _status: SessionStatus = 'idle';\\n 384→ private _currentTaskId: string | null = null;\\n 385→ // Use BufferAccumulator for hot-path buffers to reduce GC pressure\\n 386→ private _terminalBuffer = new BufferAccumulator(MAX_TERMINAL_BUFFER_SIZE, TERMINAL_BUFFER_TRIM_SIZE);\\n 387→ private _outputBuffer: string = '';\\n 388→ private _textOutput = new BufferAccumulator(MAX_TEXT_OUTPUT_SIZE, TEXT_OUTPUT_TRIM_SIZE);\\n 389→ private _errorBuffer: string = '';\\n 390→ private _lastActivityAt: number;\\n 391→ private _claudeSessionId: string | null = null;\\n 392→ private _totalCost: number = 0;\\n 393→ private _messages: ClaudeMessage[] = [];\\n 394→ private _lineBuffer: string = '';\\n 395→ private _lineBufferFlushTimer: NodeJS.Timeout | null = null;\\n 396→ private resolvePromise: ((value: { result: string; cost: number }) => void) | null = null;\\n 397→ private rejectPromise: ((reason: Error) => void) | null = null;\\n 398→ private _promptResolved: boolean = false; // Guard against race conditions in runPrompt\\n 399→ private _isWorking: boolean = false;\\n 400→ private _lastPromptTime: number = 0;\\n 401→ private activityTimeout: NodeJS.Timeout | null = null;\\n 402→ private _taskTracker: TaskTracker;\\n 403→\\n 404→ // Token tracking for auto-clear\\n 405→ private _totalInputTokens: number = 0;\\n 406→ private _totalOutputTokens: number = 0;\\n 407→ private _autoClearThreshold: number = 140000; // Default 140k tokens\\n 408→ private _autoClearEnabled: boolean = false;\\n 409→ private _isClearing: boolean = false; // Prevent recursive clearing\\n 410→\\n 411→ // Auto-compact settings\\n 412→ private _autoCompactThreshold: number = 110000; // Default 110k tokens (lower than clear)\\n 413→ private _autoCompactEnabled: boolean = false;\\n 414→ private _autoCompactPrompt: string = ''; // Optional prompt for compact\\n 415→ private _isCompacting: boolean = false; // Prevent recursive compacting\\n 416→\\n 417→ // Timer tracking for cleanup (prevents memory leaks)\\n 418→ private _autoCompactTimer: NodeJS.Timeout | null = null;\\n 419→ private _autoClearTimer: NodeJS.Timeout | null = null;\\n 420→ private _promptCheckInterval: NodeJS.Timeout | null = null;\\n 421→ private _promptCheckTimeout: NodeJS.Timeout | null = null;\\n 422→ private _shellIdleTimer: NodeJS.Timeout | null = null;\\n 423→\\n 424→ // Screen session support\\n 425→ private _screenManager: ScreenManager | null = null;\\n 426→ private _screenSession: ScreenSession | null = null;\\n 427→ private _useScreen: boolean = false;\\n 428→ // Flag to prevent new timers after session is stopped\\n 429→ private _isStopped: boolean = false;\\n 430→\\n 431→ // Ralph tracking (Ralph Wiggum loops and todo lists inside Claude Code)\\n 432→ private _ralphTracker: RalphTracker;\\n 433→\\n 434→ // Agent tree tracking\\n 435→ private _parentAgentId: string | null = null;\\n 436→ private _childAgentIds: string[] = [];\\n 437→\\n 438→ // Nice prioritying configuration\\n 439→ private _niceConfig: NiceConfig = { ...DEFAULT_NICE_CONFIG };\\n 440→\\n 441→ // Store handler references for cleanup (prevents memory leaks)\\n 442→ private _taskTrackerHandlers: {\\n 443→ taskCreated: (task: BackgroundTask) => void;\\n 444→ taskUpdated: (task: BackgroundTask) => void;\\n 445→ taskCompleted: (task: BackgroundTask) => void;\\n 446→ taskFailed: (task: BackgroundTask, error: string) => void;\\n 447→ } | null = null;\\n 448→\\n 449→ private _ralphHandlers: {\\n 450→ loopUpdate: (state: RalphTrackerState) => void;\\n 451→ todoUpdate: (todos: RalphTodoItem[]) => void;\\n 452→ completionDetected: (phrase: string) => void;\\n 453→ statusBlockDetected: (block: import('./types.js').RalphStatusBlock) => void;\\n 454→ circuitBreakerUpdate: (status: import('./types.js').CircuitBreakerStatus) => void;\\n 455→ exitGateMet: (data: { completionIndicators: number; exitSignal: boolean }) => void;\\n 456→ } | null = null;\\n 457→\\n 458→ // Bash tool tracking (file paths for live log viewing)\\n 459→ private _bashToolParser: BashToolParser;\\n 460→ private _bashToolHandlers: {\\n 461→ toolStart: (tool: ActiveBashTool) => void;\\n 462→ toolEnd: (tool: ActiveBashTool) => void;\\n 463→ toolsUpdate: (tools: ActiveBashTool[]) => void;\\n 464→ } | null = null;\\n 465→\\n 466→ // Task descriptions parsed from terminal output (e.g., \\\"Explore(Description)\\\")\\n 467→ // Used to correlate with SubagentWatcher discoveries for better window titles\\n 468→ private _recentTaskDescriptions: Map = new Map(); // timestamp -> description\\n 469→ private static readonly TASK_DESCRIPTION_MAX_AGE_MS = 30000; // Keep descriptions for 30 seconds\\n 470→\\n 471→ constructor(config: Partial & {\\n 472→ workingDir: string;\\n 473→ mode?: SessionMode;\\n 474→ name?: string;\\n 475→ screenManager?: ScreenManager;\\n 476→ useScreen?: boolean;\\n 477→ screenSession?: ScreenSession; // For restored sessions - pass the existing screen\\n 478→ niceConfig?: NiceConfig; // Nice prioritying configuration\\n 479→ }) {\\n 480→ super();\\n 481→ this.id = config.id || uuidv4();\\n 482→ this.workingDir = config.workingDir;\\n 483→ this.createdAt = config.createdAt || Date.now();\\n 484→ this.mode = config.mode || 'claude';\\n 485→ this._name = config.name || '';\\n 486→ this._lastActivityAt = this.createdAt;\\n 487→ this._screenManager = config.screenManager || null;\\n 488→ this._useScreen = config.useScreen ?? (this._screenManager !== null && ScreenManager.isScreenAvailable());\\n 489→ this._screenSession = config.screenSession || null; // Use existing screen if provided\\n 490→\\n 491→ // Apply Nice priority configuration if provided\\n 492→ if (config.niceConfig) {\\n 493→ this._niceConfig = { ...config.niceConfig };\\n 494→ }\\n 495→\\n 496→ // Initialize task tracker and forward events (store handlers for cleanup)\\n 497→ this._taskTracker = new TaskTracker();\\n 498→ this._taskTrackerHandlers = {\\n 499→ taskCreated: (task) => this.emit('taskCreated', task),\\n 500→ taskUpdated: (task) => this.emit('taskUpdated', task),\\n 501→ taskCompleted: (task) => this.emit('taskCompleted', task),\\n 502→ taskFailed: (task, error) => this.emit('taskFailed', task, error),\\n 503→ };\\n 504→ this._taskTracker.on('taskCreated', this._taskTrackerHandlers.taskCreated);\\n 505→ this._taskTracker.on('taskUpdated', this._taskTrackerHandlers.taskUpdated);\\n 506→ this._taskTracker.on('taskCompleted', this._taskTrackerHandlers.taskCompleted);\\n 507→ this._taskTracker.on('taskFailed', this._taskTrackerHandlers.taskFailed);\\n 508→\\n 509→ // Initialize Ralph tracker and forward events (store handlers for cleanup)\\n 510→ this._ralphTracker = new RalphTracker();\\n 511→ this._ralphHandlers = {\\n 512→ loopUpdate: (state) => this.emit('ralphLoopUpdate', state),\\n 513→ todoUpdate: (todos) => this.emit('ralphTodoUpdate', todos),\\n 514→ completionDetected: (phrase) => this.emit('ralphCompletionDetected', phrase),\\n 515→ statusBlockDetected: (block) => this.emit('ralphStatusBlockDetected', block),\\n 516→ circuitBreakerUpdate: (status) => this.emit('ralphCircuitBreakerUpdate', status),\\n 517→ exitGateMet: (data) => this.emit('ralphExitGateMet', data),\\n 518→ };\\n 519→ this._ralphTracker.on('loopUpdate', this._ralphHandlers.loopUpdate);\\n 520→ this._ralphTracker.on('todoUpdate', this._ralphHandlers.todoUpdate);\\n 521→ this._ralphTracker.on('completionDetected', this._ralphHandlers.completionDetected);\\n 522→ this._ralphTracker.on('statusBlockDetected', this._ralphHandlers.statusBlockDetected);\\n 523→ this._ralphTracker.on('circuitBreakerUpdate', this._ralphHandlers.circuitBreakerUpdate);\\n 524→ this._ralphTracker.on('exitGateMet', this._ralphHandlers.exitGateMet);\\n 525→\\n 526→ // Initialize Bash tool parser and forward events (store handlers for cleanup)\\n 527→ this._bashToolParser = new BashToolParser({ sessionId: this.id, workingDir: this.workingDir });\\n 528→ this._bashToolHandlers = {\\n 529→ toolStart: (tool) => this.emit('bashToolStart', tool),\\n 530→ toolEnd: (tool) => this.emit('bashToolEnd', tool),\\n 531→ toolsUpdate: (tools) => this.emit('bashToolsUpdate', tools),\\n 532→ };\\n 533→ this._bashToolParser.on('toolStart', this._bashToolHandlers.toolStart);\\n 534→ this._bashToolParser.on('toolEnd', this._bashToolHandlers.toolEnd);\\n 535→ this._bashToolParser.on('toolsUpdate', this._bashToolHandlers.toolsUpdate);\\n 536→\\n 537→ }\\n 538→\\n 539→ get status(): SessionStatus {\\n 540→ return this._status;\\n 541→ }\\n 542→\\n 543→ get currentTaskId(): string | null {\\n 544→ return this._currentTaskId;\\n 545→ }\\n 546→\\n 547→ get pid(): number | null {\\n 548→ return this._pid;\\n 549→ }\\n 550→\\n 551→ get terminalBuffer(): string {\\n 552→ return this._terminalBuffer.value;\\n 553→ }\\n 554→\\n 555→ get outputBuffer(): string {\\n 556→ return this._outputBuffer;\\n 557→ }\\n 558→\\n 559→ get textOutput(): string {\\n 560→ return this._textOutput.value;\\n 561→ }\\n 562→\\n 563→ get errorBuffer(): string {\\n 564→ return this._errorBuffer;\\n 565→ }\\n 566→\\n 567→ get lastActivityAt(): number {\\n 568→ return this._lastActivityAt;\\n 569→ }\\n 570→\\n 571→ get claudeSessionId(): string | null {\\n 572→ return this._claudeSessionId;\\n 573→ }\\n 574→\\n 575→ get totalCost(): number {\\n 576→ return this._totalCost;\\n 577→ }\\n 578→\\n 579→ get messages(): ClaudeMessage[] {\\n 580→ return this._messages;\\n 581→ }\\n 582→\\n 583→ get isWorking(): boolean {\\n 584→ return this._isWorking;\\n 585→ }\\n 586→\\n 587→ get lastPromptTime(): number {\\n 588→ return this._lastPromptTime;\\n 589→ }\\n 590→\\n 591→ get taskTracker(): TaskTracker {\\n 592→ return this._taskTracker;\\n 593→ }\\n 594→\\n 595→ get runningTaskCount(): number {\\n 596→ return this._taskTracker.getRunningCount();\\n 597→ }\\n 598→\\n 599→ get taskTree(): BackgroundTask[] {\\n 600→ return this._taskTracker.getTaskTree();\\n 601→ }\\n 602→\\n 603→ get taskStats(): { total: number; running: number; completed: number; failed: number } {\\n 604→ return this._taskTracker.getStats();\\n 605→ }\\n 606→\\n 607→ // Ralph tracking getters\\n 608→ get ralphTracker(): RalphTracker {\\n 609→ return this._ralphTracker;\\n 610→ }\\n 611→\\n 612→ get ralphLoopState(): RalphTrackerState {\\n 613→ return this._ralphTracker.loopState;\\n 614→ }\\n 615→\\n 616→ get ralphTodos(): RalphTodoItem[] {\\n 617→ return this._ralphTracker.todos;\\n 618→ }\\n 619→\\n 620→ get ralphTodoStats(): { total: number; pending: number; inProgress: number; completed: number } {\\n 621→ return this._ralphTracker.getTodoStats();\\n 622→ }\\n 623→\\n 624→ // Bash tool tracking getters\\n 625→ get bashToolParser(): BashToolParser {\\n 626→ return this._bashToolParser;\\n 627→ }\\n 628→\\n 629→ get activeTools(): ActiveBashTool[] {\\n 630→ return this._bashToolParser.activeTools;\\n 631→ }\\n 632→\\n 633→ get parentAgentId(): string | null {\\n 634→ return this._parentAgentId;\\n 635→ }\\n 636→\\n 637→ set parentAgentId(value: string | null) {\\n 638→ this._parentAgentId = value;\\n 639→ }\\n 640→\\n 641→ get childAgentIds(): string[] {\\n 642→ return [...this._childAgentIds];\\n 643→ }\\n 644→\\n 645→ addChildAgentId(agentId: string): void {\\n 646→ if (!this._childAgentIds.includes(agentId)) {\\n 647→ this._childAgentIds.push(agentId);\\n 648→ }\\n 649→ }\\n 650→\\n 651→ removeChildAgentId(agentId: string): void {\\n 652→ const idx = this._childAgentIds.indexOf(agentId);\\n 653→ if (idx >= 0) this._childAgentIds.splice(idx, 1);\\n 654→ }\\n 655→\\n 656→ // Nice priority config getters and setters\\n 657→ get niceConfig(): NiceConfig {\\n 658→ return { ...this._niceConfig };\\n 659→ }\\n 660→\\n 661→ /**\\n 662→ * Set CPU priority configuration.\\n 663→ * Note: This only affects new sessions; existing running processes won't be changed.\\n 664→ */\\n 665→ setNice(config: Partial): void {\\n 666→ if (config.enabled !== undefined) {\\n 667→ this._niceConfig.enabled = config.enabled;\\n 668→ }\\n 669→ if (config.niceValue !== undefined) {\\n 670→ // Clamp to valid range\\n 671→ this._niceConfig.niceValue = Math.max(-20, Math.min(19, config.niceValue));\\n 672→ }\\n 673→ }\\n 674→\\n 675→ // Token tracking getters and setters\\n 676→ get totalTokens(): number {\\n 677→ return this._totalInputTokens + this._totalOutputTokens;\\n 678→ }\\n 679→\\n 680→ get inputTokens(): number {\\n 681→ return this._totalInputTokens;\\n 682→ }\\n 683→\\n 684→ get outputTokens(): number {\\n 685→ return this._totalOutputTokens;\\n 686→ }\\n 687→\\n 688→ /**\\n 689→ * Restore token and cost values from saved state.\\n 690→ * Called when recovering sessions after server restart.\\n 691→ */\\n 692→ restoreTokens(inputTokens: number, outputTokens: number, totalCost: number): void {\\n 693→ // Sanity check: reject absurdly large values (max 500k tokens per session)\\n 694→ const MAX_SESSION_TOKENS = 500_000;\\n 695→ if (inputTokens > MAX_SESSION_TOKENS || outputTokens > MAX_SESSION_TOKENS) {\\n 696→ console.warn(`[Session ${this.id}] Rejected absurd restored tokens: input=${inputTokens}, output=${outputTokens}`);\\n 697→ return;\\n 698→ }\\n 699→ // Reject negative values\\n 700→ if (inputTokens < 0 || outputTokens < 0 || totalCost < 0) {\\n 701→ console.warn(`[Session ${this.id}] Rejected negative restored tokens: input=${inputTokens}, output=${outputTokens}, cost=${totalCost}`);\\n 702→ return;\\n 703→ }\\n 704→\\n 705→ this._totalInputTokens = inputTokens;\\n 706→ this._totalOutputTokens = outputTokens;\\n 707→ this._totalCost = totalCost;\\n 708→ }\\n 709→\\n 710→ get autoClearThreshold(): number {\\n 711→ return this._autoClearThreshold;\\n 712→ }\\n 713→\\n 714→ get autoClearEnabled(): boolean {\\n 715→ return this._autoClearEnabled;\\n 716→ }\\n 717→\\n 718→ get name(): string {\\n 719→ return this._name;\\n 720→ }\\n 721→\\n 722→ set name(value: string) {\\n 723→ this._name = value;\\n 724→ }\\n 725→\\n 726→ setAutoClear(enabled: boolean, threshold?: number): void {\\n 727→ this._autoClearEnabled = enabled;\\n 728→ if (threshold !== undefined) {\\n 729→ this._autoClearThreshold = threshold;\\n 730→ }\\n 731→ }\\n 732→\\n 733→ get autoCompactThreshold(): number {\\n 734→ return this._autoCompactThreshold;\\n 735→ }\\n 736→\\n 737→ get autoCompactEnabled(): boolean {\\n 738→ return this._autoCompactEnabled;\\n 739→ }\\n 740→\\n 741→ get autoCompactPrompt(): string {\\n 742→ return this._autoCompactPrompt;\\n 743→ }\\n 744→\\n 745→ setAutoCompact(enabled: boolean, threshold?: number, prompt?: string): void {\\n 746→ this._autoCompactEnabled = enabled;\\n 747→ if (threshold !== undefined) {\\n 748→ this._autoCompactThreshold = threshold;\\n 749→ }\\n 750→ if (prompt !== undefined) {\\n 751→ this._autoCompactPrompt = prompt;\\n 752→ }\\n 753→ }\\n 754→\\n 755→ isIdle(): boolean {\\n 756→ return this._status === 'idle';\\n 757→ }\\n 758→\\n 759→ isBusy(): boolean {\\n 760→ return this._status === 'busy';\\n 761→ }\\n 762→\\n 763→ isRunning(): boolean {\\n 764→ return this._status === 'idle' || this._status === 'busy';\\n 765→ }\\n 766→\\n 767→ toState(): SessionState {\\n 768→ return {\\n 769→ id: this.id,\\n 770→ pid: this.pid,\\n 771→ status: this._status,\\n 772→ workingDir: this.workingDir,\\n 773→ currentTaskId: this._currentTaskId,\\n 774→ createdAt: this.createdAt,\\n 775→ lastActivityAt: this._lastActivityAt,\\n 776→ name: this._name,\\n 777→ mode: this.mode,\\n 778→ autoClearEnabled: this._autoClearEnabled,\\n 779→ autoClearThreshold: this._autoClearThreshold,\\n 780→ autoCompactEnabled: this._autoCompactEnabled,\\n 781→ autoCompactThreshold: this._autoCompactThreshold,\\n 782→ autoCompactPrompt: this._autoCompactPrompt,\\n 783→ totalCost: this._totalCost,\\n 784→ inputTokens: this._totalInputTokens,\\n 785→ outputTokens: this._totalOutputTokens,\\n 786→ ralphEnabled: this._ralphTracker.enabled,\\n 787→ ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined,\\n 788→ parentAgentId: this._parentAgentId || undefined,\\n 789→ childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined,\\n 790→ niceEnabled: this._niceConfig.enabled,\\n 791→ niceValue: this._niceConfig.niceValue,\\n 792→ };\\n 793→ }\\n 794→\\n 795→ toDetailedState() {\\n 796→ return {\\n 797→ ...this.toState(),\\n 798→ name: this._name,\\n 799→ mode: this.mode,\\n 800→ claudeSessionId: this._claudeSessionId,\\n 801→ totalCost: this._totalCost,\\n 802→ textOutput: this._textOutput.value,\\n 803→ terminalBuffer: this._terminalBuffer.value,\\n 804→ messageCount: this._messages.length,\\n 805→ isWorking: this._isWorking,\\n 806→ lastPromptTime: this._lastPromptTime,\\n 807→ // Buffer statistics for monitoring long-running sessions\\n 808→ bufferStats: {\\n 809→ terminalBufferSize: this._terminalBuffer.length,\\n 810→ textOutputSize: this._textOutput.length,\\n 811→ messageCount: this._messages.length,\\n 812→ maxTerminalBuffer: MAX_TERMINAL_BUFFER_SIZE,\\n 813→ maxTextOutput: MAX_TEXT_OUTPUT_SIZE,\\n 814→ maxMessages: MAX_MESSAGES,\\n 815→ },\\n 816→ // Background task tracking\\n 817→ taskStats: this._taskTracker.getStats(),\\n 818→ taskTree: this._taskTracker.getTaskTree(),\\n 819→ // Token tracking\\n 820→ tokens: {\\n 821→ input: this._totalInputTokens,\\n 822→ output: this._totalOutputTokens,\\n 823→ total: this._totalInputTokens + this._totalOutputTokens,\\n 824→ },\\n 825→ autoClear: {\\n 826→ enabled: this._autoClearEnabled,\\n 827→ threshold: this._autoClearThreshold,\\n 828→ },\\n 829→ // CPU priority configuration\\n 830→ nice: {\\n 831→ enabled: this._niceConfig.enabled,\\n 832→ niceValue: this._niceConfig.niceValue,\\n 833→ },\\n 834→ // Ralph tracking state\\n 835→ ralphLoop: this._ralphTracker.loopState,\\n 836→ ralphTodos: this._ralphTracker.todos,\\n 837→ ralphTodoStats: this._ralphTracker.getTodoStats(),\\n 838→ };\\n 839→ }\\n 840→\\n 841→ /**\\n 842→ * Starts an interactive Claude CLI session with full terminal support.\\n 843→ *\\n 844→ * This spawns Claude CLI with `--dangerously-skip-permissions` flag in\\n 845→ * interactive mode. If screen wrapping is enabled, the session runs inside\\n 846→ * a GNU Screen session for persistence across disconnects.\\n 847→ *\\n 848→ * @throws {Error} If a process is already running in this session\\n 849→ *\\n 850→ * @example\\n 851→ * ```typescript\\n 852→ * const session = new Session({ workingDir: '/project', useScreen: true });\\n 853→ * await session.startInteractive();\\n 854→ * session.on('terminal', (data) => process.stdout.write(data));\\n 855→ * session.write('help me with this code\\\\r');\\n 856→ * ```\\n 857→ */\\n 858→ async startInteractive(): Promise {\\n 859→ if (this.ptyProcess) {\\n 860→ throw new Error('Session already has a running process');\\n 861→ }\\n 862→\\n 863→ this._status = 'busy';\\n 864→ this._terminalBuffer.clear();\\n 865→ this._outputBuffer = '';\\n 866→ this._textOutput.clear();\\n 867→ this._errorBuffer = '';\\n 868→ this._messages = [];\\n 869→ this._lineBuffer = '';\\n 870→ this._lastActivityAt = Date.now();\\n 871→\\n 872→ console.log('[Session] Starting interactive Claude session' + (this._useScreen ? ' (with screen)' : ''));\\n 873→\\n 874→ // If screen wrapping is enabled, create or attach to a screen session\\n 875→ if (this._useScreen && this._screenManager) {\\n 876→ try {\\n 877→ // Check if we already have a screen session (restored session)\\n 878→ const isRestoredSession = this._screenSession !== null;\\n 879→ if (isRestoredSession) {\\n 880→ console.log('[Session] Attaching to existing screen session:', this._screenSession!.screenName);\\n 881→ } else {\\n 882→ // Create a new screen session\\n 883→ this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude', this._name, this._niceConfig);\\n 884→ console.log('[Session] Created screen session:', this._screenSession.screenName);\\n 885→\\n 886→ // Wait a moment for screen to fully start\\n 887→ await new Promise(resolve => setTimeout(resolve, SCREEN_STARTUP_DELAY_MS));\\n 888→ }\\n 889→\\n 890→ // Attach to the screen session via PTY\\n 891→ try {\\n 892→ this.ptyProcess = pty.spawn('screen', [\\n 893→ '-x', this._screenSession!.screenName\\n 894→ ], {\\n 895→ name: 'xterm-256color',\\n 896→ cols: 120,\\n 897→ rows: 40,\\n 898→ cwd: this.workingDir,\\n 899→ env: { ...process.env, TERM: 'xterm-256color' },\\n 900→ });\\n 901→ } catch (spawnErr) {\\n 902→ console.error('[Session] Failed to spawn PTY for screen attachment:', spawnErr);\\n 903→ this.emit('error', `Failed to attach to screen: ${spawnErr}`);\\n 904→ throw spawnErr;\\n 905→ }\\n 906→\\n 907→ // For NEW screens: wait for prompt to appear then clean buffer\\n 908→ // For RESTORED screens: don't do anything - client will fetch buffer on tab switch\\n 909→ if (!isRestoredSession) {\\n 910→ this._promptCheckInterval = setInterval(() => {\\n 911→ // Wait for the prompt character (❯) which means Claude is fully initialized\\n 912→ const bufferValue = this._terminalBuffer.value;\\n 913→ if (bufferValue.includes('❯') || bufferValue.includes('\\\\u276f')) {\\n 914→ if (this._promptCheckInterval) {\\n 915→ clearInterval(this._promptCheckInterval);\\n 916→ this._promptCheckInterval = null;\\n 917→ }\\n 918→ if (this._promptCheckTimeout) {\\n 919→ clearTimeout(this._promptCheckTimeout);\\n 920→ this._promptCheckTimeout = null;\\n 921→ }\\n 922→ // Clean the buffer - remove screen init junk before actual content\\n 923→ // Strip: cursor movement (\\\\x1b[nA/B/C/D), positioning (\\\\x1b[n;nH),\\n 924→ // clear screen (\\\\x1b[2J), scroll region (\\\\x1b[n;nr), and whitespace\\n 925→ this._terminalBuffer.set(\\n 926→ bufferValue.replace(/^(\\\\x1b\\\\[\\\\??[\\\\d;]*[A-Za-z]|[\\\\s\\\\r\\\\n])+/, '')\\n 927→ );\\n 928→ // Signal client to refresh\\n 929→ this.emit('clearTerminal');\\n 930→ }\\n 931→ }, 50);\\n 932→ // Timeout after 5 seconds if prompt not found\\n 933→ this._promptCheckTimeout = setTimeout(() => {\\n 934→ if (this._promptCheckInterval) {\\n 935→ clearInterval(this._promptCheckInterval);\\n 936→ this._promptCheckInterval = null;\\n 937→ }\\n 938→ this._promptCheckTimeout = null;\\n 939→ }, 5000);\\n 940→ }\\n 941→ } catch (err) {\\n 942→ console.error('[Session] Failed to create screen session, falling back to direct PTY:', err);\\n 943→ this._useScreen = false;\\n 944→ this._screenSession = null;\\n 945→ }\\n 946→ }\\n 947→\\n 948→ // Fallback to direct PTY if screen is not used\\n 949→ if (!this.ptyProcess) {\\n 950→ try {\\n 951→ this.ptyProcess = pty.spawn('claude', [\\n 952→ '--dangerously-skip-permissions'\\n 953→ ], {\\n 954→ name: 'xterm-256color',\\n 955→ cols: 120,\\n 956→ rows: 40,\\n 957→ cwd: this.workingDir,\\n 958→ env: {\\n 959→ ...process.env,\\n 960→ PATH: getAugmentedPath(),\\n 961→ TERM: 'xterm-256color',\\n 962→ // Inform Claude it's running within Claudeman (helps prevent self-termination)\\n 963→ CLAUDEMAN_SCREEN: '1',\\n 964→ CLAUDEMAN_SESSION_ID: this.id,\\n 965→ CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',\\n 966→ },\\n 967→ });\\n 968→ } catch (spawnErr) {\\n 969→ console.error('[Session] Failed to spawn Claude PTY:', spawnErr);\\n 970→ this._status = 'stopped';\\n 971→ this.emit('error', `Failed to start Claude: ${spawnErr}`);\\n 972→ throw new Error(`Failed to spawn Claude process: ${spawnErr}`);\\n 973→ }\\n 974→ }\\n 975→\\n 976→ this._pid = this.ptyProcess.pid;\\n 977→ console.log('[Session] Interactive PTY spawned with PID:', this._pid);\\n 978→\\n 979→ this.ptyProcess.onData((rawData: string) => {\\n 980→ // Filter out focus escape sequences and Ctrl+L (form feed)\\n 981→ const data = rawData\\n 982→ .replace(FOCUS_ESCAPE_FILTER, '')\\n 983→ .replace(/\\\\x0c/g, ''); // Remove Ctrl+L\\n 984→ if (!data) return; // Skip if only filtered sequences\\n 985→\\n 986→ // BufferAccumulator handles auto-trimming when max size exceeded\\n 987→ this._terminalBuffer.append(data);\\n 988→ this._lastActivityAt = Date.now();\\n 989→\\n 990→ this.emit('terminal', data);\\n 991→ this.emit('output', data);\\n 992→\\n 993→ // Forward to Ralph tracker to detect Ralph loops and todos\\n 994→ this._ralphTracker.processTerminalData(data);\\n 995→\\n 996→ // Forward to Bash tool parser to detect file-viewing commands\\n 997→ this._bashToolParser.processTerminalData(data);\\n 998→\\n 999→ // Parse token count from status line (e.g., \\\"123.4k tokens\\\" or \\\"5234 tokens\\\")\\n 1000→ this.parseTokensFromStatusLine(data);\\n 1001→\\n 1002→ // Parse task descriptions from terminal output (e.g., \\\"Explore(Check files)\\\")\\n 1003→ // This enables correlating subagent windows with their short descriptions\\n 1004→ this.parseTaskDescriptionsFromTerminalData(data);\\n 1005→\\n 1006→ // Detect if Claude is working or at prompt\\n 1007→ // The prompt line contains \\\"❯\\\" when waiting for input\\n 1008→ if (data.includes('❯') || data.includes('\\\\u276f')) {\\n 1009→ // Reset activity timeout - if no activity for 2 seconds after prompt, Claude is idle\\n 1010→ if (this.activityTimeout) clearTimeout(this.activityTimeout);\\n 1011→ this.activityTimeout = setTimeout(() => {\\n 1012→ // Emit idle if either:\\n 1013→ // 1. Claude was working and is now at prompt (normal case)\\n 1014→ // 2. Session just started and is ready (status is 'busy' but _isWorking is false)\\n 1015→ const wasWorking = this._isWorking;\\n 1016→ const isInitialReady = this._status === 'busy' && !this._isWorking;\\n 1017→ if (wasWorking || isInitialReady) {\\n 1018→ this._isWorking = false;\\n 1019→ this._status = 'idle';\\n 1020→ this._lastPromptTime = Date.now();\\n 1021→ this.emit('idle');\\n 1022→ }\\n 1023→ }, IDLE_DETECTION_DELAY_MS);\\n 1024→ }\\n 1025→\\n 1026→ // Detect when Claude starts working (thinking, writing, etc)\\n 1027→ if (data.includes('Thinking') || data.includes('Writing') || data.includes('Reading') ||\\n 1028→ data.includes('Running') || data.includes('⠋') || data.includes('⠙') ||\\n 1029→ data.includes('⠹') || data.includes('⠸') || data.includes('⠼') ||\\n 1030→ data.includes('⠴') || data.includes('⠦') || data.includes('⠧')) {\\n 1031→ if (!this._isWorking) {\\n 1032→ this._isWorking = true;\\n 1033→ this._status = 'busy';\\n 1034→ this.emit('working');\\n 1035→ }\\n 1036→ // Reset timeout since Claude is active\\n 1037→ if (this.activityTimeout) clearTimeout(this.activityTimeout);\\n 1038→ }\\n 1039→ });\\n 1040→\\n 1041→ this.ptyProcess.onExit(({ exitCode }) => {\\n 1042→ console.log('[Session] Interactive PTY exited with code:', exitCode);\\n 1043→ this.ptyProcess = null;\\n 1044→ this._pid = null;\\n 1045→ this._status = 'idle';\\n 1046→ // Clear all timers to prevent memory leaks\\n 1047→ if (this.activityTimeout) {\\n 1048→ clearTimeout(this.activityTimeout);\\n 1049→ this.activityTimeout = null;\\n 1050→ }\\n 1051→ if (this._promptCheckInterval) {\\n 1052→ clearInterval(this._promptCheckInterval);\\n 1053→ this._promptCheckInterval = null;\\n 1054→ }\\n 1055→ if (this._promptCheckTimeout) {\\n 1056→ clearTimeout(this._promptCheckTimeout);\\n 1057→ this._promptCheckTimeout = null;\\n 1058→ }\\n 1059→ // If using screen, mark the screen as detached but don't kill it\\n 1060→ if (this._screenSession && this._screenManager) {\\n 1061→ this._screenManager.setAttached(this.id, false);\\n 1062→ }\\n 1063→ this.emit('exit', exitCode);\\n 1064→ });\\n 1065→ }\\n 1066→\\n 1067→ /**\\n 1068→ * Starts a plain shell session (bash/zsh) without Claude CLI.\\n 1069→ *\\n 1070→ * Useful for debugging, testing, or when you just need a terminal.\\n 1071→ * Uses the user's default shell from $SHELL or falls back to /bin/bash.\\n 1072→ *\\n 1073→ * @throws {Error} If a process is already running in this session\\n 1074→ *\\n 1075→ * @example\\n 1076→ * ```typescript\\n 1077→ * const session = new Session({ workingDir: '/project', mode: 'shell' });\\n 1078→ * await session.startShell();\\n 1079→ * session.write('ls -la\\\\r');\\n 1080→ * ```\\n 1081→ */\\n 1082→ async startShell(): Promise {\\n 1083→ if (this.ptyProcess) {\\n 1084→ throw new Error('Session already has a running process');\\n 1085→ }\\n 1086→\\n 1087→ this._status = 'busy';\\n 1088→ this._terminalBuffer.clear();\\n 1089→ this._outputBuffer = '';\\n 1090→ this._textOutput.clear();\\n 1091→ this._errorBuffer = '';\\n 1092→ this._messages = [];\\n 1093→ this._lineBuffer = '';\\n 1094→ this._lastActivityAt = Date.now();\\n 1095→\\n 1096→ // Use user's default shell or bash\\n 1097→ const shell = process.env.SHELL || '/bin/bash';\\n 1098→ console.log('[Session] Starting shell session with:', shell + (this._useScreen ? ' (with screen)' : ''));\\n 1099→\\n 1100→ // If screen wrapping is enabled, create or attach to a screen session\\n 1101→ if (this._useScreen && this._screenManager) {\\n 1102→ try {\\n 1103→ // Check if we already have a screen session (restored session)\\n 1104→ const isRestoredSession = this._screenSession !== null;\\n 1105→ if (isRestoredSession) {\\n 1106→ console.log('[Session] Attaching to existing screen session:', this._screenSession!.screenName);\\n 1107→ } else {\\n 1108→ // Create a new screen session\\n 1109→ this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell', this._name, this._niceConfig);\\n 1110→ console.log('[Session] Created screen session:', this._screenSession.screenName);\\n 1111→\\n 1112→ // Wait a moment for screen to fully start\\n 1113→ await new Promise(resolve => setTimeout(resolve, SCREEN_STARTUP_DELAY_MS));\\n 1114→ }\\n 1115→\\n 1116→ // Attach to the screen session via PTY\\n 1117→ try {\\n 1118→ this.ptyProcess = pty.spawn('screen', [\\n 1119→ '-x', this._screenSession!.screenName\\n 1120→ ], {\\n 1121→ name: 'xterm-256color',\\n 1122→ cols: 120,\\n 1123→ rows: 40,\\n 1124→ cwd: this.workingDir,\\n 1125→ env: { ...process.env, TERM: 'xterm-256color' },\\n 1126→ });\\n 1127→ } catch (spawnErr) {\\n 1128→ console.error('[Session] Failed to spawn PTY for shell screen attachment:', spawnErr);\\n 1129→ this.emit('error', `Failed to attach to screen: ${spawnErr}`);\\n 1130→ throw spawnErr;\\n 1131→ }\\n 1132→\\n 1133→ // For NEW screens: clear by sending 'clear' command to the shell\\n 1134→ // For RESTORED screens: don't clear - we want to see the existing output\\n 1135→ if (!isRestoredSession) {\\n 1136→ setTimeout(() => {\\n 1137→ if (this.ptyProcess) {\\n 1138→ this._terminalBuffer.clear();\\n 1139→ this.ptyProcess.write('clear\\\\n');\\n 1140→ }\\n 1141→ }, 100);\\n 1142→ }\\n 1143→ } catch (err) {\\n 1144→ console.error('[Session] Failed to create screen session, falling back to direct PTY:', err);\\n 1145→ this._useScreen = false;\\n 1146→ this._screenSession = null;\\n 1147→ }\\n 1148→ }\\n 1149→\\n 1150→ // Fallback to direct PTY if screen is not used\\n 1151→ if (!this.ptyProcess) {\\n 1152→ try {\\n 1153→ this.ptyProcess = pty.spawn(shell, [], {\\n 1154→ name: 'xterm-256color',\\n 1155→ cols: 120,\\n 1156→ rows: 40,\\n 1157→ cwd: this.workingDir,\\n 1158→ env: {\\n 1159→ ...process.env,\\n 1160→ TERM: 'xterm-256color',\\n 1161→ CLAUDEMAN_SCREEN: '1',\\n 1162→ CLAUDEMAN_SESSION_ID: this.id,\\n 1163→ CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',\\n 1164→ },\\n 1165→ });\\n 1166→ } catch (spawnErr) {\\n 1167→ console.error('[Session] Failed to spawn shell PTY:', spawnErr);\\n 1168→ this._status = 'stopped';\\n 1169→ this.emit('error', `Failed to start shell: ${spawnErr}`);\\n 1170→ throw new Error(`Failed to spawn shell process: ${spawnErr}`);\\n 1171→ }\\n 1172→ }\\n 1173→\\n 1174→ this._pid = this.ptyProcess.pid;\\n 1175→ console.log('[Session] Shell PTY spawned with PID:', this._pid);\\n 1176→\\n 1177→ this.ptyProcess.onData((rawData: string) => {\\n 1178→ // Filter out focus escape sequences\\n 1179→ const data = rawData.replace(FOCUS_ESCAPE_FILTER, '');\\n 1180→ if (!data) return; // Skip if only focus sequences\\n 1181→\\n 1182→ // BufferAccumulator handles auto-trimming when max size exceeded\\n 1183→ this._terminalBuffer.append(data);\\n 1184→ this._lastActivityAt = Date.now();\\n 1185→\\n 1186→ this.emit('terminal', data);\\n 1187→ this.emit('output', data);\\n 1188→ });\\n 1189→\\n 1190→ this.ptyProcess.onExit(({ exitCode }) => {\\n 1191→ console.log('[Session] Shell PTY exited with code:', exitCode);\\n 1192→ this.ptyProcess = null;\\n 1193→ this._pid = null;\\n 1194→ this._status = 'idle';\\n 1195→ // Clear timers to prevent memory leaks\\n 1196→ if (this._shellIdleTimer) {\\n 1197→ clearTimeout(this._shellIdleTimer);\\n 1198→ this._shellIdleTimer = null;\\n 1199→ }\\n 1200→ if (this.activityTimeout) {\\n 1201→ clearTimeout(this.activityTimeout);\\n 1202→ this.activityTimeout = null;\\n 1203→ }\\n 1204→ // If using screen, mark the screen as detached but don't kill it\\n 1205→ if (this._screenSession && this._screenManager) {\\n 1206→ this._screenManager.setAttached(this.id, false);\\n 1207→ }\\n 1208→ this.emit('exit', exitCode);\\n 1209→ });\\n 1210→\\n 1211→ // Mark as idle after a short delay (shell is ready)\\n 1212→ this._shellIdleTimer = setTimeout(() => {\\n 1213→ this._shellIdleTimer = null;\\n 1214→ this._status = 'idle';\\n 1215→ this._isWorking = false;\\n 1216→ this.emit('idle');\\n 1217→ }, 500);\\n 1218→ }\\n 1219→\\n 1220→ /**\\n 1221→ * Runs a one-shot prompt and returns the result.\\n 1222→ *\\n 1223→ * This spawns Claude CLI with `--output-format stream-json` to get\\n 1224→ * structured JSON output. The promise resolves when Claude completes\\n 1225→ * the response.\\n 1226→ *\\n 1227→ * @param prompt - The prompt text to send to Claude\\n 1228→ * @param options - Optional configuration\\n 1229→ * @param options.model - Model to use ('opus', 'sonnet', or full model name). Defaults to default model.\\n 1230→ * @param options.onProgress - Callback for progress updates (token count, status)\\n 1231→ * @returns Promise resolving to the result text and total cost in USD\\n 1232→ * @throws {Error} If a process is already running in this session\\n 1233→ *\\n 1234→ * @example\\n 1235→ * ```typescript\\n 1236→ * const session = new Session({ workingDir: '/project' });\\n 1237→ * const { result, cost } = await session.runPrompt('Explain this code', { model: 'opus' });\\n 1238→ * console.log(`Response: ${result}`);\\n 1239→ * console.log(`Cost: $${cost.toFixed(4)}`);\\n 1240→ * ```\\n 1241→ */\\n 1242→ async runPrompt(\\n 1243→ prompt: string,\\n 1244→ options?: { model?: string; onProgress?: (info: { tokens?: number; status?: string }) => void }\\n 1245→ ): Promise<{ result: string; cost: number }> {\\n 1246→ return new Promise((resolve, reject) => {\\n 1247→ if (this.ptyProcess) {\\n 1248→ reject(new Error('Session already has a running process'));\\n 1249→ return;\\n 1250→ }\\n 1251→\\n 1252→ this._status = 'busy';\\n 1253→ this._terminalBuffer.clear();\\n 1254→ this._outputBuffer = '';\\n 1255→ this._textOutput.clear();\\n 1256→ this._errorBuffer = '';\\n 1257→ this._messages = [];\\n 1258→ this._lineBuffer = '';\\n 1259→ this._lastActivityAt = Date.now();\\n 1260→ this._promptResolved = false; // Reset race condition guard\\n 1261→\\n 1262→ this.resolvePromise = resolve;\\n 1263→ this.rejectPromise = reject;\\n 1264→\\n 1265→ try {\\n 1266→ // Spawn claude in a real PTY\\n 1267→ const model = options?.model;\\n 1268→ console.log('[Session] Spawning PTY for claude with prompt:', prompt.substring(0, 50), model ? `(model: ${model})` : '');\\n 1269→\\n 1270→ const args = [\\n 1271→ '-p',\\n 1272→ '--dangerously-skip-permissions',\\n 1273→ '--output-format', 'stream-json',\\n 1274→ ];\\n 1275→ if (model) {\\n 1276→ args.push('--model', model);\\n 1277→ }\\n 1278→ args.push(prompt);\\n 1279→\\n 1280→ try {\\n 1281→ this.ptyProcess = pty.spawn('claude', args, {\\n 1282→ name: 'xterm-256color',\\n 1283→ cols: 120,\\n 1284→ rows: 40,\\n 1285→ cwd: this.workingDir,\\n 1286→ env: {\\n 1287→ ...process.env,\\n 1288→ PATH: getAugmentedPath(),\\n 1289→ TERM: 'xterm-256color',\\n 1290→ // Inform Claude it's running within Claudeman\\n 1291→ CLAUDEMAN_SCREEN: '1',\\n 1292→ CLAUDEMAN_SESSION_ID: this.id,\\n 1293→ CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',\\n 1294→ },\\n 1295→ });\\n 1296→ } catch (spawnErr) {\\n 1297→ console.error('[Session] Failed to spawn Claude PTY for runPrompt:', spawnErr);\\n 1298→ this.emit('error', `Failed to spawn Claude: ${spawnErr instanceof Error ? spawnErr.message : String(spawnErr)}`);\\n 1299→ throw spawnErr;\\n 1300→ }\\n 1301→\\n 1302→ this._pid = this.ptyProcess.pid;\\n 1303→ console.log('[Session] PTY spawned with PID:', this._pid);\\n 1304→\\n 1305→ // Handle terminal data\\n 1306→ this.ptyProcess.onData((rawData: string) => {\\n 1307→ // Filter out focus escape sequences\\n 1308→ const data = rawData.replace(FOCUS_ESCAPE_FILTER, '');\\n 1309→ if (!data) return; // Skip if only focus sequences\\n 1310→\\n 1311→ // BufferAccumulator handles auto-trimming when max size exceeded\\n 1312→ this._terminalBuffer.append(data);\\n 1313→ this._lastActivityAt = Date.now();\\n 1314→\\n 1315→ this.emit('terminal', data);\\n 1316→ this.emit('output', data);\\n 1317→\\n 1318→ // Also try to parse JSON lines for structured data\\n 1319→ this.processOutput(data);\\n 1320→ });\\n 1321→\\n 1322→ // Handle exit\\n 1323→ this.ptyProcess.onExit(({ exitCode }) => {\\n 1324→ console.log('[Session] PTY exited with code:', exitCode);\\n 1325→ this.ptyProcess = null;\\n 1326→ this._pid = null;\\n 1327→\\n 1328→ // Guard against race conditions: only process once per runPrompt call\\n 1329→ if (this._promptResolved) {\\n 1330→ this.emit('exit', exitCode);\\n 1331→ return;\\n 1332→ }\\n 1333→ this._promptResolved = true;\\n 1334→\\n 1335→ // Capture callbacks atomically before processing\\n 1336→ const resolve = this.resolvePromise;\\n 1337→ const reject = this.rejectPromise;\\n 1338→ this.resolvePromise = null;\\n 1339→ this.rejectPromise = null;\\n 1340→\\n 1341→ // Find result from parsed messages or use text output\\n 1342→ const resultMsg = this._messages.find(m => m.type === 'result');\\n 1343→\\n 1344→ if (resultMsg && !resultMsg.is_error) {\\n 1345→ this._status = 'idle';\\n 1346→ const cost = resultMsg.total_cost_usd || 0;\\n 1347→ this._totalCost += cost;\\n 1348→ this.emit('completion', resultMsg.result || '', cost);\\n 1349→ if (resolve) {\\n 1350→ resolve({ result: resultMsg.result || '', cost });\\n 1351→ }\\n 1352→ } else if (exitCode !== 0 || (resultMsg && resultMsg.is_error)) {\\n 1353→ this._status = 'error';\\n 1354→ if (reject) {\\n 1355→ reject(new Error(this._errorBuffer || this._textOutput.value || 'Process exited with error'));\\n 1356→ }\\n 1357→ } else {\\n 1358→ this._status = 'idle';\\n 1359→ if (resolve) {\\n 1360→ resolve({ result: this._textOutput.value || this._terminalBuffer.value, cost: this._totalCost });\\n 1361→ }\\n 1362→ }\\n 1363→\\n 1364→ this.emit('exit', exitCode);\\n 1365→ });\\n 1366→\\n 1367→ } catch (err) {\\n 1368→ this._status = 'error';\\n 1369→ reject(err);\\n 1370→ }\\n 1371→ });\\n 1372→ }\\n 1373→\\n 1374→ private processOutput(data: string): void {\\n 1375→ // Early return if session is stopped to prevent any processing or timer creation\\n 1376→ if (this._isStopped) return;\\n 1377→\\n 1378→ // Try to extract JSON from output (Claude may output JSON in stream mode)\\n 1379→ this._lineBuffer += data;\\n 1380→\\n 1381→ // Prevent unbounded line buffer growth for very long lines\\n 1382→ if (this._lineBuffer.length > MAX_LINE_BUFFER_SIZE) {\\n 1383→ // Force flush the oversized buffer as text output\\n 1384→ this._textOutput.append(this._lineBuffer + '\\\\n');\\n 1385→ this._lineBuffer = '';\\n 1386→ }\\n 1387→\\n 1388→ // Start flush timer if not running (handles partial lines after 100ms)\\n 1389→ if (!this._lineBufferFlushTimer && this._lineBuffer.length > 0 && !this._isStopped) {\\n 1390→ this._lineBufferFlushTimer = setTimeout(() => {\\n 1391→ this._lineBufferFlushTimer = null;\\n 1392→ if (this._lineBuffer.length > 0 && !this._isStopped) {\\n 1393→ // Flush partial line as text output\\n 1394→ this._textOutput.append(this._lineBuffer);\\n 1395→ this._lineBuffer = '';\\n 1396→ }\\n 1397→ }, LINE_BUFFER_FLUSH_INTERVAL);\\n 1398→ }\\n 1399→\\n 1400→ const lines = this._lineBuffer.split('\\\\n');\\n 1401→ this._lineBuffer = lines.pop() || '';\\n 1402→\\n 1403→ // Clear flush timer if buffer is now empty\\n 1404→ if (this._lineBuffer.length === 0 && this._lineBufferFlushTimer) {\\n 1405→ clearTimeout(this._lineBufferFlushTimer);\\n 1406→ this._lineBufferFlushTimer = null;\\n 1407→ }\\n 1408→\\n 1409→ for (const line of lines) {\\n 1410→ const trimmed = line.trim();\\n 1411→ // Remove ANSI escape codes for JSON parsing (use pre-compiled pattern)\\n 1412→ const cleanLine = trimmed.replace(ANSI_ESCAPE_PATTERN, '');\\n 1413→\\n 1414→ if (cleanLine.startsWith('{') && cleanLine.endsWith('}')) {\\n 1415→ try {\\n 1416→ const msg = JSON.parse(cleanLine) as ClaudeMessage;\\n 1417→ this._messages.push(msg);\\n 1418→ this.emit('message', msg);\\n 1419→\\n 1420→ // Trim messages array for long-running sessions\\n 1421→ if (this._messages.length > MAX_MESSAGES) {\\n 1422→ this._messages = this._messages.slice(-Math.floor(MAX_MESSAGES * 0.8));\\n 1423→ }\\n 1424→\\n 1425→ if (msg.type === 'system' && msg.session_id) {\\n 1426→ this._claudeSessionId = msg.session_id;\\n 1427→ }\\n 1428→\\n 1429→ // Process message for task tracking\\n 1430→ this._taskTracker.processMessage(msg);\\n 1431→\\n 1432→ if (msg.type === 'assistant' && msg.message?.content) {\\n 1433→ for (const block of msg.message.content) {\\n 1434→ if (block.type === 'text' && block.text) {\\n 1435→ this._textOutput.append(block.text);\\n 1436→ }\\n 1437→ }\\n 1438→ // Track tokens from usage (with validation)\\n 1439→ if (msg.message.usage) {\\n 1440→ const inputDelta = msg.message.usage.input_tokens || 0;\\n 1441→ const outputDelta = msg.message.usage.output_tokens || 0;\\n 1442→\\n 1443→ // Sanity check: max 100k tokens per message (generous limit)\\n 1444→ const MAX_TOKENS_PER_MESSAGE = 100_000;\\n 1445→ if (inputDelta > 0 && inputDelta <= MAX_TOKENS_PER_MESSAGE) {\\n 1446→ this._totalInputTokens += inputDelta;\\n 1447→ }\\n 1448→ if (outputDelta > 0 && outputDelta <= MAX_TOKENS_PER_MESSAGE) {\\n 1449→ this._totalOutputTokens += outputDelta;\\n 1450→ }\\n 1451→\\n 1452→ // Check if we should auto-compact or auto-clear\\n 1453→ this.checkAutoCompact();\\n 1454→ this.checkAutoClear();\\n 1455→ }\\n 1456→ }\\n 1457→\\n 1458→ if (msg.type === 'result' && msg.total_cost_usd) {\\n 1459→ this._totalCost = msg.total_cost_usd;\\n 1460→ }\\n 1461→ } catch {\\n 1462→ // Not JSON, just regular output\\n 1463→ this._textOutput.append(line + '\\\\n');\\n 1464→ }\\n 1465→ } else if (trimmed) {\\n 1466→ this._textOutput.append(line + '\\\\n');\\n 1467→ }\\n 1468→\\n 1469→ // Parse task descriptions from terminal output (e.g., \\\"Explore(Description)\\\")\\n 1470→ // This captures the short description from Claude Code's Task tool output\\n 1471→ this.parseTaskDescriptionsFromLine(cleanLine);\\n 1472→ }\\n 1473→ // Note: BufferAccumulator auto-trims when max size exceeded\\n 1474→ }\\n 1475→\\n 1476→ /**\\n 1477→ * Parse task descriptions from raw terminal data (may contain mult\niple lines).\\n 1478→ * Called from interactive mode's onData handler.\\n 1479→ */\\n 1480→ private parseTaskDescriptionsFromTerminalData(data: string): void {\\n 1481→ // Quick pre-check: skip if no parentheses present\\n 1482→ if (!data.includes('(') || !data.includes(')')) return;\\n 1483→\\n 1484→ // Split by newlines and process each line\\n 1485→ const lines = data.split(/\\\\r?\\\\n/);\\n 1486→ for (const line of lines) {\\n 1487→ this.parseTaskDescriptionsFromLine(line);\\n 1488→ }\\n 1489→ }\\n 1490→\\n 1491→ /**\\n 1492→ * Parse task descriptions from terminal output.\\n 1493→ * Claude Code outputs Task tool calls as \\\"ToolName(Description)\\\" in the terminal.\\n 1494→ * We capture these descriptions to use as window titles for subagents.\\n 1495→ */\\n 1496→ private parseTaskDescriptionsFromLine(line: string): void {\\n 1497→ // Quick pre-check: skip expensive regex if no common tool patterns present\\n 1498→ if (!line.includes('(') || !line.includes(')')) return;\\n 1499→\\n 1500→ // Strip ANSI codes before matching - terminal output has embedded codes like [1mExplore[0m\\n 1501→ const cleanLine = line.replace(ANSI_ESCAPE_PATTERN, '');\\n 1502→\\n 1503→ // Reset regex lastIndex for global pattern\\n 1504→ TASK_TOOL_PATTERN.lastIndex = 0;\\n 1505→\\n 1506→ let match;\\n 1507→ while ((match = TASK_TOOL_PATTERN.exec(cleanLine)) !== null) {\\n 1508→ const description = match[2].trim();\\n 1509→ if (description && description.length > 0) {\\n 1510→ const now = Date.now();\\n 1511→ this._recentTaskDescriptions.set(now, description);\\n 1512→\\n 1513→ // Cleanup old entries\\n 1514→ this.cleanupOldTaskDescriptions();\\n 1515→ }\\n 1516→ }\\n 1517→ }\\n 1518→\\n 1519→ /** Maximum number of task descriptions to keep */\\n 1520→ private static readonly MAX_TASK_DESCRIPTIONS = 100;\\n 1521→\\n 1522→ /**\\n 1523→ * Remove task descriptions older than TASK_DESCRIPTION_MAX_AGE_MS.\\n 1524→ * Also enforces MAX_TASK_DESCRIPTIONS size limit.\\n 1525→ */\\n 1526→ private cleanupOldTaskDescriptions(): void {\\n 1527→ const cutoff = Date.now() - Session.TASK_DESCRIPTION_MAX_AGE_MS;\\n 1528→ // Collect keys to delete first, then delete (avoids modifying Map during iteration)\\n 1529→ const keysToDelete: number[] = [];\\n 1530→ for (const [timestamp] of this._recentTaskDescriptions) {\\n 1531→ if (timestamp < cutoff) {\\n 1532→ keysToDelete.push(timestamp);\\n 1533→ }\\n 1534→ }\\n 1535→ for (const key of keysToDelete) {\\n 1536→ this._recentTaskDescriptions.delete(key);\\n 1537→ }\\n 1538→\\n 1539→ // Enforce size limit by removing oldest entries\\n 1540→ if (this._recentTaskDescriptions.size > Session.MAX_TASK_DESCRIPTIONS) {\\n 1541→ const sortedKeys = Array.from(this._recentTaskDescriptions.keys()).sort((a, b) => a - b);\\n 1542→ const keysToRemove = sortedKeys.slice(0, this._recentTaskDescriptions.size - Session.MAX_TASK_DESCRIPTIONS);\\n 1543→ for (const key of keysToRemove) {\\n 1544→ this._recentTaskDescriptions.delete(key);\\n 1545→ }\\n 1546→ }\\n 1547→ }\\n 1548→\\n 1549→ /**\\n 1550→ * Get recent task descriptions parsed from terminal output.\\n 1551→ * Returns descriptions sorted by timestamp (most recent first).\\n 1552→ */\\n 1553→ getRecentTaskDescriptions(): Array<{ timestamp: number; description: string }> {\\n 1554→ this.cleanupOldTaskDescriptions();\\n 1555→ const results: Array<{ timestamp: number; description: string }> = [];\\n 1556→ for (const [timestamp, description] of this._recentTaskDescriptions) {\\n 1557→ results.push({ timestamp, description });\\n 1558→ }\\n 1559→ return results.sort((a, b) => b.timestamp - a.timestamp);\\n 1560→ }\\n 1561→\\n 1562→ /**\\n 1563→ * Find a task description that was parsed close to a given timestamp.\\n 1564→ * Used to correlate with SubagentWatcher discoveries.\\n 1565→ *\\n 1566→ * @param subagentStartTime - The timestamp when the subagent was discovered\\n 1567→ * @param maxAgeMs - Maximum age difference to consider (default 10 seconds)\\n 1568→ * @returns The matching description or undefined\\n 1569→ */\\n 1570→ findTaskDescriptionNear(subagentStartTime: number, maxAgeMs: number = 10000): string | undefined {\\n 1571→ this.cleanupOldTaskDescriptions();\\n 1572→\\n 1573→ // Find the most recent description that was parsed before or around the subagent start time\\n 1574→ let bestMatch: { timestamp: number; description: string } | undefined;\\n 1575→ let bestDiff = Infinity;\\n 1576→\\n 1577→ for (const [timestamp, description] of this._recentTaskDescriptions) {\\n 1578→ const diff = Math.abs(subagentStartTime - timestamp);\\n 1579→ if (diff < maxAgeMs && diff < bestDiff) {\\n 1580→ bestMatch = { timestamp, description };\\n 1581→ bestDiff = diff;\\n 1582→ }\\n 1583→ }\\n 1584→\\n 1585→ return bestMatch?.description;\\n 1586→ }\\n 1587→\\n 1588→ // Parse token count from Claude's status line in interactive mode\\n 1589→ // Matches patterns like \\\"123.4k tokens\\\", \\\"5234 tokens\\\", \\\"1.2M tokens\\\"\\n 1590→ //\\n 1591→ // SAFETY LIMITS:\\n 1592→ // - Max tokens per session: 500k (Claude's context is ~200k)\\n 1593→ // - Max delta per update: 100k (prevents sudden jumps from parsing errors)\\n 1594→ // - Rejects \\\"M\\\" suffix values > 0.5 (500k) to prevent false matches\\n 1595→ private parseTokensFromStatusLine(data: string): void {\\n 1596→ // Quick pre-check: skip expensive regex if \\\"token\\\" not present (performance optimization)\\n 1597→ if (!data.includes('token')) return;\\n 1598→\\n 1599→ // Remove ANSI escape codes for cleaner parsing (use pre-compiled pattern)\\n 1600→ const cleanData = data.replace(ANSI_ESCAPE_PATTERN, '');\\n 1601→\\n 1602→ // Match patterns: \\\"123.4k tokens\\\", \\\"5234 tokens\\\", \\\"1.2M tokens\\\"\\n 1603→ // The status line typically shows total tokens like \\\"1.2k tokens\\\" near the prompt\\n 1604→ const tokenMatch = cleanData.match(TOKEN_PATTERN);\\n 1605→\\n 1606→ if (tokenMatch) {\\n 1607→ let tokenCount = parseFloat(tokenMatch[1]);\\n 1608→ const suffix = tokenMatch[2]?.toLowerCase();\\n 1609→\\n 1610→ // Convert k/M suffix to actual number\\n 1611→ if (suffix === 'k') {\\n 1612→ tokenCount *= 1000;\\n 1613→ } else if (suffix === 'm') {\\n 1614→ // Safety: Reject M values that would result in > 500k tokens\\n 1615→ // Claude's context window is ~200k, so anything claiming millions is likely a false match\\n 1616→ if (tokenCount > 0.5) {\\n 1617→ console.warn(`[Session ${this.id}] Rejected suspicious M token value: ${tokenMatch[0]} (would be ${tokenCount * 1000000} tokens)`);\\n 1618→ return;\\n 1619→ }\\n 1620→ tokenCount *= 1000000;\\n 1621→ }\\n 1622→\\n 1623→ // Safety: Absolute maximum of 500k tokens per session\\n 1624→ const MAX_SESSION_TOKENS = 500_000;\\n 1625→ if (tokenCount > MAX_SESSION_TOKENS) {\\n 1626→ console.warn(`[Session ${this.id}] Rejected token count exceeding max: ${tokenCount} > ${MAX_SESSION_TOKENS}`);\\n 1627→ return;\\n 1628→ }\\n 1629→\\n 1630→ // Only update if the new count is higher (tokens only increase within a session)\\n 1631→ // We use total tokens as an estimate - Claude shows combined input+output\\n 1632→ const currentTotal = this._totalInputTokens + this._totalOutputTokens;\\n 1633→ if (tokenCount > currentTotal) {\\n 1634→ const delta = tokenCount - currentTotal;\\n 1635→\\n 1636→ // Safety: Reject suspiciously large jumps (max 100k per update)\\n 1637→ const MAX_DELTA_PER_UPDATE = 100_000;\\n 1638→ if (delta > MAX_DELTA_PER_UPDATE) {\\n 1639→ console.warn(`[Session ${this.id}] Rejected suspicious token jump: ${currentTotal} -> ${tokenCount} (delta: ${delta})`);\\n 1640→ return;\\n 1641→ }\\n 1642→\\n 1643→ // Estimate: split roughly 60% input, 40% output (common ratio)\\n 1644→ // This is an approximation since interactive mode doesn't give us the breakdown\\n 1645→ this._totalInputTokens += Math.round(delta * 0.6);\\n 1646→ this._totalOutputTokens += Math.round(delta * 0.4);\\n 1647→\\n 1648→ // Check if we should auto-compact or auto-clear\\n 1649→ this.checkAutoCompact();\\n 1650→ this.checkAutoClear();\\n 1651→ }\\n 1652→ }\\n 1653→ }\\n 1654→\\n 1655→ // Check if we should auto-compact based on token threshold\\n 1656→ private checkAutoCompact(): void {\\n 1657→ if (!this._autoCompactEnabled || this._isCompacting || this._isClearing || this._isStopped) return;\\n 1658→\\n 1659→ const totalTokens = this._totalInputTokens + this._totalOutputTokens;\\n 1660→ if (totalTokens >= this._autoCompactThreshold) {\\n 1661→ this._isCompacting = true;\\n 1662→ console.log(`[Session] Auto-compact triggered: ${totalTokens} tokens >= ${this._autoCompactThreshold} threshold`);\\n 1663→\\n 1664→ // Wait for Claude to be idle before compacting\\n 1665→ const checkAndCompact = () => {\\n 1666→ // Check if session is still valid (not stopped)\\n 1667→ if (!this._isCompacting || this._isStopped) return;\\n 1668→\\n 1669→ if (!this._isWorking) {\\n 1670→ // Send /compact command with optional prompt\\n 1671→ const compactCmd = this._autoCompactPrompt\\n 1672→ ? `/compact ${this._autoCompactPrompt}\\\\r`\\n 1673→ : '/compact\\\\r';\\n 1674→ this.writeViaScreen(compactCmd);\\n 1675→ this.emit('autoCompact', {\\n 1676→ tokens: totalTokens,\\n 1677→ threshold: this._autoCompactThreshold,\\n 1678→ prompt: this._autoCompactPrompt || undefined\\n 1679→ });\\n 1680→\\n 1681→ // Wait a moment then re-enable (longer than clear since compact takes time)\\n 1682→ if (!this._isStopped) {\\n 1683→ this._autoCompactTimer = setTimeout(() => {\\n 1684→ this._autoCompactTimer = null;\\n 1685→ this._isCompacting = false;\\n 1686→ }, 10000);\\n 1687→ }\\n 1688→ } else {\\n 1689→ // Check again after delay\\n 1690→ if (!this._isStopped) {\\n 1691→ this._autoCompactTimer = setTimeout(checkAndCompact, AUTO_RETRY_DELAY_MS);\\n 1692→ }\\n 1693→ }\\n 1694→ };\\n 1695→\\n 1696→ // Start checking after a short delay\\n 1697→ if (!this._isStopped) {\\n 1698→ this._autoCompactTimer = setTimeout(checkAndCompact, AUTO_INITIAL_DELAY_MS);\\n 1699→ }\\n 1700→ }\\n 1701→ }\\n 1702→\\n 1703→ // Check if we should auto-clear based on token threshold\\n 1704→ private checkAutoClear(): void {\\n 1705→ if (!this._autoClearEnabled || this._isClearing || this._isCompacting || this._isStopped) return;\\n 1706→\\n 1707→ const totalTokens = this._totalInputTokens + this._totalOutputTokens;\\n 1708→ if (totalTokens >= this._autoClearThreshold) {\\n 1709→ this._isClearing = true;\\n 1710→ console.log(`[Session] Auto-clear triggered: ${totalTokens} tokens >= ${this._autoClearThreshold} threshold`);\\n 1711→\\n 1712→ // Wait for Claude to be idle before clearing\\n 1713→ const checkAndClear = () => {\\n 1714→ // Check if session is still valid (not stopped)\\n 1715→ if (!this._isClearing || this._isStopped) return;\\n 1716→\\n 1717→ if (!this._isWorking) {\\n 1718→ // Send /clear command\\n 1719→ this.writeViaScreen('/clear\\\\r');\\n 1720→ // Reset token counts\\n 1721→ this._totalInputTokens = 0;\\n 1722→ this._totalOutputTokens = 0;\\n 1723→ this.emit('autoClear', { tokens: totalTokens, threshold: this._autoClearThreshold });\\n 1724→\\n 1725→ // Wait a moment then re-enable\\n 1726→ if (!this._isStopped) {\\n 1727→ this._autoClearTimer = setTimeout(() => {\\n 1728→ this._autoClearTimer = null;\\n 1729→ this._isClearing = false;\\n 1730→ }, 5000);\\n 1731→ }\\n 1732→ } else {\\n 1733→ // Check again after delay\\n 1734→ if (!this._isStopped) {\\n 1735→ this._autoClearTimer = setTimeout(checkAndClear, AUTO_RETRY_DELAY_MS);\\n 1736→ }\\n 1737→ }\\n 1738→ };\\n 1739→\\n 1740→ // Start checking after a short delay\\n 1741→ if (!this._isStopped) {\\n 1742→ this._autoClearTimer = setTimeout(checkAndClear, AUTO_INITIAL_DELAY_MS);\\n 1743→ }\\n 1744→ }\\n 1745→ }\\n 1746→\\n 1747→ /**\\n 1748→ * Sends input directly to the PTY process.\\n 1749→ *\\n 1750→ * For interactive sessions, this is how you send user input to Claude.\\n 1751→ * Remember to include `\\\\r` (carriage return) to simulate pressing Enter.\\n 1752→ *\\n 1753→ * @param data - The input data to send (text, escape sequences, etc.)\\n 1754→ *\\n 1755→ * @example\\n 1756→ * ```typescript\\n 1757→ * session.write('hello world'); // Text only, no Enter\\n 1758→ * session.write('\\\\r'); // Enter key\\n 1759→ * session.write('ls -la\\\\r'); // Command with Enter\\n 1760→ * ```\\n 1761→ */\\n 1762→ write(data: string): void {\\n 1763→ if (this.ptyProcess) {\\n 1764→ this.ptyProcess.write(data);\\n 1765→ }\\n 1766→ }\\n 1767→\\n 1768→ /**\\n 1769→ * Sends input via GNU Screen's `screen -X stuff` command.\\n 1770→ *\\n 1771→ * More reliable than direct PTY write for programmatic input, especially\\n 1772→ * with Claude CLI which uses Ink (React for terminals). Text and Enter\\n 1773→ * are sent as separate commands internally.\\n 1774→ *\\n 1775→ * @param data - Input data with optional `\\\\r` for Enter\\n 1776→ * @returns true if input was sent, false if no screen session or PTY\\n 1777→ *\\n 1778→ * @example\\n 1779→ * ```typescript\\n 1780→ * session.writeViaScreen('/clear\\\\r'); // Send /clear command\\n 1781→ * session.writeViaScreen('/init\\\\r'); // Send /init command\\n 1782→ * ```\\n 1783→ */\\n 1784→ writeViaScreen(data: string): boolean {\\n 1785→ if (this._screenManager && this._screenSession) {\\n 1786→ return this._screenManager.sendInput(this.id, data);\\n 1787→ }\\n 1788→ // Fallback to PTY write\\n 1789→ if (this.ptyProcess) {\\n 1790→ this.ptyProcess.write(data);\\n 1791→ return true;\\n 1792→ }\\n 1793→ return false;\\n 1794→ }\\n 1795→\\n 1796→ /**\\n 1797→ * Resizes the PTY terminal dimensions.\\n 1798→ *\\n 1799→ * Call this when the frontend terminal is resized to keep PTY in sync.\\n 1800→ *\\n 1801→ * @param cols - Number of columns (width in characters)\\n 1802→ * @param rows - Number of rows (height in lines)\\n 1803→ */\\n 1804→ resize(cols: number, rows: number): void {\\n 1805→ if (this.ptyProcess) {\\n 1806→ this.ptyProcess.resize(cols, rows);\\n 1807→ }\\n 1808→ }\\n 1809→\\n 1810→ // Legacy method for compatibility with session-manager\\n 1811→ async start(): Promise {\\n 1812→ this._status = 'idle';\\n 1813→ }\\n 1814→\\n 1815→ // Legacy method for sending input - wraps runPrompt\\n 1816→ async sendInput(input: string): Promise {\\n 1817→ this._status = 'busy';\\n 1818→ this._lastActivityAt = Date.now();\\n 1819→ this.runPrompt(input).catch(err => {\\n 1820→ this.emit('error', err.message);\\n 1821→ });\\n 1822→ }\\n 1823→\\n 1824→ /**\\n 1825→ * Remove event listeners from TaskTracker and RalphTracker.\\n 1826→ * Prevents memory leaks by ensuring handlers don't persist after session stop.\\n 1827→ */\\n 1828→ private cleanupTrackerListeners(): void {\\n 1829→ // Remove TaskTracker handlers\\n 1830→ if (this._taskTrackerHandlers) {\\n 1831→ this._taskTracker.off('taskCreated', this._taskTrackerHandlers.taskCreated);\\n 1832→ this._taskTracker.off('taskUpdated', this._taskTrackerHandlers.taskUpdated);\\n 1833→ this._taskTracker.off('taskCompleted', this._taskTrackerHandlers.taskCompleted);\\n 1834→ this._taskTracker.off('taskFailed', this._taskTrackerHandlers.taskFailed);\\n 1835→ this._taskTrackerHandlers = null;\\n 1836→ }\\n 1837→\\n 1838→ // Remove RalphTracker handlers\\n 1839→ if (this._ralphHandlers) {\\n 1840→ this._ralphTracker.off('loopUpdate', this._ralphHandlers.loopUpdate);\\n 1841→ this._ralphTracker.off('todoUpdate', this._ralphHandlers.todoUpdate);\\n 1842→ this._ralphTracker.off('completionDetected', this._ralphHandlers.completionDetected);\\n 1843→ this._ralphTracker.off('statusBlockDetected', this._ralphHandlers.statusBlockDetected);\\n 1844→ this._ralphTracker.off('circuitBreakerUpdate', this._ralphHandlers.circuitBreakerUpdate);\\n 1845→ this._ralphTracker.off('exitGateMet', this._ralphHandlers.exitGateMet);\\n 1846→ this._ralphHandlers = null;\\n 1847→ }\\n 1848→\\n 1849→ // Remove BashToolParser handlers\\n 1850→ if (this._bashToolHandlers) {\\n 1851→ this._bashToolParser.off('toolStart', this._bashToolHandlers.toolStart);\\n 1852→ this._bashToolParser.off('toolEnd', this._bashToolHandlers.toolEnd);\\n 1853→ this._bashToolParser.off('toolsUpdate', this._bashToolHandlers.toolsUpdate);\\n 1854→ this._bashToolHandlers = null;\\n 1855→ }\\n 1856→ this._bashToolParser.destroy();\\n 1857→\\n 1858→ }\\n 1859→\\n 1860→ /**\\n 1861→ * Stops the session and cleans up resources.\\n 1862→ *\\n 1863→ * This kills the PTY process and optionally the associated GNU Screen\\n 1864→ * session. All buffers are cleared and the session is marked as stopped.\\n 1865→ *\\n 1866→ * @param killScreen - Whether to also kill the screen session (default: true)\\n 1867→ *\\n 1868→ * @example\\n 1869→ * ```typescript\\n 1870→ * // Stop and kill everything\\n 1871→ * await session.stop();\\n 1872→ *\\n 1873→ * // Stop but keep screen running for later reattachment\\n 1874→ * await session.stop(false);\\n 1875→ * ```\\n 1876→ */\\n 1877→ async stop(killScreen: boolean = true): Promise {\\n 1878→ // Set stopped flag first to prevent new timers from being created\\n 1879→ this._isStopped = true;\\n 1880→\\n 1881→ // Clear activity timeout to prevent memory leak\\n 1882→ if (this.activityTimeout) {\\n 1883→ clearTimeout(this.activityTimeout);\\n 1884→ this.activityTimeout = null;\\n 1885→ }\\n 1886→\\n 1887→ // Clear line buffer flush timer\\n 1888→ if (this._lineBufferFlushTimer) {\\n 1889→ clearTimeout(this._lineBufferFlushTimer);\\n 1890→ this._lineBufferFlushTimer = null;\\n 1891→ }\\n 1892→\\n 1893→ // Clear auto-compact/auto-clear timers to prevent memory leaks\\n 1894→ if (this._autoCompactTimer) {\\n 1895→ clearTimeout(this._autoCompactTimer);\\n 1896→ this._autoCompactTimer = null;\\n 1897→ }\\n 1898→ this._isCompacting = false;\\n 1899→\\n 1900→ if (this._autoClearTimer) {\\n 1901→ clearTimeout(this._autoClearTimer);\\n 1902→ this._autoClearTimer = null;\\n 1903→ }\\n 1904→ this._isClearing = false;\\n 1905→\\n 1906→ // Clear prompt check timers\\n 1907→ if (this._promptCheckInterval) {\\n 1908→ clearInterval(this._promptCheckInterval);\\n 1909→ this._promptCheckInterval = null;\\n 1910→ }\\n 1911→ if (this._promptCheckTimeout) {\\n 1912→ clearTimeout(this._promptCheckTimeout);\\n 1913→ this._promptCheckTimeout = null;\\n 1914→ }\\n 1915→\\n 1916→ // Clear shell idle timer\\n 1917→ if (this._shellIdleTimer) {\\n 1918→ clearTimeout(this._shellIdleTimer);\\n 1919→ this._shellIdleTimer = null;\\n 1920→ }\\n 1921→\\n 1922→ // Immediately cleanup Promise callbacks to prevent orphaned references\\n 1923→ // during the rest of stop() processing (e.g., if screen kill times out)\\n 1924→ if (this.rejectPromise) {\\n 1925→ this.rejectPromise(new Error('Session stopped'));\\n 1926→ }\\n 1927→ this.resolvePromise = null;\\n 1928→ this.rejectPromise = null;\\n 1929→\\n 1930→ // Remove event listeners from trackers to prevent memory leaks\\n 1931→ this.cleanupTrackerListeners();\\n 1932→\\n 1933→ if (this.ptyProcess) {\\n 1934→ const pid = this.ptyProcess.pid;\\n 1935→\\n 1936→ // First try graceful SIGTERM\\n 1937→ try {\\n 1938→ this.ptyProcess.kill();\\n 1939→ } catch {\\n 1940→ // Process may already be dead\\n 1941→ }\\n 1942→\\n 1943→ // Give it a moment to terminate gracefully\\n 1944→ await new Promise(resolve => setTimeout(resolve, GRACEFUL_SHUTDOWN_DELAY_MS));\\n 1945→\\n 1946→ // Force kill with SIGKILL if still alive\\n 1947→ try {\\n 1948→ if (pid) {\\n 1949→ process.kill(pid, 'SIGKILL');\\n 1950→ }\\n 1951→ } catch {\\n 1952→ // Process already terminated\\n 1953→ }\\n 1954→\\n 1955→ // Also try to kill any child processes in the process group\\n 1956→ try {\\n 1957→ if (pid) {\\n 1958→ process.kill(-pid, 'SIGKILL');\\n 1959→ }\\n 1960→ } catch {\\n 1961→ // Process group may not exist or already terminated\\n 1962→ }\\n 1963→\\n 1964→ this.ptyProcess = null;\\n 1965→ }\\n 1966→ this._pid = null;\\n 1967→ this._status = 'stopped';\\n 1968→ this._currentTaskId = null;\\n 1969→\\n 1970→ // Kill the associated screen session if requested\\n 1971→ if (killScreen && this._screenManager) {\\n 1972→ // Try to kill screen even if _screenSession is not set (e.g., restored sessions)\\n 1973→ try {\\n 1974→ const killed = await this._screenManager.killScreen(this.id);\\n 1975→ if (killed) {\\n 1976→ console.log('[Session] Killed screen session for:', this.id);\\n 1977→ }\\n 1978→ } catch (err) {\\n 1979→ console.error('[Session] Failed to kill screen session:', err);\\n 1980→ }\\n 1981→ this._screenSession = null;\\n 1982→ } else if (this._screenSession && !killScreen) {\\n 1983→ console.log('[Session] Keeping screen session alive:', this._screenSession.screenName);\\n 1984→ this._screenSession = null; // Detach but don't kill\\n 1985→ }\\n 1986→ }\\n 1987→\\n 1988→ assignTask(taskId: string): void {\\n 1989→ this._currentTaskId = taskId;\\n 1990→ this._status = 'busy';\\n 1991→ this._terminalBuffer.clear();\\n 1992→ this._outputBuffer = '';\\n 1993→ this._textOutput.clear();\\n 1994→ this._errorBuffer = '';\\n 1995→ this._messages = [];\\n 1996→ this._lastActivityAt = Date.now();\\n 1997→ }\\n 1998→\\n 1999→ clearTask(): void {\\n 2000→ this._currentTaskId = null;\\n 2001→ this._status = 'idle';\\n 2002→ this._lastActivityAt = Date.now();\\n 2003→ }\\n 2004→\\n 2005→ getOutput(): string {\\n 2006→ return this._textOutput.value;\\n 2007→ }\\n 2008→\\n 2009→ getError(): string {\\n 2010→ return this._errorBuffer;\\n 2011→ }\\n 2012→\\n 2013→ getTerminalBuffer(): string {\\n 2014→ return this._terminalBuffer.value;\\n 2015→ }\\n 2016→\\n 2017→ clearBuffers(): void {\\n 2018→ this._terminalBuffer.clear();\\n 2019→ this._outputBuffer = '';\\n 2020→ this._textOutput.clear();\\n 2021→ this._errorBuffer = '';\\n 2022→ this._messages = [];\\n 2023→ this._taskTracker.clear();\\n 2024→ this._ralphTracker.clear();\\n 2025→ }\\n 2026→}\\n 2027→\\n\\n\\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\\n\\n\"}]},\"parent_tool_use_id\":\"toolu_013zudXiqSyV3WcHAEKoyzSi\",\"session_id\":\"e84b8b18-61ba-45c7-ad9f-1a7f6f270685\",\"uuid\":\"ad76977f-dc04-48c2-a23c-63baaf9fd8c6\"}\r\nCredit balance is too low","durationMs":90543},{"agentType":"testing","items":[{"category":"unit","content":"Write test for API client connection to Claudeman server with valid host/port","rationale":"Core functionality - app must establish connections to remote Claudeman instances"},{"category":"unit","content":"Write test for SSE event stream parsing from /api/events endpoint","rationale":"Real-time updates are critical - Claudeman uses SSE for terminal output, session state, and agent activity"},{"category":"unit","content":"Write test for session list rendering with multiple sessions in different states","rationale":"Users need to see all sessions at a glance with clear status indicators (idle, busy, error)"},{"category":"unit","content":"Write test for terminal view rendering ANSI escape sequences correctly","rationale":"Terminal output must display colors and formatting - Claudeman streams raw PTY output with ANSI codes"},{"category":"unit","content":"Write test for server configuration persistence in secure storage","rationale":"Users configure multiple servers - credentials must persist securely between app launches"},{"category":"integration","content":"Write test for full session lifecycle: create -> send input -> receive output -> delete","rationale":"End-to-end flow validation ensures all API endpoints work together correctly"},{"category":"integration","content":"Write test for real-time session state synchronization via SSE","rationale":"Mobile app must stay in sync with server state - sessions can be modified from web UI or other clients"},{"category":"integration","content":"Write test for respawn controller start/stop from mobile app","rationale":"Respawn is a key Claudeman feature - users need to enable autonomous loops from mobile"},{"category":"integration","content":"Write test for subagent window display when Claude spawns Task agents","rationale":"Claudeman tracks spawned subagents - mobile UI should show agent hierarchy and progress"},{"category":"edge-case","content":"Write test for handling server disconnection during active session","rationale":"Network reliability varies on mobile - app must gracefully handle dropped connections"},{"category":"edge-case","content":"Write test for handling large terminal buffer (2MB+) without memory issues","rationale":"Claudeman caps terminal buffers at 2MB - mobile app must handle this without crashing"},{"category":"edge-case","content":"Write test for switching between multiple connected servers","rationale":"Users may have Claudeman on home server and cloud - seamless switching is expected"},{"category":"edge-case","content":"Write test for handling 20+ simultaneous sessions as per Claudeman design","rationale":"Claudeman is designed for 20 concurrent sessions - mobile UI must scale"},{"category":"error","content":"Write test for invalid server URL input validation","rationale":"User input validation prevents confusing errors and improves UX"},{"category":"error","content":"Write test for API error responses (404, 500, network timeout)","rationale":"Users need clear feedback when operations fail - not cryptic error messages"},{"category":"error","content":"Write test for authentication failure when server requires HTTPS","rationale":"Claudeman supports HTTPS mode - app should detect and handle TLS requirements"},{"category":"verification","content":"Write test for iOS accessibility compliance (VoiceOver support)","rationale":"iOS apps must be accessible - terminal content needs proper labels for screen readers"},{"category":"verification","content":"Write test for dark mode support with proper color contrast","rationale":"iOS dark mode is expected - terminal colors must remain readable"},{"category":"verification","content":"Write test for haptic feedback on key interactions","rationale":"iOS apps provide tactile feedback - improves perceived quality"},{"category":"unit","content":"Write test for Ralph loop status display and todo list rendering","rationale":"Ralph tracker shows autonomous loop progress - key feature for monitoring long-running sessions"},{"category":"unit","content":"Write test for quick-start flow creating case and session in one tap","rationale":"Quick-start is primary onboarding path - POST /api/quick-start creates case folder and session"},{"category":"integration","content":"Write test for push notification when session completes or errors","rationale":"Mobile users may background the app - notifications for important events improve UX"},{"category":"edge-case","content":"Write test for app backgrounding and foregrounding state restoration","rationale":"iOS aggressively kills background apps - state must restore seamlessly"},{"category":"verification","content":"Write test for keyboard handling in terminal input on iOS","rationale":"Terminal input needs special key handling (Ctrl+C, arrow keys, etc.) on soft keyboard"}],"success":true,"durationMs":82947},{"agentType":"risks","items":[{"category":"dependency","content":"iOS development requires Xcode which only runs on macOS, but development is on Linux","rationale":"Use React Native or Flutter which support Linux development with iOS simulator alternatives, or use Expo Go for testing on physical iOS devices during development"},{"category":"dependency","content":"App Store submission requires Apple Developer account ($99/year) and macOS for final builds","rationale":"Plan for eventual macOS access for release builds; use CI/CD services like GitHub Actions with macOS runners or services like Codemagic/Bitrise"},{"category":"security","content":"SSH/connection credentials stored on mobile device could be compromised if device is lost or jailbroken","rationale":"Use iOS Keychain for secure storage, implement biometric authentication, consider session tokens with expiry rather than storing raw credentials"},{"category":"security","content":"WebSocket/SSE connections to remote Claudeman servers expose attack surface for man-in-the-middle attacks","rationale":"Enforce TLS/HTTPS only, implement certificate pinning, validate server certificates properly"},{"category":"failure","content":"Network connectivity loss during active Claude session could cause data loss or corrupted state","rationale":"Implement robust reconnection logic with exponential backoff, queue pending inputs locally, sync state on reconnect, show clear offline indicators"},{"category":"edge-case","content":"Terminal rendering on small mobile screens may truncate or wrap output incorrectly","rationale":"Implement pinch-to-zoom, landscape mode, configurable font sizes, and smart text wrapping; test with various terminal output widths"},{"category":"performance","content":"High-frequency SSE updates from active Claude sessions could drain battery rapidly","rationale":"Implement intelligent batching, reduce update frequency when app is backgrounded, use iOS background task APIs appropriately"},{"category":"edge-case","content":"iOS app suspension/termination when backgrounded will disconnect active sessions","rationale":"Use iOS background modes for network connections where appropriate, clearly communicate session state to users, implement quick reconnect on foreground"},{"category":"failure","content":"Testing on Linux cannot fully validate iOS-specific behaviors like gestures, keyboard interactions, and system integrations","rationale":"Use Expo Go on physical iOS device for real testing, document iOS-specific features needing manual verification before release"},{"category":"security","content":"Connecting to arbitrary Linux/macOS/Windows hosts means trusting user-provided endpoints","rationale":"Validate host inputs, warn users about connecting to unknown servers, implement connection timeout limits, avoid storing sensitive data from untrusted hosts"},{"category":"edge-case","content":"Different Claudeman server versions across connected machines could have API incompatibilities","rationale":"Implement version negotiation on connect, gracefully handle missing endpoints, display server version to user"},{"category":"performance","content":"Rendering multiple concurrent Claude sessions with xterm-like output could overwhelm mobile GPU/memory","rationale":"Limit concurrent visible sessions, implement virtual scrolling, lazy-load terminal buffers, cap buffer sizes more aggressively than web version"},{"category":"dependency","content":"React Native or Flutter terminal emulator libraries may lack full xterm.js feature parity","rationale":"Evaluate react-native-terminal-view or similar libraries early, may need to use WebView with xterm.js as fallback for complex rendering"},{"category":"edge-case","content":"iOS keyboard may conflict with terminal input expectations (special keys, escape sequences, Ctrl combinations)","rationale":"Implement custom toolbar with common terminal shortcuts, support external keyboards, provide configurable key mappings"},{"category":"failure","content":"Screen session attachment from mobile could conflict with existing web UI attachments to same session","rationale":"Implement session locking or multi-viewer mode, clearly show when another client is attached, handle concurrent input gracefully"}],"success":true,"durationMs":27493}],"synthesisStats":{"totalFromSubagents":54,"afterDedup":53,"sourceBreakdown":{"requirements":15,"testing":24,"risks":15}},"verificationGaps":[],"verificationWarnings":[],"qualityScore":0.75,"totalDurationMs":93348},"progressLog":[{"phase":"parallel-analysis","detail":"Spawning analysis subagents...","timestamp":1769546489621},{"phase":"subagent","detail":"Running requirements analysis...","timestamp":1769546489621},{"phase":"subagent","detail":"Running architecture analysis...","timestamp":1769546489627},{"phase":"subagent","detail":"Running testing analysis...","timestamp":1769546489630},{"phase":"subagent","detail":"Running risks analysis...","timestamp":1769546489634},{"phase":"subagent","detail":"requirements complete (15 items)","timestamp":1769546508886},{"phase":"subagent","detail":"risks complete (15 items)","timestamp":1769546517126},{"phase":"subagent","detail":"testing complete (24 items)","timestamp":1769546572577},{"phase":"synthesis","detail":"Synthesizing subagent outputs...","timestamp":1769546580169},{"phase":"verification","detail":"Running verification subagent...","timestamp":1769546580178},{"phase":"verification","detail":"Validating plan quality...","timestamp":1769546580179},{"phase":"review-injection","detail":"Ensuring review tasks for all implementations...","timestamp":1769546582969}]}} \ No newline at end of file diff --git a/package.json b/package.json index 181db80d..b8cc855f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1400", + "version": "0.1402", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/plan-orchestrator.ts b/src/plan-orchestrator.ts index d17e1712..1db63506 100644 --- a/src/plan-orchestrator.ts +++ b/src/plan-orchestrator.ts @@ -124,14 +124,29 @@ export interface DetailedPlanResult { export type ProgressCallback = (phase: string, detail: string) => void; +/** Event types for plan subagent visibility */ +export interface PlanSubagentEvent { + type: 'started' | 'progress' | 'completed' | 'failed'; + agentId: string; + agentType: 'requirements' | 'architecture' | 'testing' | 'risks' | 'verification'; + model: string; + status: string; + detail?: string; + itemCount?: number; + durationMs?: number; + error?: string; +} + +export type SubagentCallback = (event: PlanSubagentEvent) => void; + // ============================================================================ // Constants // ============================================================================ -const SUBAGENT_TIMEOUT_MS = 45000; // 45 seconds per subagent -const VERIFICATION_TIMEOUT_MS = 60000; // 60 seconds for verification -const MODEL_ANALYSIS = 'haiku'; // Fast model for parallel analysis -const MODEL_VERIFICATION = 'sonnet'; // Better reasoning for verification +const SUBAGENT_TIMEOUT_MS = 300000; // 5 minutes per subagent (Opus needs time for complex analysis) +const VERIFICATION_TIMEOUT_MS = 480000; // 8 minutes for verification (Opus + large plans) +const MODEL_ANALYSIS = 'opus'; // Best model for thorough analysis +const MODEL_VERIFICATION = 'opus'; // Best model for verification // ============================================================================ // Subagent Prompts @@ -375,12 +390,35 @@ Be critical but constructive. A thorough review catches issues that tests miss.` export class PlanOrchestrator { private screenManager: ScreenManager; private workingDir: string; + private runningSessions: Set = new Set(); + private cancelled = false; constructor(screenManager: ScreenManager, workingDir: string = process.cwd()) { this.screenManager = screenManager; this.workingDir = workingDir; } + /** + * Cancel all running subagent sessions. + * Call this when the client disconnects or user clicks Stop. + */ + async cancel(): Promise { + this.cancelled = true; + console.log(`[PlanOrchestrator] Cancelling ${this.runningSessions.size} running sessions...`); + + const stopPromises = Array.from(this.runningSessions).map(async (session) => { + try { + await session.stop(); + } catch { + // Ignore cleanup errors + } + }); + + await Promise.all(stopPromises); + this.runningSessions.clear(); + console.log('[PlanOrchestrator] All sessions cancelled'); + } + /** * Generate a detailed implementation plan using subagent orchestration. * @@ -391,7 +429,8 @@ export class PlanOrchestrator { */ async generateDetailedPlan( taskDescription: string, - onProgress?: ProgressCallback + onProgress?: ProgressCallback, + onSubagent?: SubagentCallback ): Promise { const startTime = Date.now(); let totalCost = 0; @@ -399,7 +438,7 @@ export class PlanOrchestrator { try { // Phase 1: Parallel Analysis onProgress?.('parallel-analysis', 'Spawning analysis subagents...'); - const subagentResults = await this.runParallelAnalysis(taskDescription, onProgress); + const subagentResults = await this.runParallelAnalysis(taskDescription, onProgress, onSubagent); totalCost += subagentResults.reduce((sum, r) => sum + (r.success ? 0.002 : 0), 0); // Estimate @@ -421,7 +460,8 @@ export class PlanOrchestrator { const verificationResult = await this.runVerification( taskDescription, synthesisResult.items, - onProgress + onProgress, + onSubagent ); totalCost += 0.01; // Verification cost estimate @@ -462,7 +502,8 @@ export class PlanOrchestrator { */ private async runParallelAnalysis( taskDescription: string, - onProgress?: ProgressCallback + onProgress?: ProgressCallback, + onSubagent?: SubagentCallback ): Promise { const subagents: Array<{ type: SubagentResult['agentType']; @@ -476,7 +517,7 @@ export class PlanOrchestrator { // Run all subagents in parallel const promises = subagents.map(({ type, prompt }) => - this.runSubagent(type, prompt, onProgress) + this.runSubagent(type, prompt, onProgress, onSubagent) ); return Promise.all(promises); @@ -488,10 +529,34 @@ export class PlanOrchestrator { private async runSubagent( agentType: SubagentResult['agentType'], prompt: string, - onProgress?: ProgressCallback + onProgress?: ProgressCallback, + onSubagent?: SubagentCallback ): Promise { + const agentId = `plan-${agentType}-${Date.now()}`; + + // Check if already cancelled + if (this.cancelled) { + return { + agentType, + items: [], + success: false, + error: 'Cancelled', + durationMs: 0, + }; + } + const startTime = Date.now(); + // Emit started event + onSubagent?.({ + type: 'started', + agentId, + agentType, + model: MODEL_ANALYSIS, + status: 'running', + detail: `Analyzing ${agentType}...`, + }); + const session = new Session({ workingDir: this.workingDir, screenManager: this.screenManager, @@ -499,6 +564,9 @@ export class PlanOrchestrator { mode: 'claude', }); + // Track this session for cancellation + this.runningSessions.add(session); + try { onProgress?.('subagent', `Running ${agentType} analysis...`); @@ -507,9 +575,38 @@ export class PlanOrchestrator { this.timeout(SUBAGENT_TIMEOUT_MS), ]); + // Check if cancelled during execution + if (this.cancelled) { + onSubagent?.({ + type: 'failed', + agentId, + agentType, + model: MODEL_ANALYSIS, + status: 'cancelled', + error: 'Cancelled', + durationMs: Date.now() - startTime, + }); + return { + agentType, + items: [], + success: false, + error: 'Cancelled', + durationMs: Date.now() - startTime, + }; + } + // Parse JSON from result const jsonMatch = result.match(/\[[\s\S]*\]/); if (!jsonMatch) { + onSubagent?.({ + type: 'failed', + agentId, + agentType, + model: MODEL_ANALYSIS, + status: 'failed', + error: 'No JSON array found in response', + durationMs: Date.now() - startTime, + }); return { agentType, items: [], @@ -521,6 +618,15 @@ export class PlanOrchestrator { const parsed = JSON.parse(jsonMatch[0]); if (!Array.isArray(parsed)) { + onSubagent?.({ + type: 'failed', + agentId, + agentType, + model: MODEL_ANALYSIS, + status: 'failed', + error: 'Response is not an array', + durationMs: Date.now() - startTime, + }); return { agentType, items: [], @@ -544,6 +650,17 @@ export class PlanOrchestrator { onProgress?.('subagent', `${agentType} complete (${items.length} items)`); + // Emit completed event + onSubagent?.({ + type: 'completed', + agentId, + agentType, + model: MODEL_ANALYSIS, + status: 'completed', + itemCount: items.length, + durationMs: Date.now() - startTime, + }); + return { agentType, items, @@ -551,14 +668,26 @@ export class PlanOrchestrator { durationMs: Date.now() - startTime, }; } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + onSubagent?.({ + type: 'failed', + agentId, + agentType, + model: MODEL_ANALYSIS, + status: 'failed', + error: errorMsg, + durationMs: Date.now() - startTime, + }); return { agentType, items: [], success: false, - error: err instanceof Error ? err.message : String(err), + error: errorMsg, durationMs: Date.now() - startTime, }; } finally { + // Remove from tracking and clean up + this.runningSessions.delete(session); try { await session.stop(); } catch { @@ -702,8 +831,28 @@ export class PlanOrchestrator { private async runVerification( taskDescription: string, synthesizedItems: PlanItem[], - onProgress?: ProgressCallback + onProgress?: ProgressCallback, + onSubagent?: SubagentCallback ): Promise { + const agentId = `plan-verification-${Date.now()}`; + + // Check if already cancelled + if (this.cancelled) { + return this.fallbackVerification(synthesizedItems); + } + + // Emit started event + onSubagent?.({ + type: 'started', + agentId, + agentType: 'verification', + model: MODEL_VERIFICATION, + status: 'running', + detail: 'Validating and prioritizing plan...', + }); + + const startTime = Date.now(); + const session = new Session({ workingDir: this.workingDir, screenManager: this.screenManager, @@ -711,6 +860,9 @@ export class PlanOrchestrator { mode: 'claude', }); + // Track this session for cancellation + this.runningSessions.add(session); + try { // Format plan for verification const planText = synthesizedItems @@ -728,6 +880,11 @@ export class PlanOrchestrator { this.timeout(VERIFICATION_TIMEOUT_MS), ]); + // Check if cancelled during execution + if (this.cancelled) { + return this.fallbackVerification(synthesizedItems); + } + // Parse JSON from result const jsonMatch = result.match(/\{[\s\S]*\}/); if (!jsonMatch) { @@ -787,6 +944,17 @@ export class PlanOrchestrator { onProgress?.('verification', `Verification complete (quality: ${Math.round((parsed.qualityScore || 0.8) * 100)}%)`); + // Emit completed event + onSubagent?.({ + type: 'completed', + agentId, + agentType: 'verification', + model: MODEL_VERIFICATION, + status: 'completed', + itemCount: validatedPlan.length, + durationMs: Date.now() - startTime, + }); + return { validatedPlan, gaps: Array.isArray(parsed.gaps) ? parsed.gaps.map(String) : [], @@ -795,8 +963,19 @@ export class PlanOrchestrator { }; } catch (err) { console.error('[PlanOrchestrator] Verification failed:', err); + onSubagent?.({ + type: 'failed', + agentId, + agentType: 'verification', + model: MODEL_VERIFICATION, + status: 'failed', + error: err instanceof Error ? err.message : String(err), + durationMs: Date.now() - startTime, + }); return this.fallbackVerification(synthesizedItems); } finally { + // Remove from tracking and clean up + this.runningSessions.delete(session); try { await session.stop(); } catch { @@ -808,8 +987,10 @@ export class PlanOrchestrator { /** * Fallback verification when the verification subagent fails. * Assigns heuristic priorities and adds basic verification criteria. + * Note: This is a silent fallback - no warning shown to user since the result is still useful. */ private fallbackVerification(items: PlanItem[]): VerificationResult { + console.log('[PlanOrchestrator] Using heuristic priorities (verification subagent timed out or failed)'); return { validatedPlan: items.map((item, idx) => ({ ...item, @@ -824,8 +1005,8 @@ export class PlanOrchestrator { version: 1, })), gaps: [], - warnings: ['Verification subagent failed - using heuristic priorities'], - qualityScore: 0.7, + warnings: [], // Silent fallback - heuristics are good enough, no need to alarm user + qualityScore: 0.75, // Slightly higher since fallback still produces useful results }; } diff --git a/src/ralph-tracker.ts b/src/ralph-tracker.ts index 0038ee6d..ac5922f9 100644 --- a/src/ralph-tracker.ts +++ b/src/ralph-tracker.ts @@ -31,6 +31,72 @@ import { createInitialCircuitBreakerStatus, } from './types.js'; +// ========== Enhanced Plan Task Interface ========== + +/** Task execution status for plan tracking */ +export type PlanTaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked'; + +/** TDD phase categories */ +export type TddPhase = 'setup' | 'test' | 'impl' | 'verify' | 'review'; + +/** + * Enhanced plan task with verification criteria, dependencies, and execution tracking. + * Supports TDD workflow, failure tracking, and plan versioning. + */ +export interface EnhancedPlanTask { + /** Unique identifier (e.g., "P0-001") */ + id: string; + /** Task description */ + content: string; + /** Criticality level */ + priority: 'P0' | 'P1' | 'P2' | null; + /** How to verify completion */ + verificationCriteria?: string; + /** Command to run for verification */ + testCommand?: string; + /** IDs of tasks that must complete first */ + dependencies: string[]; + /** Current execution status */ + status: PlanTaskStatus; + /** How many times attempted */ + attempts: number; + /** Most recent failure reason */ + lastError?: string; + /** Timestamp of completion */ + completedAt?: number; + /** Plan version this belongs to */ + version: number; + /** TDD phase category */ + tddPhase?: TddPhase; + /** ID of paired test/impl task */ + pairedWith?: string; + /** Estimated complexity */ + complexity?: 'low' | 'medium' | 'high'; + /** Checklist items for review tasks (tddPhase: 'review') */ + reviewChecklist?: string[]; +} + +/** Checkpoint review data */ +export interface CheckpointReview { + iteration: number; + timestamp: number; + summary: { + total: number; + completed: number; + failed: number; + blocked: number; + pending: number; + inProgress: number; + }; + stuckTasks: Array<{ + id: string; + content: string; + attempts: number; + lastError?: string; + }>; + recommendations: string[]; +} + // ========== Configuration Constants ========== /** @@ -444,6 +510,28 @@ export class RalphTracker extends EventEmitter { /** Path to the @fix_plan.md file being watched */ private _fixPlanPath: string | null = null; + // ========== Enhanced Plan Management ========== + + /** Current version of the plan (incremented on changes) */ + private _planVersion: number = 1; + + /** History of plan versions for rollback support */ + private _planHistory: Array<{ + version: number; + timestamp: number; + tasks: Map; + summary: string; + }> = []; + + /** Enhanced plan tasks with execution tracking */ + private _planTasks: Map = new Map(); + + /** Checkpoint intervals (iterations at which to trigger review) */ + private _checkpointIterations: number[] = [5, 10, 20, 30, 50, 75, 100]; + + /** Last checkpoint iteration */ + private _lastCheckpointIteration: number = 0; + /** * Creates a new RalphTracker instance. * Starts in disabled state until Ralph patterns are detected. @@ -778,7 +866,11 @@ export class RalphTracker extends EventEmitter { * @returns Shallow copy of loop state (safe to modify) */ get loopState(): RalphTrackerState { - return { ...this._loopState }; + return { + ...this._loopState, + planVersion: this._planVersion, + planHistoryLength: this._planHistory.length, + }; } /** @@ -2338,4 +2430,327 @@ export class RalphTracker extends EventEmitter { return newTodos.length; } + + // ========== Enhanced Plan Management Methods ========== + + /** + * Initialize plan tasks from generated plan items. + * Called when wizard generates a new plan. + */ + initializePlanTasks(items: Array<{ + id?: string; + content: string; + priority?: 'P0' | 'P1' | 'P2' | null; + verificationCriteria?: string; + testCommand?: string; + dependencies?: string[]; + tddPhase?: TddPhase; + pairedWith?: string; + complexity?: 'low' | 'medium' | 'high'; + }>): void { + // Save current plan to history before replacing + if (this._planTasks.size > 0) { + this._savePlanToHistory('Plan replaced with new generation'); + } + + // Clear and rebuild + this._planTasks.clear(); + this._planVersion++; + + items.forEach((item, idx) => { + const id = item.id || `task-${idx}`; + const task: EnhancedPlanTask = { + id, + content: item.content, + priority: item.priority || null, + verificationCriteria: item.verificationCriteria, + testCommand: item.testCommand, + dependencies: item.dependencies || [], + status: 'pending', + attempts: 0, + version: this._planVersion, + tddPhase: item.tddPhase, + pairedWith: item.pairedWith, + complexity: item.complexity, + }; + this._planTasks.set(id, task); + }); + + this.emit('planInitialized', { version: this._planVersion, taskCount: this._planTasks.size }); + } + + /** + * Update a specific plan task's status, attempts, or error. + */ + updatePlanTask(taskId: string, update: { + status?: PlanTaskStatus; + error?: string; + incrementAttempts?: boolean; + }): { success: boolean; task?: EnhancedPlanTask; error?: string } { + const task = this._planTasks.get(taskId); + if (!task) { + return { success: false, error: 'Task not found' }; + } + + if (update.status) { + task.status = update.status; + if (update.status === 'completed') { + task.completedAt = Date.now(); + } + } + + if (update.error) { + task.lastError = update.error; + } + + if (update.incrementAttempts) { + task.attempts++; + + // After 3 failed attempts, mark as blocked and emit warning + if (task.attempts >= 3 && task.status === 'failed') { + task.status = 'blocked'; + this.emit('taskBlocked', { + taskId, + content: task.content, + attempts: task.attempts, + lastError: task.lastError, + }); + } + } + + // Update blocked tasks when a dependency completes + if (update.status === 'completed') { + this._unblockDependentTasks(taskId); + } + + // Check for checkpoint + this._checkForCheckpoint(); + + this.emit('planTaskUpdate', { taskId, task }); + return { success: true, task }; + } + + /** + * Unblock tasks that were waiting on a completed dependency. + */ + private _unblockDependentTasks(completedTaskId: string): void { + for (const [_, task] of this._planTasks) { + if (task.dependencies.includes(completedTaskId)) { + // Check if all dependencies are now complete + const allDepsComplete = task.dependencies.every(depId => { + const dep = this._planTasks.get(depId); + return dep && dep.status === 'completed'; + }); + + if (allDepsComplete && task.status === 'blocked') { + task.status = 'pending'; + this.emit('taskUnblocked', { taskId: task.id }); + } + } + } + } + + /** + * Check if current iteration is a checkpoint and emit review if so. + */ + private _checkForCheckpoint(): void { + const currentIteration = this._loopState.cycleCount; + if (this._checkpointIterations.includes(currentIteration) && + currentIteration > this._lastCheckpointIteration) { + this._lastCheckpointIteration = currentIteration; + const checkpoint = this.generateCheckpointReview(); + this.emit('planCheckpoint', checkpoint); + } + } + + /** + * Generate a checkpoint review summarizing plan progress and stuck tasks. + */ + generateCheckpointReview(): CheckpointReview { + const tasks = Array.from(this._planTasks.values()); + + const summary = { + total: tasks.length, + completed: tasks.filter(t => t.status === 'completed').length, + failed: tasks.filter(t => t.status === 'failed').length, + blocked: tasks.filter(t => t.status === 'blocked').length, + pending: tasks.filter(t => t.status === 'pending').length, + inProgress: tasks.filter(t => t.status === 'in_progress').length, + }; + + // Find stuck tasks (3+ attempts or blocked) + const stuckTasks = tasks + .filter(t => t.attempts >= 3 || t.status === 'blocked') + .map(t => ({ + id: t.id, + content: t.content, + attempts: t.attempts, + lastError: t.lastError, + })); + + // Generate recommendations + const recommendations: string[] = []; + + if (stuckTasks.length > 0) { + recommendations.push(`${stuckTasks.length} task(s) are stuck. Consider breaking them into smaller steps.`); + } + + if (summary.failed > summary.completed && summary.total > 5) { + recommendations.push('More tasks have failed than completed. Review approach and consider plan adjustment.'); + } + + const progressPercent = Math.round((summary.completed / summary.total) * 100); + if (progressPercent < 20 && this._loopState.cycleCount > 10) { + recommendations.push('Progress is slow. Consider simplifying tasks or reviewing dependencies.'); + } + + if (summary.blocked > summary.total / 3) { + recommendations.push('Many tasks are blocked. Review dependency chain for bottlenecks.'); + } + + return { + iteration: this._loopState.cycleCount, + timestamp: Date.now(), + summary, + stuckTasks, + recommendations, + }; + } + + /** + * Save current plan state to history. + */ + private _savePlanToHistory(summary: string): void { + // Clone current tasks + const tasksCopy = new Map(); + for (const [id, task] of this._planTasks) { + tasksCopy.set(id, { ...task }); + } + + this._planHistory.push({ + version: this._planVersion, + timestamp: Date.now(), + tasks: tasksCopy, + summary, + }); + + // Limit history size + if (this._planHistory.length > 10) { + this._planHistory.shift(); + } + } + + /** + * Get plan version history. + */ + getPlanHistory(): Array<{ + version: number; + timestamp: number; + summary: string; + stats: { total: number; completed: number; failed: number }; + }> { + return this._planHistory.map(h => { + const tasks = Array.from(h.tasks.values()); + return { + version: h.version, + timestamp: h.timestamp, + summary: h.summary, + stats: { + total: tasks.length, + completed: tasks.filter(t => t.status === 'completed').length, + failed: tasks.filter(t => t.status === 'failed').length, + }, + }; + }); + } + + /** + * Rollback to a previous plan version. + */ + rollbackToVersion(version: number): { success: boolean; plan?: EnhancedPlanTask[]; error?: string } { + const historyEntry = this._planHistory.find(h => h.version === version); + if (!historyEntry) { + return { success: false, error: `Version ${version} not found in history` }; + } + + // Save current state first + this._savePlanToHistory(`Rolled back from v${this._planVersion} to v${version}`); + + // Restore the historical version + this._planTasks.clear(); + for (const [id, task] of historyEntry.tasks) { + // Reset execution state for retry + this._planTasks.set(id, { + ...task, + status: task.status === 'completed' ? 'completed' : 'pending', + attempts: task.status === 'completed' ? task.attempts : 0, + lastError: undefined, + }); + } + + this._planVersion++; + this.emit('planRollback', { version, newVersion: this._planVersion }); + + return { success: true, plan: Array.from(this._planTasks.values()) }; + } + + /** + * Add a new task to the plan (for runtime adaptation). + */ + addPlanTask(task: { + content: string; + priority?: 'P0' | 'P1' | 'P2'; + verificationCriteria?: string; + dependencies?: string[]; + insertAfter?: string; + }): { task: EnhancedPlanTask } { + // Generate unique ID + const existingIds = Array.from(this._planTasks.keys()); + const prefix = task.priority || 'P1'; + let counter = existingIds.filter(id => id.startsWith(prefix)).length + 1; + let id = `${prefix}-${String(counter).padStart(3, '0')}`; + while (this._planTasks.has(id)) { + counter++; + id = `${prefix}-${String(counter).padStart(3, '0')}`; + } + + const newTask: EnhancedPlanTask = { + id, + content: task.content, + priority: task.priority || null, + verificationCriteria: task.verificationCriteria || 'Task completed successfully', + dependencies: task.dependencies || [], + status: 'pending', + attempts: 0, + version: this._planVersion, + }; + + this._planTasks.set(id, newTask); + this.emit('planTaskAdded', { task: newTask }); + + return { task: newTask }; + } + + /** + * Get all plan tasks. + */ + getPlanTasks(): EnhancedPlanTask[] { + return Array.from(this._planTasks.values()); + } + + /** + * Get current plan version. + */ + get planVersion(): number { + return this._planVersion; + } + + /** + * Check if checkpoint review is due for current iteration. + */ + isCheckpointDue(): boolean { + const currentIteration = this._loopState.cycleCount; + return this._checkpointIterations.includes(currentIteration) && + currentIteration > this._lastCheckpointIteration; + } } diff --git a/src/types.ts b/src/types.ts index 856f4281..ebe161ae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -738,6 +738,10 @@ export interface RalphTrackerState { lastActivity: number; /** Elapsed hours if detected */ elapsedHours: number | null; + /** Current plan version (for versioning UI) */ + planVersion?: number; + /** Number of versions in history (for versioning UI) */ + planHistoryLength?: number; } /** diff --git a/src/web/public/app.js b/src/web/public/app.js index 00047e48..75af90eb 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -464,6 +464,11 @@ class ClaudemanApp { this._subagentHideTimeout = null; // Timeout for hover-based dropdown hide this.ralphStatePanelCollapsed = true; // Default to collapsed + // Plan subagent windows (visible Opus agents during plan generation) + this.planSubagents = new Map(); // Map + this.planSubagentWindowZIndex = 1100; + this.planGenerationStopped = false; // Flag to ignore SSE events after Stop + // Project Insights tracking (active Bash tools with clickable file paths) this.projectInsights = new Map(); // Map this.logViewerWindows = new Map(); // Map @@ -1907,6 +1912,13 @@ class ClaudemanApp { }, 5 * 60 * 1000); // 5 minutes }); + // Plan subagent visibility events (show Opus agents during plan generation) + addListener('plan:subagent', (e) => { + const data = JSON.parse(e.data); + console.log('[Plan Subagent]', data); + this.handlePlanSubagentEvent(data); + }); + // Plan generation progress events (for detailed mode with subagents) addListener('plan:progress', (e) => { const data = JSON.parse(e.data); @@ -2578,7 +2590,7 @@ class ClaudemanApp { generatedPlan: null, // [{content, priority, enabled, id}] or null planGenerated: false, skipPlanGeneration: false, - planDetailLevel: 'standard', // 'brief', 'standard', 'detailed' + planDetailLevel: 'detailed', // 'brief', 'standard', 'detailed' }; planLoadingTimer = null; planLoadingStartTime = null; @@ -2596,7 +2608,7 @@ class ClaudemanApp { generatedPlan: null, planGenerated: false, skipPlanGeneration: false, - planDetailLevel: 'standard', + planDetailLevel: 'detailed', // Existing plan detection existingPlan: null, // { todos, stats, content } from @fix_plan.md useExistingPlan: false, // User chose to use existing plan @@ -2620,10 +2632,16 @@ class ClaudemanApp { this.updateRalphWizardUI(); document.getElementById('ralphWizardModal').classList.add('active'); document.getElementById('ralphTaskDescription').focus(); + + // Update connection lines to show wizard connections + this.updateConnectionLines(); } closeRalphWizard() { document.getElementById('ralphWizardModal')?.classList.remove('active'); + + // Update connection lines (wizard closed, revert to normal) + this.updateConnectionLines(); } populateRalphCaseSelector() { @@ -2925,6 +2943,10 @@ class ClaudemanApp { const spinnerEl = document.querySelector('.plan-spinner'); if (spinnerEl) spinnerEl.style.display = ''; + // Reset stopped indicator + const stoppedIndicator = document.getElementById('planStoppedIndicator'); + if (stoppedIndicator) stoppedIndicator.style.display = 'none'; + // Reset existing plan badge const badge = document.getElementById('existingPlanBadge'); if (badge) badge.style.display = 'none'; @@ -2937,6 +2959,9 @@ class ClaudemanApp { // Stop any existing generation first this.stopPlanGeneration(); + // Reset stopped flag to allow new SSE events + this.planGenerationStopped = false; + // Create abort controller for this generation this.planGenerationAbortController = new AbortController(); @@ -3164,6 +3189,10 @@ class ClaudemanApp { const errorEl = document.getElementById('planGenerationError'); const msgEl = document.getElementById('planErrorMsg'); + const stoppedIndicator = document.getElementById('planStoppedIndicator'); + + // Hide the stopped indicator for real errors, show error message + if (stoppedIndicator) stoppedIndicator.style.display = 'none'; if (msgEl) msgEl.textContent = message; errorEl?.classList.remove('hidden'); } @@ -3176,6 +3205,9 @@ class ClaudemanApp { document.getElementById('planGenerationError')?.classList.add('hidden'); document.getElementById('planEditor')?.classList.remove('hidden'); + // Close plan subagent windows since generation is complete + this.closePlanSubagentWindows(); + const list = document.getElementById('planItemsList'); if (!list) return; @@ -3565,13 +3597,183 @@ class ClaudemanApp { cancelPlanGeneration() { this.stopPlanGeneration(); - this.showToast('Plan generation cancelled', 'info'); + this.planGenerationStopped = true; // Ignore future SSE events + this.showToast('Plan generation stopped', 'info'); + // Allow user to proceed without a plan by clicking Next + this.ralphWizardConfig.skipPlanGeneration = true; + + // Show stopped state with clear visual indicator const errorEl = document.getElementById('planGenerationError'); const msgEl = document.getElementById('planErrorMsg'); - if (msgEl) msgEl.textContent = 'Plan generation was cancelled.'; - errorEl?.classList.remove('hidden'); + const stoppedIndicator = document.getElementById('planStoppedIndicator'); + + // Show the stopped indicator, hide error message since this was intentional + if (stoppedIndicator) stoppedIndicator.style.display = 'flex'; + if (msgEl) msgEl.textContent = ''; + + // Hide spinner immediately and show stopped state document.getElementById('planGenerationLoading')?.classList.add('hidden'); + errorEl?.classList.remove('hidden'); + + // Close all plan subagent windows + this.closePlanSubagentWindows(); + } + + // ========== Plan Subagent Windows (Visible Opus Agents) ========== + + handlePlanSubagentEvent(event) { + // Ignore events if user has stopped plan generation + if (this.planGenerationStopped) return; + + const { type, agentId, agentType, model, status, detail, itemCount, durationMs, error } = event; + + if (type === 'started') { + // Create new plan subagent window + this.createPlanSubagentWindow(agentId, agentType, model, detail); + } else if (type === 'completed' || type === 'failed') { + // Update window to show completion/failure + this.updatePlanSubagentWindow(agentId, status, itemCount, durationMs, error); + } else if (type === 'progress') { + // Update progress detail + const windowData = this.planSubagents.get(agentId); + if (windowData?.element) { + const detailEl = windowData.element.querySelector('.plan-subagent-detail'); + if (detailEl) detailEl.textContent = detail || ''; + } + } + } + + createPlanSubagentWindow(agentId, agentType, model, detail) { + if (this.planSubagents.has(agentId)) return; + + const wizardModal = document.getElementById('ralphWizardModal'); + const wizardContent = wizardModal?.querySelector('.modal-content'); + const wizardRect = wizardContent?.getBoundingClientRect(); + + // Calculate position - alternate left/right of wizard + const windowWidth = 280; + const windowHeight = 100; + const windowCount = this.planSubagents.size; + const viewportWidth = window.innerWidth; + + let x, y; + if (wizardRect) { + const centerX = wizardRect.left + wizardRect.width / 2; + const gap = 30; + + // Stack windows vertically on each side + const side = windowCount % 2 === 0 ? 'right' : 'left'; + const sideIndex = Math.floor(windowCount / 2); + + if (side === 'right') { + x = wizardRect.right + gap; + } else { + x = wizardRect.left - windowWidth - gap; + } + y = wizardRect.top + sideIndex * (windowHeight + 15); + + // Clamp to viewport + x = Math.max(10, Math.min(x, viewportWidth - windowWidth - 10)); + y = Math.max(60, Math.min(y, window.innerHeight - windowHeight - 10)); + } else { + // Fallback positioning + x = 50 + windowCount * 30; + y = 120 + windowCount * 30; + } + + // Create window element + const win = document.createElement('div'); + win.className = 'plan-subagent-window'; + win.id = `plan-subagent-${agentId}`; + win.style.left = `${x}px`; + win.style.top = `${y}px`; + win.style.zIndex = ++this.planSubagentWindowZIndex; + + const typeLabels = { + requirements: 'Requirements Analyst', + architecture: 'Architecture Planner', + testing: 'TDD Specialist', + risks: 'Risk Analyst', + verification: 'Verification Expert', + }; + + const typeIcons = { + requirements: '📋', + architecture: '🏗️', + testing: '🧪', + risks: '⚠️', + verification: '✓', + }; + + win.innerHTML = ` +
+ ${typeIcons[agentType] || '🤖'} + ${typeLabels[agentType] || agentType} + ${model} +
+
+
+ + Running... +
+
${detail || ''}
+
+ `; + + document.body.appendChild(win); + + // Store reference + this.planSubagents.set(agentId, { + type: agentType, + model, + status: 'running', + startTime: Date.now(), + element: win, + }); + + // Update connection lines + this.updateConnectionLines(); + } + + updatePlanSubagentWindow(agentId, status, itemCount, durationMs, error) { + const windowData = this.planSubagents.get(agentId); + if (!windowData?.element) return; + + const win = windowData.element; + const statusEl = win.querySelector('.plan-subagent-status'); + const statusTextEl = win.querySelector('.plan-subagent-status-text'); + const spinnerEl = win.querySelector('.plan-subagent-spinner'); + const detailEl = win.querySelector('.plan-subagent-detail'); + + windowData.status = status; + + if (status === 'completed') { + statusEl?.classList.remove('running'); + statusEl?.classList.add('completed'); + if (spinnerEl) spinnerEl.style.display = 'none'; + if (statusTextEl) statusTextEl.textContent = `Done (${itemCount || 0} items)`; + if (detailEl && durationMs) detailEl.textContent = `${(durationMs / 1000).toFixed(1)}s`; + } else if (status === 'failed' || status === 'cancelled') { + statusEl?.classList.remove('running'); + statusEl?.classList.add('failed'); + if (spinnerEl) spinnerEl.style.display = 'none'; + if (statusTextEl) statusTextEl.textContent = status === 'cancelled' ? 'Cancelled' : 'Failed'; + if (detailEl) detailEl.textContent = error || ''; + } + + // Update connection lines + this.updateConnectionLines(); + } + + closePlanSubagentWindows() { + for (const [agentId, windowData] of this.planSubagents) { + if (windowData.element) { + windowData.element.remove(); + } + } + this.planSubagents.clear(); + this.updateConnectionLines(); } skipPlanGeneration() { @@ -7768,34 +7970,118 @@ class ClaudemanApp { svg.innerHTML = ''; + // Check if Ralph wizard modal is open + const wizardModal = document.getElementById('ralphWizardModal'); + const wizardOpen = wizardModal && !wizardModal.classList.contains('hidden') && + wizardModal.style.display !== 'none'; + const wizardContent = wizardOpen ? wizardModal.querySelector('.modal-content') : null; + for (const [agentId, windowInfo] of this.subagentWindows) { if (windowInfo.minimized || windowInfo.hidden) continue; const agent = this.subagents.get(agentId); - if (!agent?.parentSessionId) continue; - - const tab = document.querySelector(`.session-tab[data-id="${agent.parentSessionId}"]`); const win = windowInfo.element; - if (!tab || !win) continue; + if (!win) continue; - const tabRect = tab.getBoundingClientRect(); const winRect = win.getBoundingClientRect(); - // Draw curved line from tab bottom-center to window top-center - const x1 = tabRect.left + tabRect.width / 2; - const y1 = tabRect.bottom; - const x2 = winRect.left + winRect.width / 2; - const y2 = winRect.top; + // If wizard is open, draw lines from wizard to subagent windows + if (wizardOpen && wizardContent) { + const wizardRect = wizardContent.getBoundingClientRect(); - // Bezier curve control points for smooth curve - const midY = (y1 + y2) / 2; - const path = `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`; + // Determine which side of wizard the window is on + const winCenterX = winRect.left + winRect.width / 2; + const wizardCenterX = wizardRect.left + wizardRect.width / 2; - const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - line.setAttribute('d', path); - line.setAttribute('class', 'connection-line'); - line.setAttribute('data-agent-id', agentId); - svg.appendChild(line); + let x1, y1, x2, y2; + + if (winCenterX < wizardCenterX) { + // Window is on the left - connect from wizard left edge + x1 = wizardRect.left; + y1 = wizardRect.top + wizardRect.height / 2; + x2 = winRect.right; + y2 = winRect.top + winRect.height / 2; + } else { + // Window is on the right - connect from wizard right edge + x1 = wizardRect.right; + y1 = wizardRect.top + wizardRect.height / 2; + x2 = winRect.left; + y2 = winRect.top + winRect.height / 2; + } + + // Bezier curve for smooth horizontal connection + const midX = (x1 + x2) / 2; + const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`; + + const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + line.setAttribute('d', path); + line.setAttribute('class', 'connection-line wizard-connection'); + line.setAttribute('data-agent-id', agentId); + svg.appendChild(line); + } else if (agent?.parentSessionId) { + // Normal mode - connect to parent session tab + const tab = document.querySelector(`.session-tab[data-id="${agent.parentSessionId}"]`); + if (!tab) continue; + + const tabRect = tab.getBoundingClientRect(); + + // Draw curved line from tab bottom-center to window top-center + const x1 = tabRect.left + tabRect.width / 2; + const y1 = tabRect.bottom; + const x2 = winRect.left + winRect.width / 2; + const y2 = winRect.top; + + // Bezier curve control points for smooth curve + const midY = (y1 + y2) / 2; + const path = `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`; + + const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + line.setAttribute('d', path); + line.setAttribute('class', 'connection-line'); + line.setAttribute('data-agent-id', agentId); + svg.appendChild(line); + } + } + + // Draw lines from wizard to plan subagent windows (Opus agents during plan generation) + if (wizardOpen && wizardContent && this.planSubagents.size > 0) { + const wizardRect = wizardContent.getBoundingClientRect(); + + for (const [agentId, windowData] of this.planSubagents) { + const win = windowData.element; + if (!win) continue; + + const winRect = win.getBoundingClientRect(); + + // Determine which side of wizard the window is on + const winCenterX = winRect.left + winRect.width / 2; + const wizardCenterX = wizardRect.left + wizardRect.width / 2; + + let x1, y1, x2, y2; + + if (winCenterX < wizardCenterX) { + // Window is on the left + x1 = wizardRect.left; + y1 = wizardRect.top + wizardRect.height / 3 + (this.planSubagents.size > 3 ? 0 : 50); + x2 = winRect.right; + y2 = winRect.top + winRect.height / 2; + } else { + // Window is on the right + x1 = wizardRect.right; + y1 = wizardRect.top + wizardRect.height / 3 + (this.planSubagents.size > 3 ? 0 : 50); + x2 = winRect.left; + y2 = winRect.top + winRect.height / 2; + } + + const midX = (x1 + x2) / 2; + const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`; + + const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + line.setAttribute('d', path); + line.setAttribute('class', 'connection-line wizard-connection plan-subagent-line'); + line.setAttribute('data-plan-agent-id', agentId); + svg.appendChild(line); + } } } @@ -7874,11 +8160,48 @@ class ClaudemanApp { const windowWidth = 420; const windowHeight = 350; const gap = 20; - const startX = 50; - const startY = 120; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; - const maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1; + + // Check if Ralph wizard modal is open - if so, position windows on the sides + const wizardModal = document.getElementById('ralphWizardModal'); + const wizardOpen = wizardModal && !wizardModal.classList.contains('hidden') && + wizardModal.style.display !== 'none'; + + let startX, startY, maxCols; + + if (wizardOpen) { + // Wizard is ~720px wide, centered. Position windows on left/right sides + const wizardWidth = 720; + const centerX = viewportWidth / 2; + const wizardLeft = centerX - wizardWidth / 2; + const wizardRight = centerX + wizardWidth / 2; + + // Alternate between left and right sides of the wizard + const leftSideSpace = wizardLeft - 20; + const rightSideSpace = viewportWidth - wizardRight - 20; + + if (windowCount % 2 === 0 && rightSideSpace >= windowWidth) { + // Even windows go to the right + startX = wizardRight + 20; + maxCols = Math.floor(rightSideSpace / (windowWidth + gap)) || 1; + } else if (leftSideSpace >= windowWidth) { + // Odd windows go to the left + startX = Math.max(10, wizardLeft - windowWidth - 20); + maxCols = 1; // Usually only room for 1 column on left + } else { + // Not enough side space, use right side + startX = wizardRight + 20; + maxCols = 1; + } + startY = 80; // Start higher when wizard is open + } else { + // Normal positioning + startX = 50; + startY = 120; + maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1; + } + const maxRows = Math.floor((viewportHeight - startY - 50) / (windowHeight + gap)) || 1; const col = windowCount % maxCols; const row = Math.floor(windowCount / maxCols) % maxRows; // Wrap rows to stay in viewport diff --git a/src/web/public/index.html b/src/web/public/index.html index 4db67e7f..e6f45e16 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -172,6 +172,13 @@ Elapsed 0m +
@@ -1013,16 +1020,23 @@

Initializing deep reasoning model

- +
+ + +
- + @@ -1034,8 +1048,8 @@
- - + +
@@ -1045,6 +1059,10 @@
diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 174d71fa..424e5c28 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -4692,6 +4692,19 @@ kbd { stroke-dashoffset: 1000; } +/* Wizard connection lines - different color to show plan generation link */ +.connection-line.wizard-connection { + stroke: #60a5fa; + filter: drop-shadow(0 0 4px #60a5fa); + stroke-dasharray: 8 4; + animation: wizard-pulse 1.5s ease-in-out infinite; +} + +@keyframes wizard-pulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 0.9; } +} + /* ========== Project Insights Panel (Bash File Viewers) ========== */ .project-insights-panel { @@ -5366,10 +5379,23 @@ kbd { gap: 0.5rem; } -/* Wizard Modal - wider layout */ +/* Wizard Modal - wider layout with height constraints */ .modal-wizard { width: 720px; max-width: 95vw; + max-height: 80vh; + display: flex; + flex-direction: column; +} + +.modal-wizard .modal-body { + overflow-y: auto; + flex: 1; + min-height: 0; +} + +.modal-wizard .wizard-footer { + flex-shrink: 0; } .wizard-page-compact { @@ -5641,20 +5667,158 @@ kbd { display: flex; flex-direction: column; gap: 0.35rem; - max-height: 300px; + max-height: 200px; overflow-y: auto; margin-bottom: 0.5rem; } -.plan-item { +.plan-loading-actions { + display: flex; + gap: 0.5rem; + justify-content: center; +} + +.plan-cancel-btn { + color: var(--red); +} + +/* Plan Stopped State */ +.plan-stopped-indicator { display: flex; align-items: center; - gap: 0.4rem; - padding: 0.35rem 0.5rem; - background: var(--bg-input); - border-radius: 4px; + justify-content: center; + gap: 0.5rem; + margin-bottom: 1rem; + padding: 0.75rem; + background: var(--surface); + border-radius: 6px; + border: 1px solid var(--border); } +.plan-stopped-icon { + color: var(--red); + font-size: 1rem; +} + +.plan-stopped-text { + font-weight: 500; + color: var(--text); +} + +.plan-actions-single { + justify-content: center; +} + +.plan-skip-hint { + font-size: 0.75rem; + color: var(--text-muted); + text-align: center; + margin-top: 0.75rem; +} + +.plan-skip-hint strong { + color: var(--text-dim); +} + +/* ========== Plan Subagent Windows (Visible Opus Agents) ========== */ +.plan-subagent-window { + position: fixed; + width: 280px; + background: var(--bg-card); + border: 1px solid var(--accent); + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 20px rgba(96, 165, 250, 0.2); + overflow: hidden; + animation: planSubagentSpawn 0.3s ease-out; +} + +@keyframes planSubagentSpawn { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.plan-subagent-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(96, 165, 250, 0.1)); + border-bottom: 1px solid var(--border); +} + +.plan-subagent-icon { + font-size: 1rem; +} + +.plan-subagent-title { + flex: 1; + font-weight: 500; + font-size: 0.8rem; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.plan-subagent-model { + font-size: 0.65rem; + padding: 0.15rem 0.4rem; + background: #7c3aed; + color: white; + border-radius: 3px; + text-transform: uppercase; + font-weight: 600; +} + +.plan-subagent-body { + padding: 0.75rem; +} + +.plan-subagent-status { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.35rem; +} + +.plan-subagent-status.running .plan-subagent-spinner { + width: 14px; + height: 14px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.plan-subagent-status.completed .plan-subagent-status-text { + color: var(--green); +} + +.plan-subagent-status.failed .plan-subagent-status-text { + color: var(--red); +} + +.plan-subagent-status-text { + font-size: 0.8rem; + font-weight: 500; +} + +.plan-subagent-detail { + font-size: 0.7rem; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Note: main .plan-item styles are in "Enhanced Plan Item Styling" section below */ + .plan-item-checkbox { flex-shrink: 0; width: 14px; @@ -5701,6 +5865,8 @@ kbd { color: var(--red); } +/* Note: .plan-item-phase and .plan-item-verify enhanced styles are in section below */ + .plan-item:has(.plan-item-checkbox:not(:checked)) { opacity: 0.5; } @@ -5761,6 +5927,571 @@ kbd { color: #fbbf24; } +/* Enhanced Plan Item Styling */ +.plan-item { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.5rem; + background: var(--bg-input); + border-radius: 4px; + border-left: 3px solid transparent; + transition: border-color 0.15s, background-color 0.15s, transform 0.15s; + cursor: default; +} + +.plan-item.dragging { + opacity: 0.5; + transform: scale(0.98); +} + +.plan-item.drag-over { + background: var(--accent-dim); + border-left-color: var(--accent); +} + +/* Priority color borders */ +.plan-item.priority-p0 { + border-left-color: #ef4444; +} + +.plan-item.priority-p1 { + border-left-color: #f59e0b; +} + +.plan-item.priority-p2 { + border-left-color: #3b82f6; +} + +/* Drag handle */ +.plan-item-drag { + cursor: grab; + color: var(--text-muted); + font-size: 0.9rem; + padding: 0 0.2rem; + opacity: 0.5; + user-select: none; +} + +.plan-item-drag:hover { + opacity: 1; +} + +.plan-item-drag:active { + cursor: grabbing; +} + +/* TDD Phase badge - enhanced */ +.plan-item-phase { + flex-shrink: 0; + min-width: 22px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + border-radius: 3px; + background: var(--bg-card); +} + +.plan-item-phase.phase-setup { + background: #6366f1; + color: white; +} + +.plan-item-phase.phase-test { + background: #8b5cf6; + color: white; +} + +.plan-item-phase.phase-impl { + background: #06b6d4; + color: white; +} + +.plan-item-phase.phase-review { + background: #f59e0b; + color: white; +} + +.plan-item-phase.phase-verify { + background: #22c55e; + color: white; +} + +/* Enhanced verification indicator */ +.plan-item-verify { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.15rem; + color: var(--green); + font-size: 0.65rem; + cursor: help; + padding: 0.1rem 0.25rem; + background: rgba(34, 197, 94, 0.1); + border-radius: 3px; + opacity: 0.85; +} + +.plan-item-verify:hover { + opacity: 1; + background: rgba(34, 197, 94, 0.2); +} + +/* Dependencies indicator */ +.plan-item-deps { + flex-shrink: 0; + font-size: 0.65rem; + color: var(--text-muted); + padding: 0.1rem 0.25rem; + background: var(--bg-card); + border-radius: 3px; + cursor: help; +} + +.plan-item-deps:hover { + color: var(--accent); +} + +/* Review checklist indicator */ +.plan-item-checklist { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.15rem; + color: #f59e0b; + font-size: 0.65rem; + cursor: help; + padding: 0.1rem 0.25rem; + background: rgba(245, 158, 11, 0.1); + border-radius: 3px; + opacity: 0.85; +} + +.plan-item-checklist:hover { + opacity: 1; + background: rgba(245, 158, 11, 0.2); +} + +/* Checklist modal */ +.checklist-modal-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.checklist-modal { + padding: 1rem; +} + +.checklist-modal h3 { + margin: 0 0 0.5rem 0; + color: var(--text); +} + +.checklist-task { + color: var(--text-muted); + font-size: 0.85rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} + +.checklist-list { + list-style: none; + padding: 0; + margin: 0 0 1rem 0; + max-height: 300px; + overflow-y: auto; +} + +.checklist-item { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.35rem 0; +} + +.checklist-checkbox { + flex-shrink: 0; + margin-top: 0.15rem; +} + +.checklist-item label { + color: var(--text); + font-size: 0.85rem; + cursor: pointer; +} + +.checklist-footer { + text-align: right; +} + +/* Group headers for TDD phases */ +.plan-group-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.3rem 0.5rem; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + color: var(--text-muted); + border-bottom: 1px solid var(--border); + margin-top: 0.5rem; + cursor: pointer; + user-select: none; +} + +.plan-group-header:first-child { + margin-top: 0; +} + +.plan-group-header:hover { + color: var(--text); +} + +.plan-group-header .group-icon { + font-size: 0.8rem; +} + +.plan-group-header .group-count { + margin-left: auto; + color: var(--text-dim); + font-weight: normal; +} + +.plan-group-header .group-chevron { + transition: transform 0.15s; +} + +.plan-group-header.collapsed .group-chevron { + transform: rotate(-90deg); +} + +.plan-group-items { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.25rem 0; +} + +.plan-group-items.collapsed { + display: none; +} + +/* View mode toggle */ +.plan-view-toggle { + display: flex; + gap: 0.25rem; +} + +.plan-view-btn { + padding: 0.2rem 0.4rem; + font-size: 0.7rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 3px; + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s; +} + +.plan-view-btn:hover { + color: var(--text); + border-color: var(--accent); +} + +.plan-view-btn.active { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +/* Verification criteria tooltip expansion */ +.plan-item-verify-expanded { + position: absolute; + left: 0; + right: 0; + bottom: calc(100% + 4px); + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.5rem; + font-size: 0.75rem; + color: var(--text); + z-index: 10; + box-shadow: 0 -2px 8px rgba(0,0,0,0.2); + max-width: 300px; +} + +.plan-item-verify-expanded::after { + content: ''; + position: absolute; + bottom: -6px; + left: 50%; + transform: translateX(-50%); + border: 5px solid transparent; + border-top-color: var(--border); +} + +/* Plan toolbar improvements */ +.plan-editor-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} + +.plan-toolbar-left { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.plan-toolbar-right { + display: flex; + gap: 0.5rem; + align-items: center; +} + +/* Plan item inline actions */ +.plan-item-actions { + display: flex; + gap: 0.1rem; + opacity: 0; + transition: opacity 0.15s; +} + +.plan-item:hover .plan-item-actions { + opacity: 1; +} + +.plan-item-action { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 0.75rem; + padding: 0.1rem 0.2rem; + border-radius: 2px; +} + +.plan-item-action:hover { + color: var(--text); + background: var(--bg-card); +} + +/* Runtime task card enhancements */ +.ralph-task-card { + position: relative; +} + +.ralph-task-attempts { + position: absolute; + top: 2px; + right: 2px; + font-size: 0.6rem; + color: var(--text-muted); + background: var(--bg-card); + padding: 0 3px; + border-radius: 2px; +} + +.ralph-task-attempts.has-errors { + color: var(--red); + background: rgba(239, 68, 68, 0.1); +} + +.ralph-task-verify-badge { + position: absolute; + bottom: 2px; + right: 2px; + font-size: 0.6rem; + color: var(--green); + opacity: 0.7; +} + +.ralph-task-deps-indicator { + position: absolute; + top: 2px; + left: 2px; + font-size: 0.55rem; + color: var(--accent); +} + +/* Quick actions on task hover */ +.ralph-task-actions { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: none; + gap: 0.25rem; + background: rgba(0,0,0,0.85); + padding: 0.25rem; + border-radius: 4px; +} + +.ralph-task-card:hover .ralph-task-actions { + display: flex; +} + +.ralph-task-action-btn { + padding: 0.2rem 0.35rem; + font-size: 0.65rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 3px; + color: var(--text); + cursor: pointer; +} + +.ralph-task-action-btn:hover { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +/* Plan version indicator */ +.plan-version-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + color: var(--text-muted); + padding: 0.25rem 0.5rem; + background: var(--bg-input); + border-radius: 4px; +} + +.plan-version-badge { + background: var(--accent); + color: white; + padding: 0.1rem 0.3rem; + border-radius: 3px; + font-size: 0.65rem; + font-weight: 500; +} + +.plan-version-history { + cursor: pointer; + color: var(--accent); +} + +.plan-version-history:hover { + text-decoration: underline; +} + +/* Plan history dropdown */ +.plan-history-dropdown { + position: absolute; + top: 100%; + right: 0; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + z-index: 100; + min-width: 200px; + max-height: 200px; + overflow-y: auto; +} + +.plan-history-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: 4px; +} + +.plan-history-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--border); + cursor: pointer; + font-size: 0.8rem; + transition: background-color 0.15s; +} + +.plan-history-item:last-child { + border-bottom: none; +} + +.plan-history-item:hover { + background: var(--bg-input); +} + +.plan-history-item.current { + background: var(--accent-dim); + cursor: default; +} + +.plan-history-item.current::after { + content: '(current)'; + font-size: 0.65rem; + color: var(--accent); + margin-left: 0.5rem; +} + +.plan-history-version { + font-weight: 500; + color: var(--text); +} + +.plan-history-tasks { + font-size: 0.7rem; + color: var(--text-muted); + margin-left: 0.5rem; +} + +.plan-history-time { + color: var(--text-muted); + font-size: 0.7rem; +} + +/* Ralph panel version info */ +.ralph-version-info { + display: flex; + align-items: center; + gap: 0.35rem; + font-size: 0.7rem; + color: var(--text-muted); + margin-left: auto; +} + +.ralph-version-info .version-badge { + background: var(--bg-card); + padding: 0.1rem 0.25rem; + border-radius: 2px; + font-family: 'SF Mono', Monaco, monospace; +} + +.ralph-version-info .rollback-btn { + background: none; + border: none; + color: var(--accent); + cursor: pointer; + font-size: 0.65rem; + padding: 0.1rem 0.25rem; +} + +.ralph-version-info .rollback-btn:hover { + text-decoration: underline; +} + /* Textarea with AI Assist button */ .textarea-with-assist { position: relative; diff --git a/test/api-generate-plan.test.ts b/test/api-generate-plan.test.ts new file mode 100644 index 00000000..14106e14 --- /dev/null +++ b/test/api-generate-plan.test.ts @@ -0,0 +1,184 @@ +/** + * @fileoverview Tests for /api/generate-plan endpoint validation + * + * Tests request validation for the plan generation API. + * Port: 3191 + */ + +import { describe, it, expect } from 'vitest'; + +describe('Generate Plan API Validation', () => { + // Validation logic extracted from server.ts for unit testing + const validateGeneratePlanRequest = (req: { + taskDescription?: unknown; + detailLevel?: unknown; + }): { valid: boolean; error?: string } => { + const { taskDescription, detailLevel } = req; + + if (!taskDescription || typeof taskDescription !== 'string') { + return { valid: false, error: 'Task description is required' }; + } + + if (taskDescription.length === 0) { + return { valid: false, error: 'Task description is required' }; + } + + if (taskDescription.length > 10000) { + return { valid: false, error: 'Task description too long (max 10000 chars)' }; + } + + // Detail level validation (optional, defaults to 'standard') + if (detailLevel !== undefined) { + const validLevels = ['brief', 'standard', 'detailed']; + if (!validLevels.includes(detailLevel as string)) { + return { valid: false, error: 'Invalid detail level. Must be: brief, standard, or detailed' }; + } + } + + return { valid: true }; + }; + + describe('taskDescription validation', () => { + it('should reject missing taskDescription', () => { + const result = validateGeneratePlanRequest({}); + expect(result.valid).toBe(false); + expect(result.error).toContain('required'); + }); + + it('should reject null taskDescription', () => { + const result = validateGeneratePlanRequest({ taskDescription: null }); + expect(result.valid).toBe(false); + expect(result.error).toContain('required'); + }); + + it('should reject empty string taskDescription', () => { + const result = validateGeneratePlanRequest({ taskDescription: '' }); + expect(result.valid).toBe(false); + expect(result.error).toContain('required'); + }); + + it('should reject non-string taskDescription', () => { + const result = validateGeneratePlanRequest({ taskDescription: 123 }); + expect(result.valid).toBe(false); + expect(result.error).toContain('required'); + }); + + it('should reject taskDescription over 10000 chars', () => { + const longDescription = 'a'.repeat(10001); + const result = validateGeneratePlanRequest({ taskDescription: longDescription }); + expect(result.valid).toBe(false); + expect(result.error).toContain('too long'); + }); + + it('should accept taskDescription at exactly 10000 chars', () => { + const maxDescription = 'a'.repeat(10000); + const result = validateGeneratePlanRequest({ taskDescription: maxDescription }); + expect(result.valid).toBe(true); + }); + + it('should accept valid taskDescription', () => { + const result = validateGeneratePlanRequest({ + taskDescription: 'Implement user authentication with JWT tokens', + }); + expect(result.valid).toBe(true); + }); + + it('should accept multi-line taskDescription', () => { + const result = validateGeneratePlanRequest({ + taskDescription: `Fix the Ralph Loop wizard issues: +1. Two Skip buttons confusing +2. Modal height overflow +3. Dead code references`, + }); + expect(result.valid).toBe(true); + }); + }); + + describe('detailLevel validation', () => { + it('should accept brief detail level', () => { + const result = validateGeneratePlanRequest({ + taskDescription: 'Build a feature', + detailLevel: 'brief', + }); + expect(result.valid).toBe(true); + }); + + it('should accept standard detail level', () => { + const result = validateGeneratePlanRequest({ + taskDescription: 'Build a feature', + detailLevel: 'standard', + }); + expect(result.valid).toBe(true); + }); + + it('should accept detailed detail level', () => { + const result = validateGeneratePlanRequest({ + taskDescription: 'Build a feature', + detailLevel: 'detailed', + }); + expect(result.valid).toBe(true); + }); + + it('should accept missing detailLevel (defaults to standard)', () => { + const result = validateGeneratePlanRequest({ + taskDescription: 'Build a feature', + }); + expect(result.valid).toBe(true); + }); + + it('should reject invalid detailLevel', () => { + const result = validateGeneratePlanRequest({ + taskDescription: 'Build a feature', + detailLevel: 'verbose', + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid detail level'); + }); + + it('should reject numeric detailLevel', () => { + const result = validateGeneratePlanRequest({ + taskDescription: 'Build a feature', + detailLevel: 3, + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid detail level'); + }); + }); + + describe('edge cases', () => { + it('should handle taskDescription with only whitespace', () => { + // Whitespace-only is technically a valid non-empty string + // The API accepts it (server-side trim could be added if needed) + const result = validateGeneratePlanRequest({ taskDescription: ' ' }); + expect(result.valid).toBe(true); + }); + + it('should handle taskDescription with unicode characters', () => { + const result = validateGeneratePlanRequest({ + taskDescription: 'Implement 日本語 support with emoji 🎉', + }); + expect(result.valid).toBe(true); + }); + + it('should handle taskDescription with special characters', () => { + const result = validateGeneratePlanRequest({ + taskDescription: 'Fix bug: user@example.com fails with ', + }); + expect(result.valid).toBe(true); + }); + + it('should handle array as taskDescription', () => { + const result = validateGeneratePlanRequest({ + taskDescription: ['item1', 'item2'], + }); + expect(result.valid).toBe(false); + }); + + it('should handle object as taskDescription', () => { + const result = validateGeneratePlanRequest({ + taskDescription: { text: 'description' }, + }); + expect(result.valid).toBe(false); + }); + }); +});