diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b1a2bf7..197128c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -27,7 +27,8 @@ ], "commands": [ "./command/enhance-claude-md.md", - "./command/sync-claude-md.md" + "./command/sync-claude-md.md", + "./command/claude-to-agents.md" ], "agents": [ "./agent/claude-md-guardian.md" diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e9a1d..db5f372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added (wave 5 — CLAUDE.md → AGENTS.md conversion for Codex / Gemini) + +Cross-tool adoption: every project using ClaudeForge can now share its instructions with non-Claude tools (OpenAI Codex, Gemini Code Assist, anything honouring the AGENTS.md convention) without maintaining two files. + +- **`/claude-to-agents`** slash command (`command/claude-to-agents.md`) with three modes: + - `--symlink` (default on macOS / Linux) — `AGENTS.md` becomes a symlink to `CLAUDE.md`. One source of truth; edits propagate instantly. Windows falls back to `--copy` automatically with a stderr notice. + - `--copy` — byte-for-byte snapshot. Use when the user wants to fork the instructions or when their VCS/build pipeline doesn't follow symlinks. + - `--inline-chain` — recursively walks every `@path/.../CLAUDE.md` import and writes a single flat AGENTS.md with all sub-file content inlined. **Recommended for Codex / Gemini in modular projects** because those tools don't auto-resolve `@`-imports. +- **`hooks/claude-to-agents.py`** — standalone, idempotent script. Backs up an existing `AGENTS.md` to `AGENTS.md.backup.` before overwrite (unless `--force`). Strips Claude-only scaffolding (backlink lines, `@`-import lines) from `--inline-chain` output. Cycle-safe (each file read at most once). +- Plugin manifest registers the new command. Both installers list it in their banner and uninstall instructions. + ### Added (wave 4 — forked task-style audit skills) Three new task-style skills under `skill/`, each using Anthropic's `context: fork` + `agent: Explore` so they run in an isolated subagent context (no caller chat history, ≤500-token summary back to the main session): diff --git a/README.md b/README.md index 9013704..d0a9c27 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ ClaudeForge is a comprehensive toolkit that eliminates the tedious process of ma - **`/enhance-claude-md`** (`command/enhance-claude-md.md`) — multi-phase init/enhance workflow with `argument-hint`, `when_to_use`, `allowed-tools`, and `disallowedTools` (blocks `WebFetch` / `WebSearch`). Delegates deep codebase scans to the Explore subagent. - **`/sync-claude-md`** (`command/sync-claude-md.md`) — inventory → prune stale refs → enforce the 150-line cap → repair root ↔ sub chain. **New `--weekly` flag** orchestrates the three audit skills in parallel before doing sync work. +- **`/claude-to-agents`** (`command/claude-to-agents.md`) — convert the project's CLAUDE.md tree into an `AGENTS.md` for Codex / Gemini Code Assist / any tool honouring the AGENTS.md convention. Three modes: `--symlink` (one source of truth, default on macOS/Linux), `--copy` (snapshot), `--inline-chain` (flattens the `@path` chain into one self-contained file — recommended for modular projects since Codex/Gemini don't auto-resolve `@` imports). Backs up an existing AGENTS.md before overwrite. ### Agent @@ -299,7 +300,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 📊 Quick Stats - **5** skills (`claudeforge-skill`, `karpathy-guidelines`, plus three forked audit skills) -- **2** slash commands (`/enhance-claude-md`, `/sync-claude-md` with `--weekly`) +- **3** slash commands (`/enhance-claude-md`, `/sync-claude-md` with `--weekly`, `/claude-to-agents`) - **1** agent (`claude-md-guardian`, fail-closed contract) - **3** hook scripts wired across `PostToolUse`, `InstructionsLoaded`, `Stop` - **5** Python modules under `skill/` (analyzer, validator, generator, template_selector, workflow) diff --git a/command/claude-to-agents.md b/command/claude-to-agents.md new file mode 100644 index 0000000..3e90617 --- /dev/null +++ b/command/claude-to-agents.md @@ -0,0 +1,61 @@ +--- +description: Convert the project's CLAUDE.md (and chained sub-files) into an AGENTS.md for Codex / Gemini Code Assist / other tools that follow the AGENTS.md convention. Three modes — symlink for one source of truth, copy for a snapshot, inline-chain for a self-contained flat file that doesn't depend on @-import resolution. +argument-hint: "[--symlink | --copy | --inline-chain] [--force]" +when_to_use: | + Use when the user asks "make an AGENTS.md", "support codex", "support gemini", + "convert CLAUDE.md", "share my instructions with non-Claude tools", or when + adopting ClaudeForge in a repo that already has cross-tool contributors. +allowed-tools: + - Read + - Write + - Glob + - "Bash(python3:*)" + - "Bash(ls:*)" + - "Bash(test:*)" + - "Bash(readlink:*)" +disallowedTools: + - WebFetch + - WebSearch +permissions: + allow: + - Read + - Write + - Glob + - "Bash(python3:*)" + - "Bash(ls:*)" + - "Bash(test:*)" + - "Bash(readlink:*)" +--- + +# /claude-to-agents — convert CLAUDE.md → AGENTS.md + +Wraps `hooks/claude-to-agents.py` so non-Claude tools (OpenAI Codex, Gemini Code Assist, anything else honouring the `AGENTS.md` convention) can read the same instructions as Claude. + +## Mode selection + +Decide first by asking which guarantee the user wants: + +- **`--symlink`** (default on macOS / Linux): `AGENTS.md` becomes a symlink to `CLAUDE.md`. One source of truth — edits to CLAUDE.md show up in AGENTS.md instantly. Codex/Gemini read it transparently. On Windows the script falls back to `--copy` and prints a notice. +- **`--copy`**: byte-for-byte snapshot. Use when the user wants to fork the instructions for non-Claude tooling (Codex/Gemini reading a different policy) or when their VCS / build pipeline doesn't follow symlinks. +- **`--inline-chain`**: walk every `@path/.../CLAUDE.md` chain import recursively and write a single flat AGENTS.md with all sub-file content inlined. **Recommended for Codex/Gemini in modular projects** — those tools don't resolve `@`-imports, so without inlining they'd only see the root file. + +If the user is silent on mode, default to `--symlink` for simple projects and recommend `--inline-chain` for projects with > 1 CLAUDE.md (run `find . -name CLAUDE.md -type f -not -path '*/.git/*' -not -path '*/node_modules/*' | wc -l` first to decide). + +## Execution + +1. **Pre-flight.** `test -f CLAUDE.md` — if missing, tell the user `/enhance-claude-md` is the right command first. +2. **Run the script** with the chosen flags from the repo root: + ```bash + python3 "${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/hooks/claude-to-agents.py" --mode + ``` +3. **Report.** Echo whether AGENTS.md was created or backed up, its size, and which mode produced it. +4. **Verify** the result: + - For `--symlink`: `readlink AGENTS.md` should print `CLAUDE.md`. + - For `--copy`: `diff -q CLAUDE.md AGENTS.md` should return clean. + - For `--inline-chain`: AGENTS.md must contain content from every chained sub-file; the script strips backlinks and `@`-import lines automatically. + +## Safety + +- An existing `AGENTS.md` is renamed to `AGENTS.md.backup.` before overwrite. Pass `--force` to skip the backup (destructive). +- The script never writes outside the current directory tree. +- Read-only modes (`--symlink`) leave CLAUDE.md untouched. diff --git a/hooks/claude-to-agents.py b/hooks/claude-to-agents.py new file mode 100755 index 0000000..2330a7c --- /dev/null +++ b/hooks/claude-to-agents.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""ClaudeForge: convert a CLAUDE.md tree into an AGENTS.md for Codex / Gemini users. + +Three modes: + +* ``--symlink`` (default on macOS/Linux): create ``AGENTS.md`` as a symlink to + ``CLAUDE.md``. Single source of truth forever. Windows falls back to + ``--copy`` with a warning printed to stderr. +* ``--copy``: byte-copy ``CLAUDE.md`` → ``AGENTS.md``. Snapshot, not live-linked. +* ``--inline-chain``: walk every ``@path/.../CLAUDE.md`` import recursively and + produce a single flat ``AGENTS.md`` with every chained file inlined under a + header that names its relative path. The right choice for Codex / Gemini + because those tools don't auto-resolve Claude's ``@`` imports. + +An existing ``AGENTS.md`` is backed up to ``AGENTS.md.backup.`` +before overwrite, unless ``--force`` is passed. + +Exit codes: ``0`` success, ``1`` user error (missing source, unsafe overwrite, +unknown mode), ``2`` filesystem error (permission denied, symlink unsupported). +""" +from __future__ import annotations + +import argparse +import datetime as _dt +import os +import re +import shutil +import sys +from pathlib import Path + +CHAIN_IMPORT = re.compile(r"^\s*@([^\s]+)\s*$") +BACKLINK_LINE = re.compile( + r"^\s*>\s*(Parent context:|Chained import:).*$", re.IGNORECASE +) + + +def _ts() -> str: + # Microsecond precision so two backups in the same second don't collide + # (the smoke test for the inline-chain workflow re-backs-up within ms). + return _dt.datetime.utcnow().strftime("%Y%m%d_%H%M%S_%f") + + +def _backup(dest: Path) -> Path | None: + if not dest.exists() and not dest.is_symlink(): + return None + backup = dest.with_name(f"{dest.name}.backup.{_ts()}") + # ``shutil.move`` preserves symlinks-as-symlinks across same-filesystem. + shutil.move(str(dest), str(backup)) + return backup + + +def _resolve_imports(root: Path, seen: set[Path]) -> list[tuple[Path, str]]: + """Depth-first walk of ``@path`` chain imports rooted at ``root``. + + Returns a list of ``(relative_path, content)`` tuples in encounter order. + Cycles are short-circuited (a file is read at most once). + """ + root = root.resolve() + if root in seen: + return [] + if not root.exists(): + return [] + seen.add(root) + + lines = root.read_text(encoding="utf-8").splitlines() + cleaned: list[str] = [] + children: list[tuple[Path, str]] = [] + parent_dir = root.parent + + for line in lines: + m = CHAIN_IMPORT.match(line) + if m: + target = (parent_dir / m.group(1)).resolve() + if target.suffix == "" and target.is_dir(): + target = target / "CLAUDE.md" + children.extend(_resolve_imports(target, seen)) + # Drop the @-import line from the inlined parent body. + continue + if BACKLINK_LINE.match(line): + # Backlinks like "> Parent context: see the root [CLAUDE.md]..." + # are Claude-specific scaffolding. Drop in inline-chain output. + continue + cleaned.append(line) + + rel = str(root.relative_to(Path.cwd().resolve())) if root.is_relative_to( + Path.cwd().resolve() + ) else root.name + return [(rel, "\n".join(cleaned).strip() + "\n")] + children + + +def _do_symlink(src: Path, dest: Path) -> int: + if os.name == "nt": + print( + "claude-to-agents: symlinks on Windows require admin / dev mode; " + "falling back to --copy", + file=sys.stderr, + ) + return _do_copy(src, dest) + try: + dest.symlink_to(src.name) + except OSError as exc: + print(f"claude-to-agents: symlink failed ({exc}); use --copy or --inline-chain", file=sys.stderr) + return 2 + return 0 + + +def _do_copy(src: Path, dest: Path) -> int: + shutil.copyfile(src, dest) + return 0 + + +def _do_inline_chain(src: Path, dest: Path) -> int: + chain = _resolve_imports(src, set()) + if not chain: + print(f"claude-to-agents: source {src} not found", file=sys.stderr) + return 1 + out: list[str] = [ + "", + "", + "", + ] + for rel, body in chain: + out.append(f"") + out.append("") + out.append(body.strip()) + out.append("") + dest.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8") + return 0 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="claude-to-agents", description=__doc__.split("\n\n")[0]) + parser.add_argument("--source", default="CLAUDE.md") + parser.add_argument("--output", default="AGENTS.md") + parser.add_argument( + "--mode", + choices=("symlink", "copy", "inline-chain"), + default="symlink", + ) + parser.add_argument("--force", action="store_true", help="overwrite without backup") + args = parser.parse_args(argv) + + src = Path(args.source) + dest = Path(args.output) + if not src.exists(): + print(f"claude-to-agents: source {src} not found", file=sys.stderr) + return 1 + + if (dest.exists() or dest.is_symlink()) and not args.force: + backup = _backup(dest) + if backup is not None: + print(f"claude-to-agents: backed up existing {dest} -> {backup}", file=sys.stderr) + elif args.force and (dest.exists() or dest.is_symlink()): + dest.unlink() + + handlers = { + "symlink": _do_symlink, + "copy": _do_copy, + "inline-chain": _do_inline_chain, + } + rc = handlers[args.mode](src, dest) + if rc == 0: + kind = "symlink" if args.mode == "symlink" and dest.is_symlink() else "file" + print(f"claude-to-agents: wrote {dest} ({args.mode}, {kind})", file=sys.stderr) + return rc + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/install.ps1 b/install.ps1 index 4bea2e8..e61e672 100644 --- a/install.ps1 +++ b/install.ps1 @@ -183,6 +183,7 @@ Write-Host " • Skill: $skillsDir\claude-md-link-check\" Write-Host " • Skill: $skillsDir\claude-md-dependency-rescan\" Write-Host " • Command: $commandsDir\enhance-claude-md.md" Write-Host " • Command: $commandsDir\sync-claude-md.md" +Write-Host " • Command: $commandsDir\claude-to-agents.md" Write-Host " • Agent: $agentsDir\claude-md-guardian.md" Write-Host "" @@ -403,6 +404,7 @@ if ($scope -eq "user-level") { Write-Host " Remove-Item -Recurse -Force ~\.claude\skills\claude-md-dependency-rescan" Write-Host " Remove-Item -Force ~\.claude\commands\enhance-claude-md.md" Write-Host " Remove-Item -Force ~\.claude\commands\sync-claude-md.md" + Write-Host " Remove-Item -Force ~\.claude\commands\claude-to-agents.md" Write-Host " Remove-Item -Force ~\.claude\agents\claude-md-guardian.md" } else { Write-Host " Remove-Item -Recurse -Force .\.claude\skills\claudeforge-skill" @@ -412,6 +414,7 @@ if ($scope -eq "user-level") { Write-Host " Remove-Item -Recurse -Force .\.claude\skills\claude-md-dependency-rescan" Write-Host " Remove-Item -Force .\.claude\commands\enhance-claude-md.md" Write-Host " Remove-Item -Force .\.claude\commands\sync-claude-md.md" + Write-Host " Remove-Item -Force .\.claude\commands\claude-to-agents.md" Write-Host " Remove-Item -Force .\.claude\agents\claude-md-guardian.md" } Write-Host "" diff --git a/install.sh b/install.sh index 0a4b6bc..a120496 100755 --- a/install.sh +++ b/install.sh @@ -164,6 +164,7 @@ echo " • Skill: $SKILLS_DIR/claude-md-link-check/" echo " • Skill: $SKILLS_DIR/claude-md-dependency-rescan/" echo " • Command: $COMMANDS_DIR/enhance-claude-md.md" echo " • Command: $COMMANDS_DIR/sync-claude-md.md" +echo " • Command: $COMMANDS_DIR/claude-to-agents.md" echo " • Agent: $AGENTS_DIR/claude-md-guardian.md" echo "" @@ -360,6 +361,7 @@ if [ "$SCOPE" == "user-level" ]; then echo " rm -rf ~/.claude/skills/claude-md-dependency-rescan" echo " rm -f ~/.claude/commands/enhance-claude-md.md" echo " rm -f ~/.claude/commands/sync-claude-md.md" + echo " rm -f ~/.claude/commands/claude-to-agents.md" echo " rm -f ~/.claude/agents/claude-md-guardian.md" else echo " rm -rf ./.claude/skills/claudeforge-skill" @@ -369,6 +371,7 @@ else echo " rm -rf ./.claude/skills/claude-md-dependency-rescan" echo " rm -f ./.claude/commands/enhance-claude-md.md" echo " rm -f ./.claude/commands/sync-claude-md.md" + echo " rm -f ./.claude/commands/claude-to-agents.md" echo " rm -f ./.claude/agents/claude-md-guardian.md" fi echo ""