diff --git a/.github/workflows/rpgkit-pre-release.yml b/.github/workflows/rpgkit-pre-release.yml index 6846e28..00c7b30 100644 --- a/.github/workflows/rpgkit-pre-release.yml +++ b/.github/workflows/rpgkit-pre-release.yml @@ -4,7 +4,6 @@ on: push: branches: - dev - - "dev/**" paths: - "RPG-Kit/**" - ".github/workflows/rpgkit-pre-release.yml" diff --git a/.github/workflows/scripts/rpgkit/create-release-packages.ps1 b/.github/workflows/scripts/rpgkit/create-release-packages.ps1 index 48f8918..866a946 100755 --- a/.github/workflows/scripts/rpgkit/create-release-packages.ps1 +++ b/.github/workflows/scripts/rpgkit/create-release-packages.ps1 @@ -156,6 +156,11 @@ function Build-Variant { $specDir = Join-Path $baseDir ".rpgkit" New-Item -ItemType Directory -Path $specDir -Force | Out-Null + if (Test-Path "pyproject.toml") { + Copy-Item -Path "pyproject.toml" -Destination (Join-Path $specDir "pyproject.toml") -Force + Write-Host "Copied pyproject.toml -> .rpgkit" + } + # Copy memory directory if (Test-Path "memory") { Copy-Item -Path "memory" -Destination $specDir -Recurse -Force diff --git a/.github/workflows/scripts/rpgkit/create-release-packages.sh b/.github/workflows/scripts/rpgkit/create-release-packages.sh index 9891028..1c519ce 100755 --- a/.github/workflows/scripts/rpgkit/create-release-packages.sh +++ b/.github/workflows/scripts/rpgkit/create-release-packages.sh @@ -18,6 +18,7 @@ if [[ $# -ne 1 ]]; then exit 1 fi NEW_VERSION="$1" +PYTHON_BIN="${PYTHON:-python3}" if [[ ! $NEW_VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then echo "Version must look like v0.0.0 or v0.0.0-dev.1" >&2 exit 1 @@ -87,7 +88,7 @@ EOF create_archive() { local source_dir=$1 archive_path=$2 - python - "$source_dir" "$archive_path" <<'PY' + "$PYTHON_BIN" - "$source_dir" "$archive_path" <<'PY' from pathlib import Path import sys import zipfile @@ -113,7 +114,9 @@ build_variant() { # Create empty data directory for runtime output mkdir -p "$SPEC_DIR/data" - + + [[ -f pyproject.toml ]] && { cp pyproject.toml "$SPEC_DIR/pyproject.toml"; echo "Copied pyproject.toml -> .rpgkit"; } + [[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .rpgkit"; } # Only copy the relevant script variant directory diff --git a/.gitignore b/.gitignore index 92c43d8..878121c 100644 --- a/.gitignore +++ b/.gitignore @@ -420,3 +420,4 @@ FodyWeavers.xsd # RPG-Kit release artifacts RPG-Kit/.genreleases/ release_notes.md +workspace/ diff --git a/README.md b/README.md index 3047d72..367d9bc 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ **RPG-ZeroRepo turns Repository Planning Graphs into a control layer for long-horizon AI coding agents.** -[![Paper 1: arXiv:2509.16198](https://img.shields.io/badge/Paper%201-arXiv%3A2509.16198-b31a1b)](https://arxiv.org/abs/2509.16198) -[![Paper 2: arXiv:2602.02084](https://img.shields.io/badge/Paper%202-arXiv%3A2602.02084-b31a1b)](https://arxiv.org/abs/2602.02084) +[![RPG-ZeroRepo: arXiv:2509.16198](https://img.shields.io/badge/Paper%201-arXiv%3A2509.16198-b31a1b)](https://arxiv.org/abs/2509.16198) +[![RPG-Encoder: arXiv:2602.02084](https://img.shields.io/badge/Paper%202-arXiv%3A2602.02084-b31a1b)](https://arxiv.org/abs/2602.02084) [![ICLR 2026](https://img.shields.io/badge/ICLR-2026-blue.svg)](https://arxiv.org/abs/2509.16198) [![ICML 2026](https://img.shields.io/badge/ICML-2026-blue.svg)](https://arxiv.org/abs/2602.02084) [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) @@ -21,11 +21,13 @@ The repository also includes the research code: **[ZeroRepo](#zerorepo-requireme ## News -- [2026-05-15] 🔥 **RPG-Kit** is now open source for Claude Code and GitHub Copilot. It uses Repository Planning Graphs as a control layer for long-horizon coding agents, including planning, multi-file generation, repository understanding, and graph-aware updates. +- [2026-05-15] 🚀 **RPG-Kit** is now open source for Claude Code and GitHub Copilot. It uses Repository Planning Graphs as a control layer for long-horizon coding agents, including planning, multi-file generation, repository understanding, and graph-aware updates. - [2026-05-01] 🎉 **RPG-Encoder** ([*Closing the Loop: Universal Repository Representation with RPG-Encoder*](https://arxiv.org/abs/2602.02084)) has been accepted to **ICML 2026**. -- [2026-03-02] 🔥 We have open-sourced the **EpiCoder Feature Tree** at [Hugging Face](https://huggingface.co/datasets/microsoft/EpiCoder-meta-features), providing structured knowledge for repository planning in **ZeroRepo**. -- [2026-02-27] 🔥 We released the code for [RPG-Encoder](zerorepo/rpg_encoder/) and [RepoCraft](repocraft/). -- [2026-01-26] 🔥 [RPG-ZeroRepo](https://arxiv.org/abs/2509.16198) was accepted as a poster at ICLR 2026. +- [2026-03-02] 🚀 We have open-sourced the **EpiCoder Feature Tree** at [Hugging Face](https://huggingface.co/datasets/microsoft/EpiCoder-meta-features), providing structured knowledge for repository planning in **ZeroRepo**. +- [2026-02-27] 🚀 We released the code for [RPG-Encoder](zerorepo/rpg_encoder/) and [RepoCraft](repocraft/). +- [2026-02-02] 🔥 Our paper "[Closing the Loop: Universal Repository Representation with RPG-Encoder](https://arxiv.org/abs/2602.02084)" has been released on arXiv. +- [2026-01-26] 🎉 [RPG-ZeroRepo](https://arxiv.org/abs/2509.16198) was accepted as a poster at ICLR 2026. +- [2025-09-19] 🔥 Our paper "[RPG: A Repository Planning Graph for Unified and Scalable Codebase Generation](https://arxiv.org/abs/2509.16198)" has been released on arXiv. --- diff --git a/RPG-Kit/src/rpgkit_cli/__init__.py b/RPG-Kit/src/rpgkit_cli/__init__.py index a054f5f..62447ac 100644 --- a/RPG-Kit/src/rpgkit_cli/__init__.py +++ b/RPG-Kit/src/rpgkit_cli/__init__.py @@ -551,6 +551,9 @@ def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) # Runtime workspace (logs, generated data, trajectory) .rpgkit/ +# RPG-Kit Python environment +.venv_rpgkit/ + # Codegen dev environments .venv_dev/ .rpgkit_dev_env/ @@ -1375,7 +1378,7 @@ def _workspace_has_python_code(project_path: Path) -> bool: ``__pycache__``) are pruned too — a ``*.py`` under any of them would not indicate user code. """ - PRUNE = {".rpgkit", ".git", ".venv", "venv", "node_modules", + PRUNE = {".rpgkit", ".git", ".venv", ".venv_rpgkit", "venv", "node_modules", "__pycache__", ".tox", ".mypy_cache", ".pytest_cache", ".ruff_cache", "dist", "build"} for dirpath, dirnames, filenames in os.walk(project_path): @@ -2753,6 +2756,116 @@ def download_template_from_github( return zip_path, metadata +def _resolve_rpgkit_source_root(source: Path) -> Path: + source = source.expanduser().resolve() + candidates = [source] + if (source / "RPG-Kit").is_dir(): + candidates.insert(0, source / "RPG-Kit") + + for candidate in candidates: + if ( + (candidate / "templates" / "commands").is_dir() + and (candidate / "scripts").is_dir() + and (candidate / "pyproject.toml").is_file() + ): + return candidate + + raise RuntimeError( + f"Invalid RPG-Kit source path: {source}. Expected the RPG-Kit directory " + "or the repository root containing RPG-Kit/." + ) + + +def _build_local_template_package( + source: Path, + ai_assistant: str, + script_type: str, +) -> Tuple[Path, dict]: + source_root = _resolve_rpgkit_source_root(source) + repo_root = source_root.parent + project_dir = source_root.relative_to(repo_root).as_posix() + scripts_root = repo_root / ".github" / "workflows" / "scripts" / "rpgkit" + version = "v0.0.0-local" + env = os.environ.copy() + env.update( + { + "GITHUB_WORKSPACE": str(repo_root), + "PROJECT_DIR": project_dir, + "AGENTS": ai_assistant, + "SCRIPTS": script_type, + "PYTHON": sys.executable, + } + ) + + if os.name == "nt": + release_script = scripts_root / "create-release-packages.ps1" + runner = shutil.which("pwsh") + command = ( + [ + runner, + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + str(release_script), + version, + "-Agents", + ai_assistant, + "-Scripts", + script_type, + ] + if runner + else None + ) + else: + release_script = scripts_root / "create-release-packages.sh" + runner = shutil.which("bash") + command = [runner, str(release_script), version] if runner else None + + if not release_script.is_file(): + raise RuntimeError( + f"Release packaging script not found: {release_script}. " + "Pass the RPG-ZeroRepo root or its RPG-Kit/ directory to --source." + ) + if command is None: + requirement = "PowerShell 7 (pwsh)" if os.name == "nt" else "bash" + raise RuntimeError( + f"Local --source packaging requires {requirement}, but it was not found on PATH." + ) + + result = subprocess.run( + command, + cwd=repo_root, + env=env, + text=True, + capture_output=True, + ) + if result.returncode != 0: + detail = ( + result.stderr or result.stdout or "local package build failed" + ).strip() + raise RuntimeError( + f"Failed to build local RPG-Kit template package from {source_root}: {detail}" + ) + + archive = ( + source_root + / ".genreleases" + / f"rpgkit-template-{ai_assistant}-{script_type}-{version}.zip" + ) + if not archive.is_file(): + raise RuntimeError( + f"Local RPG-Kit template package was not created: {archive}" + ) + + return archive, { + "filename": archive.name, + "size": archive.stat().st_size, + "release": version, + "source": str(source_root), + } + + def download_and_extract_template( project_path: Path, ai_assistant: str, @@ -2765,32 +2878,44 @@ def download_and_extract_template( debug: bool = False, github_token: str = None, pre: bool = False, + source: Path | None = None, ) -> Path: - """Download the latest release and extract it to create a new project. + """Download or build a template archive and extract it to create a project. Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup). """ current_dir = Path.cwd() + cleanup_zip = source is None if tracker: - tracker.start("fetch", "contacting GitHub API") - try: - zip_path, meta = download_template_from_github( - ai_assistant, - current_dir, - script_type=script_type, - verbose=verbose and tracker is None, - show_progress=(tracker is None), - client=client, - debug=debug, - github_token=github_token, - pre=pre, + fetch_detail = ( + "building local template package" if source else "contacting GitHub API" ) + tracker.start("fetch", fetch_detail) + try: + if source: + zip_path, meta = _build_local_template_package( + source, + ai_assistant, + script_type, + ) + else: + zip_path, meta = download_template_from_github( + ai_assistant, + current_dir, + script_type=script_type, + verbose=verbose and tracker is None, + show_progress=(tracker is None), + client=client, + debug=debug, + github_token=github_token, + pre=pre, + ) if tracker: tracker.complete( - "fetch", f"release {meta['release']} ({meta['size']:,} bytes)" + "fetch", f"template {meta['release']} ({meta['size']:,} bytes)" ) - tracker.add("download", "Download template") + tracker.add("download", "Use template archive" if source else "Download template") tracker.complete("download", meta["filename"]) except Exception as e: if tracker: @@ -2942,12 +3067,14 @@ def download_and_extract_template( if tracker: tracker.add("cleanup", "Remove temporary archive") - if zip_path.exists(): + if cleanup_zip and zip_path.exists(): zip_path.unlink() if tracker: tracker.complete("cleanup") elif verbose: console.print(f"Cleaned up: {zip_path.name}") + elif tracker: + tracker.skip("cleanup", "local package retained") return project_path @@ -3007,6 +3134,71 @@ def ensure_executable_scripts( console.print(f" - {f}") +def setup_venv_rpgkit( + project_path: Path, tracker: StepTracker | None = None +) -> None: + """Create or update .venv_rpgkit with RPG-Kit Python dependencies.""" + venv_dir = project_path / ".venv_rpgkit" + rpgkit_dir = project_path / ".rpgkit" + pyproject = rpgkit_dir / "pyproject.toml" + + if tracker: + tracker.start("venv") + + if not pyproject.is_file(): + msg = ".rpgkit/pyproject.toml not found — cannot install Python dependencies" + if tracker: + tracker.skip("venv", msg) + else: + console.print(f"[yellow]Warning:[/yellow] {msg}") + return + + try: + is_new = not venv_dir.exists() + + if is_new: + subprocess.run( + ["uv", "venv", str(venv_dir)], + check=True, + capture_output=True, + text=True, + ) + + if os.name == "nt": + pip_python = venv_dir / "Scripts" / "python.exe" + else: + pip_python = venv_dir / "bin" / "python3" + + subprocess.run( + ["uv", "pip", "install", str(rpgkit_dir), "--python", str(pip_python)], + check=True, + capture_output=True, + text=True, + ) + + if tracker: + tracker.complete( + "venv", + "created .venv_rpgkit" if is_new else "updated .venv_rpgkit", + ) + except FileNotFoundError: + msg = "uv not found — install uv (https://docs.astral.sh/uv/) to enable auto-setup" + if tracker: + tracker.skip("venv", msg) + console.print(f"[yellow]Warning:[/yellow] {msg}") + except subprocess.CalledProcessError as e: + detail = e.stderr.strip() if e.stderr else str(e) + msg = f"Failed to set up .venv_rpgkit:\n{detail}" + if tracker: + tracker.error("venv", detail[:120]) + console.print(f"[red]Error:[/red] {msg}") + except Exception as e: + msg = f"Failed to set up .venv_rpgkit: {e}" + if tracker: + tracker.error("venv", str(e)[:120]) + console.print(f"[red]Error:[/red] {msg}") + + def ensure_rpgkit_runtime_dirs( project_path: Path, tracker: StepTracker | None = None ) -> None: @@ -3126,6 +3318,14 @@ def init( "--pre", help="Download the latest pre-release (dev build) instead of the latest stable release", ), + source: Optional[Path] = typer.Option( + None, + "--source", + help=( + "Use a local RPG-Kit source checkout to build and install the " + "template package instead of downloading a release asset." + ), + ), no_mcp: bool = typer.Option( False, "--no-mcp", @@ -3293,6 +3493,12 @@ def init( console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}") console.print(f"[cyan]Selected script type:[/cyan] {selected_script}") + if source: + console.print(f"[cyan]Template source:[/cyan] {source}") + if pre: + console.print( + "[yellow]Warning:[/yellow] --pre is ignored when --source is provided" + ) tracker = StepTracker("Initialize RPG-Kit Project") @@ -3305,8 +3511,15 @@ def init( tracker.add("script-select", "Select script type") tracker.complete("script-select", selected_script) for key, label in [ - ("fetch", "Fetch latest pre-release" if pre else "Fetch latest release"), - ("download", "Download template"), + ( + "fetch", + "Build local template package" + if source + else "Fetch latest pre-release" + if pre + else "Fetch latest release", + ), + ("download", "Use local template package" if source else "Download template"), ("extract", "Extract template"), ("zip-list", "Archive contents"), ("extracted-summary", "Extraction summary"), @@ -3314,6 +3527,7 @@ def init( ("gitignore", "Configure .gitignore"), ("mcp", "Configure MCP server"), ("legacy-cleanup", "Remove obsolete persistent rules"), + ("venv", "Set up Python environment"), ("cleanup", "Cleanup"), ("git", "Initialize git repository"), ("hooks", "Install auto-update hooks"), @@ -3344,6 +3558,7 @@ def init( debug=debug, github_token=github_token, pre=pre, + source=source, ) ensure_executable_scripts(project_path, tracker=tracker) @@ -3381,6 +3596,8 @@ def init( except Exception as exc: tracker.error("legacy-cleanup", str(exc)) + setup_venv_rpgkit(project_path, tracker=tracker) + if not no_git: tracker.start("git") if is_git_repo(project_path): @@ -3496,6 +3713,18 @@ def init( ) step_num += 1 + venv_path = project_path / ".venv_rpgkit" + if os.name == "nt": + activate_cmd = r".venv_rpgkit\Scripts\activate" + else: + activate_cmd = "source .venv_rpgkit/bin/activate" + steps_lines.append( + f"{step_num}. Activate the RPG-Kit Python environment: " + f"[cyan]{activate_cmd}[/cyan]" + ) + + step_num += 1 + steps_lines.append(f"{step_num}. Start using slash commands with your AI agent:") steps_lines.extend([ @@ -3592,6 +3821,14 @@ def update( "--pre", help="Download the latest pre-release (dev build) instead of the latest stable release", ), + source: Optional[Path] = typer.Option( + None, + "--source", + help=( + "Use a local RPG-Kit source checkout to build and install the " + "template package instead of downloading a release asset." + ), + ), no_mcp: bool = typer.Option( False, "--no-mcp", @@ -3677,6 +3914,12 @@ def update( console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}") console.print(f"[cyan]Selected script type:[/cyan] {selected_script}") + if source: + console.print(f"[cyan]Template source:[/cyan] {source}") + if pre: + console.print( + "[yellow]Warning:[/yellow] --pre is ignored when --source is provided" + ) # Build step tracker tracker = StepTracker("Update RPG-Kit Project") @@ -3688,8 +3931,15 @@ def update( tracker.add("script-select", "Select script type") tracker.complete("script-select", selected_script) for key, label in [ - ("fetch", "Fetch latest pre-release" if pre else "Fetch latest release"), - ("download", "Download template"), + ( + "fetch", + "Build local template package" + if source + else "Fetch latest pre-release" + if pre + else "Fetch latest release", + ), + ("download", "Use local template package" if source else "Download template"), ("extract", "Extract template"), ("zip-list", "Archive contents"), ("extracted-summary", "Extraction summary"), @@ -3697,6 +3947,7 @@ def update( ("gitignore", "Configure .gitignore"), ("mcp", "Configure MCP server"), ("legacy-cleanup", "Remove obsolete persistent rules"), + ("venv", "Set up Python environment"), ("hooks", "Install auto-update hooks"), ("cleanup", "Cleanup"), ("final", "Finalize"), @@ -3723,6 +3974,7 @@ def update( debug=debug, github_token=github_token, pre=pre, + source=source, ) ensure_executable_scripts(project_path, tracker=tracker) @@ -3765,6 +4017,8 @@ def update( except Exception as exc: tracker.error("legacy-cleanup", str(exc)) + setup_venv_rpgkit(project_path, tracker=tracker) + # Re-install hooks so behavior fixes propagate to existing # workspaces. Without this, the .git/hooks/* files stay # frozen at whatever version was active during the original @@ -3811,7 +4065,23 @@ def update( f"[dim]Updated: scripts, templates, and {AGENT_CONFIG[selected_ai]['name']} " f"command definitions in [cyan]{project_path}[/cyan][/dim]" ) - + console.print() + venv_path = Path(project_path) / ".venv_rpgkit" + if venv_path.exists(): + activate_cmd = ( + r".venv_rpgkit\Scripts\activate" + if os.name == "nt" + else "source .venv_rpgkit/bin/activate" + ) + console.print( + Panel( + "Activate the RPG-Kit Python environment before using slash commands:\n\n" + f"[cyan]{activate_cmd}[/cyan]", + title="[yellow]Environment Setup[/yellow]", + border_style="yellow", + padding=(1, 2), + ) + ) @app.command() def check():