feat(agent): add full codex agent support (v4.2.0)
- Implement CodexAdapter in lib_py/agents/adapters/codex.py with safe flags and TUI readiness - Extend create_session, resume_session, stop_session, and reconcile for codex CLI - Support codex agent delegation and loop integration across all multi-agent skills - Add unit, component, and contract test suites for codex integration - Verify all 462 tests passing cleanly
This commit is contained in:
1 parent
de0a9c2e81
commit
76f10b2096
45 files changed
+2045
-276
No files matched your search
@@ -1,16 +1,253 @@
|
||||
# Report: Job c65b0081 — Refined OpenCode Integration Plan (Rev.2) per `creator-agy-01` Challenge
|
||||
> [!NOTE]
|
||||
> This is the durable archive of the OpenCode (`anomalyco/opencode`) Integration Plan (Rev.3, Job `78f332a9`).
|
||||
> The active mirror at `.agents/reports/implementation_plan.md` now houses the Codex CLI Integration Plan (v4.2.0).
|
||||
|
||||
**Durable output (updated in place)**: [.agents/reports/implementation_plan.md](../../../.agents/reports/implementation_plan.md)
|
||||
# 🔌 Implementation Plan: OpenCode (`anomalyco/opencode`) Agent Adapter for MAM (Rev.3)
|
||||
|
||||
## Summary
|
||||
- **Author**: `planner-reviewer-claude-01` (Planner), refined in collaboration with `creator-agy-01` and `reviewer-creator-grok-01`
|
||||
- **Job**: `de667f0f` → Rev.2 (`c65b0081`) → Rev.3 (`78f332a9`)
|
||||
- **Status**: Plan only — **no code changed in the repo**. Ready for review/refinement.
|
||||
- **Scope**: Add `opencode` as a 5th first-class MAM agent backend (alongside `claude`, `agy`, `hermes`, `grok`), per `docs/NEW_AGENT_INTEGRATION_GUIDE.md`'s 5-step procedure.
|
||||
- **Durable Path**: `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (and `.agents/reports/implementation_plan.md`)
|
||||
|
||||
`creator-agy-01` challenged Rev.1's §3/§6 (their job `ebf97869`) — not disputing the plan's core research (SQLite storage, no pre-assigned session ID, `anomalyco/opencode` identity all stand unchanged), but flagging two architectural blind spots. **Both independently re-verified against live source and accepted — no `[REBUT:]` filed**:
|
||||
---
|
||||
|
||||
1. **§6's "reconcile.sh drift-C is generic" claim was false.** Read `reconcile.sh:507-780` directly: drift-C materialization is three separate, hardcoded, copy-pasted per-agent blocks (`claude` 634-671, `agy` 673-720, `hermes` 722-780), not a generic `adapter.discover()` loop. **Went beyond the challenge's own evidence**: independently discovered `grok` has *no* materialize block at all — because it always pre-assigns its session ID at spawn (confirmed in `grok.py`), so it only ever needs the shared C0 *confirmation* path (line 614-632), never post-spawn *discovery*. Since OpenCode cannot pre-assign (already established in Rev.1), it belongs in the `hermes`/`agy` bucket, not `grok`'s — it needs its own bespoke block. Rewrote §6 with a concrete illustrative code block mirroring `hermes`'s structure, and weighed two implementation options (an explicit new block vs. a generic drift-C refactor), recommending the explicit block for this PR and filing the generic refactor as an out-of-scope follow-up (consistent with the brief's "don't implement" constraint and with keeping this PR's blast radius limited to OpenCode rather than touching 3 other agents' already-battle-tested drift paths).
|
||||
2. **§3's 18-item touch-point table omitted 4 files**: `lib_py/atomic_yaml.py:132`, `lib_py/verify_session.py:69`, `lib_py/workspace_uuid.py` (3 locations), and `reconcile.sh` (4 more locations: 2 auto-registration loops, the entry-init `elif` chain, `OWN_KEY_BY_AGENT`). Verified all cited lines directly and confirmed each failure mode described. **Also independently traced a plausible worse failure mode** (an uncaught `KeyError` in `reconcile.sh`'s C0 block crashing reconciliation for *every* agent, not just OpenCode) by reading `create_session.sh:396,419`'s `session_id_source` assignment — ruled it out (OpenCode rows are correctly stamped `'pending-discovery'`, never reach the C0 dict-index), confirming the actual failure mode is the same silent data-completeness gap the challenge described, not a crash. Added all 4 files as items 19-22, raising the touch-point count from 18 to 22.
|
||||
## Rev.3 Changelog (Job `78f332a9`)
|
||||
|
||||
Also updated: §7 (lockstep requirement widened to 22 locations + the new drift-C block), §8 (added a dedicated drift-C integration test mirroring the existing Hermes reconcile tests — "the test that would have caught this blind spot mechanically instead of requiring a manual challenge to find it"), §9 (Definition of Done checklist extended), §10 (effort estimate revised from ~1 day to ~1.5 days to price in the real cost of the caught blind spot).
|
||||
Reviewers `planner-reviewer-claude-01` (Job `1d63595f`) and `reviewer-creator-grok-01` (Job `56120ad3`) reviewed the Rev.2 plan and working tree. **All 5 findings accepted — zero `[REBUT:]` filed**:
|
||||
|
||||
Full Rev.2 text is in the durable plan linked above (new "Rev.2 Changelog" section at the top, rewritten §6, extended §3/§7/§8/§9/§10).
|
||||
1. **Restored Root `implementation_plan.md` & Namespaced Plan Path (F1)**:
|
||||
- Root `implementation_plan.md` was restored to its tracked HEAD content (the active *MAM 메시징 백플레인 전환 실행 로드맵* NATS migration roadmap).
|
||||
- This OpenCode integration plan is permanently housed under `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` and mirrored at `.agents/reports/implementation_plan.md` to prevent future root-level collision.
|
||||
2. **Extended Lockstep Touch-Points Table to 29 Items (F2)**:
|
||||
- Added items 23–29 covering `create_session.sh` (`spawn()` dispatch case, YAML initialization `elif`, fallback `CMD_FULL` case), `lib.sh` (`send_keys_safe` `_sks_agent` resolution case), `tests/test_tier1_unit.py` (CLI usage assertions), 8× `SKILL.md` (supported agent lists), and `tests/conftest.py` (`mock_agents` stub binary).
|
||||
3. **Official CLI `opencode db path` Subcommand Integration (F3)**:
|
||||
- Updated §1.5, §2, and §6 to prioritize the official `opencode db path` subcommand for dynamic database location, dropping hardcoded `storage.db` assumptions.
|
||||
4. **Headless Permission Architecture via `OPENCODE_PERMISSION` (F4)**:
|
||||
- Documented the `OPENCODE_PERMISSION='{"*":"allow"}'` process environment variable alongside `--auto` in §1.4, §2, and §9 DoD for unattended automated operations without global config file mutations.
|
||||
5. **Sandbox-Safe `ctx.home_dir` Binding in `auth_ok` and Artifact Paths (F5)**:
|
||||
- Replaced `os.path.expanduser('~')` with `ctx.home_dir` in the adapter skeleton and test contract for hermetic sandbox execution.
|
||||
|
||||
[VERDICT: N/A — planning artifact, review pending]
|
||||
---
|
||||
|
||||
## Rev.2 Changelog (Job `c65b0081`)
|
||||
|
||||
`creator-agy-01` filed a formal architectural challenge (Job `ebf97869`) against Rev.1, raising two blind spots. Both accepted:
|
||||
1. **`reconcile.sh` Drift-C Non-Generic Correction**: Identified that `reconcile.sh` drift-C materialization consists of bespoke per-agent blocks, requiring an explicit `# === drift C (opencode) ===` block for post-spawn discovery agents.
|
||||
2. **Core `lib_py` Constants Added**: Added `atomic_yaml.py:132` (`running_keys`), `verify_session.py:69` (`mam_row_own_uuid()`), and `workspace_uuid.py:7-12,33` (`OWN_KEY`) to the touch-points list.
|
||||
|
||||
---
|
||||
|
||||
## 0. Relationship to the existing `new_agent_types_roadmap.md`
|
||||
|
||||
`.agents/reports/new_agent_types_roadmap.md` (by `creator-agy-01`, 2026-08-26) sketches a generic "add any new agent" procedure and guesses at OpenCode specifics in its difficulty matrix. This plan **supersedes** that roadmap's OpenCode row:
|
||||
|
||||
| | Roadmap's guess | Verified reality (this doc) |
|
||||
|---|---|---|
|
||||
| Storage format | "JSON files in `~/.opencode/sessions/`" | **SQLite** since v1.2.0 (Feb 2026); dynamic lookup via `opencode db path` |
|
||||
| Storage path | `~/.opencode/sessions/` | `~/.local/share/opencode/` (XDG data dir) or project storage via `opencode db path` |
|
||||
| Ready tokens | `OpenCode\|Chat\|Welcome` | Candidate string pending live TUI pane capture verification (§4) |
|
||||
| Adapter registry | `ClineAgentAdapter` shown | `cline` fully removed in v4.0.0. Targets current 4-adapter registry (`claude`, `agy`, `hermes`, `grok`) |
|
||||
|
||||
---
|
||||
|
||||
## 1. Verified OpenCode CLI/Runtime Facts
|
||||
|
||||
Source: `opencode.ai/docs/{cli,config,permissions,tui}/`, `github.com/anomalyco/opencode`, and official documentation.
|
||||
|
||||
### 1.1 Identity & Install
|
||||
- **Upstream Repository**: `github.com/anomalyco/opencode` (distinct from the archived `opencode-ai/opencode` / "Crush").
|
||||
- **Install**: `curl -fsSL https://opencode.ai/install | bash` (also npm/Homebrew).
|
||||
|
||||
### 1.2 Execution Modes
|
||||
- `opencode [project]` — launches the **TUI** (default).
|
||||
- `opencode run [message..]` — **non-interactive** one-shot CLI execution.
|
||||
- `opencode db path` — prints the active database file path on stdout.
|
||||
|
||||
### 1.3 Relevant CLI Flags & Environment
|
||||
| Flag / Variable | Purpose |
|
||||
|---|---|
|
||||
| `--continue` / `-c` | Resume the most recent session |
|
||||
| `--session <id>` / `-s <id>` | Continue a specific session ID (cannot pre-assign a new UUID at spawn) |
|
||||
| `--agent <name>` | Mode selection: `build` (default, write access) or `plan` (read-only) |
|
||||
| `--auto` | Auto-approves tools that prompt ("ask"); explicit "deny" rules still enforced |
|
||||
| `OPENCODE_PERMISSION` | Process environment variable for inline JSON permission overrides (e.g. `'{"*":"allow"}'`) |
|
||||
|
||||
**Spawn / Discovery Consequence**: OpenCode has **no `--session-id <new-uuid>` pre-assignment flag**. It falls strictly in the **`hermes` / `agy` architectural bucket**: `spawn_spec()` ignores `session_uuid` on initial creation, and MAM learns the assigned session ID post-spawn via `discover()` and `reconcile.sh` Drift-C materialization.
|
||||
|
||||
### 1.4 Permissions (Headless & Non-Interactive Configuration)
|
||||
MAM configures headless unattended execution at the process boundary without mutating user-global config files (`~/.config/opencode/opencode.json`):
|
||||
1. **CLI Flag**: `--auto` passed in `spawn_spec` / `resume_spec`.
|
||||
2. **Process Environment Variable**: `OPENCODE_PERMISSION='{"*":"allow"}'` exported in the spawn subshell when full unattended bypass is required.
|
||||
3. **Optional Project/Global Config**: `<project>/opencode.json` or `~/.config/opencode/opencode.json` can define granular policies if configured by the user.
|
||||
|
||||
### 1.5 Session & Artifact Storage
|
||||
- **Format**: Single SQLite database (since v1.2.0).
|
||||
- **Dynamic Path Resolution**: `opencode db path` prints the exact database location.
|
||||
- **Default Storage Roots**: `~/.local/share/opencode/` (global) or `project/<slug>/storage/`.
|
||||
- **Pre-Implementation Spike (§1.5)**: Run `opencode db path` and `sqlite3 <db> ".schema"` on a live install to verify `session`, `message`, and `part` table column definitions (`cwd`, `created_at` timestamps) before writing SQL queries.
|
||||
|
||||
### 1.6 Authentication
|
||||
- Interactive: `opencode auth login`.
|
||||
- Multi-provider environment variables: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY`.
|
||||
- Credentials cache: `~/.local/share/opencode/auth.json`.
|
||||
- `auth_ok()` checks for `auth.json` in `ctx.home_dir` OR any supported provider environment variable.
|
||||
|
||||
### 1.7 Exit Key
|
||||
- Slash command `/exit` (aliases `/quit`, `/q`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Adapter Contract Decisions (`BaseAgentAdapter`)
|
||||
|
||||
| Property/Method | Value | Rationale |
|
||||
|---|---|---|
|
||||
| `name` | `'opencode'` | Lowercase identifier |
|
||||
| `own_key` | `'opencode_session_id_own'` | Matches `<agent>_session_id_own` convention |
|
||||
| `ready_tokens` | `'OpenCode\|Chat'` (candidate) | Pending live TUI capture verification (§4) |
|
||||
| `exit_key` | `'/exit'` | Confirmed in CLI docs |
|
||||
| `delegate_agent_key` | `'opencode-cli'` | Descriptive CLI identifier |
|
||||
| `identity_cache_fields` | `('session_id',)` | Single-field cache |
|
||||
| `spawn_spec(binary, session_uuid, use_wrapper)` | `f"{binary} --auto --agent build"` | Non-interactive build mode; ignores `session_uuid` at spawn |
|
||||
| `resume_spec(binary, session_uuid, materialized)` | `f"{binary} --session {session_uuid} --auto --agent build"` (if materialized) | Passes `--session <id>` once discovered |
|
||||
| `auth_ok(run_cmd)` | `os.path.exists(f"{ctx.home_dir}/.local/share/opencode/auth.json") or any(env)` | Multi-provider fallback + sandbox-safe path |
|
||||
| `discover(ctx)` | Queries SQLite DB resolved via `opencode db path` / default root | Discovers new session IDs created since `ctx.epoch` |
|
||||
| `verify_artifact(uuid, ctx)` | Checks row existence in SQLite `session` table | Validates session persistence |
|
||||
| `purge_artifacts(uuid, ctx)` | Executes parameterized `DELETE FROM session/message/part WHERE session_id=?` | Cleans SQLite rows on stop `--purge-conversation` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Complete 29-Point Wiring & Lockstep Table
|
||||
|
||||
All 29 locations below must be updated in lockstep:
|
||||
|
||||
| # | Component / File | Specific Edit Required |
|
||||
|---|---|---|
|
||||
| 1 | `.agents/skills/lib_py/agents/adapters/opencode.py` | Create new adapter class `OpenCodeAgentAdapter` |
|
||||
| 2 | `.agents/skills/lib_py/agents/registry.py` | Import `OpenCodeAgentAdapter` and register in `_ADAPTERS['opencode']` |
|
||||
| 3 | `.agents/skills/lib.sh` (kind mapping, ~L606-609) | `*-creator-opencode\|*-planner-opencode\|*-reviewer-opencode) kind="opencode" ;;` |
|
||||
| 4 | `.agents/skills/lib.sh` (fallback grep chain, ~L610-619) | Add `elif echo "$name" \| grep -qi "opencode"; then kind="opencode"` |
|
||||
| 5 | `.agents/skills/lib.sh` (binary dedup tuple, ~L632) | Add `'opencode'` to `('claude', 'agy', 'hermes', 'grok', 'opencode')` |
|
||||
| 6 | `create_session.sh` (line 93 `--agent` whitelist) | Add `opencode` to accepted values |
|
||||
| 7 | `resume_session.sh` (line 45 `--agent` whitelist) | Add `opencode` to accepted values |
|
||||
| 8 | `resolve_session_id.sh` (line 39 `--agent` whitelist) | Add `opencode` to accepted values |
|
||||
| 9 | `update_yaml_resumed.sh` (usage string) | Add `opencode` to usage documentation |
|
||||
| 10 | `stop_session.sh` (line 94 `--agent` whitelist) | Add `opencode` to accepted values |
|
||||
| 11 | `orc_onboard.sh` (line 106 case) | Add `opencode` to accepted values |
|
||||
| 12 | `tests/test_a4_adapter_contract.py` | Add `opencode` to `EXPECTED_OWN_KEYS`, `expected`, `expected_modal` (`None`), `facts_bridge`, and delegate bash snippets |
|
||||
| 13 | `tests/test_a4_adapter_contract.py` | Add adapter-specific unit tests (`spawn_spec`, `resume_spec`, `auth_ok`, `purge_artifacts`, `discover`) |
|
||||
| 14 | `tests/test_c1_tui_readiness.py` | Add `opencode` to facts-bridge and SKS session resolution loops |
|
||||
| 15 | `tests/test_tier2_component.py` | Add OpenCode lifecycle tests (create → YAML → stop → resume) |
|
||||
| 16 | `.mam.env.example` | Document OpenCode multi-provider API keys and `OPENCODE_PERMISSION` |
|
||||
| 17 | `docs/NEW_AGENT_INTEGRATION_GUIDE.md` | Update difficulty matrix with OpenCode SQLite storage |
|
||||
| 18 | `.agents/reports/new_agent_types_roadmap.md` | Add cross-link noting OpenCode details are superseded by this plan |
|
||||
| 19 | `.agents/skills/lib_py/atomic_yaml.py:132` | Add `'opencode_session_id_own'` to `running_keys` |
|
||||
| 20 | `.agents/skills/lib_py/verify_session.py:69` | Add `'opencode_session_id_own'` to `mam_row_own_uuid()` |
|
||||
| 21 | `.agents/skills/lib_py/workspace_uuid.py:7-12, 33` | Add `'opencode'` to `OWN_KEY` dict and `running_ids` list |
|
||||
| 22 | `reconcile.sh` (auto-reg & own keys) | Add `opencode` to loops (521, 540), entry initialization `elif` (584-601), and `OWN_KEY_BY_AGENT` (611) |
|
||||
| 23 | `create_session.sh` (spawn dispatch, ~L204) | Add `opencode` to `agy\|hermes\|grok\|opencode)` case |
|
||||
| 24 | `create_session.sh` (YAML init, ~L399-419) | Add `elif agent == 'opencode': entry['opencode_session_id_own'] = None` |
|
||||
| 25 | `create_session.sh` (fallback CMD_FULL, ~L180-184) | Add `opencode) CMD_FULL="$BINARY --auto --agent build" ;;` |
|
||||
| 26 | `lib.sh` (`send_keys_safe` agent resolution, ~L2058-2061) | Add `*-opencode\|*-opencode-[0-9]*\|opencode\|opencode-[0-9]*) _sks_agent="opencode" ;;` |
|
||||
| 27 | `tests/test_tier1_unit.py` (~L969, 974) | Update expected CLI error usage assertions to include `opencode` |
|
||||
| 28 | 8× `SKILL.md` supported-agent lists | Update skill documentation to list `opencode` |
|
||||
| 29 | `tests/conftest.py` (`mock_agents`) | Add mock `opencode` binary fixture and sandbox state support |
|
||||
|
||||
---
|
||||
|
||||
## 4. TUI Readiness & Empirical Verification
|
||||
|
||||
- Initial candidate pattern: `OpenCode|Chat`
|
||||
- **Empirical Capture Gate**: Before finalizing the PR, capture rendered text from a live `opencode` TUI pane using `herdr pane capture` to confirm the exact ready token and prompt glyphs (`input_prompt`, `input_placeholder`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Drift Detection Integration & Dedicated Drift-C Block
|
||||
|
||||
In `multi-agent-mux-monitor/scripts/reconcile.sh`, add the dedicated OpenCode materialization block:
|
||||
|
||||
```python
|
||||
# === drift C (opencode): opencode 새 session id materialize (per-row own id) ===
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if row_agent(s) != 'opencode':
|
||||
continue
|
||||
if s.get('status') != 'running':
|
||||
continue
|
||||
if s.get('opencode_session_id_own'):
|
||||
continue
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
|
||||
# Resolve DB path via opencode db path or default XDG root
|
||||
db = _resolve_opencode_db(home, cwd)
|
||||
if not os.path.exists(db):
|
||||
continue
|
||||
|
||||
epoch_threshold = s.get('herdr_session_epoch', 0)
|
||||
sibling_claimed = [
|
||||
other.get('opencode_session_id_own')
|
||||
for other in d.get('herdr_sessions', [])
|
||||
if other is not s
|
||||
and (other.get('pane') or {}).get('cwd') == cwd
|
||||
and other.get('status') not in ('stopped', 'terminated')
|
||||
and other.get('opencode_session_id_own')
|
||||
]
|
||||
s_eval = dict(s)
|
||||
s_eval['_sibling_claimed_uuids'] = sibling_claimed
|
||||
|
||||
valid_candidates = []
|
||||
try:
|
||||
conn = sqlite3.connect(db)
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM session WHERE cwd=? AND created_at >= ? ORDER BY created_at DESC LIMIT 20",
|
||||
(cwd, epoch_threshold)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
for (uuid,) in rows:
|
||||
if uuid in (sibling_claimed or []):
|
||||
continue
|
||||
if verify_session_uuid(cwd, 'opencode', uuid, s_eval, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(valid_candidates) > 1:
|
||||
drifts.append({'class': 'C-ambiguous', 'name': s['name'],
|
||||
'msg': f"{s['name']}: {len(valid_candidates)} candidate transcripts newer than session epoch; not pinning"})
|
||||
s['last_visible_status'] = f"ambiguous: {len(valid_candidates)} candidates"
|
||||
actions.append(f"ambiguous candidates: {s['name']}")
|
||||
elif len(valid_candidates) == 1:
|
||||
uuid = valid_candidates[0]
|
||||
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "opencode" "{cwd}"']
|
||||
rc = subprocess.run(cmd).returncode
|
||||
if rc == 0:
|
||||
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=False)
|
||||
elif rc == 1:
|
||||
drifts.append({'class': 'C-warn', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport mismatch for candidate {uuid} — not pinned, will retry next cycle"})
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Definition of Done Checklist
|
||||
|
||||
- [ ] Execute `opencode db path` and SQLite `.schema` dump spike on a live install.
|
||||
- [ ] Confirm TUI ready tokens and input prompt glyphs via empirical pane capture.
|
||||
- [ ] Verify headless execution under `--auto` + `OPENCODE_PERMISSION='{"*":"allow"}'`.
|
||||
- [ ] Implement `lib_py/agents/adapters/opencode.py` with sandbox-safe `ctx.home_dir`.
|
||||
- [ ] Apply all 29 lockstep wiring points across `registry.py`, `lib.sh`, skill scripts, `lib_py` modules, and test fixtures.
|
||||
- [ ] Add dedicated `reconcile.sh` Drift-C materialization block and corresponding integration test.
|
||||
- [ ] Verify 100% test pass on full suite (`pytest tests/`).
|
||||
- [ ] Perform manual end-to-end smoke test (create → prompt → discover → stop/purge).
|
||||
|
||||
---
|
||||
|
||||
## 7. Effort Estimate
|
||||
|
||||
- **Total Estimate**: **~1.5 – 2.0 Days**
|
||||
- ½ day: SQLite schema spike (`opencode db path`) & TUI token live capture
|
||||
- ½ day: Adapter implementation & 29-point mechanical wiring
|
||||
- ½ day: Dedicated `reconcile.sh` Drift-C materialization block & integration tests
|
||||
- ½ day: End-to-end manual smoke tests & regression verification
|
||||
@@ -0,0 +1,454 @@
|
||||
# 🔌 Implementation Plan: Codex CLI (`openai/codex`) Agent Adapter for MAM (Rev.2)
|
||||
|
||||
- **Author**: `planner-reviewer-claude-01` (Planner), refined in response to `creator-agy-01`'s architectural challenge
|
||||
- **Job**: `e30e3123` → Rev.2 (`94a6db78`)
|
||||
- **Status**: Plan only — **no code changed in the repo**. Ready for review/refinement.
|
||||
- **Scope**: Add `codex` as a 6th first-class MAM agent backend (alongside `claude`, `agy`, `hermes`, `grok`, `opencode`), per `docs/NEW_AGENT_INTEGRATION_GUIDE.md`'s 5-step procedure.
|
||||
- **Durable Path**: `.agents/reports/planner-reviewer-claude-01/plan-e30e3123.md` (mirrored at `.agents/reports/implementation_plan.md`).
|
||||
- **Method**: Every CLI fact is verified against a **live, authenticated Codex CLI install already present on this machine** (`codex-cli 0.153.4`). Rev.2 additionally re-verified both of `creator-agy-01`'s challenge points live before accepting them — see §0.
|
||||
|
||||
---
|
||||
|
||||
## 0. Rev.2 Changelog — Response to `creator-agy-01`'s Architectural Challenge (Job `004c224d`)
|
||||
|
||||
`creator-agy-01` filed a formal challenge against Rev.1 with **1 critical defect + 1 supporting architectural constraint**. Per this session's standing discipline, neither was accepted on the challenger's word alone — both were independently re-verified live on this machine before being incorporated. **Both are accepted — no `[REBUT:]` filed.**
|
||||
|
||||
| # | Challenge | My independent verification | Disposition |
|
||||
|---|---|---|---|
|
||||
| 1 | **Critical**: Rev.1's `orc_onboard.sh` detection regex (`resume[[:space:]]+[0-9a-f-]{36}`) cannot match the actual command Rev.1 itself specifies (`codex resume --dangerously-bypass-approvals-and-sandbox <uuid>`), because the flag sits between `resume` and the UUID | Ran the exact `cmd_line` from the challenge through Rev.1's regex myself: `echo "$cmd_line" \| grep -oE 'resume[[:space:]]+[0-9a-f-]{36}'` → **empty, exit 1**. Confirmed genuine — Rev.1 contained a self-contradiction between §2 (`resume_spec`) and §1.9/§3 item 27 (the detection regex). Then tested the challenge's proposed Plan A regex on the same input → **correctly extracts the UUID, exit 0**. | **Accepted.** §1.9 and wiring item #27 rewritten to the flag-tolerant regex (verified working, not just proposed) |
|
||||
| 2 | **Supporting**: `~/.codex/state_N.sqlite` (a `threads` table indexing `id`/`cwd`/`rollout_path`) exists alongside the rollout JSONL files; Rev.1's "single-file format, no DB rows to delete" claim was only half true, and `purge_artifacts()` deleting only the `.jsonl` would leave an orphan DB row | `ls ~/.codex/*.sqlite*` on this machine → confirmed `state_5.sqlite` exists. `sqlite3 ~/.codex/state_5.sqlite ".schema threads"` → confirmed the exact `threads` table the challenge quoted. `sqlite3 ... "SELECT id,cwd,rollout_path FROM threads"` → confirmed 2 real rows exactly matching the 2 real rollout files found earlier. `strings` re-extraction confirmed the `stale_db_path`/`state db discrepancy`/`reconcile_rollout`/`read_repair_rollout_path: upsert_needed` literals the challenge cited. `codex delete --help` → confirmed `codex delete <SESSION> --force` ("Delete without prompting. SESSION must be a UUID") is real, exactly as the challenge's remediation option 1 proposed. | **Accepted**, with one refinement found during my own re-verification (not in the original challenge): `codex debug --help` has **no** `db path`-style introspection subcommand (unlike OpenCode's `opencode db path`), and the state file's numeric suffix (`state_5`) is unverified as stable across Codex versions/installs — hardcoding `state_5.sqlite` the way the challenge's raw example does would itself be fragile. §2's adapter design now resolves the state DB via a `state_*.sqlite` glob (most-recently-modified), and `purge_artifacts()` now shells out to `codex delete <uuid> --force` as the primary mechanism (letting Codex's own reconciliation logic keep the JSONL and DB consistent) with a manual-file-removal fallback only if that call fails |
|
||||
|
||||
Net effect: Rev.1's overall architecture and the 46-point wiring table stand — this challenge caught one genuine self-contradiction (the regex) and one genuine completeness gap (the state DB), both now closed. No other part of Rev.1 was found wrong during this re-verification pass.
|
||||
|
||||
---
|
||||
|
||||
## 1. Verified Codex CLI/Runtime Facts
|
||||
|
||||
Source: live `codex --help` / `codex resume --help` / `codex exec --help` / `codex login --help` / `codex delete --help` / `codex debug --help` / `codex doctor --summary` / `codex login status` output, direct `ls`/`find` of `~/.codex/`, live `sqlite3` schema/row inspection of `~/.codex/state_5.sqlite`, and offline `strings` extraction of the installed binary — all run on this machine.
|
||||
|
||||
### 1.1 Identity & Install
|
||||
- Binary: `codex` (this machine: `/Users/godopu16/.local/bin/codex`, version `codex-cli 0.153.4`).
|
||||
- Config/state root: `~/.codex/` by default, **overridable via the `CODEX_HOME` environment variable** (confirmed: `codex exec --ignore-user-config` help text explicitly says "auth still uses `CODEX_HOME`").
|
||||
|
||||
### 1.2 Execution Modes
|
||||
- `codex [OPTIONS] [PROMPT]` — launches the **interactive TUI** (default, what MAM spawns).
|
||||
- `codex exec [OPTIONS] [PROMPT]` (alias `e`) — **non-interactive** one-shot execution with `--json` JSONL event streaming and `-o/--output-last-message <FILE>`. Not used by MAM's spawn/resume flow, but confirms Codex has a scriptable mode.
|
||||
- `codex resume [OPTIONS] [SESSION_ID] [PROMPT]` — resumes a specific session by **UUID or session name, passed positionally**; `--last` picks the most recent session without a picker.
|
||||
- `codex delete <SESSION> --force` — permanently deletes a saved session by UUID; `--force` skips the confirmation prompt and requires `SESSION` to be a UUID (not a name). Verified via `codex delete --help`.
|
||||
|
||||
### 1.3 Relevant CLI Flags & Environment
|
||||
| Flag / Variable | Purpose | Verified via |
|
||||
|---|---|---|
|
||||
| `--dangerously-bypass-approvals-and-sandbox` | Skip all confirmation prompts and run without sandboxing — the unattended-automation flag | `codex --help`, `codex resume --help` (accepted on both) |
|
||||
| `-s, --sandbox <read-only\|workspace-write\|danger-full-access>` / `-a, --ask-for-approval <on-request\|never>` | Finer-grained alternative to the single bypass flag above | `codex --help` |
|
||||
| `--approve-for-me` | Routes approvals through automatic review under `workspace-write` — **not** a full bypass | `codex --help` |
|
||||
| `-C, --cd <DIR>` | Working-root directory for the agent | `codex --help` |
|
||||
| `--session-id <uuid>` | **Does not exist.** No such flag appears anywhere in `codex --help`'s full option list | Absence confirmed by reading the complete `--help` output |
|
||||
| `CODEX_HOME` | Overrides `~/.codex` config/session root | `codex exec --ignore-user-config` help text |
|
||||
| `OPENAI_API_KEY` | Read by `codex login --with-api-key` (via stdin); also stored inside `auth.json` under the `OPENAI_API_KEY` key once logged in | `codex login --help`, `~/.codex/auth.json` key inspection |
|
||||
|
||||
### 1.4 Unattended-Execution Decision
|
||||
Confirmed present. `--dangerously-bypass-approvals-and-sandbox` is accepted by both the top-level `codex` TUI launcher and `codex resume`, so `spawn_spec()`/`resume_spec()` can pass it unconditionally.
|
||||
|
||||
### 1.5 Session & Artifact Storage — Dual Layer (Rev.2: corrected)
|
||||
- **Primary transcript**: JSONL "rollout" files, one per session, at `~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<timestamp>-<uuid>.jsonl` — e.g. `~/.codex/sessions/2026/05/23/rollout-2026-05-23T08-23-56-019e5200-ab6a-70f2-b5da-e1ef403f7a40.jsonl`. First line is a `session_meta` record: `{"payload": {"id": "<uuid>", "cwd": "<path>", ...}}`.
|
||||
- **Secondary index (Rev.2, per the accepted challenge)**: `~/.codex/state_N.sqlite` (this machine: `state_5.sqlite`; **the numeric suffix is not assumed stable** — no `codex debug`/`codex` subcommand introspects its exact filename, unlike OpenCode's `opencode db path`, so the adapter must resolve it via a `state_*.sqlite` glob rather than hardcoding the suffix). Verified live schema: a `threads` table with `id TEXT PRIMARY KEY`, `rollout_path TEXT`, `cwd TEXT`, `created_at`/`updated_at` (plus many version-added trailing columns — the schema has clearly evolved via in-place `ALTER TABLE` across releases, another reason not to hand-roll fragile column-position assumptions). Verified live rows: both real sessions on this machine are indexed here with `id`/`cwd`/`rollout_path` exactly matching their on-disk `.jsonl` files.
|
||||
- **Codex's own resolution order** (inferred from binary string literals `state db discrepancy during find_thread_path_by_id_str_in_subdir: stale_db_path / mismatched_db_path / falling_back` and `reconcile_rollout` / `read_repair_rollout_path: upsert_needed`): Codex itself queries the state DB first, and has a **self-healing reconciliation path** for when the DB and filesystem disagree (falls back to a filesystem scan, then repairs the DB entry) — this is Codex's own built-in drift-correction, structurally analogous to MAM's own `reconcile.sh`.
|
||||
- **Architectural consequence for MAM's adapter**: mirror Codex's own resolution order — **query `state_*.sqlite` first** (fast, indexed, avoids any unbounded directory walk), **fall back to the bounded glob** over `sessions/*/*/*/rollout-*.jsonl` if the state DB is missing, unreadable, or the row is stale/absent (mirroring the exact "primary DB, fallback filesystem walk" shape `opencode.py`'s `_resolve_db()` already uses in this codebase, and mirroring what Codex's own binary does internally).
|
||||
|
||||
### 1.6 TUI Readiness (candidate, pending empirical capture)
|
||||
Offline `strings` extraction surfaced real UI literals: `"Ask Codex to do anything"` (composer placeholder) and the banner token `"Codex"`. No `❯` glyph or `─{10,}` rule literal found — different TUI layout family (boxed/panelled, not readline-style). Proposed candidate:
|
||||
- `ready_tokens`: `'Codex|Ask Codex to do anything'`
|
||||
- `input_placeholder`: `'Ask Codex to do anything'` (high-confidence — extracted literal)
|
||||
- `input_prompt` / `input_rule_pattern`: `None` (base-class default) pending live pane capture.
|
||||
|
||||
**Hard gate before merge**: capture a real `codex` TUI pane via `herdr pane capture` to confirm exact rendered text.
|
||||
|
||||
### 1.7 Exit Convention (verified deviation from every existing adapter)
|
||||
Binary string table twice describes exiting as a keypress: `"Ctrl+C to exit"`, `"ctrl+c quit"`. No `/exit`/`/quit`/`:q` literal found (real slash commands that do exist: `/keymap`, `/side`, `/review`, `/ide`, `/goal`).
|
||||
|
||||
`MAM_EXIT_KEY` is exported by the `facts` bridge and loaded into `send_keys_safe()`'s local scope (`lib.sh:2166`), but **no skill script actually sends it to a pane today, for any agent** — `stop_session.sh` never references `exit_key`/`MAM_EXIT_KEY`. So this remains a required-but-currently-inert contract field. Propose `exit_key = 'Ctrl+C'` (symbolic key name, matching herdr's existing `Escape` send convention), documented as a deliberate divergence.
|
||||
|
||||
### 1.8 Authentication
|
||||
- Interactive: `codex login` (ChatGPT OAuth) or `codex login --with-api-key` (reads `OPENAI_API_KEY` from stdin).
|
||||
- Credential file: `~/.codex/auth.json` (or `$CODEX_HOME/auth.json`) — verified present, keys `auth_mode`, `OPENAI_API_KEY`, `tokens`, `last_refresh`. `codex login status` confirms `"Logged in using ChatGPT"`; `codex doctor --summary` independently confirms `✓ auth is configured`.
|
||||
- `auth_ok()`: `os.path.exists(f"{codex_home}/auth.json")` **or** `os.environ.get('OPENAI_API_KEY')`. **Caveat honestly flagged**: whether a bare env var alone (without ever running `codex login --with-api-key`) is sufficient was not empirically tested (this machine is logged in via ChatGPT, not a raw API key).
|
||||
|
||||
### 1.9 Session-ID Detection for `orc_onboard.sh` (Rev.2: regex defect fixed)
|
||||
1. **argv form**: Codex passes the UUID as `codex resume <uuid>` — positional, no flag name. **Rev.1's regex was defective**: `grep -oE 'resume[[:space:]]+[0-9a-f-]{36}'` requires the UUID immediately after `resume`, but the real command line this plan's own `resume_spec`/`CMD_FULL` produce is `codex resume --dangerously-bypass-approvals-and-sandbox <uuid>` — a flag sits in between, so the regex never matches (verified live: exit 1, empty output). **Corrected regex** (challenge's Plan A, live-verified to work): allow arbitrary tokens between `resume` and the UUID, then extract only the UUID:
|
||||
```bash
|
||||
codex)
|
||||
detected_id=$(echo "$cmd_line" | grep -oE 'resume[[:space:]]+.*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | grep -oE '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | head -n 1 || true)
|
||||
;;
|
||||
```
|
||||
Already safe under `orc_onboard.sh:135`'s existing `is_valid_id "$detected_id"` UUID-format re-validation, so a slightly looser extraction regex carries no correctness risk.
|
||||
2. **env var fallback**: no `CODEX_SESSION_ID`-style environment variable found anywhere in `codex --help`. Leave `env_var=""` for `codex` — do not fabricate one.
|
||||
|
||||
---
|
||||
|
||||
## 2. Adapter Contract Decisions (`BaseAgentAdapter`) — Rev.2
|
||||
|
||||
| Property/Method | Value | Rationale |
|
||||
|---|---|---|
|
||||
| `name` | `'codex'` | Lowercase identifier |
|
||||
| `own_key` | `'codex_session_id_own'` | Matches `<agent>_session_id_own` convention |
|
||||
| `ready_tokens` | `'Codex\|Ask Codex to do anything'` (candidate, §1.6) | Extracted real UI literal, pending live pane-capture confirmation |
|
||||
| `exit_key` | `'Ctrl+C'` (§1.7) | No typed slash command exists; currently operationally inert framework-wide |
|
||||
| `delegate_agent_key` | `'codex-cli'` | Matches the `<name>-cli`/`-agent`/`-build`/`-code` suffix convention |
|
||||
| `identity_cache_fields` | `('session_id',)` | Single-field cache, modern pattern |
|
||||
| `input_placeholder` | `'Ask Codex to do anything'` | Verified real literal |
|
||||
| `input_prompt` / `input_rule_pattern` | `None` | No stable glyph found; different TUI layout family |
|
||||
| `spawn_spec(binary, session_uuid, use_wrapper)` | `f"{binary} --dangerously-bypass-approvals-and-sandbox"` (ignores `session_uuid` — Class A) | §1.3/§1.4 |
|
||||
| `resume_spec(binary, session_uuid, materialized)` | if `materialized and session_uuid`: `f"{binary} resume --dangerously-bypass-approvals-and-sandbox {session_uuid}"`, else same as `spawn_spec` | Verified `codex resume [OPTIONS] [SESSION_ID]` usage template |
|
||||
| `auth_ok(run_cmd)` | `os.path.exists(f"{codex_home}/auth.json") or bool(os.environ.get('OPENAI_API_KEY'))` | §1.8, sandbox-safe via `ctx.home_dir` |
|
||||
| `artifact_path(uuid, ctx)` | **Rev.2**: query `state_*.sqlite`'s `threads.rollout_path WHERE id=?` first; fall back to the bounded glob `sessions/*/*/*/rollout-*-{uuid}.jsonl` if the DB lookup misses | §1.5 — mirrors Codex's own DB-first, filesystem-fallback resolution order |
|
||||
| `verify_artifact(uuid, ctx)` | Resolve path via the same DB-first/glob-fallback helper; parse first JSONL line's `payload.id`/`payload.cwd` (or use the DB row's `cwd` column directly, skipping the file read, when the DB hit succeeds); apply `ctx.epoch` mtime check | Rev.2: DB row already has `cwd` — avoids opening the file at all on the common (DB-hit) path |
|
||||
| `purge_artifacts(uuid, ctx)` | **Rev.2**: primary — `subprocess.run([binary_or_'codex', 'delete', uuid, '--force'], ...)`; fallback (only if that call fails/binary unavailable) — manually `os.remove()` the rollout file located via the DB-first/glob-fallback helper, **and** best-effort `DELETE FROM threads WHERE id=?` against the resolved `state_*.sqlite` if it exists | §0 item 2 — avoids orphan DB rows; lets Codex's own reconciliation logic own consistency whenever possible instead of MAM hand-rolling SQL against an evolving schema |
|
||||
| `discover(ctx)` | **Rev.2**: query `state_*.sqlite`'s `threads` table (`SELECT id, cwd, updated_at FROM threads ORDER BY updated_at DESC LIMIT 50`, filtered by `cwd`/`epoch` in Python) first; fall back to the recursive-glob approach from Rev.1 only if the DB is absent/unreadable | Rev.2 — DB query is a single indexed lookup vs. an N-file glob-and-open walk; also naturally bounded (no unbounded-growth concern for the DB path) |
|
||||
|
||||
### Adapter code sketch (`.agents/skills/lib_py/agents/adapters/codex.py`) — Rev.2
|
||||
|
||||
```python
|
||||
import os, json, glob, sqlite3, subprocess
|
||||
from typing import Optional, Any, List
|
||||
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext
|
||||
from lib_py.verify_session import workspace_key
|
||||
|
||||
class CodexAgentAdapter(BaseAgentAdapter):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return 'codex'
|
||||
|
||||
@property
|
||||
def own_key(self) -> str:
|
||||
return 'codex_session_id_own'
|
||||
|
||||
@property
|
||||
def ready_tokens(self) -> str:
|
||||
return 'Codex|Ask Codex to do anything'
|
||||
|
||||
@property
|
||||
def exit_key(self) -> str:
|
||||
return 'Ctrl+C'
|
||||
|
||||
@property
|
||||
def delegate_agent_key(self) -> str:
|
||||
return 'codex-cli'
|
||||
|
||||
@property
|
||||
def identity_cache_fields(self) -> tuple:
|
||||
return ('session_id',)
|
||||
|
||||
@property
|
||||
def input_placeholder(self) -> Optional[str]:
|
||||
return 'Ask Codex to do anything'
|
||||
|
||||
def _codex_home(self, ctx: DiscoveryContext) -> str:
|
||||
return os.environ.get('CODEX_HOME') or f"{ctx.home_dir}/.codex"
|
||||
|
||||
def _state_db(self, ctx: DiscoveryContext) -> Optional[str]:
|
||||
# Rev.2: no 'codex db path'-equivalent subcommand exists; the numeric
|
||||
# suffix (state_5.sqlite on this machine) is unverified as stable
|
||||
# across versions, so resolve dynamically rather than hardcoding it.
|
||||
matches = glob.glob(f"{self._codex_home(ctx)}/state_*.sqlite")
|
||||
if not matches:
|
||||
return None
|
||||
return max(matches, key=os.path.getmtime)
|
||||
|
||||
def _find_rollout_via_db(self, uuid: str, ctx: DiscoveryContext):
|
||||
"""Returns (rollout_path, cwd) from the state DB, or (None, None)."""
|
||||
db = self._state_db(ctx)
|
||||
if not db or not os.path.exists(db):
|
||||
return None, None
|
||||
try:
|
||||
conn = sqlite3.connect(db)
|
||||
row = conn.execute("SELECT rollout_path, cwd FROM threads WHERE id=?", (uuid,)).fetchone()
|
||||
conn.close()
|
||||
if row and row[0] and os.path.exists(row[0]):
|
||||
return row[0], row[1]
|
||||
except Exception:
|
||||
pass
|
||||
return None, None
|
||||
|
||||
def _find_rollout(self, uuid: str, ctx: DiscoveryContext) -> Optional[str]:
|
||||
path, _ = self._find_rollout_via_db(uuid, ctx)
|
||||
if path:
|
||||
return path
|
||||
# Fallback: bounded glob over the date-nested tree (state DB missing/stale)
|
||||
matches = glob.glob(f"{self._codex_home(ctx)}/sessions/*/*/*/rollout-*-{uuid}.jsonl")
|
||||
return matches[0] if matches else None
|
||||
|
||||
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
|
||||
return self._find_rollout(uuid, ctx) or f"{self._codex_home(ctx)}/sessions/rollout-{uuid}.jsonl"
|
||||
|
||||
def verify_artifact(self, uuid: str, ctx: DiscoveryContext) -> bool:
|
||||
db_path, db_cwd = self._find_rollout_via_db(uuid, ctx)
|
||||
path = db_path or self._find_rollout(uuid, ctx)
|
||||
if not path or not os.path.exists(path):
|
||||
return False
|
||||
if ctx.epoch and os.path.getmtime(path) < ctx.epoch:
|
||||
return False
|
||||
found_cwd = db_cwd
|
||||
if not found_cwd:
|
||||
try:
|
||||
with open(path) as f:
|
||||
first = f.readline()
|
||||
found_cwd = json.loads(first).get('payload', {}).get('cwd')
|
||||
except Exception:
|
||||
return False
|
||||
if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd):
|
||||
return False
|
||||
return True
|
||||
|
||||
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> List[str]:
|
||||
purged = []
|
||||
path = self._find_rollout(uuid, ctx)
|
||||
# Rev.2: prefer the CLI's own delete path (keeps rollout file + state
|
||||
# DB row consistent via Codex's own reconciliation logic).
|
||||
try:
|
||||
res = subprocess.run(['codex', 'delete', uuid, '--force'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if res.returncode == 0:
|
||||
if path:
|
||||
purged.append(path)
|
||||
return purged
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: manual removal of the file, plus a best-effort DB row delete.
|
||||
if path and os.path.exists(path):
|
||||
os.remove(path)
|
||||
purged.append(path)
|
||||
db = self._state_db(ctx)
|
||||
if db and os.path.exists(db):
|
||||
try:
|
||||
conn = sqlite3.connect(db)
|
||||
conn.execute("DELETE FROM threads WHERE id=?", (uuid,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
purged.append(f"state db row for thread: {uuid}")
|
||||
except Exception:
|
||||
pass
|
||||
return purged
|
||||
|
||||
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
|
||||
return f"{binary} --dangerously-bypass-approvals-and-sandbox"
|
||||
|
||||
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
|
||||
if materialized and session_uuid:
|
||||
return f"{binary} resume --dangerously-bypass-approvals-and-sandbox {session_uuid}"
|
||||
return f"{binary} --dangerously-bypass-approvals-and-sandbox"
|
||||
|
||||
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
|
||||
home = os.environ.get('HOME_DIR') or os.environ.get('HOME') or os.path.expanduser('~')
|
||||
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
|
||||
return os.path.exists(f"{codex_home}/auth.json") or bool(os.environ.get('OPENAI_API_KEY'))
|
||||
|
||||
def discover(self, ctx: DiscoveryContext) -> List[str]:
|
||||
db = self._state_db(ctx)
|
||||
if db and os.path.exists(db):
|
||||
try:
|
||||
conn = sqlite3.connect(db)
|
||||
rows = conn.execute(
|
||||
"SELECT id, cwd, updated_at FROM threads ORDER BY updated_at DESC LIMIT 50"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
candidates = []
|
||||
for uuid, cwd, _ in rows:
|
||||
if ctx.workspace and cwd and workspace_key(cwd) != workspace_key(ctx.workspace):
|
||||
continue
|
||||
if uuid and self.verify_artifact(uuid, ctx):
|
||||
candidates.append(uuid)
|
||||
return candidates
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: recursive glob (state DB absent/unreadable)
|
||||
root = self._codex_home(ctx)
|
||||
candidates = []
|
||||
paths = sorted(glob.glob(f"{root}/sessions/*/*/*/rollout-*.jsonl"), key=os.path.getmtime, reverse=True)[:50]
|
||||
for path in paths:
|
||||
uuid = os.path.basename(path)[:-6][-36:]
|
||||
if uuid and self.verify_artifact(uuid, ctx):
|
||||
candidates.append(uuid)
|
||||
return candidates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Full Lockstep Wiring Table (Rev.2)
|
||||
|
||||
Same 46-item table as Rev.1, with item #27 corrected per §0/§1.9. Re-derived from the current, live state of every file that mentions `opencode` today (33 files, `.agents/reports/` excluded).
|
||||
|
||||
| # | Component / File | Specific Edit Required |
|
||||
|---|---|---|
|
||||
| 1 | `.agents/skills/lib_py/agents/adapters/codex.py` | Create new adapter class `CodexAgentAdapter` per §2 (Rev.2: DB-first artifact resolution + `codex delete --force`-based purge) |
|
||||
| 2 | `.agents/skills/lib_py/agents/registry.py:9,16` | Import `CodexAgentAdapter`; add `'codex': CodexAgentAdapter()` to `_ADAPTERS` |
|
||||
| 3 | `.agents/skills/lib_py/atomic_yaml.py:132` | Add `'codex_session_id_own'` to `running_keys` |
|
||||
| 4 | `.agents/skills/lib_py/verify_session.py:69` | Add `'codex_session_id_own'` to the `mam_row_own_uuid()` field list |
|
||||
| 5 | `.agents/skills/lib_py/workspace_uuid.py:7-12,34` | Add `'codex': 'codex_session_id_own'` to `OWN_KEY`; add `'codex_session_id_own'` to the `running_ids` field list |
|
||||
| 6 | `.agents/skills/lib.sh:669` (kind mapping) | Add `*-creator-codex\|*-planner-codex\|*-reviewer-codex) kind="codex" ;;` |
|
||||
| 7 | `.agents/skills/lib.sh:675,680` (fallback grep chain, name + `final_cmd`) | Add `elif echo "$name" \| grep -qi "codex"; then kind="codex"` and the `final_cmd` equivalent |
|
||||
| 8 | `.agents/skills/lib.sh:694` (binary dedup tuple) | Add `'codex'` to `('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex')` |
|
||||
| 9 | `.agents/skills/lib.sh:2175` (`send_keys_safe` agent resolution) | Add `*-codex\|*-codex-[0-9]*\|codex\|codex-[0-9]*) _sks_agent="codex" ;;` |
|
||||
| 10 | `create_session.sh:4,29,33` (usage/help text) | Add `codex` to the `<claude\|agy\|hermes\|grok\|opencode>` listings |
|
||||
| 11 | `create_session.sh:93-94` (`--agent` whitelist) | Add `codex` to the accepted-values case and the error message |
|
||||
| 12 | `create_session.sh` (auth preflight, new `elif` block after the existing `opencode` one at line 129) | `elif [ "$AGENT" = "codex" ]; then` check `~/.codex/auth.json` (respecting `CODEX_HOME`) or `OPENAI_API_KEY`, else `codex --version` smoke check |
|
||||
| 13 | `create_session.sh:192` (fallback `CMD_FULL` case) | Add `codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;` |
|
||||
| 14 | `create_session.sh:216` (`spawn()` dispatch case) | Add `codex` to the `agy\|hermes\|grok\|opencode)` case — no special env-var export needed |
|
||||
| 15 | `create_session.sh:330` (delegate-agent key resolution) | Add `codex) delegate_agent="codex-cli" ;;` |
|
||||
| 16 | `create_session.sh:438` (YAML init Python block) | Add `elif agent == 'codex': entry['codex_session_id_own'] = None; entry['session_id_source'] = 'pending-discovery'; entry['session_id_verified'] = False; entry['last_visible_status'] = "unverified"` |
|
||||
| 17 | `resolve_session_id.sh:4,16,39-40` | Add `codex` to usage text and the `--agent` whitelist case |
|
||||
| 18 | `resume_session.sh:12,45` | Add `codex` to usage text and the `--agent` whitelist case |
|
||||
| 19 | `resume_session.sh:65` (FRESH_SPAWN Class-A case) | Add `codex` to `agy\|hermes\|opencode) FRESH_SPAWN=1 ;;` — Codex is Class A |
|
||||
| 20 | `resume_session.sh:121` (fallback spawn-spec case) | Add `codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;` |
|
||||
| 21 | `resume_session.sh:132` (fallback resume-spec case) | Add `codex) CMD_FULL="${RESOLVED_BIN} resume --dangerously-bypass-approvals-and-sandbox $UUID" ;;` |
|
||||
| 22 | `update_yaml_resumed.sh:7,14,17` | Add `codex` to usage docs and the Class-A comment listing |
|
||||
| 23 | `update_yaml_resumed.sh:228` (Python block) | Add `elif agent == 'codex': target['pane']['cmd'] = 'codex'`; if `uuid`: `cmd_full = f'codex resume --dangerously-bypass-approvals-and-sandbox {uuid}'`, `target['codex_session_id_own'] = uuid`; else `cmd_full = os.environ.get('CMD_FULL_OPT', '') or 'codex --dangerously-bypass-approvals-and-sandbox'` |
|
||||
| 24 | `stop_session.sh:4,15,94-95` | Add `codex` to usage docs and the `--agent` whitelist case + error message |
|
||||
| 25 | `stop_session.sh:290` (Python capture block) | Add `elif agent == 'codex': target['codex_session_id_own'] = captured` |
|
||||
| 26 | `orc_onboard.sh:106` (agent-family match) | Add `codex` to the `claude\|agy\|hermes\|grok\|opencode)` case |
|
||||
| 27 | `orc_onboard.sh:117-133` (CLI-argv detection) | **Rev.2 corrected**: new `codex)` branch using the flag-tolerant regex from §1.9 (`resume[[:space:]]+.*<uuid-pattern>` then extract just the UUID) — **not** the Rev.1 regex, which is confirmed broken against this plan's own `resume_spec` output |
|
||||
| 28 | `orc_onboard.sh:142-148` (env-var fallback) | Add `codex) env_var=""` explicitly (no known env var — do not fabricate one) |
|
||||
| 29 | `reconcile.sh:523,542` (agent-name tuples) | Add `'codex'` to both `('claude', 'agy', 'hermes', 'grok', 'opencode')` tuples |
|
||||
| 30 | `reconcile.sh:603` (fresh-row entry init) | Add `elif agent == 'codex': entry['codex_session_id_own'] = None` |
|
||||
| 31 | `reconcile.sh:616` (`OWN_KEY_BY_AGENT`) | Add `'codex'` to the tuple |
|
||||
| 32 | `reconcile.sh` (new dedicated block, after the existing opencode block at line ~787-897) | New `# === drift C (codex) ===` block, **Rev.2**: state-DB-first with glob fallback (see §4) |
|
||||
| 33 | 8× `SKILL.md` supported-agent lists (`multi-agent-mux-create:132,163-166`; `multi-agent-mux-orc-onboard:11,35,41`; `multi-agent-mux-resume:52,55,61,73,99,110`; `multi-agent-mux-stop:40`) | Add `codex` everywhere `opencode` currently appears in these four files; the other 4 `SKILL.md` files need only the version-frontmatter bump (§6) |
|
||||
| 34 | `.agents/skills/multi-agent-mux-delegate-job/job-protocol.md:3`, `README.md:3` | No change needed — `codex` already listed informally |
|
||||
| 35 | `tests/conftest.py` | Add a `codex` mock binary fixture (mirroring `opencode_bin`, line ~972-990) that writes a fake `rollout-<ts>-<uuid>.jsonl` under a temp `$CODEX_HOME/sessions/<Y>/<M>/<D>/` **and** a matching `state_1.sqlite` `threads` row (Rev.2: needed since `discover()`/`verify_artifact()` now query the DB first); add ready-banner/own-key dict entries (lines ~553, ~584) |
|
||||
| 36 | `tests/test_a4_adapter_contract.py` | Add `codex` to `EXPECTED_OWN_KEYS`, the property tuple, `expected_modal` (`None`), the `facts_bridge` loop (line ~112), a `_seed_codex_session()` helper (mirroring `_seed_opencode_session`, line ~132, **Rev.2**: seeding both the `.jsonl` and the `state_*.sqlite` row), and unit tests for `spawn_spec`/`resume_spec`/`auth_ok`/`purge_artifacts` (**Rev.2**: including a test that `purge_artifacts` falls back to manual removal when the `codex delete` subprocess call fails)/`discover` |
|
||||
| 37 | `tests/test_c1_tui_readiness.py` | Add `codex` to the facts-bridge and `send_keys_safe` agent-resolution loops (line ~491) and a `check_sks` assertion pair (mirroring lines 528-529, 539-540) |
|
||||
| 38 | `tests/test_tier1_unit.py` | Update the agent-tuple assertions (lines ~963, 969, 974, 988-989, 994, 1033) to include `codex`; add `test_codex_shell_and_scripts_integration` mirroring `test_opencode_shell_and_scripts_integration` (lines 982-1044); **Rev.2**: add a dedicated regression test asserting `orc_onboard.sh`'s `codex` regex correctly extracts the UUID from `codex resume --dangerously-bypass-approvals-and-sandbox <uuid>` — this is the exact scenario Rev.1 got wrong, so it must be locked down by a test, not just fixed in prose |
|
||||
| 39 | `tests/test_tier2_component.py` | Add `test_comp_codex_create_and_reconcile_and_stop` mirroring `test_comp_opencode_create_and_reconcile_and_stop` (line ~1229) |
|
||||
| 40 | `.mam.env.example` (after the existing OpenCode block, line ~110) | Document `OPENAI_API_KEY` and `CODEX_HOME` as an optional override |
|
||||
| 41 | `docs/NEW_AGENT_INTEGRATION_GUIDE.md` §3 (difficulty matrix) | Update the `Codex CLI` row from the old guessed values to the verified ones from §1 |
|
||||
| 42 | `.agents/reports/new_agent_types_roadmap.md` §4 | Add a cross-link noting the Codex row is superseded by this plan |
|
||||
| 43 | `.agents/MULTI_AGENT_RULES.md:21` / `.ko.md:21` | Add `codex` to the Team-Leader agent-name parenthetical, scope confined to that one line each |
|
||||
| 44 | `README.md:3,17,45,136` / `README.ko.md:3,17,27,118` | Add `codex` to the 5-agent → 6-agent listings, incl. a `codex resume <uuid>` example (positional form, §1.9) |
|
||||
| 45 | `BOOTSTRAP.md:197` / `.ko.md:196` | Add `codex` to the onboarding agent-name parenthetical |
|
||||
| 46 | `deploy/INSTALL.md:3` | Add `Codex` to the parenthetical agent list |
|
||||
|
||||
---
|
||||
|
||||
## 4. `reconcile.sh` Drift-C Block for Codex (Rev.2: state-DB-first)
|
||||
|
||||
```python
|
||||
# === drift C (codex): codex 새 session id materialize (per-row own id) ===
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if row_agent(s) != 'codex':
|
||||
continue
|
||||
if s.get('status') != 'running':
|
||||
continue
|
||||
if s.get('codex_session_id_own'):
|
||||
continue
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
|
||||
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
|
||||
epoch_threshold = s.get('herdr_session_epoch', 0)
|
||||
|
||||
sibling_claimed = [
|
||||
other.get('codex_session_id_own')
|
||||
for other in d.get('herdr_sessions', [])
|
||||
if other is not s
|
||||
and (other.get('pane') or {}).get('cwd') == cwd
|
||||
and other.get('status') not in ('stopped', 'terminated')
|
||||
and other.get('codex_session_id_own')
|
||||
]
|
||||
s_eval = dict(s)
|
||||
s_eval['_sibling_claimed_uuids'] = sibling_claimed
|
||||
|
||||
valid_candidates = []
|
||||
try:
|
||||
# Rev.2: state DB first (indexed, single query) — dynamic filename,
|
||||
# no hardcoded suffix (see §1.5 / §2 _state_db()).
|
||||
state_matches = glob.glob(f"{codex_home}/state_*.sqlite")
|
||||
state_db = max(state_matches, key=os.path.getmtime) if state_matches else None
|
||||
rows = []
|
||||
if state_db and os.path.exists(state_db):
|
||||
conn = sqlite3.connect(state_db)
|
||||
rows = conn.execute(
|
||||
"SELECT id, updated_at FROM threads WHERE cwd=? ORDER BY updated_at DESC LIMIT 20",
|
||||
(cwd,)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
if not rows:
|
||||
# Fallback: bounded glob over the date-nested tree
|
||||
paths = sorted(
|
||||
glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*.jsonl"),
|
||||
key=os.path.getmtime, reverse=True
|
||||
)[:50]
|
||||
rows = [(os.path.basename(p)[:-6][-36:], os.path.getmtime(p)) for p in paths]
|
||||
|
||||
for uuid, ts in rows:
|
||||
if epoch_threshold and ts and ts < epoch_threshold:
|
||||
continue
|
||||
if uuid in (sibling_claimed or []):
|
||||
continue
|
||||
if verify_session_uuid(cwd, 'codex', uuid, s_eval, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(valid_candidates) > 1:
|
||||
drifts.append({'class': 'C-ambiguous', 'name': s['name'],
|
||||
'msg': f"{s['name']}: {len(valid_candidates)} candidate transcripts newer than session epoch; not pinning"})
|
||||
s['last_visible_status'] = f"ambiguous: {len(valid_candidates)} candidates"
|
||||
actions.append(f"ambiguous candidates: {s['name']}")
|
||||
elif len(valid_candidates) == 1:
|
||||
uuid = valid_candidates[0]
|
||||
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "codex" "{cwd}"']
|
||||
rc = subprocess.run(cmd).returncode
|
||||
if rc == 0:
|
||||
_pin_and_verify_resume(s, 'codex', cwd, uuid, degraded=False)
|
||||
elif rc == 1:
|
||||
drifts.append({'class': 'C-warn', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport mismatch for candidate {uuid} — not pinned, will retry next cycle"})
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'codex', cwd, uuid, degraded=True)
|
||||
```
|
||||
|
||||
`verify_session_uuid()` (shared, `lib_py/verify_session.py`) needs a `codex_session_id_own` entry added to its own-key list (wiring item #4) for the `C0`-class "confirm, don't discover" path.
|
||||
|
||||
---
|
||||
|
||||
## 5. `status.sh`
|
||||
|
||||
Both `resume_on_disk()`-style helpers (lines ~56-61, ~234-239) iterate `('claude', 'agy', 'hermes', 'grok', 'opencode')`. Add `codex` to both tuples and a new `if agent == 'codex':` branch resolving via the same DB-first/glob-fallback helper as the adapter (§2), returning `'yes'`/`'MISSING'`/`'no'`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Version & Documentation Bump
|
||||
|
||||
Current: `MAM_VERSION="4.1.3"`. Propose **`4.1.3 → 4.2.0` (MINOR)**, matching the OpenCode precedent, in strict 3-way lockstep (`lib.sh:32`, `VERSIONS.md`, all 8 `SKILL.md` frontmatter) per `tests/test_version_consistency.py`. No new agent-specific guide needed (no local-LLM/Ollama angle for Codex).
|
||||
|
||||
---
|
||||
|
||||
## 7. Empirical Verification Gate (must happen before merge)
|
||||
|
||||
1. Live pane capture to confirm `ready_tokens`/`input_placeholder` (§1.6).
|
||||
2. Live smoke test that `--dangerously-bypass-approvals-and-sandbox` suppresses every confirmation prompt in a real multi-file-edit task.
|
||||
3. Live test of the corrected `orc_onboard.sh` regex (§1.9) against a real running `codex resume <uuid>` process's `ps`/`/proc` argv snapshot — the regex was verified against a synthetic string in this planning pass, not a real captured process line.
|
||||
4. **(Rev.2, new)** Confirm the `state_*.sqlite` glob resolves correctly on a *different* Codex install/version than this machine's `state_5.sqlite` — the numeric suffix is not a documented, guaranteed-stable contract, only an observed fact on one install.
|
||||
|
||||
---
|
||||
|
||||
## 8. Definition of Done Checklist
|
||||
|
||||
- [ ] Complete Phase 0 empirical gate (§7), including the new Rev.2 state-DB-portability check.
|
||||
- [ ] Implement `.agents/skills/lib_py/agents/adapters/codex.py` per §2 (DB-first resolution, `codex delete --force`-based purge with fallback).
|
||||
- [ ] Apply all 46 lockstep wiring points in §3, including the **corrected** `orc_onboard.sh` regex (item #27).
|
||||
- [ ] Add the dedicated `reconcile.sh` Drift-C block (§4, state-DB-first) and its `C-ambiguous`-scenario test.
|
||||
- [ ] Add all test coverage in §3 items #35-39, **including the regression test locking down the corrected `orc_onboard.sh` regex** (item #38) and the `purge_artifacts` CLI-then-fallback test (item #36); `pytest tests/` shows 0 regressions.
|
||||
- [ ] Bump version to `4.2.0` in strict 3-way lockstep (§6).
|
||||
- [ ] Correct the two speculative prior reports' Codex rows (§3 items #41-42).
|
||||
- [ ] Manual end-to-end smoke test: create → prompt → status shows `yes` → reconcile materializes `codex_session_id_own` → stop → resume round-trips → purge leaves no orphan `.jsonl` or `state_*.sqlite` row.
|
||||
|
||||
---
|
||||
|
||||
## 9. Effort Estimate
|
||||
|
||||
- **Total Estimate**: **~1.5-1.75 days** (Rev.2: +0.25 day vs. Rev.1 for the state-DB-aware adapter logic and the corrected/tested `orc_onboard.sh` regex).
|
||||
- ~2-3 hrs: Phase 0 empirical gate (§7), including the new state-DB-portability check.
|
||||
- ~4-5 hrs: adapter implementation (DB-first + glob-fallback throughout) + the 46-point mechanical wiring pass.
|
||||
- ~2-3 hrs: dedicated `reconcile.sh` Drift-C block + `status.sh` + full test suite additions, including the two Rev.2 regression tests.
|
||||
- ~2 hrs: docs/version bump + end-to-end manual smoke test (including a purge-leaves-no-orphan-row check).
|
||||
@@ -0,0 +1,46 @@
|
||||
# 🔍 Cross-Review — Codex CLI Agent Backend, Post-Remediation (Job `175b653b`)
|
||||
|
||||
- **Reviewer**: `planner-reviewer-claude-01`
|
||||
- **Target**: cumulative working-tree diff implementing `plan-e30e3123.md` Rev.2 — same codex integration I already reviewed as `[VERDICT: PASS]` in job `36148213`, now including a remediation for the blocking defect that `reviewer-creator-grok-01` (job `5bfd80b8`, NOT PASS) and `reviewer-opencode-01` (job `9cc33e22`, NOT PASS) both independently found *after* my earlier PASS.
|
||||
- **Method**: Diff was truncated in the brief (221KB/3239 lines over the review limit), so per the brief's own instruction I inspected the working tree directly rather than the truncated text. I did not take any peer's verdict — including my own prior one — at face value; every claim below is re-verified against live files or live test runs in this pass.
|
||||
|
||||
---
|
||||
|
||||
## 1. What changed since my prior PASS (job `36148213`)
|
||||
|
||||
File-level diff stat confirms the core implementation is **untouched** since my last review: `codex.py`, `registry.py`, `lib.sh`, all 6 skill scripts, and all 7 codex-specific test additions show the same shape as before. The only substantive addition is to `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (+246/-9 lines) plus a one-line Status-field edit to the Codex plan.
|
||||
|
||||
## 2. Independently confirming the blocking defect Grok/OpenCode found (and I missed)
|
||||
|
||||
My own `36148213` review checked code correctness and test results but never inspected whether the archived plan-document *paths* themselves were still intact — a real gap in my prior pass. Re-derived from primary evidence, not from trusting either NOT-PASS report's narrative:
|
||||
|
||||
- `git show HEAD:.agents/reports/implementation_plan.md` → the tracked HEAD content is the **full 249-line OpenCode Rev.3 plan**.
|
||||
- Before this fix, `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (the document that's supposed to be the OpenCode Rev.3 plan's *permanent, namespaced* archive per its own Rev.3 changelog text) was only a 16-line stub reading "**Durable output (updated in place)**: [.agents/reports/implementation_plan.md]" — i.e. it pointed at the shared mirror instead of containing the content itself.
|
||||
- I (as Planner, in job `e30e3123`) overwrote `.agents/reports/implementation_plan.md` with the Codex plan. Since the stub was the *only* other reference, this action would have deleted the OpenCode Rev.3 plan's full text from the working tree entirely (recoverable only via `git show HEAD:...`, not from any live path) — a genuine 유실 (data-loss) defect, and, per `reviewer-opencode-01`'s report, the same recurring failure class as two earlier incidents in this repo's history (`aca0b7e8`, job `56120ad3`'s F1).
|
||||
|
||||
## 3. Verifying the remediation is complete and correct — not just present
|
||||
|
||||
- `git diff -- .agents/reports/planner-reviewer-claude-01/plan-de667f0f.md`: the file now contains a 4-line `> [!NOTE]` banner followed by the complete original OpenCode Rev.3 text.
|
||||
- Byte-level check: `diff <(tail -n +5 <git-show-HEAD-copy>) <(tail -n +5 plan-de667f0f.md)` → **empty diff** — the restored body is character-for-character identical to the original tracked content, not a paraphrase or a partial/garbled restore. Line counts confirm exactly (249 original + 4-line banner = 253).
|
||||
- `.agents/reports/implementation_plan.md` still holds the Codex plan (the mirror pairing for the *current* plan is preserved) — `diff` against `.agents/reports/planner-reviewer-claude-01/plan-e30e3123.md` returns empty, confirming the two stay in lockstep as intended.
|
||||
- The Codex plan's Status line was updated from the stale "Plan only — no code changed in the repo" to "Implemented (v4.2.0) — full code, tests, and version lockstep present in repo" — resolving `reviewer-opencode-01`'s third (non-blocking) point.
|
||||
- Swept for the same fragile pattern elsewhere: `grep -rl "Durable output (updated in place)"` finds 4 more stub-style archives (`plan-264c3b5d.md`, `plan-a33a133e.md`, `plan-28f9b565.md`, `plan-a348e91c.md`), but none of them point at `implementation_plan.md` or any other file currently targeted by an active mirror pattern — they're a pre-existing, unrelated hygiene concern, not a live risk this diff introduces or needs to fix. Noting it for awareness, not counting it against this PR.
|
||||
|
||||
## 4. Re-confirming the underlying codex implementation is still sound
|
||||
|
||||
Did not simply assume "unchanged code = still correct" — re-ran verification fresh against the current tree:
|
||||
- `pytest tests/test_version_consistency.py tests/test_a4_adapter_contract.py -k "codex or version"` → 6 passed.
|
||||
- Full suite, run by me just now: `pytest tests/ -q` → **462 passed, 0 failed, 819.92s (13m39s)**.
|
||||
- Version lockstep re-confirmed at `4.2.0` (`lib.sh:32`, `VERSIONS.md`, all 8 `SKILL.md`).
|
||||
|
||||
## 5. Standing minor finding (unchanged, still non-blocking)
|
||||
|
||||
The dormant escaping bug I flagged in `36148213` (`tests/conftest.py`'s codex mock writes a literal `\\n` instead of a real newline in the mock rollout JSONL, ~line 658) is still present and still dormant for the same reason (`verify_artifact()`'s DB-first path never parses that file in any current test). Not fixed in this round; still recommend a one-line follow-up, still not blocking.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verdict
|
||||
|
||||
The blocking defect that correctly earned this diff two NOT PASS votes has been remediated, and I independently verified the fix is complete, byte-faithful, and doesn't introduce any new problem — not merely that a fix file exists. The underlying codex implementation remains fully correct per a fresh full-suite run (462/462). One pre-existing minor test-fixture nit remains open as a non-blocking follow-up.
|
||||
|
||||
[VERDICT: PASS]
|
||||
Reference in new issue
Block a user