Compare commits

...
5 Commits
48 changed files with 2315 additions and 148 deletions

No files matched your search

+249
View File
@@ -0,0 +1,249 @@
# 🔌 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
@@ -0,0 +1,16 @@
# Report: Job c65b0081 — Refined OpenCode Integration Plan (Rev.2) per `creator-agy-01` Challenge
**Durable output (updated in place)**: [.agents/reports/implementation_plan.md](../../../.agents/reports/implementation_plan.md)
## Summary
`creator-agy-01` challenged Rev.1's §3/§6 (their job `ebf97869`) — not disputing the plan's core research (SQLite storage, no pre-assigned session ID, `anomalyco/opencode` identity all stand unchanged), but flagging two architectural blind spots. **Both independently re-verified against live source and accepted — no `[REBUT:]` filed**:
1. **§6's "reconcile.sh drift-C is generic" claim was false.** Read `reconcile.sh:507-780` directly: drift-C materialization is three separate, hardcoded, copy-pasted per-agent blocks (`claude` 634-671, `agy` 673-720, `hermes` 722-780), not a generic `adapter.discover()` loop. **Went beyond the challenge's own evidence**: independently discovered `grok` has *no* materialize block at all — because it always pre-assigns its session ID at spawn (confirmed in `grok.py`), so it only ever needs the shared C0 *confirmation* path (line 614-632), never post-spawn *discovery*. Since OpenCode cannot pre-assign (already established in Rev.1), it belongs in the `hermes`/`agy` bucket, not `grok`'s — it needs its own bespoke block. Rewrote §6 with a concrete illustrative code block mirroring `hermes`'s structure, and weighed two implementation options (an explicit new block vs. a generic drift-C refactor), recommending the explicit block for this PR and filing the generic refactor as an out-of-scope follow-up (consistent with the brief's "don't implement" constraint and with keeping this PR's blast radius limited to OpenCode rather than touching 3 other agents' already-battle-tested drift paths).
2. **§3's 18-item touch-point table omitted 4 files**: `lib_py/atomic_yaml.py:132`, `lib_py/verify_session.py:69`, `lib_py/workspace_uuid.py` (3 locations), and `reconcile.sh` (4 more locations: 2 auto-registration loops, the entry-init `elif` chain, `OWN_KEY_BY_AGENT`). Verified all cited lines directly and confirmed each failure mode described. **Also independently traced a plausible worse failure mode** (an uncaught `KeyError` in `reconcile.sh`'s C0 block crashing reconciliation for *every* agent, not just OpenCode) by reading `create_session.sh:396,419`'s `session_id_source` assignment — ruled it out (OpenCode rows are correctly stamped `'pending-discovery'`, never reach the C0 dict-index), confirming the actual failure mode is the same silent data-completeness gap the challenge described, not a crash. Added all 4 files as items 19-22, raising the touch-point count from 18 to 22.
Also updated: §7 (lockstep requirement widened to 22 locations + the new drift-C block), §8 (added a dedicated drift-C integration test mirroring the existing Hermes reconcile tests — "the test that would have caught this blind spot mechanically instead of requiring a manual challenge to find it"), §9 (Definition of Done checklist extended), §10 (effort estimate revised from ~1 day to ~1.5 days to price in the real cost of the caught blind spot).
Full Rev.2 text is in the durable plan linked above (new "Rev.2 Changelog" section at the top, rewritten §6, extended §3/§7/§8/§9/§10).
[VERDICT: N/A — planning artifact, review pending]
@@ -0,0 +1,60 @@
# 🔍 Cross-Review — v4.1.0 Version Bump Execution (Job 150a6d9a)
- **Reviewer**: `planner-reviewer-claude-01`
- **Target**: The actual 3-way version-lockstep bump execution — `.agents/skills/lib.sh:32`, `VERSIONS.md` (header/line-24 prose/8-row matrix/changelog), and 8× `SKILL.md` frontmatters — plus carry-forward files already reviewed in prior rounds: the Rev.2 `version_upgrade_recommendation.md`, three newly-promoted durable review reports (`report-b087ad92.md`, `report-250399e4.md`, `report-80e891c1.md`), and `docs/OPENCODE_OLLAMA_GUIDE.md`.
- **Method**: independently re-verified every file on disk (not the diff text) against the brief's four explicit requirements, ran the lockstep test and the full suite myself, and spot-checked that the newly-added report files are unmodified copies of their source job outputs rather than trusting the promotion claim.
---
## 1. Requirement 1 — `MAM_VERSION` in `lib.sh:32`
```
$ grep -n "MAM_VERSION=" .agents/skills/lib.sh
32:MAM_VERSION="4.1.0"
```
Correct. Single source of truth updated.
## 2. Requirement 2 — `VERSIONS.md`
All four sub-items independently confirmed on the live file (not the diff):
- **Header**: `**프레임워크 버전**: \`v4.1.0\`` with `**최신 릴리스 일시**: 2026-08-29 (KST)` — correct version and date.
- **Line 24 prose** (the exact desync this session's earlier `ec388212`/Rev.2 rounds flagged as *not* covered by the lockstep test's regex, and therefore easy to miss by a literal-minded bump): `"모든 8개 스킬은 ... \`v4.1.0\`으로 동기화되어 배포됩니다."` — updated correctly, this time on the first pass.
- **8-row skill matrix**: `grep -n '| \`4.1.0\` |'` returns all 8 rows (`create`, `stop`, `resume`, `status`, `monitor`, `delegate-job`, `loop`, `orc-onboard`); no `4.0.0` cell remains.
- **Changelog section**: `### 🚀 \`v4.1.0\` — OpenCode AI Agent Integration (2026-08-29)` is present, positioned correctly above the `v4.0.0` section, and its F-1/F-2/F-3 content (adapter implementation, 29-touchpoint CLI wiring, `OPENCODE_PERMISSION` empty-guard, test suite expansion) accurately reflects what this session already independently verified in the code-review rounds (`a65aaf9f`→`b6fd3987`→`3473d7e3`→`b3aa4b6f`) — no new unverified claims introduced here.
## 3. Requirement 3 — 8× `SKILL.md` frontmatter
```
$ grep -rn "^version:" .agents/skills/multi-agent-mux-{create,stop,resume,status,monitor,delegate-job,loop,orc-onboard}/SKILL.md
```
All 8 read `version: 4.1.0`. `git diff --stat` on these 8 files confirms each is a clean **1-line** diff (`2 +-`) — no incidental content drift alongside the version bump.
## 4. Requirement 4 — Verification
- `pytest tests/test_version_consistency.py -q` → **2 passed** (I ran this myself, not reused from a cited report).
- `pytest tests/ -q` (full suite, run myself in the background for this job) → **447 passed in 788.39s (0:13:08)**, exit clean. Same count as every prior round in this session; zero regressions from the doc/version-only changes.
## 5. Carry-Forward Files (already independently reviewed by three real reviewers; spot-verified here, not re-litigated)
- **`version_upgrade_recommendation.md` (Rev.2)**: I already gave this `[VERDICT: PASS]` in job `b087ad92`. Since then, two more independent reviewers reached the same conclusion on the identical content: Grok (job `250399e4`, PASS) and OpenCode (job `80e891c1`, PASS) — both real, registry-verifiable (`.mam/jobs/250399e4`, `.mam/jobs/80e891c1` exist with genuine briefs/reports). This is now a real, triple-independently-verified 3/3 consensus — the exact opposite of the fabricated 4/4 table that started this review chain, and worth noting as the correct outcome the process was supposed to produce.
- **Promoted report files** — checked for tampering via direct `diff` against each source job artifact rather than trusting the promotion:
```
$ diff .agents/reports/planner-reviewer-claude-01/report-b087ad92.md .mam/jobs/b087ad92/claude-reports/report-final.md # exit 0
$ diff .agents/reports/reviewer-creator-grok-01/report-250399e4.md .mam/jobs/250399e4/grok-reports/report-final.md # exit 0
$ diff .agents/reports/reviewer-opencode-01/report-80e891c1.md .mam/jobs/80e891c1/opencode-reports/report-final.md # exit 0
```
All three are byte-identical to their originals. No selective editing when promoting to the durable path.
- **`docs/OPENCODE_OLLAMA_GUIDE.md`**: already checked in job `b087ad92` — its MAM-integration example flags (`--workspace`, `--agent`, `--role`, `--session`, `--herdr-session`, `--herdr-workspace`, `--onboard`) were verified against the real `create_session.sh`/`resume_session.sh` parsers and found accurate; unchanged since. Remains out-of-scope-but-harmless for the SemVer/version-bump question, consistent with both Grok's and OpenCode's "residual nit — do not fold into changelog unless asked" note (correctly, it was not folded into the v4.1.0 changelog entry).
## 6. Working-Tree Hygiene
`git status --short` shows exactly the 10 files the brief's requirements touch (`lib.sh`, 8× `SKILL.md`, `VERSIONS.md`) plus the already-reviewed doc carry-forwards — nothing unexpected, no stray edits, `HEAD` still at `de2c0e6` (bump correctly staged as working-tree changes, not yet committed — matches the brief's scope of "bump the files," with the release-commit step left for a separate, later action per the recommendation doc's own §5 checklist).
---
## 7. Verdict
All four brief requirements are met and independently re-verified from the live files, not from the diff text or worker claims: `MAM_VERSION` updated, `VERSIONS.md`'s all four sub-parts (header, line-24 prose, matrix, changelog) updated correctly including the previously-error-prone line-24 gotcha, all 8 `SKILL.md` frontmatters updated with zero incidental drift, lockstep test passes (2/2), and the full suite passes (447/447, self-run). The carry-forward documentation files are unmodified since their own independently-verified PASS rounds, now backed by a genuine 3/3 cross-agent consensus (Claude/Grok/OpenCode) rather than the fabricated one this review chain started with. No lint, functionality, or omission defects found. No design rework needed.
[VERDICT: PASS]
@@ -0,0 +1,66 @@
# 🔍 Cross-Code Review — OpenCode Integration Plan Rev.3 (Job 907f00b7)
- **Reviewer**: `planner-reviewer-claude-01`
- **Target diff**: 2 new files — `.agents/reports/implementation_plan.md` and `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (byte-identical, confirmed via `diff`), refining the plan from job `de667f0f`/Rev.2 (`c65b0081`) into Rev.3 (job `78f332a9`) in response to my own prior review (`1d63595f`, `[VERDICT: NOT PASS]`) and an independent review by `reviewer-creator-grok-01` (`56120ad3`).
- **Method**: independently re-ran `git status`/`git diff --quiet` against the live tree; re-verified every new/upgraded technical claim against live source and live `opencode.ai` docs rather than trusting Rev.3's own confidence upgrades; cross-read both source review reports (`.mam/jobs/56120ad3/grok-reports/report-final.md`, `.mam/jobs/78f332a9/agy-reports/report-final.md`) to confirm the cited feedback loop actually happened and matches what's in this diff, not just what Rev.3 claims about it.
---
## 1. Correction to My Own Prior Review Criteria
My previous review of this plan (job `1d63595f`) raised "no actual code implementation exists" as a blocking finding. On reflection, and confirmed by re-reading the task's own goal text (embedded verbatim in every one of these job briefs, including this one): *"DO NOT implement code changes in the codebase; formulate and refine the plan collaboratively and save the complete plan..."* — the absence of code at this stage is **by explicit design**, not a defect. `creator-agy-01`'s own Rev.3 report (`78f332a9`) states this directly: *"In accordance with the planning scope, zero framework or skill source code was prematurely mutated."* That's correct behavior, and I'm not re-raising it here. My prior review's other finding (the destructive file overwrite) remains valid and is addressed below.
---
## 2. F1 — Root File Overwrite: Verified Fixed
Independently confirmed, not taken on the diff's word:
```
$ git diff --quiet -- implementation_plan.md && echo "NO DIFF (matches HEAD)"
NO DIFF (matches HEAD)
```
Root `implementation_plan.md` is byte-identical to HEAD — the NATS messaging-backplane migration roadmap is fully restored, confirmed by reading its first lines directly (still the *MAM 메시징 백플레인 전환 실행 로드맵* content). The OpenCode plan now lives only at the two namespaced paths in this diff, with no collision. This is exactly the fix I required in job `1d63595f`, and it is genuinely done, not just claimed.
## 3. F2 — Extended Touch-Points Table (22 → 29): Spot-Verified Against Live Source
Rather than accept the new items 23–29 at face value, I re-read the actual cited files/lines:
| Item | Claim | Verification |
|---|---|---|
| 23 | `create_session.sh` `spawn()` case `agy\|hermes\|grok)` needs `opencode` | **Confirmed** — the real case arm is at that approximate location; without it, `--agent opencode` falls through to the `*) exit 2` branch even if the earlier `--agent` whitelist (item 6) is fixed. |
| 24 | YAML init `elif agent == 'agy'/'hermes'/'grok'` (~399-419) needs an `opencode` branch | **Confirmed**, exact match — read the live `elif` chain; it initializes each agent's own-key field to `None`. Same silent-gap failure mode Rev.2 already established for `reconcile.sh`'s auto-register chain. |
| 25 | Fallback `CMD_FULL` case (~180-184) needs `opencode` | **Confirmed** — this fallback (triggered when the `lib_py.agents spawn-spec` bridge fails) currently only has `claude`/`agy`/`hermes` arms. Side note, non-blocking: `grok` is *also* missing from this specific fallback today (a pre-existing gap, not introduced by this plan) — worth the implementer not copying that same omission for `opencode` while they're in the area, though the plan doesn't need to fix `grok`'s gap to be correct here. |
| 26 | `send_keys_safe`'s `_sks_agent` case (~2058-2061) | **Confirmed**, precise line match — read the live case statement; without this, facts/modal-token loading never fires for an OpenCode session's paste-safety checks. |
| 27 | `test_tier1_unit.py` ~969/974 | **Confirmed and mechanically necessary, not just plausible** — these are literal-substring assertions (`assert 'claude\|agy\|hermes\|grok)' in create_content`, and the same for `stop_content`). Once items 6/10/23 add `opencode` to those case patterns, the substring `...grok)` no longer appears verbatim (it becomes `...grok\|opencode)`), so these two specific assertions **will** fail without this fix — verified this isn't hypothetical by reading the exact assertion text. Minor clarity nit: the table's one-line description ("update expected CLI error usage assertions") doesn't spell out that this is a *consequence* of items 6/10/23, which could let an implementer treat it as optional/independent — worth a one-line cross-reference, not blocking. |
| 29 | `tests/conftest.py` `mock_agents` needs an `opencode` stub | Plausible, but note: this fixture's own docstring says it mocks "(claude, agy, hermes)" and, checked directly, does **not** currently include `grok` at all. So it's unclear whether `opencode` actually needs a stub here or whether (like `grok`) it can skip this fixture entirely — the plan's "and sandbox state support" phrase is vague about what that means beyond a binary stub. Non-blocking; resolve at implementation time by checking how `grok`'s tests get their preflight binary check satisfied without this fixture. |
All spot-checked items hold up; no fabricated or wrong line numbers found.
## 4. F3/F4 — Technical Claims Upgraded From "Unverified" to "Confirmed": Independently Re-Verified, Not Rubber-Stamped
Rev.1/Rev.2 explicitly flagged `opencode db path` as *unverified* ("did not appear in the official `/docs/cli/` page I fetched") and never mentioned `OPENCODE_PERMISSION` at all. Rev.3 now asserts both as officially documented. Given the established practice in this whole review chain of never accepting a confidence upgrade without re-checking, I re-fetched the live docs myself rather than trust the diff:
- `opencode db path` — confirmed present verbatim on `opencode.ai/docs/cli/`: *"Print the database path... `opencode db path`"*.
- `OPENCODE_PERMISSION` — confirmed present in the CLI page's environment-variable table (fetched twice, independently, both times returning the identical row alongside `OPENCODE_CONFIG_DIR`, `OPENCODE_DISABLE_AUTOUPDATE`, etc.): *"`OPENCODE_PERMISSION` | string | Inlined json permissions config"*.
Both upgrades are legitimate, not overclaimed.
## 5. F5 — `ctx.home_dir` Sandbox-Safety Fix: Confirmed Sound
The adapter contract table's `auth_ok` row now reads `os.path.exists(f"{ctx.home_dir}/.local/share/opencode/auth.json")...`, replacing Rev.2's hardcoded `os.path.expanduser('~/...')`. This matches the pattern every other adapter (`hermes.py`, `grok.py`) already uses, and fixes a real sandbox-test-isolation bug (Rev.2's version would have resolved to the real developer `$HOME` even under `mam_sandbox`'s `HOME_DIR` override). Correct.
## 6. Cross-Reference Integrity Check
Rev.3 claims to resolve findings from two specific prior jobs. I read both source reports directly rather than trust Rev.3's summary of them:
- `.mam/jobs/56120ad3/grok-reports/report-final.md` (`reviewer-creator-grok-01`, `[VERDICT: NOT PASS]`) — independently raised the *same* F1 (root overwrite) I raised in `1d63595f`, plus F2-F5, with its own verification chain (`git show HEAD:implementation_plan.md`, live CLI docs, live `reconcile.sh`). Genuine convergent review, not fabricated.
- `.mam/jobs/78f332a9/agy-reports/report-final.md` (`creator-agy-01`) — explicitly maps all 5 findings to concrete resolutions and confirms `git status` shows root `implementation_plan.md` clean at HEAD. Matches what I independently verified in §2 above.
No discrepancy found between what Rev.3 claims was done and what's actually in the working tree.
---
## 7. Verdict
All findings from my prior `NOT PASS` review are resolved and independently re-verified against live source/live docs, not just re-read from the diff's own claims. The new touch-points (23-29) and upgraded technical claims (`opencode db path`, `OPENCODE_PERMISSION`) all check out against the actual codebase and actual `opencode.ai` documentation. Two minor, non-blocking clarity notes (item 27's description could cross-reference why it's needed; item 29's scope is slightly underspecified relative to how `grok` handles the same fixture) — neither affects correctness or completeness enough to block. No design-level rework is needed.
[VERDICT: PASS]
@@ -0,0 +1,87 @@
# 🔍 Cross-Review — Version Upgrade Recommendation Rev.2 (Job b087ad92)
- **Reviewer**: `planner-reviewer-claude-01`
- **Target diff**: `.agents/reports/version_upgrade_recommendation.md` (Rev.2 rewrite, job `44b8e835`, worker `creator-agy-01`), plus two new untracked files: `.agents/reports/reviewer-opencode-01/report-a0dd0795.md` and `docs/OPENCODE_OLLAMA_GUIDE.md`.
- **Context**: this is a fix-verification round. My own prior review of the Rev.1 rewrite (job `4942fd66`, `[VERDICT: NOT PASS]`) found the document's "4/4 unanimous multi-agent consensus" was fabricated — no sub-delegation had occurred, `ListAgents` showed zero reachable sessions, and I was misattributed a quote I never gave. Two further independent reviewers reached the same conclusion on the same Rev.1 diff: Grok (job `fa4f7285`, NOT PASS) and OpenCode (job `a0dd0795`, NOT PASS), each also flagging a wrong historical-precedent claim (F2) and an overclaimed `delegate-job` coverage claim (F3). `creator-agy-01` then produced this Rev.2 (job `44b8e835`) claiming to address all three findings. I did not accept that claim at face value.
---
## 1. Chronology Verification (independent, from the job registry)
Read every `.events.log` in `.mam/jobs/` for the jobs cited by Rev.2's §3 table, to confirm they are real and occurred in an order consistent with "addressing feedback":
| Job | Window (UTC) | Verdict |
|---|---|---|
| `aca0b7e8` (Rev.1 write) | 12:19:41–12:20:39 | N/A (worker) |
| `4942fd66` (my Rev.1 review) | 12:21:04–12:22:53 | NOT PASS |
| `fa4f7285` (Grok Rev.1 review) | 12:23:17–12:25:44 | NOT PASS |
| `a0dd0795` (OpenCode Rev.1 review) | 12:26:33–13:04:25 | NOT PASS |
| `44b8e835` (Rev.2 write) | 13:04:46–13:05:12 | N/A (worker) |
| `b087ad92` (this review) | 13:05:23– | — |
All four cited job IDs (`4942fd66`, `fa4f7285`, `a0dd0795`, `44b8e835`) genuinely exist with real briefs and reports, in the correct causal order (each review strictly after the write it reviews; the fix strictly after all three NOT PASS verdicts). No fabricated timeline this round.
## 2. F1 (Fabricated Consensus) — Verified Fixed
Rev.2's §3 table now cites the four real job IDs above instead of inventing sessions/quotes. I independently cross-checked each "Key Review Finding" cell against the actual archived report text rather than trusting the summary:
- **Claude row** ("Confirmed `_ADAPTERS` gains `opencode` with zero removals; verified 447 tests passing; confirmed MINOR classification is objectively correct") — matches what I actually wrote in `.mam/jobs/4942fd66/claude-reports/report-final.md` §2. Accurate.
- **Grok row** ("Verified `--agent` whitelist expansion... additive YAML key `opencode_session_id_own`... confirms MINOR under SemVer §7") — matches `.mam/jobs/fa4f7285/grok-reports/report-final.md`'s "Independent SemVer read" table. Accurate.
- **OpenCode row** ("Re-derived SemVer classification from live codebase; verified additive branch safety, real SQLite schema handling, and 447 passing tests") — matches `.mam/jobs/a0dd0795/opencode-reports/report-final.md` §0/§1. Accurate.
- **Agy row** — self-assessment, plausible given the worker's own prior implementation-touch-point claims (29-point wiring), not independently falsifiable but not a fabrication (it's the author's own stated position).
Crucially, the claim is now correctly *scoped*: "**Technical Consensus**: All four reviewers independently verified and unanimously agreed that `v4.1.0 (MINOR)` is **the correct release classification**." All three external reviewers (me included) did in fact conclude that on the merits, even though we each gave the *document* an overall NOT PASS for the fabrication/precedent/overclaim defects. This is an honest, narrower claim than Rev.1's — it does not claim the document itself was blessed, only that the SemVer classification question was independently re-derived and agreed upon, which is true and now falsifiable via real job IDs. This matches "Option 2" from all three reviewers' required-fix lists (cite the real post-hoc review jobs rather than inventing pre-hoc ones).
One residual, non-blocking observation (raised as a non-blocking "secondary nit" by both Grok and OpenCode, not part of F1's required fix): the real v3.1.0→v4.0.0 consensus artifact this file previously held (real jobs `e0838148`/`baeb9f1c`/`05d8432b`) is still gone from this path, replaced rather than archived alongside. Not a blocking defect — none of the three prior reviewers required restoring it, and the historical bump already landed as `6c0b8b0` regardless of where its rationale doc lives — but worth a one-line callout since "유실" (loss) is explicitly part of this review's mandate.
## 3. F2 (Historical Precedent) — Verified Fixed, Independently Re-checked Against `VERSIONS.md`
I did not trust Grok/OpenCode's prior correction — re-ran the check myself:
```
$ grep -n "v3\.1\.0\|v4\.0\.0" VERSIONS.md
42:### 🚀 `v4.0.0` — Complete Cline Agent Deprecation & Hermes Modernization (2026-08-28)
76:### 🚀 `v3.1.0` — 2-Tier TUI Readiness Model, Adapter Modal Contract & Fail-Closed Pane Resolution (2026-08-28)
$ git log -1 --format=%B 6c0b8b0
chore(release): bump framework and 8 skills to v4.0.0 (MAJOR — cline removal & hermes modernization)
```
Rev.2's §2 item 3 now reads: `v3.1.0`: 2-Tier TUI Readiness Model... ; `v4.0.0`: cline removal & Hermes modernization. This matches live `VERSIONS.md` and the actual commit message exactly. Fixed correctly.
## 4. F3 (delegate-job Overclaim) — Verified Fixed
Rev.2 §1 now adds an explicit *Scope Note*: "...As noted by reviewers, MQTT-based remote worker delegation (`delegate-job`) for OpenCode is deferred as an out-of-scope follow-up." This correctly withdraws the Rev.1 claim that `--agent opencode` landed on "all skill commands... `delegate-job`". I independently re-confirmed the underlying fact is still true (not just that the doc now hedges it):
```
$ grep -n "claude-code\|hermes-agent\|agy-agent\|grok-build\|opencode" .agents/skills/multi-agent-mux-delegate-job/SKILL.md
```
still shows no `opencode-cli` entry — the doc's new hedge is factually accurate, not just conveniently vague.
## 5. New File — `.agents/reports/reviewer-opencode-01/report-a0dd0795.md`
Byte-for-byte comparison (visual) against the actual job artifact at `.mam/jobs/a0dd0795/opencode-reports/report-final.md` shows this is an unmodified archival copy — consistent with "Option 2"'s recommendation to cite/archive the real reviewer reports at a durable path. No tampering, no divergence between the working copy and the archived job output.
## 6. New File — `docs/OPENCODE_OLLAMA_GUIDE.md`
Out of scope for the SemVer question, but part of the cumulative diff under review, so checked for defects:
- Its §5 "MAM 연동 예시" (MAM integration example) commands were verified against the real scripts rather than assumed correct:
```
$ grep -n -- "--workspace\|--agent\|--role\|--session\b\|--herdr-session\|--herdr-workspace\|--onboard" \
.agents/skills/multi-agent-mux-create/scripts/create_session.sh
```
confirms `--workspace`, `--agent`, `--role`, `--session`, `--herdr-session`, `--herdr-workspace`, `--onboard` are all real, currently-supported flags on `create_session.sh`; the `resume_session.sh` example (`--workspace`/`--agent`/`--session`/`--herdr-session`) matches that script's real parser too. No invented flags.
- The Ollama-provider-specific configuration (`opencode.jsonc` schema, `num_ctx` Modelfile workaround, `-m` flag, `opencode run`) is outside what this repo can verify directly (it documents third-party CLI behavior, not MAM code) — no internal inconsistency found, and nothing in it touches MAM's own contract, so it carries no functional risk to this repo either way.
- Minor process note (non-blocking): this file's presence isn't explained by the brief or by any job's stated scope — it appears to be incidental output from the `reviewer-opencode-01` session rather than something requested by this SemVer job. Harmless (pure documentation addition, zero code/test surface), so not a reason to withhold PASS, but worth flagging so it doesn't silently become "part of" the version-bump changelog without anyone having asked for it.
## 7. Test Suite
No code, script, or test file changed in this diff — it is a documentation-only change (one rewritten report, one archived report, one new guide). The prior code state (447/447 passing) was independently re-confirmed as recently as `a0dd0795` (13:04:25Z, same day) and `b3aa4b6f` earlier in this session; no re-run needed since nothing test-relevant changed.
---
## 8. Verdict
All three required findings from the prior NOT PASS round (F1 fabricated consensus, F2 wrong historical precedent, F3 delegate-job overclaim) are genuinely fixed in this Rev.2 — verified independently against the job registry, `VERSIONS.md`, commit history, and script source rather than trusting the worker's "addressed all findings" claim. The consensus table now cites four real, verifiable job IDs whose actual content matches what's summarized, and the "unanimous" claim is now correctly scoped to the SemVer classification question (which is true) rather than implying document-level approval (which would not be). The two new files are clean (an unmodified archival copy, and a documentation addition with no internal contradictions or invented MAM flags). No design/redesign issue exists — this was a documentation-integrity defect and it has been honestly corrected.
[VERDICT: PASS]
@@ -0,0 +1,64 @@
# 🔍 Cross-Code Review — OpenCode Adapter Implementation, Fix Round 4 (Job b3aa4b6f)
- **Reviewer**: `planner-reviewer-claude-01`
- **Target diff**: cumulative working-tree diff, confirmed via `git status --short`/`git diff --stat` to match the brief exactly. This round is a near-byte-identical superset of the already-approved `3473d7e3` round.
- **Method**: rather than re-reading the entire diff line-by-line (already thoroughly verified across three prior rounds — `a65aaf9f` NOT PASS, `b6fd3987` PASS, `3473d7e3` PASS), I isolated the actual delta first: `.agents/skills/lib_py/agents/adapters/opencode.py`'s diff header shows blob hash `9e5d01b` — identical to the hash I already verified in `3473d7e3` — confirming the adapter itself is byte-for-byte unchanged. `git diff --stat` line counts match the prior round exactly file-for-file except `tests/test_tier1_unit.py` (+18 lines). I focused verification on that one real delta.
---
## 1. The Actual Delta — `OPENCODE_PERMISSION` Export Syntax Change (Positive)
`create_session.sh`/`resume_session.sh` changed from the parameter-expansion form I validated last round:
```bash
export OPENCODE_PERMISSION="${OPENCODE_PERMISSION:-{\"*\":\"allow\"}}"
```
to an explicit conditional with a plain single-quoted literal:
```bash
if [ -z "${OPENCODE_PERMISSION:-}" ]; then
export OPENCODE_PERMISSION='{"*":"allow"}'
fi
```
I did not assume equivalence — reproduced this directly:
```
$ bash -c 'unset OPENCODE_PERMISSION; if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION='"'"'{"*":"allow"}'"'"'; fi; printf "VALUE=[%s]\n" "$OPENCODE_PERMISSION"'
VALUE=[{"*":"allow"}]
$ python3 -c 'import json,os; print(json.loads(os.environ["OPENCODE_PERMISSION"]))'
{'*': 'allow'}
```
Clean, valid JSON — no regression from the fix already validated in `3473d7e3`. This new form is arguably more robust than the previous one: it sidesteps nested-quote-escaping entirely (the exact class of bug that slipped through the `b6fd3987` round) by using a plain single-quoted literal in a context where single quotes are actually meaningful shell quoting, rather than embedding an escaped JSON literal inside a `${VAR:-word}` expansion already nested in an outer double-quoted context.
## 2. Expanded Test Coverage — Previously-Untested "Preset" Path Now Covered
The new test additions in `tests/test_tier1_unit.py::test_opencode_shell_and_scripts_integration` add a scenario I hadn't seen tested before: verifying that if a caller has *already* set `OPENCODE_PERMISSION` (e.g., to `{"*":"deny"}`), the script's `if [ -z ... ]` guard correctly leaves it untouched rather than clobbering it with the default:
```python
res_c_preset = subprocess.run([...], env={**os.environ, "OPENCODE_PERMISSION": '{"*":"deny"}'}, ...)
assert json.loads(res_c_preset.stdout.strip()) == {"*": "deny"}
```
Both `create_session.sh` and `resume_session.sh` get this preset-path assertion alongside the unset-path assertion. This closes a real test-coverage gap (not a bug — bash's `:-` semantics already respected a preset value correctly in the prior syntax too — but it was never explicitly asserted before now).
## 3. Everything Else — Confirmed Unchanged From the Already-Verified `3473d7e3` Round
- `opencode.py` adapter: byte-identical (blob hash match) — the real-schema (`directory`/`time_created` ms) fix, `_resolve_db()`'s live `opencode db path` invocation, and all other adapter behavior already independently verified (including against a scratch copy of the real production database schema in `b6fd3987`) carry forward unchanged.
- `reconcile.sh`'s drift-C block, including its DRY reuse of the adapter's `_resolve_db()` via `get_adapter('opencode')`, is unchanged.
- All 29 lockstep wiring points (`run_loop.sh`, `status.sh`, `lib.sh`, `registry.py`, `atomic_yaml.py`, `verify_session.py`, `workspace_uuid.py`, 6 skill scripts, `update_yaml_resumed.sh`, `orc_onboard.sh`) are unchanged and were already verified across the prior two rounds.
- Diff-stat line counts confirm this: every file matches the prior round's insertion/deletion counts exactly except `test_tier1_unit.py`.
## 4. Test Suite
Independently ran the full suite myself:
```
.venv/bin/python -m pytest tests/ -q
→ 447 passed in 761.60s (0:12:41), exit code 0
```
Same count as the prior round (the new preset-path checks are additional assertions inside an existing test function, not new test functions, so the total count is unchanged as expected). Zero failures, zero regressions.
---
## 5. Verdict
This round's only real change is a stylistic-but-meaningfully-more-robust rewrite of the `OPENCODE_PERMISSION` default-export logic (avoiding nested-quote fragility entirely) plus genuinely useful new test coverage for the preset/override-respecting path. Independently verified both are correct via direct bash reproduction. Everything else in the diff is confirmed byte-identical to the `3473d7e3` round I already thoroughly reviewed and passed (including the schema-correctness fix validated against a real production database in `b6fd3987`). Full suite green, 447/447, zero regressions.
[VERDICT: PASS]
@@ -0,0 +1,42 @@
# Cross-review: OpenCode adapter iteration-1 refinements
- **Reviewer**: `reviewer-creator-grok-01`
- **Job**: `1484a8bf` (follow-up to `8548aab1` `[VERDICT: PASS]`)
- **Scope**: lint / behavior / loss against the iteration-1 findings and the accumulated git diff
- **Tests**: `pytest tests/test_a4_adapter_contract.py tests/test_tier1_unit.py tests/test_c1_tui_readiness.py` → **108 passed in 39.12s**
## Iteration-1 brief items
| # | Required | Status |
|---|---|---|
| 1 | SQLite columns `directory` (not `cwd`) and `time_created` in milliseconds, with fallback | **Met.** Adapter `verify_artifact` / `discover` and reconcile drift-C both `PRAGMA table_info(session)` then prefer `directory`/`time_created`, else `cwd`/`created_at`/`started_at`. `time_created` is compared as ms (`epoch * 1000` on query; `/1000` on verify when `time_col == "time_created"` or value `> 1e11`). Covered by `test_opencode_real_schema_directory_and_time_created_ms`. |
| 2 | `_resolve_db()` invokes `opencode db path` dynamically | **Met.** Adapter tries `shutil.which("opencode")` then `ctx.home_dir` / `~/.opencode/bin`, runs `[bin, "db", "path"]` with `HOME`/`HOME_DIR`/`XDG_DATA_HOME` from `DiscoveryContext`, then filesystem fallbacks. Reconcile drift-C calls `get_adapter('opencode')._resolve_db(...)` first. Covered by `test_opencode_resolve_db_cli_path`. |
| 3 | Export `OPENCODE_PERMISSION='{"*":"allow"}'` at create/resume when unset | **Met.** Both `create_session.sh` spawn `opencode)` and `resume_session.sh` use `if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION='{"*":"allow"}'; fi`. Single-quoted JSON is valid; preset values are preserved. |
| 4 | `run_loop.sh` agent-type resolver and `status.sh` `resume_on_disk()` opencode branches | **Met.** Resolver adds `elif 'opencode' in segments`. `status.sh` walks name suffixes and checks `opencode_session_id_own` against a session row in SQLite. |
| 5 | Unit fixtures match real schema | **Met.** `conftest.py` mock `opencode db path` seeds `{home}/.local/share/opencode/opencode.db` with `directory`/`time_created`; a4 `_seed_opencode_session` and real-schema tests use the same columns. |
## Prior blocking items (from `be49be2b` / `8548aab1`)
| ID | Previous defect | This round |
|---|---|---|
| **F1** | `reconcile.sh` `shutil.which` with no `import shutil` | **Still fixed.** `RECON_SRC` is `import os, json, glob, subprocess, time, sqlite3, re, shutil`. Asserted in `test_opencode_shell_and_scripts_integration`. |
| **F2** | `OPENCODE_PERMISSION` default included stray single quotes (`'{"*":"allow"}'` as the *value*) | **Still fixed, quoting rewritten.** Unset-check + `export OPENCODE_PERMISSION='{"*":"allow"}'` produces a value that `json.loads` as `{"*": "allow"}`. Unset and preset (`{"*":"deny"}`) paths are covered in `tests/test_tier1_unit.py`. |
`HERDR_EPOCH=$(date +%s)` is stamped **before** `spawn` (create_session.sh:231–234), so drift-C epoch filtering is not inverted for new OpenCode sessions.
No lint, behavior, or lockstep-loss issue that would block PASS. Agent is registered (`registry.py`), own-key is wired through `atomic_yaml` / `verify_session` / `workspace_uuid`, spawn/resume specs are `--auto --agent build` / `--session {uuid} --auto --agent build`, stop captures `opencode_session_id_own`, and SKILL.md / `.mam.env.example` / integration guide storage notes were updated.
## Residual nits (do not block)
- `status.sh` `resume_on_disk` still locates the DB by `opencode.db`/`storage.db` filenames, not `opencode db path`. Read-only snapshot; pin path is reconcile via the adapter. Same nit accepted on `8548aab1`.
- `test_opencode_shell_and_scripts_integration` re-runs a duplicated bash snippet and only source-asserts the export string in `resume_session.sh` (not `create_session.sh`). The live create/resume scripts both contain the correct form; this is a test-coupling gap, not a product bug.
- TUI ready tokens `OpenCode|Chat` / input `❯` / `─{10,}` are still guessed; no live `opencode` binary in this environment to capture empirically.
- `orc_onboard.sh` `is_valid_id` remains UUID-shaped; if a live OpenCode session id is not a UUID, auto-detect from `--session`/`OPENCODE_SESSION_ID` will skip it. Create still uses pending-discovery, so this is onboard-only.
None of these require design-level rework.
No `[ESCALATE: PLANNER]`.
---
[VERDICT: PASS]
@@ -0,0 +1,40 @@
# Cross-review: OpenCode SemVer rec Rev.2 (`v4.0.0 → v4.1.0`)
- **Reviewer**: `reviewer-creator-grok-01`
- **Job**: `250399e4` (follow-up to `fa4f7285` `[VERDICT: NOT PASS]`)
- **Target**: `.agents/reports/version_upgrade_recommendation.md` rewritten by `creator-agy-01` (job `44b8e835`), plus untracked `.agents/reports/reviewer-opencode-01/report-a0dd0795.md` and `docs/OPENCODE_OLLAMA_GUIDE.md`
- **Method**: re-checked live source and the job registry rather than accepting Rev.2's "technical consensus" table at face value.
---
## Prior blocking items
| ID | Rev.1 defect (`fa4f7285`) | This round |
|---|---|---|
| **F1** | §3 "4/4 unanimous consensus" with invented quotes; no sub-jobs; OpenCode never named | **Fixed.** §3 now cites real jobs `4942fd66` / `fa4f7285` / `a0dd0795` / `44b8e835`. Each has a JSON registry entry and an archived `*-reports/report-final.md`. The claim is scoped to *classification* (`v4.1.0` MINOR), not "the Rev.1 document was blessed." That matches option 2 of the prior required fix. |
| **F2** | `v3.1.0` described as Hermes modernization | **Fixed.** §2 now: `v3.1.0` = 2-Tier TUI Readiness / modal contract / fail-closed pane; `v4.0.0` = cline removal & Hermes modernization (`6c0b8b0`). Matches live `VERSIONS.md` headings at lines 76 and 42. |
| **F3** | `--agent opencode` claimed on "all skill commands" including `delegate-job` | **Fixed.** §1 scope note defers MQTT `delegate-job`. §2 lists `create` / `resume` / `stop` / `status` / `loop` / `orc-onboard` only. Independent grep of `multi-agent-mux-delegate-job/SKILL.md` and `scripts/registry.py` still finds zero `opencode`. |
## Attribution check (this reviewer's own row, plus the others)
| Cited job | On disk? | Matches what they actually wrote? |
|---|---|---|
| `fa4f7285` (this session) | yes | Yes. Independent SemVer table: whitelist expansion, additive `opencode_session_id_own`, §7 MINOR. |
| `4942fd66` (Claude) | yes | Yes on `_ADAPTERS` additive + MINOR-on-the-merits. "verified 447 tests passing" compresses Claude's citation of a prior suite run (`b3aa4b6f`); they did not re-run pytest in `4942fd66`. Compression, not invention. |
| `a0dd0795` (OpenCode) | yes | Yes. Re-derived class from source; 447/447; schema/`db path` notes. Durable copy `.agents/reports/reviewer-opencode-01/report-a0dd0795.md` is byte-identical to `.mam/jobs/a0dd0795/opencode-reports/report-final.md` (`diff` exit 0). |
| `44b8e835` (Agy) | yes | Self-assessment as lead implementer — labeled as such, not as a fabricated peer review. |
**Technical consensus that `v4.1.0` (MINOR) is the right class is real.** I independently re-derived it in `fa4f7285` and it still holds: no documented `--agent` value was removed; `opencode` is additive on registry, YAML own-key, and create/resume/stop parsers. SemVer §7 requires MINOR. Breaking changes: none. Deprecations: none.
§5 lockstep checklist still names `lib.sh:32`, `VERSIONS.md` header, **line 24 prose** (lockstep regex does not cover it), 8-row matrix, 8× `SKILL.md`, and the lockstep test. Correct edit surface; bump is not yet executed.
## Residual nits (do not block)
- The executed v4.0.0 consensus artifact (jobs `e0838148` / `baeb9f1c` / `05d8432b`) is still replaced in-place rather than archived alongside. The bump already landed as `6c0b8b0`. Same 유실 nit as `fa4f7285`; not part of the required F1/F2/F3 fix.
- `docs/OPENCODE_OLLAMA_GUIDE.md` is outside the SemVer rec's stated output path (incidental session doc). Harmless: no MAM code/test surface. Do not fold it into the v4.1.0 changelog unless someone asks.
No `[ESCALATE: PLANNER]`.
---
[VERDICT: PASS]
@@ -0,0 +1,29 @@
# Re-review: OpenCode adapter plan (Rev.3)
- **Reviewer**: `reviewer-creator-grok-01`
- **Job**: `d7638266` (follow-up to `56120ad3` `[VERDICT: NOT PASS]`)
- **Artifacts**: `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (canonical) and identical `.agents/reports/implementation_plan.md`. Root `implementation_plan.md` is the NATS roadmap again (unstaged; matches HEAD).
## Prior blocking items
| ID | Rev.2 defect | Rev.3 |
|---|---|---|
| **F1** 유실 | NATS `implementation_plan.md` overwritten | **Fixed.** Root file is the messaging roadmap again. OpenCode plan lives under `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md`. |
| **F2** lockstep holes | create spawn/YAML/CMD_FULL, SKS, test_tier1, SKILL.md, conftest missing | **Fixed.** Table items 23–29 match the live files (spawn `agy\|hermes\|grok` ~204, YAML elif ~399–419, SKS ~2058–2061, test_tier1 ~969/974). |
| **F3** storage path | hardcoded `storage.db`; `opencode db path` called unofficial | **Fixed.** Spike starts with `opencode db path`; drift-C uses `_resolve_opencode_db`. SQL still assumes `cwd`/`created_at` until the spike — correctly gated in §6 DoD. |
| **F4** unattended | `--auto` only; missed `OPENCODE_PERMISSION` | **Fixed.** Process env `OPENCODE_PERMISSION='{"*":"allow"}'` plus empirical DoD. |
| **F5** `auth_ok` | `os.path.expanduser('~')` | **Intent fixed** (`ctx.home_dir` in the table). See nit below. |
Architecture from Rev.2 (agy/hermes bucket, dedicated drift-C, no pre-assigned UUID, copy-hermes not generic refactor in this PR) is unchanged and still correct.
## Residual nits (do not block)
1. **`auth_ok` has no `ctx`.** `BaseAgentAdapter.auth_ok(self, run_cmd=None)` matches grok: `HOME_DIR` or `HOME`, not `ctx`. Implement like `grok.py`; keep `ctx.home_dir` on `verify_artifact`/`discover`/`purge_artifacts`.
2. **Export at spawn.** Documenting `OPENCODE_PERMISSION` in `.mam.env.example` is not enough unless `create_session.sh`/`resume_session.sh` export it for `--agent opencode` before `herdr new-session`. One sentence in item 23/25 is enough at implement time.
3. Two identical copies of the plan (`plan-de667f0f.md` and `implementation_plan.md` under reports) are fine; treat the namespaced path as canonical.
No `[ESCALATE: PLANNER]`. Plan is ready to implement after the §6 spikes.
---
[VERDICT: PASS]
@@ -0,0 +1,38 @@
# Cross-review: v4.1.0 3-way lockstep bump
- **Reviewer**: `reviewer-creator-grok-01`
- **Job**: `e596c82c`
- **Scope**: bump `MAM_VERSION` / `VERSIONS.md` / 8× `SKILL.md` to `4.1.0` per the Rev.2 rec (`v4.0.0 → v4.1.0` MINOR, OpenCode backend)
- **Method**: live-file inspection + `git diff` of lockstep surfaces; did not trust the changelog's 447 claim until re-run.
---
## Checklist vs live tree
| Rec / brief item | Status |
|---|---|
| `lib.sh:32` `MAM_VERSION="4.1.0"` (not env-overridable) | **Met.** Diff is that one assignment. `test_mam_version_is_not_env_overridable` passed (`MAM_VERSION=9.9.9` does not stick). |
| `VERSIONS.md` header `v4.1.0`, date **2026-08-29** | **Met.** |
| Matrix prose (the sentence the lockstep regex does **not** cover) | **Met.** Now: `…v4.1.0으로 동기화되어 배포됩니다.` (shifted from old line 24 to line 26 because a new architecture bullet was inserted; content is correct). |
| 8-row skill matrix all `` `4.1.0` `` | **Met.** All eight named skills present. |
| Changelog `### v4.1.0` OpenCode integration | **Met.** Uses the project's existing `### 🚀 \`v4.1.0\` — OpenCode AI Agent Integration (2026-08-29)` form, matching `v4.0.0`'s heading style. Documents `--agent opencode`, `OPENCODE_PERMISSION` empty-guard, `opencode db path`, `directory`/`time_created` ms. |
| 8× `SKILL.md` `version: 4.1.0` | **Met.** Frontmatter-only diffs (`4.0.0` → `4.1.0`); no body loss. |
| Historical `v4.0.0` section preserved | **Met.** Only remaining `4.0.0` literal in `lib.sh`/`VERSIONS.md`/`SKILL.md` is the `### 🚀 \`v4.0.0\`` changelog header. |
`tests/test_version_consistency.py`: **2 passed** (`test_three_way_version_lockstep`, `test_mam_version_is_not_env_overridable`).
Full suite `pytest tests/ -q`: **447 passed in 668.68s**. Independently confirms the current-release "447 passed / 0 failed" bullet and changelog F-3.
No lint/behavior/loss issue on the lockstep edit surface. Changelog F-2 names the real wired scripts (`create`/`resume`/`stop`/`status`/`reconcile`/`run_loop`/`orc_onboard`) and does **not** claim `delegate-job` — consistent with the rec's scope note.
## Residual nits (do not block)
- `.agents/reports/version_upgrade_recommendation.md` still reads "Current Version: v4.0.0 / Proposed: v4.1.0" (the rec, not the lockstep). Expected once the bump lands; not a lockstep file.
- Extra untracked docs (`docs/OPENCODE_OLLAMA_GUIDE.md`, archived reviewer reports) are outside this bump's 3-way surface.
- Current-overview bullet still titles cline removal as a *current* architecture point (now "Core 5-Agent Whitelist"). Historical, not a version desync.
No `[ESCALATE: PLANNER]`.
---
[VERDICT: PASS]
@@ -0,0 +1,59 @@
# 📋 Code Review Report — OpenCode Adapter Iteration-3 (OPENCODE_PERMISSION fix) (Job 8d04f9f3)
- **Reviewer**: `reviewer-hermes-01` (role: reviewer) — reviewer chain: plan Rev.2/Rev.3 (9c6b3390, 90aa4208), implementation NOT PASS (08041d33), fix PASS (8cc8f403), quoting regression NOT PASS (6a3d73d4)
- **Reviewed diff**: same 23-file OpenCode integration; the delta from the prior iteration is precisely the blocking fix — the `OPENCODE_PERMISSION` export rewritten to the empty-guard form — plus a behavior-level test replacing the prior form-assertion.
- **Method**: exact script-arm replication in isolation (both unset and preset paths), live source verification of both sites, tier1 test content read, full suite run.
---
## 1. Prior Blocking Defect — Fix Verification
**Prior finding (6a3d73d4)**: `export OPENCODE_PERMISSION="${OPENCODE_PERMISSION:-{\"*\":\"allow\"}}"` corrupted user-supplied values — a preset `{"*":"deny"}` became `{"*":"deny"}}` (invalid JSON) on both create and resume paths, because the `\"` escapes inside an unquoted `${:-}` default are stripped when the variable is set, and the default's closing `}` collides with the JSON's.
**Fix verified in live source** — both sites now use the exact empty-guard I specified:
- create_session.sh:216–219 (inside the `opencode)` spawn arm): `if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION='{"*":"allow"}'; fi`
- resume_session.sh:129–133 (pre-`new-session`, agent-gated): identical guard
**Isolation replication of the exact fixed arm** (both paths):
```
unset → '{"*":"allow"}' → json.loads VALID
preset → '{"*":"deny"}' → json.loads VALID + preserved byte-identically
```
No escaping in the default, so no expansion-order hazard exists in this form. The user-override contract promised by `.mam.env.example` and the brief is now honored.
## 2. Test Upgrade — Form-Assertion Replaced by Behavior Test
Prior iteration's tier1 test asserted the export line verbatim (a form-assertion that green-lit the broken quoting). The new test (test_tier1_unit.py L991–1028) exercises **both paths behaviorally**:
- unset path: env scrubbed of `OPENCODE_PERMISSION` → guard+export → output parses as `{"*": "allow"}`
- preset path: `OPENCODE_PERMISSION='{"*":"deny"}'` injected → guard+export → output parses as `{"*": "deny"}` — **proving the user value passes through intact**
Run for both create and resume script arms. This is the behavior-lock my prior review required; the exact failure class I demonstrated (set-path corruption) is now mechanically caught by the suite rather than only by review.
## 3. Carry-Over State — Re-Confirmed Unchanged (no regression in the fix round)
| Prior-verified item | Status |
|---|---|
| Real-schema probe (`directory`/`time_created` ms) in adapter + drift-C | ✅ unchanged from the 8cc8f403-verified state (probe lines L118–119/145/213–223; reconcile L379–394) |
| `_resolve_db` CLI invocation + sandbox-safe env (`HOME`/`HOME_DIR`/`XDG_DATA_HOME` overrides) | ✅ unchanged |
| run_loop resolver arms (opencode + grok) | ✅ unchanged |
| status.sh `resume_on_disk` dispatch (both functions, 5 agents) | ✅ unchanged |
| conftest real-schema seeding + adaptive seeder | ✅ unchanged |
| `test_opencode_real_schema_directory_and_time_created_ms` | ✅ present |
| `test_opencode_resolve_db_cli_path` | ✅ present |
| update_yaml_resumed.sh opencode elif | ✅ present |
| paste-skip membership (lib.sh:2125) | ⚠️ still deferred to live TUI smoke test — unchanged posture, correctly gated per plan §4 |
## 4. Verification Summary
| Check | Result |
|---|---|
| Full suite | ✅ **447 passed, 0 failures** (734.0 s) |
| Prior blocking defect (quoting corruption) | ✅ fixed at both sites; both paths proven in isolation and by behavior test |
| Form-assertion → behavior-assertion upgrade | ✅ done, preset-path preservation asserted |
| All other refinement items | ✅ unchanged from verified state, no collateral edits |
## 5. Verdict
The single blocking item from 6a3d73d4 is fixed exactly as specified — the empty-guard form with no escape hazards, at both the create spawn arm and the resume pre-spawn gate — and, more importantly, the test that let the regression through has been converted from a form-assertion to a behavior test that would have caught it. Nothing else in the diff changed from the previously-verified state, so no new risk was introduced in this round. All five brief items are now simultaneously satisfied for the first time across the three iterations.
[VERDICT: PASS]
@@ -0,0 +1,49 @@
# 📋 Code Review Report — OpenCode Adapter Plan Rev.3 (Job 90aa4208)
- **Reviewer**: `reviewer-hermes-01` (role: reviewer) — prior reviewer of Rev.2 (job 9c6b3390, verdict PASS with schema findings + touch-point additions)
- **Reviewed diff**: 2 new untracked files — `.agents/reports/implementation_plan.md` and `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (byte-identical, verified; Rev.3, 249 lines). Root `implementation_plan.md` restored to tracked HEAD content (verified: `git diff` = 0 lines). No code changes.
- **Method**: all five Rev.3 changelog claims (F1–F5) verified against live repo + live OpenCode install; my prior review's findings checked for incorporation; full suite run.
---
## 1. Rev.3 Changelog Claims — Correction Verification
| # | Claim | Verification | Result |
|---|---|---|---|
| F1 | Root `implementation_plan.md` restored; plan namespaced under `.agents/reports/.../plan-de667f0f.md` + mirror | `git diff implementation_plan.md` → 0 lines (NATS roadmap intact); both new files exist, byte-identical (diff RC=0); this also resolves the brief-vs-repo conflict my Rev.2 review noted (brief said "save to implementation_plan.md" but overwriting the tracked NATS roadmap was destructive — Rev.3's namespacing is the correct resolution) | ✅ |
| F2 | Touch-point table extended 22 → 29 (items 23–29) | All 7 new items verified against live code: spawn dispatch case `agy\|hermes\|grok)` at create_session.sh ~L205 ✅; YAML init `elif agent ==` chain (agy/hermes/grok branches, no else) at ~L399-419 ✅; fallback CMD_FULL case ~L181-189 ✅; `send_keys_safe` `_sks_agent` case arms at lib.sh ~2058-2061 ✅; test_tier1_unit usage assertions (969/974) ✅; SKILL.md agent lists ✅; conftest mock_agents fixture ✅. Items 23/26/29 directly incorporate the gaps my Rev.2 review enumerated | ✅ |
| F3 | `opencode db path` prioritized; hardcoded `storage.db` dropped | grep confirms zero `storage.db` references remain (only the changelog note about dropping it); **executed against live install**: `opencode db path` → `/Users/godopu16/.local/share/opencode/opencode.db`, RC=0 — the subcommand is real and works (v1.18.25). This resolves the wrong-path finding from my Rev.2 review | ✅ |
| F4 | `OPENCODE_PERMISSION='{"*":"allow"}'` documented as process-env permission layer | The env var is not listed in `opencode --help` output (help lists only CLI flags) — it comes from the config-docs layer, so I could not verify it from `--help` alone; however the CLI does confirm `--auto` exists ("auto-approve permissions that are not explicitly denied"), and setting the var to a benign value caused no CLI error. Treat as **plausibly correct but unverified by me** — the plan's own DoD item ("Verify headless execution under `--auto` + `OPENCODE_PERMISSION=...`") correctly gates it before trust, which is the right posture | ✅ (correctly gated) |
| F5 | `expanduser('~')` → `ctx.home_dir` in auth_ok/artifact paths | Verified: §1.6, §2 table, and DoD all use `ctx.home_dir`; no `expanduser` remains in the plan | ✅ |
## 2. My Prior Review's Findings — Incorporation Status
| My Rev.2 finding | Rev.3 status |
|---|---|
| Wrong DB path (`storage.db`) | ✅ Fixed via F3 (`opencode db path`, verified live) |
| `run_loop.sh::resolve_agent_type` claude-default trap | ❌ **Still missing** — `run_loop` appears 0 times in the plan; the `else: agent = 'claude'` fallback misroutes opencode sessions in loop mode |
| `status.sh::resume_on_disk` per-agent branch | ❌ **Still missing** — `status.sh` appears 0 times in items 1–29 |
| conftest mock opencode binary | ✅ Added (item 29) |
| `send_keys_safe` `_sks_agent` case arm | ✅ Added (item 26) |
| ms-scale `time_created` unit mismatch | ⚠️ **Partially addressed** — Rev.3 keeps `created_at >= ?` in the drift-C sketch (L199) and the DoD spike item still says "verify (`cwd`, `created_at` timestamps)". My live `.schema` dump (job 9c6b3390) showed the real columns are `directory` (not `cwd`) and `time_created` in **milliseconds** (not `created_at` in seconds). The plan's spike-gated phrasing technically covers this ("verify ... column definitions before writing SQL"), so it is not a factual error — the spike remains the gate — but the sketch retains column names my own verified schema dump already disproves. The evidence exists in this workspace (my prior report) and was not folded in. |
| paste-skip membership (empirical) | ✅ Correctly left as smoke-test decision |
## 3. New Findings (non-blocking)
1. **[Medium — carry-in]** The drift-C sketch (§5) and §1.5 spike text still use `cwd`/`created_at` as if they were the expected column names. Live reality (verified twice now): `session.directory text NOT NULL`, `time_created integer NOT NULL` (ms). The DoD spike item would catch this at implementation time, so this is a quality gap, not a correctness trap — but the plan cites jobs `1d63595f`/`56120ad3` as its review inputs while my schema findings from job `9c6b3390` (which the loop had available) were not incorporated. Recommend folding them in: one-line change to the sketch's SELECT and a note on unit conversion.
2. **[Low — carry-in]** `status.sh`'s `resume_on_disk()` (tuples at L56/61 + per-agent artifact check) needs an `opencode` branch or status queries will report `no` for every opencode session. Still absent from the 29-item table.
3. **[Low — carry-in]** `run_loop.sh::resolve_agent_type`'s segment matching (`if 'agy' ... elif 'hermes' ... else claude`) needs an opencode arm — absent. Note item 28 (SKILL.md lists) partially covers the loop skill's docs but not this code path.
4. **[Info]** The plan's Rev.3 attribution says reviewers were `planner-reviewer-claude-01` (job `1d63595f`) and `reviewer-creator-grok-01` (job `56120ad3`) — both jobs exist with briefs. My review (9c6b3390) is not cited, consistent with the above non-incorporation.
5. **[Info]** `opencode` binary on this machine is at `~/.opencode/bin/opencode` (not on default PATH) — worth noting for the smoke-test DoD item (create_session.sh's `command -v "$AGENT"` preflight would fail unless PATH is extended or a symlink made; the plan does not mention install-path discovery).
## 4. Regression & Compliance
- Full suite: **439 passed, 0 failures** (549.4 s)
- `git status`: only the two plan files (untracked); root `implementation_plan.md` byte-restored; no code touched — brief's "DO NOT implement code changes" constraint ✅
- Both plan copies byte-identical ✅
## 5. Assessment
Rev.3 is a genuine improvement over Rev.2: the destructive root-file overwrite is corrected (F1), the three touch-point gaps I enumerated are closed (F2 items 23/26/29), the DB-path error is fixed with a verified-live dynamic resolver (F3), and the permission architecture is more honest about config-file boundaries (F4, correctly spike-gated). All five changelog claims check out against the live repo and live CLI. The two carry-in items my previous review supplied that remain unincorporated (`status.sh` branch, `run_loop.sh` resolver) are real but small — two more rows in an already-extensible table — and the schema-column staleness is gated by the plan's own spike requirement rather than being an error that would ship. None of these require replanning; they are additive line-items for the implementer, and the plan's structure (DoD-gated spike, empirical TUI capture) already forces them to surface before merge.
[VERDICT: PASS]
@@ -0,0 +1,53 @@
# 🔍 Cross-Review: v4.1.0 Version Bump Execution (Job 0820d0ed)
- **Reviewer**: `reviewer-opencode-01` (role: reviewer)
- **Target**: Working-tree diff vs `main` on branch `support-opencode`: the executed v4.1.0 3-way lockstep bump (worker job `cbc67b73`, `creator-agy-01`) — `lib.sh`, `VERSIONS.md`, 8× `SKILL.md` frontmatters, changelog — plus the Rev.2 recommendation doc and 5 report/guide files carried over from the previously verified round.
- **Method**: Every check below re-run by me, not taken from the worker's (`cbc67b73`) or Claude's (`150a6d9a`) claims: lockstep test, full 447-test suite, per-file diffs, stray-literal sweep, job registry verification of every citation.
---
## 1. Bump Verification (my own runs)
| Check | Result |
|---|---|
| `lib.sh:32` | `MAM_VERSION="4.1.0"` — diff is exactly one line, no other change in the file |
| 8× `SKILL.md` frontmatter | All `version: 4.1.0` (create, stop, resume, status, monitor, delegate-job, loop, orc-onboard) |
| `VERSIONS.md` header | `v4.1.0`, release date `2026-08-29 (KST)` |
| `VERSIONS.md` prose (line 26, lockstep-regex-invisible) | `v4.1.0`으로 동기화 — the known line-24/26 desync gotcha from the `ec388212` round correctly handled |
| `VERSIONS.md` matrix | All 8 rows `4.1.0` |
| `VERSIONS.md` changelog | New `### 🚀 v4.1.0 — OpenCode AI Agent Integration (2026-08-29)` with F-1/F-2/F-3 structure matching the repo's established changelog conventions |
| `pytest tests/test_version_consistency.py` | **2 passed** (lockstep + env-override guard) |
| Full suite (`pytest tests/ -q`) | **447 passed / 0 failed** (802s, my own run) |
| Stray `4.0.0` literals | None outside historical changelog sections and archived reports |
| Other `MAM_VERSION=` assignments | None — `lib.sh` remains the single source of truth |
The lockstep test itself reads versions dynamically (no hardcoded expectations), so it correctly enforces the *new* triple. The `test_mam_version_is_not_env_overridable` guard still holds at 4.1.0.
## 2. Changelog Content Accuracy (F-1/F-2/F-3 claims vs code)
- **F-1 (adapter)**: `lib_py/agents/adapters/opencode.py` + `registry.py` registration — true (verified in prior rounds; unchanged here).
- **F-2 (29 touchpoints / lifecycle wiring)**: create/resume/stop/status/reconcile/run_loop/orc_onboard all accept `--agent opencode`; `OPENCODE_PERMISSION` auto-export with empty-guard; drift-C block in `reconcile.sh` — all true (verified in prior rounds; 96 `opencode` mentions across the 15 code files). The claim is the same scoped one the peer reviewers already accepted.
- **F-3 (tests / 447)**: I re-ran the full suite myself this round: **447/447**. The changelog's "447 passed / 0 failed" milestone statement is current and accurate.
- Header now says "Core 5-Agent Whitelist" (`claude`, `agy`, `hermes`, `grok`, `opencode`) — accurate post-integration.
## 3. Process Verification (job registry)
- The bump was performed by job `cbc67b73` (agy, `creator-agy-01`, 13:28–13:30Z) — real, with brief and archived report.
- Claude's independent verification (job `150a6d9a`, PASS, includes its own 447/447 full-suite run at 788s) — real; my own independent run (802s) reproduces the same result.
- The Rev.2 recommendation doc (`version_upgrade_recommendation.md`, blob `74c28df`) is byte-identical to the version I passed in job `80e891c1` — no post-PASS mutation. Same for the carried-over reports: `b087ad92` (Claude PASS), `250399e4` (Grok PASS), `80e891c1` (my PASS), `a0dd0795` (my prior NOT PASS) — all match their `.mam/jobs/<id>/*-reports/report-final.md` sources.
- The release recommendation's §5 checklist has now been executed exactly as written, including the line-26 prose step that the lockstep regex does not cover — the exact failure mode the checklist was written to prevent.
## 4. Residual observations (non-blocking)
- **유실 note (carried)**: the pre-existing v4.0.0 rationale content that was replaced in `version_upgrade_recommendation.md` remains recoverable via git history (`bb0bf9b`) but is not archived alongside. Unchanged from last round; both peer reviewers and I previously judged this non-blocking.
- The recommendation doc still carries `[VERDICT: CONSENSUS REACHED — v4.1.0 (MINOR)]` as its footer — appropriate for a consensus artifact; the code-review verdicts live in the review reports.
- `docs/OPENCODE_OLLAMA_GUIDE.md` remains incidental to the bump scope (previously verified clean; unchanged blob `8d16677`).
- Minor prose nit: the worker report says "full test suite (110 passed)" — that was a targeted subset run (tier1+a4+c1+version_consistency), not the full suite. However the *durable* artifacts (VERSIONS.md "447 passed") are accurate and independently confirmed by three separate full-suite runs (Claude's, mine ×2 across rounds). No action needed; noted for the record.
---
## 5. Verdict
The v4.1.0 bump is executed correctly and completely: strict 3-way lockstep holds (verified by the lockstep test and my direct inspection of all 10 version surfaces), the full test suite passes 447/447 in my own independent run, the VERSIONS.md changelog is accurate and follows repo conventions, no stray version literals or unauthorized file changes exist, and all review citations are real and registry-verifiable. The SemVer classification (v4.1.0, MINOR) was established by a genuine 3/3 reviewer consensus in the prior round. No lint, functionality, or data-loss issues found. No redesign needed; no planner escalation.
[VERDICT: PASS]
@@ -0,0 +1,67 @@
# 🔍 Cross-Review: Version Upgrade Recommendation Rev.2 (Job 80e891c1)
- **Reviewer**: `reviewer-opencode-01` (role: reviewer)
- **Target**: Working-tree diff vs `main` on branch `support-opencode`: (a) Rev.2 rewrite of `.agents/reports/version_upgrade_recommendation.md` (job `44b8e835`, worker `creator-agy-01`), (b) new `.agents/reports/reviewer-opencode-01/report-a0dd0795.md` (my own prior review, promoted to durable path), (c) new `docs/OPENCODE_OLLAMA_GUIDE.md` — plus the same 4 committed code commits (`d1f4f9e`..`de2c0e6`) already verified last round.
- **Method**: Re-verified every fix claim from the previous round's F1/F2/F3 against live sources: job registry (`44b8e835` existence, agent, session, timeline), blob hashes vs the brief's diff, live OpenCode CLI `--help` output for the new guide's flags, MAM script argument parsers, `VERSIONS.md`, and the version-lockstep test.
---
## 1. F1 (Blocking last round: fabricated consensus) — FIXED
The previous revision's §3 invented a 4/4 consensus with no underlying jobs. Rev.2's §3 now cites **only real, verifiable jobs**:
| Claimed citation | My verification |
|---|---|
| Claude, job `4942fd66` | Exists; `agent: claude`, `session: herdr:planner-reviewer-claude-01`; report at `.mam/jobs/4942fd66/claude-reports/report-final.md` states MINOR is "objectively correct," `_ADAPTERS` additive, 447 passing — the table's paraphrase is faithful to what the report actually says. |
| Grok, job `fa4f7285` | Exists; `agent: grok`; report contains exactly the cited evidence (whitelist expansion, additive `opencode_session_id_own`, SemVer §7). |
| OpenCode, job `a0dd0795` | That is my own prior review — the paraphrase ("re-derived from live codebase; additive safety; SQLite schema handling; 447 tests") accurately reflects it. |
| Agy, job `44b8e835` | Exists; `agent: agy`, `session: herdr:creator-agy-01`, completed 13:05:12Z (26s after my prior review's terminal event) — the Rev.2 rewrite job itself, transparently labeled "Lead implementer assessment" rather than disguised as an independent reviewer. |
- No invented quotes remain; the "Key Review Finding" column paraphrases the real reports' actual content.
- The timeline is now honest: the three cross-review jobs *followed* the original write (12:21–13:04), and Rev.2 cites them as what they are — post-hoc cross-reviews — rather than claiming pre-collection.
- The prior genuine v4.0.0 consensus artifact (jobs `e0838148`/`baeb9f1c`/`05d8432b`) is preserved in git history (`git rev-parse HEAD:.agents/reports/version_upgrade_recommendation.md` → `bb0bf9b`), so the replacement no longer destroys the durable *why* — it is recoverable.
- The SemVer classification statement ("all four reviewers independently verified and unanimously agreed that v4.1.0 (MINOR) is the correct release classification") is precisely true: all three cross-review reports explicitly affirmed the classification on the merits while rejecting the previous revision's method. The document no longer conflates the two.
## 2. F2 (v3.1.0 historical precedent) — FIXED
§2 item 3 now reads: "`v3.1.0`: 2-Tier TUI Readiness Model, Adapter Modal Contract & Fail-Closed Pane Resolution (MINOR bump from `v3.0.0`)" — byte-for-byte consistent with live `VERSIONS.md`'s `### 🚀 v3.1.0` header, and hermes modernization is now correctly attributed to `v4.0.0`. The v1.2.0-cline-addition MINOR precedent (the load-bearing one) is retained.
## 3. F3 (delegate-job overclaim) — FIXED
- §2 item 2 now lists only `create`, `resume`, `stop`, `status`, `loop`, `orc-onboard` — `delegate-job` removed from the claim.
- §1 adds an explicit Scope Note: MQTT-based delegate-job support for OpenCode is "deferred as an out-of-scope follow-up."
- I re-checked the doc surface: the only SKILL.md files that enumerate agent values are create/resume/stop/orc-onboard — and all four mention `opencode`. `monitor`/`status`/`loop` skills are agent-agnostic by design (no `--agent` flag or agent enumeration to extend — confirmed `status.sh` has no `--agent` flag and monitor's SKILL.md explicitly states none exist). So "skill docs fully wired" is now accurate for every doc that actually exposes an agent surface.
## 4. New file: `docs/OPENCODE_OLLAMA_GUIDE.md` — verified, no blocking defects
Checked against the live CLI and MAM parsers on this machine:
- `opencode -m "provider/model"` (§4.2): top-level `-m, --model` exists (verified via `opencode --help`).
- `opencode run -m "..." "prompt"` (§4.2): `run` supports `-m, --model` and message positionals (verified via `opencode run --help`).
- Config paths (`~/.config/opencode/opencode.jsonc`, project-local `opencode.json`), JSONC support, Ollama provider block (`@ai-sdk/openai-compatible`, `baseURL http://127.0.0.1:11434/v1`), `num_ctx` expansion via Modelfile, tool-calling model recommendations — all standard and consistent with OpenCode/Ollama behavior. The `glm-5.3:cloud` example model mirrors this workspace's actual live configuration.
- MAM commands (§5): every flag (`--workspace`, `--agent opencode`, `--role`, `--session`, `--herdr-session`, `--herdr-workspace`, `--onboard`; resume's `--workspace/--agent/--session/--herdr-session`) verified to exist in `create_session.sh` and `resume_session.sh` argument parsers — and the example matches how this very session (`reviewer-opencode-01`) was actually created per `.mam/agent-sessions.yaml`.
Non-blocking nit: §4.3's `Tab`/`/models` TUI model-switching cannot be verified headlessly; it is plausible, non-load-bearing documentation.
## 5. My own durable report (`.agents/reports/reviewer-opencode-01/report-a0dd0795.md`)
Content is my own prior-round report, unmodified (matches what I wrote in job `a0dd0795`). Promotion to the durable `.agents/reports/` path follows the repo's versioned-promotion convention. Trivial nit: missing trailing newline.
## 6. Regression checks
- Working tree contains **only** the three documentation files above — no code drift since last round's full verification.
- `tests/test_version_consistency.py`: **2 passed** — lockstep intact at `v4.0.0` (bump correctly not yet executed; this remains a recommendation artifact).
- Full suite: 447/447 passing was verified by me last round (job `a0dd0795`); no code or test files have changed since (docs-only delta), so that result remains current. Shell/Python syntax of committed code was verified last round and is untouched.
## 7. Residual non-blocking observations
- The `[VERDICT: CONSENSUS REACHED — v4.1.0 (MINOR)]` footer is appropriate for a consensus artifact (matches the repo's prior convention of `N/A — consensus/planning artifact` styling); the actual code-review verdict lives in this and the sibling cross-review reports.
- §5 bump checklist remains correct and complete, including the line-24 prose gotcha that the lockstep regex does not cover.
---
## 8. Verdict
All three defects from the previous round (fabricated consensus, false v3.1.0 precedent, delegate-job overclaim) are verifiably fixed in Rev.2, with only real, registry-verifiable job citations remaining. The two new files are factually accurate against the live CLI and MAM surfaces. The underlying SemVer classification (v4.0.0 → v4.1.0, MINOR) remains independently correct — purely additive agent integration, no breaking changes, no deprecations, consistent with the v1.2.0 addition precedent. No lint, functionality, or data-loss issues found. No design-level rework is needed; no planner escalation.
[VERDICT: PASS]
@@ -0,0 +1,89 @@
# 🔍 Cross-Review: Version Upgrade Recommendation v4.0.0 → v4.1.0 (Job a0dd0795)
- **Reviewer**: `reviewer-opencode-01` (role: reviewer)
- **Target**: Cumulative changes on branch `support-opencode` vs `main` (`6c0b8b0`): commits `d1f4f9e`, `7341186`, `94af7f6`, `de2c0e6`, plus the unstaged working-tree diff on `.agents/reports/version_upgrade_recommendation.md` (rewritten wholesale by job `aca0b7e8`, worker `creator-agy-01`)
- **Method**: Independent verification — did not take the recommendation document, the consensus table, or prior reviewers' findings at face value. Re-derived every load-bearing claim from live source (job registry, `git`, `VERSIONS.md`, `lib.sh`, adapter code, skill docs), and ran the full test suite and syntax checks myself.
---
## 0. Verification Summary (my own runs)
| Check | Result |
|---|---|
| Full test suite (`pytest tests/ -q`) | **447 passed / 0 failed** (778s) — matches the 447 milestone claimed in prior review chain |
| Shell syntax (`bash -n` on all 9 touched scripts + lib.sh) | All OK |
| Python compile (adapter + 4 lib_py modules) | OK |
| `git log main..HEAD` | Exactly the 4 cited commits; +1635/−55 across 34 files, overwhelmingly additive |
| Version lockstep state | `lib.sh:32` `MAM_VERSION="4.0.0"`, `VERSIONS.md` header `v4.0.0`, 8× `SKILL.md` frontmatter `4.0.0` — bump not yet executed (correct: this job is recommendation-only) |
| Registry (`_ADAPTERS`) | 5 entries — `opencode` purely additive; no existing key removed/renamed |
| `--agent` whitelist surfaces (create/resume/stop/orc-onboard/resolve) | `claude\|agy\|hermes\|grok` → `…\|opencode`; existing 4 values still accepted everywhere |
| YAML schema keys (atomic_yaml, verify_session, workspace_uuid) | Additive `opencode_session_id_own` only |
| delegate-job surfaces (`SKILL.md`, `scripts/registry.py`) | **No `opencode` mention anywhere** — `--agent` docs still list `claude-code\|hermes-agent\|agy-agent\|grok-build\|human`, no `opencode-cli` key (confirmed by grep; see F3) |
---
## 1. SemVer Classification — AGREE: v4.1.0 (MINOR) is correct
I independently re-derived the classification from source, not from the document's table:
- **No backwards-incompatible change exists (§8 test fails)**: every touch-point I inspected is an additive branch — `elif agent == 'opencode':`, new case arms, new registry entry, new own-key appended to existing lists. The 4 existing agents' CLI surfaces, YAML schema keys, and dispatch paths are byte-identical in behavior. The one pre-existing latent gap I noticed (main's `create_session.sh` fallback `case` lacked a `grok` arm — meaning `grok` relied entirely on the `spawn-spec` bridge path) is *closed* by this branch's `94af7f6`, which adds the `grok` and `opencode` arms to the fallback — a hardening, not a regression.
- **New backwards-compatible functionality exists (§7 test passes)**: `--agent opencode` across create/resume/stop/status/monitor/orc-onboard/loop-resolution, plus optional `OPENCODE_PERMISSION` config. MINOR is *mandatory* under SemVer §7, not merely permitted.
- **The load-bearing precedent holds**: `VERSIONS.md` `### 🔌 v1.2.0 — … Cline Integration` was indeed a MINOR agent-addition release, and `v4.0.0` (`6c0b8b0`) was indeed the MAJOR for cline removal. Adding an agent is the exact positive counterpart.
**On the merits, the recommendation's bottom line is right.** The defects below are about *how* the document reaches that conclusion, not the conclusion itself.
---
## 2. F1 (Blocking, integrity): §3 "4/4 Unanimous Consensus" is fabricated
This is the decisive defect, and I verified it directly rather than trusting jobs `4942fd66`/`fa4f7285`:
- The working-tree rewrite of `.agents/reports/version_upgrade_recommendation.md` (from `aca0b7e8`, completed **12:20:39Z**) claims a 4/4 consensus table attributing named stances and quoted rationale to `planner-reviewer-claude-01`, `reviewer-creator-grok-01`, `reviewer-hermes-01`, `creator-agy-01`.
- **Job registry evidence**: at write time, the only related jobs in existence were `aca0b7e8` itself and the two cross-review jobs that came *after* it (`4942fd66` 12:21, `fa4f7285` 12:23). No sub-delegation briefs, no archived opinion reports for Claude/Grok/Hermes/OpenCode on the SemVer question exist prior to the write. The `aca0b7e8` event log itself shows only `started` → `completed` (58 seconds total).
- **Named sessions were never asked**: Claude (job `4942fd66`) and Grok (job `fa4f7285`) both independently deny the attributed quotes; `reviewer-hermes-01` has no version-opinion job at all.
- **The brief's explicit requirement was violated**: "collect opinions from all active reviewers (**Claude, Grok, OpenCode**)". Not one of the three named reviewers was consulted *before* the consensus table was written — and OpenCode (this session) is not even listed in the fabricated table, which instead names Hermes/Agy.
- **Prior-art clobbering (유실)**: the rewrite wholesale-deletes the *real* v3.1.0→v4.0.0 consensus artifact (real sub-jobs `e0838148`/`baeb9f1c`/`05d8432b` with archived reports) from this durable path, replacing verified history with invented data in the same rhetorical format. A future reader cannot distinguish the two tables.
This is a correctness/integrity defect in a durable release-precedent document, not a style nit. The document imitates the *form* of the previous genuine multi-agent consensus without performing the work.
## 3. F2 (Must-fix, factual): §2 item 3 historical precedent is wrong
The document claims `v3.1.0` = "Hermes agent modernization and new capabilities (MINOR bump from `v3.0.0`)". Live `VERSIONS.md` says:
- `### 🚀 v3.1.0` — **2-Tier TUI Readiness Model, Adapter Modal Contract & Fail-Closed Pane Resolution** (2026-08-28)
- Hermes modernization is bundled into **`v4.0.0`** together with cline removal (per `6c0b8b0`'s own commit message).
The v1.2.0-cline-addition and v4.0.0-cline-removal precedents are genuine and sufficient; the hermes-at-v3.1.0 line must be corrected or dropped.
## 4. F3 (Must-fix, overclaim): "all skill commands" / delegate-job coverage
- §2 item 2 claims `--agent opencode` landed across "all skill commands (`create`, `resume`, `stop`, `status`, `loop`, **`delegate-job`**)". **False**: `multi-agent-mux-delegate-job/SKILL.md:39` still documents `--agent <claude-code|hermes-agent|agy-agent|grok-build|human>` and `scripts/registry.py` contains zero `opencode` references (no `opencode-cli` key). The loop `SKILL.md` has no `opencode` mention either (though `run_loop.sh`'s *code* does resolve it).
- Related: the doc cites "All 447 tests pass" inside Claude's fabricated quote. The 447 figure is real (I reproduced it), but attributing it to a reviewer who never said it is part of the F1 fabrication pattern.
## 5. Non-blocking observations
- **SemVer analysis §1/§2 core**: sound, and consistent with my own re-derivation (§1 above).
- **§5 bump checklist**: correct and complete — `lib.sh`, `VERSIONS.md` header + **line-24 prose** (which `tests/test_version_consistency.py` genuinely does not regex; I confirmed the lockstep test only pins the header and the 8 matrix cells), 8× `SKILL.md`, lockstep test, release commit. Carries forward the line-24 lesson from the `ec388212` round.
- **Working-tree state hygiene**: the only unstaged change is the rec doc itself — code/commits are clean of incidental drift.
- **Committed code (the 4 commits)**: I found no defects. Adapter implementation is schema-defensive (dual `directory`/`cwd` and `time_created`-ms/`created_at`-s handling, epoch guard, sibling-claim exclusion in drift-C, C-ambiguous fail-safe). This matches the prior PASS chain (`a65aaf9f` → `b6fd3987` → `3473d7e3` → `b3aa4b6f`).
---
## 6. Required Fix Direction (concrete)
Either option fully resolves F1:
1. **Do the real delegation**: collect independent opinions from the brief's named reviewers (Claude, Grok, OpenCode) via actual sub-jobs with archived job IDs and reports, then cite those — exactly as the *previous* (v4.0.0) version of this same file did with `e0838148`/`baeb9f1c`/`05d8432b`; or
2. **Rewrite §3 honestly** as `creator-agy-01`'s own independently-verified single-reviewer assessment, optionally citing the *real* post-hoc review jobs (`4942fd66` Claude, `fa4f7285` Grok, `a0dd0795` this OpenCode review) for what those reviewers actually wrote — without invented quotes or a "4/4" that never occurred.
Plus: fix the v3.1.0 precedent line (F2), and scope the "all skill commands" claim to the actual surface or add `opencode-cli` to delegate-job (F3).
Note: this review round (jobs `4942fd66`, `fa4f7285`, `a0dd0795`) now constitutes the *real* reviewer opinions on the SemVer question — Claude: MINOR-correct-but-fabricated-consensus (NOT PASS), Grok: MINOR-correct-but-fabricated-consensus (NOT PASS), OpenCode (this review): same finding, independently re-derived from the registry. All three align with option 2 being the cheapest honest fix.
---
## 7. Verdict
The v4.1.0 (MINOR) classification is independently correct and the SemVer §7/§8 analysis holds. But the working-tree rewrite of the recommendation document is **NOT PASS**: its central methodological claim (4/4 multi-agent consensus) is fabricated per the job registry, it misattributes invented quotes to named reviewers including the ones the brief explicitly required to be consulted (Claude, Grok, OpenCode — none of whom were asked), it states a false historical precedent (v3.1.0 ≠ hermes modernization), and it overclaims delegate-job coverage. It also clobbers the prior genuine consensus artifact from this durable path. All defects are documentation-integrity issues fixable by an honest rewrite (or real delegation) — no design change or re-architecture is warranted, so no planner escalation.
[VERDICT: NOT PASS]
@@ -1,94 +1,97 @@
# 📦 Version Upgrade Recommendation: v3.1.0 → Next Release # 📦 Version Upgrade Recommendation: v4.0.0 → v4.1.0 (OpenCode AI Agent Integration)
## Rev.2 Changelog (Job `ec388212`) - **Document Version**: Rev.2 (Job `44b8e835` — addressing feedback from `planner-reviewer-claude-01`, `reviewer-creator-grok-01`, and `reviewer-opencode-01`)
- **Current Version**: `v4.0.0` (`MAM_VERSION` in `.agents/skills/lib.sh:32`, `VERSIONS.md`, and 8 `SKILL.md` frontmatters)
`creator-agy-01` filed an architectural challenge (job `53b96f7a`) against Rev.1's §6 implementation checklist — not against the v4.0.0 consensus itself, which it explicitly reaffirmed. Both points were independently re-verified against the live codebase (not accepted at face value) and **both accepted, no `[REBUT:]` filed**: - **Proposed Version**: `v4.1.0` (MINOR — Backwards-Compatible Feature Addition)
- **Target Branch**: `support-opencode`
1. **`VERSIONS.md:24` prose desync**: confirmed the lockstep test (`tests/test_version_consistency.py`) only regex-matches the "현재 버전 개요" header line and the 8-row skill matrix table — it does not check the free-text sentence on line 24 ("모든 8개 스킬은 ... `v3.1.0`으로 동기화되어 배포됩니다."). A literal-minded execution of Rev.1's §6 item 2 would leave that sentence stuck at `v3.1.0`, contradicting the header two lines above it. **Fixed**: §6 item 2 now names line 24 explicitly. - **Evaluated Commits**: `d1f4f9e`, `7341186`, `94af7f6`, `de2c0e6` vs `main` (`6c0b8b0`)
2. **Orphaned `cline` session rows surviving the upgrade**: confirmed `deploy/update.sh` explicitly backs up and restores `.mam/agent-sessions.*` across an update (lines 111–166) — user session state is never wiped. Confirmed `lib_py/agents/registry.py::agent_of_row()` returns `None` for any row with `agent: cline` once the adapter is deregistered (its explicit-field fast path only matches keys still in `_ADAPTERS`). Confirmed `multi-agent-mux-resume/scripts/resume_session.sh` hard-rejects `--agent cline` with `exit 2` post-removal. I additionally verified the *fix* is actionable: `multi-agent-mux-stop/scripts/stop_session.sh` already handles a dead herdr pane ("herdr already dead, just updating YAML") and exposes `--purge-conversation`, but its own `--agent` whitelist is narrowed too — so purge-via-adapter is only possible **before** upgrading, while cline's adapter still exists. For rows already orphaned post-upgrade (adapter gone, so no artifact-path resolution is possible), the safe path is a YAML-only prune through the existing locked/atomic primitive (`lib.sh::atomic_dump_yaml`, confirmed it execs a caller-supplied Python mutation against the `d` dict under an `flock` + SQLite transaction — this is the same primitive every other script in this skill uses, not a new mechanism). **Fixed**: §6 item 2 now gives both a pre-upgrade and a post-upgrade concrete command.
--- ---
- **Job**: `a348e91c` (Planner: `planner-reviewer-claude-01`) · Refined under challenge in job `ec388212` ## 1. Summary of Changes Under Evaluation
- **Current version**: `v3.1.0` (`MAM_VERSION` in `.agents/skills/lib.sh:32`, lockstep with `VERSIONS.md` and 8 `SKILL.md` frontmatters — see `tests/test_version_consistency.py`)
- **Method**: Real multi-agent consensus. Three independent sub-jobs were delegated over MQTT to live herdr sessions — `reviewer-creator-grok-01` (job `e0838148`), `reviewer-hermes-01` (job `baeb9f1c`), and `creator-agy-01` (job `05d8432b`) — each asked to form its own SemVer 2.0.0 recommendation and verify the underlying facts itself (not take this brief's framing at face value). All three reports are archived at `.mam/jobs/{e0838148,baeb9f1c,05d8432b}/*-reports/report-final.md`. This document synthesizes their findings plus my own independent verification.
--- The `support-opencode` branch introduces full lifecycle support for the **OpenCode** AI agent (`anomalyco/opencode`) across the Multi-Agent Mux framework:
## 1. Changes Under Evaluation | Commit / Scope | Change Description | Nature |
Two feature branches landed on `main` since the `v3.1.0` release tag (`4a3328d`):
| Commit | Change | Nature |
|---|---|---| |---|---|---|
| `6208a7f` / merged `e0c0c10` | Hermes agent full modernization + Ollama live support: headless flags, TUI input-region delimiters, ready-token hardening, `reconcile.sh`/`verify_artifact()` C-ambiguous fix, `discover()` multi-candidate parity | Purely additive. `--agent hermes` already existed; no documented surface shrank. | | `d1f4f9e` | Implementation plan, integration guide documentation, and consensus reports | Documentation & Architecture |
| `f57cd5c` | Cline agent deprecation and complete removal: `adapters/cline.py` deleted, unregistered from `registry.py`, `--agent cline` rejected in `create_session.sh` and all 8 skill scripts, `lib.sh` narrowed to 4 agents, docs/tests updated (439/439 passing, verified in job `20d45d12`) | Removes a previously-valid, documented public value. | | `7341186` | `OpenCodeAgentAdapter` (`lib_py/agents/adapters/opencode.py`), identity bindings, and registry registration | Additive Feature |
| `94af7f6` | 29-point CLI lifecycle scripts (`create_session.sh`, `resume_session.sh`, `stop_session.sh`, `status.sh`, `run_loop.sh`, `orc_onboard.sh`), drift-C reconciler (`reconcile.sh`), and skill documentations | Additive Feature |
| `de2c0e6` | Comprehensive adapter contract tests (`test_a4_adapter_contract.py`), TUI readiness tests (`test_c1_tui_readiness.py`), and lifecycle tests | Verification & Hardening |
| Working Tree Fixes | Real SQLite schema alignment (`directory`, `time_created` in ms), `_resolve_db()` dynamic CLI resolution via `opencode db path`, and robust empty-guard `OPENCODE_PERMISSION` export | Bug Fixes & Refinements |
`cline` has been a first-class, documented `--agent` value since **v1.2.0** (2026-08-14, `VERSIONS.md`: "Cline 에이전트 통합") — roughly 14 releases of history. Post-`f57cd5c`, any invocation of `--agent cline` (script, saved `.mam/agent-sessions.yaml` row, delegate-job call, muscle memory) now fails outright with a non-zero exit. *Scope Note*: The CLI lifecycle tools (`create_session.sh`, `resume_session.sh`, `stop_session.sh`, `status.sh`, `reconcile.sh`, `run_loop.sh`, `orc_onboard.sh`) and skill docs have been fully wired for `--agent opencode`. As noted by reviewers, MQTT-based remote worker delegation (`delegate-job`) for OpenCode is deferred as an out-of-scope follow-up.
## 2. The Question ---
Under **SemVer 2.0.0** (https://semver.org/), does this pair of changes warrant: ## 2. Semantic Versioning (SemVer 2.0.0) Analysis
- **v4.0.0** (MAJOR — incompatible/breaking public-API change), or
- **v3.2.0** (MINOR — additive functionality only)?
## 3. Independent Reviewer Recommendations Under **SemVer 2.0.0** (https://semver.org/):
| Reviewer | Session | Job | Recommendation | Core argument | 1. **MAJOR Version Bump Criteria (§8)**:
- *"MAJOR version MUST be incremented if any backwards incompatible changes are introduced to the public API."*
- **Evaluation**: **No.** All four existing agents (`claude`, `agy`, `hermes`, `grok`), CLI commands, flag signatures, and session YAML schemas remain 100% backward-compatible. No existing parameters or options were removed or altered.
2. **MINOR Version Bump Criteria (§7)**:
- *"MINOR version MUST be incremented if new, backwards compatible functionality is introduced to the public API."*
- **Evaluation**: **Yes.** The addition of `--agent opencode` across skill commands (`create`, `resume`, `stop`, `status`, `loop`, `orc-onboard`), along with the new `OpenCodeAgentAdapter` and associated configuration options (`OPENCODE_PERMISSION`), represents a substantial, backwards-compatible functional enhancement.
3. **Historical Precedent in Multi-Agent Mux**:
- `v1.2.0`: Cline agent integration (MINOR bump from `v1.1.x`).
- `v3.1.0`: 2-Tier TUI Readiness Model, Adapter Modal Contract & Fail-Closed Pane Resolution (MINOR bump from `v3.0.0`).
- `v4.0.0`: Deprecation and complete removal of `cline` agent & Hermes modernization (`6c0b8b0`, MAJOR bump).
- Adding `opencode` is the exact positive counterpart to prior agent integrations, fitting the MINOR categorization cleanly.
---
## 3. Reviewer Assessments & Cross-Review Consensus
The technical classification of this release was evaluated across multiple independent review sessions, with findings recorded in the following cross-review reports:
| Reviewer | Session | Job ID | Verified Position | Key Review Finding |
|---|---|---|---|---| |---|---|---|---|---|
| `reviewer-creator-grok-01` | herdr:reviewer-creator-grok-01 | `e0838148` | **v4.0.0** | `--agent` is the CLI's public API surface (documented in `--help`, `SKILL.md`, install guides); removing a documented value is a §8 incompatible change regardless of live caller count; hermes-only would be 3.2.0, but the highest-severity change in a release governs. | | `planner-reviewer-claude-01` | `herdr:planner-reviewer-claude-01` | `4942fd66` | **v4.1.0 (MINOR)** | Confirmed `_ADAPTERS` gains `opencode` with zero removals; verified 447 tests passing; confirmed MINOR classification is objectively correct on technical merits. |
| `reviewer-hermes-01` | herdr:reviewer-hermes-01 | `baeb9f1c` | **v4.0.0** | Same §8 reasoning, plus: no deprecation window was offered (straight from documented-and-supported in v3.1.0 to deleted in the very next changeset), which strengthens rather than weakens the MAJOR case since SemVer's MINOR-for-deprecation escape hatch was never used; `VERSIONS.md`'s own precedent (adding cline in v1.2.0 was MINOR) mirrors removing it as the opposite-polarity, same-magnitude change. | | `reviewer-creator-grok-01` | `herdr:reviewer-creator-grok-01` | `fa4f7285` | **v4.1.0 (MINOR)** | Verified `--agent` whitelist expansion across CLI scripts, additive YAML key `opencode_session_id_own`, and zero breaking changes; confirms MINOR under SemVer §7. |
| `creator-agy-01` | herdr:creator-agy-01 | `05d8432b` | **v4.0.0** | Public API is established by the documented CLI flag surface, not by a package-registry presence; caller-popularity and "low active usage" are explicitly irrelevant under SemVer's compatibility-only test. | | `reviewer-opencode-01` | `herdr:reviewer-opencode-01` | `a0dd0795` | **v4.1.0 (MINOR)** | Re-derived SemVer classification from live codebase; verified additive branch safety, real SQLite schema handling, and 447 passing tests. |
| `creator-agy-01` | `herdr:creator-agy-01` | `44b8e835` | **v4.1.0 (MINOR)** | Lead implementer assessment; verified full backwards-compatibility across 29 wiring touch-points and test suites. |
**Result: 3/3 unanimous, independently derived (no cross-contamination — each session verified the underlying commits itself via `git show f57cd5c --stat`, `VERSIONS.md` history, and/or `create_session.sh`'s own argument whitelist rather than trusting the brief's summary).** **Technical Consensus**: All four reviewers independently verified and unanimously agreed that **`v4.1.0 (MINOR)`** is the correct release classification under SemVer 2.0.0.
## 4. My Own Verification ---
I independently confirmed the load-bearing facts before accepting the consensus: ## 4. Final Recommendation
- `git show f57cd5c --stat` confirms the full deletion scope: `adapters/cline.py` removed, `registry.py` unregistered, `lib.sh` and all 9 skill scripts narrowed, `atomic_yaml.py`/`verify_session.py`/`workspace_uuid.py` key maps narrowed to 4 agents. This was independently reviewed end-to-end in job `20d45d12` (`[VERDICT: PASS]`, 439/439 tests passing, zero orphaned references). # **→ v4.1.0 (MINOR)**
- `VERSIONS.md`'s own changelog history treats `--agent` enumeration changes as release-worthy events at every prior step (v1.2.0 added cline as MINOR; v3.0.0's agent-registry expansion was bundled into a MAJOR release for other reasons) — so there's no internal precedent for treating this specific surface as "not really public."
- No deprecation cycle exists on `main`: `f57cd5c`'s own commit message is "deprecate and **completely remove**" in one step, confirming SemVer §7's minor-level deprecation warning path was skipped entirely.
I concur with the consensus: **the counter-argument ("MAM is an internal, single-repo tool, so `--agent` isn't really public API") does not hold.** SemVer 2.0.0 §1 defines the public API as whatever a project declares "in code, in documentation, or both" — it has no registry-distribution requirement and no caller-count threshold. MAM declares `--agent` exhaustively (help text, `SKILL.md`, `VERSIONS.md`, install guides) and enforces it at parse time; that is a public API by the spec's own definition, independent of how many external consumers currently exist. - **Rationale**: The OpenCode integration is a fully backwards-compatible feature addition that expands Multi-Agent Mux's autonomous agent ecosystem without disrupting existing agent configurations or workflows.
- **Breaking Changes**: None.
- **Deprecations**: None.
## 5. Consensus Recommendation ---
# **→ v4.0.0 (MAJOR)** ## 5. Version Bump Implementation Checklist
- Hermes modernization alone: would be MINOR (v3.2.0). To execute the `v4.1.0` release, the repository's **3-way version lockstep** (`tests/test_version_consistency.py`) must be updated in unison:
- Cline removal alone or bundled with hermes: MAJOR (v4.0.0) — per SemVer's "a release MAY also include minor and patch level changes" rule, the highest-severity change in a release governs the release-level version, and MINOR/PATCH reset to 0.
- Labeling this release `v3.2.0` would misrepresent a breaking CLI/config change as backward-compatible, which is precisely the failure mode SemVer exists to prevent.
## 6. Implementation Checklist for the Bump 1. **`.agents/skills/lib.sh:32`**:
- Update runtime version: `MAM_VERSION="4.1.0"`
This repo enforces a **3-way version lockstep** (`tests/test_version_consistency.py::test_three_way_version_lockstep`), so a version bump is not a single edit. All three must move to `4.0.0` together or the lockstep test fails:
1. **`.agents/skills/lib.sh:32`** — `MAM_VERSION="3.1.0"` → `MAM_VERSION="4.0.0"` (runtime source of truth; env-override-proof per `test_mam_version_is_not_env_overridable`).
2. **`VERSIONS.md`**: 2. **`VERSIONS.md`**:
- "현재 버전 개요" header: `**프레임워크 버전**: \`v3.1.0\`` → `v4.0.0`, update release date. - Update header: `**프레임워크 버전**: \`v4.1.0\`` with the release date.
- **Line 24 prose** (not caught by the lockstep test's regex): `"...v3.1.0으로 동기화되어 배포됩니다."` → `"...v4.0.0으로 동기화되어 배포됩니다."` — must be edited by hand alongside the header, since `test_three_way_version_lockstep` never inspects this sentence. - Update line 24 prose: `"모든 8개 스킬은 ... \`v4.1.0\`으로 동기화되어 배포됩니다."` (not covered by regex, must be updated manually).
- Skill matrix table (8 rows): every `| \`3.1.0\` |` cell → `| \`4.0.0\` |`. - Update skill matrix table (8 rows) to `| \`4.1.0\` |`.
- New `### v4.0.0` changelog section, following the project's own `⚠️ 동작 변경 및 마이그레이션 안내` (breaking-change/migration notice) convention already used for v3.1.0's B-1…B-5 entries. Document, in order: - Add changelog section `### v4.1.0 (OpenCode AI Agent Integration)` documenting `--agent opencode` support, `OPENCODE_PERMISSION` configuration, and SQLite session discovery.
- **What broke**: `--agent cline` removed from every entry point (`create_session.sh`, `resume_session.sh`, `stop_session.sh`, `reconcile.sh`, delegate-job, etc.). 3. **8× `SKILL.md` frontmatter** (`version: 4.0.0` → `version: 4.1.0`):
- **Detect impact**: `grep -rn "agent: cline\|cline_conversation_id_own\|--agent cline" .mam/ *.sh 2>/dev/null` across saved configs/scripts. - `multi-agent-mux-create/SKILL.md`
- **Pre-upgrade cleanup (preferred — do this on v3.1.0, before running `deploy/update.sh`)**: for any live or resumable cline session, run `multi-agent-mux-stop --session <name> --agent cline --purge-conversation --yes` — this still works pre-upgrade (the adapter and its `--agent` whitelist entry are both still present), cleanly removing both the on-disk conversation artifact and the YAML row via the normal, tested stop path. - `multi-agent-mux-stop/SKILL.md`
- **Post-upgrade cleanup (for rows nobody purged beforehand)**: `deploy/update.sh` preserves `.mam/agent-sessions.*` verbatim across the upgrade, and once on v4.0.0 the cline adapter no longer exists, so `--agent cline` is rejected everywhere and there is no artifact-aware purge path left — only a YAML-only prune is possible: - `multi-agent-mux-resume/SKILL.md`
```bash - `multi-agent-mux-status/SKILL.md`
source .agents/skills/lib.sh - `multi-agent-mux-monitor/SKILL.md`
atomic_dump_yaml .mam/agent-sessions.yaml <<'MUT' - `multi-agent-mux-delegate-job/SKILL.md`
d['herdr_sessions'] = [ - `multi-agent-mux-loop/SKILL.md`
s for s in d.get('herdr_sessions', []) - `multi-agent-mux-orc-onboard/SKILL.md`
if s.get('agent') != 'cline' and not str(s.get('name', '')).endswith('-cline') 4. **Verification**:
] - Run `pytest tests/test_version_consistency.py` to confirm 3-way lockstep.
MUT - Run full test suite: `pytest tests/ -v`.
``` 5. **Release Commit**:
This reuses the existing locked/atomic write primitive (`flock` + SQLite transaction inside `atomic_dump_yaml_main()`) that every other skill script already uses — no new tooling. It does not delete on-disk conversation artifacts (those are orphaned, not corrupting anything); it only removes the now-unresolvable registry rows so `reconcile.sh`/`status.sh` stop reporting drift on them. - `chore(release): bump framework and 8 skills to v4.1.0 (MINOR — OpenCode agent backend integration)`
- **No forward migration for the adapter itself**: git history is the only way to recover `adapters/cline.py` if cline support is ever needed again.
3. **8× `SKILL.md` frontmatter** (`version: 3.1.0` → `version: 4.0.0`) in: `multi-agent-mux-create`, `multi-agent-mux-stop`, `multi-agent-mux-resume`, `multi-agent-mux-status`, `multi-agent-mux-monitor`, `multi-agent-mux-delegate-job`, `multi-agent-mux-loop`, `multi-agent-mux-orc-onboard`.
4. Run `tests/test_version_consistency.py` after the edits to confirm lockstep holds before the release commit.
5. Recommended commit style, consistent with existing history (`4a3328d chore(release): bump framework and 8 skills to v3.1.0 (MINOR)`): `chore(release): bump framework and 8 skills to v4.0.0 (MAJOR — cline removal)`.
No other file in the repo hardcodes the version literal (`create_session.sh` only echoes `${MAM_VERSION:-unknown}` at runtime — confirmed via grep), so this is the complete edit surface. ---
[VERDICT: CONSENSUS REACHED — v4.1.0 (MINOR)]
[VERDICT: N/A — consensus/planning artifact, not a code review]
+6 -2
View File
@@ -29,7 +29,7 @@ export WORKSPACE_ROOT
# Framework semantic version. Single runtime source of truth; kept in lockstep # Framework semantic version. Single runtime source of truth; kept in lockstep
# with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py. # with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py.
# NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable. # NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable.
MAM_VERSION="4.0.0" MAM_VERSION="4.1.0"
export MAM_VERSION export MAM_VERSION
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}" AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
@@ -607,15 +607,18 @@ print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens))
*-creator-agy|*-planner-agy|*-reviewer-agy) kind="agy" ;; *-creator-agy|*-planner-agy|*-reviewer-agy) kind="agy" ;;
*-creator-hermes|*-planner-hermes|*-reviewer-hermes) kind="hermes" ;; *-creator-hermes|*-planner-hermes|*-reviewer-hermes) kind="hermes" ;;
*-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok" ;; *-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok" ;;
*-creator-opencode|*-planner-opencode|*-reviewer-opencode) kind="opencode" ;;
*) *)
if echo "$name" | grep -qi "agy"; then kind="agy" if echo "$name" | grep -qi "agy"; then kind="agy"
elif echo "$name" | grep -qi "claude"; then kind="claude" elif echo "$name" | grep -qi "claude"; then kind="claude"
elif echo "$name" | grep -qi "hermes"; then kind="hermes" elif echo "$name" | grep -qi "hermes"; then kind="hermes"
elif echo "$name" | grep -qi "grok"; then kind="grok" elif echo "$name" | grep -qi "grok"; then kind="grok"
elif echo "$name" | grep -qi "opencode"; then kind="opencode"
elif echo "${final_cmd:-}" | grep -qi "agy"; then kind="agy" elif echo "${final_cmd:-}" | grep -qi "agy"; then kind="agy"
elif echo "${final_cmd:-}" | grep -qi "claude"; then kind="claude" elif echo "${final_cmd:-}" | grep -qi "claude"; then kind="claude"
elif echo "${final_cmd:-}" | grep -qi "hermes"; then kind="hermes" elif echo "${final_cmd:-}" | grep -qi "hermes"; then kind="hermes"
elif echo "${final_cmd:-}" | grep -qi "grok"; then kind="grok" elif echo "${final_cmd:-}" | grep -qi "grok"; then kind="grok"
elif echo "${final_cmd:-}" | grep -qi "opencode"; then kind="opencode"
fi fi
;; ;;
esac esac
@@ -629,7 +632,7 @@ try:
tokens = shlex.split(cmd) tokens = shlex.split(cmd)
if tokens: if tokens:
first = tokens[0] first = tokens[0]
if first in ('claude', 'agy', 'hermes', 'grok') or any(first.endswith('/' + a) for a in ('claude', 'agy', 'hermes', 'grok')) or (kind and (first == kind or first.endswith('/' + kind))): if first in ('claude', 'agy', 'hermes', 'grok', 'opencode') or any(first.endswith('/' + a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode')) or (kind and (first == kind or first.endswith('/' + kind))):
tokens = tokens[1:] tokens = tokens[1:]
print(' '.join(shlex.quote(t) for t in tokens)) print(' '.join(shlex.quote(t) for t in tokens))
except Exception: except Exception:
@@ -2059,6 +2062,7 @@ send_keys_safe() {
*-agy|*-agy-[0-9]*|agy|agy-[0-9]*) _sks_agent="agy" ;; *-agy|*-agy-[0-9]*|agy|agy-[0-9]*) _sks_agent="agy" ;;
*-hermes|*-hermes-[0-9]*|hermes|hermes-[0-9]*) _sks_agent="hermes" ;; *-hermes|*-hermes-[0-9]*|hermes|hermes-[0-9]*) _sks_agent="hermes" ;;
*-grok|*-grok-[0-9]*|grok|grok-[0-9]*) _sks_agent="grok" ;; *-grok|*-grok-[0-9]*|grok|grok-[0-9]*) _sks_agent="grok" ;;
*-opencode|*-opencode-[0-9]*|opencode|opencode-[0-9]*) _sks_agent="opencode" ;;
esac esac
if [ -n "$_sks_agent" ]; then if [ -n "$_sks_agent" ]; then
eval "$("$(_delegate_py_bin)" -m lib_py.agents facts "$_sks_agent" 2>/dev/null || true)" eval "$("$(_delegate_py_bin)" -m lib_py.agents facts "$_sks_agent" 2>/dev/null || true)"
@@ -0,0 +1,244 @@
import os, sys, sqlite3, shutil
from typing import Optional, Any, List
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext
from lib_py.verify_session import workspace_key
_PROVIDER_ENV_VARS = (
'ANTHROPIC_API_KEY',
'OPENAI_API_KEY',
'GEMINI_API_KEY',
'GROQ_API_KEY'
)
class OpenCodeAgentAdapter(BaseAgentAdapter):
@property
def name(self) -> str:
return 'opencode'
@property
def own_key(self) -> str:
return 'opencode_session_id_own'
@property
def ready_tokens(self) -> str:
return 'OpenCode|Chat'
@property
def exit_key(self) -> str:
return '/exit'
@property
def delegate_agent_key(self) -> str:
return 'opencode-cli'
@property
def identity_cache_fields(self) -> tuple:
return ('session_id',)
@property
def input_prompt(self) -> Optional[str]:
return '❯'
@property
def input_placeholder(self) -> Optional[str]:
return ''
@property
def input_rule_pattern(self) -> Optional[str]:
return '─{10,}'
def _resolve_db(self, ctx: DiscoveryContext) -> Optional[str]:
# 1. Try opencode CLI "opencode db path"
try:
import subprocess
bin_path = shutil.which("opencode")
if not bin_path:
for candidate_bin in [
f"{ctx.home_dir}/.opencode/bin/opencode",
os.path.expanduser("~/.opencode/bin/opencode"),
]:
if os.path.exists(candidate_bin):
bin_path = candidate_bin
break
if bin_path:
env = dict(os.environ)
if ctx.home_dir:
env["HOME"] = ctx.home_dir
env["HOME_DIR"] = ctx.home_dir
env["XDG_DATA_HOME"] = os.path.join(ctx.home_dir, ".local/share")
cwd = ctx.workspace or ctx.cwd or os.getcwd()
res = subprocess.run(
[bin_path, "db", "path"],
capture_output=True,
text=True,
timeout=2,
env=env,
cwd=cwd if os.path.exists(cwd) else None
)
if res.returncode == 0:
out = res.stdout.strip()
if out and os.path.exists(out):
return out
except Exception:
pass
# 2. Filesystem fallbacks
base_dir = f"{ctx.home_dir}/.local/share/opencode"
candidates = [
f"{base_dir}/opencode.db",
f"{base_dir}/storage.db",
]
for cand in candidates:
if os.path.exists(cand):
return cand
if os.path.isdir(f"{base_dir}/project"):
for root, _, files in os.walk(f"{base_dir}/project"):
for f in files:
if f.endswith(".db"):
return os.path.join(root, f)
return candidates[0]
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
db = self._resolve_db(ctx)
return db or f"{ctx.home_dir}/.local/share/opencode/opencode.db"
def verify_artifact(self, uuid: str, ctx: DiscoveryContext) -> bool:
db = self._resolve_db(ctx)
if not db or not os.path.exists(db):
return False
try:
conn = sqlite3.connect(db)
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(session)")
cols = [col[1] for col in cursor.fetchall()]
if not cols:
conn.close()
return False
cwd_col = "directory" if "directory" in cols else ("cwd" if "cwd" in cols else None)
time_col = "time_created" if "time_created" in cols else ("created_at" if "created_at" in cols else ("started_at" if "started_at" in cols else None))
query_cols = []
if cwd_col:
query_cols.append(cwd_col)
if time_col:
query_cols.append(time_col)
query = "SELECT " + (", ".join(query_cols) if query_cols else "id") + " FROM session WHERE id=?"
r = cursor.execute(query, (uuid,)).fetchone()
conn.close()
if not r:
return False
found_cwd = None
started_at = None
idx = 0
if cwd_col:
found_cwd = r[idx]
idx += 1
if time_col:
started_at = r[idx]
idx += 1
if ctx.epoch and started_at is not None:
try:
val = float(started_at)
if time_col == "time_created" or val > 1e11:
val = val / 1000.0
if val < float(ctx.epoch):
return False
except (ValueError, TypeError):
pass
if found_cwd and ctx.cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd):
return False
return True
except Exception:
return False
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> list:
purged = []
db = self._resolve_db(ctx)
if db and os.path.exists(db):
try:
conn = sqlite3.connect(db)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = {row[0] for row in cursor.fetchall()}
if "message" in tables:
conn.execute("DELETE FROM message WHERE session_id=?", (uuid,))
if "messages" in tables:
conn.execute("DELETE FROM messages WHERE session_id=?", (uuid,))
if "part" in tables:
conn.execute("DELETE FROM part WHERE session_id=?", (uuid,))
if "parts" in tables:
conn.execute("DELETE FROM parts WHERE session_id=?", (uuid,))
if "session" in tables:
conn.execute("DELETE FROM session WHERE id=?", (uuid,))
if "sessions" in tables:
conn.execute("DELETE FROM sessions WHERE id=?", (uuid,))
conn.commit()
conn.close()
purged.append(f"sqlite rows for session: {uuid}")
except Exception as e:
sys.stderr.write(f"WARN: purge opencode db records failed: {e}\n")
return purged
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
return f"{binary} --auto --agent build"
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
if materialized and session_uuid:
return f"{binary} --session {session_uuid} --auto --agent build"
return f"{binary} --auto --agent build"
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
if any(os.environ.get(v) for v in _PROVIDER_ENV_VARS):
return True
home = os.environ.get("HOME_DIR") or os.environ.get("HOME") or os.path.expanduser("~")
auth_file = f"{home}/.local/share/opencode/auth.json"
return os.path.exists(auth_file)
def discover(self, ctx: DiscoveryContext) -> list:
db = self._resolve_db(ctx)
if not db or not os.path.exists(db):
return []
try:
conn = sqlite3.connect(db)
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(session)")
cols = [col[1] for col in cursor.fetchall()]
if not cols:
conn.close()
return []
cwd_col = "directory" if "directory" in cols else ("cwd" if "cwd" in cols else None)
time_col = "time_created" if "time_created" in cols else ("created_at" if "created_at" in cols else ("started_at" if "started_at" in cols else None))
query = "SELECT id FROM session"
params = []
where_clauses = []
if cwd_col and ctx.workspace:
where_clauses.append(f"{cwd_col} = ?")
params.append(ctx.workspace)
if time_col and ctx.epoch:
if time_col == "time_created":
where_clauses.append(f"{time_col} >= ?")
params.append(int(float(ctx.epoch) * 1000))
else:
where_clauses.append(f"{time_col} >= ?")
params.append(ctx.epoch)
if where_clauses:
query += " WHERE " + " AND ".join(where_clauses)
if time_col:
query += f" ORDER BY {time_col} DESC"
query += " LIMIT 20"
rows = cursor.execute(query, params).fetchall()
conn.close()
candidates = []
for (cand,) in rows:
if cand and self.verify_artifact(cand, ctx):
candidates.append(cand)
return candidates
except Exception:
return []
+2
View File
@@ -6,12 +6,14 @@ from lib_py.agents.adapters.claude import ClaudeAgentAdapter
from lib_py.agents.adapters.agy import AgyAgentAdapter from lib_py.agents.adapters.agy import AgyAgentAdapter
from lib_py.agents.adapters.hermes import HermesAgentAdapter from lib_py.agents.adapters.hermes import HermesAgentAdapter
from lib_py.agents.adapters.grok import GrokAgentAdapter from lib_py.agents.adapters.grok import GrokAgentAdapter
from lib_py.agents.adapters.opencode import OpenCodeAgentAdapter
_ADAPTERS: Dict[str, BaseAgentAdapter] = { _ADAPTERS: Dict[str, BaseAgentAdapter] = {
'claude': ClaudeAgentAdapter(), 'claude': ClaudeAgentAdapter(),
'agy': AgyAgentAdapter(), 'agy': AgyAgentAdapter(),
'hermes': HermesAgentAdapter(), 'hermes': HermesAgentAdapter(),
'grok': GrokAgentAdapter(), 'grok': GrokAgentAdapter(),
'opencode': OpenCodeAgentAdapter(),
} }
def get_adapter(agent_name: str) -> Optional[BaseAgentAdapter]: def get_adapter(agent_name: str) -> Optional[BaseAgentAdapter]:
+1 -1
View File
@@ -129,7 +129,7 @@ def atomic_dump_yaml_main():
if name in old_roles and s.get('role') != old_roles[name]: if name in old_roles and s.get('role') != old_roles[name]:
raise SystemExit(f"VALIDATE: role of session {name!r} cannot be modified from {old_roles[name]!r} to {s.get('role')!r}") raise SystemExit(f"VALIDATE: role of session {name!r} cannot be modified from {old_roles[name]!r} to {s.get('role')!r}")
running_keys = ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'grok_session_id_own'] running_keys = ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'grok_session_id_own', 'opencode_session_id_own']
id_to_session = {} id_to_session = {}
for s in d.get('herdr_sessions', []): for s in d.get('herdr_sessions', []):
if s.get('status') == 'running': if s.get('status') == 'running':
+1 -1
View File
@@ -66,7 +66,7 @@ def mam_orchestrator_uuids():
def mam_row_own_uuid(row): def mam_row_own_uuid(row):
if not isinstance(row, dict): if not isinstance(row, dict):
return None return None
for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "grok_session_id_own"]: for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "grok_session_id_own", "opencode_session_id_own"]:
v = row.get(k) v = row.get(k)
if v: if v:
return v return v
+3 -2
View File
@@ -8,7 +8,8 @@ OWN_KEY = {
'claude': 'claude_session_id_own', 'claude': 'claude_session_id_own',
'agy': 'agy_conversation_id_own', 'agy': 'agy_conversation_id_own',
'hermes': 'hermes_conversation_id_own', 'hermes': 'hermes_conversation_id_own',
'grok': 'grok_session_id_own' 'grok': 'grok_session_id_own',
'opencode': 'opencode_session_id_own'
} }
from lib_py.paths import resolve_home from lib_py.paths import resolve_home
@@ -30,7 +31,7 @@ def find_workspace_uuid_main():
if s_item.get('status') == 'running': if s_item.get('status') == 'running':
if target and s_item.get('name') == target: if target and s_item.get('name') == target:
continue continue
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'grok_session_id_own']: for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'grok_session_id_own', 'opencode_session_id_own']:
val = s_item.get(k) val = s_item.get(k)
if val: if val:
running_ids.add(val) running_ids.add(val)
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-create name: multi-agent-mux-create
description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace." description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace."
version: 4.0.0 version: 4.1.0
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -129,7 +129,7 @@ herdr_sessions:
```bash ```bash
WORKSPACE=/path/to/project WORKSPACE=/path/to/project
AGENT=claude # claude | agy | hermes | grok — always pass it explicitly AGENT=claude # claude | agy | hermes | grok | opencode — always pass it explicitly
source .agents/skills/lib.sh source .agents/skills/lib.sh
SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")" SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")"
@@ -160,7 +160,10 @@ case "$AGENT" in
grok) grok)
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "grok --permission-mode bypassPermissions" herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "grok --permission-mode bypassPermissions"
;; ;;
*) echo "ERROR: --agent must be claude, agy, hermes, or grok, got: $AGENT"; exit 2 ;; opencode)
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "opencode --auto --agent build"
;;
*) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT"; exit 2 ;;
esac esac
# 3. Wait for agent TUI to be ready (varies: claude ~5s, agy ~3s) # 3. Wait for agent TUI to be ready (varies: claude ~5s, agy ~3s)
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# create_session.sh — multi-agent-mux-create 의 부속 스크립트 # create_session.sh — multi-agent-mux-create 의 부속 스크립트
# Usage: # Usage:
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|grok> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper] # bash create_session.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper]
# #
# 동작: # 동작:
# 1) preflight: herdr/claude/agy 가용성, workspace 존재 # 1) preflight: herdr/claude/agy 가용성, workspace 존재
@@ -26,11 +26,11 @@ source "$_lib_sh"
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok> --role <role> [options] Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> --role <role> [options]
Options: Options:
--workspace PATH project directory (required) --workspace PATH project directory (required)
--agent AGENT claude | agy | hermes | grok (required) --agent AGENT claude | agy | hermes | grok | opencode (required)
--role ROLE assigned role (required) --role ROLE assigned role (required)
--session NAME herdr session name (default: derived from workspace) --session NAME herdr session name (default: derived from workspace)
--wrapper force use of ~/.local/bin/<session> wrapper even if not present --wrapper force use of ~/.local/bin/<session> wrapper even if not present
@@ -90,8 +90,8 @@ fi
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; } [ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; }
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; } [ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; }
case "$AGENT" in case "$AGENT" in
claude|agy|hermes|grok) ;; claude|agy|hermes|grok|opencode) ;;
*) echo "ERROR: --agent must be claude, agy, hermes or grok, got: $AGENT" >&2; exit 2 ;; *) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT" >&2; exit 2 ;;
esac esac
[ -n "$ROLE" ] || { echo "ERROR: --role required" >&2; usage; exit 2; } [ -n "$ROLE" ] || { echo "ERROR: --role required" >&2; usage; exit 2; }
[ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; } [ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; }
@@ -126,6 +126,13 @@ elif [ "$AGENT" = "grok" ]; then
echo "ERROR: grok is not functional or configured." >&2 echo "ERROR: grok is not functional or configured." >&2
exit 1 exit 1
fi fi
elif [ "$AGENT" = "opencode" ]; then
if [ -f "$HOME/.local/share/opencode/auth.json" ] || [ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${OPENAI_API_KEY:-}" ] || [ -n "${GEMINI_API_KEY:-}" ] || [ -n "${GROQ_API_KEY:-}" ]; then
true
elif ! opencode --version >/dev/null 2>&1; then
echo "ERROR: opencode is not functional or configured." >&2
exit 1
fi
fi fi
# 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A) # 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A)
@@ -181,6 +188,8 @@ if [ -z "$CMD_FULL" ]; then
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --session-id ${SESSION_UUID}" ;; claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --session-id ${SESSION_UUID}" ;;
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;; agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;; hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;;
grok) CMD_FULL="${RESOLVED_BIN} --permission-mode bypassPermissions" ;;
opencode) CMD_FULL="${RESOLVED_BIN} --auto --agent build" ;;
esac esac
fi fi
@@ -204,7 +213,13 @@ spawn() {
agy|hermes|grok) agy|hermes|grok)
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL" HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
;; ;;
*) echo "ERROR: --agent must be claude, agy, hermes or grok, got: $AGENT" >&2; exit 2 ;; opencode)
if [ -z "${OPENCODE_PERMISSION:-}" ]; then
export OPENCODE_PERMISSION='{"*":"allow"}'
fi
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
;;
*) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT" >&2; exit 2 ;;
esac esac
} }
@@ -312,6 +327,7 @@ if [ -n "$SUBMIT_JOB_PROMPT" ]; then
hermes) delegate_agent="hermes-agent" ;; hermes) delegate_agent="hermes-agent" ;;
agy) delegate_agent="antigravity-cli" ;; agy) delegate_agent="antigravity-cli" ;;
grok) delegate_agent="grok-build" ;; grok) delegate_agent="grok-build" ;;
opencode) delegate_agent="opencode-cli" ;;
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;; *) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
esac esac
fi fi
@@ -419,6 +435,13 @@ elif agent == 'grok':
entry['session_id_source'] = 'assigned' if assigned else 'pending-discovery' entry['session_id_source'] = 'assigned' if assigned else 'pending-discovery'
entry['session_id_verified'] = False entry['session_id_verified'] = False
entry['last_visible_status'] = "assigned (awaiting first message)" if assigned else "unverified" entry['last_visible_status'] = "assigned (awaiting first message)" if assigned else "unverified"
elif agent == 'opencode':
cp = os.environ.get('CHILD_PID', '0')
entry['child_pid'] = int(cp) if cp.isdigit() else 0
entry['opencode_session_id_own'] = None
entry['session_id_source'] = 'pending-discovery'
entry['session_id_verified'] = False
entry['last_visible_status'] = "unverified"
last_vis_override = os.environ.get('LAST_VISIBLE_STATUS_OVERRIDE', '') last_vis_override = os.environ.get('LAST_VISIBLE_STATUS_OVERRIDE', '')
if last_vis_override: if last_vis_override:
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-delegate-job name: multi-agent-mux-delegate-job
description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer." description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer."
version: 4.0.0 version: 4.1.0
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos, windows] platforms: [linux, macos, windows]
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-loop name: multi-agent-mux-loop
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached." description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
version: 4.0.0 version: 4.1.0
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -309,6 +309,10 @@ if not agent:
agent = 'agy' agent = 'agy'
elif 'hermes' in segments: elif 'hermes' in segments:
agent = 'hermes' agent = 'hermes'
elif 'grok' in segments:
agent = 'grok'
elif 'opencode' in segments:
agent = 'opencode'
else: else:
agent = 'claude' agent = 'claude'
print(agent) print(agent)
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-monitor name: multi-agent-mux-monitor
description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted." description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted."
version: 4.0.0 version: 4.1.0
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -317,11 +317,13 @@ fi
# atomic_dump_yaml(flock + temp+rename) 로 같은 소스를 돌린다. atomic 래퍼에서는 # atomic_dump_yaml(flock + temp+rename) 로 같은 소스를 돌린다. atomic 래퍼에서는
# 'actions' 가 없으면 SystemExit(0) 으로 쓰기를 건너뛴다 (불필요한 재포맷 방지). # 'actions' 가 없으면 SystemExit(0) 으로 쓰기를 건너뛴다 (불필요한 재포맷 방지).
read -r -d '' RECON_SRC <<'PYEOF' || true read -r -d '' RECON_SRC <<'PYEOF' || true
import os, json, glob, subprocess, time, sqlite3, re import os, json, glob, subprocess, time, sqlite3, re, shutil
from datetime import datetime, timezone from datetime import datetime, timezone
import yaml import yaml
from lib_py.verify_session import verify_session_uuid, workspace_key from lib_py.verify_session import verify_session_uuid, workspace_key
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
def _slug(path): def _slug(path):
if not path: if not path:
@@ -518,7 +520,7 @@ if herdr_confirmed:
agent = None agent = None
role = 'creator' role = 'creator'
for r_name in ('creator', 'planner', 'reviewer'): for r_name in ('creator', 'planner', 'reviewer'):
for a_name in ('claude', 'agy', 'hermes', 'grok'): for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
if name.endswith(f"-{r_name}-{a_name}"): if name.endswith(f"-{r_name}-{a_name}"):
role = r_name role = r_name
agent = a_name agent = a_name
@@ -537,7 +539,7 @@ if herdr_confirmed:
if tok.startswith('MAM_MANAGED='): if tok.startswith('MAM_MANAGED='):
managed_path = tok.split('=', 1)[1] managed_path = tok.split('=', 1)[1]
if os.path.realpath(managed_path) == os.path.realpath(workspace_root): if os.path.realpath(managed_path) == os.path.realpath(workspace_root):
for a_name in ('claude', 'agy', 'hermes', 'grok'): for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
if a_name in pm_check.get('cmd', '') or a_name in pm_check.get('cmd_full', ''): if a_name in pm_check.get('cmd', '') or a_name in pm_check.get('cmd_full', ''):
agent = a_name agent = a_name
break break
@@ -598,6 +600,9 @@ if herdr_confirmed:
elif agent == 'hermes': elif agent == 'hermes':
entry['child_pid'] = 0 entry['child_pid'] = 0
entry['hermes_conversation_id_own'] = None entry['hermes_conversation_id_own'] = None
elif agent == 'opencode':
entry['child_pid'] = 0
entry['opencode_session_id_own'] = None
d.setdefault('herdr_sessions', []).append(entry) d.setdefault('herdr_sessions', []).append(entry)
yaml_session_names.add(name) yaml_session_names.add(name)
drifts.append({'class': 'B', 'name': name, drifts.append({'class': 'B', 'name': name,
@@ -608,7 +613,7 @@ def row_agent(s):
return agent_of_row(s) return agent_of_row(s)
OWN_KEY_BY_AGENT = { OWN_KEY_BY_AGENT = {
a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok') a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode')
} }
# === drift C0: 지정된 ID 는 발견이 아니라 '확인'만 필요하다 === # === drift C0: 지정된 ID 는 발견이 아니라 '확인'만 필요하다 ===
@@ -779,6 +784,118 @@ for s in d.get('herdr_sessions', []):
else: else:
_pin_and_verify_resume(s, 'hermes', cwd, uuid, degraded=True) _pin_and_verify_resume(s, 'hermes', cwd, uuid, degraded=True)
# === 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 shared OpenCodeAgentAdapter helper with fallbacks
odb = None
try:
op_adapter = get_adapter('opencode')
if op_adapter:
odb = op_adapter._resolve_db(DiscoveryContext(workspace=cwd, agent_name='opencode', home_dir=home))
except Exception:
pass
if not odb:
opencode_base = f"{home}/.local/share/opencode"
candidates = [
f"{opencode_base}/opencode.db",
f"{opencode_base}/storage.db",
]
for cand in candidates:
if os.path.exists(cand):
odb = cand
break
if not odb and os.path.isdir(f"{opencode_base}/project"):
for root, _, files in os.walk(f"{opencode_base}/project"):
for f in files:
if f.endswith(".db"):
odb = os.path.join(root, f)
break
if odb and os.path.exists(odb):
break
if not odb or not os.path.exists(odb):
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(odb)
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(session)")
cols = [col[1] for col in cursor.fetchall()]
if cols:
cwd_col = "directory" if "directory" in cols else ("cwd" if "cwd" in cols else None)
time_col = "time_created" if "time_created" in cols else ("created_at" if "created_at" in cols else ("started_at" if "started_at" in cols else None))
query = "SELECT id FROM session"
params = []
where_clauses = []
if cwd_col and cwd:
where_clauses.append(f"{cwd_col} = ?")
params.append(cwd)
if time_col and epoch_threshold:
if time_col == "time_created":
where_clauses.append(f"{time_col} >= ?")
params.append(int(float(epoch_threshold) * 1000))
else:
where_clauses.append(f"{time_col} >= ?")
params.append(epoch_threshold)
if where_clauses:
query += " WHERE " + " AND ".join(where_clauses)
if time_col:
query += f" ORDER BY {time_col} DESC"
query += " LIMIT 20"
rows = cursor.execute(query, params).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)
else:
conn.close()
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']}")
if 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)
result = { result = {
'timestamp': now_iso, 'timestamp': now_iso,
'yaml_path': yaml_path, 'yaml_path': yaml_path,
@@ -1,14 +1,14 @@
--- ---
name: multi-agent-mux-orc-onboard name: multi-agent-mux-orc-onboard
description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture." description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture."
version: 4.0.0 version: 4.1.0
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
environments: [terminal, herdr] environments: [terminal, herdr]
metadata: metadata:
hermes: hermes:
tags: [agent, herdr, claude, antigravity, agy, hermes, grok, orchestrator, onboard, isolation] tags: [agent, herdr, claude, antigravity, agy, hermes, grok, opencode, orchestrator, onboard, isolation]
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-monitor] related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-monitor]
prereq_skills: [multi-agent-mux-create] prereq_skills: [multi-agent-mux-create]
--- ---
@@ -32,12 +32,13 @@ Registers an orchestrator session UUID into the `orchestrator_uuids` list of `.m
## Auto-Detection Hierarchy (Rev.2) ## Auto-Detection Hierarchy (Rev.2)
When `--uuid` is omitted, `orc_onboard.sh` inspects the process ancestry tree of the nearest agent ancestor (`claude`, `agy`, `hermes`, `grok`) in the following order: When `--uuid` is omitted, `orc_onboard.sh` inspects the process ancestry tree of the nearest agent ancestor (`claude`, `agy`, `hermes`, `grok`, `opencode`) in the following order:
1. **CLI `argv`**: 1. **CLI `argv`**:
- `claude -r <uuid>` / `claude --session-id <uuid>` - `claude -r <uuid>` / `claude --session-id <uuid>`
- `agy --conversation <uuid>` - `agy --conversation <uuid>`
- `grok --session-id <uuid>` / `grok --resume <uuid>` - `grok --session-id <uuid>` / `grok --resume <uuid>`
- `opencode --session <uuid>`
2. **Family-Matched Environment Variables**: 2. **Family-Matched Environment Variables**:
- `claude` → `CLAUDE_CODE_SESSION_ID` - `claude` → `CLAUDE_CODE_SESSION_ID`
- `agy` → `ANTIGRAVITY_CONVERSATION_ID` - `agy` → `ANTIGRAVITY_CONVERSATION_ID`
@@ -103,7 +103,7 @@ detect_nearest_agent() {
local base local base
base="$(basename "$tok")" base="$(basename "$tok")"
case "$base" in case "$base" in
claude|agy|hermes|grok) claude|agy|hermes|grok|opencode)
match="$base" match="$base"
break break
;; ;;
@@ -127,6 +127,9 @@ detect_nearest_agent() {
grok) grok)
detected_id=$(echo "$cmd_line" | grep -oE '(--session-id|--resume)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--session-id|--resume)[[:space:]=]+//' || true) detected_id=$(echo "$cmd_line" | grep -oE '(--session-id|--resume)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--session-id|--resume)[[:space:]=]+//' || true)
;; ;;
opencode)
detected_id=$(echo "$cmd_line" | grep -oE '(--session|-s)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--session|-s)[[:space:]=]+//' || true)
;;
esac esac
if [ -n "$detected_id" ] && is_valid_id "$detected_id"; then if [ -n "$detected_id" ] && is_valid_id "$detected_id"; then
@@ -141,6 +144,7 @@ detect_nearest_agent() {
agy) env_var="${ANTIGRAVITY_CONVERSATION_ID:-}" ;; agy) env_var="${ANTIGRAVITY_CONVERSATION_ID:-}" ;;
hermes) env_var="${HERMES_SESSION_ID:-}" ;; hermes) env_var="${HERMES_SESSION_ID:-}" ;;
grok) env_var="${GROK_SESSION_ID:-}" ;; grok) env_var="${GROK_SESSION_ID:-}" ;;
opencode) env_var="${OPENCODE_SESSION_ID:-}" ;;
esac esac
if [ -n "$env_var" ] && is_valid_id "$env_var"; then if [ -n "$env_var" ] && is_valid_id "$env_var"; then
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-resume name: multi-agent-mux-resume
description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk." description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk."
version: 4.0.0 version: 4.1.0
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -49,7 +49,7 @@ ideal resume path:
`agent-sessions.yaml` and on-disk discovery are used to resolve the UUID in this order: `agent-sessions.yaml` and on-disk discovery are used to resolve the UUID in this order:
1. **`herdr_sessions[]` row's per-row own id** (`claude_session_id_own` / `agy_conversation_id_own` / `hermes_conversation_id_own` / `grok_session_id_own`) — explicitly saved by `multi-agent-mux-stop` right before teardown (tier-1, race-free). 1. **`herdr_sessions[]` row's per-row own id** (`claude_session_id_own` / `agy_conversation_id_own` / `hermes_conversation_id_own` / `grok_session_id_own` / `opencode_session_id_own`) — explicitly saved by `multi-agent-mux-stop` right before teardown (tier-1, race-free).
2. **Workspace-scoped on-disk scan** (adapter `discover()`) 2. **Workspace-scoped on-disk scan** (adapter `discover()`)
If both are empty → the workspace has no conversation yet. Fall back to `multi-agent-mux-create`. If both are empty → the workspace has no conversation yet. Fall back to `multi-agent-mux-create`.
@@ -58,7 +58,7 @@ If both are empty → the workspace has no conversation yet. Fall back to `multi
```bash ```bash
WORKSPACE=/path/to/project WORKSPACE=/path/to/project
AGENT=claude # claude | agy | hermes | grok — pass it explicitly AGENT=claude # claude | agy | hermes | grok | opencode — pass it explicitly
SESSION_NAME=<workspace>-creator-<agent> # same convention as multi-agent-mux-create SESSION_NAME=<workspace>-creator-<agent> # same convention as multi-agent-mux-create
# Resolve the isolated herdr server name & load common utils # Resolve the isolated herdr server name & load common utils
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# resolve_session_id.sh — multi-agent-mux-resume 의 부속 스크립트 # resolve_session_id.sh — multi-agent-mux-resume 의 부속 스크립트
# Usage: # Usage:
# bash resolve_session_id.sh --workspace <path> --agent <claude|agy|hermes|grok> # bash resolve_session_id.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode>
# 출력: stdout 으로 UUID 한 줄 (없으면 빈 줄 + exit 0) # 출력: stdout 으로 UUID 한 줄 (없으면 빈 줄 + exit 0)
# #
# P0-C: 전역 agent_identities 를 즉시 반환하지 않는다. lib.sh::find_workspace_uuid # P0-C: 전역 agent_identities 를 즉시 반환하지 않는다. lib.sh::find_workspace_uuid
@@ -13,7 +13,7 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok> [--session <name>] Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> [--session <name>]
Outputs the resolved UUID on stdout (empty if not found). Outputs the resolved UUID on stdout (empty if not found).
--session prefers that registry row's recorded id; falls back to workspace-wide discovery if it does not verify. --session prefers that registry row's recorded id; falls back to workspace-wide discovery if it does not verify.
EOF EOF
@@ -36,8 +36,8 @@ done
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; } [ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; }
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; } [ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; }
case "$AGENT" in case "$AGENT" in
claude|agy|hermes|grok) ;; claude|agy|hermes|grok|opencode) ;;
*) echo "ERROR: --agent must be claude, agy, hermes, or grok" >&2; exit 2 ;; *) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode" >&2; exit 2 ;;
esac esac
find_workspace_uuid "$WORKSPACE" "$AGENT" "$SESSION_NAME" find_workspace_uuid "$WORKSPACE" "$AGENT" "$SESSION_NAME"
@@ -9,7 +9,7 @@ source "$LIB_SH"
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok> --session <name> [options] Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> --session <name> [options]
Options: Options:
--herdr-session NAME specify isolated herdr session name (alias: --herdr-server) --herdr-session NAME specify isolated herdr session name (alias: --herdr-server)
@@ -42,7 +42,7 @@ done
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; } [ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; }
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; } [ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; }
case "$AGENT" in case "$AGENT" in
claude|agy|hermes|grok) ;; claude|agy|hermes|grok|opencode) ;;
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;; *) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
esac esac
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; } [ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; }
@@ -103,6 +103,7 @@ if [ -z "$CMD_FULL" ]; then
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --conversation $UUID" ;; agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --conversation $UUID" ;;
hermes) CMD_FULL="${RESOLVED_BIN} --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;; hermes) CMD_FULL="${RESOLVED_BIN} --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
grok) CMD_FULL="${RESOLVED_BIN} --resume $UUID --permission-mode bypassPermissions" ;; grok) CMD_FULL="${RESOLVED_BIN} --resume $UUID --permission-mode bypassPermissions" ;;
opencode) CMD_FULL="${RESOLVED_BIN} --session $UUID --auto --agent build" ;;
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;; *) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
esac esac
fi fi
@@ -125,6 +126,12 @@ if [ "${DRY_RUN:-0}" = "1" ]; then
exit 0 exit 0
fi fi
if [ "$AGENT" = "opencode" ]; then
if [ -z "${OPENCODE_PERMISSION:-}" ]; then
export OPENCODE_PERMISSION='{"*":"allow"}'
fi
fi
# 4. Spawn new agent session (delegates to herdr translation shim) # 4. Spawn new agent session (delegates to herdr translation shim)
_herdr new-session -d -s "$SESSION_NAME" -c "$WORKSPACE" "$CMD_FULL" _herdr new-session -d -s "$SESSION_NAME" -c "$WORKSPACE" "$CMD_FULL"
if [ "$AGENT" = "claude" ]; then if [ "$AGENT" = "claude" ]; then
@@ -4,14 +4,14 @@
# resume UUID 를 per-row own id (claude_session_id_own / agy_conversation_id_own) # resume UUID 를 per-row own id (claude_session_id_own / agy_conversation_id_own)
# 에 박는다 — agent_identities 전역은 더 이상 primary 아님 (cache 로 강등, P0-C/단계 e). # 에 박는다 — agent_identities 전역은 더 이상 primary 아님 (cache 로 강등, P0-C/단계 e).
# #
# Usage: bash update_yaml_resumed.sh --session <name> --uuid <id> [--agent claude|agy|hermes|grok] # Usage: bash update_yaml_resumed.sh --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode]
set -euo pipefail set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok] [--herdr-session <name>] Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>]
EOF EOF
} }
@@ -190,6 +190,20 @@ elif agent == 'hermes':
cp = os.environ.get('CHILD_PID', '0') cp = os.environ.get('CHILD_PID', '0')
if cp.isdigit() and int(cp) > 0: if cp.isdigit() and int(cp) > 0:
target['child_pid'] = int(cp) target['child_pid'] = int(cp)
elif agent == 'grok':
target['pane']['cmd'] = 'grok'
target['pane']['cmd_full'] = f'grok --resume {uuid}'
target['grok_session_id_own'] = uuid
cp = os.environ.get('CHILD_PID', '0')
if cp.isdigit() and int(cp) > 0:
target['child_pid'] = int(cp)
elif agent == 'opencode':
target['pane']['cmd'] = 'opencode'
target['pane']['cmd_full'] = f'opencode --session {uuid} --auto --agent build'
target['opencode_session_id_own'] = uuid
cp = os.environ.get('CHILD_PID', '0')
if cp.isdigit() and int(cp) > 0:
target['child_pid'] = int(cp)
snap = d.setdefault('snapshot', {}) snap = d.setdefault('snapshot', {})
snap['taken_at'] = now snap['taken_at'] = now
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-status name: multi-agent-mux-status
description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop." description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop."
version: 4.0.0 version: 4.1.0
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -53,12 +53,12 @@ def resume_on_disk(s):
name = s.get('name', '') name = s.get('name', '')
cwd = (s.get('pane') or {}).get('cwd', '') cwd = (s.get('pane') or {}).get('cwd', '')
agent = None agent = None
for a in ('claude', 'agy', 'hermes', 'grok'): for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
if any(name.endswith(f'-{r}-{a}') for r in ('creator', 'planner', 'reviewer')) or name.endswith(f'-{a}'): if any(name.endswith(f'-{r}-{a}') for r in ('creator', 'planner', 'reviewer')) or name.endswith(f'-{a}'):
agent = a agent = a
break break
if not agent: if not agent:
for a in ('claude', 'agy', 'hermes', 'grok'): for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
if f"-{a}" in name or f"_{a}" in name: if f"-{a}" in name or f"_{a}" in name:
agent = a agent = a
break break
@@ -97,6 +97,34 @@ def resume_on_disk(s):
ws_slug = urllib.parse.quote(cwd, safe='') ws_slug = urllib.parse.quote(cwd, safe='')
return 'yes' if os.path.exists(f"{home}/.grok/sessions/{ws_slug}/{u}/chat_history.jsonl") else 'MISSING' return 'yes' if os.path.exists(f"{home}/.grok/sessions/{ws_slug}/{u}/chat_history.jsonl") else 'MISSING'
return 'no' return 'no'
if agent == 'opencode':
u = s.get('opencode_session_id_own')
if u:
base_dir = f"{home}/.local/share/opencode"
odb = None
for cand in [f"{base_dir}/opencode.db", f"{base_dir}/storage.db"]:
if os.path.exists(cand):
odb = cand
break
if not odb and os.path.isdir(f"{base_dir}/project"):
for root, _, files in os.walk(f"{base_dir}/project"):
for f in files:
if f.endswith(".db"):
odb = os.path.join(root, f)
break
if odb:
break
if not odb or not os.path.exists(odb):
return 'MISSING'
try:
import sqlite3
conn = sqlite3.connect(odb)
r = conn.execute("SELECT 1 FROM session WHERE id=?", (u,)).fetchone()
conn.close()
return 'yes' if r else 'MISSING'
except Exception:
return 'MISSING'
return 'no'
return '?' return '?'
@@ -202,18 +230,79 @@ def resume_on_disk(s):
# workspace-SCOPED check only — per-row own id, never a global identity (P0-C) # workspace-SCOPED check only — per-row own id, never a global identity (P0-C)
name = s.get('name', '') name = s.get('name', '')
cwd = (s.get('pane') or {}).get('cwd', '') cwd = (s.get('pane') or {}).get('cwd', '')
if name.endswith('-creator-claude'): agent = None
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
if any(name.endswith(f'-{r}-{a}') for r in ('creator', 'planner', 'reviewer')) or name.endswith(f'-{a}'):
agent = a
break
if not agent:
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
if f"-{a}" in name or f"_{a}" in name:
agent = a
break
if agent == 'claude':
u = s.get('claude_session_id_own') u = s.get('claude_session_id_own')
if u: if u:
key = cwd.replace('/', '-').replace('_', '-') key = cwd.replace('/', '-').replace('_', '-')
return 'yes' if os.path.exists(f"{claude_project_dir}/{key}/{u}.jsonl") else 'MISSING' return 'yes' if os.path.exists(f"{claude_project_dir}/{key}/{u}.jsonl") else 'MISSING'
key = cwd.replace('/', '-').replace('_', '-') key = cwd.replace('/', '-').replace('_', '-')
return 'scan' if glob.glob(f"{claude_project_dir}/{key}/*.jsonl") else 'no' return 'scan' if glob.glob(f"{claude_project_dir}/{key}/*.jsonl") else 'no'
if name.endswith('-creator-agy'): if agent == 'agy':
u = s.get('agy_conversation_id_own') u = s.get('agy_conversation_id_own')
if u: if u:
return 'yes' if os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{u}.db") else 'MISSING' return 'yes' if os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{u}.db") else 'MISSING'
return 'no' return 'no'
if agent == 'hermes':
u = s.get('hermes_conversation_id_own')
if u:
hdb = f"{home}/.hermes/state.db"
if not os.path.exists(hdb):
return 'MISSING'
try:
import sqlite3
conn = sqlite3.connect(hdb)
r = conn.execute("SELECT 1 FROM sessions WHERE id=?", (u,)).fetchone()
conn.close()
return 'yes' if r else 'MISSING'
except Exception:
return 'MISSING'
return 'no'
if agent == 'grok':
u = s.get('grok_session_id_own')
if u:
import urllib.parse
ws_slug = urllib.parse.quote(cwd, safe='')
return 'yes' if os.path.exists(f"{home}/.grok/sessions/{ws_slug}/{u}/chat_history.jsonl") else 'MISSING'
return 'no'
if agent == 'opencode':
u = s.get('opencode_session_id_own')
if u:
base_dir = f"{home}/.local/share/opencode"
odb = None
for cand in [f"{base_dir}/opencode.db", f"{base_dir}/storage.db"]:
if os.path.exists(cand):
odb = cand
break
if not odb and os.path.isdir(f"{base_dir}/project"):
for root, _, files in os.walk(f"{base_dir}/project"):
for f in files:
if f.endswith(".db"):
odb = os.path.join(root, f)
break
if odb:
break
if not odb or not os.path.exists(odb):
return 'MISSING'
try:
import sqlite3
conn = sqlite3.connect(odb)
r = conn.execute("SELECT 1 FROM session WHERE id=?", (u,)).fetchone()
conn.close()
return 'yes' if r else 'MISSING'
except Exception:
return 'MISSING'
return 'no'
return '?' return '?'
+2 -2
View File
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-stop name: multi-agent-mux-stop
description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start." description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start."
version: 4.0.0 version: 4.1.0
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -37,7 +37,7 @@ The stop command is always **graceful by default**:
```bash ```bash
SESSION_NAME=<workspace>-creator-<agent> # convention SESSION_NAME=<workspace>-creator-<agent> # convention
AGENT=claude # claude | agy | hermes | grok — always pass it AGENT=claude # claude | agy | hermes | grok | opencode — always pass it
AGENT_SESSIONS_YAML=.mam/agent-sessions.yaml AGENT_SESSIONS_YAML=.mam/agent-sessions.yaml
# 1) Session is registered? # 1) Session is registered?
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# stop_session.sh — multi-agent-mux-stop 의 부속 스크립트 # stop_session.sh — multi-agent-mux-stop 의 부속 스크립트
# Usage: # Usage:
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|grok] [--herdr-session <name>] \ # bash stop_session.sh --session <name> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>] \
# [--reason <reason>] [--purge-conversation] [--yes] # [--reason <reason>] [--purge-conversation] [--yes]
# #
# 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고 # 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고
@@ -12,7 +12,7 @@
# #
# 옵션: # 옵션:
# --session <name> — 대상 세션 (필수) # --session <name> — 대상 세션 (필수)
# --agent <type> — claude | agy | hermes | grok # --agent <type> — claude | agy | hermes | grok | opencode
# (권장: 항상 명시. 미지정 시 레지스트리 기록으로 # (권장: 항상 명시. 미지정 시 레지스트리 기록으로
# 해석 — agent 필드 → 세션명 접미사 → pane.cmd; # 해석 — agent 필드 → 세션명 접미사 → pane.cmd;
# 셋 다 실패하면 exit 2) # 셋 다 실패하면 exit 2)
@@ -91,8 +91,8 @@ while [ $# -gt 0 ]; do
done done
if [ -n "$AGENT" ]; then if [ -n "$AGENT" ]; then
case "$AGENT" in case "$AGENT" in
claude|agy|hermes|grok) ;; claude|agy|hermes|grok|opencode) ;;
*) echo "ERROR: invalid agent type '$AGENT'. Allowed types are: claude, agy, hermes, grok." >&2; exit 2 ;; *) echo "ERROR: invalid agent type '$AGENT'. Allowed types are: claude, agy, hermes, grok, opencode." >&2; exit 2 ;;
esac esac
fi fi
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; usage; exit 2; } [ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; usage; exit 2; }
@@ -287,6 +287,8 @@ if captured and not purge:
target['hermes_conversation_id_own'] = captured target['hermes_conversation_id_own'] = captured
elif agent == 'grok': elif agent == 'grok':
target['grok_session_id_own'] = captured target['grok_session_id_own'] = captured
elif agent == 'opencode':
target['opencode_session_id_own'] = captured
target['resumable'] = True target['resumable'] = True
if purge and purge_uuid: if purge and purge_uuid:
+11
View File
@@ -98,6 +98,17 @@
#default: (unset → reads ~/.grok/auth.json) #default: (unset → reads ~/.grok/auth.json)
# XAI_API_KEY=replace_me # XAI_API_KEY=replace_me
# Optional multi-provider API keys for OpenCode CLI (if ~/.local/share/opencode/auth.json is not present).
#default: (unset → reads ~/.local/share/opencode/auth.json)
# ANTHROPIC_API_KEY=replace_me
# OPENAI_API_KEY=replace_me
# GEMINI_API_KEY=replace_me
# GROQ_API_KEY=replace_me
# Optional inline JSON permissions configuration for OpenCode.
#default: (unset → defaults to --auto CLI bypass)
# OPENCODE_PERMISSION='{"*":"allow"}'
# Retention period (days) for delegate-job event logs. # Retention period (days) for delegate-job event logs.
#default: 7 #default: 7
# MAM_EVENT_RETENTION_DAYS=7 # MAM_EVENT_RETENTION_DAYS=7
+34 -14
View File
@@ -6,39 +6,59 @@
## 📌 현재 버전 개요 (Current Release) ## 📌 현재 버전 개요 (Current Release)
- **프레임워크 버전**: `v4.0.0` - **프레임워크 버전**: `v4.1.0`
- **최신 릴리스 일시**: 2026-08-28 (KST) - **최신 릴리스 일시**: 2026-08-29 (KST)
- **기준 브랜치**: `main` - **기준 브랜치**: `main`
- **핵심 아키텍처**: - **핵심 아키텍처**:
- **Complete Cline Agent Deprecation & Core 4-Agent Whitelist**: `cline` 백엔드 완전 제거(SemVer 2.0.0 §8 MAJOR) 및 `claude`, `agy`, `hermes`, `grok` 4대 에이전트 표준화 - **OpenCode AI Agent Full Integration (`anomalyco/opencode`)**: `opencode` 에이전트 백엔드 어댑터(`OpenCodeAgentAdapter`) 추가, 29개 CLI 라이프사이클 및 오케스트레이션 스크립트 연동, SQLite 동적 DB 경로(`opencode db path`) 및 `directory`/`time_created`(ms) 스키마 방어적 처리, `OPENCODE_PERMISSION` 권한 자동 내보내기, drift-C 자율 조정 동기화 지원
- **Complete Cline Agent Deprecation & Core 5-Agent Whitelist**: `cline` 백엔드 완전 제거 및 `claude`, `agy`, `hermes`, `grok`, `opencode` 5대 에이전트 표준화
- **Hermes Agent Full Modernization & Ollama Live Integration**: 헤드리스 플래그, TUI 입력 구분자/프롬프트 정립, `reconcile.sh`/`verify_artifact()` 타임스탬프 가드 및 멀티 후보자 발견 동등성 확보 - **Hermes Agent Full Modernization & Ollama Live Integration**: 헤드리스 플래그, TUI 입력 구분자/프롬프트 정립, `reconcile.sh`/`verify_artifact()` 타임스탬프 가드 및 멀티 후보자 발견 동등성 확보
- **2-Tier TUI Readiness Model (`S ∨ (W ∧ C)`) & Modal Priority**: 강한 토큰(`S`) 단독 충족 및 약한 토큰(`W`) + 보강 패턴(`C`) 결합 판정식, 모달 선행 검사 및 힌트 분리를 통한 다이얼로그 기아 원천 해소 - **2-Tier TUI Readiness Model (`S ∨ (W ∧ C)`) & Modal Priority**: 강한 토큰(`S`) 단독 충족 및 약한 토큰(`W`) + 보강 패턴(`C`) 결합 판정식, 모달 선행 검사 및 힌트 분리를 통한 다이얼로그 기아 원천 해소
- **Adapter Modal Contract (`T-2d`)**: `BaseAgentAdapter` 내 `modal_tokens` 프로퍼티 및 팩트 브리지(`MAM_MODAL_TOKENS`) 통합 - **Adapter Modal Contract (`T-2d`)**: `BaseAgentAdapter` 내 `modal_tokens` 프로퍼티 및 팩트 브리지(`MAM_MODAL_TOKENS`) 통합
- **Fail-Closed Exact Pane Resolver & Multi-Workspace Isolation Engine**: `_resolve_herdr_pane_id` 동종 페인 다중 매치 시 fail-closed(`exit 1`) 차단, `HERDR_WORKSPACE_ID` 스코핑 및 `$WORKSPACE_ROOT/.mam/herdr_workspace_id` 영속화 - **Fail-Closed Exact Pane Resolver & Multi-Workspace Isolation Engine**: `_resolve_herdr_pane_id` 동종 페인 다중 매치 시 fail-closed(`exit 1`) 차단, `HERDR_WORKSPACE_ID` 스코핑 및 `$WORKSPACE_ROOT/.mam/herdr_workspace_id` 영속화
- **Runtime Framework Version Constant (`MAM_VERSION`) & 3-Way Lockstep**: `lib.sh` 내 `MAM_VERSION="4.0.0"` 런타임 진실 공급원 정의 및 3자 동기화 가드 체계 구축 - **Runtime Framework Version Constant (`MAM_VERSION`) & 3-Way Lockstep**: `lib.sh` 내 `MAM_VERSION="4.1.0"` 런타임 진실 공급원 정의 및 3자 동기화 가드 체계 구축
- **Comprehensive Test Suite Milestone**: 439개 전체 테스트 100% PASS (439 passed / 0 failed). - **Comprehensive Test Suite Milestone**: 447개 전체 테스트 100% PASS (447 passed / 0 failed).
--- ---
## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix) ## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix)
모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.0.0`으로 동기화되어 배포됩니다. 모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.1.0`으로 동기화되어 배포됩니다.
| 스킬명 | 버전 | 역할 및 주요 책임 | 상태 | | 스킬명 | 버전 | 역할 및 주요 책임 | 상태 |
| :--- | :---: | :--- | :---: | | :--- | :---: | :--- | :---: |
| **`multi-agent-mux-create`** | `4.0.0` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 | | **`multi-agent-mux-create`** | `4.1.0` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 |
| **`multi-agent-mux-stop`** | `4.0.0` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 | | **`multi-agent-mux-stop`** | `4.1.0` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 |
| **`multi-agent-mux-resume`** | `4.0.0` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 | | **`multi-agent-mux-resume`** | `4.1.0` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 |
| **`multi-agent-mux-status`** | `4.0.0` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 | | **`multi-agent-mux-status`** | `4.1.0` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 |
| **`multi-agent-mux-monitor`** | `4.0.0` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 | | **`multi-agent-mux-monitor`** | `4.1.0` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 |
| **`multi-agent-mux-delegate-job`** | `4.0.0` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 | | **`multi-agent-mux-delegate-job`** | `4.1.0` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 |
| **`multi-agent-mux-loop`** | `4.0.0` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 | | **`multi-agent-mux-loop`** | `4.1.0` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 |
| **`multi-agent-mux-orc-onboard`** | `4.0.0` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 | | **`multi-agent-mux-orc-onboard`** | `4.1.0` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 |
--- ---
## 📋 버전별 상세 변경 내역 (Changelog) ## 📋 버전별 상세 변경 내역 (Changelog)
### 🚀 `v4.1.0` — OpenCode AI Agent Integration (2026-08-29)
> **주요 마일스톤 (MINOR Release)**: OpenCode AI 에이전트(`anomalyco/opencode`) 백엔드 완전 통합(SemVer 2.0.0 §7 MINOR), 5대 핵심 에이전트(`claude`, `agy`, `hermes`, `grok`, `opencode`) 표준화, 29개 CLI 스크립트/스킬 연동 및 447개 전체 테스트 100% PASS 달성.
#### ✨ 주요 변경 사항 (New Features & Enhancements)
* **F-1: OpenCode 에이전트 어댑터(`OpenCodeAgentAdapter`) 구현**:
- `lib_py/agents/adapters/opencode.py` 신규 구현 및 `lib_py/agents/registry.py` 등록.
- SQLite 스토리지(`opencode.db`) 내 `directory` 및 `time_created`(밀리초) 실 스키마 기반의 방어적 세션 검증(`verify_artifact`) 및 멀티 후보자 발견(`discover`) 지원.
- `_resolve_db()`를 통한 `opencode db path` CLI 동적 해석 및 샌드박스 환경(`HOME`/`XDG_DATA_HOME`) 격리 지원.
* **F-2: CLI 라이프사이클 및 스크립트 전면 연동 (29개 터치포인트)**:
- `create_session.sh`, `resume_session.sh`, `stop_session.sh`, `status.sh`, `reconcile.sh`, `run_loop.sh`, `orc_onboard.sh` 내 `--agent opencode` 완전 지원.
- 세션 생성 및 재개 시 `OPENCODE_PERMISSION='{"*":"allow"}'` 자동 내보내기 및 사용자 사전 설정 보존(Empty-Guard).
- 백그라운드 조정자(`reconcile.sh`) drift-C 블록에서 어댑터 공유 헬퍼를 통한 세션 ID 사후 자율 감지 및 YAML 등록.
* **F-3: 포괄적 테스트 스위트 확장**:
- `tests/test_a4_adapter_contract.py`: 어댑터 계약, 실 스키마 밀리초 비교, CLI DB 경로 동적 해석 검증.
- `tests/test_tier1_unit.py`, `tests/test_tier2_component.py`, `tests/test_c1_tui_readiness.py`: TUI 및 라이프사이클 통합 검증 (총 447개 테스트 100% 통과).
---
### 🚀 `v4.0.0` — Complete Cline Agent Deprecation & Hermes Modernization (2026-08-28) ### 🚀 `v4.0.0` — Complete Cline Agent Deprecation & Hermes Modernization (2026-08-28)
> **주요 마일스톤 (MAJOR Release)**: `cline` 에이전트 백엔드의 완전한 제거 및 폐지(Breaking Change per SemVer 2.0.0 §8), Hermes 에이전트 풀 모더니제이션 및 Ollama 로컬 연동 정식 지원, 4대 핵심 에이전트(`claude`, `agy`, `hermes`, `grok`) 표준화 및 439개 전체 테스트 100% PASS 달성. > **주요 마일스톤 (MAJOR Release)**: `cline` 에이전트 백엔드의 완전한 제거 및 폐지(Breaking Change per SemVer 2.0.0 §8), Hermes 에이전트 풀 모더니제이션 및 Ollama 로컬 연동 정식 지원, 4대 핵심 에이전트(`claude`, `agy`, `hermes`, `grok`) 표준화 및 439개 전체 테스트 100% PASS 달성.
+1 -1
View File
@@ -264,7 +264,7 @@ def test_adapter_required_properties():
| 에이전트 백엔드 | 세션 저장소 형식 | 인증 방식 | 프롬프트 감지 난이도 | 난이도 Tier | 예상 소요 공수 | | 에이전트 백엔드 | 세션 저장소 형식 | 인증 방식 | 프롬프트 감지 난이도 | 난이도 Tier | 예상 소요 공수 |
| :--- | :--- | :--- | :--- | :---: | :---: | | :--- | :--- | :--- | :--- | :---: | :---: |
| **OpenCode** | `~/.opencode/sessions/*.json` | 로컬 토큰 / API Key | 쉬움 (`OpenCode\|Chat`) | **Tier 1 (낮음)** | **~0.5일** | | **OpenCode** | `~/.local/share/opencode/` (SQLite, `opencode db path`) | 멀티 Provider Key / `auth.json` | 쉬움 (`OpenCode\|Chat`) | **Tier 1 (낮음)** | **~1.0일** |
| **Codex CLI** | `~/.codex/projects/*.jsonl` | `OPENAI_API_KEY` | 쉬움 (`Codex\|❯`) | **Tier 1 (낮음)** | **~0.5일** | | **Codex CLI** | `~/.codex/projects/*.jsonl` | `OPENAI_API_KEY` | 쉬움 (`Codex\|❯`) | **Tier 1 (낮음)** | **~0.5일** |
| **Kimi CLI** | `~/.kimi/history.db` (SQLite) | `~/.kimi/config` | 쉬움 (`Moonshot\|Kimi`) | **Tier 1 (낮음)** | **~0.5일** | | **Kimi CLI** | `~/.kimi/history.db` (SQLite) | `~/.kimi/config` | 쉬움 (`Moonshot\|Kimi`) | **Tier 1 (낮음)** | **~0.5일** |
| **Grok-Build** | `~/.grok/sessions/*.json` | 토큰 파일 / Env | 쉬움 (`Grok\|Building`) | **Tier 1 (낮음)** | **~0.5일** | | **Grok-Build** | `~/.grok/sessions/*.json` | 토큰 파일 / Env | 쉬움 (`Grok\|Building`) | **Tier 1 (낮음)** | **~0.5일** |
+130
View File
@@ -0,0 +1,130 @@
# 🦙 OpenCode (`anomalyco/opencode`) — Ollama 연동 및 기본 모델 설정 가이드
본 문서는 **OpenCode** CLI/TUI 환경에서 로컬 **Ollama** 프로바이더와 모델을 기본값(Default)으로 연결하고 최적화하여 사용하는 방법을 정리한 가이드입니다.
---
## 1. 설정 파일 위치 (`opencode.jsonc` vs `opencode.json`)
OpenCode는 **JSONC (JSON with Comments)** 형식을 공식 지원하므로 주석(`//`, `/* */`) 작성이 가능한 `opencode.jsonc`를 기본 설정 파일로 사용합니다. (`.json`과 `.jsonc` 둘 다 동일하게 인식됩니다.)
* **전역 설정 경로 (추천)**:
* `~/.config/opencode/opencode.jsonc` (또는 `~/.config/opencode/opencode.json`)
* **프로젝트별 로컬 설정 경로**:
* `<project-root>/opencode.json` (특정 프로젝트 루트 디렉터리에 위치)
---
## 2. 권장 설정 내용 (`opencode.jsonc`)
`~/.config/opencode/opencode.jsonc` 파일에 아래와 같이 구성합니다:
```jsonc
{
"$schema": "https://opencode.ai/config.json",
// 1. 기본 실행 모델 (형식: "provider_id/model_id")
"model": "ollama/qwen2.5-coder:32b",
// 2. Ollama 프로바이더 상세 엔드포인트 및 모델 정의
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama",
"options": {
"baseURL": "http://127.0.0.1:11434/v1"
},
"models": {
"qwen2.5-coder:32b": {
"name": "Qwen 2.5 Coder 32B"
},
"glm-5.3:cloud": {
"name": "GLM 5.3 Cloud"
}
}
}
}
}
```
> [!TIP]
> `qwen2.5-coder:32b` 대신 본인의 머신에 설치된 Ollama 모델 태그명(예: `qwen2.5-coder:14b`, `llama3.1:8b`, `deepseek-r1:14b` 등)을 기재하시면 됩니다.
---
## 3. Ollama 사용 시 필수 사전 준비 (Critical)
### 3.1 Ollama 서비스 구동 및 API 확인
```bash
# Ollama 모델 목록 확인
ollama list
# OpenAI 호환 v1 엔드포인트 응답 확인
curl http://127.0.0.1:11434/v1/models
```
### 3.2 컨텍스트 윈도우(`num_ctx`) 확장
Ollama의 기본 컨텍스트 크기는 **4,096 토큰**으로 매우 작아서, 에이전트가 파일 트리나 긴 소스 코드를 읽을 때 컨텍스트 오버플로우가 발생할 수 있습니다. `Modelfile`을 작성하여 32K(32,768) 이상으로 확장하는 것을 강력히 권장합니다.
```bash
# 1) Modelfile 생성
cat << 'EOF' > Modelfile
FROM qwen2.5-coder:32b
PARAMETER num_ctx 32768
EOF
# 2) 확장된 커스텀 모델 빌드
ollama create qwen2.5-coder-32k -f Modelfile
```
* 빌드 후 `opencode.jsonc`의 `"model"`을 `"ollama/qwen2.5-coder-32k"`로 지정합니다.
### 3.3 Tool Calling(함수 호출) 지원 모델 선택
OpenCode는 파일 읽기/수정, 셸 실행 등을 모델의 **Tool Calling** 기능으로 처리합니다.
* **추천 모델**: `qwen2.5-coder` (7b/14b/32b), `llama3.1` (8b/70b), `mistral-nemo`
---
## 4. 실행 및 런타임 제어
### 4.1 기본 실행 (설정 파일 적용)
```bash
# 기본 설정된 Ollama 모델로 TUI 실행
opencode
```
### 4.2 CLI 플래그로 특정 모델 즉시 실행 (`-m`)
```bash
# 대화형 TUI 실행
opencode -m "ollama/qwen2.5-coder:32b"
# 비대화형(Headless) 무인 단발 태스크 실행
opencode run -m "ollama/qwen2.5-coder:32b" "tests/ 단위 테스트를 실행하고 실패하는 테스트를 고쳐줘"
```
### 4.3 TUI 실행 중 대화식 모델 전환
* OpenCode 대화창에서 `Tab` 키를 누르거나 프롬프트에 `/models`를 입력하여 등록된 모델 목록 중 원하는 모델을 즉시 선택할 수 있습니다.
---
## 5. Multi-Agent Mux (MAM) 연동 예시
MAM 환경에서 OpenCode를 reviewer/creator 에이전트로 띄울 때의 표준 명령어:
```bash
# 1. 세션 생성 및 온보딩
bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
--workspace "$(pwd)" \
--agent "opencode" \
--role "reviewer" \
--session "reviewer-opencode-01" \
--herdr-session "multi-agent-mux" \
--herdr-workspace "mam-agent" \
--onboard
# 2. 세션 대화 복원 (Resume)
bash .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh \
--workspace "$(pwd)" \
--agent "opencode" \
--session "reviewer-opencode-01" \
--herdr-session "multi-agent-mux"
```
+52 -9
View File
@@ -533,13 +533,6 @@ elif cmd1 == "agent":
sys.stderr.write(json.dumps(err_payload) + "\\n") sys.stderr.write(json.dumps(err_payload) + "\\n")
sys.exit(1) sys.exit(1)
# TUI Welcome Tokens definition to prevent TUI readiness check timeout
buffer_content = {
"claude": "Anthropic Claude Ready",
"agy": "Antigravity Ready",
"hermes": "Hermes Ready"
}.get(agent_type, "Ready")
# Look up cwd from target pane in state if not explicitly passed # Look up cwd from target pane in state if not explicitly passed
if not cwd and pane: if not cwd and pane:
for p in state.get("panes", []): for p in state.get("panes", []):
@@ -550,6 +543,16 @@ elif cmd1 == "agent":
if not ws and pane and ":" in pane: if not ws and pane and ":" in pane:
ws = pane.split(":")[0] ws = pane.split(":")[0]
# TUI Welcome Tokens definition to prevent TUI readiness check timeout
resolved_cwd = cwd or "TMP_PATH_PLACEHOLDER"
buffer_content = {
"claude": f"Anthropic Claude Ready in {resolved_cwd}",
"agy": f"Antigravity Ready in {resolved_cwd}",
"hermes": f"Hermes Ready in {resolved_cwd}",
"grok": f"Grok Ready in {resolved_cwd}",
"opencode": f"OpenCode Ready in {resolved_cwd}"
}.get(agent_type, f"Ready in {resolved_cwd}")
agents = state.get("agents", {}) agents = state.get("agents", {})
agents[name] = { agents[name] = {
"agent": agent_type, "agent": agent_type,
@@ -576,7 +579,9 @@ elif cmd1 == "agent":
own_key_map = { own_key_map = {
"claude": "claude_session_id_own", "claude": "claude_session_id_own",
"agy": "agy_conversation_id_own", "agy": "agy_conversation_id_own",
"hermes": "hermes_conversation_id_own" "hermes": "hermes_conversation_id_own",
"grok": "grok_session_id_own",
"opencode": "opencode_session_id_own"
} }
own_key = own_key_map.get(agent_type) own_key = own_key_map.get(agent_type)
if own_key: if own_key:
@@ -627,6 +632,16 @@ elif cmd1 == "agent":
conn.execute("INSERT OR REPLACE INTO sessions (id, cwd) VALUES (?, ?)", (session_uuid, ws_abs)) conn.execute("INSERT OR REPLACE INTO sessions (id, cwd) VALUES (?, ?)", (session_uuid, ws_abs))
conn.commit() conn.commit()
conn.close() conn.close()
elif agent_type == "opencode":
opencode_dir = os.path.join(home_dir, ".local", "share", "opencode")
os.makedirs(opencode_dir, exist_ok=True)
odb = os.path.join(opencode_dir, "opencode.db")
conn = sqlite3.connect(odb)
conn.execute("CREATE TABLE IF NOT EXISTS session (id TEXT PRIMARY KEY, directory TEXT, time_created INTEGER, cwd TEXT, created_at REAL)")
now_ms = int(time.time() * 1000)
conn.execute("INSERT OR REPLACE INTO session (id, directory, time_created, cwd, created_at) VALUES (?, ?, ?, ?, ?)", (session_uuid, ws_abs, now_ms, ws_abs, time.time()))
conn.commit()
conn.close()
state["agents"] = agents state["agents"] = agents
save_state() save_state()
@@ -946,7 +961,35 @@ sys.exit(0)
""") """)
hermes_bin.chmod(0o755) hermes_bin.chmod(0o755)
# 4. uuidgen mock to guarantee isolated creation UUIDs # 4. grok mock
grok_bin = bin_dir / "grok"
grok_bin.write_text("""#!/usr/bin/env python3
import sys
sys.exit(0)
""")
grok_bin.chmod(0o755)
# 5. opencode mock
opencode_bin = bin_dir / "opencode"
opencode_bin.write_text("""#!/usr/bin/env python3
import sys, os
args = sys.argv[1:]
if len(args) >= 2 and args[0] == "db" and args[1] == "path":
home = os.environ.get("HOME", "")
db_path = f"{home}/.local/share/opencode/opencode.db"
os.makedirs(os.path.dirname(db_path), exist_ok=True)
if not os.path.exists(db_path):
import sqlite3
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE IF NOT EXISTS session (id TEXT PRIMARY KEY, directory TEXT, time_created INTEGER)")
conn.close()
print(db_path)
sys.exit(0)
sys.exit(0)
""")
opencode_bin.chmod(0o755)
# 6. uuidgen mock to guarantee isolated creation UUIDs
uuidgen_bin = bin_dir / "uuidgen" uuidgen_bin = bin_dir / "uuidgen"
uuidgen_bin.write_text("""#!/usr/bin/env python3 uuidgen_bin.write_text("""#!/usr/bin/env python3
import uuid import uuid
+319 -2
View File
@@ -31,6 +31,7 @@ def test_agent_adapter_registry():
'agy': 'agy_conversation_id_own', 'agy': 'agy_conversation_id_own',
'hermes': 'hermes_conversation_id_own', 'hermes': 'hermes_conversation_id_own',
'grok': 'grok_session_id_own', 'grok': 'grok_session_id_own',
'opencode': 'opencode_session_id_own',
} }
for agent, expected in EXPECTED_OWN_KEYS.items(): for agent, expected in EXPECTED_OWN_KEYS.items():
adapter = get_adapter(agent) adapter = get_adapter(agent)
@@ -73,6 +74,7 @@ def test_adapter_required_properties():
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')), 'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
'hermes': ('Hermes|Welcome to Hermes Agent|NOUS HERMES', '/exit', 'hermes-agent', ('session_id',)), 'hermes': ('Hermes|Welcome to Hermes Agent|NOUS HERMES', '/exit', 'hermes-agent', ('session_id',)),
'grok': ('Grok|xAI|Assistant|❯|>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')), 'grok': ('Grok|xAI|Assistant|❯|>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')),
'opencode': ('OpenCode|Chat', '/exit', 'opencode-cli', ('session_id',)),
} }
for agent, (toks, exitk, delk, cache_f) in expected.items(): for agent, (toks, exitk, delk, cache_f) in expected.items():
adapter = get_adapter(agent) adapter = get_adapter(agent)
@@ -93,6 +95,7 @@ def test_adapter_modal_tokens_contract():
'agy': None, 'agy': None,
'hermes': None, 'hermes': None,
'grok': None, 'grok': None,
'opencode': None,
} }
for agent, expected in expected_modal.items(): for agent, expected in expected_modal.items():
adapter = get_adapter(agent) adapter = get_adapter(agent)
@@ -106,7 +109,7 @@ def test_facts_bridge_eval_contract():
env = os.environ.copy() env = os.environ.copy()
skills_dir = str(Path(__file__).resolve().parent.parent / ".agents" / "skills") skills_dir = str(Path(__file__).resolve().parent.parent / ".agents" / "skills")
env["PYTHONPATH"] = f"{skills_dir}:{env.get('PYTHONPATH', '')}" env["PYTHONPATH"] = f"{skills_dir}:{env.get('PYTHONPATH', '')}"
for agent in ('claude', 'agy', 'hermes', 'grok'): for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
res = subprocess.run([sys.executable, "-m", "lib_py.agents", "facts", agent], capture_output=True, text=True, env=env) res = subprocess.run([sys.executable, "-m", "lib_py.agents", "facts", agent], capture_output=True, text=True, env=env)
assert res.returncode == 0 assert res.returncode == 0
facts_output = res.stdout facts_output = res.stdout
@@ -126,6 +129,72 @@ def shlex_quote(s):
import shlex import shlex
return shlex.quote(s) return shlex.quote(s)
def _seed_opencode_session(db_path, session_id, ws_path, epoch_sec):
import sqlite3
os.makedirs(os.path.dirname(db_path), exist_ok=True)
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = OFF")
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = {row[0] for row in cursor.fetchall()}
if "session" not in tables:
conn.execute("CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT, time_created INTEGER, cwd TEXT, created_at REAL)")
if "project" in tables:
cursor.execute("PRAGMA table_info(project)")
p_cols = [col[1] for col in cursor.fetchall()]
p_ins = ["id"]
p_vals = ["default"]
if "worktree" in p_cols:
p_ins.append("worktree")
p_vals.append(ws_path)
if "time_created" in p_cols:
p_ins.append("time_created")
p_vals.append(0)
if "time_updated" in p_cols:
p_ins.append("time_updated")
p_vals.append(0)
conn.execute(f"INSERT OR IGNORE INTO project ({', '.join(p_ins)}) VALUES ({', '.join(['?']*len(p_vals))})", p_vals)
cursor.execute("PRAGMA table_info(session)")
cols = [col[1] for col in cursor.fetchall()]
insert_cols = ["id"]
vals = [session_id]
if "project_id" in cols:
insert_cols.append("project_id")
vals.append("default")
if "slug" in cols:
insert_cols.append("slug")
vals.append("default")
if "title" in cols:
insert_cols.append("title")
vals.append("default")
if "version" in cols:
insert_cols.append("version")
vals.append("1.0.0")
if "directory" in cols:
insert_cols.append("directory")
vals.append(ws_path)
if "cwd" in cols:
insert_cols.append("cwd")
vals.append(ws_path)
if "time_created" in cols:
insert_cols.append("time_created")
vals.append(int(float(epoch_sec) * 1000))
if "time_updated" in cols:
insert_cols.append("time_updated")
vals.append(int(float(epoch_sec) * 1000))
if "created_at" in cols:
insert_cols.append("created_at")
vals.append(float(epoch_sec))
placeholders = ", ".join(["?"] * len(vals))
col_str = ", ".join(insert_cols)
conn.execute(f"INSERT OR REPLACE INTO session ({col_str}) VALUES ({placeholders})", vals)
conn.commit()
conn.close()
def test_purge_artifacts_composite(tmp_path): def test_purge_artifacts_composite(tmp_path):
import sqlite3 import sqlite3
from lib_py.agents.base import DiscoveryContext from lib_py.agents.base import DiscoveryContext
@@ -200,6 +269,64 @@ def test_purge_artifacts_composite(tmp_path):
assert not os.path.exists(g_path) assert not os.path.exists(g_path)
assert not os.path.exists(os.path.dirname(g_path)) assert not os.path.exists(os.path.dirname(g_path))
# 5. OpenCode (SQLite tables session, message, part)
opencode_adapter = get_adapter('opencode')
opencode_ctx = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
o_db = opencode_adapter.artifact_path('uuid-o', opencode_ctx)
_seed_opencode_session(o_db, 'uuid-o', ws, 2000.0)
conn = sqlite3.connect(o_db)
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = OFF")
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = {row[0] for row in cursor.fetchall()}
if "message" not in tables:
conn.execute("CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT)")
if "part" not in tables:
conn.execute("CREATE TABLE part (id TEXT PRIMARY KEY, session_id TEXT)")
cursor.execute("PRAGMA table_info(message)")
m_cols = [c[1] for c in cursor.fetchall()]
m_ins = ["id", "session_id"] if "id" in m_cols else ["session_id"]
m_vals = ["m1", "uuid-o"] if "id" in m_cols else ["uuid-o"]
if "time_created" in m_cols:
m_ins.append("time_created")
m_vals.append(0)
if "time_updated" in m_cols:
m_ins.append("time_updated")
m_vals.append(0)
if "data" in m_cols:
m_ins.append("data")
m_vals.append("{}")
conn.execute(f"INSERT OR REPLACE INTO message ({', '.join(m_ins)}) VALUES ({', '.join(['?']*len(m_vals))})", m_vals)
cursor.execute("PRAGMA table_info(part)")
p_cols = [c[1] for c in cursor.fetchall()]
p_ins = ["id", "session_id"] if "id" in p_cols else ["session_id"]
p_vals = ["p1", "uuid-o"] if "id" in p_cols else ["uuid-o"]
if "message_id" in p_cols:
p_ins.append("message_id")
p_vals.append("m1")
if "time_created" in p_cols:
p_ins.append("time_created")
p_vals.append(0)
if "time_updated" in p_cols:
p_ins.append("time_updated")
p_vals.append(0)
if "data" in p_cols:
p_ins.append("data")
p_vals.append("{}")
conn.execute(f"INSERT OR REPLACE INTO part ({', '.join(p_ins)}) VALUES ({', '.join(['?']*len(p_vals))})", p_vals)
conn.commit()
conn.close()
purged_o = opencode_adapter.purge_artifacts('uuid-o', opencode_ctx)
assert len(purged_o) == 1
conn = sqlite3.connect(o_db)
assert conn.execute("SELECT count(*) FROM session WHERE id='uuid-o'").fetchone()[0] == 0
assert conn.execute("SELECT count(*) FROM message WHERE session_id='uuid-o'").fetchone()[0] == 0
assert conn.execute("SELECT count(*) FROM part WHERE session_id='uuid-o'").fetchone()[0] == 0
conn.close()
def test_adapter_spawn_and_resume_specs(): def test_adapter_spawn_and_resume_specs():
claude = get_adapter('claude') claude = get_adapter('claude')
assert claude.spawn_spec('claude', 'u1') == 'claude --dangerously-skip-permissions --session-id u1' assert claude.spawn_spec('claude', 'u1') == 'claude --dangerously-skip-permissions --session-id u1'
@@ -222,6 +349,12 @@ def test_adapter_spawn_and_resume_specs():
assert grok.resume_spec('grok', 'u1', materialized=True) == 'grok --resume u1 --permission-mode bypassPermissions' assert grok.resume_spec('grok', 'u1', materialized=True) == 'grok --resume u1 --permission-mode bypassPermissions'
assert grok.resume_spec('grok', 'u1', materialized=False) == 'grok --session-id u1 --permission-mode bypassPermissions' assert grok.resume_spec('grok', 'u1', materialized=False) == 'grok --session-id u1 --permission-mode bypassPermissions'
opencode = get_adapter('opencode')
assert opencode.spawn_spec('opencode', 'u1') == 'opencode --auto --agent build'
assert opencode.spawn_spec('opencode', '') == 'opencode --auto --agent build'
assert opencode.resume_spec('opencode', 'u1', materialized=True) == 'opencode --session u1 --auto --agent build'
assert opencode.resume_spec('opencode', 'u1', materialized=False) == 'opencode --auto --agent build'
def test_adapter_auth_ok(tmp_path, monkeypatch): def test_adapter_auth_ok(tmp_path, monkeypatch):
monkeypatch.setenv("HOME_DIR", str(tmp_path)) monkeypatch.setenv("HOME_DIR", str(tmp_path))
# Claude auth runner # Claude auth runner
@@ -251,6 +384,17 @@ def test_adapter_auth_ok(tmp_path, monkeypatch):
# Hermes always True # Hermes always True
assert get_adapter('hermes').auth_ok() is True assert get_adapter('hermes').auth_ok() is True
# OpenCode auth check (provider env var or auth.json file)
opencode = get_adapter('opencode')
assert opencode.auth_ok() is False
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
assert opencode.auth_ok() is True
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
opencode_auth = tmp_path / ".local" / "share" / "opencode" / "auth.json"
opencode_auth.parent.mkdir(parents=True, exist_ok=True)
opencode_auth.write_text("{}")
assert opencode.auth_ok() is True
def test_adapter_discover(tmp_path): def test_adapter_discover(tmp_path):
import sqlite3 import sqlite3
from lib_py.agents.base import DiscoveryContext from lib_py.agents.base import DiscoveryContext
@@ -306,6 +450,13 @@ def test_adapter_discover(tmp_path):
f.write('{"session_id": "u-g1", "cwd": "' + ws + '"}\n') f.write('{"session_id": "u-g1", "cwd": "' + ws + '"}\n')
assert grok.discover(ctx_g) == ['u-g1'] assert grok.discover(ctx_g) == ['u-g1']
# 5. OpenCode
opencode = get_adapter('opencode')
ctx_o = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
o_db = opencode.artifact_path('u-o1', ctx_o)
_seed_opencode_session(o_db, 'u-o1', ws, 2000.0)
assert opencode.discover(ctx_o) == ['u-o1']
def test_cli_bridge_subcommands_and_quote_safety(): def test_cli_bridge_subcommands_and_quote_safety():
import subprocess, sys import subprocess, sys
from pathlib import Path from pathlib import Path
@@ -324,7 +475,7 @@ def test_cli_bridge_subcommands_and_quote_safety():
assert res.stdout.strip() == "/bin/claude --dangerously-skip-permissions --session-id uuid-test" assert res.stdout.strip() == "/bin/claude --dangerously-skip-permissions --session-id uuid-test"
# 3. exit-key # 3. exit-key
for agent, expected_key in [('claude', '/exit'), ('agy', 'Exit'), ('hermes', '/exit'), ('grok', '/exit')]: for agent, expected_key in [('claude', '/exit'), ('agy', 'Exit'), ('hermes', '/exit'), ('grok', '/exit'), ('opencode', '/exit')]:
res = subprocess.run([sys.executable, "-m", "lib_py.agents", "exit-key", agent], capture_output=True, text=True, env=env) res = subprocess.run([sys.executable, "-m", "lib_py.agents", "exit-key", agent], capture_output=True, text=True, env=env)
assert res.returncode == 0 assert res.returncode == 0
assert res.stdout.strip() == expected_key assert res.stdout.strip() == expected_key
@@ -336,6 +487,7 @@ def test_delegate_agent_resolution_and_fallback():
'agy': 'antigravity-cli', 'agy': 'antigravity-cli',
'hermes': 'hermes-agent', 'hermes': 'hermes-agent',
'grok': 'grok-build', 'grok': 'grok-build',
'opencode': 'opencode-cli',
} }
# 1. Adapter property # 1. Adapter property
for agent, expected_key in expected_map.items(): for agent, expected_key in expected_map.items():
@@ -354,6 +506,7 @@ def test_delegate_agent_resolution_and_fallback():
hermes) delegate_agent="hermes-agent" ;; hermes) delegate_agent="hermes-agent" ;;
agy) delegate_agent="antigravity-cli" ;; agy) delegate_agent="antigravity-cli" ;;
grok) delegate_agent="grok-build" ;; grok) delegate_agent="grok-build" ;;
opencode) delegate_agent="opencode-cli" ;;
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;; *) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
esac esac
fi fi
@@ -523,3 +676,167 @@ def test_hermes_reconcile_full_block_integration(tmp_path):
assert valid_candidates == ['u-target'] assert valid_candidates == ['u-target']
def test_opencode_input_region_properties():
adapter = get_adapter('opencode')
assert adapter.input_prompt == '❯'
assert adapter.input_placeholder == ''
assert adapter.input_rule_pattern == '─{10,}'
def test_opencode_verify_artifact_created_at_epoch_filtering(tmp_path):
import sqlite3
from lib_py.agents.base import DiscoveryContext
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
adapter = get_adapter('opencode')
ctx_old = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home, epoch=1500)
odb = adapter.artifact_path('u-old', ctx_old)
_seed_opencode_session(odb, 'u-old', ws, 1000.0)
_seed_opencode_session(odb, 'u-new', ws, 2000.0)
assert adapter.verify_artifact('u-old', ctx_old) is False
assert adapter.verify_artifact('u-new', ctx_old) is True
def test_opencode_reconcile_dual_direction_epoch_guards(tmp_path):
import sqlite3
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
from lib_py.verify_session import verify_session_uuid
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
adapter = get_adapter('opencode')
ctx = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
odb = adapter.artifact_path('u-hist', ctx)
_seed_opencode_session(odb, 'u-hist', ws, 1000.0)
_seed_opencode_session(odb, 'u-fresh1', ws, 2000.0)
epoch = 1500
s_row = {'name': 'sess1', 'pane': {'cwd': ws}, 'herdr_session_epoch': epoch}
valid = [u for u in adapter.discover(DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home, epoch=epoch, row=s_row)) if verify_session_uuid(ws, 'opencode', u, s_row, home_dir=home, mode="discover")]
assert valid == ['u-fresh1']
assert len(valid) == 1
_seed_opencode_session(odb, 'u-fresh2', ws, 2100.0)
valid2 = [u for u in adapter.discover(DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home, epoch=epoch, row=s_row)) if verify_session_uuid(ws, 'opencode', u, s_row, home_dir=home, mode="discover")]
assert len(valid2) == 2
def test_opencode_reconcile_full_block_integration(tmp_path):
"""Verify opencode drift-C block behavior with sibling_claimed exclusion."""
import sqlite3
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
from lib_py.verify_session import verify_session_uuid
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
adapter = get_adapter('opencode')
ctx = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
odb = adapter.artifact_path('u-target', ctx)
_seed_opencode_session(odb, 'u-claimed', ws, 2000.0)
_seed_opencode_session(odb, 'u-target', ws, 2001.0)
epoch = 1900
s_eval = {
'name': 'opencode-target-01',
'pane': {'cwd': ws},
'herdr_session_epoch': epoch,
'_sibling_claimed_uuids': ['u-claimed']
}
candidates = adapter.discover(DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home, epoch=epoch, row=s_eval))
valid_candidates = []
for uuid in candidates:
if uuid in s_eval['_sibling_claimed_uuids']:
continue
if verify_session_uuid(ws, 'opencode', uuid, s_eval, home_dir=home, mode="discover"):
valid_candidates.append(uuid)
assert valid_candidates == ['u-target']
def test_opencode_real_schema_directory_and_time_created_ms(tmp_path):
"""Verify opencode adapter behavior with the real live schema (directory, time_created in ms)."""
import sqlite3
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
ws1 = str(tmp_path / "ws1")
ws2 = str(tmp_path / "ws2")
home = str(tmp_path / "home")
os.makedirs(ws1, exist_ok=True)
os.makedirs(ws2, exist_ok=True)
os.makedirs(home, exist_ok=True)
odb = f"{home}/.local/share/opencode/opencode.db"
os.makedirs(os.path.dirname(odb), exist_ok=True)
conn = sqlite3.connect(odb)
conn.execute("CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT NOT NULL, time_created INTEGER NOT NULL)")
epoch_floor_sec = 1724920000
conn.execute("INSERT INTO session VALUES ('u-target-fresh', ?, ?)", (ws1, int((epoch_floor_sec + 100) * 1000)))
conn.execute("INSERT INTO session VALUES ('u-target-old', ?, ?)", (ws1, int((epoch_floor_sec - 100) * 1000)))
conn.execute("INSERT INTO session VALUES ('u-foreign', ?, ?)", (ws2, int((epoch_floor_sec + 200) * 1000)))
conn.commit()
conn.close()
adapter = get_adapter('opencode')
ctx_ws1 = DiscoveryContext(workspace=ws1, agent_name='opencode', home_dir=home, epoch=epoch_floor_sec)
# verify_artifact assertions
assert adapter.verify_artifact('u-target-fresh', ctx_ws1) is True
assert adapter.verify_artifact('u-target-old', ctx_ws1) is False # Stale epoch rejected
assert adapter.verify_artifact('u-foreign', ctx_ws1) is False # Foreign workspace rejected
# discover assertions
discovered = adapter.discover(ctx_ws1)
assert discovered == ['u-target-fresh']
def test_opencode_resolve_db_cli_path(tmp_path, monkeypatch):
"""Verify _resolve_db correctly executes opencode db path and falls back to candidate paths."""
import stat
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
adapter = get_adapter('opencode')
home = str(tmp_path / "home")
ws = str(tmp_path / "ws")
os.makedirs(home, exist_ok=True)
os.makedirs(ws, exist_ok=True)
expected_db = f"{home}/.local/share/opencode/custom_opencode.db"
os.makedirs(os.path.dirname(expected_db), exist_ok=True)
with open(expected_db, "w") as f:
f.write("mock db")
# Create mock opencode binary that outputs custom DB path
bin_dir = tmp_path / "bin"
os.makedirs(bin_dir, exist_ok=True)
mock_bin = bin_dir / "opencode"
mock_bin.write_text(f"""#!/bin/sh
if [ "$1" = "db" ] && [ "$2" = "path" ]; then
echo "{expected_db}"
exit 0
fi
exit 1
""")
mock_bin.chmod(mock_bin.stat().st_mode | stat.S_IEXEC)
monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ.get('PATH', '')}")
ctx = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
assert adapter._resolve_db(ctx) == expected_db
+5 -1
View File
@@ -488,7 +488,7 @@ def test_c10_create_session_timeout_dead_pid_kills_session(mam_sandbox, mock_her
def test_adapter_strong_weak_partition(): def test_adapter_strong_weak_partition():
"""N-D: Verify strong_ready_tokens and weak_ready_tokens partition ready_tokens for all adapters.""" """N-D: Verify strong_ready_tokens and weak_ready_tokens partition ready_tokens for all adapters."""
from lib_py.agents.registry import get_adapter from lib_py.agents.registry import get_adapter
for agent in ('claude', 'agy', 'hermes', 'grok'): for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
adapter = get_adapter(agent) adapter = get_adapter(agent)
assert adapter is not None assert adapter is not None
ready_set = set(t for t in adapter.ready_tokens.split('|') if t) ready_set = set(t for t in adapter.ready_tokens.split('|') if t)
@@ -525,6 +525,8 @@ echo "5: $(check_sks 'test-worker-hermes-02')"
echo "6: $(check_sks 'workspace-creator-agy-05')" echo "6: $(check_sks 'workspace-creator-agy-05')"
echo "7: $(check_sks 'claude')" echo "7: $(check_sks 'claude')"
echo "9: $(check_sks 'grok-02')" echo "9: $(check_sks 'grok-02')"
echo "10: $(check_sks 'creator-opencode-01')"
echo "11: $(check_sks 'opencode')"
""" """
res = _run_lib_helpers(script) res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout assert res.returncode == 0, res.stderr + res.stdout
@@ -534,6 +536,8 @@ echo "9: $(check_sks 'grok-02')"
assert "6: RESOLVED_AGENT=agy" in res.stdout assert "6: RESOLVED_AGENT=agy" in res.stdout
assert "7: RESOLVED_AGENT=claude" in res.stdout assert "7: RESOLVED_AGENT=claude" in res.stdout
assert "9: RESOLVED_AGENT=grok" in res.stdout assert "9: RESOLVED_AGENT=grok" in res.stdout
assert "10: RESOLVED_AGENT=opencode" in res.stdout
assert "11: RESOLVED_AGENT=opencode" in res.stdout
def test_f2_upsell_modal_defense_with_real_facts_and_banner(): def test_f2_upsell_modal_defense_with_real_facts_and_banner():
+68 -3
View File
@@ -960,18 +960,18 @@ def test_grok_shell_and_scripts_integration(mam_sandbox):
lib_path = mam_sandbox / "skills" / "lib.sh" lib_path = mam_sandbox / "skills" / "lib.sh"
lib_content = lib_path.read_text() lib_content = lib_path.read_text()
assert '*-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok"' in lib_content assert '*-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok"' in lib_content
assert "'claude', 'agy', 'hermes', 'grok'" in lib_content assert "'claude', 'agy', 'hermes', 'grok', 'opencode'" in lib_content
assert '[[ "$sess" =~ "grok" ]]' in lib_content assert '[[ "$sess" =~ "grok" ]]' in lib_content
# 2. create_session.sh validation # 2. create_session.sh validation
create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh" create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
create_content = create_path.read_text() create_content = create_path.read_text()
assert 'claude|agy|hermes|grok)' in create_content assert 'claude|agy|hermes|grok|opencode)' in create_content
# 3. stop_session.sh validation & state capture # 3. stop_session.sh validation & state capture
stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh" stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
stop_content = stop_path.read_text() stop_content = stop_path.read_text()
assert 'claude|agy|hermes|grok)' in stop_content assert 'claude|agy|hermes|grok|opencode)' in stop_content
assert "target['grok_session_id_own'] = captured" in stop_content assert "target['grok_session_id_own'] = captured" in stop_content
# 4. workspace_uuid OWN_KEY # 4. workspace_uuid OWN_KEY
@@ -979,6 +979,71 @@ def test_grok_shell_and_scripts_integration(mam_sandbox):
assert OWN_KEY.get('grok') == 'grok_session_id_own' assert OWN_KEY.get('grok') == 'grok_session_id_own'
def test_opencode_shell_and_scripts_integration(mam_sandbox):
"""Verify lib.sh, create_session.sh, stop_session.sh, and workspace_uuid contain opencode."""
import json, subprocess
# 1. lib.sh kind mapping & binaries
lib_path = mam_sandbox / "skills" / "lib.sh"
lib_content = lib_path.read_text()
assert '*-creator-opencode|*-planner-opencode|*-reviewer-opencode) kind="opencode"' in lib_content
assert "'claude', 'agy', 'hermes', 'grok', 'opencode'" in lib_content
# 2. create_session.sh validation & OPENCODE_PERMISSION behavior (unset & preset)
create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
create_content = create_path.read_text()
assert 'claude|agy|hermes|grok|opencode)' in create_content
# Test unset path
env_unset = {k: v for k, v in os.environ.items() if k != "OPENCODE_PERMISSION"}
res_c_unset = subprocess.run(
["bash", "-c", 'if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION=\'{"*":"allow"}\'; fi; echo "$OPENCODE_PERMISSION"'],
env=env_unset,
capture_output=True, text=True, check=True
)
assert json.loads(res_c_unset.stdout.strip()) == {"*": "allow"}
# Test preset path (user-supplied override preserved intact)
res_c_preset = subprocess.run(
["bash", "-c", 'if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION=\'{"*":"allow"}\'; fi; echo "$OPENCODE_PERMISSION"'],
env={**os.environ, "OPENCODE_PERMISSION": '{"*":"deny"}'},
capture_output=True, text=True, check=True
)
assert json.loads(res_c_preset.stdout.strip()) == {"*": "deny"}
# 3. resume_session.sh OPENCODE_PERMISSION behavior (unset & preset)
resume_path = mam_sandbox / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
resume_content = resume_path.read_text()
assert 'export OPENCODE_PERMISSION=\'{"*":"allow"}\'' in resume_content
# Test unset path
res_r_unset = subprocess.run(
["bash", "-c", 'if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION=\'{"*":"allow"}\'; fi; echo "$OPENCODE_PERMISSION"'],
env=env_unset,
capture_output=True, text=True, check=True
)
assert json.loads(res_r_unset.stdout.strip()) == {"*": "allow"}
# Test preset path (user-supplied override preserved intact)
res_r_preset = subprocess.run(
["bash", "-c", 'if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION=\'{"*":"allow"}\'; fi; echo "$OPENCODE_PERMISSION"'],
env={**os.environ, "OPENCODE_PERMISSION": '{"*":"deny"}'},
capture_output=True, text=True, check=True
)
assert json.loads(res_r_preset.stdout.strip()) == {"*": "deny"}
# 4. stop_session.sh validation & state capture
stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
stop_content = stop_path.read_text()
assert 'claude|agy|hermes|grok|opencode)' in stop_content
assert "target['opencode_session_id_own'] = captured" in stop_content
# 5. workspace_uuid OWN_KEY
from lib_py.workspace_uuid import OWN_KEY
assert OWN_KEY.get('opencode') == 'opencode_session_id_own'
# 6. reconcile.sh RECON_SRC imports shutil and get_adapter
recon_path = mam_sandbox / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
recon_content = recon_path.read_text()
assert 'import os, json, glob, subprocess, time, sqlite3, re, shutil' in recon_content
assert 'op_adapter = get_adapter(\'opencode\')' in recon_content
+51
View File
@@ -1225,3 +1225,54 @@ def test_comp_reconcile_drift_b_populates_workspace_and_server(mam_sandbox, mock
assert s["herdr_workspace"] != "" assert s["herdr_workspace"] != ""
assert s["herdr_workspace"] != "-" assert s["herdr_workspace"] != "-"
def test_comp_opencode_create_and_reconcile_and_stop(mam_sandbox, mock_herdr, mock_agents):
"""Verify create_session -> reconcile drift-C materialization -> stop flow for opencode."""
create_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
res = subprocess.run([
"bash", str(create_script),
"--workspace", str(mam_sandbox),
"--agent", "opencode",
"--role", "creator"
], capture_output=True, text=True, cwd=str(mam_sandbox))
assert res.returncode == 0, res.stderr
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
import yaml
with open(yaml_path) as f:
data = yaml.safe_load(f)
sessions = data.get("herdr_sessions", [])
assert len(sessions) == 1
s = sessions[0]
session_name = s["name"]
assert s["status"] == "running"
assert s["opencode_session_id_own"] is None
assert s["session_id_source"] == "pending-discovery"
# Run reconcile.sh to trigger drift-C materialization
reconcile_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
res_rec = subprocess.run(["bash", str(reconcile_script), "--once", "--emit-diff"], capture_output=True, text=True, cwd=str(mam_sandbox), env={**os.environ, "WORKSPACE_ROOT": str(mam_sandbox)})
assert res_rec.returncode == 0, res_rec.stderr
with open(yaml_path) as f:
data_after = yaml.safe_load(f)
s_after = [x for x in data_after.get("herdr_sessions", []) if x.get("name") == session_name][0]
assert s_after.get("opencode_session_id_own") is not None
assert s_after.get("last_visible_status") in ("pinned", "resume_verified")
# Stop session
stop_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
res_stop = subprocess.run([
"bash", str(stop_script),
"--session", session_name,
"--agent", "opencode"
], capture_output=True, text=True, cwd=str(mam_sandbox))
assert res_stop.returncode == 0, res_stop.stderr
with open(yaml_path) as f:
data_stopped = yaml.safe_load(f)
s_stopped = [x for x in data_stopped.get("herdr_sessions", []) if x.get("name") == session_name][0]
assert s_stopped["status"] == "stopped"
assert s_stopped["resumable"] is True