# Ticket 1.1 — OpenCode Process Manager

**Phase**: 1 — OpenCode Bridge (Core)
**Priority**: Critical (blocking)
**Depends on**: 0.1, 0.2

## Description

Manage the lifecycle of the OpenCode server process (`opencode serve`). The relay needs OpenCode's HTTP API running to function. This module starts it as a child process, monitors health, and handles shutdown — or connects to an already-running instance.

## Acceptance Criteria

### AC1: Starts OpenCode server as child process

```gherkin
Given OpenCode is installed and available on PATH
And no OpenCode server is currently running on the target port
When the process manager starts
Then it spawns `opencode serve` as a detached child process
And it captures stdout/stderr for logging
And the child process runs on a configurable port (default: 4096)
```

### AC2: Health check confirms server readiness

```gherkin
Given the OpenCode child process has been spawned
When the process manager polls the health endpoint
Then it retries with exponential backoff (100ms, 200ms, 400ms, ...)
And it succeeds when the server responds to a GET request
And it times out after 30 seconds with a clear error message
```

### AC3: Connects to existing OpenCode instance

```gherkin
Given an OpenCode server is already running on port 4096
When the process manager starts with --oc-port 4096
Then it detects the running instance via health check (GET /global/health → { healthy: true, version })
And it skips spawning a new child process
And it logs "Connected to existing OpenCode instance on port 4096"
```

### AC4: Handles OpenCode crash and restart

```gherkin
Given the OpenCode child process is running
When the child process exits unexpectedly
Then the process manager detects the exit event
And it logs the exit code and signal
And it attempts to restart after a 2-second delay
And it gives up after 3 consecutive failures within 30 seconds
And it notifies the relay server of the failure
```

### AC5: Graceful shutdown stops child process

```gherkin
Given the OpenCode child process is running
When the process manager receives a shutdown signal
Then it sends SIGTERM to the child process
And it waits up to 5 seconds for graceful exit
And it sends SIGKILL if the process hasn't exited
And it resolves only after the child process is fully stopped
```

### AC6: Port conflict detection

```gherkin
Given another process is using the target port
And the process is NOT an OpenCode server
When the process manager starts
Then it reports a clear error: "Port {port} is in use by another process"
And it suggests using --oc-port to specify a different port
And it does NOT attempt to kill the other process
```

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| `startProcess(config)` | Unit | Spawns with correct args, port, detached flag |
| `healthCheck(port)` | Unit | Retries with backoff, succeeds on 200, times out correctly |
| `connectExisting(port)` | Unit | Detects running instance, skips spawn |
| `handleCrash(exitCode)` | Unit | Restarts with delay, gives up after max retries |
| `shutdown()` | Unit | Sends SIGTERM, escalates to SIGKILL, resolves on exit |
| Port conflict | Fixture | Detects occupied port, reports useful error |
| Full lifecycle | Fixture | Start → health check → serve → shutdown |

## API Shape

```ts
// src/lib/opencode-bridge.ts (process manager section)
export class OpenCodeProcess {
  constructor(options = {})           // { port, command, maxRetries, healthTimeout }
  async start()                       // Start or connect to OpenCode
  async stop()                        // Graceful shutdown
  isRunning()                         // Boolean health status
  getPort()                           // Current port number
  on(event, handler)                  // Events: 'ready', 'crash', 'stopped', 'error'
}
```

## Reference

- claude-relay does NOT manage Claude Code's process (the SDK handles it internally)
- OpenCode's server: `../opencode/packages/opencode/src/cli/cmd/serve.ts`
- OpenCode default port: 4096 (see `server.ts`)
- OpenCode health endpoint: `GET /global/health` → `{ healthy: true, version: string }`
- OpenCode supports `--port` and `--hostname` flags for `opencode serve`
- OpenCode supports HTTP Basic Auth via `OPENCODE_SERVER_PASSWORD` env var
