Problem
charter serve hangs indefinitely when the .ai/ directory is missing or invalid. The process must be killed — it never exits on its own.
$ charter serve --ai-dir /tmp/nonexistent
# (hangs forever, no output, no exit)
The error-guard logic in serveCommand() is correct — it detects the missing path, writes a JSON-RPC error to stdout, and throws CLIError. But the process cannot exit.
Root Cause
serve.ts imports StdioServerTransport at the top of the file:
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
The MCP SDK's StdioServerTransport binds process.stdin to a raw/flowing read loop at module load time. This registers an active handle in Node's event loop before any command logic runs. Even after CLIError is thrown and caught, the stdin handle keeps the process alive indefinitely.
Confirmed root cause:
require('@modelcontextprotocol/sdk/server/stdio.js');
process.exit(0); // ← never exits
Because serve.ts is unconditionally imported by index.ts, every charter invocation loads the MCP SDK and hooks stdin — not just charter serve.
Steps to Reproduce
charter serve --ai-dir /tmp/nonexistent
- Observe: process hangs, no output
- Confirm:
timeout 3 charter serve --ai-dir /tmp/nonexistent; echo $? → exit 124 (killed by timeout)
Expected Behavior
Process exits within ~100ms with a non-zero code and a clear error message:
charter: No .ai/ directory found. Run: charter init
Actual Behavior
Process hangs indefinitely. Agents and shells must kill it via timeout or SIGKILL.
Fix
Replace the top-level import with a dynamic import inside serveCommand(), after all path/manifest guards have passed:
// After fs.existsSync guards pass — acquire stdin only on the happy path
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
const stdioTransport = new StdioServerTransport();
await server.connect(stdioTransport);
Also add a child-process integration test that asserts charter serve --ai-dir /tmp/nonexistent exits within 1s with non-zero status.
Environment
- Charter CLI: v0.15.1
- Discovered during: ADX-008 dogfood session (v0.15.1 release validation)
Problem
charter servehangs indefinitely when the.ai/directory is missing or invalid. The process must be killed — it never exits on its own.The error-guard logic in
serveCommand()is correct — it detects the missing path, writes a JSON-RPC error to stdout, and throwsCLIError. But the process cannot exit.Root Cause
serve.tsimportsStdioServerTransportat the top of the file:The MCP SDK's
StdioServerTransportbindsprocess.stdinto a raw/flowing read loop at module load time. This registers an active handle in Node's event loop before any command logic runs. Even afterCLIErroris thrown and caught, the stdin handle keeps the process alive indefinitely.Confirmed root cause:
Because
serve.tsis unconditionally imported byindex.ts, everycharterinvocation loads the MCP SDK and hooks stdin — not justcharter serve.Steps to Reproduce
charter serve --ai-dir /tmp/nonexistenttimeout 3 charter serve --ai-dir /tmp/nonexistent; echo $?→ exit 124 (killed by timeout)Expected Behavior
Process exits within ~100ms with a non-zero code and a clear error message:
Actual Behavior
Process hangs indefinitely. Agents and shells must kill it via timeout or SIGKILL.
Fix
Replace the top-level import with a dynamic import inside
serveCommand(), after all path/manifest guards have passed:Also add a child-process integration test that asserts
charter serve --ai-dir /tmp/nonexistentexits within 1s with non-zero status.Environment