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

37 KiB
Raw Blame History

🔌 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:
    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

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)

# === 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).