This plan details how to integrate [OpenCode](https://opencode.ai) — the popular open-source AI coding CLI (111k+ GitHub stars, 75+ model providers) — into Codeman as a first-class session type alongside Claude Code and shell sessions.
Extend the existing `SessionMode` type from `'claude' | 'shell'` to `'claude' | 'shell' | 'opencode'`, and propagate this new mode through the tmux manager, session class, API routes, and frontend UI. OpenCode will be spawned inside tmux exactly like Claude Code, with its TUI rendered in xterm.js.
### Two Integration Strategies (Incremental)
| Strategy | Approach | Complexity | Value |
|----------|----------|------------|-------|
| **A: TUI-in-tmux** | Spawn `opencode` CLI in tmux, render in xterm.js | Medium | Full visual parity with Claude Code |
**Recommended path**: Start with Strategy A (TUI-in-tmux) since it mirrors the existing Claude Code pattern exactly. Then layer Strategy B on top for advanced features like structured token tracking and model switching.
### Scope Decision: MVP First, Claude-Coupled Systems Later
> **Decision (2026-02-26)**: The first integration ships **spawn + render + basic UI only**. The following Claude-coupled systems are **explicitly out of scope** for the initial integration and should NOT be attempted until OpenCode sessions are stable and we have real PTY output data to calibrate against:
>
> | System | Why Excluded | Prerequisite |
> |--------|-------------|--------------|
> | **Token tracking** | Claude-specific status line format. OpenCode uses different format inside Bubble Tea TUI. 75+ model providers = different cost/token semantics per model. | Phase 8 server API or verified TUI regex |
> | **Ralph Loop** | Depends entirely on `<promise>PHRASE</promise>` tags and `---RALPH_STATUS---` blocks — custom Claude protocols that don't exist in OpenCode. Would be a broken timeout-based prompt repeater. | Alternative completion signaling mechanism |
> | **Circuit breaker** | Signals (`consecutiveNoProgress`, `consecutiveTestsFailure`, `BLOCKED`) all come from `RALPH_STATUS` blocks. No OpenCode equivalent. | Ralph Tracker adaptation |
> | **AI idle checker** | Spawns `claude -p` for analysis. Won't work if only OpenCode is installed. | Claude CLI availability or `opencode run` fallback |
> **What ships in the MVP**: Phases 0-3 (spawn in tmux, render in xterm.js) + Phase 5 (API routes, mode selector, tab badges, create/kill sessions). Users can interact with OpenCode manually — type prompts, see output, manage sessions from the Codeman web UI. This alone is the core value: multi-model AI sessions in one management interface.
> **[REVIEW]** Multiple reviewers emphasized that Phase 0 findings are critical prerequisites for Phases 4-7. Do not write any code until these pass.
- [ ]**Capture raw PTY output** from an OpenCode session for idle detection calibration — record 5+ minutes of working vs idle output to measure actual patterns
- [ ]**Verify `writeViaMux()` input delivery** — Bubble Tea (Go) may handle `\r` (Enter) differently than Ink (React). Test `tmux send-keys -l "Hello"` followed by `tmux send-keys Enter` and confirm prompt submission works
- [ ]**Measure TUI redraw frequency** during idle state — does Bubble Tea emit cursor/timer redraws when the AI model is idle? This determines whether output-silence detection is viable at all
- [ ]**Test mouse protocol output** — Bubble Tea can enable mouse reporting (`CSI ?1000h`/`CSI ?1006h`), generating PTY output on mouse movements. This would defeat silence-based idle detection
- [ ]**Install a test OpenCode plugin** — write a minimal `.opencode/plugins/test.js` plugin, verify which events actually fire, confirm exact event names and callback signatures (the event names in Appendix B are speculative)
- [ ]**Test OpenCode auto-compaction behavior** — trigger compaction during a session, observe TUI output pattern (could appear as "working" to idle detector)
- [ ]**Check `opencode --version`** — record version for compatibility tracking. OpenCode is rapidly evolving; CLI flags and plugin API may change between versions
> **[REVIEW C5]** `SessionMode` is a named type export in `session.ts:204` (`export type SessionMode = 'claude' | 'shell'`). `types.ts:162` has an **inline anonymous union** (`mode?: 'claude' | 'shell'`) inside `SessionState` — NOT a named `SessionMode` type. Additionally, `mux-interface.ts` has 3 inline `'claude' | 'shell'` unions (MuxSession.mode, createSession param, respawnPane param). All 5 locations must stay in sync. **Fix**: Create `SessionMode` in `types.ts`, import and use it in `session.ts` and `mux-interface.ts`.
> **[REVIEW M6]** `createSession()` already has 8 positional parameters. Adding `openCodeConfig` as the 9th is a code smell. **Fix**: Refactor `createSession()` and `respawnPane()` in `TerminalMultiplexer` to accept an options object before adding OpenCode. This also eliminates the need to keep parameter order in sync.
Make `TmuxManager.createSession()` and `respawnPane()` support `mode: 'opencode'`.
This is the **critical integration point** — once OpenCode runs in tmux, everything downstream (PTY attachment, SSE streaming, xterm.js rendering) works automatically.
> **[REVIEW]** `createSession()` and `respawnPane()` already duplicate Claude command construction (lines 261-311 vs 426-454). Adding OpenCode creates a 3rd branch in both. **Fix**: Extract a shared `buildSpawnCommand(mode, options)` helper that both methods call.
#### New Helper: `setOpenCodeEnvVars()` (from review)
> **[REVIEW M7]** API keys passed via inline `export` in the tmux command are visible in `ps` output and tmux history. Use `tmux setenv` to set sensitive vars on the session instead.
```typescript
/**
* Set sensitive environment variables on a tmux session via setenv.
* These are inherited by panes but not visible in ps or tmux history.
*
* [RE-REVIEW] API keys may contain ", $, or other shell metacharacters.
* Use single-quote wrapping with escaped inner single quotes to prevent injection.
#### New Helper: `setOpenCodeConfigContent()` (from re-review)
> **[RE-REVIEW]** `OPENCODE_CONFIG_CONTENT` must NEVER be embedded in a shell command string — user-supplied JSON can contain shell metacharacters (`$()`, `;`, `&&`). Use `tmux setenv` instead, which treats the value as a raw string.
```typescript
/**
* Set OPENCODE_CONFIG_CONTENT on a tmux session via setenv.
> **[REVIEW P1-6]** OpenCode does not have `--session-id <codeman-id>` for initial session creation (only `--session <id>` for continuing existing sessions). This means:
> - `_claudeSessionId = this.id` correlation (session.ts:405) won't work for OpenCode
> - Subagent-session correlation via Session ID matching won't work
> - Transcript watching via Claude session ID path won't work
>
> **Mitigation**: Let OpenCode manage its own sessions. Store the OpenCode session ID mapping in `SessionState` after the first session is created.
> **[REVIEW P1-7]** The `toState()` method in `session.ts:782-816` must be updated to include `openCodeConfig` in the serialized state. Without this, state persistence silently drops the config and OpenCode sessions can't be restored after server restart.
## 9. Phase 4: Output Parsing & Idle Detection *(DEFERRED — not in MVP)*
> **DEFERRED**: This entire phase (except `waitForOpenCodeReady()`) is out of MVP scope. Idle detection, ANSI content filter, working/busy state tracking, and token parsing are all deferred until we have real PTY output data from stable OpenCode sessions. Only the basic TUI ready detection from `waitForOpenCodeReady()` is needed for the MVP and is included in Phase 3.
> **Update 2026-10-09 (working/idle shipped, from measured data):** the registry entry now declares `capabilities.workDetect` for opencode, measured on a live opencode 1.3.0 pane (pane captures every 250-300 ms through real turns at 40, 60, 120 and 200 columns, plus the raw PTY stream). Every composer row starts with a `┃` bar, which arms the shared screen-probed idle check; a running turn puts an 8-cell knight-rider spinner (`⬝■■■■■■⬝ esc interrupt`) at the head of the footer row, redrawn about every 40 ms, and `[⬝■]{8}` is the working line. The label is not the anchor: tmux ships `esc` and `interrupt` as separate words joined by cursor moves, and below about 45 columns the footer wraps it. At rest the TUI is silent (no cursor or timer redraws), and a pending permission prompt replaces the composer and stops the spinner, so it reads as idle. Before this, a turn that ran a tool latched the session `busy` for good (the tool row's braille spinner tripped the generic spinner detector, and nothing ever armed the idle check). Token parsing and the ANSI content filter remain deferred.
> **[REVIEW C1]** Bubble Tea redraws the **entire screen** on every update. Even when the AI model is idle, Bubble Tea may emit cursor blinking/repositioning sequences, timer-based redraws (e.g., clock display updates), and mouse protocol output. This means `lastActivityAt` will be constantly reset by TUI maintenance redraws, and the silence threshold will **never trigger**.
>
> **Mandatory fix**: Add an ANSI content-change filter between the PTY and idle detection. Only treat actual text content changes (not cursor movements, screen redraws, or mouse protocol output) as "real output".
```typescript
// New utility: src/utils/ansi-content-filter.ts
// Strips non-content ANSI sequences so idle detection only sees real text changes
> **[RE-REVIEW]** The code snippets below reference `this._idleConfig.silenceThresholdMs` but `_idleConfig` does not exist on `Session`. The `getIdleDetectionConfig()` helper's return value must be stored as a private property, initialized in the constructor:
### Gate `_processExpensiveParsers()` for OpenCode (from re-review)
> **[RE-REVIEW]** The 150ms-throttled `_processExpensiveParsers()` path in session.ts handles Ralph tracking, bash tool parsing, and token parsing — all Claude-specific. For OpenCode sessions, these must be gated:
> **[REVIEW M5]** 500ms silence threshold is too short — Bubble Tea may pause between component renders, triggering a false "ready" signal. Increased to 2000ms. Also: do NOT clear the terminal after ready detection — unlike Claude Code, the OpenCode TUI's initial render IS the useful content.
> **[REVIEW]** The original logic had a bug: `Date.now() - this._lastActivityAt > 100` is checked AFTER setting `_lastActivityAt = Date.now()`, so the condition would never be true (0 > 100 = false). Fixed below.
> **[REVIEW M1]** The `session.isWorking` property is used by the respawn controller as a final safety check. For Claude, it's set by detecting JSON messages and working patterns. For OpenCode, `isWorking` must be driven by the ANSI content filter — mark working when real content appears after a period of silence.
> **[RE-REVIEW]** `writeHooksConfig(casePath)` is called unconditionally at line 2625 in quick-start. Must guard with `mode !== 'opencode'` — Claude hooks are irrelevant for OpenCode sessions.
> Also: lifecycle logs at lines 1657 and 2675 hardcode `mode: 'claude'` — should use `session.mode`.
> **[REVIEW C4]** Also update `ALLOWED_ENV_PREFIXES` to include `'OPENCODE_'`, and add `OPENCODE_SERVER_PASSWORD` to `BLOCKED_ENV_KEYS` (security-sensitive). Also update `QuickStartSchema` to include `'opencode'` mode and `openCodeConfig` field.
>
> **[RE-REVIEW]** These MUST be an atomic change — shipping the `OPENCODE_` prefix addition without simultaneously blocking `OPENCODE_SERVER_PASSWORD` creates a security window where the server password can be set via API.
> **DEFERRED**: Plugin event names are speculative and unverified. This phase requires Phase 0 plugin verification first. The entire hooks/plugin system is out of MVP scope — OpenCode sessions in the MVP are manual-interaction only.
>
> **[REVIEW C3] Phase reordered**: This was originally Phase 7 but has been moved before Respawn. The plugin bridge provides the reliable `session.idle` event that the respawn controller needs. Without it, respawn relies on output-silence-only detection, which is unreliable with Bubble Tea TUIs (see Review C1). **Do not implement respawn (Phase 7) until this phase is complete and tested.**
> **[REVIEW]** Add `session.compacted` to the plugin bridge — it's free and useful for tracking when OpenCode auto-compacts (relevant for token tracking and respawn timing).
> **[REVIEW M3]** The event names in this section and Appendix B are speculative — they have not been verified against OpenCode's actual plugin API. Event names, callback signatures, and firing timing may differ from what's documented here.
>
> **HARD PREREQUISITE**: Before implementing this phase, complete Phase 0's plugin verification checklist — install a test plugin and confirm which events actually fire and when. The `session.idle` event is particularly critical: does it fire when the model finishes generating, or when the TUI returns to the input prompt? This distinction matters enormously for idle detection timing.
## 12. Phase 7: Respawn & Ralph Loop Adaptation *(DEFERRED — not in MVP)*
> **DEFERRED**: Respawn controller (3,500 lines, 13 states), Ralph Loop, and Ralph Tracker are all deeply coupled to Claude's output protocols (`Worked for Xm Xs`, `<promise>` tags, `---RALPH_STATUS---` blocks, spinner chars, `❯` prompt). With 75+ model providers in OpenCode producing different output formats, these systems will break in unpredictable ways. Deferring until we have: (1) stable OpenCode sessions with real PTY data, (2) verified plugin bridge from Phase 6, (3) empirical idle detection calibration.
>
> **[REVIEW C3] Phase reordered**: This was originally Phase 6 but has been moved after Plugin Bridge. The plugin bridge's `session.idle` event is the primary reliable idle signal for OpenCode sessions. Implementing respawn before the bridge exists means relying on output-silence-only detection, which is unreliable.
>
> **[REVIEW]** Respawn should ship **disabled by default** for OpenCode sessions. Enable only after the plugin bridge (Phase 6) is proven to work reliably.
> **[REVIEW C2]** The RespawnController is ~3,500 lines with 13 states, `assertNever()` exhaustive switches in multiple places, and dozens of methods. Sprinkling `if (mode === 'opencode')` throughout would be unmaintainable. **Fix**: Create a `CompletionDetector` interface.
```typescript
// New interface in src/types.ts or src/completion-detector.ts:
interfaceCompletionDetector{
/** Check if terminal output indicates the AI has finished */
isCompletionMessage(data: string):boolean;
/** Check if terminal output indicates the AI is currently working */
hasWorkingPattern(data: string):boolean;
/** Get the idle timeout for this backend */
getIdleTimeoutMs():number;
/** Get the completion silence threshold */
getCompletionSilenceMs():number;
/** Whether this detector supports AI-powered idle checking */
The respawn controller sends Claude CLI commands (`/clear`, `/compact`, `/init`, `/update`) which are Claude-specific. For OpenCode, we need alternative commands.
#### Command Mapping
| Claude Code | OpenCode Equivalent | Notes |
|-------------|---------------------|-------|
| `/clear` | `/clear` | Same command! Clears conversation |
| `/update prompt` | Just type the prompt | Direct text input |
| `/resume` | `--continue` flag on restart | Handled at spawn time |
> **[REVIEW M4]** `writeViaMux()` sends `\r` for Enter — it is untested whether Bubble Tea handles this correctly. This is a Phase 0 validation item. If Bubble Tea requires a different key sequence, the entire respawn command delivery mechanism fails.
#### Changes to `src/respawn-controller.ts`
> **[REVIEW]** Use the `CompletionDetector` interface instead of inline mode checks.
```typescript
// In the respawn cycle method:
privateasyncstartRespawnCycle():Promise<void>{
if(this.session.mode==='opencode'){
// OpenCode respawn: simpler cycle
// 1. Wait for idle (plugin bridge session.idle event OR output silence via content filter)
// 2. Optionally compact: skip (OpenCode manages this internally via compaction.auto)
// 3. Send new prompt via writeViaMux()
// 4. Wait for completion (plugin bridge session.idle event again)
// Send prompt directly — OpenCode TUI accepts typed text
> **[REVIEW]** These items are NOT addressed above and need design work:
>
> 1. **Circuit breaker signals for OpenCode**: The circuit breaker tracks `no-progress`, `same-error-repeated`, and `tests-failing-too-long`. These signals come from Ralph tracker's `RALPH_STATUS` blocks, which are Claude-specific output patterns. Need alternative signal sources for OpenCode (possibly via plugin bridge `session.error` events).
>
> 2. **`/compact` equivalent**: The plan says "OpenCode handles this internally via `compaction.auto`" — but the respawn controller may need to trigger compaction at specific points in the cycle. No programmatic compaction control is available without Phase 8's server API.
>
> 3. **`sendUpdateDocs()` equivalent**: The respawn cycle starts by sending a progress-summary prompt that writes to CLAUDE.md. For OpenCode, what prompt makes sense? OpenCode uses `opencode.json`, not CLAUDE.md.
>
> 4. **No `elicitation_dialog` equivalent**: Claude Code has hooks that signal when Claude asks the user a question. The respawn controller uses this to skip auto-accept. OpenCode may not have an equivalent — if auto-accept is enabled, it could accept prompts that are actually questions.
### Ralph Loop
> **[REVIEW]** Ralph Loop for OpenCode should be marked **experimental** and deferred to a later phase. Without `<promise>PHRASE</promise>` tags, Ralph loses its primary completion signal and becomes a timeout-based prompt repeater — a significantly degraded experience.
The Ralph Loop's polling/assignment mechanism is CLI-agnostic (it uses `session.sendInput()` and `writeViaMux()`). However, its completion signaling is Claude-specific: `sessionCompletion` events are only emitted when ralph-tracker detects `<promise>PHRASE</promise>` tags in output, which is a custom Claude protocol. The main adaptation needed:
```typescript
// In ralph-loop.ts:
// Completion detection for OpenCode:
if(session.mode==='opencode'){
// Use plugin bridge session.idle event (preferred) or silence-based detection (fallback)
// Ralph tracker's completion phrase detection is skipped
// WARNING: This makes Ralph Loop significantly less reliable for OpenCode
}
```
### Respawn Presets for OpenCode *(DEFERRED)*
> These presets are deferred with the rest of Phase 7. Values below are speculative — they need calibration against real PTY output data from stable OpenCode sessions.
```javascript
// DEFERRED — add to app.js when Phase 7 ships:
constOPENCODE_RESPAWN_PRESETS={
'opencode-solo':{
label:'OpenCode Solo Work',
idleTimeoutSec:8,// Longer than Claude (TUI rendering creates brief output bursts)
maxDurationMin:60,
completionSilenceMs:8000,
},
'opencode-autonomous':{
label:'OpenCode Autonomous',
idleTimeoutSec:15,
maxDurationMin:480,
completionSilenceMs:10000,
},
};
```
---
## 13. Phase 8: OpenCode Server API Integration (Advanced) *(DEFERRED — not in MVP)*
> **DEFERRED**: Advanced feature. Requires stable MVP sessions first, plus resolution of shared SQLite state problem (TUI + serve dual-process).
> **[REVIEW M8]** Running both `opencode` (TUI) and `opencode serve` in the same project directory creates a **shared SQLite state problem**. The TUI creates sessions in SQLite; the server reads from the same SQLite. But they're separate processes — there's no guarantee of session ID consistency between them.
> **[REVIEW]** The original `Array.from(this.sessions.keys()).indexOf(this.id)` is fragile — session deletion creates gaps, concurrent creation can collide. Use an incrementing counter or port pool instead.
// The proxy needs to inject: Authorization: Basic base64("opencode:" + password)
this._openCodeServerAuth=Buffer.from(`opencode:${crypto.randomUUID()/* generateSessionToken() does not exist — use crypto.randomUUID() */}`).toString('base64');
- **P1** (38 tasks): Full integration — CLI resolver, API routes, Zod schemas, config management, respawn adaptation, Ralph tracker, BashToolParser, hooks, frontend UI, subagent watcher, env vars, integration tests, documentation
- **P2** (8 tasks): Advanced — `opencode serve` integration, API client, Ollama model management, cost tracking, mixed-backend Ralph Loop
The `plan.json` approach is more heavyweight (full backend abstraction with DI), suitable if we plan to add more backends. This document recommends the simpler "mode extension" approach for initial implementation.
> **[REVIEW]** `plan.json` and this document are somewhat contradictory — plan.json recommends Option B (full DI abstraction with `LLMBackend`, `ClaudeBackend`, `OpenCodeBackend`, `BackendFactory`) while this document recommends Option A (simple mode extension). The P0 tasks in plan.json (14 tasks for backend abstraction) should be marked "skip for now" or removed. Some P1 tasks reference `BackendFactory` while others just extend `SessionMode`. **Reconcile before implementation begins.**
- **Feature gating** (Phase 5) — Just `if` checks on `session.mode`
### Medium Risk
- **Tmux command construction** (Phase 3) — Must handle: missing binary, bad model string, env var escaping, shell metacharacter injection. Mitigated by following the exact Claude pattern and input validation.
- **Idle detection** (Phase 4) — Output silence is less precise than prompt detection. **[REVIEW C1]** Bubble Tea's continuous screen redraws mean raw output silence will never trigger — the ANSI content filter (mandatory) mitigates this but needs Phase 0 calibration.
- **`configContent` field** (Phase 3) — Arbitrary JSON passed as `OPENCODE_CONFIG_CONTENT`. A malicious client could inject any OpenCode config, including custom tool definitions or MCP servers. Mitigated by shell escaping, but consider validating JSON structure (whitelist allowed keys) or marking as admin-only.
- **Respawn for OpenCode** (Phase 7) — **[REVIEW C2, C3]** The respawn controller is 3,500 lines with 13 states. Without the plugin bridge's `session.idle` event, idle detection relies solely on output silence through the ANSI filter — the weakest possible detection. **Mitigated by**: shipping respawn disabled by default for OpenCode, requiring Phase 6 plugin bridge first, using `CompletionDetector` interface instead of mode-check spaghetti.
- **OpenCode server API** (Phase 8) — **[REVIEW M8]** Running TUI + serve alongside creates a shared SQLite state problem. Port allocation races, zombie processes, auth header injection needed. Mitigated by making it optional and considering serve-only alternative.
- **Plugin bridge reliability** (Phase 6) — **[REVIEW M3]** Plugin event names are speculative. The generated plugin depends on OpenCode loading it correctly and the fetch calls not failing silently. Need Phase 0 verification of actual event names and signatures.
- **Ralph Loop for OpenCode** — **[REVIEW]** Without `<promise>` tags, Ralph loses its primary completion signal. Becomes a timeout-based prompt repeater — significantly degraded experience. Should be marked experimental.
- **[REVIEW]** Plugin event names and callback signatures — are the names in Appendix B correct?
- **[REVIEW]** `writeViaMux()``\r` handling — does Bubble Tea handle Enter the same way as Ink?
- **[REVIEW]** OpenCode auto-compaction behavior — what does TUI output look like during compaction?
- **[REVIEW]** OpenCode version compatibility — CLI flags and plugin API may change between versions
### Additional Risks (from review)
- **API key exposure** — **[REVIEW M7]** Passing API keys via inline `export` in tmux commands exposes them in `ps` output and tmux history. Mitigated by using `tmux setenv` instead.
- **Network firewall risk** — **[REVIEW]** OpenCode's `opencode serve` binds to ports — in containerized/firewalled environments, this may be blocked.
- **OpenCode version compatibility** — **[REVIEW]** No version pinning or check. OpenCode is rapidly evolving; CLI flags, plugin API, and serve endpoints could change between versions. Add version detection in CLI resolver and document minimum supported version.
- **No `elicitation_dialog` equivalent** — **[REVIEW]** Claude Code has hooks that signal when Claude asks a question. OpenCode may not have equivalent. If auto-accept is enabled for OpenCode, it could accept prompts that are actually questions to the user.
- **[REVIEW]** If only OpenCode is installed (no Claude CLI), AI checkers silently fail — removing two important safety layers
- **[REVIEW]** `opencode run --format json` could serve as an alternative AI checker backend
- Recommendation: Always use Claude for AI checks if available. If Claude CLI is not available but OpenCode is, consider using `opencode run` as a fallback. Document this limitation.
### Recommended Sequence (updated per re-review — MVP-first approach)
> **[SCOPE DECISION]** Token tracking, respawn controller, Ralph Loop/Tracker, circuit breaker, hooks plugin bridge, and AI idle checker are **all deferred**. These systems are deeply coupled to Claude's output format and protocols. With 75+ model providers in OpenCode, they will break in unpredictable ways. Ship spawn + render + basic UI first, then layer intelligence on top with real PTY data.
> **Note on Phase 4**: Only `waitForOpenCodeReady()` (basic TUI ready detection) is needed for the MVP. The full idle detection, ANSI content filter, working/busy state tracking, and token parsing are all deferred. The MVP treats OpenCode sessions as "manual interaction only" — no automated idle/completion detection.
| C2 | Respawn controller is ~3,500 lines with 13 states (`watching`, `confirming_idle`, `ai_checking`, `sending_update`, `waiting_update`, `sending_clear`, `waiting_clear`, `sending_init`, `waiting_init`, `monitoring_init`, `sending_kickstart`, `waiting_kickstart`, `stopped`) — can't just add `if (mode === 'opencode')` | Added `CompletionDetector` interface pattern in Phase 7 |
| C3 | Phase 6 (Respawn) depends on Phase 7 (Plugin Bridge) for reliable idle detection | Swapped phases: Plugin Bridge is now Phase 6, Respawn is Phase 7 |
| C4 | `schemas.ts` env var allowlist blocks `OPENCODE_*` vars | Added `OPENCODE_` to `ALLOWED_ENV_PREFIXES`, `OPENCODE_SERVER_PASSWORD` to `BLOCKED_ENV_KEYS` |
| C5 | `SessionMode` defined separately in `session.ts` and `types.ts` — will drift | Unified: move to `types.ts`, import in `session.ts` |
### Major Issues (8)
| ID | Issue | Resolution |
|----|-------|------------|
| M1 | No `session.isWorking` strategy for OpenCode | Added ANSI content filter-driven `isWorking` logic in Phase 4 |
| M2 | Working pattern detection is Claude-specific | Deferred to Phase 0 empirical data + OpenCodeCompletionDetector |
| M3 | Plugin event names are speculative / unverified | Added hard prerequisite: Phase 0 must verify events via test plugin |
| M4 | `writeViaMux()``\r` handling untested with Bubble Tea | Added to Phase 0 validation checklist |
| M5 | `waitForOpenCodeReady()` 500ms threshold too short | Increased to 2000ms, added content-change-only tracking |
| M6 | `createSession()`/`respawnPane()` have 8 positional params | Refactored to options objects (`CreateSessionOptions`, `RespawnPaneOptions`) |
| M7 | API keys exposed in `ps` output via inline `export` | Use `tmux setenv` for sensitive vars instead |
| M8 | Phase 8 TUI + serve dual-process has shared SQLite problem | Documented alternatives (serve-only, attach pattern), port pool allocation |
### Key Design Decisions Made During Review
1.**Simple extension (Option A) confirmed** — correct YAGNI call over full DI abstraction
2.**`CompletionDetector` interface** — clean abstraction boundary between respawn controller and backend-specific detection
3.**ANSI content filter** — mandatory new component, not a nice-to-have
4.**Respawn disabled by default** for OpenCode — opt-in only after plugin bridge proven
5.**Ralph Loop marked experimental** for OpenCode — without `<promise>` tags, it's a degraded experience
6.**`tmux setenv`** for API keys — security improvement over inline exports
7.**Phase 0 expanded** — additional validation items make it 1-2 hours instead of 30 minutes
8.**plan.json needs reconciliation** — contradicts this document's approach (DI vs simple extension)
---
## 22. Re-Review Findings
> **Re-reviewed**: 2026-02-26 by a 4-agent team (types-reviewer, parsing-reviewer, respawn-reviewer, api-reviewer). Each agent cross-referenced specific plan sections against the actual codebase.
| **parsing-reviewer** | Output parsing, idle detection (Phase 4) | 6 bugs in code snippets: wrong property names, invalid status value, missing property init, misdescribed Claude detection |
| **respawn-reviewer** | Respawn, Ralph Loop, hooks (Phases 6-7) | 13 states not 11, Ralph Loop "agnostic" overstated, CompletionDetector feasible but scope underestimated |
| RC1 | `_lastOutputTime` property does not exist — actual is `_lastActivityAt`. All Phase 4 snippets used wrong name. | Find-and-replace applied throughout |
| RC2 | `_status = 'working'` is invalid — `SessionStatus` is `'idle' \| 'busy' \| 'stopped' \| 'error'`. Must be `'busy'`. | Fixed in code snippets |
| RC4 | `OPENCODE_SERVER_PASSWORD` must be blocked simultaneously with adding `OPENCODE_` to `ALLOWED_ENV_PREFIXES` | Added atomic-change note to schemas section |
### High Issues (7)
| ID | Issue | Resolution |
|----|-------|------------|
| RH1 | 13 respawn states, not 11 — plan undercounted by 2 (`monitoring_init`, `sending_kickstart`) | Fixed all references to 13 |
| RH2 | Missing `_idleConfig` property — referenced but never initialized | Added constructor initialization code |
| RH3 | `_processExpensiveParsers()` not addressed — Ralph/bash/token parsers fire for all modes | Added mode-gating section to Phase 4 |
| RH4 | `this.cleanup.addCustom()` doesn't exist — real API is `registerCleanup(type, fn, description)` | Fixed in Phase 8 code |
| RH5 | `generateSessionToken()` doesn't exist anywhere in codebase | Replaced with `crypto.randomUUID()` |
| RH6 | `setOpenCodeEnvVars()` — API keys with `"` or `$` break the tmux setenv command | Added single-quote escaping with inner quote escape |
| RH7 | `writeHooksConfig()` called unconditionally in quick-start — generates Claude hooks for OpenCode sessions | Added `mode !== 'opencode'` guard |
### Medium Issues (6)
| ID | Issue | Resolution |
|----|-------|------------|
| RM1 | "Duplicate SessionMode" (C5) imprecise — types.ts has inline union, not named export. mux-interface.ts has 3 locations, not 2. | Corrected description in C5 note |
| RM2 | Claude idle detection misdescribed — uses prompt char + debounce + spinner, NOT silence threshold. No `completionPattern` in session.ts. | Added correction comment to `getIdleDetectionConfig()` |
| RM3 | Ralph Loop "CLI-agnostic" overstated — polling is agnostic but completion signaling depends on Claude-specific `<promise>` tags | Reworded in Phase 7 |
| RM4 | Lifecycle logs hardcode `mode: 'claude'` at 2 locations in server.ts | Added note + fix in Phase 5 quick-start section |
| RM5 | Respawn section line number wrong (10254 → ~10289) | Fixed |
| RM6 | `completionPattern: /Worked for \d+[ms]/` shown in session.ts config but actually lives in respawn-controller.ts | Fixed — set to `null` in session config with comment |
### Confirmed Correct (from original review)
All items confirmed correct by the original review remain valid. Additionally confirmed:
-`createSession()` has 8 positional params, `respawnPane()` has 7 (no `name` param)