Files
multi-agent-mux/.agents/reports/implementation_plan.md
T
Godopu 76f10b2096 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
2026-09-05 23:02:29 +09:00

455 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔌 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**: 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.
---
## 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).