Skip to content

Commit 3f5bc0e

Browse files
patricka3125claudehaofeif
authored
feat: Add OpenCode CLI provider support (#193)
* feat(opencode): Phase 1 — foundation primitives for OpenCode CLI provider - Add ProviderType.OPENCODE_CLI = "opencode_cli" to the provider enum - Add OPENCODE_CONFIG_DIR / OPENCODE_AGENTS_DIR / OPENCODE_CONFIG_FILE path constants pointing at ~/.aws/opencode_cli/ - New OpenCodeAgentConfig Pydantic model (description, mode, permission) that serializes to OpenCode-compatible YAML frontmatter via frontmatter.dumps() - New cao_tools_to_opencode_permission() translator: two-step algorithm from §9 of the design doc (shorthand expansion + CAO-category → OpenCode tool mapping + hardcoded non-vocabulary deny/allow policies) - New opencode_config.py read-modify-write helper for the shared opencode.json (upsert_mcp_server, upsert_agent_tools, remove_agent_tools, read_config, write_config) - Port 5 TUI probe captures into test/providers/fixtures/ (plain + ANSI variants for idle-splash, idle-post-completion, processing, completed, permission states) - 54 new unit tests covering all Phase 1 modules; all 1368 tests pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(opencode): Phase 1 review items — ANSI fixture, model cleanup, guard-rail Item 1: Replace opencode_cli_processing.ansi.txt with a genuine PROCESSING frame re-captured via tmux probe (md5 9cbe2723, distinct from completed frame). Add test/providers/fixtures/OPENCODE_FIXTURES.md documenting all fixture sources and the remaining idle_post_completion.ansi.txt reuse. Item 2: Remove dead Pydantic v1 `class Config: exclude_none = True` block from OpenCodeAgentConfig — it is a no-op under Pydantic v2. Item 3: Add inline comment to OpenCodeAgentConfig.permission documenting the deliberate Phase 1 type simplification and when to widen it. Item 4: Replace unreachable `else: result[tool] = "deny"` in opencode_permissions.py with `raise AssertionError(...)` so any future tool added to ALL_OPENCODE_TOOLS without a policy update fails loudly. Item 5: Add test_noop_on_completely_missing_file to TestRemoveAgentTools — exercises the read_config() skeleton-return path when opencode.json does not exist yet. All 1369 tests pass; mypy/black/isort clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(install): add opencode_cli provider branch with --auto-approve flag Adds the `opencode_cli` branch to `cao install` (Phase 2): - writes agent `<name>.md` with YAML frontmatter (description, mode, permission) using compose_agent_prompt for the body and cao_tools_to_opencode_permission for the per-tool allow/ask/deny map - `--auto-approve` flag emits `allow` instead of `ask` for permitted tools; has no effect on other providers - if the agent profile declares mcpServers, upserts top-level mcp/tools entries (default-deny) and per-agent tool re-enables into opencode.json - full unit-test coverage in test/cli/commands/test_install_opencode.py (fresh install, idempotency, auto-approve, MCP wiring, config preservation, safe filename) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(install): apply Phase 2 review polish - Rename agent_config_oc → agent_config in opencode_cli branch for consistency with Kiro/Q/Copilot sibling branches (Item 1) - Strengthen test_agent_md_has_body: assert sentinel prompt text via profile.prompt frontmatter field instead of weak non-empty check (Item 2) - Bump live smoke-test subprocess timeout 30s → 60s to survive cold-cache npm plugin installs on CI (Item 4) Items 3 (MCP collision coverage already in Phase 1) and 5 (context-file parent mkdir — out of Phase 2 scope) intentionally not addressed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(providers): add OpenCodeCliProvider (Phase 3) Implements the opencode_cli runtime provider per §8 of the design doc: - OpenCodeCliProvider with full BaseProvider interface (initialize, get_status, extract_last_message_from_script, exit_cli, cleanup) - 5-state detection (IDLE/PROCESSING/COMPLETED/WAITING_USER_ANSWER/ERROR) with line-level position guard against stale alt-screen esc-interrupt remnants (lesson #16) - COMPLETED vs IDLE-post-completion distinguished by checking for a subsequent ▣ token after the last full completion marker - 120s initialize() timeout for first-run npm install cold-start (§8.2) - Inline-env launch command with all stability env vars (§5) - --model flag included only when profile.model is set (§3.1 exception) - Registered in ProviderManager; "opencode_cli" added to PROVIDERS_REQUIRING_WORKSPACE_ACCESS in launch.py - 43 unit tests at 96% line coverage against Phase 1 fixtures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add Phase 3 OpenCode provider runtime development walkthrough report Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(opencode): Phase 3 review polish — correct report line count, add dual-pattern comment Item 1: development report corrected 125 → 332 lines for opencode_cli.py. Item 4: inline comment at extract_last_message_from_script explains why the unanchored r"┃\s{2}" is used instead of the module-level USER_MESSAGE_PATTERN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(opencode): Phase 4 — e2e test, provider docs, README/CHANGELOG - test/e2e/conftest.py: add require_opencode fixture (skips if opencode not on PATH) - test/e2e/test_assign.py: add TestOpenCodeCliAssign with data_analyst, report_generator, and assign_with_callback tests covering all four orchestration modes - docs/opencode-cli.md: new provider doc covering prerequisites, quick start, config isolation, permission/tool mapping, MCP wiring, known limitations, troubleshooting - README.md: add opencode_cli row to provider table + cao launch example - CHANGELOG.md: add Unreleased entry announcing OpenCode CLI provider Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add Phase 4 OpenCode e2e and docs development walkthrough report Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(opencode): translate CAO mcpServer format to OpenCode's opencode.json format (Phase 3 regression) CAO profiles store MCP servers with {type: "stdio", command: str, args: list}. OpenCode's opencode.json requires {type: "local", command: list, enabled: true}. The install branch was passing raw CAO config directly, causing OpenCode to reject the config with "Configuration is invalid: Invalid input mcp.cao-mcp-server". Fix: add translate_mcp_server_config() to opencode_config.py and call it in the opencode_cli install branch before upsert_mcp_server(). Also translates env→environment. 6 unit tests added for the translator. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(opencode): add --yolo DANGEROUS caveat to permission troubleshooting (Phase 4 review polish) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update Phase 4 report with e2e results and Phase 3 regression fix notes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(opencode): native skill discovery via OPENCODE_CONFIG_DIR/skills symlink At install time, create a skills → SKILLS_DIR symlink under OPENCODE_CONFIG_DIR so OpenCode auto-discovers CAO skills through its native skill tool (§5.1). Uses profile.system_prompt or profile.prompt as the lean agent body — the skill catalog is no longer baked into the OpenCode system prompt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(opencode): handle Nm Ns duration format and extend extraction buffer Two status/extraction bugs revealed by e2e runs with full system prompts: 1. COMPLETION_MARKER_PATTERN now matches the Nm Ns duration format that OpenCode emits for responses that take more than 60 seconds (e.g. "1m 8s"). The old pattern only matched the pure-seconds form, causing get_status() to stall at PROCESSING indefinitely for longer turns. 2. Add extraction_tail_lines property to BaseProvider (default None) and override to 2000 in OpenCodeCliProvider. terminal_service.get_output uses this value for the LAST-mode tmux capture so long responses don't push the user-message marker (┃ ) beyond the 200-line default window. Status-check captures are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(opencode): single capture-pane per get_output call; add wrong-target symlink test Item 2: Eliminate double capture-pane in get_output(mode=LAST). Previously the function always captured at 200 lines then recaptured if the provider declared extraction_tail_lines. Now FULL mode returns after a single capture at the default depth; LAST mode resolves extract_lines from the provider once and makes exactly one capture before the retry loop. Item 1: Add test_warns_and_skips_when_symlink_points_elsewhere to TestEnsureSkillsSymlink, covering the branch at opencode_config.py:37-42 where the target is a symlink that resolves to a different directory. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(opencode): rename on-disk config directory from opencode_cli to opencode Change OPENCODE_CONFIG_DIR from ~/.aws/opencode_cli to ~/.aws/opencode in constants.py; OPENCODE_AGENTS_DIR and OPENCODE_CONFIG_FILE update transitively. Update all path string references in docs, CHANGELOG, and the constants unit test. Provider identifier (ProviderType.OPENCODE_CLI.value == "opencode_cli") is unchanged. Add CHANGELOG migration note for users who need to re-run cao install. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(opencode): fall back to first agent-indented line when user message scrolled off viewport OpenCode renders in alt-screen mode so the tmux scrollback only holds the current visible frame (~41 lines, history_size≈2). For long responses the user-message bar (┃ ) scrolls off the top before extraction runs, causing "No user message found". When no ┃ is found before the completion marker, scan for the first 5-space-indented agent line as the left boundary instead of raising. The visible frame already contains only the current turn's content, so multi-turn disambiguation is not needed here. Adds unit test test_fallback_extracts_when_user_message_scrolled_off. e2e: 3/3 PASSED in 161s on port 9888. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(terminal_service): guard build_skill_catalog() call and update skill-delivery comments Cleanup 1: build_skill_catalog() now runs only when the provider is in RUNTIME_SKILL_PROMPT_PROVIDERS, skipping the file reads/YAML parsing/Pydantic validation for providers that deliver skills natively (OpenCode symlink, Kiro skill:// resources) or via install-time baking (Q, Copilot). The skill_prompt kwarg at the create_provider call site simplifies to skill_prompt=skill_prompt since the guard now lives one line above. Cleanup 2: update comments in the RUNTIME_SKILL_PROMPT_PROVIDERS block and create_terminal Steps 3b/4 to reflect Phase 5's native OpenCode skill discovery. Adds two new tests asserting the lazy-call invariant: - test_build_skill_catalog_called_for_runtime_prompt_provider (call_count == 1) - test_build_skill_catalog_not_called_for_native_or_baked_provider (parametrized over opencode_cli, kiro_cli, q_cli, copilot_cli; assert_not_called) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update Phase 6 dev report with alt-screen extraction fix and cleanup commits Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(opencode): correct extraction_tail_lines docstring + black format + cleanup polish Item 1: black reformats terminal_service.py line 155 from a three-line expression to the single 100-char form black prefers. Item 2: rewrite extraction_tail_lines docstring — the old text claimed responses push ┃ beyond a 200-line window, which is wrong; OpenCode's alt-screen mode caps history_size near 2 making the override a no-op. Docstring now accurately describes the belt-and-braces rationale and cross-references the within-viewport fallback. Item 3: add single-turn alt-screen assumption comment to the normal extraction path. Item 4: CHANGELOG migration note gains a rm -rf cleanup hint for pre-release users. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(opencode): correct OPENCODE_DISABLE_MOUSE rationale and add UX callout - Update design doc stability-env-vars entry: footer patterns (ctrl+p, esc interrupt) are pinned and scroll-safe; the completion marker (▣ agent · model · Ns) is conversation content and scrolls off, preventing COMPLETED detection if mouse reporting is enabled - Add 'Scrolling enters tmux copy mode' Known Limitations entry in opencode-cli.md explaining the trade-off and how to work around it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * untrack out of scope docs * fix(opencode): align auto-approve, agent ID, and MCP cleanup semantics Addresses PR 193 review feedback. - Drop `cao install --auto-approve` and the `auto_approve` arg on the permission translator. CAO owns the permission decision, so `cao_tools_to_opencode_permission` now emits only `allow` or `deny`. `cao launch --auto-approve` retains its repo-wide meaning (skip CAO's confirmation prompt) and no longer has a provider-specific reinterpretation. - Remove stale `agent.<id>.tools` entries when a profile is reinstalled without `mcpServers` so revoked MCP grants do not persist. - Introduce `to_opencode_agent_id()` and use it consistently for the installed `.md` filename and the `agent.<id>.tools` key in `opencode.json`, keeping both aligned with the runtime `opencode --agent` argument for slash-containing profile names. - Strip phase numbers and design-doc section references from shipped source files and the provider doc per reviewer request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(opencode): drop reference to removed design doc from changelog Addresses review comment: the design doc link in the Unreleased entry referred to a file that is not included in this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(opencode): scope extraction_tail_lines to OpenCodeCliProvider The property was opencode-specific but lived on BaseProvider, which meant every provider carried an attribute it had no use for. Remove it from the base class, keep it as a provider-local property on OpenCodeCliProvider, and have terminal_service.get_output read it via a getattr capability check so the base class stays agnostic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(opencode): close codecov gaps in cli provider and permission translator Adds four tests to cover lines flagged as missing on the codecov report: - get_status ERROR fallback for non-empty output with no recognized marker - extract_last_message_from_script residual ``┃`` line + blank-line branches when raw_response contains leftover bar-prefixed lines - extract_last_message_from_script empty-response ValueError - cao_tools_to_opencode_permission AssertionError when a tool appears in ALL_OPENCODE_TOOLS without a matching policy Brings opencode_cli.py and opencode_permissions.py to 100% patch coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(opencode): mark provider experimental — single-agent flows only Add a warning badge to docs/opencode-cli.md and tag the README provider table row, both linking the post-settle inbox-delivery deadlock tracked in #203. Multi-agent flows are not yet reliable on opencode_cli; this signals the constraint to evaluators before they hit it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: haofeif <56006724+haofeif@users.noreply.github.com>
1 parent eda4da2 commit 3f5bc0e

33 files changed

Lines changed: 3292 additions & 13 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **OpenCode CLI provider** — Full integration with [OpenCode](https://opencode.ai), a terminal-based AI assistant whose native agent format (Markdown + YAML frontmatter) maps directly onto CAO profiles. Supports `cao install --provider opencode_cli`, all five terminal states (IDLE, PROCESSING, COMPLETED, WAITING_USER_ANSWER, ERROR), permission translation from CAO `allowedTools` to OpenCode `permission:` frontmatter, MCP server wiring via a CAO-owned `opencode.json`, and config isolation from the user's personal OpenCode setup. CAO's on-disk config directory for OpenCode is `~/.aws/opencode/` — users who installed an earlier pre-release build (which used `~/.aws/opencode_cli`) must re-run `cao install --provider opencode_cli` to populate the new location. The old directory can be removed with: `rm -rf ~/.aws/opencode_cli`. Provider docs: [`docs/opencode-cli.md`](docs/opencode-cli.md).
1013
## [2.1.0] - 2026-04-22
1114

1215
### Added

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Before using CAO, install at least one supported CLI agent tool:
103103
| **Gemini CLI** | [Provider docs](docs/gemini-cli.md) · [Installation](https://github.com/google-gemini/gemini-cli) | Google AI API key |
104104
| **Kimi CLI** | [Provider docs](docs/kimi-cli.md) · [Installation](https://platform.moonshot.cn/docs/kimi-cli) | Moonshot API key |
105105
| **GitHub Copilot CLI** | [Provider docs](docs/copilot-cli.md) · [Installation](https://github.com/features/copilot/cli) | GitHub auth |
106+
| **OpenCode CLI** *(experimental — single-agent only, [#203](https://github.com/awslabs/cli-agent-orchestrator/issues/203))* | [Provider docs](docs/opencode-cli.md) · [Installation](https://opencode.ai) | Per-model API key |
106107
| **Q CLI** | [Installation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line.html) | AWS credentials |
107108

108109
## Quick Start
@@ -151,6 +152,7 @@ cao launch --agents code_supervisor --provider codex
151152
cao launch --agents code_supervisor --provider gemini_cli
152153
cao launch --agents code_supervisor --provider kimi_cli
153154
cao launch --agents code_supervisor --provider copilot_cli
155+
cao launch --agents code_supervisor --provider opencode_cli
154156
# Unrestricted access + skip confirmation (DANGEROUS)
155157
cao launch --agents code_supervisor --yolo
156158
```

docs/opencode-cli.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# OpenCode CLI Provider
2+
3+
> ⚠️ **Experimental — single-agent flows only.** Multi-agent orchestration (assign / send_message back to a supervisor) is **not yet reliable** on `opencode_cli`: the supervisor's inbox can deadlock with `pending` messages after its turn settles. Single-agent and pure handoff workflows are unaffected. Tracking: [#203](https://github.com/awslabs/cli-agent-orchestrator/issues/203).
4+
5+
## Overview
6+
7+
The OpenCode CLI provider enables CLI Agent Orchestrator (CAO) to work with **OpenCode**, a terminal-based AI assistant with a native agent system. OpenCode uses Markdown files with YAML frontmatter as its agent format — nearly identical to CAO's own profile format — making this integration especially clean.
8+
9+
## Prerequisites
10+
11+
1. **OpenCode binary** — install from [opencode.ai](https://opencode.ai):
12+
```bash
13+
npm install -g opencode-ai
14+
# or
15+
curl -fsSL https://opencode.ai/install | bash
16+
```
17+
2. **Node.js 18+** — required by OpenCode for its plugin system
18+
3. **tmux 3.3+** — required by CAO for terminal management
19+
4. **API credentials** — configure whichever model provider you want OpenCode to use (Anthropic, OpenAI, etc.) per [OpenCode's auth docs](https://opencode.ai/docs/auth)
20+
21+
### First-launch delay
22+
23+
On its **first ever launch** against a fresh CAO config directory (`~/.aws/opencode/`), OpenCode runs `npm install @opencode-ai/plugin` — roughly 57 MB of dependencies that take **5–30 seconds** to install. The TUI will appear blank until the install completes. This is expected; CAO's 120-second initialization timeout covers it automatically.
24+
25+
Subsequent launches complete in ~2 seconds.
26+
27+
## Quick Start
28+
29+
### 1. Install agent profiles
30+
31+
```bash
32+
# Built-in profiles
33+
cao install code_supervisor --provider opencode_cli
34+
cao install developer --provider opencode_cli
35+
cao install reviewer --provider opencode_cli
36+
37+
# Custom or example profiles
38+
cao install examples/assign/data_analyst.md --provider opencode_cli
39+
cao install examples/assign/report_generator.md --provider opencode_cli
40+
```
41+
42+
### 2. Start the CAO server
43+
44+
```bash
45+
uv run cao-server
46+
```
47+
48+
### 3. Launch an agent
49+
50+
```bash
51+
# Standard launch — shows tool summary and asks for confirmation
52+
cao launch --agents developer --provider opencode_cli
53+
54+
# Skip CAO's launch-time confirmation prompt (tool restrictions still enforced)
55+
cao launch --agents developer --provider opencode_cli --auto-approve
56+
57+
# Specify model override
58+
cao launch --agents developer --provider opencode_cli --model anthropic/claude-sonnet-4-6
59+
60+
# Unrestricted (DANGEROUS) — agent can run any command
61+
cao launch --agents developer --provider opencode_cli --yolo
62+
```
63+
64+
Via HTTP API:
65+
66+
```bash
67+
curl -X POST "http://localhost:9889/sessions?provider=opencode_cli&agent_profile=developer"
68+
```
69+
70+
## Config Isolation
71+
72+
CAO runs OpenCode with `OPENCODE_CONFIG_DIR` and `OPENCODE_CONFIG` both pointing at `~/.aws/opencode/`, which is separate from the user's personal OpenCode config at `~/.config/opencode/`. This means:
73+
74+
- CAO-installed agents are visible in OpenCode's agent picker alongside the built-ins
75+
- CAO's MCP wiring (`opencode.json`) never touches the user's personal setup
76+
- Switching between `cao launch` and personal `opencode` usage is safe — they use independent config trees
77+
78+
Storage layout:
79+
80+
```
81+
~/.aws/opencode/
82+
├── opencode.json # MCP servers + per-agent tool gating (written by cao install)
83+
├── package.json # written by opencode on first launch
84+
├── node_modules/ # ~57 MB, written by opencode on first launch
85+
└── agents/
86+
├── code_supervisor.md
87+
├── developer.md
88+
└── ...
89+
```
90+
91+
## Permission and Tool Mapping
92+
93+
OpenCode enforces permissions natively via `permission:` YAML frontmatter in each agent file. CAO translates its `allowedTools` list to an OpenCode `permission:` dict at install time — **no entry in `utils/tool_mapping.py` is needed**.
94+
95+
CAO owns the permission decision, so the translator only ever emits `allow` or `deny`. The `ask` value — OpenCode's native runtime prompt — is intentionally never written, which keeps OpenCode aligned with the other CAO providers (Kiro, Q, Claude Code) where allowed tools are allowed outright.
96+
97+
### Summary
98+
99+
| CAO category | OpenCode tools enabled |
100+
|---|---|
101+
| `execute_bash` | `bash` |
102+
| `fs_read` | `read` |
103+
| `fs_write` | `edit`, `write` |
104+
| `fs_list` | `glob`, `grep` |
105+
| `fs_*` | `read`, `edit`, `write`, `glob`, `grep` |
106+
| `@<mcp-server-name>` | Handled in `opencode.json` (not frontmatter) |
107+
108+
Tools not in any enabled category default to `deny`. The following tools have hardcoded policies regardless of `allowedTools`:
109+
110+
| Tool | Policy | Reason |
111+
|---|---|---|
112+
| `task` | deny | Sub-agents escape CAO's terminal tracking |
113+
| `question` | deny | Blocks unattended flows indefinitely |
114+
| `webfetch`, `websearch`, `codesearch` | deny | Network egress — opt-in only |
115+
| `todowrite`, `skill` | allow | In-memory / additive, no side-effects |
116+
117+
Pass `--yolo` (or set `allowedTools: ["*"]` in the profile) to allow all 13 tools including the above.
118+
119+
### `cao launch --auto-approve`
120+
121+
`--auto-approve` on `cao launch` matches the repo-wide semantics: it skips CAO's launch-time confirmation prompt only. Tool restrictions are still enforced, and this flag does not modify any files in `OPENCODE_CONFIG_DIR`. It has **no** `cao install` counterpart — install-time permissions are driven entirely by the profile's `allowedTools` / `role`.
122+
123+
## Skills
124+
125+
CAO skills (e.g. `cao-supervisor-protocols`, `cao-worker-protocols`) are exposed to OpenCode agents through OpenCode's **native `skill` tool** with progressive loading — they are **not** baked into the agent's system prompt.
126+
127+
At `cao install --provider opencode_cli` time, CAO creates a symlink:
128+
129+
```
130+
~/.aws/opencode/skills → ~/.aws/cli-agent-orchestrator/skills/
131+
```
132+
133+
OpenCode auto-discovers `<OPENCODE_CONFIG_DIR>/skills/` and makes its contents available through the `skill` tool. Metadata (name, description) is listed up front; full skill bodies are loaded on demand. This means:
134+
135+
- Skill additions or removals under `~/.aws/cli-agent-orchestrator/skills/` take effect on the next OpenCode launch with no reinstall required.
136+
- The agent's system prompt stays lean — only `profile.system_prompt`/`profile.prompt` is written to the `.md` body, with no catalog injection.
137+
- CAO's `load_skill` MCP tool remains available as a second path to the same content (cross-provider parity).
138+
139+
## Status Detection
140+
141+
The provider detects terminal state from the tmux capture buffer (ANSI-stripped):
142+
143+
| State | Marker |
144+
|---|---|
145+
| `IDLE` | `ctrl+p commands` footer, no `esc interrupt` |
146+
| `PROCESSING` | `esc interrupt` footer keybind |
147+
| `COMPLETED` | `▣ <agent> · <model> · Ns` completion marker followed by idle footer |
148+
| `WAITING_USER_ANSWER` | `△ Permission required` or `△ Always allow` heading |
149+
| `ERROR` | Fallback — no state marker matched |
150+
151+
## MCP Server Wiring
152+
153+
`cao install --provider opencode_cli` writes MCP server declarations into `~/.aws/opencode/opencode.json`:
154+
155+
- Each `mcpServers` entry from the agent profile is added under the top-level `mcp` key
156+
- The server's tools are default-denied globally (`"<servername>*": false` under `tools`)
157+
- Re-enabled per-agent under `agent.<agent_id>.tools`
158+
159+
The agent ID is the slash-sanitized form of the profile name (`/``__`) — the same identifier used for the installed `.md` filename and the runtime `opencode --agent <id>` argument. This keeps the filename, the `--agent` arg, and the `opencode.json` key aligned for any profile name.
160+
161+
Reinstalling an agent whose profile no longer declares `mcpServers` explicitly removes its `agent.<agent_id>` entry from `opencode.json`, so previously-granted MCP tools do not survive as stale grants.
162+
163+
`CAO_TERMINAL_ID` is **not** written to `opencode.json`. OpenCode spawns MCP subprocesses that inherit the tmux window's environment, so the terminal ID propagates naturally — the same mechanism Kiro uses.
164+
165+
## End-to-End Testing
166+
167+
```bash
168+
# Install profiles first
169+
cao install examples/assign/data_analyst.md --provider opencode_cli
170+
cao install examples/assign/report_generator.md --provider opencode_cli
171+
cao install developer --provider opencode_cli
172+
173+
# Start CAO server
174+
uv run cao-server
175+
176+
# Run all OpenCode CLI e2e tests
177+
uv run pytest -m e2e test/e2e/test_assign.py -k opencode -v
178+
179+
# Run a single test
180+
uv run pytest -m e2e test/e2e/test_assign.py::TestOpenCodeCliAssign::test_assign_with_callback -v
181+
```
182+
183+
The `test_assign_with_callback` test validates all four orchestration modes:
184+
- **assign** (non-blocking): supervisor terminal created and stays IDLE
185+
- **send_message** (inbox delivery): worker pushes result to supervisor inbox
186+
- **status transitions**: IDLE → PROCESSING → COMPLETED across concurrent terminals
187+
- **handoff** (blocking): inbox delivery triggers supervisor state transition
188+
189+
## Known Limitations
190+
191+
### Project-local `opencode.json` override
192+
193+
OpenCode's config merge precedence places a project-local `opencode.json` in the current working directory **above** `OPENCODE_CONFIG` (the CAO-managed file). If you `cao launch` in a directory that has its own `opencode.json` with conflicting `agent.<name>.tools` or `tools` entries, CAO's MCP wiring can be silently overridden for that agent.
194+
195+
**Workaround:** remove or rename the project-local `opencode.json` before launching CAO, or move it under `.opencode/` (a subdirectory OpenCode also searches but at a lower priority level).
196+
197+
### Scrolling enters tmux copy mode
198+
199+
When you scroll (mouse wheel or trackpad) inside a CAO-managed OpenCode terminal, tmux enters copy mode instead of scrolling the TUI conversation history. This is intentional.
200+
201+
CAO launches OpenCode with `OPENCODE_DISABLE_MOUSE=1`, which prevents OpenCode from requesting application mouse-reporting mode (`\x1b[?1000h`). Without that request, tmux does not forward scroll events to the OpenCode process — it intercepts them and enters copy mode instead.
202+
203+
The reason for this trade-off: if OpenCode owned scroll events, scrolling the conversation history would move the completion marker (`▣ <agent> · <model> · Ns`) off screen. The footer (`ctrl+p commands`, `esc interrupt`) is pinned to the bottom of the TUI and remains visible regardless of scroll position, so IDLE and PROCESSING detection are unaffected. But COMPLETED detection requires both the completion marker and the idle footer to be present simultaneously in the captured frame — if the marker has scrolled away, CAO never detects COMPLETED even after the agent finishes. Disabling mouse keeps the frame locked to the most recent render.
204+
205+
Press `q` or `Escape` to exit copy mode. If you need to read earlier conversation history, use the `get_output` API endpoint or the `/terminals/<id>/output` endpoint to retrieve the full captured log.
206+
207+
### `opencode.json` concurrent writes
208+
209+
Parallel `cao install --provider opencode_cli` invocations (e.g., from a batch script) can race on the shared `~/.aws/opencode/opencode.json` file. The second writer may clobber the first's agent entry. **Sequential installs are safe.** File locking is deferred to a future release.
210+
211+
## Troubleshooting
212+
213+
### First-launch blank TUI (5–30 seconds)
214+
215+
OpenCode installs `@opencode-ai/plugin` into `~/.aws/opencode/node_modules/` on the first launch. The terminal will appear blank until `npm install` completes. CAO's 120-second initialization timeout covers this automatically.
216+
217+
To pre-populate `node_modules/` before the first CAO launch (optional):
218+
```bash
219+
OPENCODE_CONFIG_DIR=~/.aws/opencode opencode --help
220+
```
221+
222+
### "Unknown provider" error from the server
223+
224+
Ensure the CAO server running on port 9889 is the **dev version**, not the pre-installed binary:
225+
```bash
226+
# Kill any stale installed binary
227+
pkill -f 'cao-server'
228+
# Start the dev server
229+
uv run cao-server
230+
```
231+
232+
### Authentication / model errors
233+
234+
OpenCode itself handles model authentication. Verify your credentials are set for the model provider you want to use. Check `~/.config/opencode/opencode.json` (your personal config) for provider API keys, or set them via environment variables before launching.
235+
236+
### Permission prompt blocking an automated flow
237+
238+
CAO emits only `allow` or `deny` in the permission frontmatter, so `△ Permission required` should not appear for CAO-managed tools. If it does:
239+
1. Verify the profile's `allowedTools` / `role` grants the tool in question and reinstall — CAO translates allowed tools directly to `permission: allow`.
240+
2. If the prompt comes from a tool outside CAO's vocabulary, respond to it manually in the tmux window, or use `--yolo` to disable all restrictions **(DANGEROUS — allows any command including `aws`, `rm`, `curl`)**.
241+
242+
### Status stuck as `PROCESSING`
243+
244+
This can happen if:
245+
- OpenCode launched but the TUI hasn't painted yet (transient — the poller recovers)
246+
- A `node_modules` install is still in progress (wait up to 120s)
247+
- The `opencode` binary isn't on PATH in the tmux window's shell (check `echo $PATH` inside tmux)

src/cli_agent_orchestrator/cli/commands/install.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,27 @@
1515
DEFAULT_PROVIDER,
1616
KIRO_AGENTS_DIR,
1717
LOCAL_AGENT_STORE_DIR,
18+
OPENCODE_AGENTS_DIR,
1819
PROVIDERS,
1920
Q_AGENTS_DIR,
2021
SKILLS_DIR,
2122
)
2223
from cli_agent_orchestrator.models.copilot_agent import CopilotAgentConfig
2324
from cli_agent_orchestrator.models.kiro_agent import KiroAgentConfig
25+
from cli_agent_orchestrator.models.opencode_agent import OpenCodeAgentConfig
2426
from cli_agent_orchestrator.models.provider import ProviderType
2527
from cli_agent_orchestrator.models.q_agent import QAgentConfig
2628
from cli_agent_orchestrator.utils.agent_profiles import parse_agent_profile_text
2729
from cli_agent_orchestrator.utils.env import resolve_env_vars, set_env_var
30+
from cli_agent_orchestrator.utils.opencode_config import (
31+
ensure_skills_symlink,
32+
remove_agent_tools,
33+
to_opencode_agent_id,
34+
translate_mcp_server_config,
35+
upsert_agent_tools,
36+
upsert_mcp_server,
37+
)
38+
from cli_agent_orchestrator.utils.opencode_permissions import cao_tools_to_opencode_permission
2839
from cli_agent_orchestrator.utils.skill_injection import compose_agent_prompt
2940

3041

@@ -250,6 +261,42 @@ def install(agent_source: str, provider: str, env_vars: tuple[str, ...]):
250261
)
251262
agent_file.write_text(frontmatter.dumps(agent_post), encoding="utf-8")
252263

264+
elif provider == ProviderType.OPENCODE_CLI.value:
265+
OPENCODE_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
266+
ensure_skills_symlink()
267+
# Use the raw profile prompt as the body — skills are delivered natively
268+
# via OpenCode's skill tool through the OPENCODE_CONFIG_DIR/skills symlink.
269+
# compose_agent_prompt is NOT called here so the skill catalog stays out
270+
# of the system prompt.
271+
body = profile.system_prompt or profile.prompt or ""
272+
agent_config = OpenCodeAgentConfig(
273+
description=profile.description,
274+
mode="all",
275+
permission=cao_tools_to_opencode_permission(allowed_tools),
276+
)
277+
agent_id = to_opencode_agent_id(profile.name)
278+
agent_file = OPENCODE_AGENTS_DIR / f"{agent_id}.md"
279+
agent_post = frontmatter.Post(
280+
body.rstrip() if body else "",
281+
**agent_config.model_dump(exclude_none=True),
282+
)
283+
agent_file.write_text(frontmatter.dumps(agent_post), encoding="utf-8")
284+
285+
# Upsert MCP server declarations and per-agent tool gating into opencode.json.
286+
# When a profile no longer declares MCP servers, explicitly strip any stale
287+
# agent.<id>.tools entry from a previous install so revoked grants do not
288+
# survive a reinstall.
289+
if profile.mcpServers:
290+
mcp_names = list(profile.mcpServers.keys())
291+
for mcp_name, mcp_cfg in profile.mcpServers.items():
292+
# Translate CAO's mcpServer format to OpenCode's opencode.json format
293+
# before writing (type:stdio+command str+args → type:local+command list).
294+
opencode_mcp_cfg = translate_mcp_server_config(dict(mcp_cfg))
295+
upsert_mcp_server(mcp_name, opencode_mcp_cfg)
296+
upsert_agent_tools(agent_id, mcp_names)
297+
else:
298+
remove_agent_tools(agent_id)
299+
253300
click.echo(f"✓ Agent '{profile.name}' installed successfully")
254301
if env_vars:
255302
click.echo(f"✓ Set {len(env_vars)} env var(s) in {CAO_ENV_FILE}")

src/cli_agent_orchestrator/cli/commands/launch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"gemini_cli",
2626
"kimi_cli",
2727
"kiro_cli",
28+
"opencode_cli",
2829
}
2930

3031

src/cli_agent_orchestrator/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
Q_AGENTS_DIR = Path.home() / ".aws" / "amazonq" / "cli-agents" # Q CLI agents
8383
KIRO_AGENTS_DIR = Path(os.environ.get("CAO_AGENTS_DIR", str(Path.home() / ".kiro" / "agents")))
8484
COPILOT_AGENTS_DIR = Path.home() / ".copilot" / "agents" # Copilot custom agents
85+
OPENCODE_CONFIG_DIR = Path.home() / ".aws" / "opencode" # OpenCode CAO-managed config root
86+
OPENCODE_AGENTS_DIR = OPENCODE_CONFIG_DIR / "agents" # OpenCode agent .md files
87+
OPENCODE_CONFIG_FILE = OPENCODE_CONFIG_DIR / "opencode.json" # OpenCode MCP + tool gating config
8588

8689
# =============================================================================
8790
# Database Configuration

0 commit comments

Comments
 (0)