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
@@ -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