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
@@ -18,7 +18,7 @@
|
||||
- **모호성 제거**: 사용자의 요구사항에 모호한 부분이 있다면 작업을 추측하여 진행하지 말고, 즉시 사용자에게 질문하여 명확히 해야 합니다 (`/grill-me` 슬래시 명령어 권장).
|
||||
|
||||
### 👥 Team Leaders (팀장)
|
||||
새롭게 생성되는 에이전트(`antigravity`, `claude`, `hermes`, `grok`, `opencode` 등)는 각 팀의 **팀장** 역할을 수행합니다. 총괄 매니저로부터 작업을 위임받아 개발 또는 리뷰 워크플로우를 주도합니다.
|
||||
새롭게 생성되는 에이전트(`antigravity`, `claude`, `hermes`, `grok`, `opencode`, `codex` 등)는 각 팀의 **팀장** 역할을 수행합니다. 총괄 매니저로부터 작업을 위임받아 개발 또는 리뷰 워크플로우를 주도합니다.
|
||||
- **Developer Team Leader (개발 팀장)**:
|
||||
- 총괄 매니저로부터 작업을 위임받습니다.
|
||||
- **작업 분석 및 계획**: 주어진 작업을 철저히 분석하고, 작은 단위로 문제를 나누어 세부 계획을 수립합니다.
|
||||
|
||||
@@ -18,7 +18,7 @@ We clearly separate responsibilities and permissions between roles to reduce bot
|
||||
- **Ambiguity Resolution**: If a user's requirements contain ambiguous details, do not guess. Immediately ask the user for clarification (we recommend using the `/grill-me` slash command).
|
||||
|
||||
### 👥 Team Leaders (팀장)
|
||||
Newly spawned agents (e.g., `antigravity`, `claude`, `hermes`, `grok`, `opencode`) act as **Team Leaders** of their respective groups. They receive delegated tasks from the General Manager and manage implementation or review workflows.
|
||||
Newly spawned agents (e.g., `antigravity`, `claude`, `hermes`, `grok`, `opencode`, `codex`) act as **Team Leaders** of their respective groups. They receive delegated tasks from the General Manager and manage implementation or review workflows.
|
||||
- **Developer Team Leader (개발 팀장)**:
|
||||
- Receives tasks from the General Manager.
|
||||
- **Task Breakdown & Planning**: Thoroughly analyzes the task, breaks it down into small units, and creates a plan.
|
||||
|
||||
@@ -1,209 +1,391 @@
|
||||
# 🔌 Implementation Plan: OpenCode (`anomalyco/opencode`) Agent Adapter for MAM (Rev.3)
|
||||
# 🔌 Implementation Plan: Codex CLI (`openai/codex`) Agent Adapter for MAM (Rev.2)
|
||||
|
||||
- **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`)
|
||||
- **Author**: `planner-reviewer-claude-01` (Planner), refined in response to `creator-agy-01`'s architectural challenge
|
||||
- **Job**: `e30e3123` → Rev.2 (`94a6db78`)
|
||||
- **Status**: Implemented (v4.2.0) — **full code, tests, and version lockstep present in repo**.
|
||||
- **Scope**: Add `codex` as a 6th first-class MAM agent backend (alongside `claude`, `agy`, `hermes`, `grok`, `opencode`), per `docs/NEW_AGENT_INTEGRATION_GUIDE.md`'s 5-step procedure.
|
||||
- **Durable Path**: `.agents/reports/planner-reviewer-claude-01/plan-e30e3123.md` (mirrored at `.agents/reports/implementation_plan.md`).
|
||||
- **Method**: Every CLI fact is verified against a **live, authenticated Codex CLI install already present on this machine** (`codex-cli 0.153.4`). Rev.2 additionally re-verified both of `creator-agy-01`'s challenge points live before accepting them — see §0.
|
||||
|
||||
---
|
||||
|
||||
## Rev.3 Changelog (Job `78f332a9`)
|
||||
## 0. Rev.2 Changelog — Response to `creator-agy-01`'s Architectural Challenge (Job `004c224d`)
|
||||
|
||||
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**:
|
||||
`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.**
|
||||
|
||||
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.
|
||||
| # | 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.
|
||||
|
||||
---
|
||||
|
||||
## Rev.2 Changelog (Job `c65b0081`)
|
||||
## 1. Verified Codex CLI/Runtime Facts
|
||||
|
||||
`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.
|
||||
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
|
||||
- **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).
|
||||
- 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
|
||||
- `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.
|
||||
- `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 |
|
||||
|---|---|
|
||||
| `--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"}'`) |
|
||||
| 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 |
|
||||
|
||||
**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 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.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 — 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.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 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.
|
||||
|
||||
### 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.
|
||||
**Hard gate before merge**: capture a real `codex` TUI pane via `herdr pane capture` to confirm exact rendered text.
|
||||
|
||||
### 1.7 Exit Key
|
||||
- Slash command `/exit` (aliases `/quit`, `/q`).
|
||||
### 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`)
|
||||
## 2. Adapter Contract Decisions (`BaseAgentAdapter`) — Rev.2
|
||||
|
||||
| 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` |
|
||||
| `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. Complete 29-Point Wiring & Lockstep Table
|
||||
## 3. Full Lockstep Wiring Table (Rev.2)
|
||||
|
||||
All 29 locations below must be updated in lockstep:
|
||||
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/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 |
|
||||
| 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. 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:
|
||||
## 4. `reconcile.sh` Drift-C Block for Codex (Rev.2: state-DB-first)
|
||||
|
||||
```python
|
||||
# === drift C (opencode): opencode 새 session id materialize (per-row own id) ===
|
||||
# === drift C (codex): codex 새 session id materialize (per-row own id) ===
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if row_agent(s) != 'opencode':
|
||||
if row_agent(s) != 'codex':
|
||||
continue
|
||||
if s.get('status') != 'running':
|
||||
continue
|
||||
if s.get('opencode_session_id_own'):
|
||||
if s.get('codex_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
|
||||
|
||||
codex_home = os.environ.get('CODEX_HOME') or f"{home}/.codex"
|
||||
epoch_threshold = s.get('herdr_session_epoch', 0)
|
||||
|
||||
sibling_claimed = [
|
||||
other.get('opencode_session_id_own')
|
||||
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('opencode_session_id_own')
|
||||
and other.get('codex_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:
|
||||
# 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, 'opencode', uuid, s_eval, mode="discover"):
|
||||
if verify_session_uuid(cwd, 'codex', uuid, s_eval, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -215,35 +397,58 @@ for s in d.get('herdr_sessions', []):
|
||||
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}"']
|
||||
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, 'opencode', cwd, uuid, degraded=False)
|
||||
_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, 'opencode', cwd, uuid, degraded=True)
|
||||
_pin_and_verify_resume(s, 'codex', 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).
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
## 7. Effort Estimate
|
||||
## 5. `status.sh`
|
||||
|
||||
- **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
|
||||
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).
|
||||
@@ -200,7 +200,7 @@ class NewAgentAdapter(BaseAgentAdapter):
|
||||
| Agent Type | Target CLI / Tool | Session Storage Format | Auth Strategy | Ready Tokens Pattern | Complexity Tier | Estimated Effort |
|
||||
|---|---|---|---|---|:---:|:---:|
|
||||
| **OpenCode** | `opencode` (CLI / TUI) | JSON files in `~/.opencode/sessions/` | Local token / API key | `OpenCode\|Chat\|Welcome` | **Tier 1 (Low)** | ~0.5 day |
|
||||
| **Codex CLI** | `codex` / `openai-codex` | JSONL in `~/.codex/projects/` | API Key (`OPENAI_API_KEY`) | `OpenAI Codex\|Assistant\|❯` | **Tier 1 (Low)** | ~0.5 day |
|
||||
| **Codex CLI** | `codex` | Rollouts in `~/.codex/sessions/` & SQLite `state_*.sqlite` | `auth.json` / `OPENAI_API_KEY` | `Codex\|Ask Codex to do anything` | **Implemented (v4.2.0)** | Done |
|
||||
| **Kimi CLI** | `kimi` (Moonshot CLI) | SQLite DB in `~/.kimi/history.db` | API Key in `~/.kimi/config` | `Kimi\|Moonshot\|Chat` | **Tier 1 (Low)** | ~0.5 day |
|
||||
| **Grok-Build** | `grok` / `grok-build` | JSON in `~/.grok/sessions/` | Token file / Env var | `Grok\|xAI\|Building` | **Tier 1 (Low)** | ~0.5 day |
|
||||
| **Cursor CLI** | `cursor` (Headless/RPC) | State DB in `~/.cursor/` or RPC port | OAuth / Cookie | `Cursor\|Connected\|Listening` | **Tier 2 (Medium)** | ~1.5 days |
|
||||
|
||||
@@ -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]
|
||||
@@ -0,0 +1,69 @@
|
||||
# Cross-review: Codex (`openai/codex`) Rev.2 plan + v4.2.0 implementation (re-review)
|
||||
|
||||
- **Reviewer**: `reviewer-creator-grok-01`
|
||||
- **Job**: `607ee97f` (re-review after `5bfd80b8` NOT PASS)
|
||||
- **Prior finding**: F1 — OpenCode Rev.3 유실 at `.agents/reports/implementation_plan.md` (`plan-de667f0f.md` was a 16-line stub).
|
||||
- **Diff**: 39 files +1223/−277, plus untracked `codex.py` and `plan-e30e3123.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prior F1 — fixed
|
||||
|
||||
Independent check (not taken from the creator's word):
|
||||
|
||||
| Path | Now |
|
||||
|---|---|
|
||||
| `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` | 253 lines. 4-line NOTE names it the durable OpenCode Rev.3 archive (job `78f332a9`) and notes the mirror now holds Codex. Body after the NOTE is **byte-identical** to `git show HEAD:.agents/reports/implementation_plan.md` (249 lines). |
|
||||
| `.agents/reports/implementation_plan.md` | Codex Rev.2. `cmp` identical to `plan-e30e3123.md`. |
|
||||
| `.agents/reports/planner-reviewer-claude-01/plan-e30e3123.md` | Full Codex Rev.2 (454 lines). |
|
||||
| Root `implementation_plan.md` | NATS *MAM 메시징 백플레인 전환 실행 로드맵* intact. |
|
||||
| Codex **Status** | `Implemented (v4.2.0) — full code, tests, and version lockstep present in repo.` (was “Plan only — no code changed”). |
|
||||
|
||||
Required fix from `5bfd80b8` items 1–4 all landed. OpenCode Rev.3 is no longer only in git history.
|
||||
|
||||
The archived OpenCode body's own “Durable Path” line still lists `.agents/reports/implementation_plan.md` as a mirror — that is the original Rev.3 sentence, preserved because the body is identical. The NOTE header supersedes it. Residual nit, not 유실.
|
||||
|
||||
---
|
||||
|
||||
## 2. Codex implementation (unchanged, still correct)
|
||||
|
||||
Spot-checked after the archive edit; wiring was not regressed.
|
||||
|
||||
| Item | Result |
|
||||
|---|---|
|
||||
| Adapter | Untracked `codex.py` (158 lines). Class A spawn; `resume_spec` with positional UUID after `--dangerously-bypass-approvals-and-sandbox`; `_state_db()` `state_*.sqlite` glob by mtime; DB-first / glob fallback; `purge_artifacts` `codex delete <uuid> --force` then file+SQL fallback. `py_compile` OK. |
|
||||
| Challenge 1 | `orc_onboard.sh:133-134` flag-tolerant regex; `codex) env_var=""`; `test_orc_onboard_codex_flag_tolerant_regex` present. |
|
||||
| Challenge 2 | No hardcoded `state_5` in the adapter; dedicated `# === drift C (codex) ===` with PRAGMA-tolerant order column. |
|
||||
| Class A resume | `resume_session.sh:65` `agy\|hermes\|opencode\|codex) FRESH_SPAWN=1`. |
|
||||
| Spawn | `create_session.sh` `agy\|hermes\|grok\|codex)` — not folded into OpenCode's `OPENCODE_PERMISSION` branch. Correct. |
|
||||
| Shell | `bash -n` on create/resume/stop/orc_onboard/reconcile/status — OK. |
|
||||
| SemVer | `lib.sh:32` `MAM_VERSION="4.2.0"`; `VERSIONS.md` header/date/8-row matrix/changelog; 8× SKILL.md `version: 4.2.0`. MINOR class matches OpenCode v4.1.0. |
|
||||
|
||||
Live CLI facts from the previous pass still hold on this machine (`codex-cli 0.153.4`; `--force` exists; `updated_at` is unix seconds).
|
||||
|
||||
---
|
||||
|
||||
## 3. Tests
|
||||
|
||||
```
|
||||
pytest tests/test_a4_adapter_contract.py tests/test_c1_tui_readiness.py \
|
||||
tests/test_tier1_unit.py tests/test_version_consistency.py \
|
||||
tests/test_tier2_component.py -q --tb=line
|
||||
→ 160 passed in 316.03s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Residuals (non-blocking)
|
||||
|
||||
1. **TUI empirical gate still open.** `ready_tokens='Codex|Ask Codex to do anything'` remains a candidate. Do not tick plan §7 from this review.
|
||||
2. **`auth_ok` uses `HOME_DIR`/`HOME`**, not `ctx` — same as grok/agy/opencode; tests patch `HOME_DIR`.
|
||||
3. **`test_codex_purge_artifacts` exercises fallback only** (`u-del` is not a UUID, so `codex delete --force` fails). CLI-success path untested.
|
||||
4. **Resume `SKILL.md` Workflow** still interpolates `$UUID` into `CMD_FULL` after the Class A empty-UUID branch. Real `resume_session.sh` uses spawn-spec. Pre-existing nit (`901bfbe8`).
|
||||
5. Untracked `.agents/reports/reviewer-opencode-01/report-9cc33e22.md` is a peer review written against the pre-archive tree (that report's NOT PASS is now stale). Not a Codex code defect.
|
||||
|
||||
No `[ESCALATE: PLANNER]`.
|
||||
|
||||
---
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,38 @@
|
||||
# 🔍 Cross-Review: Codex Integration — F1 Remediation Round (Job 6e55fa1e)
|
||||
|
||||
- **Reviewer**: `reviewer-opencode-01` (role: reviewer)
|
||||
- **Target**: The same `support-codex` working tree as my job `9cc33e22` NOT PASS, now with the F1 remediation applied (brief's diff was truncated at 221KB/3239 lines — I inspected the working tree directly, as the brief instructed).
|
||||
- **Method**: F1 remediation verified byte-level against HEAD's Rev.3 content; code state re-confirmed identical to what I fully verified in `9cc33e22` (462/462 full suite); targeted regression re-run this round.
|
||||
|
||||
---
|
||||
|
||||
## 1. F1 (blocking in `9cc33e22`): OpenCode Rev.3 prior-art clobbering — ALL REMEDIATION ITEMS VERIFIED
|
||||
|
||||
My prior NOT PASS required three mechanical fixes (convergent with Grok's `5bfd80b8`). All are applied:
|
||||
|
||||
1. **Rev.3 restored to its namespaced path** — `.agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` is now 253 lines: a 3-line `[!NOTE]` header (honestly explaining that the mirror now houses the Codex plan) followed by the **full Rev.3 body, byte-identical to `git show HEAD:.agents/reports/implementation_plan.md`** (249/249 lines, verified programmatically — the only difference is the intentional note header). The OpenCode Rev.3 plan — the executed rationale for the shipped v4.1.0 integration — is durably preserved at the exact path its own Rev.3 changelog convention named.
|
||||
2. **Codex plan kept at both paths** — `implementation_plan.md` and `plan-e30e3123.md` are byte-identical (verified with `diff`, exit 0). The pairing convention is intact.
|
||||
3. **Status line synced** — both Codex plan copies now read `**Status**: Implemented (v4.2.0) — full code, tests, and version lockstep present in repo.` — no longer contradicting the tree (the old "Plan only — no code changed" line was stale the moment the implementation landed).
|
||||
|
||||
## 2. Code state — unchanged from my full verification in `9cc33e22`
|
||||
|
||||
The working-tree code diff vs HEAD is exactly what I fully verified last round (the stat-sheet line-count differences vs the previous brief are base-artifact variance, not new edits — confirmed by inspecting the actual `lib.sh` diff hunks: the same 5 codex touchpoints + version line, nothing more):
|
||||
|
||||
- Adapter `codex.py` (verified line-by-line last round: dynamic `state_*.sqlite` glob, DB-first/glob-fallback, `codex delete --force` purge, Class A specs).
|
||||
- 46-point wiring complete; Class B untouched; flag-tolerant regex landed + regression-locked.
|
||||
- Lockstep at v4.2.0 MINOR (correct precedent — new agent = v4.1.0 opencode case).
|
||||
- **Full suite 462/462 in my own run** (`9cc33e22`, 955s) — no code changed since, so that result stands; this round I re-ran the lockstep + a4 contract suites (**31 passed**) and the codex tier-2 lifecycle test (**1 passed**) as spot confirmation.
|
||||
- The `new_agent_types_roadmap.md` Codex row now correctly reads "Implemented (v4.2.0) / Done" with the verified storage/auth/token facts — the speculative row is superseded as planned.
|
||||
|
||||
## 3. Process notes
|
||||
|
||||
- The brief's diff was truncated (over the 200KB limit) and explicitly instructed reviewing the working tree directly — done; the file-level summary matched what I found on disk.
|
||||
- My own `report-9cc33e22.md` appears as an untracked file in this diff — the standard report-archival convention, to be committed with the next docs commit.
|
||||
- The other new jobs in this window (`e20c098f`, `feb11a70`, `05fa8dd2`) are sandbox worker fixtures, not peer reviews — not cited as evidence.
|
||||
- Minor observation: the remediation note header in `plan-de667f0f.md` leaves one extra blank line vs HEAD's body — cosmetic, intentional-looking (header separation), immaterial.
|
||||
|
||||
## 4. Verdict
|
||||
|
||||
The single blocking defect from my `9cc33e22` NOT PASS (and Grok's convergent `5bfd80b8`) is fully and correctly remediated: the OpenCode Rev.3 plan is durably restored at its designated namespaced path (byte-identical body), the Codex plan correctly occupies both its mirror and its own archive path, and the stale Status line is synced. The Codex integration itself was already verified sound last round (462/462 full suite, live CLI ground truth, challenge fixes landed and regression-locked), and spot regression this round confirms nothing regressed. No lint, functionality, or data-loss issues remain; no planner escalation.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,58 @@
|
||||
# 🔍 Cross-Review: Codex CLI Agent Integration & v4.2.0 Bump (Job 9cc33e22)
|
||||
|
||||
- **Reviewer**: `reviewer-opencode-01` (role: reviewer)
|
||||
- **Target**: Branch `support-codex` working tree implementing `plan-e30e3123.md` Rev.2 (the Codex 6th-agent integration + v4.2.0 MINOR bump): 34 modified + 2 new files, 46-point wiring table, plus the plan documents.
|
||||
- **Method**: Every plan claim re-verified against live source, the live `codex` CLI (0.153.4) on this machine, my own test runs (full 462-test suite), and my own re-execution of the challenge-critical regex. Peer verdicts (Claude PASS `36148213`, Grok NOT PASS `5bfd80b8`) checked against primary evidence — they conflict, and I resolved the conflict myself.
|
||||
|
||||
---
|
||||
|
||||
## 1. Codex implementation — verified correct (multi-layer)
|
||||
|
||||
**My verification runs**:
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| `bash -n` all 6 touched shell scripts + `py_compile codex.py` | OK |
|
||||
| `pytest tests/test_version_consistency.py` | **2 passed** — lockstep at 4.2.0 |
|
||||
| `pytest tests/test_a4_adapter_contract.py` | **29 passed** (was 25; +4 codex) |
|
||||
| Full suite `pytest tests/ -q` | **462 passed / 0 failed** (955s, my own run) — 455 prior + 7 new codex tests |
|
||||
| Collect-only | 462 tests — count matches |
|
||||
|
||||
**Adapter** (`codex.py`, 158 lines): matches the Rev.2 plan sketch exactly — `state_*.sqlite` glob with mtime-max (no hardcoded suffix, per the accepted challenge refinement), DB-first/glob-fallback rollout resolution, `verify_artifact` using the DB row's `cwd` when available, `purge_artifacts` shelling `codex delete <uuid> --force` first with manual file+DB-row fallback, Class A spawn/resume specs (`--dangerously-bypass-approvals-and-sandbox`). The facts bridge emits the exact contract values.
|
||||
|
||||
**Live CLI ground truth** (this machine): `codex-cli 0.153.4` confirmed; `~/.codex/state_5.sqlite` exists with the exact `threads(id, cwd, rollout_path)` schema and 2 real rows matching the 2 real rollout JSONL files — the plan's §1.5 dual-layer storage description is accurate, and the adapter's DB-first order mirrors Codex's own reconciliation literals.
|
||||
|
||||
**Flag-tolerant regex** (challenge item 1): I re-ran both regexes myself against `codex resume --dangerously-bypass-approvals-and-sandbox <uuid>` — Rev.1's regex returns empty (exit 1, defect confirmed), the landed regex correctly extracts the UUID (exit 0). The regression test `test_orc_onboard_codex_flag_tolerant_regex` locks it down, and the wiring landed at `orc_onboard.sh:133-135` exactly as planned.
|
||||
|
||||
**Wiring spot-checks** (from the 46-point table): lib.sh kind mapping (`*-creator-codex`), `send_keys_safe` `_sks_agent="codex"`, create auth preflight (line 136), FRESH_SPAWN Class A arm (`resume_session.sh:65`), stop capture (`codex_session_id_own = captured`), reconcile drift-C codex block (state-DB-first with a schema-tolerant order-column PRAGMA check — a defensive improvement beyond the plan), status.sh codex branches in both formatters — all present and correct. Class B (claude/grok) paths untouched.
|
||||
|
||||
**SemVer v4.2.0 MINOR**: correct — new agent backend = the v4.1.0 (opencode) precedent exactly. Lockstep complete (lib.sh:32, VERSIONS.md header/date/matrix/changelog, 8× SKILL.md at 4.2.0), and the header's `기준 브랜치: support-codex` honestly reflects the working branch.
|
||||
|
||||
## 2. The blocking defect — prior-art clobbering at `.agents/reports/implementation_plan.md`
|
||||
|
||||
Grok (`5bfd80b8`, NOT PASS) flagged this; Claude's PASS (`36148213`) did not address it. I independently confirmed Grok is right:
|
||||
|
||||
- **HEAD's** `.agents/reports/implementation_plan.md` is the full **OpenCode Rev.3 plan** (249 lines, tracked since the v4.1.0 integration) — the executed rationale for the opencode backend that shipped.
|
||||
- The working tree **replaces it wholesale** with the Codex Rev.2 plan (391 lines).
|
||||
- The Rev.3 changelog's own convention says the plan is "permanently housed under `plan-de667f0f.md` and **mirrored** at `implementation_plan.md`" — but `plan-de667f0f.md` is only a **16-line stub** report pointing at the mirror. The mirror was the *only* full copy; this change deletes it from the tree.
|
||||
- Verified: no working-tree file contains the OpenCode Rev.3 full text (grep for its title across all `.md` — empty). Git history retains it, but the durable named path loses it.
|
||||
- This is the same 유실/prior-art-clobbering class as the `aca0b7e8` round (v4.0.0 consensus artifact) and job `56120ad3`'s F1 (OpenCode Rev.2 overwrote the root NATS plan) — a recurring failure mode this session has rejected every time.
|
||||
|
||||
**Required fix (mechanical, not design-level — Grok's proposed remediation is correct and I concur)**:
|
||||
1. Restore the full OpenCode Rev.3 into the namespaced path: `git show HEAD:.agents/reports/implementation_plan.md > .agents/reports/planner-reviewer-claude-01/plan-de667f0f.md` (optionally with a 2-line header noting the mirror now holds the Codex plan).
|
||||
2. Keep the Codex plan at both `implementation_plan.md` and `plan-e30e3123.md` (that pairing is correct).
|
||||
3. Update the Codex plan's Status line ("Plan only — no code changed in the repo") — it now contradicts the tree, which contains the full implementation.
|
||||
|
||||
**Notably**: Claude is the author of both plans (the overwritten Rev.3 and the overwriting Rev.2) and its PASS review focused on the code wiring (which is genuinely correct) but did not examine the archive-path loss. Grok's review is the one that checked the durable-paths claim — and its finding survives my independent verification.
|
||||
|
||||
## 3. Secondary observations (non-blocking)
|
||||
|
||||
- Claude's PASS also flagged (correctly, in its §"self-report citations") that the Worker's self-report contains several wrong file:line citations — the code is right but the report's citations don't all check out. Worth fixing for the record but not blocking.
|
||||
- `conftest.py`'s codex mock seeds `state_5.sqlite` with a fixed name — fine for a hermetic sandbox (the adapter globs `state_*.sqlite`, so any name works); no portability concern.
|
||||
- The drift-C codex block's schema-tolerant `PRAGMA table_info` order-column detection is *more* defensive than the plan required — a good hardening against Codex's evolving schema.
|
||||
|
||||
## 4. Verdict
|
||||
|
||||
The Codex integration itself is exemplary: plan verified against live CLI facts, the challenge round caught a genuine regex defect (now fixed, regression-locked, and re-verified by me) and a genuine storage-layer gap (now handled with DB-first resolution and CLI-owned purge), the 46-point wiring is complete and correct, Class B contracts untouched, lockstep holds at 4.2.0, and the full suite passes 462/462 in my own run. But the diff also clobbers the tracked OpenCode Rev.3 plan from its only full-copy durable path — a prior-art 유실 defect of the same class this session has consistently rejected, confirmed by me from primary evidence and not addressed by the plan author's own review. It is mechanically fixable (restore Rev.3 to its namespaced path, keep the Codex pairing, sync the Status line) without touching any code.
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
@@ -29,7 +29,7 @@ export WORKSPACE_ROOT
|
||||
# Framework semantic version. Single runtime source of truth; kept in lockstep
|
||||
# with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py.
|
||||
# NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable.
|
||||
MAM_VERSION="4.1.3"
|
||||
MAM_VERSION="4.2.0"
|
||||
export MAM_VERSION
|
||||
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
|
||||
|
||||
@@ -667,17 +667,20 @@ print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens))
|
||||
*-creator-hermes|*-planner-hermes|*-reviewer-hermes) kind="hermes" ;;
|
||||
*-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok" ;;
|
||||
*-creator-opencode|*-planner-opencode|*-reviewer-opencode) kind="opencode" ;;
|
||||
*-creator-codex|*-planner-codex|*-reviewer-codex) kind="codex" ;;
|
||||
*)
|
||||
if echo "$name" | grep -qi "agy"; then kind="agy"
|
||||
elif echo "$name" | grep -qi "claude"; then kind="claude"
|
||||
elif echo "$name" | grep -qi "hermes"; then kind="hermes"
|
||||
elif echo "$name" | grep -qi "grok"; then kind="grok"
|
||||
elif echo "$name" | grep -qi "opencode"; then kind="opencode"
|
||||
elif echo "$name" | grep -qi "codex"; then kind="codex"
|
||||
elif echo "${final_cmd:-}" | grep -qi "agy"; then kind="agy"
|
||||
elif echo "${final_cmd:-}" | grep -qi "claude"; then kind="claude"
|
||||
elif echo "${final_cmd:-}" | grep -qi "hermes"; then kind="hermes"
|
||||
elif echo "${final_cmd:-}" | grep -qi "grok"; then kind="grok"
|
||||
elif echo "${final_cmd:-}" | grep -qi "opencode"; then kind="opencode"
|
||||
elif echo "${final_cmd:-}" | grep -qi "codex"; then kind="codex"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -691,7 +694,7 @@ try:
|
||||
tokens = shlex.split(cmd)
|
||||
if tokens:
|
||||
first = tokens[0]
|
||||
if first in ('claude', 'agy', 'hermes', 'grok', 'opencode') or any(first.endswith('/' + a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode')) or (kind and (first == kind or first.endswith('/' + kind))):
|
||||
if first in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex') or any(first.endswith('/' + a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex')) or (kind and (first == kind or first.endswith('/' + kind))):
|
||||
tokens = tokens[1:]
|
||||
print(' '.join(shlex.quote(t) for t in tokens))
|
||||
except Exception:
|
||||
@@ -2173,6 +2176,7 @@ send_keys_safe() {
|
||||
*-hermes|*-hermes-[0-9]*|hermes|hermes-[0-9]*) _sks_agent="hermes" ;;
|
||||
*-grok|*-grok-[0-9]*|grok|grok-[0-9]*) _sks_agent="grok" ;;
|
||||
*-opencode|*-opencode-[0-9]*|opencode|opencode-[0-9]*) _sks_agent="opencode" ;;
|
||||
*-codex|*-codex-[0-9]*|codex|codex-[0-9]*) _sks_agent="codex" ;;
|
||||
esac
|
||||
if [ -n "$_sks_agent" ]; then
|
||||
eval "$("$(_delegate_py_bin)" -m lib_py.agents facts "$_sks_agent" 2>/dev/null || true)"
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
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]:
|
||||
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
|
||||
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)
|
||||
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
|
||||
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)
|
||||
cur = conn.cursor()
|
||||
cur.execute("PRAGMA table_info(threads)")
|
||||
cols = {r[1] for r in cur.fetchall()}
|
||||
order_col = "updated_at" if "updated_at" in cols else ("created_at" if "created_at" in cols else None)
|
||||
order_clause = f"ORDER BY {order_col} DESC" if order_col else ""
|
||||
rows = conn.execute(
|
||||
f"SELECT id, cwd FROM threads {order_clause} 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
|
||||
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
|
||||
@@ -7,6 +7,7 @@ from lib_py.agents.adapters.agy import AgyAgentAdapter
|
||||
from lib_py.agents.adapters.hermes import HermesAgentAdapter
|
||||
from lib_py.agents.adapters.grok import GrokAgentAdapter
|
||||
from lib_py.agents.adapters.opencode import OpenCodeAgentAdapter
|
||||
from lib_py.agents.adapters.codex import CodexAgentAdapter
|
||||
|
||||
_ADAPTERS: Dict[str, BaseAgentAdapter] = {
|
||||
'claude': ClaudeAgentAdapter(),
|
||||
@@ -14,6 +15,7 @@ _ADAPTERS: Dict[str, BaseAgentAdapter] = {
|
||||
'hermes': HermesAgentAdapter(),
|
||||
'grok': GrokAgentAdapter(),
|
||||
'opencode': OpenCodeAgentAdapter(),
|
||||
'codex': CodexAgentAdapter(),
|
||||
}
|
||||
|
||||
def get_adapter(agent_name: str) -> Optional[BaseAgentAdapter]:
|
||||
|
||||
@@ -129,7 +129,7 @@ def atomic_dump_yaml_main():
|
||||
if name in old_roles and s.get('role') != old_roles[name]:
|
||||
raise SystemExit(f"VALIDATE: role of session {name!r} cannot be modified from {old_roles[name]!r} to {s.get('role')!r}")
|
||||
|
||||
running_keys = ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'grok_session_id_own', 'opencode_session_id_own']
|
||||
running_keys = ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'grok_session_id_own', 'opencode_session_id_own', 'codex_session_id_own']
|
||||
id_to_session = {}
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('status') == 'running':
|
||||
|
||||
@@ -66,7 +66,7 @@ def mam_orchestrator_uuids():
|
||||
def mam_row_own_uuid(row):
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "grok_session_id_own", "opencode_session_id_own"]:
|
||||
for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "grok_session_id_own", "opencode_session_id_own", "codex_session_id_own"]:
|
||||
v = row.get(k)
|
||||
if v:
|
||||
return v
|
||||
|
||||
@@ -9,7 +9,8 @@ OWN_KEY = {
|
||||
'agy': 'agy_conversation_id_own',
|
||||
'hermes': 'hermes_conversation_id_own',
|
||||
'grok': 'grok_session_id_own',
|
||||
'opencode': 'opencode_session_id_own'
|
||||
'opencode': 'opencode_session_id_own',
|
||||
'codex': 'codex_session_id_own'
|
||||
}
|
||||
|
||||
from lib_py.paths import resolve_home
|
||||
@@ -31,7 +32,7 @@ def find_workspace_uuid_main():
|
||||
if s_item.get('status') == 'running':
|
||||
if target and s_item.get('name') == target:
|
||||
continue
|
||||
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'grok_session_id_own', 'opencode_session_id_own']:
|
||||
for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'grok_session_id_own', 'opencode_session_id_own', 'codex_session_id_own']:
|
||||
val = s_item.get(k)
|
||||
if val:
|
||||
running_ids.add(val)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: multi-agent-mux-create
|
||||
description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace."
|
||||
version: 4.1.3
|
||||
version: 4.2.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
@@ -129,7 +129,7 @@ herdr_sessions:
|
||||
|
||||
```bash
|
||||
WORKSPACE=/path/to/project
|
||||
AGENT=claude # claude | agy | hermes | grok | opencode — always pass it explicitly
|
||||
AGENT=claude # claude | agy | hermes | grok | opencode | codex — always pass it explicitly
|
||||
source .agents/skills/lib.sh
|
||||
SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")"
|
||||
|
||||
@@ -163,7 +163,10 @@ case "$AGENT" in
|
||||
opencode)
|
||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "opencode --auto --agent build"
|
||||
;;
|
||||
*) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT"; exit 2 ;;
|
||||
codex)
|
||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "codex --dangerously-bypass-approvals-and-sandbox"
|
||||
;;
|
||||
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT"; exit 2 ;;
|
||||
esac
|
||||
|
||||
# 3. Wait for agent TUI to be ready (varies: claude ~5s, agy ~3s)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# create_session.sh — multi-agent-mux-create 의 부속 스크립트
|
||||
# Usage:
|
||||
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper]
|
||||
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper]
|
||||
#
|
||||
# 동작:
|
||||
# 1) preflight: herdr/claude/agy 가용성, workspace 존재
|
||||
@@ -26,11 +26,11 @@ source "$_lib_sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> --role <role> [options]
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> --role <role> [options]
|
||||
|
||||
Options:
|
||||
--workspace PATH project directory (required)
|
||||
--agent AGENT claude | agy | hermes | grok | opencode (required)
|
||||
--agent AGENT claude | agy | hermes | grok | opencode | codex (required)
|
||||
--role ROLE assigned role (required)
|
||||
--session NAME herdr session name (default: derived from workspace)
|
||||
--wrapper force use of ~/.local/bin/<session> wrapper even if not present
|
||||
@@ -90,8 +90,8 @@ fi
|
||||
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; }
|
||||
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; }
|
||||
case "$AGENT" in
|
||||
claude|agy|hermes|grok|opencode) ;;
|
||||
*) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT" >&2; exit 2 ;;
|
||||
claude|agy|hermes|grok|opencode|codex) ;;
|
||||
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT" >&2; exit 2 ;;
|
||||
esac
|
||||
[ -n "$ROLE" ] || { echo "ERROR: --role required" >&2; usage; exit 2; }
|
||||
[ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; }
|
||||
@@ -133,6 +133,14 @@ elif [ "$AGENT" = "opencode" ]; then
|
||||
echo "ERROR: opencode is not functional or configured." >&2
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$AGENT" = "codex" ]; then
|
||||
codex_home="${CODEX_HOME:-$HOME/.codex}"
|
||||
if [ -f "$codex_home/auth.json" ] || [ -n "${OPENAI_API_KEY:-}" ]; then
|
||||
true
|
||||
elif ! codex --version >/dev/null 2>&1; then
|
||||
echo "ERROR: codex is not functional or configured." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A)
|
||||
@@ -190,6 +198,7 @@ if [ -z "$CMD_FULL" ]; then
|
||||
hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;;
|
||||
grok) CMD_FULL="${RESOLVED_BIN} --permission-mode bypassPermissions" ;;
|
||||
opencode) CMD_FULL="${RESOLVED_BIN} --auto --agent build" ;;
|
||||
codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -210,7 +219,7 @@ spawn() {
|
||||
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
|
||||
fi
|
||||
;;
|
||||
agy|hermes|grok)
|
||||
agy|hermes|grok|codex)
|
||||
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
|
||||
;;
|
||||
opencode)
|
||||
@@ -219,7 +228,7 @@ spawn() {
|
||||
fi
|
||||
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
|
||||
;;
|
||||
*) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode, got: $AGENT" >&2; exit 2 ;;
|
||||
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT" >&2; exit 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -328,6 +337,7 @@ if [ -n "$SUBMIT_JOB_PROMPT" ]; then
|
||||
agy) delegate_agent="antigravity-cli" ;;
|
||||
grok) delegate_agent="grok-build" ;;
|
||||
opencode) delegate_agent="opencode-cli" ;;
|
||||
codex) delegate_agent="codex-cli" ;;
|
||||
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
@@ -442,6 +452,13 @@ elif agent == 'opencode':
|
||||
entry['session_id_source'] = 'pending-discovery'
|
||||
entry['session_id_verified'] = False
|
||||
entry['last_visible_status'] = "unverified"
|
||||
elif agent == 'codex':
|
||||
cp = os.environ.get('CHILD_PID', '0')
|
||||
entry['child_pid'] = int(cp) if cp.isdigit() else 0
|
||||
entry['codex_session_id_own'] = None
|
||||
entry['session_id_source'] = 'pending-discovery'
|
||||
entry['session_id_verified'] = False
|
||||
entry['last_visible_status'] = "unverified"
|
||||
|
||||
last_vis_override = os.environ.get('LAST_VISIBLE_STATUS_OVERRIDE', '')
|
||||
if last_vis_override:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: multi-agent-mux-delegate-job
|
||||
description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer."
|
||||
version: 4.1.3
|
||||
version: 4.2.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: multi-agent-mux-loop
|
||||
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
|
||||
version: 4.1.3
|
||||
version: 4.2.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
environments: [terminal, herdr]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [agent, herdr, multi-agent, loop, planning, review, orchestrator, grok]
|
||||
tags: [agent, herdr, multi-agent, loop, planning, review, orchestrator, grok, opencode, codex]
|
||||
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-delegate-job]
|
||||
prereq_skills: [multi-agent-mux-create]
|
||||
---
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: multi-agent-mux-monitor
|
||||
description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted."
|
||||
version: 4.1.3
|
||||
version: 4.2.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
environments: [terminal, herdr]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [agent, herdr, claude, antigravity, agy, grok, monitor, observation, reconciliation]
|
||||
tags: [agent, herdr, claude, antigravity, agy, grok, opencode, codex, monitor, observation, reconciliation]
|
||||
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-status]
|
||||
prereq_skills: [multi-agent-mux-create]
|
||||
---
|
||||
|
||||
@@ -520,7 +520,7 @@ if herdr_confirmed:
|
||||
agent = None
|
||||
role = 'creator'
|
||||
for r_name in ('creator', 'planner', 'reviewer'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
if name.endswith(f"-{r_name}-{a_name}"):
|
||||
role = r_name
|
||||
agent = a_name
|
||||
@@ -539,7 +539,7 @@ if herdr_confirmed:
|
||||
if tok.startswith('MAM_MANAGED='):
|
||||
managed_path = tok.split('=', 1)[1]
|
||||
if os.path.realpath(managed_path) == os.path.realpath(workspace_root):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||
for a_name in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
if a_name in pm_check.get('cmd', '') or a_name in pm_check.get('cmd_full', ''):
|
||||
agent = a_name
|
||||
break
|
||||
@@ -547,6 +547,10 @@ if herdr_confirmed:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not agent:
|
||||
if pm_check and pm_check.get('cmd') in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
agent = pm_check['cmd']
|
||||
|
||||
if not agent:
|
||||
continue
|
||||
|
||||
@@ -603,6 +607,9 @@ if herdr_confirmed:
|
||||
elif agent == 'opencode':
|
||||
entry['child_pid'] = 0
|
||||
entry['opencode_session_id_own'] = None
|
||||
elif agent == 'codex':
|
||||
entry['child_pid'] = 0
|
||||
entry['codex_session_id_own'] = None
|
||||
d.setdefault('herdr_sessions', []).append(entry)
|
||||
yaml_session_names.add(name)
|
||||
drifts.append({'class': 'B', 'name': name,
|
||||
@@ -613,7 +620,7 @@ def row_agent(s):
|
||||
return agent_of_row(s)
|
||||
|
||||
OWN_KEY_BY_AGENT = {
|
||||
a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode')
|
||||
a: _get_own_key(a) for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex')
|
||||
}
|
||||
|
||||
# === drift C0: 지정된 ID 는 발견이 아니라 '확인'만 필요하다 ===
|
||||
@@ -896,6 +903,88 @@ for s in d.get('herdr_sessions', []):
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'opencode', cwd, uuid, degraded=True)
|
||||
|
||||
# === 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):
|
||||
try:
|
||||
conn = sqlite3.connect(state_db)
|
||||
cols = [c[1] for c in conn.execute("PRAGMA table_info(threads)").fetchall()]
|
||||
order_col = "updated_at" if "updated_at" in cols else ("created_at" if "created_at" in cols else None)
|
||||
if order_col:
|
||||
rows = conn.execute(
|
||||
f"SELECT id, {order_col} FROM threads WHERE cwd=? ORDER BY {order_col} DESC LIMIT 20",
|
||||
(cwd,)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = [(r[0], int(time.time())) for r in conn.execute("SELECT id FROM threads WHERE cwd=? LIMIT 20", (cwd,)).fetchall()]
|
||||
conn.close()
|
||||
except Exception:
|
||||
rows = []
|
||||
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)
|
||||
|
||||
result = {
|
||||
'timestamp': now_iso,
|
||||
'yaml_path': yaml_path,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: multi-agent-mux-orc-onboard
|
||||
description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture."
|
||||
version: 4.1.3
|
||||
version: 4.2.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
environments: [terminal, herdr]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [agent, herdr, claude, antigravity, agy, hermes, grok, opencode, orchestrator, onboard, isolation]
|
||||
tags: [agent, herdr, claude, antigravity, agy, hermes, grok, opencode, codex, orchestrator, onboard, isolation]
|
||||
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-monitor]
|
||||
prereq_skills: [multi-agent-mux-create]
|
||||
---
|
||||
@@ -32,13 +32,14 @@ Registers an orchestrator session UUID into the `orchestrator_uuids` list of `.m
|
||||
|
||||
## Auto-Detection Hierarchy (Rev.2)
|
||||
|
||||
When `--uuid` is omitted, `orc_onboard.sh` inspects the process ancestry tree of the nearest agent ancestor (`claude`, `agy`, `hermes`, `grok`, `opencode`) in the following order:
|
||||
When `--uuid` is omitted, `orc_onboard.sh` inspects the process ancestry tree of the nearest agent ancestor (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`) in the following order:
|
||||
|
||||
1. **CLI `argv`**:
|
||||
- `claude -r <uuid>` / `claude --session-id <uuid>`
|
||||
- `agy --conversation <uuid>`
|
||||
- `grok --session-id <uuid>` / `grok --resume <uuid>`
|
||||
- `opencode --session <uuid>`
|
||||
- `codex resume <uuid>`
|
||||
2. **Family-Matched Environment Variables**:
|
||||
- `claude` → `CLAUDE_CODE_SESSION_ID`
|
||||
- `agy` → `ANTIGRAVITY_CONVERSATION_ID`
|
||||
|
||||
@@ -103,7 +103,7 @@ detect_nearest_agent() {
|
||||
local base
|
||||
base="$(basename "$tok")"
|
||||
case "$base" in
|
||||
claude|agy|hermes|grok|opencode)
|
||||
claude|agy|hermes|grok|opencode|codex)
|
||||
match="$base"
|
||||
break
|
||||
;;
|
||||
@@ -130,6 +130,9 @@ detect_nearest_agent() {
|
||||
opencode)
|
||||
detected_id=$(echo "$cmd_line" | grep -oE '(--session|-s)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--session|-s)[[:space:]=]+//' || true)
|
||||
;;
|
||||
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)
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "$detected_id" ] && is_valid_id "$detected_id"; then
|
||||
@@ -145,6 +148,7 @@ detect_nearest_agent() {
|
||||
hermes) env_var="${HERMES_SESSION_ID:-}" ;;
|
||||
grok) env_var="${GROK_SESSION_ID:-}" ;;
|
||||
opencode) env_var="${OPENCODE_SESSION_ID:-}" ;;
|
||||
codex) env_var="" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$env_var" ] && is_valid_id "$env_var"; then
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: multi-agent-mux-resume
|
||||
description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk."
|
||||
version: 4.1.3
|
||||
version: 4.2.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
@@ -49,16 +49,16 @@ ideal resume path:
|
||||
|
||||
`agent-sessions.yaml` and on-disk discovery are used to resolve the UUID in this order:
|
||||
|
||||
1. **`herdr_sessions[]` row's per-row own id** (`claude_session_id_own` / `agy_conversation_id_own` / `hermes_conversation_id_own` / `grok_session_id_own` / `opencode_session_id_own`) — explicitly saved by `multi-agent-mux-stop` right before teardown (tier-1, race-free).
|
||||
1. **`herdr_sessions[]` row's per-row own id** (`claude_session_id_own` / `agy_conversation_id_own` / `hermes_conversation_id_own` / `grok_session_id_own` / `opencode_session_id_own` / `codex_session_id_own`) — explicitly saved by `multi-agent-mux-stop` right before teardown (tier-1, race-free).
|
||||
2. **Workspace-scoped on-disk scan** (adapter `discover()`)
|
||||
|
||||
If both are empty → for Class A agents (`agy`, `hermes`, `opencode`), `resume_session.sh` automatically falls back to `FRESH_SPAWN=1` (`spawn-spec`). For Class B agents (`claude`, `grok`), fall back to `multi-agent-mux-create`.
|
||||
If both are empty → for Class A agents (`agy`, `hermes`, `opencode`, `codex`), `resume_session.sh` automatically falls back to `FRESH_SPAWN=1` (`spawn-spec`). For Class B agents (`claude`, `grok`), fall back to `multi-agent-mux-create`.
|
||||
|
||||
## Workflow
|
||||
|
||||
```bash
|
||||
WORKSPACE=/path/to/project
|
||||
AGENT=claude # claude | agy | hermes | grok | opencode — pass it explicitly
|
||||
AGENT=claude # claude | agy | hermes | grok | opencode | codex — pass it explicitly
|
||||
SESSION_NAME=<workspace>-creator-<agent> # same convention as multi-agent-mux-create
|
||||
|
||||
# Resolve the isolated herdr server name & load common utils
|
||||
@@ -70,7 +70,7 @@ UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh
|
||||
|
||||
if [ -z "$UUID" ]; then
|
||||
case "$AGENT" in
|
||||
agy|hermes|opencode)
|
||||
agy|hermes|opencode|codex)
|
||||
: # resume_session.sh handles fresh-spawn fallback automatically (FRESH_SPAWN=1)
|
||||
;;
|
||||
*)
|
||||
@@ -97,6 +97,7 @@ case "$AGENT" in
|
||||
hermes) CMD_FULL="hermes --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
|
||||
grok) CMD_FULL="grok --resume $UUID --permission-mode bypassPermissions" ;;
|
||||
opencode) CMD_FULL="opencode --session $UUID --auto --agent build" ;;
|
||||
codex) CMD_FULL="codex resume --dangerously-bypass-approvals-and-sandbox $UUID" ;;
|
||||
esac
|
||||
|
||||
# 4. Spawn new herdr session + run agent with the saved id
|
||||
@@ -107,7 +108,7 @@ case "$AGENT" in
|
||||
# auto-handle trust / bypass dialogs
|
||||
handle_startup_dialogs "$SESSION_NAME" 20
|
||||
;;
|
||||
agy|hermes|grok|opencode)
|
||||
agy|hermes|grok|opencode|codex)
|
||||
eval "herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# resolve_session_id.sh — multi-agent-mux-resume 의 부속 스크립트
|
||||
# Usage:
|
||||
# bash resolve_session_id.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode>
|
||||
# bash resolve_session_id.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex>
|
||||
# 출력: stdout 으로 UUID 한 줄 (없으면 빈 줄 + exit 0)
|
||||
#
|
||||
# P0-C: 전역 agent_identities 를 즉시 반환하지 않는다. lib.sh::find_workspace_uuid
|
||||
@@ -13,7 +13,7 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> [--session <name>]
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> [--session <name>]
|
||||
Outputs the resolved UUID on stdout (empty if not found).
|
||||
--session prefers that registry row's recorded id; falls back to workspace-wide discovery if it does not verify.
|
||||
EOF
|
||||
@@ -36,8 +36,8 @@ done
|
||||
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; }
|
||||
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; }
|
||||
case "$AGENT" in
|
||||
claude|agy|hermes|grok|opencode) ;;
|
||||
*) echo "ERROR: --agent must be claude, agy, hermes, grok, or opencode" >&2; exit 2 ;;
|
||||
claude|agy|hermes|grok|opencode|codex) ;;
|
||||
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
find_workspace_uuid "$WORKSPACE" "$AGENT" "$SESSION_NAME"
|
||||
@@ -9,7 +9,7 @@ source "$LIB_SH"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode> --session <name> [options]
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> --session <name> [options]
|
||||
|
||||
Options:
|
||||
--herdr-session NAME specify isolated herdr session name (alias: --herdr-server)
|
||||
@@ -42,7 +42,7 @@ done
|
||||
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; exit 2; }
|
||||
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; exit 2; }
|
||||
case "$AGENT" in
|
||||
claude|agy|hermes|grok|opencode) ;;
|
||||
claude|agy|hermes|grok|opencode|codex) ;;
|
||||
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
|
||||
esac
|
||||
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; }
|
||||
@@ -62,7 +62,7 @@ UUID=$(bash "$(dirname "${BASH_SOURCE[0]}")/resolve_session_id.sh" \
|
||||
FRESH_SPAWN=0
|
||||
if [ -z "$UUID" ]; then
|
||||
case "$AGENT" in
|
||||
agy|hermes|opencode) FRESH_SPAWN=1 ;;
|
||||
agy|hermes|opencode|codex) FRESH_SPAWN=1 ;;
|
||||
*)
|
||||
echo "ERROR: No saved session for $WORKSPACE ($AGENT). Use multi-agent-mux-create first." >&2
|
||||
exit 1
|
||||
@@ -119,6 +119,7 @@ if [ "$FRESH_SPAWN" = "1" ]; then
|
||||
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
||||
hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;;
|
||||
opencode) CMD_FULL="${RESOLVED_BIN} --auto --agent build" ;;
|
||||
codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;
|
||||
esac
|
||||
fi
|
||||
else
|
||||
@@ -130,6 +131,7 @@ else
|
||||
hermes) CMD_FULL="${RESOLVED_BIN} --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
|
||||
grok) CMD_FULL="${RESOLVED_BIN} --resume $UUID --permission-mode bypassPermissions" ;;
|
||||
opencode) CMD_FULL="${RESOLVED_BIN} --session $UUID --auto --agent build" ;;
|
||||
codex) CMD_FULL="${RESOLVED_BIN} resume --dangerously-bypass-approvals-and-sandbox $UUID" ;;
|
||||
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
# resume UUID 를 per-row own id (claude_session_id_own / agy_conversation_id_own)
|
||||
# 에 박는다 — agent_identities 전역은 더 이상 primary 아님 (cache 로 강등, P0-C/단계 e).
|
||||
#
|
||||
# Usage: bash update_yaml_resumed.sh --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode]
|
||||
# Usage: bash update_yaml_resumed.sh --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode|codex]
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>] [--cmd-full <cmd>]
|
||||
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|grok|opencode|codex] [--herdr-session <name>] [--cmd-full <cmd>]
|
||||
|
||||
--uuid may be empty for an Issue-3 Class A fresh-spawn resume (agy/hermes/
|
||||
opencode 0-turn stopped session, no conversation id ever assigned) - pass
|
||||
opencode/codex 0-turn stopped session, no conversation id ever assigned) - pass
|
||||
--cmd-full so the display command can be recorded without a uuid to embed.
|
||||
EOF
|
||||
}
|
||||
@@ -235,6 +235,16 @@ elif agent == 'opencode':
|
||||
cp = os.environ.get('CHILD_PID', '0')
|
||||
if cp.isdigit() and int(cp) > 0:
|
||||
target['child_pid'] = int(cp)
|
||||
elif agent == 'codex':
|
||||
target['pane']['cmd'] = 'codex'
|
||||
if uuid:
|
||||
target['pane']['cmd_full'] = f'codex resume --dangerously-bypass-approvals-and-sandbox {uuid}'
|
||||
target['codex_session_id_own'] = uuid
|
||||
else:
|
||||
target['pane']['cmd_full'] = os.environ.get('CMD_FULL_OPT', '') or 'codex --dangerously-bypass-approvals-and-sandbox'
|
||||
cp = os.environ.get('CHILD_PID', '0')
|
||||
if cp.isdigit() and int(cp) > 0:
|
||||
target['child_pid'] = int(cp)
|
||||
|
||||
snap = d.setdefault('snapshot', {})
|
||||
snap['taken_at'] = now
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: multi-agent-mux-status
|
||||
description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop."
|
||||
version: 4.1.3
|
||||
version: 4.2.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
environments: [terminal, herdr]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [agent, herdr, claude, antigravity, agy, grok, status, read-only, snapshot]
|
||||
tags: [agent, herdr, claude, antigravity, agy, grok, opencode, codex, status, read-only, snapshot]
|
||||
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-monitor]
|
||||
prereq_skills: [multi-agent-mux-create, multi-agent-mux-monitor]
|
||||
---
|
||||
|
||||
@@ -53,12 +53,12 @@ def resume_on_disk(s):
|
||||
name = s.get('name', '')
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
agent = None
|
||||
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
if any(name.endswith(f'-{r}-{a}') for r in ('creator', 'planner', 'reviewer')) or name.endswith(f'-{a}'):
|
||||
agent = a
|
||||
break
|
||||
if not agent:
|
||||
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
if f"-{a}" in name or f"_{a}" in name:
|
||||
agent = a
|
||||
break
|
||||
@@ -125,6 +125,29 @@ def resume_on_disk(s):
|
||||
except Exception:
|
||||
return 'MISSING'
|
||||
return 'no'
|
||||
if agent == 'codex':
|
||||
u = s.get('codex_session_id_own')
|
||||
if u:
|
||||
codex_home = os.environ.get('CODEX_HOME', f"{home}/.codex")
|
||||
db_matches = glob.glob(f"{codex_home}/state_*.sqlite")
|
||||
if db_matches:
|
||||
db = max(db_matches, key=os.path.getmtime)
|
||||
try:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(db)
|
||||
r = conn.execute("SELECT rollout_path FROM threads WHERE id=?", (u,)).fetchone()
|
||||
conn.close()
|
||||
if r and r[0] and os.path.exists(r[0]):
|
||||
return 'yes'
|
||||
if r:
|
||||
return 'yes'
|
||||
except Exception:
|
||||
pass
|
||||
m = glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*-{u}.jsonl")
|
||||
if m and os.path.exists(m[0]):
|
||||
return 'yes'
|
||||
return 'MISSING'
|
||||
return 'no'
|
||||
return '?'
|
||||
|
||||
|
||||
@@ -231,12 +254,12 @@ def resume_on_disk(s):
|
||||
name = s.get('name', '')
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
agent = None
|
||||
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
if any(name.endswith(f'-{r}-{a}') for r in ('creator', 'planner', 'reviewer')) or name.endswith(f'-{a}'):
|
||||
agent = a
|
||||
break
|
||||
if not agent:
|
||||
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||
for a in ('claude', 'agy', 'hermes', 'grok', 'opencode', 'codex'):
|
||||
if f"-{a}" in name or f"_{a}" in name:
|
||||
agent = a
|
||||
break
|
||||
@@ -303,6 +326,29 @@ def resume_on_disk(s):
|
||||
except Exception:
|
||||
return 'MISSING'
|
||||
return 'no'
|
||||
if agent == 'codex':
|
||||
u = s.get('codex_session_id_own')
|
||||
if u:
|
||||
codex_home = os.environ.get('CODEX_HOME', f"{home}/.codex")
|
||||
db_matches = glob.glob(f"{codex_home}/state_*.sqlite")
|
||||
if db_matches:
|
||||
db = max(db_matches, key=os.path.getmtime)
|
||||
try:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(db)
|
||||
r = conn.execute("SELECT rollout_path FROM threads WHERE id=?", (u,)).fetchone()
|
||||
conn.close()
|
||||
if r and r[0] and os.path.exists(r[0]):
|
||||
return 'yes'
|
||||
if r:
|
||||
return 'yes'
|
||||
except Exception:
|
||||
pass
|
||||
m = glob.glob(f"{codex_home}/sessions/*/*/*/rollout-*-{u}.jsonl")
|
||||
if m and os.path.exists(m[0]):
|
||||
return 'yes'
|
||||
return 'MISSING'
|
||||
return 'no'
|
||||
return '?'
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: multi-agent-mux-stop
|
||||
description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start."
|
||||
version: 4.1.3
|
||||
version: 4.2.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
@@ -37,7 +37,7 @@ The stop command is always **graceful by default**:
|
||||
|
||||
```bash
|
||||
SESSION_NAME=<workspace>-creator-<agent> # convention
|
||||
AGENT=claude # claude | agy | hermes | grok | opencode — always pass it
|
||||
AGENT=claude # claude | agy | hermes | grok | opencode | codex — always pass it
|
||||
AGENT_SESSIONS_YAML=.mam/agent-sessions.yaml
|
||||
|
||||
# 1) Session is registered?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# stop_session.sh — multi-agent-mux-stop 의 부속 스크립트
|
||||
# Usage:
|
||||
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|grok|opencode] [--herdr-session <name>] \
|
||||
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|grok|opencode|codex] [--herdr-session <name>] \
|
||||
# [--reason <reason>] [--purge-conversation] [--yes]
|
||||
#
|
||||
# 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고
|
||||
@@ -12,7 +12,7 @@
|
||||
#
|
||||
# 옵션:
|
||||
# --session <name> — 대상 세션 (필수)
|
||||
# --agent <type> — claude | agy | hermes | grok | opencode
|
||||
# --agent <type> — claude | agy | hermes | grok | opencode | codex
|
||||
# (권장: 항상 명시. 미지정 시 레지스트리 기록으로
|
||||
# 해석 — agent 필드 → 세션명 접미사 → pane.cmd;
|
||||
# 셋 다 실패하면 exit 2)
|
||||
@@ -91,8 +91,8 @@ while [ $# -gt 0 ]; do
|
||||
done
|
||||
if [ -n "$AGENT" ]; then
|
||||
case "$AGENT" in
|
||||
claude|agy|hermes|grok|opencode) ;;
|
||||
*) echo "ERROR: invalid agent type '$AGENT'. Allowed types are: claude, agy, hermes, grok, opencode." >&2; exit 2 ;;
|
||||
claude|agy|hermes|grok|opencode|codex) ;;
|
||||
*) echo "ERROR: invalid agent type '$AGENT'. Allowed types are: claude, agy, hermes, grok, opencode, codex." >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; usage; exit 2; }
|
||||
@@ -289,6 +289,8 @@ if captured and not purge:
|
||||
target['grok_session_id_own'] = captured
|
||||
elif agent == 'opencode':
|
||||
target['opencode_session_id_own'] = captured
|
||||
elif agent == 'codex':
|
||||
target['codex_session_id_own'] = captured
|
||||
target['resumable'] = True
|
||||
|
||||
if purge and purge_uuid:
|
||||
|
||||
Reference in new issue
Block a user