250 lines
16 KiB
Markdown
250 lines
16 KiB
Markdown
# 🔌 Implementation Plan: OpenCode (`anomalyco/opencode`) Agent Adapter for MAM (Rev.3)
|
||
|
||
- **Author**: `planner-reviewer-claude-01` (Planner), refined in collaboration with `creator-agy-01` and `reviewer-creator-grok-01`
|
||
- **Job**: `de667f0f` → Rev.2 (`c65b0081`) → Rev.3 (`78f332a9`)
|
||
- **Status**: Plan only — **no code changed in the repo**. Ready for review/refinement.
|
||
- **Scope**: Add `opencode` as a 5th first-class MAM agent backend (alongside `claude`, `agy`, `hermes`, `grok`), per `docs/NEW_AGENT_INTEGRATION_GUIDE.md`'s 5-step procedure.
|
||
- **Durable Path**: `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (and `.agents/reports/implementation_plan.md`)
|
||
|
||
---
|
||
|
||
## Rev.3 Changelog (Job `78f332a9`)
|
||
|
||
Reviewers `planner-reviewer-claude-01` (Job `1d63595f`) and `reviewer-creator-grok-01` (Job `56120ad3`) reviewed the Rev.2 plan and working tree. **All 5 findings accepted — zero `[REBUT:]` filed**:
|
||
|
||
1. **Restored Root `implementation_plan.md` & Namespaced Plan Path (F1)**:
|
||
- Root `implementation_plan.md` was restored to its tracked HEAD content (the active *MAM 메시징 백플레인 전환 실행 로드맵* NATS migration roadmap).
|
||
- This OpenCode integration plan is permanently housed under `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` and mirrored at `.agents/reports/implementation_plan.md` to prevent future root-level collision.
|
||
2. **Extended Lockstep Touch-Points Table to 29 Items (F2)**:
|
||
- Added items 23–29 covering `create_session.sh` (`spawn()` dispatch case, YAML initialization `elif`, fallback `CMD_FULL` case), `lib.sh` (`send_keys_safe` `_sks_agent` resolution case), `tests/test_tier1_unit.py` (CLI usage assertions), 8× `SKILL.md` (supported agent lists), and `tests/conftest.py` (`mock_agents` stub binary).
|
||
3. **Official CLI `opencode db path` Subcommand Integration (F3)**:
|
||
- Updated §1.5, §2, and §6 to prioritize the official `opencode db path` subcommand for dynamic database location, dropping hardcoded `storage.db` assumptions.
|
||
4. **Headless Permission Architecture via `OPENCODE_PERMISSION` (F4)**:
|
||
- Documented the `OPENCODE_PERMISSION='{"*":"allow"}'` process environment variable alongside `--auto` in §1.4, §2, and §9 DoD for unattended automated operations without global config file mutations.
|
||
5. **Sandbox-Safe `ctx.home_dir` Binding in `auth_ok` and Artifact Paths (F5)**:
|
||
- Replaced `os.path.expanduser('~')` with `ctx.home_dir` in the adapter skeleton and test contract for hermetic sandbox execution.
|
||
|
||
---
|
||
|
||
## Rev.2 Changelog (Job `c65b0081`)
|
||
|
||
`creator-agy-01` filed a formal architectural challenge (Job `ebf97869`) against Rev.1, raising two blind spots. Both accepted:
|
||
1. **`reconcile.sh` Drift-C Non-Generic Correction**: Identified that `reconcile.sh` drift-C materialization consists of bespoke per-agent blocks, requiring an explicit `# === drift C (opencode) ===` block for post-spawn discovery agents.
|
||
2. **Core `lib_py` Constants Added**: Added `atomic_yaml.py:132` (`running_keys`), `verify_session.py:69` (`mam_row_own_uuid()`), and `workspace_uuid.py:7-12,33` (`OWN_KEY`) to the touch-points list.
|
||
|
||
---
|
||
|
||
## 0. Relationship to the existing `new_agent_types_roadmap.md`
|
||
|
||
`.agents/reports/new_agent_types_roadmap.md` (by `creator-agy-01`, 2026-08-26) sketches a generic "add any new agent" procedure and guesses at OpenCode specifics in its difficulty matrix. This plan **supersedes** that roadmap's OpenCode row:
|
||
|
||
| | Roadmap's guess | Verified reality (this doc) |
|
||
|---|---|---|
|
||
| Storage format | "JSON files in `~/.opencode/sessions/`" | **SQLite** since v1.2.0 (Feb 2026); dynamic lookup via `opencode db path` |
|
||
| Storage path | `~/.opencode/sessions/` | `~/.local/share/opencode/` (XDG data dir) or project storage via `opencode db path` |
|
||
| Ready tokens | `OpenCode\|Chat\|Welcome` | Candidate string pending live TUI pane capture verification (§4) |
|
||
| Adapter registry | `ClineAgentAdapter` shown | `cline` fully removed in v4.0.0. Targets current 4-adapter registry (`claude`, `agy`, `hermes`, `grok`) |
|
||
|
||
---
|
||
|
||
## 1. Verified OpenCode CLI/Runtime Facts
|
||
|
||
Source: `opencode.ai/docs/{cli,config,permissions,tui}/`, `github.com/anomalyco/opencode`, and official documentation.
|
||
|
||
### 1.1 Identity & Install
|
||
- **Upstream Repository**: `github.com/anomalyco/opencode` (distinct from the archived `opencode-ai/opencode` / "Crush").
|
||
- **Install**: `curl -fsSL https://opencode.ai/install | bash` (also npm/Homebrew).
|
||
|
||
### 1.2 Execution Modes
|
||
- `opencode [project]` — launches the **TUI** (default).
|
||
- `opencode run [message..]` — **non-interactive** one-shot CLI execution.
|
||
- `opencode db path` — prints the active database file path on stdout.
|
||
|
||
### 1.3 Relevant CLI Flags & Environment
|
||
| Flag / Variable | Purpose |
|
||
|---|---|
|
||
| `--continue` / `-c` | Resume the most recent session |
|
||
| `--session <id>` / `-s <id>` | Continue a specific session ID (cannot pre-assign a new UUID at spawn) |
|
||
| `--agent <name>` | Mode selection: `build` (default, write access) or `plan` (read-only) |
|
||
| `--auto` | Auto-approves tools that prompt ("ask"); explicit "deny" rules still enforced |
|
||
| `OPENCODE_PERMISSION` | Process environment variable for inline JSON permission overrides (e.g. `'{"*":"allow"}'`) |
|
||
|
||
**Spawn / Discovery Consequence**: OpenCode has **no `--session-id <new-uuid>` pre-assignment flag**. It falls strictly in the **`hermes` / `agy` architectural bucket**: `spawn_spec()` ignores `session_uuid` on initial creation, and MAM learns the assigned session ID post-spawn via `discover()` and `reconcile.sh` Drift-C materialization.
|
||
|
||
### 1.4 Permissions (Headless & Non-Interactive Configuration)
|
||
MAM configures headless unattended execution at the process boundary without mutating user-global config files (`~/.config/opencode/opencode.json`):
|
||
1. **CLI Flag**: `--auto` passed in `spawn_spec` / `resume_spec`.
|
||
2. **Process Environment Variable**: `OPENCODE_PERMISSION='{"*":"allow"}'` exported in the spawn subshell when full unattended bypass is required.
|
||
3. **Optional Project/Global Config**: `<project>/opencode.json` or `~/.config/opencode/opencode.json` can define granular policies if configured by the user.
|
||
|
||
### 1.5 Session & Artifact Storage
|
||
- **Format**: Single SQLite database (since v1.2.0).
|
||
- **Dynamic Path Resolution**: `opencode db path` prints the exact database location.
|
||
- **Default Storage Roots**: `~/.local/share/opencode/` (global) or `project/<slug>/storage/`.
|
||
- **Pre-Implementation Spike (§1.5)**: Run `opencode db path` and `sqlite3 <db> ".schema"` on a live install to verify `session`, `message`, and `part` table column definitions (`cwd`, `created_at` timestamps) before writing SQL queries.
|
||
|
||
### 1.6 Authentication
|
||
- Interactive: `opencode auth login`.
|
||
- Multi-provider environment variables: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY`.
|
||
- Credentials cache: `~/.local/share/opencode/auth.json`.
|
||
- `auth_ok()` checks for `auth.json` in `ctx.home_dir` OR any supported provider environment variable.
|
||
|
||
### 1.7 Exit Key
|
||
- Slash command `/exit` (aliases `/quit`, `/q`).
|
||
|
||
---
|
||
|
||
## 2. Adapter Contract Decisions (`BaseAgentAdapter`)
|
||
|
||
| Property/Method | Value | Rationale |
|
||
|---|---|---|
|
||
| `name` | `'opencode'` | Lowercase identifier |
|
||
| `own_key` | `'opencode_session_id_own'` | Matches `<agent>_session_id_own` convention |
|
||
| `ready_tokens` | `'OpenCode\|Chat'` (candidate) | Pending live TUI capture verification (§4) |
|
||
| `exit_key` | `'/exit'` | Confirmed in CLI docs |
|
||
| `delegate_agent_key` | `'opencode-cli'` | Descriptive CLI identifier |
|
||
| `identity_cache_fields` | `('session_id',)` | Single-field cache |
|
||
| `spawn_spec(binary, session_uuid, use_wrapper)` | `f"{binary} --auto --agent build"` | Non-interactive build mode; ignores `session_uuid` at spawn |
|
||
| `resume_spec(binary, session_uuid, materialized)` | `f"{binary} --session {session_uuid} --auto --agent build"` (if materialized) | Passes `--session <id>` once discovered |
|
||
| `auth_ok(run_cmd)` | `os.path.exists(f"{ctx.home_dir}/.local/share/opencode/auth.json") or any(env)` | Multi-provider fallback + sandbox-safe path |
|
||
| `discover(ctx)` | Queries SQLite DB resolved via `opencode db path` / default root | Discovers new session IDs created since `ctx.epoch` |
|
||
| `verify_artifact(uuid, ctx)` | Checks row existence in SQLite `session` table | Validates session persistence |
|
||
| `purge_artifacts(uuid, ctx)` | Executes parameterized `DELETE FROM session/message/part WHERE session_id=?` | Cleans SQLite rows on stop `--purge-conversation` |
|
||
|
||
---
|
||
|
||
## 3. Complete 29-Point Wiring & Lockstep Table
|
||
|
||
All 29 locations below must be updated in lockstep:
|
||
|
||
| # | Component / File | Specific Edit Required |
|
||
|---|---|---|
|
||
| 1 | `.agents/skills/lib_py/agents/adapters/opencode.py` | Create new adapter class `OpenCodeAgentAdapter` |
|
||
| 2 | `.agents/skills/lib_py/agents/registry.py` | Import `OpenCodeAgentAdapter` and register in `_ADAPTERS['opencode']` |
|
||
| 3 | `.agents/skills/lib.sh` (kind mapping, ~L606-609) | `*-creator-opencode\|*-planner-opencode\|*-reviewer-opencode) kind="opencode" ;;` |
|
||
| 4 | `.agents/skills/lib.sh` (fallback grep chain, ~L610-619) | Add `elif echo "$name" \| grep -qi "opencode"; then kind="opencode"` |
|
||
| 5 | `.agents/skills/lib.sh` (binary dedup tuple, ~L632) | Add `'opencode'` to `('claude', 'agy', 'hermes', 'grok', 'opencode')` |
|
||
| 6 | `create_session.sh` (line 93 `--agent` whitelist) | Add `opencode` to accepted values |
|
||
| 7 | `resume_session.sh` (line 45 `--agent` whitelist) | Add `opencode` to accepted values |
|
||
| 8 | `resolve_session_id.sh` (line 39 `--agent` whitelist) | Add `opencode` to accepted values |
|
||
| 9 | `update_yaml_resumed.sh` (usage string) | Add `opencode` to usage documentation |
|
||
| 10 | `stop_session.sh` (line 94 `--agent` whitelist) | Add `opencode` to accepted values |
|
||
| 11 | `orc_onboard.sh` (line 106 case) | Add `opencode` to accepted values |
|
||
| 12 | `tests/test_a4_adapter_contract.py` | Add `opencode` to `EXPECTED_OWN_KEYS`, `expected`, `expected_modal` (`None`), `facts_bridge`, and delegate bash snippets |
|
||
| 13 | `tests/test_a4_adapter_contract.py` | Add adapter-specific unit tests (`spawn_spec`, `resume_spec`, `auth_ok`, `purge_artifacts`, `discover`) |
|
||
| 14 | `tests/test_c1_tui_readiness.py` | Add `opencode` to facts-bridge and SKS session resolution loops |
|
||
| 15 | `tests/test_tier2_component.py` | Add OpenCode lifecycle tests (create → YAML → stop → resume) |
|
||
| 16 | `.mam.env.example` | Document OpenCode multi-provider API keys and `OPENCODE_PERMISSION` |
|
||
| 17 | `docs/NEW_AGENT_INTEGRATION_GUIDE.md` | Update difficulty matrix with OpenCode SQLite storage |
|
||
| 18 | `.agents/reports/new_agent_types_roadmap.md` | Add cross-link noting OpenCode details are superseded by this plan |
|
||
| 19 | `.agents/skills/lib_py/atomic_yaml.py:132` | Add `'opencode_session_id_own'` to `running_keys` |
|
||
| 20 | `.agents/skills/lib_py/verify_session.py:69` | Add `'opencode_session_id_own'` to `mam_row_own_uuid()` |
|
||
| 21 | `.agents/skills/lib_py/workspace_uuid.py:7-12, 33` | Add `'opencode'` to `OWN_KEY` dict and `running_ids` list |
|
||
| 22 | `reconcile.sh` (auto-reg & own keys) | Add `opencode` to loops (521, 540), entry initialization `elif` (584-601), and `OWN_KEY_BY_AGENT` (611) |
|
||
| 23 | `create_session.sh` (spawn dispatch, ~L204) | Add `opencode` to `agy\|hermes\|grok\|opencode)` case |
|
||
| 24 | `create_session.sh` (YAML init, ~L399-419) | Add `elif agent == 'opencode': entry['opencode_session_id_own'] = None` |
|
||
| 25 | `create_session.sh` (fallback CMD_FULL, ~L180-184) | Add `opencode) CMD_FULL="$BINARY --auto --agent build" ;;` |
|
||
| 26 | `lib.sh` (`send_keys_safe` agent resolution, ~L2058-2061) | Add `*-opencode\|*-opencode-[0-9]*\|opencode\|opencode-[0-9]*) _sks_agent="opencode" ;;` |
|
||
| 27 | `tests/test_tier1_unit.py` (~L969, 974) | Update expected CLI error usage assertions to include `opencode` |
|
||
| 28 | 8× `SKILL.md` supported-agent lists | Update skill documentation to list `opencode` |
|
||
| 29 | `tests/conftest.py` (`mock_agents`) | Add mock `opencode` binary fixture and sandbox state support |
|
||
|
||
---
|
||
|
||
## 4. TUI Readiness & Empirical Verification
|
||
|
||
- Initial candidate pattern: `OpenCode|Chat`
|
||
- **Empirical Capture Gate**: Before finalizing the PR, capture rendered text from a live `opencode` TUI pane using `herdr pane capture` to confirm the exact ready token and prompt glyphs (`input_prompt`, `input_placeholder`).
|
||
|
||
---
|
||
|
||
## 5. Drift Detection Integration & Dedicated Drift-C Block
|
||
|
||
In `multi-agent-mux-monitor/scripts/reconcile.sh`, add the dedicated OpenCode materialization block:
|
||
|
||
```python
|
||
# === drift C (opencode): opencode 새 session id materialize (per-row own id) ===
|
||
for s in d.get('herdr_sessions', []):
|
||
if row_agent(s) != 'opencode':
|
||
continue
|
||
if s.get('status') != 'running':
|
||
continue
|
||
if s.get('opencode_session_id_own'):
|
||
continue
|
||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||
if not cwd:
|
||
continue
|
||
|
||
# Resolve DB path via opencode db path or default XDG root
|
||
db = _resolve_opencode_db(home, cwd)
|
||
if not os.path.exists(db):
|
||
continue
|
||
|
||
epoch_threshold = s.get('herdr_session_epoch', 0)
|
||
sibling_claimed = [
|
||
other.get('opencode_session_id_own')
|
||
for other in d.get('herdr_sessions', [])
|
||
if other is not s
|
||
and (other.get('pane') or {}).get('cwd') == cwd
|
||
and other.get('status') not in ('stopped', 'terminated')
|
||
and other.get('opencode_session_id_own')
|
||
]
|
||
s_eval = dict(s)
|
||
s_eval['_sibling_claimed_uuids'] = sibling_claimed
|
||
|
||
valid_candidates = []
|
||
try:
|
||
conn = sqlite3.connect(db)
|
||
rows = conn.execute(
|
||
"SELECT id FROM session WHERE cwd=? AND created_at >= ? ORDER BY created_at DESC LIMIT 20",
|
||
(cwd, epoch_threshold)
|
||
).fetchall()
|
||
conn.close()
|
||
for (uuid,) in rows:
|
||
if uuid in (sibling_claimed or []):
|
||
continue
|
||
if verify_session_uuid(cwd, 'opencode', uuid, s_eval, mode="discover"):
|
||
valid_candidates.append(uuid)
|
||
except Exception:
|
||
pass
|
||
|
||
if len(valid_candidates) > 1:
|
||
drifts.append({'class': 'C-ambiguous', 'name': s['name'],
|
||
'msg': f"{s['name']}: {len(valid_candidates)} candidate transcripts newer than session epoch; not pinning"})
|
||
s['last_visible_status'] = f"ambiguous: {len(valid_candidates)} candidates"
|
||
actions.append(f"ambiguous candidates: {s['name']}")
|
||
elif len(valid_candidates) == 1:
|
||
uuid = valid_candidates[0]
|
||
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "opencode" "{cwd}"']
|
||
rc = subprocess.run(cmd).returncode
|
||
if rc == 0:
|
||
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=False)
|
||
elif rc == 1:
|
||
drifts.append({'class': 'C-warn', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport mismatch for candidate {uuid} — not pinned, will retry next cycle"})
|
||
else:
|
||
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=True)
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Definition of Done Checklist
|
||
|
||
- [ ] Execute `opencode db path` and SQLite `.schema` dump spike on a live install.
|
||
- [ ] Confirm TUI ready tokens and input prompt glyphs via empirical pane capture.
|
||
- [ ] Verify headless execution under `--auto` + `OPENCODE_PERMISSION='{"*":"allow"}'`.
|
||
- [ ] Implement `lib_py/agents/adapters/opencode.py` with sandbox-safe `ctx.home_dir`.
|
||
- [ ] Apply all 29 lockstep wiring points across `registry.py`, `lib.sh`, skill scripts, `lib_py` modules, and test fixtures.
|
||
- [ ] Add dedicated `reconcile.sh` Drift-C materialization block and corresponding integration test.
|
||
- [ ] Verify 100% test pass on full suite (`pytest tests/`).
|
||
- [ ] Perform manual end-to-end smoke test (create → prompt → discover → stop/purge).
|
||
|
||
---
|
||
|
||
## 7. Effort Estimate
|
||
|
||
- **Total Estimate**: **~1.5 – 2.0 Days**
|
||
- ½ day: SQLite schema spike (`opencode db path`) & TUI token live capture
|
||
- ½ day: Adapter implementation & 29-point mechanical wiring
|
||
- ½ day: Dedicated `reconcile.sh` Drift-C materialization block & integration tests
|
||
- ½ day: End-to-end manual smoke tests & regression verification
|