feat(agent): add full codex agent support (v4.2.0)

- Implement CodexAdapter in lib_py/agents/adapters/codex.py with safe flags and TUI readiness
- Extend create_session, resume_session, stop_session, and reconcile for codex CLI
- Support codex agent delegation and loop integration across all multi-agent skills
- Add unit, component, and contract test suites for codex integration
- Verify all 462 tests passing cleanly
This commit is contained in:
Godopu committed 2026-09-05 23:02:29 +09:00
1 parent de0a9c2e81
commit 76f10b2096
45 files changed
+2045 -276

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@
- **모호성 제거**: 사용자의 요구사항에 모호한 부분이 있다면 작업을 추측하여 진행하지 말고, 즉시 사용자에게 질문하여 명확히 해야 합니다 (`/grill-me` 슬래시 명령어 권장).
### 👥 Team Leaders (팀장)
새롭게 생성되는 에이전트(`antigravity`, `claude`, `hermes`, `grok`, `opencode` 등)는 각 팀의 **팀장** 역할을 수행합니다. 총괄 매니저로부터 작업을 위임받아 개발 또는 리뷰 워크플로우를 주도합니다.
새롭게 생성되는 에이전트(`antigravity`, `claude`, `hermes`, `grok`, `opencode`, `codex` 등)는 각 팀의 **팀장** 역할을 수행합니다. 총괄 매니저로부터 작업을 위임받아 개발 또는 리뷰 워크플로우를 주도합니다.
- **Developer Team Leader (개발 팀장)**:
- 총괄 매니저로부터 작업을 위임받습니다.
- **작업 분석 및 계획**: 주어진 작업을 철저히 분석하고, 작은 단위로 문제를 나누어 세부 계획을 수립합니다.
+1 -1
View File
@@ -18,7 +18,7 @@ We clearly separate responsibilities and permissions between roles to reduce bot
- **Ambiguity Resolution**: If a user's requirements contain ambiguous details, do not guess. Immediately ask the user for clarification (we recommend using the `/grill-me` slash command).
### 👥 Team Leaders (팀장)
Newly spawned agents (e.g., `antigravity`, `claude`, `hermes`, `grok`, `opencode`) act as **Team Leaders** of their respective groups. They receive delegated tasks from the General Manager and manage implementation or review workflows.
Newly spawned agents (e.g., `antigravity`, `claude`, `hermes`, `grok`, `opencode`, `codex`) act as **Team Leaders** of their respective groups. They receive delegated tasks from the General Manager and manage implementation or review workflows.
- **Developer Team Leader (개발 팀장)**:
- Receives tasks from the General Manager.
- **Task Breakdown & Planning**: Thoroughly analyzes the task, breaks it down into small units, and creates a plan.
+370 -165
View File
@@ -1,209 +1,391 @@
# 🔌 Implementation Plan: OpenCode (`anomalyco/opencode`) Agent Adapter for MAM (Rev.3)
# 🔌 Implementation Plan: Codex CLI (`openai/codex`) Agent Adapter for MAM (Rev.2)
- **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`)
- **Author**: `planner-reviewer-claude-01` (Planner), refined in response to `creator-agy-01`'s architectural challenge
- **Job**: `e30e3123` → Rev.2 (`94a6db78`)
- **Status**: Implemented (v4.2.0) — **full code, tests, and version lockstep present in repo**.
- **Scope**: Add `codex` as a 6th first-class MAM agent backend (alongside `claude`, `agy`, `hermes`, `grok`, `opencode`), per `docs/NEW_AGENT_INTEGRATION_GUIDE.md`'s 5-step procedure.
- **Durable Path**: `.agents/reports/planner-reviewer-claude-01/plan-e30e3123.md` (mirrored at `.agents/reports/implementation_plan.md`).
- **Method**: Every CLI fact is verified against a **live, authenticated Codex CLI install already present on this machine** (`codex-cli 0.153.4`). Rev.2 additionally re-verified both of `creator-agy-01`'s challenge points live before accepting them — see §0.
---
## Rev.3 Changelog (Job `78f332a9`)
## 0. Rev.2 Changelog — Response to `creator-agy-01`'s Architectural Challenge (Job `004c224d`)
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**:
`creator-agy-01` filed a formal challenge against Rev.1 with **1 critical defect + 1 supporting architectural constraint**. Per this session's standing discipline, neither was accepted on the challenger's word alone — both were independently re-verified live on this machine before being incorporated. **Both are accepted — no `[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.
| # | Challenge | My independent verification | Disposition |
|---|---|---|---|
| 1 | **Critical**: Rev.1's `orc_onboard.sh` detection regex (`resume[[:space:]]+[0-9a-f-]{36}`) cannot match the actual command Rev.1 itself specifies (`codex resume --dangerously-bypass-approvals-and-sandbox <uuid>`), because the flag sits between `resume` and the UUID | Ran the exact `cmd_line` from the challenge through Rev.1's regex myself: `echo "$cmd_line" \| grep -oE 'resume[[:space:]]+[0-9a-f-]{36}'` → **empty, exit 1**. Confirmed genuine — Rev.1 contained a self-contradiction between §2 (`resume_spec`) and §1.9/§3 item 27 (the detection regex). Then tested the challenge's proposed Plan A regex on the same input → **correctly extracts the UUID, exit 0**. | **Accepted.** §1.9 and wiring item #27 rewritten to the flag-tolerant regex (verified working, not just proposed) |
| 2 | **Supporting**: `~/.codex/state_N.sqlite` (a `threads` table indexing `id`/`cwd`/`rollout_path`) exists alongside the rollout JSONL files; Rev.1's "single-file format, no DB rows to delete" claim was only half true, and `purge_artifacts()` deleting only the `.jsonl` would leave an orphan DB row | `ls ~/.codex/*.sqlite*` on this machine → confirmed `state_5.sqlite` exists. `sqlite3 ~/.codex/state_5.sqlite ".schema threads"` → confirmed the exact `threads` table the challenge quoted. `sqlite3 ... "SELECT id,cwd,rollout_path FROM threads"` → confirmed 2 real rows exactly matching the 2 real rollout files found earlier. `strings` re-extraction confirmed the `stale_db_path`/`state db discrepancy`/`reconcile_rollout`/`read_repair_rollout_path: upsert_needed` literals the challenge cited. `codex delete --help` → confirmed `codex delete <SESSION> --force` ("Delete without prompting. SESSION must be a UUID") is real, exactly as the challenge's remediation option 1 proposed. | **Accepted**, with one refinement found during my own re-verification (not in the original challenge): `codex debug --help` has **no** `db path`-style introspection subcommand (unlike OpenCode's `opencode db path`), and the state file's numeric suffix (`state_5`) is unverified as stable across Codex versions/installs — hardcoding `state_5.sqlite` the way the challenge's raw example does would itself be fragile. §2's adapter design now resolves the state DB via a `state_*.sqlite` glob (most-recently-modified), and `purge_artifacts()` now shells out to `codex delete <uuid> --force` as the primary mechanism (letting Codex's own reconciliation logic keep the JSONL and DB consistent) with a manual-file-removal fallback only if that call fails |
Net effect: Rev.1's overall architecture and the 46-point wiring table stand — this challenge caught one genuine self-contradiction (the regex) and one genuine completeness gap (the state DB), both now closed. No other part of Rev.1 was found wrong during this re-verification pass.
---
## Rev.2 Changelog (Job `c65b0081`)
## 1. Verified Codex CLI/Runtime Facts
`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.
Source: live `codex --help` / `codex resume --help` / `codex exec --help` / `codex login --help` / `codex delete --help` / `codex debug --help` / `codex doctor --summary` / `codex login status` output, direct `ls`/`find` of `~/.codex/`, live `sqlite3` schema/row inspection of `~/.codex/state_5.sqlite`, and offline `strings` extraction of the installed binary — all run on this machine.
### 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).
- Binary: `codex` (this machine: `/Users/godopu16/.local/bin/codex`, version `codex-cli 0.153.4`).
- Config/state root: `~/.codex/` by default, **overridable via the `CODEX_HOME` environment variable** (confirmed: `codex exec --ignore-user-config` help text explicitly says "auth still uses `CODEX_HOME`").
### 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.
- `codex [OPTIONS] [PROMPT]` — launches the **interactive TUI** (default, what MAM spawns).
- `codex exec [OPTIONS] [PROMPT]` (alias `e`) — **non-interactive** one-shot execution with `--json` JSONL event streaming and `-o/--output-last-message <FILE>`. Not used by MAM's spawn/resume flow, but confirms Codex has a scriptable mode.
- `codex resume [OPTIONS] [SESSION_ID] [PROMPT]` — resumes a specific session by **UUID or session name, passed positionally**; `--last` picks the most recent session without a picker.
- `codex delete <SESSION> --force` — permanently deletes a saved session by UUID; `--force` skips the confirmation prompt and requires `SESSION` to be a UUID (not a name). Verified via `codex delete --help`.
### 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"}'`) |
| Flag / Variable | Purpose | Verified via |
|---|---|---|
| `--dangerously-bypass-approvals-and-sandbox` | Skip all confirmation prompts and run without sandboxing — the unattended-automation flag | `codex --help`, `codex resume --help` (accepted on both) |
| `-s, --sandbox <read-only\|workspace-write\|danger-full-access>` / `-a, --ask-for-approval <on-request\|never>` | Finer-grained alternative to the single bypass flag above | `codex --help` |
| `--approve-for-me` | Routes approvals through automatic review under `workspace-write` — **not** a full bypass | `codex --help` |
| `-C, --cd <DIR>` | Working-root directory for the agent | `codex --help` |
| `--session-id <uuid>` | **Does not exist.** No such flag appears anywhere in `codex --help`'s full option list | Absence confirmed by reading the complete `--help` output |
| `CODEX_HOME` | Overrides `~/.codex` config/session root | `codex exec --ignore-user-config` help text |
| `OPENAI_API_KEY` | Read by `codex login --with-api-key` (via stdin); also stored inside `auth.json` under the `OPENAI_API_KEY` key once logged in | `codex login --help`, `~/.codex/auth.json` key inspection |
**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 Unattended-Execution Decision
Confirmed present. `--dangerously-bypass-approvals-and-sandbox` is accepted by both the top-level `codex` TUI launcher and `codex resume`, so `spawn_spec()`/`resume_spec()` can pass it unconditionally.
### 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 — Dual Layer (Rev.2: corrected)
- **Primary transcript**: JSONL "rollout" files, one per session, at `~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<timestamp>-<uuid>.jsonl` — e.g. `~/.codex/sessions/2026/05/23/rollout-2026-05-23T08-23-56-019e5200-ab6a-70f2-b5da-e1ef403f7a40.jsonl`. First line is a `session_meta` record: `{"payload": {"id": "<uuid>", "cwd": "<path>", ...}}`.
- **Secondary index (Rev.2, per the accepted challenge)**: `~/.codex/state_N.sqlite` (this machine: `state_5.sqlite`; **the numeric suffix is not assumed stable** — no `codex debug`/`codex` subcommand introspects its exact filename, unlike OpenCode's `opencode db path`, so the adapter must resolve it via a `state_*.sqlite` glob rather than hardcoding the suffix). Verified live schema: a `threads` table with `id TEXT PRIMARY KEY`, `rollout_path TEXT`, `cwd TEXT`, `created_at`/`updated_at` (plus many version-added trailing columns — the schema has clearly evolved via in-place `ALTER TABLE` across releases, another reason not to hand-roll fragile column-position assumptions). Verified live rows: both real sessions on this machine are indexed here with `id`/`cwd`/`rollout_path` exactly matching their on-disk `.jsonl` files.
- **Codex's own resolution order** (inferred from binary string literals `state db discrepancy during find_thread_path_by_id_str_in_subdir: stale_db_path / mismatched_db_path / falling_back` and `reconcile_rollout` / `read_repair_rollout_path: upsert_needed`): Codex itself queries the state DB first, and has a **self-healing reconciliation path** for when the DB and filesystem disagree (falls back to a filesystem scan, then repairs the DB entry) — this is Codex's own built-in drift-correction, structurally analogous to MAM's own `reconcile.sh`.
- **Architectural consequence for MAM's adapter**: mirror Codex's own resolution order — **query `state_*.sqlite` first** (fast, indexed, avoids any unbounded directory walk), **fall back to the bounded glob** over `sessions/*/*/*/rollout-*.jsonl` if the state DB is missing, unreadable, or the row is stale/absent (mirroring the exact "primary DB, fallback filesystem walk" shape `opencode.py`'s `_resolve_db()` already uses in this codebase, and mirroring what Codex's own binary does internally).
### 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 TUI Readiness (candidate, pending empirical capture)
Offline `strings` extraction surfaced real UI literals: `"Ask Codex to do anything"` (composer placeholder) and the banner token `"Codex"`. No `❯` glyph or `─{10,}` rule literal found — different TUI layout family (boxed/panelled, not readline-style). Proposed candidate:
- `ready_tokens`: `'Codex|Ask Codex to do anything'`
- `input_placeholder`: `'Ask Codex to do anything'` (high-confidence — extracted literal)
- `input_prompt` / `input_rule_pattern`: `None` (base-class default) pending live pane capture.
### 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.
**Hard gate before merge**: capture a real `codex` TUI pane via `herdr pane capture` to confirm exact rendered text.
### 1.7 Exit Key
- Slash command `/exit` (aliases `/quit`, `/q`).
### 1.7 Exit Convention (verified deviation from every existing adapter)
Binary string table twice describes exiting as a keypress: `"Ctrl+C to exit"`, `"ctrl+c quit"`. No `/exit`/`/quit`/`:q` literal found (real slash commands that do exist: `/keymap`, `/side`, `/review`, `/ide`, `/goal`).
`MAM_EXIT_KEY` is exported by the `facts` bridge and loaded into `send_keys_safe()`'s local scope (`lib.sh:2166`), but **no skill script actually sends it to a pane today, for any agent** — `stop_session.sh` never references `exit_key`/`MAM_EXIT_KEY`. So this remains a required-but-currently-inert contract field. Propose `exit_key = 'Ctrl+C'` (symbolic key name, matching herdr's existing `Escape` send convention), documented as a deliberate divergence.
### 1.8 Authentication
- Interactive: `codex login` (ChatGPT OAuth) or `codex login --with-api-key` (reads `OPENAI_API_KEY` from stdin).
- Credential file: `~/.codex/auth.json` (or `$CODEX_HOME/auth.json`) — verified present, keys `auth_mode`, `OPENAI_API_KEY`, `tokens`, `last_refresh`. `codex login status` confirms `"Logged in using ChatGPT"`; `codex doctor --summary` independently confirms `✓ auth is configured`.
- `auth_ok()`: `os.path.exists(f"{codex_home}/auth.json")` **or** `os.environ.get('OPENAI_API_KEY')`. **Caveat honestly flagged**: whether a bare env var alone (without ever running `codex login --with-api-key`) is sufficient was not empirically tested (this machine is logged in via ChatGPT, not a raw API key).
### 1.9 Session-ID Detection for `orc_onboard.sh` (Rev.2: regex defect fixed)
1. **argv form**: Codex passes the UUID as `codex resume <uuid>` — positional, no flag name. **Rev.1's regex was defective**: `grep -oE 'resume[[:space:]]+[0-9a-f-]{36}'` requires the UUID immediately after `resume`, but the real command line this plan's own `resume_spec`/`CMD_FULL` produce is `codex resume --dangerously-bypass-approvals-and-sandbox <uuid>` — a flag sits in between, so the regex never matches (verified live: exit 1, empty output). **Corrected regex** (challenge's Plan A, live-verified to work): allow arbitrary tokens between `resume` and the UUID, then extract only the UUID:
```bash
codex)
detected_id=$(echo "$cmd_line" | grep -oE 'resume[[:space:]]+.*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | grep -oE '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | head -n 1 || true)
;;
```
Already safe under `orc_onboard.sh:135`'s existing `is_valid_id "$detected_id"` UUID-format re-validation, so a slightly looser extraction regex carries no correctness risk.
2. **env var fallback**: no `CODEX_SESSION_ID`-style environment variable found anywhere in `codex --help`. Leave `env_var=""` for `codex` — do not fabricate one.
---
## 2. Adapter Contract Decisions (`BaseAgentAdapter`)
## 2. Adapter Contract Decisions (`BaseAgentAdapter`) — Rev.2
| 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` |
| `name` | `'codex'` | Lowercase identifier |
| `own_key` | `'codex_session_id_own'` | Matches `<agent>_session_id_own` convention |
| `ready_tokens` | `'Codex\|Ask Codex to do anything'` (candidate, §1.6) | Extracted real UI literal, pending live pane-capture confirmation |
| `exit_key` | `'Ctrl+C'` (§1.7) | No typed slash command exists; currently operationally inert framework-wide |
| `delegate_agent_key` | `'codex-cli'` | Matches the `<name>-cli`/`-agent`/`-build`/`-code` suffix convention |
| `identity_cache_fields` | `('session_id',)` | Single-field cache, modern pattern |
| `input_placeholder` | `'Ask Codex to do anything'` | Verified real literal |
| `input_prompt` / `input_rule_pattern` | `None` | No stable glyph found; different TUI layout family |
| `spawn_spec(binary, session_uuid, use_wrapper)` | `f"{binary} --dangerously-bypass-approvals-and-sandbox"` (ignores `session_uuid` — Class A) | §1.3/§1.4 |
| `resume_spec(binary, session_uuid, materialized)` | if `materialized and session_uuid`: `f"{binary} resume --dangerously-bypass-approvals-and-sandbox {session_uuid}"`, else same as `spawn_spec` | Verified `codex resume [OPTIONS] [SESSION_ID]` usage template |
| `auth_ok(run_cmd)` | `os.path.exists(f"{codex_home}/auth.json") or bool(os.environ.get('OPENAI_API_KEY'))` | §1.8, sandbox-safe via `ctx.home_dir` |
| `artifact_path(uuid, ctx)` | **Rev.2**: query `state_*.sqlite`'s `threads.rollout_path WHERE id=?` first; fall back to the bounded glob `sessions/*/*/*/rollout-*-{uuid}.jsonl` if the DB lookup misses | §1.5 — mirrors Codex's own DB-first, filesystem-fallback resolution order |
| `verify_artifact(uuid, ctx)` | Resolve path via the same DB-first/glob-fallback helper; parse first JSONL line's `payload.id`/`payload.cwd` (or use the DB row's `cwd` column directly, skipping the file read, when the DB hit succeeds); apply `ctx.epoch` mtime check | Rev.2: DB row already has `cwd` — avoids opening the file at all on the common (DB-hit) path |
| `purge_artifacts(uuid, ctx)` | **Rev.2**: primary — `subprocess.run([binary_or_'codex', 'delete', uuid, '--force'], ...)`; fallback (only if that call fails/binary unavailable) — manually `os.remove()` the rollout file located via the DB-first/glob-fallback helper, **and** best-effort `DELETE FROM threads WHERE id=?` against the resolved `state_*.sqlite` if it exists | §0 item 2 — avoids orphan DB rows; lets Codex's own reconciliation logic own consistency whenever possible instead of MAM hand-rolling SQL against an evolving schema |
| `discover(ctx)` | **Rev.2**: query `state_*.sqlite`'s `threads` table (`SELECT id, cwd, updated_at FROM threads ORDER BY updated_at DESC LIMIT 50`, filtered by `cwd`/`epoch` in Python) first; fall back to the recursive-glob approach from Rev.1 only if the DB is absent/unreadable | Rev.2 — DB query is a single indexed lookup vs. an N-file glob-and-open walk; also naturally bounded (no unbounded-growth concern for the DB path) |
### Adapter code sketch (`.agents/skills/lib_py/agents/adapters/codex.py`) — Rev.2
```python
import os, json, glob, sqlite3, subprocess
from typing import Optional, Any, List
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext
from lib_py.verify_session import workspace_key
class CodexAgentAdapter(BaseAgentAdapter):
@property
def name(self) -> str:
return 'codex'
@property
def own_key(self) -> str:
return 'codex_session_id_own'
@property
def ready_tokens(self) -> str:
return 'Codex|Ask Codex to do anything'
@property
def exit_key(self) -> str:
return 'Ctrl+C'
@property
def delegate_agent_key(self) -> str:
return 'codex-cli'
@property
def identity_cache_fields(self) -> tuple:
return ('session_id',)
@property
def input_placeholder(self) -> Optional[str]:
return 'Ask Codex to do anything'
def _codex_home(self, ctx: DiscoveryContext) -> str:
return os.environ.get('CODEX_HOME') or f"{ctx.home_dir}/.codex"
def _state_db(self, ctx: DiscoveryContext) -> Optional[str]:
# Rev.2: no 'codex db path'-equivalent subcommand exists; the numeric
# suffix (state_5.sqlite on this machine) is unverified as stable
# across versions, so resolve dynamically rather than hardcoding it.
matches = glob.glob(f"{self._codex_home(ctx)}/state_*.sqlite")
if not matches:
return None
return max(matches, key=os.path.getmtime)
def _find_rollout_via_db(self, uuid: str, ctx: DiscoveryContext):
"""Returns (rollout_path, cwd) from the state DB, or (None, None)."""
db = self._state_db(ctx)
if not db or not os.path.exists(db):
return None, None
try:
conn = sqlite3.connect(db)
row = conn.execute("SELECT rollout_path, cwd FROM threads WHERE id=?", (uuid,)).fetchone()
conn.close()
if row and row[0] and os.path.exists(row[0]):
return row[0], row[1]
except Exception:
pass
return None, None
def _find_rollout(self, uuid: str, ctx: DiscoveryContext) -> Optional[str]:
path, _ = self._find_rollout_via_db(uuid, ctx)
if path:
return path
# Fallback: bounded glob over the date-nested tree (state DB missing/stale)
matches = glob.glob(f"{self._codex_home(ctx)}/sessions/*/*/*/rollout-*-{uuid}.jsonl")
return matches[0] if matches else None
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
return self._find_rollout(uuid, ctx) or f"{self._codex_home(ctx)}/sessions/rollout-{uuid}.jsonl"
def verify_artifact(self, uuid: str, ctx: DiscoveryContext) -> bool:
db_path, db_cwd = self._find_rollout_via_db(uuid, ctx)
path = db_path or self._find_rollout(uuid, ctx)
if not path or not os.path.exists(path):
return False
if ctx.epoch and os.path.getmtime(path) < ctx.epoch:
return False
found_cwd = db_cwd
if not found_cwd:
try:
with open(path) as f:
first = f.readline()
found_cwd = json.loads(first).get('payload', {}).get('cwd')
except Exception:
return False
if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd):
return False
return True
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> List[str]:
purged = []
path = self._find_rollout(uuid, ctx)
# Rev.2: prefer the CLI's own delete path (keeps rollout file + state
# DB row consistent via Codex's own reconciliation logic).
try:
res = subprocess.run(['codex', 'delete', uuid, '--force'],
capture_output=True, text=True, timeout=5)
if res.returncode == 0:
if path:
purged.append(path)
return purged
except Exception:
pass
# Fallback: manual removal of the file, plus a best-effort DB row delete.
if path and os.path.exists(path):
os.remove(path)
purged.append(path)
db = self._state_db(ctx)
if db and os.path.exists(db):
try:
conn = sqlite3.connect(db)
conn.execute("DELETE FROM threads WHERE id=?", (uuid,))
conn.commit()
conn.close()
purged.append(f"state db row for thread: {uuid}")
except Exception:
pass
return purged
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
return f"{binary} --dangerously-bypass-approvals-and-sandbox"
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
if materialized and session_uuid:
return f"{binary} resume --dangerously-bypass-approvals-and-sandbox {session_uuid}"
return f"{binary} --dangerously-bypass-approvals-and-sandbox"
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
home = os.environ.get('HOME_DIR') or os.environ.get('HOME') or os.path.expanduser('~')
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
return os.path.exists(f"{codex_home}/auth.json") or bool(os.environ.get('OPENAI_API_KEY'))
def discover(self, ctx: DiscoveryContext) -> List[str]:
db = self._state_db(ctx)
if db and os.path.exists(db):
try:
conn = sqlite3.connect(db)
rows = conn.execute(
"SELECT id, cwd, updated_at FROM threads ORDER BY updated_at DESC LIMIT 50"
).fetchall()
conn.close()
candidates = []
for uuid, cwd, _ in rows:
if ctx.workspace and cwd and workspace_key(cwd) != workspace_key(ctx.workspace):
continue
if uuid and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
return candidates
except Exception:
pass
# Fallback: recursive glob (state DB absent/unreadable)
root = self._codex_home(ctx)
candidates = []
paths = sorted(glob.glob(f"{root}/sessions/*/*/*/rollout-*.jsonl"), key=os.path.getmtime, reverse=True)[:50]
for path in paths:
uuid = os.path.basename(path)[:-6][-36:]
if uuid and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
return candidates
```
---
## 3. Complete 29-Point Wiring & Lockstep Table
## 3. Full Lockstep Wiring Table (Rev.2)
All 29 locations below must be updated in lockstep:
Same 46-item table as Rev.1, with item #27 corrected per §0/§1.9. Re-derived from the current, live state of every file that mentions `opencode` today (33 files, `.agents/reports/` excluded).
| # | 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 |
| 1 | `.agents/skills/lib_py/agents/adapters/codex.py` | Create new adapter class `CodexAgentAdapter` per §2 (Rev.2: DB-first artifact resolution + `codex delete --force`-based purge) |
| 2 | `.agents/skills/lib_py/agents/registry.py:9,16` | Import `CodexAgentAdapter`; add `'codex': CodexAgentAdapter()` to `_ADAPTERS` |
| 3 | `.agents/skills/lib_py/atomic_yaml.py:132` | Add `'codex_session_id_own'` to `running_keys` |
| 4 | `.agents/skills/lib_py/verify_session.py:69` | Add `'codex_session_id_own'` to the `mam_row_own_uuid()` field list |
| 5 | `.agents/skills/lib_py/workspace_uuid.py:7-12,34` | Add `'codex': 'codex_session_id_own'` to `OWN_KEY`; add `'codex_session_id_own'` to the `running_ids` field list |
| 6 | `.agents/skills/lib.sh:669` (kind mapping) | Add `*-creator-codex\|*-planner-codex\|*-reviewer-codex) kind="codex" ;;` |
| 7 | `.agents/skills/lib.sh:675,680` (fallback grep chain, name + `final_cmd`) | Add `elif echo "$name" \| grep -qi "codex"; then kind="codex"` and the `final_cmd` equivalent |
| 8 | `.agents/skills/lib.sh:694` (binary dedup tuple) | Add `'codex'` to `('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex')` |
| 9 | `.agents/skills/lib.sh:2175` (`send_keys_safe` agent resolution) | Add `*-codex\|*-codex-[0-9]*\|codex\|codex-[0-9]*) _sks_agent="codex" ;;` |
| 10 | `create_session.sh:4,29,33` (usage/help text) | Add `codex` to the `<claude\|agy\|hermes\|grok\|opencode>` listings |
| 11 | `create_session.sh:93-94` (`--agent` whitelist) | Add `codex` to the accepted-values case and the error message |
| 12 | `create_session.sh` (auth preflight, new `elif` block after the existing `opencode` one at line 129) | `elif [ "$AGENT" = "codex" ]; then` check `~/.codex/auth.json` (respecting `CODEX_HOME`) or `OPENAI_API_KEY`, else `codex --version` smoke check |
| 13 | `create_session.sh:192` (fallback `CMD_FULL` case) | Add `codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;` |
| 14 | `create_session.sh:216` (`spawn()` dispatch case) | Add `codex` to the `agy\|hermes\|grok\|opencode)` case — no special env-var export needed |
| 15 | `create_session.sh:330` (delegate-agent key resolution) | Add `codex) delegate_agent="codex-cli" ;;` |
| 16 | `create_session.sh:438` (YAML init Python block) | Add `elif agent == 'codex': entry['codex_session_id_own'] = None; entry['session_id_source'] = 'pending-discovery'; entry['session_id_verified'] = False; entry['last_visible_status'] = "unverified"` |
| 17 | `resolve_session_id.sh:4,16,39-40` | Add `codex` to usage text and the `--agent` whitelist case |
| 18 | `resume_session.sh:12,45` | Add `codex` to usage text and the `--agent` whitelist case |
| 19 | `resume_session.sh:65` (FRESH_SPAWN Class-A case) | Add `codex` to `agy\|hermes\|opencode) FRESH_SPAWN=1 ;;` — Codex is Class A |
| 20 | `resume_session.sh:121` (fallback spawn-spec case) | Add `codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;` |
| 21 | `resume_session.sh:132` (fallback resume-spec case) | Add `codex) CMD_FULL="${RESOLVED_BIN} resume --dangerously-bypass-approvals-and-sandbox $UUID" ;;` |
| 22 | `update_yaml_resumed.sh:7,14,17` | Add `codex` to usage docs and the Class-A comment listing |
| 23 | `update_yaml_resumed.sh:228` (Python block) | Add `elif agent == 'codex': target['pane']['cmd'] = 'codex'`; if `uuid`: `cmd_full = f'codex resume --dangerously-bypass-approvals-and-sandbox {uuid}'`, `target['codex_session_id_own'] = uuid`; else `cmd_full = os.environ.get('CMD_FULL_OPT', '') or 'codex --dangerously-bypass-approvals-and-sandbox'` |
| 24 | `stop_session.sh:4,15,94-95` | Add `codex` to usage docs and the `--agent` whitelist case + error message |
| 25 | `stop_session.sh:290` (Python capture block) | Add `elif agent == 'codex': target['codex_session_id_own'] = captured` |
| 26 | `orc_onboard.sh:106` (agent-family match) | Add `codex` to the `claude\|agy\|hermes\|grok\|opencode)` case |
| 27 | `orc_onboard.sh:117-133` (CLI-argv detection) | **Rev.2 corrected**: new `codex)` branch using the flag-tolerant regex from §1.9 (`resume[[:space:]]+.*<uuid-pattern>` then extract just the UUID) — **not** the Rev.1 regex, which is confirmed broken against this plan's own `resume_spec` output |
| 28 | `orc_onboard.sh:142-148` (env-var fallback) | Add `codex) env_var=""` explicitly (no known env var — do not fabricate one) |
| 29 | `reconcile.sh:523,542` (agent-name tuples) | Add `'codex'` to both `('claude', 'agy', 'hermes', 'grok', 'opencode')` tuples |
| 30 | `reconcile.sh:603` (fresh-row entry init) | Add `elif agent == 'codex': entry['codex_session_id_own'] = None` |
| 31 | `reconcile.sh:616` (`OWN_KEY_BY_AGENT`) | Add `'codex'` to the tuple |
| 32 | `reconcile.sh` (new dedicated block, after the existing opencode block at line ~787-897) | New `# === drift C (codex) ===` block, **Rev.2**: state-DB-first with glob fallback (see §4) |
| 33 | 8× `SKILL.md` supported-agent lists (`multi-agent-mux-create:132,163-166`; `multi-agent-mux-orc-onboard:11,35,41`; `multi-agent-mux-resume:52,55,61,73,99,110`; `multi-agent-mux-stop:40`) | Add `codex` everywhere `opencode` currently appears in these four files; the other 4 `SKILL.md` files need only the version-frontmatter bump (§6) |
| 34 | `.agents/skills/multi-agent-mux-delegate-job/job-protocol.md:3`, `README.md:3` | No change needed — `codex` already listed informally |
| 35 | `tests/conftest.py` | Add a `codex` mock binary fixture (mirroring `opencode_bin`, line ~972-990) that writes a fake `rollout-<ts>-<uuid>.jsonl` under a temp `$CODEX_HOME/sessions/<Y>/<M>/<D>/` **and** a matching `state_1.sqlite` `threads` row (Rev.2: needed since `discover()`/`verify_artifact()` now query the DB first); add ready-banner/own-key dict entries (lines ~553, ~584) |
| 36 | `tests/test_a4_adapter_contract.py` | Add `codex` to `EXPECTED_OWN_KEYS`, the property tuple, `expected_modal` (`None`), the `facts_bridge` loop (line ~112), a `_seed_codex_session()` helper (mirroring `_seed_opencode_session`, line ~132, **Rev.2**: seeding both the `.jsonl` and the `state_*.sqlite` row), and unit tests for `spawn_spec`/`resume_spec`/`auth_ok`/`purge_artifacts` (**Rev.2**: including a test that `purge_artifacts` falls back to manual removal when the `codex delete` subprocess call fails)/`discover` |
| 37 | `tests/test_c1_tui_readiness.py` | Add `codex` to the facts-bridge and `send_keys_safe` agent-resolution loops (line ~491) and a `check_sks` assertion pair (mirroring lines 528-529, 539-540) |
| 38 | `tests/test_tier1_unit.py` | Update the agent-tuple assertions (lines ~963, 969, 974, 988-989, 994, 1033) to include `codex`; add `test_codex_shell_and_scripts_integration` mirroring `test_opencode_shell_and_scripts_integration` (lines 982-1044); **Rev.2**: add a dedicated regression test asserting `orc_onboard.sh`'s `codex` regex correctly extracts the UUID from `codex resume --dangerously-bypass-approvals-and-sandbox <uuid>` — this is the exact scenario Rev.1 got wrong, so it must be locked down by a test, not just fixed in prose |
| 39 | `tests/test_tier2_component.py` | Add `test_comp_codex_create_and_reconcile_and_stop` mirroring `test_comp_opencode_create_and_reconcile_and_stop` (line ~1229) |
| 40 | `.mam.env.example` (after the existing OpenCode block, line ~110) | Document `OPENAI_API_KEY` and `CODEX_HOME` as an optional override |
| 41 | `docs/NEW_AGENT_INTEGRATION_GUIDE.md` §3 (difficulty matrix) | Update the `Codex CLI` row from the old guessed values to the verified ones from §1 |
| 42 | `.agents/reports/new_agent_types_roadmap.md` §4 | Add a cross-link noting the Codex row is superseded by this plan |
| 43 | `.agents/MULTI_AGENT_RULES.md:21` / `.ko.md:21` | Add `codex` to the Team-Leader agent-name parenthetical, scope confined to that one line each |
| 44 | `README.md:3,17,45,136` / `README.ko.md:3,17,27,118` | Add `codex` to the 5-agent → 6-agent listings, incl. a `codex resume <uuid>` example (positional form, §1.9) |
| 45 | `BOOTSTRAP.md:197` / `.ko.md:196` | Add `codex` to the onboarding agent-name parenthetical |
| 46 | `deploy/INSTALL.md:3` | Add `Codex` to the parenthetical agent list |
---
## 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:
## 4. `reconcile.sh` Drift-C Block for Codex (Rev.2: state-DB-first)
```python
# === drift C (opencode): opencode 새 session id materialize (per-row own id) ===
# === drift C (codex): codex 새 session id materialize (per-row own id) ===
for s in d.get('herdr_sessions', []):
if row_agent(s) != 'opencode':
if row_agent(s) != 'codex':
continue
if s.get('status') != 'running':
continue
if s.get('opencode_session_id_own'):
if s.get('codex_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
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
epoch_threshold = s.get('herdr_session_epoch', 0)
sibling_claimed = [
other.get('opencode_session_id_own')
other.get('codex_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')
and other.get('codex_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:
# Rev.2: state DB first (indexed, single query) — dynamic filename,
# no hardcoded suffix (see §1.5 / §2 _state_db()).
state_matches = glob.glob(f"{codex_home}/state_*.sqlite")
state_db = max(state_matches, key=os.path.getmtime) if state_matches else None
rows = []
if state_db and os.path.exists(state_db):
conn = sqlite3.connect(state_db)
rows = conn.execute(
"SELECT id, updated_at FROM threads WHERE cwd=? ORDER BY updated_at DESC LIMIT 20",
(cwd,)
).fetchall()
conn.close()
if not rows:
# Fallback: bounded glob over the date-nested tree
paths = sorted(
glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*.jsonl"),
key=os.path.getmtime, reverse=True
)[:50]
rows = [(os.path.basename(p)[:-6][-36:], os.path.getmtime(p)) for p in paths]
for uuid, ts in rows:
if epoch_threshold and ts and ts < epoch_threshold:
continue
if uuid in (sibling_claimed or []):
continue
if verify_session_uuid(cwd, 'opencode', uuid, s_eval, mode="discover"):
if verify_session_uuid(cwd, 'codex', uuid, s_eval, mode="discover"):
valid_candidates.append(uuid)
except Exception:
pass
@@ -215,35 +397,58 @@ for s in d.get('herdr_sessions', []):
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}"']
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "codex" "{cwd}"']
rc = subprocess.run(cmd).returncode
if rc == 0:
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=False)
_pin_and_verify_resume(s, 'codex', 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)
_pin_and_verify_resume(s, 'codex', 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).
`verify_session_uuid()` (shared, `lib_py/verify_session.py`) needs a `codex_session_id_own` entry added to its own-key list (wiring item #4) for the `C0`-class "confirm, don't discover" path.
---
## 7. Effort Estimate
## 5. `status.sh`
- **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
Both `resume_on_disk()`-style helpers (lines ~56-61, ~234-239) iterate `('claude', 'agy', 'hermes', 'grok', 'opencode')`. Add `codex` to both tuples and a new `if agent == 'codex':` branch resolving via the same DB-first/glob-fallback helper as the adapter (§2), returning `'yes'`/`'MISSING'`/`'no'`.
---
## 6. Version & Documentation Bump
Current: `MAM_VERSION="4.1.3"`. Propose **`4.1.3 → 4.2.0` (MINOR)**, matching the OpenCode precedent, in strict 3-way lockstep (`lib.sh:32`, `VERSIONS.md`, all 8 `SKILL.md` frontmatter) per `tests/test_version_consistency.py`. No new agent-specific guide needed (no local-LLM/Ollama angle for Codex).
---
## 7. Empirical Verification Gate (must happen before merge)
1. Live pane capture to confirm `ready_tokens`/`input_placeholder` (§1.6).
2. Live smoke test that `--dangerously-bypass-approvals-and-sandbox` suppresses every confirmation prompt in a real multi-file-edit task.
3. Live test of the corrected `orc_onboard.sh` regex (§1.9) against a real running `codex resume <uuid>` process's `ps`/`/proc` argv snapshot — the regex was verified against a synthetic string in this planning pass, not a real captured process line.
4. **(Rev.2, new)** Confirm the `state_*.sqlite` glob resolves correctly on a *different* Codex install/version than this machine's `state_5.sqlite` — the numeric suffix is not a documented, guaranteed-stable contract, only an observed fact on one install.
---
## 8. Definition of Done Checklist
- [ ] Complete Phase 0 empirical gate (§7), including the new Rev.2 state-DB-portability check.
- [ ] Implement `.agents/skills/lib_py/agents/adapters/codex.py` per §2 (DB-first resolution, `codex delete --force`-based purge with fallback).
- [ ] Apply all 46 lockstep wiring points in §3, including the **corrected** `orc_onboard.sh` regex (item #27).
- [ ] Add the dedicated `reconcile.sh` Drift-C block (§4, state-DB-first) and its `C-ambiguous`-scenario test.
- [ ] Add all test coverage in §3 items #35-39, **including the regression test locking down the corrected `orc_onboard.sh` regex** (item #38) and the `purge_artifacts` CLI-then-fallback test (item #36); `pytest tests/` shows 0 regressions.
- [ ] Bump version to `4.2.0` in strict 3-way lockstep (§6).
- [ ] Correct the two speculative prior reports' Codex rows (§3 items #41-42).
- [ ] Manual end-to-end smoke test: create → prompt → status shows `yes` → reconcile materializes `codex_session_id_own` → stop → resume round-trips → purge leaves no orphan `.jsonl` or `state_*.sqlite` row.
---
## 9. Effort Estimate
- **Total Estimate**: **~1.5-1.75 days** (Rev.2: +0.25 day vs. Rev.1 for the state-DB-aware adapter logic and the corrected/tested `orc_onboard.sh` regex).
- ~2-3 hrs: Phase 0 empirical gate (§7), including the new state-DB-portability check.
- ~4-5 hrs: adapter implementation (DB-first + glob-fallback throughout) + the 46-point mechanical wiring pass.
- ~2-3 hrs: dedicated `reconcile.sh` Drift-C block + `status.sh` + full test suite additions, including the two Rev.2 regression tests.
- ~2 hrs: docs/version bump + end-to-end manual smoke test (including a purge-leaves-no-orphan-row check).
+1 -1
View File
@@ -200,7 +200,7 @@ class NewAgentAdapter(BaseAgentAdapter):
| Agent Type | Target CLI / Tool | Session Storage Format | Auth Strategy | Ready Tokens Pattern | Complexity Tier | Estimated Effort |
|---|---|---|---|---|:---:|:---:|
| **OpenCode** | `opencode` (CLI / TUI) | JSON files in `~/.opencode/sessions/` | Local token / API key | `OpenCode\|Chat\|Welcome` | **Tier 1 (Low)** | ~0.5 day |
| **Codex CLI** | `codex` / `openai-codex` | JSONL in `~/.codex/projects/` | API Key (`OPENAI_API_KEY`) | `OpenAI Codex\|Assistant\|❯` | **Tier 1 (Low)** | ~0.5 day |
| **Codex CLI** | `codex` | Rollouts in `~/.codex/sessions/` & SQLite `state_*.sqlite` | `auth.json` / `OPENAI_API_KEY` | `Codex\|Ask Codex to do anything` | **Implemented (v4.2.0)** | Done |
| **Kimi CLI** | `kimi` (Moonshot CLI) | SQLite DB in `~/.kimi/history.db` | API Key in `~/.kimi/config` | `Kimi\|Moonshot\|Chat` | **Tier 1 (Low)** | ~0.5 day |
| **Grok-Build** | `grok` / `grok-build` | JSON in `~/.grok/sessions/` | Token file / Env var | `Grok\|xAI\|Building` | **Tier 1 (Low)** | ~0.5 day |
| **Cursor CLI** | `cursor` (Headless/RPC) | State DB in `~/.cursor/` or RPC port | OAuth / Cookie | `Cursor\|Connected\|Listening` | **Tier 2 (Medium)** | ~1.5 days |
@@ -1,16 +1,253 @@
# Report: Job c65b0081 — Refined OpenCode Integration Plan (Rev.2) per `creator-agy-01` Challenge
> [!NOTE]
> This is the durable archive of the OpenCode (`anomalyco/opencode`) Integration Plan (Rev.3, Job `78f332a9`).
> The active mirror at `.agents/reports/implementation_plan.md` now houses the Codex CLI Integration Plan (v4.2.0).
**Durable output (updated in place)**: [.agents/reports/implementation_plan.md](../../../.agents/reports/implementation_plan.md)
# 🔌 Implementation Plan: OpenCode (`anomalyco/opencode`) Agent Adapter for MAM (Rev.3)
## Summary
- **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`)
`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.
## Rev.3 Changelog (Job `78f332a9`)
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).
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**:
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).
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.
[VERDICT: N/A — planning artifact, review pending]
---
## 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,454 @@
# 🔌 Implementation Plan: Codex CLI (`openai/codex`) Agent Adapter for MAM (Rev.2)
- **Author**: `planner-reviewer-claude-01` (Planner), refined in response to `creator-agy-01`'s architectural challenge
- **Job**: `e30e3123` → Rev.2 (`94a6db78`)
- **Status**: Plan only — **no code changed in the repo**. Ready for review/refinement.
- **Scope**: Add `codex` as a 6th first-class MAM agent backend (alongside `claude`, `agy`, `hermes`, `grok`, `opencode`), per `docs/NEW_AGENT_INTEGRATION_GUIDE.md`'s 5-step procedure.
- **Durable Path**: `.agents/reports/planner-reviewer-claude-01/plan-e30e3123.md` (mirrored at `.agents/reports/implementation_plan.md`).
- **Method**: Every CLI fact is verified against a **live, authenticated Codex CLI install already present on this machine** (`codex-cli 0.153.4`). Rev.2 additionally re-verified both of `creator-agy-01`'s challenge points live before accepting them — see §0.
---
## 0. Rev.2 Changelog — Response to `creator-agy-01`'s Architectural Challenge (Job `004c224d`)
`creator-agy-01` filed a formal challenge against Rev.1 with **1 critical defect + 1 supporting architectural constraint**. Per this session's standing discipline, neither was accepted on the challenger's word alone — both were independently re-verified live on this machine before being incorporated. **Both are accepted — no `[REBUT:]` filed.**
| # | Challenge | My independent verification | Disposition |
|---|---|---|---|
| 1 | **Critical**: Rev.1's `orc_onboard.sh` detection regex (`resume[[:space:]]+[0-9a-f-]{36}`) cannot match the actual command Rev.1 itself specifies (`codex resume --dangerously-bypass-approvals-and-sandbox <uuid>`), because the flag sits between `resume` and the UUID | Ran the exact `cmd_line` from the challenge through Rev.1's regex myself: `echo "$cmd_line" \| grep -oE 'resume[[:space:]]+[0-9a-f-]{36}'` → **empty, exit 1**. Confirmed genuine — Rev.1 contained a self-contradiction between §2 (`resume_spec`) and §1.9/§3 item 27 (the detection regex). Then tested the challenge's proposed Plan A regex on the same input → **correctly extracts the UUID, exit 0**. | **Accepted.** §1.9 and wiring item #27 rewritten to the flag-tolerant regex (verified working, not just proposed) |
| 2 | **Supporting**: `~/.codex/state_N.sqlite` (a `threads` table indexing `id`/`cwd`/`rollout_path`) exists alongside the rollout JSONL files; Rev.1's "single-file format, no DB rows to delete" claim was only half true, and `purge_artifacts()` deleting only the `.jsonl` would leave an orphan DB row | `ls ~/.codex/*.sqlite*` on this machine → confirmed `state_5.sqlite` exists. `sqlite3 ~/.codex/state_5.sqlite ".schema threads"` → confirmed the exact `threads` table the challenge quoted. `sqlite3 ... "SELECT id,cwd,rollout_path FROM threads"` → confirmed 2 real rows exactly matching the 2 real rollout files found earlier. `strings` re-extraction confirmed the `stale_db_path`/`state db discrepancy`/`reconcile_rollout`/`read_repair_rollout_path: upsert_needed` literals the challenge cited. `codex delete --help` → confirmed `codex delete <SESSION> --force` ("Delete without prompting. SESSION must be a UUID") is real, exactly as the challenge's remediation option 1 proposed. | **Accepted**, with one refinement found during my own re-verification (not in the original challenge): `codex debug --help` has **no** `db path`-style introspection subcommand (unlike OpenCode's `opencode db path`), and the state file's numeric suffix (`state_5`) is unverified as stable across Codex versions/installs — hardcoding `state_5.sqlite` the way the challenge's raw example does would itself be fragile. §2's adapter design now resolves the state DB via a `state_*.sqlite` glob (most-recently-modified), and `purge_artifacts()` now shells out to `codex delete <uuid> --force` as the primary mechanism (letting Codex's own reconciliation logic keep the JSONL and DB consistent) with a manual-file-removal fallback only if that call fails |
Net effect: Rev.1's overall architecture and the 46-point wiring table stand — this challenge caught one genuine self-contradiction (the regex) and one genuine completeness gap (the state DB), both now closed. No other part of Rev.1 was found wrong during this re-verification pass.
---
## 1. Verified Codex CLI/Runtime Facts
Source: live `codex --help` / `codex resume --help` / `codex exec --help` / `codex login --help` / `codex delete --help` / `codex debug --help` / `codex doctor --summary` / `codex login status` output, direct `ls`/`find` of `~/.codex/`, live `sqlite3` schema/row inspection of `~/.codex/state_5.sqlite`, and offline `strings` extraction of the installed binary — all run on this machine.
### 1.1 Identity & Install
- Binary: `codex` (this machine: `/Users/godopu16/.local/bin/codex`, version `codex-cli 0.153.4`).
- Config/state root: `~/.codex/` by default, **overridable via the `CODEX_HOME` environment variable** (confirmed: `codex exec --ignore-user-config` help text explicitly says "auth still uses `CODEX_HOME`").
### 1.2 Execution Modes
- `codex [OPTIONS] [PROMPT]` — launches the **interactive TUI** (default, what MAM spawns).
- `codex exec [OPTIONS] [PROMPT]` (alias `e`) — **non-interactive** one-shot execution with `--json` JSONL event streaming and `-o/--output-last-message <FILE>`. Not used by MAM's spawn/resume flow, but confirms Codex has a scriptable mode.
- `codex resume [OPTIONS] [SESSION_ID] [PROMPT]` — resumes a specific session by **UUID or session name, passed positionally**; `--last` picks the most recent session without a picker.
- `codex delete <SESSION> --force` — permanently deletes a saved session by UUID; `--force` skips the confirmation prompt and requires `SESSION` to be a UUID (not a name). Verified via `codex delete --help`.
### 1.3 Relevant CLI Flags & Environment
| Flag / Variable | Purpose | Verified via |
|---|---|---|
| `--dangerously-bypass-approvals-and-sandbox` | Skip all confirmation prompts and run without sandboxing — the unattended-automation flag | `codex --help`, `codex resume --help` (accepted on both) |
| `-s, --sandbox <read-only\|workspace-write\|danger-full-access>` / `-a, --ask-for-approval <on-request\|never>` | Finer-grained alternative to the single bypass flag above | `codex --help` |
| `--approve-for-me` | Routes approvals through automatic review under `workspace-write` — **not** a full bypass | `codex --help` |
| `-C, --cd <DIR>` | Working-root directory for the agent | `codex --help` |
| `--session-id <uuid>` | **Does not exist.** No such flag appears anywhere in `codex --help`'s full option list | Absence confirmed by reading the complete `--help` output |
| `CODEX_HOME` | Overrides `~/.codex` config/session root | `codex exec --ignore-user-config` help text |
| `OPENAI_API_KEY` | Read by `codex login --with-api-key` (via stdin); also stored inside `auth.json` under the `OPENAI_API_KEY` key once logged in | `codex login --help`, `~/.codex/auth.json` key inspection |
### 1.4 Unattended-Execution Decision
Confirmed present. `--dangerously-bypass-approvals-and-sandbox` is accepted by both the top-level `codex` TUI launcher and `codex resume`, so `spawn_spec()`/`resume_spec()` can pass it unconditionally.
### 1.5 Session & Artifact Storage — Dual Layer (Rev.2: corrected)
- **Primary transcript**: JSONL "rollout" files, one per session, at `~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<timestamp>-<uuid>.jsonl` — e.g. `~/.codex/sessions/2026/05/23/rollout-2026-05-23T08-23-56-019e5200-ab6a-70f2-b5da-e1ef403f7a40.jsonl`. First line is a `session_meta` record: `{"payload": {"id": "<uuid>", "cwd": "<path>", ...}}`.
- **Secondary index (Rev.2, per the accepted challenge)**: `~/.codex/state_N.sqlite` (this machine: `state_5.sqlite`; **the numeric suffix is not assumed stable** — no `codex debug`/`codex` subcommand introspects its exact filename, unlike OpenCode's `opencode db path`, so the adapter must resolve it via a `state_*.sqlite` glob rather than hardcoding the suffix). Verified live schema: a `threads` table with `id TEXT PRIMARY KEY`, `rollout_path TEXT`, `cwd TEXT`, `created_at`/`updated_at` (plus many version-added trailing columns — the schema has clearly evolved via in-place `ALTER TABLE` across releases, another reason not to hand-roll fragile column-position assumptions). Verified live rows: both real sessions on this machine are indexed here with `id`/`cwd`/`rollout_path` exactly matching their on-disk `.jsonl` files.
- **Codex's own resolution order** (inferred from binary string literals `state db discrepancy during find_thread_path_by_id_str_in_subdir: stale_db_path / mismatched_db_path / falling_back` and `reconcile_rollout` / `read_repair_rollout_path: upsert_needed`): Codex itself queries the state DB first, and has a **self-healing reconciliation path** for when the DB and filesystem disagree (falls back to a filesystem scan, then repairs the DB entry) — this is Codex's own built-in drift-correction, structurally analogous to MAM's own `reconcile.sh`.
- **Architectural consequence for MAM's adapter**: mirror Codex's own resolution order — **query `state_*.sqlite` first** (fast, indexed, avoids any unbounded directory walk), **fall back to the bounded glob** over `sessions/*/*/*/rollout-*.jsonl` if the state DB is missing, unreadable, or the row is stale/absent (mirroring the exact "primary DB, fallback filesystem walk" shape `opencode.py`'s `_resolve_db()` already uses in this codebase, and mirroring what Codex's own binary does internally).
### 1.6 TUI Readiness (candidate, pending empirical capture)
Offline `strings` extraction surfaced real UI literals: `"Ask Codex to do anything"` (composer placeholder) and the banner token `"Codex"`. No `❯` glyph or `─{10,}` rule literal found — different TUI layout family (boxed/panelled, not readline-style). Proposed candidate:
- `ready_tokens`: `'Codex|Ask Codex to do anything'`
- `input_placeholder`: `'Ask Codex to do anything'` (high-confidence — extracted literal)
- `input_prompt` / `input_rule_pattern`: `None` (base-class default) pending live pane capture.
**Hard gate before merge**: capture a real `codex` TUI pane via `herdr pane capture` to confirm exact rendered text.
### 1.7 Exit Convention (verified deviation from every existing adapter)
Binary string table twice describes exiting as a keypress: `"Ctrl+C to exit"`, `"ctrl+c quit"`. No `/exit`/`/quit`/`:q` literal found (real slash commands that do exist: `/keymap`, `/side`, `/review`, `/ide`, `/goal`).
`MAM_EXIT_KEY` is exported by the `facts` bridge and loaded into `send_keys_safe()`'s local scope (`lib.sh:2166`), but **no skill script actually sends it to a pane today, for any agent** — `stop_session.sh` never references `exit_key`/`MAM_EXIT_KEY`. So this remains a required-but-currently-inert contract field. Propose `exit_key = 'Ctrl+C'` (symbolic key name, matching herdr's existing `Escape` send convention), documented as a deliberate divergence.
### 1.8 Authentication
- Interactive: `codex login` (ChatGPT OAuth) or `codex login --with-api-key` (reads `OPENAI_API_KEY` from stdin).
- Credential file: `~/.codex/auth.json` (or `$CODEX_HOME/auth.json`) — verified present, keys `auth_mode`, `OPENAI_API_KEY`, `tokens`, `last_refresh`. `codex login status` confirms `"Logged in using ChatGPT"`; `codex doctor --summary` independently confirms `✓ auth is configured`.
- `auth_ok()`: `os.path.exists(f"{codex_home}/auth.json")` **or** `os.environ.get('OPENAI_API_KEY')`. **Caveat honestly flagged**: whether a bare env var alone (without ever running `codex login --with-api-key`) is sufficient was not empirically tested (this machine is logged in via ChatGPT, not a raw API key).
### 1.9 Session-ID Detection for `orc_onboard.sh` (Rev.2: regex defect fixed)
1. **argv form**: Codex passes the UUID as `codex resume <uuid>` — positional, no flag name. **Rev.1's regex was defective**: `grep -oE 'resume[[:space:]]+[0-9a-f-]{36}'` requires the UUID immediately after `resume`, but the real command line this plan's own `resume_spec`/`CMD_FULL` produce is `codex resume --dangerously-bypass-approvals-and-sandbox <uuid>` — a flag sits in between, so the regex never matches (verified live: exit 1, empty output). **Corrected regex** (challenge's Plan A, live-verified to work): allow arbitrary tokens between `resume` and the UUID, then extract only the UUID:
```bash
codex)
detected_id=$(echo "$cmd_line" | grep -oE 'resume[[:space:]]+.*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | grep -oE '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | head -n 1 || true)
;;
```
Already safe under `orc_onboard.sh:135`'s existing `is_valid_id "$detected_id"` UUID-format re-validation, so a slightly looser extraction regex carries no correctness risk.
2. **env var fallback**: no `CODEX_SESSION_ID`-style environment variable found anywhere in `codex --help`. Leave `env_var=""` for `codex` — do not fabricate one.
---
## 2. Adapter Contract Decisions (`BaseAgentAdapter`) — Rev.2
| Property/Method | Value | Rationale |
|---|---|---|
| `name` | `'codex'` | Lowercase identifier |
| `own_key` | `'codex_session_id_own'` | Matches `<agent>_session_id_own` convention |
| `ready_tokens` | `'Codex\|Ask Codex to do anything'` (candidate, §1.6) | Extracted real UI literal, pending live pane-capture confirmation |
| `exit_key` | `'Ctrl+C'` (§1.7) | No typed slash command exists; currently operationally inert framework-wide |
| `delegate_agent_key` | `'codex-cli'` | Matches the `<name>-cli`/`-agent`/`-build`/`-code` suffix convention |
| `identity_cache_fields` | `('session_id',)` | Single-field cache, modern pattern |
| `input_placeholder` | `'Ask Codex to do anything'` | Verified real literal |
| `input_prompt` / `input_rule_pattern` | `None` | No stable glyph found; different TUI layout family |
| `spawn_spec(binary, session_uuid, use_wrapper)` | `f"{binary} --dangerously-bypass-approvals-and-sandbox"` (ignores `session_uuid` — Class A) | §1.3/§1.4 |
| `resume_spec(binary, session_uuid, materialized)` | if `materialized and session_uuid`: `f"{binary} resume --dangerously-bypass-approvals-and-sandbox {session_uuid}"`, else same as `spawn_spec` | Verified `codex resume [OPTIONS] [SESSION_ID]` usage template |
| `auth_ok(run_cmd)` | `os.path.exists(f"{codex_home}/auth.json") or bool(os.environ.get('OPENAI_API_KEY'))` | §1.8, sandbox-safe via `ctx.home_dir` |
| `artifact_path(uuid, ctx)` | **Rev.2**: query `state_*.sqlite`'s `threads.rollout_path WHERE id=?` first; fall back to the bounded glob `sessions/*/*/*/rollout-*-{uuid}.jsonl` if the DB lookup misses | §1.5 — mirrors Codex's own DB-first, filesystem-fallback resolution order |
| `verify_artifact(uuid, ctx)` | Resolve path via the same DB-first/glob-fallback helper; parse first JSONL line's `payload.id`/`payload.cwd` (or use the DB row's `cwd` column directly, skipping the file read, when the DB hit succeeds); apply `ctx.epoch` mtime check | Rev.2: DB row already has `cwd` — avoids opening the file at all on the common (DB-hit) path |
| `purge_artifacts(uuid, ctx)` | **Rev.2**: primary — `subprocess.run([binary_or_'codex', 'delete', uuid, '--force'], ...)`; fallback (only if that call fails/binary unavailable) — manually `os.remove()` the rollout file located via the DB-first/glob-fallback helper, **and** best-effort `DELETE FROM threads WHERE id=?` against the resolved `state_*.sqlite` if it exists | §0 item 2 — avoids orphan DB rows; lets Codex's own reconciliation logic own consistency whenever possible instead of MAM hand-rolling SQL against an evolving schema |
| `discover(ctx)` | **Rev.2**: query `state_*.sqlite`'s `threads` table (`SELECT id, cwd, updated_at FROM threads ORDER BY updated_at DESC LIMIT 50`, filtered by `cwd`/`epoch` in Python) first; fall back to the recursive-glob approach from Rev.1 only if the DB is absent/unreadable | Rev.2 — DB query is a single indexed lookup vs. an N-file glob-and-open walk; also naturally bounded (no unbounded-growth concern for the DB path) |
### Adapter code sketch (`.agents/skills/lib_py/agents/adapters/codex.py`) — Rev.2
```python
import os, json, glob, sqlite3, subprocess
from typing import Optional, Any, List
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext
from lib_py.verify_session import workspace_key
class CodexAgentAdapter(BaseAgentAdapter):
@property
def name(self) -> str:
return 'codex'
@property
def own_key(self) -> str:
return 'codex_session_id_own'
@property
def ready_tokens(self) -> str:
return 'Codex|Ask Codex to do anything'
@property
def exit_key(self) -> str:
return 'Ctrl+C'
@property
def delegate_agent_key(self) -> str:
return 'codex-cli'
@property
def identity_cache_fields(self) -> tuple:
return ('session_id',)
@property
def input_placeholder(self) -> Optional[str]:
return 'Ask Codex to do anything'
def _codex_home(self, ctx: DiscoveryContext) -> str:
return os.environ.get('CODEX_HOME') or f"{ctx.home_dir}/.codex"
def _state_db(self, ctx: DiscoveryContext) -> Optional[str]:
# Rev.2: no 'codex db path'-equivalent subcommand exists; the numeric
# suffix (state_5.sqlite on this machine) is unverified as stable
# across versions, so resolve dynamically rather than hardcoding it.
matches = glob.glob(f"{self._codex_home(ctx)}/state_*.sqlite")
if not matches:
return None
return max(matches, key=os.path.getmtime)
def _find_rollout_via_db(self, uuid: str, ctx: DiscoveryContext):
"""Returns (rollout_path, cwd) from the state DB, or (None, None)."""
db = self._state_db(ctx)
if not db or not os.path.exists(db):
return None, None
try:
conn = sqlite3.connect(db)
row = conn.execute("SELECT rollout_path, cwd FROM threads WHERE id=?", (uuid,)).fetchone()
conn.close()
if row and row[0] and os.path.exists(row[0]):
return row[0], row[1]
except Exception:
pass
return None, None
def _find_rollout(self, uuid: str, ctx: DiscoveryContext) -> Optional[str]:
path, _ = self._find_rollout_via_db(uuid, ctx)
if path:
return path
# Fallback: bounded glob over the date-nested tree (state DB missing/stale)
matches = glob.glob(f"{self._codex_home(ctx)}/sessions/*/*/*/rollout-*-{uuid}.jsonl")
return matches[0] if matches else None
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
return self._find_rollout(uuid, ctx) or f"{self._codex_home(ctx)}/sessions/rollout-{uuid}.jsonl"
def verify_artifact(self, uuid: str, ctx: DiscoveryContext) -> bool:
db_path, db_cwd = self._find_rollout_via_db(uuid, ctx)
path = db_path or self._find_rollout(uuid, ctx)
if not path or not os.path.exists(path):
return False
if ctx.epoch and os.path.getmtime(path) < ctx.epoch:
return False
found_cwd = db_cwd
if not found_cwd:
try:
with open(path) as f:
first = f.readline()
found_cwd = json.loads(first).get('payload', {}).get('cwd')
except Exception:
return False
if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd):
return False
return True
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> List[str]:
purged = []
path = self._find_rollout(uuid, ctx)
# Rev.2: prefer the CLI's own delete path (keeps rollout file + state
# DB row consistent via Codex's own reconciliation logic).
try:
res = subprocess.run(['codex', 'delete', uuid, '--force'],
capture_output=True, text=True, timeout=5)
if res.returncode == 0:
if path:
purged.append(path)
return purged
except Exception:
pass
# Fallback: manual removal of the file, plus a best-effort DB row delete.
if path and os.path.exists(path):
os.remove(path)
purged.append(path)
db = self._state_db(ctx)
if db and os.path.exists(db):
try:
conn = sqlite3.connect(db)
conn.execute("DELETE FROM threads WHERE id=?", (uuid,))
conn.commit()
conn.close()
purged.append(f"state db row for thread: {uuid}")
except Exception:
pass
return purged
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
return f"{binary} --dangerously-bypass-approvals-and-sandbox"
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
if materialized and session_uuid:
return f"{binary} resume --dangerously-bypass-approvals-and-sandbox {session_uuid}"
return f"{binary} --dangerously-bypass-approvals-and-sandbox"
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
home = os.environ.get('HOME_DIR') or os.environ.get('HOME') or os.path.expanduser('~')
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
return os.path.exists(f"{codex_home}/auth.json") or bool(os.environ.get('OPENAI_API_KEY'))
def discover(self, ctx: DiscoveryContext) -> List[str]:
db = self._state_db(ctx)
if db and os.path.exists(db):
try:
conn = sqlite3.connect(db)
rows = conn.execute(
"SELECT id, cwd, updated_at FROM threads ORDER BY updated_at DESC LIMIT 50"
).fetchall()
conn.close()
candidates = []
for uuid, cwd, _ in rows:
if ctx.workspace and cwd and workspace_key(cwd) != workspace_key(ctx.workspace):
continue
if uuid and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
return candidates
except Exception:
pass
# Fallback: recursive glob (state DB absent/unreadable)
root = self._codex_home(ctx)
candidates = []
paths = sorted(glob.glob(f"{root}/sessions/*/*/*/rollout-*.jsonl"), key=os.path.getmtime, reverse=True)[:50]
for path in paths:
uuid = os.path.basename(path)[:-6][-36:]
if uuid and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
return candidates
```
---
## 3. Full Lockstep Wiring Table (Rev.2)
Same 46-item table as Rev.1, with item #27 corrected per §0/§1.9. Re-derived from the current, live state of every file that mentions `opencode` today (33 files, `.agents/reports/` excluded).
| # | Component / File | Specific Edit Required |
|---|---|---|
| 1 | `.agents/skills/lib_py/agents/adapters/codex.py` | Create new adapter class `CodexAgentAdapter` per §2 (Rev.2: DB-first artifact resolution + `codex delete --force`-based purge) |
| 2 | `.agents/skills/lib_py/agents/registry.py:9,16` | Import `CodexAgentAdapter`; add `'codex': CodexAgentAdapter()` to `_ADAPTERS` |
| 3 | `.agents/skills/lib_py/atomic_yaml.py:132` | Add `'codex_session_id_own'` to `running_keys` |
| 4 | `.agents/skills/lib_py/verify_session.py:69` | Add `'codex_session_id_own'` to the `mam_row_own_uuid()` field list |
| 5 | `.agents/skills/lib_py/workspace_uuid.py:7-12,34` | Add `'codex': 'codex_session_id_own'` to `OWN_KEY`; add `'codex_session_id_own'` to the `running_ids` field list |
| 6 | `.agents/skills/lib.sh:669` (kind mapping) | Add `*-creator-codex\|*-planner-codex\|*-reviewer-codex) kind="codex" ;;` |
| 7 | `.agents/skills/lib.sh:675,680` (fallback grep chain, name + `final_cmd`) | Add `elif echo "$name" \| grep -qi "codex"; then kind="codex"` and the `final_cmd` equivalent |
| 8 | `.agents/skills/lib.sh:694` (binary dedup tuple) | Add `'codex'` to `('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex')` |
| 9 | `.agents/skills/lib.sh:2175` (`send_keys_safe` agent resolution) | Add `*-codex\|*-codex-[0-9]*\|codex\|codex-[0-9]*) _sks_agent="codex" ;;` |
| 10 | `create_session.sh:4,29,33` (usage/help text) | Add `codex` to the `<claude\|agy\|hermes\|grok\|opencode>` listings |
| 11 | `create_session.sh:93-94` (`--agent` whitelist) | Add `codex` to the accepted-values case and the error message |
| 12 | `create_session.sh` (auth preflight, new `elif` block after the existing `opencode` one at line 129) | `elif [ "$AGENT" = "codex" ]; then` check `~/.codex/auth.json` (respecting `CODEX_HOME`) or `OPENAI_API_KEY`, else `codex --version` smoke check |
| 13 | `create_session.sh:192` (fallback `CMD_FULL` case) | Add `codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;` |
| 14 | `create_session.sh:216` (`spawn()` dispatch case) | Add `codex` to the `agy\|hermes\|grok\|opencode)` case — no special env-var export needed |
| 15 | `create_session.sh:330` (delegate-agent key resolution) | Add `codex) delegate_agent="codex-cli" ;;` |
| 16 | `create_session.sh:438` (YAML init Python block) | Add `elif agent == 'codex': entry['codex_session_id_own'] = None; entry['session_id_source'] = 'pending-discovery'; entry['session_id_verified'] = False; entry['last_visible_status'] = "unverified"` |
| 17 | `resolve_session_id.sh:4,16,39-40` | Add `codex` to usage text and the `--agent` whitelist case |
| 18 | `resume_session.sh:12,45` | Add `codex` to usage text and the `--agent` whitelist case |
| 19 | `resume_session.sh:65` (FRESH_SPAWN Class-A case) | Add `codex` to `agy\|hermes\|opencode) FRESH_SPAWN=1 ;;` — Codex is Class A |
| 20 | `resume_session.sh:121` (fallback spawn-spec case) | Add `codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;` |
| 21 | `resume_session.sh:132` (fallback resume-spec case) | Add `codex) CMD_FULL="${RESOLVED_BIN} resume --dangerously-bypass-approvals-and-sandbox $UUID" ;;` |
| 22 | `update_yaml_resumed.sh:7,14,17` | Add `codex` to usage docs and the Class-A comment listing |
| 23 | `update_yaml_resumed.sh:228` (Python block) | Add `elif agent == 'codex': target['pane']['cmd'] = 'codex'`; if `uuid`: `cmd_full = f'codex resume --dangerously-bypass-approvals-and-sandbox {uuid}'`, `target['codex_session_id_own'] = uuid`; else `cmd_full = os.environ.get('CMD_FULL_OPT', '') or 'codex --dangerously-bypass-approvals-and-sandbox'` |
| 24 | `stop_session.sh:4,15,94-95` | Add `codex` to usage docs and the `--agent` whitelist case + error message |
| 25 | `stop_session.sh:290` (Python capture block) | Add `elif agent == 'codex': target['codex_session_id_own'] = captured` |
| 26 | `orc_onboard.sh:106` (agent-family match) | Add `codex` to the `claude\|agy\|hermes\|grok\|opencode)` case |
| 27 | `orc_onboard.sh:117-133` (CLI-argv detection) | **Rev.2 corrected**: new `codex)` branch using the flag-tolerant regex from §1.9 (`resume[[:space:]]+.*<uuid-pattern>` then extract just the UUID) — **not** the Rev.1 regex, which is confirmed broken against this plan's own `resume_spec` output |
| 28 | `orc_onboard.sh:142-148` (env-var fallback) | Add `codex) env_var=""` explicitly (no known env var — do not fabricate one) |
| 29 | `reconcile.sh:523,542` (agent-name tuples) | Add `'codex'` to both `('claude', 'agy', 'hermes', 'grok', 'opencode')` tuples |
| 30 | `reconcile.sh:603` (fresh-row entry init) | Add `elif agent == 'codex': entry['codex_session_id_own'] = None` |
| 31 | `reconcile.sh:616` (`OWN_KEY_BY_AGENT`) | Add `'codex'` to the tuple |
| 32 | `reconcile.sh` (new dedicated block, after the existing opencode block at line ~787-897) | New `# === drift C (codex) ===` block, **Rev.2**: state-DB-first with glob fallback (see §4) |
| 33 | 8× `SKILL.md` supported-agent lists (`multi-agent-mux-create:132,163-166`; `multi-agent-mux-orc-onboard:11,35,41`; `multi-agent-mux-resume:52,55,61,73,99,110`; `multi-agent-mux-stop:40`) | Add `codex` everywhere `opencode` currently appears in these four files; the other 4 `SKILL.md` files need only the version-frontmatter bump (§6) |
| 34 | `.agents/skills/multi-agent-mux-delegate-job/job-protocol.md:3`, `README.md:3` | No change needed — `codex` already listed informally |
| 35 | `tests/conftest.py` | Add a `codex` mock binary fixture (mirroring `opencode_bin`, line ~972-990) that writes a fake `rollout-<ts>-<uuid>.jsonl` under a temp `$CODEX_HOME/sessions/<Y>/<M>/<D>/` **and** a matching `state_1.sqlite` `threads` row (Rev.2: needed since `discover()`/`verify_artifact()` now query the DB first); add ready-banner/own-key dict entries (lines ~553, ~584) |
| 36 | `tests/test_a4_adapter_contract.py` | Add `codex` to `EXPECTED_OWN_KEYS`, the property tuple, `expected_modal` (`None`), the `facts_bridge` loop (line ~112), a `_seed_codex_session()` helper (mirroring `_seed_opencode_session`, line ~132, **Rev.2**: seeding both the `.jsonl` and the `state_*.sqlite` row), and unit tests for `spawn_spec`/`resume_spec`/`auth_ok`/`purge_artifacts` (**Rev.2**: including a test that `purge_artifacts` falls back to manual removal when the `codex delete` subprocess call fails)/`discover` |
| 37 | `tests/test_c1_tui_readiness.py` | Add `codex` to the facts-bridge and `send_keys_safe` agent-resolution loops (line ~491) and a `check_sks` assertion pair (mirroring lines 528-529, 539-540) |
| 38 | `tests/test_tier1_unit.py` | Update the agent-tuple assertions (lines ~963, 969, 974, 988-989, 994, 1033) to include `codex`; add `test_codex_shell_and_scripts_integration` mirroring `test_opencode_shell_and_scripts_integration` (lines 982-1044); **Rev.2**: add a dedicated regression test asserting `orc_onboard.sh`'s `codex` regex correctly extracts the UUID from `codex resume --dangerously-bypass-approvals-and-sandbox <uuid>` — this is the exact scenario Rev.1 got wrong, so it must be locked down by a test, not just fixed in prose |
| 39 | `tests/test_tier2_component.py` | Add `test_comp_codex_create_and_reconcile_and_stop` mirroring `test_comp_opencode_create_and_reconcile_and_stop` (line ~1229) |
| 40 | `.mam.env.example` (after the existing OpenCode block, line ~110) | Document `OPENAI_API_KEY` and `CODEX_HOME` as an optional override |
| 41 | `docs/NEW_AGENT_INTEGRATION_GUIDE.md` §3 (difficulty matrix) | Update the `Codex CLI` row from the old guessed values to the verified ones from §1 |
| 42 | `.agents/reports/new_agent_types_roadmap.md` §4 | Add a cross-link noting the Codex row is superseded by this plan |
| 43 | `.agents/MULTI_AGENT_RULES.md:21` / `.ko.md:21` | Add `codex` to the Team-Leader agent-name parenthetical, scope confined to that one line each |
| 44 | `README.md:3,17,45,136` / `README.ko.md:3,17,27,118` | Add `codex` to the 5-agent → 6-agent listings, incl. a `codex resume <uuid>` example (positional form, §1.9) |
| 45 | `BOOTSTRAP.md:197` / `.ko.md:196` | Add `codex` to the onboarding agent-name parenthetical |
| 46 | `deploy/INSTALL.md:3` | Add `Codex` to the parenthetical agent list |
---
## 4. `reconcile.sh` Drift-C Block for Codex (Rev.2: state-DB-first)
```python
# === drift C (codex): codex 새 session id materialize (per-row own id) ===
for s in d.get('herdr_sessions', []):
if row_agent(s) != 'codex':
continue
if s.get('status') != 'running':
continue
if s.get('codex_session_id_own'):
continue
cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd:
continue
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
epoch_threshold = s.get('herdr_session_epoch', 0)
sibling_claimed = [
other.get('codex_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('codex_session_id_own')
]
s_eval = dict(s)
s_eval['_sibling_claimed_uuids'] = sibling_claimed
valid_candidates = []
try:
# Rev.2: state DB first (indexed, single query) — dynamic filename,
# no hardcoded suffix (see §1.5 / §2 _state_db()).
state_matches = glob.glob(f"{codex_home}/state_*.sqlite")
state_db = max(state_matches, key=os.path.getmtime) if state_matches else None
rows = []
if state_db and os.path.exists(state_db):
conn = sqlite3.connect(state_db)
rows = conn.execute(
"SELECT id, updated_at FROM threads WHERE cwd=? ORDER BY updated_at DESC LIMIT 20",
(cwd,)
).fetchall()
conn.close()
if not rows:
# Fallback: bounded glob over the date-nested tree
paths = sorted(
glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*.jsonl"),
key=os.path.getmtime, reverse=True
)[:50]
rows = [(os.path.basename(p)[:-6][-36:], os.path.getmtime(p)) for p in paths]
for uuid, ts in rows:
if epoch_threshold and ts and ts < epoch_threshold:
continue
if uuid in (sibling_claimed or []):
continue
if verify_session_uuid(cwd, 'codex', 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"]}" "codex" "{cwd}"']
rc = subprocess.run(cmd).returncode
if rc == 0:
_pin_and_verify_resume(s, 'codex', 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, 'codex', cwd, uuid, degraded=True)
```
`verify_session_uuid()` (shared, `lib_py/verify_session.py`) needs a `codex_session_id_own` entry added to its own-key list (wiring item #4) for the `C0`-class "confirm, don't discover" path.
---
## 5. `status.sh`
Both `resume_on_disk()`-style helpers (lines ~56-61, ~234-239) iterate `('claude', 'agy', 'hermes', 'grok', 'opencode')`. Add `codex` to both tuples and a new `if agent == 'codex':` branch resolving via the same DB-first/glob-fallback helper as the adapter (§2), returning `'yes'`/`'MISSING'`/`'no'`.
---
## 6. Version & Documentation Bump
Current: `MAM_VERSION="4.1.3"`. Propose **`4.1.3 → 4.2.0` (MINOR)**, matching the OpenCode precedent, in strict 3-way lockstep (`lib.sh:32`, `VERSIONS.md`, all 8 `SKILL.md` frontmatter) per `tests/test_version_consistency.py`. No new agent-specific guide needed (no local-LLM/Ollama angle for Codex).
---
## 7. Empirical Verification Gate (must happen before merge)
1. Live pane capture to confirm `ready_tokens`/`input_placeholder` (§1.6).
2. Live smoke test that `--dangerously-bypass-approvals-and-sandbox` suppresses every confirmation prompt in a real multi-file-edit task.
3. Live test of the corrected `orc_onboard.sh` regex (§1.9) against a real running `codex resume <uuid>` process's `ps`/`/proc` argv snapshot — the regex was verified against a synthetic string in this planning pass, not a real captured process line.
4. **(Rev.2, new)** Confirm the `state_*.sqlite` glob resolves correctly on a *different* Codex install/version than this machine's `state_5.sqlite` — the numeric suffix is not a documented, guaranteed-stable contract, only an observed fact on one install.
---
## 8. Definition of Done Checklist
- [ ] Complete Phase 0 empirical gate (§7), including the new Rev.2 state-DB-portability check.
- [ ] Implement `.agents/skills/lib_py/agents/adapters/codex.py` per §2 (DB-first resolution, `codex delete --force`-based purge with fallback).
- [ ] Apply all 46 lockstep wiring points in §3, including the **corrected** `orc_onboard.sh` regex (item #27).
- [ ] Add the dedicated `reconcile.sh` Drift-C block (§4, state-DB-first) and its `C-ambiguous`-scenario test.
- [ ] Add all test coverage in §3 items #35-39, **including the regression test locking down the corrected `orc_onboard.sh` regex** (item #38) and the `purge_artifacts` CLI-then-fallback test (item #36); `pytest tests/` shows 0 regressions.
- [ ] Bump version to `4.2.0` in strict 3-way lockstep (§6).
- [ ] Correct the two speculative prior reports' Codex rows (§3 items #41-42).
- [ ] Manual end-to-end smoke test: create → prompt → status shows `yes` → reconcile materializes `codex_session_id_own` → stop → resume round-trips → purge leaves no orphan `.jsonl` or `state_*.sqlite` row.
---
## 9. Effort Estimate
- **Total Estimate**: **~1.5-1.75 days** (Rev.2: +0.25 day vs. Rev.1 for the state-DB-aware adapter logic and the corrected/tested `orc_onboard.sh` regex).
- ~2-3 hrs: Phase 0 empirical gate (§7), including the new state-DB-portability check.
- ~4-5 hrs: adapter implementation (DB-first + glob-fallback throughout) + the 46-point mechanical wiring pass.
- ~2-3 hrs: dedicated `reconcile.sh` Drift-C block + `status.sh` + full test suite additions, including the two Rev.2 regression tests.
- ~2 hrs: docs/version bump + end-to-end manual smoke test (including a purge-leaves-no-orphan-row check).
@@ -0,0 +1,46 @@
# 🔍 Cross-Review — Codex CLI Agent Backend, Post-Remediation (Job `175b653b`)
- **Reviewer**: `planner-reviewer-claude-01`
- **Target**: cumulative working-tree diff implementing `plan-e30e3123.md` Rev.2 — same codex integration I already reviewed as `[VERDICT: PASS]` in job `36148213`, now including a remediation for the blocking defect that `reviewer-creator-grok-01` (job `5bfd80b8`, NOT PASS) and `reviewer-opencode-01` (job `9cc33e22`, NOT PASS) both independently found *after* my earlier PASS.
- **Method**: Diff was truncated in the brief (221KB/3239 lines over the review limit), so per the brief's own instruction I inspected the working tree directly rather than the truncated text. I did not take any peer's verdict — including my own prior one — at face value; every claim below is re-verified against live files or live test runs in this pass.
---
## 1. What changed since my prior PASS (job `36148213`)
File-level diff stat confirms the core implementation is **untouched** since my last review: `codex.py`, `registry.py`, `lib.sh`, all 6 skill scripts, and all 7 codex-specific test additions show the same shape as before. The only substantive addition is to `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (+246/-9 lines) plus a one-line Status-field edit to the Codex plan.
## 2. Independently confirming the blocking defect Grok/OpenCode found (and I missed)
My own `36148213` review checked code correctness and test results but never inspected whether the archived plan-document *paths* themselves were still intact — a real gap in my prior pass. Re-derived from primary evidence, not from trusting either NOT-PASS report's narrative:
- `git show HEAD:.agents/reports/implementation_plan.md` → the tracked HEAD content is the **full 249-line OpenCode Rev.3 plan**.
- Before this fix, `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (the document that's supposed to be the OpenCode Rev.3 plan's *permanent, namespaced* archive per its own Rev.3 changelog text) was only a 16-line stub reading "**Durable output (updated in place)**: [.agents/reports/implementation_plan.md]" — i.e. it pointed at the shared mirror instead of containing the content itself.
- I (as Planner, in job `e30e3123`) overwrote `.agents/reports/implementation_plan.md` with the Codex plan. Since the stub was the *only* other reference, this action would have deleted the OpenCode Rev.3 plan's full text from the working tree entirely (recoverable only via `git show HEAD:...`, not from any live path) — a genuine 유실 (data-loss) defect, and, per `reviewer-opencode-01`'s report, the same recurring failure class as two earlier incidents in this repo's history (`aca0b7e8`, job `56120ad3`'s F1).
## 3. Verifying the remediation is complete and correct — not just present
- `git diff -- .agents/reports/planner-reviewer-claude-01/plan-de667f0f.md`: the file now contains a 4-line `> [!NOTE]` banner followed by the complete original OpenCode Rev.3 text.
- Byte-level check: `diff <(tail -n +5 <git-show-HEAD-copy>) <(tail -n +5 plan-de667f0f.md)` → **empty diff** — the restored body is character-for-character identical to the original tracked content, not a paraphrase or a partial/garbled restore. Line counts confirm exactly (249 original + 4-line banner = 253).
- `.agents/reports/implementation_plan.md` still holds the Codex plan (the mirror pairing for the *current* plan is preserved) — `diff` against `.agents/reports/planner-reviewer-claude-01/plan-e30e3123.md` returns empty, confirming the two stay in lockstep as intended.
- The Codex plan's Status line was updated from the stale "Plan only — no code changed in the repo" to "Implemented (v4.2.0) — full code, tests, and version lockstep present in repo" — resolving `reviewer-opencode-01`'s third (non-blocking) point.
- Swept for the same fragile pattern elsewhere: `grep -rl "Durable output (updated in place)"` finds 4 more stub-style archives (`plan-264c3b5d.md`, `plan-a33a133e.md`, `plan-28f9b565.md`, `plan-a348e91c.md`), but none of them point at `implementation_plan.md` or any other file currently targeted by an active mirror pattern — they're a pre-existing, unrelated hygiene concern, not a live risk this diff introduces or needs to fix. Noting it for awareness, not counting it against this PR.
## 4. Re-confirming the underlying codex implementation is still sound
Did not simply assume "unchanged code = still correct" — re-ran verification fresh against the current tree:
- `pytest tests/test_version_consistency.py tests/test_a4_adapter_contract.py -k "codex or version"` → 6 passed.
- Full suite, run by me just now: `pytest tests/ -q` → **462 passed, 0 failed, 819.92s (13m39s)**.
- Version lockstep re-confirmed at `4.2.0` (`lib.sh:32`, `VERSIONS.md`, all 8 `SKILL.md`).
## 5. Standing minor finding (unchanged, still non-blocking)
The dormant escaping bug I flagged in `36148213` (`tests/conftest.py`'s codex mock writes a literal `\\n` instead of a real newline in the mock rollout JSONL, ~line 658) is still present and still dormant for the same reason (`verify_artifact()`'s DB-first path never parses that file in any current test). Not fixed in this round; still recommend a one-line follow-up, still not blocking.
---
## 6. Verdict
The blocking defect that correctly earned this diff two NOT PASS votes has been remediated, and I independently verified the fix is complete, byte-faithful, and doesn't introduce any new problem — not merely that a fix file exists. The underlying codex implementation remains fully correct per a fresh full-suite run (462/462). One pre-existing minor test-fixture nit remains open as a non-blocking follow-up.
[VERDICT: PASS]
@@ -0,0 +1,69 @@
# Cross-review: Codex (`openai/codex`) Rev.2 plan + v4.2.0 implementation (re-review)
- **Reviewer**: `reviewer-creator-grok-01`
- **Job**: `607ee97f` (re-review after `5bfd80b8` NOT PASS)
- **Prior finding**: F1 — OpenCode Rev.3 유실 at `.agents/reports/implementation_plan.md` (`plan-de667f0f.md` was a 16-line stub).
- **Diff**: 39 files +1223/−277, plus untracked `codex.py` and `plan-e30e3123.md`.
---
## 1. Prior F1 — fixed
Independent check (not taken from the creator's word):
| Path | Now |
|---|---|
| `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` | 253 lines. 4-line NOTE names it the durable OpenCode Rev.3 archive (job `78f332a9`) and notes the mirror now holds Codex. Body after the NOTE is **byte-identical** to `git show HEAD:.agents/reports/implementation_plan.md` (249 lines). |
| `.agents/reports/implementation_plan.md` | Codex Rev.2. `cmp` identical to `plan-e30e3123.md`. |
| `.agents/reports/planner-reviewer-claude-01/plan-e30e3123.md` | Full Codex Rev.2 (454 lines). |
| Root `implementation_plan.md` | NATS *MAM 메시징 백플레인 전환 실행 로드맵* intact. |
| Codex **Status** | `Implemented (v4.2.0) — full code, tests, and version lockstep present in repo.` (was “Plan only — no code changed”). |
Required fix from `5bfd80b8` items 1–4 all landed. OpenCode Rev.3 is no longer only in git history.
The archived OpenCode body's own “Durable Path” line still lists `.agents/reports/implementation_plan.md` as a mirror — that is the original Rev.3 sentence, preserved because the body is identical. The NOTE header supersedes it. Residual nit, not 유실.
---
## 2. Codex implementation (unchanged, still correct)
Spot-checked after the archive edit; wiring was not regressed.
| Item | Result |
|---|---|
| Adapter | Untracked `codex.py` (158 lines). Class A spawn; `resume_spec` with positional UUID after `--dangerously-bypass-approvals-and-sandbox`; `_state_db()` `state_*.sqlite` glob by mtime; DB-first / glob fallback; `purge_artifacts` `codex delete <uuid> --force` then file+SQL fallback. `py_compile` OK. |
| Challenge 1 | `orc_onboard.sh:133-134` flag-tolerant regex; `codex) env_var=""`; `test_orc_onboard_codex_flag_tolerant_regex` present. |
| Challenge 2 | No hardcoded `state_5` in the adapter; dedicated `# === drift C (codex) ===` with PRAGMA-tolerant order column. |
| Class A resume | `resume_session.sh:65` `agy\|hermes\|opencode\|codex) FRESH_SPAWN=1`. |
| Spawn | `create_session.sh` `agy\|hermes\|grok\|codex)` — not folded into OpenCode's `OPENCODE_PERMISSION` branch. Correct. |
| Shell | `bash -n` on create/resume/stop/orc_onboard/reconcile/status — OK. |
| SemVer | `lib.sh:32` `MAM_VERSION="4.2.0"`; `VERSIONS.md` header/date/8-row matrix/changelog; 8× SKILL.md `version: 4.2.0`. MINOR class matches OpenCode v4.1.0. |
Live CLI facts from the previous pass still hold on this machine (`codex-cli 0.153.4`; `--force` exists; `updated_at` is unix seconds).
---
## 3. Tests
```
pytest tests/test_a4_adapter_contract.py tests/test_c1_tui_readiness.py \
tests/test_tier1_unit.py tests/test_version_consistency.py \
tests/test_tier2_component.py -q --tb=line
→ 160 passed in 316.03s
```
---
## 4. Residuals (non-blocking)
1. **TUI empirical gate still open.** `ready_tokens='Codex|Ask Codex to do anything'` remains a candidate. Do not tick plan §7 from this review.
2. **`auth_ok` uses `HOME_DIR`/`HOME`**, not `ctx` — same as grok/agy/opencode; tests patch `HOME_DIR`.
3. **`test_codex_purge_artifacts` exercises fallback only** (`u-del` is not a UUID, so `codex delete --force` fails). CLI-success path untested.
4. **Resume `SKILL.md` Workflow** still interpolates `$UUID` into `CMD_FULL` after the Class A empty-UUID branch. Real `resume_session.sh` uses spawn-spec. Pre-existing nit (`901bfbe8`).
5. Untracked `.agents/reports/reviewer-opencode-01/report-9cc33e22.md` is a peer review written against the pre-archive tree (that report's NOT PASS is now stale). Not a Codex code defect.
No `[ESCALATE: PLANNER]`.
---
[VERDICT: PASS]
@@ -0,0 +1,38 @@
# 🔍 Cross-Review: Codex Integration — F1 Remediation Round (Job 6e55fa1e)
- **Reviewer**: `reviewer-opencode-01` (role: reviewer)
- **Target**: The same `support-codex` working tree as my job `9cc33e22` NOT PASS, now with the F1 remediation applied (brief's diff was truncated at 221KB/3239 lines — I inspected the working tree directly, as the brief instructed).
- **Method**: F1 remediation verified byte-level against HEAD's Rev.3 content; code state re-confirmed identical to what I fully verified in `9cc33e22` (462/462 full suite); targeted regression re-run this round.
---
## 1. F1 (blocking in `9cc33e22`): OpenCode Rev.3 prior-art clobbering — ALL REMEDIATION ITEMS VERIFIED
My prior NOT PASS required three mechanical fixes (convergent with Grok's `5bfd80b8`). All are applied:
1. **Rev.3 restored to its namespaced path** — `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` is now 253 lines: a 3-line `[!NOTE]` header (honestly explaining that the mirror now houses the Codex plan) followed by the **full Rev.3 body, byte-identical to `git show HEAD:.agents/reports/implementation_plan.md`** (249/249 lines, verified programmatically — the only difference is the intentional note header). The OpenCode Rev.3 plan — the executed rationale for the shipped v4.1.0 integration — is durably preserved at the exact path its own Rev.3 changelog convention named.
2. **Codex plan kept at both paths** — `implementation_plan.md` and `plan-e30e3123.md` are byte-identical (verified with `diff`, exit 0). The pairing convention is intact.
3. **Status line synced** — both Codex plan copies now read `**Status**: Implemented (v4.2.0) — full code, tests, and version lockstep present in repo.` — no longer contradicting the tree (the old "Plan only — no code changed" line was stale the moment the implementation landed).
## 2. Code state — unchanged from my full verification in `9cc33e22`
The working-tree code diff vs HEAD is exactly what I fully verified last round (the stat-sheet line-count differences vs the previous brief are base-artifact variance, not new edits — confirmed by inspecting the actual `lib.sh` diff hunks: the same 5 codex touchpoints + version line, nothing more):
- Adapter `codex.py` (verified line-by-line last round: dynamic `state_*.sqlite` glob, DB-first/glob-fallback, `codex delete --force` purge, Class A specs).
- 46-point wiring complete; Class B untouched; flag-tolerant regex landed + regression-locked.
- Lockstep at v4.2.0 MINOR (correct precedent — new agent = v4.1.0 opencode case).
- **Full suite 462/462 in my own run** (`9cc33e22`, 955s) — no code changed since, so that result stands; this round I re-ran the lockstep + a4 contract suites (**31 passed**) and the codex tier-2 lifecycle test (**1 passed**) as spot confirmation.
- The `new_agent_types_roadmap.md` Codex row now correctly reads "Implemented (v4.2.0) / Done" with the verified storage/auth/token facts — the speculative row is superseded as planned.
## 3. Process notes
- The brief's diff was truncated (over the 200KB limit) and explicitly instructed reviewing the working tree directly — done; the file-level summary matched what I found on disk.
- My own `report-9cc33e22.md` appears as an untracked file in this diff — the standard report-archival convention, to be committed with the next docs commit.
- The other new jobs in this window (`e20c098f`, `feb11a70`, `05fa8dd2`) are sandbox worker fixtures, not peer reviews — not cited as evidence.
- Minor observation: the remediation note header in `plan-de667f0f.md` leaves one extra blank line vs HEAD's body — cosmetic, intentional-looking (header separation), immaterial.
## 4. Verdict
The single blocking defect from my `9cc33e22` NOT PASS (and Grok's convergent `5bfd80b8`) is fully and correctly remediated: the OpenCode Rev.3 plan is durably restored at its designated namespaced path (byte-identical body), the Codex plan correctly occupies both its mirror and its own archive path, and the stale Status line is synced. The Codex integration itself was already verified sound last round (462/462 full suite, live CLI ground truth, challenge fixes landed and regression-locked), and spot regression this round confirms nothing regressed. No lint, functionality, or data-loss issues remain; no planner escalation.
[VERDICT: PASS]
@@ -0,0 +1,58 @@
# 🔍 Cross-Review: Codex CLI Agent Integration & v4.2.0 Bump (Job 9cc33e22)
- **Reviewer**: `reviewer-opencode-01` (role: reviewer)
- **Target**: Branch `support-codex` working tree implementing `plan-e30e3123.md` Rev.2 (the Codex 6th-agent integration + v4.2.0 MINOR bump): 34 modified + 2 new files, 46-point wiring table, plus the plan documents.
- **Method**: Every plan claim re-verified against live source, the live `codex` CLI (0.153.4) on this machine, my own test runs (full 462-test suite), and my own re-execution of the challenge-critical regex. Peer verdicts (Claude PASS `36148213`, Grok NOT PASS `5bfd80b8`) checked against primary evidence — they conflict, and I resolved the conflict myself.
---
## 1. Codex implementation — verified correct (multi-layer)
**My verification runs**:
| Check | Result |
|---|---|
| `bash -n` all 6 touched shell scripts + `py_compile codex.py` | OK |
| `pytest tests/test_version_consistency.py` | **2 passed** — lockstep at 4.2.0 |
| `pytest tests/test_a4_adapter_contract.py` | **29 passed** (was 25; +4 codex) |
| Full suite `pytest tests/ -q` | **462 passed / 0 failed** (955s, my own run) — 455 prior + 7 new codex tests |
| Collect-only | 462 tests — count matches |
**Adapter** (`codex.py`, 158 lines): matches the Rev.2 plan sketch exactly — `state_*.sqlite` glob with mtime-max (no hardcoded suffix, per the accepted challenge refinement), DB-first/glob-fallback rollout resolution, `verify_artifact` using the DB row's `cwd` when available, `purge_artifacts` shelling `codex delete <uuid> --force` first with manual file+DB-row fallback, Class A spawn/resume specs (`--dangerously-bypass-approvals-and-sandbox`). The facts bridge emits the exact contract values.
**Live CLI ground truth** (this machine): `codex-cli 0.153.4` confirmed; `~/.codex/state_5.sqlite` exists with the exact `threads(id, cwd, rollout_path)` schema and 2 real rows matching the 2 real rollout JSONL files — the plan's §1.5 dual-layer storage description is accurate, and the adapter's DB-first order mirrors Codex's own reconciliation literals.
**Flag-tolerant regex** (challenge item 1): I re-ran both regexes myself against `codex resume --dangerously-bypass-approvals-and-sandbox <uuid>` — Rev.1's regex returns empty (exit 1, defect confirmed), the landed regex correctly extracts the UUID (exit 0). The regression test `test_orc_onboard_codex_flag_tolerant_regex` locks it down, and the wiring landed at `orc_onboard.sh:133-135` exactly as planned.
**Wiring spot-checks** (from the 46-point table): lib.sh kind mapping (`*-creator-codex`), `send_keys_safe` `_sks_agent="codex"`, create auth preflight (line 136), FRESH_SPAWN Class A arm (`resume_session.sh:65`), stop capture (`codex_session_id_own = captured`), reconcile drift-C codex block (state-DB-first with a schema-tolerant order-column PRAGMA check — a defensive improvement beyond the plan), status.sh codex branches in both formatters — all present and correct. Class B (claude/grok) paths untouched.
**SemVer v4.2.0 MINOR**: correct — new agent backend = the v4.1.0 (opencode) precedent exactly. Lockstep complete (lib.sh:32, VERSIONS.md header/date/matrix/changelog, 8× SKILL.md at 4.2.0), and the header's `기준 브랜치: support-codex` honestly reflects the working branch.
## 2. The blocking defect — prior-art clobbering at `.agents/reports/implementation_plan.md`
Grok (`5bfd80b8`, NOT PASS) flagged this; Claude's PASS (`36148213`) did not address it. I independently confirmed Grok is right:
- **HEAD's** `.agents/reports/implementation_plan.md` is the full **OpenCode Rev.3 plan** (249 lines, tracked since the v4.1.0 integration) — the executed rationale for the opencode backend that shipped.
- The working tree **replaces it wholesale** with the Codex Rev.2 plan (391 lines).
- The Rev.3 changelog's own convention says the plan is "permanently housed under `plan-de667f0f.md` and **mirrored** at `implementation_plan.md`" — but `plan-de667f0f.md` is only a **16-line stub** report pointing at the mirror. The mirror was the *only* full copy; this change deletes it from the tree.
- Verified: no working-tree file contains the OpenCode Rev.3 full text (grep for its title across all `.md` — empty). Git history retains it, but the durable named path loses it.
- This is the same 유실/prior-art-clobbering class as the `aca0b7e8` round (v4.0.0 consensus artifact) and job `56120ad3`'s F1 (OpenCode Rev.2 overwrote the root NATS plan) — a recurring failure mode this session has rejected every time.
**Required fix (mechanical, not design-level — Grok's proposed remediation is correct and I concur)**:
1. Restore the full OpenCode Rev.3 into the namespaced path: `git show HEAD:.agents/reports/implementation_plan.md > .agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (optionally with a 2-line header noting the mirror now holds the Codex plan).
2. Keep the Codex plan at both `implementation_plan.md` and `plan-e30e3123.md` (that pairing is correct).
3. Update the Codex plan's Status line ("Plan only — no code changed in the repo") — it now contradicts the tree, which contains the full implementation.
**Notably**: Claude is the author of both plans (the overwritten Rev.3 and the overwriting Rev.2) and its PASS review focused on the code wiring (which is genuinely correct) but did not examine the archive-path loss. Grok's review is the one that checked the durable-paths claim — and its finding survives my independent verification.
## 3. Secondary observations (non-blocking)
- Claude's PASS also flagged (correctly, in its §"self-report citations") that the Worker's self-report contains several wrong file:line citations — the code is right but the report's citations don't all check out. Worth fixing for the record but not blocking.
- `conftest.py`'s codex mock seeds `state_5.sqlite` with a fixed name — fine for a hermetic sandbox (the adapter globs `state_*.sqlite`, so any name works); no portability concern.
- The drift-C codex block's schema-tolerant `PRAGMA table_info` order-column detection is *more* defensive than the plan required — a good hardening against Codex's evolving schema.
## 4. Verdict
The Codex integration itself is exemplary: plan verified against live CLI facts, the challenge round caught a genuine regex defect (now fixed, regression-locked, and re-verified by me) and a genuine storage-layer gap (now handled with DB-first resolution and CLI-owned purge), the 46-point wiring is complete and correct, Class B contracts untouched, lockstep holds at 4.2.0, and the full suite passes 462/462 in my own run. But the diff also clobbers the tracked OpenCode Rev.3 plan from its only full-copy durable path — a prior-art 유실 defect of the same class this session has consistently rejected, confirmed by me from primary evidence and not addressed by the plan author's own review. It is mechanically fixable (restore Rev.3 to its namespaced path, keep the Codex pairing, sync the Status line) without touching any code.
[VERDICT: NOT PASS]
+6 -2
View File
@@ -29,7 +29,7 @@ export WORKSPACE_ROOT
# 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.
# NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable.
MAM_VERSION="4.1.3"
MAM_VERSION="4.2.0"
export MAM_VERSION
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
@@ -667,17 +667,20 @@ print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens))
*-creator-hermes|*-planner-hermes|*-reviewer-hermes) kind="hermes" ;;
*-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok" ;;
*-creator-opencode|*-planner-opencode|*-reviewer-opencode) kind="opencode" ;;
*-creator-codex|*-planner-codex|*-reviewer-codex) kind="codex" ;;
*)
if echo "$name" | grep -qi "agy"; then kind="agy"
elif echo "$name" | grep -qi "claude"; then kind="claude"
elif echo "$name" | grep -qi "hermes"; then kind="hermes"
elif echo "$name" | grep -qi "grok"; then kind="grok"
elif echo "$name" | grep -qi "opencode"; then kind="opencode"
elif echo "$name" | grep -qi "codex"; then kind="codex"
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 "hermes"; then kind="hermes"
elif echo "${final_cmd:-}" | grep -qi "grok"; then kind="grok"
elif echo "${final_cmd:-}" | grep -qi "opencode"; then kind="opencode"
elif echo "${final_cmd:-}" | grep -qi "codex"; then kind="codex"
fi
;;
esac
@@ -691,7 +694,7 @@ try:
tokens = shlex.split(cmd)
if tokens:
first = tokens[0]
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))):
if first in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex') or any(first.endswith('/' + a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex')) or (kind and (first == kind or first.endswith('/' + kind))):
tokens = tokens[1:]
print(' '.join(shlex.quote(t) for t in tokens))
except Exception:
@@ -2173,6 +2176,7 @@ send_keys_safe() {
*-hermes|*-hermes-[0-9]*|hermes|hermes-[0-9]*) _sks_agent="hermes" ;;
*-grok|*-grok-[0-9]*|grok|grok-[0-9]*) _sks_agent="grok" ;;
*-opencode|*-opencode-[0-9]*|opencode|opencode-[0-9]*) _sks_agent="opencode" ;;
*-codex|*-codex-[0-9]*|codex|codex-[0-9]*) _sks_agent="codex" ;;
esac
if [ -n "$_sks_agent" ]; then
eval "$("$(_delegate_py_bin)" -m lib_py.agents facts "$_sks_agent" 2>/dev/null || true)"
@@ -0,0 +1,158 @@
import os, json, glob, sqlite3, subprocess
from typing import Optional, Any, List
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext
from lib_py.verify_session import workspace_key
class CodexAgentAdapter(BaseAgentAdapter):
@property
def name(self) -> str:
return 'codex'
@property
def own_key(self) -> str:
return 'codex_session_id_own'
@property
def ready_tokens(self) -> str:
return 'Codex|Ask Codex to do anything'
@property
def exit_key(self) -> str:
return 'Ctrl+C'
@property
def delegate_agent_key(self) -> str:
return 'codex-cli'
@property
def identity_cache_fields(self) -> tuple:
return ('session_id',)
@property
def input_placeholder(self) -> Optional[str]:
return 'Ask Codex to do anything'
def _codex_home(self, ctx: DiscoveryContext) -> str:
return os.environ.get('CODEX_HOME') or f"{ctx.home_dir}/.codex"
def _state_db(self, ctx: DiscoveryContext) -> Optional[str]:
matches = glob.glob(f"{self._codex_home(ctx)}/state_*.sqlite")
if not matches:
return None
return max(matches, key=os.path.getmtime)
def _find_rollout_via_db(self, uuid: str, ctx: DiscoveryContext):
"""Returns (rollout_path, cwd) from the state DB, or (None, None)."""
db = self._state_db(ctx)
if not db or not os.path.exists(db):
return None, None
try:
conn = sqlite3.connect(db)
row = conn.execute("SELECT rollout_path, cwd FROM threads WHERE id=?", (uuid,)).fetchone()
conn.close()
if row and row[0] and os.path.exists(row[0]):
return row[0], row[1]
except Exception:
pass
return None, None
def _find_rollout(self, uuid: str, ctx: DiscoveryContext) -> Optional[str]:
path, _ = self._find_rollout_via_db(uuid, ctx)
if path:
return path
matches = glob.glob(f"{self._codex_home(ctx)}/sessions/*/*/*/rollout-*-{uuid}.jsonl")
return matches[0] if matches else None
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
return self._find_rollout(uuid, ctx) or f"{self._codex_home(ctx)}/sessions/rollout-{uuid}.jsonl"
def verify_artifact(self, uuid: str, ctx: DiscoveryContext) -> bool:
db_path, db_cwd = self._find_rollout_via_db(uuid, ctx)
path = db_path or self._find_rollout(uuid, ctx)
if not path or not os.path.exists(path):
return False
if ctx.epoch and os.path.getmtime(path) < ctx.epoch:
return False
found_cwd = db_cwd
if not found_cwd:
try:
with open(path) as f:
first = f.readline()
found_cwd = json.loads(first).get('payload', {}).get('cwd')
except Exception:
return False
if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd):
return False
return True
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> List[str]:
purged = []
path = self._find_rollout(uuid, ctx)
try:
res = subprocess.run(['codex', 'delete', uuid, '--force'],
capture_output=True, text=True, timeout=5)
if res.returncode == 0:
if path:
purged.append(path)
return purged
except Exception:
pass
if path and os.path.exists(path):
os.remove(path)
purged.append(path)
db = self._state_db(ctx)
if db and os.path.exists(db):
try:
conn = sqlite3.connect(db)
conn.execute("DELETE FROM threads WHERE id=?", (uuid,))
conn.commit()
conn.close()
purged.append(f"state db row for thread: {uuid}")
except Exception:
pass
return purged
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
return f"{binary} --dangerously-bypass-approvals-and-sandbox"
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
if materialized and session_uuid:
return f"{binary} resume --dangerously-bypass-approvals-and-sandbox {session_uuid}"
return f"{binary} --dangerously-bypass-approvals-and-sandbox"
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
home = os.environ.get('HOME_DIR') or os.environ.get('HOME') or os.path.expanduser('~')
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
return os.path.exists(f"{codex_home}/auth.json") or bool(os.environ.get('OPENAI_API_KEY'))
def discover(self, ctx: DiscoveryContext) -> List[str]:
db = self._state_db(ctx)
if db and os.path.exists(db):
try:
conn = sqlite3.connect(db)
cur = conn.cursor()
cur.execute("PRAGMA table_info(threads)")
cols = {r[1] for r in cur.fetchall()}
order_col = "updated_at" if "updated_at" in cols else ("created_at" if "created_at" in cols else None)
order_clause = f"ORDER BY {order_col} DESC" if order_col else ""
rows = conn.execute(
f"SELECT id, cwd FROM threads {order_clause} LIMIT 50"
).fetchall()
conn.close()
candidates = []
for uuid, cwd in rows:
if ctx.workspace and cwd and workspace_key(cwd) != workspace_key(ctx.workspace):
continue
if uuid and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
return candidates
except Exception:
pass
root = self._codex_home(ctx)
candidates = []
paths = sorted(glob.glob(f"{root}/sessions/*/*/*/rollout-*.jsonl"), key=os.path.getmtime, reverse=True)[:50]
for path in paths:
uuid = os.path.basename(path)[:-6][-36:]
if uuid and self.verify_artifact(uuid, ctx):
candidates.append(uuid)
return candidates
+2
View File
@@ -7,6 +7,7 @@ from lib_py.agents.adapters.agy import AgyAgentAdapter
from lib_py.agents.adapters.hermes import HermesAgentAdapter
from lib_py.agents.adapters.grok import GrokAgentAdapter
from lib_py.agents.adapters.opencode import OpenCodeAgentAdapter
from lib_py.agents.adapters.codex import CodexAgentAdapter
_ADAPTERS: Dict[str, BaseAgentAdapter] = {
'claude': ClaudeAgentAdapter(),
@@ -14,6 +15,7 @@ _ADAPTERS: Dict[str, BaseAgentAdapter] = {
'hermes': HermesAgentAdapter(),
'grok': GrokAgentAdapter(),
'opencode': OpenCodeAgentAdapter(),
'codex': CodexAgentAdapter(),
}
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]:
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', 'opencode_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', 'codex_session_id_own']
id_to_session = {}
for s in d.get('herdr_sessions', []):
if s.get('status') == 'running':
+1 -1
View File
@@ -66,7 +66,7 @@ def mam_orchestrator_uuids():
def mam_row_own_uuid(row):
if not isinstance(row, dict):
return None
for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "grok_session_id_own", "opencode_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", "codex_session_id_own"]:
v = row.get(k)
if v:
return v
+3 -2
View File
@@ -9,7 +9,8 @@ OWN_KEY = {
'agy': 'agy_conversation_id_own',
'hermes': 'hermes_conversation_id_own',
'grok': 'grok_session_id_own',
'opencode': 'opencode_session_id_own'
'opencode': 'opencode_session_id_own',
'codex': 'codex_session_id_own'
}
from lib_py.paths import resolve_home
@@ -31,7 +32,7 @@ def find_workspace_uuid_main():
if s_item.get('status') == 'running':
if target and s_item.get('name') == target:
continue
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'grok_session_id_own', 'opencode_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', 'codex_session_id_own']:
val = s_item.get(k)
if val:
running_ids.add(val)
@@ -1,7 +1,7 @@
---
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."
version: 4.1.3
version: 4.2.0
author: godopu
license: MIT
platforms: [linux, macos]
@@ -129,7 +129,7 @@ herdr_sessions:
```bash
WORKSPACE=/path/to/project
AGENT=claude # claude | agy | hermes | grok | opencode — always pass it explicitly
AGENT=claude # claude | agy | hermes | grok | opencode | codex — always pass it explicitly
source .agents/skills/lib.sh
SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")"
@@ -163,7 +163,10 @@ case "$AGENT" in
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 ;;
codex)
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "codex --dangerously-bypass-approvals-and-sandbox"
;;
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT"; exit 2 ;;
esac
# 3. Wait for agent TUI to be ready (varies: claude ~5s, agy ~3s)
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# create_session.sh — multi-agent-mux-create 의 부속 스크립트
# Usage:
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper]
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper]
#
# 동작:
# 1) preflight: herdr/claude/agy 가용성, workspace 존재
@@ -26,11 +26,11 @@ source "$_lib_sh"
usage() {
cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> --role <role> [options]
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> --role <role> [options]
Options:
--workspace PATH project directory (required)
--agent AGENT claude | agy | hermes | grok | opencode (required)
--agent AGENT claude | agy | hermes | grok | opencode | codex (required)
--role ROLE assigned role (required)
--session NAME herdr session name (default: derived from workspace)
--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 "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; }
case "$AGENT" in
claude|agy|hermes|grok|opencode) ;;
*) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT" >&2; exit 2 ;;
claude|agy|hermes|grok|opencode|codex) ;;
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT" >&2; exit 2 ;;
esac
[ -n "$ROLE" ] || { echo "ERROR: --role required" >&2; usage; exit 2; }
[ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; }
@@ -133,6 +133,14 @@ elif [ "$AGENT" = "opencode" ]; then
echo "ERROR: opencode is not functional or configured." >&2
exit 1
fi
elif [ "$AGENT" = "codex" ]; then
codex_home="${CODEX_HOME:-$HOME/.codex}"
if [ -f "$codex_home/auth.json" ] || [ -n "${OPENAI_API_KEY:-}" ]; then
true
elif ! codex --version >/dev/null 2>&1; then
echo "ERROR: codex is not functional or configured." >&2
exit 1
fi
fi
# 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A)
@@ -190,6 +198,7 @@ if [ -z "$CMD_FULL" ]; then
hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;;
grok) CMD_FULL="${RESOLVED_BIN} --permission-mode bypassPermissions" ;;
opencode) CMD_FULL="${RESOLVED_BIN} --auto --agent build" ;;
codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;
esac
fi
@@ -210,7 +219,7 @@ spawn() {
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
fi
;;
agy|hermes|grok)
agy|hermes|grok|codex)
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
;;
opencode)
@@ -219,7 +228,7 @@ spawn() {
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 ;;
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT" >&2; exit 2 ;;
esac
}
@@ -328,6 +337,7 @@ if [ -n "$SUBMIT_JOB_PROMPT" ]; then
agy) delegate_agent="antigravity-cli" ;;
grok) delegate_agent="grok-build" ;;
opencode) delegate_agent="opencode-cli" ;;
codex) delegate_agent="codex-cli" ;;
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
esac
fi
@@ -442,6 +452,13 @@ elif agent == 'opencode':
entry['session_id_source'] = 'pending-discovery'
entry['session_id_verified'] = False
entry['last_visible_status'] = "unverified"
elif agent == 'codex':
cp = os.environ.get('CHILD_PID', '0')
entry['child_pid'] = int(cp) if cp.isdigit() else 0
entry['codex_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', '')
if last_vis_override:
@@ -1,7 +1,7 @@
---
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."
version: 4.1.3
version: 4.2.0
author: godopu
license: MIT
platforms: [linux, macos, windows]
+2 -2
View File
@@ -1,14 +1,14 @@
---
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."
version: 4.1.3
version: 4.2.0
author: godopu
license: MIT
platforms: [linux, macos]
environments: [terminal, herdr]
metadata:
hermes:
tags: [agent, herdr, multi-agent, loop, planning, review, orchestrator, grok]
tags: [agent, herdr, multi-agent, loop, planning, review, orchestrator, grok, opencode, codex]
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-delegate-job]
prereq_skills: [multi-agent-mux-create]
---
@@ -1,14 +1,14 @@
---
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."
version: 4.1.3
version: 4.2.0
author: godopu
license: MIT
platforms: [linux, macos]
environments: [terminal, herdr]
metadata:
hermes:
tags: [agent, herdr, claude, antigravity, agy, grok, monitor, observation, reconciliation]
tags: [agent, herdr, claude, antigravity, agy, grok, opencode, codex, monitor, observation, reconciliation]
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-status]
prereq_skills: [multi-agent-mux-create]
---
@@ -520,7 +520,7 @@ if herdr_confirmed:
agent = None
role = 'creator'
for r_name in ('creator', 'planner', 'reviewer'):
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
if name.endswith(f"-{r_name}-{a_name}"):
role = r_name
agent = a_name
@@ -539,7 +539,7 @@ if herdr_confirmed:
if tok.startswith('MAM_MANAGED='):
managed_path = tok.split('=', 1)[1]
if os.path.realpath(managed_path) == os.path.realpath(workspace_root):
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
if a_name in pm_check.get('cmd', '') or a_name in pm_check.get('cmd_full', ''):
agent = a_name
break
@@ -547,6 +547,10 @@ if herdr_confirmed:
except Exception:
pass
if not agent:
if pm_check and pm_check.get('cmd') in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
agent = pm_check['cmd']
if not agent:
continue
@@ -603,6 +607,9 @@ if herdr_confirmed:
elif agent == 'opencode':
entry['child_pid'] = 0
entry['opencode_session_id_own'] = None
elif agent == 'codex':
entry['child_pid'] = 0
entry['codex_session_id_own'] = None
d.setdefault('herdr_sessions', []).append(entry)
yaml_session_names.add(name)
drifts.append({'class': 'B', 'name': name,
@@ -613,7 +620,7 @@ def row_agent(s):
return agent_of_row(s)
OWN_KEY_BY_AGENT = {
a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode')
a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex')
}
# === drift C0: 지정된 ID 는 발견이 아니라 '확인'만 필요하다 ===
@@ -896,6 +903,88 @@ for s in d.get('herdr_sessions', []):
else:
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=True)
# === drift C (codex): codex 새 session id materialize (per-row own id) ===
for s in d.get('herdr_sessions', []):
if row_agent(s) != 'codex':
continue
if s.get('status') != 'running':
continue
if s.get('codex_session_id_own'):
continue
cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd:
continue
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
epoch_threshold = s.get('herdr_session_epoch', 0)
sibling_claimed = [
other.get('codex_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('codex_session_id_own')
]
s_eval = dict(s)
s_eval['_sibling_claimed_uuids'] = sibling_claimed
valid_candidates = []
try:
# Rev.2: state DB first (indexed, single query) — dynamic filename,
# no hardcoded suffix (see §1.5 / §2 _state_db()).
state_matches = glob.glob(f"{codex_home}/state_*.sqlite")
state_db = max(state_matches, key=os.path.getmtime) if state_matches else None
rows = []
if state_db and os.path.exists(state_db):
try:
conn = sqlite3.connect(state_db)
cols = [c[1] for c in conn.execute("PRAGMA table_info(threads)").fetchall()]
order_col = "updated_at" if "updated_at" in cols else ("created_at" if "created_at" in cols else None)
if order_col:
rows = conn.execute(
f"SELECT id, {order_col} FROM threads WHERE cwd=? ORDER BY {order_col} DESC LIMIT 20",
(cwd,)
).fetchall()
else:
rows = [(r[0], int(time.time())) for r in conn.execute("SELECT id FROM threads WHERE cwd=? LIMIT 20", (cwd,)).fetchall()]
conn.close()
except Exception:
rows = []
if not rows:
# Fallback: bounded glob over the date-nested tree
paths = sorted(
glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*.jsonl"),
key=os.path.getmtime, reverse=True
)[:50]
rows = [(os.path.basename(p)[:-6][-36:], os.path.getmtime(p)) for p in paths]
for uuid, ts in rows:
if epoch_threshold and ts and ts < epoch_threshold:
continue
if uuid in (sibling_claimed or []):
continue
if verify_session_uuid(cwd, 'codex', 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"]}" "codex" "{cwd}"']
rc = subprocess.run(cmd).returncode
if rc == 0:
_pin_and_verify_resume(s, 'codex', 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, 'codex', cwd, uuid, degraded=True)
result = {
'timestamp': now_iso,
'yaml_path': yaml_path,
@@ -1,14 +1,14 @@
---
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."
version: 4.1.3
version: 4.2.0
author: godopu
license: MIT
platforms: [linux, macos]
environments: [terminal, herdr]
metadata:
hermes:
tags: [agent, herdr, claude, antigravity, agy, hermes, grok, opencode, orchestrator, onboard, isolation]
tags: [agent, herdr, claude, antigravity, agy, hermes, grok, opencode, codex, orchestrator, onboard, isolation]
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-monitor]
prereq_skills: [multi-agent-mux-create]
---
@@ -32,13 +32,14 @@ Registers an orchestrator session UUID into the `orchestrator_uuids` list of `.m
## 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`, `opencode`) 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`, `codex`) in the following order:
1. **CLI `argv`**:
- `claude -r <uuid>` / `claude --session-id <uuid>`
- `agy --conversation <uuid>`
- `grok --session-id <uuid>` / `grok --resume <uuid>`
- `opencode --session <uuid>`
- `codex resume <uuid>`
2. **Family-Matched Environment Variables**:
- `claude` → `CLAUDE_CODE_SESSION_ID`
- `agy` → `ANTIGRAVITY_CONVERSATION_ID`
@@ -103,7 +103,7 @@ detect_nearest_agent() {
local base
base="$(basename "$tok")"
case "$base" in
claude|agy|hermes|grok|opencode)
claude|agy|hermes|grok|opencode|codex)
match="$base"
break
;;
@@ -130,6 +130,9 @@ detect_nearest_agent() {
opencode)
detected_id=$(echo "$cmd_line" | grep -oE '(--session|-s)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--session|-s)[[:space:]=]+//' || true)
;;
codex)
detected_id=$(echo "$cmd_line" | grep -oE 'resume[[:space:]]+.*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | grep -oE '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | head -n 1 || true)
;;
esac
if [ -n "$detected_id" ] && is_valid_id "$detected_id"; then
@@ -145,6 +148,7 @@ detect_nearest_agent() {
hermes) env_var="${HERMES_SESSION_ID:-}" ;;
grok) env_var="${GROK_SESSION_ID:-}" ;;
opencode) env_var="${OPENCODE_SESSION_ID:-}" ;;
codex) env_var="" ;;
esac
if [ -n "$env_var" ] && is_valid_id "$env_var"; then
@@ -1,7 +1,7 @@
---
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."
version: 4.1.3
version: 4.2.0
author: godopu
license: MIT
platforms: [linux, macos]
@@ -49,16 +49,16 @@ ideal resume path:
`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` / `opencode_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` / `codex_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()`)
If both are empty → for Class A agents (`agy`, `hermes`, `opencode`), `resume_session.sh` automatically falls back to `FRESH_SPAWN=1` (`spawn-spec`). For Class B agents (`claude`, `grok`), fall back to `multi-agent-mux-create`.
If both are empty → for Class A agents (`agy`, `hermes`, `opencode`, `codex`), `resume_session.sh` automatically falls back to `FRESH_SPAWN=1` (`spawn-spec`). For Class B agents (`claude`, `grok`), fall back to `multi-agent-mux-create`.
## Workflow
```bash
WORKSPACE=/path/to/project
AGENT=claude # claude | agy | hermes | grok | opencode — pass it explicitly
AGENT=claude # claude | agy | hermes | grok | opencode | codex — pass it explicitly
SESSION_NAME=<workspace>-creator-<agent> # same convention as multi-agent-mux-create
# Resolve the isolated herdr server name & load common utils
@@ -70,7 +70,7 @@ UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh
if [ -z "$UUID" ]; then
case "$AGENT" in
agy|hermes|opencode)
agy|hermes|opencode|codex)
: # resume_session.sh handles fresh-spawn fallback automatically (FRESH_SPAWN=1)
;;
*)
@@ -97,6 +97,7 @@ case "$AGENT" in
hermes) CMD_FULL="hermes --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
grok) CMD_FULL="grok --resume $UUID --permission-mode bypassPermissions" ;;
opencode) CMD_FULL="opencode --session $UUID --auto --agent build" ;;
codex) CMD_FULL="codex resume --dangerously-bypass-approvals-and-sandbox $UUID" ;;
esac
# 4. Spawn new herdr session + run agent with the saved id
@@ -107,7 +108,7 @@ case "$AGENT" in
# auto-handle trust / bypass dialogs
handle_startup_dialogs "$SESSION_NAME" 20
;;
agy|hermes|grok|opencode)
agy|hermes|grok|opencode|codex)
eval "herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
;;
esac
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# resolve_session_id.sh — multi-agent-mux-resume 의 부속 스크립트
# Usage:
# bash resolve_session_id.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode>
# bash resolve_session_id.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex>
# 출력: stdout 으로 UUID 한 줄 (없으면 빈 줄 + exit 0)
#
# P0-C: 전역 agent_identities 를 즉시 반환하지 않는다. lib.sh::find_workspace_uuid
@@ -13,7 +13,7 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
usage() {
cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> [--session <name>]
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> [--session <name>]
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.
EOF
@@ -36,8 +36,8 @@ done
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; }
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; }
case "$AGENT" in
claude|agy|hermes|grok|opencode) ;;
*) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode" >&2; exit 2 ;;
claude|agy|hermes|grok|opencode|codex) ;;
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex" >&2; exit 2 ;;
esac
find_workspace_uuid "$WORKSPACE" "$AGENT" "$SESSION_NAME"
@@ -9,7 +9,7 @@ source "$LIB_SH"
usage() {
cat <<EOF
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> --session <name> [options]
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> --session <name> [options]
Options:
--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 "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; }
case "$AGENT" in
claude|agy|hermes|grok|opencode) ;;
claude|agy|hermes|grok|opencode|codex) ;;
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
esac
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; }
@@ -62,7 +62,7 @@ UUID=$(bash "$(dirname "${BASH_SOURCE[0]}")/resolve_session_id.sh" \
FRESH_SPAWN=0
if [ -z "$UUID" ]; then
case "$AGENT" in
agy|hermes|opencode) FRESH_SPAWN=1 ;;
agy|hermes|opencode|codex) FRESH_SPAWN=1 ;;
*)
echo "ERROR: No saved session for $WORKSPACE ($AGENT). Use multi-agent-mux-create first." >&2
exit 1
@@ -119,6 +119,7 @@ if [ "$FRESH_SPAWN" = "1" ]; then
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;;
opencode) CMD_FULL="${RESOLVED_BIN} --auto --agent build" ;;
codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;
esac
fi
else
@@ -130,6 +131,7 @@ else
hermes) CMD_FULL="${RESOLVED_BIN} --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
grok) CMD_FULL="${RESOLVED_BIN} --resume $UUID --permission-mode bypassPermissions" ;;
opencode) CMD_FULL="${RESOLVED_BIN} --session $UUID --auto --agent build" ;;
codex) CMD_FULL="${RESOLVED_BIN} resume --dangerously-bypass-approvals-and-sandbox $UUID" ;;
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
esac
fi
@@ -4,17 +4,17 @@
# resume UUID 를 per-row own id (claude_session_id_own / agy_conversation_id_own)
# 에 박는다 — agent_identities 전역은 더 이상 primary 아님 (cache 로 강등, P0-C/단계 e).
#
# Usage: bash update_yaml_resumed.sh --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode]
# Usage: bash update_yaml_resumed.sh --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode|codex]
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
usage() {
cat <<EOF
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>] [--cmd-full <cmd>]
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode|codex] [--herdr-session <name>] [--cmd-full <cmd>]
--uuid may be empty for an Issue-3 Class A fresh-spawn resume (agy/hermes/
opencode 0-turn stopped session, no conversation id ever assigned) - pass
opencode/codex 0-turn stopped session, no conversation id ever assigned) - pass
--cmd-full so the display command can be recorded without a uuid to embed.
EOF
}
@@ -235,6 +235,16 @@ elif agent == 'opencode':
cp = os.environ.get('CHILD_PID', '0')
if cp.isdigit() and int(cp) > 0:
target['child_pid'] = int(cp)
elif agent == 'codex':
target['pane']['cmd'] = 'codex'
if uuid:
target['pane']['cmd_full'] = f'codex resume --dangerously-bypass-approvals-and-sandbox {uuid}'
target['codex_session_id_own'] = uuid
else:
target['pane']['cmd_full'] = os.environ.get('CMD_FULL_OPT', '') or 'codex --dangerously-bypass-approvals-and-sandbox'
cp = os.environ.get('CHILD_PID', '0')
if cp.isdigit() and int(cp) > 0:
target['child_pid'] = int(cp)
snap = d.setdefault('snapshot', {})
snap['taken_at'] = now
@@ -1,14 +1,14 @@
---
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."
version: 4.1.3
version: 4.2.0
author: godopu
license: MIT
platforms: [linux, macos]
environments: [terminal, herdr]
metadata:
hermes:
tags: [agent, herdr, claude, antigravity, agy, grok, status, read-only, snapshot]
tags: [agent, herdr, claude, antigravity, agy, grok, opencode, codex, status, read-only, snapshot]
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-monitor]
prereq_skills: [multi-agent-mux-create, multi-agent-mux-monitor]
---
@@ -53,12 +53,12 @@ def resume_on_disk(s):
name = s.get('name', '')
cwd = (s.get('pane') or {}).get('cwd', '')
agent = None
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
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'):
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
if f"-{a}" in name or f"_{a}" in name:
agent = a
break
@@ -125,6 +125,29 @@ def resume_on_disk(s):
except Exception:
return 'MISSING'
return 'no'
if agent == 'codex':
u = s.get('codex_session_id_own')
if u:
codex_home = os.environ.get('CODEX_HOME', f"{home}/.codex")
db_matches = glob.glob(f"{codex_home}/state_*.sqlite")
if db_matches:
db = max(db_matches, key=os.path.getmtime)
try:
import sqlite3
conn = sqlite3.connect(db)
r = conn.execute("SELECT rollout_path FROM threads WHERE id=?", (u,)).fetchone()
conn.close()
if r and r[0] and os.path.exists(r[0]):
return 'yes'
if r:
return 'yes'
except Exception:
pass
m = glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*-{u}.jsonl")
if m and os.path.exists(m[0]):
return 'yes'
return 'MISSING'
return 'no'
return '?'
@@ -231,12 +254,12 @@ def resume_on_disk(s):
name = s.get('name', '')
cwd = (s.get('pane') or {}).get('cwd', '')
agent = None
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
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'):
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
if f"-{a}" in name or f"_{a}" in name:
agent = a
break
@@ -303,6 +326,29 @@ def resume_on_disk(s):
except Exception:
return 'MISSING'
return 'no'
if agent == 'codex':
u = s.get('codex_session_id_own')
if u:
codex_home = os.environ.get('CODEX_HOME', f"{home}/.codex")
db_matches = glob.glob(f"{codex_home}/state_*.sqlite")
if db_matches:
db = max(db_matches, key=os.path.getmtime)
try:
import sqlite3
conn = sqlite3.connect(db)
r = conn.execute("SELECT rollout_path FROM threads WHERE id=?", (u,)).fetchone()
conn.close()
if r and r[0] and os.path.exists(r[0]):
return 'yes'
if r:
return 'yes'
except Exception:
pass
m = glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*-{u}.jsonl")
if m and os.path.exists(m[0]):
return 'yes'
return 'MISSING'
return 'no'
return '?'
+2 -2
View File
@@ -1,7 +1,7 @@
---
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."
version: 4.1.3
version: 4.2.0
author: godopu
license: MIT
platforms: [linux, macos]
@@ -37,7 +37,7 @@ The stop command is always **graceful by default**:
```bash
SESSION_NAME=<workspace>-creator-<agent> # convention
AGENT=claude # claude | agy | hermes | grok | opencode — always pass it
AGENT=claude # claude | agy | hermes | grok | opencode | codex — always pass it
AGENT_SESSIONS_YAML=.mam/agent-sessions.yaml
# 1) Session is registered?
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# stop_session.sh — multi-agent-mux-stop 의 부속 스크립트
# Usage:
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>] \
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|grok|opencode|codex] [--herdr-session <name>] \
# [--reason <reason>] [--purge-conversation] [--yes]
#
# 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고
@@ -12,7 +12,7 @@
#
# 옵션:
# --session <name> — 대상 세션 (필수)
# --agent <type> — claude | agy | hermes | grok | opencode
# --agent <type> — claude | agy | hermes | grok | opencode | codex
# (권장: 항상 명시. 미지정 시 레지스트리 기록으로
# 해석 — agent 필드 → 세션명 접미사 → pane.cmd;
# 셋 다 실패하면 exit 2)
@@ -91,8 +91,8 @@ while [ $# -gt 0 ]; do
done
if [ -n "$AGENT" ]; then
case "$AGENT" in
claude|agy|hermes|grok|opencode) ;;
*) echo "ERROR: invalid agent type '$AGENT'. Allowed types are: claude, agy, hermes, grok, opencode." >&2; exit 2 ;;
claude|agy|hermes|grok|opencode|codex) ;;
*) echo "ERROR: invalid agent type '$AGENT'. Allowed types are: claude, agy, hermes, grok, opencode, codex." >&2; exit 2 ;;
esac
fi
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; usage; exit 2; }
@@ -289,6 +289,8 @@ if captured and not purge:
target['grok_session_id_own'] = captured
elif agent == 'opencode':
target['opencode_session_id_own'] = captured
elif agent == 'codex':
target['codex_session_id_own'] = captured
target['resumable'] = True
if purge and purge_uuid:
+6 -2
View File
@@ -98,13 +98,17 @@
#default: (unset → reads ~/.grok/auth.json)
# 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)
# Optional multi-provider API keys for OpenCode CLI and OpenAI API key for Codex CLI.
#default: (unset → reads ~/.local/share/opencode/auth.json or ~/.codex/auth.json)
# ANTHROPIC_API_KEY=replace_me
# OPENAI_API_KEY=replace_me
# GEMINI_API_KEY=replace_me
# GROQ_API_KEY=replace_me
# Optional override for Codex CLI state / session directory.
#default: $HOME/.codex
# CODEX_HOME=$HOME/.codex
# Optional inline JSON permissions configuration for OpenCode.
#default: (unset → defaults to --auto CLI bypass)
# OPENCODE_PERMISSION='{"*":"allow"}'
+1 -1
View File
@@ -193,6 +193,6 @@ rm -f ".mam/jobs/$JID.json" ".mam/jobs/$JID.lock"
## 7. 에이전트 온보딩 가이드 (New Agent Onboarding)
본 환경 구축을 무사히 마쳤다면, 협업하는 에이전트(`claude`, `agy`, `hermes`, `grok`, `opencode`)는 즉시 .agents/ 디렉터리에 있는 **[MULTI_AGENT_RULES.ko.md](.agents/MULTI_AGENT_RULES.ko.md)** 문서를 읽어야 합니다.
본 환경 구축을 무사히 마쳤다면, 협업하는 에이전트(`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`)는 즉시 .agents/ 디렉터리에 있는 **[MULTI_AGENT_RULES.ko.md](.agents/MULTI_AGENT_RULES.ko.md)** 문서를 읽어야 합니다.
해당 문서에는 에이전트가 각 역할(PM, Worker, Reviewer)로 구동될 때 지켜야 할 **수술적 변경 규칙, 교차 검증 통과 규약, Herdr 뷰포트 유실 방지를 위한 스냅샷 패턴** 등이 서술되어 있어 안정적인 멀티 에이전트 워크플로우에 즉시 기여할 수 있도록 돕습니다.
+1 -1
View File
@@ -194,6 +194,6 @@ rm -f ".mam/jobs/$JID.json" ".mam/jobs/$JID.lock"
## 7. Onboarding Collaborating Agents (New Agent Onboarding)
Once the setup is verified, onboarding agents (`claude`, `agy`, `hermes`, `grok`, `opencode`) should immediately read the **[MULTI_AGENT_RULES.md](.agents/MULTI_AGENT_RULES.md)** guidelines in the .agents/ directory.
Once the setup is verified, onboarding agents (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`) should immediately read the **[MULTI_AGENT_RULES.md](.agents/MULTI_AGENT_RULES.md)** guidelines in the .agents/ directory.
The guidelines describe essential workflows—such as **surgical change constraints, cross-verification review loops, and pane snapshotting to prevent viewport truncation**—allowing new agents to quickly and safely integrate with the multi-agent workflow.
+5 -5
View File
@@ -1,8 +1,8 @@
# herdr-agent-orchestration (다중 에이전트 Herdr 오케스트레이션 및 메시징 백플레인)
Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. `claude`, `agy`, `hermes`, `grok`, `opencode` 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다.
Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다.
[![Version: v4.1.3](https://img.shields.io/badge/version-v4.1.3-blue.svg)](VERSIONS.md)
[![Version: v4.2.0](https://img.shields.io/badge/version-v4.2.0-blue.svg)](VERSIONS.md)
---
@@ -14,7 +14,7 @@ Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이
1. **Herdr 기반 프로세스 격리:** 개별적으로 격리된 herdr 환경 내부에 에이전트 CLI 세션을 띄워, 백그라운드에서 끊김 없이 장시간 작업이 영속되도록 보장합니다. 백그라운드 데몬 서버는 독립된 OS 세션/프로세스 그룹(`start_new_session=True`)으로 분리되어 부모 셸의 시그널 브로드캐스트로부터 안전하게 격리됩니다.
2. **비동기 이벤트 기반 아키텍처:** MQTT 브로커를 메시징 백플레인으로 활용하여, 에이전트 간 상태 전이 단계(`started`, `progress`, `completed`, `error`)를 긴밀하게 제어 및 조정합니다.
3. **Multi-Agent Mux (MAM):** 파일 기반의 어드바이저리 락(`fcntl`) 및 SQLite WAL 데이터베이스(`.mam/agent-sessions.db`)를 결합하여, 동시성 작업 선점 경쟁을 방지하고 에이전트 세션의 라이프사이클을 드리프트 없이 관리합니다.
4. **리뷰어 기반 고신뢰 검증 루프:** Worker 에이전트가 구현한 코드 변경 사항에 대해 상이한 강점을 지닌 전문 검증 에이전트(Claude, Hermes, Grok, OpenCode 등)들로부터 최종 `PASS` 판정을 획득한 뒤 머지하도록 교차 검증 루프를 자동화합니다.
4. **리뷰어 기반 고신뢰 검증 루프:** Worker 에이전트가 구현한 코드 변경 사항에 대해 상이한 강점을 지닌 전문 검증 에이전트(Claude, Hermes, Grok, OpenCode, Codex 등)들로부터 최종 `PASS` 판정을 획득한 뒤 머지하도록 교차 검증 루프를 자동화합니다.
---
@@ -24,7 +24,7 @@ Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이
* **`multi-agent-mux-create`**: 격리된 herdr 세션을 생성하고 특정 에이전트 CLI를 백그라운드에서 구동합니다. 프로세스 PID 캡처, 메타데이터 레지스트리 업데이트 및 에이전트 인증 검증을 처리합니다.
* **`multi-agent-mux-stop`**: 에이전트 CLI 세션을 정상 종료 키 입력(`/exit` 또는 `Exit`)을 통해 안전하게 닫고, 격리된 대화 히스토리 및 데이터베이스 로그를 삭제(purge)하는 클린업 작업을 수행합니다.
* **`multi-agent-mux-resume`**: 디스크 또는 캐시에서 특정 워크스페이스의 세션 UUID를 조회하여 기존 대화 상태(`claude -r <uuid>`, `agy --conversation <uuid>`, `hermes --resume <uuid>`, `grok --resume <uuid>`, `opencode --session <uuid>`) 그대로 세션을 복구하고 재개합니다.
* **`multi-agent-mux-resume`**: 디스크 또는 캐시에서 특정 워크스페이스의 세션 UUID를 조회하여 기존 대화 상태(`claude -r <uuid>`, `agy --conversation <uuid>`, `hermes --resume <uuid>`, `grok --resume <uuid>`, `opencode --session <uuid>`, `codex resume <uuid>`) 그대로 세션을 복구하고 재개합니다.
* **`multi-agent-mux-status`**: 활성화된 모든 세션의 실시간 작동 상태를 쿼리하여 PID 정합성, 실행 명령 포맷, herdr 실제 상태와 데이터베이스 간의 동기화 드리프트를 감지합니다.
* **`multi-agent-mux-monitor`**: 백그라운드 Reconcile 프로세스로 실행되어, 실시간 herdr 세션 변화를 모니터링하고 `.mam/agent-sessions.yaml` 메타데이터 파일에 상태를 동기화합니다.
* **`multi-agent-mux-delegate-job`**: 태스크를 비동기식 독립 잡으로 위임 및 관리하는 핵심 모듈입니다:
@@ -115,7 +115,7 @@ sequenceDiagram
Note over W: 코드 변경 및 자체 검증 수행
W->>M: 'completed' (또는 'error') 발행
PM->>R: 병렬 리뷰 요청 (Diff 제공)
Note over R: 에이전트 교차 검증 (Claude, Hermes, Grok, OpenCode)
Note over R: 에이전트 교차 검증 (Claude, Hermes, Grok, OpenCode, Codex)
alt 리뷰 피드백 (NOT PASS)
R->>PM: NOT PASS (대안 코드 블록과 함께 피드백 전달)
Note over PM: PM 직접 수정 또는 다시 위임 결정
+5 -5
View File
@@ -1,8 +1,8 @@
# herdr-agent-orchestration
An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`).
An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`).
[![Version: v4.1.3](https://img.shields.io/badge/version-v4.1.3-blue.svg)](VERSIONS.md)
[![Version: v4.2.0](https://img.shields.io/badge/version-v4.2.0-blue.svg)](VERSIONS.md)
---
@@ -14,7 +14,7 @@ Modern agentic workflows often suffer from session timeout, lack of process isol
1. **Herdr-based Process Isolation:** Spawning LLM client sessions inside dedicated, isolated herdr environments to support persistent background runs. Background daemon servers detach into their own OS session/process group (`start_new_session=True`), ensuring they survive signal broadcasts to the parent shell.
2. **Asynchronous Event-Driven Architecture:** Leveraging an MQTT broker as a message backplane to coordinate state transitions (`started`, `progress`, `completed`, `error`) between collaborating agents.
3. **Multi-Agent Mux (MAM):** Combining local file-based locks (fcntl) and an ACID-compliant SQLite WAL database (`.mam/agent-sessions.db`) to manage concurrent job claims and track running agent sessions without drift.
4. **Automated Review & Quality Loop:** Implementing parallel reviewer loops where worker agents must receive a `PASS` rating from various specialized verification agents (e.g., Claude, Hermes, Grok, OpenCode) before merging code.
4. **Automated Review & Quality Loop:** Implementing parallel reviewer loops where worker agents must receive a `PASS` rating from various specialized verification agents (e.g., Claude, Hermes, Grok, OpenCode, Codex) before merging code.
---
@@ -42,7 +42,7 @@ All orchestration functionalities are structured under the `.agents/skills/` dir
* **`multi-agent-mux-create`**: Spawns isolated herdr sessions running specified agent CLI wrappers. It captures system processes, updates metadata registries, and enforces authentication checks.
* **`multi-agent-mux-stop`**: Gracefully terminates agent CLI sessions (using key macros like `/exit` or `Exit`) and handles disk purge operations (removing conversation JSON files and SQLite logs for deleted workspaces).
* **`multi-agent-mux-resume`**: Restores stopped sessions by resolving workspace UUIDs from disk or cache, and invokes the underlying agent using session-resume parameters (e.g., `claude -r <uuid>`, `agy --conversation <uuid>`, `hermes --resume <uuid>`, `grok --resume <uuid>`, or `opencode --session <uuid>`).
* **`multi-agent-mux-resume`**: Restores stopped sessions by resolving workspace UUIDs from disk or cache, and invokes the underlying agent using session-resume parameters (e.g., `claude -r <uuid>`, `agy --conversation <uuid>`, `hermes --resume <uuid>`, `grok --resume <uuid>`, `opencode --session <uuid>`, or `codex resume <uuid>`).
* **`multi-agent-mux-status`**: Queries the running states of all active sessions, detecting PID mismatches, command signatures, and drifts between actual herdr instances and the registry database.
* **`multi-agent-mux-monitor`**: A long-running reconcile worker that dynamically monitors herdr sessions and synchronizes states to `.mam/agent-sessions.yaml`.
* **`multi-agent-mux-delegate-job`**: The core asynchronous task distribution module containing:
@@ -133,7 +133,7 @@ sequenceDiagram
Note over W: Implement & verify code
W->>M: Publish 'completed' (or 'error')
PM->>R: Request parallel reviews (Provide Diff)
Note over R: Parallel analysis (Claude, Hermes, Grok, OpenCode)
Note over R: Parallel analysis (Claude, Hermes, Grok, OpenCode, Codex)
alt Review Feedback (NOT PASS)
R->>PM: NOT PASS (Feedback with code blocks)
Note over PM: Apply fixes or re-delegate
+40 -15
View File
@@ -6,35 +6,60 @@
## 📌 현재 버전 개요 (Current Release)
- **프레임워크 버전**: `v4.1.3`
- **최신 릴리스 일시**: 2026-08-31 (KST)
- **기준 브랜치**: `main`
- **프레임워크 버전**: `v4.2.0`
- **최신 릴리스 일시**: 2026-09-05 (KST)
- **기준 브랜치**: `support-codex`
- **핵심 아키텍처**:
- **Herdr Daemon Process Group Isolation (`setsid`)**: `.mam/shim/herdr` 내 백그라운드 서버 스폰 시 Python `subprocess.Popen(..., start_new_session=True)`를 적용하여 데몬을 독립된 SID/PGID로 분리, 상위 프로세스/테스트 러너 시그널 전파 사망 차단
- **Class A 0-Turn Resume Fallback & Discovery Epoch Refresh**: 0-turn 중지된 Class A(`agy`/`hermes`/`opencode`) 세션 복원 시 `FRESH_SPAWN`으로 안전하게 fallback 스폰하고 `herdr_session_epoch`를 갱신하여 stale 트랜스크립트 오인 캡처 방지
- **Runtime Framework Version Constant (`MAM_VERSION`) & 3-Way Lockstep**: `lib.sh` 내 `MAM_VERSION="4.1.3"` 런타임 진실 공급원 정의 및 8개 스킬 3자 동기화 가드 유지
- **Codex First-Class Agent Integration**: OpenAI Codex CLI(`codex`)를 6번째 1급 에이전트 백엔드로 전면 통합 (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`)
- **Class A Codex Lifecycle & State DB Resolution**: 동적 `state_*.sqlite` SQLite 인덱스 및 date-nested rollout jsonl 폴백 지원, `--dangerously-bypass-approvals-and-sandbox` 비대화형 실행 연동
- **Runtime Framework Version Constant (`MAM_VERSION`) & 3-Way Lockstep**: `lib.sh` 내 `MAM_VERSION="4.2.0"` 런타임 진실 공급원 및 8개 스킬 3자 동기화 가드 유지
---
## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix)
모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.1.3`으로 동기화되어 배포됩니다.
모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.2.0`으로 동기화되어 배포됩니다.
| 스킬명 | 버전 | 역할 및 주요 책임 | 상태 |
| :--- | :---: | :--- | :---: |
| **`multi-agent-mux-create`** | `4.1.3` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 |
| **`multi-agent-mux-stop`** | `4.1.3` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 |
| **`multi-agent-mux-resume`** | `4.1.3` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 |
| **`multi-agent-mux-status`** | `4.1.3` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 |
| **`multi-agent-mux-monitor`** | `4.1.3` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 |
| **`multi-agent-mux-delegate-job`** | `4.1.3` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 |
| **`multi-agent-mux-loop`** | `4.1.3` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 |
| **`multi-agent-mux-orc-onboard`** | `4.1.3` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 |
| **`multi-agent-mux-create`** | `4.2.0` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 |
| **`multi-agent-mux-stop`** | `4.2.0` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 |
| **`multi-agent-mux-resume`** | `4.2.0` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 |
| **`multi-agent-mux-status`** | `4.2.0` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 |
| **`multi-agent-mux-monitor`** | `4.2.0` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 |
| **`multi-agent-mux-delegate-job`** | `4.2.0` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 |
| **`multi-agent-mux-loop`** | `4.2.0` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 |
| **`multi-agent-mux-orc-onboard`** | `4.2.0` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 |
---
## 📋 버전별 상세 변경 내역 (Changelog)
### 🚀 `v4.2.0` — OpenAI Codex First-Class Agent Backend Integration (2026-09-05)
> **주요 마일스톤 (MINOR Release)**: OpenAI Codex CLI(`codex`)를 6번째 정식 1급 에이전트 백엔드로 통합. 46개 전체 아키텍처 결합 지점(Shell 라이프사이클, Python 어댑터, 레지스트리, 상태 머신, 동기화 루프, 테스트 스위트, 문서) 전면 연동 완료.
#### 🌟 신규 기능 및 아키텍처 확장 (New Features & Architecture)
* **A-1: Codex Agent Adapter (`CodexAgentAdapter`) 신규 구현 (`codex.py`, `registry.py`)**:
- `BaseAgentAdapter` 완전 구현: `name='codex'`, `own_key='codex_session_id_own'`, `ready_tokens='Codex|Ask Codex to do anything'`, `exit_key='Ctrl+C'`, `delegate_agent_key='codex-cli'`.
- Class A 에이전트 라이프사이클 채택: 세션 생성 시 UUID를 사전 할당하지 않고 런타임에 동적으로 발견.
- Rev.2 동적 SQLite DB 탐색: 고정 버전 파일 대신 `state_*.sqlite` 패턴을 최신 mtime 순으로 탐색하여 `threads` 테이블에서 `(rollout_path, cwd)`를 O(1)로 색인.
- Fallback 탐색: SQLite 미존재 시 `~/.codex/sessions/*/*/*/rollout-*-<uuid>.jsonl` date-nested 경로 및 재귀 패턴 지원.
- Safe Purge: `codex delete <uuid> --force` CLI 호출 및 온디스크 jsonl/DB 레코드 폴백 삭제.
* **A-2: Shell 라이프사이클 및 오케스트레이션 46개 지점 완전 연동**:
- `lib.sh`: `resolve_agent_type_from_registry`, `_sanitize_herdr_agent_name`, `send_keys_safe`에 codex 등록.
- `create_session.sh`: `--agent codex` 허용, `~/.codex/auth.json` / `OPENAI_API_KEY` 프리플라이트 검증, `--dangerously-bypass-approvals-and-sandbox` 플래그 스폰, `codex_session_id_own: null` 초기화.
- `resume_session.sh`: `codex` 복원 지원, 0-turn 세션에 대한 `FRESH_SPAWN=1` Class A 안전 폴백.
- `update_yaml_resumed.sh`: `codex` 세션 복원 시 YAML 필드 원자적 갱신.
- `stop_session.sh`: `codex` 지원, 종료 시 최종 session id 캡처 및 저장.
- `orc_onboard.sh`: flag-tolerant regex 기반으로 `codex resume [options] <uuid>` 명령행에서 UUID 정확 추출.
- `reconcile.sh`: drift C (codex) 전용 감지 블록 추가 (state DB 우선 + glob 폴백), discovery sweep 연동.
- `status.sh`: text/JSON 포맷터 양측에 codex 온디스크 복원 가능 여부 검사 로직 연동.
* **A-3: 문서 및 표준 헌장 동기화**:
- `README.md`, `README.ko.md`, `BOOTSTRAP.md`, `BOOTSTRAP.ko.md`, `deploy/INSTALL.md`, `.agents/MULTI_AGENT_RULES.md`, `.mam.env.example`, `docs/NEW_AGENT_INTEGRATION_GUIDE.md` 전면 업데이트.
---
### 🛠️ `v4.1.3` — Herdr Daemon Isolation & Class A 0-Turn Resume Reliability Fixes (2026-08-31)
> **주요 마일스톤 (PATCH Release)**: Issue #3 결함 해소 — Herdr 백그라운드 데몬의 `start_new_session=True` 기반 프로세스 그룹(setsid) 완전 격리, Class A(`agy`, `hermes`, `opencode`) 에이전트 0-turn 중지 세션의 `FRESH_SPAWN` fallback 및 discovery epoch 원자적 재설정, Class B(`claude`, `grok`) 계약 불변 보존, 455개 전체 테스트 스위트 100% PASS 달성.
+1 -1
View File
@@ -1,6 +1,6 @@
# 🛠️ Multi-Agent Mux (MAM) 설치 및 적용 가이드
MAM은 단일 워크스페이스 상에서 복수의 에이전트(Claude, Agy, Hermes, Grok, OpenCode 등)들이 서로의 상태를 오염시키지 않고 협업할 수 있도록 프로세스 격리 및 라이프사이클 관리를 제공하는 프레임워크입니다.
MAM은 단일 워크스페이스 상에서 복수의 에이전트(Claude, Agy, Hermes, Grok, OpenCode, Codex 등)들이 서로의 상태를 오염시키지 않고 협업할 수 있도록 프로세스 격리 및 라이프사이클 관리를 제공하는 프레임워크입니다.
이 가이드는 기존의 다른 프로젝트/레포지토리에 MAM을 신속하게 도입하고 적용하는 절차를 설명합니다.
+1 -1
View File
@@ -265,7 +265,7 @@ def test_adapter_required_properties():
| 에이전트 백엔드 | 세션 저장소 형식 | 인증 방식 | 프롬프트 감지 난이도 | 난이도 Tier | 예상 소요 공수 |
| :--- | :--- | :--- | :--- | :---: | :---: |
| **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/sessions/*/*/*/rollout-*.jsonl` (SQLite `state_*.sqlite`) | `auth.json` / `OPENAI_API_KEY` | 쉬움 (`Codex\|Ask Codex to do anything`) | **v4.2.0 완료** | **완료** |
| **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일** |
| **Cursor CLI** | `~/.cursor/` (Headless/RPC) | OAuth / 쿠키 | 보통 (RPC 포트/상태 파싱) | **Tier 2 (중간)** | **~1.5일** |
+34 -3
View File
@@ -550,7 +550,8 @@ elif cmd1 == "agent":
"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}"
"opencode": f"OpenCode Ready in {resolved_cwd}",
"codex": f"Codex Ready in {resolved_cwd}"
}.get(agent_type, f"Ready in {resolved_cwd}")
agents = state.get("agents", {})
@@ -581,7 +582,8 @@ elif cmd1 == "agent":
"agy": "agy_conversation_id_own",
"hermes": "hermes_conversation_id_own",
"grok": "grok_session_id_own",
"opencode": "opencode_session_id_own"
"opencode": "opencode_session_id_own",
"codex": "codex_session_id_own"
}
own_key = own_key_map.get(agent_type)
if own_key:
@@ -642,6 +644,21 @@ elif cmd1 == "agent":
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()
elif agent_type == "codex":
codex_home = os.environ.get("CODEX_HOME", os.path.join(home_dir, ".codex"))
os.makedirs(codex_home, exist_ok=True)
db_path = os.path.join(codex_home, "state_5.sqlite")
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE IF NOT EXISTS threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT, created_at INTEGER, updated_at INTEGER)")
sessions_dir = os.path.join(codex_home, "sessions", "2026", "09", "05")
os.makedirs(sessions_dir, exist_ok=True)
rollout_file = os.path.join(sessions_dir, f"rollout-001-{session_uuid}.jsonl")
now_ts = int(time.time())
with open(rollout_file, "w") as rf:
rf.write(json.dumps({"payload": {"cwd": ws_abs, "id": session_uuid}}) + "\\\\n")
conn.execute("INSERT OR REPLACE INTO threads (id, cwd, rollout_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", (session_uuid, ws_abs, rollout_file, now_ts, now_ts))
conn.commit()
conn.close()
state["agents"] = agents
save_state()
@@ -989,7 +1006,21 @@ sys.exit(0)
""")
opencode_bin.chmod(0o755)
# 6. uuidgen mock to guarantee isolated creation UUIDs
# 6. codex mock
codex_bin = bin_dir / "codex"
codex_bin.write_text("""#!/usr/bin/env python3
import sys, os
args = sys.argv[1:]
if len(args) >= 1 and args[0] == "--version":
print("codex 0.1.0")
sys.exit(0)
if len(args) >= 1 and args[0] == "delete":
sys.exit(0)
sys.exit(0)
""")
codex_bin.chmod(0o755)
# 7. uuidgen mock to guarantee isolated creation UUIDs
uuidgen_bin = bin_dir / "uuidgen"
uuidgen_bin.write_text("""#!/usr/bin/env python3
import uuid
+161 -2
View File
@@ -32,6 +32,7 @@ def test_agent_adapter_registry():
'hermes': 'hermes_conversation_id_own',
'grok': 'grok_session_id_own',
'opencode': 'opencode_session_id_own',
'codex': 'codex_session_id_own',
}
for agent, expected in EXPECTED_OWN_KEYS.items():
adapter = get_adapter(agent)
@@ -75,6 +76,7 @@ def test_adapter_required_properties():
'hermes': ('Hermes|Welcome to Hermes Agent|NOUS HERMES', '/exit', 'hermes-agent', ('session_id',)),
'grok': ('Grok|xAI|Assistant|❯|>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')),
'opencode': ('OpenCode|Chat|Ask anything|tab agents|ctrl\\+p|Build auto|commands', '/exit', 'opencode-cli', ('session_id',)),
'codex': ('Codex|Ask Codex to do anything', 'Ctrl+C', 'codex-cli', ('session_id',)),
}
for agent, (toks, exitk, delk, cache_f) in expected.items():
adapter = get_adapter(agent)
@@ -96,6 +98,7 @@ def test_adapter_modal_tokens_contract():
'hermes': None,
'grok': None,
'opencode': None,
'codex': None,
}
for agent, expected in expected_modal.items():
adapter = get_adapter(agent)
@@ -109,7 +112,7 @@ def test_facts_bridge_eval_contract():
env = os.environ.copy()
skills_dir = str(Path(__file__).resolve().parent.parent / ".agents" / "skills")
env["PYTHONPATH"] = f"{skills_dir}:{env.get('PYTHONPATH', '')}"
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
res = subprocess.run([sys.executable, "-m", "lib_py.agents", "facts", agent], capture_output=True, text=True, env=env)
assert res.returncode == 0
facts_output = res.stdout
@@ -355,6 +358,12 @@ def test_adapter_spawn_and_resume_specs():
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'
codex = get_adapter('codex')
assert codex.spawn_spec('codex', 'u1') == 'codex --dangerously-bypass-approvals-and-sandbox'
assert codex.spawn_spec('codex', '') == 'codex --dangerously-bypass-approvals-and-sandbox'
assert codex.resume_spec('codex', 'u1', materialized=True) == 'codex resume --dangerously-bypass-approvals-and-sandbox u1'
assert codex.resume_spec('codex', 'u1', materialized=False) == 'codex --dangerously-bypass-approvals-and-sandbox'
def test_adapter_auth_ok(tmp_path, monkeypatch):
monkeypatch.setenv("HOME_DIR", str(tmp_path))
# Claude auth runner
@@ -395,6 +404,17 @@ def test_adapter_auth_ok(tmp_path, monkeypatch):
opencode_auth.write_text("{}")
assert opencode.auth_ok() is True
# Codex auth check (env var, auth.json file, or CLI)
codex = get_adapter('codex')
assert codex.auth_ok(run_cmd=lambda cmd: (1, '', '')) is False
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
assert codex.auth_ok() is True
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
codex_auth = tmp_path / ".codex" / "auth.json"
codex_auth.parent.mkdir(parents=True, exist_ok=True)
codex_auth.write_text("{}")
assert codex.auth_ok() is True
def test_adapter_discover(tmp_path):
import sqlite3
from lib_py.agents.base import DiscoveryContext
@@ -457,6 +477,23 @@ def test_adapter_discover(tmp_path):
_seed_opencode_session(o_db, 'u-o1', ws, 2000.0)
assert opencode.discover(ctx_o) == ['u-o1']
# 6. Codex
codex = get_adapter('codex')
ctx_x = DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home)
codex_home = f"{home}/.codex"
os.makedirs(codex_home, exist_ok=True)
c_db = f"{codex_home}/state_5.sqlite"
conn = sqlite3.connect(c_db)
conn.execute("CREATE TABLE threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT)")
c_rollout = f"{codex_home}/sessions/2026/09/05/rollout-001-u-x1.jsonl"
os.makedirs(os.path.dirname(c_rollout), exist_ok=True)
with open(c_rollout, 'w') as f:
f.write('{"payload": {"cwd": "' + ws + '", "id": "u-x1"}}\n')
conn.execute("INSERT INTO threads VALUES ('u-x1', ?, ?)", (ws, c_rollout))
conn.commit()
conn.close()
assert codex.discover(ctx_x) == ['u-x1']
def test_cli_bridge_subcommands_and_quote_safety():
import subprocess, sys
from pathlib import Path
@@ -475,7 +512,7 @@ def test_cli_bridge_subcommands_and_quote_safety():
assert res.stdout.strip() == "/bin/claude --dangerously-skip-permissions --session-id uuid-test"
# 3. exit-key
for agent, expected_key in [('claude', '/exit'), ('agy', 'Exit'), ('hermes', '/exit'), ('grok', '/exit'), ('opencode', '/exit')]:
for agent, expected_key in [('claude', '/exit'), ('agy', 'Exit'), ('hermes', '/exit'), ('grok', '/exit'), ('opencode', '/exit'), ('codex', 'Ctrl+C')]:
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.stdout.strip() == expected_key
@@ -488,6 +525,7 @@ def test_delegate_agent_resolution_and_fallback():
'hermes': 'hermes-agent',
'grok': 'grok-build',
'opencode': 'opencode-cli',
'codex': 'codex-cli',
}
# 1. Adapter property
for agent, expected_key in expected_map.items():
@@ -507,6 +545,7 @@ def test_delegate_agent_resolution_and_fallback():
agy) delegate_agent="antigravity-cli" ;;
grok) delegate_agent="grok-build" ;;
opencode) delegate_agent="opencode-cli" ;;
codex) delegate_agent="codex-cli" ;;
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
esac
fi
@@ -837,6 +876,126 @@ exit 1
assert adapter._resolve_db(ctx) == expected_db
def _seed_codex_session(home_dir, session_id, ws_path, epoch_sec):
import sqlite3, json, time
codex_home = f"{home_dir}/.codex"
os.makedirs(codex_home, exist_ok=True)
db_path = f"{codex_home}/state_5.sqlite"
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE IF NOT EXISTS threads (id TEXT PRIMARY KEY, cwd TEXT, rollout_path TEXT)")
sessions_dir = f"{codex_home}/sessions/2026/09/05"
os.makedirs(sessions_dir, exist_ok=True)
rollout_file = f"{sessions_dir}/rollout-001-{session_id}.jsonl"
with open(rollout_file, "w") as rf:
rf.write(json.dumps({"payload": {"cwd": ws_path, "id": session_id}}) + "\n")
if epoch_sec:
os.utime(rollout_file, (epoch_sec, epoch_sec))
conn.execute("INSERT OR REPLACE INTO threads (id, cwd, rollout_path) VALUES (?, ?, ?)", (session_id, ws_path, rollout_file))
conn.commit()
conn.close()
return rollout_file
def test_codex_purge_artifacts(tmp_path):
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
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('codex')
ctx = DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home)
rollout = _seed_codex_session(home, 'u-del', ws, 2000.0)
assert os.path.exists(rollout)
purged = adapter.purge_artifacts('u-del', ctx)
assert len(purged) == 2
assert not os.path.exists(rollout)
import sqlite3
conn = sqlite3.connect(f"{home}/.codex/state_5.sqlite")
assert conn.execute("SELECT count(*) FROM threads WHERE id='u-del'").fetchone()[0] == 0
conn.close()
def test_codex_verify_artifact_epoch_filtering(tmp_path):
from lib_py.agents.registry import get_adapter
from lib_py.agents.base import DiscoveryContext
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('codex')
ctx_old = DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home, epoch=1500)
_seed_codex_session(home, 'u-old', ws, 1000.0)
_seed_codex_session(home, '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_codex_reconcile_dual_direction_epoch_guards(tmp_path):
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('codex')
_seed_codex_session(home, 'u-hist', ws, 1000.0)
_seed_codex_session(home, '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='codex', home_dir=home, epoch=epoch, row=s_row)) if verify_session_uuid(ws, 'codex', u, s_row, home_dir=home, mode="discover")]
assert valid == ['u-fresh1']
assert len(valid) == 1
_seed_codex_session(home, 'u-fresh2', ws, 2100.0)
valid2 = [u for u in adapter.discover(DiscoveryContext(workspace=ws, agent_name='codex', home_dir=home, epoch=epoch, row=s_row)) if verify_session_uuid(ws, 'codex', u, s_row, home_dir=home, mode="discover")]
assert len(valid2) == 2
def test_codex_reconcile_full_block_integration(tmp_path):
"""Verify codex drift-C block behavior with sibling_claimed exclusion."""
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('codex')
_seed_codex_session(home, 'u-claimed', ws, 2000.0)
_seed_codex_session(home, 'u-target', ws, 2001.0)
epoch = 1900
s_eval = {
'name': 'codex-target-01',
'pane': {'cwd': ws},
'herdr_session_epoch': epoch,
'_sibling_claimed_uuids': ['u-claimed']
}
candidates = adapter.discover(DiscoveryContext(workspace=ws, agent_name='codex', 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, 'codex', uuid, s_eval, home_dir=home, mode="discover"):
valid_candidates.append(uuid)
assert valid_candidates == ['u-target']
+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():
"""N-D: Verify strong_ready_tokens and weak_ready_tokens partition ready_tokens for all adapters."""
from lib_py.agents.registry import get_adapter
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
adapter = get_adapter(agent)
assert adapter is not None
ready_set = set(t for t in adapter.ready_tokens.split('|') if t)
@@ -527,6 +527,8 @@ echo "7: $(check_sks 'claude')"
echo "9: $(check_sks 'grok-02')"
echo "10: $(check_sks 'creator-opencode-01')"
echo "11: $(check_sks 'opencode')"
echo "12: $(check_sks 'creator-codex-01')"
echo "13: $(check_sks 'codex')"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
@@ -538,6 +540,8 @@ echo "11: $(check_sks 'opencode')"
assert "9: RESOLVED_AGENT=grok" in res.stdout
assert "10: RESOLVED_AGENT=opencode" in res.stdout
assert "11: RESOLVED_AGENT=opencode" in res.stdout
assert "12: RESOLVED_AGENT=codex" in res.stdout
assert "13: RESOLVED_AGENT=codex" in res.stdout
def test_f2_upsell_modal_defense_with_real_facts_and_banner():
+55 -6
View File
@@ -960,18 +960,18 @@ def test_grok_shell_and_scripts_integration(mam_sandbox):
lib_path = mam_sandbox / "skills" / "lib.sh"
lib_content = lib_path.read_text()
assert '*-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok"' in lib_content
assert "'claude', 'agy', 'hermes', 'grok', 'opencode'" in lib_content
assert "'claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'" in lib_content
assert '[[ "$sess" =~ "grok" ]]' in lib_content
# 2. create_session.sh validation
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
assert 'claude|agy|hermes|grok|opencode|codex)' in create_content
# 3. 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 'claude|agy|hermes|grok|opencode|codex)' in stop_content
assert "target['grok_session_id_own'] = captured" in stop_content
# 4. workspace_uuid OWN_KEY
@@ -986,12 +986,12 @@ def test_opencode_shell_and_scripts_integration(mam_sandbox):
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
assert "'claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'" 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
assert 'claude|agy|hermes|grok|opencode|codex)' 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(
@@ -1030,7 +1030,7 @@ def test_opencode_shell_and_scripts_integration(mam_sandbox):
# 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 'claude|agy|hermes|grok|opencode|codex)' in stop_content
assert "target['opencode_session_id_own'] = captured" in stop_content
# 5. workspace_uuid OWN_KEY
@@ -1044,6 +1044,55 @@ def test_opencode_shell_and_scripts_integration(mam_sandbox):
assert 'op_adapter = get_adapter(\'opencode\')' in recon_content
def test_codex_shell_and_scripts_integration(mam_sandbox):
"""Verify lib.sh, create_session.sh, stop_session.sh, and workspace_uuid contain codex."""
# 1. lib.sh kind mapping & binaries
lib_path = mam_sandbox / "skills" / "lib.sh"
lib_content = lib_path.read_text()
assert '*-creator-codex|*-planner-codex|*-reviewer-codex)' in lib_content
assert "'claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'" in lib_content
# 2. create_session.sh validation
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|codex)' in create_content
# 3. resume_session.sh validation
resume_path = mam_sandbox / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
resume_content = resume_path.read_text()
assert 'claude|agy|hermes|grok|opencode|codex)' in resume_content
# 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|codex)' in stop_content
assert "target['codex_session_id_own'] = captured" in stop_content
# 5. workspace_uuid OWN_KEY
from lib_py.workspace_uuid import OWN_KEY
assert OWN_KEY.get('codex') == 'codex_session_id_own'
# 6. reconcile.sh drift C block contains codex
recon_path = mam_sandbox / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
recon_content = recon_path.read_text()
assert 'drift C (codex)' in recon_content
def test_orc_onboard_codex_flag_tolerant_regex():
"""Verify orc_onboard.sh flag-tolerant regex correctly extracts UUID from codex resume command."""
import subprocess
cmd_line = "codex resume --dangerously-bypass-approvals-and-sandbox 12345678-1234-1234-1234-123456789abc"
sh_snippet = f'''
cmd_line="{cmd_line}"
detected_id=$(echo "$cmd_line" | grep -oE 'resume[[:space:]]+.*[0-9a-fA-F]{{8}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{12}}' | grep -oE '[0-9a-fA-F]{{8}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{12}}' | head -n 1 || true)
echo "$detected_id"
'''
res = subprocess.run(["bash", "-c", sh_snippet], capture_output=True, text=True)
assert res.returncode == 0
assert res.stdout.strip() == "12345678-1234-1234-1234-123456789abc"
+50
View File
@@ -1276,3 +1276,53 @@ def test_comp_opencode_create_and_reconcile_and_stop(mam_sandbox, mock_herdr, mo
assert s_stopped["resumable"] is True
def test_comp_codex_create_and_reconcile_and_stop(mam_sandbox, mock_herdr, mock_agents):
"""Verify create_session -> reconcile drift-C materialization -> stop flow for codex."""
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", "codex",
"--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["codex_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("codex_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", "codex"
], 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