- 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
37 KiB
🔌 Implementation Plan: Codex CLI (openai/codex) Agent Adapter for MAM (Rev.2)
- Author:
planner-reviewer-claude-01(Planner), refined in response tocreator-agy-01's architectural challenge - Job:
e30e3123→ Rev.2 (94a6db78) - Status: Plan only — no code changed in the repo. Ready for review/refinement.
- Scope: Add
codexas a 6th first-class MAM agent backend (alongsideclaude,agy,hermes,grok,opencode), perdocs/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 ofcreator-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, versioncodex-cli 0.153.4). - Config/state root:
~/.codex/by default, overridable via theCODEX_HOMEenvironment variable (confirmed:codex exec --ignore-user-confighelp text explicitly says "auth still usesCODEX_HOME").
1.2 Execution Modes
codex [OPTIONS] [PROMPT]— launches the interactive TUI (default, what MAM spawns).codex exec [OPTIONS] [PROMPT](aliase) — non-interactive one-shot execution with--jsonJSONL 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;--lastpicks the most recent session without a picker.codex delete <SESSION> --force— permanently deletes a saved session by UUID;--forceskips the confirmation prompt and requiresSESSIONto be a UUID (not a name). Verified viacodex 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 asession_metarecord:{"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 — nocodex debug/codexsubcommand introspects its exact filename, unlike OpenCode'sopencode db path, so the adapter must resolve it via astate_*.sqliteglob rather than hardcoding the suffix). Verified live schema: athreadstable withid 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-placeALTER TABLEacross releases, another reason not to hand-roll fragile column-position assumptions). Verified live rows: both real sessions on this machine are indexed here withid/cwd/rollout_pathexactly matching their on-disk.jsonlfiles. - 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_backandreconcile_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 ownreconcile.sh. - Architectural consequence for MAM's adapter: mirror Codex's own resolution order — query
state_*.sqlitefirst (fast, indexed, avoids any unbounded directory walk), fall back to the bounded glob oversessions/*/*/*/rollout-*.jsonlif the state DB is missing, unreadable, or the row is stale/absent (mirroring the exact "primary DB, fallback filesystem walk" shapeopencode.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) orcodex login --with-api-key(readsOPENAI_API_KEYfrom stdin). - Credential file:
~/.codex/auth.json(or$CODEX_HOME/auth.json) — verified present, keysauth_mode,OPENAI_API_KEY,tokens,last_refresh.codex login statusconfirms"Logged in using ChatGPT";codex doctor --summaryindependently confirms✓ auth is configured. auth_ok():os.path.exists(f"{codex_home}/auth.json")oros.environ.get('OPENAI_API_KEY'). Caveat honestly flagged: whether a bare env var alone (without ever runningcodex 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)
- 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 afterresume, but the real command line this plan's ownresume_spec/CMD_FULLproduce iscodex 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 betweenresumeand the UUID, then extract only the UUID:Already safe undercodex) 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) ;;orc_onboard.sh:135's existingis_valid_id "$detected_id"UUID-format re-validation, so a slightly looser extraction regex carries no correctness risk. - env var fallback: no
CODEX_SESSION_ID-style environment variable found anywhere incodex --help. Leaveenv_var=""forcodex— 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)
- Live pane capture to confirm
ready_tokens/input_placeholder(§1.6). - Live smoke test that
--dangerously-bypass-approvals-and-sandboxsuppresses every confirmation prompt in a real multi-file-edit task. - Live test of the corrected
orc_onboard.shregex (§1.9) against a real runningcodex resume <uuid>process'sps//procargv snapshot — the regex was verified against a synthetic string in this planning pass, not a real captured process line. - (Rev.2, new) Confirm the
state_*.sqliteglob resolves correctly on a different Codex install/version than this machine'sstate_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.pyper §2 (DB-first resolution,codex delete --force-based purge with fallback). - Apply all 46 lockstep wiring points in §3, including the corrected
orc_onboard.shregex (item #27). - Add the dedicated
reconcile.shDrift-C block (§4, state-DB-first) and itsC-ambiguous-scenario test. - Add all test coverage in §3 items #35-39, including the regression test locking down the corrected
orc_onboard.shregex (item #38) and thepurge_artifactsCLI-then-fallback test (item #36);pytest tests/shows 0 regressions. - Bump version to
4.2.0in 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 materializescodex_session_id_own→ stop → resume round-trips → purge leaves no orphan.jsonlorstate_*.sqliterow.
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.shregex).- ~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.shDrift-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).