# 🔌 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 `), 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 --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 --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 `. 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 --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 ` / `-a, --ask-for-approval ` | 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 ` | Working-root directory for the agent | `codex --help` | | `--session-id ` | **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///
/rollout--.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": "", "cwd": "", ...}}`. - **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 ` — 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 ` — 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 `_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 `-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 `` 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:]]+.*` 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--.jsonl` under a temp `$CODEX_HOME/sessions////` **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 ` — 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 ` 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 ` 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).