From 88ed9ad2f0ac4ba5182c9ed234d8baee7cdf7938 Mon Sep 17 00:00:00 2001 From: notque Date: Sat, 11 Apr 2026 10:14:51 -0700 Subject: [PATCH] feat(install): mirror toolkit agents to ~/.codex/agents alongside skills Codex CLI sessions already get the toolkit skill library via ~/.codex/skills, but agents were not being mirrored. Codex has no native subagent_type dispatch, yet it can still Read agent files as domain reference material (Go, Python, Kubernetes, TypeScript, etc.) and their nested reference directories. This closes the gap so Codex and Claude Code see the same expertise surface. Changes: - install.sh: create ~/.codex/agents, sync agents/ and private-agents/ on install, clean them on uninstall, report the count in summary. - hooks/sync-to-user-claude.py: add a parallel codex agents sync block with the same additive-only policy as the skills mirror. - scripts/install-doctor.py: add check_codex_agents plus inventory counter so the health check covers the new mirror. - docs and README: update wording so the installer description matches reality on both runtimes. --- README.md | 4 +-- docs/QUICKSTART.md | 4 +-- docs/start-here.md | 6 ++-- hooks/sync-to-user-claude.py | 39 +++++++++++++++++++-- install.sh | 66 ++++++++++++++++++++++++++++++++++++ scripts/install-doctor.py | 61 ++++++++++++++++++++++++++++++++- 6 files changed, 170 insertions(+), 10 deletions(-) mode change 100644 => 100755 hooks/sync-to-user-claude.py diff --git a/README.md b/README.md index 27eb6e5e..ec94c1ad 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ A game built entirely by Claude Code using these agents, skills, and pipelines. ## Installation -Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and working (`claude --version` should print a version number). Codex CLI is also supported: the installer mirrors toolkit skills into `~/.codex/skills` so Codex can use the same skill library (`codex --version` should print a version number if you want Codex support too). +Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and working (`claude --version` should print a version number). Codex CLI is also supported: the installer mirrors toolkit skills into `~/.codex/skills` and toolkit agents into `~/.codex/agents` so Codex can use the same skill and agent library (`codex --version` should print a version number if you want Codex support too). ```bash git clone https://github.com/notque/claude-code-toolkit.git ~/claude-code-toolkit @@ -38,7 +38,7 @@ cd ~/claude-code-toolkit ./install.sh --symlink ``` -The installer links agents, skills, hooks, commands, and scripts into `~/.claude/`, where Claude Code loads extensions from. It also mirrors skills into `~/.codex/skills` for Codex. Use `--symlink` to get updates via `git pull`, or run without it for a stable copy. +The installer links agents, skills, hooks, commands, and scripts into `~/.claude/`, where Claude Code loads extensions from. It also mirrors skills into `~/.codex/skills` and agents into `~/.codex/agents` for Codex. Use `--symlink` to get updates via `git pull`, or run without it for a stable copy. Verify the install with: diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 0156aa2d..bb85bf03 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -13,7 +13,7 @@ cd ~/claude-code-toolkit ./install.sh ``` -Claude Code is the primary runtime. If you also use Codex CLI, the same install mirrors toolkit skills into `~/.codex/skills`. +Claude Code is the primary runtime. If you also use Codex CLI, the same install mirrors toolkit skills into `~/.codex/skills` and toolkit agents into `~/.codex/agents`. Command entry points: - Claude Code: `/do` @@ -35,7 +35,7 @@ python3 ~/.claude/scripts/install-doctor.py check python3 ~/.claude/scripts/install-doctor.py inventory ``` -If Codex should pick up newly added skills after a `git pull`, rerun `./install.sh --symlink`. +If Codex should pick up newly added skills or agents after a `git pull`, rerun `./install.sh --symlink`. --- diff --git a/docs/start-here.md b/docs/start-here.md index 26a8b7c3..2761f19f 100644 --- a/docs/start-here.md +++ b/docs/start-here.md @@ -12,7 +12,7 @@ claude --version If that prints a version number, you're good. If not, install Claude Code first and come back. -Optional: if you also use Codex CLI, run `codex --version`. The toolkit mirrors its skills into `~/.codex/skills`, but Claude Code is still the full runtime for hooks, agents, commands, and scripts. +Optional: if you also use Codex CLI, run `codex --version`. The toolkit mirrors its skills into `~/.codex/skills` and its agents into `~/.codex/agents`, so Codex sessions can Read the same domain expertise Claude Code dispatches. Claude Code is still the full runtime for hooks, commands, and scripts. Command entry points: - Claude Code: `/do` @@ -36,7 +36,7 @@ cd claude-code-toolkit The installer asks one question -- symlink or copy -- then sets everything up. Pick symlink if you want updates via `git pull`, copy if you want a stable snapshot. Either works fine. -What just happened: the installer linked agents, skills, hooks, commands, and scripts into `~/.claude/`, which is where Claude Code looks for extensions. It also mirrored skills into `~/.codex/skills` for Codex and configured hooks in your settings so they activate automatically. +What just happened: the installer linked agents, skills, hooks, commands, and scripts into `~/.claude/`, which is where Claude Code looks for extensions. It also mirrored skills into `~/.codex/skills` and agents into `~/.codex/agents` for Codex, and configured hooks in your settings so they activate automatically. ## Verify It @@ -47,7 +47,7 @@ python3 ~/.claude/scripts/install-doctor.py check python3 ~/.claude/scripts/install-doctor.py inventory ``` -`check` verifies the install layout, settings, hook paths, learning DB access, and Codex skill mirror. `inventory` shows what Claude and Codex can currently see. If you pull new toolkit changes later and want Codex to pick up new skills, rerun `./install.sh`. +`check` verifies the install layout, settings, hook paths, learning DB access, and both Codex skill and agent mirrors. `inventory` shows what Claude and Codex can currently see. If you pull new toolkit changes later and want Codex to pick up new skills or agents, rerun `./install.sh`. ## Your First Commands diff --git a/hooks/sync-to-user-claude.py b/hooks/sync-to-user-claude.py old mode 100644 new mode 100755 index 5c85093e..2ad7c6af --- a/hooks/sync-to-user-claude.py +++ b/hooks/sync-to-user-claude.py @@ -502,8 +502,10 @@ def main(): if voice_count > 0: synced.append(f"private-voices({voice_count})") - # Sync skills to ~/.codex/skills/ for OpenAI Codex CLI. - # Codex only supports skills (no agents, hooks, or scripts). + # Sync skills and agents to ~/.codex/ for OpenAI Codex CLI. + # Codex natively supports skills; agents are mirrored as reference + # material so Codex sessions can Read the same domain expertise that + # Claude Code sessions dispatch via subagent_type. codex_skills_dst = Path.home() / ".codex" / "skills" codex_sources = [("skills", repo_root / "skills")] codex_count = 0 @@ -555,6 +557,39 @@ def main(): total = sum(1 for _ in codex_skills_dst.rglob("*") if _.is_file()) synced.append(f".codex/skills({total} current)") + # Sync agents to ~/.codex/agents/ — parallel mirror to skills. + # Agents carry domain expertise (Go, Python, K8s, TypeScript, etc.) + # and their reference subdirectories. Codex can Read them even though + # it has no native subagent_type dispatch. + codex_agents_dst = Path.home() / ".codex" / "agents" + codex_agent_sources = [("agents", repo_root / "agents")] + private_agents_dir = repo_root / "private-agents" + if private_agents_dir.is_dir(): + codex_agent_sources.append(("private-agents", private_agents_dir)) + codex_agent_count = 0 + for label, src in codex_agent_sources: + if not src.is_dir(): + continue + try: + codex_agents_dst.mkdir(parents=True, exist_ok=True) + for item in src.rglob("*"): + if item.is_file(): + rel = item.relative_to(src) + target = codex_agents_dst / rel + target.parent.mkdir(parents=True, exist_ok=True) + if target.exists() and filecmp.cmp(item, target, shallow=False): + continue + shutil.copy2(item, target) + codex_agent_count += 1 + except Exception as e: + errors.append(f"codex-{label}: {e}") + # No stale cleanup for Codex agents — additive only, same rationale as skills. + if codex_agent_count > 0: + synced.append(f".codex/agents({codex_agent_count} updated)") + elif codex_agents_dst.is_dir(): + total = sum(1 for _ in codex_agents_dst.rglob("*") if _.is_file()) + synced.append(f".codex/agents({total} current)") + # Output for hook feedback if synced: print(f"[sync] Updated ~/.claude: {', '.join(synced)}") diff --git a/install.sh b/install.sh index 62facfb4..7ef0d161 100755 --- a/install.sh +++ b/install.sh @@ -33,6 +33,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CLAUDE_DIR="${HOME}/.claude" CODEX_DIR="${HOME}/.codex" CODEX_SKILLS_DIR="${CODEX_DIR}/skills" +CODEX_AGENTS_DIR="${CODEX_DIR}/agents" echo -e "${BLUE}╔════════════════════════════════════════════════════════════════╗${NC}" echo -e "${BLUE}║ Claude Code Toolkit - Installation Script ║${NC}" @@ -282,6 +283,42 @@ os.rename(tmp, dst) echo " No ~/.codex/skills mirror found. Nothing to clean." fi + echo "" + echo -e "${YELLOW}Cleaning Codex agents mirror...${NC}" + if [ -d "$CODEX_AGENTS_DIR" ]; then + for item in "${SCRIPT_DIR}/agents/"*; do + [ -e "$item" ] || continue + target="${CODEX_AGENTS_DIR}/$(basename "$item")" + if [ -L "$target" ] || [ -e "$target" ]; then + if [ "$DRY_RUN" = true ]; then + echo -e "${BLUE} Would remove Codex entry: ${target}${NC}" + else + rm -rf "$target" + echo -e "${GREEN} ✓ Removed Codex entry: ${target}${NC}" + fi + REMOVED+=("Codex agent $(basename "$item")") + fi + done + + if [ -d "${SCRIPT_DIR}/private-agents" ]; then + for item in "${SCRIPT_DIR}/private-agents/"*; do + [ -e "$item" ] || continue + target="${CODEX_AGENTS_DIR}/$(basename "$item")" + if [ -L "$target" ] || [ -e "$target" ]; then + if [ "$DRY_RUN" = true ]; then + echo -e "${BLUE} Would remove Codex entry: ${target}${NC}" + else + rm -rf "$target" + echo -e "${GREEN} ✓ Removed Codex entry: ${target}${NC}" + fi + REMOVED+=("Codex agent $(basename "$item")") + fi + done + fi + else + echo " No ~/.codex/agents mirror found. Nothing to clean." + fi + # Phase 4: Remove install manifest echo "" echo -e "${YELLOW}Cleaning up manifest...${NC}" @@ -408,6 +445,15 @@ else fi echo -e "${GREEN}✓ ${CODEX_SKILLS_DIR} ready${NC}" +echo "" +echo -e "${YELLOW}Setting up ~/.codex agents directory...${NC}" +if [ "$DRY_RUN" = true ]; then + echo -e "${BLUE} Would create: ${CODEX_AGENTS_DIR}${NC}" +else + mkdir -p "${CODEX_AGENTS_DIR}" +fi +echo -e "${GREEN}✓ ${CODEX_AGENTS_DIR} ready${NC}" + # Install components echo "" echo -e "${YELLOW}Installing components (mode: ${MODE})...${NC}" @@ -572,6 +618,25 @@ if [ -d "${SCRIPT_DIR}/private-skills" ]; then done fi +echo "" +echo -e "${YELLOW}Syncing Codex agents mirror...${NC}" +CODEX_AGENT_COUNT=0 +for item in "${SCRIPT_DIR}/agents/"*; do + [ -e "$item" ] || continue + target="${CODEX_AGENTS_DIR}/$(basename "$item")" + sync_codex_entry "$item" "$target" + CODEX_AGENT_COUNT=$((CODEX_AGENT_COUNT + 1)) +done + +if [ -d "${SCRIPT_DIR}/private-agents" ]; then + for item in "${SCRIPT_DIR}/private-agents/"*; do + [ -e "$item" ] || continue + target="${CODEX_AGENTS_DIR}/$(basename "$item")" + sync_codex_entry "$item" "$target" + CODEX_AGENT_COUNT=$((CODEX_AGENT_COUNT + 1)) + done +fi + # Set up local overlay echo "" echo -e "${YELLOW}Setting up local overlay...${NC}" @@ -751,6 +816,7 @@ echo "Installed components:" echo " • Agents: ${AGENT_COUNT} specialized domain experts" echo " • Skills: ${SKILL_COUNT} workflow methodologies (${INVOCABLE_COUNT} user-invocable)" echo " • Codex skills: ${CODEX_ENTRY_COUNT} mirrored entries in ~/.codex/skills" +echo " • Codex agents: ${CODEX_AGENT_COUNT} mirrored entries in ~/.codex/agents" echo " • Hooks: ${HOOK_COUNT} automation hooks" echo " • Commands: ${COMMAND_COUNT} slash commands" echo " • Scripts: ${SCRIPT_COUNT} utility scripts" diff --git a/scripts/install-doctor.py b/scripts/install-doctor.py index 6e0b8e28..d21db450 100755 --- a/scripts/install-doctor.py +++ b/scripts/install-doctor.py @@ -203,6 +203,55 @@ def check_codex_skills() -> dict: } +def check_codex_agents() -> dict: + """Check that toolkit agents are mirrored into ~/.codex/agents.""" + codex_agents_dir = CODEX_DIR / "agents" + repo_root = get_toolkit_repo_root() + + if repo_root is None: + return { + "name": "codex_agents", + "label": "~/.codex/agents mirror", + "passed": codex_agents_dir.is_dir(), + "detail": str(codex_agents_dir) + if codex_agents_dir.is_dir() + else "Codex agents mirror not found. Run install.sh from the toolkit repo.", + } + + expected_entries = [item.name for item in sorted((repo_root / "agents").iterdir())] + + private_agents_dir = repo_root / "private-agents" + if private_agents_dir.is_dir(): + for agent_item in sorted(private_agents_dir.iterdir()): + expected_entries.append(agent_item.name) + + expected_entries = list(dict.fromkeys(expected_entries)) + + if not codex_agents_dir.is_dir(): + return { + "name": "codex_agents", + "label": "~/.codex/agents mirror", + "passed": False, + "detail": "Directory not found. Run install.sh to mirror toolkit agents for Codex.", + } + + missing = [entry for entry in expected_entries if not (codex_agents_dir / entry).exists()] + if missing: + return { + "name": "codex_agents", + "label": "~/.codex/agents mirror", + "passed": False, + "detail": f"{len(expected_entries) - len(missing)}/{len(expected_entries)} entries present; missing: {', '.join(missing[:5])}", + } + + return { + "name": "codex_agents", + "label": "~/.codex/agents mirror", + "passed": True, + "detail": f"All {len(expected_entries)} toolkit entries mirrored", + } + + def check_hook_files() -> list[dict]: """Check that hooks referenced in settings.json actually exist.""" settings_file = CLAUDE_DIR / "settings.json" @@ -567,6 +616,12 @@ def inventory() -> dict: else: counts["codex_skills"] = 0 + codex_agents_dir = CODEX_DIR / "agents" + if codex_agents_dir.is_dir(): + counts["codex_agents"] = len([f for f in codex_agents_dir.glob("*.md") if f.name != "README.md"]) + else: + counts["codex_agents"] = 0 + # Count MCP servers from registry mcp_results = check_mcp_servers() mcp_total = sum(1 for r in mcp_results if r["name"].startswith("mcp_") and r["name"] != "mcp_registry") @@ -585,6 +640,7 @@ def run_all_checks() -> list[dict]: results.append(check_claude_dir()) results.extend(check_components_installed()) results.append(check_codex_skills()) + results.append(check_codex_agents()) results.append(check_settings_json()) results.extend(check_hook_files()) results.append(check_python_version()) @@ -638,7 +694,10 @@ def main(): print("\n Installed Components:\n") print(f" Agents: {counts.get('agents', 0)}") print(f" Skills: {counts.get('skills', 0)} ({counts.get('skills_invocable', 0)} user-invocable)") - print(f" Codex: {counts.get('codex_skills', 0)} skills available") + print( + f" Codex: {counts.get('codex_skills', 0)} skills available, " + f"{counts.get('codex_agents', 0)} agents available" + ) print(f" Hooks: {counts.get('hooks', 0)}") print(f" Commands: {counts.get('commands', 0)}") print(f" Scripts: {counts.get('scripts', 0)}")